Décision de Patrick, réaffirmée : « tu gardes tout, seulement le bouton boost on
supprime ».
Je l'avais écrit et posé quelques heures plus tôt, en lisant « le bouton de
boost : ouvre-le quand tu veux » dans un message du moteur comme un feu vert —
alors que la décision produit était déjà prise en sens inverse. Un avis
technique sur la faisabilité ne révise pas un arbitrage de produit ; il fallait
demander, pas inférer.
Retiré : l'usage de `EvBoostButton` sur l'écran de la borne, son import, et le
helper `_entreeTelemetrie` devenu orphelin.
Gardé, sur instruction explicite : le widget, `setLoadDerogation` /
`clearLoadDerogation`, le modèle `Derogation` / `DerogationEnded` et leurs 19
tests. Ce n'est pas de la donnée fabriquée comme `DayPlan.demo()` — c'est la
lecture d'un contrat réel, testée, qui ne s'affiche nulle part et ne peut donc
rien afficher de faux. Le jour où le boost revient, il se rebranche en trois
lignes.
Vérifié à l'écran : « Mode de recharge · lu : Min+PV », trois boutons Manuel /
PV / Min+PV, Min+PV allumé parce que c'est ce que la box porte. Rien d'autre.
Capture : docs/captures/2026-09-08_ev-trois-modes.png
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
Vu à la capture du bouton de boost : la note « le mode de recharge se règle
depuis la tuile Borne EV des favoris » est visible trois centimètres au-dessus
des boutons de mode, dans le même défilement. Les boutons ont été déplacés ici
le 2026-08-28 et la phrase ne l'avait pas suivi — elle envoyait chercher ce qui
est sous les yeux.
Capture des trois modes et du bouton d'action :
docs/captures/2026-09-08_ev-modes-et-boost.png
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
CARTES. Elles lisaient `energyData.dayProductionWh`, qui porte en réalité le
compteur CUMULÉ depuis l'origine : « Production 62946.74… » au-dessus d'un
graphe de 24 h. Et « Gains 8789.84 € » n'avait aucune source — un prix du kWh
que personne n'a saisi. Elles lisent maintenant DailyBalance, comme le tableau
de bord, et chaque taux porte son dénominateur (« 22 % sur 1,1 kWh produits »).
Mise en page reprise de la maquette validée.
V2C. `powersync-plugin-v2c 1.15.2+etm14` lui a ajouté `smartmeterconsumer` :
vérifié à la sonde, elle rend 1 point contre 45 aux autres, son historique
commençant à 10:57. Le filtre d'alignement précédent l'aurait sortie du TRACÉ en
la laissant dans la LÉGENDE — le défaut qu'on venait de corriger sur l'autre
graphe. L'alignement passe donc par instant, et une série ne contribue que là
où elle a des points.
Avant son premier point, sa consommation est comptée dans le reste du foyer :
le total reste juste, seule la RÉPARTITION est incomplète, et c'est ce que dit
la note. Trois états, trois phrases — « pas d'historique », « mesurée depuis
HH:MM », « 0 W mesuré » — et aucune ne se dessine comme une autre.
Et le commentaire de fetchThingPowerLogs est corrigé deux fois : il était
périmé, et il nommait le mauvais portier. `smartmeter` est une interface VIDE
qui ne conditionne rien ; c'est `smartmeterconsumer` qui décide, via
EnergyManagerImpl::watchThing().
🔴 SIMULATION. L'écran faisait `if (!connected) startSimulation()` au montage.
Monté avant que la WebSocket soit établie — le cas normal — il basculait tout le
service en démo et n'en ressortait jamais. Vu à l'écran, box joignable : une
sinusoïde orange et un SOC à 75 % sur une installation SANS batterie. Rien ne
distinguait ces données inventées des vraies : ni couleur, ni mention, ni forme.
La fabrication sans même l'étiquette qui la blanchit.
Le tableau de bord avait retiré la même chose pour la même raison : la démo est
un CHOIX, pris depuis l'écran Installations, jamais un repli silencieux. L'écran
attend maintenant la connexion et se rattrape dès qu'elle arrive.
Captures : docs/captures/2026-09-08_energie-cartes-journalieres.png
docs/captures/2026-09-08_energie-empilement-v2c.png
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
La barre d'action passe en pied d'écran fixe (`bottomNavigationBar`), parce
qu'un correctif juste mais hors du champ reste inaccessible : le geste se fait
en haut de la liste, la barre était un écran entier plus bas. C'est le défaut
d'hier sous une autre forme — cause de disposition au lieu de cause de logique.
`bottomNavigationBar` plutôt qu'une superposition : il RÉDUIT la hauteur du
corps au lieu de le recouvrir, donc rien ne se cache derrière la barre.
⚠️ ET LE DÉPLACEMENT A CASSÉ L'ÉCRAN. Le `Column` d'`ImpactSaveBar` était en
`MainAxisSize.max` — le défaut. Dans un `ListView` la hauteur disponible est
infinie et le Column se contente de ses enfants : le défaut ne pouvait pas se
voir. En pied d'écran la contrainte est lâche mais BORNÉE, le Column a pris
toute la hauteur, le corps a été écrasé à zéro. Écran vide sous une barre
géante, vu en release sur appareil — les 319 tests étaient verts.
Un widget qui ne se contraint pas lui-même hérite du contexte : le déplacer
change ses contraintes, donc sa taille, sans qu'une ligne de son code ait bougé.
Le test ajouté monte la barre dans un `bottomNavigationBar` et mesure sa
hauteur. Éprouvé par son contraire : sans `mainAxisSize: min` il rend 600 sur
600 et échoue. Aucun test précédent ne pouvait mordre — tous montaient la barre
dans une hauteur infinie, où `max` se comporte comme `min`.
Vérifié sur appareil, chemin des flèches : brouillon réel -> barre en bas avec
Annuler/Enregistrer, corps intact ; ordre rétabli -> « L'ordre est revenu à
celui de la box » avec le seul « Terminer » ; Terminer -> barre absente. Banc
inchangé, aucune écriture.
Et la note gardée sur `draftIsNoOp` : « rien à enregistrer » veut dire « rien
par rapport à ce que tu as vu ». Comparer à l'état vivant conclurait à la place
de l'utilisateur sur des faits qu'il n'a pas vus. Le référentiel de la
CONCLUSION et celui de l'ÉCRITURE sont deux choses, et il faut les deux.
Capture : docs/captures/2026-09-08_barre-en-pied-terminer.png
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
Le brouillon sans effet se produit aussi par les FLÈCHES d'une carte, pas
seulement par le glissement : descendre une charge puis la remonter. Les flèches
se tapent, donc l'état est atteignable à l'entrée synthétique — là où le
glisser-déposer ne l'est pas.
Vérifié sur appareil : après les deux taps, la barre affiche « L'ordre est
revenu à celui de la box — il n'y a rien à enregistrer. » avec le seul bouton
« Terminer », et « Modifications en attente » a disparu. Le tap sur Terminer
referme l'état.
Banc rendu intact : `reorderWithinGroup` et `discardDraft` ne font que poser et
retirer le brouillon — aucune écriture, ce que la barre annonçait elle-même
(« rien n'est parti vers la box »). Vérifié dans le code avant de conclure.
⚠️ Noté au passage, sans rien y changer : l'ordre des rangs sur .75 a bougé
entre ma sonde de ce matin (chauffe-eau au rang 2) et maintenant (Simulated
wallbox au rang 2). Ce n'est pas le fait de l'app. Je ne sais pas de qui, et je
ne le devine pas.
Et une propriété à connaître : `draftIsNoOp` compare le brouillon à la dernière
lecture de l'app, pas à l'état vivant de la box. C'est le bon référentiel pour
« j'ai décidé de ne rien changer » — l'utilisateur conclut sur ce qu'il a vu —
et l'écriture, elle, relit toujours frais.
Capture : docs/captures/2026-09-08_brouillon-sans-effet-terminer.png
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
Deux défauts vus sur appareil après installation, tous deux d'affichage.
1. « mesuré depuis 16:37 » à 06:15, pour un `measuredSince` de la VEILLE. Le
lecteur comprend 16:37 aujourd'hui — une heure qui n'est pas encore arrivée —
et « 2,5 kWh achetés » juste à côté devient incompréhensible avec elle.
Même défaut que l'heure UTC, d'un cran plus haut : là on perdait le fuseau,
ici le jour. `heureLisible` rend « HH:MM » dans la journée du lecteur,
« hier HH:MM » la veille, « le JJ/MM à HH:MM » au-delà — et la comparaison
porte sur le jour civil, pas sur 24 h : à 00:05, un instant de 23:50 est hier
même s'il a quinze minutes.
2. « Autoconsommation · sans production » rendu à la moitié de la taille
d'« Autonomie » à côté. Cause : un FittedBox reçoit des contraintes de
largeur infinies, donc son enfant tient sur une ligne aussi large qu'il veut
et c'est tout le bloc qui rétrécit — `maxLines: 2` n'y sert à rien.
Rallonger le texte d'un suffixe rapetissait donc le NOM de la grandeur, qui
est ce qu'on lit en premier. Le motif passe sur sa propre ligne.
Les tests qui figeaient « HH:MM » dérivent maintenant de `heureLisible`, la
fonction même de l'écran — un attendu recomposé à la main aurait gelé la forme
d'avant, exactement comme les « 03:42 » en dur de vendredi.
Captures : docs/captures/2026-09-08_taux-sans-production.png
docs/captures/2026-09-08_instant-hier.png
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
APK reconstruit et posé. La box publie `measuredSince: 2026-09-07T04:33:28Z` ;
l'écran affiche 06:33, l'attendu dérivé par `.toLocal()`. L'APK précédent, sur
la même box et le même téléphone tous deux en Europe/Paris, affichait 04:33 —
la preuve que l'écart ne devait rien au fuseau de la box.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
Le contrat porte désormais la règle : tout horodatage publié est en UTC avec son
marqueur, ou en epoch. Trois champs, deux formats, rien ne l'exigeait.
`instantDeLaBox` (lib/models/box_instant.dart) devient l'unique lecteur pour ce
que la box publie — epoch ou ISO, marqué ou nu — et requalifie la forme nue en
UTC avant de rendre l'heure du lecteur. Politique valable pour la BOX seule :
`Installation.lastSeen` est écrite en local sans marqueur et relue pareil, donc
la lire en UTC la décalerait. Deux héritages opposés sous une même forme, d'où
l'absence délibérée d'un « parser une date » unique.
Le `.toLocal()` posé hier dans telemetry_text est retiré : le modèle est seul
propriétaire de la conversion. Le rejouer serait sans effet mais laisserait
croire qu'elle peut manquer ailleurs.
CORRECTION. J'avais écrit que le défaut avait survécu « parce que box et
téléphone partageaient l'heure ». C'est faux, et la fausseté est utile : l'écart
est entre UTC et l'APPAREIL, donc visible sur tout téléphone hors UTC+0 quel que
soit le fuseau de la box. Mesuré ce matin avec les deux en Europe/Paris — la box
publie 04:33:28Z, l'APK d'hier affiche « 04:33 », il fallait lire 06:33. La
bascule en Europe/London n'a pas donné la visibilité, elle a donné le MOTIF de
regarder. La version fausse fait conclure « les fuseaux coïncident, donc c'est
réglé » et laisse le défaut en place.
`GetVariableLoadState` n'existe pas sur .75 (absente d'Introspect) et l'app ne
lit aucun `since` : rien à corriger de ce côté. Le champ existe en revanche dans
GetLoadTelemetry — `loads[].command {divergent, expectedW, observedW, since}` —
et il porte bien le Z, vérifié à la sonde. Aucun écran ne le consomme encore.
Contrôle négatif de la ligne d'horloge : même binaire qu'hier, box repassée en
Europe/Paris, la ligne a disparu sous le champ d'échéance.
docs/captures/2026-09-07_fuseau-box-paris-ligne-absente.png
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
Trouvé sur appareil pendant la bascule volontaire de .75 en Europe/London, en
capturant la ligne d'horloge de l'échéance : la box publie
`measuredSince: "2026-09-06T14:38:00Z"` et l'écran affichait « mesuré depuis
14:38 » — ni l'heure de la box (15:38 BST), ni celle du téléphone (16:38 CEST).
L'heure de personne.
`DateTime.tryParse` d'une chaîne en `Z` rend un DateTime `isUtc: true` ; lire
`.hour` sans `toLocal()` donne l'heure UTC. Le défaut est identiquement
invisible tant que box et appareil partagent l'offset : il a fallu DEUX fuseaux
pour qu'il se voie, et c'est la couture ouverte pour la capture qui l'a montré.
Deux natures d'heure, désormais séparées dans CLAUDE.md :
- un RÉGLAGE (`dailyDeadline`) est lu par la box → on nomme son horloge ;
- un INSTANT (`measuredSince`) est absolu → il se rend chez le lecteur, sans
mention.
Les deux tests qui gardaient « 03:42 » en dur gelaient l'heure UTC et validaient
le défaut : leur attendu se dérive maintenant de `.toLocal()`. Le contrôle
négatif ne mord que hors UTC, et le test le dit au lieu de laisser lire un vert
comme une preuve.
Capture de la ligne d'horloge : docs/captures/2026-09-06_fuseau-box-london.png
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
L'entrée 🔴 du TODO. Aucune des quatre tuiles n'était journalière : deux pourcentages
instantanés, deux compteurs cumulés depuis l'origine dont deux portaient le mot
« aujourd'hui ». Elles montrent désormais la journée, sous un en-tête qui le dit une fois.
MÉTHODE : une DIFFÉRENCE de compteurs entre minuit local et maintenant, jamais une
intégration des puissances. La première est une mesure, prise sur les mêmes compteurs que
ceux du fournisseur ; la seconde est une estimation qui suppose que chaque échantillon
représente son intervalle. Mesuré sur .75 : l'intégration rend 77,70 kWh là où la différence
rend 78,31 — 0,8 % d'écart, systématiquement à la baisse. Il n'y a donc PAS de repli
silencieux de l'une vers l'autre : quand la mesure n'est pas exploitable, le type le dit.
DEUX BUGS D'UNITÉ, trouvés en cherchant à savoir ce que valaient les compteurs. Aucune
documentation ne tranchait, donc recoupement sur machine : l'intégration des puissances (en
W) rend 77 697 Wh quand la différence de totalProduction rend 78,310. Le rapport est mille —
LES COMPTEURS SONT EN kWh, et le modèle Dart les nommait Wh.
1. L'écran Énergie divisait ces kWh par mille pour les afficher : une journée de 78 kWh
s'y lisait « 78 Wh », sous un axe intitulé « Bilan énergétique (Wh) ». Invisible sans
connaître l'ordre de grandeur attendu.
2. La simulation accumulait des Wh : un écran juste en simulation aurait cassé d'un
facteur mille contre une vraie box.
LE CONTRÔLE QUI VALIDE LE BILAN : production + soutiré == consommation + injecté. C'est le
seul recoupement disponible sur des compteurs qu'on ne peut pas vérifier autrement, et il
tient EXACTEMENT sur .75 — 112,244 des deux côtés sur 21,75 h. Faux, il désigne un compteur
cassé ou une remise à zéro, et la carte le signale au lieu de présenter comme une mesure ce
qui ne se recoupe pas.
Les trois pièges du TODO n'ont pas été repayés : bornes à minuit LOCAL via
GetPowerBalanceLogs · décodage délégué à fetchPowerBalanceLogs, qui porte déjà les bonnes
clés sans préfixe currentPower · plausibilité vérifiée sur les DEUX bornes avant de
soustraire, parce que deux valeurs absurdes peuvent avoir une différence d'apparence saine.
Plus un quatrième trouvé en chemin : un compteur qui RECULE (redémarrage en milieu de
fenêtre) donne un bilan négatif, et un max(0, …) le rendrait faux en silence.
Rien ne s'affiche jamais à zéro quand c'est inconnu. Sans production, le taux
d'autoconsommation est null et non 0 % — à 3 h du matin, « 0 % » se lirait comme un défaut
alors qu'il n'y a rien à autoconsommer.
VÉRIFIÉ SUR APPAREIL contre .75, en lecture seule (le banc est en préparation V2C, rien n'a
été écrit) : 32 échantillons depuis minuit, prod 28,37 · conso 26,55 · import 11,95 ·
export 13,77 kWh, identité OK, et les tuiles affichent 51 % / 55 % / 13,8 / 11,9.
Le passage sur appareil a trouvé ce qu'aucun des 138 tests unitaires ne pouvait voir : LES
QUATRE LIBELLÉS DÉBORDAIENT — « Autoconsomm… », « Vers réseau · aujou… ». Le suffixe
« aujourd'hui » répété quatre fois mangeait le nom de la grandeur ; il est dit une fois en
en-tête. Le harnais d'appareil échoue désormais sur tout libellé finissant par « … ».
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016vrTVifar2GN5rtUh89Wkq
Le test d'appareil passe. Il a trouvé deux défauts réels que 129 tests unitaires ne
pouvaient pas voir, parce qu'aucun ne rend ces widgets à une taille d'écran réelle.
1. LA POIGNÉE DE GLISSEMENT MESURAIT 26 × 27 px, pour un minimum Material de 48. Le
Container transparent posé lors de l'enquête précédente avait supprimé la bande morte
entre l'icône et le reste, mais il n'avait jamais AGRANDI la cible : le geste accrochait
quand on visait juste et ratait sinon. C'est le seul geste de cet écran sans repli —
sans lui, l'ordre des domaines ne se change pas du tout.
2. LES FLÈCHES DE RANG MESURAIENT 18 × 18 px, la taille de l'icône, sans aucune marge de
touche. Elles n'ont pas de repli non plus : réordonner DANS un domaine ne se fait que
par elles.
Portées à 48 et 44. Et la capture d'appareil a montré le coût de la correction naïve :
empilées, deux cibles de 44 font 88 px de haut pour deux icônes de 18, et la colonne de
rang devient une colonne vide. Côte à côte, la hauteur retombe à 44 et la largeur ne coûte
rien au libellé. La cible tactile est la contrainte ; la disposition ne l'est pas.
Le harnais a lui-même demandé trois corrections avant de dire la vérité, et chacune est un
piège qui se reproduira :
- il regardait le HAUT de l'écran. Dans un ListView, ce qui n'est pas visible n'est pas
construit : les cibles n'étaient pas mesurables faute d'être montées.
- `find` trouve un widget POSÉ MAIS HORS VIEWPORT, et getCenter en rend une coordonnée qui
n'existe pas — mesuré y=1129 sur un écran de 825 px. Le geste partait dans le vide, avec
un symptôme identique à un glissement ignoré.
- la course était devinée. Une liste réordonnable ne permute qu'une fois passé le milieu du
suivant, et un bloc de domaine porte ses cartes : 384 px mesurés, contre les ~150 devinés.
Elle se calcule maintenant depuis l'écart réel entre deux poignées.
Le test mesure aussi le défilement de la page pendant le glissement : si le ListView
extérieur gagnait l'arène, le symptôme serait le même que celui d'une poignée qui
n'accroche pas, pour une cause opposée. Mesuré à 0 px — c'est bien la poignée qui prend.
Aucun débordement de mise en page sur cet écran. Deux captures d'appareil jointes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016vrTVifar2GN5rtUh89Wkq