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:
@@ -403,3 +403,41 @@ export async function listMissingCovers(
|
||||
init
|
||||
);
|
||||
}
|
||||
|
||||
// ---- AI content analysis ---------------------------------------------------
|
||||
|
||||
/** Options for the scoped analysis re-enqueue. `mangaId` and `chapterId`
|
||||
* are mutually exclusive; omit both to target the whole library. */
|
||||
export type ReenqueueAnalysisOptions = {
|
||||
/** Skip pages that already have a completed analysis (default true).
|
||||
* When false, in-scope pages are force re-analyzed. */
|
||||
onlyUnanalyzed?: boolean;
|
||||
mangaId?: string;
|
||||
chapterId?: string;
|
||||
};
|
||||
|
||||
/** Bulk-enqueue `analyze_page` jobs for all / a manga's / a chapter's
|
||||
* pages. Returns how many jobs were enqueued. Requires ANALYSIS_ENABLED
|
||||
* on the backend (503 otherwise). */
|
||||
export async function reenqueueAnalysis(
|
||||
opts: ReenqueueAnalysisOptions = {}
|
||||
): Promise<{ enqueued: number }> {
|
||||
const body: Record<string, unknown> = {
|
||||
only_unanalyzed: opts.onlyUnanalyzed ?? true
|
||||
};
|
||||
if (opts.mangaId) body.manga_id = opts.mangaId;
|
||||
if (opts.chapterId) body.chapter_id = opts.chapterId;
|
||||
return request<{ enqueued: number }>('/v1/admin/analysis/reenqueue', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
}
|
||||
|
||||
/** Force re-analysis of a single page (used by the reader context menu). */
|
||||
export async function analyzePage(pageId: string): Promise<{ enqueued: boolean }> {
|
||||
return request<{ enqueued: boolean }>(
|
||||
`/v1/admin/pages/${encodeURIComponent(pageId)}/analyze`,
|
||||
{ method: 'POST' }
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user