Improve learning discovery and dashboard i18n
This commit is contained in:
@@ -288,6 +288,69 @@ const STATUS_TIMEOUT_MS = 2000;
|
||||
const DASHBOARD_TIMEOUT_MS = 3000;
|
||||
const I18N = {
|
||||
de: {
|
||||
ui: {
|
||||
tagline: "Geräte, Lernen, Freigaben und Systemzustand.",
|
||||
menu: "Menü",
|
||||
nav_status: "Startseite / System",
|
||||
nav_learning: "Lernen",
|
||||
nav_discovery: "Discovery & Einrichtung",
|
||||
nav_settings: "Einstellungen",
|
||||
page_ready: "Seite bereit, Status folgt ...",
|
||||
discovery_title: "Discovery & Einrichtung",
|
||||
entity_id: "Entity-ID",
|
||||
actuator_placeholder: "z. B. light.licht_abstellraum",
|
||||
type: "Typ",
|
||||
all_actuators: "Alle steuerbaren Typen",
|
||||
lights: "Lichter",
|
||||
switches: "Schalter / Helper",
|
||||
buttons: "Buttons",
|
||||
helper_buttons: "Helper-Buttons",
|
||||
helper_switches: "Helper-Schalter",
|
||||
covers: "Rollläden / Cover",
|
||||
climate: "Heizungen / Klima",
|
||||
locks: "Schlösser",
|
||||
fans: "Lüftung / Ventilatoren",
|
||||
humidifiers: "Befeuchter / Entfeuchter",
|
||||
media: "TV / Medien",
|
||||
remotes: "Fernbedienungen",
|
||||
scenes: "Szenen",
|
||||
numbers: "Numerische Helper",
|
||||
valves: "Ventile",
|
||||
search_list: "Liste durchsuchen",
|
||||
search_placeholder: "Raum, Gerät oder Entity",
|
||||
device_list: "Geräteliste",
|
||||
device_list_lazy: "Geräteliste bei Bedarf laden",
|
||||
add_device: "Gerät hinzufügen und Beobachtung starten",
|
||||
load_device_list: "Geräteliste laden",
|
||||
ready: "Bereit.",
|
||||
show_suggestions: "Vorschläge anzeigen",
|
||||
load_suggestions: "Vorschläge laden",
|
||||
observed_devices: "Beobachtete Geräte",
|
||||
refresh: "Aktualisieren",
|
||||
details: "Details",
|
||||
back: "Zurück",
|
||||
detail_empty: "Öffne bei einem beobachteten Gerät die Details.",
|
||||
system_cache: "System & Cache",
|
||||
check_status: "Status prüfen",
|
||||
checking: "Prüfung läuft ...",
|
||||
settings: "Einstellungen",
|
||||
settings_hint: "Sprache und Standardwerte für die Bedienoberfläche.",
|
||||
language: "Sprache",
|
||||
no_prediction: "Keine fällige Aktion",
|
||||
open: "offen",
|
||||
no_area: "Ohne Bereich",
|
||||
loading_start: "Startdaten laden ...",
|
||||
loading_devices: "Beobachtete Geräte werden geladen ...",
|
||||
delayed_start: "Startdaten verzögert",
|
||||
unavailable_start: "Startdaten sind gerade nicht verfügbar.",
|
||||
system_loading: "Systemübersicht lädt ...",
|
||||
system_delayed: "Systemübersicht verzögert",
|
||||
context_detected: "Kontext erkannt",
|
||||
active_approved: "aktiv freigegeben",
|
||||
shadow_prediction: "Prüfmodus mit Vorhersage",
|
||||
learning_blocked: "Lernen blockiert",
|
||||
collecting_actions: "sammelt Handlungen",
|
||||
},
|
||||
safety_stage: {
|
||||
observe: "Nur beobachten",
|
||||
suggest: "Vorschläge anzeigen",
|
||||
@@ -358,6 +421,69 @@ const I18N = {
|
||||
},
|
||||
},
|
||||
en: {
|
||||
ui: {
|
||||
tagline: "Devices, learning, approvals, and system health.",
|
||||
menu: "Menu",
|
||||
nav_status: "Home / System",
|
||||
nav_learning: "Learning",
|
||||
nav_discovery: "Discovery & setup",
|
||||
nav_settings: "Settings",
|
||||
page_ready: "Page ready, status pending ...",
|
||||
discovery_title: "Discovery & setup",
|
||||
entity_id: "Entity ID",
|
||||
actuator_placeholder: "e.g. light.storage_room",
|
||||
type: "Type",
|
||||
all_actuators: "All controllable types",
|
||||
lights: "Lights",
|
||||
switches: "Switches / helpers",
|
||||
buttons: "Buttons",
|
||||
helper_buttons: "Helper buttons",
|
||||
helper_switches: "Helper switches",
|
||||
covers: "Shutters / covers",
|
||||
climate: "Heating / climate",
|
||||
locks: "Locks",
|
||||
fans: "Ventilation / fans",
|
||||
humidifiers: "Humidifiers / dehumidifiers",
|
||||
media: "TV / media",
|
||||
remotes: "Remotes",
|
||||
scenes: "Scenes",
|
||||
numbers: "Numeric helpers",
|
||||
valves: "Valves",
|
||||
search_list: "Search list",
|
||||
search_placeholder: "Room, device, or entity",
|
||||
device_list: "Device list",
|
||||
device_list_lazy: "Load device list when needed",
|
||||
add_device: "Add device and start observing",
|
||||
load_device_list: "Load device list",
|
||||
ready: "Ready.",
|
||||
show_suggestions: "Show suggestions",
|
||||
load_suggestions: "Load suggestions",
|
||||
observed_devices: "Observed devices",
|
||||
refresh: "Refresh",
|
||||
details: "Details",
|
||||
back: "Back",
|
||||
detail_empty: "Open details from an observed device.",
|
||||
system_cache: "System & cache",
|
||||
check_status: "Check status",
|
||||
checking: "Checking ...",
|
||||
settings: "Settings",
|
||||
settings_hint: "Language and UI defaults.",
|
||||
language: "Language",
|
||||
no_prediction: "No due action",
|
||||
open: "open",
|
||||
no_area: "No area",
|
||||
loading_start: "Loading start data ...",
|
||||
loading_devices: "Loading observed devices ...",
|
||||
delayed_start: "Start data delayed",
|
||||
unavailable_start: "Start data is currently unavailable.",
|
||||
system_loading: "Loading system overview ...",
|
||||
system_delayed: "System overview delayed",
|
||||
context_detected: "Context detected",
|
||||
active_approved: "actively approved",
|
||||
shadow_prediction: "Review mode with prediction",
|
||||
learning_blocked: "Learning blocked",
|
||||
collecting_actions: "collecting actions",
|
||||
},
|
||||
safety_stage: {
|
||||
observe: "Observe only",
|
||||
suggest: "Show suggestions",
|
||||
@@ -468,11 +594,15 @@ function showView(viewId) {
|
||||
function setLanguage(language) {
|
||||
uiLang = I18N[language] ? language : "de";
|
||||
localStorage.setItem("sillyhome.ui.language", uiLang);
|
||||
document.documentElement.lang = uiLang;
|
||||
applyStaticTranslations();
|
||||
syncSettingsView();
|
||||
renderActuatorSelect();
|
||||
if (cachedActuators) renderConfiguredActuators();
|
||||
if (cachedSystemOverview) renderDashboardStatus(cachedSystemOverview);
|
||||
if (currentActuatorId && cachedDetailHtml.has(currentActuatorId)) {
|
||||
document.getElementById("actuator-detail").innerHTML = cachedDetailHtml.get(currentActuatorId);
|
||||
if (currentActuatorId) {
|
||||
cachedDetailHtml.delete(currentActuatorId);
|
||||
void showActuator(currentActuatorId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -482,16 +612,95 @@ function syncSettingsView() {
|
||||
}
|
||||
|
||||
function translate(group, value, fallback = "") {
|
||||
if (value == null || value === "") return fallback || "offen";
|
||||
if (value == null || value === "") return fallback || ui("open");
|
||||
return I18N[uiLang]?.[group]?.[value] || fallback || String(value);
|
||||
}
|
||||
|
||||
function ui(key) {
|
||||
return I18N[uiLang]?.ui?.[key] || I18N.de.ui[key] || key;
|
||||
}
|
||||
|
||||
function setText(selector, key) {
|
||||
const element = document.querySelector(selector);
|
||||
if (element) element.textContent = ui(key);
|
||||
}
|
||||
|
||||
function setPlaceholder(selector, key) {
|
||||
const element = document.querySelector(selector);
|
||||
if (element) element.placeholder = ui(key);
|
||||
}
|
||||
|
||||
function applyStaticTranslations() {
|
||||
setText("header .brand p", "tagline");
|
||||
setText("label[for='section-jump']", "menu");
|
||||
const navOptions = document.querySelectorAll("#section-jump option");
|
||||
[
|
||||
"nav_status",
|
||||
"nav_learning",
|
||||
"nav_discovery",
|
||||
"nav_settings",
|
||||
].forEach((key, index) => {
|
||||
if (navOptions[index]) navOptions[index].textContent = ui(key);
|
||||
});
|
||||
setText("#load-budget", "page_ready");
|
||||
setText("#choose h2", "discovery_title");
|
||||
setText("label[for='actuator-input']", "entity_id");
|
||||
setPlaceholder("#actuator-input", "actuator_placeholder");
|
||||
setText("label[for='actuator-domain-filter']", "type");
|
||||
const domainOptions = document.querySelectorAll("#actuator-domain-filter option");
|
||||
[
|
||||
"all_actuators",
|
||||
"lights",
|
||||
"switches",
|
||||
"buttons",
|
||||
"helper_buttons",
|
||||
"helper_switches",
|
||||
"covers",
|
||||
"climate",
|
||||
"locks",
|
||||
"fans",
|
||||
"humidifiers",
|
||||
"media",
|
||||
"remotes",
|
||||
"scenes",
|
||||
"numbers",
|
||||
"valves",
|
||||
].forEach((key, index) => {
|
||||
if (domainOptions[index]) domainOptions[index].textContent = ui(key);
|
||||
});
|
||||
setText("label[for='actuator-search']", "search_list");
|
||||
setPlaceholder("#actuator-search", "search_placeholder");
|
||||
setText("label[for='actuator-select']", "device_list");
|
||||
const lazyOption = document.querySelector("#actuator-select option[value='']");
|
||||
if (lazyOption) lazyOption.textContent = ui("device_list_lazy");
|
||||
const chooseButtons = document.querySelectorAll("#choose > button");
|
||||
if (chooseButtons[0]) chooseButtons[0].textContent = ui("add_device");
|
||||
if (chooseButtons[1]) chooseButtons[1].textContent = ui("load_device_list");
|
||||
setText("#actuator-config-result", "ready");
|
||||
setText("#choose .manual-context summary", "show_suggestions");
|
||||
const suggestionButton = document.querySelector("#choose .manual-context button");
|
||||
if (suggestionButton) suggestionButton.textContent = ui("load_suggestions");
|
||||
setText("#observed h2", "observed_devices");
|
||||
const refreshButton = document.querySelector("#observed .panel-title button");
|
||||
if (refreshButton) refreshButton.textContent = ui("refresh");
|
||||
setText("#detail h2", "details");
|
||||
const backButton = document.querySelector("#detail .panel-title button");
|
||||
if (backButton) backButton.textContent = ui("back");
|
||||
setText("#actuator-detail", "detail_empty");
|
||||
setText("#status-section h2", "system_cache");
|
||||
const statusButton = document.querySelector("#status-section .panel-title button");
|
||||
if (statusButton) statusButton.textContent = ui("check_status");
|
||||
setText("#settings h2", "settings");
|
||||
setText("#settings .muted", "settings_hint");
|
||||
setText("label[for='language-select']", "language");
|
||||
}
|
||||
|
||||
function formatDateTime(value) {
|
||||
if (!value) return "noch offen";
|
||||
if (!value) return ui("open");
|
||||
const parsed = new Date(value);
|
||||
return Number.isNaN(parsed.getTime())
|
||||
? String(value)
|
||||
: parsed.toLocaleString("de-DE");
|
||||
: parsed.toLocaleString(uiLang === "en" ? "en-US" : "de-DE");
|
||||
}
|
||||
|
||||
function uniqueValues(values) {
|
||||
@@ -532,7 +741,7 @@ async function apiWithTimeout(path, timeoutMs = STATUS_TIMEOUT_MS) {
|
||||
function lifecycleLabel(record) {
|
||||
const behaviorStatus = record.behavior_status || record.behavior?.status;
|
||||
const lifecycleStatus = record.lifecycle_status || record.lifecycle?.status;
|
||||
if (behaviorStatus === "trained") return "Kontext erkannt";
|
||||
if (behaviorStatus === "trained") return ui("context_detected");
|
||||
const labels = {
|
||||
trained: translate("lifecycle_status", "trained"),
|
||||
pending_history: translate("lifecycle_status", "pending_history"),
|
||||
@@ -556,10 +765,10 @@ function statusClass(record) {
|
||||
function behaviorLabel(record) {
|
||||
const mode = record.behavior_mode || record.behavior?.mode;
|
||||
const status = record.behavior_status || record.behavior?.status;
|
||||
if (mode === "active") return "aktiv freigegeben";
|
||||
if (status === "trained") return "Prüfmodus mit Vorhersage";
|
||||
if (status === "blocked") return "Lernen blockiert";
|
||||
return "sammelt Handlungen";
|
||||
if (mode === "active") return ui("active_approved");
|
||||
if (status === "trained") return ui("shadow_prediction");
|
||||
if (status === "blocked") return ui("learning_blocked");
|
||||
return ui("collecting_actions");
|
||||
}
|
||||
|
||||
function predictionLabel(record) {
|
||||
@@ -567,11 +776,11 @@ function predictionLabel(record) {
|
||||
const confidence = record.prediction_confidence ?? record.behavior?.prediction?.confidence;
|
||||
return target
|
||||
? `${target} (${Math.round(confidence * 100)} %)`
|
||||
: "Keine fällige Aktion";
|
||||
: ui("no_prediction");
|
||||
}
|
||||
|
||||
function entityLabel(entity) {
|
||||
const area = entity.area_name || "Ohne Bereich";
|
||||
const area = entity.area_name || ui("no_area");
|
||||
const name = entity.friendly_name || entity.entity_id;
|
||||
return `${area} - ${name} (${entity.entity_id})`;
|
||||
}
|
||||
@@ -655,9 +864,9 @@ async function loadOverview() {
|
||||
async function doLoadOverview() {
|
||||
const startedAt = performance.now();
|
||||
const budget = document.getElementById("load-budget");
|
||||
if (budget) budget.textContent = "Startdaten laden ...";
|
||||
if (budget) budget.textContent = ui("loading_start");
|
||||
if (!cachedActuators) {
|
||||
document.getElementById("configured-actuators").innerHTML = "<p class='muted'>Beobachtete Geräte werden geladen ...</p>";
|
||||
document.getElementById("configured-actuators").innerHTML = `<p class='muted'>${escapeHtml(ui("loading_devices"))}</p>`;
|
||||
}
|
||||
try {
|
||||
const dashboard = await api("v1/actuators/dashboard/start");
|
||||
@@ -675,12 +884,12 @@ async function doLoadOverview() {
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById("configured-actuators").innerHTML = `<p class="bad">${escapeHtml(error.message)}</p>`;
|
||||
if (budget) budget.textContent = "Startdaten verzögert";
|
||||
if (budget) budget.textContent = ui("delayed_start");
|
||||
try {
|
||||
await loadSummaryData();
|
||||
renderConfiguredActuators();
|
||||
} catch (_) {
|
||||
document.getElementById("configured-actuators").innerHTML = "<div class='empty-state'>Startdaten sind gerade nicht verfügbar.</div>";
|
||||
document.getElementById("configured-actuators").innerHTML = `<div class='empty-state'>${escapeHtml(ui("unavailable_start"))}</div>`;
|
||||
}
|
||||
}
|
||||
scheduleDashboardExtras();
|
||||
@@ -697,7 +906,7 @@ async function loadSystemOverview() {
|
||||
async function doLoadSystemOverview() {
|
||||
const startedAt = performance.now();
|
||||
const budget = document.getElementById("load-budget");
|
||||
if (budget) budget.textContent = "Systemübersicht lädt ...";
|
||||
if (budget) budget.textContent = ui("system_loading");
|
||||
try {
|
||||
const dashboard = await api("v1/actuators/dashboard/system");
|
||||
dashboard._load_elapsed_ms = Math.round(performance.now() - startedAt);
|
||||
@@ -713,7 +922,7 @@ async function doLoadSystemOverview() {
|
||||
scheduleDashboardExtras();
|
||||
} catch (error) {
|
||||
document.getElementById("status").innerHTML = `<p class="warn">Systemübersicht verzögert: ${escapeHtml(error.message)}</p>`;
|
||||
if (budget) budget.textContent = "Systemübersicht verzögert";
|
||||
if (budget) budget.textContent = ui("system_delayed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1950,9 +2159,11 @@ async function removeActuator(actuatorId) {
|
||||
}
|
||||
|
||||
async function startDashboard() {
|
||||
document.getElementById("status").innerHTML = "<p class='muted'>Status lädt nach ...</p>";
|
||||
document.getElementById("configured-actuators").innerHTML = "<div class='empty-state'>Öffne „Lernen“, um Geräte zu laden.</div>";
|
||||
document.getElementById("actuator-detail").innerHTML = "<div class='empty-state'>Wähle später ein Gerät aus der Übersicht.</div>";
|
||||
document.documentElement.lang = uiLang;
|
||||
applyStaticTranslations();
|
||||
document.getElementById("status").innerHTML = `<p class='muted'>${escapeHtml(uiLang === "en" ? "Status loading ..." : "Status lädt nach ...")}</p>`;
|
||||
document.getElementById("configured-actuators").innerHTML = `<div class='empty-state'>${escapeHtml(uiLang === "en" ? "Open Learning to load devices." : "Öffne „Lernen“, um Geräte zu laden.")}</div>`;
|
||||
document.getElementById("actuator-detail").innerHTML = `<div class='empty-state'>${escapeHtml(uiLang === "en" ? "Select a device from the overview later." : "Wähle später ein Gerät aus der Übersicht.")}</div>`;
|
||||
syncSettingsView();
|
||||
const initialView = localStorage.getItem("sillyhome.ui.view") === "detail"
|
||||
? "observed"
|
||||
|
||||
Reference in New Issue
Block a user