From 4fccf21676e1be19c85f3075bbb812dedc3dfd9d Mon Sep 17 00:00:00 2001 From: Tobi G Date: Mon, 24 Aug 2026 19:39:40 +0200 Subject: [PATCH] =?UTF-8?q?Selbst-Update:=20Ergebnis=20erscheint=20ohne=20?= =?UTF-8?q?Neuladen,=20Panel=20=C3=BCbersteht=20Neustart?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zwei Meldungen des Besitzers ("Update installieren" braucht manuelles Neuladen, um den Neustart-Knopf zu zeigen; die "HA startet neu..."- Anzeige ebenso) auf denselben Grund zurückgeführt: datenLaden() rief render() nur bei Änderungen an profil/fahrten/tank/status/batt auf, nie bei einer reinen Änderung des Versions-Sensors. Dazu ein bereits im Code kommentiertes, bisher nur per Tab-Klick umgangenes Rennen beim echten HA-Neustart gefunden und behoben: panel_custom baut das Element neu auf, aber nichts zeichnete es automatisch, bis der Nutzer irgendwo klickte. companion-app erhielt den passenden Fix für die Neustart-Anzeige (Panel-Bug 1 betraf sie nicht, siehe AGENTS.md). Alle drei Fixes live im Testcontainer über einen echten homeassistant.restart bestätigt, nicht nur simuliert. Co-Authored-By: Claude Sonnet 5 --- AGENTS.md | 91 +++++++++++++++++++ companion-app/src/screens/Einstellungen.tsx | 16 +++- .../frontend/audi-dashboard-app.js | 56 +++++++++++- .../audi_dashboard/manifest.json | 2 +- 4 files changed, 162 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c0fe103..5b1f378 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2710,6 +2710,97 @@ reported by the owner as a problem — worth a look if it ever is.) Manifest bumped to `2026.8.24.12` for this fix. +**A fourth same-day fix (2026.8.24.13): the two "needs a manual page refresh" reports the owner filed +right after — turned out to be the exact gap flagged (but not investigated) at the end of the previous +paragraph.** Two distinct reports, one shared root cause on the panel side: + +1. **"Update installieren" needs a manual page refresh to see the restart button.** Traced to + `datenLaden()`: it unconditionally reads `sensor.audi_dashboard_app_version`'s `daten.integration_update` + into the module-level `INTEGRATION_UPDATE` variable on every tick, but only calls `render()` inside the + `geaendert` block — which only compares `profilState`/`fahrtenState`/`tankState`/`statusState`/`battState` + identity, never `versionState`. A version-only state change (exactly what `update_pruefen`/ + `update_installieren` produce) updated the in-memory value but never triggered a re-render on its own — + the UI only caught up whenever some unrelated entity happened to change next, which in practice meant + "never, until a manual reload re-initializes everything from scratch." Fixed by tracking `letzterVersionState` + the same way the other five states are tracked, and calling `render()` directly whenever `versionState`'s + identity changes and nothing else already triggered one (a small `if (versionGeaendert && DATEN_GELADEN) { + render(); return; }` right after the existing `geaendert` block, left untouched). +2. **The "Home Assistant startet neu …" spinner also needs a manual page refresh after the restart + actually completes.** Root cause was different: `INTEGRATION_UPDATE_LAEUFT` is set to `"neustart"` + right before calling the `homeassistant.restart` service, and the *only* place that ever cleared it + back to `null` was the `.catch()` error handler on that same call — deliberate, per section L's original + comment, since a successful restart normally disconnects the page before the success path could run + anyway. But nothing ever picked it back up on the other side: once the websocket reconnects after HA + comes back, `set hass()` fires again (`HASS` was never nulled out while disconnected, so this isn't the + `erster`/first-connect path), but no code ever reset the stuck `"neustart"` flag, so the spinner stayed + forever. Fixed by listening once for the underlying `home-assistant-js-websocket` connection's own + `"ready"` event (fires on every successful (re)connect, confirmed live via + `HASS.connection.addEventListener`) — wired in `set hass()`, guarded by a new module-level + `VERBINDUNG_UEBERWACHT` flag so it's only attached once rather than once per `hass` push — and clearing + `INTEGRATION_UPDATE_LAEUFT`/re-rendering there if it was still `"neustart"`. + +**Checked companion-app for the same two bugs (parity rule) rather than assuming React's own re-render +model made it immune:** bug 1 does *not* reproduce there — `updatePruefenAusloesen()`/ +`updateInstallierenAusloesen()` in `Einstellungen.tsx` already `await neuLaden()` right after the service +call resolves, which re-fetches `appVersionAngabeLesen()` directly over REST and updates React state +synchronously, no push-event dependency. Bug 2, however, **does** reproduce identically: `updateNeustartLaeuft` +is local `useState` in `Einstellungen.tsx`, deliberately left out of the `try/finally` (same reasoning as +the panel), and nothing else ever reset it — `DatenKontext.tsx`'s own reconnect handling +(`api.live.aufVerbindung()` → `neuLaden()` on `"verbunden"`) only refreshes the *shared* `integrationUpdate` +context value, which this screen-local boolean doesn't derive from. Fixed with a small `useEffect` in +`Einstellungen.tsx` watching the already-available `verbindung` context field, clearing +`updateNeustartLaeuft` when it flips to `"verbunden"` while still `true`. + +**Verified (panel, first pass):** `node --check` clean. Live in the test container at `2026.8.24.13`: +clicked "Update installieren" (Gitea test remote genuinely offered `.12`, since that's what had last been +pushed there) — the tile transitioned on its own from "Installiere …" straight to "Version 2026.8.24.12 +installiert … Jetzt neu starten" with **no page reload**, confirming bug 1 fixed. Clicked "Jetzt neu +starten" and waited for the container to fully restart — this is where a third, previously-undiscovered +bug surfaced (below), which blocked a clean confirmation of bug 2 until it was also fixed. + +**A third bug found while verifying bug 2 for real (2026.8.24.14), not a new report — an already-known, +already-commented race in the code that a genuine backend restart finally exercised end to end.** After +the real `homeassistant.restart` completed, the browser tab reconnected but the panel stayed stuck on the +static "Lädt …" placeholder indefinitely — not the Einstellungen page with a lingering spinner (bug 2's +literal symptom), but back on Übersicht, showing nothing. Traced to: Home Assistant's `panel_custom` +mechanism recreates the `` custom element from scratch after a real backend restart +(confirmed live — a fresh `_aufbauen()` run, fresh skeleton DOM), but the JS *module* itself is not +reloaded, so all its module-level state (`CONFIG`/`CAR`/`DATEN_GELADEN`/etc.) survives untouched from +before the restart. The fresh element's first `datenLaden()` call didn't reliably produce a `render()` +(the exact scenario `go()`'s own existing comment already describes: *"Bekanntes Race … auf einem langsam +startenden Backend kann die App auf 'Lädt …' hängen bleiben … der bislang einzige beobachtete Ausweg war +ein Tabwechsel"*), and — unlike `go()`, which unconditionally calls `render()` on every tab click regardless +of this race — nothing about `_aufbauen()` itself ever painted anything beyond the static skeleton. +Confirmed live: clicking any tab immediately showed full correct data (proving `CONFIG`/`CAR` were fine in +memory all along — only the *paint* was missing), exactly matching `go()`'s own workaround comment. +Fixed the same way `go()` already does it, just made unconditional instead of click-triggered: `_aufbauen()` +now calls `render()` immediately after building a fresh skeleton, if `DATEN_GELADEN` is already `true` (i.e. +this is a rebuild after already having loaded once, not the very first paint — that case is correctly left +to `datenLaden()`'s own arrival). This is the real, complete fix for "the app doesn't recover cleanly after +HA restarts without user interaction" — bug 2's spinner-not-clearing report was one visible symptom of it; +this fix (plus the earlier `"ready"`-listener reset of `INTEGRATION_UPDATE_LAEUFT`) is what makes the whole +class of "just restarted, tab open, nobody touches it" scenario self-heal. + +**Verified (panel, final pass) at `2026.8.24.14`:** `node --check` clean. Triggered a real +`homeassistant.restart` directly (`HASS.callService("homeassistant", "restart", {})` from the open tab, no +button click, to isolate this from the update-install flow specifically), confirmed via `docker logs` that +the integration set up a second time end to end (`s6-rc` stop/start sequence, "Audi Dashboard 2026.8.24.14 +eingerichtet" logged twice). The browser tab — never manually reloaded, never clicked — automatically showed +full real content (vehicle name, range, "Nächster Service", …) the moment it reconnected. No stuck "Lädt …", +no manual refresh, no tab click needed. This also confirms bug 2 properly this time (the spinner-clearing +`"ready"` listener from the first pass runs as part of the same reconnect path this test exercises). +- companion-app: `tsc --noEmit` clean, full suite still green at 145/145 (no behavior change needed + testing beyond the existing suite for its own bug-2 fix — a reconnect-driven reset of already-tested + local state, not new business logic; the deeper element-recreation race is panel-specific, since + companion-app is a normal React SPA that doesn't get its component tree torn down and rebuilt by an + external host on backend restart). + +Manifest bumped to `2026.8.24.13`, then `2026.8.24.14` once the deeper race above was found and fixed in +the same live-testing session. `npm run ota` not rerun yet for this round — only the `useEffect` addition +in `Einstellungen.tsx` touches companion-app source (the panel-side fixes don't affect the OTA bundle, +which ships companion-app only), no OTA bundle rebuild triggered until the owner asks or the next round +bundles it together with something else. + --- ## Working conventions (observed — keep them) diff --git a/companion-app/src/screens/Einstellungen.tsx b/companion-app/src/screens/Einstellungen.tsx index d1d8c01..c1210a3 100644 --- a/companion-app/src/screens/Einstellungen.tsx +++ b/companion-app/src/screens/Einstellungen.tsx @@ -4,7 +4,7 @@ * Datenausgabe und Zugang. */ -import { useRef, useState } from "react" +import { useEffect, useRef, useState } from "react" import { ActionButton, Feld, Seg, Switch, Tile } from "@audi-dash/ui" @@ -39,6 +39,7 @@ export function Einstellungen({ rohprofil, otaBuendel, integrationUpdate, + verbindung, api, profilSpeichern, jetztAktualisieren, @@ -139,6 +140,19 @@ export function Einstellungen({ } } + // Ergänzt das absichtliche Fehlen des try/finally oben: die Verbindung + // trennt sich für die Dauer des HA-Neustarts, DatenKontext meldet das über + // `verbindung`/`api.live.aufVerbindung()` und lädt bei "verbunden" bereits + // integrationUpdate & Co. neu (siehe DatenKontext.tsx) - aber + // updateNeustartLaeuft ist reiner Bildschirm-Zustand hier und blieb davon + // unberührt. Ohne diesen Effekt stand "Home Assistant startet neu …" nach + // einem erfolgreichen Neustart weiter da, bis die Seite von Hand neu + // geladen wurde. + useEffect(() => { + if (verbindung === "verbunden" && updateNeustartLaeuft) setzeUpdateNeustartLaeuft(false) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [verbindung]) + if (!einstellungen || !fahrzeug) return null const werte = entwurf ?? einstellungen diff --git a/custom_components/audi_dashboard/frontend/audi-dashboard-app.js b/custom_components/audi_dashboard/frontend/audi-dashboard-app.js index 3849722..0e51238 100644 --- a/custom_components/audi_dashboard/frontend/audi-dashboard-app.js +++ b/custom_components/audi_dashboard/frontend/audi-dashboard-app.js @@ -3302,6 +3302,11 @@ let INTEGRATION_UPDATE = null; // unbestimmt lange dauern (Netzwerk zu Gitea, Entpacken) und die Oberfläche // das sichtbar machen soll - siehe .lade-spinner's eigener Kommentar dazu. let INTEGRATION_UPDATE_LAEUFT = null; +// Nur einmal die "ready"-Verbindung des HA-Websockets belauschen (siehe +// deren Verdrahtung in set hass() unten) - HASS wird bei jedem Tick neu +// zugewiesen, ohne diese Sperre würde sich sonst pro hass-Push ein weiterer +// Listener aufhäufen. +let VERBINDUNG_UEBERWACHT = false; /* Auswahl für "Modell" unter Einrichten - setzt nur CONFIG.fahrzeugtitel (Zeile "Modell" in der Identität, Kopfzeilen, Kalender-Einträge, und den @@ -4960,7 +4965,7 @@ function zustandsDaten(entityId) { return st && st.attributes ? st.attributes.daten : undefined; } -let letzterProfilState = null, letzterFahrtenState = null, letzterTankState = null, letzterStatusState = null, letzterBelegState = null, letzterBattState = null; +let letzterProfilState = null, letzterFahrtenState = null, letzterTankState = null, letzterStatusState = null, letzterBelegState = null, letzterBattState = null, letzterVersionState = null; /* Ergebnis eines Beleg-Uploads verarbeiten (siehe belegEntwurf/belegFehler weiter oben) - kommt über dieselbe Zustand-statt-Rückgabewert-Route wie @@ -5092,6 +5097,18 @@ async function datenLaden(erzwingen) { } if (entitaetenState && entitaetenState.attributes) ENTITAETEN = entitaetenState.attributes.daten; + // sensor.audi_dashboard_app_version änderte sich (z. B. nach "Auf Update + // prüfen"/"Update installieren" - siehe dienste.py's update_pruefen()/ + // update_installieren()) - ohne diesen Vergleich lief zwar die Zuweisung + // von INTEGRATION_UPDATE oben bei jedem Tick mit, aber render() darunter + // feuert nur, wenn der geaendert-Block das anstößt. Der prüfte bisher nur + // profil/fahrten/tank/status/batt: das Ergebnis der Prüfung/Installation + // stand zwar korrekt im Speicher, blieb aber unsichtbar, bis zufällig + // eines der anderen fünf sich ändert - sichtbar als "braucht ein manuelles + // Neuladen der Seite, um den Neustart-Knopf zu zeigen". + const versionGeaendert = versionState !== letzterVersionState; + letzterVersionState = versionState; + if (belegState && belegState !== letzterBelegState) { letzterBelegState = belegState; belegErgebnisVerarbeiten(belegState.attributes && belegState.attributes.daten); @@ -5123,6 +5140,12 @@ async function datenLaden(erzwingen) { } } + // Nur die Version hat sich geändert (siehe versionGeaendert oben), sonst + // nichts - der geaendert-Block darüber ist dafür nicht zuständig (der + // bricht bei fehlendem profil sogar früher ab). Ein eigener, schlanker + // Aufruf statt profilZuConfig()/profilZuCar() & Co. unnötig neu zu rechnen. + if (versionGeaendert && DATEN_GELADEN) { render(); return; } + if (!DATEN_GELADEN) nachladeAnstossen(); // Backend noch nicht bereit (erster Tick nach Neustart) } @@ -5165,6 +5188,23 @@ class AudiDashboardPanel extends HTMLElement { if (erster) { this._aufbauen(); } + // "ready" feuert bei jedem (Wieder-)Verbindungsaufbau des HA-Websockets, + // auch nach einem über den "Jetzt neu starten"-Knopf ausgelösten + // HA-Neustart: die Seite verliert dabei die Verbindung, INTEGRATION_UPDATE_ + // LAEUFT blieb aber auf "neustart" stehen - dessen einzige Rücksetzung + // greift nur im Fehlerfall (siehe deren Klick-Handler), nicht beim + // Erfolgsfall, weil dort niemand mehr zusieht, während HA neu startet. + // Ohne dies blieb der "Home Assistant startet neu …"-Spinner nach einem + // erfolgreichen Neustart stehen, bis der Nutzer die Seite von Hand neu lud. + if (!VERBINDUNG_UEBERWACHT && HASS.connection && HASS.connection.addEventListener) { + VERBINDUNG_UEBERWACHT = true; + HASS.connection.addEventListener("ready", () => { + if (INTEGRATION_UPDATE_LAEUFT === "neustart") { + INTEGRATION_UPDATE_LAEUFT = null; + render(); + } + }); + } datenLaden(false); } get hass() { return HASS; } @@ -5242,6 +5282,20 @@ class AudiDashboardPanel extends HTMLElement { standortMenuVerdrahten(); themeIcon(); setInterval(standAlterTicken, 1000); + + // Frisch aufgebautes Markup zeigt bis hierhin nur das statische "Lädt …"- + // Grundgerüst aus dem Template oben - dasselbe bekannte Rennen wie in + // go()'s Kopfkommentar (siehe dort/nachladeAnstossen): panel_custom baut + // dieses Element bei einem echten HA-Neustart neu auf (ein neues + // customElements-Objekt, nicht dasselbe wiederverwendet), aber die + // modul-globalen Daten (CONFIG/CAR/...) und DATEN_GELADEN überleben das + // unverändert, weil das JS-Modul selbst nicht neu geladen wird. Ohne + // diese Zeile blieb genau dieses frische Grundgerüst für immer stehen, + // bis der Nutzer zufällig einen Tab anklickte (go()'s eigener, + // unbedingter render()-Aufruf) oder die Seite von Hand neu lud - obwohl + // in CONFIG/CAR längst brauchbare (ggf. kurz veraltete, durch den + // nächsten Datenabgleich ohnehin gleich aufgefrischte) Daten lagen. + if (DATEN_GELADEN) render(); } } diff --git a/custom_components/audi_dashboard/manifest.json b/custom_components/audi_dashboard/manifest.json index aae119c..e82d40b 100644 --- a/custom_components/audi_dashboard/manifest.json +++ b/custom_components/audi_dashboard/manifest.json @@ -1,7 +1,7 @@ { "domain": "audi_dashboard", "name": "Audi Dashboard", - "version": "2026.8.24.12", + "version": "2026.8.24.14", "documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md", "issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues", "codeowners": ["@paul"],