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 ? (
+
+
+
+ Installiere …
+
+
+ ) : updateNeustartLaeuft ? (
+
+
+
+ Home Assistant startet neu …
+
+
+ ) : 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.
}
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 `