etm-powersync-app/docs/mockups/config_ecs_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

250 lines
15 KiB
HTML
Raw Permalink 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 — Configurer charge pilotée (ECS)</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;margin:0 0 6px}
.head p{margin:0;color:var(--muted);font-size:13.5px;line-height:1.55;max-width:700px}
.head code{color:#9fd4ee}
.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}
.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:812px}
.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}
.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}
/* segmented Dynamique/Fixe */
.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:13px;padding:10px;border-radius:8px;cursor:pointer;transition:.15s}
.seg button.on{background:var(--blue);color:#04222f}
.seg .hint{}
.mode-hint{margin-top:9px;font-size:11.5px;color:var(--muted2);line-height:1.5}
.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}
/* paliers chips (le champ réellement sauvé) */
.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}
.chips{display:flex;flex-wrap:wrap;gap:8px;align-items:center}
.pchip{display:flex;align-items:center;gap:8px;background:var(--card2);border:1px solid var(--line2);
border-radius:9px;padding:9px 11px;font-family:'Poppins';font-weight:600;font-size:13.5px}
.pchip i{font-style:normal;color:var(--muted2);font-weight:500;font-size:11px}
.pchip.off{opacity:.6}
.pchip .x{color:var(--muted2);cursor:pointer;font-size:13px;margin-left:2px}
.add{width:36px;height:36px;border-radius:9px;border:1px dashed var(--line2);color:var(--muted);
display:grid;place-items:center;font-size:19px;background:transparent;cursor:pointer}
/* assistant */
.assist-btn{margin-top:11px;display:flex;align-items:center;gap:8px;width:100%;justify-content:center;
background:transparent;border:1px dashed rgba(49,163,221,.4);color:var(--blue);font-family:'Poppins';
font-weight:600;font-size:12.5px;padding:10px;border-radius:10px;cursor:pointer}
.assist{margin-top:10px;border:1px solid rgba(49,163,221,.28);background:rgba(49,163,221,.06);
border-radius:12px;padding:13px;display:none}
.assist.open{display:block}
.assist h4{font-family:'Poppins';font-size:12px;margin:0 0 3px;color:var(--blue)}
.assist p{margin:0 0 11px;font-size:11px;color:var(--muted2);line-height:1.45}
.res-row{display:flex;gap:8px;margin-bottom:9px}
.res-row input{flex:1;background:var(--panel);border:1px solid var(--line2);border-radius:9px;color:var(--txt);
font-family:'Poppins';font-weight:600;font-size:13px;padding:9px 11px;width:100%;text-align:center}
.res-row input::placeholder{color:var(--muted2);font-weight:500}
.preview{font-size:11.5px;color:var(--muted);margin:4px 0 12px;line-height:1.5}
.preview b{color:var(--green);font-family:'Poppins'}
.assist .apply{width:100%;background:var(--blue);color:#04222f;border:0;border-radius:9px;padding:10px;
font-family:'Poppins';font-weight:600;font-size:12.5px;cursor:pointer}
.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:13px}.stepper .lab small{display:block;color:var(--muted2);font-size:11px;margin-top:1px}
.stepper .ctl{display:flex;align-items:center;gap:13px}
.stepper .ctl b{font-family:'Poppins';font-weight:600;font-size:14px;min-width:64px;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;cursor:pointer}
.prio{width:24px;height:24px;border-radius:7px;background:var(--blue);color:#04222f;font-family:'Poppins';
font-weight:700;font-size:12px;display:grid;place-items:center}
.sw{width:40px;height:23px;border-radius:20px;background:rgba(53,201,138,.85);position:relative;cursor:pointer}
.sw::after{content:"";position:absolute;top:2px;left:19px;width:19px;height:19px;border-radius:50%;background:#fff}
.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.test{flex:1.3;background:transparent;border:1px solid var(--line2);color:var(--txt)}
.note{max-width:392px;font-size:11.5px;color:var(--muted2);line-height:1.55;text-align:left}
.note b{color:var(--yellow)}
</style>
</head>
<body>
<div class="head">
<h1>Configurer une charge pilotée — ECS</h1>
<p>Mode <b>Dynamique</b> (consigne continue) ou <b>Fixe</b> (paliers). Le champ sauvé est <b>toujours la liste de paliers en W</b> — jamais les résistances. L'assistant n'est qu'un <i>générateur de saisie côté UI</i> : il additionne les résistances et pré-remplit les paliers, puis disparaît. L'energymanager ne reçoit que <code>powerLevels: [0,600,1200,1800,2400]</code> et envoie un setpoint W ; le thing ECS décide la combinaison.</p>
</div>
<div class="stage">
<!-- MODE FIXE -->
<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">Relay-ECS</div><div class="s">multipallier · 4 sorties</div></div>
<div class="typeflag">etmvariableload</div></div>
</div>
<div class="field">
<label>Mode de pilotage</label>
<div class="seg">
<button>Dynamique</button>
<button class="on">Fixe (paliers)</button>
</div>
<div class="mode-hint">Fixe : l'energymanager arrondit le surplus au palier atteignable ≤ surplus, puis l'envoie. Le thing choisit quelles résistances composent ce palier.</div>
</div>
<div class="field">
<label>Paliers de puissance <span class="saved-flag">enregistré · powerLevels</span><br><small>valeurs envoyées à l'energymanager</small></label>
<div class="chips" id="levels">
<div class="pchip off">0 W <i>off</i></div>
<div class="pchip">600 W<span class="x">×</span></div>
<div class="pchip">1200 W<span class="x">×</span></div>
<div class="pchip">1800 W<span class="x">×</span></div>
<div class="pchip">2400 W<span class="x">×</span></div>
<button class="add" title="Ajouter un palier">+</button>
</div>
<button class="assist-btn" onclick="document.getElementById('asst').classList.toggle('open')">⚡ Calculer depuis mes résistances</button>
<div class="assist" id="asst">
<h4>Assistant résistances</h4>
<p>Entrez la puissance de chaque résistance. L'UI calcule les combinaisons et remplit les paliers ci-dessus. Rien de tout ça n'est transmis — seuls les paliers le sont.</p>
<div class="res-row">
<input value="600"><input value="600"><input value="600"><input value="600">
</div>
<div class="preview">4 résistances → paliers atteignables : <b>0 · 600 · 1200 · 1800 · 2400 W</b></div>
<button class="apply" onclick="document.getElementById('asst').classList.remove('open')">Remplir les paliers</button>
</div>
</div>
<div class="field">
<label>Stratégie</label>
<div class="stepper"><div class="lab">Rang de service<small>défini au glisser-déposer</small></div><div class="ctl"><div class="prio">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>Besoins · needs</label>
<div class="stepper"><div class="lab">Eau chaude prête à<small>dailyDeadline</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">Mode Fixe · paliers + assistant</div>
</div>
<!-- MODE DYNAMIQUE -->
<div class="col">
<div class="phone">
<div class="appbar"><div class="ic"></div><h2>Configurer · Routeur solaire</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">Routeur PV</div><div class="s">triac · modulation continue</div></div>
<div class="typeflag">etmvariableload</div></div>
</div>
<div class="field">
<label>Mode de pilotage</label>
<div class="seg">
<button class="on">Dynamique</button>
<button>Fixe (paliers)</button>
</div>
<div class="mode-hint">Dynamique : l'energymanager envoie n'importe quelle valeur entre 0 et la puissance nominale. Pas de paliers.</div>
</div>
<div class="field">
<label>Puissance nominale max <span class="saved-flag">enregistré · maxPowerW</span></label>
<div class="stepper"><div class="lab">Résistance ECS<small>plafond de modulation</small></div><div class="ctl"><button class="pm"></button><b>3000 W</b><button class="pm">+</button></div></div>
</div>
<div class="field">
<label>Stratégie</label>
<div class="stepper"><div class="lab">Rang de service<small>défini au glisser-déposer</small></div><div class="ctl"><div class="prio">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>Besoins · needs</label>
<div class="stepper"><div class="lab">Eau chaude prête à<small>dailyDeadline</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">Mode Dynamique · puissance max seule</div>
</div>
</div>
<div style="display:flex;justify-content:center;margin-top:24px">
<div class="note">
<b>Frontière.</b> Aucune notion de résistance ne franchit l'app. En Fixe, le contrat est <code style="color:#9fd4ee">powerLevels:[…]</code> ; en Dynamique, <code style="color:#9fd4ee">maxPowerW</code>. Dans les deux cas, l'energymanager raisonne en W et le thing décide du matériel.
</div>
</div>
<script>
document.querySelectorAll('.seg button').forEach(b=>b.addEventListener('click',()=>{
b.parentNode.querySelectorAll('button').forEach(x=>x.classList.remove('on'));b.classList.add('on');
}));
document.querySelectorAll('.sw').forEach(s=>s.addEventListener('click',()=>{
s.style.background=s.style.background.includes('53,201')?'rgba(255,255,255,.16)':'rgba(53,201,138,.85)';
s.querySelector?s.firstChild:0;
}));
document.querySelectorAll('.pchip .x').forEach(x=>x.addEventListener('click',e=>e.target.closest('.pchip').remove()));
</script>
</body>
</html>