From b3000ca2d5817b4241a823b0b70959f8b1dbdd1a Mon Sep 17 00:00:00 2001 From: Patrick Schurig Date: Sat, 5 Sep 2026 17:07:20 +0200 Subject: [PATCH] =?UTF-8?q?fix(=C3=A9cran):=20trois=20d=C3=A9fauts=20trouv?= =?UTF-8?q?=C3=A9s=20sur=20appareil,=20dont=20un=20de=20forme=20et=20un=20?= =?UTF-8?q?de=20moi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Passage sur le Redmi contre `.75`. Aucun des trois n'était visible en test. 1. LA PRODUCTION S'AFFICHAIT NÉGATIVE — « -8.9 kW · Solaire » sous 9 kW de soleil. `currentPowerProduction` est publié négatif par nymea et le service garde les signes bruts par politique ; la carte imprimait la valeur telle quelle. Ce qui rend le défaut net plutôt qu'opinable : la même carte normalisait DÉJÀ le réseau et la batterie par `.abs()` — la branche solaire était la seule oubliée, donc un oubli et non une convention. 2. LE LIBELLÉ SE COUPAIT AU MILIEU DU MOT — « Autoconsommat / ion », alors que le commentaire du code disait vouloir l'éviter. La cause n'est pas la longueur mais l'ÉCHELLE du texte : police système au-dessus de 100 %, et `maxLines: 2` ne protège de rien puisque Flutter coupe un mot long sans espace, sans même afficher l'ellipse. `FittedBox(scaleDown)` garde le mot entier et ne fait rien quand la place suffit. 3. MA LIGNE DES RATIOS RÉPÉTAIT LES TUILES — « Instantané depuis 09:23 — autoconsommation 52 %, autonomie 54 % » sous des tuiles affichant 52 % et 54 %. Les deux chemins étaient corrects et les valeurs coïncidaient vraiment (bilan 51,8/54,1 sur 00:00→16:45, box 51,9/53,7 depuis 09:23). Rien de faux, et illisible quand même : je déclenchais sur l'ORIGINE, alors qu'un lecteur compare des NOMBRES. La ligne sort désormais quand l'origine diffère ET que l'écart dépasse 2 points ; le relevé du jour est devenu un test. Vérifié à l'écran après correction : le libellé tient sur une ligne, la ligne des ratios se tait. Le signe, lui, N'A PAS pu être revu — la production du banc était retombée à 0 W à 17:03, et `abs(0) == 0`. Une correction qu'on ne peut pas voir demande une garde qui se voit : `flux_signe_test.dart` la pose. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw --- .../dashboard/widgets/energy_flow_card.dart | 8 +++- lib/features/dashboard/widgets/kpi_row.dart | 48 +++++++++++++++++-- test/flux_signe_test.dart | 48 +++++++++++++++++++ test/kpi_ratios_test.dart | 41 ++++++++++++++++ 4 files changed, 140 insertions(+), 5 deletions(-) create mode 100644 test/flux_signe_test.dart diff --git a/lib/features/dashboard/widgets/energy_flow_card.dart b/lib/features/dashboard/widgets/energy_flow_card.dart index 371fea8..1b5495b 100644 --- a/lib/features/dashboard/widgets/energy_flow_card.dart +++ b/lib/features/dashboard/widgets/energy_flow_card.dart @@ -346,7 +346,13 @@ class _NodeLayerState extends State<_NodeLayer> icon: Icons.wb_sunny_rounded, color: EtmTokens.solarOf(context), label: 'Solaire', - value: '${(d.pvPower / 1000).toStringAsFixed(1)} kW', + // ⚠️ `currentPowerProduction` est publié NÉGATIF par nymea (-8 980 W = + // 8,98 kW produits), et le service garde les signes bruts par politique. + // L'AFFICHAGE, lui, doit dire ce qu'un client comprend : une production est + // une quantité positive. Vu sur appareil le 2026-09-05, « -8.9 kW Solaire » + // sous un soleil de 9 kW — la carte normalisait déjà le réseau et la + // batterie par `.abs()`, la branche solaire était la seule oubliée. + value: '${(d.pvPower.abs() / 1000).toStringAsFixed(1)} kW', tappable: true, ), ), diff --git a/lib/features/dashboard/widgets/kpi_row.dart b/lib/features/dashboard/widgets/kpi_row.dart index d40290f..3fa0348 100644 --- a/lib/features/dashboard/widgets/kpi_row.dart +++ b/lib/features/dashboard/widgets/kpi_row.dart @@ -25,6 +25,34 @@ import '../painters/ring_painter.dart'; /// corrompus, ou compteur en recul. Les tuiles affichent alors « — » et le disent. Elles /// n'affichent **jamais zéro** : « rien produit » et « on ne sait pas » sont deux états /// différents, et à 3 h du matin le premier est vrai sans que le second le soit. +/// **La ligne ne s'affiche que si elle DIT quelque chose.** +/// +/// Première version : elle sortait dès que l'origine différait du début de journée. Vu sur +/// appareil le 2026-09-05, ça donnait ceci — tuiles **52 % / 54 %**, et juste dessous +/// « Instantané depuis 09:23 — autoconsommation **52 %**, autonomie **54 %** ». Les mêmes +/// deux nombres, écrits deux fois. +/// +/// Les valeurs coïncidaient pour de bon : bilan sur 00:00→16:45 = 51,8 / 54,1, box depuis +/// 09:23 = 51,9 / 53,7. Rien n'était faux — et la ligne était quand même illisible, parce +/// qu'**un lecteur compare des nombres, pas des origines**. Devant deux fois « 52 % », il +/// doit raisonner pour découvrir que les fenêtres diffèrent : exactement ce que la ligne +/// existait pour lui épargner. +/// +/// Elle sort donc quand l'origine diffère **et** que l'écart se voit. Sous le seuil, les +/// tuiles suffisent — et une ligne permanente cesse d'être lue, comme un avertissement +/// permanent. +/// +/// Sans bilan, elle sort quand même : c'est alors le seul chiffre disponible. +bool _ligneUtile(EnergyRatiosSnapshot? r, DailyBalance? b) { + if (r == null || !r.baselineRepriseEnJournee) return false; + if (b == null) return true; + double ecart(double? x, double? y) => + (x == null || y == null) ? double.infinity : (x - y).abs(); + const seuil = 2.0; // points de pourcentage — sous ça, l'arrondi affiché est identique + return ecart(r.selfConsumptionRate, b.selfConsumptionRate) >= seuil || + ecart(r.autonomyRate, b.autonomyRate) >= seuil; +} + /// La phrase des ratios de la box — **avec leur origine, puisqu'elle diffère**. /// /// Appelée seulement quand la baseline a été reposée après le début de journée : sans ça, @@ -116,7 +144,7 @@ class KpiRow extends StatelessWidget { ), ], ), - if (ratios?.baselineRepriseEnJournee ?? false) ...[ + if (_ligneUtile(ratios, b)) ...[ const SizedBox(height: 8), Padding( padding: const EdgeInsets.only(left: 2), @@ -291,7 +319,20 @@ class _RingTileState extends State<_RingTile> ), ), const SizedBox(height: 3), - Text( + // ⚠️ **La coupure se fait AU MILIEU du mot quand le texte est agrandi.** + // Vu sur appareil le 2026-09-05, réglage de police système au-dessus de + // 100 % : « Autoconsommat / ion ». `maxLines: 2` ne protège de rien — + // Flutter coupe un mot long sans espace plutôt que de déborder, et + // l'ellipse ne s'affiche même pas. Le défaut n'est donc pas la longueur du + // libellé mais la mise à l'échelle, et il reviendra sur n'importe quel mot + // à la première langue plus verbeuse. + // + // `scaleDown` garde le mot ENTIER en réduisant ce qu'il faut, et ne touche + // à rien quand la place suffit — donc aucun effet à échelle normale. + FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerLeft, + child: Text( widget.value == null ? '${widget.label} · pas mesurable' : widget.label, @@ -299,10 +340,9 @@ class _RingTileState extends State<_RingTile> size: 11, color: EtmTokens.mutedOf(context), ), - // Deux lignes plutôt qu'un mot coupé : « Autoconsomm… » ne se lit pas, - // et la tuile a la hauteur pour le porter entier. maxLines: 2, overflow: TextOverflow.ellipsis, + ), ), ], ), diff --git a/test/flux_signe_test.dart b/test/flux_signe_test.dart new file mode 100644 index 0000000..974185e --- /dev/null +++ b/test/flux_signe_test.dart @@ -0,0 +1,48 @@ +/// **Une production ne s'affiche pas en négatif.** +/// +/// Vu sur appareil le 2026-09-05 à 16:52 : « **-8.9 kW · Solaire** » sous un soleil de +/// 9 kW. `Energy.GetPowerBalance` publiait `currentPowerProduction: -8980.4` — nymea donne +/// la production en négatif, et `NymeaService` garde les signes bruts par politique (§7.1). +/// La carte imprimait donc la valeur telle quelle. +/// +/// Ce qui rend le défaut net plutôt qu'opinable : **la même carte normalisait déjà** le +/// réseau et la batterie par `.abs()`. La branche solaire était la seule oubliée — donc ce +/// n'était pas une convention d'affichage assumée, c'était un oubli. +/// +/// Ce test existe parce que l'écran ne pouvait plus le montrer : à 17:03 la production du +/// banc était retombée à 0 W, et `abs(0) == 0`. Une correction qu'on ne peut pas voir +/// demande une garde qui, elle, se voit. +library; + +import 'package:etm_powersync_app/features/dashboard/widgets/energy_flow_card.dart'; +import 'package:etm_powersync_app/models/energy_data.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +Future _poser(WidgetTester tester, EnergyData d) async { + await tester.binding.setSurfaceSize(const Size(1000, 1600)); + addTearDown(() => tester.binding.setSurfaceSize(null)); + await tester.pumpWidget( + MaterialApp(home: Scaffold(body: EnergyFlowCard(data: d)))); + await tester.pump(const Duration(seconds: 2)); +} + +void main() { + testWidgets('la production publiée NÉGATIVE s\'affiche positive', (tester) async { + // La charge utile réelle du 2026-09-05 16:52. + await _poser(tester, const EnergyData(pvPower: -8980.4, homePower: 3249.4)); + + // -8 980,4 W -> 8,98 kW -> « 9.0 kW » à une décimale. (Sur appareil la valeur de + // l'instant donnait « -8.9 kW » ; c'est le SIGNE qui est en cause, pas l'arrondi.) + expect(find.text('9.0 kW'), findsOneWidget); + expect(find.text('-9.0 kW'), findsNothing, + reason: 'un client lirait « mes panneaux consomment 9 kW »'); + }); + + testWidgets('zéro reste zéro — et ne prouve rien', (tester) async { + // L'état du banc à 17:03, celui qui a rendu la correction invisible à l'écran. + await _poser(tester, + const EnergyData(pvPower: 0, homePower: 3250, gridPower: 3250)); + expect(find.text('0.0 kW'), findsOneWidget); + }); +} diff --git a/test/kpi_ratios_test.dart b/test/kpi_ratios_test.dart index 3e0d8ce..70d3dc3 100644 --- a/test/kpi_ratios_test.dart +++ b/test/kpi_ratios_test.dart @@ -82,6 +82,47 @@ void main() { expect(find.textContaining('Instantané depuis'), findsNothing); }); + testWidgets('les deux coïncident : la ligne se TAIT — vu sur appareil', (tester) async { + await tester.binding.setSurfaceSize(const Size(1000, 2000)); + addTearDown(() => tester.binding.setSurfaceSize(null)); + // Relevé du 2026-09-05 16:52 : bilan 51,8 / 54,1 sur 00:00→16:45, box 51,9 / 53,7 + // depuis 09:23. L'origine diffère bien, mais les nombres non — et la ligne écrivait + // « 52 %, 54 % » juste sous des tuiles qui affichaient déjà 52 % et 54 %. + final coincide = EnergyRatiosSnapshot.fromJson({ + 'autonomyRate': 53.67, + 'selfConsumptionRate': 51.92, + 'periodStartedAt': 1788559200, + 'measuredSince': 1788592995, + 'timeZone': 'Europe/Paris', + }); + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: SingleChildScrollView( + child: KpiRow( + data: const EnergyData(), + // 8 kWh produits / 1 exporté -> 87,5 % ; 12 consommés / 5 importés -> 58,3 %. + // On force ici un bilan proche des valeurs de la box pour éprouver le seuil. + balance: DailyBalance( + from: DateTime(2026, 9, 5, 0, 15), + to: DateTime(2026, 9, 5, 16, 45), + periodStart: DateTime(2026, 9, 5), + sampleCount: 72, + productionKwh: 59.89, + consumptionKwh: 57.31, + importKwh: 26.31, + exportKwh: 28.89, + ), + ratios: coincide, + ), + ), + ), + )); + await tester.pumpAndSettle(); + expect(find.textContaining('Instantané depuis'), findsNothing, + reason: 'répéter les mêmes nombres oblige le lecteur à raisonner pour ' + 'découvrir que les fenêtres diffèrent'); + }); + testWidgets('box antérieure : aucune ligne, aucune supposition', (tester) async { await tester.binding.setSurfaceSize(const Size(1000, 2000)); addTearDown(() => tester.binding.setSurfaceSize(null));