Selbst-Update: Ergebnis erscheint ohne Neuladen, Panel übersteht Neustart
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
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 `<audi-dashboard-panel>` 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)
|
## Working conventions (observed — keep them)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* Datenausgabe und Zugang.
|
* Datenausgabe und Zugang.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useRef, useState } from "react"
|
import { useEffect, useRef, useState } from "react"
|
||||||
|
|
||||||
import { ActionButton, Feld, Seg, Switch, Tile } from "@audi-dash/ui"
|
import { ActionButton, Feld, Seg, Switch, Tile } from "@audi-dash/ui"
|
||||||
|
|
||||||
@@ -39,6 +39,7 @@ export function Einstellungen({
|
|||||||
rohprofil,
|
rohprofil,
|
||||||
otaBuendel,
|
otaBuendel,
|
||||||
integrationUpdate,
|
integrationUpdate,
|
||||||
|
verbindung,
|
||||||
api,
|
api,
|
||||||
profilSpeichern,
|
profilSpeichern,
|
||||||
jetztAktualisieren,
|
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
|
if (!einstellungen || !fahrzeug) return null
|
||||||
const werte = entwurf ?? einstellungen
|
const werte = entwurf ?? einstellungen
|
||||||
|
|
||||||
|
|||||||
@@ -3302,6 +3302,11 @@ let INTEGRATION_UPDATE = null;
|
|||||||
// unbestimmt lange dauern (Netzwerk zu Gitea, Entpacken) und die Oberfläche
|
// unbestimmt lange dauern (Netzwerk zu Gitea, Entpacken) und die Oberfläche
|
||||||
// das sichtbar machen soll - siehe .lade-spinner's eigener Kommentar dazu.
|
// das sichtbar machen soll - siehe .lade-spinner's eigener Kommentar dazu.
|
||||||
let INTEGRATION_UPDATE_LAEUFT = null;
|
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
|
/* Auswahl für "Modell" unter Einrichten - setzt nur CONFIG.fahrzeugtitel
|
||||||
(Zeile "Modell" in der Identität, Kopfzeilen, Kalender-Einträge, und den
|
(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;
|
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
|
/* Ergebnis eines Beleg-Uploads verarbeiten (siehe belegEntwurf/belegFehler
|
||||||
weiter oben) - kommt über dieselbe Zustand-statt-Rückgabewert-Route wie
|
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;
|
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) {
|
if (belegState && belegState !== letzterBelegState) {
|
||||||
letzterBelegState = belegState;
|
letzterBelegState = belegState;
|
||||||
belegErgebnisVerarbeiten(belegState.attributes && belegState.attributes.daten);
|
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)
|
if (!DATEN_GELADEN) nachladeAnstossen(); // Backend noch nicht bereit (erster Tick nach Neustart)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5165,6 +5188,23 @@ class AudiDashboardPanel extends HTMLElement {
|
|||||||
if (erster) {
|
if (erster) {
|
||||||
this._aufbauen();
|
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);
|
datenLaden(false);
|
||||||
}
|
}
|
||||||
get hass() { return HASS; }
|
get hass() { return HASS; }
|
||||||
@@ -5242,6 +5282,20 @@ class AudiDashboardPanel extends HTMLElement {
|
|||||||
standortMenuVerdrahten();
|
standortMenuVerdrahten();
|
||||||
themeIcon();
|
themeIcon();
|
||||||
setInterval(standAlterTicken, 1000);
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"domain": "audi_dashboard",
|
"domain": "audi_dashboard",
|
||||||
"name": "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",
|
"documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md",
|
||||||
"issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues",
|
"issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues",
|
||||||
"codeowners": ["@paul"],
|
"codeowners": ["@paul"],
|
||||||
|
|||||||
Reference in New Issue
Block a user