Add room management settings
This commit is contained in:
@@ -100,6 +100,14 @@
|
||||
.card-title .chip { flex:0 1 auto; white-space:normal; text-align:center; }
|
||||
.actuator-card strong { overflow-wrap:anywhere; word-break:break-word; }
|
||||
.entity-id { overflow-wrap:anywhere; word-break:break-word; font-weight:800; }
|
||||
.room-board { display:grid; gap:10px; margin-top:12px; }
|
||||
.room-card { background:#121922; border:1px solid var(--border); border-radius:8px; padding:10px; min-width:0; }
|
||||
.room-card header { padding:0; border:0; background:transparent; display:flex; justify-content:space-between; gap:10px; flex-wrap:wrap; }
|
||||
.sensor-strip { display:flex; flex-wrap:wrap; gap:6px; margin:8px 0; }
|
||||
.sensor-pill { border:1px solid var(--border); border-radius:8px; padding:6px 8px; background:#17202b; max-width:100%; overflow-wrap:anywhere; }
|
||||
.sensor-pill.active { border-color:var(--ok); }
|
||||
.sensor-pill.optional { border-color:var(--complement); }
|
||||
.sensor-pill.not-required { opacity:.72; border-style:dashed; }
|
||||
.metric-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(120px,1fr)); gap:6px; margin:8px 0; }
|
||||
.metric { background:var(--panel-soft); border:1px solid var(--border); border-radius:8px; padding:8px; min-width:0; overflow-wrap:anywhere; word-break:break-word; }
|
||||
.metric strong { display:block; margin-bottom:4px; color:#cfe0ec; font-size:.84rem; }
|
||||
@@ -246,8 +254,9 @@
|
||||
<div class="panel-title">
|
||||
<div>
|
||||
<h2>Einstellungen</h2>
|
||||
<p class="muted">Sprache und Standardwerte für die Bedienoberfläche.</p>
|
||||
<p class="muted">Sprache, Räume, Sensoren, Aktoren und Vorhersagen an einem Ort.</p>
|
||||
</div>
|
||||
<button class="secondary compact" onclick="loadRoomManagement(true)">Räume laden</button>
|
||||
</div>
|
||||
<div class="grid-two">
|
||||
<div>
|
||||
@@ -257,6 +266,13 @@
|
||||
<option value="en">English</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="room-management-filter">Raum suchen</label>
|
||||
<input id="room-management-filter" placeholder="z. B. Abstellkammer, Wohnbereich" oninput="renderRoomManagement()">
|
||||
</div>
|
||||
</div>
|
||||
<div id="room-management" class="room-board">
|
||||
<div class="empty-state">Lade die Raumverwaltung, um Sensoren, Aktoren und Vorhersagen zusammen zu pflegen.</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
@@ -276,6 +292,7 @@ let cachedEntities = null;
|
||||
let cachedDiscovery = null;
|
||||
let cachedSystemOverview = null;
|
||||
let cachedDashboardOverview = null;
|
||||
let cachedRoomManagement = null;
|
||||
let cachedDetailHtml = new Map();
|
||||
let discoveryLoadPromise = null;
|
||||
let overviewLoadPromise = null;
|
||||
@@ -334,8 +351,12 @@ const I18N = {
|
||||
check_status: "Status prüfen",
|
||||
checking: "Prüfung läuft ...",
|
||||
settings: "Einstellungen",
|
||||
settings_hint: "Sprache und Standardwerte für die Bedienoberfläche.",
|
||||
settings_hint: "Sprache, Räume, Sensoren, Aktoren und Vorhersagen an einem Ort.",
|
||||
language: "Sprache",
|
||||
load_rooms: "Räume laden",
|
||||
room_search: "Raum suchen",
|
||||
room_search_placeholder: "z. B. Abstellkammer, Wohnbereich",
|
||||
room_management_empty: "Lade die Raumverwaltung, um Sensoren, Aktoren und Vorhersagen zusammen zu pflegen.",
|
||||
no_prediction: "Keine fällige Aktion",
|
||||
open: "offen",
|
||||
no_area: "Ohne Bereich",
|
||||
@@ -469,8 +490,12 @@ const I18N = {
|
||||
check_status: "Check status",
|
||||
checking: "Checking ...",
|
||||
settings: "Settings",
|
||||
settings_hint: "Language and UI defaults.",
|
||||
settings_hint: "Language, rooms, sensors, actuators, and predictions in one place.",
|
||||
language: "Language",
|
||||
load_rooms: "Load rooms",
|
||||
room_search: "Search room",
|
||||
room_search_placeholder: "e.g. storage room, living area",
|
||||
room_management_empty: "Load room management to maintain sensors, actuators, and predictions together.",
|
||||
no_prediction: "No due action",
|
||||
open: "open",
|
||||
no_area: "No area",
|
||||
@@ -767,6 +792,7 @@ function showView(viewId) {
|
||||
if (cachedDiscovery) renderActuatorDiscovery();
|
||||
} else if (viewId === "settings") {
|
||||
syncSettingsView();
|
||||
void loadRoomManagement();
|
||||
}
|
||||
document.getElementById(viewId)?.scrollIntoView({behavior: "smooth", block: "start"});
|
||||
}
|
||||
@@ -790,6 +816,9 @@ function setLanguage(language) {
|
||||
function syncSettingsView() {
|
||||
const select = document.getElementById("language-select");
|
||||
if (select) select.value = uiLang;
|
||||
if (cachedRoomManagement) {
|
||||
renderRoomManagement();
|
||||
}
|
||||
}
|
||||
|
||||
function translate(group, value, fallback = "") {
|
||||
@@ -939,6 +968,14 @@ function applyStaticTranslations() {
|
||||
setText("#settings h2", "settings");
|
||||
setText("#settings .muted", "settings_hint");
|
||||
setText("label[for='language-select']", "language");
|
||||
const roomLoadButton = document.querySelector("#settings .panel-title button");
|
||||
if (roomLoadButton) roomLoadButton.textContent = ui("load_rooms");
|
||||
setText("label[for='room-management-filter']", "room_search");
|
||||
setPlaceholder("#room-management-filter", "room_search_placeholder");
|
||||
const roomManagement = document.getElementById("room-management");
|
||||
if (roomManagement && !cachedRoomManagement) {
|
||||
roomManagement.innerHTML = `<div class="empty-state">${escapeHtml(ui("room_management_empty"))}</div>`;
|
||||
}
|
||||
localizeFragment(document);
|
||||
}
|
||||
|
||||
@@ -960,6 +997,7 @@ function invalidateDashboardCache() {
|
||||
cachedDiscovery = null;
|
||||
cachedDashboardOverview = null;
|
||||
cachedSystemOverview = null;
|
||||
cachedRoomManagement = null;
|
||||
cachedDetailHtml.clear();
|
||||
}
|
||||
|
||||
@@ -1343,6 +1381,168 @@ function renderJobQueue(jobs) {
|
||||
localizeFragment(jobsBox);
|
||||
}
|
||||
|
||||
async function loadRoomManagement(force = false) {
|
||||
const box = document.getElementById("room-management");
|
||||
if (!box) return;
|
||||
if (cachedRoomManagement && !force) {
|
||||
renderRoomManagement();
|
||||
return;
|
||||
}
|
||||
box.innerHTML = `<p class='muted'>${escapeHtml(uiLang === "en" ? "Loading rooms ..." : "Räume werden geladen ...")}</p>`;
|
||||
try {
|
||||
cachedRoomManagement = await api("v1/actuators/settings/rooms");
|
||||
renderRoomManagement();
|
||||
} catch (error) {
|
||||
box.innerHTML = `<p class="bad">${escapeHtml(error.message)}</p>`;
|
||||
}
|
||||
localizeFragment(box);
|
||||
}
|
||||
|
||||
function renderRoomManagement() {
|
||||
const box = document.getElementById("room-management");
|
||||
if (!box) return;
|
||||
const overview = cachedRoomManagement;
|
||||
if (!overview) {
|
||||
box.innerHTML = `<div class="empty-state">${escapeHtml(ui("room_management_empty"))}</div>`;
|
||||
return;
|
||||
}
|
||||
const query = normalizedSearch(document.getElementById("room-management-filter")?.value || "");
|
||||
const rooms = (overview.rooms || []).filter(room =>
|
||||
!query
|
||||
|| normalizedSearch(room.room).includes(query)
|
||||
|| (room.actuators || []).some(actuator =>
|
||||
matchesSearch({
|
||||
entity_id: actuator.actuator_entity_id,
|
||||
friendly_name: actuator.friendly_name,
|
||||
area_name: room.room,
|
||||
}, query)
|
||||
)
|
||||
);
|
||||
const unmanaged = overview.unmanaged_actuators || [];
|
||||
box.innerHTML = `
|
||||
${rooms.length ? rooms.map(room => `
|
||||
<article class="room-card">
|
||||
<header>
|
||||
<div>
|
||||
<h3>${escapeHtml(room.room)}</h3>
|
||||
<p class="muted">${escapeHtml(room.actuator_count)} ${escapeHtml(uiLang === "en" ? "actuator(s)" : "Aktor(en)")}</p>
|
||||
</div>
|
||||
<span class="chip">${escapeHtml((room.sensors || []).filter(sensor => sensor.active).length)} ${escapeHtml(uiLang === "en" ? "active sensors" : "aktive Sensoren")}</span>
|
||||
</header>
|
||||
<div class="sensor-strip">
|
||||
${(room.sensors || []).slice(0, 12).map(sensor => sensorPill(sensor)).join("") || `<span class="muted">${escapeHtml(uiLang === "en" ? "No sensors selected yet." : "Noch keine Sensoren ausgewählt.")}</span>`}
|
||||
</div>
|
||||
<div class="decision-list">
|
||||
${(room.prediction_rules || []).slice(0, 5).map(rule => `<div class="decision-row">${escapeHtml(rule)}</div>`).join("")}
|
||||
</div>
|
||||
<div class="card-list">
|
||||
${(room.actuators || []).map(actuator => roomActuatorCard(actuator)).join("")}
|
||||
</div>
|
||||
</article>
|
||||
`).join("") : `<div class="empty-state">${escapeHtml(uiLang === "en" ? "No matching rooms." : "Keine passenden Räume.")}</div>`}
|
||||
${unmanaged.length ? `
|
||||
<article class="room-card">
|
||||
<header>
|
||||
<h3>${escapeHtml(uiLang === "en" ? "Unmanaged suggestions" : "Noch nicht verwaltete Vorschläge")}</h3>
|
||||
<span class="chip">${escapeHtml(unmanaged.length)}</span>
|
||||
</header>
|
||||
<div class="card-list">
|
||||
${unmanaged.slice(0, 6).map(item => `
|
||||
<article class="actuator-card">
|
||||
<div class="card-title">
|
||||
<div>
|
||||
<strong>${escapeHtml(item.friendly_name || item.entity_id)}</strong>
|
||||
<div class="entity-id">${escapeHtml(item.entity_id)}</div>
|
||||
<div class="muted">${escapeHtml(item.area_name || item.device_name || item.domain)}</div>
|
||||
</div>
|
||||
<span class="chip">${Math.round(item.confidence * 100)} %</span>
|
||||
</div>
|
||||
<p class="muted">${escapeHtml(item.reason)}</p>
|
||||
<button class="secondary" onclick="configureSuggestedActuator('${escapeHtml(item.entity_id)}')">${escapeHtml(uiLang === "en" ? "Manage" : "Verwalten")}</button>
|
||||
</article>
|
||||
`).join("")}
|
||||
</div>
|
||||
</article>
|
||||
` : ""}
|
||||
`;
|
||||
localizeFragment(box);
|
||||
}
|
||||
|
||||
function sensorPill(sensor) {
|
||||
const classes = [
|
||||
"sensor-pill",
|
||||
sensor.active ? "active" : "",
|
||||
sensor.optional ? "optional" : "",
|
||||
sensor.not_required ? "not-required" : "",
|
||||
].filter(Boolean).join(" ");
|
||||
const state = sensor.state == null ? "" : ` · ${sensor.state}`;
|
||||
const status = sensor.not_required
|
||||
? (uiLang === "en" ? "not needed" : "nicht nötig")
|
||||
: sensor.active
|
||||
? (uiLang === "en" ? "active" : "aktiv")
|
||||
: sensor.optional
|
||||
? (uiLang === "en" ? "optional" : "optional")
|
||||
: (uiLang === "en" ? "suggested" : "Vorschlag");
|
||||
return `
|
||||
<span class="${classes}" title="${escapeHtml(sensor.reason)}">
|
||||
<strong>${escapeHtml(sensor.friendly_name || sensor.entity_id)}</strong>
|
||||
<span class="muted"> · ${escapeHtml(sensor.category)} · ${escapeHtml(status)}${escapeHtml(state)}</span>
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
|
||||
function roomActuatorCard(actuator) {
|
||||
const activeSensorIds = (actuator.sensors || [])
|
||||
.filter(sensor => sensor.active && !sensor.not_required)
|
||||
.map(sensor => sensor.entity_id);
|
||||
const selectedContextIds = (actuator.sensors || [])
|
||||
.filter(sensor => sensor.active && sensor.entity_id !== actuator.selected_numeric_entity_id)
|
||||
.map(sensor => sensor.entity_id);
|
||||
return `
|
||||
<article class="actuator-card">
|
||||
<div class="card-title">
|
||||
<div>
|
||||
<strong>${escapeHtml(actuator.friendly_name || actuator.actuator_entity_id)}</strong>
|
||||
<div class="entity-id">${escapeHtml(actuator.actuator_entity_id)}</div>
|
||||
<div class="muted">${escapeHtml(behaviorLabel(actuator))} · ${escapeHtml(lifecycleLabel(actuator))}</div>
|
||||
</div>
|
||||
<span class="chip">${escapeHtml(actuator.sample_count)} ${escapeHtml(uiLang === "en" ? "actions" : "Handlungen")}</span>
|
||||
</div>
|
||||
<p class="muted">${escapeHtml(actuator.management_hint)}</p>
|
||||
<div class="sensor-strip">
|
||||
${(actuator.sensors || []).slice(0, 8).map(sensor => sensorPill(sensor)).join("")}
|
||||
</div>
|
||||
<div class="decision-list">
|
||||
${(actuator.prediction_rules || []).slice(0, 4).map(rule => `<div class="decision-row">${escapeHtml(rule)}</div>`).join("")}
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="secondary" onclick="showActuator('${escapeHtml(actuator.actuator_entity_id)}')">${escapeHtml(uiLang === "en" ? "Open details" : "Details öffnen")}</button>
|
||||
<button onclick="saveRoomAssignment('${escapeHtml(actuator.actuator_entity_id)}', '${escapeHtml(actuator.selected_numeric_entity_id || "")}', '${escapeHtml(selectedContextIds.join(","))}')">${escapeHtml(uiLang === "en" ? "Save selection" : "Auswahl speichern")}</button>
|
||||
</div>
|
||||
<p class="muted">${escapeHtml(uiLang === "en" ? "Current selection:" : "Aktuelle Auswahl:")} ${escapeHtml(activeSensorIds.join(", ") || (uiLang === "en" ? "none" : "keine"))}</p>
|
||||
</article>
|
||||
`;
|
||||
}
|
||||
|
||||
async function saveRoomAssignment(actuatorId, numericEntityId, contextCsv) {
|
||||
const contextEntityIds = contextCsv.split(",").map(item => item.trim()).filter(Boolean);
|
||||
try {
|
||||
await api(`v1/actuators/${encodeURIComponent(actuatorId)}/assignment`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
numeric_entity_id: numericEntityId || null,
|
||||
context_entity_ids: contextEntityIds,
|
||||
note: "In der Raumverwaltung gespeichert",
|
||||
}),
|
||||
});
|
||||
invalidateDashboardCache();
|
||||
cachedRoomManagement = null;
|
||||
await loadRoomManagement(true);
|
||||
} catch (error) {
|
||||
alert(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSummaryData() {
|
||||
const actuators = await api("v1/actuators/summary");
|
||||
cachedActuators = actuators;
|
||||
|
||||
Reference in New Issue
Block a user