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:
MechaCat02
2026-06-13 20:39:54 +02:00
parent 824f5acf22
commit 6bb72b8775
8 changed files with 902 additions and 269 deletions

View File

@@ -0,0 +1,56 @@
<script lang="ts">
/**
* Analysis-coverage badge: `analyzed/total` colored by state —
* green Full, amber Partial, grey None. Reused for manga and chapter
* rows in the admin Analysis section.
*/
let {
analyzed,
total,
testid
}: { analyzed: number; total: number; testid?: string } = $props();
const state = $derived(
total === 0 || analyzed === 0
? 'none'
: analyzed >= total
? 'full'
: 'partial'
);
const word = $derived(
state === 'full' ? 'Full' : state === 'partial' ? 'Partial' : 'None'
);
</script>
<span class="badge {state}" data-testid={testid} data-state={state}>
{word} {analyzed}/{total}
</span>
<style>
.badge {
display: inline-flex;
align-items: center;
gap: var(--space-1);
font-size: var(--font-xs);
font-weight: var(--weight-semibold);
padding: 1px var(--space-2);
border-radius: var(--radius-pill);
border: 1px solid transparent;
white-space: nowrap;
}
.full {
background: color-mix(in srgb, #2e7d32 16%, transparent);
color: #2e7d32;
border-color: color-mix(in srgb, #2e7d32 40%, transparent);
}
.partial {
background: color-mix(in srgb, #d4762a 16%, transparent);
color: #b85e1a;
border-color: color-mix(in srgb, #d4762a 40%, transparent);
}
.none {
background: var(--surface-elevated);
color: var(--text-muted);
border-color: var(--border);
}
</style>