feat(analysis): admin UI — reader context-menu action + dashboard section

Surfaces the scoped re-enqueue and per-page force-analyze in the UI:

- api/admin: reenqueueAnalysis({onlyUnanalyzed, mangaId, chapterId}) and
  analyzePage(pageId).
- Reader PageContextMenu gains an admin-only "Queue for analysis" item
  (canAnalyze/onAnalyzePage/analyzeState props) wired to the force-analyze
  endpoint with inline busy/done/error feedback; reset per page.
- New /admin/analysis dashboard section + nav tab: scope selector
  (whole library / one manga / one chapter), id input, and an
  "include already-analyzed" toggle (default off), with busy/notice/error.

Tests: vitest for the two clients; Playwright for the context-menu action
(admin vs non-admin) and the admin section (scope + include toggle +
validation). svelte-check + build clean; 262 vitest, 10 new e2e green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-13 19:57:03 +02:00
parent 9607488278
commit 8b7ea2e1b2
11 changed files with 551 additions and 8 deletions

View File

@@ -7,7 +7,7 @@
import { preferences } from '$lib/preferences.svelte';
import { updateReadProgress } from '$lib/api/read_progress';
import { chapterLabel } from '$lib/api/chapters';
import { resyncChapter } from '$lib/api/admin';
import { resyncChapter, analyzePage } from '$lib/api/admin';
import { readerFullscreen } from '$lib/reader-fullscreen.svelte';
import { session } from '$lib/session.svelte';
import Sheet from '$lib/components/Sheet.svelte';
@@ -118,6 +118,8 @@
let actionSheetOpen = $state(false);
let collectionsModalOpen = $state(false);
let tagsModalOpen = $state(false);
// Admin-only "Queue for analysis" action feedback, reset per page.
let analyzeState = $state<'idle' | 'busy' | 'done' | 'error'>('idle');
// Monotonically-increasing token so a slow loadPageSummary for
// page A can't clobber a fresh load for page B. The user right-
@@ -148,13 +150,27 @@
activePageId = pageId;
contextMenuAnchor = anchor;
contextMenuOpen = true;
analyzeState = 'idle';
void loadPageSummary(pageId);
}
async function handleAnalyzePage() {
const pageId = activePageId;
if (!pageId || !session.user?.is_admin || analyzeState === 'busy') return;
analyzeState = 'busy';
try {
await analyzePage(pageId);
analyzeState = 'done';
} catch {
analyzeState = 'error';
}
}
function openActionSheet(pageId: string) {
if (!session.user) return;
activePageId = pageId;
actionSheetOpen = true;
analyzeState = 'idle';
void loadPageSummary(pageId);
}
@@ -1454,6 +1470,9 @@
onCopyLink={handleCopyLink}
collectionsCount={activePageCollectionCount}
tags={activePageTags}
canAnalyze={!!session.user?.is_admin}
onAnalyzePage={handleAnalyzePage}
{analyzeState}
/>
<!-- Mobile action sheet: same two actions, larger touch targets. -->