feat: add batch metadata jobs, series filters, and translate backoffice to French

- Add metadata_batch job type with background processing via tokio::spawn
- Auto-apply metadata only when single result at 100% confidence
- Support primary + fallback provider per library, "none" to opt out
- Add batch report/results API endpoints and job detail UI
- Add series_status and has_missing filters to both series listing pages
- Add GET /series/statuses endpoint for dynamic filter options
- Normalize series_metadata status values (migration 0036)
- Hide ComicVine provider tab when no API key configured
- Translate entire backoffice UI from English to French

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-18 18:26:44 +01:00
parent 9a8c1577af
commit b955c2697c
46 changed files with 2161 additions and 379 deletions

View File

@@ -45,15 +45,15 @@ export default async function TokensPage({
<svg className="w-8 h-8 text-destructive" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
</svg>
API Tokens
Jetons API
</h1>
</div>
{params.created ? (
<Card className="mb-6 border-success/50 bg-success/5">
<CardHeader>
<CardTitle className="text-success">Token Created</CardTitle>
<CardDescription>Copy it now, it won't be shown again</CardDescription>
<CardTitle className="text-success">Jeton créé</CardTitle>
<CardDescription>Copiez-le maintenant, il ne sera plus affiché</CardDescription>
</CardHeader>
<CardContent>
<pre className="p-4 bg-background rounded-lg text-sm font-mono text-foreground overflow-x-auto border">{params.created}</pre>
@@ -63,22 +63,22 @@ export default async function TokensPage({
<Card className="mb-6">
<CardHeader>
<CardTitle>Create New Token</CardTitle>
<CardDescription>Generate a new API token with the desired scope</CardDescription>
<CardTitle>Créer un nouveau jeton</CardTitle>
<CardDescription>Générer un nouveau jeton API avec la portée souhaitée</CardDescription>
</CardHeader>
<CardContent>
<form action={createTokenAction}>
<FormRow>
<FormField className="flex-1 min-w-48">
<FormInput name="name" placeholder="Token name" required />
<FormInput name="name" placeholder="Nom du jeton" required />
</FormField>
<FormField className="w-32">
<FormSelect name="scope" defaultValue="read">
<option value="read">Read</option>
<option value="read">Lecture</option>
<option value="admin">Admin</option>
</FormSelect>
</FormField>
<Button type="submit">Create Token</Button>
<Button type="submit">Créer le jeton</Button>
</FormRow>
</form>
</CardContent>
@@ -89,10 +89,10 @@ export default async function TokensPage({
<table className="w-full">
<thead>
<tr className="border-b border-border/60 bg-muted/50">
<th className="px-4 py-3 text-left text-xs font-semibold text-muted-foreground uppercase tracking-wider">Name</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-muted-foreground uppercase tracking-wider">Scope</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-muted-foreground uppercase tracking-wider">Prefix</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-muted-foreground uppercase tracking-wider">Status</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-muted-foreground uppercase tracking-wider">Nom</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-muted-foreground uppercase tracking-wider">Portée</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-muted-foreground uppercase tracking-wider">Préfixe</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-muted-foreground uppercase tracking-wider">Statut</th>
<th className="px-4 py-3 text-left text-xs font-semibold text-muted-foreground uppercase tracking-wider">Actions</th>
</tr>
</thead>
@@ -110,9 +110,9 @@ export default async function TokensPage({
</td>
<td className="px-4 py-3 text-sm">
{token.revoked_at ? (
<Badge variant="error">Revoked</Badge>
<Badge variant="error">Révoqué</Badge>
) : (
<Badge variant="success">Active</Badge>
<Badge variant="success">Actif</Badge>
)}
</td>
<td className="px-4 py-3">
@@ -123,7 +123,7 @@ export default async function TokensPage({
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Revoke
Révoquer
</Button>
</form>
) : (
@@ -133,7 +133,7 @@ export default async function TokensPage({
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
Delete
Supprimer
</Button>
</form>
)}