Phase 9: Tests fuer die portierte Rechenlogik

41 weitere Tests fuer Statistik, Service-Prognose und Formatierung. Die
Faelle sind entlang der Regeln gewaehlt, die beim Portieren wichtig waren:
Wochenbeginn Montag, Nachtspanne ueber Mitternacht, mengengewichteter
Durchschnittspreis statt Mittel der Einzelpreise, Plausibilitaetsfenster beim
Langzeitverbrauch, Neustart der Oelprognose ab dem letzten Wechsel samt
Zeitlimit-Deckelung, BOM und Semikolon in der CSV-Ausgabe.

Ein Test war zunaechst falsch: er verglich das UTC-Datum gegen eine
Ortszeit-Erwartung. Der Code rechnet bewusst lokal - sonst begaenne der
Monat in unserer Zeitzone einen Tag zu frueh.

companion-app/README.md auf den erreichten Stand gebracht.

Gesamt: 90 Unit- und Rendertests plus 9 Pruefungen gegen die laufende
Home-Assistant-Instanz.
This commit is contained in:
Paul Nothaft
2026-08-11 11:21:56 +02:00
parent 571d12310f
commit a68817065b
4 changed files with 451 additions and 60 deletions
+55 -60
View File
@@ -1,81 +1,76 @@
# DataMetric360 — companion app
# companion-app — DataMetric360
Private vehicle app for one car. Ships three ways from one codebase: native iOS and Android via
Capacitor, and embedded as a plain iframe in the Home Assistant dashboard. Architecture and the
decisions behind it: [`../COMPANION_APP_ARCHITECTURE.md`](../COMPANION_APP_ARCHITECTURE.md).
Private Fahrzeug-App für einen Audi RS 4 Avant competition. Läuft als Web-App
im Browser, als PWA auf dem Homescreen, später über Capacitor nativ auf
iOS/Android und eingebettet als Iframe im Home-Assistant-Dashboard.
## Status
**Kein eigenes Backend.** Die App spricht direkt mit der REST- und
WebSocket-Schnittstelle von Home Assistant und ruft dieselben
`pyscript.audi_dashboard_*`-Dienste auf wie das bestehende Panel. Alle Daten
liegen auf dem eigenen Server; das Gerät hält nur den Zugangstoken und einen
Zwischenspeicher für die Offline-Anzeige.
**Data layer only.** No UI yet — the screens come from the Claude Design draft
([`../DESIGN_BRIEF_DATAMETRIC360.md`](../DESIGN_BRIEF_DATAMETRIC360.md)), and get implemented on top
of `design-system/`'s React components once that draft settles. This package was written first on
purpose: how the app talks to Home Assistant doesn't depend on what the screens look like, so it
survives every design iteration untouched.
Architektur: `../COMPANION_APP_ARCHITECTURE.md` · Umsetzungsschritte:
`../UMSETZUNGSPLAN.md` · Projektstand: `../AGENTS.md`
Not yet added (deliberately, they'd be guesses today): React/Vite, Capacitor, the native secure-storage
adapter, and the Audi brand assets (fonts/rings/badges — those come from `homeassistant/www/` at
implementation time, never into `design-system/`, see the licence note in the architecture doc).
## Loslegen
## Layout
```
src/api/
├── types.ts HA state shapes + domain types (Fahrt, Tankvorgang, …) + the entity-ID table
├── umgebung.ts runtime detection (capacitor/iframe/browser), credential storage, URL helpers
├── rest.ts REST client — replaces hass.states / hass.callService
├── live.ts WebSocket client — push updates, auto-reconnect with backoff
├── warteschlange.ts offline queue for writes made without a connection
└── index.ts DataMetricApi — ties the three together, exposes the domain operations
scripts/smoke.ts verification against a running HA instance
```bash
npm install # im Repo-Wurzelverzeichnis, nicht hier (npm-Workspace)
npm run build:ds # Design-System bauen, die App importiert aus dessen dist/
npm run dev --workspace datametric360
```
Comments are in German, matching the rest of the project.
Beim ersten Start fragt die App nach Server-Adresse und Zugangstoken. Für die
Entwicklung setzt `../testumgebung/aufsetzen.sh` eine vollständige
Home-Assistant-Instanz mit dem echten Backend auf.
## What replaces what
## Aufbau
The old panel got a `hass` object injected by `panel_custom`. That object only exists inside the HA
frontend, which is exactly why the old panel can't run as a standalone app. The mapping:
| Old panel | Here |
| Ordner | Inhalt |
|---|---|
| `hass.states[id].attributes.daten` | `rest.datenLesen(id)` / `DataMetricApi.profilLesen()` etc. |
| `hass.callService(...)` | `warteschlange.einreihen(...)` via the `DataMetricApi` methods |
| automatic re-render on state push | `live.aufZustand(...)` |
| `src/api/` | Datenschicht: REST, WebSocket mit Wiederverbindung, Offline-Warteschlange, Zugangsdaten |
| `src/daten/` | Fachlogik: Profil-Umrechnung, Statistik, Service-Prognose, Datenkontext |
| `src/screens/` | Die 21 Seiten der App |
| `src/stile/` | Layout, Bildschirmbausteine, Audi-Schrift |
| `src/assets/audi/` | Schrift, Vier-Ringe, Typenschilder — **lizenzpflichtig, siehe unten** |
| `src/tests/` | Testaufbau und Beispieldaten |
Same entities, same pyscript services, same backend files written — only the transport changes.
Gestaltung kommt vollständig aus `@audi-dash/ui` (`../design-system/`). Neue
Komponenten entstehen dort, nicht hier.
Every **write** goes through the queue rather than straight to REST. That's what makes offline edits
behave the same as online ones, just delayed: a receipt photographed in a dead zone is persisted and
sent when the connection returns, surviving an app restart in between.
## Commands
Node is installed at `C:\Program Files\nodejs` but is **not on PATH** — prefix it:
## Prüfen
```bash
$env:Path = "C:\Program Files\nodejs;" + $env:Path
npm run typecheck # TypeScript, sehr streng (exactOptionalPropertyTypes u. a.)
npm run test # 90 Tests: Rechenlogik, Layoutwechsel, alle Seiten gerendert
npm run smoke # Adressbildung und Endpunkte, ohne Token
npm run smoke:auth # gegen die laufende Testinstanz, mit Token
npm run build # Produktionsbündel
```
```bash
npm run typecheck
```
`smoke:auth` liest Adresse und Token aus `.env.local` (schreibt
`../testumgebung/aufsetzen.sh`, gitignored). Ohne die Datei überspringt der
Test sich selbst, statt fehlzuschlagen.
```bash
npm run smoke
```
Die Rendertests decken alle 21 Seiten in beiden Layouts ab — schmal mit
Tab-Leiste, breit mit Seitenleiste.
`smoke` targets `http://localhost:18123` (the `audi_ha_test` Docker container) by default; pass a
different base URL as the first argument.
## Rechtlicher Hinweis
## Verification status (2026-08-10)
`src/assets/audi/` enthält die Hausschrift Audi Type, das Vier-Ringe-Zeichen
und die Modell-Typenschilder. Diese sind lizenz- beziehungsweise
markenrechtlich geschützt und ausschließlich für diese eine private,
unveröffentlichte Installation freigegeben (`../bauauftrag.md` §8/§12).
`tsc --noEmit` clean under `strict` plus `noUncheckedIndexedAccess` and `exactOptionalPropertyTypes`.
All 7 smoke checks pass against the running test container, covering URL normalisation, the
http→ws/https→wss mapping, that `GET /api/` exists and rejects an unauthenticated request with 401,
and that the WebSocket opens with `auth_required` — which is the message `live.ts`'s whole auth flow
is built around.
Daraus folgt: **Die App wird nur seitlich installiert, nie in einem App Store
veröffentlicht**, und dieses Repository bleibt privat. `../design-system/`
kommt bewusst ohne diese Dateien aus, weil es nach außen hochgeladen wird —
dort niemals Markendateien ablegen.
**Not yet verified — needs a token:** authenticated reads, service calls, and the queue's real
round-trip. Those need a Long-Lived Access Token, which is a deliberate manual step (see the LLAT
provisioning decision in the architecture doc). To do it: create a token in the HA profile page, then
extend `scripts/smoke.ts` with authenticated cases.
## Noch offen
- Capacitor-Hülle und sichere Ablage des Tokens (Keychain/Keystore)
- QR-Einrichtung als Alternative zum Einfügen des Tokens
- Live-Ansicht der laufenden Fahrt: gebaut, aber über
`src/funktionen.ts` abgeschaltet, bis der FMM003 echte Werte liefert