Proactif, M3, voix du CMS — et le périmètre de D0 ramené à l'installation neuve

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 <noreply@anthropic.com>
This commit is contained in:
Thomas Fransolet 2026-08-13 11:58:10 +02:00
parent 6c455e4d7c
commit 2c49acf79e
4 changed files with 73 additions and 37 deletions

View File

@ -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.<br>⚠️ **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

View File

@ -458,9 +458,9 @@
<div class="stat"><span class="n n-info">1</span><span class="k">Migration v3</span></div>
<div class="stat"><span class="n n-warn">2</span><span class="k">Bugs ouverts</span></div>
<div class="stat"><span class="n">6</span><span class="k">À tester</span></div>
<div class="stat"><span class="n">25</span><span class="k">Planifié</span></div>
<div class="stat"><span class="n">24</span><span class="k">Planifié</span></div>
<div class="stat"><span class="n n-gate">8</span><span class="k">Bascule prod</span></div>
<div class="stat"><span class="n n-good">51</span><span class="k">Fait récemment</span></div>
<div class="stat"><span class="n n-good">54</span><span class="k">Fait récemment</span></div>
</section>
<div class="filters" role="group" aria-label="Filtrer par domaine">
@ -520,6 +520,17 @@
<div class="col-head"><h2>Bugs ouverts</h2><span class="count">2</span></div>
<div class="stack">
<article class="card" data-area="backend visitapp" data-horizon="v1">
<div class="card-meta"><span class="tag">backend</span><span class="tag">visitapp</span><span class="flag f-critical">Bloque l'activation — 13/08</span></div>
<h3>Les déclenchements proactifs pollueraient « Ce que demandent vos visiteurs »</h3>
<p><strong>Le proactif est câblé le 13/08</strong> (garde, cycle de vie, points GPS depuis <code>meterZoneGPS</code>, réglage visiteur) et <strong>M3 est fermé avec lui</strong> — APK <code>dev</code> vert. <strong>Il reste ce point, et il est bloquant avant d'activer le mode chez qui que ce soit.</strong></p>
<p>⚠️ <strong><code>RecordVisitorQuestion</code> écrit <code>Question = request.Message</code></strong> (<code>AiController:135</code>). 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 <strong>ajouterait donc une fausse « question de visiteur »</strong> dans l'onglet Guide IA — un écran qui existe précisément pour montrer ce que les humains demandent.</p>
<p><strong>Et ça ne s'arrête pas à du bruit</strong> : <code>HasAnswer</code> se déduit des sources, donc un prompt d'accueil sans citation viendrait aussi <strong>gonfler le bloc ambre « questions sans réponse »</strong>, celui qui sert à repérer les trous de contenu. Même famille que <code>WeatherSyncService</code> noyant le journal d'audit : le flot machine noie le signal humain dans l'écran fait pour lui.</p>
<p><strong>À faire</strong> : un drapeau porté par <code>AiChatRequest</code> (déclenchement automatique vs question posée), et <code>RecordVisitorQuestion</code> qui n'enregistre pas — ou marque distinctement — les tours d'origine machine. <strong>Deux repos</strong> : <code>manager-service</code> puis <code>mymuseum-visitapp</code>. ⚠️ À trancher en même temps : ces tours consomment des jetons du quota client, donc <code>RecordUsage</code>, lui, doit continuer à les compter.</p>
<p><strong>Écouteurs : ne pas les détecter</strong> — confirmé le 13/08. Le mode est opt-in via un switch dans <code>VoiceModeSheet</code> ; le sous-titre dit « écouteurs recommandés ». On informe, on n'arbitre pas à la place du visiteur.</p>
<span class="src">v1-plan.md lot F — proactif &amp; M3</span>
</article>
<article class="card" data-area="visitapp">
<div class="card-meta"><span class="tag">visitapp</span></div>
<h3>Échecs de téléchargement silencieux</h3>
@ -527,18 +538,6 @@
<span class="src">v2/offline-visit-plan.md</span>
</article>
<article class="card" data-area="visitapp">
<div class="card-meta"><span class="tag">mobile</span><span class="tag">M3</span></div>
<h3>meterZoneGPS ignoré</h3>
<p>Le rayon de déclenchement configuré par section n'a aucun effet — une constante en dur à 100 m le remplace.</p>
<p><strong>Périmètre tranché le 12/08 : <code>mymuseum-visitapp</code> + <code>visitapp-web</code>.</strong> <code>tablet-app</code> est exclu — SectionParcours est hors périmètre kiosk (tranché en K3), le code n'y serait jamais exécuté.</p>
<p>⚠️ <strong>Confirmé le 12/08 : la colonne est correctement mappée côté serveur</strong> (les 13 sous-types dans <code>SectionFactory</code>), et <strong>aucun</strong> 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.</p>
<p>⚠️ <strong>Trouvé le 12/08 : il y a DEUX implémentations de géodéclenchement qui s'ignorent</strong>, et ça change le chantier.</p>
<p><strong>1. La vivante</strong><code>configuration_page.dart:43</code> : <code>int meterToBeacon = 100; // 15 meters</code>. <strong>La valeur dit 100, le commentaire dit 15.</strong> C'est la constante que M3 désigne.<br><strong>2. La morte</strong><code>GeoBeaconTriggerService</code> : 20 m en dur pour le GPS, 3 m pour les beacons, jamais démarrée (aucun appel à <code>start()</code>).</p>
<p><strong>Aucune des deux ne lit <code>meterZoneGPS</code>.</strong> Corriger la première en laissant la seconde, c'est se préparer à re-trouver le même bug. ✅ <strong>Fusionné le 12/08 avec le branchement du mode proactif</strong> (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.</p>
<span class="src">v1-plan.md lot F — proactif &amp; M3 · parity-manager-visitapp.md §2</span>
</article>
</div>
</section>
@ -596,7 +595,7 @@
<!-- PLANIFIÉ -->
<section class="col" style="--stripe: var(--ink-3)">
<div class="col-head"><h2>Planifié</h2><span class="count">25</span></div>
<div class="col-head"><h2>Planifié</h2><span class="count">24</span></div>
<div class="stack">
@ -668,18 +667,6 @@
<span class="src">v1-plan.md — lot J · cgu-myinfomate.md §8 · mention-information-visiteurs.md</span>
</article>
<article class="card" data-area="visitapp" data-horizon="v1">
<div class="card-meta"><span class="tag">visitapp</span><span class="flag f-critical">Périmètre V1 neuf — 12/08</span></div>
<h3>Déclenchement proactif pour tous, pas seulement les lunettes</h3>
<p><strong>Tranché le 12/08.</strong> Un téléphone qui parle à l'approche d'une œuvre, écouteurs aux oreilles, <strong>c'est l'audioguide de musée classique</strong> — vendable bien au-delà des Ray-Ban, et sur du matériel que le visiteur a déjà.</p>
<p>⚠️ <strong>Ce n'est pas « retirer une garde », c'est câbler une fonction morte.</strong> Mesuré le 12/08 : <code>GeoBeaconTriggerService.instance.start()</code> <strong>n'est appelé nulle part</strong>, <code>glassesEnabled</code> est déclaré <code>= false</code> et <strong>jamais assigné</strong>, <code>proactiveModeEnabled</code> non plus, et <code>geoPoints</code> porte le commentaire « à peupler depuis la configuration » — ce qui n'est fait nulle part. Quatre choses à brancher.</p>
<p><strong>Le service couvre GPS <em>et</em> beacons</strong> : entrée dans le rayon d'un <code>GeoTriggerPoint</code> (20 m par défaut) ou beacon BLE à moins de 3 m, avec 30 s de cooldown par point.</p>
<p><strong>À faire</strong> : la garde redevient <code>proactiveModeEnabled</code> seul — <strong>ce que la doc de la classe décrivait déjà</strong>, le <code>&amp;&amp; glassesEnabled</code> n'y a jamais figuré (sixième « le commentaire dit autre chose que le code » du projet) ; un appel à <code>start()</code> ; <code>geoPoints</code> peuplé depuis la configuration ; un réglage visiteur ; et <code>VoiceSource</code> qui devient <strong>obligatoire</strong> — un prompt automatique atterrit dans <code>VisitorQuestion</code> comme s'il venait du visiteur.</p>
<p><strong>Écouteurs : ne pas les détecter.</strong> 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.</p>
<p>⚠️ <strong>Fusionne avec le bug M3</strong> (carte « meterZoneGPS ignoré ») : deux implémentations de géodéclenchement s'ignorent, aucune ne lit le rayon configuré. Un seul chantier. <strong>~2 à 3 jours, M3 compris.</strong> Nouveau périmètre V1 assumé, à arbitrer contre la date de bascule au même titre que K9.</p>
<span class="src">v1-plan.md lot F — proactif &amp; M3</span>
</article>
<article class="card" data-area="visitapp" data-horizon="v1">
<div class="card-meta"><span class="tag">visitapp</span><span class="tag">lunettes</span><span class="flag f-warn">Nouveau 12/08</span></div>
<h3>Miroir de la conversation vocale dans le chat</h3>
@ -888,9 +875,30 @@
<section class="done">
<h2>Fait récemment</h2>
<p>Cinquante-et-un chantiers clos entre le 5 et le 13 août 2026.</p>
<p>Cinquante-quatre chantiers clos entre le 5 et le 13 août 2026.</p>
<div class="done-grid">
<div class="done-item">
<strong>Déclenchement proactif câblé, et M3 fermé avec lui</strong>
<span>Quatre branchements : la garde, le cycle de vie, les points GPS, le réglage visiteur. APK <code>dev</code> vert. ⚠️ <strong>La garde recommandée par le plan aurait coûté de l'argent</strong> : <code>_trigger</code> appelle le LLM d'abord et ne parle qu'ensuite via <code>activeVoiceOrchestrator?.ttsEngine</code>, dont le <code>?.</code> avale le cas « aucun mode vocal actif ». Avec <code>proactiveModeEnabled</code> 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 ».</span>
<span><strong>Il n'y avait pas de troisième mode à inventer</strong> : <code>VoiceMode.voiceOnly</code> existe déjà et construit le <strong>même</strong> orchestrateur que le mode lunettes — l'audioguide sur téléphone était là. Le proactif est donc un <strong>sous-réglage</strong>, pas une tuile : l'opposer aux deux autres forcerait à choisir entre « je pose des questions » et « on me parle ». <code>glassesEnabled</code> est <strong>supprimé</strong>, ses trois usages étaient morts — dont une pastille « Lunettes / Déconnecté » que personne n'avait jamais vue.</span>
<span><strong>M3</strong> : <code>meterZoneGPS</code> devient le rayon par section, la constante ne servant plus que de défaut. ⚠️ Deux pièges de format vérifiés : <code>currentSections</code> porte des <strong>maps JSON brutes</strong>, pas des DTO, et <code>latitude</code>/<code>longitude</code> y sont des <strong>chaînes</strong>. ⚠️ <strong>Reste un bloquant avant d'activer le mode</strong> — voir la carte « Ce que demandent vos visiteurs » en Bugs ouverts.</span>
</div>
<div class="done-item">
<strong>La voix choisie par le client n'était pas celle qu'entendaient les visiteurs</strong>
<span><strong>Le sélecteur Viva / Marco de manager-app écrivait dans le vide.</strong> Il existe depuis le 07/08 et enregistre <code>Instance.GuideVoiceId</code>, mais <code>mymuseum-visitapp</code> ne lisait <strong>jamais</strong> ce champ : il utilisait une constante de build, dont le défaut était <code>Algieba</code> — une voix que personne n'a jamais écoutée, héritée d'avant le sélecteur. <strong>Même forme que W1</strong> : un réglage offert par le CMS que l'app visiteur ignore. La voix vient désormais de l'instance ; le repli passe à <code>Sulafat</code> (Viva).</span>
<span>⚠️ <strong>Et aucun APK ne parlait avec la voix du produit.</strong> <code>GeminiTtsEngine</code> n'est choisi que si <code>GEMINI_API_KEY</code> est injectée au build — elle ne l'était nulle part, donc tous les builds retombaient <strong>en silence</strong> 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, <code>constants.dart</code> porte la commande, et <code>launch.json</code> a une configuration « Dev + voix Gemini ».</span>
<span><strong>Au passage, une ligne périmée de STATUS tombe</strong> : « TTS = ElevenLabs, basculer sur Gemini avant de vendre l'add-on » — <strong>c'est déjà fait</strong>, <code>constants.dart</code> dit « ElevenLabs retiré du pipeline (trop cher) ». Le risque de marge qui motivait cette ligne est écarté. <code>impl/elevenlabs_tts_engine.dart</code> est <strong>conservé</strong> comme option, à rouvrir seulement si un client juge la voix Gemini insuffisante.</span>
</div>
<div class="done-item">
<strong>Trois services morts supprimés — et une conclusion de STATUS retournée</strong>
<span><code>wake_word_service.dart</code>, <code>glasses_qr_scanner_service.dart</code> et <code>glasses_tts_service.dart</code> portaient les <strong>4 erreurs Dart</strong> connues (<code>kElevenLabsApiKey</code>, <code>kElevenLabsVoiceId</code>) et l'APK se construisait quand même : personne ne les importait — un îlot hors du graphe de <code>main.dart</code>, même mécanique que les fichiers orphelins de <code>manager_api_new</code>. Supprimés le 13/08 : <strong><code>flutter analyze lib</code> rend zéro erreur, une première pour ce repo.</strong> ⚠️ <strong>Piège d'homonymie</strong> : <code>android/…/WakeWordService.kt</code> 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.</span>
<span><strong>Mais le §5bis en tirait « l'APK se construit sans le POC dedans », ce qui est faux.</strong> Ces deux fichiers sont les <strong>ancêtres</strong> de <code>Services/Glasses/</code> ; le POC actuel, lui, est bien dans l'APK. Conséquence inverse de celle qui était écrite : <strong>« démontrable » ne suppose pas de remettre les constantes ElevenLabs</strong> — le chemin vivant choisit <code>GeminiTtsEngine</code> et ne les touche jamais. La bascule TTS → Gemini réclamée avant de vendre l'add-on <strong>est déjà faite dans le code</strong>.</span>
<span>⚠️ <strong>Et ça rétrécit le chantier du miroir vocal</strong> : j'avais compté cinq <code>AssistantService</code> distincts, donc cinq historiques. Deux sont dans ce code mort. <strong>Il y en a trois de vivants</strong> — le chat, le vocal, le proactif.</span>
</div>
<div class="done-item">
<strong>FAQ de la landing — le plan à 179 € s'appelait encore « Bundle »</strong>
<span>41 occurrences en 4 langues (FR, EN, NL, DE) dans <code>src/data/segments.ts</code>, alors que les cartes de tarifs disent « Premium » : un prospect qui lisait la grille puis la FAQ voyait <strong>deux noms pour la même offre</strong>. Les formes composées des langues germaniques suivent (<code>Bundle-abonnementen</code><code>Premium-abonnementen</code>, <code>Bundle-Pläne</code><code>Premium-Pläne</code>). Vérifié : plus aucun « Bundle » dans <code>myinfomate-landing/src</code>, et les 41 occurrences étaient toutes de la prose — aucun identifiant touché. <code>npm run build</code> vert.</span>

View File

@ -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 `<id>.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.<br>**À 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é | |

File diff suppressed because one or more lines are too long