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:
parent
9f4dfee232
commit
a3cdfa9dc2
5 changed files with 110 additions and 6 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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; }}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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::*;
|
||||
|
|
|
|||
3
tasks.md
3
tasks.md
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue