DOCS/v1-mediatheque-plan.md
2026-09-04 16:48:04 +02:00

357 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# V1 — Médiathèque (refonte de l'onglet Ressources)
> **Statut** : **implémenté le 2026-09-02.** Backend + front livrés, `dotnet test` et
> `flutter build web` verts. Reste à valider à l'œil dans le navigateur (checklist §6).
> **Autonome.** Aucun appel à un modèle, aucune clé API nouvelle, aucun coût variable.
> Ne dépend ni du Studio ni de la migration Postgres. **Peut se livrer seul.**
>
> Le Studio (V2) se greffe dessus plus tard sans le réécrire — voir
> [v2/studio-plan.md §3.0](v2/studio-plan.md). Ce document est **exécutable tel quel** :
> il ne suppose aucune connaissance de la conversation de conception.
---
## 0. Pourquoi ce lot existe
Il est né en cherchant où loger les images générées par le futur Studio. La réponse a été
« nulle part de nouveau, l'écran existant suffit » — **à condition de le refondre**, parce qu'à
312 ressources il est illisible.
Les défauts sont ceux d'aujourd'hui, mesurés dans le code. Le lot les corrige, plus deux bugs.
| Défaut | Où |
|---|---|
| Tuiles carrées de 160 px, affichées d'un bloc sans ordre ni groupement | `resource_body_grid.dart:78``SliverGridDelegateWithMaxCrossAxisExtent(maxCrossAxisExtent: 160, childAspectRatio: 1.0)` |
| Chips de type en `Wrap`**un seul axe de filtre** | `resource_body_grid.dart:133` |
| Ni tri, ni groupement, ni sélection multiple, ni action en lot | absent |
| Popup 520 px, `borderRadius: 20`, aperçu encadré d'une bordure grise de **3 px à `borderRadius: 30`** — hors de l'échelle de l'app (5/8/10) | `show_resource_popup.dart:20, 50-55` |
| Champ Label **au-dessus** de l'aperçu : on nomme avant d'avoir vu | `show_resource_popup.dart:36` |
| Quatre `RoundedButton` de même poids dans un `Wrap` — destructif collé au primaire, ça se stacke | `show_resource_popup.dart:67-110` |
| Aucune métadonnée affichée, alors que `SizeBytes`, `FileName`, `DateCreation`, `Type` sont en base | — |
| Impossible de savoir **où** une ressource est utilisée, ni si elle l'est | — |
| 🐛 **Le téléchargement force `.json`** quel que soit le type : un PNG se télécharge en `label.json` | `show_resource_popup.dart:91` |
| 🐛 **`Resource.FileName` n'est pas dans `ToDTO()`** — le front n'a pas de quoi nommer le fichier | `Data/Resource.cs` |
---
## 1. Périmètre
### Dans le lot
1. Renommage **Ressources → Médiathèque** (libellé de menu et titre d'écran).
2. **Rail de facettes cumulables** avec compteurs : Type · Usage · Origine · Configuration.
3. **Tri** (date, nom, poids, usages) et **groupement par mois**, bascule grille ↔ liste.
4. **Sélection multiple** et actions en lot (télécharger, supprimer).
5. **Compteur d'usages sur la vignette.**
6. Le détail passe de la modale au **panneau latéral** : aperçu d'abord, métadonnées réelles,
liste « Utilisée dans » cliquable, boutons hiérarchisés, **Supprimer désactivé tant que la
ressource sert**.
7. Backend : **index inverse des usages** (2 endpoints) + enrichissement de `ResourceDTO`.
8. Les deux bugs ci-dessus.
### Hors du lot — explicitement
- Toute génération IA (onglet « Générer », crédits, provenance) → **V2**.
- Le filtre « Origine Générées par IA » : la facette existe dans le rail, mais **inutile en V1**
puisque rien n'est généré. **La coder quand même** (elle lit `AiProvenance`, absent en V1 → 0
résultat) ou la masquer tant que `studioEnabled == false`. **Recommandé : la masquer**, pour ne
pas afficher une facette vide.
- Upload serveur (`IResourceBlobService.UploadAsync`) → V2, lot 0. L'upload reste navigateur → Firebase.
- Thumbnails / variantes / CDN → hors périmètre, aucune des deux versions n'en a besoin.
- `GuidedStep.ImageUrl``ImageResourceId` : **V2**. Conséquence assumée en V1, voir §3.4.
---
## 2. Backend — `manager-service`
### 2.1 Enrichir `ResourceDTO`
`Data/Resource.cs`, méthode `ToDTO()`. Champs à ajouter :
```csharp
fileName = FileName, // existe en base, jamais exposé — corrige le bug du .json
width = Width, // nouvelle colonne, nullable
height = Height, // nouvelle colonne, nullable
usageCount = null, // rempli seulement par les endpoints qui le calculent (§2.2)
```
**Migration** : `Resource.Width` et `Resource.Height`, `int?`. Nullables et **non backfillées**
l'UI n'affiche les dimensions que si elles sont présentes. Elles sont renseignées à la création par
manager-app, qui les connaît déjà (`ImageCompressor` décode l'image avant de la compresser).
⚠️ `FileName` peut être nul sur les ressources anciennes. Le front retombe alors sur
`label` + l'extension déduite du `Type`.
### 2.2 Index inverse des usages
**Le mécanisme existe déjà** : `GetReferencedResourceIds(string language = null)` est implémentée
sur les **13 sous-types de `Section`**, plus `GuidedPath` et `GuidedStep`. C'est elle qui fait
marcher l'export offline (`ConfigurationController.Export`). L'index inverse consiste à la
parcourir dans l'autre sens.
```
GET /api/Resource/{id}/usages
→ [ { kind, id, label, configurationId, configurationLabel, sectionId, field, path } ]
GET /api/Resource/usage-map?instanceId={id}
→ { usages: { "<resourceId>": { count, configurationIds: [] }, ... },
configurations: { "<configurationId>": "<label>", ... } }
```
`sectionId` (ajouté à l'implémentation) porte la section à ouvrir quand on clique une ligne
« Utilisée dans » — nul pour `kind = Configuration`.
La forme de `usage-map` s'est enrichie par rapport à la spécification (un simple compteur) :
la facette **Configuration** du rail a besoin de savoir **dans quelles configurations** une
ressource sert, et de leurs libellés. Le tout en **une** requête, comme prévu.
`kind``Section` | `GuidedPath` | `GuidedStep` | `Configuration`.
`path` est la chaîne lisible affichée au front : `"Escape game Parcours image"`.
Implémentation recommandée : un `ResourceUsageService` qui construit la carte complète pour une
instance en une passe, et la met en cache mémoire courte (30 s) — `usage-map` est appelée à chaque
ouverture de l'écran, et l'écran est ouvert souvent.
#### ⚠️ Quatre pièges, à traiter explicitement
1. ~~**Union sur toutes les langues.**~~ **Vérifié dans le code à l'implémentation : faux.**
`SectionText.ResourceIds` et `ResourceIdsFromValues` filtrent par `language == null || …`, et
`GetReferencedResourceIds(string language = null)` est documentée « null rend toutes les langues ».
**Appeler la méthode sans argument suffit** — pas de boucle sur `Configuration.Languages`. Le
danger décrit (une image posée qu'en NL comptée orpheline) est réel si on passe une langue :
c'est ce que fige le test `ImageUsedOnlyInDutch_IsNotOrphan`.
2. **`Configuration.ImageId` et `LoaderImageId` sont hors sections.** Voir
`ConfigurationController.cs:407-414`, qui les ajoute séparément dans l'export. Les inclure, sans
quoi l'image d'accueil d'une configuration s'afficherait « jamais utilisée ».
3. **`GuidedStep.ImageUrl` est une URL absolue, pas un id** — commentaire explicite en
`GuidedStep.cs:74`. Une image posée là **sera comptée orpheline** en V1. C'est une limite connue :
la migration en `ImageResourceId` est en V2. **À écrire dans le tooltip du filtre**, sinon un
utilisateur supprimera une image utilisée par une étape de parcours.
4. **Un `GuidedPath` peut pendre d'un `SectionEvent`** (`GuidedPath.SectionEventId`), et
`SectionEvent.GetReferencedResourceIds` **ne descend pas dans les parcours** — seul
`SectionParcours` le fait. Piège trouvé à l'implémentation, absent de la spécification initiale.
Les `GuidedPath` sont donc parcourus **séparément**, et volontairement **pas chargés** sur
`SectionParcours` (sans quoi chaque usage compterait double). Figé par `GuidedPathOnEvent_IsWalked`.
> **`AsNoTracking` est proscrit** dans ce service : les colonnes `jsonb` passent par un
> convertisseur sans `ValueComparer`, et le provider EF InMemory des tests rend alors des
> collections **vides** sur une requête détachée — les tests d'usage seraient verts sans rien couvrir.
### 2.3 Suppression protégée
`DELETE /api/Resource/{id}` : renvoyer **409** avec la liste des usages si `usageCount > 0`.
Aujourd'hui rien ne protège. Le front désactive déjà le bouton, mais la règle doit vivre côté serveur
— l'appel peut venir d'ailleurs.
Ajouter `DELETE /api/Resource/bulk` (corps : `{ ids: [] }`) pour l'action en lot, avec la même règle
par élément et un rapport partiel : `{ deleted: [], refused: [{id, usageCount}] }`.
---
## 3. Front — `manager-app`
### 3.1 Renommage
| Où | Aujourd'hui | Après |
|---|---|---|
| `l10n/app_fr.arb:57` | `"menuResources": "Ressources"` | `"Médiathèque"` |
| `app_en.arb`, `app_nl.arb` | idem | `"Media library"` / `"Mediatheek"` |
| `main_screen.dart:302` | `case 'resources': return l.menuResources;` | inchangé (la clé suffit) |
⚠️ La route reste `/main/resources`**ne pas la renommer**, les liens et les habitudes existent.
Le libellé change, pas l'identifiant.
### 3.2 Fichiers touchés
| Fichier | Action |
|---|---|
| `Screens/Resources/resources_screen.dart` (469 l. au total du dossier) | Garde son rôle de conteneur et son `FutureBuilder`. Charge en plus la `usage-map`. **Ne pas casser sa double vie** : il est aussi embarqué par `showSelectResourceModal` (§3.3) |
| `Screens/Resources/resource_body_grid.dart` | Le gros du travail : rail de facettes, tri, groupement, sélection, nouvelles vignettes |
| `Screens/Resources/show_resource_popup.dart` | **Supprimé.** À l'implémentation, `grep` a montré que `showResource()` n'était appelée que par `resources_screen.dart` : garder un export mort pour « ne pas casser les appelants » n'avait pas d'objet. Remplacé par `resource_detail_panel.dart` |
| `Screens/Resources/resource_detail_panel.dart` | **Nouveau** — le panneau latéral (aperçu, nom, métadonnées, « Utilisée dans », pied hiérarchisé) |
| `Screens/Resources/resource_formatting.dart` | **Nouveau** — extension de téléchargement, poids, mois, dates |
| `Screens/Resources/resource_download.dart` | **Nouveau**`downloadResource()`, la correction du `.json` en dur |
| `Helpers/ImageCompressor.dart` | `CompressedImage` expose `width`/`height` : c'est le seul endroit qui décode l'image avant l'upload |
| `Screens/Resources/select_resource_modal.dart` | **Contenu** inchangé ; **cadre** repris par la refonte Configuration/Sections — voir §3.3 |
| `constants.dart` | Aucun token nouveau à créer : `kSurface*`, `kInk*`, `kLine*`, `kSpace*`, `kRadius*` couvrent tout |
### 3.3 ⚠️ Le piège central : un écran, deux vies
```dart
// select_resource_modal.dart:24
child: ResourcesScreen(
isAddButton: isAddButton,
isSelect: isSelect,
isRemoveButton: isRemoveButton,
onGetResult: (ResourceDTO? resource) { ... },
resourceTypes: resourceTypes,
),
```
**`showSelectResourceModal` embarque `ResourcesScreen`.** L'onglet Médiathèque et le sélecteur de
champ de n'importe quel éditeur sont **le même widget**. Toute régression touche donc les 13 types de
section, les POI et les étapes d'un coup.
Règles à respecter :
- En mode `isSelect: true`, un clic sur une vignette **sélectionne** (comportement actuel), il
n'ouvre pas le panneau de détail.
- Le rail de facettes doit être **réductible ou masqué** dans la modale : elle fait déjà
`size.width * 0.85`, et un rail de 194 px y est acceptable — mais le vérifier à 1280 px de large.
- ⚠️ **Cette règle dépend de la largeur `size.width * 0.85`.** La refonte des écrans
Configuration/Sections reprend le *cadre* de cette modale (voir plus bas) et **conserve
délibérément cette formule de largeur** pour ne pas invalider la règle ci-dessus. Si quelqu'un
veut un jour rétrécir la modale, c'est cette ligne-là qu'il faut rouvrir d'abord.
- Le mode sélection multiple est **désactivé** quand `isSelect: true`.
- `resourceTypes` filtre déjà en amont : la facette Type doit se limiter aux types reçus, comme
aujourd'hui (`resource_body_grid.dart:42`).
**Test de non-régression obligatoire** : ouvrir un champ image d'une section, choisir une ressource,
enregistrer. C'est le chemin le plus emprunté de toute l'app.
#### Le cadre de la modale — repris par un autre lot
Le *contenu* de `showSelectResourceModal` appartient à ce lot et n'est pas retouché ailleurs. Son
*cadre*, en revanche, est repris par la refonte des écrans Configuration/Sections — maquette :
`DOCS/claude design/refonte-configuration-sections.html`, section 06.
Motif : ce n'est pas qu'une question de style. Un `AlertDialog` dispose de `hauteur 48` (marge
verticale de 24 px), le contenu en demande `0,85 × hauteur`, plus un titre et une ligne d'actions de
70 px — d'où le `SingleChildScrollView` qui l'entoure, et un **défilement dans un défilement** : on
fait glisser le dialogue au lieu de la grille.
Ce que l'autre lot change, et rien de plus :
- coquille standard (`Dialog` + en-tête + pied) à la place de `AlertDialog` et de
`title: Center(Text(...))`, `kRadiusShell` au lieu du rayon 20 ;
- hauteur figée → corps souple sous un `maxHeight`, ce qui supprime le double défilement ;
- le bouton « Annuler » de 180 × 70 px descend dans le pied de dialogue ;
- `showValues` (lignes 64-78) supprimé : c'est du code mort. La copie qu'en garde
`multi_input_modal.dart` ligne 186 l'est aussi — son unique appel, ligne 53, est en commentaire.
Les deux peuvent partir.
**La largeur `size.width * 0.85` n'est pas touchée**, exprès — voir l'avertissement des règles
ci-dessus. Aucune ligne de `ResourcesScreen` n'est modifiée.
### 3.4 Le rail de facettes
```
TYPE Tout · Images · Audio · Vidéo · PDF (compteurs)
USAGE Utilisées · Jamais utilisées
ORIGINE (masqué en V1 — voir §1)
CONFIGURATION une ligne par configuration + « Aucune »
```
- **Cumulables entre groupes** (ET), une seule valeur par groupe. Recliquer la valeur active
l'efface ; « Tout » efface l'axe Type.
- Les compteurs portent sur le corpus complet de l'instance, pas sur le résultat filtré.
- Une barre au-dessus de la grille récapitule les filtres actifs + « tout effacer ».
- **Tooltip sur « Jamais utilisées »** : mentionner la limite `GuidedStep.ImageUrl` (§2.2 piège 3).
### 3.5 La grille
- `SliverGridDelegateWithMaxCrossAxisExtent(maxCrossAxisExtent: 150)` mais **ratio 4/3 avec un pied
de carte** : nom, poids, origine, et le compteur d'usages en pastille sur la vignette
(`2 usages` / `libre`).
- Groupement par mois par défaut (en-tête `Septembre 2026`), replié quand le groupe est vide après filtrage.
- Tri : plus récentes (défaut), nom A→Z, poids décroissant, usages croissants.
- État vide explicite : « Aucune ressource ne correspond à ces filtres ».
### 3.6 Le panneau de détail
Remplace la modale. Ordre imposé, de haut en bas :
1. **Aperçu** — d'abord. Pas de bordure grise de 3 px ; `kRadiusCard` et `kLineSoft`.
2. **Nom** — champ éditable, *après* l'aperçu.
3. **Métadonnées** : type, dimensions (si présentes), poids, date, auteur.
4. **Utilisée dans** — liste cliquable, chaque ligne navigue vers le contenu. Si vide : un encart
ambre « Jamais utilisée. Aucun visiteur ne la verra ».
5. **Pied d'actions**, hiérarchisé : `Enregistrer` (primaire, à gauche) · `Remplacer le fichier` ·
`Télécharger` · `Supprimer` (destructif, isolé à droite, **désactivé si `usageCount > 0`**).
Le panneau reste ouvert quand on clique une autre vignette — c'est le geste réel quand on trie une
médiathèque.
### 3.7 Correction du téléchargement
```dart
// show_resource_popup.dart:91 — aujourd'hui
anchorElement.download = '${resourceDTO.label}.json';
```
Devient : `resource.fileName` s'il existe, sinon `label` + l'extension déduite de `Type`
(`Image → .jpg`, `Audio → .mp3`, `Video → .mp4`, `PDF → .pdf`, `JSON → .json`, `Word → .docx`,
`PowerPoint → .pptx`, `Text → .txt`).
---
## 4. Ce que la V1 laisse volontairement en place
- L'upload passe toujours par le navigateur → Firebase (`resources_screen.dart:247-256`).
- Pas de `AiProvenance`, pas de badge « généré par IA », pas de crédits.
- `ResourceType` inchangé — **ne rien ajouter à l'enum**, il est persisté en int.
- La jauge de stockage du menu garde son bug de rafraîchissement (relevé le 2026-08-25,
`quota_bars_widget.dart:23`). **Hors périmètre, mais tentant** : le corriger ici coûte peu et
l'écran touche justement au stockage. À arbitrer au démarrage du lot.
---
## 5. Ordre d'exécution suggéré
> **Suivi à l'implémentation (2026-09-02)** — les 8 étapes sont codées. Ce qui reste est
> de la vérification à l'œil : la checklist §6 n'a pas été passée dans un navigateur.
> Non fait, délibérément : la jauge de stockage du menu (§4), laissée hors périmètre.
| # | Étape | Vérifiable par |
|---|---|---|
| 1 | Migration `Width`/`Height` + `fileName` dans `ToDTO()` + correction du `.json` | `dotnet test`, puis télécharger un PNG et vérifier son extension |
| 2 | `ResourceUsageService` + les 2 endpoints, avec les 3 pièges traités | Un test qui pose une image en NL seulement et vérifie qu'elle n'est **pas** orpheline |
| 3 | `409` sur `DELETE` si utilisée, + `DELETE /bulk` | Test d'intégration |
| 4 | Renommage + rail de facettes + compteurs | `flutter build web`, puis l'écran |
| 5 | Tri, groupement, état vide | l'écran |
| 6 | Panneau de détail + « Utilisée dans » + suppression protégée | l'écran |
| 7 | Sélection multiple + actions en lot | l'écran |
| 8 | **Non-régression du sélecteur de ressource** | Ouvrir un champ image d'une section, choisir, enregistrer |
⚠️ `flutter analyze` ne suffit pas — seul `flutter build web` dit la vérité (leçon consignée dans
[STATUS.md §1bis](STATUS.md)).
---
## 6. Checklist de test
> À passer dans un navigateur. Aucune de ces cases n'est cochée par le code seul —
> `flutter build web` dit que ça compile, pas que ça marche.
- [ ] Le menu affiche « Médiathèque », la route reste `/main/resources`
- [ ] Deux facettes de groupes différents se cumulent (ex. Audio + Jamais utilisées)
- [ ] Recliquer une facette active l'efface ; « Tout » efface l'axe Type
- [ ] Les compteurs du rail ne bougent pas quand on filtre
- [ ] Un groupe de mois vide après filtrage disparaît
- [ ] Le compteur d'usages sur la vignette correspond au « Utilisée dans » du panneau
- [ ] Une image utilisée **uniquement en NL** n'apparaît pas dans « Jamais utilisées »
- [ ] L'image d'accueil d'une configuration n'apparaît pas dans « Jamais utilisées »
- [ ] Une image posée sur `GuidedStep.ImageUrl` apparaît orpheline **et le tooltip l'explique**
- [ ] `Supprimer` est désactivé sur une ressource utilisée, et le serveur renvoie 409 si on force
- [ ] Suppression en lot : les utilisées sont refusées, les autres supprimées, le rapport est lisible
- [ ] Un PNG se télécharge en `.png`, un MP3 en `.mp3`
- [ ] Une ressource ancienne sans `FileName` se télécharge quand même avec la bonne extension
- [ ] **Le sélecteur de ressource d'un champ de section fonctionne toujours** — sélection, annulation, ajout
- [ ] `flutter build web` passe
---
## 7. Ce que la V2 viendra greffer
Pour que l'implémentation V1 ne ferme aucune porte :
| V2 ajoutera | Point d'ancrage à prévoir en V1 |
|---|---|
| Onglet « Générer » dans le sélecteur | `ResourceTab` / `showNewResource` gardent une structure à onglets |
| Facette « Générées par IA » | Le rail est déjà générique : une facette de plus, pas une refonte |
| Badge et bloc provenance | Le pied de vignette et le panneau ont la place ; lire `AiProvenance` nullable |
| Console audio (narrateurs) | Vit dans `Configuration Personnages`, **pas ici** |
Rien de tout ça ne demande de revenir sur la V1.