# MyInfoMate — Tableau de bord > Point d'entrée unique : "où j'en suis, qu'est-ce que je peux avancer". Chaque section renvoie vers le document détaillé pertinent — ce fichier ne duplique pas le contenu, il donne juste la vue d'ensemble et l'état. > > Dernière mise à jour : 2026-08-09. --- ## 1. Produit — features manquantes / en cours → Détail complet, granulaire, à jour : **[todo-features.md](todo-features.md)** Résumé de la table de suivi de ce fichier : - **Fait** : SectionEvent, Escape/Puzzle games, GuidedPath + progression carte, push, stats tracking, sync agenda/météo, traduction IA, quotas (IA/stockage/stats) par plan, audit log backend fiabilisé, refacto SectionParcours/GuidedPath/SectionGame (backend, vérifié 2026-07-15). - **Partiel** : Beacons/geofencing (manager-app UI ⚠️), AI Assistant (manager-app ⚠️), ~~**mode offline**~~ ✅ **corrigé le 2026-08-11 (D1) puis complété le 13/08 (D5)** — le pipeline de collecte n'est plus commenté, une visite embarque désormais contenus, audios et icônes, et une visite incomplète ne s'annonce plus téléchargée. **Reste à confirmer sur device** (§21 / D0), c'est le seul point encore ouvert du volet offline. Repasse UI globale, refacto SectionParcours côté manager-app/visitapp-web/mymuseum-visitapp (UI présente mais sous-comportements non revérifiés en détail — voir section "Refactoring majeur" de todo-features.md). - **🧪 Implémenté mais jamais testé (2026-08-05)** : **onboarding self-service complet** — inscription sur myinfomate-landing (`/[lang]/signup`), création auto de l'instance + du premier user, essai 14j, Stripe (customer/Checkout/webhook/Tax), 9 emails Resend (domaine vérifié, envoi réel validé), job Hangfire du cycle d'essai, watermark « Aperçu » visitapp-web, écran Abonnement manager-app, mot de passe oublié + invitation user, plafond IA d'essai. Les 4 repos compilent, migrations appliquées en base locale, **aucun parcours exécuté** → checklist de test dans `todo-features.md` (section "🧪 Checklist de test end-to-end"). Reste à faire côté config : alias mail `onboarding@myinfomate.be`, et `whsec_` de la Stripe CLI pour tester le webhook en local. - **À faire** : dashboard super-admin V2, facturation V2. ✅ **Le Stripe Customer Portal est complet le 2026-08-13** (backend + bouton manager-app). - ⚠️ **Cette ligne était périmée sur six points, revérifiés dans le code le 2026-08-13.** Sont **faits** : écran audit log manager-app (front livré le 12/08), **rate limiting API Keys** (livré, pas seulement décidé — politique `ai` dans `Startup.cs:279`, `UseRateLimiter()` en `:344`, les deux endpoints décorés), gestion des users par instance (plafond serveur + compteur UI, 12/08), nettoyage `SectionEvent.ParcoursIds` (11/08), et le **backend** du Customer Portal (13/08). L'**unification `QuestionType`** n'est pas « à faire » mais **écartée** : le trio TextLibre/Digicode/ExpectedAnswer n'a jamais existé dans le code, l'enum a trois valeurs et le digicode est un comportement dérivé, pas un type — reste une propreté front, elle aussi livrée (alias nommés, 11/08). - **📦 Reporté en V2 (décidé le 2026-08-07)** : **SectionForm**, **ressource 360°**, **AR image tracking (Mind AR)**. Les trois sont spécifiés en détail dans [todo-features.md](todo-features.md) mais sortent du périmètre V1 — aucun n'est un prérequis de la migration Postgres ni de la mise en prod. À reprendre tels quels quand la V1 sera stabilisée. **Prochaine chose à faire si tu as 1h** : jouer le **§19.13 cas E** du plan de test (chasse au trésor / carnaval) de bout en bout, depuis la création dans manager-app. C'est le scénario le plus complet — il valide SectionMap + SectionParcours + géodéclenchement + questions + mode jeu d'un coup. --- ## 1bis. État des builds — mesuré le 2026-08-06 > Aucune app n'était buildable au début de l'audit. Deux corrections ont débloqué les 3 apps Flutter. > Détail : **[parity-manager-visitapp.md §6](parity-manager-visitapp.md)** · checklist : **[test-plan.md §0](test-plan.md)** | Repo | Build | Note | |---|---|---| | manager-service | ✅ `dotnet build` et ✅ `dotnet test` **124/124** (2026-08-06) | La remise en route de la suite a révélé un **bug backend réel** : `BuildAuditEntries()` ajoutait un `AuditLog` au contexte *pendant* l'énumération du `ChangeTracker` → `InvalidOperationException: Collection was modified` sur toute écriture d'entité auditée (Section, Resource, Configuration, Device, User, Instance). Corrigé : énumération matérialisée, logs ajoutés après la boucle. Invisible jusque-là parce que les tests ne compilaient plus | | manager-app | ✅ `flutter build web` | débloqué : `// @dart=2.18` manquant dans `manager_api_new/lib/api/onboarding_api.dart` (fichier ajouté à la main pour l'onboarding, sans l'annotation de version que porte `api.dart`) — **cassait les 3 apps Flutter d'un coup** | | mymuseum-visitapp | ✅ `flutter build apk` — **les 3 flavors, vérifiés le 2026-08-12** (`dev`, `mdlf`, `fortsaintheribert`) | ⛔ **Le ❌ « revérifié le 2026-08-11 » était périmé.** Le build passe, et sa config Android était **déjà à niveau** — c'est `tablet-app` qui était en retard sur lui, pas l'inverse. K5 s'est donc réduit à deux alignements que le build réclamait en avertissement : 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é par Flutter ; même version que `tablet-app`). L'APK passe de 382 à **349 Mo**. Les 4 erreurs Dart Ray-Ban restent hors du graphe de `main.dart` et ne gênent pas le build (§5bis) | | visitapp-web | ✅ `npm run build` | débloqué : helper `getGeoPointLatLng()` dans `src/lib/geo.ts`, les 7 erreurs venaient d'un `unknown` non narrowé. ⚠️ ce type d'erreur est **invisible en `next dev`** | | tablet-app | ✅ `flutter build apk` — **réparé le 2026-08-12**, APK produit pour la première fois depuis le 16 avril | ⛔ **Le diagnostic « un seul défaut, purement Gradle, peut-être réglé par `5a6701d` » était doublement faux.** Il a tenu des mois parce que personne n'avait lancé la commande. Réalité : **dix crans d'outillage** (Gradle 7.5 → 8.11.1, AGP 7.2.0 → 8.9.1, Kotlin 1.9.0 → 2.3.10, option AGP supprimée, jcenter mort, heap Gradle 1536M → 4096M, Jetifier coupé, `resolutionStrategy` sur `androidx.lifecycle` retiré) — **corrigés le 2026-08-12** — **puis 132 erreurs Dart** que le Gradle masquait : l'app est restée sur **l'ancien contrat d'API**, celui d'avant Postgres v3. Détail complet et suite : **[v1-plan.md lot K](v1-plan.md)** | **Leçon** : `flutter analyze` et `next dev` ne suffisent pas. Seuls `flutter build` / `npm run build` / `dotnet test` disent la vérité — d'où la nouvelle section §0 du plan de test. **Leçon renforcée le 2026-08-12** : un build **jamais lancé** ne vaut pas mieux qu'un build vert supposé. Trois affirmations de ce tableau ont sauté d'un coup dès qu'on a tapé la commande. Corollaire à retenir : *« à reconfirmer par un vrai build »* dans une doc veut dire **que ce n'est pas confirmé**, et un point non confirmé ne doit pas être chiffré comme une demi-heure. ⚠️ **Et un piège de vérification à connaître** : `flutter build apk … | tail` renvoie **le code de sortie du `tail`**, pas celui du build. Un échec y ressort en « exit code 0 ». Rediriger vers un fichier et lire `$?` séparément. --- ## 1ter. Chantier « migration Postgres v3 » — ordre de bataille (ajouté le 2026-08-07) > Postgres n'est pas encore en prod. Trois chantiers ont été analysés le 2026-08-07 et découpés selon une règle unique : > > **Ce qui touche aux données déjà écrites part avec la migration. Ce qui est purement additif attend.** > > Chaque ligne est faite pour être attaquée dans une conversation dédiée : lire le doc, faire l'étape. > > ⚠️ Cette section dit **ce qui doit entrer dans le schéma**, pas comment on remplit la base. L'opération de bascule elle-même — et l'état de `MigrationController` — sont au **§1quinquies**. ### Doit partir avec la migration (schéma — quelques heures) | # | Quoi | Doc | Pourquoi maintenant | |---|---|---|---| | 1 | ✅ **Fait 2026-08-09** — Image Docker `postgis` + `pgvector` (`Deployment/Dockerfile.postgres`, digest épinglé) | [v2/rag-pgvector-integration-plan.md](v2/rag-pgvector-integration-plan.md) | Base vide = 4 lignes de Dockerfile. Plus tard = fenêtre de maintenance sur la prod | | 2 | ✅ **Fait 2026-08-09** — Table `ContentEmbedding` + index HNSW | idem | Table vide, ne dérange personne. Additif mais gratuit maintenant | | 3 | ✅ **Fait 2026-08-09** — `IEmbeddingService` + `GoogleEmbeddingService`, **dimension 768 validée contre l'API réelle** | idem | **Gèle la dimension du vecteur** — décision de schéma, à valider avant la migration | | 4 | ✅ **Fait 2026-08-09** — Colonnes `Resource` : `StoragePath`, `FileName`, `IncludeInAiKnowledge`, `AiIndexStatus`… (`SizeBytes` existait déjà) | [v2/media-storage-plan.md](v2/media-storage-plan.md) | Les URL Firebase absolues sont dispersées jusque dans le JSONB des sections | | 5 | Backfill `SizeBytes` + `StoragePath` + inventaire des blobs orphelins | idem | La migration touche déjà chaque ligne `Resource` | | 6 | Flag watermark sur `Instance` (+ exposition dans la config) | idem | Remplace un `if` sur un id d'instance codé en dur | ### Avant la release, hors schéma | # | Quoi | Doc | |---|---|---| | 7 | Compression images côté client — **2560 px / JPEG q82**, ~12× de gain | [v2/media-storage-plan.md](v2/media-storage-plan.md) | | 8 | Alerte de budget GCP (5 min) | idem | | 9 | Quota : pré-vol + contrôle autoritaire à `Create` + `Delete` du blob | idem | ### Bugs découverts — indépendants de la migration | # | Quoi | Doc | |---|---|---| | 10 | **Visite hors ligne quasi non fonctionnelle** — le `switch` de collecte des ressources est commenté côté backend *et* côté visitapp : ni images d'articles, ni audios | [v2/offline-visit-plan.md](v2/offline-visit-plan.md) | | 11 | Ressource modifiée jamais re-téléchargée (filtre sur la présence, pas la version) | idem | | 12 | `audio/mpeg` absent de la table d'extensions → MP3 probablement en `.unknown` | idem | | 13 | Purge des fichiers obsolètes désactivée sur le device | idem | ### V1 — après la migration (décidé le 2026-08-07) Le RAG **est** dans la V1, dans son périmètre restreint : `IVectorStoreService`, jobs Hangfire sur le contenu CMS, endpoint `/ask`, et l'écran **[Guide IA](v2/guide-ia-screen-plan.md)** dans manager-app. Pas d'ingestion documentaire, pas d'OCR, pas de TTS — c'est ce qui le rend tenable. **Deux écrans manager-app sont spécifiés et maquettés** : | Écran | Plan | Maquette | |---|---|---| | **Guide IA** — 🔨 **onglet Configuration livré le 2026-08-07** | [v2/guide-ia-screen-plan.md](v2/guide-ia-screen-plan.md) | https://claude.ai/code/artifact/c43fe02d-3283-48c2-992f-d687e18acdfa | | **Statistiques** — 🔨 **refonte livrée le 2026-08-07** | [v2/stats-screen-plan.md](v2/stats-screen-plan.md) | https://claude.ai/code/artifact/052fe8f3-0288-4847-b63f-4c7de06068ec | Tableau de chantiers global : https://claude.ai/code/artifact/4c317904-2e23-45d3-ac14-dcca678ae5d3 > 🔨 **Premier morceau livré le 2026-08-07** — migration `AddGuideIaConfigToInstance` (4 colonnes nullable, additive), champs sur `Instance`/`InstanceDTO`, client `manager_api_new` étendu à la main, écran `Screens/GuideIa/` avec 35 clés i18n FR/EN/NL. `dotnet build` et `flutter build web` passent. > > ✅ **`AssistantService` branché sur la configuration le 2026-08-07** — l'écran ne pilote plus dans le vide. C'est le lot 1 du **§1quater** ci-dessous, qui donne l'ordre d'exécution de tout le chantier Guide IA. > > 📄 **Repartir de zéro dans une conversation neuve** : ordre de lecture et amorce à copier dans [v2/guide-ia-screen-plan.md](v2/guide-ia-screen-plan.md) § « Reprendre ce chantier dans une conversation neuve ». > > ~~Relevé en vérifiant : tout le system prompt est codé en dur, dans deux blocs…~~ **Corrigé le 2026-08-07.** Il y avait en fait **quatre** blocs de prompt et non deux ; les quatre lisent maintenant la configuration du client. Détail : [v2/guide-ia-screen-plan.md](v2/guide-ia-screen-plan.md). > 🔨 **Statistiques — refonte livrée le 2026-08-07, export PDF compris**, `Screens/Statistics/statistics_screen.dart` réécrit, **aucun changement backend**. Les 7 défauts sont corrigés : barres horizontales monochromes à la place des barres verticales tronquées et des deux anneaux, barre de filtres unique avec les volumes par canal, canaux filtrés sur ceux que l'instance possède réellement, règle mono-canal, 4 KPI portant chacun leur variation, bandeau « à retenir », courbe en aire avec bandes de week-end et survol, bloc rapport avec **bouton de téléchargement actif** (PDF généré côté client, paquet `pdf` Dart). ~55 clés i18n FR/EN/NL. `flutter analyze lib/Screens/Statistics` est propre ; `flutter test test/statistics_report_test.dart` 4/4. > > ⚠️ **Rien de tout ça n'a été ouvert dans un navigateur** — ni l'écran sur des données réelles, ni le PDF produit. **Cases de test : [test-plan.md §8bis](test-plan.md) (écran, 9 cas) et [§8ter](test-plan.md) (export PDF, 6 cas)**. Trois écarts assumés par rapport au plan (comparaison de période côté client, bandeau généré côté client, « visiteurs uniques » remplacé par « contenus consultés » car non calculable) : détail dans [v2/stats-screen-plan.md](v2/stats-screen-plan.md) § État d'implémentation. > > ℹ️ **Précision sur `flutter analyze`** : le projet a **68 erreurs**, toutes dans les fichiers modèle orphelins de `manager_api_new` (+ `lib/api/openApiTest.dart`) — c'est la dette « client généré désynchronisé » déjà listée, pas une régression. `flutter build web` passe malgré elles : ces fichiers ne sont pas dans le graphe de compilation. Ne pas lire un `flutter analyze` global comme un feu vert, filtrer sur le dossier travaillé. ⚠️ Point de correction V1 : l'indexation doit respecter `IsActive`. Une section non publiée ne doit pas être indexée, et la désactiver doit purger ses embeddings — sinon le guide révèle du contenu que le client prépare. ### V2 — incréments **Côté IA** : ingestion documentaire (PDF/Word/images + OCR Gemini), TTS pré-généré, talking head, génération de visites, guides multiples et guide thématique par section. Détail : [v2/rag-pgvector-integration-plan.md](v2/rag-pgvector-integration-plan.md). **Côté produit — reporté en V2 le 2026-08-07** (spécifié dans [todo-features.md](todo-features.md), rien n'en dépend en V1) : | Chantier | Pourquoi ça peut attendre | |---|---| | **SectionForm** — formulaires personnalisés (feedback, satisfaction, inscription atelier) | Nouveau type de section + table de réponses : purement additif, aucun impact sur le schéma existant | | **Ressource 360°** — images panoramiques (`ResourceType.Panorama360` + package `panorama`) | Une valeur d'enum et un branchement dans l'affichage des ressources ; se greffe sans toucher aux sections | | **AR image tracking (Mind AR)** — modèle `ArAnchor`, microservice Node de compilation `.mind`, WebView scanner unifiée QR + image | Le plus gros des trois : nouveau service Docker, nouveau modèle, refonte du scanner visiteur. Fort effet démo, zéro urgence | | **Kiosk web** — version Flutter Web de `tablet-app` | Aucun client ne le demande. ⚠️ **Ne pas confondre avec le visiteur web** (`visitapp-web`), lui déjà écrit et attendu en V1. *Mise à jour du 2026-08-12 : l'argument « et tablet-app ne compile pas » est tombé, l'APK est produit depuis K1.* | | **Portrait et paysage sur toute la borne** — ajouté le 2026-08-12 | `tablet-app` n'a **aucune stratégie d'orientation** : les écrans sont écrits pour la forme qu'on leur a vue, et la maquette K3/K7 tranche le paysage pour deux types seulement. Or une borne se monte aussi bien en portrait (totem d'accueil) qu'en paysage (comptoir), et le choix appartient au lieu, pas à l'app. Le chantier n'est donc pas « ajouter le portrait » mais **rendre les treize types indifférents à l'orientation** — un jeu de points de rupture et une règle unique du type « la colonne de texte reste plafonnée, le rail passe dessous quand la largeur manque ». C'est une repasse UI transversale, pas un correctif : hors V1 pour la même raison que la repasse UI globale de `manager-app` (DB1). ⚠️ **La maquette du 12/08 est compatible** — le rail qui tombe sous le texte est exactement le comportement portrait — mais elle ne l'a pas spécifié | **Prochaine chose à faire si tu as 1h** : le **§21 du plan de test** — vérifier sur un device l'état réel de la visite hors ligne. C'est peut-être déjà remonté comme « l'appli marche mal dans le fort » sans que la cause ait été identifiée, et ça conditionne l'urgence des points 10-13. --- ## 1quater. Chantier « Guide IA V1 » — ordre d'exécution (arrêté le 2026-08-07) > Cette section ne décrit **rien de neuf** : elle donne l'ordre dans lequel exécuter ce que trois plans détaillent déjà, parce que le chantier les traverse tous les trois et qu'aucun ne porte la vue d'ensemble. > > **Décision : tout est fait d'un coup, sans mise en prod intermédiaire.** Rien ne part en prod avant que le backlog (kanban + ce fichier) soit terminé — voir §1ter, la base Postgres est vide et c'est une fenêtre à ne pas refermer. ### ✅ Lot 1 — Rendre vrai ce qui est déjà livré · **fait le 2026-08-07** → **[v2/guide-ia-screen-plan.md](v2/guide-ia-screen-plan.md)** § « Configuration branchée sur `AssistantService` » `BuildGuideIdentity()` et `BuildFallbackInstruction()` dans `AssistantService`, consommés par les **4** blocs de prompt — le relevé initial n'en annonçait que deux, il en existe quatre (scope configuration / scope instance × vocal / texte). Ton codé en dur retiré, seconde politique de repli de la ligne 550 réconciliée, règle hors-sujet **ajoutée aux deux variantes texte qui n'en avaient aucune**. Les 4 `new HttpClient()` des outils d'agenda passent par `IHttpClientFactory` (§3, priorité haute). `dotnet build` 0 erreur · `dotnet test` **124/124**. ⚠️ **Pas encore vu tourner** : aucun échange réel joué contre un guide configuré. À couvrir au moment du plan de test. ### ✅ Lot 2 — Le schéma · **livré le 2026-08-09** → **[v2/rag-pgvector-integration-plan.md](v2/rag-pgvector-integration-plan.md)** · **[v2/media-storage-plan.md](v2/media-storage-plan.md)** | # §1ter | Livré | Vérifié en base | |---|---|---| | 1 | Image `postgis` + `pgvector` — `Deployment/Dockerfile.postgres`, base **épinglée par digest** | `postgis 3.4.3` + `vector 0.8.6` | | 2 | Table `ContentEmbedding` + 3 index (dont HNSW `vector_cosine_ops`) | migration `AddContentEmbedding` | | 3 | `IEmbeddingService` + `GoogleEmbeddingService` (batch par 50) | **768 validé contre l'API réelle**, insertion + recherche jouées en base | | 4 | Colonnes `Resource` (7) + `Word`/`PowerPoint`/`Text` en **fin** d'enum | migration `AddResourceStorageAndAiColumns` | | 7 | Table `VisitorQuestion` + `ConversationId` sur `AiChatRequest` | migration `AddVisitorQuestion` | `dotnet build` 0 erreur · `dotnet test` **124/124** · 62 migrations, 26 tables · données intactes (52 sections, 45 ressources). **Ce que l'appel réel à l'API a appris (2026-08-09)** — trois choses qu'aucun plan ne disait : 1. **`gemini-embedding-001` renvoie 3072 dimensions par défaut.** Le paramètre `dimensions: 768` est **obligatoire** dans la requête, sinon la colonne `vector(768)` rejette l'insertion. 2. **Les vecteurs réduits ne sont pas normalisés.** Norme L2 mesurée : **0,578**, pas 1. La distance cosinus de pgvector suppose des vecteurs normalisés — sans normalisation côté client, le classement des résultats serait faussé **sans qu'aucune erreur ne soit levée**. `GoogleEmbeddingService.Normalize()` s'en charge. 3. **Google ne renvoie pas le champ `index`** du contrat OpenAI (chaque item ne porte que `embedding` et `object`). Un `OrderBy(d => d.Index)` trierait sur des zéros : retiré, l'ordre du tableau fait foi. **Recherche cross-lingue vérifiée en base**, question posée en néerlandais sur du contenu FR/NL : | Contenu | Similarité | |---|---| | NL, même sujet | **0,8145** | | FR, même sujet | **0,7982** | | FR, hors sujet | 0,4818 | Le pari « pas de filtre de langue » tient : le FR pertinent bat largement le FR hors-sujet. Et l'écart NL/FR n'est que de **0,016** — trop faible pour trier tout seul, ce qui confirme la nécessité du **bonus de score** par langue plutôt que de s'en remettre à la similarité brute. Vérifié aussi : la contrainte anti-doublon rejette bien un rejeu Hangfire, et un vecteur de 512 dimensions est refusé par la colonne. **Schéma `ContentEmbedding` validé le 2026-08-09** — il diverge du plan écrit sur trois points, sur la base du code réel : - `string InstanceId` et non `int VenueId` : tous les Id du projet sont des `string` (héritage ObjectId Mongo). **Filtre dur** — frontière entre deux clients. - `string? ConfigurationId` **nullable**, absent du plan : `Resource` ne porte pas de `ConfigurationId`. Sert de **bonus de score**, jamais de filtre — décision produit : le guide connaît tout le lieu et priorise la visite en cours. Même patron que le multilingue. - `long Id` : table technique à fort volume, jamais référencée ailleurs. **Relevé en chemin :** - ⚠️ **La base locale avait une migration de retard.** `AddGuideIaConfigToInstance` (lot 1) n'était pas appliquée : les colonnes `Guide*` n'existaient pas, donc **toute requête sur `Instances` aurait planté** — l'API entière, dès le login. Appliqué le 2026-08-09. Le compilateur ne dit rien de l'état de la base. - ⚠️ **Warning de collation** sur la base locale (`glibc 2.36` → `2.31`), corrigé par `REINDEX` **puis** `REFRESH COLLATION VERSION` — dans cet ordre, le refresh seul masque le symptôme sans réparer les index. C'est la raison de l'épinglage par digest : un tag mobile rejouerait ça sur des données clients. - `SizeBytes` **existait déjà** sur `Resource`, contrairement à ce qu'annonçait le plan médias — mais jamais renseigné (`= 0` partout). Le backfill (point 5) reste entier. - `ResourceDTO` volontairement non modifié : exposer les nouvelles colonnes désynchroniserait `manager_api_new`, qui s'édite à la main. L'UI est V2. - pgvector **0.8.6** et non 0.5 : les *iterative index scans* existent (`hnsw.iterative_scan`), vraie parade au post-filtrage HNSW là où le plan ne prévoyait que de monter `ef_search`. À régler au lot 3. ### ⚠️ Dette ouverte — aucun test ne couvrira le RAG Ajouter `ContentEmbedding` a cassé **116 des 124 tests** d'un coup : ils tournent sur **EF InMemory**, qui ne connaît pas le type `Vector` et refuse de valider le modèle. Contournement appliqué — l'entité est exclue quand le provider n'est pas Npgsql (`Database.IsNpgsql()` dans `OnModelCreating`). Les tests repassent, mais **la conséquence est structurelle** : vector store, recherche cosinus, index HNSW, contrainte anti-doublon Hangfire — rien de tout ça n'est testable tant que la suite ne tourne pas sur un vrai Postgres. C'est la dette « tests EF InMemory peu représentatifs » du §3, qui cesse d'être théorique : elle rend le cœur du lot 3 non couvert. Piste : Testcontainers (`Testcontainers.PostgreSql`) sur l'image `Dockerfile.postgres`, au moins pour les tests du vector store. Pas chiffré, pas planifié. ### Lot 3 — Le RAG · 1 à 2 semaines → **[v2/rag-pgvector-integration-plan.md](v2/rag-pgvector-integration-plan.md)** § « Ensuite — incrément indépendant, à froid » `IVectorStoreService` (delete-then-insert transactionnel) · jobs Hangfire sur les saves de `SectionController`, queue dédiée · **respect d'`IsActive` avec purge des embeddings à la désactivation** (non négociable) · endpoint `/ask`, recherche cross-lingue sans filtre de langue avec bonus de score, réglage du post-filtrage HNSW, **sources renvoyées dans tous les cas**. > **Le point de charge caché du chantier.** `GetEmbeddableText()` est à écrire pour chacun des **13 sous-types de section** — ingrat, sans difficulté technique, mais c'est lui qui détermine la qualité des réponses : ce qu'on n'extrait pas, le guide ne le connaîtra jamais. > > Il se double de `GetReferencedResourceIds()`, réclamé par [v2/offline-visit-plan.md](v2/offline-visit-plan.md) — mêmes sous-types, mêmes structures parcourues. **À écrire dans la même passe.** Les faire séparément, c'est payer deux fois. > > ✅ **La passe commune a bien eu lieu** — vérifié le 2026-08-11 : `GetReferencedResourceIds()` existe dans les 13 sous-types de `Data/SubSection/`. Mais il n'est **appelé nulle part** : le switch de collecte de `ConfigurationController.Export` est toujours commenté en bloc (~180 lignes mortes). Le bug offline nº1 coûte donc le branchement d'une méthode existante, pas une passe sur 13 sous-types — il descend en coût, pas en priorité. > **✅ Pipeline d'ingestion livré le 2026-08-10.** `IIngestionService` / `IngestionService` : chargement des collections filles par sous-type (sans `Include`, `GetEmbeddableText` rendait un texte amputé de ses événements, étapes et questions **sans rien signaler**), un jeu de morceaux par langue renseignée, `ChunkIndex` continu toutes langues confondues pour respecter la contrainte d'unicité. Branché sur les trois chemins de `SectionController` (création, mise à jour, suppression), sur `AgendaSyncService.SyncSectionAsync` — après son `SaveChanges`, sinon on indexerait les anciennes dates — et servi par un **serveur Hangfire séparé** (`ingestion`, 2 workers) pour que les extractions ne prennent pas les workers de la file générale. > > **✅ Garde par plan.** `AiTokensPerMonth > 0` vérifié dans le job, pas seulement à l'enqueue : le plan peut changer entre les deux, et c'est ce contrôle-là qui décide de l'appel facturé. > > **✅ Rattrapage à l'upgrade branché le 2026-08-10.** `BackfillInstanceAsync` part automatiquement depuis `InstanceController.Updateinstance` quand `AiTokensPerMonth` passe de 0 à une valeur, et manuellement via `POST /api/Ai/reindex/{instanceId}` — **SuperAdmin uniquement**, avec le bouton correspondant dans l'écran Guide IA de `manager-app`. Réservé au support, volontairement : exposé au client, il serait cliqué à chaque réponse décevante du guide, pour un coût d'embedding complet et sans rien améliorer. > > ⚠️ **Le webhook Stripe n'est pas un point d'accroche**, contrairement à ce que supposait le plan : `checkout.session.completed` ne touche ni `SubscriptionPlanId` ni `AiTokensPerMonth`, il bascule `IsTrialActive` et stocke l'id d'abonnement. L'attribution de plan est manuelle. Le jour où le webhook changera le plan, il devra reprendre la même garde 0 → >0. > > 🐛 **`Updateinstance` ne recopiait pas les quotas du nouveau plan** — `CreateInstance` le faisait, pas lui. Passer un client de Starter à Premium changeait `SubscriptionPlanId` et rien d'autre : 1 Go de stockage et 0 jeton IA conservés. L'endpoint `/quota` masquait la moitié du problème en retombant sur le plan à la lecture, mais `AiController` lit `instance.AiTokensPerMonth` — **le client payait un plan avec IA et restait sans IA**. Extrait dans `ApplyPlanQuotas`, appelé par les deux chemins, avec test. > > **✅ `SearchKnowledge`** ajouté aux outils d'`AssistantService` — pas d'endpoint `/ask` séparé, qui aurait donné deux assistants concurrents : l'un sachant interroger l'agenda mais ignorant le contenu, l'autre l'inverse. Rend une phrase explicite quand la recherche ne trouve rien, sinon le modèle comble le silence avec ses connaissances générales. > > **✅ Deux angles morts du découpage corrigés le 2026-08-10.** Ils frappaient le même contenu — l'article, le texte le plus riche du CMS : > - **HTML retiré** avant l'embedding (`StripHtml`, remplacement par une espace + `HtmlDecode`). Les balises sont identiques dans tous les contenus : elles tiraient les vecteurs vers un fond commun et écrasaient les écarts de score, bien plus gênant que leur coût en jetons. Volontairement distinct du `StripHtml` d'`AssistantService`, qui remplace par `""` — correct pour de l'affichage, mais y accoler deux paragraphes fusionnerait leurs mots. > - **ligne plus longue que `MaxChunkChars` découpée** (`SplitLongLine`, coupe à la dernière fin de phrase, repli sur l'espace). Le HTML ne contient pas de `\n` : une fois les balises retirées, un article formait une seule ligne de toute sa longueur, dépassait l'entrée maximale du modèle, et l'échec emportait les 49 autres morceaux de son lot d'embedding. Symptôme : « le guide ne connaît pas mes articles », visible nulle part ailleurs que dans le dashboard `/hangfire`. > > ⚠️ Vérifier l'entrée maximale réelle de `gemini-embedding-001` dans la doc Google au moment du test de charge — `MaxChunkChars = 1200` (~300 jetons) a de la marge, mais la valeur n'a pas été confirmée. > > **✅ Downgrade tranché le 2026-08-10 : on conserve.** Le code purgeait les embeddings dès que l'instance perdait l'IA. Rétabli sur la décision du plan — quelques Mo de vecteurs ne pèsent rien, l'usage est bloqué en amont, et purger ferait payer une réindexation complète pour un incident de paiement réglé le lendemain. La purge reste sur les deux cas qui la justifient : section supprimée, section désactivée (`IsActive`). > > 🐛 **Trou de facturation trouvé et corrigé le 2026-08-10.** `CheckQuota` ne bloquait que si `quota > 0 && AiTokensThisMonth >= quota` : avec `AiTokensPerMonth = 0`, **aucun blocage et aucun compteur**. L'ambiguïté vient du code lui-même — `0` veut dire *illimité* pour `StorageQuotaBytes` mais *pas d'IA* pour `AiTokensPerMonth`. Comme `IsAssistant` est un drapeau manuel indépendant du plan (`InstanceController:197`), une instance `plan-starter` avec `IsAssistant = true` consommait de l'IA gratuite et non comptée. **Pas théorique** : `MigrationController` ne reprend pas `SubscriptionPlanId` (écart **c** du §1quinquies), donc toute instance migrée depuis Mongo arrive à 0 — rien ne s'indexe *et* l'IA tourne sans compteur. Corrigé en 403 avant tout appel au modèle, avec un test dédié. > > **✅ Déclenchement unifié le 2026-08-10 → [v2/rag-indexing-trigger-decision.md](v2/rag-indexing-trigger-decision.md).** Deux mécanismes concurrents coexistaient (un `Enqueue` par contrôleur *et* un intercepteur EF), ce qui doublait chaque enqueue et cassait 2 tests — `BackgroundJob.Enqueue` est statique et lève sans `JobStorage.Current`. **L'enqueue par contrôleur seul ne pouvait pas marcher** : mesuré, les 5 sous-contrôleurs totalisent **30 `SaveChanges` et 0 `Enqueue`**, et ils enregistrent l'entité fille sans jamais toucher la ligne `Section` (`SectionMapController:148`). Un client ajoutant 40 points d'intérêt à une carte n'aurait rien réindexé — précisément le contenu que les 13 `GetEmbeddableText()` savent extraire. Retenu : **`SectionIndexingInterceptor`** seul, avec sa table de résolution enfant → section et un filtre `{Order, DateUpdate}` qui évite de payer un embedding par section déplacée lors d'un réordonnancement. > **✅ Première exécution réelle — 2026-08-10.** Le pipeline a tourné contre la base locale et l'API Google, hors HTTP (harnais jetable, pas de contournement d'auth). **52 sections → 2150 morceaux en 53 s.** L'index se remplit, la recherche cosinus répond, le parcours itératif HNSW ne bronche pas, et le bonus de langue fait son travail : question NL → morceaux NL en tête, avec du contenu d'une autre langue qui remonte quand même. > > ⚠️ **Constat en passant, en données réelles** : l'instance était sur `plan-standard` (5 M jetons) avec `AiTokensPerMonth = 0` sur la ligne `Instance`. C'est le bug `Updateinstance` en vrai, pas en théorie — sans le correctif, la garde d'indexation aurait écarté les 52 sections en silence. > > **Deux défauts que seule l'exécution pouvait montrer, corrigés :** > - **Les gabarits étaient indexés.** `LanguageInit.Init` pose « FR - Title », « NL - Description » à la création d'une section, et ils restent tant que la langue n'est pas remplie. Résultat mesuré : les **cinq** premiers résultats d'une question en néerlandais étaient des `NL - Title NL - Description`. Le bonus de langue suffit à faire passer un gabarit vide devant du vrai contenu français — soit exactement le cas que la recherche cross-lingue devait servir. Filtrés à l'indexation (`WithoutPlaceholders`) : une langue qui n'a que des gabarits ne produit plus aucun morceau. −255 morceaux sur 2405. > - **Le même texte occupait trois des cinq résultats**, au même score : une description recopiée sur chacun des événements d'un agenda. `DistinctBy(Text)` avant le `Take(topK)` — sinon c'est 60 % du contexte du modèle gâché. > > Après correction, les cinq premiers résultats FR **et** NL sont du contenu réel et distinct. > > ⚠️ Restent non vérifiés : l'entrée maximale réelle de `gemini-embedding-001` (aucun morceau n'a atteint la limite sur ce jeu) et le comportement du post-filtrage HNSW **à plusieurs instances** — il n'y en a qu'une en base locale, ce qui est précisément le cas où le problème ne se voit pas. **Jalon démontrable en clientèle** : à ce point le guide répond correctement sur le contenu CMS seul. Tout le reste est de l'amélioration. ### Lot 4 — Le journal et le reste de l'écran · ~1 semaine → **[v2/guide-ia-screen-plan.md](v2/guide-ia-screen-plan.md)** §7 et § « Ensuite » Insertion `VisitorQuestion` dans `AiController.Chat`, à côté du `RecordUsage` existant · job de regroupement en thèmes · job de purge à 90 jours · onglet « Ce que demandent vos visiteurs » · aperçu de conversation (à faire converger avec le « Mode preview / cible Assistant-Persona » de [todo-features.md](todo-features.md), pas construire deux fois) · onglet « Vocal » dans les stats. **Volet RGPD dans les CGU avant mise en service.** ### ⚠️ Ce que les 4 lots ne couvrent PAS **Les lots 1 à 4 découpent le chantier Guide IA, pas tout le §1ter.** Le lot 2 a pris le point 4 (colonnes `Resource`) parce que c'est du schéma partagé avec le RAG, mais **les points 5, 6, 8 et 9 n'appartiennent à aucun lot** — ils relèvent du chantier médias & stockage, jamais découpé. Constaté le 2026-08-09 : le tableau du lot 2 liste `1, 2, 3, 4, 7` sans le dire. ### Lot médias — orphelin de lot jusqu'au 2026-08-09 → **[v2/media-storage-plan.md](v2/media-storage-plan.md)** § « État mesuré en base le 2026-08-09 » Ordre à respecter, sous peine de travailler deux fois : | # | Quoi | Note | |---|---|---| | 1 | `IStorageService` + **`StoragePath` écrit à `Create`** | La colonne existe mais **n'est affectée nulle part** : elle reste vide même pour les nouvelles ressources | | 2 | **`SizeBytes` renseigné à `Create`** | Aujourd'hui alimenté dans `MigrationController`, l'endpoint legacy `Upload` et `Update` — jamais dans `Create`, le chemin réel | | 3 | Backfill (§1ter point 5) | **`StoragePath` est un simple `UPDATE` SQL** (chemin déterministe) ; seul `SizeBytes` exige de lister Firebase | | 4 | Flag watermark (§1ter point 6) | Le `if instanceId == "633ee379…"` de `ResourceController:265` est intact, TODO d'origine compris | | 5-7 | Compression images, quota autoritaire, alerte budget GCP (§1ter 7-9) | Hors schéma, avant la release | ⚠️ **Le backfill ne porte pas sur « chaque ligne `Resource` ».** Mesuré : sur 45 lignes, **37** ont un blob, **7** sont des types URL (`ImageUrl`/`VideoUrl`/`JSONUrl`) sans aucun fichier — leur donner un `StoragePath` ou les compter dans le quota serait faux — et **1** est de type fichier sans URL du tout. `SizeBytes` vaut **0 partout** aujourd'hui : le quota de stockage ne veut rien dire pour personne. ### Restes non chiffrés, à caser en cours de route ~~Ratio jetons → questions (`/1000`) à caler sur du réel~~ ✅ **des deux côtés le 2026-08-13.** Serveur : `InstanceQuotaDTO.aiTokensPerQuestion`, mesuré sur les `VisitorQuestion.TokensUsed` de l'instance, avec un seuil de 20 questions avant de faire foi ; en dessous, repli sur l'hypothèse de la grille tarifaire. Front : `guide_ia_screen` lit ce diviseur au lieu du `/1000` codé en dur, et **masque la ligne « ~N questions »** si l'appel échoue. ⚠️ **Le `/1000` était faux d'un facteur 10** — la grille vend Premium à 20 M de jetons pour ~2 000 questions, soit **10 000 par question** — et c'est un chiffre montré au client : le crédit restant affiché était dix fois trop généreux. ⛔ ~~Endpoint de mise à jour d'`ApplicationInstance`~~ **n'a jamais manqué** (vérifié le 2026-08-13) : `ApplicationInstanceController` porte un CRUD complet (`POST :75`, `PUT :115`) et `InstanceController.Updateinstance` écrit déjà `IsMobile`/`IsTablet`/`IsWeb` (`:209-211`). Activer un canal est faisable par l'API depuis le début ; ce qui manque est l'**écran** SuperAdmin, parqué en V2 (voir §« Add-on IA »). ⛔ ~~Quotas seed 5M/20M~~ **déjà ajustés** par `AlignSubscriptionPlansWithPricing` : Essentiel 0, Pro 0, Premium 20 M, Enterprise `long.MaxValue`. Le « 5M » datait d'avant l'alignement. ~~`features.reqPerMonth` de la landing~~ ✅ corrigé, la clé n'existe plus dans `myinfomate-landing/src` (vérifié le 2026-08-11). ### Hors périmètre V1, assumé Ingestion documentaire (PdfPig + OCR + formats + UI `IncludeInAiKnowledge`, avec quota et `StoragePath` en prérequis), TTS pré-généré, guides multiples et thématiques, talking head, génération de visites. ### Décisions figées — ne pas rouvrir `gemini-embedding-001` à 768 dims · voix **Viva** (`Sulafat`) / **Marco** (`Umbriel`), ✅ **écoutées et validées le 2026-08-07** · découpage V1/V2 · pas de bascule « répondre avec ses connaissances générales » · sources visibles côté client, pas côté visiteur. **Ordre de grandeur total : ~3 semaines**, dont une bonne moitié dans le lot 3 et les 13 sous-types. ### 📍 ~~Reprendre ici~~ — périmé, conservé pour la trace (ordre arrêté le 2026-08-10) > ⛔ **Ne plus repartir d'ici : les trois étapes ci-dessous sont terminées** (A le 10/08, B le 10/08, C le 12/08), et le développement V1 est clos depuis le 2026-08-13. **Le point d'entrée est [v1-plan.md](v1-plan.md)** ; l'état du jour est au § « Les lots F et J sont clos » ci-dessus. Ce qui reste avant la prod est du test, du juridique et de l'infra — plus une ligne de code applicatif. Les lots 1 et 2 sont terminés. **Le lot 3 et le lot médias sont tous les deux obligatoires** — l'ordre ci-dessous ne trie pas par importance mais par coût de report. Ni l'un ni l'autre ne bloque plus la bascule : ce qui devait partir avec la migration, c'était le schéma, et il est en place. | Étape | Quoi | Pourquoi dans cet ordre | |---|---|---| | ~~**A**~~ ✅ 2026-08-10 | **`StoragePath` + `SizeBytes` renseignés à `Create`** (`ResourceController:340`, types URL 2/3/7 exclus) + `manager-app` envoie `sizeBytes` **avant** l'upload (`resources_screen.dart:210`) | Ce n'est pas un chantier, c'est une fuite : chaque upload créait une ligne incomplète de plus. Corrigée, le backfill de l'étape C est **définitif**. Reste non autoritaire : la taille vient du client (§4 point 2 du plan médias) | | **B** ✅ **exécuté** 2026-08-10 | **Lot 3 — le RAG**, livré *et* joué contre la vraie base + la vraie API Google : **52 sections → 2150 morceaux en 53 s**, recherche cosinus et bonus de langue vérifiés en conditions réelles. Voir le § « Première exécution réelle » ci-dessous | Le jalon démontrable en clientèle est atteint sur le contenu CMS. Reste le jugement produit — la qualité des réponses — qu'aucun test ne donnera | | **C** | **Reste du lot médias** : backfill des 37 lignes + inventaire des orphelins, watermark, compression, quota autoritaire, alerte budget | Le backfill exige de lister le bucket Firebase — plus long, et aucun intérêt à le jouer deux fois | **Amorce pour l'étape A**, utilisable telle quelle : > *« Lis `DOCS/v2/media-storage-plan.md` §5. Renseigne `StoragePath` et `SizeBytes` à la création d'une `Resource` dans `ResourceController.Create` — c'est le chemin réellement emprunté depuis la bascule Firebase, et aucun des deux n'y est écrit aujourd'hui. Le chemin est `pictures/{instanceId}/{resourceId}`. Ne rien écrire pour les types URL (`ImageUrl`, `VideoUrl`, `JSONUrl`) : ils n'ont pas de blob. »* **Amorce pour l'étape B** : > *« Lis `DOCS/STATUS.md` §1quater puis `DOCS/v2/rag-pgvector-integration-plan.md`. Le schéma, `IEmbeddingService`, `IVectorStoreService`, les 13 `GetEmbeddableText()`, le pipeline Hangfire et l'outil `SearchKnowledge` sont livrés et compilent. Reste à **faire tourner la chaîne pour de vrai** contre une base Postgres avec pgvector : vérifier qu'une section enregistrée produit bien ses morceaux, que le guide les retrouve, et régler les deux angles morts du découpage (HTML non retiré, ligne plus longue que la limite non coupée). »* --- ## 1sexies. Ordre d'exécution de toute la V1 — et le lot design (ajouté le 2026-08-11) > Ce fichier dit **ce qu'il reste**, par domaine. Le kanban dit **où en est chaque chantier**. Ni l'un ni l'autre ne disait **dans quel ordre**, ni ce qui bloque quoi. > > → **[v1-plan.md](v1-plan.md)** — 9 lots (A à I plus D-bis), 18 liens de dépendance, chemin critique, arbitrages en attente. **4 à 5 semaines**, portées à 5-6 avec le lot design. **Chemin critique** : A (assainir) → B (geler le schéma) → G (`MigrationController`) → H (tests) → **J (RGPD)** → I (bascule). C, D, D-bis, E, F se parallélisent. ### Lot K — les apps visiteur parlent l'ancien contrat (ajouté le 2026-08-12) **Un lot entier qui manquait, et il bloque la bascule.** Découvert en lançant le premier vrai `flutter build apk` sur `tablet-app` depuis des mois — la commande que trois documents disaient « à reconfirmer ». **Deux couches, pas une** : dix crans d'outillage Android (**corrigés le 12/08**, détail dans [v1-plan.md lot K](v1-plan.md) — Gradle 7.5 → 8.11.1, AGP 7.2.0 → 8.9.1, Kotlin 1.9.0 → 2.3.10, jcenter mort, heap Gradle relevée, Jetifier coupé, forçage d'`androidx.lifecycle` retiré), **puis 132 erreurs Dart** que le Gradle masquait depuis le début. ⚠️ **Le compte de « six crans » qui circulait 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. C'est le même piège que le tableau ci-dessus documente — un état intermédiaire pris pour un état final. **Les 132 erreurs tiennent en 3 causes** : `ConfigurationDTO.roundedValue`/`isDate`/`isHour`/`isSectionImageBackground`/`screenPercentageSectionsMainPage` ont migré vers **`AppConfigurationLinkDTO`** (73) · `GeoPointDTO.latitude/longitude` sont devenus **`GeometryDTO geometry`** avec PostGIS (40) · les `min()` sur `dynamic` en découlent (19). **`mymuseum-visitapp` a déjà fait ce portage** (`visitAppContext.currentAppConfigurationLink`, `Models/visitContext.dart`) : c'est une copie, pas une conception. Côté `visitapp-web`, le pendant existe aussi — le helper `getGeoPointLatLng()` de `src/lib/geo.ts`. ⚠️ **Ce qui en fait un bloquant de la bascule — lien L19, que le plan ne portait nulle part.** Les tablettes en service chez MDLF et au Fort parlent l'ancien contrat. Le jour J, elles ne s'arrêteraient pas proprement : cartes sans points, écrans dégradés, **aucune erreur visible**. Et le parc des téléphones visiteurs, lui, ne se force pas — d'où la stratégie de coexistence adoptée au lot I (deux serveurs, l'ancienne prod figée, aucun DNS qui bouge). **✅ K5 livré le 2026-08-12 — et le lot n'était pas ce que le plan décrivait.** Les **trois flavors** de `mymuseum-visitapp` (`dev`, `mdlf`, `fortsaintheribert`) construisent, exit 0, APK à l'appui. **La prémisse « son APK ne compile pas » était périmée** : 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 du 12/08 ont amené **`tablet-app` jusqu'à `mymuseum`** — il n'y avait pas la même migration à refaire ici. Le réflexe du diff noté ci-dessus a donné la réponse en deux `cat`. Restait le contenu réel du lot, 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** (sous le seuil 2.2.20 annoncé comme rupture par Flutter ; c'est la version de `tablet-app`, donc un alignement, pas une version neuve). Six builds — les trois flavors avant, les trois après. APK de **382 à 349 Mo**. ⚠️ **Un cran de plus existe, délibérément non pris.** Kotlin réglé, le validateur Flutter en révèle deux autres : Gradle 8.11.1 → 8.14.0 et AGP 8.9.0 → 8.11.1, « soon be dropped ». Même cascade que les dix crans de `tablet-app` — chaque correctif découvre le suivant. Arrêté là parce que 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 au lieu de les aligner. À faire d'un bloc sur les deux, ou pas du tout — c'est la dette d'outillage déjà parquée en V2. ✅ ~~**Question de branche, à trancher avant K6**~~ — **tranché le 2026-08-13** : `Meta-Rayban-Test` (et `AI-Assistant-test` côté `tablet-app`) **sont** les branches de travail à jour. On développe et on publie depuis elles, il n'y a pas de merge préalable à faire. Voir le §5bis, dont la description « POC jamais mergé » est ce qui avait fabriqué cette alerte. **Ce que K5 débloque** : **D0** (test-plan §21 sur device), donc la mesure des bugs offline D2-D5 et la confirmation du volet visiteur de D1 — qui ne tient aujourd'hui que sur `flutter analyze`. **✅ D2, D3 et D4 livrés le 2026-08-12**, sans attendre D0 (arbitrage du plan : défauts certains, pas des hypothèses à mesurer). `audio/mpeg` ajouté à la table d'extensions, purge des fichiers obsolètes réactivée, et filtre de fraîcheur livré de bout en bout (`dateUpdate` serveur → DTO → client généré → base locale v4). `dotnet test` **205/205**, `flutter build web` (manager-app) ✅, APK `dev` de `mymuseum-visitapp` ✅. **Reste D0 et D5.** ⚠️ **Deux prémisses du plan étaient fausses, corrigées dans `v1-plan.md`** : - **« Les deux chemins de téléchargement divergent »** (le motif `Create`/`Upload` de C1/C3, rapproché de D4) : **il n'y a qu'un seul chemin vivant.** Les lignes `328-461` de `downloadConfiguration.dart` sont dans un bloc commenté — le `cleanLocalResources(:455)` cité comme preuve est du code mort, et cette fonction n'est appelée **nulle part**. - **`cleanLocalResources` n'est pas réactivable telle quelle** : la table locale `resources` n'a **pas de colonne `configurationId`**, elle est globale à toutes les visites téléchargées. L'activer sur les ids d'une seule configuration aurait effacé les lignes des autres. Sans bénéfice, de surcroît : rien ne lit ces lignes pour le rendu, le fichier est retrouvé en listant le répertoire. ⚠️ **D2 ne réparait pas ce que la doc annonçait — vérifié dans le code le 2026-08-12.** « Une image remplacée dans le CMS ne remonte jamais sur le device » : **ce cas n'existe pas.** `Upload` génère un nouvel id à chaque téléversement, manager-app écrit dans `pictures/{instanceId}/{resourceId}` — remplacer une image donne donc **un nouvel id et une nouvelle URL**, que l'ancien filtre téléchargeait déjà. La popup d'édition ne change que le libellé. **Aucun flux ne réécrit le blob d'un id existant.** ✅ **Le vrai cas de péremption est celui que D3 vient de créer** : les MP3 déjà sur les devices y sont en `.unknown`, et « le fichier est présent » les déclarait à jour. **Sans D2, D3 ne réparait que les installations neuves** — le parc existant serait resté muet. `isResourceOutdated` traite `.unknown` comme absent. ⚠️ **Deux pièges tranchés en écrivant D2** : la montée v3 → v4 laisse les dates existantes à `NULL` et les considère **à jour** (backfiller à zéro aurait fait re-télécharger toutes les visites de tous les visiteurs sur leur réseau mobile) ; et la boucle en masse héritée de D1 **écrasait la date** que la boucle de téléchargement venait d'écrire, `DatabaseHelper.insert` faisant un UPDATE de la ligne entière. ⚠️ **Dette relevée au passage, non traitée** : les trois `lib/api/swagger.yaml` (manager-app, mymuseum-visitapp, tablet-app) sont **périmés** — leur `ResourceDTO` n'a ni `sizeBytes` (C1/C3) ni `dateUpdate`. Ce sont des artefacts de génération, et le client s'édite à la main : ils ne sont plus la source de vérité, mais ils la contredisent en silence. **✅ K3 et K7 livrés le 2026-08-12 — `flutter build apk --debug` vert, APK produit.** La borne couvre désormais **12 des 13 types** : `SectionArticle` (K7) et `SectionEvent` (K3) sont branchés dans `main_view.getContent`. Seul `SectionParcours` reste dehors, et **définitivement** — un parcours guidé fait marcher le visiteur. Les deux écrans suivent la maquette `DOCS/claude design/kiosk-paysage-article-event.html` : rail média ancré + colonne de lecture plafonnée pour l'article, bande héros à 26 % + programme + carte vive pour l'événement, détail d'un bloc **sous la carte** plutôt qu'en `showModalBottomSheet`. **Quatre choses que le code a dictées, contre ce que la maquette ou mymuseum annonçaient :** - ⚠️ **La barre de pied de la maquette n'appartient pas aux écrans.** `section_page_detail:184/198` dessine déjà le bouton retour, avec les clés `back`/`menu` selon `isFromMenu`. Les écrans qui auraient dessiné le leur en auraient affiché deux. La maquette est à corriger sur ce point. - ⚠️ **Un seul constructeur de marqueurs au lieu des quatre de mymuseum.** `MapAnnotationDTO` et `MapAnnotation` ont des champs **rigoureusement identiques** mais sont deux types Dart distincts — d'où la duplication là-bas. Normalisé ici par une classe interne à deux fabriques. C'est **L5** appliqué au front, et il porte d'autant plus que la convention `[lng, lat]` est l'endroit exact où une divergence **ne lève aucune erreur**. - ⚠️ **L'audio d'article se résout dans `contents` avant l'API.** `ContentDTO` porte son `resource` complet — idiome déjà utilisé par `marker_view` — donc l'audio est trouvé sans appel réseau quand il est embarqué, donc **hors ligne aussi**. `resourceGetDetail` n'est qu'un repli. Mymuseum appelle l'API systématiquement en ligne. - ⚠️ **Nouvelle clé i18n `event.live` dans les 10 langues.** Sans les 10, `getFromLocale` renvoie `""` et la pastille « en cours » rendrait une boîte vide. **FR, EN, NL, DE, IT, ES sont sûres ; PL, CN, UK et AR sont de ma main et demandent une relecture humaine.** ⚠️ **Ce qui n'est pas prouvé** : le rendu. Aucun des deux écrans n'a été vu à l'œil — build vert ne veut pas dire mise en page juste, c'est exactement la leçon du §1bis appliquée au design. Et **aucun `SectionEvent` n'existe en base** (le type est né avec Postgres v3) : c'est le §19.13 cas E qui en créera un. **Périmètre kiosk tranché le 2026-08-12** : `tablet-app` couvre 11 des 13 types — ⛔ **corrigé le 2026-08-12 : c'est 10, et le compte cachait un vrai manque.** Le `switch` de `main_view.getContent` traite Map, Slider, Video, Web, Menu, Quiz, Pdf, Game, Agenda, Weather. **`SectionArticle` (type 6) n'est pas traité** : son `case` est commenté « TODO » depuis l'origine et `Screens/Article/article_view.dart` fait 1 Ko. Un article tombe donc dans le `default` — « Ce type n'est pas supporté ». Contrairement à `SectionEvent` et `SectionParcours`, **ce type-là existait dans Mongo** : c'est un vrai retard, pas un type né avec Postgres v3. À trancher — le supporter ou l'assumer — mais pas à compter comme couvert. `SectionParcours` est **exclu définitivement** (un parcours guidé fait marcher le visiteur — pas de sens sur borne fixe) ; `SectionEvent` est **à supporter** (borne d'accueil affichant le programme du jour, §19.13 cas E). Ni l'un ni l'autre n'est un retard : **les deux types sont nés avec Postgres v3**. S'ajoute le renommage `SectionPuzzle` → `SectionGame` et le nouveau `SlidingPuzzle`, à récupérer depuis `mymuseum-visitapp/lib/Screens/Sections/Game/` — sa version est moins buguée que le `Puzzle/` de tablet-app. **Ce que ça coûte** : +3 à 5 jours. Ce n'est pas du périmètre ajouté, c'est du travail déjà dû que personne n'avait vu. ✅ **`tablet-app` construit à nouveau — `flutter build apk --debug` vert le 2026-08-12.** K1, K2 et K4 sont livrés et **prouvés par un build**, pas seulement par `flutter analyze`. L'outillage a demandé **dix crans** (le compte de « six » écrit plus tôt dans la journée datait du moment où la chaîne Gradle passait, avant que la compilation Kotlin ne révèle la suite) — tableau complet dans [v1-plan.md lot K](v1-plan.md). ⚠️ **Trois des quatre derniers crans étaient de simples alignements sur `mymuseum-visitapp`** : `-Xmx4096M` au lieu de 1536M, `enableJetifier=false`, et surtout **le retrait d'un `resolutionStrategy` qui forçait `androidx.lifecycle` à 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). J'ai cherché ces causes une par une alors qu'un `diff` des deux `gradle.properties` et des deux `build.gradle` les donnait toutes d'un coup. **Réflexe pour K5 : commencer par ce diff.** **✅ K4 livré le 2026-08-12** — les 6 fichiers de `mymuseum-visitapp/lib/Screens/Sections/Game/` repris dans `tablet-app/lib/Screens/Game/`, `Screens/Puzzle/` supprimé, `main_view` sur `SectionType.Game`. La tablette gagne le **puzzle glissant**, le bouton d'indice et un dimensionnement au ratio de l'image. ⚠️ **Un choix de rendu à valider à l'œil** : mymuseum code ses couleurs en dur par flavor client (rouge MDLF, bleu Fort) ; `tablet-app` n'ayant pas de flavor — un seul APK, la charte vient de la configuration — le dégradé est **dérivé de `configuration.primaryColor`**. Le rendu diffère donc de la version mobile. **✅ K2 livré le 2026-08-12 — les 132 erreurs de contrat sont tombées.** `flutter analyze lib` ne renvoie plus que 6 erreurs `PuzzleDTO`, qui relèvent de K4. Livré : `TabletAppContext.currentAppConfigurationLink`, `lib/Helpers/geo.dart` (extension `lat`/`lng` + `coordinateKey`), 67 accès `roundedValue` rebranchés, `main_view` et les 4 vues carte portées. **Deux bugs latents trouvés au passage — la raison pour laquelle un chercher-remplacer aurait été dangereux :** - ⚠️ **`applicationInstanceDTO` servait de drapeau d'assistant.** Il n'était assigné **que si** `instanceDTO.isAssistant == true` (`main_view:339`). Ce DTO portant désormais les `AppConfigurationLink`, le nouveau getter aurait renvoyé `null` sur **toute instance sans IA — donc MDLF et le Fort Saint-Héribert** — et `roundedValue`, `isDate`, `isHour`, `isSectionImageBackground`, `screenPercentageSectionsMainPage` seraient tous retombés sur leurs valeurs par défaut. **Sans erreur, sans log** : des écrans qui ne ressemblent plus à ce qui est configuré. Corrigé par une assignation inconditionnelle et un drapeau explicite `isAssistantEnabled`, qui **préserve le ET** instance × canal — les deux gardes qu'on retrouve côté serveur en `AiController:315` et `:360`. - ⚠️ **Le filtre « configurations pour tablette » n'avait plus de source.** `ConfigurationDTO.isTablet` a disparu : le canal est porté par l'`ApplicationInstance` et le rattachement vit dans ses `AppConfigurationLink`. `getConfigurations` filtre désormais là-dessus. **À 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é — donc à revérifier sur les 4 instances. ⚠️ **Troisième piège de vérification de la journée** : `flutter analyze` écrit `error - `, pas `error • `. Un grep sur la mauvaise forme a fait conclure « 0 erreur » alors qu'il en restait 10. Après le `| tail` qui masque le code de sortie, c'est le deuxième faux vert obtenu par un filtre mal formé — **toujours compter les erreurs en relisant le log complet**. #### ⚠️ Deux conventions de coordonnées coexistent — vérifié le 2026-08-12, ne pas « harmoniser » à l'aveugle Le projet écrit les coordonnées **dans deux ordres différents selon le producteur**. Les deux sont en production, et **une confusion ne lève aucune erreur** : elle place les points ailleurs, sur une carte qui reste plausible. | Donnée | Ordre | Écrit par | Lu par | |---|---|---|---| | **`GeoPointDTO.geometry`**, et toute géométrie passée par `GeometryMapper` | **`[lng, lat]`** — GeoJSON standard | `GeometryMapper.ToDto` sérialise `{ point.X, point.Y }` ; `MigrationController:798` construit `new Point(lon, lat)` | `mymuseum-visitapp/Models/agenda.dart:72` lit bien `coords[1]`=lat, `coords[0]`=lng · `tablet-app/lib/Helpers/geo.dart` (créé le 12/08) | | **`GuidedStep.geometry`** | **`[lat, lng]`** — inversé | `manager-app`, `map_geometry_picker` | `visitapp-web/src/lib/geo.ts`, qui le documente explicitement | **Pourquoi on ne corrige pas** : l'ordre inversé des GuidedStep est déjà écrit en base. Le redresser demanderait une migration de données **et** une reprise simultanée de tous les lecteurs — pour un gain purement esthétique, sur un chantier qui n'en a pas besoin avant la bascule. **Ce qu'il faut faire à la place** : ne jamais recopier un helper de coordonnées d'un contexte à l'autre. `getStepGeometryCenter` (visitapp-web) et `GeoPointPosition` (tablet-app) se ressemblent et sont **incompatibles**. Chaque helper porte sa convention en commentaire — les lire avant de s'en servir. ⚠️ **À rouvrir si SectionParcours arrive un jour dans une app qui lit aussi des GeoPoint** : elle aurait alors les deux conventions dans le même écran. C'est aujourd'hui le cas de `visitapp-web` et de `mymuseum-visitapp` — vérifié : chacun utilise le bon helper pour la bonne donnée, mais c'est l'endroit exact où une future erreur se logera. **D1 — le bug offline nº1 est corrigé (2026-08-11).** Le `switch` de collecte des ressources était commenté **des deux côtés** : 157 lignes mortes dans `ConfigurationController.Export`, 126 dans `Import`, et le pendant dans `downloadConfiguration.dart`. Une visite téléchargée n'embarquait donc que l'image de la configuration, celle du loader et l'image de chaque section. Remplacé par `GetReferencedResourceIds()`, déjà implémentée sur les 13 sous-types. Côté import et côté client, la charge est enregistrée en une passe au lieu d'être redécouverte section par section — ce qui répare au passage la purge des fichiers obsolètes, pilotée par `usedImageOrAudioIds`. **4 tests**, dont un qui vérifie par réflexion que les 13 sous-types implémentent la collecte : le trou venait d'un `switch` où un type oublié passait dans le `default` sans bruit. `dotnet test` **148/148**. ⚠️ Le volet visiteur ne tient que sur `flutter analyze` — l'APK de `mymuseum-visitapp` ne compile pas (Gradle/NDK, §1bis). **À confirmer sur device au §21.** **Lot G — les 8 écarts sont clos et le dry run est joué (2026-08-11).** `dotnet build` vert, `dotnet test` **143/143**. **Trois des huit n'existaient pas.** Le plan les décrivait en « perte de données » ; l'export Mongo dit qu'il n'y a rien à perdre : - **(a)** `BuildSection` couvre exactement les types présents : les 327 sections de l'export ne contiennent que les valeurs 0 à 10. `SectionEvent` et `SectionParcours` sont nés avec Postgres v3. - **(f)** `IsQRCode`/`IsSearchText`/`IsSearchNumber` n'existent pas dans Mongo — `false` est le bon défaut. Contrôle inverse fait : les cinq réglages qui *existent* (`IsDate`, `IsHour`, `IsSectionImageBackground`, `RoundedValue`, `ScreenPercentageSectionsMainPage`) sont bien mappés sur `AppConfigurationLink`. - **(g)** tombé avec le rename du lot B. **Recette de bascule** : [test-plan.md §22](test-plan.md) — comparaison contenu par contenu entre l'app d'aujourd'hui et la base migrée, à jouer **pendant que Mongo est encore lisible**. Reliée depuis I7. **Dry run** : automatisé en test (`MigrationDryRunTests`, se saute sans Mongo), joué sur l'export du 1ᵉʳ avril. Il a trouvé du premier coup **20 sections manquantes avec `Erreurs : 0`** — elles référencent 2 configurations supprimées dans Mongo (contenu MDLF). Pas un défaut de migration, mais un silence : elles sont désormais signalées dans `Skipped`. ⚠️ **Décision produit en attente** : recréer les 2 configurations avant la bascule pour récupérer ce contenu, ou acter sa perte. ⚠️ **À rejouer sur un dump frais** avant le jour J — l'export a 4 mois et la prod tourne encore sur Mongo. **Les cinq vrais, corrigés** : - **(b)** mesuré : **5 sections quiz portent 41 questions**, toutes jetées. Migrées avec leurs réponses, en `MultipleChoice` (le type de validation n'existait pas dans l'ancien modèle). Les agendas n'ont aucun événement dans Mongo et les parcours n'y existent pas : leurs collections vides sont correctes. - **(c)** tout est généré ou dérivé, cf. v1-plan. - **(d)** Mongo n'a **pas de champ Role** : le `ContentEditor` en dur dégradait les 10 utilisateurs en silence. Passé à `InstanceAdmin`. ⚠️ Aucun `SuperAdmin` n'est créé par la migration. - **(e)** passe par `ResourceStorage` (L5) ; `StoragePath` n'était pas écrit du tout. L'échec du `HEAD` remonte désormais dans le rapport. - **(h)** transaction unique, et **500 au lieu de 200 OK** sur échec fatal. **Lot F — le volet manager-app est livré le 2026-08-12 : écran d'audit log et compteur d'utilisateurs.** `flutter build web` ✅, `flutter analyze` propre sur les dossiers travaillés. **`manager_api_new` n'a pas été touché** — l'écran passe par un `http.get` direct au Bearer, comme `_loadKnowledge` / `_loadInsights` / `_reindex` du Guide IA : une lecture seule ne justifie pas d'étendre un client qui s'édite à la main, et le déclencheur de génération n'existe plus depuis le lot A. - **Écran d'audit** : `Screens/Audit/audit_screen.dart` + `audit_entry.dart`, entrée de menu `menuId: 13` conditionnée à `role.value == 0` — même règle que la policy `SuperAdmin` de l'endpoint. Filtres instance / type d'entité / utilisateur / plage de dates, pagination 50, clic sur une ligne → détail avant-après en table. Les ids d'instance et d'utilisateur sont résolus en noms via `instanceGet()` / `userGet()`, un id inconnu de l'annuaire restant affiché tel quel : le journal doit rester lisible après la suppression de ce qu'il décrit. - **Compteur utilisateurs** : « X / 5 » et bouton d'ajout désactivé au plafond. **Le SuperAdmin en est exclu** — son `GET /api/User` renvoie toutes les instances, compter cette liste contre un plafond *par instance* n'aurait aucun sens. Le point « masquer le rôle SuperAdmin à un InstanceAdmin », resté en question dans `todo-features.md`, **était déjà fait** (`_allowedRoles` filtre sur `r >= callerRole`) : vérifié, pas réimplémenté. - **Trois pièges relevés en câblant, à ne pas re-découvrir** : `AuditController` renvoie les entités **brutes**, pas un DTO (champs d'`AuditLog` en camelCase) ; `invokeAPI` **ne lève pas** sur un code d'erreur et son résultat était ignoré dans `users_screen`, donc un e-mail déjà pris (409) ne produisait aucun message — corrigé, ce qui rendra le futur 422 du plafond visible sans retouche ; et `DropdownButtonFormField` **ne relit pas `initialValue`** sur reconstruction (`FormField.didUpdateWidget` ne traite que `forceErrorText`), donc « Réinitialiser les filtres » vidait la requête sans vider l'affichage — remplacé par un `DropdownButton` piloté. ✅ **Les deux dettes backend qu'il avait ouvertes sont fermées le 2026-08-12** — voir le volet `manager-service` ci-dessous. **Lot F — le volet `manager-service` est livré le 2026-08-12 : les quatre chantiers de dette backend.** `dotnet test` **163 → 200**, aucun test sauté. Quatre commits, `manager-service` seul. ✅ **Volet `manager-service` refermé le 2026-08-13** — Stripe Customer Portal (backend) et ratio jetons → questions calé sur du réel. **6 tests**, `dotnet test` **211 : 197 passés, 14 sautés** (Testcontainers sans démon Docker), 0 échec. **Trois des cinq points restants étaient déjà faits** : rate limiting, endpoint `ApplicationInstance`, quotas seed — détail au §« Restes non chiffrés » ci-dessus et dans [v1-plan.md lot F](v1-plan.md). > ⚠️ **Ce qui reste du lot F n'est plus dans `manager-service`** : le bouton du portail et le diviseur `aiTokensPerQuestion` dans `manager-app` (petit), et surtout les trois chantiers `mymuseum-visitapp` — déclenchement proactif (+ M3), miroir de la conversation vocale, émission des événements vocaux. Plus l'aperçu de conversation (**L9**), indépendant. --- ### 🎉 Les lots F et J sont clos le 2026-08-13 — il ne reste plus de développement V1 > Tout ce qui suit a été livré dans la même journée, sur 5 repos. `dotnet test` **229** (213 passés, 16 sautés faute de démon Docker), APK `dev` de `mymuseum-visitapp` ✅, `flutter build web` de `manager-app` ✅, `npm run build` de `visitapp-web` ✅, `flutter analyze lib` **sans erreur** sur les deux apps Flutter. **Lot F** — Stripe Customer Portal (backend + bouton), ratio jetons → questions mesuré, déclenchement proactif, **M3**, miroir de la conversation vocale, `conversationId`, émission des événements vocaux. **L9 n'était pas un chantier** : la « cible Assistant / Persona » du Mode preview est ce que le §5 du Guide IA a livré le 11/08 — un malentendu entre deux documents, pas du travail. **Lot J** — table d'agrégats de thèmes, job de regroupement quotidien, interrupteur de collecte par instance (avec son UI), mention d'information dans les **deux** apps visiteur, purge du journal d'audit. **D5** est livré avec eux. **Les cinq pièges de la journée, à ne pas re-découvrir :** 1. ⚠️ **La garde du proactif recommandée par le plan aurait coûté de l'argent.** `_trigger` appelle le LLM **avant** de parler, et le `?.` sur l'orchestrateur avale le cas « aucun mode vocal actif » : un visiteur traversant une zone avec l'app en poche et le vocal éteint consommait des jetons facturés au client pour une phrase que personne n'entend. La garde interroge l'orchestrateur, pas le matériel. 2. ⚠️ **Un écran peut mentir sans erreur.** Sans état d'échec, une visite incomplète restait sur « téléchargement en cours » **indéfiniment** — le compteur n'étant incrémenté qu'en cas de succès, il n'atteignait jamais le total. Le bug d'origine (D5) était le `print` avalé ; celui-là était pire et n'était écrit nulle part. 3. ⚠️ **Le choix de voix du client n'était pas honoré.** Le sélecteur Viva/Marco existait depuis le 07/08 et écrivait `GuideVoiceId` ; l'app visiteur ne lisait jamais ce champ. **Même forme que W1.** Et aucun APK ne parlait avec la voix du produit, `GEMINI_API_KEY` n'étant injectée nulle part — repli **silencieux** sur la voix système. 4. ⚠️ **Trois écrans journalisaient de fausses questions de visiteur** : le mode proactif (son prompt machine) et l'aperçu de conversation (le gestionnaire testant sa personnalité). Le drapeau s'appelle `IsVisitorQuestion`, défaut `true` — un client publié qui ne l'envoie pas continue de journaliser. 5. ⚠️ **Le même code copié trois fois porte sa décision une seule fois.** `_toLangCode` limitait le vocal à FR/NL/EN/DE dans les trois copies, mais ne le **disait** que dans une : les deux autres ressemblaient à un oubli à corriger. Idem pour les trois `AssistantService`, qui coupaient la conversation en morceaux. **Ce qui reste, et qui ne s'écrit pas en code** : la relecture juridique du §8 des CGU, les conditions réelles de Google, le DPA séparé, et les traductions relues de la mention visiteurs (FR/NL/EN aujourd'hui, repli anglais assumé). Puis **K6**, **K9**, le **lot H** (tests, dont D0 sur device) et le **lot I**. **1. Le journal d'audit voyait tout sauf le contenu.** `AuditedTypes.Contains(entry.Entity.GetType())` exigeait l'égalité **exacte** de type, or `Section` est **abstraite** : le type runtime est toujours `SectionMap`, `SectionQuiz`… Aucune des 13 sortes de section n'était journalisée — soit précisément ce que l'écran du 12/08 devait tracer. `Resource`, `Configuration`, `Device`, `User` et `Instance` passaient, eux : ils sont concrets, et **c'est ce qui rendait le trou invisible**. Remplacé par une remontée à la classe de base auditée (`IsInstanceOfType`). > **Tranché : normalisation côté serveur.** `EntityType` porte `Section`, pas `SectionMap`. Trois raisons : le filtre « Section » **déjà présent** dans l'écran se met à rendre des lignes sans toucher `manager-app`, donc sans coordonner deux repos ; élargir le filtre front aurait coûté 13 entrées de liste et 39 clés i18n **et** laissé tout futur sous-type sortir du filtre en silence — la classe de panne exacte qu'on ferme ici ; et le sous-type concret **n'est pas perdu**, le discriminateur TPH est une propriété du modèle, donc sérialisée dans `NewValues` (`"Discriminator": "Article"`). Un test le vérifie — ce n'était pas une supposition. 8 tests, dont un **par réflexion** qui affirme que les 13 sous-types concrets résolvent vers `Section` : la liste de types d'origine n'obligeait personne à la suivre, c'est ce qui l'a laissée devenir fausse. **2. Le plafond de 5 utilisateurs existe enfin côté serveur.** `CreateUser` rend **422**. Deux décisions, écrites dans le code : **5 en dur** — le faire varier par plan serait une colonne sur `SubscriptionPlan`, donc une migration après le gel du lot B, **dette V1 assumée** ; et **SuperAdmin non soumis au plafond**, seule porte de service tant qu'aucun champ ne permet de relever la limite d'un client, ce qui s'aligne sur le front qui ne lui montre déjà pas le compteur. L'inscription self-service n'est pas concernée (premier utilisateur d'une instance neuve). Rien à retoucher côté front : le résultat d'`invokeAPI` est déjà lu depuis le correctif du 409. **3. `GetSummary` agrège en SQL** au lieu de charger 13 mois en mémoire. Les six agrégats qui se lisent dans le JSON de `Metadata` ne remontent plus que **deux colonnes**, et seulement pour leur type d'événement. Effet de bord voulu : les stats avancées ne sont plus calculées puis effacées pour les plans qui n'y ont pas droit. Un changement de sémantique délibéré : « la langue de la session » était prise sur le premier événement rendu par la base, **dans un ordre indéfini** — c'est désormais le plus ancien horodatage. ⚠️ **La branche « stats avancées » n'était couverte par aucun test** : les cas existants ne seedent pas d'`Instance`, donc `hasAdvancedStats` était toujours faux et la moitié de la méthode n'était jamais exécutée. 11 tests ajoutés, **plus 4 contre un vrai Postgres** — le provider InMemory évalue tout côté client, il ne dit rien de la traduction SQL et une requête intraduisible y passerait au vert. **4. Le vector store est éprouvé à deux instances, sur un vrai Postgres.** 14 tests Testcontainers sur l'image de `Deployment/Dockerfile.postgres`. Ils se sautent proprement (`SkippableFact`) sans démon Docker — vérifié : **186 passés / 14 sautés / 0 échec**. Établi : pgvector **0.8.6** (donc `SET hnsw.iterative_scan`, posé à chaque recherche par `SearchAsync`, est accepté — sur une version antérieure **toute recherche échouait en production**) ; extensions `vector` et `postgis` bien créées par les migrations ; et à deux instances déséquilibrées 30 contre 1, `SearchAsync` rend le bon nombre de résultats, tous de la bonne instance, aucune fuite. > ⚠️ **Deux résultats contraires à ce que le plan supposait — à ne pas re-supposer à l'envers.** > > - **Ce qui protège du post-filtrage n'est pas le parcours itératif, c'est l'index sur `(InstanceId, ContentType)`.** Le planificateur filtre par instance d'abord et trie exactement : l'index HNSW n'est jamais touché, donc il n'y a rien à post-filtrer. Vérifié à 620 lignes (test) et à **22 000** hors suite, avec 2 000 lignes pour l'instance cible. Un test fige ce plan — si cet index disparaissait, la recherche se dégraderait **sans qu'aucune erreur ne le dise**. > - **Et cet index retiré, `hnsw.iterative_scan = relaxed_order` ne rattrape rien** : le parcours s'épuise après ~335 lignes (`Rows Removed by Filter` à l'`EXPLAIN ANALYZE`) sans atteindre l'instance minoritaire, et rend zéro. Le même jeu de données avec l'index HNSW construit **après** l'insertion rend bien ses 20 lignes : c'est la **connectivité du graphe** qui décide, et nos migrations créent l'index sur une table vide. Le SET est correct et sans coût, on le garde — mais il ne couvre pas ce qu'on croyait. **Outillage, pour ne pas le redécouvrir** : `Testcontainers.PostgreSql` est épinglé en **3.10.0** — la 4.x parle l'API Docker 1.44 et l'engine local plafonne à 1.43. Et l'image est construite par le **CLI docker**, pas par le constructeur d'images de Testcontainers : celui-ci relit le `FROM` pour pré-tirer l'image de base et ne sait pas parser `tag@sha256:`. Ce digest protège la base d'un changement de glibc sous ses index — il ne se retire pas pour arranger un test. **5. Le journal ne suit plus les écritures de la machine — régression du point 1, corrigée le même jour.** `WeatherSyncService` écrit `section.WeatherResult` sur cron, à **6 h et 13 h**. Le job était bénin tant que les sections n'étaient pas auditées ; depuis, chaque rafraîchissement produisait une ligne portant la **prévision OpenWeather complète en avant ET en après** — quelques dizaines de Ko, deux fois par jour, par section météo, indéfiniment. Le stockage est le moindre problème : **ce bruit noie les modifications humaines** que l'écran d'audit existe pour montrer. Correctif : une liste de colonnes machine (`WeatherResult`, `WeatherUpdatedDate`, `DateUpdate`) exclues du journal, et **aucune ligne produite** quand une modification ne touche qu'elles — plutôt qu'une ligne au diff vide, qui aurait déplacé le bruit sans le retirer. `DateUpdate` y est pour une raison distincte de la météo : estampillé à chaque `SaveChanges`, il figurait dans tous les diffs sans jamais rien y apprendre. **Le signal était déjà là** — un test écrit le matin même devait l'écarter à la main (`newValues.Keys.Where(k => k != "DateUpdate")`) pour rester lisible. Quand un test doit filtrer une donnée pour être lisible, la donnée n'a rien à y faire. Vérifié au passage : `AgendaSyncService` écrit des `EventAgenda`, non audités, et ne touche pas la ligne `Section` — lui n'est pas concerné. > ⚠️ **Comment il a été trouvé, parce que la méthode compte plus que le bug** : en cherchant à justifier un index sur `Timestamp` que j'avais signalé par réflexe. La question « pourquoi un index ? » a montré que ce qui faisait grossir la table n'était pas ce que je croyais. ⚠️ **Deux dettes nouvelles, et une fausse alerte retirée** (détail dans [v1-plan.md lot F](v1-plan.md)) : 1. **`AuditLog` n'a aucune purge.** `VisitEvent` purge à 13 mois, `VisitorQuestion` à 90 jours, `AuditLog` à jamais. **C'est un sujet RGPD et rien d'autre** — la table porte `UserId`, et les valeurs avant-après d'une modification de `User` contiennent e-mail, prénom et nom. Ce n'est **pas** un sujet de volume, le flot machine étant coupé. **Donc à traiter au lot J.** Recommandation : **12 mois, uniforme**, avec le même verrou que les deux purges existantes — inerte tant qu'`Audit:RetentionDays` n'est pas défini, **le pg_dump quotidien n'étant toujours pas en place** : supprimer des lignes d'audit sans restauration fine est la pire combinaison. 2. **La bascule écrira ~2 700 lignes d'audit dans la transaction globale** de `MigrationController` (2 374 ressources + 307 sections + le reste), chacune avec un instantané JSON complet. Pas fatal, mais c'est neuf pour les sections et c'est la transaction qui ne doit pas échouer. À surveiller au prochain dry run. 3. ⛔ **`AuditLog` sans index sur `Timestamp` — fausse alerte, retirée.** Signalée par réflexe sur « la table va grossir » ; elle allait grossir à cause de la météo. Le flot machine coupé, il reste les éditions humaines — de l'ordre de quelques dizaines de milliers de lignes par an sur 4 clients, qu'un `ORDER BY Timestamp DESC LIMIT 50` trie en millisecondes sans index. **Pas d'index, pas de migration, gel du lot B intact.** **Lot C2 et C3 — livrés le 2026-08-12. Le lot médias est terminé côté serveur.** `dotnet build` vert, `dotnet test` **163/163** (148 au départ, +7 pour C2, +8 pour C3). **C2 — backfill.** `POST /api/Resource/backfill-storage?dryRun=&instanceId=`, SuperAdmin, **`dryRun` à true par défaut** : la migration se joue sur une base vide, ce backfill sur des lignes de production. Il rend son propre inventaire plutôt que de reprendre le « 37 lignes sur 45 » du plan, invérifiable d'ici — `Orphans` (aucune URL, blob peut-être jamais téléversé) et `Unsized` (URL présente, bucket muet) sont **deux causes distinctes**, jamais additionnées. - ⚠️ **La méthode annoncée était inapplicable.** Le plan disait « `SizeBytes` par listing du bucket Firebase » : le serveur n'avait aucun client de stockage. 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 pour `ResourceStorage` : deux copies auraient divergé sur ce qui compte, le sort réservé aux échecs. - ⚠️ **L'extraction a bouché un trou que personne ne cherchait.** L'original ne notait l'échec que dans son `catch`, or un HEAD sur un blob absent **ne lève pas** : il répond 404, sans `Content-Length`. Ces ressources arrivaient à 0 octet **sans figurer dans le rapport** — invisibles au quota et invisibles au diagnostic, exactement ce que le commentaire d'origine voulait empêcher. Corrigé des deux côtés ; `MigrationController` peut donc remonter quelques lignes de rapport de plus qu'avant, sur des cas réellement muets. **C3 — quota autoritaire.** Pré-vol sur les **deux** chemins de création, suppression du blob à `Delete`, angle mort d'`Update` tranché. - ⚠️ **Le pré-vol existait déjà à moitié, et ce n'était écrit nulle part.** `Upload` (multipart) contrôlait et renvoyait 413 ; `Create` (JSON) ne contrôlait rien — or **c'est le chemin qu'emprunte manager-app**, qui crée la ligne puis téléverse. Tout dépassement passait par la porte de service. - ⚠️ **Les deux lectures du quota divergeaient.** `Upload` lisait le quota du **plan**, `GetQuota` celui de l'**instance** avec le plan en repli. Une instance à quota surchargé — le mécanisme même de l'add-on — affichait un chiffre à l'écran et se faisait bloquer sur un autre. Fermé par `Helpers/StorageQuota`, désormais seule source de vérité pour les deux. - **`Delete` supprime le blob AVANT la ligne**, et un échec renvoie **502** en conservant la ligne. Le choix se résume ainsi : une ressource encore listée se rattrape, un blob dont plus aucune ligne ne porte le chemin est facturé sans que rien ne le désigne. Non configuré, le service renvoie `NotConfigured` — il ne fait jamais semblant d'avoir nettoyé. - ✅ **L'angle mort laissé ouvert par C1 était une fausse crainte.** On redoutait qu'un recalcul de chemin « pointe vers un objet qui n'a pas bougé ». Or `PathFor` ne construit 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`. **Aucun secret nouveau, contrairement à ce qu'on croyait en tranchant.** `FirebaseAdmin` était déjà référencé pour les notifications push et `Startup` charge déjà un service account via `Firebase:CredentialsPath` : `Google.Cloud.Storage.V1` réutilise le même `GoogleCredential`. Seule s'ajoute la clé `Firebase:StorageBucket`, **vide par défaut** — donc à renseigner en prod (I9), sans quoi `Delete` ne supprime rien. ⚠️ **Deux suites à ne pas perdre** : `manager-app` supprime toujours le blob de son côté (`show_resource_popup.dart:130-137`), après coup et en avalant l'échec — c'est devenu **du code mort** depuis C3, à retirer (C6), mais **pas avant** que `Firebase:StorageBucket` soit posé. Et `dotnet restore` échoue en 401 si la source NuGet `git.dev-espaces-naturels.lu` est déclarée dans l'image Docker : elle n'a rien à voir avec ce projet, et `Google.Cloud.Storage.V1` est le premier paquet ajouté depuis longtemps — le prochain build d'image la rencontrera. **Lot C1 — livré le 2026-08-11.** Calculateur `StoragePath`/`SizeBytes` extrait dans `Helpers/ResourceStorage.cs`, avec 13 tests (`dotnet test` **143/143**). Il ferme le lien **L5** : C2 (backfill) et l'écart (e) du lot G appelleront le même code au lieu d'en écrire deux. - ⚠️ **L'extraction a prouvé la divergence qu'elle devait empêcher.** `ResourceController` a **deux** chemins de création : le chemin **multipart** écrivait `SizeBytes` mais laissait **`StoragePath` nul**, seul le chemin JSON écrivait les deux. Le §1quater annonçait « `StoragePath`/`SizeBytes` écrits à `Create` depuis le 10/08 » — c'était vrai d'un chemin sur deux. Une partie des lignes à rattraper par C2 vient de là. - **Angle mort laissé ouvert sciemment** : `Update` peut faire passer `Type` d'un type URL à un type fichier, laissant `StoragePath` nul sur une ligne qui a désormais un blob. Recalculer le chemin sur une ressource existante pointerait vers un objet qui n'a pas bougé dans le bucket — à trancher avec C3 (quota). **Lot B — livré le 2026-08-11. Le schéma est gelé, le lot G est débloqué.** Une seule migration EF (`20260811130738_LotB_FreezeSchema`) : rename `SectionMap.MapResourceId` → `IconResourceId` (**l'écart (g) du §1quinquies tombe avec**), suppression de `SectionEvent.ParcoursIds`, et `Instance.IsImageWatermark` remplaçant le `instanceId == "633ee379…"` en dur de `ResourceController`. **130/130 tests avant comme après**, build Debug **et** Release verts. - ⚠️ **Le rename imposait aussi la propriété de navigation** `MapResource` → `IconResource` : la convention EF l'appariait au FK, la laisser en place aurait fabriqué un FK fantôme. Elle n'était utilisée nulle part ailleurs. - ✅ **Point de risque levé par vérification** : EF a généré un `RenameColumn`, **pas** un drop+add — les icônes déjà configurées survivent. L'avertissement « may result in the loss of data » ne porte que sur le `DropColumn` de `ParcoursIds`, qui est l'intention du lot. - ⛔ **« Supprimer `SectionEvent.IconResourceId` » était une erreur de doc — ne pas rouvrir.** `SectionEvent` n'a pas ce champ : la ligne visée appartient à la classe **imbriquée `MapAnnotation`**, partagée par SectionEvent, SectionAgenda et SectionMap, et lue par cinq contrôleurs plus les deux `SelectMany` de `GetReferencedResourceIds`. La supprimer aurait cassé les icônes d'annotation des trois types de section et la collecte offline. - **Sécurité rapide du lot A, dans la même passe** : le « backdoor `#if DEBUG` » était un bloc de `AuthenticationController.Authenticate` qui **écrasait l'email et le mot de passe reçus** par `test@email.be` — toute compilation Debug authentifiait n'importe quelle saisie. Retiré. `EnableSensitiveDataLogging` passe sous `#if DEBUG` (idiome déjà utilisé dans `Startup.cs` pour CORS et Hangfire ; le Dockerfile publiant en `-c Release`, c'est un verrou réel). - **Reste du volet sécurité, chiffré et non fait** : **112** retours `new ObjectResult(ex.Message) { StatusCode = 500 }` exposent le message d'exception interne, répartis sur 17 contrôleurs. Les 136 autres `ex.Message` sont des 400/404 volontaires, à garder. `Startup.UseExceptionHandler(HandleError)` existe mais ne traite que `RequestException`. C'est un chantier à part, pas une retouche. **Lot E — livré le 2026-08-11.** PDF en média d'étape (les trois volets : liste de types paramétrable, `ResourceViewer.tsx`, `CachedCustomResource`), comparaisons `QuestionType` par entier brut, et **W1 tranché : le web reste sur Leaflet** (option a — ni MapBox ni Google Maps JS, donc aucun coût récurrent ajouté avant la prod). ⚠️ **W1 ne pouvait pas se faire comme l'option le disait.** « Retirer le champ pour les instances web » est **impossible** : le fournisseur est porté par la **section** — `SectionMap.MapMapProvider` et `SectionAgenda.AgendaMapProvider` — pas par l'instance. Et une même `Configuration` est liée à plusieurs `ApplicationInstance` via `AppConfigurationLink`, donc servie au mobile **comme** au web : masquer le champ aurait supprimé le réglage mobile. Le champ est donc conservé, et manager-app cesse de laisser croire qu'il vaut pour le web (mention sous les deux sélecteurs). Côté `visitapp-web`, zéro ligne : Leaflet inconditionnel était déjà le comportement. ⚠️ **Piège de doc corrigé au passage** : le plan disait « cas PDF dans `getElementForResource` ». Cette fonction de `mymuseum-visitapp` fait un `return CachedCustomResource(...)` **avant** son `switch` — tout son switch est du code mort. Le vrai dispatcher est `CachedCustomResource`, et il en a **deux** (ressource distante / fichier local de l'offline). Suivre la doc à la lettre aurait produit un correctif inopérant et « vérifié » par une analyse verte. ### Add-on IA sur un plan sans IA — mode d'emploi et correctifs (2026-08-12) **Le mécanisme tient sans code.** `ApplyPlanQuotas` ne repart du plan **que si `SubscriptionPlanId` change** (`InstanceController:227`) : surcharger `Instance.AiTokensPerMonth` sur une instance Pro survit, et le compteur mensuel se réarme seul sur `AiUsageMonthKey`. C'est ce qui permet de donner l'IA à MDLF et au Fort Saint-Héribert **sans les passer en Premium** — nécessaire pour éprouver le post-filtrage HNSW sur 4 instances plutôt que 2 (**L14**). **Mais l'activation est une manip, pas un geste produit.** `AiTokensPerMonth` n'est exposé dans **aucun DTO** — `UpdateInstance` ne le lit jamais. Trois gestes, dans cet ordre : 1. `UPDATE "Instances" SET "AiTokensPerMonth" = …, "IsAssistant" = true WHERE "Id" = '…'` 2. `IsAssistant = true` sur les `ApplicationInstance` du client — **garde séparée** (`AiController:360`), celle qu'on oublie : l'instance est dotée, le chat refuse quand même 3. Le bouton de relance d'indexation de l'écran Guide IA (`POST /api/Ai/reindex/{id}`, SuperAdmin) — **obligatoire** : le rattrapage automatique est câblé dans `UpdateInstance` en C# (`:234`), un UPDATE SQL ne le déclenche pas, et le client paierait un guide qui ignore tout de son contenu déjà saisi ⚠️ **Changer son plan plus tard écrase la surcharge** et le remet à 0 jeton sans rien signaler. Pro → Premium est sans risque (Premium donne plus) ; tout autre mouvement casse l'add-on en silence. ⚠️ **Aucune valeur d'add-on n'est arrêtée.** Premium est à 20 M. Tant qu'un chiffre n'est pas fixé, deux clients payant la même chose n'auront pas le même quota. **Facturation : rien de neuf.** `StripeService` ne connaît qu'`EssentielPriceId` (`:56`) — Pro, Premium et Enterprise sont **déjà** facturés hors Stripe, à la main. L'add-on rejoint un processus existant, il n'ajoute pas de dette. **Deux correctifs livrés le 2026-08-12** — `dotnet build` vert, `dotnet test` **148/148** : - **`BackfillInstanceAsync` met en file un job par section** au lieu de les traiter en boucle. L'unité de reprise devient la section : avec `[AutomaticRetry(Attempts = 2)]` posé sur la classe, un échec d'embedding à la 280ᵉ section faisait retenter le job entier, donc **ré-embedder les 279 déjà indexées, deux fois** — des appels facturés pour rien et autant de chances de retomber sur la limite de débit. Le chemin de backfill cesse d'être un chemin à part : c'est celui de l'intercepteur. Effet de bord utile : l'avancement est visible section par section dans `/hangfire`. `IngestSectionCountingAsync` disparaît, son `int` ne servait plus. - **Backoff sur 429/503 dans `GoogleEmbeddingService`** : 3 tentatives, `Retry-After` s'il est fourni, exponentiel sinon. Absorbe la rafale courte sans jamais remonter jusqu'à Hangfire. > 📅 **Écran SuperAdmin d'activation — reporté en V2, décidé le 2026-08-12.** Exposer `AiTokensPerMonth` dans `InstanceDTO` ferait passer les trois gestes ci-dessus à un seul, et le backfill repartirait tout seul puisque `UpdateInstance` le déclenche déjà sur la transition `0 → >0`. Mais **la V1 n'a besoin d'aucun add-on** : les 4 clients de la bascule sont affectés à leur plan (§1quinquies, plans), et doter une instance à la main reste un geste interne, fait une fois, sur une base qu'on contrôle. Ce qui rend le report tenable, c'est que la procédure SQL ci-dessus est écrite : elle ne se redécouvre pas. > > **Ce qui ferait remonter ce chantier** : vendre l'add-on à un client réel **avant** la bascule. Une manip SQL sur une instance en production qui tourne n'a pas le même coût que sur une base de dev, et c'est là que le formulaire devient le moyen sûr. > > À faire d'un bloc, le jour venu, avec l'endpoint de mise à jour d'`ApplicationInstance` (canaux activables) — c'est **le même écran**. ⚠️ Mais seule la partie add-on part en V2 : **le volet canaux reste au lot F**, il est dans les restes non chiffrés de la V1. ### Contrôle du code du 2026-08-11 (2e passe) — ce qui a bougé depuis la rédaction du plan > Tout vérifié dans le code, pas dans les docs. Deux points du lot A tombent, un point du lot A est différé, un nouveau suspect apparaît. | Point | Ce que disaient les docs (matin du 11/08) | Ce que dit le code (soir du 11/08) | |---|---|---| | **Lot A étape 1 — committer** | « 44 fichiers modifiés dont 9 non suivis, prérequis absolu » | ✅ **Fait.** **9 repos** propres et synchronisés avec leur remote — `DOCS/` en est un, ce qu'on oublie en énumérant les repos de code. Voir §4 | | **Lot A — rotation des secrets** | Verrou L1, **ouvre** le lot A | 📅 **Différée en fin de backlog**, juste avant I3, à la demande du 2026-08-11 — repos privés auto-hébergés. **Reste le verrou de I3**, ne disparaît pas. Voir §3 | | **Lot A — nettoyage `manager_api_new`** | « 14 fichiers modèle orphelins » | ✅ **Diagnostic fermé** : 139 `part` déclarés pour 153 fichiers, les 14 sont hors graphe de compilation → suppression sans effet possible sur un build. Plus `openApiTest.dart`, qui est le déclencheur de génération. Voir §3 | | **Lot A — build tablet-app** | « échec Gradle, seul build cassé » | ❓ commit `5a6701d` (« drapeaux du migrateur Flutter ») a peut-être réglé le Gradle, à reconfirmer par un vrai build. **Le problème reste purement Gradle** : une piste Dart a été ouverte puis écartée le même jour — `marker_view.dart:456` référence bien `ContentGeoPoint` (classe hors graphe), mais **la ligne est dans un bloc commenté**, comme sa jumelle de `mymuseum-visitapp:367`. `flutter analyze` sur les deux fichiers : zéro erreur. Ne pas rouvrir cette piste | | **DB3 — écran Guide IA** | « 🔨 front livré » | ✅ **Confirmé livré des deux côtés.** Onglets par boutons custom (`guide_ia_screen.dart:249-265`) — pas un `TabBar`, ce qui explique pourquoi le grep du plan concluait « aucun onglet » : la coquille existe bien. Backend : `AiController` `knowledge/{instanceId}:190` et `insights/{instanceId}:232`, `VisitorQuestionPurgeService.cs` présent | | **Onglet « Vocal » des stats** | « reste à faire » | ✅ Confirmé absent. `statsChannelVoice` n'est qu'un **libellé de canal** (`statistics_screen.dart:189`), pas un onglet | | **Lot B — les 4 changements de modèle** | « rien fait » | ✅ Confirmé, aucun n'a bougé : `SectionEvent.ParcoursIds:28`, `SectionMap.MapResourceId:25`, hardcode watermark `ResourceController.cs:265`, et `MeterZoneGPS` **sans aucun usage visiteur** (zéro occurrence dans `mymuseum-visitapp/lib` hors swagger, zéro dans `visitapp-web/src`) — le bug M3 est réel | | **Lot C1 — calculateur extrait** | « à faire » | ✅ Confirmé à faire : `StoragePath` n'apparaît que dans `ResourceController` et `Resource.cs`, aucun service | | **Lot D1** | « méthode existe, appelée nulle part » | ✅ Confirmé : hors des 13 sous-types, `GetReferencedResourceIds` n'apparaît que dans sa déclaration abstraite (`Section.cs:85`) et un test | **RGPD reporté en fin de backlog le 2026-08-11** (lot J) — table d'agrégats de thèmes, job de regroupement, interrupteur de collecte par instance, mention affichée aux visiteurs, relecture juridique du §8 des CGU, vérification des conditions Google. C'est tenable **parce que** la porte « rien en prod avant la fin du backlog » tient : la journalisation ne tourne d'ici là que sur des bases de développement, aucun visiteur réel n'est concerné. Si un déploiement anticipé était décidé, ce lot redeviendrait bloquant. **Le lot B n'existait nulle part** : tout changement de modèle encore ouvert (rename `MapResourceId`, arbitrage `ParcoursIds`, unification `QuestionType`, flag watermark) doit passer **avant** la réécriture de `BuildSection`, sinon on écrit le mapping deux fois — et après la bascule, c'est une migration de données sur la prod. ### Lot D-bis — les trois écrans de design Constat du 2026-08-11, plus ouvert que ce que ce fichier annonçait : l'écran Guide IA « livré le 07/08 » était **5 cartes en colonne unique sans onglets**, les stats de l'assistant étaient **à zéro**, et la refonte SectionParcours n'avait pas commencé. **Cause identifiée** : les maquettes vivaient dans des artifacts d'un autre compte, illisibles depuis la machine de dev — chaque session redessinait. **Réglé** : le HTML est rapatrié dans `claude design/` (`guide-ia-screen.html`, `statistics-screen.html`, `sectionparcours-refonte-flux.html`). Même découpage que le kanban — contenu dans le repo, diffusion par artifact. | Étape | État | |---|---| | DB0 — rapatrier les maquettes | ✅ 2026-08-11 | | DB1 — socle visuel `constants.dart` | ✅ 2026-08-11 — 11 rôles typographiques, 8 espacements, 5 rayons. **Échelle des maquettes, couleurs de l'app** : un écran neuf se fond dans manager-app, il n'y ouvre pas une seconde palette. Pas de thème sombre | | DB2 — sauvegarde au fil de l'eau SectionParcours | ✅ 2026-08-11, **puis retiré le même jour par DB4**. Le garde-fou (confirmation d'abandon sur les trois dialogues + `PopScope`) a fermé la perte silencieuse le temps que la persistance arrive. Les trois dialogues n'existent plus et il n'y a plus de travail non enregistré à perdre : les 3 clés i18n sont supprimées. C'était l'issue prévue par l'arbitrage — les deux étaient alternatives, pas cumulables | | DB3 — écran Guide IA 2 onglets | 🔨 2026-08-11 — coquille à onglets, aperçu de conversation sur le **vrai** `/api/AI/chat`, onglet « Ce que demandent vos visiteurs » **alimenté** par `GET /api/Ai/insights/{id}`, carte « Ce que connaît votre guide » sur `GET /api/Ai/knowledge/{id}`, journalisation `VisitorQuestion` branchée dans `AiController.Chat`. 34 clés i18n FR/EN/NL. **Reste** : le job de regroupement en thèmes, la purge 90 j, **le RGPD dans les CGU avant mise en service**, l'onglet « Vocal » des stats | | DB4 — forme SectionParcours | 🔨 2026-08-11 — **option A livrée : une fenêtre, un rail d'étapes, profondeur 2**. Les trois `showNewOrUpdate…` sont supprimés au profit de `guided_path_editor.dart` (coquille : fil d'Ariane, rail réordonnable, panneau, pied « Enregistré ») et de trois widgets de champs autonomes — `ParcoursFields`, `EtapeFields`, `QuestionFields` dans `Parcours/Fields/`. Une question se **déplie dans le panneau de l'étape**, plus dans une 4ᵉ fenêtre.
**Sauvegarde à la saisie** : `guided_path_api.dart` masque le choix `sectionParcoursApi` / `sectionMapApi`, un débounce de 700 ms écrit le parcours (PUT), chaque étape (POST/PUT/DELETE) et ses questions avec elle. Le parcours est **créé à la première modification**, pas à l'ouverture — fermer une fenêtre neuve sans rien saisir ne laisse rien en base. **Deux pièges du backend** : `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 feraient un doublon) ; et les questions n'ayant pas d'endpoint, leur id entier est récupéré après coup **par `order`**, seul repère stable entre les deux listes. Échec d'écriture : la fenêtre refuse de se fermer et le pied de page porte un « Réessayer ». 14 clés i18n FR/EN/NL, 3 retirées (DB2). `flutter analyze` propre sur le dossier, `flutter build web` ✅.
**Reste** : DB5, la vérification à l'œil contre la maquette | **`AIApi` était dans le client généré mais exposé nulle part** dans `client.dart` — les 18 autres façades y sont. Câblé le 2026-08-11. **Arbitrages tranchés le 2026-08-11** — le lot B s'allège : - **`SectionEvent.ParcoursIds` → supprimer.** Le champ n'est **lu ni écrit nulle part** (entité, DTO, migrations — zéro contrôleur, zéro service), et son commentaire dit `// Liens vers GeoPoints spécifiques`, ce qui contredit son nom. Le lien événement↔parcours existe dans l'autre sens, `GuidedPath.SectionEventId`, lui bien entretenu. Décisif : `ParcoursIds` est porté par **`SectionEvent`, pas par `ProgrammeBlock`** — il ne peut donc pas exprimer « les parcours de tel jour », précisément le cas carnaval invoqué pour le garder. - **`QuestionType` → on ne touche pas au schéma, et il sort du lot B.** Le trio TextLibre/Digicode/ExpectedAnswer **n'existe pas dans le code** : l'enum vaut `Simple` / `MultipleChoice` / `Puzzle`, référencé à **un seul endroit** côté serveur, et le digicode est un comportement *dérivé* d'une réponse numérique, pas un type. Reste une propreté front (comparaisons par entier brut dans `showNewOrUpdateGuidedStep.dart:409` et `guided_step_challenge.dart:14`), déplacée au lot E. **`MigrationController` n'est plus bloqué que par un seul changement de modèle.** --- ## 1quinquies. La bascule elle-même — Mongo → Postgres en prod (ajouté le 2026-08-09) > **Trou identifié le 2026-08-09** : le §1ter dit *ce qui doit entrer dans le schéma avant que la base se remplisse*. Il ne dit nulle part **comment on remplit la base**. L'opération de bascule n'était écrite ni ici, ni dans le kanban, ni dans aucun plan de `v2/`. > > Rappel : la prod tourne **encore sur MongoDB**. Le transfert se fait par `MigrationController` (`POST /api/migration/run`, paramètres `dryRun` et `instanceId`), qui rejoue Instances → Resources → Users → Configurations → ApplicationInstances → Sections → Devices, puis relie les menus et enrichit les ressources PDF. ### ⚠️ `MigrationController` a pris du retard sur le modèle Écrit avant l'essentiel des chantiers de 2026 (`SectionEvent`, `SectionParcours`, abonnements, onboarding, guide IA, colonnes médias). Relevé le 2026-08-09 sur [ManagerService/Controllers/MigrationController.cs](../manager-service/ManagerService/Controllers/MigrationController.cs) : | # | Écart | Où | Conséquence si on bascule tel quel | |---|---|---|---| | a | `BuildSection` ne couvre que **11 types** (Map, Slider, Video, Web, Menu, Quiz, Article, PDF, Game, Agenda, Weather) | `:458-613` | **`SectionEvent` et `SectionParcours` tombent dans le `default`** → ligne d'erreur dans le rapport, section non migrée, silencieusement | | b | Les collections filles ne sont jamais remplies : `QuizQuestions = new()`, `EventAgendas = new()`, aucun `GuidedPath`/`GuidedStep` | `:526`, `:591` | Un quiz arrive **sans ses questions**, un agenda sans ses événements | | c | `Instance` : **4 champs migrés sur ~35** | `:106-112` | `SubscriptionPlanId` null → instance sans plan (quotas à 0, `HasStats` false) ; `PublicApiKey` null → **les apps visiteur ne s'authentifient plus** (`X-Api-Key`) ; `WebSlug` null → visitapp-web injoignable ; `IsMobile`/`IsTablet`/`IsWeb`/`IsAssistant` à false | | d | `Role = UserRole.ContentEditor` en dur | `:225` | **Plus aucun administrateur** après la bascule | | e | `Resource` : `StoragePath`, `FileName`, `IncludeInAiKnowledge`, `AiIndexStatus` non renseignés ; `SizeBytes` déduit d'un `HEAD` sur l'URL Firebase, échec avalé → 0 | `:172-181` | C'est **le point 5 du §1ter** (backfill) : à faire *dans* la migration, pas après | | f | `IsQRCode` / `IsSearchText` / `IsSearchNumber` forcés à `false` | `:276-278` | Réglages de configuration perdus | | g | `MapResourceId = dto?.iconResourceId` | `:469` | À répercuter le jour où la colonne est renommée en `IconResourceId` (§6bis) | | h | Pas de transaction globale : 9 étapes, un `SaveChangesAsync` chacune, `catch` qui renvoie **200 OK** avec `FatalError` dans le corps | `:68-86` | Un échec au milieu laisse la base à moitié remplie. La reprise ne tient qu'aux tests `AnyAsync` d'idempotence — qui existent, eux, et sont corrects | > Le point **c** est le plus dangereux : il ne produit **aucune erreur**. La migration se déclare réussie et le lieu est inaccessible aux visiteurs. ### La bascule, dans l'ordre Numérotation continue avec le §1ter (1-13). | # | Quoi | Note | |---|---|---| | 14 | **Remettre `MigrationController` à niveau** — les 8 écarts a→h ci-dessus | Le plus gros morceau. À faire avant tout le reste | | 15 | **Dry run** sur un export Mongo de prod, puis comparer les comptages par entité aux comptages Mongo | `dryRun=true` n'écrit rien. Un export existe déjà dans `manager-service/migration-data/` | | 16 | **Créer l'environnement prod Postgres** : `Deployment/Dockerfile.postgres` (épinglé par digest), compose prod, Traefik, **port 5432 fermé** | Dépend de la rotation des secrets (§3) — ne pas créer la prod avec les clés committées | | 17 | `dotnet ef database update` sur la base vide, puis vérifier `postgis` **et** `vector` présents | Le §1quater rappelle qu'une base en retard d'une migration fait planter l'API dès le login | | 18 | **`pg_dump` avant et après**, puis cron quotidien | Déjà noté en §4 comme non fait. Ici il devient bloquant : c'est le seul filet du jour J | | 19 | **Rejouer pour de vrai, instance par instance** (`instanceId=…`), en commençant par la plus petite | Le paramètre existe précisément pour ça | | 20 | **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 points b et c ne se voient qu'ici | | 21 | **Bascule DNS / API**, Mongo gardé en lecture seule quelques jours | | | 22 | **Plan de retour arrière écrit avant le jour J** | Un rollback qu'on improvise à 23 h n'est pas un rollback | ### Après la bascule — relationnel, pas technique | Quoi | Note | |---|---| | **Reprendre contact avec Louise Smets — Musée d'Ixelles** | À faire **une fois la prod stabilisée**, pas avant : on n'ouvre pas une conversation en montrant un produit en cours de bascule. Objectif d'abord d'écoute — comprendre ce qu'est réellement son travail au quotidien et sur quoi elle bute — avant de parler de MyInfoMate. L'entraide se décide après, pas dans le premier message. Rien n'est su de son rôle exact à ce stade : à ne pas supposer. | **Rappel de la porte** : rien de tout ça ne démarre avant que le reste du backlog soit terminé (§1quater). La base Postgres vide est une fenêtre — chaque chantier de schéma passé maintenant est un chantier qui n'exigera pas de fenêtre de maintenance plus tard. --- ## 2. Tests → Détail complet : **[test-plan.md](test-plan.md)** Checklist manuelle organisée en 20 sections + non-régression. À cocher au fur et à mesure avant une mise en prod. Section "Hors scope" = features non implémentées, pas testées. Ordre conseillé : 0. **§21 — Visite hors ligne** (ajouté le 2026-08-07) : bugs suspectés d'après le code, à confirmer sur device avant de planifier le chantier 1. **§0 — Porte d'entrée build** (5 commandes, bloquant absolu) 2. **§19 — Parité manager-app → apps visiteur**, par type de section : c'est le chapitre qui répond à « tout ce que je configure est-il visible ? ». Le §19.13 (SectionParcours, 5 cas d'usage) est le plus à risque. 3. §18 — Onboarding self-service (jamais exécuté) 4. §1-17 — features par lot, §16 non-régression --- ## 2bis. Parité manager-app → apps visiteur → **[parity-manager-visitapp.md](parity-manager-visitapp.md)** (audit du 2026-08-05, sur le code réel) - **Couverture par type de section : complète** — les 13 `SectionType` sont dispatchés dans `mymuseum-visitapp` et `visitapp-web`, aucun écran vide. - **17 écarts au niveau champ** : 7 côté mobile (dont le centrage de carte, les horaires des points d'intérêt, et surtout le mode contenu d'un parcours sans géodéclenchement), 10 côté web (dont le provider de carte, les icônes de catégorie, et l'absence totale d'assistant IA). - Suivi des corrections : table du **[test-plan.md §20](test-plan.md)**. **✅ Résolu le 2026-08-06 — assistant IA dans `visitapp-web`** (écart W8) L'assistant n'existait que sur mobile, alors que le plan Essentiel est web-only et que l'IA est vendue en add-on. `components/assistant/AssistantBubble.tsx` : bulle flottante montée par le layout de configuration, uniquement si `isAssistant` est vrai **sur l'instance et sur l'app Web** (même règle que `AiController`). Consomme `POST /api/AI/chat` en `AppType.Web` — aucun changement backend. - Rend les trois formes de réponse du modèle : texte HTML, `cards[]`, et l'action `navigation` qui route vers la section. - **Suggestions d'ouverture dérivées du contenu réel** (`lib/assistantSuggestions.ts`, 9 tests) : les sections déjà chargées donnent les questions proposées — un lieu sans agenda n'en propose pas sur l'agenda, et la carte cite un point réel. Zéro configuration client, zéro token. - `expectsReply` sert uniquement à ne pas réarmer la dictée quand le modèle clôt l'échange — pas au focus du champ, où il n'apporterait rien. - Quota atteint (429) → message neutre, le visiteur ne lit jamais le motif technique. - Maquette de référence : artifact « Assistant de visite — maquette visitapp-web ». **Répercuté sur `mymuseum-visitapp` le 2026-08-06** — l'assistant mobile n'était pas aligné : - **Quota IA (429) traité** : `AssistantChatSheet` affichait « Une erreur est survenue, **réessayez** » pour *toute* erreur, y compris le quota épuisé — le visiteur était donc invité à boucler sur un mur. Nouvelle `AssistantUnavailableException` levée par `assistantService`, message neutre côté sheet. - **Suggestions d'ouverture** ajoutées (`Helpers/assistantSuggestions.dart`), dérivées de `VisitAppContext.currentSections` déjà en mémoire. Même liste de phrases que le web — garder les deux fichiers alignés. - **Forme alignée sur la maquette** : pastille ronde pleine + « Votre guide » + nom du lieu en en-tête ; trois points animés au lieu du `CircularProgressIndicator` + « ... » ; bandeau « Je vous écoute » avec onde pendant la dictée. Le pattern reste une **modale bottom sheet** (`showGeneralDialog` + voile) — c'est le bon geste sur mobile, la bulle flottante du web n'aurait pas de sens sur 375 px. **✅ Résolu le 2026-08-06 — refonte de la configuration des parcours** (écarts M4, M7, M8, W6, W7, X1) Le client ne manipule plus 9 booléens répartis sur 3 niveaux, mais répond à **3 questions** : | Question | Où | Ce qu'elle pilote | |---|---|---| | **Où se déroule le parcours ?** — En salle / Sur le terrain | SectionParcours | `ShowMap`. « En salle » masque automatiquement position et zone sur toutes les étapes | | **Quelle ambiance ?** — Visite / Jeu | GuidedPath | `IsGameMode` + messages de début/fin | | **Comment le visiteur progresse-t-il ?** — Libre / Dans l'ordre / Étape par étape (+ masquer les étapes non atteintes) | GuidedPath | `IsLinear`, `RequireSuccessToAdvance`, `HideNextStepsUntilComplete` — dérivés dans `Parcours/progression_mode.dart` | - `IsStepLocked`, `IsHiddenInitially`, `FactContent` **supprimés** du modèle (migration `RemoveDeadGuidedStepFlags`). `IsStepLocked` rendait une étape *définitivement* infranchissable même après réussite du défi. - Navigation libre réellement implémentée (pins et segments de progression cliquables), verrouillage dérivé de la progression, masquage appliqué aussi en mode contenu — mobile **et** web. - Bascule carte/contenu unifiée sur `ShowMap` seul dans les deux apps. - Note ajoutée dans l'éditeur de question : une réponse attendue uniquement numérique déclenche automatiquement le pavé digicode côté visiteur. **Reprise du 2026-08-09 — 4 correctifs sur ce même écran** (relecture après coup, à valider via test-plan §19.13 cas 0) - **La progression enregistrée ne correspondait pas à celle affichée.** Un `GuidedPathDTO` neuf naissait avec `isLinear` nul : `progressionModeOf` affichait « Dans l'ordre », mais `isLinear ??= false` à la sauvegarde enregistrait « Libre ». Tout parcours créé sans toucher à la question partait donc en mode libre. Les valeurs sont désormais posées à la construction. Le test `progression_mode_test.dart` (12/12) ne pouvait pas l'attraper : il couvre la lecture/écriture des booléens, pas le DTO fabriqué par la popup. - **« Quelle ambiance ? »** était restée une case à cocher alors que les deux autres questions sont des radios — remise en Visite / Jeu, conforme au tableau ci-dessus. - **Médias d'étape rendus en dur comme des images** dans les deux apps visiteur (`guided_path_content_progression_page.dart` forçait `ResourceType.Image`, `StepCarousel` un ``). Le sélecteur accepte pourtant vidéo et audio depuis toujours : ces deux types donnaient une vignette cassée. Corrigé des deux côtés ; le libellé « Images » devient « Médias ». Le PDF, lui, reste à faire — voir todo-features.md. - **Menu « Carte de base »** : disparaissait en silence quand la configuration n'avait aucune SectionMap ; il reste maintenant visible et désactivé, avec l'explication. **Reste à trancher — la forme, pas le vocabulaire.** Configurer une question sur une étape et l'écrire en NL traverse **5 surfaces empilées**. Deux options maquettées le 2026-08-09 : **[artifact « Configurer un parcours sans empiler quatre fenêtres »](https://claude.ai/code/artifact/c9d9a5c0-1ba4-4e50-a522-28a0cf2ecd15)** — A, une fenêtre unique avec rail d'étapes (recommandée) ; B, écran plein cadre à 3 colonnes. Rien ne se code avant l'arbitrage ; détail et ordre d'exécution dans [todo-features.md](todo-features.md) § « SectionParcours — refonte du flux de configuration ». --- ## 3. Sécurité & dette technique → Détail complet : **[security/audit-securite-manager-service.md](security/audit-securite-manager-service.md)** et **[security/audit-manager-app.md](security/audit-manager-app.md)** (audit du 2026-07-13/14) ### manager-service — reste à traiter (par priorité) - [ ] **Critique en exposition, différée en calendrier** — Secrets committés en clair dans Git (JWT signing key, clés API, connection strings, mot de passe MQTT, token Telegram) → rotation + variables d'environnement + purge historique Git + suppression `RELEASE/` - 📅 **Déplacée en fin de backlog le 2026-08-11, à la demande.** Motif : les 6 repos sont **privés, sur un serveur Gitea auto-hébergé** — l'exposition réelle est celle d'un poste de dev, pas d'un dépôt public. La gravité ne change pas, la fenêtre d'urgence oui. - ⚠️ **Ce que ça déplace dans le plan** : la rotation était le verrou L1 du lot A (« ne pas créer la prod avec les clés committées »). Elle reste ce verrou, mais **juste avant le lot I** au lieu d'ouvrir le lot A. La contrainte à ne pas perdre : **elle doit précéder I3** (compose/Traefik prod), sinon la prod naît avec les clés de l'historique et il faut rotationner deux fois. - 🔒 **Condition de validité** : tenable tant que les repos restent privés et qu'aucun collaborateur externe n'est ajouté. Un repo rendu public, un fork, ou un CI tiers qui clone l'historique → la rotation remonte devant immédiatement. - [ ] **Haute** — `EnableSensitiveDataLogging` actif en prod ; services Mongo legacy en singleton à corriger ; `ValueComparer` manquant sur les JSONB ; ~~`AssistantService` `new HttpClient()` en boucle~~ ✅ corrigé le 2026-08-07 (`IHttpClientFactory`, 4 occurrences) - [ ] **Moyenne** — JWT sans expiration forcée, clés API en clair + comparaison non constant-time, bootstrap PIN sans rate limiting, exceptions internes exposées, backdoor `#if DEBUG`, double `SaveChangesAsync` audit (⚠️ vérifier après le fix du 2026-07-15 sur `SaveChanges`/`SaveChangesAsync` — peut être partiellement résolu), `SectionFactory` dupliqué, tests EF InMemory peu représentatifs - [ ] **Basse** — conventions REST hétérogènes, `PasswordUtils` avec RNG non crypto, code mort (MQTT, CORS AllowAll), CORS en dur, ordre middleware ### manager-app — reste à traiter (par priorité) - [x] ✅ **Critique — résolu le 2026-08-11.** Client généré désynchronisé (`manager_api_new/`) : 14 fichiers modèle orphelins → `flutter analyze` inexploitable (183/200 erreurs de bruit, 68 au 09/08). **Mesuré après nettoyage** : `flutter analyze manager_api_new` passe de **67 à 3 issues**, l'analyse globale de manager-app de 68 erreurs de bruit à **3 erreurs**, toutes dans `test/widget_test.dart` (cassé et connu, voir « Basse » ci-dessous). `flutter build web` ✅ avant comme après — attendu, ces fichiers n'étant dans aucun graphe. **`flutter analyze` redevient un feu vert utilisable**, ce qui lève le lien L8 - **Caractérisé précisément le 2026-08-11.** `lib/api.dart` déclare **139 `part`** pour **153 fichiers** dans `lib/model/`. Les 14 restants portent bien `part of openapi.api;` mais **ne sont déclarés `part` par personne** — en Dart, un fichier `part` non listé par sa bibliothèque est **hors du graphe de compilation**. D'où l'asymétrie qui a coûté du temps : l'analyzer les lit (il parcourt tout `lib/`), le compilateur ne les voit jamais. **Les supprimer ne peut pas changer un build.** - Les 14 : `agenda_event_stat_dto`, `app_configuration_link_dto_application_instance`, `content_dto_resource`, `content_geo_point`, `day_stat_dto`, `game_stat_dto`, `level_dto`, `map_dto_map_provider`, `map_dto_map_type`, `map_dto_map_type_mapbox`, `poi_stat_dto`, `quiz_stat_dto`, `section_stat_dto`, `user`. - **Fausse alerte levée le 2026-08-11, à ne pas rouvrir** : `ContentGeoPoint` (un des 14) *semble* référencé par du code vivant — `tablet-app/lib/Screens/Map/marker_view.dart:456` et `mymuseum-visitapp/lib/Screens/Sections/Map/marker_view.dart:367`, les deux apps dépendant de `manager_api_new` par `path:`. **Les deux lignes sont dans un bloc commenté** (`/*` ouvert respectivement en 418 et 329) : du code mort, invisible pour le compilateur *et* pour l'analyzer. Vérifié par comptage de délimiteurs et confirmé par `flutter analyze` sur les deux fichiers — zéro erreur. **Aucun des 14 n'a de référence vivante.** - 🔒 **Les déclencheurs de génération partent avec eux, et pour une autre raison** : ce n'est pas du modèle mort, c'est le moyen de régénérer (annotation `@Openapi` + `build_runner`). Importés nulle part. Les supprimer ne retire aucun code exécuté — ça **verrouille la règle « `manager_api_new` s'édite à la main »** : tant qu'ils sont 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`). - ⚠️ **Ils étaient trois, pas un — trouvé le 2026-08-11.** La règle n'était verrouillée que dans manager-app : - `manager-app/lib/api/openApiTest.dart` (dernier run daté du 2026-05-07) - `mymuseum-visitapp/lib/api/openApiTest.dart` — **le plus dangereux** : il génère depuis **son propre `lib/api/swagger.yaml`** vers un `manager_api_new` local, ce qui aurait créé un second client divergent dans un repo qui consomme celui de manager-app par `path:`. Il portait en plus une des 5 erreurs Dart du repo - `tablet-app/lib/api/openApi.dart` — même configuration (nom de fichier différent, d'où le fait qu'il ait échappé aux recherches précédentes sur `openApiTest`) - plus un résidu `tablet-app/manager_api_new/` ne contenant qu'un `pubspec.lock`, non suivi par git, daté d'avril — une génération avortée Les trois sont supprimés. **Contrôle de non-régression du verrou** : `grep -rl "@Openapi" */lib` ne doit renvoyer aucun fichier. - [ ] **Haute** — Duplication du shell de dialog (15 occurrences), composants quasi-copiés (translation containers), méthodes `build()` monolithiques (440-640 lignes), i18n contournée (~100+ strings en dur) - [ ] **Moyenne** — Clé AES en dur + IV fixe (chiffrement de session inutile), erreurs API avalées silencieusement (`catch (_) {}`), `_localPath` cassé, fichiers morts, TODO visibles à l'écran en prod, dépendances pubspec inutilisées - [ ] **Basse** — façades API nullable jamais renullifiées, 58 `print()` de debug, credentials de test en dur, nommage non conforme Dart, `test/widget_test.dart` cassé (seul test du projet, ne compile pas) **Prochaine chose à faire si tu as 1h** : ~~la rotation des secrets Git~~ → **le nettoyage de `manager_api_new`** (Critique manager-app). La rotation des secrets a été déplacée en fin de backlog le 2026-08-11 (repos privés auto-hébergés, cf. ci-dessus) ; le nettoyage du client, lui, est une demi-heure sans risque de build et il rend `flutter analyze` utilisable pour tout le lot design. --- ## 4. Infrastructure & Ops - [x] ✅ **Travail non committé — résorbé. Revérifié le 2026-08-11 (2e passe) : plus rien en attente.** Les **9 repos** (DOCS compris) sont propres (`git status --porcelain` vide partout) **et à jour avec leur remote** (aucun `ahead`/`behind`). L'alerte du 2026-08-09 et sa révision du matin du 11/08 (« ~41 fichiers en working tree dont 9 non suivis ») sont toutes deux **périmées** : le lot 3 RAG est dans `18e4240`, le socle visuel + Guide IA dans `eb8e849`. **L'étape 1 du lot A est terminée** — ne pas la re-planifier - Derniers commits par repo : `manager-service` `18e4240` (RAG : pipeline d'ingestion, endpoints du guide IA, journalisation RGPD) · `manager-app` `eb8e849` (socle visuel et écran Guide IA à deux onglets) · `visitapp-web` `473fc3a` · `mymuseum-visitapp` `af44927` · `tablet-app` `5a6701d` (drapeaux du migrateur Flutter) · `myinfomate-landing` `d91cf7f` · `unov-landing` `cfa70c4` · `OpenGlasses` `1d887d7` - Rappel de l'état des branches : `manager-service` → `MyInfoMate_3.0`, `manager-app` → `Interface-refacto`, `mymuseum-visitapp` → `Meta-Rayban-Test` (23 commits d'avance sur `master`, voir §5bis), `tablet-app` → `AI-Assistant-test`, `visitapp-web` et les deux landings → `master` - [ ] **pg_dump quotidien** pour `manager-service` (Postgres `my_info_mate`), en complément du snapshot OVH — voir mémoire `project_pgdump_todo`, pas encore fait au 2026-07-15. ⚠️ **Devient bloquant au moment de la bascule** : c'est le point 18 du [§1quinquies](#1quinquies-la-bascule-elle-même--mongo--postgres-en-prod-ajouté-le-2026-08-09), le seul filet du jour J - ⛔ **Deuxième chose qu'il débloque** : le job `visit-events-purge` (purge des stats au-delà de 13 mois) est codé et enregistré mais **volontairement inactif** — il ne supprimera rien tant que `Stats:RetentionDays` n'est pas défini. Ne l'activer (`Stats__RetentionDays=395`) qu'une fois ce pg_dump en place - [ ] **Fermer le port PostgreSQL exposé publiquement** via Traefik/Docker (détail dans [todo-features.md](todo-features.md), section "Infrastructure — Sécurité réseau") — accès DBeaver via tunnel SSH uniquement - [ ] **Backup Mongo automatique en cron** — voir [todo-landing-deploy.md](todo-landing-deploy.md), section 5 (dump + `docker cp` vers l'hôte, pas seulement dans le conteneur) - [x] Déploiement landing page (`docker-compose-landing.yml` séparé, ne jamais toucher au compose principal) — voir [todo-landing-deploy.md](todo-landing-deploy.md) pour les règles à respecter --- ## 5. Roadmap produit — nouveaux canaux & IA Plans détaillés, pas encore démarrés en dev (sauf mention contraire) : | Chantier | Détail | Statut | |---|---|---| | Kiosk web (Flutter Web, extension tablet-app) | [architecture-web-saas.md](architecture-web-saas.md) | 📦 **V2** (2026-08-07) — pas commencé, ~2-3j estimés. Aucun client ne l'attend, et `tablet-app` ne compile même pas aujourd'hui | | Visiteur web (Next.js, `app.myinfomate.be/[slug]`) | [architecture-web-saas.md](architecture-web-saas.md) | ⚠️ **V1, presque fini** — `visitapp-web` existe, build ✅, les 13 types de section sont rendus, assistant IA inclus. **Il ne manque que le Dockerfile + l'entrée `app.myinfomate.be` dans le compose.** Bloquant : le plan Essentiel vendu par l'onboarding est web-only | | Export PDF stats — **à la demande** | [plan-import-ia-stats-subsides.md](plan-import-ia-stats-subsides.md) §1 | ✅ **Livré le 2026-08-07**, généré côté client (paquet `pdf` Dart), aucun changement backend. **Inclus dans tous les plans qui ont les stats** (décidé le 09/08) — la différenciation vient du contenu, pas d'un verrou | | Historique des stats — **13 mois pour tous** | idem · [todo-features.md](todo-features.md) § Plans & Quotas | ✅ **Décidé et appliqué le 2026-08-09**. L'axe « durée » disparaît, `HasAdvancedStats` devient le seul différenciateur stats. Migration `UnifyStatsRetentionTo13Months` **à appliquer**. Purge codée mais inactive jusqu'au pg_dump | | Export PDF stats — **envoi mensuel automatique** | idem | v2. C'est là que QuestPDF + Hangfire reprennent leur sens. Manquent : config destinataires/fréquence, méthode d'envoi avec pièce jointe, et `libfontconfig1` au Dockerfile | | Import IA de contenu + RAG (pgvector) | [plan-import-ia-stats-subsides.md](plan-import-ia-stats-subsides.md), [v2/rag-pgvector-integration-plan.md](v2/rag-pgvector-integration-plan.md) | Priorité 2 — chantier fusionné, ~4-6 semaines. **Le schéma part avec la migration v3, voir §1ter** | | Médias & stockage (compression, `IStorageService`, quota) | [v2/media-storage-plan.md](v2/media-storage-plan.md) | **Prérequis à la release de migration** — voir §1ter | | Visite hors ligne — remise en état | [v2/offline-visit-plan.md](v2/offline-visit-plan.md) | **Bugs**, pas de la roadmap — voir §1ter | | Écran Guide IA (manager-app) | [v2/guide-ia-screen-plan.md](v2/guide-ia-screen-plan.md) | **V1**, spécifié et maquetté — voir §1ter | | Écran Statistiques — refonte | [v2/stats-screen-plan.md](v2/stats-screen-plan.md) | ✅ **Livré le 2026-08-07**, export PDF à la demande compris — les 7 défauts corrigés. **Pas encore ouvert dans un navigateur** : [test-plan.md §8bis et §8ter](test-plan.md) | | TTS pré-généré (audioguide multilingue) | [v2/tts-pregenerated-plan.md](v2/tts-pregenerated-plan.md) | Priorité 3 | | Talking head (avatar animé) | [v2/talking-head-plan.md](v2/talking-head-plan.md) | Priorité 6 — polish, bouche-trou | | AI Persona par venue (guide IA personnalisable) | [v2/myinfomate-ai-persona-analysis.md](v2/myinfomate-ai-persona-analysis.md) | Architecture de référence pour les chantiers IA ci-dessus | | Lunettes Ray-Ban Meta | [roadmap.md](roadmap.md) (section XR), [rayban-meta-integration.md](rayban-meta-integration.md) | 🔨 **POC fonctionnel — bien plus avancé que ce que cette ligne disait jusqu'au 2026-08-09.** Voir §5bis | | VR Meta Quest (borne office tourisme) | [roadmap.md](roadmap.md) (section XR) | Idée, **zéro ligne de code** dans tous les repos — vérifié le 2026-08-09. La landing annonce pourtant « Meta Quest — Bientôt » en 4 langues (`translations.ts`, clé `mode4Desc`) | | Canal subsides | [plan-import-ia-stats-subsides.md](plan-import-ia-stats-subsides.md) | Actions administratives/réseau, pas de dev | --- ## 5bis. Lunettes Ray-Ban Meta — état réel du code (relevé le 2026-08-09) > Cette section corrige une erreur de ce fichier : la roadmap classait l'AR lunettes en « pas commencé, pilote subventionné quand le SDK sort de preview ». **Un POC fonctionnel existe depuis mai-juin 2026**, ~2 700 lignes de Dart dans `mymuseum-visitapp`, sur la branche **`Meta-Rayban-Test`** (23 commits d'avance sur `master`, jamais mergée — c'est la branche de travail courante du repo). | Brique | Fichier | État | |---|---|---| | SDK Meta | `pubspec.yaml:84` — `meta_wearables_dat: ^0.1.3` | Dépendance **active**, pas commentée | | Connexion / photo / stream vidéo | `lib/Services/meta_glasses_service.dart` (241 l.) | Cycle de vie complet, routage audio HFP via canal natif | | Pipeline mains-libres | `lib/Services/Glasses/voice_orchestrator.dart` (408 l.) | wake word → STT → dispatch → LLM ou scan QR → TTS, sons d'état | | Moteurs interchangeables | `lib/Services/Glasses/engines/` + `impl/` (~1 040 l.) | 4 interfaces, **11 implémentations** : openWakeWord, Porcupine, speech_to_text, Whisper, Home Assistant STT, Gemini TTS, ElevenLabs, flutter_tts, client LLM MyInfoMate | | Service de fond | `lib/Services/Glasses/glasses_background_service.dart` | Téléphone en poche, écran verrouillé | | UI | `VoiceModeSheet`, `GlassesStatusWidget`, `GlassesDebugPanel` (450 l., via `AdminPopup`) | Monté dans `main.dart:109-112` | Commit `c652604` : *« Working flow also in background ! With custom wakeword working (hey visit et hey viva). flow llm working + take photo + scan qr code working »*. ### Ce qui bloque réellement la commercialisation Pas « le SDK est en preview » — la liste est plus concrète : - **Android uniquement** (`if (!Platform.isAndroid) return;` dans `meta_glasses_service.dart`). Le modèle **BYOD tombe** pour tous les visiteurs iPhone. Seul le modèle « kit prêté par le lieu » tient aujourd'hui. - **Pas de distribution store** tant que le SDK est en developer preview : chaque installation passe par les credentials développeur. - ✅ ~~**TTS = ElevenLabs, facturé au caractère** — basculer sur Gemini avant de vendre l'add-on~~ **Déjà fait, constaté le 2026-08-13.** `constants.dart:20` dit « ElevenLabs retiré du pipeline (trop cher) » et `VoiceController` ne construit que `GeminiTtsEngine` ou la voix système. Le risque de marge qui motivait cette ligne est écarté. `impl/elevenlabs_tts_engine.dart` **est conservé** comme option — à rouvrir seulement si des clients jugent la voix Gemini insuffisante, en sachant que c'est beaucoup plus cher. - ⚠️ **Mais deux défauts ont vécu derrière cette ligne, corrigés le 2026-08-13.** 1. **Le choix de voix du client n'était pas honoré.** Le sélecteur Viva (`Sulafat`) / Marco (`Umbriel`) existe dans manager-app depuis le 07/08 et écrit `Instance.GuideVoiceId` ; `mymuseum-visitapp` ne lisait **jamais** ce champ et utilisait une constante de build, dont le défaut était `Algieba` — une voix que personne n'a écoutée. Même forme que W1 : un réglage offert par le CMS que l'app visiteur ignore. La voix vient désormais de l'instance, la constante n'étant plus qu'un repli (passé à `Sulafat`). 2. **Aucun APK ne parlait avec la voix du produit.** `GeminiTtsEngine` n'est choisi que si `GEMINI_API_KEY` est injectée au build — elle ne l'était nulle part, donc tous les builds retombaient sur `FlutterTtsEngine`, la voix système d'Android, **en silence**. Le repli s'annonce maintenant dans les logs, `constants.dart` porte la commande, et `launch.json` a une configuration « Dev + voix Gemini ». ⚠️ **Le repli reste le défaut, et c'est voulu** : les tests de terrain ne consomment ni jetons ni argent. - ⚠️ **Le STT n'est pas Google non plus** : `WhisperSttEngine` pointe sur `api.openai.com` si `WHISPER_API_KEY` est définie, sinon repli sur `speech_to_text`, le moteur du device. La clé est vide aujourd'hui, donc c'est le device qui transcrit. **À trancher au lot J** : activer Whisper ajouterait OpenAI comme sous-traitant, que le §8.5 des CGU ne mentionne pas — il ne parle que de Google. - **Wake word de prod non réglé** : `porcupine_flutter` est commenté dans `pubspec.yaml` (payant), l'implémentation courante s'appuie sur `speech_to_text` / openWakeWord. - **Branche jamais mergée** dans `master`. - **Le POC ne compile plus en l'état — relevé le 2026-08-11.** `flutter analyze lib` sur `mymuseum-visitapp` remonte **4 erreurs**, toutes le même manque : `kElevenLabsApiKey` et `kElevenLabsVoiceId` sont **absents de `constants.dart`** alors que `glasses_qr_scanner_service.dart:110-111` et `wake_word_service.dart:133-134` les utilisent et importent bien `constants.dart`. La doc de `glasses_tts_service.dart:31-32` confirme l'intention (« typiquement `kElevenLabsApiKey` de `constants.dart` »). Ces deux constantes sont des **secrets qui n'ont jamais été committés** — le POC tournait avec un `constants.dart` local. Conséquence pratique : **« démontrable » suppose de les remettre**, ce n'est pas un `git checkout` qui suffit. À traiter avec la bascule TTS → Gemini ci-dessus, qui les rend inutiles. - ✅ ~~**`flutter build apk` ne passe plus non plus**~~ — **levé le 2026-08-12 par K5.** Les trois flavors construisent ; le NDK est passé à 28.2.13676358 et Kotlin à 2.3.10. - ⛔ **Correction du 2026-08-13 : « l'APK se construit sans le POC dedans » est faux, et la conclusion qu'on en tirait aussi.** Les 4 erreurs ne sont pas dans le POC vivant, elles sont dans ses **ancêtres** : `wake_word_service.dart` n'est importé par personne, et `glasses_qr_scanner_service.dart` seulement par lui — un îlot de deux fichiers hors du graphe de `main.dart`, la génération d'avant l'orchestrateur. Le POC **actuel**, lui, est bien dans l'APK : `Services/Glasses/` est importé par `VoiceController`, et `GlassesStatusWidget` est monté sur l'accueil.
⚠️ **Conséquence pratique, inverse de celle qui était écrite ici** : « démontrable » **ne suppose pas** de remettre les deux constantes ElevenLabs. Le chemin vivant choisit `GeminiTtsEngine` dès que `kGeminiApiKey` est renseignée (`voice_controller.dart:94-100`) et ne touche jamais `kElevenLabs*`. La bascule TTS → Gemini réclamée ci-dessus **est déjà faite dans le code** ; ce qui reste est de supprimer les deux ancêtres morts. - ✅ ~~**Branche jamais mergée / à trancher avant K6**~~ — **tranché le 2026-08-13 par le propriétaire du projet : `Meta-Rayban-Test` est la branche de travail à jour, pas un POC de côté.** Le nom est trompeur, il date de la première expérimentation ; tout le travail V1 de `mymuseum-visitapp` y vit et les lunettes n'en sont qu'une partie — invisibles, d'ailleurs, si l'instance n'a pas l'assistant. On développe et on publie depuis elle. Idem `tablet-app` sur `AI-Assistant-test`. **Rien à clarifier avant K6** ; c'est ce paragraphe qui affirmait le contraire et fabriquait l'alerte. ### Latence, langues et accusés de réception — relevé le 2026-08-13 > Plan complet : **[voice-latency-plan.md](voice-latency-plan.md)**, découpé en « maintenant / après les tests / V2 ». Ce qui suit n'en garde que les constats vérifiés dans le code. - ⛔ **L'assistant vocal ne parle réellement que FR/NL/EN/DE, et personne ne le disait.** `_toLangCode` (`voice_orchestrator.dart:399-407`) ne mappe que ces quatre langues et **renvoie `fr-FR` par défaut** ; `constants.dart:59-70` en déclare 10. Un visiteur en italien se fait répondre en français, **sans erreur ni trace**. Décidé le 2026-08-13 : la limite est assumée et documentée, le reste de l'app garde ses 10 langues. Le coût d'en rajouter une est faible et c'est vérifié — les traductions `voice.*` existent **déjà pour les 10 langues** dans `translations.dart`, Whisper prend le code générique et est multilingue, le wake word est phonétique. - ⛔ **Et les quatre langues annoncées ne le sont pas non plus.** `_isStopCommand`, `_isRepeatCommand`, `_isQrScanCommand`, `_isPhotoCommand` (`:375-397`) cherchent des mots **français en dur** — « répète », « arrête », « prends », « regarde ». Elles avaient été écrites en français pour tester et n'ont jamais été reprises. Un néerlandophone qui dit « herhaal » n'est pas compris, et `_isQrScanCommand` matche sur `code` : « what's the **code** of this painting » déclenche un scan QR. **À corriger avant le lot H** — sinon les tests multilingues valident autre chose que ce qu'on croit. - ⚠️ **`done.mp3` retarde la réponse de toute sa durée.** `:212` fait `await _playDoneSound()` juste avant `ttsEngine.speak()`, et `_playSound` attend `play()`, dont le future ne se résout qu'à **la fin de la lecture**. En prime c'est un doublon : la parole *est* le signal de fin. - ⚠️ **Le time-to-first-audio est la somme de tout.** `LlmClient.chat()` retourne un future de réponse complète (pas de flux) et `GeminiTtsEngine._synthesize()` fait un `generateContent` unaire qui attend **tout** le PCM avant d'écrire le WAV. D'où le son de réflexion en boucle, qui bouche ce trou. Le remède qui ne dépend d'aucune API nouvelle : **découper la réponse en phrases** et synthétiser la première pendant que les suivantes se préparent — contenu dans `GeminiTtsEngine`, sans toucher au backend. - 💡 **Idée retenue mais repoussée : remplacer le bip du wake word par une phrase parlée** (« Oui, je vous écoute ») dans la langue et la voix du visiteur, 2-3 variantes. Repoussée **après les tests** parce qu'elle demande de générer et valider à l'oreille ~40 fichiers (2 voix × 4 langues × 5 phrases), et qu'une partie du besoin qu'elle compense disparaît si la latence baisse. ⚠️ Règle de cohérence si on la fait : les acks ne s'activent que si le moteur runtime est **Gemini** et que la voix correspond à `guideVoiceId` — des acks en Sulafat suivis d'une réponse en voix système Android seraient pires que le bip. - 🔭 **Piste V2 : le Live API de Gemini** (WebSocket, audio natif bidirectionnel) supprimerait les trois maillons Whisper → LLM → TTS et débloquerait le barge-in et le VAD serveur. Faisable, mais le tool calling devrait passer par un proxy WebSocket dans `manager-service` (option retenue sur le papier), le modèle de coût passe à la **session ouverte** avec des jetons audio bien plus chers, et les modèles sont **en preview**. **À ouvrir par un spike chiffré d'une journée, pas par une décision.** ### Ce que ça change côté commercial Le POC est **démontrable**. Face à un concurrent mono-usage type Musa Guide, une démo qui tourne pèse plus qu'une ligne « bientôt » sur la landing. À condition d'assumer le kit Android prêté, et de ne pas vendre l'add-on avant la bascule TTS. --- ## 6. Sous-projets MyInfoMate - **MyInfoMate Sport** — pilote Hockey Namur. Vue d'ensemble : [sport/solution-overview.md](sport/solution-overview.md), spec : [sport/myinfomate-sport-spec.md](sport/myinfomate-sport-spec.md), pitch : [sport/pitch-commercial.md](sport/pitch-commercial.md) - **MyInfoMate Crèche** — pilote childcare en cours. Vue d'ensemble : [creche/solution-overview.md](creche/solution-overview.md), spec : [creche/myinfomate-creche-spec.md](creche/myinfomate-creche-spec.md), pitch : [creche/pitch-commercial.md](creche/pitch-commercial.md) --- ## 6bis. Cohérence de la doc — audit du 2026-08-07 Passage en revue de tous les fichiers `DOCS/` (hors `sport/` et `creche/`). Corrigé directement : | Fichier | Ce qui était faux | |---|---| | [roadmap.md](roadmap.md) | Annonçait **Claude API** comme LLM à 4 endroits — c'est **Gemini 2.5 Flash-Lite**. Quota IA en requêtes → tokens | | [parity-manager-visitapp.md](parity-manager-visitapp.md) | L'intro du §6 disait « visitapp-web reste cassé » et B2 « tests backend ❌ » alors que les deux sont corrigés depuis le 2026-08-06 — le doc se contredisait lui-même | | [v2/tts-pregenerated-plan.md](v2/tts-pregenerated-plan.md) | Chemin Firebase « à confirmer » et faux. Le vrai est `pictures/{instanceId}/{resourceId}`, sans extension. Le refactor uploads est partiellement devancé par `media-storage-plan.md` | | [plan-import-ia-stats-subsides.md](plan-import-ia-stats-subsides.md) | « Aucune infra email n'existe » → **Resend est en place** (9 templates, domaine vérifié). Quota `AiRequestsPerMonth` → `AiTokensPerMonth` | | [security/audit-securite-manager-service.md](security/audit-securite-manager-service.md) | `ImageHelper` : confirmé **code mort** qui planterait sur Linux. Double `SaveChangesAsync` : à revérifier après le fix `BuildAuditEntries` du 2026-08-06 | | [solution-overview.md](solution-overview.md) | Grille tarifaire **Starter/Standard/Premium (69/99/199 €)** — structure abandonnée. Marquée obsolète | ### 🔴 Demande ton arbitrage — non corrigé volontairement **[cgu-myinfomate.md](cgu-myinfomate.md) §5 décrit des plans qui n'existent plus** : Starter / Standard / Premium (69 / 99 / 199 € HTVA), quotas IA en requêtes. La réalité est Essentiel / Pro / Premium / Enterprise (39 / 99 / 179 €), quotas en tokens. C'est un **document contractuel** — je ne l'ai pas réécrit. À reprendre en vérifiant d'abord ce que les clients existants ont effectivement signé. ### Ménage — décidé le 2026-08-07 - **`offre-commerciale.md` : ne pas supprimer.** Vérification faite, il ne contient pas qu'une grille : clients cibles, services complémentaires, forfait événementiel, frais de mise en place, projection de revenus. **Retirer les seules tables de prix** et garder le reste comme document de stratégie commerciale. - Quatre documents portaient une grille tarifaire (roadmap, solution-overview, offre-commerciale, cgu) alors que la source de vérité est `myinfomate-landing` → **supprimer les tables, garder un lien**. - ✅ **CGU §5 reprises le 2026-08-07** depuis la landing : Essentiel/Pro/Premium/Enterprise (39/99/179/devis), et un §5.1bis qui définit le quota IA **en tokens** en renvoyant au back-office pour les valeurs — plutôt que de figer un chiffre dans un contrat. - ✅ **Landing corrigée** — la clé `features.reqPerMonth` n'existe plus dans `myinfomate-landing/src` (vérifié le 2026-08-11). Ce point était encore listé comme ouvert à deux endroits de ce fichier alors que le kanban le donnait fait : c'est le kanban qui avait raison. - ⚠️ **Champs morts — partiellement traités.** `FactContent`, `IsStepLocked`, `IsHiddenInitially` sont bien supprimés (migration `RemoveDeadGuidedStepFlags`). **Restent en base** : `SectionEvent.ParcoursIds`, `SectionEvent.IconResourceId`, `SectionMap.MapResourceId` — ce dernier étant en plus mappé vers `iconResourceId` dans son DTO, ce qui ajoute une confusion de nommage. - **Champs « morts » — décision révisée le 2026-08-07 après vérification du code.** Deux des trois ne sont pas morts : - `SectionMap.MapResourceId` → **à renommer, pas à supprimer.** Il est branché de bout en bout (`SectionFactory:179/511`, `SectionMap.ToDTO:58`, `MigrationController:469`) et exposé au front sous le nom `iconResourceId` : la colonne porte l'icône de la carte, pas une « ressource carte ». Le renommer en `IconResourceId` supprime la confusion. Reste à décider si le web doit l'honorer (écart W10). - `SectionEvent.ParcoursIds` → **à investiguer avant de trancher.** Intention produit documentée (depuis un SectionEvent mis en avant, retrouver les parcours liés aux jours d'un carnaval). `GuidedPath.SectionEventId` couvre le lien au niveau *événement* ; reste à vérifier qu'il couvre aussi le niveau *jour / bloc de programme*. Ne pas supprimer tant que ce n'est pas établi. - `SectionEvent.IconResourceId` → sans usage visiteur identifié, seul candidat réel à la suppression. - **`Section.meterZoneGPS` : à implémenter, pas à retirer.** Le rayon de déclenchement a une vraie raison d'être variable — une salle de musée demande ~10 m, un parcours extérieur comme le Fort en demande 50-100. La constante en dur à 100 m est fausse dans les deux cas. --- ## 7. Documents de référence (pas des todos — pas besoin de "traiter") - [solution-overview.md](solution-overview.md) — présentation produit core - [parity-manager-visitapp.md](parity-manager-visitapp.md) — audit de parité manager-app → apps visiteur (2026-08-05) : couverture par section, 17 écarts au niveau champ, état des builds, méthode pour refaire l'audit - [guided-path-types.md](guided-path-types.md) — guide de référence SectionParcours - [section-event-map-setup.md](section-event-map-setup.md) — guide setup SectionEvent/SectionMap - [design-prompts.md](design-prompts.md) — prompts design pour Google Stitch/Claude Design - [competitor-analysis.md](competitor-analysis.md) — comparatif concurrents - [cgu-myinfomate.md](cgu-myinfomate.md) — CGU - ⚠️ [offre-commerciale.md](offre-commerciale.md) — **obsolète**, source de vérité = myinfomate-landing (voir mémoire `project_pricing_plans`) - ⚠️ [roadmap.md](roadmap.md) — contient aussi une table "Modules existants" + une table de prix en dehors de la section XR (section 5 ci-dessus) : cette table de prix est une **ancienne copie**, potentiellement désynchronisée de myinfomate-landing (source de vérité) — à vérifier avant de s'y fier - `openwakeword/` — scripts/notebook d'entraînement de modèle wake word (code, pas de la doc à jour manuellement) - `claude design/` — mockups HTML générés (MyInfoMate Parcours), à consulter visuellement, pas du texte à synchroniser --- ## Comment mettre à jour ce fichier > 🗂️ **Le kanban en est le reflet.** Source de vérité du contenu : **[kanban.html](kanban.html)**, versionné à côté de ce fichier. > Publié à l'adresse https://claude.ai/code/artifact/4c317904-2e23-45d3-ac14-dcca678ae5d3 > > ⚠️ **L'artifact appartient à un autre compte Claude que celui de la machine de dev.** Constaté le 2026-08-10 : ni la lecture ni la republication n'y sont possibles depuis ici — même partagé en « anyone with the link », la réponse est *« reading public artifacts that way is not enabled yet »*. D'où le découpage : > > | | Qui | Quoi | > |---|---|---| > | Contenu | n'importe quelle session | éditer `DOCS/kanban.html` — cartes **et** compteurs (colonnes + bandeau du haut) | > | Diffusion | le compte propriétaire | republier ce fichier sur l'artifact | > > Vérification avant publication : chaque compteur annoncé doit égaler le nombre réel de `
` de sa colonne. > > Un hook `PostToolUse` (`.claude/hooks/status-kanban-reminder.py`) rappelle ce report à chaque écriture sur ce fichier. Il rappelle, il ne publie pas — une correction de formulation ne justifie pas de toucher au tableau. Ce fichier est un index, pas une source de vérité duplicée : - Nouvelle feature terminée/à faire → éditer **todo-features.md**, pas ici (sauf si ça change le résumé de la section 1) - Nouveau point sécurité traité → cocher dans la section 3 ici **et** noter "Déjà corrigé" dans le fichier audit correspondant - Nouveau chantier roadmap → ajouter une ligne dans la table de la section 5, créer le plan détaillé séparément si ça dépasse quelques lignes