Add anomaly and performance monitoring
This commit is contained in:
@@ -99,6 +99,8 @@
|
||||
.metric strong { display:block; margin-bottom:4px; color:#cfe0ec; font-size:.84rem; }
|
||||
.decision-list { display:grid; gap:8px; margin:10px 0; }
|
||||
.decision-row { background:#121922; border:1px solid var(--border); border-radius:8px; padding:9px; min-width:0; overflow-wrap:anywhere; }
|
||||
.decision-row.slow,
|
||||
.decision-row.critical { border-color:var(--warn); box-shadow:0 0 0 1px rgba(243,201,105,.25); }
|
||||
.decision-row header { padding:0; border:0; background:transparent; display:flex; justify-content:space-between; gap:10px; flex-wrap:wrap; }
|
||||
.actions { display:flex; flex-wrap:wrap; gap:8px; margin-top:10px; }
|
||||
.actions button { flex:1 1 180px; margin-top:0; }
|
||||
@@ -283,7 +285,7 @@ let discoveryLoadPromise = null;
|
||||
let currentSensorWeightGroups = [];
|
||||
const ACTUATOR_RESULT_LIMIT = 50;
|
||||
const STATUS_TIMEOUT_MS = 2000;
|
||||
const DASHBOARD_TIMEOUT_MS = 4500;
|
||||
const DASHBOARD_TIMEOUT_MS = 3000;
|
||||
|
||||
function jumpToSection(target) {
|
||||
if (!target) return;
|
||||
@@ -439,11 +441,17 @@ async function loadOverview() {
|
||||
document.getElementById("configured-actuators").innerHTML = "<p class='muted'>Beobachtete Geräte werden geladen ...</p>";
|
||||
try {
|
||||
const dashboard = await apiWithTimeout("v1/actuators/dashboard", DASHBOARD_TIMEOUT_MS);
|
||||
dashboard._load_elapsed_ms = Math.round(performance.now() - startedAt);
|
||||
cachedActuators = dashboard.actuators || [];
|
||||
cachedEntities = [];
|
||||
renderDashboardStatus(dashboard);
|
||||
renderConfiguredActuators();
|
||||
if (budget) budget.textContent = `Bereit in ${Math.round(performance.now() - startedAt)} ms`;
|
||||
if (budget) {
|
||||
const loadMs = dashboard._load_elapsed_ms;
|
||||
budget.textContent = loadMs <= DASHBOARD_TIMEOUT_MS
|
||||
? `Bereit in ${loadMs} ms`
|
||||
: `Langsam: ${loadMs} ms`;
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById("configured-actuators").innerHTML = `<p class="bad">${escapeHtml(error.message)}</p>`;
|
||||
if (budget) budget.textContent = "Startdaten verzögert";
|
||||
@@ -504,15 +512,26 @@ function renderDashboardStatus(dashboard) {
|
||||
).length;
|
||||
const trainedCount = actuators.filter(record => record.behavior_status === "trained").length;
|
||||
const sampleTotal = actuators.reduce((sum, record) => sum + Number(record.sample_count || 0), 0);
|
||||
const anomalyTotal = Number(system.anomaly_count || 0);
|
||||
const criticalAnomalyTotal = Number(system.critical_anomaly_count || 0);
|
||||
const loadMs = Number(dashboard._load_elapsed_ms || 0);
|
||||
const jobs = dashboard.jobs?.jobs || [];
|
||||
const runningJobs = jobs.filter(job => job.status === "running").length;
|
||||
const slowJobs = Number(system.slow_job_count || 0);
|
||||
const p95 = system.job_p95_duration_ms == null ? "offen" : `${system.job_p95_duration_ms} ms`;
|
||||
const performanceClass = (
|
||||
loadMs > DASHBOARD_TIMEOUT_MS
|
||||
|| slowJobs > 0
|
||||
|| system.performance_status === "slow"
|
||||
) ? "warn" : "ok";
|
||||
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 class="${performanceClass}">
|
||||
Dashboard bereit in ${escapeHtml(loadMs || "offen")} ms. Budget: ${escapeHtml(system.performance_budget_ms || DASHBOARD_TIMEOUT_MS)} ms.
|
||||
</p>
|
||||
<p class="${system.websocket_status === "connected" ? "ok" : "warn"}">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 = [
|
||||
@@ -522,6 +541,8 @@ function renderDashboardStatus(dashboard) {
|
||||
`<span class="chip">Lernbereit: ${escapeHtml(system.trained_models ?? 0)}</span>`,
|
||||
`<span class="chip">Prüfen: ${escapeHtml(system.review_required ?? 0)}</span>`,
|
||||
`<span class="chip">Jobs aktiv: ${escapeHtml(runningJobs)}</span>`,
|
||||
`<span class="chip">Anomalien: ${escapeHtml(anomalyTotal)}</span>`,
|
||||
`<span class="chip">Kritisch: ${escapeHtml(criticalAnomalyTotal)}</span>`,
|
||||
].join("");
|
||||
stats.innerHTML = [
|
||||
`<div class="metric"><strong>Geladene Startdaten</strong>${escapeHtml(actuators.length)} Geräte</div>`,
|
||||
@@ -529,16 +550,20 @@ function renderDashboardStatus(dashboard) {
|
||||
`<div class="metric"><strong>Aktiv / Shadow</strong>${escapeHtml(activeCount)} / ${escapeHtml(shadowCount)}</div>`,
|
||||
`<div class="metric"><strong>Gelernt / Wartet</strong>${escapeHtml(trainedCount)} / ${escapeHtml(pendingCount)}</div>`,
|
||||
`<div class="metric"><strong>Gelernte Handlungen</strong>${escapeHtml(sampleTotal)}</div>`,
|
||||
`<div class="metric"><strong>Performance-Budget</strong>${escapeHtml(system.performance_budget_ms || 3000)} ms</div>`,
|
||||
`<div class="metric"><strong>Job p95</strong>${escapeHtml(p95)}</div>`,
|
||||
`<div class="metric"><strong>Langsame Jobs</strong>${escapeHtml(slowJobs)}</div>`,
|
||||
`<div class="metric"><strong>Anomalien</strong>${escapeHtml(anomalyTotal)} offen</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("");
|
||||
jobsBox.innerHTML = jobs.length ? `
|
||||
<h3>Job-Queue</h3>
|
||||
${jobs.slice(-6).reverse().map(job => `
|
||||
<div class="decision-row">
|
||||
<div class="decision-row ${Number(job.duration_ms || 0) >= DASHBOARD_TIMEOUT_MS ? "slow" : ""}">
|
||||
<header>
|
||||
<strong>${escapeHtml(job.kind)}${job.target ? `: ${escapeHtml(job.target)}` : ""}</strong>
|
||||
<span class="chip">${escapeHtml(job.status)}</span>
|
||||
<span class="chip">${escapeHtml(job.status)}${Number(job.duration_ms || 0) >= DASHBOARD_TIMEOUT_MS ? " · langsam" : ""}</span>
|
||||
</header>
|
||||
<p class="muted">${escapeHtml(job.summary || "Keine Zusammenfassung")}</p>
|
||||
<p class="muted">Start: ${escapeHtml(job.started_at || "offen")} · Dauer: ${escapeHtml(job.duration_ms == null ? "läuft/offen" : `${job.duration_ms} ms`)}</p>
|
||||
@@ -908,6 +933,7 @@ async function showActuator(actuatorId, evaluationMessage = "") {
|
||||
const adaptiveUpdates = record.behavior.adaptive_weight_updates || [];
|
||||
const automationConflicts = record.behavior.automation_conflicts || [];
|
||||
const timeProfiles = record.behavior.time_profiles || [];
|
||||
const anomalies = (record.behavior.anomalies || []).filter(item => !item.resolved);
|
||||
const safetyControls = `
|
||||
<details class="manual-context" open>
|
||||
<summary>Sicherheit und manuelles Gegensteuern</summary>
|
||||
@@ -1004,6 +1030,23 @@ async function showActuator(actuatorId, evaluationMessage = "") {
|
||||
`).join("") : "<li>Keine aktiven Automation-Konflikte erkannt.</li>"}</ul>
|
||||
</details>
|
||||
`;
|
||||
const anomalyPanel = `
|
||||
<details class="manual-context" ${anomalies.length ? "open" : ""}>
|
||||
<summary>v1.3 Anomalie- und Performance-Hinweise</summary>
|
||||
<div class="decision-list">
|
||||
${anomalies.length ? anomalies.map(anomaly => `
|
||||
<div class="decision-row ${anomaly.severity === "critical" ? "critical" : ""}">
|
||||
<header>
|
||||
<strong>${escapeHtml(anomaly.title)}</strong>
|
||||
<span class="chip">${escapeHtml(anomaly.severity)} · ${escapeHtml(anomaly.category)}</span>
|
||||
</header>
|
||||
<p>${escapeHtml(anomaly.detail)}</p>
|
||||
<p class="muted">Erkannt: ${escapeHtml(anomaly.detected_at || "offen")}</p>
|
||||
</div>
|
||||
`).join("") : "<p class='ok'>Keine offenen Anomalien fuer diesen Aktor.</p>"}
|
||||
</div>
|
||||
</details>
|
||||
`;
|
||||
const learnedAutomationActions = record.behavior.patterns.filter(
|
||||
pattern => pattern.source === "automation",
|
||||
).length;
|
||||
@@ -1116,6 +1159,7 @@ async function showActuator(actuatorId, evaluationMessage = "") {
|
||||
${safetyControls}
|
||||
${decisionArchive}
|
||||
${adaptivePanel}
|
||||
${anomalyPanel}
|
||||
<h3>Passende Home-Assistant-Automationen</h3>
|
||||
<p class="muted">Bei einer Übernahme pausiert SillyHome diese Automationen. Beim Stoppen können sie gezielt fortgesetzt werden.</p>
|
||||
<button class="secondary compact" onclick="refreshRelatedAutomations('${escapeHtml(record.actuator_entity_id)}')">Automationen neu suchen</button>
|
||||
|
||||
Reference in New Issue
Block a user