Selbst-Update: Ladezustand und echter Neustart-Knopf

Der erste echte Live-Test des Selbst-Updates gegen das private Gitea-Repo
lief erfolgreich (mehrfach im Test-Container bestätigt) - deckte aber zwei
UX-Lücken auf, die der Owner direkt gemeldet hat:

- Prüfen/Installieren gaben während der 15-25 Sekunden dauernden
  Netzwerkaktion keine sichtbare Rückmeldung - nicht von einem Hänger zu
  unterscheiden. Jetzt ein Ladeindikator (Panel: wiederverwendetes
  .lade-spinner; companion-app: neues .dm-spinner-Äquivalent, gab es dort
  noch gar nicht).
- Nach erfolgreicher Installation stand nur ein Hinweistext da, kein Weg
  zum eigentlich nötigen nächsten Schritt. Der Knopf wechselt jetzt zu
  "Installation abschließen - Jetzt neu starten" und stößt
  homeassistant.restart direkt an.

Das separat gemeldete "Lädt"-Hängenbleiben war kein Bug: reproduziert durch
Live-Test des neuen Neustart-Knopfs - eine echte HA-Verbindungsunterbrechung
während eines Neustarts zeigt exakt denselben, bereits bestehenden
Bootstrap-Ladebildschirm. Vermutlich derselbe Effekt durch den
Docker-Neustart früher in dieser Sitzung.

Verifiziert im Test-Container per Browser-Automatisierung: Neustart-Knopf
ausgelöst, Verbindungsabbruch beobachtet, per docker logs bestätigt, dass
Home Assistant tatsächlich neu gestartet ist und die neue Version aktiv
wurde.

Details in AGENTS.md, Abschnitt L.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-24 12:44:47 +02:00
parent 7365180550
commit 146f809e9b
8 changed files with 239 additions and 22 deletions
+9
View File
@@ -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<unknown> {
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
+80 -14
View File
@@ -53,6 +53,8 @@ export function Einstellungen({
const [otaFehler, setzeOtaFehler] = useState<string | null>(null)
const [updatePruefenLaeuft, setzeUpdatePruefenLaeuft] = useState(false)
const [updateInstallierenLaeuft, setzeUpdateInstallierenLaeuft] = useState(false)
const [updateNeustartLaeuft, setzeUpdateNeustartLaeuft] = useState(false)
const [updateAktionFehler, setzeUpdateAktionFehler] = useState<string | null>(null)
const dateiwahl = useRef<HTMLInputElement | null>(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({
<Tile>
<span className="ads-eyebrow">Integration-Update</span>
{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 ? (
<div className="dm-ladezeile">
<span className="dm-spinner" aria-hidden="true" />
<span className="dm-fussnote" style={{ margin: 0 }}>
Installiere
</span>
</div>
) : updateNeustartLaeuft ? (
<div className="dm-ladezeile">
<span className="dm-spinner" aria-hidden="true" />
<span className="dm-fussnote" style={{ margin: 0 }}>
Home Assistant startet neu
</span>
</div>
) : integrationUpdate?.installiert ? (
<>
<p className="dm-fussnote">
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.
</p>
<div className="dm-knopfreihe">
<ActionButton onClick={() => void updatePruefenAusloesen()} disabled={updatePruefenLaeuft}>
{updatePruefenLaeuft ? "Prüfe …" : "Erneut prüfen"}
<ActionButton onClick={() => void updateNeustartAusloesen()}>
Installation abschließen - Jetzt neu starten
</ActionButton>
</div>
</>
@@ -466,14 +523,18 @@ export function Einstellungen({
nicht betroffen.
</p>
<div className="dm-knopfreihe">
<ActionButton
onClick={() => void updateInstallierenAusloesen()}
disabled={updateInstallierenLaeuft}
>
{updateInstallierenLaeuft ? "Installiere …" : "Update installieren"}
<ActionButton onClick={() => void updateInstallierenAusloesen()}>
Update installieren
</ActionButton>
</div>
</>
) : updatePruefenLaeuft ? (
<div className="dm-ladezeile">
<span className="dm-spinner" aria-hidden="true" />
<span className="dm-fussnote" style={{ margin: 0 }}>
Prüfe
</span>
</div>
) : (
<>
<p className="dm-fussnote">
@@ -482,13 +543,18 @@ export function Einstellungen({
: "Noch nicht geprüft."}
</p>
<div className="dm-knopfreihe">
<ActionButton onClick={() => void updatePruefenAusloesen()} disabled={updatePruefenLaeuft}>
{updatePruefenLaeuft ? "Prüfe …" : "Auf Update prüfen"}
<ActionButton onClick={() => void updatePruefenAusloesen()}>
Auf Update prüfen
</ActionButton>
</div>
</>
)}
{integrationUpdate?.fehler && <p className="dm-fehler">{integrationUpdate.fehler}</p>}
{updateAktionFehler && <p className="dm-fehler">{updateAktionFehler}</p>}
{!updateAktionFehler &&
integrationUpdate?.fehler &&
!updatePruefenLaeuft &&
!updateInstallierenLaeuft &&
!updateNeustartLaeuft && <p className="dm-fehler">{integrationUpdate.fehler}</p>}
</Tile>
<Tile>
+28
View File
@@ -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 {