Ajouter les sagas et cycles, avec leurs tomes manquants
Première relation entre livres du projet. Une série contient des séries (un cycle EST une série de séries) et des PLACES dans l'ordre de lecture — pas des livres : une place sans livre est le trou qu'on vient voir. L'ordre est stocké, jamais déduit d'une année : une préquelle se lit avant le livre paru dix ans plus tôt. Les tomes non possédés se saisissent à la main, aucune source ne donnant l'ordre de lecture d'une saga. Le titre de la place survit à la suppression du livre (SetNull, pas de cascade), sans quoi perdre un exemplaire effacerait le tome 3 de la série. Les séries sont communes au foyer. Seule exception : la mise en envies d'un tome manquant, qui écrit dans une liste personnelle — c'est le seul point où les deux portées se rencontrent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -717,6 +717,23 @@ RapprochementRefuse (mémoire des « non » de l'utilisateur)
|
||||
├── AuteurAId ─┐ UNIQUE, couple toujours rangé (petit Id d'abord)
|
||||
└── AuteurBId ─┘ pour qu'un refus vaille dans les deux sens
|
||||
|
||||
Serie (sagas et cycles — COMMUNS au foyer, comme le catalogue)
|
||||
├── Id
|
||||
├── Titre
|
||||
├── TitreNormalise (index UNIQUE — une série, une fiche)
|
||||
├── SerieParenteId : int? (auto-référence : un cycle EST une série de séries)
|
||||
├── Position (rang dans la série parente)
|
||||
├── DateAjout
|
||||
└── AjoutePar (trace, PAS une frontière — comme Livre.AjoutePar)
|
||||
|
||||
ElementSerie (une PLACE dans l'ordre de lecture, occupée ou non)
|
||||
├── Id
|
||||
├── SerieId (FK Serie, cascade)
|
||||
├── Position (ordre de LECTURE, stocké — jamais déduit d'une année)
|
||||
├── LivreId : int? (FK Livre, ⚠ SetNull — NULL = un tome qu'on ne possède pas)
|
||||
└── Titre (toujours renseigné, pour survivre à la suppression du livre)
|
||||
UNIQUE (SerieId, LivreId) WHERE LivreId IS NOT NULL
|
||||
|
||||
Pret
|
||||
├── Id
|
||||
├── LivreId (FK Livre, cascade — index simple pour l'historique)
|
||||
@@ -1273,6 +1290,84 @@ l'œil.
|
||||
piste, avec sa limite connue — les œuvres remontent en langue originale et ne se rapprochent pas
|
||||
du catalogue par `CleOeuvre`.
|
||||
|
||||
## Sagas et cycles — migration `SagasEtCycles` (2026-08-19)
|
||||
|
||||
Cas donné : *La Légende de Drizzt*. C'est la première notion du projet qui porte sur une
|
||||
**relation entre livres** plutôt que sur un livre, et un champ texte « série » sur `Livre` n'en
|
||||
couvrirait aucun aspect.
|
||||
|
||||
| Ce que le cas impose | Ce qui le porte |
|
||||
|---|---|
|
||||
| Un cycle contient des **séries**, qui contiennent des tomes | `Serie.SerieParenteId`, auto-référence : un cycle **est** une série de séries |
|
||||
| L'**ordre de lecture** n'est pas celui de publication (*L'Elfe noir* est une préquelle) | `ElementSerie.Position`, **stockée**, jamais déduite d'une année |
|
||||
| Un livre appartient à **plusieurs** regroupements | aucune unicité sur `LivreId` seul |
|
||||
| Montrer les **trous** — « il vous manque le tome 3 » | `ElementSerie.LivreId` **nullable** |
|
||||
|
||||
### La table ne contient pas des livres, mais des places
|
||||
|
||||
⚠️ C'est la clé de tout l'écran : `ElementSerie` est une **position dans l'ordre de lecture**,
|
||||
pas « un livre de la série ». Une place sans livre n'est pas une anomalie, c'est le trou qu'on
|
||||
vient voir.
|
||||
|
||||
**Conséquence, décidée avec l'utilisateur le 2026-08-19** : les tomes non possédés **existent en
|
||||
base** et se saisissent à la main. Aucune source ne donne l'ordre de lecture d'une saga — la BnF
|
||||
ne l'expose pas, et l'ordre de publication est justement faux pour une préquelle. Sans saisie,
|
||||
l'écran ne pourrait qu'ordonner ce qu'on a déjà, donc ne rien dire de ce qui manque.
|
||||
|
||||
⚠️ **`Titre` est renseigné même quand le livre est là**, et la clé étrangère est en **`SetNull`**,
|
||||
surtout pas en cascade : supprimer un livre du catalogue laisse un **trou nommé** à sa place.
|
||||
Une cascade effacerait le tome 3 de la saga parce qu'on a perdu son exemplaire. Un test le
|
||||
verrouille.
|
||||
|
||||
⚠️ L'unicité `(SerieId, LivreId)` est un **index partiel**, comme celui des prêts en cours : les
|
||||
places vides doivent pouvoir se répéter autant que nécessaire, et deux `NULL` passeraient de
|
||||
toute façon. Un livre ne tient donc qu'une place **dans une série donnée**, mais vit dans
|
||||
plusieurs séries.
|
||||
|
||||
### Portée : commune, sauf en un seul point
|
||||
|
||||
Les séries sont **communes au foyer**, comme le catalogue et les prêts : l'ordre de lecture est
|
||||
une propriété de l'œuvre, il ne change pas selon qui regarde. Aucune méthode de `ServiceSeries`
|
||||
ne prend d'utilisateur — **sauf `MettreEnEnviesAsync`**, et c'est là que les deux portées se
|
||||
rencontrent :
|
||||
|
||||
> un tome manquant est **commun** ; l'envie qu'il devient est **personnelle**.
|
||||
|
||||
Deux membres du foyer peuvent donc vouloir le même tome absent, chacun dans sa liste. ⚠️ Aucun
|
||||
auteur n'est transmis à l'envie créée : une place dans une saga n'en porte pas, et en inventer un
|
||||
fausserait le rapprochement avec le catalogue. Le revers assumé : cette envie **ne sera pas**
|
||||
marquée « déjà au catalogue » à l'achat du livre, la règle exigeant un auteur commun.
|
||||
|
||||
### Deux garde-fous qui n'ont l'air de rien
|
||||
|
||||
- **Supprimer un cycle ne détruit pas les séries qu'il contient** (`SetNull` sur la parenté) :
|
||||
ce sont de vraies séries de vrais livres, elles remontent simplement à la racine.
|
||||
- **Une série ne peut pas devenir sa propre ancêtre.** Sans ce contrôle, l'arbre se refermerait
|
||||
sur lui-même et l'affichage boucherait à l'infini. La remontée de parenté est **bornée par le
|
||||
nombre de séries**, pour s'arrêter même si une boucle existait déjà en base.
|
||||
|
||||
### Une seule lecture, un seul instantané
|
||||
|
||||
`GET /api/series` rend **tout l'arbre à plat**, le client le rebâtit. Le détail d'une série s'y
|
||||
lit sans second appel : un instantané hors-ligne (`series`) suffit donc à servir la liste, chaque
|
||||
détail, **et** le « fait partie de » de la fiche livre — au lieu d'un instantané par série
|
||||
consultée. C'est aussi ce qui rend consultable hors-ligne une série qu'on n'avait pas ouverte
|
||||
avant la coupure, exactement comme le catalogue complet.
|
||||
|
||||
⚠️ Le rafraîchissement en tâche de fond (`RafraichirEnFondAsync`) **doit** inclure les séries,
|
||||
sans quoi une coupure juste après un ajout de tome rendrait la série d'avant.
|
||||
|
||||
### L'écran, et ce qu'il refuse de faire
|
||||
|
||||
- **Un cinquième onglet.** Vérifié à 320 px : les cinq libellés tiennent sans troncature
|
||||
(258 px sur 320), au prix d'un `font-size` passé de 0.85 à 0.8 rem.
|
||||
- **Des flèches, pas de glisser-déposer** : le drag & drop HTML5 ne fonctionne pas au doigt, et
|
||||
c'est sur téléphone qu'on consulte une saga. Même décision que pour la liste d'envies.
|
||||
- **Le titre affiché est celui du livre quand il est rattaché**, le titre saisi restant le filet.
|
||||
Rattacher le mauvais livre se voit donc immédiatement — et « Détacher » restitue le nom du tome.
|
||||
- **Aucune détection automatique** de série depuis un titre (« Tome 3 »), aucune source tierce.
|
||||
Ce qui est affiché a été saisi ; rien n'est deviné.
|
||||
|
||||
## Interface — décisions actées le 2026-08-18
|
||||
|
||||
Retours d'usage d'`IDEES.md`, appliqués et donc retirés de ce fichier-là.
|
||||
|
||||
Reference in New Issue
Block a user