Prepare SillyHome Next 1.0.0 dashboard and API rework
Some checks failed
quality / test (3.11) (push) Has been cancelled
quality / test (3.13) (push) Has been cancelled

This commit is contained in:
2026-06-17 01:13:43 +02:00
parent 4db4276b95
commit 7ba9807a4e
11 changed files with 412 additions and 44 deletions

View File

@@ -5,18 +5,18 @@
<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; scroll-behavior:smooth; }
:root { color-scheme: dark; font-family: system-ui, sans-serif; background: #111317; color: #f3f5f7; scroll-behavior:smooth; --accent:#ff8a1c; --accent-strong:#ffab45; --panel:#181c22; --panel-soft:#20262e; --border:#303842; }
body { margin: 0; font-size:15px; }
header { padding: 18px 20px; background: linear-gradient(135deg,#142b3a,#193f36); }
header { padding: 18px 20px; background: linear-gradient(135deg,#1c1f26,#3a2411 70%,#5c2d08); }
h1,h2,h3 { margin: 0 0 12px; }
header p { margin: 5px 0; color: #c3d1dc; }
main { display: grid; grid-template-columns: minmax(280px,.85fr) minmax(0,1.35fr); gap: 12px; padding: 12px; max-width:1380px; margin:0 auto; }
section { background: #18212b; border: 1px solid #2d3a47; border-radius: 8px; padding: 12px; min-width:0; }
section:target { outline:2px solid #66dfa9; outline-offset:2px; }
main { display: grid; grid-template-columns: minmax(280px,.82fr) minmax(0,1.38fr); gap: 12px; padding: 12px; max-width:1380px; margin:0 auto; }
section { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 12px; min-width:0; }
section:target { outline:2px solid var(--accent); outline-offset:2px; }
.wide { grid-column: 1 / -1; }
.quick-nav { position:sticky; top:0; z-index:10; display:flex; gap:8px; overflow-x:auto; padding:10px 14px; background:rgba(16,21,28,.94); border-bottom:1px solid #2d3a47; backdrop-filter:blur(8px); }
.quick-nav a { flex:0 0 auto; padding:10px 12px; border-radius:999px; background:#22303c; border:1px solid #31404d; color:#eaf1f8; text-decoration:none; font-weight:700; font-size:.92rem; }
.quick-nav a.primary { background:#23715b; }
.quick-nav { position:sticky; top:0; z-index:10; display:flex; gap:8px; overflow-x:auto; padding:10px 14px; background:rgba(17,19,23,.94); border-bottom:1px solid var(--border); backdrop-filter:blur(8px); }
.quick-nav a { flex:0 0 auto; padding:10px 12px; border-radius:999px; background:#222831; border:1px solid var(--border); color:#f3f5f7; text-decoration:none; font-weight:700; font-size:.92rem; }
.quick-nav a.primary { background:#8f4208; border-color:var(--accent); }
details.collapsible > summary,
.manual-context > summary,
.group-panel > summary { cursor:pointer; font-weight:800; color:#eaf1f8; }
@@ -31,39 +31,39 @@
.manual-context[open] > summary::after,
.group-panel[open] > summary::after { content:"zuklappen"; }
.steps { display:grid; grid-template-columns:repeat(auto-fit,minmax(190px,1fr)); gap:10px; margin-top:12px; }
.step { background:#111a23; border:1px solid #31404d; border-radius:8px; padding:10px; }
.step-number { display:inline-grid; place-items:center; width:28px; height:28px; border-radius:50%; background:#23715b; font-weight:700; margin-bottom:8px; }
.step { background:#12161c; border:1px solid var(--border); border-radius:8px; padding:10px; }
.step-number { display:inline-grid; place-items:center; width:28px; height:28px; border-radius:50%; background:var(--accent); color:#211204; font-weight:800; margin-bottom:8px; }
.step p { margin:5px 0; }
.ok { color: #66dfa9; }
.ok { color: #73e0a9; }
.warn { color: #f3c969; }
.bad { color: #ff8f8f; }
label { display: block; margin: 9px 0 4px; color: #b9c9d6; }
select,input,button { box-sizing: border-box; width: 100%; border-radius: 8px; border: 1px solid #3b4b5b; padding: 10px; background: #101820; color: #fff; font:inherit; min-width:0; }
select,input,button { box-sizing: border-box; width: 100%; border-radius: 8px; border: 1px solid #3b4b5b; padding: 10px; background: #11161d; color: #fff; font:inherit; min-width:0; }
select[multiple] { min-height:150px; }
button { min-height:44px; margin-top: 10px; background: #23715b; border: 0; font-weight: 700; cursor: pointer; }
button { min-height:44px; margin-top: 10px; background: var(--accent); color:#211204; border: 0; font-weight: 800; cursor: pointer; }
button.secondary { background: #37495c; }
button.danger { background: #7b3434; }
button.compact { width:auto; min-width:120px; margin-right:8px; }
table { width: 100%; border-collapse: collapse; font-size: .92rem; }
td,th { padding: 8px; border-bottom: 1px solid #2d3a47; text-align: left; vertical-align: top; }
ul { margin: 8px 0; padding-left: 18px; }
.notice { border-left: 4px solid #66dfa9; padding-left: 10px; }
.notice { border-left: 4px solid var(--accent); padding-left: 10px; }
.grid-two { display:grid; grid-template-columns:repeat(auto-fit,minmax(240px,1fr)); gap:12px; }
.chips { display:flex; flex-wrap:wrap; gap:6px; margin-top:8px; }
.chip { padding:4px 8px; border-radius:999px; background:#22303c; border:1px solid #31404d; font-size:.85rem; }
.chip { padding:4px 8px; border-radius:999px; background:#222831; border:1px solid var(--border); font-size:.85rem; }
.muted { color:#9fb0be; }
.card-list { display:grid; grid-template-columns:repeat(auto-fit,minmax(260px,1fr)); gap:10px; }
.actuator-card { background:#111a23; border:1px solid #31404d; border-radius:8px; padding:10px; min-width:0; }
.actuator-card.selected { border-color:#66dfa9; box-shadow:0 0 0 1px rgba(102,223,169,.3); }
.actuator-card { background:#12161c; border:1px solid var(--border); border-radius:8px; padding:10px; min-width:0; }
.actuator-card.selected { border-color:var(--accent); box-shadow:0 0 0 1px rgba(255,138,28,.35); }
.card-title { display:flex; justify-content:space-between; gap:10px; align-items:flex-start; margin-bottom:8px; }
.entity-id { overflow-wrap:anywhere; font-weight:800; }
.metric-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(120px,1fr)); gap:6px; margin:8px 0; }
.metric { background:#18212b; border:1px solid #2d3a47; border-radius:8px; padding:8px; min-width:0; }
.metric { background:var(--panel-soft); border:1px solid var(--border); border-radius:8px; padding:8px; min-width:0; }
.metric strong { display:block; margin-bottom:4px; color:#cfe0ec; font-size:.84rem; }
.actions { display:flex; flex-wrap:wrap; gap:8px; margin-top:10px; }
.actions button { flex:1 1 180px; margin-top:0; }
.detail-header { display:flex; justify-content:space-between; gap:12px; align-items:flex-start; flex-wrap:wrap; }
.manual-context { margin-top:12px; background:#111a23; border:1px solid #31404d; border-radius:8px; padding:10px; }
.manual-context { margin-top:12px; background:#12161c; border:1px solid var(--border); border-radius:8px; padding:10px; }
.inline-controls { display:grid; grid-template-columns:repeat(auto-fit,minmax(160px,1fr)); gap:8px; margin:8px 0; }
.manual-entry { min-height:80px; resize:vertical; }
textarea { box-sizing:border-box; width:100%; border-radius:10px; border:1px solid #3b4b5b; padding:12px; background:#101820; color:#fff; font:inherit; }
@@ -130,11 +130,12 @@
</section>
<section id="status-section">
<h2>Systemstatus</h2>
<p class="muted">Zeigt, ob Verbindung, Lernsystem und automatische Prüfungen funktionieren. Hier musst du normalerweise nichts einstellen.</p>
<h2>System & Cache</h2>
<p class="muted">Die Startansicht nutzt lokale Summaries und Cache-Daten. Home-Assistant-Discovery lädt im Hintergrund nach.</p>
<div id="status">Prüfung läuft ...</div>
<div class="chips" id="status-chips"></div>
<button class="secondary" onclick="loadOverview()">Status aktualisieren</button>
<div id="dashboard-stats" class="metric-grid"></div>
<button class="secondary" onclick="loadOverview()">Dashboard aktualisieren</button>
</section>
<section id="choose">
@@ -352,9 +353,18 @@ function optionGroups(entities, selectedIds = new Set()) {
async function loadOverview() {
document.getElementById("configured-actuators").innerHTML = "<p class='muted'>Beobachtete Geräte werden geladen ...</p>";
await loadSummaryData();
renderConfiguredActuators();
void loadStatus();
try {
const dashboard = await api("v1/actuators/dashboard");
cachedActuators = dashboard.actuators || [];
cachedEntities = [];
renderDashboardStatus(dashboard);
renderConfiguredActuators();
} catch (error) {
document.getElementById("configured-actuators").innerHTML = `<p class="bad">${escapeHtml(error.message)}</p>`;
await loadSummaryData();
renderConfiguredActuators();
void loadStatus();
}
void loadActuatorDiscovery();
void loadActuatorSuggestions();
}
@@ -392,6 +402,36 @@ async function loadStatus() {
}
}
function renderDashboardStatus(dashboard) {
const status = document.getElementById("status");
const chips = document.getElementById("status-chips");
const stats = document.getElementById("dashboard-stats");
const system = dashboard.system || {};
const cache = dashboard.cache || {};
const discoveryGroups = dashboard.discovery_groups || [];
const cacheLabel = cache.available
? `Cache aktuell mit ${cache.entity_count} Entities`
: "Cache wird nach Discovery aufgebaut";
status.innerHTML = `
<p class="${system.websocket_status === "connected" ? "ok" : "warn"}">
Dashboard bereit. WebSocket: ${escapeHtml(system.websocket_status || "unbekannt")}
</p>
<p class="muted">Letzte automatische Prüfung: ${escapeHtml(system.reconciliation_last_completed_at || "noch nicht abgeschlossen")}</p>
`;
chips.innerHTML = [
`<span class="chip">API: ${escapeHtml(system.api_status || "ok")}</span>`,
`<span class="chip">Cache: ${escapeHtml(cacheLabel)}</span>`,
`<span class="chip">Aktoren: ${escapeHtml(system.configured_actuators ?? 0)}</span>`,
`<span class="chip">Lernbereit: ${escapeHtml(system.trained_models ?? 0)}</span>`,
`<span class="chip">Prüfen: ${escapeHtml(system.review_required ?? 0)}</span>`,
].join("");
stats.innerHTML = [
`<div class="metric"><strong>Geladene Startdaten</strong>${escapeHtml((dashboard.actuators || []).length)} Geräte</div>`,
`<div class="metric"><strong>Discovery-Gruppen</strong>${escapeHtml(discoveryGroups.length)} Kategorien</div>`,
`<div class="metric"><strong>Cache-Zeitpunkt</strong>${escapeHtml(cache.updated_at || "noch offen")}</div>`,
].join("");
}
async function loadSummaryData() {
const actuators = await api("v1/actuators/summary");
cachedActuators = actuators;