etm-powersync-app/docs/mockups/charges_pilotables_mockup.html
Patrick Schurig 054cbfab3c docs: maquettes de la refonte « Charges pilotables » + CLAUDE.md recalé sur la box
Trois maquettes, neuf écrans, après le passage sur appareil qui a montré que
/energy/setup décrit autre chose que l'installation.

- charges_pilotables_mockup.html : la liste à deux niveaux, avec le bandeau
  d'arbitrage hérité de /energy/loads (qui disparaît) et la télémétrie sur la carte
  de chaque charge.
- configurer_mecanismes_mockup.html : les six « Configurer » — routeur de relais,
  modulable, SG-Ready, borne, zones HVAC, sélecteur d'appareils.
- raccordement_reseau_mockup.html : compteur réseau et protection de surcharge.

Décisions de conception que les images ne portent pas seules : les paliers
n'existent que chez le routeur de relais et y sont DÉDUITS des combinaisons ; le
mode fixed d'etmvariableload (legacy) n'est plus proposé mais reste lisible, parce
qu'ouvrir un écran ne doit pas convertir une config déployée ; SG1/SG2 sont une
convention d'app retrouvée par l'encodage standard, la box ne stocke qu'une liste
de relais par état ; le compteur par charge est dessiné inactif avec sa raison ; la
borne règle un mode par DÉFAUT, pas une commande.

CLAUDE.md corrigé contre JSONRPC.Introspect (plugin 1.15.2+etm15) :

- Energy.SetChargingMode n'existe pas — le « bug critique » décrivait un appel
  impossible. Energy.* n'a que cinq méthodes.
- Le namespace EnergyPlugin.* n'existe pas : c'est NymeaEnergy.*, 20 méthodes et
  14 notifications, toutes listées.
- ChargingInfo : chargingMode / targetPercentage / endDateTime (Uint), et non
  mode / targetSoc / endTime. minCurrent n'existe nulle part.
- phasePowerLimit est en AMPÈRES par phase, et 0 coupe toute la recharge
  intelligente.
- AirConditioning expose 8 méthodes, pas 3.

Toutes les méthodes citées ont été revérifiées une par une : aucune introuvable.
Règle ajoutée : contrôler ce fichier contre Introspect avant chaque lot — trois
fois aujourd'hui il a envoyé un agent dans le vide.

Et docs/BRIEF_plugin_depuis_maquettes.md : les six points que les maquettes
remontent vers le moteur, dont un seul blocage dur (pas de champ de courant
minimum pour le mode Min+PV).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XPUo3RMr8SzK6qbFtfBm8H
2026-08-25 21:30:32 +02:00

318 lines
18 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 — Charges pilotables (liste à deux niveaux)</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;
--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:1180px;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:860px}
.head code{color:#9fd4ee}
.head b{color:var(--yellow)}
.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}
.cap{color:var(--muted2);font-size:12px;font-family:'Poppins';font-weight:500;letter-spacing:.4px;text-transform:uppercase}
.phone{width:392px;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:860px}
.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:15.5px;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 (hérite de l'écran supprimé) ─ */
.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 .grid{display:grid;grid-template-columns:repeat(5,1fr);gap:6px}
.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}
/* ─ domaine ─ */
.dom{display:flex;align-items:center;gap:8px;margin:16px 0 8px;padding:0 2px}
.dom .grip{color:var(--muted2);font-size:15px;cursor:grab}
.dom h3{font-family:'Poppins';font-weight:600;font-size:13px;margin:0;flex:1}
.dom .arw{width:26px;height:26px;border-radius:8px;border:1px solid var(--line2);color:var(--muted);
display:grid;place-items:center;font-size:13px;background:transparent;cursor:pointer}
.dom.unclassed h3{color:var(--muted2)}
/* ─ carte de charge ─ */
.load{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:12px;margin-bottom:9px}
.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}
.tel{margin-top:10px;padding-top:9px;border-top:1px dashed var(--line);font-size:11.5px;line-height:1.5}
.tel .a{font-family:'Poppins';font-weight:600;color:var(--green);font-size:12px}
.tel .m{color:var(--muted)}
.tel .why{color:var(--muted2);margin-top:2px}
.tel .lock{color:var(--yellow);margin-top:2px}
.tel.stale .a{color:var(--muted2)}
.tel.stale{opacity:.75}
.fault{margin-top:9px;background:rgba(224,122,122,.1);border:1px solid rgba(224,122,122,.3);
border-radius:10px;padding:9px 10px;font-size:11px;line-height:1.45}
.fault b{color:var(--red);font-family:'Poppins';font-size:11.5px;display:block;margin-bottom:2px}
.fault button{margin-top:7px;background:transparent;border:1px solid rgba(224,122,122,.45);color:var(--red);
font-family:'Poppins';font-weight:600;font-size:11px;padding:6px 11px;border-radius:8px;cursor:pointer}
.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}
.noload{background:var(--card2);border:1px dashed var(--line2);border-radius:12px;padding:11px 12px;
display:flex;align-items:center;gap:10px}
.noload .nm b{font-family:'Poppins';font-weight:600;font-size:13px;display:block;color:var(--muted)}
.noload .nm span{font-size:11px;color:var(--muted2)}
.noload button{margin-left:auto;background:var(--blue);color:#04222f;border:0;border-radius:9px;
font-family:'Poppins';font-weight:600;font-size:11.5px;padding:8px 13px;cursor:pointer}
/* ─ formulaire ─ */
.field{margin-top:18px}
.field:first-child{margin-top:4px}
.field>label{display:block;font-family:'Poppins';font-weight:600;font-size:12px;color:var(--muted);
letter-spacing:.3px;margin-bottom:9px}
.field>label small{color:var(--muted2);font-weight:500;letter-spacing:0}
.saved-flag{font-size:10.5px;color:var(--green);background:rgba(53,201,138,.12);border:1px solid rgba(53,201,138,.3);
border-radius:20px;padding:2px 8px;font-weight:600;float:right;margin-top:-2px}
.derived-flag{font-size:10.5px;color:var(--muted2);background:rgba(255,255,255,.05);border:1px solid var(--line2);
border-radius:20px;padding:2px 8px;font-weight:600;float:right;margin-top:-2px}
.todo-flag{font-size:10.5px;color:var(--yellow);background:rgba(254,193,19,.1);border:1px solid rgba(254,193,19,.32);
border-radius:20px;padding:2px 8px;font-weight:600;float:right;margin-top:-2px}
.inp{width:100%;background:var(--card2);border:1px solid var(--line2);border-radius:10px;color:var(--txt);
font-family:'Poppins';font-weight:600;font-size:13.5px;padding:11px 12px}
.sel{width:100%;background:var(--card2);border:1px solid var(--line2);border-radius:10px;color:var(--txt);
font-family:'Inter';font-size:13px;padding:11px 12px;display:flex;align-items:center;gap:8px}
.sel i{margin-left:auto;font-style:normal;color:var(--muted2);font-size:11px}
.seg{display:flex;background:var(--card2);border:1px solid var(--line);border-radius:11px;padding:4px;gap:4px}
.seg button{flex:1;border:0;background:transparent;color:var(--muted);font-family:'Poppins';font-weight:600;
font-size:11.5px;padding:9px 4px;border-radius:8px;cursor:pointer}
.seg button.on{background:var(--blue);color:#04222f}
.mode-hint{margin-top:9px;font-size:11.5px;color:var(--muted2);line-height:1.5}
.relay{display:flex;align-items:center;gap:8px;margin-bottom:8px}
.relay .k{width:32px;font-family:'Poppins';font-weight:700;font-size:12px;color:var(--muted2);flex:none}
.relay .thing{flex:1;background:var(--card2);border:1px solid var(--line2);border-radius:10px;
font-size:12.5px;padding:10px 11px;display:flex;align-items:center;gap:6px}
.relay .thing i{margin-left:auto;font-style:normal;color:var(--muted2);font-size:10px}
.relay .w{width:92px;flex:none;background:var(--card2);border:1px solid var(--line2);border-radius:10px;
color:var(--txt);font-family:'Poppins';font-weight:600;font-size:13px;padding:10px 8px;text-align:center}
.relay .del{color:var(--muted2);font-size:14px;cursor:pointer;flex:none;width:16px;text-align:center}
.addrow{width:100%;background:transparent;border:1px dashed var(--line2);color:var(--muted);
font-family:'Poppins';font-weight:600;font-size:12px;padding:10px;border-radius:10px;cursor:pointer}
.preview{margin-top:11px;border:1px solid rgba(53,201,138,.22);background:rgba(53,201,138,.06);
border-radius:11px;padding:11px 12px;font-size:11.5px;color:var(--muted);line-height:1.55}
.preview b{color:var(--green);font-family:'Poppins'}
.preview em{display:block;font-style:normal;color:var(--muted2);font-size:10.5px;margin-top:6px}
.stepper{display:flex;align-items:center;justify-content:space-between;padding:11px 12px;border:1px solid var(--line);
border-radius:11px;background:var(--card2);margin-bottom:8px}
.stepper .lab{font-size:12.5px}.stepper .lab small{display:block;color:var(--muted2);font-size:10.5px;margin-top:1px}
.stepper .ctl{display:flex;align-items:center;gap:13px}
.stepper .ctl b{font-family:'Poppins';font-weight:600;font-size:13.5px;min-width:56px;text-align:right}
.pm{width:26px;height:26px;border-radius:8px;border:1px solid var(--line2);color:var(--blue);background:transparent;
font-size:16px;display:grid;place-items:center;cursor:pointer}
.blocked{border:1px dashed rgba(254,193,19,.35);background:rgba(254,193,19,.05);border-radius:12px;padding:12px}
.blocked .row{display:flex;align-items:center;gap:10px;opacity:.55}
.blocked .row .lab{font-size:12.5px;flex:1}
.blocked p{margin:9px 0 0;font-size:10.8px;color:var(--muted2);line-height:1.5}
.blocked p b{color:var(--yellow)}
.danger{margin-top:9px;font-size:10.8px;color:var(--muted2);line-height:1.5;
border-left:2px solid rgba(224,122,122,.5);padding-left:9px}
.danger b{color:var(--red)}
.save{display:flex;gap:10px;margin-top:24px}
.btn{text-align:center;font-family:'Poppins';font-weight:600;font-size:13px;padding:12px;border-radius:11px;cursor:pointer}
.btn.go{background:var(--blue);color:#04222f;flex:2;border:0}
.btn.alt{flex:1.3;background:transparent;border:1px solid var(--line2);color:var(--txt)}
.note{width:392px;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}
</style>
</head>
<body>
<div class="head">
<h1>Charges pilotables — l'ordre, la télémétrie et le mécanisme au même endroit</h1>
<p>Décision du 2026-08-25 : <b>l'écran « Ordre de service des charges » disparaît</b>. Le rang se règle
ici au glisser-déposer, le regroupement <b>à deux niveaux</b> est conservé (domaines ordonnés entre eux,
charges ordonnées dedans), et le bloc de télémétrie descend sur la carte de chaque charge.</p>
<p>Les six écrans « Configurer » qu'ouvre le bouton de chaque carte sont dans
<code>configurer_mecanismes_mockup.html</code>.</p>
<p><b>Le changement de fond n'est pas visuel :</b> ces cartes doivent <b>être</b> les charges lues par
<code>NymeaEnergy.GetLoadConfig</code>, pas des <code>LoadDescriptor</code> reconstruits depuis des
<code>EmsRole</code>. C'est la cause racine du défaut constaté sur appareil — l'écran actuel affichait
« 6/6 rôles configurés · tout est assigné » en pointant sur des appareils absents de la box, pendant que
celle-ci arbitrait deux charges qui n'apparaissaient nulle part.</p>
</div>
<div class="stage">
<!-- ══════════ ÉCRAN 1 ══════════ -->
<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>· dernier cycle il y a 41 s</small></div>
<div class="arb">
<div class="top"><span class="dot"></span><b>Budget de surplus du cycle</b><i>2 571 W entrants</i></div>
<div class="grid">
<div class="cell"><u>Surplus</u><b>2 571</b></div>
<div class="cell"><u>Réservé VE</u><b>0</b></div>
<div class="cell"><u>Recrédité</u><b>500</b></div>
<div class="cell"><u>Alloué</u><b>3 000</b></div>
<div class="cell"><u>Reliquat</u><b>71</b></div>
</div>
</div>
<div class="sect">Charges pilotables <small>· glissez les domaines, puis les charges</small></div>
<!-- domaine 1 -->
<div class="dom">
<span class="grip">⠿</span>
<h3>Eau chaude sanitaire</h3>
<button class="arw">˄</button><button class="arw">˅</button>
</div>
<div class="load">
<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 · min ON 60 s</span></div>
<span class="sw"></span>
</div>
<div class="tel">
<span class="a">Alloué 3 000 W</span> <span class="m">· palier 3 000 W sur 3 500 W</span>
<div class="why">Consigne de 3 000 W servie par le surplus, arrondie sur un palier déclaré
(3 071 W de budget).</div>
<div class="lock">🔒 Maintien ON — 60 s restantes <span style="color:var(--muted2)">(au dernier cycle)</span></div>
</div>
<button class="cfg">⚙ Configurer</button>
</div>
<!-- domaine 2 -->
<div class="dom">
<span class="grip">⠿</span>
<h3>Chauffage</h3>
<button class="arw">˄</button><button class="arw">˅</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 · maintien d'état 300 s</span></div>
<span class="sw"></span>
</div>
<div class="tel">
<span class="a">Alloué 3 000 W</span> <span class="m">· état SG-Ready 4 — estimation 3 000 W</span>
<div class="why">Pompe à chaleur en état 4 (forcé) — estimation 3 000 W sur 3 477 W de budget.</div>
</div>
<button class="cfg">⚙ Configurer</button>
</div>
<!-- domaine 3 : non classé + défaut -->
<div class="dom unclassed">
<span class="grip">⠿</span>
<h3>Non classé</h3>
<button class="arw">˄</button><button class="arw">˅</button>
</div>
<div class="load">
<div class="r1">
<span class="grip">⠿</span><span class="rank">3</span>
<div class="nm"><b>Sèche-serviettes</b><span>Routeur de relais · 1 relais</span></div>
<span class="sw off"></span>
</div>
<div class="tel stale">
<span class="a">Non arbitrée</span> <span class="m">· absente du dernier cycle publié</span>
<div class="why">Charge désactivée : son rôle reste décrit, elle ne consomme aucun budget.</div>
</div>
<button class="cfg">⚙ Configurer</button>
</div>
<div class="load" style="border-color:rgba(224,122,122,.35)">
<div class="r1">
<span class="grip">⠿</span><span class="rank">4</span>
<div class="nm"><b>Plancher chauffant</b><span>Charge modulable</span></div>
<span class="sw"></span>
</div>
<div class="fault">
<b>⚠ En défaut</b>
Échec d'écriture : l'échelle de commande est épuisée, plus aucune commande n'est émise tant que
le défaut n'est pas levé.
<button>🔓 Lever le défaut</button>
</div>
<button class="cfg">⚙ Configurer</button>
</div>
<div class="sect">Sans appareil</div>
<div class="noload">
<div class="nm"><b>Borne de recharge</b><span>Aucune charge déclarée sur la box</span></div>
<button>+ Déclarer</button>
</div>
</div>
</div>
<div class="cap">Charges pilotables — deux niveaux + télémétrie</div>
<div class="note">
<b>Ce qui part sur la box :</b> le glisser-déposer produit une suite d'entiers <code>priority</code> à plat —
la box ne sait rien des domaines, le rang de domaine se déduit de l'ordre d'apparition. Le toggle écrit
<code>enabled</code>.
<ul>
<li><b>« Non arbitrée »</b> n'est pas une erreur : une charge de <code>GetLoadConfig</code> absente de
<code>loads[]</code> se lit <b>désactivée</b>, jamais perdue. L'inclusion ne vaut que dans un sens.</li>
<li><b>« Lever le défaut »</b> n'apparaît que si <code>available: false</code>, et son résultat n'est
jamais annoncé sur le retour du RPC : mesuré sur le banc, <code>ClearLoadFault</code> répond
<code>EnergyErrorNoError</code>, lève réellement le verrou… et le cycle suivant le repose si la cause
demeure. Le verdict vient de la télémétrie relue, deux cycles plus tard.</li>
<li><b>« Déclarer »</b> mène au même formulaire que « Configurer », vide.</li>
</ul>
</div>
</div>
</div>
</body>
</html>