From 005f5c16067642f466bd467e6e54c20178dad4b1 Mon Sep 17 00:00:00 2001 From: Thomas Fransolet Date: Tue, 11 Aug 2026 15:32:19 +0200 Subject: [PATCH] =?UTF-8?q?Lot=20B,=20C1=20et=20lot=20E=20livr=C3=A9s=20;?= =?UTF-8?q?=20deux=20erreurs=20de=20doc=20corrig=C3=A9es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- STATUS.md | 21 ++++++++++- kanban.html | 101 +++++++++++++++++++++++++--------------------------- v1-plan.md | 22 ++++++++---- 3 files changed, 85 insertions(+), 59 deletions(-) diff --git a/STATUS.md b/STATUS.md index c0616e7..8fc8415 100644 --- a/STATUS.md +++ b/STATUS.md @@ -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.** diff --git a/kanban.html b/kanban.html index 3cf5466..a3eb6fd 100644 --- a/kanban.html +++ b/kanban.html @@ -454,13 +454,13 @@
-
4Urgent
-
5Migration v3
-
7Bugs ouverts
+
3Urgent
+
4Migration v3
+
6Bugs ouverts
6À tester
-
22Planifié
+
20Planifié
11Bascule prod
-
21Fait récemment
+
28Fait récemment
@@ -486,7 +486,7 @@
-

Urgent

4
+

Urgent

3
@@ -513,28 +513,20 @@ parity-manager-visitapp.md §6 — B3 · STATUS.md §1bis
-
-
manager_api_newDette
-

Client généré désynchronisé — diagnostic fermé le 11/08

-

Le compte exact : 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 — l'analyzer le lit puisqu'il parcourt tout lib/, le compilateur ne le voit jamais. Toute l'asymétrie qui a coûté du temps est là, et avec elle la garantie : supprimer ces fichiers ne peut pas changer un résultat de build.

-

Les déclencheurs de génération 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 @Openapi + build_runner). Les supprimer ne retire aucun code exécuté — ça verrouille la règle « ce client s'édite à la main » : tant qu'ils sont là, un build_runner build lancé par réflexe écrase les éditions manuelles, dont onboarding_api.dart, le câblage d'AIApi et le mapping isGoodisCorrect.

-

⚠️ Ils étaient trois, pas un. Le verrou ne tenait que dans manager-app : mymuseum-visitapp/lib/api/openApiTest.dart générait depuis son propre swagger vers un manager_api_new local — un second client divergent dans un repo qui consomme celui de manager-app par path: — et tablet-app/lib/api/openApi.dart 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 : grep -rl "@Openapi" */lib doit rester vide.

-

Aucun des 14 n'a de référence vivante — vérifié. content_geo_point en avait l'air (tablet-app et mymuseum-visitapp), mais les deux usages sont dans des blocs commentés : voir la carte tablet-app.

- security/audit-manager-app.md · STATUS.md §3 -
- -
+
-

Migration v3

5
+

Migration v3

4
manager-servicereprise

Backfill StoragePath/SizeBytes des 45 lignes

✅ Prérequis levé le 10/08 : Create écrit désormais StoragePath (pictures/{instanceId}/{resourceId}) et SizeBytes, types URL exclus, et manager-app 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 : StoragePath est un simple UPDATE SQL (chemin déterministe), seul SizeBytes exige de lister le bucket. ⚠️ Mesuré le 09/08 sur 45 lignes : 37 ont un blob, 7 sont des types URL (Wikipedia, YouTube, agenda.php) sans aucun fichier — les inclure serait faux — et 1 est de type fichier sans URL. SizeBytes vaut 0 partout : le quota de stockage ne veut rien dire pour personne.

+

⚠️ Le « prérequis levé le 10/08 » n'était vrai qu'à moitié — relevé le 11/08 en extrayant le calculateur (C1). ResourceController a deux chemins de création, et le chemin multipart (le téléversement) écrivait SizeBytes mais laissait StoragePath nul ; seul le chemin JSON écrivait les deux. Les deux passent maintenant par ResourceStorage.Apply, donc le prérequis est réellement levé — mais des lignes créées entre-temps sont à rattraper par ce backfill.

+

Appeler ResourceStorage.PathFor plutôt que réécrire le chemin : c'est tout l'objet de C1, et l'écart (e) de la migration l'appellera aussi.

v2/media-storage-plan.md §5 — État mesuré · STATUS.md §1quater — Lot médias
@@ -552,13 +544,6 @@ v2/media-storage-plan.md -
-
manager-service
-

Flag watermark sur Instance

-

Remplace un if 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.

- v2/media-storage-plan.md -
-
GCP5 min

Alerte de budget

@@ -571,7 +556,7 @@
-

Bugs ouverts

7
+

Bugs ouverts

6
@@ -616,14 +601,7 @@ parity-manager-visitapp.md §2
-
-
webW1
-

Fournisseur de carte ignoré en web

-

Un client qui choisit « Google Hybrid » voit de l'OSM : LeafletMap.tsx a un TileLayer en dur.

- parity-manager-visitapp.md §3 -
- -
+
@@ -678,7 +656,7 @@
-

Planifié

22
+

Planifié

20
@@ -745,15 +723,6 @@ v1-plan.md — DB5
-
-
manager-serviceTranché 11/08
-

Nettoyage des champs backend

-

SectionMap.MapResourceId → à renommer en IconResourceId, pas à supprimer : il est branché de bout en bout sous un nom trompeur. Le renommage est l'écart (g) de la migration — le faire ici le fait disparaître là-bas.

-

SectionEvent.ParcoursIdssupprimer. Vérifié le 11/08 : lu ni écrit nulle part, commentaire (// Liens vers GeoPoints) contredisant son nom, et le lien événement↔parcours existe déjà en sens inverse via GuidedPath.SectionEventId. Décisif : il est sur SectionEvent et non sur ProgrammeBlock, donc incapable du cas « parcours de tel jour » pour lequel on le gardait.

-

QuestionTypene pas toucher au schéma : le trio TextLibre/Digicode/ExpectedAnswer n'existe pas dans le code, et le digicode est un comportement dérivé. Reste une propreté front. Sort du lot « geler le schéma ».

- STATUS.md §6bis et §1sexies · v1-plan.md — lot B -
-
priorité 2V2

Ingestion documentaire + OCR

@@ -824,13 +793,6 @@ todo-features.md
-
-
SectionParcoursPetit · arbitré 09/08
-

PDF en média d'étape

-

Retenu à la place 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 (baseSectionMapId). Le vrai manque tient à un type de ressource : une étape accepte déjà image, vidéo et audio, PDF est le seul absent. Trois points : liste de types paramétrable côté étape, cas PDF dans getElementForResource, cas PDF dans ResourceViewer.tsx. Sans les deux derniers, le PDF s'afficherait en image cassée — le bug corrigé pour la vidéo le 09/08.

- todo-features.md — Parcours guidés -
-
polishV2

Talking head — avatar animé

@@ -939,9 +901,44 @@

Fait récemment

-

Vingt et un chantiers clos entre le 5 et le 11 août 2026.

+

Vingt-huit chantiers clos entre le 5 et le 11 août 2026.

+
+ Le schéma est gelé — lot B, et le lot G peut enfin commencer + Le point d'articulation du plan : tant qu'un changement de modèle restait ouvert, réécrire MigrationController revenait à l'écrire deux fois. Une seule migration EF porte les trois changements. SectionMap.MapResourceIdIconResourceId, ce qui fait tomber l'écart (g) de la bascule tout seul — et il fallait renommer la propriété de navigation MapResource avec, sinon la convention EF fabriquait un FK fantôme. Point vérifié plutôt que supposé : EF a produit un RenameColumn, pas un drop+add — les icônes déjà configurées survivent. SectionEvent.ParcoursIds supprimé (l'avertissement de perte de données d'EF ne porte que sur lui, c'est l'intention). Filigrane : Instance.IsImageWatermark remplace le instanceId == "633ee379…" en dur. Référence tenue : 130/130 tests avant comme après, build Debug et Release verts. +
+ +
+ Le backdoor d'authentification, et deux fuites de moins + Trouvé en cherchant le « backdoor #if DEBUG » du rapport d'audit : dans AuthenticationController.Authenticate, un bloc écrasait l'email et le mot de passe reçus par un compte de test — toute compilation en Debug authentifiait n'importe quelle saisie en tant que test@email.be. Retiré. EnableSensitiveDataLogging (qui écrit les valeurs des paramètres dans les logs : hashes, clés API, données de visiteurs) passe sous #if DEBUG, l'idiome déjà employé dans ce même fichier pour le CORS et Hangfire — et le Dockerfile publiant en -c Release, c'est un vrai verrou, ce qu'un build Release a confirmé. Les autres #if DEBUG du repo sont légitimes. +
+ +
+ Le client généré nettoyé — et la règle « pas de régénération » enfin verrouillée + Le compte exact : api.dart déclarait 139 part pour 153 fichiers. Les 14 restants portaient part of openapi.api; sans être listés par leur bibliothèque — en Dart, hors du graphe de compilation : 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é : flutter analyze sur le package passe de 67 à 3 issues, et l'analyse globale de manager-app de 68 erreurs de bruit à 3 erreurs, toutes dans test/widget_test.dart (cassé et connu) — flutter analyze redevient un feu vert utilisable, ce qui débloque les lots design. flutter build web ✅ avant comme après. +
+ +
+ Trois déclencheurs de génération, pas un + Le verrou ne tenait que dans manager-app. mymuseum-visitapp/lib/api/openApiTest.dart générait depuis son propre swagger vers un manager_api_new local — un second client divergent dans un repo qui consomme celui de manager-app par path: — et tablet-app/lib/api/openApi.dart 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 tablet-app/manager_api_new/ réduit à un pubspec.lock, non suivi par git, daté d'avril. Les trois sont supprimés : ce ne sont pas des fichiers morts, c'était le moyen d'écraser les éditions manuellesonboarding_api.dart, le câblage d'AIApi, le mapping isGoodisCorrect. Contrôle de non-régression : grep -rl "@Openapi" */lib doit rester vide. +
+ +
+ Fournisseur de carte en web — tranché, et pas comme prévu + Option a retenue : le web reste sur Leaflet quoi que le client configure. 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 c'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. Il est donc conservé, et manager-app cesse de laisser croire qu'il vaut pour le web : 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. +
+ +
+ PDF en média d'étape — et le dispatcher qui n'était pas le bon + 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 PdfViewer existant et son proxy /api/pdf — le contournement CORS était déjà écrit, il n'y avait qu'à le brancher. Côté Flutter, la carte indiquait le mauvais endroit : showElementForResource fait un return CachedCustomResource(...) avant son switch, 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 CachedCustomResource, et il a deux switch (ressource distante / fichier déjà téléchargé pour l'offline) qui tombaient tous deux sur Text("Not supported type") : les deux sont traités, le distant devant télécharger avant d'afficher puisque PDFView n'accepte qu'un chemin de fichier. Au passage : l'enum du client s'appelle ResourceType.Pdf, pas PDF. npm run build ✅, flutter build web ✅ — le volet mymuseum ne tient que sur l'analyse, son APK ne compilant pas. +
+ +
+ QuestionType — la fin des comparaisons par entier brut + 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 number0/1/2, et le front finissait donc par écrire ?.value == 2 ? 'Puzzle' : … — 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 (simple, multipleChoice, puzzle) sont ajoutés à la main dans question_type.dart, sans toucher à values : 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 number* du dialogue de question — il ne reste aucun QuestionType.number dans les trois apps. +
+
Parcours — on ne perd plus son travail sans être averti Le défaut était pire que décrit : trois dialogues faisaient Navigator.pop sur « Annuler » sans rien demander, pas un — Parcours, Étape et Question, chacun jetant tout ce qui était saisi sous 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 retour arrière du navigateur, couvert par PopScope. Détection des modifications par instantané JSON, pris après la première frame : le dialog Question normalise ses réponses pendant sa construction, un instantané pris avant aurait déclaré « modifié » une fenêtre intacte. Réutilise showConfirmationDialog plutôt que d'ouvrir un second composant de confirmation. 3 clés i18n FR/EN/NL, flutter build web ✅. diff --git a/v1-plan.md b/v1-plan.md index 06ae133..0e236a7 100644 --- a/v1-plan.md +++ b/v1-plan.md @@ -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.
⚠️ **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.
⚠️ **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.
⚠️ **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.
**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`.
`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) |