etm-powersync-app/docs/mockups/roles_appareils_mockup.html
Patrick Schurig 0ebbde49da chore(docs): range specs/contrats/refs dans docs/, track l'autorité
- specs -> docs/ (DASHBOARD_SPEC compagnon du contrat)
- ref introspection nymea-jsonRPC -> docs/reference/
- mockups HTML -> docs/mockups/ ; briefs consommés -> docs/archive/
- track AGENTS.md + INTERFACE_etmvariableload.md (étaient untracked)
- INTERFACE_*: marqué miroir, canonique = repo plugin
2026-06-28 12:49:57 +02:00

401 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 — Rôles & appareils · maquette</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:880px;margin:0 auto 26px}
.head h1{font-family:'Poppins';font-weight:700;font-size:20px;letter-spacing:.2px;margin:0 0 6px}
.head p{margin:0;color:var(--muted);font-size:13.5px;line-height:1.5;max-width:680px}
.stage{display:flex;gap:30px;justify-content:center;flex-wrap:wrap;align-items:flex-start}
.col{display:flex;flex-direction:column;align-items:center;gap:12px}
.cap{color:var(--muted2);font-size:12px;font-family:'Poppins';font-weight:500;letter-spacing:.4px;text-transform:uppercase}
/* phone */
.phone{width:390px;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:780px}
.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:16px;margin:0;flex:1;text-align:center}
.scroll{overflow:auto;padding:14px 14px 24px}
.scroll::-webkit-scrollbar{width:0}
/* counter */
.counter{display:flex;align-items:center;gap:11px;background:rgba(49,163,221,.10);
border:1px solid rgba(49,163,221,.28);border-radius:12px;padding:11px 13px;margin-bottom:16px}
.counter .dot{width:30px;height:30px;border-radius:8px;background:rgba(49,163,221,.18);
display:grid;place-items:center;color:var(--blue);font-size:15px}
.counter b{font-family:'Poppins';font-weight:600;font-size:14px}
.counter span{color:var(--muted);font-size:12px;display:block;margin-top:1px}
.sec{display:flex;align-items:baseline;gap:8px;margin:18px 4px 9px}
.sec h3{font-family:'Poppins';font-weight:600;font-size:11.5px;letter-spacing:1.3px;
text-transform:uppercase;color:var(--blue);margin:0}
.sec small{color:var(--muted2);font-size:11px;margin-left:auto;text-align:right;max-width:185px;line-height:1.35}
.card{background:var(--card);border:1px solid var(--line);border-radius:var(--r);
padding:14px;margin-bottom:11px}
.row{display:flex;align-items:center;gap:11px}
.emoji{width:34px;height:34px;border-radius:9px;background:rgba(255,255,255,.05);
display:grid;place-items:center;font-size:18px;flex:none}
.name{font-family:'Poppins';font-weight:600;font-size:14.5px}
.sub{color:var(--muted);font-size:12.5px;margin-top:2px}
.tag{font-size:10.5px;font-weight:600;padding:3px 8px;border-radius:20px;white-space:nowrap}
.tag.root{background:rgba(254,193,19,.14);color:var(--yellow)}
.tag.auto{background:rgba(255,255,255,.06);color:var(--muted)}
.val{font-family:'Poppins';font-weight:600;font-size:15px;color:var(--green)}
.chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:11px}
.chip{font-size:11px;color:var(--muted);background:rgba(255,255,255,.04);
border:1px solid var(--line);border-radius:7px;padding:4px 8px}
.chip b{color:var(--txt);font-weight:600}
/* toggle */
.sw{width:40px;height:23px;border-radius:20px;background:rgba(53,201,138,.85);
position:relative;flex:none;cursor:pointer;transition:.15s}
.sw::after{content:"";position:absolute;top:2px;left:19px;width:19px;height:19px;
border-radius:50%;background:#fff;transition:.15s}
.sw.off{background:rgba(255,255,255,.16)}
.sw.off::after{left:2px}
/* draggable load card */
.load{background:var(--card);border:1px solid var(--line);border-radius:var(--r);
padding:13px 13px 13px 6px;margin-bottom:11px;display:flex;gap:8px;align-items:flex-start;
cursor:grab;transition:border-color .15s, transform .12s, box-shadow .15s}
.load:active{cursor:grabbing}
.load.drag{opacity:.4}
.load.over{border-color:var(--blue);box-shadow:0 0 0 1px var(--blue)}
.grip{display:flex;flex-direction:column;align-items:center;gap:7px;padding-top:2px;flex:none}
.grip .dots{color:var(--muted2);font-size:15px;line-height:.5;letter-spacing:0}
.prio{width:22px;height:22px;border-radius:7px;background:var(--blue);color:#04222f;
font-family:'Poppins';font-weight:700;font-size:12px;display:grid;place-items:center}
.load .body{flex:1;min-width:0}
.load .top{display:flex;align-items:center;gap:10px}
/* todo / tri-state */
.todo .sub{color:var(--muted2)}
.acts{display:flex;gap:8px;margin-top:12px}
.btn{flex:1;text-align:center;font-family:'Poppins';font-weight:600;font-size:12.5px;
padding:9px;border-radius:9px;border:1px solid transparent;cursor:pointer}
.btn.prim{background:rgba(49,163,221,.14);color:var(--blue);border-color:rgba(49,163,221,.3)}
.btn.ghost{background:transparent;color:var(--muted);border-color:var(--line2)}
.legend{margin-top:18px;padding:12px 13px;border-radius:12px;background:rgba(254,193,19,.06);
border:1px solid rgba(254,193,19,.18);color:#d8c79a;font-size:11.5px;line-height:1.55}
.legend b{color:var(--yellow)}
/* detail sheet */
.field{margin-top:16px}
.field>label{display:block;font-family:'Poppins';font-weight:600;font-size:12px;
color:var(--muted);letter-spacing:.3px;margin-bottom:8px}
.pick{display:flex;align-items:center;gap:10px;padding:11px;border-radius:11px;
border:1px solid var(--line);background:var(--card2);margin-bottom:8px}
.pick.sel{border-color:var(--blue);background:rgba(49,163,221,.08)}
.pick .radio{width:18px;height:18px;border-radius:50%;border:2px solid var(--muted2);flex:none}
.pick.sel .radio{border-color:var(--blue);background:radial-gradient(circle,var(--blue) 42%,transparent 46%)}
.pick .t{font-size:13.5px;font-weight:500}
.pick .s{font-size:11.5px;color:var(--muted2);margin-top:1px}
.typeflag{font-size:11px;color:var(--muted);background:rgba(255,255,255,.05);
border:1px solid var(--line);border-radius:7px;padding:3px 9px;display:inline-block}
.stages{display:flex;align-items:center;gap:7px;flex-wrap:wrap}
.stage-chip{display:flex;align-items:center;gap:6px;background:var(--card2);
border:1px solid var(--line2);border-radius:9px;padding:8px 11px;font-family:'Poppins';
font-weight:600;font-size:13px}
.stage-chip i{font-style:normal;color:var(--muted2);font-weight:500;font-size:11px}
.stage-chip.off{opacity:.6}
.add{width:34px;height:34px;border-radius:9px;border:1px dashed var(--line2);
color:var(--muted);display:grid;place-items:center;font-size:18px;background:transparent}
.stepper{display:flex;align-items:center;justify-content:space-between;padding:10px 12px;
border:1px solid var(--line);border-radius:11px;background:var(--card2);margin-bottom:8px}
.stepper .lab{font-size:13px}
.stepper .lab small{display:block;color:var(--muted2);font-size:11px;margin-top:1px}
.stepper .ctl{display:flex;align-items:center;gap:14px}
.stepper .ctl b{font-family:'Poppins';font-weight:600;font-size:14px;min-width:54px;text-align:right}
.pm{width:28px;height:28px;border-radius:8px;border:1px solid var(--line2);color:var(--blue);
background:transparent;font-size:17px;display:grid;place-items:center}
.save{display:flex;gap:10px;margin-top:22px}
.save .btn.go{background:var(--blue);color:#04222f;flex:2}
.save .btn.test{flex:1.4;background:transparent;border-color:var(--line2);color:var(--txt)}
@media(max-width:860px){ .stage{gap:40px} }
</style>
</head>
<body>
<div class="head">
<h1>Rôles &amp; appareils — maquette de validation</h1>
<p>Deux zones distinctes : les <b>compteurs</b> (mesure, jamais priorisés) et les <b>charges pilotables</b> (réordonnables par glisser-déposer → écrit <code style="color:#9fd4ee">LoadDescriptor.priority</code>). Tri-état présent / absent / non&nbsp;configuré. À droite, l'éditeur qu'ouvre « Configurer » : les champs <code style="color:#9fd4ee">declared / limits / needs</code> du protocole §5. <b style="color:#fec113">Essayez de glisser les cartes de la 2ᵉ zone.</b></p>
</div>
<div class="stage">
<!-- ░░░░░░░░░░ ÉCRAN 1 : LISTE ░░░░░░░░░░ -->
<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="counter">
<div class="dot"></div>
<div>
<b>3 / 6 rôles assignés</b>
<span>1 détecté automatiquement · 1 reste à configurer</span>
</div>
</div>
<!-- COMPTEURS -->
<div class="sec"><h3>Compteurs</h3><small>Mesure — hors priorité</small></div>
<div class="card">
<div class="row">
<div class="emoji"></div>
<div style="flex:1">
<div class="name">Compteur réseau</div>
<div class="sub">GRID · Eastron SDM630</div>
</div>
<div class="tag root">rootMeter</div>
</div>
<div class="chips">
<span class="chip">Souscrit <b>12 kVA</b></span>
<span class="chip">Limite <b>63 A</b>/phase</span>
</div>
</div>
<div class="card">
<div class="row">
<div class="emoji">☀️</div>
<div style="flex:1">
<div class="name">Production solaire</div>
<div class="sub">Fronius Primo GEN24 10.0</div>
</div>
<div style="text-align:right">
<div class="val">3.3 kW</div>
<div class="tag auto" style="margin-top:4px">auto · lecture seule</div>
</div>
</div>
</div>
<!-- CHARGES PILOTABLES -->
<div class="sec"><h3>Charges pilotables</h3><small>Glissez pour l'ordre de service du surplus</small></div>
<div id="loadlist">
<div class="load" draggable="true">
<div class="grip"><div class="dots"></div><div class="prio">1</div></div>
<div class="body">
<div class="top">
<div class="emoji">🔋</div>
<div style="flex:1">
<div class="name">Batterie</div>
<div class="sub">BYD HVS 10.2 · détecté</div>
</div>
<div class="sw"></div>
</div>
<div class="chips">
<span class="chip"><b>10.2 kWh</b></span>
<span class="chip">réserve <b>10 %</b></span>
<span class="chip">charge <b>5 kW</b></span>
</div>
</div>
</div>
<div class="load" draggable="true">
<div class="grip"><div class="dots"></div><div class="prio">2</div></div>
<div class="body">
<div class="top">
<div class="emoji">🌡️</div>
<div style="flex:1">
<div class="name">Chauffe-eau</div>
<div class="sub">Relay3 · C3 — relay-stages</div>
</div>
<div class="sw"></div>
</div>
<div class="chips">
<span class="chip">paliers <b>0 / 1200 / 2400 W</b></span>
</div>
</div>
</div>
<div class="load" draggable="true">
<div class="grip"><div class="dots"></div><div class="prio">3</div></div>
<div class="body">
<div class="top">
<div class="emoji">🚗</div>
<div style="flex:1">
<div class="name">EV Charger</div>
<div class="sub">Terra AC (TCP)</div>
</div>
<div class="sw"></div>
</div>
<div class="chips">
<span class="chip"><b>632 A</b></span>
<span class="chip"><b></b></span>
<span class="chip">cible <b>80 %</b> @ 18:00</span>
</div>
</div>
</div>
</div>
<!-- À CONFIGURER (tri-état) -->
<div class="sec"><h3>À configurer</h3></div>
<div class="card todo">
<div class="row">
<div class="emoji">♨️</div>
<div style="flex:1">
<div class="name">Pompe à chaleur</div>
<div class="sub">Aucun appareil configuré</div>
</div>
</div>
<div class="acts">
<div class="btn prim">+ Configurer</div>
<div class="btn ghost">Pas d'appareil</div>
</div>
</div>
<div class="legend">
<b>Ordre indicatif.</b> Il fixe le service par défaut du surplus. Le pilote peut le bousculer automatiquement les jours <b>Tempo rouge</b> ou à l'approche d'une <b>échéance</b> de recharge.
</div>
</div>
</div>
<div class="cap">Écran 1 · liste + priorité</div>
</div>
<!-- ░░░░░░░░░░ ÉCRAN 2 : DÉTAIL ░░░░░░░░░░ -->
<div class="col">
<div class="phone">
<div class="appbar">
<div class="ic"></div>
<h2>Configurer · Chauffe-eau</h2>
<div class="ic"></div>
</div>
<div class="scroll">
<div class="field">
<label>Appareil</label>
<div class="pick sel">
<div class="radio"></div>
<div style="flex:1"><div class="t">Relay3</div><div class="s">C3 · GPIO 5</div></div>
<div class="typeflag">relay-stages</div>
</div>
<div class="pick">
<div class="radio"></div>
<div style="flex:1"><div class="t">Relay4</div><div class="s">C4 · GPIO 6</div></div>
</div>
</div>
<div class="field">
<label>Paliers de puissance · declared.stages</label>
<div class="stages">
<div class="stage-chip off">0 W <i>off</i></div>
<div class="stage-chip">1200 W</div>
<div class="stage-chip">2400 W</div>
<button class="add">+</button>
</div>
</div>
<div class="field">
<label>Priorité &amp; activation</label>
<div class="stepper">
<div class="lab">Rang de service<small>défini par glisser-déposer</small></div>
<div class="ctl"><div class="prio" style="width:24px;height:24px">2</div></div>
</div>
<div class="stepper">
<div class="lab">Inclure dans l'arbitrage</div>
<div class="ctl"><div class="sw"></div></div>
</div>
</div>
<div class="field">
<label>Anti-rebond · limits</label>
<div class="stepper">
<div class="lab">Durée minimale ON<small>minOnS</small></div>
<div class="ctl"><button class="pm"></button><b>120 s</b><button class="pm">+</button></div>
</div>
<div class="stepper">
<div class="lab">Durée minimale OFF<small>minOffS</small></div>
<div class="ctl"><button class="pm"></button><b>180 s</b><button class="pm">+</button></div>
</div>
</div>
<div class="field">
<label>Besoins · needs</label>
<div class="stepper">
<div class="lab">Échéance quotidienne<small>dailyDeadline · eau chaude prête</small></div>
<div class="ctl"><b>06:00</b></div>
</div>
<div class="stepper">
<div class="lab">Énergie min. / jour<small>minEnergyWhPerDay</small></div>
<div class="ctl"><button class="pm"></button><b>4 kWh</b><button class="pm">+</button></div>
</div>
</div>
<div class="save">
<div class="btn test">Tester</div>
<div class="btn go">Enregistrer</div>
</div>
</div>
</div>
<div class="cap">Écran 2 · « Configurer » (declared / limits / needs)</div>
</div>
</div>
<script>
// toggles
document.querySelectorAll('.sw').forEach(s=>s.addEventListener('click',()=>s.classList.toggle('off')));
// drag-reorder priority list
const list=document.getElementById('loadlist');
let dragged=null;
list.querySelectorAll('.load').forEach(card=>{
card.addEventListener('dragstart',()=>{dragged=card;card.classList.add('drag');});
card.addEventListener('dragend',()=>{card.classList.remove('drag');renumber();});
card.addEventListener('dragover',e=>{
e.preventDefault();
const t=e.currentTarget;
if(t===dragged)return;
t.classList.add('over');
const r=t.getBoundingClientRect();
const after=(e.clientY-r.top)/r.height>0.5;
list.insertBefore(dragged, after?t.nextSibling:t);
});
card.addEventListener('dragleave',e=>e.currentTarget.classList.remove('over'));
card.addEventListener('drop',e=>e.currentTarget.classList.remove('over'));
});
function renumber(){
list.querySelectorAll('.over').forEach(c=>c.classList.remove('over'));
list.querySelectorAll('.load .prio').forEach((p,i)=>p.textContent=i+1);
}
</script>
</body>
</html>