feat(api,web): modélisation série + volume et parcours de série

- Table series + colonnes seriesId/volumeNumber/volumeLabel sur les livres,
  extraction souple des numéros (T03, 003, etc.) via extract-series-volume
- Contrôleur et page de série, badge volume sur les cartes de livre,
  styles associés (fond/clarté série, ajustements globaux)
- Statuts de scan/enrichissement et provenance des métadonnées en base

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Git Agent
2026-08-24 09:11:10 +02:00
parent 34159e5e9b
commit 93bb40a8cd
24 changed files with 1481 additions and 76 deletions

View File

@ -3,6 +3,25 @@ import type { ContinueItem } from "./types";
const now = new Date().toISOString();
type BookPipelineStatus = "idle" | "running" | "succeeded" | "failed";
type BookMetadataStatus = "enriched" | "partial" | "none";
type MockBookDto = Omit<BookDto, "metadataStatus" | "metadataProvenance" | "scanStatus" | "enrichmentStatus"> & {
metadataStatus?: BookMetadataStatus;
metadataProvenance?: Record<string, string>;
scanStatus?: BookPipelineStatus;
enrichmentStatus?: BookPipelineStatus;
};
function mockBook(book: MockBookDto): BookDto {
return {
metadataStatus: "partial",
metadataProvenance: { local: "fixture" },
scanStatus: "idle",
enrichmentStatus: "idle",
...book
} as BookDto;
}
export const mockUser: UserDto = {
id: 1,
email: "admin@readabook.local",
@ -17,7 +36,7 @@ export const mockLibraries: LibraryDto[] = [
];
export const mockBooks: BookDto[] = [
{
mockBook({
id: 1,
libraryId: 1,
title: "L'Herbier des machines",
@ -35,8 +54,8 @@ export const mockBooks: BookDto[] = [
fileMtime: now,
createdAt: now,
updatedAt: now
},
{
}),
mockBook({
id: 2,
libraryId: 2,
title: "Cartographie des songes",
@ -54,8 +73,8 @@ export const mockBooks: BookDto[] = [
fileMtime: now,
createdAt: now,
updatedAt: now
},
{
}),
mockBook({
id: 3,
libraryId: 2,
title: "Les vitrines de verre",
@ -73,8 +92,8 @@ export const mockBooks: BookDto[] = [
fileMtime: now,
createdAt: now,
updatedAt: now
},
{
}),
mockBook({
id: 4,
libraryId: 2,
title: "Cabinet noir",
@ -92,7 +111,7 @@ export const mockBooks: BookDto[] = [
fileMtime: now,
createdAt: now,
updatedAt: now
}
})
];
export const mockProgress: ProgressDto[] = [

View File

@ -36,3 +36,17 @@ export type ReaderPreferencesDto = {
mode: ReaderMode;
fit?: ReaderFit;
};
export function hasActiveCoverWork(jobs: JobDto[]) {
return jobs.some((job) => {
if (job.status !== "queued" && job.status !== "running") return false;
const type = job.type.toLowerCase();
return type.includes("scan") || type.includes("enrich") || type.includes("metadata") || type.includes("cover");
});
}
export function isBookCoverUpdating(book: BookDto, fallbackActive = false) {
const statuses = book as BookDto & { scanStatus?: string; enrichmentStatus?: string };
if (statuses.scanStatus === "running" || statuses.enrichmentStatus === "running") return true;
return statuses.scanStatus === undefined && statuses.enrichmentStatus === undefined && fallbackActive;
}