feat(analysis): admin coverage overview + per-page result inspector UI
Reworks /admin/analysis from a blind enqueue form into a coverage browser: - Loads a paginated overview of mangas with a CoverageBadge (Full/Partial/None, analyzed/total) per row; debounced search filters it. - Drill manga → chapters (coverage badge each) → page grid (chips colored done/queued/failed/none) with a legend. - Click a page chip → Modal showing the full result: status, model, time, NSFW + content-warning chips, scene description, tags, and OCR lines (labeled by kind). Unanalyzed/failed pages get a "Queue this page" action (force re-analyze). - Keeps "Queue all" + per-manga/chapter enqueue and the include-analyzed toggle; enqueue refreshes the open chapter's grid so queued pages show. API client: getAnalysisMangaCoverage / ChapterCoverage / ChapterPages / PageDetail + types. New CoverageBadge component. Tests: vitest for the four clients; Playwright for overview+badges+queue-all, drill+inspect, and queue-from-detail. svelte-check + build clean; 266 vitest. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -28,7 +28,11 @@ import {
|
||||
listActiveJobs,
|
||||
listMissingCovers,
|
||||
reenqueueAnalysis,
|
||||
analyzePage
|
||||
analyzePage,
|
||||
getAnalysisMangaCoverage,
|
||||
getAnalysisChapterCoverage,
|
||||
getAnalysisChapterPages,
|
||||
getAnalysisPageDetail
|
||||
} from './admin';
|
||||
|
||||
function ok(body: unknown, status = 200): Response {
|
||||
@@ -534,4 +538,46 @@ describe('admin crawler api client', () => {
|
||||
expect(url).toMatch(/\/v1\/admin\/pages\/p1\/analyze$/);
|
||||
expect(fetchSpy.mock.calls[0][1]!.method).toBe('POST');
|
||||
});
|
||||
|
||||
it('getAnalysisMangaCoverage passes search + pagination', async () => {
|
||||
fetchSpy.mockResolvedValueOnce(
|
||||
ok({ items: [], page: { limit: 25, offset: 0, total: 0 } })
|
||||
);
|
||||
await getAnalysisMangaCoverage({ search: 'ber', limit: 25, offset: 50 });
|
||||
const url = fetchSpy.mock.calls[0][0] as string;
|
||||
expect(url).toContain('/v1/admin/analysis/mangas?');
|
||||
expect(url).toContain('search=ber');
|
||||
expect(url).toContain('limit=25');
|
||||
expect(url).toContain('offset=50');
|
||||
});
|
||||
|
||||
it('getAnalysisChapterCoverage unwraps items', async () => {
|
||||
fetchSpy.mockResolvedValueOnce(
|
||||
ok({ items: [{ chapter_id: 'c1', number: 1, title: null, total_pages: 2, analyzed_pages: 1 }] })
|
||||
);
|
||||
const items = await getAnalysisChapterCoverage('m1');
|
||||
const url = fetchSpy.mock.calls[0][0] as string;
|
||||
expect(url).toMatch(/\/v1\/admin\/analysis\/mangas\/m1\/chapters$/);
|
||||
expect(items).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('getAnalysisChapterPages unwraps items', async () => {
|
||||
fetchSpy.mockResolvedValueOnce(
|
||||
ok({ items: [{ page_id: 'p1', page_number: 1, status: 'done' }] })
|
||||
);
|
||||
const items = await getAnalysisChapterPages('c1');
|
||||
const url = fetchSpy.mock.calls[0][0] as string;
|
||||
expect(url).toMatch(/\/v1\/admin\/analysis\/chapters\/c1\/pages$/);
|
||||
expect(items[0].status).toBe('done');
|
||||
});
|
||||
|
||||
it('getAnalysisPageDetail hits the page endpoint', async () => {
|
||||
fetchSpy.mockResolvedValueOnce(
|
||||
ok({ page_id: 'p1', status: 'none', ocr: [], tags: [], content_warnings: [] })
|
||||
);
|
||||
const d = await getAnalysisPageDetail('p1');
|
||||
const url = fetchSpy.mock.calls[0][0] as string;
|
||||
expect(url).toMatch(/\/v1\/admin\/analysis\/pages\/p1$/);
|
||||
expect(d.status).toBe('none');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user