Le fil rouge de ces corrections : des replis silencieux. Une exception
rattrapee, une image absente, un fichier local jamais rattache — rien ne
plantait, mais rien ne marchait non plus, et aucun message ne le disait.
Splash et loader
- assets/splash et assets/loader ne contenaient que des PNG 1x1
transparents. errorBuilder ne se declenche pas : le fichier est valide,
juste vide. Logo et loader etaient donc invisibles PARTOUT, splash comme
ecrans de chargement. assets/loader est supprime, kLoaderAsset avec.
- Le splash natif etait blanc (launch_background en @android:color/white,
theme Light) puis l'app basculait en sombre. Fond unifie sur #111111,
cote natif comme Dart, avec values-v31 : Android 12+ ignore
windowBackground et repart sur le theme sans ces attributs.
- Le loader et l'image principale viennent maintenant du manager
(Applications -> Mobile). InstanceImages les garde sur le device : le
splash s'affiche avant tout appel API, il ne peut lire qu'un cache.
Alimente au boot depuis instanceGetDetail, qui porte deja les
ApplicationInstances — aucun appel supplementaire.
- Ouvrir une section montrait une page blanche avec un loader au milieu.
SlideFromRightRoute devient non opaque et SectionPage garde un fond
transparent tant que la section n'est pas prete : le visiteur garde sous
les yeux l'ecran d'ou il vient.
Hors ligne
- L'audio d'un article ne repartait jamais du MP3 local : audioFile
n'etait jamais renseigne, le lecteur retombait toujours sur l'URL.
- L'image de titre d'un article passait par NetworkImage, donc une croix
rouge des que le reseau manque, alors que le fichier est sur le device.
- L'accueil ne gardait en base que order/gridSpan : une visite jamais
telechargee disparaissait de la grille hors ligne. La visite entiere est
desormais mise en cache, et une visite injoignable reste affichee,
ternie et non ouvrable, plutot qu'absente.
- L'etat reseau datait du premier chargement et n'etait jamais remesure.
WidgetsBindingObserver etait declare mais jamais enregistre : aucun
rappel n'arrivait. L'accueil se recharge au retour dans l'app et
remesure avant de refuser l'ouverture d'une visite.
Divers
- Le futur de getSectionDetail etait construit dans future:, donc relance
a chaque rebuild — chaque rotation d'ecran refaisait l'appel reseau.
- ImageCustomProvider listait le repertoire d'une visite non telechargee :
exception a chaque construction de chaque image, pour finir de toute
facon sur le reseau.
- Scanner un QR d'une autre visite empilait les ConfigurationPage.
pushAndRemoveUntil : une visite est une destination de premier niveau.
- Le scanner arrive sur l'accueil, en pastille de verre comme les autres
actions de cet ecran. ScannerBouton prend size et transparent, et garde
son apparence d'origine ailleurs.
- Le titre d'un article n'etait pas centre : customStylesBuilder ne
s'applique qu'aux elements, et un titre sans balise n'en a aucun.
- Le lecteur audio replie ne lancait pas la lecture au premier appui, il
ouvrait le panneau. Il lit, puis ouvre. En lecture auto il reste replie.
- Le dialogue de telechargement faisait la hauteur de l'ecran : un Center
prend toute la hauteur maximale que lui donne un AlertDialog.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ce commit boucle le travail en cours sur la branche (assistant lunettes Meta,
passage du lecteur audio flottant a un onglet, ajustements scanner / liste de
configurations / telechargement) et y ajoute le badge de version.
Le badge, en bas de la feuille Parametres, affiche « flavor . version . commit ».
Un APK pose sur une tablette du terrain n'etait rattachable a aucun commit
precis : la version du pubspec ne bougeait pas d'un build a l'autre et rien
n'indiquait le flavor reellement installe. kGitSha suit le meme schema que
kApiBaseUrl, injecte par --dart-define, et kFlavor expose le flavor deja calcule.
package_info_plus etait deja une dependance transitive ; il devient direct,
puisqu'il est desormais importe.
/!\ Un --dart-define modifie n'est PAS pris en compte sans `flutter clean` sur
ce projet : verifie a la sentinelle, le SHA restait absent de libapp.so tant que
le cache Dart n'etait pas vide. Un build de release destine au terrain doit donc
toujours passer par un clean, sinon le badge affiche le SHA du build precedent.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Le manager propose deux voix, Viva et Marco, et annonce au client que ses
visiteurs diront « Hey Viva » ou « Hey Marco ». Or l'orchestrateur ne
chargeait que hey_marco, hey_alba et hey_vasco : le mot de réveil promis
pour la voix Viva n'était jamais écouté.
Le modèle existe déjà dans assets/files/ et le dossier est embarqué en bloc
par pubspec.yaml — seul son nom manquait dans la liste.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La fonction était recopiée dans VoiceOrchestrator, AssistantChatSheet et
GeoBeaconTriggerService. La limite « FR/NL/EN/DE seulement, le reste retombe sur
le français » est une décision assumée, mais elle n'était documentée que dans la
première : les deux autres ressemblaient à un oubli qu'on aurait envie de
« corriger » en ajoutant des langues, ce qui aurait produit un support partiel
silencieux — les listes de commandes vocales, elles, ne couvrent que ces quatre
langues.
Une seule copie dans Helpers/voiceLanguage.dart, une seule décision.
flutter analyze lib sans erreur, flutter build apk --debug --flavor dev vert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
⚠️ Travail d'une autre session, committé tel quel pour ne pas le laisser en
working tree — il n'est pas de cette session-ci et n'a pas été relu ici. Le plan
correspondant est DOCS/voice-latency-plan.md.
Les deux sons sont préchargés une fois pour toutes : setAsset décodait à chaque
wake word, sur le chemin le plus sensible à la latence de tout le pipeline. Et
une nappe de réflexion apparaît après 700 ms — en dessous, le visiteur vient de
finir de parler et n'attend pas encore de réponse.
D5 — les échecs de téléchargement ne disparaissent plus dans un print : ils sont
collectés et la visite n'est déclarée téléchargée que si elle l'est vraiment.
Nouvelle clé downloadIncomplete dans les 10 langues, sans quoi getFromLocale
aurait rendu une chaîne vide.
Trouvé en câblant l'écran, et c'est pire que le bug d'origine : sans état
d'échec, une visite incomplète restait sur « téléchargement en cours »
indéfiniment. Le compteur d'avancement n'est incrémenté qu'en cas de succès, donc
il n'atteignait jamais le total et l'écran ne concluait jamais. Relancer ne
re-télécharge que ce qui manque.
J4 — mention d'information accessible depuis l'assistant lui-même, là où la
collecte a lieu (§8.3 des CGU). Texte repris mot pour mot de
DOCS/mention-information-visiteurs.md.
FR/NL/EN seulement, repli sur l'anglais : traduire une mention de protection des
données sans relecture humaine serait pire que la servir en anglais. Les 7 autres
langues attendent une traduction relue, à demander avec la relecture juridique.
flutter analyze lib sans erreur, flutter build apk --debug --flavor dev vert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
StatisticsService.track porte isVoice, qui pose "voice": true dans
VisitEvent.Metadata. Le flux lunettes n'émettait aucun VisitEvent : une visite
vocale ne produisait ni session, ni section vue, ni durée.
Le sectionView vocal part après la synthèse, pas avant : un déclenchement dont le
TTS échoue n'a rien fait entendre, le compter serait faux. Aucun é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.
isAutoTriggered suit le renommage serveur en isVisitorQuestion.
flutter analyze lib sans erreur, flutter build apk --debug --flavor dev vert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le chat écrit, le vocal et le déclenchement proactif partagent désormais une
seule conversation, portée par VisitAppContext.assistant. Les trois
AssistantService séparés ont disparu : poser une question aux lunettes puis
ouvrir le chat donnait un guide qui ne savait rien de ce qu'on venait de
demander, et produisait deux lignes VisitorQuestion sans lien pour un visiteur
qui avait simplement changé de surface.
Le maxHistory du vocal (6) s'aligne sur 10 : deux surfaces qui partagent une
conversation ne peuvent pas la tronquer différemment selon le point d'entrée. La
concision vocale vient d'isVoice, qui change le prompt côté serveur.
Le vrai obstacle n'était pas l'affichage mais la forme du chat :
AssistantChatSheet gardait ses messages en List<Widget>, des bulles déjà
construites. On ne rejoue pas une conversation à partir de widgets, et un tour
vocal survenu pendant que la feuille était fermée n'aurait jamais pu y entrer. Le
service porte maintenant les tours en données et notifie ses écouteurs.
Deux listes, délibérément : celle envoyée au modèle et celle affichée ne
coïncident pas. Un message d'erreur se montre sans repartir au guide, et le
prompt d'un déclenchement proactif part au guide sans jamais s'afficher — seule
sa réponse apparaît, marquée « À voix haute ». Sans ce marquage, le visiteur
trouverait dans son chat des messages qu'il n'a jamais tapés.
conversationId est enfin envoyé, et renouvelé quand la conversation est vidée :
sinon la visite entière d'un visiteur n'en formerait qu'une.
flutter analyze lib sans erreur, flutter build apk --debug --flavor dev vert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AssistantService.chat porte isAutoTriggered, et geo_beacon_trigger_service le
passe à true. Sans lui, le prompt que le service s'écrit à lui-même atterrissait
dans VisitorQuestion, donc dans « Ce que demandent vos visiteurs », et gonflait
le bloc des questions sans réponse. Les jetons restent comptés côté serveur.
C'était le dernier bloquant avant d'activer le mode chez un client.
flutter analyze lib sans erreur, flutter build apk --debug --flavor dev vert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Déclenchement proactif (lot F) — quatre branchements : la garde, le cycle de
vie accroché à VoiceController, les points GPS peuplés depuis la configuration,
et un réglage visiteur.
⚠️ 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 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. Le proactif est donc un
sous-réglage, pas une tuile. glassesEnabled est supprimé, ses trois usages
étaient morts — dont une pastille « Lunettes / Déconnecté » jamais affichée.
M3 — meterZoneGPS devient le rayon par section, la constante n'étant plus qu'un
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.
Voix du guide — le sélecteur Viva/Marco de manager-app écrivait dans le vide :
Instance.GuideVoiceId n'était lu nulle part ici, l'app utilisant une constante
de build dont le défaut était Algieba, une voix que personne n'a écoutée. Même
forme que W1. Et aucun APK ne parlait avec la voix du produit : GeminiTtsEngine
n'est choisi que si GEMINI_API_KEY est injectée, ce qui n'était fait nulle part,
donc repli silencieux sur la voix système. Le repli reste le défaut — les tests
ne coûtent alors ni jetons ni argent — mais il s'annonce dans les logs.
Code mort supprimé : wake_word_service.dart, glasses_qr_scanner_service.dart et
glasses_tts_service.dart, les ancêtres de Services/Glasses/. Ils portaient les 4
erreurs kElevenLabs* et l'APK se construisait quand même, personne ne les
important. flutter analyze lib rend désormais zéro erreur.
⚠️ android/.../WakeWordService.kt est un homonyme bien vivant, non touché.
impl/elevenlabs_tts_engine.dart est conservé comme option.
flutter build apk --debug --flavor dev : vert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Complète d5f3686, qui rendait la reprise illimitée : un type MIME réellement
absent de la table se serait re-téléchargé à chaque visite pour se réécrire en
.unknown, en boucle, sur le réseau mobile du visiteur.
La date nulle sert de borne — elle ne vaut que pour les fichiers écrits avant
D2, donc la reprise ne joue qu'une fois. Déduire l'extension de l'URL en repli
n'est pas possible : une URL Firebase n'en porte pas.
flutter analyze : aucune erreur sur le fichier.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le filtre incrémental testait la présence du fichier, jamais sa version.
La prémisse d'origine était fausse, et ça change ce que le correctif répare :
« une image remplacée dans le CMS ne remonte jamais » n'existe pas, parce que
Upload appelle GenerateHexId() à chaque téléversement — remplacer une image
produit un nouvel id, donc une nouvelle URL, que l'ancien filtre téléchargeait
déjà.
Le vrai cas de péremption vient d'être créé par D3 : les MP3 déjà présents sur
les devices y sont en <id>.unknown, et « le fichier est présent » les déclarait
à jour. Sans D2, D3 ne réparait que les installations neuves. isResourceOutdated
traite donc .unknown comme absent.
Deux pièges tranchés en écrivant. La montée v4 laisse les dates existantes à
NULL et les considère à jour : backfiller à zéro aurait fait re-télécharger
l'intégralité des visites de tous les visiteurs sur leur réseau mobile, au
premier lancement. Et la boucle en masse héritée de D1 écrasait la date que la
boucle de téléchargement venait d'écrire, DatabaseHelper.insert faisant un
UPDATE de la ligne entière quand l'id existe ; pour un téléchargement échoué,
c'est l'ancienne date locale qui est conservée, jamais celle du serveur.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le bloc commenté parsait section.data type par type pour retrouver les
ressources à télécharger. Il n'y a plus rien à redécouvrir : le serveur
collecte désormais via GetReferencedResourceIds() et met toutes les
ressources référencées dans la charge d'export.
Répare aussi la purge des fichiers obsolètes : elle est pilotée par
usedImageOrAudioIds, qui ne contenait jusqu'ici que les images de sections —
tout le reste était donc considéré comme inutilisé.
Un nouveau type de section est couvert sans toucher à ce fichier.
flutter analyze sans erreur. ⚠️ Non vérifiable par un build : l'APK ne compile
pas (Gradle/NDK). À confirmer sur device au §21 du plan de test.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Assistant
AssistantChatSheet.dart (+252), Helpers/assistantSuggestions.dart,
Services/assistantService.dart — même comportement et mêmes suggestions que
visitapp-web, dérivées du contenu réel.
Parcours guidés
guided_path_content_progression_page (+72), guided_path_map_progression_page (+39),
parcours_page : alignés sur les 3 questions de progression qui remplacent les
9 booléens côté manager-app.
Carte
Helpers/mapCenter.dart + les trois vues (flutter_map, google_map, map_box) et
marker_view : centrage et icônes honorés.
Fix du build
guided_step_challenge.dart:83 — typage explicite. Avec le // @dart=2.18 de
manager_api_new côté manager-app, c'est ce qui débloque flutter build apk.
flutter build apk ✅.
Reste ouvert : M3 — meterZoneGPS toujours ignoré, une constante en dur à 100 m
remplace le rayon configuré par section.