diff --git a/ui/src/routes/admin/+layout.server.ts b/ui/src/routes/admin/+layout.server.ts index 24471a4..072808d 100644 --- a/ui/src/routes/admin/+layout.server.ts +++ b/ui/src/routes/admin/+layout.server.ts @@ -1,8 +1,16 @@ import { redirect } from '@sveltejs/kit'; import type { LayoutServerLoad } from './$types'; +import { listAIJobs } from '$lib/server/pocketbase'; +import { log } from '$lib/server/logger'; export const load: LayoutServerLoad = async ({ locals }) => { if (locals.user?.role !== 'admin') { redirect(302, '/'); } + const jobs = await listAIJobs().catch((e) => { + log.warn('admin/layout', 'failed to load ai jobs for sidebar badge', { err: String(e) }); + return []; + }); + const runningAiJobs = jobs.filter((j) => j.status === 'running' || j.status === 'pending').length; + return { runningAiJobs }; }; diff --git a/ui/src/routes/admin/+layout.svelte b/ui/src/routes/admin/+layout.svelte index 0577f62..f5ef1ed 100644 --- a/ui/src/routes/admin/+layout.svelte +++ b/ui/src/routes/admin/+layout.svelte @@ -1,6 +1,7 @@ @@ -136,6 +138,7 @@ {#each internalLinks as link} {@const active = page.url.pathname.startsWith(link.href)} + {@const isAiJobs = link.href === '/admin/ai-jobs'} (sidebarOpen = false)} @@ -147,7 +150,12 @@ {@html link.icon} - {link.label()} + {link.label()} + {#if isAiJobs && data.runningAiJobs > 0} + + {data.runningAiJobs} + + {/if} {/each} diff --git a/ui/src/routes/admin/ai-jobs/+page.svelte b/ui/src/routes/admin/ai-jobs/+page.svelte index a8c8b6a..32c3817 100644 --- a/ui/src/routes/admin/ai-jobs/+page.svelte +++ b/ui/src/routes/admin/ai-jobs/+page.svelte @@ -57,6 +57,7 @@ // ── Cancel ──────────────────────────────────────────────────────────────────── let cancellingIds = $state(new Set()); let cancelErrors: Record = $state({}); + let cancellingAll = $state(false); async function cancelJob(id: string) { if (cancellingIds.has(id)) return; @@ -77,6 +78,14 @@ } } + async function cancelAllRunning() { + if (cancellingAll) return; + cancellingAll = true; + const inFlight = jobs.filter((j) => j.status === 'running' || j.status === 'pending'); + await Promise.all(inFlight.map((j) => cancelJob(j.id))); + cancellingAll = false; + } + // ── Review & Apply (chapter-names jobs) ────────────────────────────────────── interface ProposedTitle { @@ -411,7 +420,9 @@ function fmtDate(s: string | undefined) { if (!s) return '—'; - return new Date(s).toLocaleString(undefined, { + const d = new Date(s); + if (d.getFullYear() < 2000) return '—'; + return d.toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', @@ -482,6 +493,27 @@ {/each} + + {#if stats.running + stats.pending > 0} + + + {#if cancellingAll} + + + + + Cancelling… + {:else} + Cancel all in-flight ({stats.running + stats.pending}) + {/if} + + + {/if} + + + + + + + + Broadcast to users + To send push notifications or in-app messages to all subscribers, use the push dashboard. + + Open push.libnovel.cc + + + + + + + - Notifications + Your Notification Inbox {#if unreadCount > 0} {unreadCount} unread {/if}
Broadcast to users
To send push notifications or in-app messages to all subscribers, use the push dashboard.
{unreadCount} unread