import { randomUUID } from 'node:crypto'; import { error, fail } from '@sveltejs/kit'; import type { Actions, PageServerLoad } from './$types'; import { requireUser } from '#lib/server/auth.js'; import { completeCurrent, clearPresentationChannel, endLive, getLiveView, goBack, issuePresentationAccess, moveOpenUsage, pauseLive, presentUsage, rotatePresentationChannelKey, resetSectionTimer, resetTotalTimer, resumeEndedLive, resumeLive, selectUsage, setShowStatus, skipCurrent, startNewLiveRun, startOrResumeLive } from '#lib/server/live.js'; import { listShows } from '#lib/server/shows.js'; function text(data: FormData, name: string) { return String(data.get(name) || ''); } function actionError(cause: unknown) { const code = cause instanceof Error ? cause.message : ''; if (code === 'CONFLICT') return { status: 409, error: 'Der Live-Stand wurde bereits geändert. Die Ansicht wurde erneuert.' }; if (code === 'NO_CURRENT') return { status: 409, error: 'Es ist kein aktuelles Piece aktiv.' }; if (code === 'NOT_STARTED') return { status: 409, error: 'Die Sendung muss vor der Auswahl gestartet werden.' }; if (code === 'NO_PREVIOUS') return { status: 409, error: 'Es gibt kein vorheriges Piece.' }; if (code === 'LIVE_ORDER_LOCKED') return { status: 409, error: 'Nur offene Pieces hinter dem aktuellen Stand können sortiert werden.' }; if (code === 'SHOW_COMPLETED') return { status: 409, error: 'Abgeschlossene Sendungen können nicht live gesteuert werden.' }; if (code === 'NOT_FOUND') return { status: 404, error: 'Sendung oder Piece wurde nicht gefunden.' }; if (code === 'TIMER_PAUSED') return { status: 409, error: 'Die Sendung ist pausiert.' }; if (code === 'ALREADY_PAUSED' || code === 'ALREADY_RUNNING') return { status: 409, error: 'Der Timerzustand wurde bereits geändert.' }; if (code === 'NO_SECTION_TIMER') return { status: 409, error: 'Es ist kein Abschnittstimer aktiv.' }; if (code === 'NOT_LIVE' || code === 'NOT_ENDED' || code === 'ALREADY_STARTED') return { status: 409, error: 'Die Sendung befindet sich nicht im passenden Zustand.' }; if (code.startsWith('INVALID_')) return { status: 400, error: 'Die Live-Aktion ist ungültig.' }; return { status: 500, error: 'Die Live-Aktion konnte nicht gespeichert werden.' }; } function attempt(intent: string, operation: () => unknown) { try { return { intent, success: true, result: operation() }; } catch (cause) { const problem = actionError(cause); return fail(problem.status, { intent, ...problem }); } } export const load: PageServerLoad = ({ locals, url }) => { requireUser(locals.user); const showId = url.searchParams.get('show') || listShows()[0]?.id; if (!showId) error(404, 'Keine Sendung vorhanden'); try { return { live: getLiveView(showId), commandId: randomUUID() }; } catch (cause) { if (cause instanceof Error && cause.message === 'NOT_FOUND') error(404, 'Sendung nicht gefunden'); throw cause; } }; export const actions = { start: async ({ locals, request }) => { requireUser(locals.user); const data = await request.formData(); return attempt('start', () => startOrResumeLive(text(data, 'showId'), text(data, 'commandId'), Number(data.get('revision'))) ); }, pause: async ({ locals, request }) => { requireUser(locals.user); const data = await request.formData(); return attempt('pause', () => pauseLive(text(data, 'showId'), text(data, 'commandId'), Number(data.get('revision'))) ); }, resume: async ({ locals, request }) => { requireUser(locals.user); const data = await request.formData(); return attempt('resume', () => resumeLive(text(data, 'showId'), text(data, 'commandId'), Number(data.get('revision'))) ); }, resetSectionTimer: async ({ locals, request }) => { requireUser(locals.user); const data = await request.formData(); return attempt('resetSectionTimer', () => resetSectionTimer(text(data, 'showId'), text(data, 'commandId'), Number(data.get('revision'))) ); }, resetTotalTimer: async ({ locals, request }) => { requireUser(locals.user); const data = await request.formData(); return attempt('resetTotalTimer', () => resetTotalTimer(text(data, 'showId'), text(data, 'commandId'), Number(data.get('revision'))) ); }, setStatus: async ({ locals, request }) => { requireUser(locals.user); const data = await request.formData(); const status = text(data, 'status'); if (status !== 'draft' && status !== 'ready') { return fail(400, { intent: 'setStatus', error: 'Der Sendungsstatus ist ungültig.' }); } return attempt('setStatus', () => setShowStatus( text(data, 'showId'), text(data, 'commandId'), Number(data.get('revision')), Number(data.get('showRevision')), status ) ); }, end: async ({ locals, request }) => { requireUser(locals.user); const data = await request.formData(); return attempt('end', () => endLive(text(data, 'showId'), text(data, 'commandId'), Number(data.get('revision'))) ); }, resumeEnded: async ({ locals, request }) => { requireUser(locals.user); const data = await request.formData(); return attempt('resumeEnded', () => resumeEndedLive(text(data, 'showId'), text(data, 'commandId'), Number(data.get('revision'))) ); }, startNewRun: async ({ locals, request }) => { requireUser(locals.user); const data = await request.formData(); return attempt('startNewRun', () => startNewLiveRun(text(data, 'showId'), text(data, 'commandId'), Number(data.get('revision'))) ); }, complete: async ({ locals, request }) => { requireUser(locals.user); const data = await request.formData(); return attempt('complete', () => completeCurrent(text(data, 'showId'), text(data, 'commandId'), Number(data.get('revision'))) ); }, skip: async ({ locals, request }) => { requireUser(locals.user); const data = await request.formData(); return attempt('skip', () => skipCurrent(text(data, 'showId'), text(data, 'commandId'), Number(data.get('revision'))) ); }, back: async ({ locals, request }) => { requireUser(locals.user); const data = await request.formData(); return attempt('back', () => goBack(text(data, 'showId'), text(data, 'commandId'), Number(data.get('revision'))) ); }, select: async ({ locals, request }) => { requireUser(locals.user); const data = await request.formData(); return attempt('select', () => selectUsage( text(data, 'showId'), text(data, 'usageId'), text(data, 'commandId'), Number(data.get('revision')) ) ); }, present: async ({ locals, request }) => { requireUser(locals.user); const data = await request.formData(); return attempt('present', () => presentUsage( text(data, 'showId'), text(data, 'usageId') || null, text(data, 'commandId'), Number(data.get('revision')) ) ); }, moveUsage: async ({ locals, request }) => { requireUser(locals.user); const data = await request.formData(); return attempt('moveUsage', () => moveOpenUsage(text(data, 'showId'), text(data, 'usageId'), Number(data.get('showRevision')), { sectionId: text(data, 'sectionId') || null, position: Number(data.get('position')) }) ); }, createOutput: async ({ locals, request }) => { requireUser(locals.user); const data = await request.formData(); try { const token = issuePresentationAccess(text(data, 'showId')); return { intent: 'createOutput', success: true, outputUrl: `/heinzlive/default#${token}` }; } catch (cause) { const problem = actionError(cause); return fail(problem.status, { intent: 'createOutput', ...problem }); } }, rotateOutput: async ({ locals, request }) => { requireUser(locals.user); const data = await request.formData(); try { const token = rotatePresentationChannelKey(text(data, 'showId')); return { intent: 'rotateOutput', success: true, outputUrl: `/heinzlive/default#${token}` }; } catch (cause) { const problem = actionError(cause); return fail(problem.status, { intent: 'rotateOutput', ...problem }); } }, clearOutput: async ({ locals }) => { requireUser(locals.user); return attempt('clearOutput', () => clearPresentationChannel()); } } satisfies Actions;