diff --git a/README.md b/README.md index 011eb6b..fb3d949 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,8 @@ nach einer ausdrücklichen Freigabe ausführen. [`docs/V1_4_0_OPERATING_GUIDE.md`](docs/V1_4_0_OPERATING_GUIDE.md) - Version 1.5.0 Menü-Dashboard und kompakte Detaildaten: [`docs/V1_5_0_OPERATING_GUIDE.md`](docs/V1_5_0_OPERATING_GUIDE.md) +- Version 1.5.1 Stabilisierung der Dashboard-Ladepfade: + [`docs/V1_5_1_OPERATING_GUIDE.md`](docs/V1_5_1_OPERATING_GUIDE.md) - Arbeitsregeln für Coding-Agenten: [`AGENTS.md`](AGENTS.md) ## Reifegrad diff --git a/addon/config.yaml b/addon/config.yaml index 9c99025..6378ab0 100644 --- a/addon/config.yaml +++ b/addon/config.yaml @@ -1,5 +1,5 @@ name: SillyHome Next -version: "1.5.0" +version: "1.5.1" slug: sillyhome_next description: Lernt automatisch aus deinem Verhalten und steuert freigegebene Aktoren url: http://192.168.6.31:3000/pino/sillyhome-next diff --git a/app/api/v1/actuators.py b/app/api/v1/actuators.py index 556f1a9..9c4df23 100644 --- a/app/api/v1/actuators.py +++ b/app/api/v1/actuators.py @@ -306,15 +306,25 @@ def list_configured_summary(request: Request) -> list[ActuatorSummary]: @router.get("/dashboard", response_model=DashboardOverview) def dashboard_overview(request: Request) -> DashboardOverview: - return _dashboard_overview(request, include_background=True) + return _dashboard_overview(request, include_background=True, include_actuators=True) @router.get("/dashboard/start", response_model=DashboardOverview) def dashboard_start(request: Request) -> DashboardOverview: - return _dashboard_overview(request, include_background=False) + return _dashboard_overview(request, include_background=False, include_actuators=True) -def _dashboard_overview(request: Request, *, include_background: bool) -> DashboardOverview: +@router.get("/dashboard/system", response_model=DashboardOverview) +def dashboard_system(request: Request) -> DashboardOverview: + return _dashboard_overview(request, include_background=False, include_actuators=False) + + +def _dashboard_overview( + request: Request, + *, + include_background: bool, + include_actuators: bool, +) -> DashboardOverview: cache_payload = _load_entity_cache_payload(request) raw_entities = cache_payload.get("entities", []) if not isinstance(raw_entities, list): @@ -329,7 +339,7 @@ def _dashboard_overview(request: Request, *, include_background: bool) -> Dashbo ] if include_background and isinstance(raw_groups, list) else [] reconciliation = _reconciliation_state_or_default(request) ws_status = getattr(request.app.state, "ws_status", None) - actuators = list_configured_summary(request) + actuators = list_configured_summary(request) if include_actuators else [] store = getattr(request.app.state, "actuator_store", None) jobs = ( store.load_job_queue() @@ -348,7 +358,11 @@ def _dashboard_overview(request: Request, *, include_background: bool) -> Dashbo if reconciliation.last_completed_at is not None else None ), - configured_actuators=len(actuators), + configured_actuators=( + len(actuators) + if include_actuators + else reconciliation.configured_actuators + ), trained_models=reconciliation.trained_models, review_required=reconciliation.review_required, job_p95_duration_ms=job_p95_duration_ms, diff --git a/app/main.py b/app/main.py index baf51e5..29f446e 100644 --- a/app/main.py +++ b/app/main.py @@ -105,7 +105,7 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]: app = FastAPI( title="SillyHome Next API", description="Lokales Smart-Home-Intelligenzsystem für Home Assistant.", - version="1.5.0", + version="1.5.1", lifespan=lifespan, ) app.state.settings = load_settings() diff --git a/app/static/index.html b/app/static/index.html index 52bd379..9838220 100644 --- a/app/static/index.html +++ b/app/static/index.html @@ -56,6 +56,8 @@ .manual-context > summary, .group-panel > summary { cursor:pointer; font-weight:800; color:#eaf1f8; } details.collapsible > summary { list-style:none; display:flex; justify-content:space-between; gap:10px; } + .manual-context > summary, + .group-panel > summary { display:flex; justify-content:space-between; gap:10px; align-items:center; } details.collapsible > summary::-webkit-details-marker, .manual-context > summary::-webkit-details-marker, .group-panel > summary::-webkit-details-marker { display:none; } @@ -526,6 +528,11 @@ async function apiWithTimeout(path, timeoutMs = STATUS_TIMEOUT_MS) { const timeout = setTimeout(() => controller.abort(), timeoutMs); try { return await api(path, {signal: controller.signal}); + } catch (error) { + if (error?.name === "AbortError") { + throw new Error("Zeitlimit erreicht; Daten laden im Hintergrund weiter."); + } + throw error; } finally { clearTimeout(timeout); } @@ -652,7 +659,7 @@ async function loadOverview() { if (budget) budget.textContent = "Startdaten laden ..."; document.getElementById("configured-actuators").innerHTML = "
Beobachtete Geräte werden geladen ...
"; try { - const dashboard = await apiWithTimeout("v1/actuators/dashboard/start", DASHBOARD_TIMEOUT_MS); + const dashboard = await api("v1/actuators/dashboard/start"); dashboard._load_elapsed_ms = Math.round(performance.now() - startedAt); cachedActuators = dashboard.actuators || []; cachedEntities = []; @@ -682,7 +689,7 @@ async function loadSystemOverview() { const budget = document.getElementById("load-budget"); if (budget) budget.textContent = "Systemübersicht lädt ..."; try { - const dashboard = await apiWithTimeout("v1/actuators/dashboard/start", DASHBOARD_TIMEOUT_MS); + const dashboard = await api("v1/actuators/dashboard/system"); dashboard._load_elapsed_ms = Math.round(performance.now() - startedAt); cachedActuators = dashboard.actuators || cachedActuators; renderDashboardStatus(dashboard); @@ -1764,7 +1771,7 @@ async function startDashboard() { document.getElementById("section-jump").value = "status-section"; showView("status-section"); await new Promise(resolve => requestAnimationFrame(resolve)); - void loadStatus(); + setTimeout(() => void loadStatus(), 100); } void startDashboard(); diff --git a/docs/V1_5_1_OPERATING_GUIDE.md b/docs/V1_5_1_OPERATING_GUIDE.md new file mode 100644 index 0000000..f58573c --- /dev/null +++ b/docs/V1_5_1_OPERATING_GUIDE.md @@ -0,0 +1,32 @@ +# SillyHome Next v1.5.1 Operating Guide + +v1.5.1 ist ein Stabilisierungshotfix für die nach v1.2.0 entstandenen +Dashboard-Änderungen. Fachlich gehört diese Arbeit zur v1.2.x-Patchlinie; die +höhere technische Versionsnummer ist nur nötig, weil Home Assistant bereits +v1.5.0 installiert hat und Add-on-Updates monoton nach oben laufen. + +## Korrekturen + +- Die System-Startseite nutzt `GET /v1/actuators/dashboard/system` und lädt + keine Aktorenliste. +- Sichtbare 3-Sekunden-Abbrüche mit Browsertexten wie `signal is aborted + without reason` wurden entfernt. +- Startdaten und Detaildaten werden ohne künstlichen Frontend-Abbruch geladen. +- Timeout-Meldungen werden deutsch und verständlich angezeigt, wenn sie bei + Nebenprüfungen auftreten. +- `summary`-Zeilen wie `anzeigenaufklappen` haben jetzt Abstand und Layout. + +## Ladeverhalten + +- Statische Seite wird sofort gerendert. +- Systemdaten laden im Hintergrund. +- Lernen/Geräte laden nur im Menü `Lernen`. +- Discovery lädt nur im Menü `Discovery & Einrichtung`. +- Aktorwerte laden erst beim Öffnen der Detailansicht. +- Kontextvorschläge laden erst auf Nutzeraktion. + +## Hinweis zur Performance-Anzeige + +Die App zeigt keine echte HA/Ingress-Navigationszeit an. Gemessen werden nur +einzelne interne Abrufe nach Start der Seite. Aussagen zur gesamten Ladezeit +müssen über Browser/Ingress oder HA-Messung geprüft werden. diff --git a/pyproject.toml b/pyproject.toml index 6b599af..f355321 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "sillyhome-next" -version = "1.5.0" +version = "1.5.1" description = "Lokales Smart-Home-Intelligenzsystem für Home Assistant" requires-python = ">=3.11" dependencies = [ diff --git a/tests/api/test_actuators.py b/tests/api/test_actuators.py index 0fc06cb..321d1c7 100644 --- a/tests/api/test_actuators.py +++ b/tests/api/test_actuators.py @@ -446,14 +446,17 @@ def test_dashboard_reports_performance_budget_and_anomalies(tmp_path: Path) -> N dashboard_response = client.get("/v1/actuators/dashboard") start_response = client.get("/v1/actuators/dashboard/start") + system_response = client.get("/v1/actuators/dashboard/system") anomalies_response = client.get("/v1/actuators/anomalies") assert dashboard_response.status_code == 200 assert start_response.status_code == 200 + assert system_response.status_code == 200 system = dashboard_response.json()["system"] start_payload = start_response.json() assert start_payload["jobs"]["jobs"] == [] assert start_payload["discovery_groups"] == [] + assert system_response.json()["actuators"] == [] assert system["performance_budget_ms"] == 3000 assert system["slow_job_count"] == 1 assert system["performance_status"] == "slow"