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:
parent
24d752024c
commit
6f50f66431
2 changed files with 72 additions and 12 deletions
|
|
@ -27,7 +27,15 @@
|
||||||
|
|
||||||
// Media node from STDB
|
// Media node from STDB
|
||||||
const mediaNode = $derived(connected ? nodeStore.get(mediaNodeId) : undefined);
|
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 casHash = $derived(metadata?.cas_hash as string | undefined);
|
||||||
const audioSrc = $derived(casHash ? casUrl(casHash) : '');
|
const audioSrc = $derived(casHash ? casUrl(casHash) : '');
|
||||||
|
|
||||||
|
|
@ -45,6 +53,16 @@
|
||||||
let rendering = $state(false);
|
let rendering = $state(false);
|
||||||
let renderJobId: string | null = $state(null);
|
let renderJobId: string | null = $state(null);
|
||||||
let resultNodeId: 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
|
// Mobile tool panel sheet
|
||||||
let showToolSheet = $state(false);
|
let showToolSheet = $state(false);
|
||||||
|
|
@ -197,41 +215,64 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MAX_POLL_FAILURES = 5;
|
||||||
|
|
||||||
function pollJobResult(jobId: string) {
|
function pollJobResult(jobId: string) {
|
||||||
// Poll job_queue for completion via simple interval
|
// Poll job_queue for completion via simple interval
|
||||||
// In production this would use SSE or WebSocket
|
// In production this would use SSE or WebSocket
|
||||||
const interval = setInterval(async () => {
|
cleanupPolling();
|
||||||
|
let failures = 0;
|
||||||
|
|
||||||
|
pollIntervalId = setInterval(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/query/job_status?job_id=${encodeURIComponent(jobId)}`, {
|
const res = await fetch(`/api/query/job_status?job_id=${encodeURIComponent(jobId)}`, {
|
||||||
headers: { Authorization: `Bearer ${accessToken}` },
|
headers: { Authorization: `Bearer ${accessToken}` },
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
failures = 0; // Reset on success
|
||||||
if (data.status === 'completed') {
|
if (data.status === 'completed') {
|
||||||
clearInterval(interval);
|
cleanupPolling();
|
||||||
resultNodeId = data.result?.processed_node_id ?? null;
|
resultNodeId = data.result?.processed_node_id ?? null;
|
||||||
rendering = false;
|
rendering = false;
|
||||||
} else if (data.status === 'error') {
|
} else if (data.status === 'error') {
|
||||||
clearInterval(interval);
|
cleanupPolling();
|
||||||
rendering = false;
|
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 {
|
} 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);
|
}, 2000);
|
||||||
|
|
||||||
// Timeout after 5 minutes
|
// Timeout after 5 minutes
|
||||||
setTimeout(() => {
|
pollTimeoutId = setTimeout(() => {
|
||||||
clearInterval(interval);
|
cleanupPolling();
|
||||||
if (rendering) {
|
if (rendering) {
|
||||||
rendering = false;
|
rendering = false;
|
||||||
console.error('Render timeout');
|
renderError = 'Rendering tok for lang tid (tidsavbrudd etter 5 minutter)';
|
||||||
}
|
}
|
||||||
}, 5 * 60 * 1000);
|
}, 5 * 60 * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cleanup polling on component destroy (navigation away)
|
||||||
|
$effect(() => {
|
||||||
|
return () => cleanupPolling();
|
||||||
|
});
|
||||||
|
|
||||||
function handleKeydown(e: KeyboardEvent) {
|
function handleKeydown(e: KeyboardEvent) {
|
||||||
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
|
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
|
||||||
if (e.key === 'Delete' || e.key === 'Backspace') {
|
if (e.key === 'Delete' || e.key === 'Backspace') {
|
||||||
|
|
@ -492,6 +533,26 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</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 -->
|
<!-- Render dialog -->
|
||||||
{#if showRenderDialog}
|
{#if showRenderDialog}
|
||||||
<RenderDialog
|
<RenderDialog
|
||||||
|
|
@ -500,6 +561,6 @@
|
||||||
jobId={renderJobId}
|
jobId={renderJobId}
|
||||||
{resultNodeId}
|
{resultNodeId}
|
||||||
onconfirm={handleRenderConfirm}
|
onconfirm={handleRenderConfirm}
|
||||||
onclose={() => { showRenderDialog = false; rendering = false; renderJobId = null; resultNodeId = null; }}
|
onclose={() => { showRenderDialog = false; rendering = false; renderJobId = null; resultNodeId = null; renderError = null; }}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
3
tasks.md
3
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.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.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.
|
- [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.
|
- [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.
|
||||||
> Påbegynt: 2026-03-18T05:50
|
|
||||||
- [ ] 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.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.
|
- [ ] 17.7 FFmpeg feilmeldinger til bruker: propager stderr fra FFmpeg-feil til frontend via strukturert feilrespons. Vis i RenderDialog.
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue