etm-powersync-app/docs/mockups/deux_passes_eco_confort_mockup.html
Patrick Schurig 1d679312c5 docs(§10): la baisse mesurée, et l'échéance est à l'heure de la BOX
Sonde en LECTURE SEULE — l'obligation est celle du moteur, rien n'a été écrit.

Le critère positif est atteint : `deliveredWh` +25 Wh/min (1 500 W, ce que le
compteur ECS annonce), `remainingWh` qui descend d'autant, `gridW` 466 → 463.
Décroissance, pas absence de mur.

Deux invariants tiennent, et l'écran les vérifiera à chaque trame :
- `deliveredWh + remainingWh == targetWh` — et c'est sous `measured` qu'elle
  mord, là où elle n'est plus le corollaire trivial d'`unmeasurable` ;
- `gridW == remainingWh / remainingS` — 466 == 466 puis 463 == 463, donc la loi
  d'étalement est vérifiable de l'extérieur, sans lire le code.

Fuseau : `remainingS` le confirme de notre côté (18 719 s à 06:48 heure box
mènent à 12:00 heure box), et `System.GetTime` publie `timeZone` — l'app peut
donc être honnête sans attendre l'arbitrage. Règle écrite dans CLAUDE.md et dans
la maquette : dire l'horloge au lieu de la choisir, fuseau de la box affiché à
côté du champ et divergence signalée. Ce que le lot moteur doit trancher reste
entier : d'où vient le fuseau, et ce qu'il fait quand il diverge.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
2026-09-04 07:50:41 +02:00

826 lines
55 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ETM PowerSync — §10 : deux passes, une commande</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@500;600;700&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root{
--bg:#081f2b; --panel:#0d2b3b; --card:#12384a; --card2:#0f3242;
--line:rgba(255,255,255,.08); --line2:rgba(255,255,255,.14);
--blue:#31a3dd; --yellow:#fec113; --green:#35c98a; --red:#e07a7a; --violet:#9d8ce0;
--txt:#e8f1f6; --muted:#90a8b6; --muted2:#6f8b9a; --r:16px;
}
*{box-sizing:border-box}
body{margin:0;background:
radial-gradient(900px 500px at 15% -10%, #0e3145 0%, transparent 60%),
radial-gradient(900px 600px at 110% 10%, #0b2535 0%, transparent 55%),
var(--bg);
font-family:'Inter',system-ui,sans-serif;color:var(--txt);min-height:100vh;padding:34px 20px 60px}
.head{max-width:1300px;margin:0 auto 26px}
.head h1{font-family:'Poppins';font-weight:700;font-size:20px;margin:0 0 6px}
.head p{margin:0 0 8px;color:var(--muted);font-size:13.5px;line-height:1.55;max-width:940px}
.head code{color:#9fd4ee} .head b{color:var(--yellow)}
.head .rep{border-left:2px solid var(--blue);padding-left:12px;margin:14px 0 0;max-width:940px}
.head .rep p{margin:0 0 5px;font-size:13px}
.head .rep b{color:var(--blue)}
.stage{display:flex;gap:34px;justify-content:center;flex-wrap:wrap;align-items:flex-start}
.col{display:flex;flex-direction:column;align-items:center;gap:12px;max-width:392px;width:100%}
.cap{color:var(--muted2);font-size:12px;font-family:'Poppins';font-weight:500;letter-spacing:.4px;text-transform:uppercase}
.cap b{color:var(--blue)}
.phone{width:min(392px,100%);background:var(--panel);border:1px solid var(--line2);border-radius:30px;
overflow:hidden;box-shadow:0 30px 80px -30px rgba(0,0,0,.7);display:flex;flex-direction:column;height:900px}
.appbar{display:flex;align-items:center;gap:12px;padding:18px 16px 14px;border-bottom:1px solid var(--line);
background:linear-gradient(#102f40,#0d2b3b)}
.appbar .ic{width:26px;height:26px;display:grid;place-items:center;color:var(--muted);font-size:18px}
.appbar h2{font-family:'Poppins';font-weight:600;font-size:15px;margin:0;flex:1;text-align:center}
.scroll{overflow:auto;padding:16px 15px 26px}
.scroll::-webkit-scrollbar{width:0}
.sect{font-family:'Poppins';font-weight:600;font-size:12px;color:var(--muted);letter-spacing:.3px;
margin:20px 0 9px;display:flex;align-items:baseline;gap:8px}
.sect:first-child{margin-top:2px}
.sect small{font-weight:500;color:var(--muted2);letter-spacing:0;font-size:11px}
/* ─ bandeau arbitrage : DEUX comptabilités ─ */
.arb{background:var(--card2);border:1px solid var(--line);border-radius:13px;padding:12px 13px}
.arb .top{display:flex;align-items:center;gap:8px;margin-bottom:8px}
.arb .dot{width:8px;height:8px;border-radius:50%;background:var(--green);box-shadow:0 0 0 4px rgba(53,201,138,.15)}
.arb .top b{font-family:'Poppins';font-size:12.5px;font-weight:600}
.arb .top i{margin-left:auto;font-style:normal;color:var(--muted2);font-size:11px}
.arb .lab{font-size:10px;color:var(--muted2);font-family:'Poppins';font-weight:600;letter-spacing:.3px;
text-transform:uppercase;margin:9px 0 5px;display:flex;align-items:center;gap:6px}
.arb .lab u{text-decoration:none;color:var(--muted2)}
.arb .lab .p1{color:var(--yellow)} .arb .lab .p2{color:var(--green)}
.arb .grid{display:grid;grid-template-columns:repeat(5,1fr);gap:6px}
.arb .grid.g4{grid-template-columns:repeat(4,1fr)}
.arb .cell{background:var(--panel);border:1px solid var(--line);border-radius:9px;padding:7px 4px;text-align:center}
.arb .cell u{display:block;text-decoration:none;font-size:9.5px;color:var(--muted2);margin-bottom:2px}
.arb .cell b{font-family:'Poppins';font-weight:600;font-size:11.5px}
.arb .cell.zero b{color:var(--muted2)}
.arb .cell.bind{border-color:rgba(224,122,122,.4);background:rgba(224,122,122,.07)}
.arb .cell.bind b{color:var(--red);font-size:10px}
/* ─ la phrase qui DIT que l'ordre affiché n'est pas l'ordre de service ─ */
.twopass{margin-top:9px;border:1px solid rgba(157,140,224,.34);background:rgba(157,140,224,.08);
border-radius:11px;padding:10px 11px;font-size:11.5px;line-height:1.5;color:var(--muted)}
.twopass b{color:var(--violet);font-family:'Poppins';font-size:11.5px;display:block;margin-bottom:3px}
.twopass a{color:var(--violet);text-decoration:none;font-weight:600;display:block;margin-top:6px}
/* ─ carte de charge ─ */
.load{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:12px;margin-bottom:9px}
.load.spot{border-color:rgba(157,140,224,.42);box-shadow:0 0 0 3px rgba(157,140,224,.07)}
.load .r1{display:flex;align-items:center;gap:10px}
.load .grip{color:var(--muted2);font-size:15px;cursor:grab}
.rank{width:23px;height:23px;border-radius:7px;background:rgba(49,163,221,.16);color:var(--blue);
font-family:'Poppins';font-weight:700;font-size:11.5px;display:grid;place-items:center;flex:none}
.load .nm{flex:1;min-width:0}
.load .nm b{font-family:'Poppins';font-weight:600;font-size:13.5px;display:block}
.load .nm span{font-size:11px;color:var(--muted2)}
.sw{width:38px;height:22px;border-radius:20px;background:rgba(53,201,138,.85);position:relative;cursor:pointer;flex:none}
.sw::after{content:"";position:absolute;top:2px;left:18px;width:18px;height:18px;border-radius:50%;background:#fff}
.sw.off{background:rgba(255,255,255,.16)} .sw.off::after{left:2px}
/* ─ LA COMMANDE : le parent. Les niveaux sont sa décomposition. ─ */
.cmd{margin-top:10px;padding:9px 10px;border:1px solid var(--line2);border-radius:11px;background:var(--card2)}
.cmd .l{display:flex;align-items:baseline;gap:8px}
.cmd .l b{font-family:'Poppins';font-weight:700;font-size:14px}
.cmd .l i{font-style:normal;font-size:10.5px;color:var(--muted2);margin-left:auto}
.cmd .l small{font-size:11px;color:var(--muted)}
.cmd.idle .l b{color:var(--muted2)}
.cmd .round{font-size:10.5px;color:var(--yellow);margin-top:4px}
.lvls{margin-top:8px;border-left:2px solid var(--line2);padding-left:10px;margin-left:4px}
.lvl{display:flex;gap:8px;padding:6px 0;font-size:11.5px;line-height:1.45}
.lvl + .lvl{border-top:1px dashed var(--line)}
.lvl .tag{flex:none;width:60px;font-family:'Poppins';font-weight:700;font-size:9.5px;letter-spacing:.4px;
padding:3px 0;text-align:center;border-radius:6px;height:19px}
.lvl .tag.eco{background:rgba(254,193,19,.14);color:var(--yellow)}
.lvl .tag.cft{background:rgba(53,201,138,.13);color:var(--green)}
.lvl .bd{flex:1;min-width:0}
.lvl .bd b{font-family:'Poppins';font-weight:600;font-size:11.5px;display:block}
.lvl .bd span{color:var(--muted2);display:block;margin-top:1px}
.lvl .bd em{font-style:normal;color:var(--muted)}
.lvl.ok .bd b{color:var(--green)}
.lvl.grid .bd b{color:var(--yellow)}
.lvl.wait .bd b{color:var(--muted)}
.lvl.no .bd b{color:var(--red)}
.lvl.mute .bd b{color:var(--muted2)}
.pill{display:inline-block;font-size:9.5px;font-family:'Poppins';font-weight:700;padding:1px 6px;border-radius:20px;
border:1px solid var(--line2);color:var(--muted2);margin-left:6px;vertical-align:1px}
.pill.meas{border-color:rgba(53,201,138,.4);color:var(--green)}
.pill.est{border-color:rgba(254,193,19,.4);color:var(--yellow)}
.pill.nm{border-color:rgba(224,122,122,.45);color:var(--red)}
.bar{height:5px;border-radius:4px;background:rgba(255,255,255,.08);margin-top:5px;overflow:hidden;display:flex}
.bar i{display:block;height:100%;background:var(--yellow)}
.bar.dash{background:repeating-linear-gradient(90deg,rgba(255,255,255,.14) 0 5px,transparent 5px 10px)}
.cfg{margin-top:10px;width:100%;background:transparent;border:1px solid rgba(49,163,221,.42);color:var(--blue);
font-family:'Poppins';font-weight:600;font-size:12px;padding:9px;border-radius:10px;cursor:pointer;
display:flex;align-items:center;justify-content:center;gap:7px}
/* ─ écran 2 : ordre de service ─ */
.hint{border:1px solid rgba(49,163,221,.28);background:rgba(49,163,221,.07);border-radius:12px;
padding:11px 12px;font-size:11.5px;color:var(--muted);line-height:1.5}
.hint b{color:var(--blue);font-family:'Poppins';font-size:12px;display:block;margin-bottom:3px}
.pass{margin-top:18px}
.pass .ph{display:flex;align-items:center;gap:9px;margin-bottom:9px}
.pass .ph .n{width:22px;height:22px;border-radius:7px;font-family:'Poppins';font-weight:700;font-size:11px;
display:grid;place-items:center;flex:none}
.pass.p1 .ph .n{background:rgba(254,193,19,.16);color:var(--yellow)}
.pass.p2 .ph .n{background:rgba(53,201,138,.16);color:var(--green)}
.pass .ph b{font-family:'Poppins';font-weight:600;font-size:12.5px}
.pass .ph i{margin-left:auto;font-style:normal;font-size:10.5px;color:var(--muted2)}
.pass.p1 .ph b{color:var(--yellow)} .pass.p2 .ph b{color:var(--green)}
.srv{display:flex;align-items:flex-start;gap:9px;padding:9px 10px;border-radius:11px;background:var(--card);
border:1px solid var(--line);margin-bottom:6px}
.srv .rk{width:21px;height:21px;border-radius:6px;background:rgba(49,163,221,.16);color:var(--blue);
font-family:'Poppins';font-weight:700;font-size:11px;display:grid;place-items:center;flex:none;margin-top:1px}
.srv .bd{flex:1;min-width:0;font-size:11.5px;line-height:1.45}
.srv .bd b{font-family:'Poppins';font-weight:600;font-size:12.5px;display:block}
.srv .bd span{color:var(--muted2);display:block;font-size:11px;margin-top:1px}
.srv .w{flex:none;text-align:right;font-family:'Poppins';font-weight:700;font-size:12.5px}
.srv .w u{display:block;text-decoration:none;font-size:9.5px;color:var(--muted2);font-weight:600;margin-top:2px}
.srv.g .w{color:var(--yellow)} .srv.s .w{color:var(--green)}
.srv.nil{opacity:.72} .srv.nil .w{color:var(--muted2)}
.srv.cut{border-color:rgba(224,122,122,.35);background:rgba(224,122,122,.06)} .srv.cut .w{color:var(--red)}
.rule{display:flex;align-items:center;gap:8px;margin:9px 0;color:var(--muted2);font-size:10.5px}
.rule:before,.rule:after{content:"";flex:1;height:1px;background:var(--line2)}
.exh{border:1px dashed rgba(224,122,122,.4);border-radius:10px;padding:8px 10px;font-size:11px;color:var(--muted);
line-height:1.45;margin-bottom:6px}
.exh b{color:var(--red);font-family:'Poppins';font-size:11px}
/* ─ écran 3 : catalogue ─ */
.cat{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:10px 11px;margin-bottom:8px}
.cat .h{display:flex;align-items:center;gap:8px;margin-bottom:6px}
.cat .h .tag{flex:none;font-family:'Poppins';font-weight:700;font-size:9.5px;letter-spacing:.4px;
padding:3px 7px;border-radius:6px}
.cat .h .tag.eco{background:rgba(254,193,19,.14);color:var(--yellow)}
.cat .h .tag.cft{background:rgba(53,201,138,.13);color:var(--green)}
.cat .h b{font-family:'Poppins';font-weight:600;font-size:12px;flex:1}
.cat .h code{font-size:10px;color:#9fd4ee}
.cat .txt{font-size:11.5px;color:var(--txt);line-height:1.45}
.cat .why{font-size:10.8px;color:var(--muted2);line-height:1.5;margin-top:4px}
.never{margin-top:16px;border:1px solid rgba(224,122,122,.32);background:rgba(224,122,122,.06);
border-radius:12px;padding:11px 12px}
.never b{color:var(--red);font-family:'Poppins';font-size:11.5px;display:block;margin-bottom:5px}
.never ul{margin:0;padding-left:16px;font-size:11px;color:var(--muted);line-height:1.6}
.never li b{display:inline;color:var(--red);font-size:11px}
.note{width:min(392px,100%);font-size:11.5px;color:var(--muted2);line-height:1.55;text-align:left}
.note b{color:var(--yellow)} .note code{color:#9fd4ee}
.note ul{margin:7px 0 0;padding-left:16px} .note li{margin-bottom:5px}
/* ─ le brief moteur ─ */
.brief{max-width:1300px;margin:44px auto 0;border-top:1px solid var(--line2);padding-top:28px}
.brief h2{font-family:'Poppins';font-weight:700;font-size:17px;margin:0 0 4px}
.brief h3{font-family:'Poppins';font-weight:600;font-size:13.5px;margin:22px 0 6px;color:var(--blue)}
.brief p{color:var(--muted);font-size:13px;line-height:1.6;max-width:940px;margin:0 0 9px}
.brief b{color:var(--yellow)} .brief code{color:#9fd4ee;font-size:12px}
.brief pre{background:#06171f;border:1px solid var(--line);border-radius:12px;padding:14px 16px;overflow:auto;
font-size:11.8px;line-height:1.55;color:#cfe6f2;max-width:940px}
.brief pre .c{color:#5d8698}
.brief pre .k{color:#9fd4ee}
.brief pre .n{color:var(--yellow)}
.brief pre .s{color:#9ee0b8}
.req{border:1px solid var(--line);background:var(--card2);border-radius:12px;padding:12px 14px;margin-bottom:9px;
max-width:940px}
.req b{font-family:'Poppins';font-weight:600;font-size:12.5px;color:var(--txt);display:block;margin-bottom:3px}
.req b u{text-decoration:none;color:var(--blue);margin-right:7px}
.req p{margin:0;font-size:12px;color:var(--muted);line-height:1.55}
.req.hard{border-color:rgba(224,122,122,.35);background:rgba(224,122,122,.05)}
.req.hard b u{color:var(--red)}
.brief .no{border-left:2px solid var(--line2);padding-left:12px;max-width:940px}
.brief .no li{color:var(--muted);font-size:12.5px;line-height:1.6;margin-bottom:4px}
</style>
</head>
<body>
<div class="head">
<h1>§10 — Éco et confort : deux passes, deux lectures, <b>une seule commande</b></h1>
<p>Réponse de l'app aux trois questions du brief plugin <code>docs/BRIEF_depuis_plugin.md</code> §9
(= <code>DESIGN_10.md</code> 8.5, LM-1002 / LM-1003 / LM-1006 couplage 2 / LM-1009). Rien n'est câblé côté app :
cette maquette est le <b>brief</b>, et la charge utile demandée est en bas de page.</p>
<p><b>Elle vaut comme contrat, pas comme feuille de route.</b> Le §10 n'est pas implémenté, le transport du
§14a n'existe pas, <code>draw</code> non plus : aucun de ces trois écrans ne se construit avant que
<code>levels[]</code> soit en ligne. Ce qu'elle fixe, c'est la <em>forme</em> de ce qui sera publié — pour
que le moteur n'ait pas à inventer une charge utile que l'écran devra ensuite arranger.</p>
<div class="rep">
<p><b>1 · La liste reste ordonnée par RANG</b> — parce que c'est la surface d'édition du rang, et parce que
l'ordre de service <em>n'est pas une propriété de la configuration</em> : il change à chaque cycle selon les
planchers déjà atteints. Une liste qui se réordonne toute seule sous le doigt est inutilisable.</p>
<p><b>2 · Une seule entrée par charge dans la liste, deux lignes DANS la carte</b> — la commande est le parent,
les deux niveaux sont sa décomposition. On ne dédouble que là où l'objet affiché est la <em>séquence</em>,
jamais là où l'objet est la <em>charge</em>.</p>
<p><b>3 · L'ordre de service devient un second écran, en lecture seule</b> — c'est lui qui « le dit ». Il montre
le rang 4 éco avant le rang 1 confort, avec les rangs visibles, donc il montre en même temps que
<em>votre ordre est respecté dans chaque passe</em>. C'est l'écran qui désamorce « l'ordre n'est pas respecté ».</p>
</div>
</div>
<div class="stage">
<!-- ══════════ ÉCRAN 1 — la liste, au rang ══════════ -->
<div class="col">
<div class="phone">
<div class="appbar"><div class="ic">‹</div><h2>Rôles &amp; appareils</h2><div class="ic">⟳</div></div>
<div class="scroll">
<div class="sect">Arbitrage en direct <small>· cycle de 06:41, il y a 22 s</small></div>
<div class="arb">
<div class="top"><span class="dot"></span><b>Matin sans soleil</b><i>2 comptabilités</i></div>
<div class="lab"><span class="p2">■</span><u>Passe 2 — surplus disponible</u></div>
<div class="grid">
<div class="cell zero"><u>Surplus</u><b>0</b></div>
<div class="cell zero" style="border-style:dashed"><u>Réservé VE *</u><b>0</b></div>
<div class="cell zero"><u>Recrédité</u><b>0</b></div>
<div class="cell zero"><u>Alloué</u><b>0</b></div>
<div class="cell zero"><u>Reliquat</u><b>0</b></div>
</div>
<div class="lab"><span class="p1">■</span><u>Passe 1 — autorisation de soutirage</u></div>
<div class="grid g4">
<div class="cell"><u>Autorisé</u><b>4 000</b></div>
<div class="cell"><u>Engagé</u><b>4 000</b></div>
<div class="cell zero"><u>Restant</u><b>0</b></div>
<div class="cell bind"><u>Borné par</u><b>GRD</b></div>
</div>
<div class="twopass">
<b>◆ Deux passes ce cycle</b>
Tous les <em>planchers éco</em> sont servis d'abord, par rang croissant ; le <em>confort</em> ensuite,
dans le même ordre. Un plancher de rang 4 passe donc avant un confort de rang 1 —
<em>c'est voulu, et votre ordre est respecté à l'intérieur de chaque passe.</em>
<a>Voir l'ordre de service de ce cycle ›</a>
</div>
</div>
<div class="sect">Charges pilotables <small>· ordonnées par rang — glissez pour classer</small></div>
<!-- LE CAS FRÉQUENT : éco servi, confort non -->
<div class="load spot">
<div class="r1">
<span class="grip">⠿</span><span class="rank">1</span>
<div class="nm"><b>Chauffe-eau</b><span>Routeur de relais · 3 relais · ECS</span></div>
<span class="sw"></span>
</div>
<div class="cmd">
<div class="l"><b>Décidé 1 400 W</b><i>une seule décision</i></div>
<div class="round">↳ appliqué au mécanisme : <b>palier 1 000 W</b> (relais 1+2) — l'adaptateur
n'a pas ce point exact</div>
</div>
<div class="lvls">
<div class="lvl grid">
<span class="tag eco">ÉCO</span>
<div class="bd">
<b>Plancher servi — 1 400 W achetés au réseau</b>
<span>2,1 kWh livrés sur les 3,0 dus avant 07:00 <em>(heure box)</em>
<span class="pill meas">MESURÉ</span></span>
<div class="bar"><i style="width:70%"></i></div>
</div>
</div>
<div class="lvl wait">
<span class="tag cft">CONFORT</span>
<div class="bd">
<b>En attente</b>
<span>Aucun surplus ce cycle — le bonus n'est pas refusé, il n'a rien à prendre.</span>
</div>
</div>
</div>
<button class="cfg">⚙ Configurer</button>
</div>
<div class="load">
<div class="r1">
<span class="grip">⠿</span><span class="rank">2</span>
<div class="nm"><b>PAC banc</b><span>SG-Ready · chauffage</span></div>
<span class="sw"></span>
</div>
<div class="cmd idle">
<div class="l"><b>Décidé 0 W</b><small>état SG-Ready 2 appliqué</small><i>une seule décision</i></div>
</div>
<div class="lvls">
<div class="lvl ok">
<span class="tag eco">ÉCO</span>
<div class="bd">
<b>Plancher atteint</b>
<span>3,0 kWh livrés sur 3,0 <span class="pill meas">MESURÉ</span> — plus rien à servir en passe 1.</span>
<div class="bar"><i style="width:100%;background:var(--green)"></i></div>
</div>
</div>
<div class="lvl wait">
<span class="tag cft">CONFORT</span>
<div class="bd"><b>En attente</b><span>Aucun surplus ce cycle.</span></div>
</div>
</div>
<button class="cfg">⚙ Configurer</button>
</div>
<div class="load">
<div class="r1">
<span class="grip">⠿</span><span class="rank">3</span>
<div class="nm"><b>Borne sans compteur de session</b><span>Borne de recharge · VE</span></div>
<span class="sw"></span>
</div>
<div class="cmd">
<div class="l"><b>Décidé 2 600 W</b><i>une seule décision</i></div>
<div class="round">↳ appliqué au mécanisme : <b>11 A · 1 phase ≈ 2 530 W</b> — le courant est
entier, la décision ne l'est pas</div>
</div>
<div class="lvls">
<div class="lvl grid">
<span class="tag eco">ÉCO</span>
<div class="bd">
<b>Plancher servi — 2 600 W achetés au réseau</b>
<span>Avancement <em>non mesurable</em> : cette borne ne publie pas d'énergie de session.
<span class="pill nm">NON MESURABLE</span></span>
<div class="bar dash"></div>
<span><b>Reste 4,0 kWh à livrer en 5 h 13</b> — et ce taux ne peut que <b>monter</b> : rien
n'est soustrait de ce qui reste, donc l'achat s'accélère jusqu'à l'échéance.</span>
<span>L'obligation est servie ; elle ne pourra jamais être annoncée <em>tenue</em>.</span>
</div>
</div>
<div class="lvl wait">
<span class="tag cft">CONFORT</span>
<div class="bd"><b>En attente</b><span>Aucun surplus ce cycle.</span></div>
</div>
</div>
<button class="cfg">⚙ Configurer</button>
</div>
<div class="load">
<div class="r1">
<span class="grip">⠿</span><span class="rank">4</span>
<div class="nm"><b>Plancher chauffant</b><span>Charge modulable · chauffage</span></div>
<span class="sw"></span>
</div>
<div class="cmd idle">
<div class="l"><b>Décidé 0 W</b><i>une seule décision</i></div>
</div>
<div class="lvls">
<div class="lvl no">
<span class="tag eco">ÉCO</span>
<div class="bd">
<b>Plancher NON servi — plafond de soutirage atteint</b>
<span>Plafond imposé par le <em>gestionnaire de réseau</em> (4 000 W), épuisé au rang 3.
Ce n'est pas la protection de surcharge de l'installation.</span>
<span>0,4 kWh livrés sur 2,0 <span class="pill meas">MESURÉ</span> — obligation reportée.</span>
<div class="bar"><i style="width:20%;background:var(--red)"></i></div>
</div>
</div>
<div class="lvl wait">
<span class="tag cft">CONFORT</span>
<div class="bd"><b>En attente</b><span>Aucun surplus ce cycle.</span></div>
</div>
</div>
<button class="cfg">⚙ Configurer</button>
</div>
<div class="load">
<div class="r1">
<span class="grip">⠿</span><span class="rank">5</span>
<div class="nm"><b>Sèche-serviettes</b><span>Routeur de relais · 1 relais</span></div>
<span class="sw"></span>
</div>
<div class="cmd idle">
<div class="l"><b>Décidé 0 W</b><i>une seule décision</i></div>
</div>
<div class="lvls">
<div class="lvl mute">
<span class="tag cft">CONFORT</span>
<div class="bd">
<b>Pas demandeur</b>
<span>22,4 °C mesurés ≥ 22 °C de consigne — le bonus n'est plus utile.</span>
</div>
</div>
</div>
<div class="lvl mute" style="padding-left:14px;border-top:1px dashed var(--line)">
<div class="bd"><span>Aucune obligation éco déclarée : cette charge ne participe qu'à la passe 2 —
c'est exactement le comportement d'aujourd'hui.</span></div>
</div>
<button class="cfg">⚙ Configurer</button>
</div>
</div>
</div>
<div class="cap">Écran 1 — <b>liste au rang</b>, une entrée par charge</div>
<div class="note">
<b>Ce qui ne change pas :</b> le glisser-déposer produit toujours une suite d'entiers <code>priority</code> à plat,
et il n'existe qu'un rang. La maquette est un <b>rejet appliqué</b> de <code>prioritéEco</code>/<code>prioritéConfort</code>.
<ul>
<li>Les deux lignes de niveau vivent <b>sous</b> la ligne de commande, décalées et rattachées par un filet :
la commande est le fait, les niveaux en sont la décomposition. C'est ce qui rend LM-1003 lisible sans
l'écrire.</li>
<li><b>L'écart affiché est décidé ↔ appliqué</b>, jamais <code>Σ targetW</code> ↔ <code>allocatedW</code> :
ces deux-là sont <b>égaux par construction</b>, tous deux du décidé. Un écran bâti sur la prémisse
inverse comparerait deux valeurs toujours égales — il ne montrerait jamais rien, <b>et sans
erreur</b>. L'appliqué se lit dans la charge utile de mécanisme (<code>stageW</code>,
<code>setpointW</code>, <code>currentA</code>), qui existe déjà aujourd'hui.</li>
<li><b>Et il ne se répartit pas non plus.</b> L'écart appartient à la charge, pas à un niveau :
l'adaptateur arrondit une consigne unique, il ne sait pas de quel besoin elle venait.</li>
<li><b>L'heure d'une échéance est celle de la BOX</b>, pas celle du client :
<code>dailyDeadline</code> est un <code>"HH:MM"</code> sans fuseau, interprété dans celui de
la box. Mesuré le 2026-09-04 — <code>.75</code> est en <code>Europe/London</code>, donc
« 12:00 » vaut 13:00 sur une horloge française. Le fuseau se lit par
<code>System.GetTime</code> : l'écran l'affiche à côté du champ et signale la divergence,
plutôt que de choisir une horloge à la place de l'installateur.</li>
<li><b>Sous <code>unmeasurable</code>, le plancher ne peut pas se refermer</b> — mesuré le
2026-09-01 sur quatre cycles : <code>remainingWh</code> figé à 4 000, <code>remainingS</code> qui
descend, <code>gridW</code> qui monte 755 → 767. Ce n'est pas un cas limite, c'est la
<b>trajectoire garantie</b> du régime : l'écran doit montrer <em>reste</em> et <em>temps</em>, pas
seulement les watts de l'instant, sans quoi « 767 W achetés » ne laisse pas deviner le mur.</li>
<li><b>* « Commandé, pas encore mesuré » — et ce n'est pas une destination.</b> Le champ
<code>budget.evReservedW</code> est un <b>terme de correction</b> : la latence entre l'ordre et sa
lecture au compteur. Il est affiché parce que la box le publie, <b>jamais sommé</b>. Son ancien
libellé, « Réservé au véhicule », affirmait deux faussetés — que c'est réservé, et que ça regarde le
véhicule plutôt que la latence de sa mesure ; réécrit le 2026-08-30 sur la définition livrée par le
moteur. Les watts achetés, eux, n'ont de registre qu'avec <code>draw.committedW</code>.</li>
<li>Le bandeau ◆ n'apparaît <b>que si au moins une charge porte une obligation éco</b>. Sans obligation,
il n'y a qu'une passe utile et la phrase serait du bruit (LM-1007).</li>
</ul>
</div>
</div>
<!-- ══════════ ÉCRAN 2 — l'ordre de service ══════════ -->
<div class="col">
<div class="phone">
<div class="appbar"><div class="ic">‹</div><h2>Ordre de service</h2><div class="ic">⟳</div></div>
<div class="scroll">
<div class="hint">
<b>Ce que le moteur a fait, dans l'ordre</b>
Cycle de 06:41 · lecture seule. Une charge apparaît <em>deux fois</em> — une par passe : ici l'objet
affiché est la <b>séquence</b>, pas la charge. Votre rang décide de l'ordre <em>à l'intérieur</em> de
chaque passe, jamais entre les deux.
</div>
<div class="pass p1">
<div class="ph"><span class="n">1</span><b>PLANCHERS ÉCO</b><i>4 000 W autorisés au réseau</i></div>
<div class="srv g">
<span class="rk">1</span>
<div class="bd"><b>Chauffe-eau</b><span>2,1 / 3,0 kWh · au réseau</span></div>
<div class="w">1 400 W<u>reste 2 600</u></div>
</div>
<div class="srv nil">
<span class="rk">2</span>
<div class="bd"><b>PAC banc</b><span>Plancher déjà atteint — rien à servir</span></div>
<div class="w">0 W<u>reste 2 600</u></div>
</div>
<div class="srv g">
<span class="rk">3</span>
<div class="bd"><b>Borne sans compteur de session</b><span>Avancement non mesurable · au réseau</span></div>
<div class="w">2 600 W<u>reste 0</u></div>
</div>
<div class="exh"><b>⚠ Autorisation de soutirage épuisée ici</b> — plafond du gestionnaire de réseau
(4 000 W). Les charges suivantes ne verront pas leur plancher ce cycle.</div>
<div class="srv cut">
<span class="rk">4</span>
<div class="bd"><b>Plancher chauffant</b><span>0,4 / 2,0 kWh · obligation reportée</span></div>
<div class="w">refusé<u>plafond GRD</u></div>
</div>
<div class="srv nil">
<span class="rk">5</span>
<div class="bd"><b>Sèche-serviettes</b><span>Aucune obligation déclarée</span></div>
<div class="w">—<u>hors passe 1</u></div>
</div>
</div>
<div class="rule">puis, sur le reliquat, et seulement là</div>
<div class="pass p2">
<div class="ph"><span class="n">2</span><b>CONFORT</b><i>0 W de surplus</i></div>
<div class="exh" style="border-color:rgba(255,255,255,.18)">
<b style="color:var(--muted)">Aucun surplus ce cycle</b> — la passe 2 a été parcourue en entier et n'a
rien pu servir. Ce n'est pas un refus par charge : c'est une passe sans budget.
</div>
<div class="srv nil"><span class="rk">1</span>
<div class="bd"><b>Chauffe-eau</b><span>Demandeur · en attente</span></div><div class="w">0 W<u>reste 0</u></div></div>
<div class="srv nil"><span class="rk">2</span>
<div class="bd"><b>PAC banc</b><span>Demandeur · en attente</span></div><div class="w">0 W<u>reste 0</u></div></div>
<div class="srv nil"><span class="rk">3</span>
<div class="bd"><b>Borne sans compteur de session</b><span>Demandeur · en attente</span></div><div class="w">0 W<u>reste 0</u></div></div>
<div class="srv nil"><span class="rk">4</span>
<div class="bd"><b>Plancher chauffant</b><span>Demandeur · en attente</span></div><div class="w">0 W<u>reste 0</u></div></div>
<div class="srv nil"><span class="rk">5</span>
<div class="bd"><b>Sèche-serviettes</b><span>Pas demandeur — 22,4 °C ≥ 22 °C</span></div>
<div class="w">—<u>sauté</u></div></div>
</div>
<div class="hint" style="margin-top:20px;border-color:rgba(157,140,224,.3);background:rgba(157,140,224,.07)">
<b style="color:var(--violet)">Pourquoi le rang 4 passe avant le rang 1</b>
Le rang classe les <em>charges</em>. La hiérarchie éco/confort classe les <em>besoins</em>, et elle
surplombe le rang : un plancher est une obligation, un confort est un bonus. Aucune charge ne double
une autre <em>à niveau égal</em> — et c'est exactement ce que cet écran donne à vérifier, ligne à ligne.
</div>
</div>
</div>
<div class="cap">Écran 2 — <b>séquence</b>, une charge deux fois</div>
<div class="note">
<b>Écran dérivé, sans champ nouveau.</b> La séquence se reconstruit entièrement côté app par un tri
lexicographique sur <code>(niveau, priority)</code> : <b>ne publiez pas d'index d'ordre</b>, il serait une
seconde source de vérité pour une donnée déjà déductible.
<ul>
<li>Les compteurs « reste N » de chaque ligne sont un <b>cumul local</b> — ils exigent seulement que
chaque niveau publie ce qu'il a pris <em>et</em> son financement.</li>
<li>Le point de rupture (« autorisation épuisée ici ») est <b>lu</b>, jamais déduit d'un solde à zéro :
il vient du motif <code>DRAW_CAP</code> de la première charge refusée, avec sa <b>source</b>.</li>
<li>Cet écran s'ouvre depuis le bandeau ◆ et depuis chaque carte ; il n'a aucun réglage, donc aucun
risque de devenir un second endroit où l'on classe.</li>
</ul>
</div>
</div>
<!-- ══════════ ÉCRAN 3 — catalogue d'états ══════════ -->
<div class="col">
<div class="phone">
<div class="appbar"><div class="ic">‹</div><h2>États d'un niveau</h2><div class="ic">?</div></div>
<div class="scroll">
<div class="hint">
<b>Le vocabulaire, et il est fermé</b>
Chaque niveau d'une charge est <em>toujours</em> dans un de ces états, et chacun est un état
<b>positif</b> — jamais « à moitié servie », jamais un pourcentage d'avancement.
</div>
<div class="sect">Niveau ÉCO <small>· présent seulement si la charge porte une obligation</small></div>
<div class="cat"><div class="h"><span class="tag eco">ÉCO</span><b>Plancher servi</b><code>ECO_FLOOR_*</code></div>
<div class="txt">« Plancher servi — 1 400 W pris sur le surplus »<br>
« Plancher servi — 1 400 W achetés au réseau »</div>
<div class="why">Deux phrases, parce que ce sont deux faits différents pour le client : l'un est gratuit,
l'autre est une facture. Le financement se lit sur le <b>niveau</b>, pas sur la charge.</div></div>
<div class="cat"><div class="h"><span class="tag eco">ÉCO</span><b>Plancher atteint</b><code>ECO_FLOOR_MET</code></div>
<div class="txt">« Plancher atteint — 3,0 / 3,0 kWh livrés »</div>
<div class="why">Le seul état qui <b>ferme</b> l'obligation. Interdit sous le régime « non mesurable » :
on ne peut pas annoncer tenu ce qu'on ne mesure pas (LM-1009 C1).</div></div>
<div class="cat"><div class="h"><span class="tag eco">ÉCO</span><b>Non servi — plafond</b><code>DRAW_CAP</code></div>
<div class="txt">« Plafond de soutirage atteint — <em>gestionnaire de réseau</em> »<br>
« … — <em>protection du branchement</em> » · « … — <em>limite que vous vous imposez</em> »</div>
<div class="why">Trois sources, trois phrases, trois gestes : subir et se renseigner, délester ou revoir
l'abonnement, ou changer un réglage. Un motif qui les confond envoie diagnostiquer le mauvais
problème.</div>
<div class="why" style="border-top:1px dashed rgba(255,255,255,.1);padding-top:5px;margin-top:6px">
<b style="color:var(--yellow)">Sa frontière</b> : <code>DRAW_CAP</code> ne sort que si
l'autorisation résiduelle est <b>nulle</b>. S'il en reste mais sous le plus petit palier, le motif
est <code>BELOW_MIN_POWER</code> — il y en a, pas au bon format. Deux gestes : revoir le plafond,
ou lire la fiche technique.</div></div>
<div class="cat"><div class="h"><span class="tag eco">ÉCO</span><b>Avancement non mesurable</b><code>regime</code></div>
<div class="txt">« Servi · avancement non mesurable » — barre <em>hachurée</em>, aucun chiffre livré.</div>
<div class="why">Un régime, pas un motif : les trois régimes coexistent dans le même cycle sur des
charges différentes (LM-1009 C2). <b>Zéro serait un mensonge</b> ; l'absence est la vérité.
<br><b style="color:var(--green)">Corrigé le 2026-08-30</b> : l'exemple ne nomme plus la V2C
Trydan — elle <b>publie</b> `sessionEnergy` depuis le plugin `+etm13`, vérifié sur charge réelle
(15,80 → 15,84 kWh en mouvement). Le régime « non mesurable » reste vrai pour les bornes qui n'ont
pas cet état.</div></div>
<div class="sect">Niveau CONFORT <small>· toujours présent</small></div>
<div class="cat"><div class="h"><span class="tag cft">CONFORT</span><b>Servi</b><code>SURPLUS_SETPOINT</code></div>
<div class="txt">« Bonus servi — 2 100 W de surplus »</div>
<div class="why">L'état d'aujourd'hui, inchangé. Une charge sans obligation éco n'a jamais que cette
ligne : le modèle à deux niveaux dégrade proprement vers l'actuel.</div></div>
<div class="cat"><div class="h"><span class="tag cft">CONFORT</span><b>En attente</b><code>SURPLUS_INSUFFICIENT</code></div>
<div class="txt">« En attente — aucun surplus ce cycle »<br>« En attente — reliquat épuisé au rang 2 »</div>
<div class="why">La charge <em>veut</em> son bonus et ne l'a pas. Un état de file d'attente, pas un
échec — et surtout pas « 0 W » tout seul, qui ne dit pas s'il y avait une demande.</div></div>
<div class="cat"><div class="h"><span class="tag cft">CONFORT</span><b>Pas demandeur</b><code>COMFORT_SKIPPED</code></div>
<div class="txt">« Pas demandeur — 22,4 °C ≥ 22 °C de consigne »</div>
<div class="why">Rien ne manque, rien n'attend : le comparateur de LM-1008 est satisfait. <b>À ne jamais
confondre avec « en attente »</b> — l'un se règle en changeant la consigne, l'autre en attendant le
soleil.</div></div>
<div class="cat" style="border-color:rgba(49,163,221,.3);background:rgba(49,163,221,.05)">
<div class="h"><span class="tag" style="background:rgba(49,163,221,.16);color:var(--blue)">×3</span>
<b>Trois nombres, et jamais le même sens</b></div>
<div class="txt">
<b>Décidé</b> <code>allocatedW</code> — ce que l'arbitre a planifié, et ce que les compteurs de
budget comptent.<br>
<b>Appliqué</b> <code>mechanism.*</code> — ce que l'adaptateur a su produire.<br>
<b>Mesuré</b> <code>measurement.powerW</code> — ce que la charge tire, quand c'est mesurable.
</div>
<div class="why">Chaque écart dit autre chose : <b>décidé ↔ appliqué</b> = l'arrondi du mécanisme,
normal et permanent ; <b>appliqué ↔ mesuré</b> = la charge n'obéit pas (et ce que ça prouve dépend
de <code>measurement.source</code>) ; <b>décidé ↔ mesuré</b> = la somme des deux, à ne jamais lire
seule — elle mélange une cause bénigne et une cause qui ne l'est pas.</div>
</div>
<div class="never">
<b>✖ Ce que l'écran n'affichera jamais</b>
<ul>
<li>Un <b>pourcentage</b> d'avancement d'obligation — l'énergie livrée, ou rien (LM-1009 D1).</li>
<li>Une charge <b>« servie à moitié »</b> : deux niveaux, deux états nommés.</li>
<li><b>Deux consignes</b> pour une charge : les deux lignes décomposent une commande unique.</li>
<li>Un <b>zéro</b> à la place d'une mesure absente.</li>
<li>Un <b>second classement</b> à régler.</li>
<li>Un écart entre <b><code>Σ targetW</code> et <code>allocatedW</code></b> : ils sont égaux par
construction. Cherché là, l'arrondi est invisible et l'écran se tait sans se tromper.</li>
<li>Une <b>exception de réconciliation</b> : avec <code>counts</code>, il n'y en a plus une seule.
Un écart qui subsiste est un défaut, et il se dit.</li>
</ul>
</div>
</div>
</div>
<div class="cap">Écran 3 — <b>catalogue</b> des états de niveau</div>
<div class="note">
<b>Pourquoi un catalogue plutôt qu'une échelle :</b> « éco servi / confort non » est le cas le plus fréquent
en régime normal. S'il s'affiche comme une <em>fraction</em> de quelque chose, il se lira comme une
dégradation permanente. Nommé, il se lit comme le fonctionnement nominal — <b>le plancher est tenu, le bonus
attend le soleil</b>.
</div>
</div>
</div>
<!-- ══════════════════ BRIEF MOTEUR ══════════════════ -->
<div class="brief">
<h2>Ce que ces écrans demandent à <code>GetLoadTelemetry</code></h2>
<p>Aucun de ces trois écrans n'est constructible sur la charge utile d'aujourd'hui : un <code>allocatedW</code> et
un <code>decision</code> par charge, un seul de chaque. Voici la forme minimale qui les rend possibles — elle
ajoute <b>un tableau</b> et ne retire rien.</p>
<pre>{
<span class="k">"loadId"</span>: "…", <span class="k">"available"</span>: true, <span class="k">"measurement"</span>: { … }, <span class="c">// inchangés</span>
<span class="k">"allocatedW"</span>: <span class="n">1400</span>, <span class="c">// INCHANGÉ — le DÉCIDÉ du cycle. Σ targetW == allocatedW, exactement.</span>
<span class="c">// L'appliqué vit dans "mechanism" ; le mesuré dans "measurement".</span>
<span class="k">"levels"</span>: [ <span class="c">// AJOUT — un élément par passe RÉELLEMENT parcourue</span>
{ <span class="k">"level"</span>: <span class="s">"eco"</span>, <span class="c">// RELEVÉ sur `.75` le 2026-09-01, +etm43</span>
<span class="k">"targetW"</span>: <span class="n">766.813994355397</span>, <span class="c">// le décidé, NON arrondi</span>
<span class="k">"funding"</span>: <span class="s">"grid"</span>,
<span class="k">"counts"</span>: { <span class="k">"draw.committedW"</span>: <span class="n">766.813994355397</span> },
<span class="k">"progress"</span>: { <span class="k">"regime"</span>: <span class="s">"unmeasurable"</span>, <span class="k">"targetWh"</span>: <span class="n">4000</span> },
<span class="c">// deliveredWh ABSENT sous unmeasurable — jamais 0</span>
<span class="k">"decision"</span>: { <span class="k">"code"</span>: <span class="s">"ECO_FLOOR_GRID"</span>, <span class="k">"params"</span>: {
<span class="k">"gridW"</span>: <span class="n">767</span>, <span class="k">"remainingWh"</span>: <span class="n">4000</span>, <span class="k">"targetWh"</span>: <span class="n">4000</span>,
<span class="k">"remainingS"</span>: <span class="n">18779</span>, <span class="k">"regime"</span>: <span class="s">"unmeasurable"</span>, <span class="k">"atMaximum"</span>: false } } },
{ <span class="k">"level"</span>: <span class="s">"comfort"</span>,
<span class="k">"targetW"</span>: <span class="n">0</span>,
<span class="k">"decision"</span>: { <span class="k">"code"</span>: <span class="s">"SURPLUS_INSUFFICIENT"</span>, <span class="k">"params"</span>: { <span class="k">"requiredW"</span>: <span class="n">900</span>, <span class="k">"availableW"</span>: <span class="n">0</span> } } }
],
<span class="k">"decision"</span>: { <span class="k">"level"</span>: <span class="s">"eco"</span>, <span class="k">"code"</span>: <span class="s">"ECO_FLOOR_GRID"</span>, <span class="k">"params"</span>: { … } } <span class="c">// CONSERVÉ + le niveau reflété</span>
}
<span class="c">// et, une fois par cycle, à côté de "budget" :</span>
<span class="k">"draw"</span>: { <span class="k">"authorisedW"</span>: <span class="n">4000</span>, <span class="k">"committedW"</span>: <span class="n">4000</span>, <span class="k">"remainingW"</span>: <span class="n">0</span>,
<span class="k">"binding"</span>: <span class="s">"gridOperator"</span>, <span class="k">"perPhaseBound"</span>: false }
<span class="c">// ⚠ committedW part SEUL, et AVANT counts (2026-08-30) : c'est le registre des watts</span>
<span class="c">// achetés, et il n'en existe aucun aujourd'hui. draw: {"committedW": 1400} tout court</span>
<span class="c">// est la bonne première livraison — les quatre autres champs ABSENTS, jamais à 0 :</span>
<span class="c">// un authorisedW forgé afficherait un plafond de soutirage qui n'existe pas.</span></pre>
<h3>Huit exigences, et chacune tient un endroit précis de la maquette</h3>
<div class="req"><b><u>R1</u>`levels[]` ne contient que les passes réellement parcourues</b>
<p>Une charge sans obligation éco porte <b>une seule entrée</b>, <code>comfort</code> — pas une entrée
<code>eco</code> à zéro. L'absence est alors l'information (« ne participe qu'à la passe 2 », LM-1007), et un
zéro forgé la détruirait. <code>levels[]</code> entièrement absent = box antérieure : l'app retombe sur
<code>allocatedW</code>/<code>decision</code> et n'affiche ni bandeau ◆ ni écran 2.</p></div>
<div class="req hard"><b><u>R2</u>`funding` descend au niveau — <span style="color:var(--green)">✔ EN COURS côté moteur</span></b>
<p>Aujourd'hui il est <em>par charge</em>. Au §10, la même charge peut être financée <b>au réseau pour son éco
et au surplus pour son confort dans le même cycle</b> : un <code>funding</code> par charge devient
indécidable. Or c'est lui qui porte notre identité de réconciliation (« somme des allocations financées au
surplus == <code>budget.allocatedW</code> »). S'il reste à la charge, cette identité cesse d'être vérifiable
sans que rien ne le signale.</p>
<p><b>Si vous hiérarchisez, prenez celle-ci en premier</b> : c'est la seule des huit qui soit un
<b>changement de forme d'un champ existant</b> et non un ajout. Chaque écran qui s'appuiera d'ici là sur
un <code>funding</code> par charge en augmentera le coût de déplacement — les sept autres attendent sans
se renchérir.</p>
<p><span style="color:var(--green)"><b>Parti le 2026-08-29</b></span> — l'argument de coût a emporté la
priorisation. C'est la première des huit à être écrite.</p></div>
<div class="req"><b><u>R3</u>Chaque watt tombe dans un compteur nommé — <span style="color:var(--green)">✔ résolu par `counts`</span></b>
<p><b>La forme du moteur est prise telle quelle</b> (2026-08-30) : à côté de <code>funding</code>, un
<code>counts</code> qui nomme les registres, avec <code>Σ counts == targetW</code> <b>toujours</b>, publié
même à une seule destination. L'identité devient calculable <b>sans exception</b> et <b>par niveau</b> :
l'exception <code>EV_GRID_START</code>, qui partageait une allocation entre deux compteurs, disparaît —
remplacée par une règle vérifiable qui vaudra telle quelle quand <code>draw.committedW</code> arrivera.</p>
<p><b>Les deux champs ne se dérivent pas l'un de l'autre</b>, et c'est ce qui empêchera qu'on en supprime un :
<code>funding</code> dit <b>l'origine</b> (« ces watts sont achetés » — voisin du motif), <code>counts</code>
dit <b>le registre</b> (« ils sont comptés ici » — une comptabilité).</p>
<p><b>Sur un niveau mixte, un code unique : <code>"grid"</code></b> — et la règle demandée n'est pas « une
approximation » mais <b>« grid l'emporte dès qu'un watt est acheté »</b>. Déterministe, indépendante des
proportions (un libellé qui bascule sur 900/480 serait pire que faux), et alignée sur ce qu'un humain doit
savoir : l'achat est le fait actionnable. Elle vaudra sans nouvelle décision pour le plancher éco du §10.
<b>Ni omission</b> (notre <code>isSurplusFunded</code> range alors le niveau au soleil, en silence)
<b>ni <code>"mixed"</code> publié</b> : une troisième valeur tomberait dans la branche par défaut de tout
client — « surplus » — donc reproduirait la panne de l'omission, et elle serait <b>dérivable de
<code>counts</code></b>, donc supprimée un jour à juste titre.</p>
<p><b>Et il a un préalable, trouvé en le câblant (2026-08-30) : <code>draw.committedW</code>.</b>
<code>budget.evReservedW</code> n'est pas un registre d'allocation mais un <b>terme de correction</b> du
budget — une borne servie au réseau qui tire déjà ce qu'on lui a commandé y contribue <b>0</b> pour un
<code>targetW</code> de 7 000 W. <code>Σ counts == targetW</code> y serait vraie au premier cycle après un
changement et fausse ensuite : une identité <b>dépendante du temps</b>, qui passe tous les tests et
certifie un total faux. <b>Les watts achetés n'ont aucun registre aujourd'hui</b> — d'où une seule identité
vérifiable, du côté surplus, et rien de l'autre côté tant que <code>committedW</code> n'est pas publié.</p>
<p><b>Ce que l'app promet en échange</b> : les clés de <code>counts</code> sont traitées comme des
<b>identifiants opaques</b>, jamais comme des chemins. Une clé inconnue d'une version ancienne n'est pas
perdue : l'identité <code>Σ counts == targetW</code> reste vérifiable sans connaître le sens des clés, et
l'écran dit « destination inconnue de cette version », <b>jamais zéro</b>. Corollaire pour le moteur : ne
jamais réutiliser une clé existante pour un sens nouveau — c'est le seul cas qu'aucun client ne peut
détecter.</p></div>
<div class="req"><b><u>R4</u>`Σ targetW == allocatedW` — une identité, pas un écart <span style="color:var(--green)">✔ tranché</span></b>
<p><b>Corrigé le 2026-08-29 : notre prémisse était fausse.</b> <code>allocatedW</code> publie le
<b>décidé</b>, pas le commandé — <code>buildTelemetry()</code> publie le plan, <code>applyActionsToAdapters()</code>
reçoit le slot en <code>const</code>, et un test attend <code>allocatedW == 3000</code> sur une borne dont
le courant commandé donne 2 990 W. Les niveaux et <code>allocatedW</code> sont donc <b>la même monnaie</b>,
et leur somme est une <b>identité exacte</b> — c'est ce qui rend <code>levels[]</code> vérifiable contre un
champ qui existe déjà. L'app traitera toute violation comme une incohérence de la box, pas comme un
arrondi.</p>
<p><b>Ce qui reste demandé</b> : ne pas attribuer l'appliqué à un niveau. L'écart vit entre
<code>allocatedW</code> et la charge utile de mécanisme (<code>stageW</code>, <code>setpointW</code>,
<code>currentA</code>) ; l'adaptateur arrondit une consigne unique et ne sait pas de quel besoin elle
venait. Toute répartition serait une fiction — le principe n'a pas bougé, seulement l'endroit où l'écart
se trouve.</p>
<p><b>Et les compteurs de budget comptent le décidé</b>, parce que c'est la valeur qui détermine ce que
reçoit la charge suivante. Conséquence d'écran, assumée : le budget d'un cycle peut annoncer 1 400 W
pendant que l'installation en tire 1 000 — d'où le bloc « trois nombres » de l'écran 3, qui empêche de
lire cet écart comme une erreur de comptage.</p></div>
<div class="req"><b><u>R5</u>Le régime est porté par le NIVEAU — <span style="color:var(--green)">✔ en ligne</span></b>
<p><b>Vérifié à la sonde le 2026-09-01</b> : <code>progress</code> et <code>decision</code> descendent au
niveau, et <code>deliveredWh</code> est bien <b>absent</b> sous <code>unmeasurable</code>.</p>
<p><b>Et le motif publie <code>remainingWh</code>, pas <code>deliveredWh</code> — accepté.</b> Ce n'est pas
« une source par fait » qui tranche (on aurait pu retirer <code>deliveredWh</code> de <code>progress</code>)
mais ceci : <b><code>remainingWh</code> n'est pas dérivable sous <code>unmeasurable</code></b>, et c'est
exactement le régime où l'on achète. Un champ qui n'existe que là où on peut le calculer ferait taire
l'écran dans le cas qui coûte de l'argent. Sous <code>unmeasurable</code>,
<code>remainingWh == targetWh</code> : « 4 000 sur 4 000 » se lit <b>« rien de mesurable »</b>, jamais
« rien livré ».</p>
<p><code>progress.regime ∈ measured | estimated | unmeasurable</code>, et <code>deliveredWh</code>
<b>absent</b> sous <code>unmeasurable</code> — jamais 0. C'est la même règle que
<code>measurement.source = "none"</code>, qui nous a déjà servis : un régime publié sépare « pas mesurable »
de « rien reçu », et c'est lui qui autorise la barre hachurée plutôt qu'une barre vide.</p></div>
<div class="req hard"><b><u>R6</u>`ECO_FLOOR_MET` interdit sous le régime `unmeasurable`</b>
<p>Une obligation non mesurable peut être <b>servie</b>, jamais annoncée <b>tenue</b> (LM-1009 C1). C'est une
garde testable côté moteur — et côté app, la combinaison sera traitée comme une <b>incohérence de la box</b>,
affichée comme telle, pas rendue. Le cas est réel dès aujourd'hui : la V2C Trydan ne publie pas
<code>sessionEnergy</code>.</p></div>
<div class="req"><b><u>R7</u>`decision` legacy conservé, avec le niveau qu'il reflète</b>
<p>Ajoutez <code>decision.level</code>. Sans lui, chaque client réinvente sa propre règle de fusion (« le
motif du dernier niveau servi ? du premier refusé ? ») et deux écrans affichent deux motifs différents pour le
même cycle. Une clé de plus, et la règle cesse d'être devinée.</p></div>
<div class="req"><b><u>R8</u>`DRAW_CAP` publie sa source, et l'app en fait trois phrases</b>
<p>Déjà prévu au <code>DESIGN_10.md</code> §6, on le confirme comme <b>condition d'écran</b> : c'est la source
qui décide du texte, et les trois textes appellent trois gestes différents. Le point de rupture de l'écran 2
est lu sur ce motif, jamais déduit d'un solde tombé à zéro.</p></div>
<h3>Ce dont nous n'avons PAS besoin — et qu'il ne faut pas fabriquer</h3>
<ul class="no">
<li>Un <b>index d'ordre de service</b> (<code>servedIndex</code>, <code>order</code>…) : la séquence de
l'écran 2 se dérive entièrement d'un tri lexicographique sur <code>(niveau, priority)</code>. Un index
publié serait une seconde source de vérité pour une donnée déductible — et diverger un jour.</li>
<li>Un <b>second rang</b>, sous quelque nom que ce soit. La maquette est construite pour rendre le rang unique
lisible <em>malgré</em> les deux passes ; elle n'a de sens que si LM-1002 tient.</li>
<li>Une <b>phrase</b> composée par la box : les motifs restent des codes à catalogue fermé avec paramètres
numériques, le rendu reste dans <code>services/telemetry_text.dart</code>.</li>
<li>Un <b>pourcentage</b> d'avancement, même « à titre indicatif ». D1 est appliqué sans exception.</li>
</ul>
<h3>La précédence des motifs — <b>tranchée</b> (Patrick, 2026-08-29)</h3>
<p>Autorisation résiduelle <b>non nulle mais inférieure au plus petit palier</b> du mécanisme :
<code>BELOW_MIN_POWER</code>. Les deux motifs sont vrais, mais pas au même niveau —
<code>DRAW_CAP</code> dit <em>« il n'y a plus d'autorisation »</em>, or ici il en reste, simplement
<b>pas au bon format</b>. C'est exactement la distinction posée en <code>+etm19</code> entre
<code>SURPLUS_INSUFFICIENT</code> (« il n'y a rien ») et <code>BELOW_MIN_POWER</code> (« il y en a,
mais pas au bon format »), transposée telle quelle au soutirage. Le geste utile est de lire la fiche
technique du mécanisme, pas de revoir le plafond.</p>
<p><code>DRAW_CAP</code> reste réservé à <b>l'autorisation résiduelle nulle</b>. Le point de rupture de
l'écran 2 (« autorisation épuisée ici ») est donc bien un <code>DRAW_CAP</code> ; une charge refusée
<em>après</em> lui pour cause de palier porterait <code>BELOW_MIN_POWER</code> et ne déplacerait pas le
point de rupture. La règle est écrite par le moteur, pas établie de chaque côté.</p>
</div>
</body>
</html>