Le meta viewport manquant n'expliquait pas tout : le clavier s'ouvrait puis se
refermait apres une demi-seconde. C'est le focus qui etait perdu, pas la saisie
qui n'arrivait pas.
Sur Flutter web, un AutofillGroup se materialise par un <form> DOM qui porte les
inputs caches par lesquels passe la saisie. L'ouverture du clavier redimensionne
la fenetre, l'ecran se reconstruit, le groupe d'autofill est renegocie et le
<form> recree : l'input focus disparait avec lui. Sur desktop rien ne
redimensionne au clic, d'ou un bug invisible hors mobile.
L'AutofillGroup est retire — les autofillHints portes par chaque champ suffisent
au navigateur pour proposer le remplissage, le groupe ne servait qu'a
finishAutofillContext. Trois protections completent la correction :
- les deux champs passent par un TextEditingController detenu par le State, donc
la saisie survit a une reconstruction, la ou initialValue repartait de zero ;
- ils portent une ValueKey stable, pour etre reapparies plutot que recrees si la
structure de leurs freres bouge ;
- le Scaffold ne se redimensionne plus a l'ouverture du clavier ; le contenu est
deja dans un SingleChildScrollView, rien ne reste masque.
Au passage, le pre-remplissage de developpement (localhost) etait ecrit dans
build() a chaque reconstruction : il passe dans initState, ou il a du sens.
Le routeur, lui, etait fabrique dans le builder d'un FutureBuilder et l'appel a
getInstanceInfo partait de l'arbre passe a runApp. Chaque passage du builder
rendait un GoRouter neuf, donc un arbre neuf et un historique perdu. Les deux
sont desormais resolus une fois, avant runApp — 45 lignes de moins.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
web/index.html n'avait aucune balise <meta name="viewport">. Les navigateurs
mobiles appliquaient donc un viewport virtuel de ~980px et dezoomaient la page,
ce qui decalait les <input> DOM caches par lesquels Flutter web gere la saisie :
le tap donnait bien le focus cote Flutter, mais le navigateur n'ouvrait pas le
clavier et tentait de zoomer sur l'element — l'ecran semblait juste se rafraichir.
Effet de bord, le LayoutBuilder du login voyait maxWidth ~980, donc isMobile
etait toujours faux et la mise en page mobile ne s'appliquait jamais.
maximum-scale=1.0 evite en plus le zoom automatique de Safari iOS au focus.
Au passage :
- suppression de window.flutterWebRenderer = "html", sans effet depuis que le
renderer HTML a ete retire de Flutter (3.29, on est en 3.44) ;
- badge de version sur l'ecran de login. kGitSha est injecte au build
(--dart-define=GIT_SHA) et affiche a cote du numero de pubspec, pour relier
un bundle deploye au commit exact qui l'a produit ;
- la Future de PackageInfo etait recreee a chaque build() du login, ce qui
faisait clignoter le libelle : elle est desormais construite une seule fois.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Regroupe la configuration de carte en deux blocs (Carte / Points d'interet),
ajoute le filtre par categorie et l'editeur de point geographique, et passe
les ecrans Users et les jauges de quota par AppLocalizations (FR/EN/NL).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
L'infobulle promettait que ces phrases seraient traduites « comme le reste
de vos contenus », mais rien ne les traduisait jamais : BuildFallbackInstruction
filtre sur la langue du visiteur et, ne trouvant rien, laisse le modèle
improviser son refus. Les formulations du client ne servaient qu'en français.
L'enregistrement traduit désormais vers les langues déclarées sur les canaux,
en n'appelant l'IA que pour ce qui en a besoin : les langues sans traduction,
et toutes les langues si une phrase source a changé depuis le chargement.
Une sauvegarde qui ne touche pas aux formulations ne consomme donc pas de
jetons. Les langues visées sont celles des canaux, pas les dix supportées,
pour la même raison.
Un échec de traduction n'empêche pas l'enregistrement : le reste est sauvé,
les traductions précédentes sont conservées et l'écran le signale. Effacer
toutes les formulations efface aussi leurs traductions, devenues sans objet.
Corrige au passage le mot de réveil affiché : le modèle OpenWakeWord est
« hey_viva » / « hey_marco », le visiteur doit donc dire « Hey Viva », pas
« Viva ».
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pw.Container délègue canSpan à son enfant, si bien qu'une carte pouvait se
scinder entre deux pages — titre seul en bas de l'une, contenu en haut de
l'autre. Enveloppe le graphique et les groupes de barres dans un widget
non sécable, que MultiPage reporte alors en entier.
Les tableaux restent sécables : un tableau long doit pouvoir se répartir
sur plusieurs pages.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La carte « Où le guide est disponible » affichait la valeur entière de l'enum
AppType (0, 1, 2…) au lieu d'un libellé. Réutilise les clés statsChannel*
déjà traduites en FR/EN/NL pour l'écran Statistiques.
L'activation de l'assistant par canal se réglait dans chaque onglet
d'application, alors que le champ vit sur ApplicationInstance et se résout
par (InstanceId, AppType). Le réglage passe dans la liste des canaux du
Guide IA, où tous les canaux sont visibles au même endroit ; le paramètre
showAssistant de AppConfigurationLinkScreen devient inutile.
Les notifications de cet écran passaient par ScaffoldMessenger (en bas),
contrairement au reste de l'application qui utilise showNotification
(toastification, topRight).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sans UI, le client ne pouvait pas exercer le refus dont il est pourtant
responsable. Le libellé dit ce que couper coûte (l'onglet cesse de se remplir) et
ce que couper ne fait pas : rien n'est effacé, les questions déjà enregistrées
vivent jusqu'à leur purge à 90 jours.
manager_api_new étendu à la main. flutter build web vert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
_voiceSessions lisait appTypeDistribution['Voice'], qui ne se serait jamais
rempli : le vocal est un mode d'interaction, pas une plateforme. Le serveur
expose désormais « sessions ayant utilisé le vocal », qui est vrai.
Et l'aperçu de conversation du Guide IA journalisait chaque essai comme une
question de visiteur : le gestionnaire qui testait sa personnalité polluait son
propre onglet « Ce que demandent vos visiteurs » et gonflait le bloc des
questions sans réponse. isVisitorQuestion: false.
manager_api_new étendu à la main (voiceSessions, renommage isVisitorQuestion).
flutter analyze lib sans erreur, flutter build web vert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le champ existait côté serveur depuis le début mais manquait dans le client
généré — c'est ce qui expliquait que personne ne l'envoyait, et donc qu'aucune
question ne soit reliée à la suivante en base.
Édité à la main, générateur non relancé. flutter build web vert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Miroir du champ ajouté côté serveur, édité à la main comme le reste du client —
le générateur n'est pas relancé.
Le client est partagé avec mymuseum-visitapp et tablet-app par dépendance
locale, donc vérifié ici aussi : flutter build web vert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Portail de facturation : le bouton manquait, et l'écran n'affichait rien
du tout hors essai — if (isTrialActive) enveloppait le seul bouton, donc
un client payant arrivait sur un écran sans action. Désormais Checkout
pendant l'essai, portail après ; les deux ne coexistent jamais.
Le 409 a son propre message : les instances venues de Mongo n'ont pas de
StripeCustomerId, « réessayez plus tard » les ferait attendre quelque
chose qui n'arrivera jamais.
Diviseur de questions : guide_ia_screen lit aiTokensPerQuestion du serveur
au lieu du /1000 codé en dur, par appel http direct — le motif déjà établi
dans cet écran pour knowledge et insights. Si l'appel échoue, la ligne
« ~N questions » disparaît plutôt que d'afficher un repli : sur une jauge
qui sert à décider d'un achat, pas de chiffre vaut mieux qu'un chiffre
faux, ce que le /1000 faisait depuis le début à un facteur 10 près.
manager_api_new étendu à la main, générateur non relancé.
4 clés i18n FR/EN/NL. flutter analyze lib sans erreur, flutter build web OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Miroir de ResourceDTO côté serveur. Édition manuelle, conformément à la règle
du projet : manager_api_new ne se régénère pas, la génération écraserait les
patches déjà accumulés dans ce client.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
flutter build web vert. Un seul helper, ImageCompressor, appelé par les
DEUX chemins d'upload de resources_screen — c'est le motif qui a fait
diverger Create et Upload côté serveur deux fois de suite : sur
StoragePath en C1, sur le pré-vol de quota en C3.
La compression se fait AVANT resourceCreate, pas avant l'upload. Le
contrôle de quota livré en C3 porte sur sizeBytes à la création : le
faire après aurait fait décider le serveur sur la taille du fichier
d'origine, et compté au quota une image qui n'existe pas. Un quota qui
compte 12x trop se remplit 12x trop vite.
Deux écarts assumés par rapport à l'énoncé « 2560 px / JPEG q82 » :
- Un PNG à canal alpha reste un PNG. Le passer en JPEG remplirait la
transparence de noir, ce qui abîmerait logos et filigranes. Le
redimensionnement s'applique dans les deux cas ; seul l'encodage
diffère, et le type MIME suit — sinon Firebase servirait un JPEG
étiqueté PNG.
- Si le ré-encodage produit un fichier plus gros que l'original — ce
qui arrive sur une petite image déjà bien encodée — l'original est
conservé. Un échec de décodage renvoie aussi l'original plutôt que
de bloquer l'upload.
Défaut préexistant corrigé au passage : le second chemin d'upload ne
renseignait sizeBytes ni avant ni après. Toutes les ressources créées
par ce chemin comptent donc 0 octet au quota sur l'existant — même
famille que ce que C1 et C3 ont trouvé côté serveur, et un argument de
plus pour le backfill C2.
image 4.2.0 déclarée explicitement : elle était déjà présente en
transitif, dépendre d'un hasard de résolution pour une fonction
utilisateur n'est pas tenable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Écran « Activité » sur GET /api/Audit, réservé au SuperAdmin comme la policy
de l'endpoint (menuId 13, conditionné à role.value == 0). Filtres instance,
type d'entité, utilisateur et plage de dates, pagination 50, clic sur une
ligne pour le détail avant/après en table plutôt que le JSON brut. Les ids
d'instance et d'utilisateur sont résolus en noms, un id absent de l'annuaire
restant affiché tel quel : le journal doit rester lisible après la
suppression de ce qu'il décrit.
manager_api_new n'est pas touché. L'écran passe par un http.get direct au
Bearer, comme _loadKnowledge / _loadInsights / _reindex du Guide IA : une
lecture seule ne justifie pas d'étendre un client qui s'édite à la main, et
le déclencheur de génération n'existe plus depuis le lot A.
Côté utilisateurs, compteur « X / 5 » et bouton d'ajout désactivé au
plafond. Le SuperAdmin en est exclu : son GET /api/User renvoie toutes les
instances, compter cette liste contre un plafond par instance n'aurait
aucun sens. Le masquage du rôle SuperAdmin pour un InstanceAdmin, resté en
question dans todo-features.md, était déjà fait — _allowedRoles filtre sur
r >= callerRole.
Trois pièges relevés en câblant :
- AuditController renvoie les entités brutes, pas un DTO — les champs sont
ceux d'AuditLog en camelCase.
- invokeAPI ne lève pas sur un code d'erreur et son résultat était ignoré
dans users_screen : un e-mail déjà utilisé (409) ou un rôle refusé (403)
ne produisait aucun message. Le statut est désormais testé et le corps
affiché, ce qui rendra visible sans retouche le futur 422 du plafond.
- DropdownButtonFormField ne relit pas initialValue sur reconstruction
(FormField.didUpdateWidget ne traite que forceErrorText) : « Réinitialiser
les filtres » vidait la requête sans vider l'affichage. Remplacé par un
DropdownButton piloté.
40 clés i18n FR/EN/NL, flutter gen-l10n relancé. flutter analyze propre sur
les dossiers travaillés, flutter build web vert.
Deux dettes serveur restent ouvertes, hors périmètre de ce commit : le
plafond de 5 n'est pas appliqué par UserController (ce qui est livré ici est
un garde-fou d'interface, un POST direct passe toujours), et aucune section
n'est journalisée — AuditedTypes.Contains exige l'égalité exacte alors que
Section est abstraite. Détail dans DOCS/todo-features.md et v1-plan.md lot F.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Option A. Il fallait cinq surfaces empilées pour poser une question sur une
étape et l'écrire en néerlandais ; la profondeur maximale retombe à 2 —
l'éditeur, puis la traduction.
Les trois showNewOrUpdate… sont supprimés au profit d'un rail d'étapes
toujours visible à gauche, d'un panneau de détail à droite, et d'une question
qui se déplie sur place au lieu d'ouvrir une quatrième fenêtre.
Les champs sont d'abord sortis en trois widgets autonomes (ParcoursFields,
EtapeFields, QuestionFields) : c'est ce qui a rendu la refonte possible sans
tout réécrire — 1663 lignes retirées, l'écran n'a pas réécrit les champs, il
les a réagencés.
SAUVEGARDE AU FIL DE L'EAU (héritée de DB2) — plus de bouton Sauvegarder,
tout part à la saisie avec un débounce de 700 ms ; ajouts, suppressions et
réordonnancements partent sans attendre. GuidedPathApi masque à l'éditeur le
fait qu'un parcours vive sous une SectionParcours ou sous une SectionMap :
mêmes opérations, seule la classe générée change.
Deux pièges du backend, trouvés en lisant le contrôleur plutôt qu'en le
supposant :
- UpdateGuidedPath supprime les étapes absentes du DTO, donc le payload
n'emporte que celles qui ont déjà un id — les autres attendent leur
CreateGuidedStep et seraient dupliquées.
- Les questions n'ont pas d'endpoint propre (c'est voulu : elles partent avec
leur étape, que GuidedStep.FromDTO synchronise). Leur id entier est donc
récupéré après coup par `order`, seul repère stable entre la liste locale
et celle du serveur.
Le parcours est créé à la première modification, pas à l'ouverture : une
fenêtre neuve refermée intacte ne laisse rien en base. Si une écriture
échoue, la fenêtre refuse de se fermer et le pied de page porte un
« Réessayer ».
Le garde-fou de DB2 est retiré, comme prévu : un avertissement de perte de
travail n'a plus d'objet quand il n'y a plus rien à perdre. 14 clés i18n
FR/EN/NL ajoutées, 3 retirées.
flutter build web ✅, analyse du dossier sans erreur. Reste la vérification à
l'œil (DB5).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PDF en média d'étape — la liste de types du sélecteur de médias devient un
paramètre (`kSliderContentResourceTypes` par défaut) et l'étape de parcours
passe `kGuidedStepResourceTypes`, soit les types du slider plus Pdf.
QuestionType — le client généré nomme ses valeurs number0/1/2, ce qui
poussait le front à comparer des entiers bruts (`?.value == 2 ? 'Puzzle'`).
Alias nommés d'après l'enum serveur ajoutés à la main dans
question_type.dart (simple / multipleChoice / puzzle), `values` inchangé :
ce sont les mêmes trois valeurs. Les 7 usages number* du dialogue de
question sont migrés avec.
Fournisseur de carte (W1) — décision : visitapp-web reste sur Leaflet. Le
champ ne peut pas être masqué pour le web, il est porté par la section et
une même configuration est servie au mobile comme au web ; il est donc
conservé et documenté dans l'interface comme réglage mobile/tablette.
flutter build web ✅, analyse du dossier sans erreur ni warning.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Les trois dialogues imbriqués (Parcours, Étape, Question) fermaient sur
« Annuler » par un Navigator.pop sans rien demander, chacun jetant tout ce
qui était saisi sous lui — le plus coûteux étant une question de quiz avec
ses réponses.
Confirmation ajoutée aux trois, plus interception du retour arrière du
navigateur par PopScope : le clic hors fenêtre et la touche Échap étaient
déjà neutralisés par barrierDismissible: false, le retour navigateur était
le seul chemin de perte encore ouvert.
Deux points non évidents :
- l'instantané de référence est pris après la première frame, parce que
ensureSimpleResponse écrit dans la question pendant la construction et
ferait passer un dialog intact pour modifié ;
- les deux dialogues partageant le même Navigator, popper depuis onYes
fermerait la confirmation et non l'éditeur — la fermeture est différée
d'une frame pour être déterministe.
Réutilise showConfirmationDialog plutôt que d'ajouter un second composant.
3 clés i18n FR/EN/NL. flutter build web ✅, analyse du dossier propre.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Suppression des 14 fichiers modèle orphelins de manager_api_new/lib/model/
et de leurs 14 stubs de test. api.dart déclarait 139 `part` pour 153
fichiers : les 14 restants portaient `part of openapi.api;` sans être
listés par leur bibliothèque, donc hors du graphe de compilation. Le
compilateur ne les voyait jamais, l'analyzer les lisait — d'où les 68
erreurs de bruit qui rendaient `flutter analyze` inutilisable comme feu
vert.
Suppression de lib/api/openApiTest.dart, qui n'est pas du modèle mort mais
le déclencheur de la génération (@Openapi + build_runner). Le retirer
verrouille la règle du projet : le client s'édite à la main, et un
build_runner lancé par réflexe écrasait onboarding_api.dart, le câblage
d'AIApi et le mapping isGood → isCorrect.
Mesuré avant / après :
- flutter analyze manager_api_new : 67 → 3 issues (les 3 warnings légitimes)
- flutter analyze global : 68 erreurs de bruit → 3, toutes dans
test/widget_test.dart, cassé et connu
- flutter build web : ✅ inchangé
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Socle visuel (constants.dart)
- 11 rôles typographiques, 8 espacements, 5 rayons, paddings de carte et de page,
extraits du CSS des maquettes validées (DOCS/claude design/).
- Les couleurs restent celles de l'app : un écran neuf se fond dans manager-app,
il n'y ouvre pas une seconde palette. Seules les valeurs sans équivalent
existant sont reprises — remplissages discrets, bordures, gris atténué, ambre.
- Mesuré avant : 11 tailles de police différentes dans lib/Components/, de 9 à 25 px.
Écran Guide IA
- Coquille à deux onglets : Configuration / Ce que demandent vos visiteurs.
- Aperçu de conversation branché sur le vrai POST /api/AI/chat, pas une simulation.
- Carte « Ce que connaît votre guide » sur GET /api/Ai/knowledge/{id} : mesuré sur
l'index vectoriel, pas sur les tables de contenu — une section désactivée en est
purgée. Les « points d'intérêt » de la maquette deviennent « morceaux de contenu » :
les points d'une carte sont indexés dans le texte de leur SectionMap.
- Onglet des questions visiteurs alimenté par GET /api/Ai/insights/{id}. Une carte
sans données se masque au lieu d'afficher un cadre creux.
Client API
- AIApi était dans le client généré mais exposé nulle part dans client.dart, où
les 18 autres façades le sont. Câblé.
34 clés i18n FR/EN/NL. flutter analyze propre, flutter build web ✅.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Statistiques — refonte complète, aucun changement backend
statistics_screen.dart réécrit (+1832) : barres horizontales monochromes à la place
des barres verticales tronquées et des deux anneaux, barre de filtres unique avec les
volumes par canal, règle mono-canal, 4 KPI portant chacun leur variation, bandeau
« à retenir », courbe en aire avec bandes de week-end. La période précédente s'obtient
en rappelant le même endpoint.
statistics_report.dart : export PDF généré côté client (paquet pdf Dart), il partage
les valeurs calculées de l'écran — un chiffre ne peut pas diverger entre l'écran et le
document envoyé à la commune. Deux puces du sommaire promettaient des données
inexistantes (parcours terminés, questions au guide IA), retirées.
⚠️ Jamais ouvert dans un navigateur. Cases de test : test-plan.md §8bis / §8ter.
Guide IA
Screens/GuideIa/guide_ia_screen.dart — onglet Configuration. Menu conditionné à
isAssistant, le même drapeau que la garde d'AiController. L'onglet « Ce que demandent
vos visiteurs » n'est pas dans ce commit : le schéma backend est prêt, l'UI non.
Onboarding self-service
Screens/Auth/ (mot de passe oublié, définition du mot de passe),
Screens/Billing/subscription_screen.dart, ai_quota_hint.dart.
⚠️ Aucun parcours joué de bout en bout — test-plan.md §18.
Parcours guidés
progression_mode.dart : 9 booléens sur 3 niveaux remplacés par 3 questions.
Popups GuidedPath / GuidedStep / QuizQuestion mises à jour en conséquence.
Client API (manager_api_new) — édité À LA MAIN, ne pas relancer la génération
onboarding_api.dart, authentication_api.dart (+80), instance_dto (champs Guide*),
guided_step / quiz_question_guided_step (flags morts retirés).
Le // @dart=2.18 manquant dans onboarding_api.dart cassait les 3 apps Flutter d'un
coup — corrigé ici.
i18n : ~180 clés par langue (FR/EN/NL) + fichiers générés.
Tests : progression_mode_test, statistics_report_test (le second a attrapé deux
plantages qui seraient sortis au premier clic).
flutter build web ✅. flutter analyze : 68 erreurs, toutes dans les fichiers modèle
orphelins de manager_api_new — dette connue, pas une régression, ces fichiers ne sont
pas dans le graphe de compilation.