@@ -274,6 +275,11 @@ const ACTUATOR_RESULT_LIMIT = 50;
const STATUS_TIMEOUT_MS = 2000;
const DASHBOARD_TIMEOUT_MS = 4500;
+function jumpToSection(target) {
+ if (!target) return;
+ document.querySelector(target)?.scrollIntoView({behavior: "smooth", block: "start"});
+}
+
function uniqueValues(values) {
return [...new Set(values.filter(Boolean))];
}
@@ -445,17 +451,16 @@ async function loadStatus() {
const chips = document.getElementById("status-chips");
status.innerHTML = "
Status wird geprüft ...
";
try {
- const [health, websocket, ml, reconciliation, actuators] = await Promise.allSettled([
+ const [health, websocket, ml, reconciliation] = await Promise.allSettled([
apiWithTimeout("health"),
apiWithTimeout("health/websocket"),
apiWithTimeout("ml/health"),
apiWithTimeout("v1/actuators/reconciliation/state"),
- apiWithTimeout("v1/actuators/summary"),
]);
- const values = [health, websocket, ml, reconciliation, actuators].map(result =>
+ const values = [health, websocket, ml, reconciliation].map(result =>
result.status === "fulfilled" ? result.value : null
);
- const [healthValue, websocketValue, mlValue, reconciliationValue, actuatorValue] = values;
+ const [healthValue, websocketValue, mlValue, reconciliationValue] = values;
const hasError = values.some(value => value === null);
status.innerHTML = hasError
? "
Status teilweise verfügbar. Das Dashboard bleibt bedienbar.
"
@@ -464,7 +469,6 @@ async function loadStatus() {
`
API: ${escapeHtml(healthValue?.status || "offen")} `,
`
WebSocket: ${escapeHtml(websocketValue?.status || "offen")} `,
`
Lernsystem: ${escapeHtml(mlValue?.status || "offen")} `,
- `
Aktoren: ${Array.isArray(actuatorValue) ? actuatorValue.length : "offen"} `,
`
Lernbereite Geräte: ${escapeHtml(reconciliationValue?.trained_models ?? "offen")} `,
].join("");
} catch (error) {
@@ -747,7 +751,7 @@ function renderConfiguredActuators() {
const groupedRows = [...groups.entries()].sort(([left], [right]) => left.localeCompare(right));
box.innerHTML = rows.length ? `
${groupedRows.map(([group, items]) => `
-
+
${escapeHtml(group)} (${items.length})
${items.map(({record}) => `
@@ -787,9 +791,10 @@ function renderConfiguredActuators() {
async function showActuator(actuatorId, evaluationMessage = "") {
currentActuatorId = actuatorId;
const box = document.getElementById("actuator-detail");
+ renderActuatorDetailShell(actuatorId);
try {
const record = await api(`v1/actuators/${encodeURIComponent(actuatorId)}`);
- await loadContextOptions(actuatorId);
+ contextOptions = [];
const contexts = [
record.assignment.selected_numeric_entity_id,
...record.assignment.selected_context_entity_ids,
@@ -827,7 +832,7 @@ async function showActuator(actuatorId, evaluationMessage = "") {
selected: manualContextIds,
};
const manualAssignment = `
-
+
Kontext selbst festlegen
Die Vorschläge sind aktorbezogen vorsortiert. Wenn etwas fehlt, trage die Entity-ID unten manuell ein, z. B. PIR, Helligkeit außen, Luftfeuchtigkeit oder Lichtzustände.
Optionaler Haupt-Messsensor
@@ -856,7 +861,7 @@ async function showActuator(actuatorId, evaluationMessage = "") {
Diese Kontext-Auswahl speichern
- Vorschläge neu laden
+ Vorschläge neu laden
`;
@@ -926,12 +931,61 @@ async function showActuator(actuatorId, evaluationMessage = "") {
${currentContextControls}
${manualAssignment}
`;
+ void hydrateContextOptions(record);
document.getElementById("detail").scrollIntoView({behavior: "smooth", block: "start"});
} catch (error) {
box.textContent = error.message;
}
}
+function renderActuatorDetailShell(actuatorId) {
+ document.getElementById("actuator-detail").innerHTML = `
+
+
+
Phase 1 Aktuelle Einstellung
+
Phase 2 Lernstand
+
Phase 3 Kontextvorschläge
+
+ `;
+}
+
+async function hydrateContextOptions(record) {
+ await loadContextOptions(record.actuator_entity_id);
+ const manualContextSelect = document.getElementById("manual-context-select");
+ const numericSelect = document.getElementById("manual-numeric-select");
+ const categorySelect = document.getElementById("manual-context-category");
+ if (!manualContextSelect || !numericSelect || !categorySelect) return;
+ const manualContextIds = new Set(record.assignment.selected_context_entity_ids || []);
+ const numericOptions = contextOptions.filter(entity => entity.domain === "sensor");
+ const contextCategories = [...new Set(contextOptions
+ .filter(entity => entity.entity_id !== record.actuator_entity_id)
+ .map(categoryForEntity))]
+ .sort();
+ manualContextState = {
+ options: contextOptions.filter(entity => entity.entity_id !== record.actuator_entity_id),
+ selected: manualContextIds,
+ };
+ numericSelect.innerHTML = `
+ Keinen numerischen Hauptsensor verwenden
+ ${optionGroups(numericOptions, new Set([record.assignment.selected_numeric_entity_id].filter(Boolean)))}
+ `;
+ categorySelect.innerHTML = `
+ Alle relevanten Vorschläge
+ ${contextCategories.map(category => `${escapeHtml(category)} `).join("")}
+ `;
+ renderManualContextSelect();
+}
+
+async function hydrateCurrentContextOptions(actuatorId) {
+ const record = await api(`v1/actuators/${encodeURIComponent(actuatorId)}`);
+ await hydrateContextOptions(record);
+}
+
async function saveManualAssignment(actuatorId) {
const numericEntityId = document.getElementById("manual-numeric-select").value || null;
const selectedContextIds = Array.from(
@@ -1096,7 +1150,16 @@ async function removeActuator(actuatorId) {
}
}
-loadOverview();
+async function startDashboard() {
+ document.getElementById("status").innerHTML = "Status lädt nach ...
";
+ document.getElementById("configured-actuators").innerHTML = "Geräte werden nach dem Status geladen.
";
+ document.getElementById("actuator-detail").innerHTML = "Wähle später ein Gerät aus der Übersicht.
";
+ await new Promise(resolve => requestAnimationFrame(resolve));
+ await loadStatus();
+ await loadOverview();
+}
+
+void startDashboard();