feat: add shared @sexy.pivoine.art/types package and fix type safety across frontend/backend

- Create packages/types with shared TypeScript domain model interfaces (User, Video, Model, Article, Comment, Recording, etc.)
- Wire both frontend and backend packages to use @sexy.pivoine.art/types via workspace:*
- Update backend Pothos objectRef types to use shared interfaces instead of inline types
- Update frontend $lib/types.ts to re-export from shared package
- Fix all type errors introduced by more accurate nullable types (avatar/banner as string|null UUIDs, author nullable, events/device_info as object[])
- Add artist_name to comment user select in backend resolver
- Widen utility function signatures (getAssetUrl, getUserInitials, calcReadingTime) to accept null/undefined

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 11:01:11 +01:00
parent c6126c13e9
commit 97269788ee
31 changed files with 839 additions and 822 deletions

View File

@@ -1,9 +1,10 @@
import { getRecording } from "$lib/services";
import type { Recording } from "$lib/types";
export async function load({ locals, url, fetch }) {
const recordingId = url.searchParams.get("recording");
let recording = null;
let recording: Recording | null = null;
if (recordingId && locals.authStatus.authenticated) {
try {
recording = await getRecording(recordingId, fetch);

View File

@@ -9,7 +9,6 @@
InputType,
DeviceOutputValueConstructor,
} from "@sexy.pivoine.art/buttplug";
import type { ButtplugMessage } from "@sexy.pivoine.art/buttplug";
import Button from "$lib/components/ui/button/button.svelte";
import { onMount } from "svelte";
import { goto } from "$app/navigation";
@@ -74,7 +73,8 @@
}
}
function handleInputReading(msg: ButtplugMessage) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function handleInputReading(msg: any) {
if (msg.InputReading === undefined) return;
const reading = msg.InputReading;
const device = devices.find((d) => d.info.index === reading.DeviceIndex);
@@ -92,7 +92,7 @@
if (!feature) return;
actuator.value = value;
const outputType = actuator.outputType as OutputType;
const outputType = actuator.outputType as typeof OutputType;
await feature.runOutput(new DeviceOutputValueConstructor(outputType).steps(value));
// Capture event if recording
@@ -225,7 +225,7 @@
}
// Check if we need to map devices
if (deviceMappings.size === 0 && data.recording.device_info.length > 0) {
if (deviceMappings.size === 0 && (data.recording.device_info?.length ?? 0) > 0) {
showMappingDialog = true;
return;
}
@@ -284,7 +284,7 @@
function scheduleNextEvent() {
if (!data.recording || !isPlaying || !playbackStartTime) return;
const events = data.recording.events;
const events = (data.recording.events ?? []) as RecordedEvent[];
if (currentEventIndex >= events.length) {
stopPlayback();
toast.success("Playback finished");
@@ -332,7 +332,7 @@
// Send command to device via feature
const feature = device.info.features.get(actuator.featureIndex);
if (feature) {
const outputType = actuator.outputType as OutputType;
const outputType = actuator.outputType as typeof OutputType;
feature.runOutput(new DeviceOutputValueConstructor(outputType).steps(deviceValue));
}
@@ -347,9 +347,10 @@
playbackProgress = targetTime;
// Find the event index at this time
currentEventIndex = data.recording.events.findIndex((e) => e.timestamp >= targetTime);
const seekEvents = (data.recording.events ?? []) as RecordedEvent[];
currentEventIndex = seekEvents.findIndex((e) => e.timestamp >= targetTime);
if (currentEventIndex === -1) {
currentEventIndex = data.recording.events.length;
currentEventIndex = seekEvents.length;
}
if (isPlaying) {
@@ -548,11 +549,11 @@
<div class="mt-4 pt-4 border-t border-border/50 grid grid-cols-3 gap-4 text-center">
<div>
<p class="text-xs text-muted-foreground">Events</p>
<p class="text-sm font-medium">{data.recording.events.length}</p>
<p class="text-sm font-medium">{data.recording.events?.length ?? 0}</p>
</div>
<div>
<p class="text-xs text-muted-foreground">Devices</p>
<p class="text-sm font-medium">{data.recording.device_info.length}</p>
<p class="text-sm font-medium">{data.recording.device_info?.length ?? 0}</p>
</div>
<div>
<p class="text-xs text-muted-foreground">Status</p>
@@ -603,7 +604,7 @@
{#if data.recording}
<DeviceMappingDialog
open={showMappingDialog}
recordedDevices={data.recording.device_info}
recordedDevices={(data.recording.device_info ?? []) as DeviceInfo[]}
connectedDevices={devices}
onConfirm={handleMappingConfirm}
onCancel={handleMappingCancel}