Port PostgreSQL exposé publiquement
À fermer via Traefik/Docker. Accès DBeaver par tunnel SSH uniquement.
todo-features.md — Sécurité réseauVue d'état consolidée depuis DOCS/. Source de vérité : STATUS.md — cette page en est le reflet, pas le remplaçant. Filtrez sur V1 pour ne voir que ce qui reste avant la mise en prod. L'ordre d'exécution, lui, est dans v1-plan.md : ce tableau dit où en est chaque chantier, pas lequel bloque lequel.
À fermer via Traefik/Docker. Accès DBeaver par tunnel SSH uniquement.
todo-features.md — Sécurité réseauLe §1bis le donnait ✅ au 06/08. Revérifié le 11/08 : flutter build apk échoue côté Gradle — ndkVersion = "28.2.13676358" réclamé dans android/app/build.gradle, avertissements KGP sur 11 plugins. Ce n'est pas le Dart : le code applicatif analyse proprement.
Même famille que tablet-app : l'environnement Android a bougé sous les deux repos, le code n'a pas changé. Les traiter ensemble plutôt qu'un par un — c'est une seule migration Gradle/NDK, pas deux bugs.
Les 4 erreurs Dart restantes sont toutes sur la branche Ray-Ban et hors du graphe de main.dart : kElevenLabsApiKey et kElevenLabsVoiceId absents de constants.dart, jamais committés parce que ce sont des secrets. Le POC « démontrable » suppose donc de les remettre — voir §5bis.
Échec Gradle sur flutter_tools/gradle/build.gradle.kts. Seul build encore cassé des cinq. Le dernier APK date d'avril. Peut-être déjà réglé par le commit 5a6701d (« drapeaux du migrateur Flutter ») — à reconfirmer par un vrai build, pas par le log.
Piste écartée le 11/08, à ne pas rouvrir : marker_view.dart:456 référence ContentGeoPoint, une classe absente de tout graphe de compilation — de quoi croire à un second défaut, côté Dart cette fois. Mais la ligne est dans un bloc commenté (/* ouvert en 418), comme sa jumelle de mymuseum-visitapp:367 (/* en 329). flutter analyze sur les deux fichiers : zéro erreur. Le problème de tablet-app est purement Gradle.
StoragePath/SizeBytes des 45 lignes✅ Prérequis levé le 10/08 : Create écrit désormais StoragePath (pictures/{instanceId}/{resourceId}) et SizeBytes, types URL exclus, et manager-app envoie la taille avant l'upload. Le backfill ne sera donc pas à refaire au prochain upload. Reste le backfill lui-même, en deux moitiés : StoragePath est un simple UPDATE SQL (chemin déterministe), seul SizeBytes exige de lister le bucket. ⚠️ Mesuré le 09/08 sur 45 lignes : 37 ont un blob, 7 sont des types URL (Wikipedia, YouTube, agenda.php) sans aucun fichier — les inclure serait faux — et 1 est de type fichier sans URL. SizeBytes vaut 0 partout : le quota de stockage ne veut rien dire pour personne.
⚠️ Le « prérequis levé le 10/08 » n'était vrai qu'à moitié — relevé le 11/08 en extrayant le calculateur (C1). ResourceController a deux chemins de création, et le chemin multipart (le téléversement) écrivait SizeBytes mais laissait StoragePath nul ; seul le chemin JSON écrivait les deux. Les deux passent maintenant par ResourceStorage.Apply, donc le prérequis est réellement levé — mais des lignes créées entre-temps sont à rattraper par ce backfill.
Appeler ResourceStorage.PathFor plutôt que réécrire le chemin : c'est tout l'objet de C1, et l'écart (e) de la migration l'appellera aussi.
Aucune compression aujourd'hui : une photo 4K part entière. ~12× de gain sur le stockage et sur chaque consultation visiteur.
v2/media-storage-plan.mdAujourd'hui contrôlé uniquement dans l'endpoint legacy. Pré-vol + contrôle autoritaire à Create avec la taille lue depuis Firebase + suppression du blob à Delete.
Bucket en Europe = aucun quota gratuit, facturation dès le premier octet. L'alerte est la vraie protection contre la surprise.
v2/media-storage-plan.mdLe filtre incrémental teste la présence du fichier, pas sa version. Une image remplacée dans le CMS ne remonte jamais sur le device. Invisible pour le client.
v2/offline-visit-plan.mdLa table mappe audio/mp3, qui n'est pas un MIME standard. Les MP3 atterrissent probablement en .unknown. À vérifier sur device.
La liste est calculée puis le deleteSync() est commenté. Le stockage occupé sur le téléphone du visiteur ne diminue jamais.
Un fichier raté fait un print et passe. La visite est annoncée téléchargée alors qu'elle est incomplète.
Le rayon de déclenchement configuré par section n'a aucun effet — une constante en dur à 100 m le remplace.
parity-manager-visitapp.md §2Écrit et analysé, jamais ouvert dans un navigateur. À vérifier sur données réelles : la règle mono-canal, le rendu des titres longs dans les barres. Sur le PDF : les accents, et le logo — s'il manque, c'est le CORS du bucket Firebase, le rapport doit se générer quand même. Et après database update : que les instances soient bien passées à 395 jours (pas seulement les plans), et que le job visit-events-purge déclenché à la main ne supprime rien.
15 cas : images d'articles, audios, extensions de fichiers, fraîcheur, dégradation des types non offline. Conditionne l'urgence de tous les bugs hors ligne.
test-plan.md §21Inscription, création d'instance, essai 14 j, Stripe, 9 emails Resend, job de cycle d'essai. Code écrit et compilé, aucun parcours joué. Reste l'alias onboarding@ et le whsec_ Stripe CLI.
Le scénario le plus complet : SectionMap + SectionParcours + géodéclenchement + questions + mode jeu, d'un seul tenant.
test-plan.md §19.13« Tout ce que je configure est-il visible ? » — le chapitre qui valide les 13 types sur les deux apps visiteur.
test-plan.md §195 commandes, bloquant absolu. flutter analyze et next dev ne prouvent rien — seuls les vrais builds disent la vérité.
Une fois la prod stabilisée, pas avant : on n'ouvre pas une conversation en montrant un produit en cours de bascule. Premier temps 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 ensuite, pas dans le premier message. ⚠️ Rien n'est su de son rôle exact à ce stade : à ne pas supposer.
STATUS.md §1quinquies — Après la basculeLe rapport à la demande est livré (07/08, généré côté client). Reste l'envoi automatique, seule partie qui exige vraiment le backend : personne n'a son navigateur ouvert le 1er à 6 h. QuestPDF + Hangfire, config destinataires/fréquence, et une méthode d'envoi avec pièce jointe — les 10 méthodes d'IEmailService sont des templates figés sans attachement. ⚠️ QuestPDF exige libfontconfig1 + une police, absents du Dockerfile : ça marche en local sous Windows et casse dans le conteneur.
GetSummary agrège en mémoireeventsQuery.ToList() charge tous les événements de la période avant d'agréger. Tenable tant que la fenêtre était de 30 jours ; elle vient de passer à 13 mois. À passer en SQL avant de vendre le rapport annuel à un gros site — sinon c'est la mémoire du conteneur qui arbitre.
Ajouter ContentEmbedding a cassé 116 des 124 tests d'un coup : la suite tourne sur EF InMemory, qui ignore le type Vector et refuse de valider le modèle. Contourné en excluant l'entité hors Npgsql — les tests repassent, mais vector store, recherche cosinus, index HNSW et contrainte anti-doublon Hangfire restent structurellement non testables. Piste : Testcontainers.PostgreSql sur l'image Dockerfile.postgres.
SearchKnowledgePersona simple : réponses sourcées sur le contenu existant. Pas d'ingestion documentaire, pas d'OCR, pas de TTS — c'est ce qui le rend tenable en V1.
Livré le 10/08 : IngestionService (chargement des collections filles par sous-type, un jeu de morceaux par langue), serveur Hangfire dédié à 2 workers, garde AiTokensPerMonth > 0 — plan-starter est à 0 — et SearchKnowledge ajouté aux outils d'AssistantService plutôt qu'un /ask concurrent, avec sa règle dans les 4 prompts.
Joué contre la vraie base et la vraie API le 10/08 : 52 sections → 2150 morceaux en 53 s. Deux défauts que seule l'exécution pouvait montrer, corrigés dans la foulée. 1) Les gabarits de LanguageInit étaient indexés : les cinq premiers résultats d'une question en néerlandais étaient des NL - Title NL - Description, le bonus de langue suffisant à les faire passer devant du vrai contenu français — le cas même que le cross-lingue devait servir. 2) Le même paragraphe occupait trois des cinq résultats, au même score : DistinctBy(Text) avant le Take. ⚠️ Le post-filtrage HNSW reste non éprouvé à plusieurs instances — il n'y en a qu'une en base locale, soit le cas où le problème ne se voit pas.
Les deux angles morts du découpage sont fermés : HTML retiré avant l'embedding, et ligne trop longue recoupée à la fin de phrase — sans quoi un article dépassait l'entrée max de gemini-embedding-001 et l'échec emportait les 50 morceaux du lot.
Déclenchement unifié : l'Enqueue par contrôleur et un intercepteur EF ont coexisté une demi-journée. Retenu : SectionIndexingInterceptor seul — les 5 sous-contrôleurs totalisaient 30 SaveChanges et 0 Enqueue, donc ajouter 40 points d'intérêt à une carte ne réindexait rien.
Reporté en fin de backlog le 11/08. Tenable parce que la porte « rien en prod avant la fin du backlog » tient : d'ici là la journalisation ne tourne 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.
Déjà fait le 11/08 : CGU §8 réécrites (elles décrivaient un service qui ne collectait que des statistiques anonymes, citaient des durées de plans abandonnés, et ne mentionnaient aucun sous-traitant alors que la question du visiteur part chez Google), texte d'information visiteurs en FR/NL/EN, et purge à 90 jours active sans condition de configuration — une durée écrite dans un contrat n'est pas un réglage.
Reste : la table d'agrégats de thèmes (les CGU promettent que les regroupements survivent à la purge, or ThemeId est une colonne de la ligne supprimée — au 91e jour le client perdrait tout), le job de regroupement, un interrupteur de collecte par instance (⚠️ le client est responsable de traitement mais ne peut pas refuser la collecte), la mention à afficher côté visiteur, la relecture juridique du §8 et la vérification des conditions réelles de Google.
Deux écrans neufs compilent (flutter build web ✅) mais n'ont jamais été affichés dans un navigateur — le défaut récurrent de ce tableau. Le Guide IA (coquille à onglets, aperçu de conversation, carte de connaissance) et l'éditeur de parcours refondu (rail d'étapes, panneau, questions dépliées, enregistrement à la saisie).
Exige un manager-service qui tourne et une session ouverte : flutter run -d chrome, puis comparaison côte à côte avec claude design/guide-ia-screen.html et claude design/sectionparcours-refonte-flux.html. Pour le parcours, rejouer aussi test-plan.md §19.13 cas 0.
PdfPig avec cascade OCR Gemini (~0,03 $ / 100 pages). Word, PowerPoint, images. Legacy .doc/.ppt refusés à l'upload.
Le produit que les musées achètent depuis toujours, à coût marginal quasi nul. Aucun concurrent de cette gamme ne le fait.
v2/tts-pregenerated-plan.md« Un parcours de 45 min pour familles ». Marche sur le smartphone du visiteur, aucun hardware. Vendable aux villes comme balade sonore IA.
plan-import-ia-stats-subsides.md §3Plusieurs personas, assignables à une section ou une configuration. Le modèle part en V1 (liste, pas champ unique) ; le comportement attend — qui répond depuis l'accueil, et sur quel périmètre de contenu, est une vraie question de conception.
v2/tts-pregenerated-plan.md — PersonaConfigapp.myinfomate.beL'app existe : build ✅, 13 types de section rendus, assistant IA inclus depuis le 06/08. Il ne manque que le Dockerfile et l'entrée Traefik dans le compose. Le plan Essentiel vendu en self-service est web-only — sans ce déploiement, l'onboarding vend un produit inaccessible.
architecture-web-saas.md — À faireExtension web de tablet-app pour les clients ne voulant pas d'installation native. Reporté en V2 le 07/08 : aucun client demandeur, et la cible native ne compile même plus — il faudrait réparer ce build avant d'en dériver une version web.
Nouveau type de section + table de réponses anonymes, constructeur de formulaire et page de résultats agrégés. Reporté en V2 le 07/08 : purement additif, aucun impact sur le schéma existant — rien n'oblige à le faire passer avec la migration.
todo-features.md — SectionFormUne valeur d'enum (Panorama360) et un branchement dans l'affichage des ressources : s'affiche partout où une ressource s'affiche, sans toucher aux sections. Reporté en V2 le 07/08 — le plus petit des trois, à reprendre en premier.
Modèle ArAnchor, microservice Node de compilation des .mind, et remplacement du scanner visiteur par une WebView unifiée QR + image. Le plus lourd des trois, fort effet démo. Reporté en V2 le 07/08 — ni la migration ni la mise en prod n'en dépendent.
Backend fait, front absent. Users : scoping backend fait, manque le plafond de 5 et le compteur dans l'UI.
todo-features.mdTrois frames par persona, lipsync sur les timestamps Google TTS. Quelques jours, effet démo. Bouche-trou entre deux chantiers.
v2/talking-head-plan.mdPilote presse uniquement, quand le SDK sort de preview. 90 % de l'expérience est livrable en audio smartphone — profil d'appel à projets, pas de ligne produit.
roadmap.md — section XRClé de signature JWT, clés API Gemini et OpenWeather, connection strings prod, mot de passe MQTT, token Telegram. Le dossier RELEASE/ republie d'anciens secrets.
Déplacée depuis « Urgent » le 11/08, à la demande : les 6 repos sont privés, sur un 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 bouge pas, la fenêtre d'urgence oui.
⚠️ Ce qui ne se négocie pas : elle doit précéder la création de l'environnement prod (étape 16), sinon la prod naît avec les clés de l'historique et il faut rotationner deux fois. Et comme le test d'onboarding exige app.myinfomate.be déployé, ça veut dire avant la fin du lot de tests, pas au tout dernier moment. Elle remonte en « Urgent » immédiatement si un repo passe public, gagne un collaborateur externe, ou est cloné par un CI tiers.
dryRun=true n'écrit rien. 🔨 Joué le 11/08 sur l'export du 1ᵉʳ avril, et automatisé en test (MigrationDryRunTests, qui se saute si aucun Mongo n'écoute — la suite reste verte sans dépendance).
Il a trouvé du premier coup ce qu'aucune lecture de code n'avait vu : 327 sections dans Mongo, 307 migrées, et Erreurs : 0. Les 20 manquantes référencent deux configurations supprimées dans Mongo — 19 articles et un slider, du contenu MDLF bien réel (« Senteur : fraises », « Pupitre vigne », « Ruche 7 »). Ce n'est pas un défaut de la migration : les sections sont collectées configuration par configuration, donc les orphelines n'étaient jamais énumérées. Le défaut était le silence — elles partent maintenant dans Skipped, et le test affirme migrées + signalées = Mongo.
⚠️ Décision produit : recréer les 2 configurations dans Mongo avant la bascule pour récupérer ces 20 sections, ou acter leur perte. À trancher avec MDLF.
⚠️ À rejouer sur un dump frais : l'export a 4 mois et la prod tourne encore sur Mongo. Et MigrationController lit un Mongo vivant, pas ces fichiers — recette d'import dans v1-plan.
Deployment/Dockerfile.postgres (digest épinglé), compose prod, Traefik, port 5432 fermé. Puis dotnet ef database update sur la base vide et vérifier que postgis et vector répondent. Ne pas créer la prod avec les clés encore committées.
Noté comme « à faire » depuis juillet et jamais fait. Ici ça cesse d'être une bonne pratique : c'est la seule chose qui permette de recommencer si la bascule tourne mal.
STATUS.md §4 · §1quinquies — étape 18Le paramètre instanceId existe précisément pour ça. Commencer par la plus petite instance : si quelque chose casse, ça casse sur le plus petit périmètre possible.
Login manager-app, une app visiteur avec sa clé API, un parcours, une carte, un PDF, un quiz avec ses questions. Les défauts « silencieux » ne se voient qu'ici — pas dans le rapport de migration.
STATUS.md §1quinquies — étape 20Bascule DNS/API, Mongo conservé en lecture seule quelques jours, et le plan de rollback écrit avant le jour J — un retour arrière qu'on improvise à 23 h n'est pas un retour arrière.
STATUS.md §1quinquies — étapes 21-22Trente et un chantiers clos entre le 5 et le 11 août 2026.
switch qui collecte les ressources d'une configuration é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 — ni contenus d'articles, ni audios, ni icônes de carte, ni images de quiz. Remplacé par GetReferencedResourceIds(), qui existait déjà sur les 13 sous-types et n'attendait que d'être appelée. Côté import et côté client, la charge est enregistrée en une passe au lieu d'être redécouverte section par section : le serveur sait maintenant ce qu'il envoie, le client n'a plus à le deviner — et ça répare au passage la purge des fichiers obsolètes, pilotée par la même liste d'ids utilisés. 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, et c'est exactement ce qu'il ne faut plus pouvoir refaire. dotnet test 148/148. ⚠️ Le volet visiteur ne tient que sur l'analyse — l'APK ne compile pas — confirmation sur device au §21, carte « Visite hors ligne sur device » déjà en attente.
SectionEvent et SectionParcours sont nés avec Postgres v3, le default est un filet et non un trou, et le « scénario carnaval » qu'on croyait menacé est du contenu à créer, pas à migrer. (f) les trois booléens forcés à false : ces colonnes n'existent pas dans Mongo, false est le bon défaut — et le contrôle inverse a été fait, les cinq réglages qui existent vraiment dans l'export (IsDate, IsHour, IsSectionImageBackground, RoundedValue, ScreenPercentageSectionsMainPage) sont bien mappés sur AppConfigurationLink. (g) était tombé avec le rename du lot B. Trois cartes de la colonne « Bascule prod » se ferment sans une ligne de code.
QuizQuestions = new() jetait les questions : mesuré sur l'export, 5 sections quiz en portent 41, réponses comprises. Migrées, en MultipleChoice puisque le type de validation n'existait pas dans l'ancien modèle. Les agendas (aucun événement dans Mongo) et les parcours (inexistants avant) gardent leurs collections vides à raison. (d) était pire que décrit : Mongo n'a pas de champ Role, donc les 10 utilisateurs y avaient un accès complet, et le ContentEditor en dur les dégradait tous en silence — plus personne n'aurait pu gérer les utilisateurs de son instance. Passé à InstanceAdmin ; aucun SuperAdmin n'est créé par la migration, à poser à la main. (c) tout est généré ou dérivé, la source ne contenant que 4 champs. (e) StoragePath n'était pas écrit du tout : passe par le calculateur commun, et l'échec du HEAD, jusqu'ici avalé, remonte dans le rapport. (h) transaction unique, et surtout 500 au lieu de 200 OK sur échec fatal — le piège était qu'un script de bascule testant le code HTTP concluait au succès. dotnet test 143/143.
MigrationController revenait à l'écrire deux fois. Une seule migration EF porte les trois changements. SectionMap.MapResourceId → IconResourceId, ce qui fait tomber l'écart (g) de la bascule tout seul — et il fallait renommer la propriété de navigation MapResource avec, sinon la convention EF fabriquait un FK fantôme. Point vérifié plutôt que supposé : EF a produit un RenameColumn, pas un drop+add — les icônes déjà configurées survivent. SectionEvent.ParcoursIds supprimé (l'avertissement de perte de données d'EF ne porte que sur lui, c'est l'intention). Filigrane : Instance.IsImageWatermark remplace le instanceId == "633ee379…" en dur. Référence tenue : 130/130 tests avant comme après, build Debug et Release verts.
#if DEBUG » du rapport d'audit : dans AuthenticationController.Authenticate, un bloc écrasait l'email et le mot de passe reçus par un compte de test — toute compilation en Debug authentifiait n'importe quelle saisie en tant que test@email.be. Retiré. EnableSensitiveDataLogging (qui écrit les valeurs des paramètres dans les logs : hashes, clés API, données de visiteurs) passe sous #if DEBUG, l'idiome déjà employé dans ce même fichier pour le CORS et Hangfire — et le Dockerfile publiant en -c Release, c'est un vrai verrou, ce qu'un build Release a confirmé. Les autres #if DEBUG du repo sont légitimes.
api.dart déclarait 139 part pour 153 fichiers. Les 14 restants portaient part of openapi.api; sans être listés par leur bibliothèque — en Dart, hors du graphe de compilation : l'analyzer les lisait, le compilateur ne les voyait jamais. Toute l'asymétrie qui a coûté du temps était là, et avec elle la garantie que les supprimer ne pouvait pas casser un build. Mesuré : flutter analyze sur le package passe de 67 à 3 issues, et l'analyse globale de manager-app de 68 erreurs de bruit à 3 erreurs, toutes dans test/widget_test.dart (cassé et connu) — flutter analyze redevient un feu vert utilisable, ce qui débloque les lots design. flutter build web ✅ avant comme après.
mymuseum-visitapp/lib/api/openApiTest.dart générait depuis son propre swagger vers un manager_api_new local — un second client divergent dans un repo qui consomme celui de manager-app par path: — et tablet-app/lib/api/openApi.dart faisait de même sous un autre nom de fichier, ce qui lui avait permis d'échapper à toutes les recherches sur « openApiTest ». Plus un résidu tablet-app/manager_api_new/ réduit à un pubspec.lock, non suivi par git, daté d'avril. Les trois sont supprimés : ce ne sont pas des fichiers morts, c'était le moyen d'écraser les éditions manuelles — onboarding_api.dart, le câblage d'AIApi, le mapping isGood → isCorrect. Contrôle de non-régression : grep -rl "@Openapi" */lib doit rester vide.
SectionMap.MapMapProvider, SectionAgenda.AgendaMapProvider), pas par l'instance — et une même Configuration est liée à plusieurs ApplicationInstance via AppConfigurationLink, donc servie au mobile comme au web. Masquer le champ aurait supprimé le réglage mobile. Il est donc conservé, et manager-app cesse de laisser croire qu'il vaut pour le web : mention explicite sous les deux sélecteurs. Zéro ligne côté visitapp-web — Leaflet inconditionnel était déjà le comportement ; ce qui manquait n'était pas du code mais une phrase honnête dans l'interface.
PdfViewer existant et son proxy /api/pdf — le contournement CORS était déjà écrit, il n'y avait qu'à le brancher. Côté Flutter, la carte indiquait le mauvais endroit : showElementForResource fait un return CachedCustomResource(...) avant son switch, donc tout ce switch est du code mort depuis un moment — ajouter le cas PDF là n'aurait rien changé et aurait été « vérifié » sans rien prouver. Le vrai dispatcher est CachedCustomResource, et il a deux switch (ressource distante / fichier déjà téléchargé pour l'offline) qui tombaient tous deux sur Text("Not supported type") : les deux sont traités, le distant devant télécharger avant d'afficher puisque PDFView n'accepte qu'un chemin de fichier. Au passage : l'enum du client s'appelle ResourceType.Pdf, pas PDF. npm run build ✅, flutter build web ✅ — le volet mymuseum ne tient que sur l'analyse, son APK ne compilant pas.
number0/1/2, et le front finissait donc par écrire ?.value == 2 ? 'Puzzle' : … — lisible seulement si l'on connaît l'ordre de l'enum serveur par cœur. Des alias nommés d'après le serveur (simple, multipleChoice, puzzle) sont ajoutés à la main dans question_type.dart, sans toucher à values : ce sont les mêmes trois valeurs, pas de nouvelles. Les deux sites visés par le plan sont corrigés, plus les 7 usages number* du dialogue de question — il ne reste aucun QuestionType.number dans les trois apps.
Navigator.pop sur « Annuler » sans rien demander, pas un — Parcours, Étape et Question, chacun jetant tout ce qui était saisi sous lui. Le plus coûteux est le plus profond : une question de quiz avec ses réponses. Ce n'était pas « la croix » comme le disait cette carte, c'était le bouton Annuler, et le clic hors fenêtre était déjà neutralisé — le vrai chemin de perte restant est le retour arrière du navigateur, couvert par PopScope. Détection des modifications par instantané JSON, pris après la première frame : le dialog Question normalise ses réponses pendant sa construction, un instantané pris avant aurait déclaré « modifié » une fenêtre intacte. Réutilise showConfirmationDialog plutôt que d'ouvrir un second composant de confirmation. 3 clés i18n FR/EN/NL, flutter build web ✅. Retiré le jour même par la carte suivante — c'était l'issue prévue : un garde-fou n'a plus d'objet quand il n'y a plus rien à perdre.
showNewOrUpdate… sont supprimés au profit d'un rail d'étapes toujours visible à gauche, d'un panneau de détail à droite, et d'une question qui se déplie sur place au lieu d'ouvrir une quatrième fenêtre : la profondeur maximale retombe à 2 — l'éditeur, puis la traduction. Les champs sont d'abord sortis en trois widgets autonomes (ParcoursFields, EtapeFields, QuestionFields) — c'est ce qui a rendu la refonte possible sans tout réécrire, et ce qui resterait transposable si l'écran plein cadre revenait un jour.
Plus de bouton « Sauvegarder » : tout part à la saisie (débounce 700 ms ; les ajouts, suppressions et réordonnancements partent sans attendre). Deux pièges du backend, trouvés en lisant le contrôleur plutôt qu'en le supposant : UpdateGuidedPath supprime les étapes absentes du DTO, donc le payload n'emporte que celles qui ont déjà un id — les autres attendent leur CreateGuidedStep et seraient dupliquées ; et comme les questions n'ont pas d'endpoint propre (c'est voulu, elles partent avec leur étape), leur id entier est récupéré après coup par order, seul repère stable entre la liste locale et celle du serveur. Le parcours est créé à la première modification, pas à l'ouverture : une fenêtre neuve refermée intacte ne laisse rien en base. Si une écriture échoue, la fenêtre refuse de se fermer et le pied de page porte un « Réessayer ». 14 clés i18n FR/EN/NL, 3 retirées, flutter build web ✅. Reste la vérification à l'œil (DB5).
git status --porcelain sur les huit repos : vide partout, et aucun ahead/behind face au remote. Le lot 3 RAG est dans 18e4240, le socle visuel et l'écran Guide IA dans eb8e849. Ce qui rendait le point urgent — neuf fichiers non suivis qui n'existaient nulle part ailleurs et disparaissaient au premier git checkout — n'existe plus. À ne pas re-planifier : c'était l'étape 1 du lot A.
claude design/, JS interactif compris. Même découpage que ce tableau : contenu dans le repo, diffusion par artifact. Constat en ouvrant les fichiers : Guide IA et Statistiques déclarent des tokens rigoureusement identiques — c'était déjà un design system, jamais porté en Dart — et --brand #264863 est kPrimaryColor.
lib/Components/ : onze tailles différentes (9 à 25 px), aucune échelle, constants.dart portant encore // TO FILL WITH CORRECT COLOR et deux styles. L'échelle, les espacements et les rayons sont extraits du CSS des maquettes ; les couleurs restent celles de l'app — un écran neuf se fond dans manager-app, il n'y ouvre pas une seconde palette. Seules les valeurs sans équivalent existant sont reprises : remplissages discrets, bordures, gris atténué, ambre. Pas de thème sombre. La reprise des écrans existants reste son propre chantier, pas un effet de bord.
/api/AI/chat, pas une simulation — au passage, AIApi était dans le client généré mais exposé nulle part dans client.dart, où les 18 autres façades sont. Câblé. Nouveau GET /api/Ai/knowledge/{id} pour la carte « Ce que connaît votre guide » : tout est agrégé sur ContentEmbedding, donc sur ce qui est réellement indexé — compter les sections publiées donnerait un chiffre plus flatteur et faux, une section désactivée étant purgée de l'index. Le « points d'intérêt » de la maquette devient « morceaux de contenu » : les points d'une carte sont indexés dans le texte de leur SectionMap, les compter à part répondrait à une autre question. 34 clés i18n FR/EN/NL, dotnet test 130/130, flutter build web ✅.
Updateinstance changeait SubscriptionPlanId mais ne recopiait jamais les quotas du nouveau plan — CreateInstance le faisait, pas lui : passer un client de Starter à Premium ne lui donnait ni stockage ni jetons IA. 2) CheckQuota ne bloquait que si quota > 0 : à 0 jeton, ni blocage ni compteur — l'ambiguïté vient du code lui-même, où 0 veut dire illimité pour le stockage et pas d'IA pour les jetons. Une instance Starter avec IsAssistant à true consommait donc gratuitement et sans compteur, et toute instance migrée depuis Mongo arrive à 0 (écart c du §1quinquies). Corrigé en 403 avant tout appel au modèle. 3) BackfillInstanceAsync n'était appelé par personne : branché automatiquement quand AiTokensPerMonth passe de 0 à une valeur, plus un bouton de relance réservé au SuperAdmin dans l'écran Guide IA — outil de réparation, pas fonctionnalité : exposé au client il serait cliqué à chaque réponse décevante, pour un coût d'embedding complet et sans rien améliorer. IBackgroundJobClient injecté au lieu de la façade statique, qui lève sans JobStorage et cassait les tests. 130/130.
IEmbeddingService + GoogleEmbeddingService (batch par 50), et surtout vérifiés contre l'API réelle. Trois choses qu'aucun plan ne disait : le modèle sort en 3072 dimensions par défaut — dimensions: 768 est obligatoire ; les vecteurs réduits ne sont pas normalisés (norme mesurée 0,578, pas 1) et sans normalisation côté client le classement cosinus serait faussé sans lever d'erreur ; et Google ne renvoie pas le champ index du contrat OpenAI. Recherche cross-lingue jouée en base : question NL → contenu NL 0,8145, FR pertinent 0,7982, FR hors-sujet 0,4818. L'écart NL/FR n'étant que de 0,016, le bonus de score par langue est bien nécessaire.
HasAdvancedStats devient le seul différenciateur stats, et l'export PDF est inclus dès qu'il y a les stats. Motif : un dossier de subside est annuel — plafonné à 30 jours le rapport n'était envoyable à personne. Et la landing ne promettait aucune durée (la chaîne « 30 jours » existe dans translations.ts mais n'est affichée nulle part). Migration UnifyStatsRetentionTo13Months — elle met à jour les plans et reprend les instances en SQL, sans quoi les clients actuels seraient restés à 30 jours : c'est Instance.StatsHistoryDays que lit le contrôleur. dotnet test 124/124. ⛔ La purge associée est codée mais inactive jusqu'à la mise en place du pg_dump.
progression_mode ne pouvaient pas le voir — ils couvrent la relecture des booléens, pas le DTO fabriqué par la popup. Aussi : vidéos et audios d'étape rendus en dur comme des images dans les deux apps visiteur, « Quelle ambiance ? » restée en case à cocher, menu « Carte de base » disparaissant sans un mot. À valider — test-plan §19.13 cas 0.
postgis + pgvector (0.8.6, base épinglée par digest), table ContentEmbedding avec index HNSW, 7 colonnes IA/stockage sur Resource, table VisitorQuestion. 62 migrations, dotnet test 124/124, données intactes. Deux surprises : la base locale avait une migration de retard — les colonnes Guide* du lot 1 n'existaient pas, toute requête sur Instances aurait planté ; et un warning de collation (glibc 2.36→2.31) qui désalignait les index texte, corrigé par REINDEX puis REFRESH COLLATION VERSION — dans cet ordre. D'où l'épinglage par digest.
dotnet test 124/124, jamais vu tourner.
pdf Dart déjà présent) et partage les valeurs calculées de l'écran — un chiffre ne peut pas diverger entre l'écran et le document envoyé à la commune. Le test de génération a attrapé deux plantages qui seraient sortis au premier clic. Au passage : VisitEvent porte bien l'AppType, le canal vocal est branché ; et deux puces du sommaire promettaient des données inexistantes (parcours terminés, questions au guide IA), retirées.
dotnet test 124/124. Un seul fichier sans // @dart=2.18 cassait les trois apps Flutter d'un coup.
BuildAuditEntries() modifiait le ChangeTracker pendant son énumération — exception sur toute écriture d'entité auditée. Invisible tant que les tests ne compilaient plus.
IsStepLocked supprimé : il rendait une étape définitivement infranchissable même après réussite.
Instance, client généré étendu à la main, écran avec 35 clés i18n FR/EN/NL. dotnet build et flutter build web passent. Menu conditionné à isAssistant, le même drapeau que la garde d'AiController.