From 2c49acf79e7cddd89c9325359574f7131e922192 Mon Sep 17 00:00:00 2001 From: Thomas Fransolet Date: Thu, 13 Aug 2026 11:58:10 +0200 Subject: [PATCH] =?UTF-8?q?Proactif,=20M3,=20voix=20du=20CMS=20=E2=80=94?= =?UTF-8?q?=20et=20le=20p=C3=A9rim=C3=A8tre=20de=20D0=20ramen=C3=A9=20?= =?UTF-8?q?=C3=A0=20l'installation=20neuve?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ce qui a été livré côté mymuseum-visitapp est répercuté dans le plan, STATUS et le kanban. Trois corrections de docs, toutes vérifiées dans le code : - Meta-Rayban-Test et AI-Assistant-test sont les branches de travail à jour, pas des POC. Le §5bis affirmait le contraire et c'est ce qui avait fabriqué la réserve « à clarifier avant K6 ». Il n'y a rien à clarifier. - « L'APK se construit sans le POC dedans » était faux : les 4 erreurs Dart étaient dans deux ancêtres morts, pas dans le POC vivant, qui est bien embarqué. Conséquence inverse de celle qui était écrite — « démontrable » ne demandait pas de remettre les secrets ElevenLabs. - « TTS = ElevenLabs, basculer sur Gemini avant de vendre l'add-on » : déjà fait. constants.dart dit « ElevenLabs retiré du pipeline (trop cher) ». Nouveau, et non résolu : les déclenchements proactifs écriraient leur prompt machine dans VisitorQuestion, donc dans l'onglet « Ce que demandent vos visiteurs » — et gonfleraient le bloc « questions sans réponse ». Même famille que WeatherSyncService noyant le journal d'audit. Carte en Bugs ouverts, à traiter dans manager-service puis mymuseum-visitapp avant d'allumer le mode. D0 — périmètre ramené aux 15 cas d'origine : aucun visiteur n'a l'app installée et la consigne est « supprimer puis réinstaller ». Vérifié que _onCreate contient déjà dateUpdate, donc une installation neuve naît en v4 sans passer par _onUpgrade. Le code qui gère les .unknown reste, il ne coûte rien. Et 21.2.1 est réécrit : il testait un scénario impossible, remplacer une image créant un nouvel id. Kanban : M3 fermé, la carte proactif devient un bug bloquant, trois entrées en Fait. Compteurs mesurés — Bugs ouverts 2, Planifié 24, Fait récemment 54. Co-Authored-By: Claude Opus 5 --- STATUS.md | 13 +++++++---- kanban.html | 66 +++++++++++++++++++++++++++++----------------------- test-plan.md | 11 +++++++-- v1-plan.md | 20 ++++++++++++++-- 4 files changed, 73 insertions(+), 37 deletions(-) diff --git a/STATUS.md b/STATUS.md index f4cd69a..b9a8130 100644 --- a/STATUS.md +++ b/STATUS.md @@ -336,7 +336,7 @@ Restait le contenu réel du lot, les deux avertissements du build : **NDK 27.0.1 ⚠️ **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.** Le travail V1 récent de `mymuseum-visitapp` (D1, lot E, lot A) est committé sur **`Meta-Rayban-Test`**, pas sur `master` — c'est donc la branche de travail réelle, alors que le §5bis la décrit comme « un POC jamais mergé, actif commercial ». `tablet-app` est dans le même cas, sur `AI-Assistant-test`. **Publier des APK depuis une branche décrite comme un POC n'est pas neutre** : à clarifier avant K6, pas pendant. +✅ ~~**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`. @@ -785,12 +785,17 @@ 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.** C'est le seul poste qui peut casser la marge d'un add-on à 20-30€/mois : un audioguide vocal génère du volume continu, contrairement au chat texte. `gemini_tts_engine.dart` existe déjà — **basculer dessus avant de vendre l'add-on**. +- ✅ ~~**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. Le point ci-dessus tient toujours : ce sont les **4 erreurs Dart** qui restent, et elles n'empêchent pas le build parce que ces fichiers sont **hors du graphe de `main.dart`**. Autrement dit, l'APK se construit **sans le POC dedans** — « démontrable » suppose toujours de remettre les deux constantes. -- ⚠️ **Et c'est la branche sur laquelle se fait le travail V1.** Le point « branche jamais mergée » ci-dessus décrit un POC de côté ; en réalité les commits V1 récents de `mymuseum-visitapp` (D1, lot E, lot A) sont **sur `Meta-Rayban-Test`**, pas sur `master`. Ce n'est donc pas une branche parallèle qu'on garde au chaud, c'est la branche de travail. À trancher **avant K6 (publier les APK)** : on publie depuis elle, ou on merge d'abord. `tablet-app` est dans le même cas, sur `AI-Assistant-test`. +- ✅ ~~**`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. ### Ce que ça change côté commercial diff --git a/kanban.html b/kanban.html index 061dbc8..2a02a3a 100644 --- a/kanban.html +++ b/kanban.html @@ -458,9 +458,9 @@
1Migration v3
2Bugs ouverts
6À tester
-
25Planifié
+
24Planifié
8Bascule prod
-
51Fait récemment
+
54Fait récemment
@@ -520,25 +520,24 @@

Bugs ouverts

2
-
+
+
backendvisitappBloque l'activation — 13/08
+

Les déclenchements proactifs pollueraient « Ce que demandent vos visiteurs »

+

Le proactif est câblé le 13/08 (garde, cycle de vie, points GPS depuis meterZoneGPS, réglage visiteur) et M3 est fermé avec lui — APK dev vert. Il reste ce point, et il est bloquant avant d'activer le mode chez qui que ce soit.

+

⚠️ RecordVisitorQuestion écrit Question = request.Message (AiController:135). Or le prompt d'un déclenchement automatique est une consigne machine : « Tu es un guide audio de musée. Le visiteur vient d'entrer dans la zone X. Accueille-le… ». Chaque passage devant une œuvre ajouterait donc une fausse « question de visiteur » dans l'onglet Guide IA — un écran qui existe précisément pour montrer ce que les humains demandent.

+

Et ça ne s'arrête pas à du bruit : HasAnswer se déduit des sources, donc un prompt d'accueil sans citation viendrait aussi gonfler le bloc ambre « questions sans réponse », celui qui sert à repérer les trous de contenu. Même famille que WeatherSyncService noyant le journal d'audit : le flot machine noie le signal humain dans l'écran fait pour lui.

+

À faire : un drapeau porté par AiChatRequest (déclenchement automatique vs question posée), et RecordVisitorQuestion qui n'enregistre pas — ou marque distinctement — les tours d'origine machine. Deux repos : manager-service puis mymuseum-visitapp. ⚠️ À trancher en même temps : ces tours consomment des jetons du quota client, donc RecordUsage, lui, doit continuer à les compter.

+

Écouteurs : ne pas les détecter — confirmé le 13/08. Le mode est opt-in via un switch dans VoiceModeSheet ; le sous-titre dit « écouteurs recommandés ». On informe, on n'arbitre pas à la place du visiteur.

+ v1-plan.md lot F — proactif & M3 +
+ +
visitapp

Échecs de téléchargement silencieux

Un fichier raté fait un print et passe. La visite est annoncée téléchargée alors qu'elle est incomplète.

v2/offline-visit-plan.md
-
-
mobileM3
-

meterZoneGPS ignoré

-

Le rayon de déclenchement configuré par section n'a aucun effet — une constante en dur à 100 m le remplace.

-

Périmètre tranché le 12/08 : mymuseum-visitapp + visitapp-web. tablet-app est exclu — SectionParcours est hors périmètre kiosk (tranché en K3), le code n'y serait jamais exécuté.

-

⚠️ Confirmé le 12/08 : la colonne est correctement mappée côté serveur (les 13 sous-types dans SectionFactory), et aucun code des trois apps ne la lit — seulement les swagger et les modèles générés. Le trou est entièrement côté visiteur.

-

⚠️ Trouvé le 12/08 : il y a DEUX implémentations de géodéclenchement qui s'ignorent, et ça change le chantier.

-

1. La vivanteconfiguration_page.dart:43 : int meterToBeacon = 100; // 15 meters. La valeur dit 100, le commentaire dit 15. C'est la constante que M3 désigne.
2. La morteGeoBeaconTriggerService : 20 m en dur pour le GPS, 3 m pour les beacons, jamais démarrée (aucun appel à start()).

-

Aucune des deux ne lit meterZoneGPS. Corriger la première en laissant la seconde, c'est se préparer à re-trouver le même bug. ✅ Fusionné le 12/08 avec le branchement du mode proactif (carte « Déclenchement proactif ») : décider laquelle devient la bonne, puis lui faire lire le rayon configuré par section. Un seul chantier, même raisonnement que L5.

- v1-plan.md lot F — proactif & M3 · parity-manager-visitapp.md §2 -
-
@@ -596,7 +595,7 @@
-

Planifié

25
+

Planifié

24
@@ -668,18 +667,6 @@ v1-plan.md — lot J · cgu-myinfomate.md §8 · mention-information-visiteurs.md -
-
visitappPérimètre V1 neuf — 12/08
-

Déclenchement proactif pour tous, pas seulement les lunettes

-

Tranché le 12/08. Un téléphone qui parle à l'approche d'une œuvre, écouteurs aux oreilles, c'est l'audioguide de musée classique — vendable bien au-delà des Ray-Ban, et sur du matériel que le visiteur a déjà.

-

⚠️ Ce n'est pas « retirer une garde », c'est câbler une fonction morte. Mesuré le 12/08 : GeoBeaconTriggerService.instance.start() n'est appelé nulle part, glassesEnabled est déclaré = false et jamais assigné, proactiveModeEnabled non plus, et geoPoints porte le commentaire « à peupler depuis la configuration » — ce qui n'est fait nulle part. Quatre choses à brancher.

-

Le service couvre GPS et beacons : entrée dans le rayon d'un GeoTriggerPoint (20 m par défaut) ou beacon BLE à moins de 3 m, avec 30 s de cooldown par point.

-

À faire : la garde redevient proactiveModeEnabled seul — ce que la doc de la classe décrivait déjà, le && glassesEnabled n'y a jamais figuré (sixième « le commentaire dit autre chose que le code » du projet) ; un appel à start() ; geoPoints peuplé depuis la configuration ; un réglage visiteur ; et VoiceSource qui devient obligatoire — un prompt automatique atterrit dans VisitorQuestion comme s'il venait du visiteur.

-

Écouteurs : ne pas les détecter. Le mode est opt-in, le visiteur qui l'active a choisi d'être parlé — détecter la sortie audio ajoute une dépendance pour arbitrer ce qu'il a déjà tranché. À revoir si le test dit le contraire.

-

⚠️ Fusionne avec le bug M3 (carte « meterZoneGPS ignoré ») : deux implémentations de géodéclenchement s'ignorent, aucune ne lit le rayon configuré. Un seul chantier. ~2 à 3 jours, M3 compris. Nouveau périmètre V1 assumé, à arbitrer contre la date de bascule au même titre que K9.

- v1-plan.md lot F — proactif & M3 -
-
visitapplunettesNouveau 12/08

Miroir de la conversation vocale dans le chat

@@ -888,9 +875,30 @@

Fait récemment

-

Cinquante-et-un chantiers clos entre le 5 et le 13 août 2026.

+

Cinquante-quatre chantiers clos entre le 5 et le 13 août 2026.

+
+ Déclenchement proactif câblé, et M3 fermé avec lui + Quatre branchements : la garde, le cycle de vie, les points GPS, le réglage visiteur. APK dev vert. ⚠️ La garde recommandée par le plan aurait coûté de l'argent : _trigger appelle le LLM d'abord et ne parle qu'ensuite via activeVoiceOrchestrator?.ttsEngine, dont le ?. avale le cas « aucun mode vocal actif ». Avec proactiveModeEnabled seul, un visiteur traversant une zone avec l'app en poche et le vocal éteint consommait des jetons Gemini facturés au client pour une phrase que personne n'entend. La garde retenue interroge l'orchestrateur : la vraie condition n'est pas « quel matériel » mais « y a-t-il quelqu'un pour écouter ». + Il n'y avait pas de troisième mode à inventer : VoiceMode.voiceOnly existe déjà et construit le même orchestrateur que le mode lunettes — l'audioguide sur téléphone était là. Le proactif est donc un sous-réglage, pas une tuile : l'opposer aux deux autres forcerait à choisir entre « je pose des questions » et « on me parle ». glassesEnabled est supprimé, ses trois usages étaient morts — dont une pastille « Lunettes / Déconnecté » que personne n'avait jamais vue. + M3 : meterZoneGPS devient le rayon par section, la constante ne servant plus que de défaut. ⚠️ Deux pièges de format vérifiés : currentSections porte des maps JSON brutes, pas des DTO, et latitude/longitude y sont des chaînes. ⚠️ Reste un bloquant avant d'activer le mode — voir la carte « Ce que demandent vos visiteurs » en Bugs ouverts. +
+ +
+ La voix choisie par le client n'était pas celle qu'entendaient les visiteurs + Le sélecteur Viva / Marco de manager-app écrivait dans le vide. Il existe depuis le 07/08 et enregistre Instance.GuideVoiceId, mais mymuseum-visitapp ne lisait jamais ce champ : il utilisait une constante de build, dont le défaut était Algieba — une voix que personne n'a jamais écoutée, héritée d'avant le sélecteur. 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 ; le repli passe à Sulafat (Viva). + ⚠️ Et 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 en silence sur la voix système d'Android. C'est le silence qui l'a rendu invisible des mois. Le repli reste le défaut, et c'est voulu : les tests de terrain ne consomment ni jetons ni argent. Il s'annonce maintenant dans les logs, constants.dart porte la commande, et launch.json a une configuration « Dev + voix Gemini ». + Au passage, une ligne périmée de STATUS tombe : « TTS = ElevenLabs, basculer sur Gemini avant de vendre l'add-on » — c'est déjà fait, constants.dart dit « ElevenLabs retiré du pipeline (trop cher) ». Le risque de marge qui motivait cette ligne est écarté. impl/elevenlabs_tts_engine.dart est conservé comme option, à rouvrir seulement si un client juge la voix Gemini insuffisante. +
+ +
+ Trois services morts supprimés — et une conclusion de STATUS retournée + wake_word_service.dart, glasses_qr_scanner_service.dart et glasses_tts_service.dart portaient les 4 erreurs Dart connues (kElevenLabsApiKey, kElevenLabsVoiceId) et l'APK se construisait quand même : personne ne les importait — un îlot hors du graphe de main.dart, même mécanique que les fichiers orphelins de manager_api_new. Supprimés le 13/08 : flutter analyze lib rend zéro erreur, une première pour ce repo. ⚠️ Piège d'homonymie : android/…/WakeWordService.kt porte le même nom et est bien vivant — service natif déclaré au manifeste, c'est lui qui fait tourner le wake word. Seul le Dart est parti. + Mais le §5bis en tirait « l'APK se construit sans le POC dedans », ce qui est faux. Ces deux fichiers sont les ancêtres de Services/Glasses/ ; le POC actuel, lui, est bien dans l'APK. Conséquence inverse de celle qui était écrite : « démontrable » ne suppose pas de remettre les constantes ElevenLabs — le chemin vivant choisit GeminiTtsEngine et ne les touche jamais. La bascule TTS → Gemini réclamée avant de vendre l'add-on est déjà faite dans le code. + ⚠️ Et ça rétrécit le chantier du miroir vocal : j'avais compté cinq AssistantService distincts, donc cinq historiques. Deux sont dans ce code mort. Il y en a trois de vivants — le chat, le vocal, le proactif. +
+
FAQ de la landing — le plan à 179 € s'appelait encore « Bundle » 41 occurrences en 4 langues (FR, EN, NL, DE) dans src/data/segments.ts, alors que les cartes de tarifs disent « Premium » : un prospect qui lisait la grille puis la FAQ voyait deux noms pour la même offre. Les formes composées des langues germaniques suivent (Bundle-abonnementenPremium-abonnementen, Bundle-PlänePremium-Pläne). Vérifié : plus aucun « Bundle » dans myinfomate-landing/src, et les 41 occurrences étaient toutes de la prose — aucun identifiant touché. npm run build vert. diff --git a/test-plan.md b/test-plan.md index 0b423af..24e6e0c 100644 --- a/test-plan.md +++ b/test-plan.md @@ -722,7 +722,14 @@ Points de vigilance à ne pas oublier au moment de tester : ## 21. Visite hors ligne — diagnostic terrain > Ajouté le 2026-08-07. **À jouer en premier** : conditionne l'urgence du chantier [v2/offline-visit-plan.md](v2/offline-visit-plan.md). -> Analyse du code : le `switch` de collecte des ressources est commenté dans `ConfigurationController.Export` **et** le bloc symétrique dans `downloadConfiguration.dart`. Attendu : une visite téléchargée ne contient ni images d'articles ni audios. À confirmer sur un vrai device. +> Analyse du code : le `switch` de collecte des ressources est commenté dans `ConfigurationController.Export` **et** le bloc symétrique dans `downloadConfiguration.dart`. Attendu : une visite téléchargée ne contient ni images d'articles ni audios. À confirmer sur un vrai device. **Corrigé en D1 le 2026-08-11** — ce §21 vérifie désormais la correction, pas le diagnostic. +> +> ✅ **Périmètre arrêté le 2026-08-13 : installation neuve, et rien d'autre.** Aucun visiteur n'a l'app installée à ce jour, et la consigne aux 4 clients est « supprimer l'app et la réinstaller ». Ça retire **deux cas** que D2 avait ouverts, et il vaut mieux savoir pourquoi avant de les remettre : +> +> - ⛔ **La montée v3 → v4 ne se teste pas** : `_onCreate` appelle `createTable(resources)`, qui contient déjà `dateUpdate TEXT` (`DatabaseHelper.dart:239`). Une installation neuve naît en v4 et n'emprunte jamais `_onUpgrade`. Vérifié plutôt que supposé — c'est exactement l'endroit où une colonne ajoutée en migration et oubliée à la création aurait fait planter toutes les installations neuves. +> - ⛔ **Le cas « device portant des `.unknown` » ne se teste pas non plus** : ces fichiers datent d'avant D3, donc d'installations qui n'existent plus. Le traitement de `.unknown` par `isResourceOutdated` **reste dans le code** — il ne coûte rien et redevient utile au premier device qui garde une vieille installation. On ne teste pas un chemin de réparation pour une population de zéro ; on ne le supprime pas pour autant. +> +> ⚠️ **Ce qui fait tenir ce périmètre, et qui peut sauter** : la table rase n'est gratuite que tant qu'il n'y a pas de visiteur réel. Dès qu'un téléphone garde une installation — après la bascule, typiquement — les deux cas ci-dessus redeviennent la seule preuve que le parc existant est réparé. ### 21.1 — État réel du téléchargement @@ -739,7 +746,7 @@ Points de vigilance à ne pas oublier au moment de tester : | # | Action | Attendu | OK | |---|--------|---------|-----| -| 21.2.1 | Télécharger une visite, puis **remplacer une image** dans manager-app (même ressource), puis relancer le téléchargement | L'image mise à jour remplace l'ancienne sur le device | | +| 21.2.1 | ⛔ ~~Télécharger une visite, puis **remplacer une image** dans manager-app (même ressource), puis relancer le téléchargement~~ **Cas impossible, réécrit le 2026-08-13** : `Upload` appelle `GenerateHexId()` à chaque téléversement (`ResourceController:276`), donc « remplacer une image » crée **un nouvel id et une nouvelle URL** — aucun flux ne réécrit le blob d'un id existant. Le cas d'origine testait un scénario qui ne peut pas se produire.
**À jouer à la place** : téléverser une **nouvelle** image, la placer dans une section déjà téléchargée, re-télécharger | La nouvelle image arrive sur le device ; l'ancienne, si plus référencée, est purgée (cf. 21.2.3) | | | 21.2.2 | Relancer un téléchargement sans aucune modification | Rien n'est re-téléchargé, message « à jour » | | | 21.2.3 | Supprimer une ressource dans manager-app puis re-télécharger | Le fichier local correspondant est purgé | | diff --git a/v1-plan.md b/v1-plan.md index e8905af..8b0a84d 100644 --- a/v1-plan.md +++ b/v1-plan.md @@ -181,6 +181,22 @@ Ne change pas : la popup de traduction (un niveau justifié, langues verticales, - `mymuseum-visitapp` : déclenchement proactif (+ M3), miroir de la conversation vocale, émission des événements vocaux. C'est désormais **tout le reste du lot F**, et le plus gros. - Non commencé, indépendant : l'aperçu de conversation (**L9**). +**Volet proactif + M3 du 2026-08-13 (`mymuseum-visitapp`) — ce que le code a corrigé de l'énoncé :** + +⚠️ **La garde ne devient pas `proactiveModeEnabled` seul, et c'est le point qui compte.** `_trigger` appelle le LLM **d'abord** et ne parle qu'ensuite via `activeVoiceOrchestrator?.ttsEngine` — le `?.` avale le cas « aucun mode vocal actif ». Avec la garde recommandée par le plan, un visiteur traversant une zone avec l'app en poche et le vocal éteint **aurait consommé des jetons Gemini facturés au quota du client pour une phrase que personne n'entend**, toutes les 30 s par point. La garde retenue est `proactiveModeEnabled && activeVoiceOrchestrator?.isRunning == true` : la vraie condition n'est pas « quel matériel », c'est « y a-t-il quelqu'un pour écouter ». + +⛔ **Il n'y avait pas de troisième mode à inventer : `VoiceMode.voiceOnly` existe déjà** (`voice_controller.dart:16`) et construit **le même** orchestrateur que le mode lunettes — wake word, STT, TTS Gemini, LLM. Le mode `glasses` n'ajoute que la connexion des lunettes. L'audioguide sur téléphone était donc déjà là, et `VoiceModeSheet` propose déjà les deux tuiles. **Le proactif est un sous-réglage, pas une tuile** : l'opposer aux deux autres forcerait à choisir entre « je pose des questions » et « on me parle ». + +⚠️ **`glassesEnabled` est supprimé, pas corrigé.** Ses trois usages étaient morts : la garde du proactif, et deux dans `AssistantChatSheet` — dont un (`:129`) déjà doublé par `MetaGlassesService.isConnected`, la vraie condition. La pastille « Lunettes / Déconnecté » (`:239`) n'avait **jamais été affichée à personne**, le drapeau n'étant jamais vrai ; elle s'accroche désormais à l'état réel de connexion. + +⚠️ **Deux pièges de format sur les points GPS, vérifiés plutôt que supposés.** `currentSections` porte des **maps JSON brutes**, pas des DTO (même idiome que `assistantSuggestions` et `ScannerDialog`), et `latitude`/`longitude` y sont des **chaînes** — c'est le type du `SectionDTO` généré. D'où `double.tryParse`. `meterZoneGPS` devient le rayon par section, la constante ne servant plus que de défaut : **c'est là que M3 se ferme**. + +⛔ **Un point n'est PAS livré, et il bloque l'activation du mode chez qui que ce soit.** `RecordVisitorQuestion` écrit `Question = request.Message` (`AiController:135`) — or le prompt d'un déclenchement automatique est une consigne machine (« Tu es un guide audio de musée. Le visiteur vient d'entrer dans la zone X. Accueille-le… »). Chaque passage devant une œuvre ajouterait donc **une fausse question de visiteur** dans l'onglet Guide IA, écran qui existe précisément pour montrer ce que les humains demandent. Et `HasAnswer` se déduisant des sources, un prompt d'accueil sans citation **gonflerait aussi le bloc ambre « questions sans réponse »**. C'est la famille de `WeatherSyncService` noyant le journal d'audit : le flot machine noie le signal humain dans l'écran fait pour lui. **À faire dans `manager-service` puis `mymuseum-visitapp`** : un drapeau sur `AiChatRequest`, et une journalisation qui distingue les tours machine — sans cesser de compter leurs jetons au quota, eux sont bien consommés. Le code proactif est en place et compile, mais **ne pas allumer le réglage avant ça**. + +✅ **Code mort supprimé le 2026-08-13 — `flutter analyze lib` rend zéro erreur, une première pour ce repo.** Trois fichiers Dart : `wake_word_service.dart`, `glasses_qr_scanner_service.dart` et `glasses_tts_service.dart` (l'ancien TTS ElevenLabs, importé par les deux seuls autres). Ils portaient les **4 erreurs** `kElevenLabsApiKey` / `kElevenLabsVoiceId` et l'APK se construisait quand même : personne ne les importait — un îlot hors du graphe de compilation, même mécanique que les 14 fichiers orphelins de `manager_api_new`. **C'étaient les ancêtres de `Services/Glasses/`.**
⚠️ **Piège d'homonymie, à ne pas re-déclencher** : `android/…/WakeWordService.kt` porte le même nom et est **bien vivant** — service natif déclaré au manifeste et piloté par `MainActivity`, c'est lui qui fait tourner le wake word. Seul le Dart est parti.
✅ **`impl/elevenlabs_tts_engine.dart` est conservé** : c'est l'implémentation de génération courante, gardée comme option si des clients jugeaient la voix Gemini insuffisante — beaucoup plus cher, donc pas un défaut.
⚠️ **Corollaire qui rétrécit le chantier du miroir vocal** : j'avais compté **cinq** `AssistantService` distincts, donc cinq historiques. Deux étaient dans ce code mort. **Il y en a trois de vivants** : le chat (`AssistantChatSheet:70`), le vocal (`MyInfoMateLlmClient:18`, `maxHistory: 6`) et le proactif (`geo_beacon_trigger_service:62`). À unifier, mais trois, pas cinq. + +⛔ **Le choix de voix du client n'était pas honoré — corrigé le 2026-08-13, et ce n'était pas au programme.** 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. **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 ; le repli du build passe d'`Algieba` — jamais écoutée par personne — à `Sulafat`.
⚠️ **Et aucun APK ne parlait avec la voix du produit** : `GeminiTtsEngine` n'est choisi que si `GEMINI_API_KEY` est injectée au build, ce qui n'était fait nulle part — tous les builds retombaient **en silence** sur la voix système d'Android. Le repli reste le défaut (les tests de terrain ne coûtent alors ni jetons ni argent) mais il s'annonce dans les logs, `constants.dart` porte la commande et `launch.json` a une configuration « Dev + voix Gemini ».
⚠️ **À trancher au lot J** : le STT n'est pas Google non plus — `WhisperSttEngine` pointe sur `api.openai.com` si `WHISPER_API_KEY` est définie (elle est vide aujourd'hui, c'est le device qui transcrit). L'activer ajouterait **OpenAI comme sous-traitant**, que le §8.5 des CGU ne mentionne pas. + **Volet `manager-app` du 2026-08-13 — deux décisions prises en écrivant, à ne pas re-trancher :** ⚠️ **Le bouton du portail ne remplace pas celui du Checkout, il prend sa place quand l'essai est fini.** L'écran n'affichait rien du tout hors essai (`if (isTrialActive)` enveloppait le seul bouton) : un client payant arrivait sur un écran sans action. C'est maintenant Checkout **pendant** l'essai, portail **après** — les deux ne coexistent jamais, souscrire deux fois n'a pas de sens. @@ -195,7 +211,7 @@ Ne change pas : la popup de traduction (un niveau justifié, langues verticales, | ~~Insertion `VisitorQuestion`~~ · ~~purge 90 j~~ · ~~onglet « Ce que demandent vos visiteurs »~~ | ✅ **Livrés le 2026-08-11** | | ~~Job de regroupement en thèmes~~ | **Déplacé au lot J** (J2) — il conditionne la promesse d'agrégats des CGU | | Aperçu de conversation | **L9** — converger avec « Mode preview / cible Assistant-Persona » | -| **Déclenchement proactif pour tous — ajouté le 2026-08-12, périmètre V1 neuf** | ✅ **Tranché : le proactif sort du domaine réservé aux lunettes.** Un téléphone qui parle à l'approche d'une œuvre, écouteurs aux oreilles, **c'est l'audioguide de musée classique** — vendable très au-delà des Ray-Ban, et sur du matériel que le visiteur a déjà.
⚠️ **Ce n'est pas « retirer une garde », c'est câbler une fonction morte.** Mesuré le 2026-08-12 : `GeoBeaconTriggerService.instance.start()` **n'est appelé nulle part**, `glassesEnabled` est déclaré `= false` et **jamais assigné**, `proactiveModeEnabled` non plus, et `geoPoints` porte le commentaire « à peupler depuis la configuration » — ce qui n'est fait nulle part. Quatre choses à brancher, pas une.
**Le travail** : (1) la garde redevient `proactiveModeEnabled` seul — **ce que la doc de la classe décrivait déjà**, le `&& glassesEnabled` n'y a jamais figuré ; (2) un appel à `start()` au bon moment du cycle de vie ; (3) `geoPoints` peuplé depuis la configuration ; (4) un réglage visiteur pour activer le mode ; (5) `VoiceSource` devient **obligatoire** et non plus confortable — voir la ligne « canal Vocal ».
✅ **Recommandation sur les écouteurs : ne pas les détecter.** Le mode est **opt-in** — le visiteur qui l'active a choisi d'être parlé. Détecter la sortie audio ajoute une dépendance pour arbitrer une situation que l'utilisateur a déjà tranchée. À revoir seulement si le test montre le contraire. | ⚠️ **Fusionne avec le bug M3**, et c'est l'économie à ne pas rater. Il existe **deux implémentations de géodéclenchement qui s'ignorent** : `GeoBeaconTriggerService` (morte, 20 m en dur pour le GPS, 3 m pour les beacons) et `configuration_page.dart:43` (vivante), dont la ligne est `int meterToBeacon = 100; // 15 meters` — **la valeur dit 100, le commentaire dit 15**. Aucune des deux ne lit `meterZoneGPS`. **Corriger M3 et brancher le proactif, c'est le même chantier** : décider laquelle des deux devient la bonne, puis lui faire lire le rayon configuré par section. Les traiter séparément, c'est corriger une constante et laisser l'autre. Même raisonnement que **L5**.
⚠️ **Nouveau périmètre V1, assumé** : ~2 à 3 jours, M3 compris. À arbitrer contre la date de bascule au même titre que K9 | +| ~~**Déclenchement proactif pour tous**~~ | 🔨 **Livré le 2026-08-13** (garde, cycle de vie, points GPS, réglage visiteur), `flutter build apk --flavor dev` vert. **M3 fermé dans la même passe**, comme l'annonçait la colonne de droite. Détail sous le tableau. Ce qui reste du proactif est le **test sur device**, pas du code.
Énoncé d'origine : ✅ **Tranché : le proactif sort du domaine réservé aux lunettes.** Un téléphone qui parle à l'approche d'une œuvre, écouteurs aux oreilles, **c'est l'audioguide de musée classique** — vendable très au-delà des Ray-Ban, et sur du matériel que le visiteur a déjà.
⚠️ **Ce n'est pas « retirer une garde », c'est câbler une fonction morte.** Mesuré le 2026-08-12 : `GeoBeaconTriggerService.instance.start()` **n'est appelé nulle part**, `glassesEnabled` est déclaré `= false` et **jamais assigné**, `proactiveModeEnabled` non plus, et `geoPoints` porte le commentaire « à peupler depuis la configuration » — ce qui n'est fait nulle part. Quatre choses à brancher, pas une.
**Le travail** : (1) la garde redevient `proactiveModeEnabled` seul — **ce que la doc de la classe décrivait déjà**, le `&& glassesEnabled` n'y a jamais figuré ; (2) un appel à `start()` au bon moment du cycle de vie ; (3) `geoPoints` peuplé depuis la configuration ; (4) un réglage visiteur pour activer le mode ; (5) `VoiceSource` devient **obligatoire** et non plus confortable — voir la ligne « canal Vocal ».
✅ **Recommandation sur les écouteurs : ne pas les détecter.** Le mode est **opt-in** — le visiteur qui l'active a choisi d'être parlé. Détecter la sortie audio ajoute une dépendance pour arbitrer une situation que l'utilisateur a déjà tranchée. À revoir seulement si le test montre le contraire. | ⚠️ **Fusionne avec le bug M3**, et c'est l'économie à ne pas rater. Il existe **deux implémentations de géodéclenchement qui s'ignorent** : `GeoBeaconTriggerService` (morte, 20 m en dur pour le GPS, 3 m pour les beacons) et `configuration_page.dart:43` (vivante), dont la ligne est `int meterToBeacon = 100; // 15 meters` — **la valeur dit 100, le commentaire dit 15**. Aucune des deux ne lit `meterZoneGPS`. **Corriger M3 et brancher le proactif, c'est le même chantier** : décider laquelle des deux devient la bonne, puis lui faire lire le rayon configuré par section. Les traiter séparément, c'est corriger une constante et laisser l'autre. Même raisonnement que **L5**.
⚠️ **Nouveau périmètre V1, assumé** : ~2 à 3 jours, M3 compris. À arbitrer contre la date de bascule au même titre que K9 | | **Miroir de la conversation vocale dans le chat — ajouté le 2026-08-12** | ✅ **Tranché : le chat affiche en direct ce qui se dit aux lunettes.** Ouvrir l'assistant pendant que les lunettes tournent montre la même conversation, transcrite au fil de l'eau — une conversation, deux surfaces.
⚠️ **Le coût n'est pas l'affichage, c'est l'unification des historiques.** Vérifié le 2026-08-12 : `MyInfoMateLlmClient` construit **son propre** `AssistantService` (`maxHistory: 6`), distinct de celui du chat écrit. Aujourd'hui, poser une question aux lunettes puis ouvrir le chat du téléphone donne un interlocuteur qui ne sait rien de ce qui vient d'être demandé. C'est **ça** le chantier ; la transcription, elle, est déjà disponible — `VoiceOrchestrator` expose `lastTranscription` et `lastTtsText` en `ValueNotifier`, personne ne les affiche.
✅ **Bénéfice de bord qui justifie de le faire proprement** : `VisitorQuestion.ConversationId` est un GUID de session, « seul lien entre deux tours ». Deux historiques = deux conversations distinctes en base pour un même visiteur qui a simplement changé de surface. **Unifier l'historique répare aussi les agrégats du Guide IA**, et donc ce que le job de thèmes du lot J regroupera.
⛔ **Écarté** : afficher passivement la dernière question/réponse dans `VoiceModeSheet` (2-3 h, zéro risque) — ça montrait que ça marche sans réparer la coupure. ~1 à 2 jours | **`mymuseum-visitapp`.** ⚠️ **Ne rien redessiner : l'UI existe déjà** — `GlassesStatusWidget` est monté en haut à droite de l'accueil (`home_3.0.dart:465`), avec état par couleur, pastille de mode actif et tap vers `VoiceModeSheet` (connexion, choix de mode, désactivation). `GlassesDebugPanel` existe aussi et **se garde** : c'est l'outil de test. Le seul écart avec « une icône tout le temps présente » est volontaire — elle se cache si `applicationInstanceDTO?.isAssistant != true` (`GlassesStatusWidget.dart:32`), donc invisible chez MDLF et le Fort, visible sur l'instance MyInfoMate. **À savoir avant de tester chez un client et de conclure à un bug.** | | **Onglet / canal « Vocal » dans les stats** | ✅ **Maintenu en V1 — décidé le 2026-08-12**, les lunettes Ray-Ban étant ramenées en V1 (activées d'abord sur la seule instance MyInfoMate, pour observer en vrai). **Mais le chantier n'est pas où le plan le disait.**
⚠️ **L'écran est déjà fait, c'est l'émission qui manque.** `statistics_screen.dart` traite déjà `AppType.Voice` : libellé de canal (`:189`), part du vocal (`:205-210`), KPI conditionnel (`:533-541`), takeaway (`:480`). **Rien à écrire dans `manager-app`** — tout le travail est dans `mymuseum-visitapp`.
**Trois trous mesurés le 2026-08-12 :** (1) `MyInfoMateLlmClient:40` envoie `AppType.Mobile` alors que son commentaire `:9` annonce Voice ; (2) `StatisticsService` est construit en dur avec `appType: 'Mobile'` (`home_3.0.dart:715`), son champ étant même documenté `// "Mobile" or "Tablet"` ; (3) **le plus lourd — le flux lunettes n'émet aucun `VisitEvent`** : tous les `track()` sont dans des écrans d'interface, `Services/Glasses/`, `wake_word_service` et `geo_beacon_trigger_service` n'en appellent aucun. Une visite vocale ne produit aujourd'hui ni session, ni section vue, ni durée.
**Périmètre des événements — tranché : session + contenu lu à voix haute.** Une session `Voice` à l'activation des lunettes, un `sectionView` quand le guide lit réellement une section (déclenchée par QR, beacon ou question). ⛔ **Pas d'événement par question posée** : elles sont déjà dans `VisitorQuestion` et remontent dans l'onglet Guide IA — les compter ici serait le même fait dans deux écrans. Ce périmètre est celui qui fait marcher les KPI existants et l'export PDF **sans retouche**, puisqu'il produit les mêmes types d'événements que le tactile | ⚠️ **Le canal du chat et celui des stats se décident séparément — c'est ce qui rend le chantier sûr**, et il a fallu le vérifier pour le savoir.
⛔ **Le « fallback Voice → Mobile » n'existe pas.** `guide-ia-screen-plan.md:226` l'affirme et le commentaire d'`AiController:356` le répète : **les deux sont faux.** Le code fait un `FirstOrDefault` sur le canal demandé puis `Forbid()` (`:357-361`). Envoyer `AppType.Voice` au chat sans avoir créé une `ApplicationInstance` de ce type rendrait donc **403 à chaque question** — extinction silencieuse des lunettes. C'est très probablement l'origine du `Mobile` en dur : le mur a été rencontré et contourné, le commentaire est resté sur l'intention.
✅ **Décision : le chat reste en `Mobile`.** `POST /api/Stats/event` est `[AllowAnonymous]`, ne cherche **aucune** `ApplicationInstance` et ne contrôle **aucun** canal (`StatsController:35-50`). Motif de fond, qui est aussi l'objection à retenir : les lunettes tournent **dans** l'app mobile, qui a déjà chargé son canal et ses configurations ; un canal Voice ferait jongler la même app entre deux canaux — piège déjà rencontré en K2 avec `getConfigurations` — pour zéro bénéfice de contenu.

⛔ **CORRECTION du 2026-08-12, quelques heures après la ligne ci-dessus : les `VisitEvent` ne partent PAS en `Voice` non plus. Le vocal n'est pas un canal, c'est un attribut.**
**Ce qui a fait tomber la première version** : la décision du miroir vocal (ligne suivante de ce tableau) rend explicite qu'un visiteur passe des lunettes à l'écran **dans une même session** — c'est tout son objet. Or `StatisticsService` génère **un seul `sessionId` par lancement**, et `AppTypeDistribution` compte **une entrée par session**, tranchant une session ambiguë sur son **événement le plus ancien** (`StatsController:196-203`). Donc : commencer au doigt puis mettre les lunettes ⇒ toute la session compte `Mobile`, le vocal est invisible ; commencer aux lunettes puis ouvrir l'écran ⇒ toute la session compte `Voice`, navigation tactile comprise. **Le KPI ne dit pas « la part du vocal » mais « la part des sessions dont le premier événement était vocal »** — un chiffre décidé par un accident d'ordre. La première décision supposait sans le dire qu'une session est *soit* vocale *soit* tactile.
✅ **Retenu : marquer les événements consommés à la voix dans `VisitEvent.Metadata`** — colonne JSON qui **existe déjà** et que `GetSummary` exploite déjà pour six agrégats, donc **aucune migration et le gel du lot B tient**. Le KPI devient « part des sessions ayant utilisé le vocal », vrai et calculable.
**C'est la conception que le code applique déjà ailleurs** : `VisitorQuestion` n'a pas de canal vocal, il a un booléen `IsVoice` **à côté** de son `AppType` — le vocal y est un mode d'interaction, pas une plateforme. `AppType.Voice` garde alors son sens d'origine : un déploiement lunettes **autonome** avec sa propre `ApplicationInstance`, cas pour lequel l'enum a été créé et qui n'est pas celui-ci.
⚠️ **Ce que cette correction coûte, et que la première version niait** : « zéro ligne dans `manager-app` » **devient faux**. `_voiceSessions` lit `appTypeDistribution['Voice']` (`statistics_screen.dart:205-206`) et devra lire un nouvel agrégat exposé par `GetSummary`. ~3 h, pas un chantier — mais à ne pas découvrir en route.
**À faire dans la même passe** : corriger le commentaire de `MyInfoMateLlmClient:9` pour qu'il dise *pourquoi* c'est Mobile, et corriger `guide-ia-screen-plan.md:226`. Les deux propagent la même croyance fausse | | ~~`GetSummary` agrégé en SQL~~ | ✅ **Livré le 2026-08-12.** Sessions distinctes, sommes de durée par session, durée moyenne par section, top sections, visites par jour, distributions par session, top articles et total des scans QR partent en SQL. Les six agrégats qui se lisent dans le JSON de `Metadata` (POI, agenda, quiz, jeux, menus, validité des QR) ne remontent plus que **deux colonnes**, et seulement pour leur type d'événement. **Les stats avancées ne sont plus calculées puis effacées** pour les plans qui n'y ont pas droit : les six requêtes ne partent pas. 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 et ne dit **rien** de la traduction SQL | **L13** | @@ -427,7 +443,7 @@ A (½-1j) ──► B (2-3j) ──► G (3-4j) ──► H (1 sem.) ──► I - **Chemin critique : A → B → G → H → I.** Tout le reste s'y greffe. - **Le lot K s'y greffe par la fin** : K2/K3/K4 sont parallélisables avec C, D, E et F, mais **K6 (publier les apps) précède la bascule** (**L19**). ~~Et **K5 précède D0**~~ — **K5 est livré le 2026-08-12, D0 n'est plus bloqué** : l'APK `mymuseum-visitapp` existe pour les trois flavors, le test §21 sur device peut être joué. C'est aujourd'hui **le prochain geste qui demande un appareil**, et le seul qui dise si les bugs offline D2-D5 se manifestent réellement (**L11**). -- ⚠️ **Une question à trancher avant K6, qui n'est pas technique** : le travail V1 des deux apps visiteur est committé sur des branches qui ne sont pas `master` — `Meta-Rayban-Test` pour `mymuseum-visitapp`, `AI-Assistant-test` pour `tablet-app`. Le §5bis décrit pourtant la première comme « un POC jamais mergé ». Publier des APK depuis une branche décrite comme un POC n'est pas neutre : clarifier avant K6, pas pendant. +- ~~**Une question à trancher avant K6**~~ ✅ **Réglée le 2026-08-13, par le propriétaire du projet : `Meta-Rayban-Test` est la branche de travail à jour, pas un POC.** Le nom est trompeur — il date de la première expérimentation — mais tout le travail V1 de `mymuseum-visitapp` y vit, les lunettes n'en sont qu'une partie (et elles restent invisibles si l'instance n'a pas l'assistant). Idem pour `AI-Assistant-test` côté `tablet-app`. **On développe et on publie depuis ces branches, il n'y a rien à clarifier avant K6.** ⚠️ **Le §5bis de STATUS.md affirmait le contraire** (« un POC jamais mergé ») et c'est ce qui a fabriqué cette fausse alerte — corrigé le 2026-08-13. - **C, D, D-bis, E, F sont parallélisables** entre eux, mais **C1 doit précéder G** (L5) et **B doit précéder G** (L2). - **DB3 doit précéder le job de thèmes du lot F** (L16) : c'est le seul endroit du plan où le design commande le backend. - **La boucle du graphe a disparu le 2026-08-11.** Elle venait de : I3 dépend de la rotation des secrets (dans le lot A), et H4 dépend de I3 (L12). En déplaçant la rotation en **I0**, la contrainte devient linéaire — `I0 → I3 → H4` — et il n'y a plus de cycle à contourner. **Contrepartie à ne pas rater** : H4 (test d'onboarding) exige `app.myinfomate.be` déployé, donc I3, donc I0. Concrètement, **la rotation des secrets doit être faite avant la fin du lot H**, pas au tout dernier moment. C'est le seul endroit où le report du 11/08 crée une échéance interne.