- 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
401 lines
18 KiB
HTML
401 lines
18 KiB
HTML
<!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 & 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 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 & 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>6–32 A</b></span>
|
||
<span class="chip"><b>3φ</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é & 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>
|