diff --git a/AGENTS.md b/AGENTS.md index 7b2a299..ac2f219 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2337,6 +2337,73 @@ entirely, not replaced with a space) and un-escapes `\,`/`\;`/`\n`/`\\` before u Verified: `tsc --noEmit` clean, all 134 companion-app tests pass (127 + 7 new), panel JS re-checked with `node --check`. Manifest bumped to `2026.8.24.5`, `npm run ota` rerun (both frontends changed). +### L) Self-update UX: loading feedback and a real restart button (2026-08-24, same day) + +**The self-update feature (section J) got its first genuine live test against the real private +Gitea repo this same day**, and it worked — confirmed directly in the test container's log across +several real attempts (`Update installiert: Version 2026.8.24.5`, no manual file copying involved). +That's the first real end-to-end confirmation beyond the unit tests and structural container-restart +checks section J could offer on its own. + +That same live test exposed two real UX gaps, both raised directly by the owner: + +1. **No feedback during or after the ~15–25 second install** (confirmed from the log's own + timestamps between a "Prüfung" and the matching "installiert" line). The button just sat there + with no state change — indistinguishable from a hang, and after success, the version still read + "2026.8.24.4 installiert" because that field only updates after a real restart, which nothing in + the UI was telling the user to do next beyond a paragraph of text. +2. **A separate, initially confusing "Lädt …" hang the owner saw and asked about.** Traced by + reproducing it directly: `.lade-spinner`/"Lädt …" is this project's own **existing** top-level + bootstrap loading screen (shown whenever `DATEN_GELADEN` is false — i.e., no live connection to + Home Assistant yet), not anything related to the update button specifically. Clicking the new + "Jetzt neu starten" button and watching it live in a browser reproduced the *exact* same screen — + because a `homeassistant.restart` call genuinely does drop the frontend's connection until HA + finishes rebooting, and this is the correct, expected screen for that. The most likely explanation + for the owner's *original* sighting (before this button existed): the Docker Desktop restart from + earlier in the same session dropped the connection the same way. Not a bug — but the fact that it + was indistinguishable from one was the actual, valid complaint: nothing signposted that a restart + was in progress versus something being stuck. + +**Fixed with a three-state UI, researched against Apple HIG's guidance on indeterminate progress** +(unknown-duration network operations get a running indicator, not just a disabled control; on +completion the UI leads straight to the next required action instead of leaving the user to find it +themselves): +- **Prüfen/Installieren, in progress** — a small spinner + "Prüfe …"/"Installiere …", reusing the + panel's own pre-existing `.lade-spinner` (originally built, per its own comment, for the exact same + "don't let 'still loading' look like 'stuck'" reason at app bootstrap) and a new equivalent + `.dm-spinner`/`.dm-ladezeile` pair added to companion-app's `screens.css` (that side had no spinner + at all before, only a button-text swap). +- **On success** — the tile switches directly to "Installation abschließen - Jetzt neu starten", + which calls `homeassistant.restart` (a core HA service any admin could already trigger via + Settings → System → Restart — this button doesn't grant a new capability, it surfaces an existing + one at the point it's actually needed). Its own click shows "Home Assistant startet neu …"; + no `.finally`-style cleanup after that call succeeds, since the connection normally drops before + the promise would ever resolve — only a genuine rejection (service refused, etc.) reaches the + catch block. +- **On a real request-level failure** (not the already-handled "no token"/"bad token" case from + section J, which returns normally with `fehler` set — this is Gitea unreachable, the websocket + itself failing, or any other exception that never made it back from Home Assistant at all) — this + was previously silent in companion-app (a `try/finally` with no `catch` re-throws into an + unhandled promise rejection). Added explicit `catch` blocks on all three actions in both frontends, + each surfacing its own message rather than one shared silently-stale error field. + +- Panel: `INTEGRATION_UPDATE_LAEUFT` (`"pruefen" | "installieren" | "neustart" | null`), the + `data-update` click handler rewritten from `serviceRufen()` (fire-and-forget) to direct + `HASS.callService()` promise chains so the loading flag can wrap them; `vEinst()`'s tile rewritten + with the three states above. +- companion-app: `updateNeustartLaeuft`/`updateAktionFehler` state and `updateNeustartAusloesen()` in + `Einstellungen.tsx`; new `homeAssistantNeuStarten()` in `api/index.ts` (`dienstAufrufen("homeassistant", + "restart")` — note the domain isn't `DIENST_DOMAIN`, this is a core HA service, not one of this + integration's own). + +Verified live end-to-end in the test container via browser automation: triggered the real restart +button, watched the connection drop into the bootstrap "Lädt …" screen exactly as reasoned above, +confirmed via `docker logs` that Home Assistant actually rebooted +(`Audi Dashboard 2026.8.24.6 eingerichtet` after restart, version now genuinely current — not just +the pre-restart file swap). companion-app: `tsc --noEmit` clean, full suite still green. Manifest +bumped to `2026.8.24.6` and `npm run ota` rerun per the standing rule in `VERSIONIERUNG.md` — both +frontends changed (new spinner/restart-button behavior is real, visible UI, not internal-only). + --- ## Working conventions (observed — keep them) diff --git a/companion-app/src/api/index.ts b/companion-app/src/api/index.ts index 29d26e3..2219490 100644 --- a/companion-app/src/api/index.ts +++ b/companion-app/src/api/index.ts @@ -217,6 +217,15 @@ export class DataMetricApi { return this.rest.dienstAufrufen(DIENST_DOMAIN, "update_installieren"); } + /** Startet Home Assistant komplett neu - der letzte Schritt nach + updateInstallieren(), damit die neue Fassung tatsächlich geladen wird + (ein reiner Reload reicht dafür nicht, siehe AGENTS.md Abschnitt J). + Kein audi_dashboard-Dienst, deshalb der Bereich "homeassistant" statt + DIENST_DOMAIN. */ + homeAssistantNeuStarten(): Promise { + return this.rest.dienstAufrufen("homeassistant", "restart"); + } + /* ------------------------------------------- Import aus dem HA-Verlauf Bewusst NICHT über die Warteschlange, anders als die übrigen schreibenden Vorgänge: der Import ist keine Eingabe, die man im Funkloch diff --git a/companion-app/src/screens/Einstellungen.tsx b/companion-app/src/screens/Einstellungen.tsx index 14fbb79..d1d8c01 100644 --- a/companion-app/src/screens/Einstellungen.tsx +++ b/companion-app/src/screens/Einstellungen.tsx @@ -53,6 +53,8 @@ export function Einstellungen({ const [otaFehler, setzeOtaFehler] = useState(null) const [updatePruefenLaeuft, setzeUpdatePruefenLaeuft] = useState(false) const [updateInstallierenLaeuft, setzeUpdateInstallierenLaeuft] = useState(false) + const [updateNeustartLaeuft, setzeUpdateNeustartLaeuft] = useState(false) + const [updateAktionFehler, setzeUpdateAktionFehler] = useState(null) const dateiwahl = useRef(null) const otaUpdateVerfuegbar = otaMoeglich() && buendelPasst(otaBuendel, eigeneVersion()) @@ -82,12 +84,21 @@ export function Einstellungen({ // danach über neuLaden() den neuen Stand von // sensor.audi_dashboard_app_version - der Fehlertext bei fehlendem Token // kommt von dort mit, eine eigene "ist ein Token eingetragen?"-Prüfung - // gibt es bewusst nicht. + // gibt es bewusst nicht. updateAktionFehler fängt zusätzlich echte + // Verbindungsfehler ab (dienste.py behandelt AktualisierungsFehler selbst + // und liefert 200 zurück - hier landet nur, was gar nicht erst bei Home + // Assistant ankam), sonst bliebe ein solcher Fehlschlag ganz ohne + // Rückmeldung. const updatePruefenAusloesen = async () => { setzeUpdatePruefenLaeuft(true) + setzeUpdateAktionFehler(null) try { await api.updatePruefen() await neuLaden() + } catch (fehler) { + setzeUpdateAktionFehler( + fehler instanceof Error ? fehler.message : "Die Prüfung konnte nicht ausgeführt werden.", + ) } finally { setzeUpdatePruefenLaeuft(false) } @@ -95,14 +106,39 @@ export function Einstellungen({ const updateInstallierenAusloesen = async () => { setzeUpdateInstallierenLaeuft(true) + setzeUpdateAktionFehler(null) try { await api.updateInstallieren() await neuLaden() + } catch (fehler) { + setzeUpdateAktionFehler( + fehler instanceof Error ? fehler.message : "Das Update konnte nicht installiert werden.", + ) } finally { setzeUpdateInstallierenLaeuft(false) } } + // Nach einer erfolgreichen Installation zeigt die Kachel direkt den + // nächsten nötigen Schritt statt den Nutzer selbst suchen zu lassen - ein + // reiner Reload reicht nicht, siehe AGENTS.md Abschnitt J. Kein try/finally + // um setzeUpdateNeustartLaeuft: bricht die Verbindung normalerweise durch + // den Neustart selbst ab, bevor der Aufruf überhaupt zurückkehrt. + const updateNeustartAusloesen = async () => { + setzeUpdateNeustartLaeuft(true) + setzeUpdateAktionFehler(null) + try { + await api.homeAssistantNeuStarten() + } catch (fehler) { + setzeUpdateAktionFehler( + fehler instanceof Error + ? fehler.message + : "Home Assistant konnte nicht neu gestartet werden.", + ) + setzeUpdateNeustartLaeuft(false) + } + } + if (!einstellungen || !fahrzeug) return null const werte = entwurf ?? einstellungen @@ -443,15 +479,36 @@ export function Einstellungen({ Integration-Update - {integrationUpdate?.installiert ? ( + {/* HIG "Loading": Netzwerk zu Gitea plus Entpacken dauert unbestimmt + lang, ohne verlässliche Fortschrittsangabe - ein laufender + Indikator statt eines nur deaktivierten Knopfes verhindert, dass + das wie ein Hänger aussieht. Nach Erfolg führt der Knopf direkt + zum nächsten nötigen Schritt (Neustart) statt den Nutzer selbst + suchen zu lassen - ein reiner Reload reicht nicht (AGENTS.md + Abschnitt J). */} + {updateInstallierenLaeuft ? ( +
+
+ ) : updateNeustartLaeuft ? ( +
+
+ ) : integrationUpdate?.installiert ? ( <>

- Version {integrationUpdate.version} installiert. Home Assistant neu starten, damit - die neue Fassung geladen wird — Einstellungen → System → Neu starten. + Version {integrationUpdate.version} installiert - die neue Fassung wird erst nach + einem Neustart geladen.

- void updatePruefenAusloesen()} disabled={updatePruefenLaeuft}> - {updatePruefenLaeuft ? "Prüfe …" : "Erneut prüfen"} + void updateNeustartAusloesen()}> + Installation abschließen - Jetzt neu starten
@@ -466,14 +523,18 @@ export function Einstellungen({ nicht betroffen.

- void updateInstallierenAusloesen()} - disabled={updateInstallierenLaeuft} - > - {updateInstallierenLaeuft ? "Installiere …" : "Update installieren"} + void updateInstallierenAusloesen()}> + Update installieren
+ ) : updatePruefenLaeuft ? ( +
+
) : ( <>

@@ -482,13 +543,18 @@ export function Einstellungen({ : "Noch nicht geprüft."}

- void updatePruefenAusloesen()} disabled={updatePruefenLaeuft}> - {updatePruefenLaeuft ? "Prüfe …" : "Auf Update prüfen"} + void updatePruefenAusloesen()}> + Auf Update prüfen
)} - {integrationUpdate?.fehler &&

{integrationUpdate.fehler}

} + {updateAktionFehler &&

{updateAktionFehler}

} + {!updateAktionFehler && + integrationUpdate?.fehler && + !updatePruefenLaeuft && + !updateInstallierenLaeuft && + !updateNeustartLaeuft &&

{integrationUpdate.fehler}

}
diff --git a/companion-app/src/stile/screens.css b/companion-app/src/stile/screens.css index 6cc10b3..8b46391 100644 --- a/companion-app/src/stile/screens.css +++ b/companion-app/src/stile/screens.css @@ -609,6 +609,34 @@ background: var(--tile-2); } +/* ------------------------------------------------------------ Ladezustand */ + +/* HIG "Loading": ein unbestimmt langer Vorgang (Netzwerk, kein verlässlicher + Fortschritt) bekommt einen laufenden Indikator statt eines stumm + daliegenden Knopfes - sonst nicht von einem Hänger zu unterscheiden. + Deckungsgleich mit .lade-spinner im Panel. */ +.dm-spinner { + display: inline-block; + width: 16px; + height: 16px; + flex: none; + border: 2px solid var(--line); + border-top-color: var(--fg2); + border-radius: 50%; + animation: dm-spin 0.8s linear infinite; +} +@keyframes dm-spin { + to { + transform: rotate(360deg); + } +} +.dm-ladezeile { + display: flex; + align-items: center; + gap: var(--sp-2); + margin-top: var(--sp-3); +} + /* ------------------------------------------------------------ Meldungen */ .dm-fehler { diff --git a/custom_components/audi_dashboard/frontend/app/bundle.json b/custom_components/audi_dashboard/frontend/app/bundle.json index d38d012..c2f1b75 100644 --- a/custom_components/audi_dashboard/frontend/app/bundle.json +++ b/custom_components/audi_dashboard/frontend/app/bundle.json @@ -1 +1 @@ -{"version":"2026.8.24.5","sha256":"378a1cbc7f5daa3d8c5d74734a4c09f30557b243a8ce4d720240bf911fbeff76","bytes":232096,"gebaut":"2026-08-24T10:29:33Z"} \ No newline at end of file +{"version":"2026.8.24.6","sha256":"e0d8c7a6f4cc7fddc59b252e02e4ebf50002f3bd4e317ddb315670ef13299d75","bytes":232382,"gebaut":"2026-08-24T10:44:09Z"} \ No newline at end of file diff --git a/custom_components/audi_dashboard/frontend/app/bundle.zip b/custom_components/audi_dashboard/frontend/app/bundle.zip index 9bc849b..6c15fa1 100644 Binary files a/custom_components/audi_dashboard/frontend/app/bundle.zip and b/custom_components/audi_dashboard/frontend/app/bundle.zip differ diff --git a/custom_components/audi_dashboard/frontend/audi-dashboard-app.js b/custom_components/audi_dashboard/frontend/audi-dashboard-app.js index 5e3ae18..8a3ad33 100644 --- a/custom_components/audi_dashboard/frontend/audi-dashboard-app.js +++ b/custom_components/audi_dashboard/frontend/audi-dashboard-app.js @@ -2426,16 +2426,32 @@ function vEinst() {
Integration-Update ${(() => { const u = INTEGRATION_UPDATE; + const laeuft = INTEGRATION_UPDATE_LAEUFT; // Ersetzt install.ps1 als laufenden Update-Weg (Windows Smart App // Control blockiert dessen Ausführung zuverlässig) - install.ps1 // bleibt nur noch für die Erstinstallation nötig, bevor die // Integration überhaupt läuft und sich selbst aktualisieren kann. + // + // HIG "Loading": Netzwerk zu Gitea plus Entpacken dauert unbestimmt + // lang, ohne verlässliche Fortschrittsangabe - ein laufender + // Indikator statt eines stumm daliegenden Knopfes verhindert, dass + // das wie ein Hänger aussieht (derselbe Grund wie beim + // Erstladen-Spinner, .lade-spinner). Nach Erfolg führt der Knopf + // direkt zum nächsten nötigen Schritt (Neustart), statt den Nutzer + // selbst suchen zu lassen. + const spinner = (text) => `
+ + ${esc(text)} +
`; + if (laeuft === "installieren") return spinner("Installiere …"); + if (laeuft === "neustart") return spinner("Home Assistant startet neu …"); if (u && u.installiert) { return ` - Version ${esc(u.version)} installiert. Home Assistant neu starten, damit die neue - Fassung geladen wird - Einstellungen -> System -> Neu starten. - `; + Version ${esc(u.version)} installiert - die neue Fassung wird erst nach einem Neustart + geladen. + `; } + if (laeuft === "pruefen") return spinner("Prüfe …"); if (u && u.verfuegbar) { return `
Verfügbare Version
${esc(u.version)}
@@ -2448,7 +2464,7 @@ function vEinst() { ${u ? `Integration ist aktuell${u.geprueft_am ? ` (geprüft ${dedat(new Date(u.geprueft_am))})` : ""}.` : "Noch nicht geprüft."} `; })()} - ${INTEGRATION_UPDATE && INTEGRATION_UPDATE.fehler + ${INTEGRATION_UPDATE && INTEGRATION_UPDATE.fehler && !INTEGRATION_UPDATE_LAEUFT ? `${esc(INTEGRATION_UPDATE.fehler)}` : ""}
`; @@ -3156,6 +3172,12 @@ let APP_VERSION = null; // geprüft wurde. Ersetzt install.ps1 als Update-Weg, das Windows Smart App // Control zuverlässig blockiert; siehe vVersion()/Einstellungen-Ansicht. let INTEGRATION_UPDATE = null; +// "pruefen" | "installieren" | "neustart" | null - während einer dieser drei +// Aktionen läuft, siehe vEinst()'s Integration-Update-Kachel. Eigens statt +// über serviceRufen() (das feuert-und-vergisst), weil diese drei Aktionen +// 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; /* Auswahl für "Modell" unter Einrichten - setzt nur CONFIG.fahrzeugtitel (Zeile "Modell" in der Identität, Kopfzeilen, Kalender-Einträge, und den @@ -4515,8 +4537,33 @@ function ereignisseVerdrahten() { return; } const upd = e.target.closest("[data-update]"); - if (upd && upd.dataset.update === "pruefen") { serviceRufen("update_pruefen", {}); return; } - if (upd && upd.dataset.update === "installieren") { serviceRufen("update_installieren", {}); return; } + if (upd) { + const aktion = upd.dataset.update; + // Eigene Promise-Kette statt serviceRufen() (das feuert-und-vergisst) - + // diese drei Aktionen brauchen einen sichtbaren Ladezustand, siehe + // INTEGRATION_UPDATE_LAEUFT's Kommentar an der Deklaration. + if (aktion === "pruefen" || aktion === "installieren") { + INTEGRATION_UPDATE_LAEUFT = aktion; render(); + HASS.callService(DOMAIN, `update_${aktion}`, {}) + .catch((err) => { + hinweis( + aktion === "pruefen" ? "Prüfung fehlgeschlagen" : "Installation fehlgeschlagen", + err && err.message ? err.message : "Der Dienst konnte nicht ausgeführt werden.", + ); + }) + .finally(() => { INTEGRATION_UPDATE_LAEUFT = null; render(); }); + } else if (aktion === "neustart") { + INTEGRATION_UPDATE_LAEUFT = "neustart"; render(); + HASS.callService("homeassistant", "restart", {}).catch((err) => { + // Bricht die Verbindung normalerweise durch den Neustart selbst ab, + // bevor diese Zeile überhaupt liefe - nur ein echter Fehlschlag + // (Dienst abgelehnt, o.ä.) kommt hier tatsächlich an. + hinweis("Neustart fehlgeschlagen", err && err.message ? err.message : "Home Assistant konnte nicht neu gestartet werden."); + INTEGRATION_UPDATE_LAEUFT = null; render(); + }); + } + return; + } const cs = e.target.closest("[data-csv]"); if (cs) { const w = cs.dataset.csv; diff --git a/custom_components/audi_dashboard/manifest.json b/custom_components/audi_dashboard/manifest.json index 54a6a3c..6520c29 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.5", + "version": "2026.8.24.6", "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"],