Fullfører oppgave 17.7: FFmpeg feilmeldinger til bruker

Tre endringer som sammen gir brukeren innsyn i FFmpeg-feil:

1. Backend: Nytt GET /query/job_status-endepunkt i queries.rs.
   Frontenden pollet allerede denne URLen, men endepunktet manglet.
   Returnerer status, result og error_msg fra job_queue.

2. RenderDialog: Ny error-tilstand med formatFfmpegError() som
   trekker ut lesbar feilmelding fra FFmpeg stderr-dump. Viser
   kort oppsummering + ekspanderbar full feilmelding via <details>.

3. Studio-side: Sender renderError til RenderDialog som errorMessage.
   Toast-varselet vises kun når dialogen er lukket (unngår duplisering).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
vegard 2026-03-18 06:03:59 +00:00
parent 9f4dfee232
commit a3cdfa9dc2
5 changed files with 110 additions and 6 deletions

View file

@ -6,13 +6,33 @@
rendering: boolean;
jobId: string | null;
resultNodeId: string | null;
errorMessage: string | null;
onconfirm: (format: string) => void;
onclose: () => void;
}
let { operations, rendering, jobId, resultNodeId, onconfirm, onclose }: Props = $props();
let { operations, rendering, jobId, resultNodeId, errorMessage, onconfirm, onclose }: Props = $props();
let format = $state('mp3');
/** Trekk ut brukerlesbar FFmpeg-feil fra stderr-dump. */
function formatFfmpegError(raw: string): { summary: string; detail: string | null } {
// Fjern "ffmpeg feilet: "-prefiks fra maskinrommet
let cleaned = raw.replace(/^ffmpeg feilet:\s*/i, '');
// Finn siste linje som inneholder en feilmelding (typisk FFmpeg-mønster)
const lines = cleaned.split('\n').filter((l) => l.trim());
const errorLine = lines.findLast(
(l) =>
/error|invalid|no such|not found|unknown|unsupported|conversion failed/i.test(l) &&
!/^frame=/i.test(l)
);
const summary = errorLine?.trim() || lines.at(-1)?.trim() || 'Ukjent FFmpeg-feil';
const detail = lines.length > 1 ? cleaned.trim() : null;
return { summary, detail };
}
</script>
<!-- svelte-ignore a11y_no_static_element_interactions -->
@ -27,7 +47,33 @@
>
<h2 class="mb-4 text-lg font-semibold text-gray-800">Render lyd</h2>
{#if resultNodeId}
{#if errorMessage}
<!-- Error -->
{@const parsed = formatFfmpegError(errorMessage)}
<div class="rounded bg-red-50 p-4">
<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 min-w-0">
<p class="text-sm font-medium text-red-800">Rendering feilet</p>
<p class="mt-1 text-sm text-red-700">{parsed.summary}</p>
{#if parsed.detail}
<details class="mt-2">
<summary class="cursor-pointer text-xs text-red-500 hover:text-red-700">Vis full feilmelding</summary>
<pre class="mt-1 max-h-48 overflow-auto whitespace-pre-wrap break-all rounded bg-red-100 p-2 text-xs text-red-800 font-mono">{parsed.detail}</pre>
</details>
{/if}
</div>
</div>
</div>
<button
onclick={onclose}
class="mt-4 w-full rounded bg-gray-100 px-4 py-2 text-sm hover:bg-gray-200"
>
Lukk
</button>
{:else if resultNodeId}
<!-- Done -->
<div class="rounded bg-green-50 p-4 text-center">
<p class="text-sm text-green-700">Rendering fullfort!</p>

View file

@ -533,8 +533,8 @@
{/if}
</div>
<!-- Render error toast -->
{#if renderError}
<!-- Render error toast (only when dialog is closed) -->
{#if renderError && !showRenderDialog}
<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">
@ -560,6 +560,7 @@
{rendering}
jobId={renderJobId}
{resultNodeId}
errorMessage={renderError}
onconfirm={handleRenderConfirm}
onclose={() => { showRenderDialog = false; rendering = false; renderJobId = null; resultNodeId = null; renderError = null; }}
/>

View file

@ -248,6 +248,7 @@ async fn main() {
.route("/admin/health", get(health::health_dashboard))
.route("/admin/health/logs", get(health::health_logs))
.route("/query/audio_info", get(intentions::audio_info))
.route("/query/job_status", get(queries::query_job_status))
.route("/pub/{slug}/feed.xml", get(rss::generate_feed))
.route("/pub/{slug}", get(publishing::serve_index))
// A/B-testing: klikk-sporing (oppgave 14.17)

View file

@ -1336,6 +1336,63 @@ pub async fn query_presentation_elements(
}))
}
// =============================================================================
// GET /query/job_status — jobbstatus for polling fra frontend (oppgave 17.7)
// =============================================================================
#[derive(Deserialize)]
pub struct JobStatusParams {
job_id: Uuid,
}
#[derive(Serialize)]
pub struct JobStatusResponse {
pub status: String,
pub result: Option<serde_json::Value>,
pub error_msg: Option<String>,
}
/// GET /query/job_status?job_id=...
///
/// Returnerer status, resultat og feilmelding for en jobb.
/// Brukes av frontend for å polle etter rendering-resultater.
pub async fn query_job_status(
State(state): State<AppState>,
_user: AuthUser,
axum::extract::Query(params): axum::extract::Query<JobStatusParams>,
) -> Result<Json<JobStatusResponse>, Response> {
let row = sqlx::query_as::<_, (String, Option<serde_json::Value>, Option<String>)>(
r#"SELECT status::text, result, error_msg FROM job_queue WHERE id = $1"#,
)
.bind(params.job_id)
.fetch_optional(&state.db)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: format!("DB-feil: {e}"),
}),
)
.into_response()
})?;
match row {
Some((status, result, error_msg)) => Ok(Json(JobStatusResponse {
status,
result,
error_msg,
})),
None => Err((
StatusCode::NOT_FOUND,
Json(ErrorResponse {
error: "Jobb ikke funnet".to_string(),
}),
)
.into_response()),
}
}
#[cfg(test)]
mod tests {
use super::*;

View file

@ -195,8 +195,7 @@ Ref: Kodegjennomgang av `b4c4bb8` (Lydstudio: lydredigering via FFmpeg).
- [x] 17.4 Frontend input-begrensninger: legg til `min`/`max` på alle tallfelter i OperationPanel (silenceThreshold, fadeMs, normTarget, compRatio). Hindre ugyldig input.
- [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.
- [x] 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.
> Påbegynt: 2026-03-18T05:58
- [x] 17.7 FFmpeg feilmeldinger til bruker: propager stderr fra FFmpeg-feil til frontend via strukturert feilrespons. Vis i RenderDialog.
## Fase 18: AI-verktøy (arbeidsflate)