K5 — les 3 flavors de mymuseum-visitapp construisent. La prémisse du lot était fausse : sa config Android était déjà à niveau, ce sont les dix crans du 12/08 qui ont amené tablet-app jusqu'à lui. Restaient deux alignements (NDK 28.2.13676358 réclamé par speech_to_text, Kotlin 2.3.10). Un cran de plus existe — Gradle 8.14.0 / AGP 8.11.1 — délibérément non pris : terrain neuf pour les deux repos, le faire ici seul les désaligne. Correction 1 — tablet-app couvre 10 des 13 types, pas 11. Le switch de main_view.getContent traite Map, Slider, Video, Web, Menu, Quiz, Pdf, Game, Agenda, Weather. SectionArticle (type 6) a son case commenté « TODO » et un article_view.dart d'1 Ko : un article rend « Ce type n'est pas supporté ». Contrairement à SectionEvent et SectionParcours, ce type existait dans Mongo — c'est un vrai retard, et il sortira de la migration avec du contenu réel. Ajouté au plan en K7. Le travail de K7 n'est pas la copie mais le passage en paysage : les écrans de mymuseum sont pensés pour un téléphone debout, une borne est large. K3 (SectionEvent) pose la même question — une seule passe de design pour les deux, sinon deux mises en page sur la même borne. Correction 2 — le travail V1 des deux apps visiteur est committé sur des branches qui ne sont pas master : Meta-Rayban-Test pour mymuseum-visitapp, AI-Assistant-test pour tablet-app. Le §5bis décrit pourtant la première comme « un POC jamais mergé ». À trancher avant K6 (publier les APK). Corollaire relevé au §5bis : l'APK se construit sans le POC dedans, les 4 erreurs Ray-Ban étant hors du graphe de main.dart. kanban.html : carte build mymuseum retirée d'Urgent (3 -> 2), done-item K5 ajouté (39 -> 40), bandeau du haut réaligné, carte périmètre kiosk corrigée. Compteurs de colonnes revérifiés un à un. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
79 KiB
V1 — plan d'implémentation jusqu'à la mise en prod
Établi le 2026-08-10 en croisant STATUS.md (§1 à §6bis), kanban.html (43 cartes) et des vérifications directes dans le code.
Ce fichier ne remplace ni STATUS.md ni le kanban : il donne l'ordre d'exécution et les liens entre chantiers, que ni l'un ni l'autre ne portent — le kanban trie par état, STATUS.md par domaine, et personne ne dit ce qui doit précéder quoi.
Règle héritée du §1ter et maintenue ici : ce qui touche au schéma part avant la bascule, ce qui est additif attend.
0. Ce que la vérification du code corrige dans les docs
Trois écarts relevés avant d'établir l'ordre. Ils déplacent des chantiers entiers.
| Ce que disent les docs | Ce que dit le code (2026-08-10) | Conséquence sur le plan |
|---|---|---|
| §4 : « Rien n'est committé depuis le 2026-07-17 », 75 + 40 + 14 + 4 fichiers en working tree | Périmé. Les 4 repos ont un commit du 2026-08-09. Reste non committé : manager-service 41 fichiers (= le lot 3 RAG du 10/08), manager-app 2, tablet-app 4, myinfomate-landing 7, visitapp-web 1 commit non poussé |
Le point reste en tête mais devient une demi-heure, pas une urgence à trois semaines de travail |
§1quater : GetReferencedResourceIds() « réclamé par le plan offline, à écrire dans la même passe » que les 13 GetEmbeddableText() |
Déjà écrit dans les 13 sous-types (Data/SubSection/*.cs) — la passe commune a bien eu lieu. Mais appelé nulle part : le switch de collecte de ConfigurationController.Export est commenté en bloc (:~400-577), 180 lignes mortes |
Le bug offline nº1, présenté comme « bloquant produit », coûte le branchement d'une méthode existante à la place d'un bloc mort. Il descend en coût, pas en priorité |
§1quater et §6bis : « reste à corriger la landing, features.reqPerMonth dit encore req/mois » |
Corrigé. La clé n'existe plus dans myinfomate-landing/src |
Un item de moins ; le kanban avait raison, STATUS.md est en retard sur deux sections |
À répercuter dans STATUS.md quand ce plan sera exécuté.
1. Les liens qui déterminent l'ordre
C'est la partie qui manque partout ailleurs. Neuf liens réels, dont quatre imposent l'ordre et cinq évitent de travailler deux fois.
Liens bloquants — l'ordre n'est pas négociable
| # | Lien | Pourquoi |
|---|---|---|
| L1 | Rotation des secrets → création de l'environnement prod (point 16 du §1quinquies) | Écrit noir sur blanc au §1quinquies : « ne pas créer la prod avec les clés committées ». Créer la prod d'abord, c'est rotationner deux fois. ⚠️ Modifié le 2026-08-11 : la rotation quitte le lot A pour le lot I (I0), mais le lien reste entier. Ce n'est pas un assouplissement du lien, c'est un déplacement des deux bouts : les 6 repos sont privés sur un Gitea auto-hébergé, donc l'exposition ne justifie pas d'ouvrir le backlog avec elle. La rotation doit toujours précéder I3. Elle redevient urgente si un repo passe public, gagne un collaborateur externe, ou est cloné par un CI tiers |
| L2 | Gel du schéma → réécriture de MigrationController |
BuildSection doit écrire dans le schéma final. Tout renommage ou unification de colonne fait après oblige à repasser dans les 13 branches du mapping. Voir §2 lot B |
| L3 | pg_dump → bascule (point 18) et → activation de visit-events-purge |
Un seul item débloque deux choses : le filet du jour J, et le job de purge des stats volontairement inactif tant que Stats:RetentionDays n'est pas défini |
| L4 | RGPD → mise en service de VisitorQuestion — repoussé en fin de backlog le 2026-08-11 (lot J) |
Ce lien reste vrai, mais il ne contraint plus l'ordre : rien ne part en prod avant que tout le backlog soit terminé (§1quater de STATUS.md). Le lot J étant le dernier avant la bascule, le volet RGPD est en place avant la première question de visiteur réelle — la journalisation ne tourne d'ici là que sur des bases de développement. La condition tient tant que cette porte tient : si un déploiement anticipé était décidé, le lot J redeviendrait bloquant |
| L19 | Portage des apps visiteur (lot K) → bascule — ajouté le 2026-08-12 | Le contrat d'API a changé avec Postgres v3 : ConfigurationDTO.roundedValue, GeoPointDTO.latitude/longitude ne sont plus servis. Les apps déployées parlent l'ancien contrat. Basculer sans avoir porté et publié les apps, c'est dégrader les clients existants le jour J — sans erreur visible, juste des cartes vides. ⚠️ Le parc des téléphones visiteurs ne se force pas : c'est ce qui impose la stratégie de coexistence du lot I plutôt qu'une bascule sèche |
Liens d'économie — même code, une seule passe
| # | Lien | Ce qu'on paie deux fois sinon |
|---|---|---|
| L5 | Backfill médias ⊂ MigrationController (écart e du §1quinquies) |
Le §1quater le range dans le lot médias, le §1quinquies dit « à faire dans la migration ». Les deux ont raison : il faut un seul calculateur de StoragePath/SizeBytes, appelé par le backfill local et par la migration. Deux implémentations divergeraient sur les 7 types URL et la ligne sans blob |
| L6 | Rename MapResourceId → IconResourceId = écart (g) du §1quinquies |
Le §6bis le classe en « nettoyage backend », le §1quinquies en écart de migration. C'est le même changement. Le faire pendant le lot B et l'écart (g) tombe tout seul |
| L7 | Port PG fermé + app.myinfomate.be + compose prod = un seul fichier |
Trois cartes distinctes du kanban (une « Urgent », une « Planifié », une dans le §1quinquies) qui touchent le même docker-compose et le même Traefik. Les faire séparément, c'est trois fenêtres de redéploiement |
| L8 | Nettoyage manager_api_new → 5 chantiers manager-app |
Compression images, exposition du watermark, écran audit log, gestion des users, Guide IA lot 4 : tous dans manager-app, tous privés de flutter analyze comme garde-fou tant que les 14 fichiers modèle orphelins produisent 68 erreurs de bruit |
| L9 | Aperçu de conversation (Guide IA lot 4) ≡ « Mode preview / cible Assistant-Persona » de todo-features | Deux specs pour le même écran, dans deux documents. Converger avant de coder, comme le note déjà le §1quater |
Liens de dépendance fonctionnelle
| # | Lien | Effet |
|---|---|---|
| L10 | Backfill SizeBytes → quota de stockage autoritaire |
Sans backfill, SizeBytes = 0 partout : un quota « autoritaire » laisserait tout passer sur l'existant. L'ordre est backfill puis contrôle, jamais l'inverse |
| L11 | §21 (test offline sur device) → priorité des 4 autres bugs offline | Le §21 est le seul moyen de savoir si « ça marche mal dans le fort » vient de là. Il conditionne l'urgence, pas l'existence, des bugs de fraîcheur / extensions / purge / échecs silencieux |
| L12 | Déploiement app.myinfomate.be → test §18 (onboarding) |
L'onboarding vend un plan Essentiel web-only. Jouer le parcours d'inscription de bout en bout sans le web déployé valide tout sauf ce que le client achète |
| L13 | Rétention 13 mois (livrée) → GetSummary en mémoire |
La fenêtre est passée de 30 jours à 13 mois : eventsQuery.ToList() charge maintenant 13 mois d'événements avant d'agréger. C'est la rétention livrée qui a rendu ce point pressant, pas un nouveau besoin |
| L14 | Bascule multi-instances → post-filtrage HNSW non éprouvé | Le RAG a tourné sur une base à une seule instance — exactement le cas où le problème de post-filtrage ne se voit pas. La bascule crée le cas de test en prod. À couvrir avant, par Testcontainers ou à défaut à la main sur deux instances locales |
| L15 | Stats de l'assistant IA ⊂ écran Guide IA | « Ce que demandent vos visiteurs » est le deuxième onglet de l'écran Guide IA (§7 du plan), et cette coquille à onglets n'existe pas encore. Deux chantiers séparés = la coquille construite deux fois |
| L16 | Design de l'onglet → job de regroupement en thèmes | Ici le design pilote le backend : la forme d'affichage détermine la forme des données agrégées. Job écrit d'abord = job réécrit |
| L17 | Socle visuel constants.dart → les 3 écrans |
11 tailles de police dans lib/Components/, pas d'échelle typo. Socle d'abord : les écrans deviennent de l'assemblage. Socle après : 3 repasses |
| L18 | §5 Aperçu de conversation → arbitrage A/B de SectionParcours | Le seul gain réel de l'option B est une colonne d'aperçu visiteur en direct — déjà couverte par le §5 du Guide IA (lui-même à converger avec le Mode preview, L9). Le gain de B tombe, A reste |
2. Les lots, dans l'ordre
Neuf lots. Le chemin critique passe par A → B → G → H ; les lots C à F sont parallélisables mais tous obligatoires avant H.
Lot A — Sauver et assainir (½ jour) — révisé le 2026-08-11
Rien de créatif, tout est un verrou pour la suite.
| Quoi | Où | Lien |
|---|---|---|
| — | ✅ Fait le 2026-08-11. Les 9 repos sont propres et synchronisés avec leur remote. manager-service 18e4240, manager-app eb8e849. ⚠️ DOCS/ est lui-même un repo git (branche main) — facile à oublier quand on énumère les repos de code, et c'est là que vivent ce plan, STATUS.md et le kanban |
|
manager-service |
📅 Déplacée au lot I (I0) le 2026-08-11 — repos privés sur Gitea auto-hébergé. L1 tient : elle précède toujours I3. Voir la note de L1 pour les conditions qui la font remonter | |
Nettoyer manager_api_new : supprimer les 14 fichiers modèle orphelins + lib/api/openApiTest.dart |
manager-app |
L8 — rend flutter analyze exploitable pour les lots D, E, F. Diagnostic fermé le 2026-08-11, détail ci-dessous |
tablet-app |
tablet-app |
➡️ Déplacé au lot K le 2026-08-12. Le diagnostic « un seul défaut, purement Gradle » était doublement faux — il a tenu tant que personne n'avait lancé un vrai build. Ce n'est pas un correctif de lot A, c'est un chantier de rattrapage. Voir le lot K |
Sécurité rapide : EnableSensitiveDataLogging coupé en prod (Startup.cs:248), backdoor #if DEBUG retirée, exceptions internes plus exposées |
manager-service |
À faire avant que la prod existe, pas après |
Pourquoi le nettoyage de manager_api_new est sans risque de build — vérifié le 2026-08-11, à ne pas re-douter :
lib/api.dart déclare 139 part pour 153 fichiers dans lib/model/. Les 14 restants portent 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 (il parcourt tout lib/), le compilateur ne le voit jamais. C'est toute l'explication des 68 erreurs de bruit — et la garantie que supprimer ces fichiers ne peut pas changer un résultat de build.
lib/api/openApiTest.dart part avec eux pour une raison différente : ce n'est pas du modèle mort, c'est le déclencheur de la génération (annotation @Openapi + build_runner, dernier run daté du 2026-05-07), importé nulle part. Le supprimer ne retire aucun code exécuté — ça verrouille la règle du projet : tant qu'il est là, un flutter pub run build_runner build lancé par réflexe régénère le client et écrase les éditions manuelles (onboarding_api.dart, le câblage d'AIApi, le mapping isGood → isCorrect).
À ne pas régénérer :
manager_api_news'édite à la main (règle du projet). Le nettoyage supprime des fichiers orphelins et le moyen de régénérer ; il ne relance rien.
Lot B — Geler le schéma (2 à 3 jours) — le point d'articulation
C'est le lot que ni STATUS.md ni le kanban n'isolent, et c'est lui qui conditionne le coût du lot G. Tant qu'un changement de modèle reste ouvert, réécrire MigrationController revient à le réécrire deux fois (L2).
| Quoi | Décision préalable | Lien |
|---|---|---|
SectionMap.MapResourceId → IconResourceId |
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 |
SectionEvent.ParcoursIdsSectionFactory: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 » |
⛔ SectionEvent.IconResourceId |
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 |
QuestionType |
✅ 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 pourMapResourceId→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
DropColumndeParcoursIds— 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 |
|---|---|---|
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 |
|
Backfill livré : POST /api/Resource/backfill-storage?dryRun=&instanceId=, SuperAdmin, dryRun à true par défaut (la migration se joue sur une base vide, celui-ci sur des lignes de production). StoragePath par ResourceStorage.PathFor, SizeBytes par HEAD. 7 tests |
⚠️ La méthode annoncée ici était inapplicable : « listing du bucket Firebase » supposait un client de stockage que le serveur n'avait pas. Le sondage passe par HEAD sur l'URL publique, comme la migration. Le sondeur est extrait, pas recopié — Helpers/ResourceSizeProbe.cs, consommé par MigrationController et le backfill (même raisonnement que L5).⚠️ L'extraction a bouché un trou : l'original ne notait l'échec que dans son catch, or un HEAD sur un blob absent ne lève pas (404 sans Content-Length). Ces ressources arrivaient à 0 octet sans figurer dans le rapport. Corrigé des deux côtés.Le « 37 sur 45 » n'était pas vérifiable : le backfill rend son propre inventaire, Orphans (pas d'URL) et Unsized (URL muette) séparés — ce sont deux causes distinctes |
|
Quota autoritaire livré : pré-vol sur les deux chemins de création, suppression du blob à Delete, et l'angle mort d'Update tranché. 8 tests, dotnet test 163/163 |
L10 respecté — après C2. ⚠️ Le pré-vol existait déjà à moitié, non documenté : Upload (multipart) contrôlait et renvoyait 413, Create (JSON) ne contrôlait rien — or c'est le chemin qu'emprunte manager-app. Le dépassement passait par la porte de service.⚠️ Et les deux lectures du quota divergeaient : Upload lisait le plan, GetQuota lisait l'instance avec le plan en repli. Une instance à quota surchargé — le mécanisme de l'add-on — affichait un chiffre et se faisait bloquer sur un autre. Fermé par Helpers/StorageQuota, seule source de vérité.✅ L'angle mort de C1 était une fausse crainte : PathFor ne produit qu'un pictures/{instanceId}/{resourceId} — le type n'entre pas dans le chemin, il décide seulement s'il y en a un. Recalculer ne peut pas pointer ailleurs. Update rejoue donc Apply |
|
| C4 | Compression images côté client, 2560 px / JPEG q82 (~12×) | manager-app. Indépendant de C2 : ne touche que les nouveaux uploads |
| C6 | Retirer la suppression de blob de manager-app — show_resource_popup.dart:130-137 |
manager-app. Depuis C3 le serveur supprime le blob avant la ligne : le client tombe désormais sur un objet déjà absent. Ce n'est plus dangereux, c'est devenu du code mort. ⚠️ Ne pas le retirer avant d'avoir renseigné Firebase:StorageBucket en prod (I9), sinon plus personne ne supprime |
| Alerte de budget GCP posée dans la console | Fait côté GCP, rien à vérifier dans le code |
Lot D — Bugs offline (1 à 2 jours, après mesure)
Commencer par le test, pas par le code (L11).
| Ordre | Quoi |
|---|---|
| D0 | Jouer le test-plan §21 sur un device — 15 cas. Sortie : lesquels des 4 bugs restants se manifestent réellement |
Fait des deux côtés, et le bloc mort était plus gros qu'annoncé : 157 lignes dans Export, plus 126 dans Import. Avant : une visite téléchargée n'embarquait que l'image de la configuration, celle du loader et l'image de chaque section — ni contenus d'articles, ni audios, ni icônes de carte, ni images de quiz. Serveur : Export matérialise les entités (pas seulement leurs DTO) et appelle GetReferencedResourceIds(language) ; Import cesse de redécouvrir les ressources section par section et crée toute la charge en une passe (createResource est idempotente). Client : downloadConfiguration.dart enregistre la charge au lieu de parser section.data type par type — ce qui répare aussi la purge, usedImageOrAudioIds pilotant la suppression des fichiers obsolètes.4 tests ajoutés, dont un qui vérifie par réflexion que les 13 sous-types implémentent la collecte : le trou d'origine venait d'un switch où un type oublié passait dans le default sans bruit. dotnet test 148/148, flutter analyze sans erreur.⚠️ Le volet visiteur n'est vérifié que par l'analyse — l'APK de mymuseum-visitapp ne compile pas (Gradle/NDK). Confirmation sur device au §21, qui reste ouvert |
|
| D2 | Filtre incrémental sur la version de la ressource, pas sa présence |
| D3 | audio/mpeg ajouté à la table d'extensions (audio/mp3 n'est pas un MIME standard) |
| D4 | Réactiver la purge des fichiers obsolètes (deleteSync() commenté) |
| D5 | Échecs de téléchargement remontés au lieu d'un print — une visite incomplète ne doit pas s'annoncer téléchargée |
Lot D-bis — Les trois chantiers de design (1,5 à 2 semaines)
État mesuré le 2026-08-10, plus ouvert que ce qu'annoncent STATUS.md et le kanban.
Écran Docs Code Guide IA « onglet Configuration livré le 07/08 » guide_ia_screen.dart: 545 l., 5 cards en colonne unique, aucunTabBar. Manquent le §4 (Sources de connaissance + recherche vectorielle) et le §5 (Aperçu de conversation)Stats assistant IA « onglet Vocal (V1) » spécifié statistics_screen.dart: 1481 l., « vocal » n'apparaît que dans un commentaire. Stats assistant à zéroSectionParcours 2 options maquettées Les 3 popups intactes. Rien n'a commencé — résolu par DB4 le 2026-08-11 : les 3 popups sont supprimées
Le problème de méthode avant le contenu. Les trois maquettes sont des artifacts hébergés sur un autre compte Claude : illisibles depuis la machine de dev (constaté le 2026-08-10, cf. le §« Comment mettre à jour ce fichier » de STATUS.md). Toute session qui reprend un de ces écrans redessine — c'est la cause directe du Guide IA livré à la moitié de sa spec.
Même découpage que le kanban : contenu versionné dans le repo, diffusion par artifact depuis le compte propriétaire. La convention existe déjà (DOCS/claude design/, 3 maquettes HTML). Les plans texte, eux, sont précis et suffisent à régénérer les maquettes.
| Ordre | Quoi | Lien |
|---|---|---|
HTML des maquettes rapatrié dans DOCS/claude design/ — guide-ia-screen.html (31,8 Ko), statistics-screen.html (27,4 Ko), sectionparcours-refonte-flux.html (45,6 Ko), JS interactif compris. Méthode : bascule temporaire sur le compte propriétaire, lecture de la source, copie. L'assistant visitapp-web n'a jamais été publié ; le kanban vit déjà dans DOCS/kanban.html |
La source de vérité du design vit désormais dans le repo, l'artifact ne sert plus qu'à la diffusion. Constat : Guide IA et Statistiques déclarent des tokens rigoureusement identiques — c'était déjà un design system, jamais porté en Dart. Et --brand #264863 est kPrimaryColor |
|
Socle visuel dans constants.dart : 11 rôles typographiques, 8 pas d'espacement, 5 rayons, paddings de carte et de page. flutter analyze propre. Décision du 2026-08-11 : l'échelle vient des maquettes, les couleurs restent celles de l'app — un écran neuf se fond dans manager-app, il n'y ouvre pas une seconde palette. Seules les valeurs sans équivalent existant sont reprises (kSurface2/3, kLine, kLineSoft, kInk3, kWarning). Pas de thème sombre. Reste : size.width * 0.2 de NumberInputContainer |
L17 — sert aussi les 12 autres types de section. La reprise des écrans existants vers ces tokens est un chantier distinct (« repasse UI globale »), pas un effet de bord de celui-ci | |
SectionParcours — plus de perte silencieuse. Confirmation avant d'abandonner du travail non enregistré, sur les trois niveaux de dialogue (Parcours, Étape, Question — chacun perdait tout ce qui était saisi sous lui), plus interception du retour navigateur par PopScope. 3 clés i18n FR/EN/NL. flutter build web ✅ |
Arbitrage du 2026-08-11 : DB2 se limite au garde-fou, la persistance incrémentale part avec DB4. Motif : les deux sont alternatives, pas cumulables — si chaque étape est persistée à la saisie, le garde-fou n'a plus d'objet. Et la persistance impose de créer le parcours dès la 1ʳᵉ étape (donc des parcours à moitié remplis en base si l'utilisateur renonce, et un « Annuler » qui n'annule plus rien) : c'est précisément la sémantique qu'apporte le rail d'étapes de l'option A. L'écrire maintenant, c'est la câbler dans les callbacks onSave que DB4 supprime |
|
| DB3 | Écran Guide IA complet, 2 onglets, stats assistant comprises. 🔨 Front livré le 2026-08-11 : coquille à onglets, §5 Aperçu de conversation branché sur le vrai POST /api/AI/chat (AIApi était dans le client généré mais jamais exposé dans client.dart — câblé), onglet « Ce que demandent vos visiteurs » avec ses tuiles, son bloc ambre des questions sans réponse et ses trois cartes de barres, 24 clés i18n FR/EN/NL. flutter analyze propre, flutter build web ✅.Complété le 2026-08-11 : carte §4 « Ce que connaît votre guide » sur GET /api/Ai/knowledge/{id} (agrégats ContentEmbedding), journalisation VisitorQuestion dans AiController.Chat, et GET /api/Ai/insights/{id} qui remplit l'onglet. dotnet test 130/130, flutter build web ✅.Reste dans ce lot : l'onglet « Vocal » des stats et la vérification à l'œil (DB5). Le job de thèmes et tout le volet RGPD sont déplacés au lot J, en fin de backlog. La purge à 90 jours est livrée ( VisitorQuestionPurgeService) |
L15, L16 — le contrat de données est fixé par GuideIaInsights dans visitor_questions_tab.dart, et GuideInsightsDTO en est le miroir exact : c'est lui que le job de thèmes doit remplir, pas l'inverse. L9 : le §5 converge avec le Mode preview |
SectionParcours — la forme, option A livrée. Les trois showNewOrUpdate… sont supprimés. Parcours/Fields/ porte les trois widgets autonomes (ParcoursFields, EtapeFields, QuestionFields) ; guided_path_editor.dart est la coquille : fil d'Ariane, rail d'étapes réordonnable toujours visible, panneau de détail, pied de page « Enregistré ». Une question se déplie dans le panneau de son étape — profondeur maximale 2, la fenêtre puis la traduction. Sauvegarde à la saisie : guided_path_api.dart unifie sectionParcoursApi / sectionMapApi, un débounce de 700 ms écrit le parcours (PUT), les étapes (POST/PUT/DELETE) et leurs questions ; les changements de structure (ajout, suppression, réordonnancement) partent sans attendre. Le garde-fou de DB2 est retiré — 3 clés i18n supprimées, 14 ajoutées FR/EN/NL. flutter build web ✅ |
L18 — A retenue. Deux pièges du backend, tranchés à la lecture du contrôleur : UpdateGuidedPath supprime les étapes absentes du DTO, donc le payload n'emporte que celles qui ont déjà un id — les autres attendent leur CreateGuidedStep et seraient dupliquées. Et il n'y a pas d'endpoint QuizQuestion (voulu) : l'id entier attribué par SyncQuizQuestions est récupéré après coup par order, seul repère stable entre la liste locale et celle du serveur. Le parcours est créé à la première modification, pas à l'ouverture : fermer une fenêtre neuve intacte ne laisse rien en base. Si une écriture échoue, la fenêtre refuse de se fermer et le pied de page porte un « Réessayer » |
|
| DB5 | Boucle de vérification, à chaque écran : flutter run -d chrome, capture, comparaison côte à côte avec la maquette |
flutter analyze ne prouve rien sur du design — et reste inexploitable tant que le lot A n'a pas nettoyé manager_api_new (L8) |
Ne change pas : la popup de traduction (un niveau justifié, langues verticales, pas d'onglets), et la conception des composants de Components/ — leur problème est le dimensionnement, pas la structure.
Lot E — Parité et finitions visiteur (1 à 2 jours)
| Quoi | Réf. |
|---|---|
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 ✅ |
|
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) |
|
QuestionType par entier brut |
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 |
| 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)
| Quoi | Lien |
|---|---|
| Déplacé au lot J, fin de backlog (décidé le 2026-08-11) | |
VisitorQuestion |
✅ Livrés le 2026-08-11 |
| Déplacé au lot J (J2) — il conditionne la promesse d'agrégats des CGU | |
| Aperçu de conversation | L9 — converger avec « Mode preview / cible Assistant-Persona » |
| Onglet « Vocal » dans les stats | |
GetSummary agrégé en SQL au lieu de eventsQuery.ToList() |
L13 — devenu pressant par la rétention 13 mois |
🔨 Front livré le 2026-08-12, manager-app seul. Écran Screens/Audit/ sur GET /api/Audit (filtres instance / type / utilisateur / dates, pagination 50, détail avant-après), entrée de menu menuId: 13 réservée à role.value == 0. Compteur « X / 5 utilisateurs » et bouton d'ajout désactivé au plafond, SuperAdmin exclu (sa liste couvre toutes les instances). 40 clés i18n FR/EN/NL, flutter build web ✅. manager_api_new intact : http.get direct au Bearer, comme le Guide IA — une lecture seule ne justifie pas d'étendre un client qui s'édite à la main.⚠️ Deux dettes backend ouvertes par ce chantier, volontairement non traitées (voir les deux lignes suivantes) |
|
| Plafond de 5 utilisateurs côté serveur | ❌ UserController.CreateUser ne compte rien, pas de 422. Ce qui est livré côté front est un garde-fou d'interface : un POST direct sur l'API passe toujours. Le plafond n'est pas non plus dans SubscriptionPlan (5 champs, aucun sur les utilisateurs) — s'il devait varier par plan, c'est une colonne, donc un changement de schéma après le gel du lot B. Le retour d'erreur du POST est déjà branché côté front : le 422 sera visible sans retouche |
| Aucune section n'est journalisée | ❌ AuditedTypes.Contains(entry.Entity.GetType()) (MyInfoMateDbContext.cs:126) exige l'égalité exacte, or Section est abstraite (Section.cs:17) : le type runtime est toujours SectionMap, SectionQuiz… donc typeof(Section) ne matche jamais. Le journal couvre Resource/Configuration/Device/User/Instance mais pas le contenu — précisément ce que l'écran devait tracer. Correctif : Any(t => t.IsInstanceOfType(...)). ⚠️ EntityType portera alors SectionMap et non Section : soit normaliser côté serveur, soit élargir la liste du filtre front aux 13 sous-types |
Restes non chiffrés : ratio jetons → questions (/1000) calé sur du réel, endpoint de mise à jour d'ApplicationInstance pour rendre les canaux activables, quotas seed 5M/20M ajustés |
§1quater |
| Rate limiting sur les API Keys, Stripe Customer Portal | §1 « À faire » |
Tests du vector store via Testcontainers.PostgreSql sur Dockerfile.postgres |
L14 — au minimum, éprouver le post-filtrage HNSW à deux instances avant la bascule |
Lot G — MigrationController à niveau + dry run (3 à 4 jours) — le plus gros
Les 8 écarts a→h du §1quinquies. Ne démarre qu'après les lots B et C1 (L2, L5).
| Écart | Quoi | Attention |
|---|---|---|
Instance : 4 champs migrés sur ~35 — requalifié : ce n'était pas un oubli de mapping. OldInstance et l'export réel ne contiennent que _id, Name, DateCreation, PinCode. Les 31 autres colonnes n'existent pas dans la source : elles sont nées avec Postgres v3. Il n'y a donc rien à mapper, il faut générer, dériver ou décider. PublicApiKey → générer · WebSlug → dériver du Name (SlugHelper existe) · IsMobile/IsTablet/IsWeb → dériver · SubscriptionPlanId → décidé.Livré : PublicApiKey générée avec le même schéma que l'inscription self-service (ap_ + 32 octets cryptographiques) · WebSlug via SlugHelper.GenerateUniqueSlug (accents et collisions déjà gérés) · IsTablet/IsMobile dérivés des configurations Mongo, comme le fait déjà MigrateApplicationInstancesAsync ; IsWeb reste faux, le canal web n'existait pas · plan affecté par nom, un nom inconnu retombant sur Pro, jamais sur un plan qui offrirait l'IA · IsAssistant allumé seulement si le plan donne des jetons.⚠️ ApplyPlanQuotas est dupliqué depuis InstanceController plutôt qu'appelé : la migration ne doit pas casser si ce contrôleur change de forme. Les deux doivent rester alignées |
Le plus dangereux : aucune erreur levée. PublicApiKey null = les apps visiteur ne s'authentifient plus ; WebSlug null = visitapp-web injoignable ; SubscriptionPlanId null = quotas à 0, donc AiTokensPerMonth = 0, donc rien ne s'indexe et l'IA tourne sans compteur. Réutiliser ApplyPlanQuotas |
|
⛔ Fausse alerte — vérifiée dans l'export le 2026-08-11. BuildSection couvre exactement les types que Mongo contient. L'enum va de Map=0 à Weather=10, puis Event=11 et Parcours=12 ; or TabletDb.Sections.json (327 sections) ne contient que les valeurs 0 à 10. SectionEvent et SectionParcours n'existaient pas dans Mongo — ce sont des types nés avec Postgres v3. Le default est un filet, pas un trou |
Le kanban parlait de « perte de données » et du « scénario carnaval » : ce scénario est du contenu à créer dans Postgres, pas du contenu à migrer. Vérifié aussi : le renommage SectionPuzzle → SectionGame est déjà traité — la branche Game parse OldPuzzleDTO et pose GameType = GameTypes.Puzzle, et la valeur 8 n'a pas bougé |
|
| Collections filles jamais remplies | Vrai, et mesuré : QuizQuestions = new() jetait les questions de 5 sections quiz, soit 41 questions, réponses comprises. Migrées. Le type de validation n'existant pas dans l'ancien modèle (tout y était à choix multiples), elles arrivent en MultipleChoice plutôt qu'au défaut Simple. En revanche EventAgendas = new() et l'absence de GuidedPath sont corrects : les agendas Mongo ne portent que mapProvider et resourceIds, aucun événement, et SectionParcours est né avec Postgres |
|
Role = UserRole.ContentEditor en dur |
Vrai, et pire que décrit : Mongo n'a pas de champ Role, donc les 10 utilisateurs y avaient un accès complet. Le défaut les dégradait tous en silence — plus personne n'aurait pu gérer les utilisateurs de son instance. Passé à InstanceAdmin, ce qui préserve leurs droits d'hier et correspond au rôle que l'inscription self-service donne au premier utilisateur. ⚠️ Aucun SuperAdmin n'est créé par la migration : à poser à la main |
|
Colonnes Resource non renseignées, SizeBytes déduit d'un HEAD avalé |
L5 fermé : passe par ResourceStorage.Apply, le même calculateur que la création et le backfill — StoragePath n'était pas écrit du tout. Et l'échec du HEAD, jusqu'ici silencieux, remonte dans le rapport : une ressource à 0 octet que le quota compte pour rien, ça se sait |
|
⛔ Fausse alerte — vérifiée dans l'export le 2026-08-11. Ces trois colonnes n'existent pas dans Mongo : les forcer à false est le bon défaut, pas une perte. Contrôle inverse fait aussi — les cinq réglages qui, eux, existent dans l'export (IsDate, IsHour, IsSectionImageBackground, RoundedValue, ScreenPercentageSectionsMainPage) sont bien mappés, sur AppConfigurationLink |
Rien à corriger | |
| g | MapResourceId = dto?.iconResourceId |
L6 — tombe avec le lot B |
Pas de transaction globale, catch qui renvoie 200 OK |
Transaction unique sur les neuf étapes : soit tout est là, soit rien. Et l'échec fatal renvoie désormais 500 — le piège était qu'un script de bascule testant le code HTTP concluait au succès, l'erreur n'étant que dans le corps. En dryRun, pas de transaction ouverte : rien n'est écrit |
Puis : dry run (dryRun=true) et comparaison des comptages par entité. C'est la seule preuve que les écarts sont fermés.
🔨 Joué le 2026-08-11 sur l'export du 1ᵉʳ avril. Automatisé en test (MigrationDryRunTests), qui se saute si aucun Mongo n'écoute sur MIGRATION_TEST_MONGO (défaut mongodb://localhost:27018) — la suite reste donc verte sans dépendance. dotnet test 144/144.
⚠️ MigrationController lit un MongoDB vivant, pas les fichiers de migration-data/. Ces JSON sont un export : il faut les importer dans un Mongo avant tout dry run. Recette (rien à installer sur l'hôte) :
docker run -d --name myim_mongo_dryrun -p 27018:27017 mongo:6
# monter ailleurs que sous /data : l'image mongo y déclare déjà des volumes
for c in Instances Users Configurations Resources Sections Devices; do
docker run --rm --network container:myim_mongo_dryrun -v "$PWD/migration-data:/import:ro" mongo:6 mongoimport --uri mongodb://localhost:27017 --db TabletDb --collection $c --file /import/TabletDb.$c.json --jsonArray --drop
done
dotnet test --filter MigrationDryRunTests
Résultat, et ce qu'il a trouvé du premier coup :
| Entité | Mongo | Migré |
|---|---|---|
| Instances | 4 | 4 |
| Users | 10 | 10 |
| Configurations | 15 | 15 |
| Resources | 2374 | 2374 |
| Sections | 327 | 307 + 20 signalées |
| Devices | 46 | 46 |
20 sections manquaient, avec Erreurs : 0 — la perte silencieuse même. Diagnostic : elles référencent deux configurations qui n'existent plus dans Mongo (6863c9af…, 6863cac9…) — 19 articles et un slider, du contenu MDLF (« Senteur : fraises », « Pupitre vigne », « Ruche 7 »). Ce n'est pas un défaut de la migration : les sections sont collectées configuration par configuration, donc les orphelines n'étaient jamais énumérées, et la FK ConfigurationId les refuserait de toute façon. Le défaut était le silence. Elles partent désormais dans Skipped, et le test affirme l'invariant qui compte : migrées + signalées = Mongo.
⚠️ Décision produit en attente : ces 20 sections sont du contenu réel qui n'arrivera pas en Postgres. Soit on recrée les 2 configurations manquantes dans Mongo avant la bascule pour les récupérer, soit on acte leur perte. À trancher avec MDLF, pas en interne.
⚠️ L'export date du 1ᵉʳ avril, la prod tourne encore sur Mongo : ce dry run valide la mécanique, pas les comptages du jour J. À rejouer sur un mongodump frais juste avant la bascule — de préférence avec un utilisateur Mongo readAnyDatabase créé pour l'occasion, plutôt qu'en pointant l'app de dev sur la prod : les six DatabaseService exposent InsertOne/ReplaceOne/DeleteOne, et dryRun ne protège que Postgres.
Lot H — Tests end-to-end (1 semaine)
Ordre du §2 de STATUS.md, corrigé par L12.
| Ordre | Quoi | Note |
|---|---|---|
| H1 | §0 — porte d'entrée build, 5 commandes | Possible seulement après le lot A (tablet-app) |
| H2 | §8bis / §8ter / §8quater — écran Statistiques, export PDF, rétention | Jamais ouvert dans un navigateur. Sur le PDF : accents, et logo — s'il manque, c'est le CORS du bucket Firebase, le rapport doit se générer quand même |
| H3 | §19 — parité par type de section, puis §19.13 cas E (chasse au trésor / carnaval) | Le cas E valide SectionMap + SectionParcours + géodéclenchement + questions + mode jeu d'un coup. Le cas 0 valide les 4 correctifs du 09/08 |
| H4 | §18 — onboarding self-service | L12 — après le déploiement web du lot I. Prérequis de config : alias onboarding@myinfomate.be et whsec_ de la Stripe CLI |
| H5 | §1-17 par lot, §16 non-régression |
Lot K — Apps visiteur à niveau pour la bascule (3 à 5 jours) — ajouté le 2026-08-12
Ce lot n'existait pas, et il est bloquant. Il est né d'un vrai build de
tablet-app, le premier depuis des mois : le diagnostic des docs (« un seul défaut, purement Gradle, peut-être déjà réglé par5a6701d») ne tenait que parce que personne n'avait lancé la commande.⚠️ Le point qui fait de ce lot un bloquant de la bascule, et que le plan ne portait nulle part : le contrat d'API a changé, les apps déployées parlent l'ancien.
ConfigurationDTO.roundedValueetGeoPointDTO.latitude/longitudene sont plus servis par Postgres v3. Une tablette en service chez MDLF ou au Fort ne s'arrêterait pas proprement le jour J — elle afficherait des cartes sans points et des écrans dégradés. Voir L19.
✅ tablet-app construit — flutter build apk --debug vert le 2026-08-12, APK produit pour la première fois depuis le 16 avril. K1, K2 et K4 sont livrés et prouvés par un build, pas seulement par flutter analyze. L'APK passe de 174 à 248 Mo : effet attendu du cran 3 (bibliothèques natives non compressées).
K1 — Outillage Android tablet-app, 2026-08-12. Dix crans, chacun dicté par l'erreur du précédent — à ne pas re-découvrir un par un. ⚠️ Le compte de « six crans » écrit plus tôt dans la journée était provisoire : il datait du moment où la chaîne Gradle passait, avant que la compilation Kotlin ne révèle la suite.
| # | Changement | Ce qu'il débloquait |
|---|---|---|
| 1 | Gradle 7.5 → 8.11.1 | le plugin Kotlin exigeait ≥ 7.6.3 |
| 2 | AGP 7.2.0 → 8.5.0, Kotlin 1.9.0 → 2.0.10 | onVariants$default : Kotlin 1.9 appelle une API absente d'AGP 7.2 |
| 3 | android.bundle.enableUncompressedNativeLibs retirée |
option supprimée en AGP 8.1. Valait false, le défaut est true : les .so ne sont plus compressés dans l'AAB |
| 4 | AGP → 8.7.3 | minimum exigé par Flutter (8.6.0) |
| 5 | AGP → 8.9.1 | réclamé par androidx.browser:1.9.0 et androidx.core:core-ktx:1.17.0, tirés par les plugins |
| 6 | jcenter() → mavenCentral() |
jcenter est arrêté depuis 2021 |
| 7 | jvmargs 1536M → 4096M |
Java heap space dans JetifyTransform sur les jars Flutter |
| 8 | android.enableJetifier true → false |
Jetifier ne sert plus à rien depuis qu'AndroidX est partout, et c'est lui qui saturait la heap |
| 9 | resolutionStrategy sur androidx.lifecycle retiré |
⚠️ Le plus instructif : il forçait la version 2.4.0 avec le commentaire « To fix mapbox issue ». Il réglait un problème mapbox d'il y a trois ans et causait celui d'aujourd'hui — mapbox_maps_flutter 2.21.1 appelle setViewTreeLifecycleOwner, absent de 2.4.0, et sa compilation Kotlin échouait. Ne pas le remettre : un commentaire le dit dans android/build.gradle |
| 10 | Kotlin 2.0.10 → 2.3.10 | les dépendances tirent kotlin-stdlib 2.3.10, métadonnées binaires incompatibles avec un compilateur 2.0. Satisfait au passage l'exigence Flutter ≥ 2.2.20, annoncée comme prochaine rupture |
Les crans 7, 8 et 9 sont des alignements sur mymuseum-visitapp, qui utilise le même plugin mapbox et ne rencontre aucun de ces problèmes : -Xmx4096M, enableJetifier=false, aucun forçage de lifecycle. Réflexe à garder pour K5 : avant de chercher, comparer les deux gradle.properties et les deux build.gradle.
⚠️ Un bloc buildscript entièrement commenté dans android/build.gradle déclarait AGP 8.5.0 / Kotlin 2.0.10 alors que la config appliquée vivait dans settings.gradle (AGP 7.2.0 / Kotlin 1.9.0). Il a fait conclure deux fois à une configuration qui n'était pas celle du build — il est supprimé, les versions ne sont plus déclarées qu'à un seul endroit. C'est le troisième piège « bloc commenté » du projet, après les deux pistes Dart écartées le 11/08.
⚠️ Dette d'outillage restante, non bloquante, à traiter en V2 : Flutter annonce l'abandon de Kotlin < 2.2.20, et 8 plugins appliquent le Kotlin Gradle Plugin (mapbox_maps_flutter, video_player_android, webview_flutter_android, device_info_plus, fluttertoast, package_info_plus, shared_preferences_android, wakelock_plus) — les futures versions de Flutter refuseront de construire dans ce cas. compileSdkVersion 36 n'est pas négociable : sans lui, plus de mise à jour publiable sur le store.
| # | Quoi | Note |
|---|---|---|
Portage livré. Les 132 erreurs de contrat sont tombées ; flutter analyze lib ne renvoie plus que les 6 erreurs PuzzleDTO, qui sont K4. Fait : TabletAppContext.currentAppConfigurationLink (copie du getter mymuseum), lib/Helpers/geo.dart avec l'extension lat/lng, 67 accès roundedValue rebranchés, main_view et les 4 vues carte portées.⚠️ Deux bugs latents trouvés — c'est ce qui justifiait de le faire à la main. (1) applicationInstanceDTO n'était assigné que si instanceDTO.isAssistant == true (main_view:339) : il servait de drapeau d'assistant. Ce DTO portant désormais les AppConfigurationLink, le getter aurait renvoyé null sur toute instance sans IA — donc MDLF et le Fort — et les cinq réglages seraient retombés sur leurs défauts, sans erreur ni log. Corrigé : assignation inconditionnelle + drapeau explicite isAssistantEnabled, qui préserve le ET instance × canal (les deux gardes de AiController:315/360).(2) ConfigurationDTO.isTablet a disparu : le filtre « configurations pour tablette » de getConfigurations n'avait plus de source. Il porte désormais sur les AppConfigurationLink du canal AppType.Tablet, même appel que main_view. ⚠️ À valider sur device (§0/§18) : si un client a des configurations non rattachées à son canal tablette, l'écran de sélection sera vide. L'ancien code avait le même angle mort, mais la donnée qui pilote le filtre a changé.⚠️ Piège de vérification : flutter analyze écrit error - , pas error • — un grep sur la mauvaise forme a fait conclure « 0 erreur » alors qu'il en restait 10 |
||
⚠️ Deux conventions de coordonnées coexistent dans le projet — GeoPointDTO en [lng, lat], géométries de GuidedStep en [lat, lng]. Une confusion ne lève aucune erreur, elle déplace les points. Détail et tableau dans STATUS.md §1sexies. Fondations déjà posées le 2026-08-12 : TabletAppContext.currentAppConfigurationLink et lib/Helpers/geo.dart.Portage du contrat d'API — 132 erreurs Dart, 3 causes : ConfigurationDTO.roundedValue/isDate/isHour/isSectionImageBackground/screenPercentageSectionsMainPage ont migré vers AppConfigurationLinkDTO (73 err.) · GeoPointDTO.latitude/longitude → GeometryDTO? geometry, effet de PostGIS (40 err.) · les min() sur dynamic en découlent (19 err.) |
Le modèle est déjà écrit : mymuseum-visitapp a fait ce portage (visitAppContext.currentAppConfigurationLink?.roundedValue, Models/visitContext.dart). Copier, ne pas concevoir. Les 40 sites latitude/longitude se traitent par une extension Dart sur GeoPointDTO plutôt qu'un à un |
|
| K3 | Périmètre kiosk — tranché le 2026-08-12. ⛔ tablet-app couvre 10 des 13 types, pas 11 — recompté dans le switch le 2026-08-12. main_view.getContent traite Map, Slider, Video, Web, Menu, Quiz, Pdf, Game, Agenda, Weather. SectionArticle (type 6) manque : son case est commenté « TODO » et Screens/Article/article_view.dart fait 1 Ko — un article tombe dans le default, « Ce type n'est pas supporté ». Ce type-là existait dans Mongo, contrairement aux deux ci-dessous : c'est un vrai retard, à trancher (le supporter ou l'assumer) et non à compter comme couvert. Énoncé d'origine, qui reste valable : tablet-app couvre 11 des 13 types. SectionParcours : exclu définitivement — un parcours guidé fait marcher le visiteur avec géodéclenchement, ça n'a aucun sens sur borne fixe. SectionEvent : à supporter — une borne d'accueil qui affiche le programme du jour et le plan est le cas d'usage kiosk par excellence, et c'est le scénario du §19.13 cas E |
Ni l'un ni l'autre n'est un retard : les deux types sont nés avec Postgres v3, ils n'ont jamais existé dans Mongo (cf. lot G, écart a) |
Écran Game repris de mymuseum. Les 6 fichiers de Screens/Sections/Game/ sont dans tablet-app/lib/Screens/Game/, contexte adapté ; lib/Screens/Puzzle/ supprimé ; main_view bascule sur SectionType.Game / GameDTO / GamePage. Gains : le puzzle glissant (mélange par mouvements valides, donc toujours soluble), le bouton d'indice, et un dimensionnement qui respecte le ratio de l'image — 508 lignes contre 237 à l'ancien puzzle_view.⚠️ Une décision de rendu à valider à l'œil : mymuseum code ses couleurs en dur par flavor client ( kMainColor0/1/2 — rouge MDLF, bleu Fort). tablet-app n'a pas de flavor : un seul APK sert tous les clients et la charte vient de la configuration choisie au pincode. Recopier ces constantes aurait affiché du bleu chez MDLF. Le dégradé et le bouton d'indice sont donc dérivés de configuration.primaryColor (même parsing que audio_player/loading_common, repli kTestSecondColor, 3 nuances par Color.lerp). Plus juste sur le fond, mais le rendu diffère de la version mobile : si le dégradé dessiné à la main est préféré, figer les trois couleurs |
||
SectionPuzzle → SectionGame + le type glissant. Le serveur a renommé et porte GameTypes { Puzzle, SlidingPuzzle } ; tablet-app a encore un écran Puzzle/ qui ne connaît que l'ancien. Reprendre le code de mymuseum-visitapp/lib/Screens/Sections/Game/ — game_page.dart et sliding_puzzle_piece.dart existent déjà là-bas, et cette version est moins buguée que celle de tablet-app |
Récupération, pas réécriture | |
Les trois flavors de mymuseum-visitapp construisent (dev, mdlf, fortsaintheribert), exit 0, APK à l'appui. D0 est débloqué.⛔ L'énoncé était faux : il n'y avait pas de « même rattrapage » à faire. Sa config Android était déjà à niveau — Gradle 8.11.1, AGP 8.9.0, -Xmx4096M, enableJetifier=false, aucun forçage d'androidx.lifecycle. Les dix crans de K1 ont amené tablet-app jusqu'à mymuseum ; le lot supposait la symétrie du problème, elle n'existait pas. Le réflexe du diff annoncé en K1 a donné la réponse en deux cat.Ce qui restait, et qui est fait : les deux avertissements du build. NDK 27.0.12077973 → 28.2.13676358 ( speech_to_text le réclame nommément) et Kotlin 2.1.0 → 2.3.10 (seuil 2.2.20 annoncé comme rupture ; c'est la version de tablet-app, donc un alignement). Six builds — trois avant, trois après. APK de 382 à 349 Mo.⚠️ Un cran de plus existe, délibérément non pris : Kotlin réglé, Flutter réclame Gradle 8.14.0 et AGP 8.11.1. C'est du terrain neuf pour les deux repos ( tablet-app est à 8.11.1/8.9.1, mêmes avertissements latents) — le faire ici seul désaligne les deux apps. À faire d'un bloc sur les deux, ou pas du tout : c'est la dette d'outillage déjà parquée en V2 |
Son code était déjà porté sur le nouveau contrat — c'est ce qui aurait dû mettre la puce à l'oreille : le repo qui a servi de modèle au portage K2 n'avait pas de raison d'être en retard d'outillage sur celui qui le copiait | |
| K7 | SectionArticle sur le kiosk — ajouté le 2026-08-12, en recomptant les types couverts. Son case est commenté « TODO » dans main_view.getContent et Screens/Article/article_view.dart fait 1 Ko : un article rend « Ce type n'est pas supporté ». Contrairement à SectionEvent et SectionParcours, ce type existait dans Mongo — c'est donc un vrai retard, pas un type né avec Postgres v3, et il sortira de la migration avec du contenu réel.Le code est une reprise de mymuseum-visitapp/lib/Screens/Sections/Article/, pas une conception. Le travail n'est pas là : il est dans le passage en paysage. Les écrans de mymuseum sont pensés pour un téléphone tenu debout ; une borne est large, et un article rendu en colonne unique sur 1280 px de large est illisible. C'est le seul endroit du lot K où il y a une décision de mise en page à prendre, pas une copie à faire — d'où la maquette ci-dessous |
À traiter avec K3 : SectionEvent pose exactement la même question de paysage (héros, programme, carte), et la trancher deux fois donnerait deux mises en page différentes sur la même borne. Une seule passe de design pour les deux, même raisonnement d'économie que L5 et L15 |
| K6 | Publier les APK à jour, puis vérifier le renouvellement du parc | L19. Les tablettes kiosk sont des appareils que tu gères — mise à jour maîtrisée. Les téléphones des visiteurs, non : aucune manœuvre technique ne force une mise à jour, d'où la stratégie de coexistence du lot I |
Lot J — RGPD & conformité (2 à 3 jours) — juste avant le lot I
Placé en fin de backlog le 2026-08-11, à la demande. C'est tenable pour une seule raison : la porte « rien en prod avant la fin du backlog ». La journalisation des questions ne tourne d'ici là que sur des bases de développement, donc aucun visiteur réel n'est concerné. Si cette porte saute, ce lot remonte devant.
Les CGU §8 ont été réécrites le 2026-08-11 (cgu-myinfomate.md) et le texte d'information visiteurs existe (mention-information-visiteurs.md). Ce qui reste :
| # | Quoi | Note |
|---|---|---|
| J1 | Table d'agrégats de thèmes | Les CGU promettent que les regroupements survivent à la purge. Or ThemeId est une colonne de VisitorQuestion : la purge du 90ᵉ jour l'emporte avec la ligne. Sans cette table, la promesse est vide et le client perd son historique au 91ᵉ jour |
| J2 | Job de regroupement en thèmes | Sans lui ThemeId reste nul, donc J1 ne contiendrait rien. Il remplit topics dans GuideInsightsDTO — forme déjà arrêtée par l'écran |
| J3 | Interrupteur de collecte par instance | ⚠️ Le client est responsable de traitement mais n'a aujourd'hui aucun moyen de refuser la collecte : elle est inconditionnelle dès que l'assistant est actif. Drapeau sur Instance + garde dans RecordVisitorQuestion |
| J4 | Afficher la mention aux visiteurs | Le texte existe, il n'est branché ni dans visitapp-web ni dans mymuseum-visitapp. Un lien depuis l'assistant suffit |
| J5 | Faire relire le §8 des CGU | Document contractuel, rédigé côté produit et non validé juridiquement. Priorité au §8.6 (sous-traitants) et au §8.5 (transfert hors UE) |
| J6 | Vérifier les conditions réelles de Google | Le §8.5 dit « peut impliquer un transfert hors UE » — prudent mais vague. Savoir si l'API Gemini utilisée offre une résidence européenne, et si un DPA est signé. La réponse réécrit le §8.5 |
| J7 | Décider si un DPA séparé est requis | L'article 28 exige un acte écrit. Le §8 en tient partiellement lieu ; une commune ou un musée subsidié en demandera un en annexe |
Un point qui n'est pas un problème : aucune demande d'effacement individuelle n'est exécutable, puisque rien n'est rattachable à une personne. C'est acceptable — la protection tient à l'absence d'identifiant plus la purge, pas à une procédure de droits qui ne pourrait pas s'appliquer.
Lot I — Infra prod et bascule (2 à 3 jours + la fenêtre)
| # | Quoi | Lien |
|---|---|---|
| I0 | Rotation des secrets : JWT signing key, clés Gemini/OpenWeather, connection strings, mot de passe MQTT, token Telegram → variables d'environnement, purge de l'historique Git, suppression de RELEASE/.⚠️ Inventaire élargi le 2026-08-12 : les apps Flutter en portent aussi. Trouvé en réparant le build tablet-app : SDK_REGISTRY_TOKEN=sk.… — un token MapBox secret (préfixe sk., pas pk.), au nom du compte perso, en clair dans tablet-app/android/gradle.properties, versionné. Il sert à télécharger le SDK MapBox au build, donc le retirer casse le build tant qu'il n'est pas passé en variable d'environnement — à faire dans la même passe, pas avant. Passer les 3 repos Flutter au peigne : l'inventaire d'origine ne couvrait que manager-service, on aurait rotationné la moitié des secrets en croyant le sujet clos |
L1 — déplacée du lot A le 2026-08-11. Doit précéder I3 : la prod ne doit pas naître avec les clés de l'historique, sinon il faut rotationner deux fois. C'est le premier geste du lot I, pas une option |
| I1 | pg_dump quotidien en cron, en complément du snapshot OVH |
L3 — puis activer Stats__RetentionDays=395 et vérifier que visit-events-purge ne supprime rien |
| I2 | Backup Mongo automatique en cron (dump + docker cp vers l'hôte) |
Filet du rollback |
| I3 | Un seul chantier compose/Traefik : Dockerfile.postgres épinglé par digest, port 5432 fermé, entrée app.myinfomate.be + Dockerfile visitapp-web |
L7 — trois cartes, un fichier. L1 : après la rotation des secrets |
| I4 | dotnet ef database update sur la base vide, puis vérifier postgis et vector présents |
Une base en retard d'une migration fait planter l'API dès le login — c'est arrivé en local le 09/08 |
| I9 | Renseigner Firebase:StorageBucket (<project-id>.appspot.com) dans la config de prod, puis lancer le backfill C2 : POST /api/Resource/backfill-storage?dryRun=true d'abord, relire Orphans/Unsized, puis dryRun=false |
⚠️ Deux conséquences si on l'oublie. Le clé est vide par défaut : sans elle, Delete ne supprime aucun blob — il ne casse rien, il ne nettoie pas, et C6 (retrait du code client) deviendrait une perte de fonction. Et sans le backfill, le quota de C3 porte sur des SizeBytes à 0 pour tout l'existant : il laisse tout passer (L10).⚠️ Piège de build repéré le 2026-08-12 : dotnet restore échoue (401) si la source NuGet git.dev-espaces-naturels.lu est déclarée dans l'image — elle est sans rapport avec ce projet. À vérifier dans le Dockerfile avant de rebuilder, le nouveau Google.Cloud.Storage.V1 étant le premier paquet ajouté depuis longtemps |
| I5 | Plan de retour arrière écrit, avant le jour J | « Un rollback qu'on improvise à 23 h n'est pas un rollback » |
| I6 | pg_dump avant, puis rejouer instance par instance (instanceId=…), en commençant par la plus petite |
Plans — décisions du 2026-08-11, à ne pas re-trancher.
1. Les lignes en base sont alignées sur la grille de tarifs (migration AlignSubscriptionPlansWithPricing, appliquée). La base semait Starter / Standard / Premium / Essentiel : noms et quotas désalignés. Corrigé :
| Id | Nom | Stockage | Jetons IA | Stats | Rétention | Avancées |
|---|---|---|---|---|---|---|
plan-essentiel |
Essentiel | 1 GB | 0 (non inclus) | ✓ | 30 j | ✗ |
plan-pro |
Pro | 15 GB | 0 (non inclus) | ✓ | 30 j | ✗ |
plan-premium |
Premium | 50 GB | 20 M (~2 000 req) | ✓ | 395 j | ✓ |
plan-enterprise |
Enterprise | 0 = illimité | long.MaxValue |
✓ | 395 j | ✓ |
plan-starter supprimé (aucun équivalent commercial, et piège à 0 jeton). plan-standard → plan-pro, avec repointage SQL des Instances avant la suppression : EF avait scaffoldé les DeleteData en tête, ce qui aurait buté sur la clé étrangère ou laissé des instances sans plan, donc à quotas nuls.
⚠️ Sémantique du 0, asymétrique et volontaire : pour le stockage 0 = illimité, pour les jetons 0 = pas d'IA. D'où la sentinelle long.MaxValue sur Enterprise — un 0 l'aurait privé d'assistant. Ne pas « harmoniser » sans repasser dans CheckQuota, AiController et SectionIndexingInterceptor.
⚠️ Le plan ne porte que 5 champs. « App native », « offline + beacons », « push », « traduction automatique » sont dans la grille commerciale mais pas dans la table : ils se règlent par instance. La base ne les applique pas, l'affectation le fait.
2. Add-on IA sur Essentiel. L'IA n'est pas incluse dans Essentiel ni Pro, mais se vend en add-on activé à la main après paiement. Aucun code à écrire : ApplyPlanQuotas ne recopie les quotas du plan que si le plan change, donc surcharger Instance.AiTokensPerMonth survit — le commentaire de la méthode le prévoit (« un client peut recevoir un geste commercial sans changer de plan »).
⚠️ Piège à connaître : le rattrapage d'indexation (
BackfillInstanceAsync) est déclenché parUpdateInstance, en C#. Activer l'add-on directement en SQL ne le déclenche pas — le client paierait un guide qui ne connaît rien de son contenu déjà saisi. Après l'UPDATE, cliquer le bouton de relance SuperAdmin de l'écran Guide IA.
3. Affectation des 4 clients existants — pour la bascule, aucun client Essentiel :
| Instance Mongo | Plan | Conséquence |
|---|---|---|
| MyInfoMate instance | plan-premium |
IA active |
| VisitNamur instance | plan-premium |
IA active |
| MDLF instance | plan-pro |
⚠️ pas d'IA |
| Fort Saint-Héribert instance | plan-pro |
⚠️ pas d'IA |
⚠️ À arbitrer avant le lot H : Pro n'inclut pas l'IA, donc rien ne s'indexera pour MDLF et le Fort et leur guide ne répondra jamais. Sur 4 instances, la chaîne RAG ne serait éprouvée que sur 2 — or le lien L14 demande justement de tester le post-filtrage HNSW à deux instances au moins. Deux sorties : les passer en Premium le temps des tests, ou leur poser un quota IA à la main (l'add-on ci-dessus), en pensant au rattrapage d'indexation.
4. Écart restant, hors bascule : la FAQ de myinfomate-landing (src/data/segments.ts) appelle encore le plan à 179€ « Bundle » — 41 occurrences en 4 langues — alors que les cartes de tarifs disent « Premium ». Un prospect qui lit la grille puis la FAQ voit deux noms pour la même offre. Chantier de texte commercial, sans effet sur la migration.
| I7 | Recette de bascule — test-plan.md §22, ajoutée le 2026-08-11. Comptages globaux, puis par type de section, collections filles, colonnes générées, et comparaison à l'écran ancienne app vs nouvelle. ⚠️ À jouer pendant que Mongo est encore lisible : c'est la seule fenêtre où les deux bases coexistent, après la comparaison est impossible | Le dry run compare des comptages, le §22 compare le contenu — un compte juste ne dit pas qu'un quiz a gardé ses questions ni qu'un article a gardé ses traductions. Les écarts b et c ne se voient qu'ici |
| I8 | Bascule DNS / API, Mongo gardé en lecture seule quelques jours, pg_dump après | ⚠️ Remplacé par la stratégie de coexistence ci-dessous, décidée le 2026-08-12 |
Stratégie de bascule — coexistence sur deux serveurs (décidée le 2026-08-12).
Plutôt qu'une bascule sèche avec fenêtre de maintenance : un second serveur porte la nouvelle prod Postgres, les nouvelles versions des apps pointent vers lui, et l'ancienne prod Mongo continue de servir les apps déjà installées. Pas de bascule DNS, pas de fenêtre, et un retour arrière qui consiste à ne rien faire.
C'est la bonne réponse au problème que L19 pose et qu'aucune manœuvre technique ne résout : on ne force pas la mise à jour du téléphone d'un visiteur.
Ce qui rend la coexistence saine — une seule source d'écriture :
Le contenu n'est écrit que par le back-office. Dès que manager-app pointe sur la nouvelle prod, l'ancienne devient figée, en lecture seule de fait. Les vieilles apps voient un contenu gelé au jour J, ce qui est acceptable quelques semaines — et surtout, les deux bases ne divergent jamais. Si les deux restaient écrivables, il n'y aurait plus de source de vérité au bout d'une journée.
⚠️ Ce qui ne marche pas : « recopier de la nouvelle prod vers l'ancienne ». Il n'existe pas de chemin Postgres → Mongo. MigrationController va dans un seul sens, et écrire l'inverse coûterait le même travail — pour alimenter une base qu'on éteint. L'ancienne prod se fige, elle ne se synchronise pas. Si un contenu doit absolument arriver sur les vieilles apps, il se saisit à la main des deux côtés, ou on attend le renouvellement du parc.
Conséquences à tenir :
- Le domaine : les anciennes apps gardent
api.mymuseum.be, les nouvelles reçoivent une nouvelle adresse. Aucun DNS ne bouge — c'est ce qui rend le rollback trivial. - Coût : un second serveur OVH le temps de la transition. À arbitrer contre le risque d'une bascule sèche ; à ce stade du projet, ça les vaut.
- Extinction : décider d'une date de fin de l'ancienne prod, sinon elle survit trois ans. Critère proposé : quand le contenu gelé devient gênant pour un client, ou quand le parc s'est renouvelé.
- Les tablettes kiosk ne sont pas concernées par l'attente : ce sont des appareils gérés, mis à jour directement — elles passent sur la nouvelle prod le jour même.
- I5 (plan de retour arrière) est à réécrire : avec deux serveurs, le rollback n'est plus une restauration de dump mais « republier l'app qui pointe vers l'ancienne adresse ». Plus simple, mais tributaire des délais de validation des stores — ce qui devient le vrai risque à couvrir.
Après la prod
Reprendre contact avec Louise Smets — Musée d'Ixelles, une fois la prod stabilisée. Premier temps d'écoute ; rien n'est su de son rôle exact.
3. Chemin critique et parallélisation
A (½-1j) ──► B (2-3j) ──► G (3-4j) ──► H (1 sem.) ──► I (2-3j + fenêtre)
│ │
│ └──► C1 ──► C2 ──► C3
│ C4 (C5 ✅)
├──► K1 ✅ ──► K5 ✅ ──► D0 (test device) ◄── débloqué ──► D1..D5
│ └──► K2 ──► K3, K4 ──► K6 (publier) ──► bascule (L19)
├──► D-bis : DB0 ──► DB1 ──┬──► DB2
│ ├──► DB3 ──► F (job de thèmes)
│ └──► DB4 ✅ (A tranchée et livrée)
├──► E
└──► F
J (RGPD) ──► I0 (secrets) ──► I3 ──► reste de I
- Chemin critique : A → B → G → H → I. Tout le reste s'y greffe.
- Le lot K s'y greffe par la fin : K2/K3/K4 sont parallélisables avec C, D, E et F, mais K6 (publier les apps) précède la bascule (L19).
Et K5 précède D0— K5 est livré le 2026-08-12, D0 n'est plus bloqué : l'APKmymuseum-visitappexiste pour les trois flavors, le test §21 sur device peut être joué. C'est aujourd'hui le prochain geste qui demande un appareil, et le seul qui dise si les bugs offline D2-D5 se manifestent réellement (L11). - ⚠️ Une question à trancher avant K6, qui n'est pas technique : le travail V1 des deux apps visiteur est committé sur des branches qui ne sont pas
master—Meta-Rayban-Testpourmymuseum-visitapp,AI-Assistant-testpourtablet-app. Le §5bis décrit pourtant la première comme « un POC jamais mergé ». Publier des APK depuis une branche décrite comme un POC n'est pas neutre : clarifier avant K6, pas pendant. - C, D, D-bis, E, F sont parallélisables entre eux, mais C1 doit précéder G (L5) et B doit précéder G (L2).
- DB3 doit précéder le job de thèmes du lot F (L16) : c'est le seul endroit du plan où le design commande le backend.
- La boucle du graphe a disparu le 2026-08-11. Elle venait de : I3 dépend de la rotation des secrets (dans le lot A), et H4 dépend de I3 (L12). En déplaçant la rotation en I0, la contrainte devient linéaire —
I0 → I3 → H4— et il n'y a plus de cycle à contourner. Contrepartie à ne pas rater : H4 (test d'onboarding) exigeapp.myinfomate.bedéployé, donc I3, donc I0. Concrètement, la rotation des secrets doit être faite avant la fin du lot H, pas au tout dernier moment. C'est le seul endroit où le report du 11/08 crée une échéance interne.
⚠️ Révision du 2026-08-12 : +3 à 5 jours pour le lot K, découvert en lançant le premier vrai build de tablet-app. Ce n'est pas du périmètre ajouté, c'est du travail qui était déjà dû et que personne n'avait vu : sans lui, la bascule dégrade les clients existants.
Ordre de grandeur : 5 à 6 semaines, dont ~1 semaine de tests, 3-4 jours sur MigrationController seul, 1,5 à 2 semaines sur le lot design et 2-3 jours sur le lot J (RGPD). Les trois arbitrages structurants sont tranchés depuis le 2026-08-11 — ParcoursIds et QuestionType au lot B, le flux SectionParcours au lot D-bis : le plan n'attend plus de décision, seulement du développement.
4. Arbitrages en attente
| Sujet | Options | Bloque |
|---|---|---|
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 | — |
| ✅ 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 | — | |
✅ 2026-08-11 — A, et livrée. Fenêtre unique avec rail d'étapes, profondeur 2. B écartée : son seul gain réel était une colonne d'aperçu visiteur, déjà couverte par le §5 du Guide IA (L18). Le travail fait pour A reste transposable — le widget à panneaux ne dépend pas du Dialog qui l'entoure |
— | |
| 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) |
4bis. Reprendre ce plan dans une conversation neuve
Ordre de lecture — trois fichiers suffisent, dans cet ordre :
- Ce fichier — les 9 lots, les 18 liens, le chemin critique. C'est le seul document qui dit dans quel ordre.
- STATUS.md §1sexies — le même découpage, relié au reste de l'état du projet.
- kanban.html — où en est chaque chantier, carte par carte.
Les plans détaillés de v2/ ne se lisent qu'au moment d'attaquer le lot concerné.
Paralléliser sur plusieurs conversations — règle apprise le 2026-08-12. Les lots se répartissent bien par repo, mais chaque session ne doit committer que son repo. Deux sessions ont débordé le même jour : l'une a committé DOCS/ avec son travail manager-app, l'autre a emporté trois fichiers manager-service d'un chantier voisin dans son commit. Rien n'a été perdu, mais un git add -A en parallèle finit par emporter du travail à moitié fini. À écrire dans le prompt de chaque session : « ne touche rien hors de <repo>/, et ne commit que celui-là ».
Amorce à copier telle quelle :
« Lis
DOCS/v1-plan.mden entier, puisDOCS/STATUS.md§1sexies. Le plan découpe la V1 en 9 lots (A à I plus D-bis) avec leurs dépendances ; le chemin critique est A → B → G → H → I. Dis-moi où en est chaque lot d'après les documents, puis attaque le lot que je te désigne. Ne relance jamais la génération demanager_api_new: il s'édite à la main. Vérifie l'état réel dans le code avant de croire un document — plusieurs points de STATUS.md se sont révélés périmés. »
Trois pièges vérifiés en vrai, à ne pas re-découvrir :
flutter analyzeglobal est inexploitable (68 erreurs de fichiers modèle orphelins, hors graphe de compilation). Filtrer sur le dossier travaillé. Seulsflutter build web,dotnet build,dotnet testdisent la vérité. Cause exacte établie le 2026-08-11 : 139partdéclarés dansapi.dartpour 153 fichiers danslib/model/— cf. lot A. Le nettoyage du lot A supprime ce bruit.- La base locale peut avoir une migration de retard, ce qui fait planter l'API dès le login sans que le compilateur ne dise rien.
- Le fichier du kanban et la version publiée peuvent avoir divergé — toujours
WebFetchl'artifact et fusionner avant de republier, jamaisforce.
✅ Le prérequis « committer avant tout » est levé. L'alerte de ce matin (manager-service 44 fichiers dont 9 non suivis, manager-app 12) est traitée : revérifié le 2026-08-11, les 9 repos (DOCS compris) sont propres et synchronisés avec leur remote. Rien n'est plus en attente sur un seul disque. Ne pas re-planifier cette étape.
5. Ce qui reste hors V1 — ne pas y toucher
Reporté en V2 et confirmé ici : SectionForm, ressource 360°, AR image tracking (Mind AR), kiosk web, ingestion documentaire + OCR, TTS pré-généré, guides multiples et thématiques, talking head, génération de visites, envoi mensuel automatique du rapport de stats, XR (Ray-Ban / Quest), dashboard super-admin V2, facturation V2.
Ajouté le 2026-08-12 : l'écran SuperAdmin d'activation de l'add-on IA et des canaux d'une instance. Il rendrait l'add-on vendable en libre-service, mais la V1 n'en a pas besoin : les 4 clients de la bascule sont affectés à leur plan (§lot I, point 3), et doter une instance à la main reste un geste interne, fait une fois, sur une base qu'on contrôle. Le mode d'emploi SQL est dans STATUS.md §1sexies « Add-on IA » — trois gestes, dont le bouton de relance d'indexation qu'un UPDATE seul ne déclenche pas. À faire d'un bloc avec l'endpoint de mise à jour d'ApplicationInstance (canaux activables), qui reste lui au lot F : c'est le même écran, mais seule la partie canaux est V1.
Le POC Ray-Ban existe et est démontrable (branche Meta-Rayban-Test, jamais mergée) — c'est un actif commercial, pas un chantier V1. Ne pas vendre l'add-on avant la bascule TTS ElevenLabs → Gemini.