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

@@ -152,7 +152,7 @@ export function JobsIndicator() {
hover:bg-accent
transition-colors duration-200
"
title="View all jobs"
title="Voir toutes les tâches"
>
<JobsIcon className="w-[18px] h-[18px]" />
</Link>
@@ -187,11 +187,11 @@ export function JobsIndicator() {
<div className="flex items-center gap-3">
<span className="text-xl">📊</span>
<div>
<h3 className="font-semibold text-foreground">Active Jobs</h3>
<h3 className="font-semibold text-foreground">Tâches actives</h3>
<p className="text-xs text-muted-foreground">
{runningJobs.length > 0
? `${runningJobs.length} running, ${pendingJobs.length} pending`
: `${pendingJobs.length} job${pendingJobs.length !== 1 ? 's' : ''} pending`
? `${runningJobs.length} en cours, ${pendingJobs.length} en attente`
: `${pendingJobs.length} tâche${pendingJobs.length !== 1 ? 's' : ''} en attente`
}
</p>
</div>
@@ -201,7 +201,7 @@ export function JobsIndicator() {
className="text-sm font-medium text-primary hover:text-primary/80 transition-colors"
onClick={() => setIsOpen(false)}
>
View All
Tout voir
</Link>
</div>
@@ -209,7 +209,7 @@ export function JobsIndicator() {
{runningJobs.length > 0 && (
<div className="px-4 py-3 border-b border-border/60">
<div className="flex items-center justify-between text-sm mb-2">
<span className="text-muted-foreground">Overall Progress</span>
<span className="text-muted-foreground">Progression globale</span>
<span className="font-semibold text-foreground">{Math.round(totalProgress)}%</span>
</div>
<ProgressBar value={totalProgress} size="sm" variant="success" />
@@ -221,7 +221,7 @@ export function JobsIndicator() {
{activeJobs.length === 0 ? (
<div className="flex flex-col items-center justify-center py-8 text-muted-foreground">
<span className="text-4xl mb-2"></span>
<p>No active jobs</p>
<p>Aucune tâche active</p>
</div>
) : (
<ul className="divide-y divide-border/60">
@@ -242,7 +242,7 @@ export function JobsIndicator() {
<div className="flex items-center gap-2 mb-1">
<code className="text-xs px-1.5 py-0.5 bg-muted rounded font-mono">{job.id.slice(0, 8)}</code>
<Badge variant={job.type === 'rebuild' ? 'primary' : job.type === 'thumbnail_regenerate' ? 'warning' : 'secondary'} className="text-[10px]">
{job.type === 'thumbnail_rebuild' ? 'Thumbnails' : job.type === 'thumbnail_regenerate' ? 'Regenerate' : job.type}
{job.type === 'thumbnail_rebuild' ? 'Miniatures' : job.type === 'thumbnail_regenerate' ? 'Regénération' : job.type}
</Badge>
</div>
@@ -281,7 +281,7 @@ export function JobsIndicator() {
{/* Footer */}
<div className="px-4 py-2 border-t border-border/60 bg-muted/50">
<p className="text-xs text-muted-foreground text-center">Auto-refreshing every 2s</p>
<p className="text-xs text-muted-foreground text-center">Actualisation automatique toutes les 2s</p>
</div>
</div>
</>
@@ -304,7 +304,7 @@ export function JobsIndicator() {
${isOpen ? 'ring-2 ring-ring ring-offset-2 ring-offset-background' : ''}
`}
onClick={() => setIsOpen(!isOpen)}
title={`${totalCount} active job${totalCount !== 1 ? 's' : ''}`}
title={`${totalCount} tâche${totalCount !== 1 ? 's' : ''} active${totalCount !== 1 ? 's' : ''}`}
>
{/* Animated spinner for running jobs */}
{runningJobs.length > 0 && (