feat(books): marquage des livres comme lus — statut lu/non lu par utilisateur sur progress (colonnes read_status/completed_at/read_status_source, migration idempotente avec backfill à 95 %), endpoint PUT /progress/:bookId/status, complétion automatique au fil de la progression, état lu exposé sur catalogue/recherche/fiche/séries, étagère « continuer » limitée aux lectures en cours, badge Lu sur cartes/fiche/accueil et bouton Marquer comme lu/non lu sur la fiche, avec tests api/shared/web — évolution #52

This commit is contained in:
Git Agent
2026-08-30 10:37:42 +02:00
parent 3faa7ca44b
commit 05f5f1cd16
20 changed files with 664 additions and 43 deletions

View File

@ -95,6 +95,9 @@ export class DatabaseService implements OnModuleDestroy {
book_id INTEGER NOT NULL REFERENCES books(id) ON DELETE CASCADE,
locator TEXT NOT NULL,
percent INTEGER NOT NULL CHECK (percent >= 0 AND percent <= 100),
read_status TEXT NOT NULL DEFAULT 'unread' CHECK (read_status IN ('unread','reading','read')),
completed_at TEXT,
read_status_source TEXT CHECK (read_status_source IN ('progress','manual')),
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
UNIQUE(user_id, book_id)
@ -165,6 +168,7 @@ export class DatabaseService implements OnModuleDestroy {
this.ensureBooksMetadataColumns();
this.ensureSeriesModel();
this.ensureReaderPreferencesTable();
this.ensureProgressReadStatusColumns();
this.ensureMetadataSourceConfigSupportsComicProviders();
this.ensureMetadataSourceConfigColumns();
this.ensureAutomationSettingsColumns();
@ -375,6 +379,35 @@ export class DatabaseService implements OnModuleDestroy {
`);
}
private ensureProgressReadStatusColumns(): void {
const names = this.columnNames("progress");
if (!names.has("read_status")) {
this.sqlite.exec("ALTER TABLE progress ADD COLUMN read_status TEXT NOT NULL DEFAULT 'unread'");
}
if (!names.has("completed_at")) {
this.sqlite.exec("ALTER TABLE progress ADD COLUMN completed_at TEXT");
}
if (!names.has("read_status_source")) {
this.sqlite.exec("ALTER TABLE progress ADD COLUMN read_status_source TEXT");
}
this.sqlite.exec(`
UPDATE progress
SET
read_status = CASE
WHEN percent >= 95 THEN 'read'
WHEN percent > 0 THEN 'reading'
ELSE 'unread'
END,
completed_at = CASE
WHEN percent >= 95 THEN COALESCE(completed_at, updated_at)
ELSE NULL
END,
read_status_source = COALESCE(read_status_source, 'progress')
WHERE read_status_source IS NULL
`);
this.sqlite.exec("CREATE INDEX IF NOT EXISTS progress_user_status_idx ON progress(user_id, read_status)");
}
private ensureMetadataSourceConfigColumns(): void {
const names = this.columnNames("metadata_source_config");
const now = sqlString(this.now());