Ortsnamen aus dem Backend, Verbrauchsfaktor aus dem Tankbeleg, Radzaehler ueber Fahrten

Ortsauflösung serverseitig (geokodierung.py): der Ort steht jetzt in der Fahrt
statt im Zwischenspeicher jedes Geräts. Beide Oberflächen fragen nichts mehr ab.

Verbrauchskorrektur (verbrauchskorrektur.py): Faktor = Beleg-Liter / Summe der
Einzelfahrten, voll zu voll. Statt einer Schranke am Faktor wird der Nenner
geprüft - decken die erkannten Fahrten 90-110 % der Tacho-Spanne ab? Ausgelöst
beim Schreiben eines Tankvorgangs, nicht erst bei der nächsten Fahrt.

Radzähler: summiert die Fahrten des montierten Satzes statt Tacho-Deltas. Der
alte Weg hatte Fahrzeugwechsel als gefahrene Strecke verbucht (375.962 km bei
einem Tacho von 61.823).

Null heißt unbekannt: ein Kilometerstand von 0 oder ein leeres Feld werden zu
None normalisiert, statt an sechs Lesestellen als "Tachostand null" zu gelten.
Momentanwerte der Sensoren gelten nur für einen Tankvorgang von jetzt.

Oberfläche, beide Codebasen: Wertespalte der Fahrtenliste ausgerichtet, Ort ->
Ort in "Zuletzt", "Räder" statt "Reifen", "Termin vereinbart" entfernt,
Markenlogo 20 % größer, Übersicht-Symbol auf volle Rasterbreite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-02 11:28:37 +02:00
parent 78a8017e95
commit 2af5c469d6
27 changed files with 1575 additions and 352 deletions
+434 -1
View File
@@ -1,6 +1,12 @@
# AGENTS.md — Project state, review findings, open items, and working rules
**Last updated: 2026-09-01** (Einzelfahrt: Orte aus den Koordinaten, Kleingedrucktes weg, „Min.",
**Last updated: 2026-09-02** (Null heisst unbekannt, Momentanwerte nur fuer
"jetzt", Uebersicht-Symbol vergroessert, `2026.9.2.3`, Abschnitt BN.
Davor: Verbrauchsfaktor aus dem Tankbeleg, `2026.9.1.32`,
Abschnitt BM. Davor: Reifenzaehler ohne Fahrzeugwechsel, „Raeder“
statt „Reifen“, `2026.9.1.29`, Abschnitt BL. Davor: Ortsnamen aus dem Backend, Verbrauchseichung an
den Betankungen, Wertespalte ausgerichtet, `2026.9.1.27`, Abschnitt BK.
Davor: Einzelfahrt: Orte aus den Koordinaten, Kleingedrucktes weg, „Min.",
`2026.9.1.23`, Abschnitt BJ - dazu Orte in der Fahrtenliste ohne Netzabruf.
Davor: Strecken gestaffelt, Abschnitt BI. Davor: Audit ueber Panel, App und Backend, Abschnitt BH. Davor: Setup mit gruener Bestaetigung und Erwartungswert je
Listenposition, Abschnitt BG. Davor: Rueckblick ohne Nullfahrten, Streckenwahl gemeinsam in `verlauf.py`,
@@ -7097,6 +7103,433 @@ Geprüft: 7 Fälle gegen den Schnitt (mit/ohne Straße, benannter Platz, ohne PL
`null`). Im Browser nachgesehen: `„Eichstätt → Adelschlag"` in der Liste, volle Anschrift auf der
Einzelfahrt.
## BK. Der Ort kommt mit der Fahrt, und der Verbrauch wird an den Betankungen geeicht (2026.9.1.25-.27)
### Die Wertespalte der Fahrtenliste stand nicht in einer Linie
Gemeldet: km und l/100 km fluchten nicht, und bei langen Ortsnamen wird der
Abstand zu eng. Gemessen: die rechte Kante der Wertespalte lag je nach Zeile
bei 549px oder 520px, obwohl beide Zeilen gleich breit sind und an derselben
Stelle beginnen.
Ursache war nicht die Breite, sondern die **Anzahl der Kinder**: eine `.leaf`
hat drei (Label, Werte, Pfeil), und `justify-content: space-between` verteilt
freien Raum ZWISCHEN allen dreien. Die Wertespalte stand damit in der Mitte,
ihre Lage abhaengig von der Laenge des Labels. Mit `flex: 1` am Label bleibt
kein freier Raum uebrig, Werte und Pfeil sitzen buendig rechts.
`min-width: 0` gehoert dazu, sonst darf ein Flex-Kind nicht unter seine
Inhaltsbreite schrumpfen und `text-overflow` greift nie.
Nachgewiesen: alle sechs Zeilen der Liste teilen sich jetzt 629/721 statt
zweier verschiedener Kanten.
**Merkposten:** eine Zeile, deren Werte "mal so, mal so" stehen, ist fast nie
ein Breitenproblem - erst die Kinder zaehlen, dann messen.
### Ortsnamen loest jetzt das Backend auf
Bis hierher tat es jede Oberflaeche selbst und legte das Ergebnis in ihren
eigenen Zwischenspeicher. Drei Folgen, alle unnoetig: jedes Geraet fragte
dieselben Koordinaten erneut ab, ein frisch eingerichtetes Telefon zeigte fuer
alte Fahrten wieder "unbekannt", und ein geleerter Browserspeicher warf alles
weg.
Neu `geokodierung.py` plus ein Screening-Schritt `_orte_screenen()`. Der Ort
steht jetzt in der Fahrt: `start_address`/`end_address` (volle Anschrift) und
`start_stadt`/`end_stadt` (nur der Ortsname, fuer die Liste).
Nominatims Nutzungsbedingungen bestimmen fast jede Entscheidung darin: eine
Anfrage je Sekunde (`ABSTAND_S = 1.1`), hoechstens acht je Durchlauf
(`PRO_DURCHLAUF`, sonst waere ein Jahresrueckblick genau der Vorratsabruf, um
dessen Unterlassung gebeten wird), ein identifizierbarer User-Agent, und jedes
Ergebnis wird in `orte.json` gemerkt statt erneut abgefragt.
**Zwei Sperren gegen das Ueberschreiben von Hand Eingetragenem**, absichtlich
beide: der Schritt fuellt nur ein LEERES Feld, und `fahrt_aktualisieren()`
laesst ohnehin jedes Feld in `edited_fields` unberuehrt. Der Ortsname wird
dagegen auch neben einer handischen Anschrift gesetzt - er beschreibt die
Koordinate, nicht die Eingabe. In der Liste sticht die handische Anschrift
trotzdem: beide Oberflaechen pruefen dafuer `edited_fields`.
Ein Netz- oder Serverfehler wird NICHT gemerkt - sonst stuende eine
voruebergehend gedrosselte Antwort fuer immer als "kein Ort hier". Ein echtes
leeres Ergebnis dagegen schon.
Beide Oberflaechen fragen jetzt gar nichts mehr ab; `ortAusCache()`,
`stadtAusCache()`, `stadtAusAnschrift()` und `fahrtOrteNachtragen()` sind
ersatzlos entfallen.
Nachgewiesen: nach zwei Screening-Laeufen tragen 12 von 14 Fahrten einen Ort,
die beiden anderen haben gar keine Koordinaten. Die Liste zeigt
"Eichstaett -> Adelschlag"; damit ist auch der gemeldete Unterschied zwischen
Panel ("Adelschlag") und App ("Adelschlag -> Eichstaett") weg - er kam daher,
dass die beiden verschiedene Zwischenspeicher hatten.
### Verbrauch: Rohwert, Faktor, Anzeigewert
Der Anzeigewert einer bereits gespeicherten Fahrt wurde nie neu gerechnet -
die `wert_ab()`-Korrektur vom selben Tag erreichte die Altfahrten also nicht.
Das Auswahlkriterium haengt jetzt am ROHwert, nicht am Anzeigewert: die Fahrt
vom 01.09. ist damit von 11,4 auf **8,7 l/100 km** gewandert, genau auf den
Wert, den die Messung vorhergesagt hatte.
Drei Felder statt einem: `verbrauch_roh_l_100km` (die reine Messung, damit die
Rechnung wiederholbar bleibt), `verbrauch_faktor`, und `verbrauch_l_100km` als
Produkt - der Wert, den die Oberflaechen anzeigen. Die brauchten deshalb keine
Aenderung.
### Die Eichung an zwei Betankungen (verbrauchskorrektur.py)
Vorschlag des Eigentuemers, und er ist besser als meiner:
Faktor = (Tankstand nach A - Tankstand vor B) / Summe der Fahrtverbraeuche
**Dafuer braucht es keinen Tankbeleg.** Zaehler und Nenner sind mit demselben
Geber gemessen; ein Massstabsfehler stuende in beiden und kuerzt sich heraus.
Damit taugt auch eine Teilbetankung - die Voll-zu-voll-Bedingung, die mein
erster Ansatz ("Verbrauch zwischen zwei Betankungen") gebraucht haette,
entfaellt.
Ebenso entfaellt mein Nichtlinearitaets-Vorbehalt: `can_fuel_volume` kommt
schon als Liter, die Kennlinie des Schwimmers hat das Steuergeraet angewandt
(Hinweis des Eigentuemers). Was bleibt, ist die **Daempfung** - der gemeldete
Stand hinkt nach, auf sieben Kilometern anteilig weit mehr als auf
dreihundert. Genau die faengt der Faktor mit ab, weil sie Kraftstoff nur
zwischen Fahrten verschiebt und die Intervallsumme stimmt.
Der Faktor enthaelt auch Verbrauch, der zu KEINER Fahrt gehoert (Standheizung,
nicht erkannte Fahrt, Rangieren unter 0,1 km). Entscheidung des Eigentuemers:
auf die Fahrten verteilen, damit deren Summe dem entspricht, was im Tank
fehlt - eine einzelne Fahrt traegt dafuer einen Anteil, den sie nicht selbst
verbraucht hat.
**Schranke 0,7-1,5**, ausserhalb wird verworfen und protokolliert. Sie hat auf
echten Daten sofort gegriffen: das Intervall 15.08. bis 27.08. ergab **5,616**
- 45 l im Tank verbraucht, aber nur 8 l aus zwei Fahrten gerechnet. Das ist
kein Rechenfehler, sondern der Befund, dass in diesen zwoelf Tagen fast keine
Fahrt erkannt wurde (die Umsteckphase des Dongles). Ohne die Schranke haetten
beide Fahrten das Fuenffache angezeigt.
Gerechnet werden nur Intervalle, in denen noch eine Fahrt ohne Faktor steht -
das Screening laeuft bei jeder Aenderung des Kilometerstands, und jedes
Intervall kostet eine eigene Abfrage des Tankverlaufs.
Eine Fahrt, die ueber eine Betankung hinweggeht, bekommt keinen Faktor: sie
laesst sich keinem der beiden Intervalle zuordnen.
**Noch offen:** in den vorhandenen Daten liefert kein Intervall einen
brauchbaren Faktor. Der Mechanismus ist nachgewiesen, seine Eichung nicht -
dafuer braucht es ein Intervall mit vollstaendig erkannten Fahrten.
## BL. Der Radzaehler summiert Fahrten statt Tacho-Deltas; Raeder statt Reifen (2026.9.1.29)
### 375.962 km auf Raedern eines Autos mit 61.823 km
Gemeldet als "der Zaehler rechnet die Gesamtkilometer des Fahrzeugs auf die
Raeder". Nachgesehen: Sommerraeder 375.962 km, Tacho 61.823.
`fortschreiben()` zaehlte jedes POSITIVE Delta zum zuletzt verarbeiteten
Kilometerstand. Ein Sprung nach unten fiel schon immer heraus - beim Umstecken
des Dongles zwischen zwei Fahrzeugen gibt es aber beide Richtungen, und
61.823 -> 209.177 ist ein positives Delta von 147.354 km.
**Erster Ansatz, vom Eigentuemer verworfen:** ein Sprungschutz am Delta, der
nicht die Groesse pruefte, sondern die daraus folgende Geschwindigkeit
(`UNPLAUSIBLE_KMH`, wie bei den Fahrten) - noetig, weil der Modulkopf
verspricht, dass Kilometer aus einem HA-Ausfall noch mitzaehlen. Dafuer haette
das Profil den Zeitpunkt des Referenzstands mitfuehren muessen. Gebaut, an
sechs Faellen belegt, dann auf die Rueckfrage "warum so kompliziert? einfach
die Trip distance addieren?" wieder entfernt.
### Der Radzaehler summiert jetzt Fahrten
Die Rueckfrage war berechtigt, und die Messung stuetzt sie. Summe der
Fahrtstrecken gegen die Tacho-Spanne im selben Zeitraum, am echten Bestand:
| Tacho-Spanne | Summe der Fahrten | erfasst |
|---|---|---|
| 61.816 -> 61.823 (7 km) | 6,9 km | 99 % |
| 209.156 -> 209.281 (125 km) | 125,0 km | 100 % |
| 21.188 -> 21.325 (137 km) | 141,0 km | 103 % |
| 20.500 -> 20.580 (80 km) | 205,0 km | 256 % |
Der Ausreisser stammt aus dem Bestand von vor den Plausibilitaetspruefungen,
nicht aus dem Verfahren. In den sauberen Bereichen trifft die Fahrtsumme den
Tacho auf ein bis drei Prozent - und sie ist genauer, weil die Fahrten seit
der GNSS-Verfeinerung auf 100 m messen statt auf ganze Kilometer (6,9 gegen 7).
Damit entfallen `referenz_odo_km`, die Delta-Buchhaltung und der eben erst
gebaute Sprungschutz: die Fahrten pruefen Rueckwaertssprunge, unmoegliches
Tempo und Mindestdauer laengst selbst.
**Die Form hat der Eigentuemer festgelegt**, nachdem ich zuerst einen Sockel
mit Rueckrechnung vorgeschlagen hatte ("nein. Ich moechte: Manuell jederzeit
einen neuen Wert eintragen koennen und jeder gefahrene Kilometer wird einfach
dazuaddiert"). Also: `saetze.<satz>.km` IST der Stand, von Hand direkt
gesetzt, und jede Fahrt legt ihre Strecke oben drauf.
Zwei neue Felder an der Fahrt tragen das:
* `reifensatz` - welcher Satz montiert war. Der Satz steht an der FAHRT, nicht
am Zeitpunkt; ein Satzwechsel braucht deshalb keine Sonderbehandlung.
* `reifen_gezaehlt_km` - wieviel davon schon gutgeschrieben ist. Verfeinert das
Screening die Strecke spaeter (7,0 -> 6,896 km ist real vorgekommen), wird
nur die Differenz nachgetragen. Ohne diesen Merker waere entweder jede
Korrektur verloren oder jede Fahrt doppelt gezaehlt.
**Der Rueckblick stempelt bewusst KEINEN Radsatz.** Er rekonstruiert
Vergangenes, und das steckt im gespeicherten Radstand laengst drin - ein
erneuter Import schriebe dieselben Kilometer sonst ein zweites Mal auf die
Raeder. Gleiches gilt fuer den Altbestand: Fahrten ohne `reifensatz` zaehlen
nie mit, der gespeicherte Stand bleibt bei der Umstellung also unveraendert.
**Was das kostet, vom Eigentuemer ausdruecklich akzeptiert:** was die
Fahrterkennung nicht sieht, landet nie auf den Raedern. Der Tacho ist die
Wahrheit des Fahrzeugs und holt sich alles zurueck, eine Fahrtsumme nicht.
Live nachgewiesen in `audi_ha_test`: von Hand auf 1000 gesetzt -> 1000,0;
Fahrt ueber 12,3 km angelegt -> 1012,3; Screening erneut ausgeloest -> 1012,3
(keine zweite Gutschrift). Protokollzeilen: `Laufleistung sommer von Hand auf
1000.0 km gesetzt` und `Fahrt t-bb1ca655...: 12.3 km auf sommer
gutgeschrieben, neuer Stand 1012.3 km`.
**Der falsche Bestand bleibt stehen.** Die 375.962 km korrigiert der
Eigentuemer auf der realen Instanz selbst ueber den Stift, unabhaengig von der
Entwicklung.
### Drei Oberflaechen-Aenderungen, beide Codebasen
* **"Termin vereinbart" samt Schalter entfernt** (Wunsch des Eigentuemers).
Damit entfaellt auch der Zusatz "vereinbart" unter dem Datum auf "Mein
Audi". Das Feld `reifen.wechsel.vereinbart` selbst bleibt im Profil stehen -
es zu loeschen waere eine Datenaenderung, keine Anzeigeaenderung.
* **"Reifen" heisst "Raeder"** in allem Sichtbaren: Seitentitel, Kachel auf
"Mein Audi", Kalendereintrag ("Radwechsel auf ..."), Loeschabfrage im Archiv,
Backup-Beschreibung. NICHT umbenannt: Route, Profilschluessel, Dienstnamen,
Funktionsnamen - dieselbe Trennung wie bei "Servicebuch"/"Wartungsplan"
(Abschnitt AB).
* **Markenlogo 20 % groesser**, 16px -> 19,2px, an allen vier Stellen
(`.marke-logo img` und die eine feste Angabe im Panel, `.dm-marke img` und
`.dm-markenlogo` in der App). Gemessen: 19,19px im Browser, drei Logos auf
der Tanken-Seite.
## BM. Der Verbrauchsfaktor kommt aus dem Tankbeleg, nicht aus dem Geber (2026.9.1.30)
### Die Formel ist die des Eigentuemers
Faktor = getankte Liter laut Beleg / Summe der Liter aus den Einzelfahrten
Genau die Rechnung, die er ohnehin von Hand macht. Die Annahme dahinter ist
seine und traegt: die Ungenauigkeit des Gebers trifft jede Fahrt
gleichermassen, ist also ein Faktor und kein Summand.
**Meine erste Fassung rechnete anders** - Tankstand nach A minus Tankstand vor
B, geteilt durch die Fahrtsumme. Die braucht keinen Beleg (der Maszstab steht
in Zaehler und Nenner und kuerzt sich heraus) und funktioniert auch bei
Teilbetankung. Sie korrigiert dafuer aber NUR die Verteilung, nicht den
absoluten Maszstab des Gebers. Die Belegvariante kann beides und ist deshalb
die richtige - der Preis ist die Voll-zu-Voll-Bedingung.
### Drei Bedingungen, jede aus einem konkreten Fehler heraus
1. **Volltankung an BEIDEN Enden.** Nur dann gilt "eingefuellte Liter = seit
dem letzten Mal verbrauchte Liter". Erkannt am Tankstand eine Minute nach
der Betankung: mindestens `VOLL_ANTEIL` (95 %) des `tankvolumen_liter` aus
dem Profil. Die eine Minute ist die Vorgabe des Eigentuemers; zu kurz zu
greifen ist die harmlose Richtung (der Stand faellt unter die Grenze, es
entsteht kein Faktor), zu lang die gefaehrliche - nach der Abfahrt sinkt der
Stand wieder und eine echte Volltankung saehe nachtraeglich unvoll aus.
2. **Die Liter muessen aus einem Beleg stammen.** Ein automatisch erkannter
Tankvorgang traegt in `liters` den vom GEBER gemessenen Anstieg. Den gegen
die ebenfalls aus dem Geber gerechneten Fahrten zu stellen, ist ein
Zirkelschluss und ergaebe immer ungefaehr 1,0 - der Faktor koennte gar
nichts korrigieren. `beleg_liter()` laesst deshalb nur durch, was einen
`receipt_key` hat, von Hand bearbeitet wurde oder `source == "manual"`
traegt. An fuenf Faellen geprueft.
3. **Die Fahrten muessen die gefahrene Strecke abdecken** (`DECKUNG_MIN`/
`DECKUNG_MAX`, 90-110 %). Nicht der Faktor wird begrenzt, sondern der
Nenner geprueft - siehe unten.
### Warum nicht der Faktor begrenzt wird
Erst stand hier eine feste Schranke 0,7-1,5. Der Eigentuemer hat sie
hinterfragt ("unsere Rechnung ist gut genug"), und die Pruefung der Frage gab
ihm halb recht:
* **Am Faktor** war sie tatsaechlich am falschen Ort. Die beiden Tore oben
sichern den Zaehler; ein echter Geberfehler liegt bei ein paar Prozent und
waere an 0,7-1,5 nie angeschlagen. Eine groessere, aber echte Abweichung
haette sie dagegen abgeklemmt.
* **Der Nenner** bleibt aber verwundbar: fehlt im Intervall eine Fahrt, ist
die Summe zu klein und der Faktor zu gross - und er multipliziert jede
Fahrt des Zeitraums mit. Genau das war der 5,616-Fall (45 l im Tank, zwei
erkannte Fahrten ueber zwoelf Tage); Beleg und Volltankung haetten dort
beide gestimmt.
Geprueft wird deshalb die Vollstaendigkeit der Fahrtmenge, an einem Mass, das
beim Radzaehler-Umbau desselben Tages schon belegt wurde:
Deckung = Summe der Fahrtstrecken / Tacho-Spanne des Intervalls
Liegt sie bei 90-110 %, gilt der Faktor - ohne obere oder untere Klemme.
Sonst wird das Intervall uebersprungen und protokolliert. Die Grenze gilt in
BEIDE Richtungen: 256 % (Altbestand mit doppelten Fahrten) ist genauso ein
Zeichen fuer einen unbrauchbaren Nenner wie 15 %.
An sechs Faellen mit den echten Zahlen geprueft, `_deckung()` direkt
aufgerufen: 100 % / 99 % / 103 % angewandt, 256 % und 15 % verworfen, ein
Rueckwaertssprung (Fahrzeugwechsel) als "nicht bestimmbar" behandelt statt
als Deckung 0.
### Standlauf verzerrt den Faktor nicht
Frage des Eigentuemers, und die Antwort ist beruhigend: laeuft der Motor im
Stand bei eingeschalteter Zuendung, bleibt die Fahrt offen. Der Verbrauch
steht damit in der Summe der Einzelfahrten UND in der getankten Menge - er
kuerzt sich heraus. Die betroffene Fahrt zeigt zu Recht einen hohen Verbrauch.
Verzerrend ist nur Verbrauch OHNE offene Fahrt (Standheizung bei Zuendung
aus): der fehlt im Nenner und verteilt sich ueber den Faktor auf alle Fahrten
des Zeitraums - genau das, was der Eigentuemer bewusst so wollte.
### Zwei Ausloeser, jeder mit eigenem Grund
Der Eigentuemer hat den wunden Punkt benannt: der Faktor steht erst
rueckwirkend fest, eine Korrektur "nach einer Fahrt" kann nicht funktionieren.
Stimmt - und die Rechnung hielt sich schon daran (die Schleife laeuft ueber
PAARE aufeinanderfolgender Tankvorgaenge, das laufende Intervall hat kein
Gegenstueck). Die Frage legte aber eine echte Luecke frei: **ein Tankvorgang
loeste gar nichts aus.** Der einzige Aufruf hing am Screening, gerechnet wurde
also erst bei der naechsten Fahrt.
| Ausloeser | wofuer |
|---|---|
| `koordinator.tankvorgang_nachbereiten()` | das Intervall schliesst sich - hier entsteht der Faktor |
| `screening.durchfuehren()` | eine Fahrt im Intervall bekommt ihren Rohwert erst spaeter |
`tankvorgang_nachbereiten()` ersetzt `tankvorgaenge_veroeffentlichen()` an
allen sechs Schreibstellen (`belege.py` 4x, `tankerkennung.py`,
`historienimport.py`). Der 60-Sekunden-Takt ruft bewusst weiter nur
`tankvorgaenge_veroeffentlichen()`: dort aendert sich nichts, was einen neuen
Faktor ergeben koennte.
Gerechnet werden nur Intervalle, in denen noch eine Fahrt ohne Faktor steht -
sonst liefen dieselben Verlaufsabfragen bei jedem Fahrtende erneut. Die teure
Voll-Pruefung kommt zuletzt, nach den billigen Ausschluessen.
**Noch nicht belegt:** in den Testdaten gibt es keinen Tankvorgang mit
Beleg-Litern, also auch kein gerechnetes Intervall. Der Mechanismus ist
geprueft, seine Anwendung auf echte Zahlen nicht.
### Uebersicht/Zuletzt zeigt Ort -> Ort
Die Zeile "Letzte Fahrt" zeigte die vollen Anschriften. Sie benutzt jetzt
dieselbe `strecke()` wie die Fahrtenliste - eine Quelle, damit die beiden
nicht auseinanderlaufen koennen; in der App dafuer aus `Fahrten.tsx`
exportiert statt ein zweites Mal gebaut. Dabei fielen in der App zwei
Abweichungen zum Panel auf und sind mitgezogen: die Strecke lief noch ueber
`de()` statt `streckeText()` (also ohne die gestaffelte Aufloesung), und im
Wertzusatz stand das Statuswort, das der Eigentuemer aus der Liste hatte
entfernen lassen - jetzt der Verbrauch wie im Panel.
## BN. Null heisst unbekannt, und der Momentanwert gehoert nur zu "jetzt" (2026.9.2.1-.3)
### Der Kilometerstand darf fehlen duerfen
Wunsch des Eigentuemers: ein Weg, einen Tankvorgang ohne Kilometerstand
einzutragen - "eine 0 waere hilfreich, bzw. ein leeres Feld wenn alle Zahlen
geloescht werden".
Beides geht jetzt, und beides landet als **None** im Datensatz, nicht als 0.
Das ist der eigentliche Punkt: **sechs Stellen** lesen `odometer_km` und
behandeln ausschliesslich None als unbekannt. Eine gespeicherte 0 waere fuer
sie ein Fahrzeug mit Tachostand null - `distanz_seit_letzter_tankung()` haette
den gesamten Tachostand als "Strecke seit der letzten Tankung" geliefert, und
der Langzeitverbrauch rechnete gegen eine Spanne, die es nie gab.
`verlauf.als_kilometerstand()` faengt das an der Eingabe ab (0, leer, negativ,
unlesbar -> None). Eine Sonderbehandlung an sechs Lesestellen waere sechsmal
die Gelegenheit gewesen, sie zu vergessen. An acht Faellen geprueft.
Beide Formulare senden bei geleertem Feld ohnehin schon `null` - geprueft, nicht
angenommen (`kmFeld === "" ? null : ...` im Panel, `Number.isFinite(kmZahl) &&
km ? ... : null` in der App).
### Ein Momentanwert gehoert zu dem Moment, in dem er gemessen wurde
Beim Pruefen fiel ein aelterer Fehler auf, den die 0 sonst wirkungslos gemacht
haette: `tankvorgang_manuell()` ersetzte einen fehlenden Kilometerstand durch
den **aktuellen** Tacho - unabhaengig vom eingetragenen Zeitpunkt. Fuer eine
nachgetragene Tankung von vorgestern war das doppelt falsch: die gefahrene
Distanz wurde gegen einen zu hohen Stand gerechnet, und die neue
Datumspruefung haette das Intervall verworfen.
`_km_aus_sensor_wenn_jetzt()` setzt den Sensorwert nur noch ein, wenn der
eingetragene Zeitpunkt hoechstens `SENSOR_ERSATZ_FENSTER` (2 h) von jetzt
entfernt liegt. Dasselbe gilt fuer `distance_km`: `distanz_seit_tankung()`
nimmt `TANK_DISTANZ_SENSOR` nur noch mit `jetzt=True` und faellt sonst auf die
Kilometerstand-Subtraktion zurueck, die zeitunabhaengig ist.
Live nachgewiesen ueber den echten Dienst, beide Faelle in einem Durchgang:
| Zeitpunkt | `km` eingegeben | gespeicherter Kilometerstand |
|---|---|---|
| jetzt | 0 (unbekannt) | **61.823** - vom Tacho, Normalfall ohne Tippen |
| vorgestern | 0 (unbekannt) | **leer** - der Tacho von heute passt nicht dazu |
### Datum und Kilometerstand muessen zusammenpassen
Die Fahrten werden ueber die ZEIT zugeordnet, nicht ueber den Tacho. Ein
falsch datierter Tankvorgang laesst die Liter deshalb auf den falschen Fahrten
landen - und die Deckungspruefung merkt es nicht, weil sie den Tacho zu
denselben verschobenen Zeitpunkten liest.
`_datum_passt()` vergleicht deshalb den eigenen `odometer_km` des
Tankvorgangs mit dem Tacho zu seinem Zeitstempel, Toleranz
`ODOMETER_TOLERANZ_KM = 5.0` (Vorgabe des Eigentuemers). Ein automatisch
erkannter Tankvorgang besteht sie zwangslaeufig - beide Werte stammen aus
demselben Datensatz. Ohne Kilometerstand (die 0 von oben) gibt es nichts zu
vergleichen, dann gilt der Zeitpunkt wie eingetragen; der Waechter faellt fuer
diesen Tankvorgang aus, was unvermeidlich und immer noch besser ist, als eine
erfundene Zahl zu verlangen. An sechs Faellen geprueft.
### Getippte Liter zaehlen als Beleg
`beleg_liter()` liess nur `receipt_key`, `source == "manual"` oder einen
Vermerk in `edited_fields` durch. Ein automatisch erkannter Tankvorgang, dessen
Liter der Nutzer nachtippt, hatte nichts davon - die Zahl galt weiter als
Geberwert und wurde verworfen. `belege.tankvorgang_aktualisieren()` traegt
jetzt `"liters"` in `edited_fields` ein, wenn Liter hereinkommen.
Das ist zuverlaessig, weil die automatische Erkennung Tankvorgaenge nur
ANLEGT und nie aktualisiert: was durch diese Funktion an Litern kommt, stammt
konstruktionsbedingt von einem Menschen (Formular oder bearbeitetes CSV).
Nebenwirkung, bewusst: `edited_fields` schuetzt das Feld vor der Automatik.
Wird spaeter doch ein PDF zu demselben Tankvorgang hochgeladen, bleibt die
getippte Zahl stehen.
Live nachgewiesen: getippte Liter akzeptiert, Korrektur lief in derselben
Millisekunde wie "Tankvorgang aktualisiert" (nicht erst bei der naechsten
Fahrt), und die Deckungspruefung verwarf das Intervall korrekt mit "0 % der
gefahrenen Strecke" - dort liegt ein Fahrzeugwechsel.
### Das Uebersicht-Symbol war das kleinste der fuenf
Gemeldet, live per `getBBox()` nachgemessen: 18,0 x 10,3 von 24 - **43 %** der
Hoehe, waehrend die vier Nachbarn zwischen 65 % (Tanken) und 89 % (Fahrten)
liegen.
Auf volle Rasterbreite gezogen (Faktor 1,33, `stroke-width` 1,125 als
Ausgleich, damit die Strichstaerke gerendert wieder 1,5 betraegt): jetzt
23,9 x 13,7, also **57 %** - die Breite entspricht exakt "Mein Audi".
**Bewusst nicht weiter:** das Symbol ist ein Halbkreis und damit bauartbedingt
halb so hoch wie breit. Fuer 72 % Hoehe braeuchte es Faktor 1,68 und waere mit
30 Einheiten breiter als das Raster. Wirklich gleich hoch wird es nur durch
Neuzeichnen des Bogens mit groesserem Winkel - dem Eigentuemer angeboten,
nicht unaufgefordert gemacht.
## Working conventions (observed — keep them)
- German is the project language: identifiers, comments, commits, UI texts. Exceptions: