Lot B, C1 et lot E livrés ; deux erreurs de doc corrigées

Deux points où suivre la doc à la lettre aurait produit un correctif
inopérant ou destructeur, tous deux consignés pour ne pas être rouverts :

- « Supprimer SectionEvent.IconResourceId » (lot B) : ce champ n'existe pas.
  La ligne visée appartient à la classe imbriquée MapAnnotation, partagée par
  trois types de section et lue par cinq contrôleurs.
- « Cas PDF dans getElementForResource » (lot E) : cette fonction retourne
  CachedCustomResource avant son switch, qui est donc du code mort. Le vrai
  dispatcher en a deux.

Également : C1 montre que le « prérequis levé le 10/08 » du backfill n'était
vrai que d'un chemin de création sur deux, et le fournisseur de carte en web
est tranché (Leaflet) avec la correction de ce que l'option impliquait
réellement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Thomas Fransolet 2026-08-11 15:32:19 +02:00
parent 8e82e5b6e7
commit 005f5c1606
3 changed files with 85 additions and 59 deletions

View File

@ -308,6 +308,25 @@ Les lots 1 et 2 sont terminés. **Le lot 3 et le lot médias sont tous les deux
**Chemin critique** : A (assainir) → B (geler le schéma) → G (`MigrationController`) → H (tests) → **J (RGPD)** → I (bascule). C, D, D-bis, E, F se parallélisent.
**Lot C1 — livré le 2026-08-11.** Calculateur `StoragePath`/`SizeBytes` extrait dans `Helpers/ResourceStorage.cs`, avec 13 tests (`dotnet test` **143/143**). Il ferme le lien **L5** : C2 (backfill) et l'écart (e) du lot G appelleront le même code au lieu d'en écrire deux.
- ⚠️ **L'extraction a prouvé la divergence qu'elle devait empêcher.** `ResourceController` a **deux** chemins de création : le chemin **multipart** écrivait `SizeBytes` mais laissait **`StoragePath` nul**, seul le chemin JSON écrivait les deux. Le §1quater annonçait « `StoragePath`/`SizeBytes` écrits à `Create` depuis le 10/08 » — c'était vrai d'un chemin sur deux. Une partie des lignes à rattraper par C2 vient de là.
- **Angle mort laissé ouvert sciemment** : `Update` peut faire passer `Type` d'un type URL à un type fichier, laissant `StoragePath` nul sur une ligne qui a désormais un blob. Recalculer le chemin sur une ressource existante pointerait vers un objet qui n'a pas bougé dans le bucket — à trancher avec C3 (quota).
**Lot B — livré le 2026-08-11. Le schéma est gelé, le lot G est débloqué.** Une seule migration EF (`20260811130738_LotB_FreezeSchema`) : rename `SectionMap.MapResourceId``IconResourceId` (**l'écart (g) du §1quinquies tombe avec**), suppression de `SectionEvent.ParcoursIds`, et `Instance.IsImageWatermark` remplaçant le `instanceId == "633ee379…"` en dur de `ResourceController`. **130/130 tests avant comme après**, build Debug **et** Release verts.
- ⚠️ **Le rename imposait aussi la propriété de navigation** `MapResource``IconResource` : la convention EF l'appariait au FK, la laisser en place aurait fabriqué un FK fantôme. Elle n'était utilisée nulle part ailleurs.
- ✅ **Point de risque levé par vérification** : EF a généré un `RenameColumn`, **pas** un drop+add — les icônes déjà configurées survivent. L'avertissement « may result in the loss of data » ne porte que sur le `DropColumn` de `ParcoursIds`, qui est l'intention du lot.
- ⛔ **« Supprimer `SectionEvent.IconResourceId` » était une erreur de doc — ne pas rouvrir.** `SectionEvent` n'a pas ce champ : la ligne visée appartient à la classe **imbriquée `MapAnnotation`**, partagée par SectionEvent, SectionAgenda et SectionMap, et lue par cinq contrôleurs plus les deux `SelectMany` de `GetReferencedResourceIds`. La supprimer aurait cassé les icônes d'annotation des trois types de section et la collecte offline.
- **Sécurité rapide du lot A, dans la même passe** : le « backdoor `#if DEBUG` » était un bloc de `AuthenticationController.Authenticate` qui **écrasait l'email et le mot de passe reçus** par `test@email.be` — toute compilation Debug authentifiait n'importe quelle saisie. Retiré. `EnableSensitiveDataLogging` passe sous `#if DEBUG` (idiome déjà utilisé dans `Startup.cs` pour CORS et Hangfire ; le Dockerfile publiant en `-c Release`, c'est un verrou réel).
- **Reste du volet sécurité, chiffré et non fait** : **112** retours `new ObjectResult(ex.Message) { StatusCode = 500 }` exposent le message d'exception interne, répartis sur 17 contrôleurs. Les 136 autres `ex.Message` sont des 400/404 volontaires, à garder. `Startup.UseExceptionHandler(HandleError)` existe mais ne traite que `RequestException`. C'est un chantier à part, pas une retouche.
**Lot E — livré le 2026-08-11.** PDF en média d'étape (les trois volets : liste de types paramétrable, `ResourceViewer.tsx`, `CachedCustomResource`), comparaisons `QuestionType` par entier brut, et **W1 tranché : le web reste sur Leaflet** (option a — ni MapBox ni Google Maps JS, donc aucun coût récurrent ajouté avant la prod).
⚠️ **W1 ne pouvait pas se faire comme l'option le disait.** « Retirer le champ pour les instances web » est **impossible** : le fournisseur est porté par la **section**`SectionMap.MapMapProvider` et `SectionAgenda.AgendaMapProvider` — pas par l'instance. Et une même `Configuration` est liée à plusieurs `ApplicationInstance` via `AppConfigurationLink`, donc servie au mobile **comme** au web : masquer le champ aurait supprimé le réglage mobile. Le champ est donc conservé, et manager-app cesse de laisser croire qu'il vaut pour le web (mention sous les deux sélecteurs). Côté `visitapp-web`, zéro ligne : Leaflet inconditionnel était déjà le comportement.
⚠️ **Piège de doc corrigé au passage** : le plan disait « cas PDF dans `getElementForResource` ». Cette fonction de `mymuseum-visitapp` fait un `return CachedCustomResource(...)` **avant** son `switch` — tout son switch est du code mort. Le vrai dispatcher est `CachedCustomResource`, et il en a **deux** (ressource distante / fichier local de l'offline). Suivre la doc à la lettre aurait produit un correctif inopérant et « vérifié » par une analyse verte.
### Contrôle du code du 2026-08-11 (2e passe) — ce qui a bougé depuis la rédaction du plan
> Tout vérifié dans le code, pas dans les docs. Deux points du lot A tombent, un point du lot A est différé, un nouveau suspect apparaît.
@ -478,7 +497,7 @@ Le client ne manipule plus 9 booléens répartis sur 3 niveaux, mais répond à
- [ ] **Basse** — conventions REST hétérogènes, `PasswordUtils` avec RNG non crypto, code mort (MQTT, CORS AllowAll), CORS en dur, ordre middleware
### manager-app — reste à traiter (par priorité)
- [ ] **Critique** Client généré désynchronisé (`manager_api_new/`) : 14 fichiers modèle orphelins → `flutter analyze` inexploitable (183/200 erreurs de bruit)
- [x] ✅ **Critique — résolu le 2026-08-11.** Client généré désynchronisé (`manager_api_new/`) : 14 fichiers modèle orphelins → `flutter analyze` inexploitable (183/200 erreurs de bruit, 68 au 09/08). **Mesuré après nettoyage** : `flutter analyze manager_api_new` passe de **67 à 3 issues**, l'analyse globale de manager-app de 68 erreurs de bruit à **3 erreurs**, toutes dans `test/widget_test.dart` (cassé et connu, voir « Basse » ci-dessous). `flutter build web` ✅ avant comme après — attendu, ces fichiers n'étant dans aucun graphe. **`flutter analyze` redevient un feu vert utilisable**, ce qui lève le lien L8
- **Caractérisé précisément le 2026-08-11.** `lib/api.dart` déclare **139 `part`** pour **153 fichiers** dans `lib/model/`. Les 14 restants portent bien `part of openapi.api;` mais **ne sont déclarés `part` par personne** — en Dart, un fichier `part` non listé par sa bibliothèque est **hors du graphe de compilation**. D'où l'asymétrie qui a coûté du temps : l'analyzer les lit (il parcourt tout `lib/`), le compilateur ne les voit jamais. **Les supprimer ne peut pas changer un build.**
- Les 14 : `agenda_event_stat_dto`, `app_configuration_link_dto_application_instance`, `content_dto_resource`, `content_geo_point`, `day_stat_dto`, `game_stat_dto`, `level_dto`, `map_dto_map_provider`, `map_dto_map_type`, `map_dto_map_type_mapbox`, `poi_stat_dto`, `quiz_stat_dto`, `section_stat_dto`, `user`.
- **Fausse alerte levée le 2026-08-11, à ne pas rouvrir** : `ContentGeoPoint` (un des 14) *semble* référencé par du code vivant — `tablet-app/lib/Screens/Map/marker_view.dart:456` et `mymuseum-visitapp/lib/Screens/Sections/Map/marker_view.dart:367`, les deux apps dépendant de `manager_api_new` par `path:`. **Les deux lignes sont dans un bloc commenté** (`/*` ouvert respectivement en 418 et 329) : du code mort, invisible pour le compilateur *et* pour l'analyzer. Vérifié par comptage de délimiteurs et confirmé par `flutter analyze` sur les deux fichiers — zéro erreur. **Aucun des 14 n'a de référence vivante.**

View File

@ -454,13 +454,13 @@
</header>
<section class="summary" aria-label="Chiffres clés">
<div class="stat"><span class="n n-critical">4</span><span class="k">Urgent</span></div>
<div class="stat"><span class="n n-info">5</span><span class="k">Migration v3</span></div>
<div class="stat"><span class="n n-warn">7</span><span class="k">Bugs ouverts</span></div>
<div class="stat"><span class="n n-critical">3</span><span class="k">Urgent</span></div>
<div class="stat"><span class="n n-info">4</span><span class="k">Migration v3</span></div>
<div class="stat"><span class="n n-warn">6</span><span class="k">Bugs ouverts</span></div>
<div class="stat"><span class="n">6</span><span class="k">À tester</span></div>
<div class="stat"><span class="n">22</span><span class="k">Planifié</span></div>
<div class="stat"><span class="n">20</span><span class="k">Planifié</span></div>
<div class="stat"><span class="n n-gate">11</span><span class="k">Bascule prod</span></div>
<div class="stat"><span class="n n-good">21</span><span class="k">Fait récemment</span></div>
<div class="stat"><span class="n n-good">28</span><span class="k">Fait récemment</span></div>
</section>
<div class="filters" role="group" aria-label="Filtrer par domaine">
@ -486,7 +486,7 @@
<!-- URGENT -->
<section class="col" style="--stripe: var(--critical)">
<div class="col-head"><h2>Urgent</h2><span class="count">4</span></div>
<div class="col-head"><h2>Urgent</h2><span class="count">3</span></div>
<div class="stack">
<article class="card" data-area="infra">
@ -513,28 +513,20 @@
<span class="src">parity-manager-visitapp.md §6 — B3 · STATUS.md §1bis</span>
</article>
<article class="card" data-area="manager">
<div class="card-meta"><span class="tag">manager_api_new</span><span class="flag f-warn">Dette</span></div>
<h3>Client généré désynchronisé — diagnostic fermé le 11/08</h3>
<p>Le compte exact : <code>api.dart</code> déclare <strong>139 <code>part</code></strong> pour <strong>153 fichiers</strong> dans <code>lib/model/</code>. Les 14 restants portent bien <code>part of openapi.api;</code> mais <strong>ne sont déclarés <code>part</code> par personne</strong>. En Dart, un fichier <code>part</code> non listé par sa bibliothèque est <strong>hors du graphe de compilation</strong> — l'analyzer le lit puisqu'il parcourt tout <code>lib/</code>, le compilateur ne le voit jamais. Toute l'asymétrie qui a coûté du temps est là, et avec elle la garantie : <strong>supprimer ces fichiers ne peut pas changer un résultat de build.</strong></p>
<p>Les <strong>déclencheurs de génération</strong> partent avec eux, pour une autre raison : ce n'est pas du modèle mort, c'est le moyen de régénérer (annotation <code>@Openapi</code> + <code>build_runner</code>). Les supprimer ne retire aucun code exécuté — ça <strong>verrouille la règle « ce client s'édite à la main »</strong> : tant qu'ils sont là, un <code>build_runner build</code> lancé par réflexe écrase les éditions manuelles, dont <code>onboarding_api.dart</code>, le câblage d'<code>AIApi</code> et le mapping <code>isGood</code><code>isCorrect</code>.</p>
<p>⚠️ <strong>Ils étaient trois, pas un.</strong> Le verrou ne tenait que dans manager-app : <code>mymuseum-visitapp/lib/api/openApiTest.dart</code> générait depuis <strong>son propre swagger</strong> vers un <code>manager_api_new</code> local — un second client divergent dans un repo qui consomme celui de manager-app par <code>path:</code> — et <code>tablet-app/lib/api/openApi.dart</code> faisait de même sous un autre nom de fichier, ce qui lui avait permis d'échapper aux recherches sur « openApiTest ». Les trois sont supprimés. Contrôle de non-régression : <code>grep -rl "@Openapi" */lib</code> doit rester vide.</p>
<p><strong>Aucun des 14 n'a de référence vivante</strong> — vérifié. <code>content_geo_point</code> en avait l'air (tablet-app et mymuseum-visitapp), mais les deux usages sont dans des blocs commentés : voir la carte tablet-app.</p>
<span class="src">security/audit-manager-app.md · STATUS.md §3</span>
</article>
</div>
</section>
<!-- MIGRATION -->
<section class="col" style="--stripe: var(--info)">
<div class="col-head"><h2>Migration v3</h2><span class="count">5</span></div>
<div class="col-head"><h2>Migration v3</h2><span class="count">4</span></div>
<div class="stack">
<article class="card" data-area="backend infra">
<div class="card-meta"><span class="tag">manager-service</span><span class="tag">reprise</span></div>
<h3>Backfill <code>StoragePath</code>/<code>SizeBytes</code> des 45 lignes</h3>
<p><strong>✅ Prérequis levé le 10/08</strong> : <code>Create</code> écrit désormais <code>StoragePath</code> (<code>pictures/{instanceId}/{resourceId}</code>) et <code>SizeBytes</code>, types URL exclus, et <code>manager-app</code> envoie la taille avant l'upload. Le backfill ne sera donc pas à refaire au prochain upload. Reste le backfill lui-même, en deux moitiés : <code>StoragePath</code> est un simple <code>UPDATE</code> SQL (chemin déterministe), seul <code>SizeBytes</code> exige de lister le bucket. ⚠️ <strong>Mesuré le 09/08 sur 45 lignes</strong> : 37 ont un blob, <strong>7 sont des types URL</strong> (Wikipedia, YouTube, <code>agenda.php</code>) sans aucun fichier — les inclure serait faux — et 1 est de type fichier sans URL. <code>SizeBytes</code> vaut <strong>0 partout</strong> : le quota de stockage ne veut rien dire pour personne.</p>
<p>⚠️ <strong>Le « prérequis levé le 10/08 » n'était vrai qu'à moitié — relevé le 11/08 en extrayant le calculateur (C1).</strong> <code>ResourceController</code> a <em>deux</em> chemins de création, et le chemin <strong>multipart</strong> (le téléversement) écrivait <code>SizeBytes</code> mais laissait <code>StoragePath</code> <strong>nul</strong> ; seul le chemin JSON écrivait les deux. Les deux passent maintenant par <code>ResourceStorage.Apply</code>, donc le prérequis est réellement levé — mais des lignes créées entre-temps sont à rattraper par ce backfill.</p>
<p><strong>Appeler <code>ResourceStorage.PathFor</code></strong> plutôt que réécrire le chemin : c'est tout l'objet de C1, et l'écart (e) de la migration l'appellera aussi.</p>
<span class="src">v2/media-storage-plan.md §5 — État mesuré · STATUS.md §1quater — Lot médias</span>
</article>
@ -552,13 +544,6 @@
<span class="src">v2/media-storage-plan.md</span>
</article>
<article class="card" data-area="backend manager">
<div class="card-meta"><span class="tag">manager-service</span></div>
<h3>Flag watermark sur Instance</h3>
<p>Remplace un <code>if</code> sur un id d'instance codé en dur. Doit remonter dans la config lue par manager-app, puisque le watermark s'applique côté client.</p>
<span class="src">v2/media-storage-plan.md</span>
</article>
<article class="card" data-area="infra">
<div class="card-meta"><span class="tag">GCP</span><span class="flag f-good">5 min</span></div>
<h3>Alerte de budget</h3>
@ -571,7 +556,7 @@
<!-- BUGS -->
<section class="col" style="--stripe: var(--warn)">
<div class="col-head"><h2>Bugs ouverts</h2><span class="count">7</span></div>
<div class="col-head"><h2>Bugs ouverts</h2><span class="count">6</span></div>
<div class="stack">
<article class="card" data-area="backend visitapp">
@ -616,13 +601,6 @@
<span class="src">parity-manager-visitapp.md §2</span>
</article>
<article class="card" data-area="visitapp">
<div class="card-meta"><span class="tag">web</span><span class="tag">W1</span></div>
<h3>Fournisseur de carte ignoré en web</h3>
<p>Un client qui choisit « Google Hybrid » voit de l'OSM : <code>LeafletMap.tsx</code> a un <code>TileLayer</code> en dur.</p>
<span class="src">parity-manager-visitapp.md §3</span>
</article>
</div>
</section>
@ -678,7 +656,7 @@
<!-- PLANIFIÉ -->
<section class="col" style="--stripe: var(--ink-3)">
<div class="col-head"><h2>Planifié</h2><span class="count">22</span></div>
<div class="col-head"><h2>Planifié</h2><span class="count">20</span></div>
<div class="stack">
<article class="card" data-area="commercial" data-horizon="v1">
@ -745,15 +723,6 @@
<span class="src">v1-plan.md — DB5</span>
</article>
<article class="card" data-area="backend" data-horizon="v1">
<div class="card-meta"><span class="tag">manager-service</span><span class="flag f-good">Tranché 11/08</span></div>
<h3>Nettoyage des champs backend</h3>
<p><code>SectionMap.MapResourceId</code> → à <em>renommer</em> en <code>IconResourceId</code>, pas à supprimer : il est branché de bout en bout sous un nom trompeur. Le renommage <strong>est</strong> l'écart (g) de la migration — le faire ici le fait disparaître là-bas.</p>
<p><code>SectionEvent.ParcoursIds</code><strong>supprimer</strong>. Vérifié le 11/08 : lu ni écrit nulle part, commentaire (<code>// Liens vers GeoPoints</code>) contredisant son nom, et le lien événement↔parcours existe déjà en sens inverse via <code>GuidedPath.SectionEventId</code>. Décisif : il est sur <code>SectionEvent</code> et non sur <code>ProgrammeBlock</code>, donc <strong>incapable du cas « parcours de tel jour »</strong> pour lequel on le gardait.</p>
<p><code>QuestionType</code><strong>ne pas toucher au schéma</strong> : le trio TextLibre/Digicode/ExpectedAnswer n'existe pas dans le code, et le digicode est un comportement dérivé. Reste une propreté front. <strong>Sort du lot « geler le schéma »</strong>.</p>
<span class="src">STATUS.md §6bis et §1sexies · v1-plan.md — lot B</span>
</article>
<article class="card" data-area="backend manager" data-horizon="v2">
<div class="card-meta"><span class="tag">priorité 2</span><span class="flag f-warn">V2</span></div>
<h3>Ingestion documentaire + OCR</h3>
@ -824,13 +793,6 @@
<span class="src">todo-features.md</span>
</article>
<article class="card" data-area="manager visitapp" data-horizon="v1">
<div class="card-meta"><span class="tag">SectionParcours</span><span class="flag f-good">Petit · arbitré 09/08</span></div>
<h3>PDF en média d'étape</h3>
<p>Retenu <em>à la place</em> de faire de SectionParcours un conteneur de sections — idée écartée le 09/08 : la moitié des types (Agenda, Météo, Event) n'a pas de sens dans un point de parcours, et le lien vers une section existante existe déjà là où il est cohérent (<code>baseSectionMapId</code>). Le vrai manque tient à un type de ressource : une étape accepte déjà image, vidéo et audio, <strong>PDF est le seul absent</strong>. Trois points : liste de types paramétrable côté étape, cas <code>PDF</code> dans <code>getElementForResource</code>, cas PDF dans <code>ResourceViewer.tsx</code>. Sans les deux derniers, le PDF s'afficherait en image cassée — le bug corrigé pour la vidéo le 09/08.</p>
<span class="src">todo-features.md — Parcours guidés</span>
</article>
<article class="card" data-area="visitapp" data-horizon="v2">
<div class="card-meta"><span class="tag">polish</span><span class="flag f-warn">V2</span></div>
<h3>Talking head — avatar animé</h3>
@ -939,9 +901,44 @@
<section class="done">
<h2>Fait récemment</h2>
<p>Vingt et un chantiers clos entre le 5 et le 11 août 2026.</p>
<p>Vingt-huit chantiers clos entre le 5 et le 11 août 2026.</p>
<div class="done-grid">
<div class="done-item">
<strong>Le schéma est gelé — lot B, et le lot G peut enfin commencer</strong>
<span>Le point d'articulation du plan : tant qu'un changement de modèle restait ouvert, réécrire <code>MigrationController</code> revenait à l'écrire deux fois. <strong>Une seule migration EF</strong> porte les trois changements. <code>SectionMap.MapResourceId</code><code>IconResourceId</code>, ce qui fait tomber l'<strong>écart (g)</strong> de la bascule tout seul — et il fallait renommer la propriété de navigation <code>MapResource</code> avec, sinon la convention EF fabriquait un FK fantôme. Point vérifié plutôt que supposé : EF a produit un <code>RenameColumn</code>, <em>pas</em> un drop+add — les icônes déjà configurées survivent. <code>SectionEvent.ParcoursIds</code> supprimé (l'avertissement de perte de données d'EF ne porte que sur lui, c'est l'intention). Filigrane : <code>Instance.IsImageWatermark</code> remplace le <code>instanceId == "633ee379…"</code> en dur. <strong>Référence tenue : 130/130 tests avant comme après</strong>, build Debug <em>et</em> Release verts.</span>
</div>
<div class="done-item">
<strong>Le backdoor d'authentification, et deux fuites de moins</strong>
<span>Trouvé en cherchant le « backdoor <code>#if DEBUG</code> » du rapport d'audit : dans <code>AuthenticationController.Authenticate</code>, un bloc <strong>écrasait l'email et le mot de passe reçus</strong> par un compte de test — toute compilation en Debug authentifiait n'importe quelle saisie en tant que <code>test@email.be</code>. Retiré. <code>EnableSensitiveDataLogging</code> (qui écrit les valeurs des paramètres dans les logs : hashes, clés API, données de visiteurs) passe sous <code>#if DEBUG</code>, l'idiome déjà employé dans ce même fichier pour le CORS et Hangfire — et le Dockerfile publiant en <code>-c Release</code>, c'est un vrai verrou, ce qu'un build Release a confirmé. Les autres <code>#if DEBUG</code> du repo sont légitimes.</span>
</div>
<div class="done-item">
<strong>Le client généré nettoyé — et la règle « pas de régénération » enfin verrouillée</strong>
<span>Le compte exact : <code>api.dart</code> déclarait <strong>139 <code>part</code> pour 153 fichiers</strong>. Les 14 restants portaient <code>part of openapi.api;</code> sans être listés par leur bibliothèque — en Dart, <strong>hors du graphe de compilation</strong> : l'analyzer les lisait, le compilateur ne les voyait jamais. Toute l'asymétrie qui a coûté du temps était là, et avec elle la garantie que les supprimer ne pouvait pas casser un build. Mesuré : <code>flutter analyze</code> sur le package passe de <strong>67 à 3 issues</strong>, et l'analyse globale de manager-app de <em>68 erreurs de bruit</em> à <strong>3 erreurs</strong>, toutes dans <code>test/widget_test.dart</code> (cassé et connu) — <code>flutter analyze</code> redevient un feu vert utilisable, ce qui débloque les lots design. <code>flutter build web</code> ✅ avant comme après.</span>
</div>
<div class="done-item">
<strong>Trois déclencheurs de génération, pas un</strong>
<span>Le verrou ne tenait que dans manager-app. <code>mymuseum-visitapp/lib/api/openApiTest.dart</code> générait depuis <strong>son propre swagger</strong> vers un <code>manager_api_new</code> local — un second client divergent dans un repo qui consomme celui de manager-app par <code>path:</code> — et <code>tablet-app/lib/api/openApi.dart</code> faisait de même sous un autre nom de fichier, ce qui lui avait permis d'échapper à toutes les recherches sur « openApiTest ». Plus un résidu <code>tablet-app/manager_api_new/</code> réduit à un <code>pubspec.lock</code>, non suivi par git, daté d'avril. Les trois sont supprimés : ce ne sont pas des fichiers morts, c'était <em>le moyen d'écraser les éditions manuelles</em><code>onboarding_api.dart</code>, le câblage d'<code>AIApi</code>, le mapping <code>isGood</code><code>isCorrect</code>. Contrôle de non-régression : <code>grep -rl "@Openapi" */lib</code> doit rester vide.</span>
</div>
<div class="done-item">
<strong>Fournisseur de carte en web — tranché, et pas comme prévu</strong>
<span>Option a retenue : <strong>le web reste sur Leaflet quoi que le client configure</strong>. Ni MapBox (token payant) ni Google Maps JS (clé facturée + seconde librairie carto à maintenir) — aucun coût récurrent ajouté avant la prod. Mais l'option annoncée disait « retirer le champ pour les instances web », et <strong>c'est impossible</strong> : le fournisseur est porté par la <em>section</em> (<code>SectionMap.MapMapProvider</code>, <code>SectionAgenda.AgendaMapProvider</code>), pas par l'instance — et une même Configuration est liée à plusieurs <code>ApplicationInstance</code> via <code>AppConfigurationLink</code>, donc servie au mobile <em>comme</em> au web. Masquer le champ aurait supprimé le réglage mobile. Il est donc conservé, et manager-app <strong>cesse de laisser croire qu'il vaut pour le web</strong> : mention explicite sous les deux sélecteurs. Zéro ligne côté visitapp-web — Leaflet inconditionnel était déjà le comportement ; ce qui manquait n'était pas du code mais une phrase honnête dans l'interface.</span>
</div>
<div class="done-item">
<strong>PDF en média d'étape — et le dispatcher qui n'était pas le bon</strong>
<span>Les trois volets sont faits. La liste de types du sélecteur de médias devient un paramètre : l'étape passe désormais « types du slider + PDF » là où c'était figé en dur. Côté web, le cas PDF réutilise le <code>PdfViewer</code> existant <em>et</em> son proxy <code>/api/pdf</code> — le contournement CORS était déjà écrit, il n'y avait qu'à le brancher. <strong>Côté Flutter, la carte indiquait le mauvais endroit</strong> : <code>showElementForResource</code> fait un <code>return CachedCustomResource(...)</code> <em>avant</em> son <code>switch</code>, donc tout ce switch est du code mort depuis un moment — ajouter le cas PDF là n'aurait rien changé et aurait été « vérifié » sans rien prouver. Le vrai dispatcher est <code>CachedCustomResource</code>, et il a <strong>deux</strong> switch (ressource distante / fichier déjà téléchargé pour l'offline) qui tombaient tous deux sur <code>Text("Not supported type")</code> : les deux sont traités, le distant devant télécharger avant d'afficher puisque <code>PDFView</code> n'accepte qu'un chemin de fichier. Au passage : l'enum du client s'appelle <code>ResourceType.Pdf</code>, pas <code>PDF</code>. <code>npm run build</code> ✅, <code>flutter build web</code> ✅ — le volet mymuseum ne tient que sur l'analyse, son APK ne compilant pas.</span>
</div>
<div class="done-item">
<strong>QuestionType — la fin des comparaisons par entier brut</strong>
<span>Descendu du lot B, où il ne restait plus qu'une propreté de front. La cause était dans le client généré : le générateur nomme les valeurs <code>number0/1/2</code>, et le front finissait donc par écrire <code>?.value == 2 ? 'Puzzle' : …</code> — lisible seulement si l'on connaît l'ordre de l'enum serveur par cœur. Des alias nommés d'après le serveur (<code>simple</code>, <code>multipleChoice</code>, <code>puzzle</code>) sont ajoutés à la main dans <code>question_type.dart</code>, sans toucher à <code>values</code> : ce sont les mêmes trois valeurs, pas de nouvelles. Les deux sites visés par le plan sont corrigés, plus les 7 usages <code>number*</code> du dialogue de question — il ne reste <strong>aucun</strong> <code>QuestionType.number</code> dans les trois apps.</span>
</div>
<div class="done-item">
<strong>Parcours — on ne perd plus son travail sans être averti</strong>
<span>Le défaut était pire que décrit : <strong>trois</strong> dialogues faisaient <code>Navigator.pop</code> sur « Annuler » sans rien demander, pas un — Parcours, Étape et Question, chacun jetant tout ce qui était saisi <em>sous</em> lui. Le plus coûteux est le plus profond : une question de quiz avec ses réponses. Ce n'était pas « la croix » comme le disait cette carte, c'était le bouton Annuler, et le clic hors fenêtre était déjà neutralisé — le vrai chemin de perte restant est le <strong>retour arrière du navigateur</strong>, couvert par <code>PopScope</code>. Détection des modifications par instantané JSON, pris <strong>après la première frame</strong> : le dialog Question normalise ses réponses pendant sa construction, un instantané pris avant aurait déclaré « modifié » une fenêtre intacte. Réutilise <code>showConfirmationDialog</code> plutôt que d'ouvrir un second composant de confirmation. 3 clés i18n FR/EN/NL, <code>flutter build web</code> ✅.</span>

View File

@ -91,22 +91,30 @@ Rien de créatif, tout est un verrou pour la suite.
| Quoi | Décision préalable | Lien |
|---|---|---|
| Renommer `SectionMap.MapResourceId``IconResourceId` (branché de bout en bout : `SectionFactory:179/511`, `SectionMap.ToDTO:58`, `MigrationController:469`) | Aucune, déjà tranché au §6bis | **L6** = écart (g) |
| **Supprimer `SectionEvent.ParcoursIds`** — champ, DTO, migration EF | ✅ **Tranché le 2026-08-11 : supprimer.** Investigation : le champ n'est **lu ni écrit nulle part** (entité + DTO + migrations, zéro contrôleur, zéro service), son commentaire dit `// Liens vers GeoPoints spécifiques` ce qui contredit son nom, et le lien événement↔parcours existe dans l'autre sens via `GuidedPath.SectionEventId`, lui bien entretenu (`SectionMapController:396/449`, `SectionController:878`). Argument décisif : il est porté par **`SectionEvent`, pas par `ProgrammeBlock`** — il ne peut donc pas exprimer « les parcours de tel jour », le cas carnaval pour lequel on le gardait | Nettoyage §1 « À faire » |
| Supprimer `SectionEvent.IconResourceId` (seul candidat sans usage visiteur identifié) | Aucune | §6bis |
| ~~Renommer `SectionMap.MapResourceId` → `IconResourceId`~~**2026-08-11** | Aucune, déjà tranché au §6bis | **L6** = écart (g), **qui tombe**. ⚠️ Il fallait renommer **aussi la propriété de navigation `MapResource`**`IconResource` : la convention EF l'appariait au FK, la laisser aurait fabriqué un FK fantôme. Elle n'était utilisée nulle part ailleurs. Sites touchés : `SectionMap.cs:25/26/45/77`, `SectionFactory:227/559`, `ResourceController:469`, `MigrationController:469` |
| ~~**Supprimer `SectionEvent.ParcoursIds`**~~**2026-08-11** — champ, DTO, `SectionFactory:35/199/529`, et **un montage de test** (`SectionEventControllerTests:47`) que l'investigation initiale n'avait pas couvert : elle disait « zéro contrôleur, zéro service », ce qui était exact, mais les tests n'étaient pas dans le périmètre. La ligne retirée était une simple initialisation, sans rapport avec l'assertion du test | ✅ **Tranché le 2026-08-11 : supprimer.** Investigation : le champ n'est **lu ni écrit nulle part** (entité + DTO + migrations, zéro contrôleur, zéro service), son commentaire dit `// Liens vers GeoPoints spécifiques` ce qui contredit son nom, et le lien événement↔parcours existe dans l'autre sens via `GuidedPath.SectionEventId`, lui bien entretenu (`SectionMapController:396/449`, `SectionController:878`). Argument décisif : il est porté par **`SectionEvent`, pas par `ProgrammeBlock`** — il ne peut donc pas exprimer « les parcours de tel jour », le cas carnaval pour lequel on le gardait | Nettoyage §1 « À faire » |
| ~~Supprimer `SectionEvent.IconResourceId`~~ **— écarté le 2026-08-11 : ce champ n'existe pas** | **Erreur de doc, à ne pas rouvrir.** `SectionEvent` n'a aucun `IconResourceId`. La ligne visée (`SectionEvent.cs:105`) appartient à la classe **imbriquée `MapAnnotation`**, déclarée dans le même fichier — et elle est **très utilisée** : `SectionEventController:59/230/330`, `SectionAgendaController:58`, `SectionMapController:349/560`, plus les deux `.SelectMany(a => ResourceId(a.IconResourceId))` de `GetReferencedResourceIds` (lignes 50 et 53). `MapAnnotation` est partagée par SectionEvent, SectionAgenda **et** SectionMap : la supprimer aurait cassé les icônes d'annotation des trois types **et** la collecte de ressources offline | §6bis |
| ~~Unifier `QuestionType`~~**sorti du lot B le 2026-08-11** | ✅ **Tranché : on ne touche pas au schéma.** Le trio TextLibre/Digicode/ExpectedAnswer **n'existe pas dans le code** : l'enum a trois valeurs (`Simple`, `MultipleChoice`, `Puzzle`) référencées à **un seul endroit** côté serveur, et le digicode n'est pas un type mais un comportement dérivé d'une réponse numérique. Reste une propreté front : les comparaisons par entier brut (`…?.value == 2 ? 'Puzzle' : …`) dans `showNewOrUpdateGuidedStep.dart:409` et `guided_step_challenge.dart:14`**déplacé au lot E** | **Ne bloque plus `MigrationController`** |
| Implémenter `Section.meterZoneGPS` côté visiteur (la colonne existe, une constante à 100 m la remplace) | Aucune, tranché au §6bis | Bug M3 du kanban — même passe |
| Flag watermark sur `Instance` + exposition dans la config, retrait du `if instanceId == "633ee379…"` de `ResourceController:265` | Aucune | Point 6 du §1ter |
Sortie du lot : `dotnet build` + `dotnet test` verts, une migration EF unique regroupant les renommages, et **plus aucun changement de modèle en attente**.
**Lot B livré le 2026-08-11.** Migration unique `20260811130738_LotB_FreezeSchema`. **Référence tenue : 130/130 tests avant comme après**, build **Debug et Release** verts (Release vérifié parce que le point sécurité ci-dessous change ce que Release compile).
Deux points vérifiés plutôt que supposés :
- **EF a généré un `RenameColumn`, pas un drop+add** pour `MapResourceId``IconResourceId` : les icônes déjà configurées survivent à la migration. C'était le vrai risque du lot.
- **L'avertissement « may result in the loss of data » d'EF ne porte que sur le `DropColumn` de `ParcoursIds`** — c'est l'intention du lot, pas un effet de bord.
**Reste hors de ce lot** : `Section.meterZoneGPS` côté visiteur (Flutter/TypeScript, pas le schéma) — la colonne serveur est correctement mappée, c'est le bug M3 qui attend son implémentation visiteur.
### Lot C — Terminer le lot médias (2 à 3 jours)
L'étape A du §1quater est faite (`StoragePath`/`SizeBytes` écrits à `Create` depuis le 10/08). Reste l'étape C.
| # | Quoi | Note |
|---|---|---|
| C1 | **Extraire le calculateur** `StoragePath` + `SizeBytes` dans un service appelable | **L5** — consommé par C2 *et* par l'écart (e) du lot G |
| ~~C1~~**2026-08-11** | **Calculateur extrait** dans `Helpers/ResourceStorage.cs` (statique, comme `ImageHelper`/`SlugHelper` — logique pure, pas d'I/O, donc appelable sans DI depuis `MigrationController` et le futur backfill). Trois membres : `HasBlob(type)`, `PathFor(type, instanceId, resourceId)`, `Apply(resource, sizeBytes)`. **13 tests** fixent l'invariant des types URL. `dotnet test` **143/143** | **L5** — consommé par C2 *et* par l'écart (e) du lot G.<br>⚠️ **L'extraction a révélé la divergence qu'elle devait empêcher** : il y a **deux** chemins de création dans `ResourceController`, et le chemin **multipart** (téléversement) écrivait `SizeBytes` mais laissait **`StoragePath` nul** — seul le chemin JSON écrivait les deux. Les deux passent désormais par `Apply`. C'est une partie des lignes que C2 doit rattraper.<br>⚠️ **Angle mort laissé volontairement** : `Update` peut faire passer `Type` d'un type URL à un type fichier, laissant `StoragePath` nul sur une ligne qui a maintenant un blob. Non corrigé ici — recalculer le chemin sur une ressource existante pointerait vers un objet qui n'a pas bougé dans le bucket. À trancher avec C3 (quota), pas avant |
| C2 | Backfill : `StoragePath` par `UPDATE` SQL (chemin déterministe `pictures/{instanceId}/{resourceId}`), `SizeBytes` par listing du bucket Firebase | **37 lignes sur 45.** Les 7 types URL (`ImageUrl`/`VideoUrl`/`JSONUrl`) n'ont pas de blob, la 8ᵉ est un type fichier sans URL — inventaire des orphelins au passage |
| C3 | Quota de stockage autoritaire : pré-vol + contrôle à `Create` + suppression du blob à `Delete` | **L10** — après C2, sinon le contrôle porte sur des zéros |
| C4 | Compression images côté client, 2560 px / JPEG q82 (~12×) | `manager-app`. Indépendant de C2 : ne touche que les nouveaux uploads |
@ -154,8 +162,9 @@ Ne change pas : la popup de traduction (un niveau justifié, langues verticales,
| Quoi | Réf. |
|---|---|
| Fournisseur de carte honoré en web (`LeafletMap.tsx`, `TileLayer` en dur) | W1 |
| PDF en média d'étape : liste de types paramétrable côté étape, cas `PDF` dans `getElementForResource` **et** dans `ResourceViewer.tsx` | Les deux derniers points ne sont pas optionnels — sans eux le PDF s'affiche en image cassée, le bug corrigé pour la vidéo le 09/08 |
| ~~Fournisseur de carte honoré en web~~**2026-08-11 — tranché : option a, le web reste sur Leaflet** | W1. **Ce n'était pas un oubli de câblage** : le web reçoit déjà `mapProvider`, `mapType` et `mapTypeMapbox` (`client.ts:44-45`). Mais « honorer le fournisseur » n'a pas d'équivalent Leaflet gratuit — côté Flutter ce sont **deux SDK** (`GoogleMapView` / `MapBoxView`, `map_page.dart:142-148`) ; MapBox exige un token payant, et les tuiles Google en Leaflet violent leurs conditions.<br>⚠️ **L'option a ne pouvait pas se faire comme annoncée.** « Retirer le champ pour les instances web » est **impossible** : le fournisseur est porté par la **section** (`SectionMap.MapMapProvider`, `SectionAgenda.AgendaMapProvider`), pas par l'instance — et une même Configuration est liée à plusieurs `ApplicationInstance` via `AppConfigurationLink`, donc **servie au mobile comme au web**. Masquer le champ aurait supprimé le réglage mobile.<br>**Ce qui a été fait à la place** : le champ reste (il gouverne toujours le mobile) et manager-app **cesse de laisser croire qu'il vaut pour le web** — mention explicite sous les deux sélecteurs (`map_config.dart`, `agenda_config.dart`), 1 clé i18n FR/EN/NL. Zéro ligne côté `visitapp-web` : Leaflet inconditionnel était déjà le comportement. `flutter build web` ✅ |
| ~~PDF en média d'étape~~**2026-08-11** | Les trois volets faits. **Côté manager-app** : la liste de types du sélecteur de médias devient un paramètre (`kSliderContentResourceTypes` par défaut) et l'étape passe `kGuidedStepResourceTypes` = les types du slider + `Pdf`. **Côté visitapp-web** : cas PDF dans `ResourceViewer.tsx`, réutilisant le `PdfViewer` existant et son proxy `/api/pdf` (contournement CORS déjà en place), importé en `dynamic({ssr:false})` comme le fait `PdfSection`. **Côté mymuseum-visitapp** : ⚠️ **pas dans `showElementForResource`** — cette fonction fait un `return CachedCustomResource(...)` **avant** son `switch`, donc tout son switch est du code mort. Le vrai dispatcher est `CachedCustomResource`, qui a **deux** switch (ressource distante / fichier local déjà téléchargé) tombant tous deux sur `Text("Not supported type")` : les deux sont traités. L'enum du client s'appelle `ResourceType.Pdf`, pas `PDF`.<br>`npm run build` ✅, `flutter build web` (manager-app) ✅. Le volet mymuseum n'est vérifié que par `flutter analyze` — son APK ne compile pas (Gradle/NDK, cf. §1bis) |
| ~~Comparaisons `QuestionType` par entier brut~~**2026-08-11** | Descendu du lot B. Le générateur nommait les valeurs `number0/1/2`, ce qui poussait le front à comparer des entiers. Alias nommés ajoutés à la main dans `question_type.dart` (`simple`, `multipleChoice`, `puzzle`, d'après l'enum serveur `Simple`/`MultipleChoice`/`Puzzle`), `values` inchangé. Les deux sites visés sont corrigés (`showNewOrUpdateGuidedStep`, `guided_step_challenge._kindOf`) **et** les 7 usages `number*` de `showNewOrUpdateQuizQuestion` migrés — plus aucun `QuestionType.number` dans les trois apps. Les libellés français en dur de ce dialogue relèvent de la dette i18n générale, laissés en place |
| ~~Refonte du flux SectionParcours~~ | **Déplacé au lot D-bis** (DB2 et DB4) — c'est un chantier de design, pas une finition de parité |
### Lot F — Guide IA lot 4 et dette produit (1 semaine)
@ -272,6 +281,7 @@ A (½-1j) ──► B (2-3j) ──► G (3-4j) ──► H (1 sem.) ──► I
|---|---|---|
| ~~`SectionEvent.ParcoursIds`~~ | ✅ **2026-08-11 — supprimer.** Champ mort, et structurellement incapable du cas jour/bloc | — |
| ~~`QuestionType`~~ | ✅ **2026-08-11 — ne pas toucher au schéma.** Sort du lot B ; reste une propreté front au lot E | — |
| ~~**Fournisseur de carte en web (W1)**~~ | ✅ **2026-08-11 — option a : le web reste sur Leaflet, quoi que le client configure.** Ni MapBox ni Google Maps JS : pas de coût récurrent ni de seconde librairie carto avant la prod. **Correction apportée à l'option** : le champ ne peut pas être retiré pour le web, il est porté par la section et partagé entre plateformes — il est donc conservé et **documenté dans l'interface** comme réglage mobile/tablette. Voir le lot E | — |
| Flux de configuration SectionParcours | **A — fenêtre unique avec rail d'étapes (recommandée)** · B — écran plein cadre 3 colonnes. Argument ajouté le 10/08 : le seul gain réel de B est une colonne d'aperçu visiteur, déjà couverte par le §5 du Guide IA (**L18**) | DB4 seulement — **pas DB2**, la sauvegarde au fil de l'eau part sans attendre |
| CGU §5 face aux contrats déjà signés | Vérifier ce que les clients existants ont signé avant de considérer le sujet clos | Lot F (volet RGPD dans le même document) |