Phase 6: Datenanbindung, Ersteinrichtung und Offline-Anzeige

Die App spricht jetzt echt mit Home Assistant. Ein Datenkontext buendelt die
vier Datentoepfe, haengt sich an den WebSocket-Ereignisstrom und reicht
Verbindungszustand und Warteschlange nach aussen; die Screens kennen weder
REST noch WebSocket. Ersteinrichtung prueft die Zugangsdaten, bevor sie sie
speichert, und unterscheidet in der Fehlermeldung zwischen abgelehntem Token
und nicht erreichbarem Server - zwei Faelle, die voellig verschiedene
Reaktionen verlangen.

Zwei echte Fehler, die erst der Test gegen die laufende Instanz zutage
brachte:

1. Die Typen der Datenschicht deklarierten tank_prozent, sicher_abgestellt
   und sicherheit. Das Backend schreibt tankprozent, gesichert und
   sicherheitscheck - die Oberflaeche haette still ueberall undefined
   gelesen, ohne dass irgendetwas fehlgeschlagen waere.
2. Der Profil-Adapter reichte lebende Verweise ins Rohprofil durch. Ein
   Formular haette damit den Rohstand mitveraendert und die Zusage gebrochen,
   den vom Backend fortgeschriebenen Reifen-Kilometerstand nie zu
   ueberschreiben. Abschnitte werden jetzt kopiert.

Ausserdem zwei Parameter-Eigenschaften in der Datenschicht aufgeloest, weil
Node sie im Strip-Modus nicht uebersetzt und genau darueber die Rauchtests
laufen.

Belegt durch neun Pruefungen gegen die laufende Instanz (Lesen, Dienstaufruf,
vollstaendiger Warteschlangenumlauf, WebSocket-Anmeldung) und 21 Unit-Tests.
This commit is contained in:
Paul Nothaft
2026-08-11 11:02:44 +02:00
parent 5c0c2d1898
commit 0103508cea
13 changed files with 1358 additions and 25 deletions
+62 -16
View File
@@ -1,35 +1,81 @@
import { useCallback, useState } from "react"
import { useCallback, useEffect, useMemo, useState } from "react"
import { DataMetricApi, zugangLesen } from "./api"
import { DatenAnbieter, useDaten } from "./daten/DatenKontext"
import { Shell } from "./Shell"
import type { Route, SeitenName } from "./navigation"
import { useTabBeschriftung, useTheme } from "./theme"
import { Platzhalter } from "./screens/Platzhalter"
import { Einrichtung } from "./screens/Einrichtung"
import { Hinweisleiste } from "./screens/Hinweisleiste"
import { SeiteFuer } from "./screens/register"
import "./stile/rahmen.css"
import "./stile/screens.css"
type Startzustand = "pruefen" | "einrichten" | "bereit"
/**
* Wurzel der App. Hält den Navigationszustand und das Theme; alles Weitere
* hängt darunter. Das äußerste Element trägt `ads-root` und `data-theme` —
* ohne diese Wurzelklasse greifen die Design-Tokens nicht
* (design-system/.design-sync/conventions.md).
* Wurzel der App. Entscheidet zuerst, ob Zugangsdaten vorliegen — ohne sie
* gibt es nichts anzuzeigen außer der Ersteinrichtung. Danach hängt alles
* unter dem Datenkontext.
*/
export function App() {
const [route, setzeRoute] = useState<Route>({ name: "home" })
const [theme] = useTheme()
const [tabBeschriftung] = useTabBeschriftung()
const [start, setzeStart] = useState<Startzustand>("pruefen")
const geheZu = useCallback((name: SeitenName, id?: string) => {
setzeRoute(id === undefined ? { name } : { name, id })
// Beim Seitenwechsel immer oben anfangen — sonst erbt die neue Seite die
// Scrollposition der alten.
document.querySelector(".dm-inhalt")?.scrollTo({ top: 0 })
// Genau eine Instanz für die Lebensdauer der App: sie hält die
// WebSocket-Verbindung und die Warteschlange.
const api = useMemo(() => new DataMetricApi(), [])
useEffect(() => {
void zugangLesen().then((zugang) => setzeStart(zugang ? "bereit" : "einrichten"))
}, [])
return (
<div className="ads-root dm-wurzel" data-theme={theme}>
<Shell route={route} geheZu={geheZu} tabBeschriftung={tabBeschriftung}>
<Platzhalter route={route} geheZu={geheZu} />
</Shell>
{start === "pruefen" && <div className="dm-laden">Einen Moment </div>}
{start === "einrichten" && <Einrichtung fertig={() => setzeStart("bereit")} />}
{start === "bereit" && (
<DatenAnbieter api={api}>
<AngemeldeteApp beiAbmeldung={() => setzeStart("einrichten")} />
</DatenAnbieter>
)}
</div>
)
}
function AngemeldeteApp({ beiAbmeldung }: { beiAbmeldung: () => void }) {
const [route, setzeRoute] = useState<Route>({ name: "home" })
const [tabBeschriftung, setzeTabBeschriftung] = useTabBeschriftung()
const daten = useDaten()
const geheZu = useCallback((name: SeitenName, id?: string) => {
setzeRoute(id === undefined ? { name } : { name, id })
document.querySelector(".dm-inhalt")?.scrollTo({ top: 0 })
}, [])
// Ein abgelaufener oder zurückgezogener Token führt zurück in die
// Ersteinrichtung statt in eine endlose Fehlerschleife.
useEffect(() => {
if (daten.verbindung === "nicht_angemeldet") beiAbmeldung()
}, [daten.verbindung, beiAbmeldung])
return (
<Shell
route={route}
geheZu={geheZu}
tabBeschriftung={tabBeschriftung}
hinweis={
<Hinweisleiste verbindung={daten.verbindung} warteschlange={daten.warteschlange} />
}
>
<SeiteFuer
route={route}
geheZu={geheZu}
tabBeschriftung={tabBeschriftung}
setzeTabBeschriftung={setzeTabBeschriftung}
beiAbmeldung={beiAbmeldung}
/>
</Shell>
)
}