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;
|
rendering: boolean;
|
||||||
jobId: string | null;
|
jobId: string | null;
|
||||||
resultNodeId: string | null;
|
resultNodeId: string | null;
|
||||||
|
errorMessage: string | null;
|
||||||
onconfirm: (format: string) => void;
|
onconfirm: (format: string) => void;
|
||||||
onclose: () => 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');
|
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>
|
</script>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
<!-- 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>
|
<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 -->
|
<!-- Done -->
|
||||||
<div class="rounded bg-green-50 p-4 text-center">
|
<div class="rounded bg-green-50 p-4 text-center">
|
||||||
<p class="text-sm text-green-700">Rendering fullfort!</p>
|
<p class="text-sm text-green-700">Rendering fullfort!</p>
|
||||||
|
|
|
||||||
|
|
@ -533,8 +533,8 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Render error toast -->
|
<!-- Render error toast (only when dialog is closed) -->
|
||||||
{#if renderError}
|
{#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="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">
|
<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">
|
<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}
|
{rendering}
|
||||||
jobId={renderJobId}
|
jobId={renderJobId}
|
||||||
{resultNodeId}
|
{resultNodeId}
|
||||||
|
errorMessage={renderError}
|
||||||
onconfirm={handleRenderConfirm}
|
onconfirm={handleRenderConfirm}
|
||||||
onclose={() => { showRenderDialog = false; rendering = false; renderJobId = null; resultNodeId = null; renderError = null; }}
|
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", get(health::health_dashboard))
|
||||||
.route("/admin/health/logs", get(health::health_logs))
|
.route("/admin/health/logs", get(health::health_logs))
|
||||||
.route("/query/audio_info", get(intentions::audio_info))
|
.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}/feed.xml", get(rss::generate_feed))
|
||||||
.route("/pub/{slug}", get(publishing::serve_index))
|
.route("/pub/{slug}", get(publishing::serve_index))
|
||||||
// A/B-testing: klikk-sporing (oppgave 14.17)
|
// 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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
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.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.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.
|
- [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.
|
- [x] 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
|
|
||||||
|
|
||||||
## Fase 18: AI-verktøy (arbeidsflate)
|
## Fase 18: AI-verktøy (arbeidsflate)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue