Files
stripstream-librarian/openspec/changes/reading-progress/design.md
Froidefond Julien 648d86970f feat: suivi de la progression de lecture par livre
- API : nouvelle table book_reading_progress (migration 0016) et module
  reading_progress.rs avec GET/PATCH /books/:id/progress (token read)
- API : GET /books/:id enrichi avec reading_status, reading_current_page,
  reading_last_read_at via LEFT JOIN
- Backoffice : badge de statut (Non lu / En cours · p.N / Lu) sur la page
  de détail et overlay sur les BookCards
- OpenSpec : change reading-progress avec proposal/design/specs/tasks

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 21:53:52 +01:00

3.6 KiB

Context

L'API est une application axum single-user protégée par tokens (admin / read). Les données sont dans PostgreSQL, gérées via sqlx. Les routes existantes sont groupées en admin_routes et read_routes dans main.rs. La table books stocke uniquement les métadonnées de contenu (titre, auteur, pages, etc.) sans aucune notion d'état de lecture.

Le GET /books/:id utilise déjà un LEFT JOIN LATERAL pour les book_files — le même pattern s'applique naturellement pour la progression de lecture.

Goals / Non-Goals

Goals:

  • Stocker et exposer l'état de lecture (unread / reading / read) par livre
  • Mémoriser la page courante pour reprendre la lecture
  • Horodater la dernière activité de lecture
  • Enrichir GET /books/:id sans breaking change
  • Documenter tous les endpoints dans Swagger via utoipa

Non-Goals:

  • Support multi-utilisateur (pas de concept d'utilisateur dans l'app)
  • Historique des sessions de lecture
  • Synchronisation entre clients
  • Progression sur GET /books (liste paginée — impact perf non justifié)

Decisions

D1 — Table séparée book_reading_progress (vs colonnes sur books)

book_reading_progress avec book_id comme PRIMARY KEY (relation 1-to-1).

Rationale : sépare les métadonnées de contenu (immuables, issues de l'indexer) des données de lecture (mutables, issues de l'utilisateur). Facilite les requêtes ciblées et isole les permissions futures si multi-user.

Alternative rejetée : colonnes directes sur books — plus simple mais mélange deux responsabilités différentes dans la même table.

D2 — Upsert sur PATCH (INSERT ... ON CONFLICT DO UPDATE)

Une seule ligne par livre, créée à la première mise à jour. Pas de created_at dans la table.

Rationale : évite un GET + INSERT/UPDATE en deux temps. La sémantique "créer si absent" est transparente pour le client.

D3 — Token read autorisé à écrire la progression

Le PATCH est ajouté dans une section de main.rs protégée par require_read (pas require_admin).

Rationale : le cas d'usage principal est une app de lecture tournant avec un token read-only. Exiger un token admin serait inutilement contraignant pour une opération purement personnelle.

D4 — Réponse par défaut si progression inexistante

GET /books/:id/progress retourne { "status": "unread", "current_page": null, "last_read_at": null } si aucune ligne n'existe, plutôt qu'un 404.

Rationale : simplifie les clients — pas besoin de gérer un 404 comme état "non lu". L'absence de progression EST l'état "unread".

D5 — current_page : valeur positive sans validation contre page_count

Accepter toute valeur > 0 sans vérifier que la page existe dans le livre.

Rationale : page_count peut être NULL (phase d'analyse pas encore terminée). La validation côté client est plus appropriée.

Risks / Trade-offs

  • Pas de validation de page → un client peut enregistrer current_page = 9999 sur un livre de 10 pages. Risque faible dans un contexte single-user personnel.
  • Token read en écriture → légère déviation du principe least-privilege. Acceptable car la progression n'affecte pas les autres données.
  • LEFT JOIN dans GET /books/:id → requête légèrement plus lourde. Impact négligeable (1 row lookup sur clé primaire).

Migration Plan

  1. Déployer la migration 0016_add_reading_progress.sql (non-destructive, nouvelle table)
  2. Déployer le binaire API avec les nouveaux endpoints
  3. Rollback : supprimer la table et redéployer l'ancienne version (aucune donnée critique perdue)