From 7bb38004907fe047d95cb97a8074dcd9ab1b671d Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 22 Apr 2026 22:39:47 +0200 Subject: [PATCH] feat(frontend): add admin card to generate thumbnails with polling Fourth card on /admin/system mirrors the mass-import pattern: - POST /api/admin/generate-thumbnails to trigger - 2000 ms polling on /api/admin/thumbnail-status while RUNNING - processed / skipped / failed counters in the DONE message - standalone pollInterval so import and thumbnail polling don't interfere with each other Paraglide keys added in de/en/es, mirroring admin_system_import_*. Refs #307 Co-Authored-By: Claude Opus 4.7 --- frontend/messages/de.json | 7 ++ frontend/messages/en.json | 7 ++ frontend/messages/es.json | 7 ++ frontend/src/routes/admin/system/+page.svelte | 109 +++++++++++++++++- 4 files changed, 129 insertions(+), 1 deletion(-) diff --git a/frontend/messages/de.json b/frontend/messages/de.json index f2e1feab..7df88f45 100644 --- a/frontend/messages/de.json +++ b/frontend/messages/de.json @@ -338,6 +338,13 @@ "admin_system_import_status_running": "Import läuft…", "admin_system_import_status_done": "Import abgeschlossen – {count} Dokumente verarbeitet.", "admin_system_import_status_failed": "Fehler: {message}", + "admin_system_thumbnails_heading": "Thumbnails erzeugen", + "admin_system_thumbnails_description": "Erzeugt Vorschaubilder für Dokumente ohne Thumbnail (z. B. nach dem Massenimport).", + "admin_system_thumbnails_btn_start": "Thumbnails erzeugen", + "admin_system_thumbnails_btn_retry": "Erneut starten", + "admin_system_thumbnails_status_running": "Thumbnail-Generierung läuft…", + "admin_system_thumbnails_status_done": "Fertig – {processed} erzeugt, {skipped} übersprungen, {failed} fehlgeschlagen.", + "admin_system_thumbnails_status_failed": "Fehler: {message}", "comp_expandable_show_more": "Mehr anzeigen", "comp_expandable_show_less": "Weniger anzeigen", "error_comment_not_found": "Der Kommentar wurde nicht gefunden.", diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 4a4080d0..dc5cbc93 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -338,6 +338,13 @@ "admin_system_import_status_running": "Import running…", "admin_system_import_status_done": "Import complete – {count} documents processed.", "admin_system_import_status_failed": "Error: {message}", + "admin_system_thumbnails_heading": "Generate thumbnails", + "admin_system_thumbnails_description": "Generates preview images for documents without a thumbnail (e.g. after the mass import).", + "admin_system_thumbnails_btn_start": "Generate thumbnails", + "admin_system_thumbnails_btn_retry": "Run again", + "admin_system_thumbnails_status_running": "Thumbnail generation running…", + "admin_system_thumbnails_status_done": "Done — {processed} generated, {skipped} skipped, {failed} failed.", + "admin_system_thumbnails_status_failed": "Error: {message}", "comp_expandable_show_more": "Show more", "comp_expandable_show_less": "Show less", "error_comment_not_found": "The comment could not be found.", diff --git a/frontend/messages/es.json b/frontend/messages/es.json index 08effdff..80ea31a0 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -338,6 +338,13 @@ "admin_system_import_status_running": "Importación en curso…", "admin_system_import_status_done": "Importación completada – {count} documentos procesados.", "admin_system_import_status_failed": "Error: {message}", + "admin_system_thumbnails_heading": "Generar miniaturas", + "admin_system_thumbnails_description": "Genera imágenes de vista previa para documentos sin miniatura (p. ej. tras la importación masiva).", + "admin_system_thumbnails_btn_start": "Generar miniaturas", + "admin_system_thumbnails_btn_retry": "Reiniciar", + "admin_system_thumbnails_status_running": "Generación de miniaturas en curso…", + "admin_system_thumbnails_status_done": "Listo — {processed} generadas, {skipped} omitidas, {failed} fallidas.", + "admin_system_thumbnails_status_failed": "Error: {message}", "comp_expandable_show_more": "Mostrar más", "comp_expandable_show_less": "Mostrar menos", "error_comment_not_found": "El comentario no pudo encontrarse.", diff --git a/frontend/src/routes/admin/system/+page.svelte b/frontend/src/routes/admin/system/+page.svelte index 2bc02c83..aa734abf 100644 --- a/frontend/src/routes/admin/system/+page.svelte +++ b/frontend/src/routes/admin/system/+page.svelte @@ -14,8 +14,20 @@ type ImportStatus = { startedAt: string | null; }; +type ThumbnailStatus = { + state: 'IDLE' | 'RUNNING' | 'DONE' | 'FAILED'; + message: string; + total: number; + processed: number; + skipped: number; + failed: number; + startedAt: string | null; +}; + let importStatus: ImportStatus | null = $state(null); +let thumbnailStatus: ThumbnailStatus | null = $state(null); let pollInterval: ReturnType | null = null; +let thumbnailPollInterval: ReturnType | null = null; function startPolling() { if (pollInterval) return; @@ -29,6 +41,18 @@ function stopPolling() { } } +function startThumbnailPolling() { + if (thumbnailPollInterval) return; + thumbnailPollInterval = setInterval(fetchThumbnailStatus, 2000); +} + +function stopThumbnailPolling() { + if (thumbnailPollInterval) { + clearInterval(thumbnailPollInterval); + thumbnailPollInterval = null; + } +} + async function fetchImportStatus() { const res = await fetch('/api/admin/import-status'); if (res.ok) { @@ -51,11 +75,37 @@ async function triggerImport() { } } +async function fetchThumbnailStatus() { + const res = await fetch('/api/admin/thumbnail-status'); + if (res.ok) { + thumbnailStatus = await res.json(); + if (thumbnailStatus!.state === 'RUNNING') { + startThumbnailPolling(); + } else { + stopThumbnailPolling(); + } + } +} + +async function triggerThumbnails() { + const res = await fetch('/api/admin/generate-thumbnails', { method: 'POST' }); + if (res.ok) { + thumbnailStatus = await res.json(); + if (thumbnailStatus!.state === 'RUNNING') { + startThumbnailPolling(); + } + } +} + $effect(() => { fetchImportStatus(); + fetchThumbnailStatus(); }); -onDestroy(() => stopPolling()); +onDestroy(() => { + stopPolling(); + stopThumbnailPolling(); +}); async function backfillVersions() { backfillLoading = true; @@ -168,5 +218,62 @@ async function backfillFileHashes() { {/if} + + +
+

+ {m.admin_system_thumbnails_heading()} +

+

{m.admin_system_thumbnails_description()}

+ + {#if thumbnailStatus?.state === 'RUNNING'} +

+ {m.admin_system_thumbnails_status_running()} + {#if thumbnailStatus.total > 0} + + ({thumbnailStatus.processed + thumbnailStatus.skipped + thumbnailStatus.failed} / + {thumbnailStatus.total}) + + {/if} +

+ {:else if thumbnailStatus?.state === 'DONE'} +

+ {m.admin_system_thumbnails_status_done({ + processed: thumbnailStatus.processed, + skipped: thumbnailStatus.skipped, + failed: thumbnailStatus.failed + })} +

+ + {:else if thumbnailStatus?.state === 'FAILED'} +

+ {m.admin_system_thumbnails_status_failed({ message: thumbnailStatus.message })} +

+ + {:else} + + {/if} +