Fullfører oppgave 17.5: Job-polling opprydding i lydstudioet

Tre forbedringer i studio-siden:

1. Interval/timeout-opprydding ved navigering: Polling-interval og
   timeout lagres i komponent-variabler og ryddes opp via $effect
   cleanup når komponenten demonteres. Forhindrer memory leaks og
   ghost-requests etter navigering bort fra studio-siden.

2. Feilmelding etter N mislykkede polling-forsøk: Etter 5 feilede
   statussjekker (nettverksfeil eller HTTP-feil) vises en
   feilmelding til brukeren i stedet for stille ignorering.
   Timeout-feil og jobb-feil vises også i UI.

3. Metadata JSON.parse i try/catch: Hindrer at ugyldig metadata-JSON
   krasjer hele studio-siden. Logger feilen og returnerer null.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
vegard 2026-03-18 05:53:28 +00:00
parent 24d752024c
commit 6f50f66431
2 changed files with 72 additions and 12 deletions

View file

@ -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<typeof setInterval> | null = null;
let pollTimeoutId: ReturnType<typeof setTimeout> | 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}
</div>
<!-- Render error toast -->
{#if renderError}
<div class="fixed bottom-4 left-4 z-50 max-w-sm rounded-lg border border-red-200 bg-red-50 px-4 py-3 shadow-lg">
<div class="flex items-start gap-2">
<svg class="mt-0.5 h-5 w-5 shrink-0 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div class="flex-1">
<p class="text-sm font-medium text-red-800">Rendering feilet</p>
<p class="mt-0.5 text-xs text-red-600">{renderError}</p>
</div>
<button onclick={() => { renderError = null; }} class="shrink-0 text-red-400 hover:text-red-600">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
{/if}
<!-- Render dialog -->
{#if showRenderDialog}
<RenderDialog
@ -500,6 +561,6 @@
jobId={renderJobId}
{resultNodeId}
onconfirm={handleRenderConfirm}
onclose={() => { showRenderDialog = false; rendering = false; renderJobId = null; resultNodeId = null; }}
onclose={() => { showRenderDialog = false; rendering = false; renderJobId = null; resultNodeId = null; renderError = null; }}
/>
{/if}

View file

@ -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.