diff --git a/AGENTS.md b/AGENTS.md index d4a3f20..76b72d4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,8 @@ # AGENTS.md — Project state, review findings, open items, and working rules -**Last updated: 2026-08-30** (owner findings batch - vehicle pin, tile chevrons, select arrows, +**Last updated: 2026-08-31** (Neustart-Festhänger der App, OTA-Prüfsumme, Statistik-Kopfzahl, +Setup-Filter und die gemessene Wahrheit über die Dongle-Konfiguration; Manifest `2026.8.31.4`, +siehe Abschnitte AT und AU. Davor: 2026-08-30 (owner findings batch - vehicle pin, tile chevrons, select arrows, SmartDeal dialog, address cache and pull-to-refresh; manifest `2026.8.30.19`, see sections AO, AP and AQ. Before that: design-audit follow-through - the five priority items applied to both codebases, plus the location preview's wrong pin; manifest `2026.8.30.8`, see section AN. @@ -6050,6 +6052,143 @@ rendering bug in a view. --- +## AT. Neustart-Festhänger, OTA-Prüfsumme, Statistik-Kopfzahl, Setup-Filter (2026.8.31.4) + +### Die App blieb nach einem Neustart von Home Assistant hängen + +Gemeldet: während Update und Neustart zeigte die App „502 Error" unter einem längst +gedrückten „Jetzt neu starten", und die Meldung blieb stehen, nachdem Home Assistant +wieder lief — erst ein Beenden der App half. Zwei Ursachen, beide in +`DatenKontext.tsx`: + +1. **502 galt als harter Ladefehler.** `ApiFehler.istNetzproblem` ist nur `status === null`; + ein Vorschaltserver, der antwortet, während Home Assistant noch hochfährt, liefert + aber 502/503/504. Neu: `istVoruebergehend` fasst beides zusammen (500 bewusst **nicht** — + das bessert sich durch Warten nicht). Solange schon Daten da sind, erscheint keine + Fehlerzeile mehr. +2. **Nach dem Wiederverbinden wurde genau einmal nachgeladen.** Die Websocket-Verbindung + steht regelmäßig ein paar Sekunden vor der REST-Schnittstelle. Schlug dieser eine + Versuch fehl, kam nie wieder ein `"verbunden"` — und damit nie wieder ein Versuch; die + App blieb bis zum Beenden auf dem Stand von vor dem Neustart, mitsamt Fehlerzeile. + Neu: `nachladenBisEsKlappt()` mit wachsenden Abständen (2/4/8/15/30/60 s, zusammen gut + zwei Minuten), sauber abgeräumt beim Verlassen. + +Dazu ein Fehler, den dieselbe Stelle verdeckt hat: `neuLaden` hing an `bereit`, der Effekt +darunter aber nur an `api` — die dort festgehaltene Fassung sah `bereit` für immer als +`false`. `bereit` liegt jetzt in einem Ref, `neuLaden` ist stabil. Vier Regressionstests +(`DatenKontext.test.tsx`), gegen den alten Stand als fehlschlagend nachgewiesen. + +Panel-seitig kein Gegenstück nötig: es hängt an Home Assistants eigener Verbindung, deren +Wiederanlauf schon in Abschnitt N behandelt ist. + +### „App-Update: failed to download / checksum failed" + +**Das Bündel ist nachweislich in Ordnung.** Geprüft von Ende zu Ende: der Blob bei +`HEAD`, die Datei auf der Platte und die tatsächlich über HTTP ausgelieferten Bytes +haben alle denselben SHA-256, den auch `bundle.json` nennt; git behandelt die Zip als +binär (`i/-text`), `core.autocrlf` kann ihr nichts anhaben. Die Prüfsumme, die das +Plugin vergleicht, ist genau dieser Wert (`CapgoUpdater.swift`, `calcChecksum` über die +heruntergeladene Zip vor dem Entpacken). + +Der Fehler fiel zusammen mit dem Neustart — ein Download, der mitten hineinläuft, liefert +je nach Zeitpunkt einen Abbruch („failed to download") oder unvollständige Bytes +(„checksum"). Zwei Änderungen in `ota.ts`: + +- Die Fassung hängt jetzt als `?v=` an der Bündel-Adresse. Home Assistant liefert + `/audi_dashboard_static/` **ohne** `Cache-Control`, aber mit `ETag` und + `Last-Modified` (nachgemessen) — genau der Fall, in dem jeder Zwischenspeicher nach + eigenem Ermessen eine Weile die alte Datei behalten darf. Dieselbe Vorsorge trifft das + Panel seit jeher für sein eigenes Bündel. +- Ein Prüfsummenfehler wird nicht mehr wortwörtlich durchgereicht, sondern erklärt: „Das + Update kam unvollständig an … sobald Home Assistant wieder läuft, noch einmal + versuchen." + +### Statistik: der Gesamtwert steht in der Kopfzeile + +Erst sollte „Absolut" eine fünfte Spalte links werden. Am laufenden Panel gemessen passt +das nicht: die Kachel ist auf dem Telefon innen **307px** breit, ein sechsstelliger +Kilometerstand wie `209.177` misst in Audi Type Wide 300 **67px** bei 16px und 54px bei +13px — fünf Spalten gingen nur mit kleinerer Schrift. Der Eigentümer hat daraufhin +entschieden, den Gesamtwert dorthin zu setzen, wo man ihn zuerst liest: **die große Zahl +oben in „Distanz und Fahrzeit" und „Getankte Menge und Kosten" ist jetzt der Gesamtwert, +nicht mehr der des laufenden Jahres** — die Jahreszahl im Titel entfällt entsprechend. +Darunter bleiben vier Spalten (Jahr/Monat/Woche/Tag), jetzt als `repeat(4, minmax(0,1fr))` +statt `auto-fit`, das auf dem Telefon nur drei Spalten zuließ und die vierte umbrach. +`ZEITRAEUME` rechnet weiter alle fünf, angezeigt wird `ZEITRAUM_SPALTEN`. + +### Kein Schatten, kein Beschnitt — und kein Schalter mehr dafür + +Gemessen statt geschätzt: das Foto ist 2000×1125 (16:9), der Kasten stand auf 16:8 und +`object-fit: cover` schnitt oben und unten je ein Viertel ab — das Dach lag darin. Statt +das Verhältnis auf 16:9 festzuschreiben (was beim nächsten Foto in einem anderen Format +wieder schneiden würde) bestimmt jetzt das Bild selbst die Höhe; der Platzhalter behält +sein festes Verhältnis, er hat keines von sich aus. Nachgemessen: Kasten 367×207 zu +2000×1125 — kein Beschnitt. + +Gleichzeitig sind der Ausblend-Verlauf und der Bodenschatten entfallen (beide Codebasen, +inklusive der `height:220px`-Regel im iOS-Overlay, die den Beschnitt sonst auf breiten +Schirmen wieder erzwungen hätte). Damit hatte der Schalter **„Fahrbahn unter dem +Fahrzeug"** nichts mehr zu schalten und ist ebenfalls weg — samt `bodenAn`/`szeneKlasse()` +im Panel und `useBoden`/`bodenKlasse`/`OHNE_SZENE` in der App. Der Regressionstest für +den geteilten Einstellungsspeicher (`theme.test.tsx`) prüft das jetzt an +`useTabBeschriftung` statt an `useBoden` — dieselbe Aussage, ein noch vorhandener Schalter. + +### Setup: „Nur passende Sensoren" sucht nach dem Signalnamen + +Domain und Einheit lassen bei einem Fahrzeug mit siebzig Sensoren immer noch Dutzende +übrig. Der Name, der neben der Überschrift als „(z. B. …)" steht, ist der tatsächliche +Signalname der Integration (`beispiel` in `FELDER`) und trifft genau einen. Er filtert +jetzt mit — **mit Rückfall**: findet er nichts, bleibt die ungefilterte Liste stehen, denn +er stammt aus dieser einen Installation. Zusätzlich zählt er in `entitaetScore()` mit 8 +Punkten, damit auch der Vorschlag für ein leeres Feld davon profitiert. + +## AU. Der Dongle: was die Konfiguration wirklich bewirkt (2026-08-31) + +Nachgemessen an der echten Aufzeichnung im Testcontainer (7 bzw. 30 Tage Recorder), nicht +aus der Konfigurationsanzeige abgelesen. + +**Was gut funktioniert.** Beim Fahren liefert das Gerät alle **10 s** (p90) einen Satz — +das ist die eingestellte `Min Period` für „Moving", und sie gilt für Geschwindigkeit wie +für Position gleichermaßen (771 bzw. 1165 Punkte in 7 Tagen). Für Strecke, Route, +Durchschnitt und vmax reicht das: in denselben 7 Tagen wurde ein Höchstwert von 118 km/h +erfasst. `can_fuel_volume` löst **0,1 l** auf (Werte wie 18,9 / 19,1 / 19,5 über 30 Tage; +in einzelnen Wochen sieht man nur ganze Liter, das ist die Meldung des Fahrzeugs, nicht +die Auflösung des Sensors) — die Grenze `MIN_STRECKE_VERBRAUCH_KM = 3.0` bleibt damit +richtig hergeleitet. Der Füllstand in Prozent springt in 1-%-Schritten. + +**Was nicht funktioniert: das rohe Zündsignal als Fahrterkennung.** +`engine_ignition_or_acc_status` ist der ungefilterte Zündungseingang. In der Aufzeichnung: +am 29.08. drei Wechsel innerhalb von **30 Millisekunden**, danach ein Aus/Ein-Paar mit +**10 Sekunden** Abstand — und am 30./31.08. dreizehn Stunden am Stück „an", ohne Fahrt. +Solange die App eine Pausentoleranz hatte, hat die das aufgefangen. Seit deren Entfernung +(Abschnitt AS) trennt jeder solche Aussetzer eine Fahrt in zwei. + +**Die Begründung für das Entfernen war falsch.** Der `Ignition OFF Timeout` von 900 s +unter *Trip \ Odometer* verzögert **nicht** dieses Signal, sondern ausschließlich das +geräteeigene Trip-Element ([Teltonika-Wiki](https://wiki.teltonika-gps.com/view/FMM003_Trip/Odometer_settings): +„timeout value to detect Trip end once the Ignition … is off"). Es gab also nie zwei +Zeitgeber hintereinander — es gab unseren, und daneben einen ungenutzten des Geräts. + +**Die Empfehlung, noch nicht umgesetzt (Entscheidung des Eigentümers):** +`ZUENDUNG_SENSOR` auf +`binary_sensor.…_trip_status_true_if_trip_started_false_if_stopped` legen. Dieses Signal + +- beginnt erst, wenn Zündung **und** Bewegung **und** `Start Speed` zusammenkommen — + dreizehn Stunden Standlicht-Zündung erzeugen damit keine Fahrt, +- endet erst 900 s nach Zündung aus — die Pausentoleranz kommt vom Gerät, ohne dass wir + einen zweiten Zeitgeber bauen, +- ist millisekundenfestes Prellen los. + +Der Preis: das Fahrtende liegt 15 Minuten hinter dem tatsächlichen. Das war mit der +eigenen Pausentoleranz genauso und ist damit kein Rückschritt; wer es genauer will, müsste +das Ende nachträglich auf den letzten Punkt mit Geschwindigkeit > 0 zurückrechnen — eigenes +Arbeitspaket, hier bewusst nicht angefangen. + +**Weiter offen, unabhängig davon:** echte Funklöcher. Im gemessenen Zeitraum gab es +mitten in der Fahrt eine Lücke von **2739 s** ohne einen einzigen Satz. Dagegen hilft keine +Einstellung am Gerät; das ist der in Abschnitt P/W beschriebene Fall, den die +Plausibilitätsgrenze (`UNPLAUSIBLE_KMH`) abfängt. + ## Working conventions (observed — keep them) - German is the project language: identifiers, comments, commits, UI texts. Exceptions: diff --git a/companion-app/src/api/rest.ts b/companion-app/src/api/rest.ts index 42b0a25..a25930d 100644 --- a/companion-app/src/api/rest.ts +++ b/companion-app/src/api/rest.ts @@ -36,6 +36,20 @@ export class ApiFehler extends Error { get istNetzproblem(): boolean { return this.status === null; } + + /** Vorübergehend nicht erreichbar: kein Netz, oder ein Server, der + gerade hochfährt. + + 502/503/504 sind genau das, was ein Neustart von Home Assistant + liefert - der Vorschaltserver antwortet schon, Home Assistant selbst + noch nicht. Das ist für die Oberfläche dasselbe wie kein Netz: warten + und den zwischengespeicherten Stand zeigen, nicht eine Fehlerzeile + stehen lassen, die niemand mehr wegbekommt. 500 gehört bewusst NICHT + dazu - das ist ein echter Fehler in der Anfrage, der sich durch + Warten nicht bessert. */ + get istVoruebergehend(): boolean { + return this.istNetzproblem || this.status === 502 || this.status === 503 || this.status === 504; + } } export interface AnfrageOptionen { diff --git a/companion-app/src/daten/DatenKontext.test.tsx b/companion-app/src/daten/DatenKontext.test.tsx new file mode 100644 index 0000000..ad5250c --- /dev/null +++ b/companion-app/src/daten/DatenKontext.test.tsx @@ -0,0 +1,134 @@ +/** + * Was passiert, wenn Home Assistant neu startet. + * + * Gemeldet vom Eigentümer (2026-08-31): während eines Neustarts zeigte die App + * „502 Error“, und die Meldung blieb stehen, nachdem Home Assistant längst + * wieder lief — erst ein Beenden der App half. Zwei Ursachen, beide hier + * abgesichert: + * + * 1. 502 galt als harter Ladefehler statt als „Server kommt gerade hoch“. + * 2. Nach dem Wiederverbinden wurde genau einmal nachgeladen. Schlug das + * fehl (die Websocket-Verbindung steht regelmäßig ein paar Sekunden vor + * der REST-Schnittstelle), kam nie wieder ein „verbunden“ — und damit nie + * wieder ein Versuch. + */ + +import { render, screen, waitFor } from "@testing-library/react" +import { describe, expect, it, vi } from "vitest" + +import { ApiFehler, type DataMetricApi } from "../api" +import { DatenAnbieter, useDaten } from "./DatenKontext" +import { beispielApi } from "../tests/beispieldaten" + +function Anzeige() { + const { bereit, ladefehler } = useDaten() + return ( +