# V1 — plan d'implémentation jusqu'à la mise en prod
> Établi le 2026-08-10 en croisant [STATUS.md](STATUS.md) (§1 à §6bis), [kanban.html](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 |
|---|---|---|
| ~~Committer le lot 3 RAG du 10/08~~ | — | ✅ **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 |
| ~~**Rotation des secrets**~~ | `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_new` s'é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 |
|---|---|---|
| Renommer `SectionMap.MapResourceId` → `IconResourceId` (branché de bout en bout : `SectionFactory:179/511`, `SectionMap.ToDTO:58`, `MigrationController:469`) | Aucune, déjà tranché au §6bis | **L6** = écart (g) |
| **Supprimer `SectionEvent.ParcoursIds`** — champ, DTO, migration EF | ✅ **Tranché le 2026-08-11 : supprimer.** Investigation : le champ n'est **lu ni écrit nulle part** (entité + DTO + migrations, zéro contrôleur, zéro service), son commentaire dit `// Liens vers GeoPoints spécifiques` ce qui contredit son nom, et le lien événement↔parcours existe dans l'autre sens via `GuidedPath.SectionEventId`, lui bien entretenu (`SectionMapController:396/449`, `SectionController:878`). Argument décisif : il est porté par **`SectionEvent`, pas par `ProgrammeBlock`** — il ne peut donc pas exprimer « les parcours de tel jour », le cas carnaval pour lequel on le gardait | Nettoyage §1 « À faire » |
| Supprimer `SectionEvent.IconResourceId` (seul candidat sans usage visiteur identifié) | Aucune | §6bis |
| ~~Unifier `QuestionType`~~ → **sorti du lot B le 2026-08-11** | ✅ **Tranché : on ne touche pas au schéma.** Le trio TextLibre/Digicode/ExpectedAnswer **n'existe pas dans le code** : l'enum a trois valeurs (`Simple`, `MultipleChoice`, `Puzzle`) référencées à **un seul endroit** côté serveur, et le digicode n'est pas un type mais un comportement dérivé d'une réponse numérique. Reste une propreté front : les comparaisons par entier brut (`…?.value == 2 ? 'Puzzle' : …`) dans `showNewOrUpdateGuidedStep.dart:409` et `guided_step_challenge.dart:14` → **déplacé au lot E** | **Ne bloque plus `MigrationController`** |
| Implémenter `Section.meterZoneGPS` côté visiteur (la colonne existe, une constante à 100 m la remplace) | Aucune, tranché au §6bis | Bug M3 du kanban — même passe |
| Flag watermark sur `Instance` + exposition dans la config, retrait du `if instanceId == "633ee379…"` de `ResourceController:265` | Aucune | Point 6 du §1ter |
Sortie du lot : `dotnet build` + `dotnet test` verts, une migration EF unique regroupant les renommages, et **plus aucun changement de modèle en attente**.
### Lot C — Terminer le lot médias (2 à 3 jours)
L'étape A du §1quater est faite (`StoragePath`/`SizeBytes` écrits à `Create` depuis le 10/08). Reste l'étape C.
| # | Quoi | Note |
|---|---|---|
| C1 | **Extraire le calculateur** `StoragePath` + `SizeBytes` dans un service appelable | **L5** — consommé par C2 *et* par l'écart (e) du lot G |
| 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, aucun `TabBar`**. 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éro |
> | SectionParcours | 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 |
|---|---|---|
| ~~**DB0**~~ ✅ 2026-08-11 | **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` |
| ~~**DB1**~~ ✅ 2026-08-11 | **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 |
| ~~**DB2**~~ 🔨 2026-08-11 | **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. |
|---|---|
| Fournisseur de carte honoré en web (`LeafletMap.tsx`, `TileLayer` en dur) | W1 |
| PDF en média d'étape : liste de types paramétrable côté étape, cas `PDF` dans `getElementForResource` **et** dans `ResourceViewer.tsx` | Les deux derniers points ne sont pas optionnels — sans eux le PDF s'affiche en image cassée, le bug corrigé pour la vidéo le 09/08 |
| ~~Refonte du flux SectionParcours~~ | **Déplacé au lot D-bis** (DB2 et DB4) — c'est un chantier de design, pas une finition de parité |
### Lot F — Guide IA lot 4 et dette produit (1 semaine)
| Quoi | Lien |
|---|---|
| ~~Volet RGPD~~ | **Déplacé au lot J**, fin de backlog (décidé le 2026-08-11) |
| ~~Insertion `VisitorQuestion`~~ · ~~purge 90 j~~ · ~~onglet « Ce que demandent vos visiteurs »~~ | ✅ **Livrés le 2026-08-11** |
| ~~Job de regroupement en thèmes~~ | **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 |
|---|---|---|
| c | `Instance` : 4 champs migrés sur ~35 | **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` |
| a | `BuildSection` ne couvre que 11 types : `SectionEvent` et `SectionParcours` tombent dans le `default` | Section non migrée, silencieusement |
| b | Collections filles jamais remplies (`QuizQuestions = new()`, `EventAgendas = new()`, aucun `GuidedPath`/`GuidedStep`) | Un quiz arrive sans ses questions |
| d | `Role = UserRole.ContentEditor` en dur | Plus aucun administrateur après la bascule |
| e | Colonnes `Resource` non renseignées, `SizeBytes` déduit d'un `HEAD` avalé | **L5** — appeler le calculateur C1 |
| f | `IsQRCode`/`IsSearchText`/`IsSearchNumber` forcés à `false` | Réglages de configuration perdus |
| g | `MapResourceId = dto?.iconResourceId` | **L6** — tombe avec le lot B |
| h | Pas de transaction globale, `catch` qui renvoie 200 OK avec `FatalError` dans le corps | Transaction + vrai code d'erreur. Les tests `AnyAsync` d'idempotence, eux, sont corrects |
Puis : **dry run** (`dryRun=true`) sur l'export Mongo de `manager-service/migration-data/`, et **comparaison des comptages par entité** avec Mongo. C'est la seule preuve que les écarts a, b et c sont fermés.
### 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 | |
| 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) exige `app.myinfomate.be` dé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 | — |
| 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 :
1. **Ce fichier** — les 9 lots, les 18 liens, le chemin critique. C'est le seul document qui dit *dans quel ordre*.
2. **[STATUS.md](STATUS.md) §1sexies** — le même découpage, relié au reste de l'état du projet.
3. **[kanban.html](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.md` en entier, puis `DOCS/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 de `manager_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 analyze` global est inexploitable** (68 erreurs de fichiers modèle orphelins, hors graphe de compilation). Filtrer sur le dossier travaillé. Seuls `flutter build web`, `dotnet build`, `dotnet test` disent la vérité. **Cause exacte établie le 2026-08-11** : 139 `part` déclarés dans `api.dart` pour 153 fichiers dans `lib/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 `WebFetch` l'artifact et fusionner avant de republier, jamais `force`.
✅ **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.