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:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user