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

@@ -9,6 +9,7 @@ export type LibraryDto = {
next_scan_at: string | null;
watcher_enabled: boolean;
metadata_provider: string | null;
fallback_metadata_provider: string | null;
};
export type IndexJobDto = {
@@ -120,6 +121,8 @@ export type SeriesDto = {
books_read_count: number;
first_book_id: string;
library_id: string;
series_status: string | null;
missing_count: number | null;
};
export function config() {
@@ -296,10 +299,14 @@ export async function fetchSeries(
libraryId: string,
page: number = 1,
limit: number = 50,
seriesStatus?: string,
hasMissing?: boolean,
): Promise<SeriesPageDto> {
const params = new URLSearchParams();
params.set("page", page.toString());
params.set("limit", limit.toString());
if (seriesStatus) params.set("series_status", seriesStatus);
if (hasMissing) params.set("has_missing", "true");
return apiFetch<SeriesPageDto>(
`/libraries/${libraryId}/series?${params.toString()}`,
@@ -313,18 +320,26 @@ export async function fetchAllSeries(
page: number = 1,
limit: number = 50,
sort?: string,
seriesStatus?: string,
hasMissing?: boolean,
): Promise<SeriesPageDto> {
const params = new URLSearchParams();
if (libraryId) params.set("library_id", libraryId);
if (q) params.set("q", q);
if (readingStatus) params.set("reading_status", readingStatus);
if (sort) params.set("sort", sort);
if (seriesStatus) params.set("series_status", seriesStatus);
if (hasMissing) params.set("has_missing", "true");
params.set("page", page.toString());
params.set("limit", limit.toString());
return apiFetch<SeriesPageDto>(`/series?${params.toString()}`);
}
export async function fetchSeriesStatuses(): Promise<string[]> {
return apiFetch<string[]>("/series/statuses");
}
export async function searchBooks(
query: string,
libraryId?: string,
@@ -726,9 +741,55 @@ export async function deleteMetadataLink(id: string) {
});
}
export async function updateLibraryMetadataProvider(libraryId: string, provider: string | null) {
export async function updateLibraryMetadataProvider(libraryId: string, provider: string | null, fallbackProvider?: string | null) {
return apiFetch<LibraryDto>(`/libraries/${libraryId}/metadata-provider`, {
method: "PATCH",
body: JSON.stringify({ metadata_provider: provider }),
body: JSON.stringify({ metadata_provider: provider, fallback_metadata_provider: fallbackProvider }),
});
}
// ---------------------------------------------------------------------------
// Batch Metadata
// ---------------------------------------------------------------------------
export type MetadataBatchReportDto = {
job_id: string;
status: string;
total_series: number;
processed: number;
auto_matched: number;
no_results: number;
too_many_results: number;
low_confidence: number;
already_linked: number;
errors: number;
};
export type MetadataBatchResultDto = {
id: string;
series_name: string;
status: string;
provider_used: string | null;
fallback_used: boolean;
candidates_count: number;
best_confidence: number | null;
best_candidate_json: Record<string, unknown> | null;
link_id: string | null;
error_message: string | null;
};
export async function startMetadataBatch(libraryId: string) {
return apiFetch<{ id: string; status: string }>("/metadata/batch", {
method: "POST",
body: JSON.stringify({ library_id: libraryId }),
});
}
export async function getMetadataBatchReport(jobId: string) {
return apiFetch<MetadataBatchReportDto>(`/metadata/batch/${jobId}/report`);
}
export async function getMetadataBatchResults(jobId: string, status?: string) {
const params = status ? `?status=${status}` : "";
return apiFetch<MetadataBatchResultDto[]>(`/metadata/batch/${jobId}/results${params}`);
}