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:
+62
-16
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user