diff --git a/frontend/src/routes/studio/[id]/+page.svelte b/frontend/src/routes/studio/[id]/+page.svelte index 8b111c4..24c2031 100644 --- a/frontend/src/routes/studio/[id]/+page.svelte +++ b/frontend/src/routes/studio/[id]/+page.svelte @@ -27,7 +27,15 @@ // Media node from STDB const mediaNode = $derived(connected ? nodeStore.get(mediaNodeId) : undefined); - const metadata = $derived(mediaNode?.metadata ? JSON.parse(mediaNode.metadata) : null); + const metadata = $derived.by(() => { + if (!mediaNode?.metadata) return null; + try { + return JSON.parse(mediaNode.metadata); + } catch { + console.error('Ugyldig metadata JSON for medienode:', mediaNodeId); + return null; + } + }); const casHash = $derived(metadata?.cas_hash as string | undefined); const audioSrc = $derived(casHash ? casUrl(casHash) : ''); @@ -45,6 +53,16 @@ let rendering = $state(false); let renderJobId: string | null = $state(null); let resultNodeId: string | null = $state(null); + let renderError: string | null = $state(null); + + // Polling cleanup + let pollIntervalId: ReturnType | null = null; + let pollTimeoutId: ReturnType | null = null; + + function cleanupPolling() { + if (pollIntervalId !== null) { clearInterval(pollIntervalId); pollIntervalId = null; } + if (pollTimeoutId !== null) { clearTimeout(pollTimeoutId); pollTimeoutId = null; } + } // Mobile tool panel sheet let showToolSheet = $state(false); @@ -197,41 +215,64 @@ } } + const MAX_POLL_FAILURES = 5; + function pollJobResult(jobId: string) { // Poll job_queue for completion via simple interval // In production this would use SSE or WebSocket - const interval = setInterval(async () => { + cleanupPolling(); + let failures = 0; + + pollIntervalId = setInterval(async () => { try { const res = await fetch(`/api/query/job_status?job_id=${encodeURIComponent(jobId)}`, { headers: { Authorization: `Bearer ${accessToken}` }, }); if (res.ok) { const data = await res.json(); + failures = 0; // Reset on success if (data.status === 'completed') { - clearInterval(interval); + cleanupPolling(); resultNodeId = data.result?.processed_node_id ?? null; rendering = false; } else if (data.status === 'error') { - clearInterval(interval); + cleanupPolling(); rendering = false; - console.error('Jobb feilet:', data.error_msg); + renderError = data.error_msg ?? 'Jobb feilet'; + } + } else { + failures++; + if (failures >= MAX_POLL_FAILURES) { + cleanupPolling(); + rendering = false; + renderError = `Statussjekk feilet ${MAX_POLL_FAILURES} ganger — prøv igjen senere`; } } } catch { - // Ignore polling errors + failures++; + if (failures >= MAX_POLL_FAILURES) { + cleanupPolling(); + rendering = false; + renderError = `Mistet kontakt med serveren etter ${MAX_POLL_FAILURES} forsøk`; + } } }, 2000); // Timeout after 5 minutes - setTimeout(() => { - clearInterval(interval); + pollTimeoutId = setTimeout(() => { + cleanupPolling(); if (rendering) { rendering = false; - console.error('Render timeout'); + renderError = 'Rendering tok for lang tid (tidsavbrudd etter 5 minutter)'; } }, 5 * 60 * 1000); } + // Cleanup polling on component destroy (navigation away) + $effect(() => { + return () => cleanupPolling(); + }); + function handleKeydown(e: KeyboardEvent) { if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return; if (e.key === 'Delete' || e.key === 'Backspace') { @@ -492,6 +533,26 @@ {/if} + +{#if renderError} +
+
+ + + +
+

Rendering feilet

+

{renderError}

+
+ +
+
+{/if} + {#if showRenderDialog} { showRenderDialog = false; rendering = false; renderJobId = null; resultNodeId = null; }} + onclose={() => { showRenderDialog = false; rendering = false; renderJobId = null; resultNodeId = null; renderError = null; }} /> {/if} diff --git a/tasks.md b/tasks.md index ba1542c..f59259c 100644 --- a/tasks.md +++ b/tasks.md @@ -193,8 +193,7 @@ Ref: Kodegjennomgang av `b4c4bb8` (Lydstudio: lydredigering via FFmpeg). - [x] 17.2 FFmpeg-parametervalidering: valider at alle numeriske verdier (threshold, gain, ratio, frekvenser) er innenfor sikre grenser i `audio.rs` før de interpoleres i filterstrenger. Avvis ugyldige verdier med feilmelding. - [x] 17.3 Fade/silence-logikk: fiks negativ fade-out start (clamp til 0), og adaptiv silence-margin (margin skal ikke overstige halve regionens varighet). Gi feilmelding ved ugyldige fade-varigheter. - [x] 17.4 Frontend input-begrensninger: legg til `min`/`max` på alle tallfelter i OperationPanel (silenceThreshold, fadeMs, normTarget, compRatio). Hindre ugyldig input. -- [~] 17.5 Job-polling opprydding: rydd opp interval/timeout ved navigering bort fra studio-siden. Vis feilmelding etter N mislykkede polling-forsøk. Wrap metadata JSON.parse i try/catch. - > Påbegynt: 2026-03-18T05:50 +- [x] 17.5 Job-polling opprydding: rydd opp interval/timeout ved navigering bort fra studio-siden. Vis feilmelding etter N mislykkede polling-forsøk. Wrap metadata JSON.parse i try/catch. - [ ] 17.6 Temp-fil opprydding: legg til periodisk jobb i maskinrommet som sletter gamle temp-filer i CAS tmp-katalog. Bruk `/tmp` eller sett TTL. - [ ] 17.7 FFmpeg feilmeldinger til bruker: propager stderr fra FFmpeg-feil til frontend via strukturert feilrespons. Vis i RenderDialog.