Files
sillyhome-next-dev/app/static/index.html
Otto 5764b27bac
Some checks failed
quality / test (3.11) (push) Has been cancelled
quality / test (3.13) (push) Has been cancelled
MVP: add dashboard and Home Assistant add-on
2026-06-13 21:12:02 +02:00

150 lines
8.2 KiB
HTML

<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>SillyHome Next</title>
<style>
:root { color-scheme: dark; font-family: system-ui, sans-serif; background: #10151c; color: #eaf1f8; }
body { margin: 0; }
header { padding: 20px; background: linear-gradient(135deg,#142b3a,#193f36); }
h1,h2 { margin: 0 0 12px; }
header p { margin: 4px 0; color: #b9c9d6; }
main { display: grid; grid-template-columns: repeat(auto-fit,minmax(310px,1fr)); gap: 14px; padding: 14px; }
section { background: #18212b; border: 1px solid #2d3a47; border-radius: 12px; padding: 16px; }
.wide { grid-column: 1 / -1; }
.ok { color: #66dfa9; } .bad { color: #ff8f8f; }
label { display: block; margin: 9px 0 4px; color: #b9c9d6; }
input,select,textarea,button { box-sizing: border-box; width: 100%; border-radius: 7px; border: 1px solid #3b4b5b; padding: 9px; background: #101820; color: #fff; }
button { margin-top: 10px; background: #23715b; border: 0; font-weight: 700; cursor: pointer; }
button.secondary { background: #37495c; }
pre { white-space: pre-wrap; max-height: 310px; overflow: auto; background: #0d141b; padding: 10px; border-radius: 7px; }
table { width: 100%; border-collapse: collapse; font-size: .9rem; }
td,th { padding: 7px; border-bottom: 1px solid #2d3a47; text-align: left; }
.notice { border-left: 4px solid #e8b34b; padding-left: 10px; }
</style>
</head>
<body>
<header>
<h1>SillyHome Next</h1>
<p>Lokale Home-Assistant-Analyse, Vorhersagen und kontrollierte Automation-Entwürfe.</p>
<p class="notice">Sicherheitsmodus: Entwürfe werden niemals automatisch in Home Assistant ausgeführt.</p>
</header>
<main>
<section>
<h2>Systemstatus</h2>
<div id="status">Prüfung läuft ...</div>
<button class="secondary" onclick="loadStatus()">Neu laden</button>
</section>
<section>
<h2>Entity Discovery</h2>
<label for="domain">Domain (optional)</label>
<input id="domain" placeholder="sensor">
<button onclick="discover()">HA-Entities analysieren</button>
<pre id="discovery">Noch nicht geladen.</pre>
</section>
<section>
<h2>Modell trainieren</h2>
<label for="train-model">Modell-ID</label><input id="train-model" value="home-model">
<label for="train-sensor">Sensor</label><input id="train-sensor" placeholder="sensor.temperatur">
<label for="train-feature">Merkmal</label><input id="train-feature" value="value">
<label for="train-values">Messwerte, komma-getrennt</label><input id="train-values" placeholder="19,20,21">
<button onclick="train()">Trainieren</button>
<pre id="training">Bereit.</pre>
</section>
<section>
<h2>Vorhersage</h2>
<label for="predict-model">Modell-ID</label><input id="predict-model" value="home-model">
<label for="predict-sensor">Sensor</label><input id="predict-sensor" placeholder="sensor.temperatur">
<label for="predict-feature">Merkmal</label><input id="predict-feature" value="value">
<label for="predict-value">Aktueller Wert</label><input id="predict-value" type="number" step="any">
<button onclick="predict()">Vorhersagen und erklären</button>
<pre id="prediction">Bereit.</pre>
</section>
<section class="wide">
<h2>Automation-Entwurf</h2>
<p>Der Entwurf muss explizit freigegeben werden. Auch danach wird nur YAML exportiert, nichts geschaltet.</p>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:8px">
<div><label for="alias">Name</label><input id="alias" value="Licht bei Dunkelheit"></div>
<div><label for="trigger">Trigger-Entity</label><input id="trigger" placeholder="sensor.flur_illuminance"></div>
<div><label for="below">Unter Grenzwert</label><input id="below" type="number" value="10"></div>
<div><label for="service">Dienst</label><select id="service"><option>light.turn_on</option><option>light.turn_off</option><option>switch.turn_on</option><option>switch.turn_off</option></select></div>
<div><label for="target">Ziel-Entity</label><input id="target" placeholder="light.flur"></div>
</div>
<button onclick="createProposal()">Entwurf speichern</button>
<button class="secondary" onclick="loadProposals()">Entwürfe aktualisieren</button>
<div id="proposals"></div>
</section>
</main>
<script>
const pretty = value => JSON.stringify(value, null, 2);
async function api(path, options={}) {
const response = await fetch(path, {headers: {"Content-Type":"application/json"}, ...options});
const body = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(body.detail || `${response.status} ${response.statusText}`);
return body;
}
async function loadStatus() {
const box=document.getElementById("status");
try {
const [health, ml, models]=await Promise.all([api("health"),api("ml/health"),api("ml/models")]);
box.innerHTML=`<p class="ok">API und ML bereit</p><p>Modelle: ${models.models.length}</p>`;
} catch(e) { box.innerHTML=`<p class="bad">${e.message}</p>`; }
}
async function discover() {
const out=document.getElementById("discovery"), domain=document.getElementById("domain").value.trim();
out.textContent="Lade ...";
try {
const rows=await api(`v1/discovery?learnable=true${domain?`&domain=${encodeURIComponent(domain)}`:""}`);
out.textContent=pretty({learnable_entities:rows.length, entities:rows.slice(0,100)});
} catch(e) { out.textContent=e.message; }
}
async function train() {
const out=document.getElementById("training");
try {
const values=document.getElementById("train-values").value.split(",").map(Number).filter(Number.isFinite);
if (!values.length) throw new Error("Mindestens einen Messwert eingeben.");
const sensor=document.getElementById("train-sensor").value.trim(), feature=document.getElementById("train-feature").value.trim();
const samples=values.map(value=>({sensor_id:sensor,values:{[feature]:value}}));
out.textContent=pretty(await api("ml/retrain",{method:"POST",body:JSON.stringify({modelId:document.getElementById("train-model").value,samples})}));
await loadStatus();
} catch(e) { out.textContent=e.message; }
}
async function predict() {
const out=document.getElementById("prediction");
try {
const feature=document.getElementById("predict-feature").value.trim();
out.textContent=pretty(await api("ml/predict",{method:"POST",body:JSON.stringify({
modelId:document.getElementById("predict-model").value,
sensor_id:document.getElementById("predict-sensor").value.trim(),
values:{[feature]:Number(document.getElementById("predict-value").value)}
})}));
} catch(e) { out.textContent=e.message; }
}
async function createProposal() {
try {
await api("v1/automations/proposals",{method:"POST",body:JSON.stringify({
alias:document.getElementById("alias").value,
description:"Manuell im SillyHome-Dashboard erstellter und nicht automatisch ausgeführter Entwurf.",
trigger:{entity_id:document.getElementById("trigger").value,below:Number(document.getElementById("below").value)},
action:{service:document.getElementById("service").value,entity_id:document.getElementById("target").value,data:{}}
})});
await loadProposals();
} catch(e) { alert(e.message); }
}
async function decide(id, revision, action) {
try { await api(`v1/automations/proposals/${id}/${action}`,{method:"POST",body:JSON.stringify({expected_revision:revision})}); await loadProposals(); }
catch(e) { alert(e.message); }
}
async function loadProposals() {
const box=document.getElementById("proposals");
try {
const rows=await api("v1/automations/proposals");
box.innerHTML=rows.length?`<table><tr><th>Name</th><th>Status</th><th>Aktion</th></tr>${rows.map(x=>`<tr><td>${x.alias}</td><td>${x.status}</td><td>${x.status==="draft"?`<button onclick="decide('${x.proposal_id}',${x.revision},'approve')">Freigeben</button><button class="secondary" onclick="decide('${x.proposal_id}',${x.revision},'reject')">Ablehnen</button>`:`${x.status==="approved"?`<a href="v1/automations/proposals/${x.proposal_id}/yaml">YAML laden</a>`:"-"}`}</td></tr>`).join("")}</table>`:"<p>Keine Entwürfe.</p>";
} catch(e) { box.textContent=e.message; }
}
loadStatus(); loadProposals();
</script>
</body>
</html>