Recette d'import documentée (MigrationController lit un Mongo vivant, pas les fichiers de migration-data/), résultats des comptages, et les deux réserves : l'export a 4 mois alors que la prod tourne encore sur Mongo, et les 20 sections orphelines sont du contenu MDLF réel dont le sort doit être tranché avec le client — recréer les 2 configurations, ou acter la perte. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
53 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 |
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 |
Réparer le build tablet-app |
tablet-app |
Porte du test-plan §0. Un seul défaut, purement Gradle (flutter_tools/gradle/build.gradle.kts) — peut-être déjà réglé par 5a6701d, à reconfirmer par un vrai build. Une piste Dart a été ouverte puis écartée le 2026-08-11 : marker_view.dart:456 référence ContentGeoPoint, classe hors graphe, mais la ligne est dans un bloc commenté (/* ouvert en 418), comme sa jumelle de mymuseum-visitapp:367 (/* en 329). flutter analyze sur les deux fichiers : zéro erreur. Ne pas rouvrir |
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 |
|
| 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 |
| C5 | Alerte de budget GCP | 5 minutes. À faire dès maintenant, pas au jour J |
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 |
| D1 | Remplacer le bloc commenté de ConfigurationController.Export (:~400-577) par des appels à GetReferencedResourceIds() — la méthode existe déjà sur les 13 sous-types. Même geste côté mymuseum-visitapp, où le switch est aussi commenté |
| 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é
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 |
| DB4 | SectionParcours — la forme, après arbitrage. Extraire d'abord ParcoursFields / EtapeFields / QuestionFields en widgets autonomes : sert A et B à l'identique. Porte aussi la sauvegarde incrémentale, héritée de DB2 le 2026-08-11 |
L18 — recommandation : A. Point de départ concret côté persistance : les endpoints CreateGuidedStep / UpdateGuidedStep / DeleteGuidedStep existent déjà sur sectionParcoursApi et sectionMapApi, et le backend persiste correctement les questions imbriquées (GuidedStep.FromDTO appelle SyncQuizQuestions) — vérifié le 2026-08-11. Il n'y a pas d'endpoint QuizQuestion : les questions partent avec leur étape, c'est voulu. Le garde-fou de DB2 devient inutile dès que cette persistance est en place, le retirer alors |
| 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 |
| Écran d'audit log (backend fait, front absent) + gestion des users par instance (plafond de 5, compteur UI) | L8 |
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 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/ |
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 |
| 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 | Vérifications post-bascule : login manager-app, une app visiteur avec sa clé API, un parcours, une carte, un PDF, un quiz avec ses questions | 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 | |
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 (indépendants)
├──► D0 (test device) ──► D1..D5
├──► D-bis : DB0 ──► DB1 ──┬──► DB2
│ ├──► DB3 ──► F (job de thèmes)
│ └──► DB4 (après arbitrage A/B)
├──► E
└──► F
J (RGPD) ──► I0 (secrets) ──► I3 ──► reste de I
- Chemin critique : A → B → G → H → I. Tout le reste s'y greffe.
- 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.
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 deux arbitrages du lot B (ParcoursIds, QuestionType) et celui du lot D-bis (flux SectionParcours) sont les seuls points où le plan attend une décision plutôt qu'un 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 | — | |
| 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) |
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é.
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.
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.