diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 155fddf..2f5a38b 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -170,9 +170,9 @@ "description": "ECO_FLOOR_GRID. L'achat est le fait actionnable : il coûte, contrairement au surplus.", "placeholders": { "gridW": { "type": "int" } } }, - "decisionEcoFloorMet": "Plancher atteint — {livre} sur {cible} livrés", + "decisionEcoFloorMet": "Plancher atteint — {cible} dus, {livre} livrés", "@decisionEcoFloorMet": { - "description": "ECO_FLOOR_MET. Le SEUL état qui ferme l'obligation, et il est interdit sous le régime unmeasurable (R6) : on n'annonce pas tenu ce qu'on ne mesure pas.", + "description": "ECO_FLOOR_MET. Le SEUL état qui ferme l'obligation, et il est interdit sous le régime unmeasurable (R6) : on n'annonce pas tenu ce qu'on ne mesure pas. ⚠️ « X sur Y livrés » était FAUX une fois le plancher dépassé — vu sur appareil le 2026-09-06, « 29,9 kWh sur 3,0 kWh livrés », un numérateur dix fois le dénominateur. Le compteur continue de courir après que l'obligation est tenue : la fraction cesse alors d'en être une. On dit donc ce qui était DÛ, puis ce qui a été livré.", "placeholders": { "livre": { "type": "String" }, "cible": { "type": "String" } } }, "decisionEcoFloorMissed": "Obligation abandonnée à la bascule — {raison}", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 08d8a91..4731187 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -493,10 +493,10 @@ abstract class L10n { /// **'Plancher servi — {gridW} W achetés au réseau'** String decisionEcoFloorGrid(int gridW); - /// ECO_FLOOR_MET. Le SEUL état qui ferme l'obligation, et il est interdit sous le régime unmeasurable (R6) : on n'annonce pas tenu ce qu'on ne mesure pas. + /// ECO_FLOOR_MET. Le SEUL état qui ferme l'obligation, et il est interdit sous le régime unmeasurable (R6) : on n'annonce pas tenu ce qu'on ne mesure pas. ⚠️ « X sur Y livrés » était FAUX une fois le plancher dépassé — vu sur appareil le 2026-09-06, « 29,9 kWh sur 3,0 kWh livrés », un numérateur dix fois le dénominateur. Le compteur continue de courir après que l'obligation est tenue : la fraction cesse alors d'en être une. On dit donc ce qui était DÛ, puis ce qui a été livré. /// /// In fr, this message translates to: - /// **'Plancher atteint — {livre} sur {cible} livrés'** + /// **'Plancher atteint — {cible} dus, {livre} livrés'** String decisionEcoFloorMet(String livre, String cible); /// ECO_FLOOR_MISSED. La cause décide de la phrase ET du geste. Pas de cause « surplus insuffisant » : l'éco achète au réseau, donc le surplus ne peut jamais l'empêcher. diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 4d52cf8..c84992b 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -276,7 +276,7 @@ class L10nFr extends L10n { @override String decisionEcoFloorMet(String livre, String cible) { - return 'Plancher atteint — $livre sur $cible livrés'; + return 'Plancher atteint — $cible dus, $livre livrés'; } @override diff --git a/lib/services/telemetry_text.dart b/lib/services/telemetry_text.dart index 6a984db..9341531 100644 --- a/lib/services/telemetry_text.dart +++ b/lib/services/telemetry_text.dart @@ -350,7 +350,21 @@ String? ecoProgressText(L10n t, LoadLevel niveau) { final livre = p.deliveredWh; if (livre == null) return null; - final base = t.ecoAvancementMesure(energieCourte(livre), energieCourte(p.targetWh)); + + // ⚠️ **`ECO_FLOOR_MET` porte DÉJÀ le livré et la cible dans son motif.** Répéter la même + // phrase juste dessous donnait, sur appareil le 2026-09-06 : + // + // ÉCO Plancher atteint — 29,9 kWh sur 3,0 kWh livrés + // 29,9 kWh sur 3,0 kWh livrés · mesuré depuis 10:00 + // + // Deux fois les mêmes chiffres, et le lecteur cherche ce que la seconde ligne ajoute. + // C'est le même défaut que la ligne des ratios instantanés du 2026-09-05 : je déclenche + // sur ce que je SAIS (il y a un avancement) plutôt que sur ce qui se VOIT (est-il déjà + // dit ?). La ligne ne garde donc que ce que le motif ne dit pas — l'origine de la mesure. + final dejaDit = d?.code == 'ECO_FLOOR_MET'; + final base = dejaDit + ? null + : t.ecoAvancementMesure(energieCourte(livre), energieCourte(p.targetWh)); final depuis = p.measuredSince; if (depuis == null) return base; final h = '${depuis.hour.toString().padLeft(2, '0')}:' @@ -358,5 +372,7 @@ String? ecoProgressText(L10n t, LoadLevel niveau) { // La date reste le PREMIER instant mesuré ; l'intermittence se dit à part. Après une // bascule en cours de période, `deliveredWh` ne compte que depuis elle pendant que la cible // reste entière — la paire n'est donc plus une fraction, et c'est cette date qui l'explique. - return '$base · ${p.parIntermittence ? t.ecoMesureIntermittente(h) : t.ecoMesureDepuis(h)}'; + final origine = + p.parIntermittence ? t.ecoMesureIntermittente(h) : t.ecoMesureDepuis(h); + return base == null ? origine : '$base · $origine'; } diff --git a/lib/widgets/load_cards.dart b/lib/widgets/load_cards.dart index 544171e..ce7fe15 100644 --- a/lib/widgets/load_cards.dart +++ b/lib/widgets/load_cards.dart @@ -180,9 +180,14 @@ class LoadLiveBlock extends StatelessWidget { size: 14, color: stale ? const Color(0xFFB0B7BF) : const Color(0xFF2E7D32)), const SizedBox(width: 4), + // ⚠️ **Le parent doit DOMINER ses deux lignes.** Vu sur appareil le 2026-09-06 : + // à taille et graisse égales, l'allocation et les deux niveaux se lisaient comme + // trois lignes sœurs — et les pastilles ÉCO/CONFORT, colorées, tiraient même + // l'œil DAVANTAGE que la commande. C'est le contresens exact que la forme doit + // empêcher (LM-1003 : deux lignes, une seule décision). Text(t.telemetryAllocated(live.allocatedW.round()), style: TextStyle( - fontSize: 12, fontWeight: FontWeight.w600, color: tone)), + fontSize: 13.5, fontWeight: FontWeight.w700, color: tone)), if (mechText != null) ...[ const SizedBox(width: 6), Flexible( @@ -746,24 +751,28 @@ List _lignesNiveaux(L10n t, LoadTelemetryEntry live, Color tone) { Padding( padding: const EdgeInsets.only(top: 3), child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Un filet vertical rattache les niveaux à la commande : ils en sont la + // décomposition, pas des lignes voisines. La pastille, elle, s'efface — colorée + // et grasse, elle captait l'œil avant le nombre qu'elle explique. Container( - width: 54, - padding: const EdgeInsets.symmetric(vertical: 1), - decoration: BoxDecoration( - color: (n.estEco ? const Color(0xFFF9A825) : const Color(0xFF2E7D32)) - .withValues(alpha: 0.12), - borderRadius: BorderRadius.circular(4), - ), + width: 2, + height: 30, + margin: const EdgeInsets.only(left: 2, right: 8, top: 2), + color: (n.estEco ? const Color(0xFFF9A825) : const Color(0xFF2E7D32)) + .withValues(alpha: 0.35), + ), + SizedBox( + width: 52, child: Text(n.estEco ? t.niveauEco : t.niveauConfort, - textAlign: TextAlign.center, style: TextStyle( fontSize: 9, - fontWeight: FontWeight.w700, - color: n.estEco - ? const Color(0xFFF9A825) - : const Color(0xFF2E7D32))), + fontWeight: FontWeight.w600, + letterSpacing: 0.3, + color: (n.estEco + ? const Color(0xFFF9A825) + : const Color(0xFF2E7D32)) + .withValues(alpha: 0.75))), ), - const SizedBox(width: 6), Expanded( child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(_phraseNiveau(t, n), diff --git a/test/eco_levels_test.dart b/test/eco_levels_test.dart index fcbefb3..75eca05 100644 --- a/test/eco_levels_test.dart +++ b/test/eco_levels_test.dart @@ -185,6 +185,43 @@ void main() { expect(find.textContaining('atteint —'), findsNothing); }); + testWidgets('le plancher DÉPASSÉ ne s\'écrit pas comme une fraction', + (tester) async { + // Vu sur appareil le 2026-09-06 : « Plancher atteint — 29,9 kWh sur 3,0 kWh + // livrés ». Le compteur continue de courir après que l'obligation est tenue, donc + // le numérateur dépasse le dénominateur et la fraction cesse d'en être une. + await tester.pumpWidget(_carte(_payload( + progressEco: { + 'regime': 'measured', 'targetWh': 3000.0, 'deliveredWh': 29900.0, + 'purchasedWh': 127.0, 'measuredSince': '2026-09-05T08:00:00Z', + }, + decisionEco: { + 'code': 'ECO_FLOOR_MET', + 'params': {'deliveredWh': 29900, 'targetWh': 3000}, + }, + ))); + expect(find.textContaining('3,0 kWh dus, 29,9 kWh livrés'), findsOneWidget); + expect(find.textContaining('sur 3,0 kWh'), findsNothing); + }); + + testWidgets('et la fraction n\'est PAS répétée sous le motif', (tester) async { + // Le motif porte déjà livré et cible ; la ligne d'avancement ne garde que ce qu'il + // ne dit pas — l'origine de la mesure. + await tester.pumpWidget(_carte(_payload( + progressEco: { + 'regime': 'measured', 'targetWh': 3000.0, 'deliveredWh': 29900.0, + 'purchasedWh': 127.0, 'measuredSince': '2026-09-05T08:00:00Z', + }, + decisionEco: { + 'code': 'ECO_FLOOR_MET', + 'params': {'deliveredWh': 29900, 'targetWh': 3000}, + }, + ))); + expect(find.textContaining('29,9 kWh livrés'), findsOneWidget, + reason: 'une seule fois, dans le motif'); + expect(find.textContaining('mesuré depuis'), findsOneWidget); + }); + testWidgets('sous `measured`, il se rend normalement', (tester) async { await tester.pumpWidget(_carte(_payload( progressEco: {