From c6536d5b9feb988fb3dfe46abb9124a8da61a3dd Mon Sep 17 00:00:00 2001 From: Admin Date: Sat, 7 Mar 2026 20:20:48 +0500 Subject: [PATCH] Add /admin/audio-jobs page for audio generation job history New page mirrors the scrape tasks pattern: loads audio_jobs from PocketBase via a new listAudioJobs() helper, shows a filterable table (slug, chapter, voice, status, started, duration, error), and live-polls every 3s while any job is pending or generating. Also fixes the /admin/audio nav active-state check (was startsWith, now exact match) to prevent it from matching /admin/audio-jobs. --- ui/src/lib/server/pocketbase.ts | 18 +++ ui/src/routes/+layout.svelte | 17 +- .../routes/admin/audio-jobs/+page.server.ts | 17 ++ ui/src/routes/admin/audio-jobs/+page.svelte | 152 ++++++++++++++++++ 4 files changed, 202 insertions(+), 2 deletions(-) create mode 100644 ui/src/routes/admin/audio-jobs/+page.server.ts create mode 100644 ui/src/routes/admin/audio-jobs/+page.svelte diff --git a/ui/src/lib/server/pocketbase.ts b/ui/src/lib/server/pocketbase.ts index 721daab..8b8c45d 100644 --- a/ui/src/lib/server/pocketbase.ts +++ b/ui/src/lib/server/pocketbase.ts @@ -657,6 +657,24 @@ export async function listScrapingTasks(): Promise { return listAll('scraping_tasks', '', '-started'); } +// ─── Audio jobs ─────────────────────────────────────────────────────────────── + +export interface AudioJob { + id: string; + cache_key: string; // "slug/chapter/voice" + slug: string; + chapter: number; + voice: string; + status: string; // "pending" | "generating" | "done" | "failed" + error_message: string; + started: string; + finished: string; +} + +export async function listAudioJobs(): Promise { + return listAll('audio_jobs', '', '-started'); +} + export async function getAudioTime( sessionId: string, slug: string, diff --git a/ui/src/routes/+layout.svelte b/ui/src/routes/+layout.svelte index 81b1bee..ce3a278 100644 --- a/ui/src/routes/+layout.svelte +++ b/ui/src/routes/+layout.svelte @@ -247,10 +247,16 @@ + {/if} (menuOpen = false)} - class="px-3 py-2.5 rounded-lg text-sm font-medium transition-colors {page.url.pathname.startsWith('/admin/audio') ? 'bg-zinc-800 text-zinc-100' : 'text-zinc-400 hover:bg-zinc-800 hover:text-zinc-100'}" + class="px-3 py-2.5 rounded-lg text-sm font-medium transition-colors {page.url.pathname === '/admin/audio' ? 'bg-zinc-800 text-zinc-100' : 'text-zinc-400 hover:bg-zinc-800 hover:text-zinc-100'}" > Audio cache + (menuOpen = false)} + class="px-3 py-2.5 rounded-lg text-sm font-medium transition-colors {page.url.pathname.startsWith('/admin/audio-jobs') ? 'bg-zinc-800 text-zinc-100' : 'text-zinc-400 hover:bg-zinc-800 hover:text-zinc-100'}" + > + Audio jobs + {/if}
diff --git a/ui/src/routes/admin/audio-jobs/+page.server.ts b/ui/src/routes/admin/audio-jobs/+page.server.ts new file mode 100644 index 0000000..83baa67 --- /dev/null +++ b/ui/src/routes/admin/audio-jobs/+page.server.ts @@ -0,0 +1,17 @@ +import { redirect } from '@sveltejs/kit'; +import type { PageServerLoad } from './$types'; +import { listAudioJobs } from '$lib/server/pocketbase'; +import { log } from '$lib/server/logger'; + +export const load: PageServerLoad = async ({ locals }) => { + if (locals.user?.role !== 'admin') { + redirect(302, '/'); + } + + const jobs = await listAudioJobs().catch((e) => { + log.warn('admin/audio-jobs', 'failed to load audio jobs', { err: String(e) }); + return []; + }); + + return { jobs }; +}; diff --git a/ui/src/routes/admin/audio-jobs/+page.svelte b/ui/src/routes/admin/audio-jobs/+page.svelte new file mode 100644 index 0000000..a960e09 --- /dev/null +++ b/ui/src/routes/admin/audio-jobs/+page.svelte @@ -0,0 +1,152 @@ + + + + Audio jobs — libnovel admin + + +
+
+
+

Audio jobs

+

+ {stats.total} total · + {stats.done} done · + {#if stats.failed > 0} + {stats.failed} failed · + {/if} + {#if stats.inFlight > 0} + {stats.inFlight} in-flight + {:else} + 0 in-flight + {/if} +

+
+
+ + + + + {#if filtered.length === 0} +

+ {q.trim() ? 'No results.' : 'No audio jobs yet.'} +

+ {:else} +
+ + + + + + + + + + + + + {#each filtered as job} + + + + + + + + + {#if job.error_message} + + + + {/if} + {/each} + +
BookCh.VoiceStatusStartedDuration
+ + {job.slug} + + {job.chapter}{job.voice} + {job.status} + {fmtDate(job.started)}{duration(job.started, job.finished)}
{job.error_message}
+
+ {/if} +