Commit Graph

108 Commits

Author SHA1 Message Date
tobias 863284e540 Hauptuntersuchung: Ableitung aus Erstzulassung und Wartungsplan, drittes Feld entfernt
Gemeldet: Wartungsplan leer, Erstzulassung 01.03.2025, erwartet 01.03.2028 -
gezeigt wurden drei verschiedene Antworten (Uebersicht 01.08.2026, Mein Audi
"August 2026", Service "kein Eintrag im Wartungsplan").

Ursache war das Profilfeld hauptuntersuchung_faellig ("08/2026"), das die
Ableitung ueberstimmte, in keiner Oberflaeche ein Eingabefeld hatte und im
Backend kein Schema. Drei Bildschirme verarbeiteten denselben Wert
verschieden. Das Feld ist samt Lese- und Schreibweg entfernt; es gibt jetzt
zwei Quellen: Wartungsplan + 24 Monate, sonst Erstzulassung + 36 Monate.

Dabei mitbehoben:

* Die Service-Kachel verschluckte abgeleitete Termine, sobald kein
  Wartungsplan-Eintrag dahinterstand.
* monatePlus() in der App verlor einen Tag ueber die Zeitumstellung (drei von
  fuenf gemessenen Faellen) - betraf Oelwechsel- und Inspektionsprognose
  genauso. Das Panel rechnete dort seit jeher richtig.
* Die App verlangte fuer jeden Wartungsplan-Eintrag eine Kilometerangabe. Eine
  Hauptuntersuchung ist rein datumsbasiert; ohne km waere der erste HU-Eintrag
  ignoriert worden. Ausserdem nahm das Panel den ersten Treffer der Liste, die
  App den juengsten - beide nehmen jetzt den juengsten.
* Monatsgenauigkeit ("03/2025") bleibt auf beiden Seiten erhalten.

Neu: paritaet_hu.test.ts vergleicht die App gegen die aus der Panel-Quelle
herausgeschnittenen Originalfunktionen, 15 Faelle. Gesamt 292 App-Tests, 73
Python-Tests mit 231 Untertests, 0 Tracebacks. Live an der Testinstanz auf
allen drei gemeldeten Bildschirmen geprueft.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 01:56:11 +02:00
tobias 5621b818cc flespi: echtes Lesen statt Zwischenspeicher, und kein Gleichstand ohne Grundlage
Der Waechter meldete am 04.09.2026 "stimmt ueberein" fuer
trip_scenario.ign_off_timeout: 900 gegen unsere Konstante NACHLAUF_S = 900.
Das Geraet hat dort 0 - das Trip-Szenario ist abgeschaltet. flespis
Zwischenspeicher trug den Stand vom 01.09. 13:15, dreieinhalb Tage alt.
Der Eigentuemer hat es aufgedeckt: "die gelesenen werte sind veraltet".

Zwei Aenderungen:

1. "Jetzt lesen" leert erst den Zwischenspeicher der ueberwachten
   Einstellungen (DELETE /gw/devices/{id}/settings/{name} - die
   API-Entsprechung des Panel-Knopfes "clear cache and synchronize"; loescht
   den gespeicherten Wert, nicht den im Geraet) und holt dann neu.
   Einstellungen mit einem AUSSTEHENDEN Wert werden uebersprungen: dasselbe
   DELETE wuerde ihn verwerfen, und die Aenderung kaeme nie an - wie
   1003/1004, die am 04.09. acht Stunden in der Warteschlange standen.

2. `abweichung` ist dreiwertig: true, false und **null (kein Urteil)**. Null
   steht dort, wo ein Vergleich nichts aussagt - der Wert wurde nie vom
   Geraet bestaetigt, oder es ist gerade eine Aenderung unterwegs. Dazu
   traegt jede Zeile `gemeldet_am` (flespis `updated`), und beide
   Oberflaechen zeigen es an: "bestaetigt 01.09.2026 (4 T. alt)" bzw.
   "nie bestaetigt".

Ein stiller Gleichstand mit einem veralteten Wert ist schlimmer als gar kein
Vergleich: er behauptet Sicherheit, wo keine ist.

tsc sauber, 271 Tests, vite build sauber, Panel als Modul geparst,
py_compile sauber, audi_ha_test auf 2026.9.4.26 ohne Traceback.
2026-09-05 01:11:23 +02:00
tobias 9ac69cdf44 Eine Neuschreibung ist kein Wechsel - Wartezeit startet nicht mehr neu
Home Assistant schreibt denselben Zustandswert auch neu, wenn sich nur ein
Attribut geruehrt hat. Am 04.09.2026 gezaehlt: von 22 Zustandszeilen der
Zuendung waren nur 6 echte Wechsel, die uebrigen 16 Neuschreibungen - und
alle 6 Wechsel lagen ausserhalb der Fahrten. Das Zuendungssignal ist
unterwegs stabil.

Der Beobachter unterschied das nicht. Fiel eine Neuschreibung von "aus" in
die laufende Wartezeit, brach Zweig 2 die wartende Bestaetigung ab und
begann die 15 Minuten von vorn. Am 04.09. ist es nicht passiert - die
Neuschreibungen lagen hinter dem Fenster -, aber das Loch war da.

_signalwechsel() bekommt jetzt mit, ob es ein echter Wechsel war. Eine
Neuschreibung, waehrend fuer diese Fahrt schon eine Bestaetigung wartet,
laesst sie stehen.

Bewusst NICHT als Filter ganz oben: _spaetes_aus() verlaesst sich darauf,
ein Zuendungs-Aus auch als off -> off zu sehen - daran haengt das
Nachtragen vorlaeufiger Enden. Die Pruefung sitzt nur in Zweig 2, und ein
eigener Test belegt, dass der andere Weg weiter offen ist.

21 Tests (von 18), Gegenprobe mit ausgeschalteter Pruefung scheitert wie
erwartet an einem. py_compile sauber, audi_ha_test auf 2026.9.4.25 ohne
Traceback.

AGENTS.md haelt ausserdem fest: die Beschreibung des Beenden-Knopfes war zu
weit gefasst (die Korrektur zieht nur nach hinten, nie nach vorn), und der
flespi-Cache laesst sich ueber DELETE auf die Einstellung zum Neulesen
zwingen - mit dem Haken, dass dabei ausstehende Werte verloren gingen.
2026-09-05 01:02:11 +02:00
tobias 242312441d Pausenregel rechnet in Geraetezeit - zwei Fahrten bleiben zwei
Am 04.09.2026 war der Dongle von 18:25 bis 23:36 ohne Verbindung. Als er
sich meldete, kamen das Zuendungs-Aus der ersten Fahrt und das Ein der
zweiten in DERSELBEN Sekunde bei Home Assistant an - ihre Geraetezeiten
lagen 5 Stunden 12 auseinander (18:23:32 und 23:35:43).

Die Pausenregel mass an der Ankunft: 0 Sekunden, also dieselbe Fahrt.
Ergebnis war ein Eintrag von 18:10 bis 23:42 ueber 5 h 33 ohne Strecke,
und die Fahrt um 23:30 gab es gar nicht.

Zwei Stellen im Code trugen dazu bei:
- zuendung_geaendert() rief bedingungslos warte_ende_ab_abbrechen(); das
  "an" loeschte damit das wartende Ende des "aus" derselben Sekunde.
- Der Fall "an, waehrend eine Fahrt laeuft" fiel wortlos durch alle drei
  Zweige von _signalwechsel(). Dazu steht seit dem 02.09. ein Kommentar im
  Code - damals wurde eine Sperre eingebaut, die die Reihenfolge richtig
  stellt, aber nicht den Ausgang.

Neu entscheidet _zuendung_kehrt_zurueck() in Geraetezeit. Der Koordinator
fuehrt mit, welches Ende gerade seine Pausenzeit absitzt (ende_wartet).
Liegt zwischen dem echten Ende und dem neuen "an" mehr als die Pausenzeit,
wird die alte Fahrt auf ihr berechnetes Ende geschlossen und eine neue
begonnen. Gemessen wird ab dem echten Ende, nicht ab dem Signal. Ohne
wartendes Ende passiert nichts - ein Schnitt auf Verdacht waere schlimmer
als eine zu lange Fahrt.

Das behebt die Verspaetung nicht, es macht sie harmlos.

Die Tests mussten zwei Dinge trennen, die vorher eine Zahl waren: PAUSE_S
(900, das Fenster der Regel) und WARTEN (0,15 s, die Wanduhrzeit des Tests).

18 Tests in test_zuendungspause.py (von 14), py_compile sauber,
audi_ha_test auf 2026.9.4.24 ohne Traceback. Gegenprobe am alten Stand:
2 der 4 neuen Tests scheitern dort, mit der Korrektur laufen alle 18.
2026-09-05 00:40:59 +02:00
tobias b0ec8b4982 Pruefdurchgang ueber alle 26 Bildschirme - fuenf Fehler behoben, HIG nachgezogen
Gemessen statt geschaut: eine Pruefroutine lief auf jedem Bildschirm und
pruefte Ueberlauf, Tippziele, abgeschnittenen Text, Bilder ohne Quelle und
WCAG-Kontrast fuer jeden Textknoten. Dazu die Live-App mit echten Daten, die
Musterseite fuer Leerzustaende und das Panel zum Abgleich.

1. Deutsche Datumsangaben wurden falsch gelesen. new Date("01.03.2025")
   liest amerikanisch: Tage bis 12 wurden still vertauscht, ab Tag 13 fiel
   der Wert ganz weg. Die Erstzulassung stand auf zwei Bildschirmen
   unterschiedlich da. alsZeitpunkt() erkennt jetzt ISO, TT.MM.JJJJ und
   MM/JJJJ, weist unmoegliche Daten ab und fuehrt die Genauigkeit mit -
   aus "08/2026" wird "August 2026", kein erfundener Erster.

2. Die Hauptuntersuchung war in beiden Oberflaechen unerreichbar. Das Panel
   leitete sie aus der Erstzulassung ab, aber nur bei MM/JJJJ, und benutzte
   den eingetragenen Wert gar nicht. Jetzt beide: eingetragene Faelligkeit
   vor Servicebuch vor Erstzulassung + 24 Monate.

3. Die Tankstellenmarke stand zweimal da, wenn der Name kein Komma hat
   ("Shell München Ost"). ohneMarkeVorn() nimmt sie vorn ab.

4. Bilder wurden geholt, obwohl beide wussten, dass es sie nicht gibt -
   fuenf 404 fuer dieselbe Datei in einem Ladevorgang. Jetzt gar keine
   Adresse bei Stand 0; nachgemessen 4 Anfragen, alle 200.

5. Ein Bildelement ohne Quelle zeigte das kaputte Bildsymbol, und onerror
   greift dort nicht: ohne src gibt es keinen Ladeversuch.

Dazu: leere Klammern beim Steuersatz, leere graue Kachel bei leerer
Leistungsgruppe, Umbruch auf dem Einrichtungsbildschirm, drei
Leerdarstellungen in einer Kachel, fehlende Grundschrift.

Apple HIG: Kontrast war an einer Stelle 3,18 statt 4,5, weisse Schrift auf
der Loeschflaeche 3,41/3,55. Rot als Schrift und Rot als Flaeche sind jetzt
getrennte Token (--bad / --bad-flaeche), beide Apples systemRed fuer
erhoehten Kontrast. Tippziele: unsichtbares 44x44-Overlay, wo die sichtbare
Groesse Teil des Bildes ist, echte Mindesthoehe, wo das Element Flaeche ist -
Formulare werden dadurch sichtbar hoeher.

Neu auf Wunsch: Wischen in der Bildergalerie (beide Richtungen) und die
HIG-konforme Loeschgeste - ein voller Wisch loescht ohne zweiten Tipp, ab
55 % der Zeilenbreite, mit wachsender roter Flaeche und erhaltener
Rueckfrage. Dabei fiel auf, dass der Zugwert aus dem React-Zustand gelesen
wurde und ein schneller Wisch dadurch verlorenging; er liegt jetzt in einer
Referenz.

Paritaet: alle 26 Routen und Titel decken sich, Fahrzeugstatus zeilengleich,
zwei ungeplante Abweichungen (Datumsformatierung, Herkunft der
Hauptuntersuchung) geschlossen.

tsc sauber, 271 Tests, vite build sauber, Design-System gebaut, Panel als
Modul geparst, audi_ha_test auf 2026.9.4.23 ohne Traceback. Live nachgemessen:
Datum auf beiden Bildschirmen gleich, keine 404 mehr, Zurueck-Pfeil sichtbar
34x34 und treffbar 44x44, Galerie wischt in beide Richtungen, voller Wisch
loest die Rueckfrage aus, keine Kontrast-Unterschreitung mehr.
2026-09-04 18:11:07 +02:00
tobias 8557bfe230 Markenerkennung: Grenzen gemessen, Summenzeile im Kopf ist keine Marke
Auf die Frage, ob Belege einheitlich gestaltet sind: nein. Die Erkennung
haengt an drei Annahmen - Marke in den ersten acht Zeilen, Marke in der
festen Liste, Marke ueberhaupt vorhanden. Alle drei sind jetzt gemessen und
in Tests festgehalten.

Dabei ein echter Defekt gefunden: "Total" ist Kopfmarke UND Summenwort. Ein
Beleg mit Betrag in den ersten acht Zeilen lieferte faelschlich die Marke
"Total" (an einem echten Beleg reproduziert). _betragszeile() filtert
Summenzeilen jetzt aus dem Kopf - verlangt werden beide Kennzeichen zugleich,
Summenwort und Geldangabe, damit "SHELL TANKSTELLE, 6450 SOELDEN" nicht
mitfaellt. Gegenprobe: ein echter "TOTAL TANKSTELLE"-Kopf ergibt weiter Total.

Der Ausfall beschaedigt nichts: mit unbekannter Marke im Kopf bleiben Liter,
Betrag, Rabatt und Anschrift unveraendert richtig, nur die Marke fehlt.

Parser-Suite 17 Tests (von 11), alle zwoelf abgelegten Belege liefern weiter
Shell, audi_ha_test auf 2026.9.4.22 ohne Traceback.
2026-09-04 17:09:12 +02:00
tobias bdbdb4a010 Tankstellenmarke als eigenes Feld, Belegfelder in der App richtig zugeordnet
Die Marke steht auf jedem Beleg, war aber nirgends gespeichert: der Parser
erkennt sie seit dem 16.08.2026, faltete sie aber nur in station_name.
Fuer jeden Beleg, den eine aeltere Fassung eingelesen hat, blieb dort der
Betreibername stehen und die Marke war danach nicht mehr zu holen.

- shell_beleg_parser: station_brand als eigenes Feld aus beiden Parserwegen,
  dazu marke_aus_text()/marke_aus_pdf() - sie lesen nur den Belegkopf und
  haengen nicht am vollstaendigen Einlesen.
- belege.marken_nachtragen(): traegt die Marke einmal beim Start aus der
  abgelegten Belegdatei nach. Nur wo das Feld gar nicht vorkommt und die
  Datei liegt; ein einmal eingetragenes Feld wird nie ueberschrieben.
- Panel und App: die gespeicherte Marke geht vor, geraten wird nur, wo keine
  da ist. Ein Teil, der fuer sich genommen eine Kopfmarke ist, taugt zudem
  nie als Ort.

Dazu Punkt 5 der Meldung vom 04.09.2026: ein eingelesener Beleg fuellte nur
Datum und Uhrzeit. Der Parser las ihn immer vollstaendig - die App fragte
liter/kosten/station/ersparnis/kraftstoff ab, veroeffentlicht werden aber die
Namen des Belegs (liters/fuel_total_eur/station_name/discount/fuel_type).
Uebereingestimmt hat allein ts. Die Zuordnung steht jetzt als
belegFormularwerte() in daten/belegentwurf.ts und wird gegen die echte
Nutzlast dieses Belegs geprueft.

tsc sauber, 258 Tests (die drei neuen Marken-Tests gegen den alten Stand als
scheiternd nachgewiesen), vite build sauber, Panel als Modul geparst,
Parser-Suite 11 Tests gegen elf echte Belege. Panel und App liefern fuer elf
Faelle byteweise dasselbe. Live in audi_ha_test auf 2026.9.4.21 ohne
Traceback: Uebersicht "Shell, Nuernberger Str., Ansbach", Einzelbeleg
zweizeilig mit Marke, und der echte geteilte Beleg fuellt durchs Backend das
ganze Formular.
2026-09-04 17:00:22 +02:00
tobias a99ae432fc Tankstellen-Nadel, doppeltes Tanken-Symbol, Tankstellenname, packageClassList
Fuenf Meldungen des Eigentuemers, vier davon behoben:

1. Tankstellen-Nadel war bei 32 px ein dunkler Fleck (poi zeichnet nur
   einen Ring). Gefuellter Punkt in currentColor zwischen Silhouette und
   Symbol - die Geometrie stand im Pfad. Rot wurde ausdruecklich
   abgelehnt, die Umsetzung aendert keine Farbe.

2. Tanken-Symbol im Standort-Blatt wurde gefuellt UND umrandet, weil das
   CI-Symbol zusaetzlich stroke-Attribute mitbekam. Jetzt ueber ciSVG
   wie das Parkplatz-Symbol daneben; in der App dafuer imRaster an
   SymbolTanken (der 0,84-Faktor gilt nur in der Reiterleiste).

3. Tankstellenname: der Bestand hat zwei Bauformen - aeltere Belege
   tragen den Betreiber in station_name und die Anschrift daneben.
   Neu tankstelleTeile() liest beide Felder, entfernt Firmierungen,
   kuerzt Strassen und trennt die Hausnummer auch ohne Leerzeichen.
   Uebersicht: Marke, Strasse, Ort. Einzelbeleg: zwei Zeilen.
   adresseTeilen() hat keinen Aufrufer mehr und ist entfernt.

4. Zwischenablage und Kalendereintrag hatten denselben Grund: Capacitor 8
   registriert nur, was in packageClassList steht, und cap sync traegt
   dort ausschliesslich npm-Pakete ein. Beide app-eigenen Plugins lagen
   im Programm und wurden nie registriert. ios-teilen-einrichten.mjs
   traegt sie jetzt nach, ios-signieren.sh bricht ab, wenn sie im
   fertigen .ipa fehlen.

5. Geteilter Beleg nur mit Datum und Uhrzeit bleibt offen - der Weg
   nutzt denselben belegLesen()-Pfad wie eine gewaehlte Datei, es fehlt
   der konkrete Beleg oder die Protokollzeile.

tsc sauber, 246 Tests, vite build sauber, Panel als Modul geparst,
audi_ha_test auf 2026.9.4.20 ohne Traceback. Panel und App liefern fuer
die Tankstelle byteweise dasselbe.
2026-09-04 16:34:46 +02:00
tobias cefb235f6d Versionsvergleich: nur eine neuere Fassung zaehlt
Nach dem Xcode-Lauf lief die App auf 2026.9.4.17, die Instanz auf
2026.9.4.5 - und die App bot an, sich auf .5 zu erneuern. Die
Gleichheitspruefung konnte den Rueckschritt nicht sehen.

versionOrdnung() sortiert jetzt Stelle fuer Stelle als Zahl, aber nur was
dem Format JJJJ.M.T.N entspricht; sonst gibt es eine Abweichung ohne
Richtung. Streifen, Startblatt und OTA-Knopf schlagen nur noch an, wenn
die App nachweislich aelter ist. Damit ist die frueher dokumentierte
Entscheidung "nur auf Gleichheit" umgedreht - Begruendung in
VERSIONIERUNG.md und AGENTS.md Abschnitt CL.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 16:10:47 +02:00
tobias e237bf1e8b Server-Adresse: das Schema richtet sich nach dem Ziel
Beim Neuverbinden stand "localhost:5173" im Feld, die App meldete "nicht
erreichbar". Ursache war basisUrlNormalisieren(): ohne Schema setzte es
blind https:// davor - womit die eigene Vorlage der App unbrauchbar war,
denn im Feld steht als Beispiel 192.168.1.20:8123, und im Heimnetz gibt
es kein Zertifikat.

Jetzt nach dem Ziel: localhost, 127.x, 10.x, 192.168.x, 172.16-31.x, ::1
und .local/.home.arpa/.localhost bekommen http, alles andere weiter https
(datametric360.de laeuft ueber Cloudflare). Steht ein Schema da, wird
nichts geraten.

Vier Regressionstests, darunter der gemeldete Fall, die Vorlage aus dem
Feld und 172.32.0.1 als Gegenprobe zum privaten Bereich.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 14:16:32 +02:00
tobias 461151cfec Rueckfrage vor loeschenden Aktionen: eigenes Blatt statt window.confirm
Gemeldet als "Verbindung trennen tut nichts". Kein Fehler der App: in
eingebetteten Ansichten sind Dialoge abgeschaltet, window.confirm kehrt
sofort mit false zurueck (gemessen: 1 ms), und abmelden() beginnt damit.

Die Suche danach brachte den eigentlichen Befund: elf Stellen hingen an
window.confirm - acht direkt, drei im Zeilenmenue (Wischgeste in Fahrten,
Tankvorgaengen, Messwerten). Jede davon eine loeschende Aktion, jede in
eingebetteten Ansichten still wirkungslos. Das Panel zeigt an denselben
Stellen seit jeher sein eigenes Blatt.

screens/bestaetigung.tsx: ein kleiner Speicher plus ein ActionSheet, das
einmal in App.tsx haengt. Kein Hook je Aufrufstelle - der haette jede der
elf Stellen gezwungen, das Blatt auch selbst zu rendern, elf
Gelegenheiten es zu vergessen, ohne dass etwas auffaellt. Ohne gehaengtes
Blatt (Testlauf, Fremdeinbettung) gilt weiter window.confirm.

LOESCH_TEXT und LOESCH_HINWEIS wortgleich aus dem Panel, samt der
Trennung in Frage, erklaerenden Satz und Knopfbeschriftung.

225 Tests (vier neue), live an der gemeldeten Stelle geprueft: das Blatt
erscheint, Abbrechen schliesst es, die Sitzung bleibt. Bestaetigt wurde
bewusst nicht - das haette den Token vom Geraet geloescht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 14:07:49 +02:00
tobias ae276fb518 Leerzustand der Karte deckend: 2,45:1 waren zu wenig
Der Kasten "Kein GPS-Signal vom Fahrzeug" liegt auf Leaflets eigenem
Hellgrau, und das ist in beiden Themen hell. --tile ist im Nachtmodus ein
durchscheinender weisser Schleier - er macht die Flaeche dort noch heller,
waehrend der Text der Nacht-Palette folgt: gemessene 2,45:1, deutlich
unter den 4,5:1 dieses Projekts.

Deckender Kasten plus Zweitfarbe, in beiden Codebasen. Live nachgemessen,
identische Werte: 9,86:1 nachts, 10,94:1 tags.

Kein Gleichlauf-Befund, sondern eine gemeinsame Schwaeche - deshalb auf
Nachfrage vorgelegt und erst nach Freigabe geaendert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 13:57:09 +02:00
tobias 9e9ca9ca28 Audit: Wisch-Zeile und Auswahlliste der App nachgezogen
Gleichlauf, Funktion und Fehler geprueft. Sauber: 28/28 Dienste ueber
const/Registrierung/services.yaml/Klarnamen, 27/27 Sensorrollen, Routen
deckungsgleich, 47 Panel-Seiten live ohne einen Konsolenfehler, App
ebenso, Fahrzeugstatus textgleich, Bestand ohne Befund ausser den drei
bekannten Altbelegen mit odometer_km als Zeichenkette.

Zwei echte Befunde, beide in der App und beide dort, wo ein juengerer
Zwilling die Korrekturen des aelteren nie bekommen hat:

WischLoeschen (Raeder-Archiv) liess den roten Loeschknopf dauerhaft
sichtbar unter der Zeile stehen - genau der Haarlinien-Fehler, den das
Panel am 16.08.2026 mit visibility:hidden behoben hat - und malte den
Zeileninhalt mit dem durchscheinenden --tile auf eine Kachel, wodurch im
Nachtmodus der Streifen heller wird und das Rot durchscheint (Regel aus
Abschnitt AP). Beides behoben und im Nachtmodus nachgemessen.

.dm-auswahl option stand auf --tile; die aufgeklappte Liste eines <select>
malt der Browser ausserhalb der Seite, ein durchscheinender Wert laesst
dort den hellen Systemgrund durch. Im Panel steht dafuer seit dem
16.08.2026 --canvas.

Zur ausgelieferten .ipa: entpackt und gelesen - Erweiterung mit echtem
Programm, beide Plugins samt Methoden im App-Programm, App-Gruppe in
beiden Profilen, Kalender- und Standortschluessel da. Fehlt nur die
CFBundleVersion der Erweiterung (der Grund fuer die abgelehnte
Installation) und der Kamera-Schluessel - beides im Skript behoben und
wirksam beim naechsten Xcode-Lauf.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 13:49:40 +02:00
tobias 68732aa10f Bilder: der Cache-Brecher haengt am Foto statt an der Startzeit
Beide Oberflaechen haengten Date.now() an jede Bildadresse - richtig
gegen ein veraltetes Foto (31 Tage Cache-Vorgabe unter /local/), aber die
Adresse aendert sich damit bei JEDEM Start, der Zwischenspeicher greift
zwischen zwei Starts also nie. Mit dem Vorladen waeren das 5,3 MB je
Start gewesen.

bilder.staende() liefert jetzt den Zeitstempel je Datei, veroeffentlicht
an sensor.audi_dashboard_app_version; eine Bildadresse aendert sich damit
genau dann, wenn das Foto ein anderes ist. Ein fehlendes Foto bekommt 0
(dann loest sich auch ein gespeicherter 404 von selbst auf), ein Backend
ohne Staende faellt auf das bisherige Verhalten zurueck, und Upload wie
Loeschen veroeffentlichen sofort neu.

Die App merkt sich den letzten Stand im Browserspeicher: sie malt ihren
ersten Bildschirm, bevor die Versionsangabe eintrifft - ohne das
Gedaechtnis trug genau dieser Durchlauf noch die Startzeit und holte das
Uebersichtsfoto doch wieder bei jedem Start.

Live gemessen: Neuladen ohne Aenderung 5.328.771 Bytes Inhalt bei 0 Bytes
uebertragen; ein per touch geaendertes Foto wird neu geholt (513.403
Bytes), die anderen nicht; App-Start ohne eine einzige Adresse mit
Startzeit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 13:29:45 +02:00
tobias d6f7993504 QR-Code aus gespeichertem Bild, stiller Debug-Knopf, HA-Zugang bei den Zugaengen
Der Scanner liest den QR jetzt auch aus einer gespeicherten Bilddatei -
in der Testumgebung ohne Kamera der einzige Weg, auf dem Telefon der
kuerzere (der Token entsteht am Rechner, das Bildschirmfoto liegt ohnehin
auf dem Geraet). Gleicher Leser, gleiche Deutung, nur eine andere Quelle;
ohne Kamera entfaellt das schwarze Quadrat.

Dabei ein echter Fehler in der ersten Fassung, gefunden an der
Musterseite: img.decode() loest bei einem GIF in Chromium nie auf - das
Blatt haette ohne Meldung ewig gewartet. Jetzt createImageBitmap, als
Rueckfall bewusst das load-Ereignis statt decode().

Debug-Mode ist nur noch ein stiller Textknopf am Seitenende, ohne Kachel
und ohne Erlaeuterung; der Zugang zu Home Assistant steht als Akkordeon in
der Kachel "Zugaenge" darin. Beides auf Vorgabe des Eigentuemers.

Die Musterseite kann jetzt auch die Ersteinrichtung (?seite=einrichtung) -
damit ist der ganze Weg ohne echten Token nachgewiesen: selbst erzeugter
QR mit Token-Text landet zeichengleich im Feld, einer mit http-Adresse im
Adressfeld, ein Bild ohne Code meldet das sauber.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 13:08:10 +02:00
tobias 4ab6b9fca5 Debug-Mode, eine Version-Kachel, Knoepfe nach HIG, Bilder ohne Verzoegerung (2026.9.4.11)
Debug-Mode: Zugaenge und Dongle stehen nicht mehr verstreut oben in den
Einstellungen, sondern zusammen hinter einem Schalter am Seitenende - dazu
neu der letzte Datensatz des Dongles (geraet.py). "Letzter Datensatz" heisst
dabei: alle Entitaeten des Geraets, deren Aktualisierung hoechstens zwei
Sekunden nach der juengsten liegt - die flespi-Integration setzt einen
Datensatz in Millisekunden. Angezeigt mit Ankunft, Geraetezeit und dem
Rueckstand zwischen beiden.

Beim Verifizieren gefunden: app_version_veroeffentlichen() lief nur beim
Start und nach einer Aktion, und beim Start haben die Entitaeten des Geraets
noch keinen Zustand - die Kachel waere dauerhaft leer geblieben. Laeuft
jetzt im Minutentakt mit, ohne Netzaufruf.

Ausserdem:
- "Version" und "Integration-Update" sind eine Kachel
- Speichern/Verwerfen als zwei gleich breite Knoepfe, der bestaetigende
  gefuellt und rechts; "Verwerfen" war in der App blosser Text
- Bilder werden nach dem ersten Zeichnen in der Leerlaufzeit vorgeladen UND
  dekodiert. Die Verzoegerung beim Seitenwechsel kam nicht vom Netz (31 Tage
  Cache), sondern vom asynchronen Dekodieren eines frisch erzeugten <img>.

214/214 Tests, Panel als Modul geparst, live gegen audi_ha_test geprueft.
2026-09-04 12:15:33 +02:00
tobias 63a45d4ee2 Die .ipa war nicht installierbar, plus QR-Scanner (2026.9.4.8)
Der Erweiterung fehlte CFBundleVersion - eine Pflichtangabe, ohne die iOS
die Installation der ganzen App verweigert ("kann nicht installiert werden").
Verursacht von der Versionsangleichung vom Vortag: das Skript reichte den
Platzhalter $(CURRENT_PROJECT_VERSION) statt einer Zahl weiter, und das per
Skript angelegte Ziel der Erweiterung kennt diese Build-Einstellung nicht.
An den entpackten .ipa nachgemessen, 2026.9.3.3 gegen 2026.9.4.5.

ios-signieren.sh reicht nur noch echte Zahlen weiter, das Gegenlesen der
fertigen .ipa bricht ab statt zu warnen, und abgelegt wird erst danach -
eine Datei, die die Pruefung nicht besteht, darf nicht in auslieferung/
landen.

Dazu:
- Kopfmarke statt Firmierung des Betreibers in der Tankstellen-Zeile
- dritter Suchbegriff: der Name ohne fuehrende Marke. Gemessen: Nominatim
  findet "Shell, Pascalstr. 8, Ingolstadt" nicht, "Pascalstr. 8, Ingolstadt"
  schon - daran blieb die Belegkarte der realen Instanz leer
- der Tankstellen-Verweis zeigt die Tankstelle, statt eine Route zu rechnen
- Cache-Brecher auf .ipa, Symbolen und manifest.plist
- QR-Scanner fuer den Zugangs-Token bei der Ersteinrichtung (jsQR statt
  GoogleMLKit, damit der Xcode-Lauf kein pod install riskiert)

214/214 Tests, Panel als Modul geparst, live gegen audi_ha_test geprueft.
2026-09-04 11:51:44 +02:00
tobias 43f25e2e0d Tankstelle zweizeilig und antippbar, Belegkarte ohne toten Knopf (2026.9.4.5)
Sieben Meldungen des Eigentuemers, beide Codebasen.

Karte im Einzelbeleg: "Position laedt nicht" und "der Button funktioniert
nicht" waren dieselbe Stelle - ohne aufloesbaren Ort malte das Panel einen
beliebigen Ausschnitt und liess den Zentrieren-Knopf fuer immer disabled.
Beide zeigen jetzt die Auskunft statt einer Karte und unterscheiden dabei
"steht nicht im Beleg" von "findet niemand". Dazu ein echter Paritaetsmangel:
die App schlug nur station_address nach, das Panel faellt seit jeher auf den
Namen zurueck - von 16 Testbelegen tragen vier eine Anschrift.

Der Suchbegriff darf Name und Anschrift NICHT zusammensetzen: gemessen findet
Nominatim "Pascalstr. 8, 85057 Ingolstadt", mit dem Betreibernamen davor
nichts. Jetzt zwei Kandidaten nacheinander (Anschrift, dann Name); fuer den
Kartendienst dagegen beides zusammen, weil Apple und Google Betriebsnamen
kennen.

Tankstelle steht ueber die volle Kachelbreite, zweizeilig (Tankstelle und
Strasse / Ort) und fuehrt beim Tippen in den Standard-Kartendienst - Apple
Karten auf Apple-Geraeten, sonst Google Maps. Dieselbe Wahl jetzt auch fuer
Fahrzeugstandort und Autohaus.

SmartDeal-Ersparnis brach um, weil die dt-Spalte 150,00px breit war und ihr
Inhalt 150,20px - zwei Zehntel Pixel, Zeile dadurch 44,8 statt 26,4px hoch.

Der Regler "Fahrt beenden" schreibt nicht mehr beim Loslassen: derselbe Wert
stellt den Schlaf-Timeout des Dongles, das soll bewusst ausgeloest werden.
Speichern/Verwerfen erscheinen nur bei echter Aenderung.

Share-Erweiterung: an der ausgelieferten .ipa gemessen trug sie 1.0, die App
2026.9.3.3 - iOS registriert eine Erweiterung mit abweichender Versionsnummer
nicht, sie fehlt dann im Teilen-Blatt. ios-signieren.sh schreibt die Version
jetzt in beide Ziele und liest die fertige .ipa gegen. Zwischenablage: dritter
Weg ueber UIPasteboard.itemProviders, und ein fehlender nativer Teil wird als
solcher gemeldet statt als "Kein Zugriff".

Am Geraet gesetzt: 1003 Network Ping Timeout 0 -> 60 s, 1004 ACK Type
TCP/IP -> AVL. Beide liegen bei flespi als pending bereit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 10:10:55 +02:00
tobias 7f386d19ea Geraetezeit statt Ankunftszeit - die Wurzel hinter der 3-km-Fahrt (2026.9.4.1)
Das Fahrtfenster stand seit dem 01.09. in der Gerätezeit, der Verlauf war nach
Ankunftszeit sortiert. Zwei Uhren, und das Gerät puffert: was verspätet ankam,
fiel aus einem Fenster heraus, in dem es der Sache nach lag. Am 03.09. hat das
eine Fahrt gekostet - 26 von 48 GPS-Punkten und 3,0 statt 7 km.

verlauf_lesen() stempelt jeden Punkt jetzt mit der Gerätezeit seines
Datensatzes. Eine Stelle, an der alle Verbraucher vorbeikommen. Dazu zwei
Riegel aus derselben Untersuchung: ein Rückschritt von 43 Metern ist keine
Zählerrücksetzung mehr (aus 87 Metern wurden vorher 13,7 km), und das
Fahrtende wird auf das letzte Lebenszeichen des Geräts geklemmt - der Nachlauf
ist eine Rechnung, kein Messwert.

Am echten Recorder nachgerechnet: Tacho 3,0 -> 7,0 km, GNSS 2,952 -> 6,877 km,
Route 26 -> 47 Punkte, Ende 23:21:57 -> 23:17:48. Beim ersten Lauf haben zwei
Fahrten eine Strecke bekommen, die vorher gar keine hatten.

Nicht das Gerät war schuld: es hat lückenlos aufgezeichnet. Die zehn Minuten
Funkstille waren eine offene, aber tote TCP-Sitzung (flespi-Log: 703 s, 16
Nachrichten) - kein Funkloch. Begründung und die Empfehlung fürs Gerät stehen
in AGENTS.md, Abschnitt CB.

Dazu die neun Paritätsbefunde, alle Richtung Panel gelöst - darunter ein
unmaskierter Punkt in beiden Codebasen, der aus "vor 3 Tage" ein "vor 3 Tag"
machte.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 02:18:12 +02:00
tobias 4a6c48e81c Zwei Textfehler der Dongle-Kachel, an der Sichtpruefung gefunden (2026.9.3.15)
"Gelesen vor 1 Std.." - der Punkt der Abkuerzung und der Satzpunkt standen
doppelt. Und im Panel stand noch der Halbsatz "Lesen und Schreiben gehen nur bei
laufender Zuendung" vor dem neuen Text - ausgerechnet die Behauptung, die am
selben Tag widerlegt wurde; meine Ersetzung hatte nur den hinteren Teil
getroffen.

Gefunden, weil die Kachel bis dahin nur an den Daten belegt war, nie angesehen.
Der erste Blick zeigte dabei eine sechs Fassungen alte Datei aus dem
Tab-Zwischenspeicher - die Lehre aus Abschnitt AQ, jetzt dort mit dem
Einzeiler zum Mitlesen der geladenen Fassung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 21:22:00 +02:00
tobias 6ec31b5275 Audit, zweite Runde: der Knopf schrieb ein erfundenes Ende (2026.9.3.14)
_letztes_lebenszeichen() fiel auf "jetzt" zurueck, sobald verlauf_lesen() eine
leere Liste lieferte - und die bedeutet sowohl "nichts aufgezeichnet" als auch
"nicht lesbar". Am 03.09. trat der zweite Fall ein: der Knopf traf einen
Neustart, der recorder war noch nicht bereit, die Abfrage lief 2:47 und kam leer
zurueck. Die Fahrt bekam 18:43 statt 17:59 als Ende, 74 Minuten statt 30.

Drei Quellen in fester Reihenfolge statt einer: Aufzeichnung, dann hass.states
(last_reported/last_updated - weiss dasselbe ohne Datenbank), dann "jetzt", und
das nur noch mit Warnung. Vier Regressionstests, drei davon gegen den alten
Stand nachweislich rot.

Dazu aus der Bestandspruefung: odometer_km liegt in drei alten Tankvorgaengen
als Zeichenkette vor, ablage.distanz_seit_letzter_tankung() rechnete damit
float - str. Laeuft jetzt durch als_kilometerstand(). Der Eingang war schon
zu; das ist Altbestand.

Und acht Dienste hatten keinen Klarnamen in translations/de.json - jetzt 28
Dienste, 28 Klarnamen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 21:15:42 +02:00
tobias 2736c3d6e0 Audit nach dem Dongle-Umbau: vier Befunde (2026.9.3.13)
1. profil_schreiben awaitete wunsch_uebernehmen und damit bis zu drei
   HTTP-Runden zu flespi - bei 39 Aufrufstellen von profilSpeichern() im Panel
   je Feldaenderung. Laeuft jetzt als eigene Aufgabe.
2. Ein gescheiterter Schreibversuch wurde bei jedem Speichern wiederholt, weil
   der Fehlerstand kein "werte" hat. Er merkt sich jetzt unter "versucht", was
   gescheitert ist. Dazu: kein zweites Schreiben, wenn der Wert schon als
   pending bereitliegt.
3. buendelPasst() versprach im Kommentar, ein aelteres Buendel abzulehnen,
   pruefte aber nur Ungleichheit - deshalb meldete die App "diese Fassung
   aendert auch Natives", obwohl nur das Buendel nach einem Versionssprung
   nicht neu gebaut war. Vergleicht jetzt gegen die Serverfassung, drei
   Regressionstests (der entscheidende gegen den alten Stand rot). Der Text
   behauptet keine Ursache mehr, die die App nicht kennen kann.
4. Das Regler-Minimum ging heute von 0 auf 1, ein bereits gespeicherter Wert
   darunter lief ungeprueft durch. Beide Oberflaechen klemmen jetzt auf 1-60.

182/182 Tests, 28 Backend-Dateien py_compile, beide Frontends als Modul
geparst, Dienst- und Katalog-Konsistenz in beide Richtungen geprueft, Buendel
auf derselben Fassung wie das Manifest.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 21:03:17 +02:00
tobias d4ba3bce7a Pending nur zeigen, wenn es sich unterscheidet (2026.9.3.11)
flespi legt beim Schreiben das ganze Objekt als pending ab. In der Kachel stand
deshalb "Sleep Mode 102: 2 -> 2 unterwegs", obwohl sich an type nichts aendert.
vergleichen() zeigt offen jetzt nur noch bei echtem Unterschied.

Gefunden an der ersten Fahrt mit echten Daten. Dieselbe Fahrt hat zwei weitere
Befunde geliefert, in AGENTS.md festgehalten: die Zuendung steht auf "an", weil
das der letzte je gesendete Wert ist (das Aus kam nie an) - der Fall, fuer den
"Fahrt beenden" gebaut ist; und address=connection hat 30 Minuten durchgehende
Verbindung nicht zum Zustellen genutzt, pending steht weiter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 20:44:45 +02:00
tobias 4d4890034a Regler stellt den Schlaf-Timeout des Dongles (2026.9.3.10)
Der Regler "Fahrt beenden" ist jetzt eine Zahl fuer zwei Dinge: unsere eigene
Wartezeit bis zum Fahrtende und der Schlaf-Timeout des FMM003 (103). So lange
bleibt der Dongle wach und wartet darauf, dass es gleich weitergeht; danach
schlaeft er. Die 180 s Ignition OFF Delay laufen davor und bleiben unsichtbar -
sie werden nur von der Fahrtdauer abgezogen. Der Regler geht deshalb von 1 bis
60 in Einerschritten; das Geraet kennt kein Timeout 0.

flespi.py liest und schreibt die Geraete-Konfiguration. Drei Annahmen sind an
der echten API gefallen und stehen als Messung im Modulkopf: der Wert liegt in
current (nicht value), die Einstellung heisst sleep_mode mit dem Wert eine Ebene
tiefer unter mode, und ein PUT braucht {"properties": ..., "address": ...}.

Die eigene Auftrags-Warteschlange ist wieder entfallen. Sie war fuer "das Geraet
schlaeft" gebaut - das gilt aber nur fuer das Geraet, nicht fuer die
Schnittstelle: die vollstaendige Konfiguration kam herein, waehrend das Fahrzeug
seit dem Vortag stand. flespi puffert selbst (pending + address=connection), ein
zweites Auftragsbuch waere eine zweite Warteschlange fuer dieselbe Aufgabe.

Gegengelesen wird nach jedem Schreiben; als Erfolg zaehlt current ODER pending -
letzteres ist bei stehendem Fahrzeug der Normalfall.

Live gegen den echten Account belegt: Token sieht genau ein Geraet, 127
Einstellungen bei parkendem Fahrzeug gelesen, Regler 15 geschrieben und als
pending bestaetigt (Nachbarfelder unveraendert), zweiter Neustart ohne erneute
Anfrage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 20:00:39 +02:00
tobias 8c96d9b78e Zugaenge: die Dienste-Token aus App und Panel bedienbar (2026.9.3.8)
Die Token liegen seit jeher in entry.options, geschrieben vom Options-Flow
unter Einstellungen -> Geraete & Dienste -> Konfigurieren. Der Weg bleibt und
ist der Rueckfall, wenn die App den Server nicht erreicht - genau der Fall, in
dem ein abgelaufener Token auffaellt. Erreichbar ist er aber nur ueber HAs
eigene Weboberflaeche; die iOS-App zeigt keine Konfigurationsdialoge. Die neue
Kachel ist die Bedienflaeche dafuer, kein zweiter Speicherort.

Der Wert kommt nie zurueck: veroeffentlicht werden nur "gesetzt/nicht gesetzt"
und die letzten vier Zeichen (unter zwoelf Zeichen Laenge gar nichts). Auch das
Protokoll bekommt ihn nicht, und das Panel leert das Eingabefeld sofort nach
dem Absenden.

Bewusst NICHT im Fahrzeugprofil: sicherung.py sichert fahrzeugprofil.json, und
"Backup exportieren" laedt es als Datei auf das Geraet - ein Token dort wanderte
in jede Sicherung und jede weitergegebene Datei.

Ein Dienst statt zwei: zugang_setzen(dienst, token), leerer Text loescht. In der
App nicht ueber die Warteschlange - ein Stunden spaeter nachgereichter Token
ueberschriebe womoeglich einen inzwischen eingetragenen neuen.

CONF_FLESPI_TOKEN ist neu und noch ohne Leser; der Flespi-Weg zum Auslesen der
Geraetekonfiguration ist damit vorbereitet, aber nicht gebaut.

Verifiziert: py_compile auf sechs Backend-Dateien, Panel als Modul geparst,
companion-app tsc sauber und 179/179, audi_ha_test auf 2026.9.3.8 sauber
gestartet. Live am laufenden Panel der ganze Kreis, Container hinterher wie
vorgefunden: Wegwerf-Token gesetzt -> "gesetzt . ...0000", der Wert selbst
taucht in den veroeffentlichten Attributen nirgends auf (darauf geprueft),
ueber die Sicherheitsabfrage geloescht -> wieder "nicht gesetzt", der
Gitea-Token unberuehrt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 15:08:55 +02:00
tobias 5ccbfec92d Fahrt beenden auf Knopfdruck, Schieberegler, Zuendungspause (2026.9.3.5-.7)
Vier Themen aus einer Sitzung.

1. Die Fahrt endet wieder bei uns statt im Dongle (Abschnitt BW). Der
   Ignition-OFF-Timeout des FMM003 (900 s) und sein Schlaf-Timeout (ebenfalls
   900 s) starten beide beim Zuendungs-Aus und fallen in derselben Sekunde -
   am 02.09. zweimal beobachtet, einmal ging das Trip-Ende verloren, einmal kam
   es eine Sekunde vor dem Schlaf an. Ausloeser ist jetzt die Zuendung, die
   Wartezeit laeuft in Home Assistant. ZUENDUNG_NACHLAUF_S = 180 wird in beiden
   Wegen abgezogen (ueber das Trip-Signal 1080 s, weil dessen Ende selbst an der
   verzoegerten Zuendung haengt).

2. Schieberegler, neu in beiden Codebasen. Schrittweite 5 Minuten; der Daumen
   war im Tagmodus weiss auf weiss und traegt jetzt einen Ring aus
   --line-strong - ein Token, das genau dort sichtbar ist, wo es gebraucht wird.

3. Knopf "Fahrt beenden" in der Zuletzt-Kachel (Abschnitt BX). Schliesst auf
   das letzte Lebenszeichen, nicht auf "jetzt", und kennzeichnet das Ende als
   vorlaeufig. Ein spaet eintreffendes Zuendungs-Aus zieht es nach - innerhalb
   von sechs Stunden, nur nach vorn, und nur bei einer vorlaeufigen Fahrt.
   ts_end wandert bewusst NICHT in edited_fields, sonst blockierte der Schutz
   fuer Handeingaben genau diese Korrektur.

4. Das OTA-Buendel kann auf dem Mac gar nicht entstehen (Abschnitt BV):
   npm run ota ist eine Windows-PowerShell-Datei, ios-signieren.sh baut ein
   frisches dist/ und fasst das Buendel nie an. Ausgeliefert war deshalb eine
   Oberflaeche ohne den Versionshinweis unter richtiger Nummer.

Verifiziert: Backend 27/27 (5 Signalwechsel, 14 Zuendungspause, 8 Fahrtende),
companion-app tsc sauber und 179/179, Panel als Modul geparst, audi_ha_test auf
2026.9.3.7 sauber gestartet. Live am laufenden Panel und ohne Rueckstand
belegt: Wartezeit samt Abbruch, die 180-s-Rechnung, der Regler im Tagmodus und
der Knopf von der laufenden Fahrt bis zum verworfenen Kurzvorgang - Fahrten
vorher 16, nachher 16.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 15:00:38 +02:00
tobias d30bb0e9f0 OTA-Buendel neu gepackt - ausgeliefert war eine Oberflaeche ohne den Versionshinweis (2026.9.3.4)
bundle.json trug 2026.9.3.3, gebaut am 02.09. 23:54 - der Versionshinweis
kam aber erst am 03.09. 09:50 dazu (c254cf5). In allen 16 Dateien der
ausgelieferten Zip kein Treffer fuer den neuen Code: die .ipa hatte das
Feature, das Buendel unter derselben Nummer nicht.

Kein vergessener Schritt. package.json bindet "ota" an eine Windows-
PowerShell-Datei; ios-signieren.sh baut ein frisches dist/, packt es in die
.ipa und fasst das Buendel nie an - npm run ota liesse sich dort auch gar
nicht ausfuehren. Jede Auslieferung vom Mac erzeugt diese Divergenz.

Schlimmer als der in VERSIONIERUNG.md beschriebene Normalfall, weil die
Nummern uebereinstimmen: die Pruefung in install.ps1 vergleicht Versionen,
nicht Inhalte, und schweigt deshalb.

Neue Nummer statt derselben, weil ein Geraet auf dem faulen .3.3 gegen den
Server "gleich" meldet und nie wieder etwas angeboten bekaeme. c254cf5 war
ausserdem eine Oberflaechenaenderung ohne Versionserhoehung - beides holt
2026.9.3.4 nach.

Verifiziert: neue Zip enthaelt dm360.version.gesehen, bundle.json auf .3.4,
sha256 54a1d06c...

Noch offen (Abschnitt BV): ota-paket.ps1 nach Node portieren und in
ios-signieren.sh hinter npm run build aufrufen, damit .ipa und Buendel aus
demselben dist/ kommen; dazu ein Waechter ueber die Git-Historie.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 10:57:43 +02:00
tobias 6016053a92 Kalender ueber EventKit, Orte nach dem Import, Fotos verkleinert, Signalsperre (2026.9.3.1-.3)
- Kalendertermine gehen nativ ueber EventKit statt ueber das Teilen-Blatt:
  Apples Kalender meldet sich beim System gar nicht als Teilen-Ziel an, ein
  anderes Dateiformat haette also nie geholfen (Abschnitt BS).
- Der Rueckblick stoesst jetzt selbst das Screening an, wenn er Fahrten
  angelegt hat - sonst blieb eine importierte Fahrt ohne Ort liegen, solange
  das Fahrzeug steht (Abschnitt BT).
- Fahrzeugfotos werden vor dem Upload im Browser auf 2000 px/WebP gebracht.
  Gemessen: 3.099.022 -> 325.994 Bytes. Damit ist die Nachrichtengrenze der
  WebSocket-Verbindung kein Thema mehr ("connection lost" auf der realen
  Instanz). Formate, die der Browser weder umwandeln noch anzeigen kann,
  werden mit klarer Meldung abgelehnt statt roh gespeichert.
- Zwei Wechsel des Fahrtsignals in derselben Sekunde kosteten eine ganze
  Fahrt: das "on" ueberholte den noch laufenden Beende-Vorgang und fiel durch
  beide Zweige. Neue asyncio-Sperre plus Regressionstest, der ohne sie
  nachweislich rot ist (Abschnitt BU).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 01:59:32 +02:00
tobias 8c95e50554 Teilen-Weg repariert, Zwischenablage nativ, Sprung in den Tankvorgang (2026.9.2.24/.25)
BEFUND. Die Uebergabe der Teilen-Erweiterung an die App konnte nie
funktionieren: sie legte den Beleg im Gruppen-Container ab, die App suchte ihn
ueber @capacitor/preferences - und dessen iOS-Code liest IMMER
UserDefaults.standard der App, configure({group}) setzt nur ein
Schluessel-Praefix. Falscher Behaelter, falscher Schluessel, und weil ein
fehlender Eintrag der Normalfall ist, meldete nichts einen Fehler.

UMBAU. Die Erweiterung schreibt das PDF als Datei in den Gruppen-Container und
gibt den Pfad im Aufruf mit; die App liest ihn ueber @capacitor/filesystem
(volle file://-Pfade) und loescht ihn danach. Abgeholt ueber getLaunchUrl()
und appUrlOpen - iOS benutzt beide. Nebenbei entfallen die
Base64-Aufblaehung und die 4-MB-Grenze der UserDefaults.

ZWEI WEITERE FEHLER AUF DEMSELBEN WEG. Ein Beleg zu einem bestehenden
Tankvorgang lief in die 20-Sekunden-Frist, obwohl der Server laengst fertig
war. Und beim Duplikat verriet das Backend nicht, welcher Vorgang gemeint ist
- jetzt kommt vorhandener_tank_id mit, und beide Oberflaechen springen
dorthin. Dazu: nach dem Speichern oeffnet sich der neue Vorgang, und waehrend
gelesen wird, steht "Tankbeleg wird verarbeitet ..." mit Zapfsaeule und
Kreisel im Formular.

ZWISCHENABLAGE. navigator.clipboard.read() gibt nur Text, HTML und Bilder
heraus - ein kopiertes PDF ist dort nie zu bekommen. UIPasteboard kennt die
Grenze nicht: ein kleines eigenes Plugin holt es, auch aus Mail (Anhang als
Verweis, ueber pasteboard.urls mit Sicherheitsbereich).

Der native Teil braucht einen Mac-Lauf; ios-teilen-einrichten.mjs spielt beide
Swift-Dateien bei jedem Bau ein.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 22:12:34 +02:00
tobias 36a6c21bfd Listenkoepfe vereinheitlicht, Zeilen mit Luft, Kalenderdateien mit Namen (2026.9.2.21-.23)
KOEPFE. Jahr und Monat tragen dasselbe Format (13px, Zweitfarbe, gemischt) -
vorher drei Elemente in drei Formaten. Die Versalien aus .20 waren falsch: das
ist der iOS-Stil vor 13. Vom Eigentuemer aus drei gerenderten Vorschlaegen
gewaehlt.

ZEILEN. padding-left 20px statt 0 - die Spalte des Aufklapp-Pfeils, damit die
Zeilen auf derselben Kante stehen wie der Text der Koepfe. Nachgemessen bei
375px: Beschriftungsspalte 157px, laengster Ortsname 142,7px.

GALERIE. Der gezeigte Platz ueberlebt den Seitenwechsel - er lag in der
Komponente und starb mit ihr. Im Panel ist er seit jeher modulweit.

KALENDER. Jeder Termin hiess "-.ics": in dateiname() fehlte das \w in der
Zeichenklasse, es ueberlebten nur w/ae/oe/ue/ss und der Bindestrich. Zwei
Termine ueberschrieben einander damit. Jetzt mit Umschrift und Rueckfall, und
ueber Share.files statt Share.url.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 21:15:50 +02:00
tobias d889353cbe Termine ohne Gedaechtnis, Listen nach HIG, zwei zu schmale Felder (2026.9.2.13-.20)
TERMIN. Der Werkstatttermin wanderte bisher ins Fahrzeugprofil, stand als
Merkmal an der Karte und liess sich "verwerfen" - eine zweite Verwaltung neben
dem Kalender. Auf Ansage entfernt: er dient jetzt allein dem Kalendereintrag
"was, wann, bei welcher Werkstatt". Dabei aufgefallen: die App schrieb gar kein
LOCATION in die .ics, obwohl das Panel es seit jeher setzt.

Ohne Speicherung startete das Feld leer ("tt.mm.jjjj", so gemeldet). Es
schlaegt jetzt die Faelligkeit der gewaehlten Art vor, sonst heute - dafuer
liefert service.ts serviceKandidaten(). "Andere" ergaenzt die Auswahl.

BREITEN. Am laufenden Panel gemessen: die vier festen 132px-Datumsfelder
brauchen ab 140px, damit die letzte Ziffer nicht unter dem Kalendersymbol
liegt (jetzt 150px); die Art-Auswahl braucht 178px fuer
"Hauptuntersuchung" (jetzt 190px). In der App war nichts davon kaputt - eine
dort vorsorglich eingebaute Breite ist wieder entfernt.

LISTEN. Fahrten- und Tankliste verloren 58px an tote Breite: 22px Einrueckung
der Monatsebene plus 40px linkes Polster der Zeile. Auf dem Telefon brach
"Eichstaett -> Adelschlag" deshalb um. Apples Richtlinien gruppieren ueber
Abschnittskoepfe statt Einrueckung; die Zeilen stehen jetzt alle auf derselben
Kante (34 statt 96px), der Monat traegt Versalien und Sperrung und liest sich
als Kopf. Beschraenkt auf .liste/.dm-liste - anderswo ist das Polster gewollt.

BILDER. Die Draufsicht ist aus der Galerie genommen (nurStatus), gehoert aber
weiter in die Auswahl der Einstellungen: nur dort laesst sie sich hochladen.
Die Galerie schneidet Fotos nicht mehr an - die Frontansicht verlor unten die
Stossstange.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 16:36:25 +02:00
tobias 38e7731a3d Rollende Zeitraeume, zwei Gesten, CI-Symbole und das Screening faellt nach (2026.9.2.8-.12)
Gemeldet vom Eigentuemer an der realen Instanz, in einem Zug abgearbeitet.

STATISTIK. "Monat kleiner als Woche" war kein Rechenfehler, sondern die
Definition: am 02.09. begann die Kalenderwoche am 31.08., der Monat erst am
01.09. Umgestellt auf rollende Fenster (1/7/30/365 Tage), damit 1 ⊂ 7 ⊂ 30 ⊂
365 an jedem Tag gilt. "Jahr" meint damit nicht mehr das Kalenderjahr - so
entschieden. Zwei neue Tests halten die Verschachtelung fest.

Aus dem Langzeitverbrauch sind Herleitung und Erklaerabsatz aus der ANSICHT
genommen, nicht aus der Rechnung; das Datum des letzten Tankstops ebenfalls.

UEBERSICHT. Unter "Letzter Tankvorgang" steht das Datum statt der Tankstelle.

BILDER. Die Draufsicht war der einzige sichtbare Bildplatz ohne Zugang -
bilder.py kannte den Dateinamen, BILDER/BILDPLAETZE nicht. Jetzt ein Platz wie
jeder andere. Die Vorschau in den Einstellungen schneidet Fotos nicht mehr an,
und der Bildbereich der Fahrzeugseite steht eingerueckt statt randlos.

ZAHNRAD. Es lag mit seiner 44px-Trefferflaeche 21px im Kachelinhalt und fing
die obere Haelfte des ersten Knopfes ab (Versicherung, "Inland"). Die Kopfzeile
bekommt jetzt die Hoehe, die es braucht.

SCREENING. Lief ausschliesslich bei einer Aenderung des Kilometerstands, also
nur waehrend der Fahrt - im Stand blieben Ortsnamen und Verbrauch liegen. Jetzt
zusaetzlich einmal nach dem Start, und danach faellt es selbst nach, solange
ein Lauf sein Ortsbudget aufbraucht UND dabei etwas aufloest. Kein Dauertakt:
die Kette endet, sobald ein Lauf mit uebrigem Budget durchkommt.

GESTEN. Zum Loeschen nach links wischen, jetzt auch im Radarchiv (das Panel
hatte die Mechanik, das Archiv war nie angeschlossen; die App hatte sie gar
nicht). Vom linken Rand nach rechts wischen = zurueck, jetzt auch in der App,
mit denselben Zahlen wie im Panel und in beiden mit sichtbarer Rueckmeldung.

CI-SYMBOLE. Das Kachel-Zahnrad ist settings-s - im Panel stand der Pfad
sechsmal wortgleich im Markup, jetzt einmal als CI.settingsS. Das
handgezeichnete Zahnrad der Seitenleiste ist ersetzt und geloescht. Die
Zapfsaeule des Tanken-Reiters ist fuel-station-s, Faktor 0,84 um die
Rastermitte - live per getBBox() gemessen, damit sie auf dieselbe senkrechte
Spanne kommt wie Statistik.

Zurueckgenommen: die Titel der Unterseiten stehen wieder mittig. Zentriert ist
Apples HIG fuer Navigationsleisten; die linksbuendigen Titel sind die der
Hauptbereiche und folgen der anderen Regel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 15:48:24 +02:00
tobias 12bc9e4220 Signierskript: Kopf auf das bezahlte Konto, OTA-Buendel auf 2026.9.2.7
Der Kopfkommentar von ios-signieren.sh beschrieb noch das kostenlose
Personal Team - keine Geraeteverwaltung, Ablauf nach 7 Tagen,
"-exportArchive ungeprueft" - und widersprach damit dem eigenen Rumpf
zwanzig Zeilen tiefer, wo die Widerlegung vom 2026-08-29 steht (Team
RMACS9VLS4, Laufzeit ein Jahr). Erprobt ist der Export laengst: die .ipa
vom 29. und 30.08. sind auf genau diesem Weg entstanden und wurden ueber
die Luft installiert.

Neu benannt ist damit auch die eigentliche Voraussetzung: nicht "iPhone
am Kabel", sondern "Geraet im Team eingetragen" - einmalig per Kabel oder
auf developer.apple.com.

Das OTA-Buendel stand auf 2026.9.2.5, die manifest.json auf 2026.9.2.7:
die beiden Symbol-Commits fehlten darin. Neu gebaut, 265.969 Bytes,
sha256 55a0da03.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 12:29:34 +02:00
tobias b73a8fb334 Meldezeit auch nach hinten prüfen - "Geparkt seit 20698 Tg." war die Unix-Epoche
geraetezeit() prüfte nur, ob die Meldezeit in der Zukunft liegt. Ein Sensor,
der 0 meldet, rutschte durch, und fromtimestamp(0) ergibt 1970 - auf der realen
Instanz sichtbar als "Geparkt seit 20698 Tg. 9 Std. 39 Min.".

Neu geraetezeit_plausibel(): nicht aus der Zukunft und nicht älter als
MELDEZEIT_RUECKLAUF (365 Tage). Geprüft wird an drei Stellen - beim Auslesen
der Gerätezeit, beim Setzen des Parkbeginns und beim LADEN aus dem
Laufzeit-Store, denn ein einmal falsch gespeicherter Wert überlebt sonst jedes
Update.

Fängt auch eine im Setup falsch zugeordnete ID-Entität ab: das flespi-Gerät hat
drei ID-Sensoren, deren Namen dem Zeitstempel ähneln, und deren Werte ergeben
als Unix-Zeit ebenfalls 1970.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 11:58:57 +02:00
tobias 960e07d7d7 Übersicht-Symbol: nochmals 10 % (Faktor 1,21) und auf die Rastermitte gelegt
Die eigene Mitte des Symbols liegt bei x=13, nicht 12. Ohne Verschiebung wäre
Faktor 1,21 rechts wieder angestoßen (24,10 statt 22,89). Jetzt Geometrie
1,11-22,89, mit Strichstärke 0,36-23,64 - im Raster.

Gemessen: 21,8 x 12,5 gegen ursprünglich 18,0 x 10,3, gerenderte Strichstärke
weiterhin 1,5 wie bei den Nachbarn.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 11:53:17 +02:00
tobias 0b497642f4 Übersicht-Symbol: Faktor 1,1 statt 1,33 - der erste Versuch stand über
Faktor 1,33 hatte das Symbol auf volle Rasterbreite gezogen und dabei von
x=3,99 bis x=27,9 geschoben - knapp vier Einheiten über den rechten Rand des
24er-Rasters, also rechts abgeschnitten. Beim Nachmessen waren Breite und Höhe
geprüft, aber nicht die Position.

Jetzt Faktor 1,1 um die Rastermitte (12,12): 19,8 x 11,3 bei x 3,2-23,0 und
y 7,6-18,9, also die vom Eigentümer gewünschten 10 % ohne Überstand. Alle fünf
Tab-Symbole nachgemessen, diesmal mit Rändern.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 11:50:23 +02:00
tobias 2af5c469d6 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>
2026-09-02 11:28:37 +02:00
tobias 78a8017e95 Verbrauch: Tankstand ab Fahrtbeginn statt davor (2026.9.1.24)
DER VERBRAUCH WAR SYSTEMATISCH ZU HOCH

Der Startwert kam aus wert_bei(), also dem letzten Literstand VOR der Fahrt -
und das ist im Stand der Wert vom Ende der VORIGEN Fahrt. An der Fahrt
Eichstaett - Adelschlag nachgemessen: 36,4 l um 13:52 (Ende der vorigen Fahrt),
36,2 l um 16:20:49 (erster Datensatz der neuen). In der Standzeit dazwischen
ist niemand gefahren; diese 0,2 l Setzung wurden der neuen Fahrt zugerechnet.

  alt: 36,4 - 35,6 = 0,8 l auf 6,896 km -> 11,6 l/100 km
  neu: 36,2 - 35,6 = 0,6 l auf 6,896 km ->  8,7 l/100 km

Neu wert_ab() in verlauf.py: der erste Wert AB dem Zeitpunkt. Fuer einen
ZAEHLER ist "zuletzt davor" richtig (der Kilometerstand aendert sich im Stand
nicht), fuer einen gemessenen FUELLSTAND nicht - der driftet. Das Fahrtende
bleibt bei wert_bei(). Livepfad und Rueckblick benutzen beide die neue
Funktion.

Bereits gerechnete Fahrten behalten ihren Wert: _verbrauch_screenen() fuellt
nur fehlende. Die Neuberechnung kommt mit dem Korrekturwert aus den
Tankvorgaengen, den der Eigentuemer vorgeschlagen hat.

ANZEIGE

Hoechstgeschwindigkeit ohne Nachkommastelle. Statuswort raus aus der
Fahrtenliste - "vollstaendig" sagt dem Nutzer nichts, und "offen" verspricht
Daten, die bei einer laengst abgeschlossenen Fahrt nie mehr kommen; fehlt ein
Wert, steht dort ein Strich.

Strecke und Verbrauch stehen jetzt in einer Spalte: die Wertspalte wuchs mit
dem Inhalt, gemessen 86px gegen 37px, und die Zahlen standen versetzt. Feste
Mindestbreite in beiden Oberflaechen, nachgemessen alle Zeilen 92px.

Verifiziert: py_compile, Panel als Modul, tsc --noEmit sauber, 165/165 Tests,
wert_ab() gegen vier Randfaelle, im Browser nachgesehen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 21:13:43 +02:00
tobias ce287ccac2 Fahrtenliste zeigt Orte aus dem Zwischenspeicher (2026.9.1.23)
Was die Einzelfahrt einmal aufgeloest hat, zeigt die Liste umsonst mit:
ortAusCache() liest nur den Zwischenspeicher und fragt nie das Netz. Ein Abruf
je Zeile waere genau der Vorratsabruf, um dessen Unterlassung Nominatim bittet.

In der Liste steht nur der ORT, nicht die Anschrift (Wunsch des Eigentuemers):
eine Zeile traegt Datum, Art, Strecke und Verbrauch, zwei volle Anschriften
passen dort nicht. Auf der Einzelfahrt bleibt die vollstaendige Anschrift.

Der Ortsname wird beim Aufloesen separat gemerkt ("ort:"-Schluessel), nicht aus
der Anschrift geschnitten - faellt Nominatim auf display_name zurueck, stuende
hinter dem letzten Komma das LAND.

Der Rueckfall war trotzdem noetig, und das Ausliefern hat es gezeigt: die Liste
stand weiter voller Anschriften, weil der Ortsname nur bei einem frischen Abruf
entsteht und die Adressen laengst im Speicher lagen. stadtAusAnschrift()
schneidet ihn deshalb aus einer gespeicherten Anschrift - aber nur, wenn hinter
dem letzten Komma eine Postleitzahl steht. Damit greift es bei unserem Format
"<Strasse>, <PLZ> <Ort>" und niemals bei display_name.

VERIFIZIERT: 7 Faelle gegen den Schnitt (mit/ohne Strasse, benannter Platz,
ohne PLZ, display_name-Form, null), Panel als Modul, tsc --noEmit und
vite build sauber, 165/165 Tests gruen. Im Browser: "Eichstaett -> Adelschlag"
in der Liste, volle Anschrift auf der Einzelfahrt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 20:36:24 +02:00
tobias 0f680780a4 Einzelfahrt: Orte aus den Koordinaten, "Min." statt "min" (2026.9.1.20)
START UND ZIEL STANDEN AUF "UNBEKANNT"

start_address/end_address stehen in HANDFELDER - das Backend fuellt sie nie von
selbst, sie kommen nur aus einer von Hand angelegten oder bearbeiteten Fahrt.
Die Koordinaten dagegen traegt das Screening ein: 12 von 14 Fahrten im
Testbestand hatten Start- und Zielposition, aber genau eine hatte eine Adresse.

Beide Oberflaechen loesen sie jetzt beim Oeffnen der Einzelfahrt auf, ueber
denselben Cache wie die Standortansicht - ein Abruf je Ort, danach nie wieder.
Bewusst NICHT in der Liste: ein Vorratsabruf fuer alle Fahrten waere genau das,
worum Nominatim in seinen Nutzungsbedingungen bittet, es nicht zu tun.

Bis zur Antwort steht "wird ermittelt ...", ohne Koordinaten oder ohne Treffer
"unbekannt". Das Panel prueft vor dem Eintragen, ob noch dieselbe Fahrt offen
ist - wer waehrend des Abrufs weiterblaettert, soll nicht die Adresse der
vorigen Fahrt in der neuen sehen.

Live: Start "Schottenau, 85072 Eichstaett", Ziel "Am Anger 7a, 85111
Adelschlag".

"MIN." STATT "MIN"

Deutsche Abkuerzung, wie die Oberflaeche sie an anderer Stelle laengst
verwendet ("Geparkt seit 2 Tg. 16 Std. 12 Min."). In dauerText() bzw. dauer(),
also ueberall wo eine Dauer erscheint. Zwei Testerwartungen mitgezogen.

Verifiziert: Panel als Modul, tsc --noEmit und vite build sauber, 165/165
Tests gruen, im Browser nachgesehen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 19:38:09 +02:00
tobias 97a1482bb5 Drei Erlaeuterungen aus der Fahrt-Detailansicht entfernt (2026.9.1.19)
Auf Wunsch des Eigentuemers entfallen die Kleingedruckten unter drei Zeilen:

  "Strecke / Dauer, Standzeiten eingerechnet"   bei der Durchschnittsgeschwindigkeit
  "groesster gemeldeter Wert, alle 10 s ein Datensatz"  bei der Hoechstgeschwindigkeit
  "Naeherung aus Tankfuellstand"                bei Verbrauch

In beiden Oberflaechen (Paritaetsregel). Die Ortsangaben unter Start und Ziel
bleiben, das sind Daten und keine Erlaeuterung.

Die Herleitungen selbst stehen weiterhin im Quelltext: durchschnitt_kmh() in
verlauf.py rechnet unveraendert Strecke durch Dauer, hoechstwert_im_fenster()
erklaert in seinem Docstring, warum der gemeldete Wert nicht die tatsaechliche
Spitze ist, und verbrauch_aus_literstaenden() begruendet die Naeherung samt
MIN_STRECKE_VERBRAUCH_KM. Es verschwindet nur die Anzeige, nicht das Wissen.

Verifiziert: Panel als Modul geparst, tsc --noEmit sauber, 165/165 Tests gruen,
audi_ha_test auf 2026.9.1.19 ohne Fehler gestartet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 19:31:14 +02:00
tobias 9a4999ccbc Strecken gestaffelt anzeigen (2026.9.1.18)
Die gewonnene Genauigkeit kam nicht an: de() formatiert ohne Nachkommastelle,
aus 6,896 km wurde in beiden Oberflaechen "7 km". Den ganzen Tag auf zehn Meter
genau gemessen und im letzten Schritt weggerundet.

Staffelung, vom Eigentuemer festgelegt: unter 1 km Meter ("400 m"), bis
99,9 km eine Nachkommastelle ("6,9 km"), ab 100 km ganze Kilometer ("104 km").
streckeTeile()/streckeText() in beiden Codebasen, wortgleich.

Gestaffelt wird nach dem GERUNDETEN Wert: 0,9996 km sind gerundet 1000 m und
gehoeren in die km-Stufe, sonst stuende dort "1.000 m". streckeTeile() gibt
Wert und Einheit getrennt zurueck, weil die Detailansicht beide in getrennten
Elementen setzt - ohne das stuende unter "400" weiterhin "km".

Im Panel heisst die Funktion streckeText, nicht strecke: den Namen gibt es dort
schon fuer die Ortsangaben einer Fahrt. node --check hat die Kollision gefunden.

Angewandt auf Einzelfahrt und Jahres-/Monatssummen. Nicht auf Kilometerstaende,
Serviceintervalle oder Reichweite - das sind Zaehlerstaende und Prognosen.

VERIFIZIERT: 12 Faelle gegen die Staffelung samt beider Grenzen, 165/165 Tests
gruen, Panel als Modul, tsc --noEmit und vite build sauber, im Browser
nachgesehen: die Fahrt vom 01.09. steht jetzt mit 6,9 km statt "7 km".

Die angepasste Testerwartung ist selbst ein Beleg: sie verlangte "43 km" fuer
eine Summe von 42,5 km - die alte Anzeige rundete schon dort, wo es etwas zu
zeigen gab.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 19:29:19 +02:00
tobias 9d70192da3 Audit: Livepfad und Rueckblick messen wieder dasselbe (2026.9.1.17)
BEFUND 1 (behoben): dieselbe Fahrt, zwei Strecken

screening.py las den Verlauf ueber verlauf_lesen(), das den Stand zu Beginn
des Fensters mitliefert. historienimport.py schnitt sein Fenster streng heraus
und verlor genau diesen Punkt. An der echten Fahrt nachgemessen: 6,896 km
gegen 6,816 km fuer denselben Zeitraum.

Neu verlauf.zaehlerstrecke(punkte, start, ende) - der Anker ist der letzte
Datensatz am oder vor dem Beginn, dieselbe Ueberlegung wie bei wert_bei().
Beide Wege schneiden jetzt durch dieselbe Funktion. Nachgewiesen: Rueckblick
6,896, Livepfad 6,896.

Dritter Fall dieser Art nach UNPLAUSIBLE_KMH und MINDESTDAUER_S - und ich
hatte ihn am selben Tag selbst eingebaut.

BEFUND 2 (behoben): STANDARDWERTE gingen unnoetig ueber die Leitung.
zuordnung.py schickte sie mit jeder Katalogantwort; gelesen hat sie seit dem
Entfernen des Zuruecksetzen-Knopfes niemand mehr. Die Konstante bleibt, sie
traegt intern die Vorgabewerte und leitet SCHLUESSEL ab.

BEFUND 3 und 4 bleiben offen und gehoeren dem Eigentuemer: die vier Tage alte
Reichweite auf der Uebersicht (RANGE_SENSOR besser leer lassen) und die
Nullfahrt-Regel, die nur der Rueckblick kennt - im Livepfad hiesse sie, einen
bereits gespeicherten Datensatz automatisch zu loeschen.

SAUBER: 24 Backend-Dateien py_compile, Panel als Modul geparst, tsc --noEmit
und vite build sauber, 165/165 Tests, 27 Katalogeintraege gegen 27
Dataclass-Felder ohne Abweichung, jedes Listenfeld mit so vielen Beispielen wie
Positionen, keine verwaisten Verweise, in der Konsole nur das bekannte
ServiceWorker-Rauschen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 19:09:59 +02:00
tobias 8e73621d85 Versionssprung, damit das rechtsbuendige Beispiel ankommt (2026.9.1.16)
Das CSS aus 2026.9.1.15 war korrekt, kam im Browser aber nicht an: Panel und
Stylesheets werden mit ?v=<Manifest-Version> geladen, und ich hatte nach dem
Versionssprung noch einmal am CSS gefeilt. Die Adresse blieb gleich, der
Browser servierte die alte Datei aus dem Zwischenspeicher.

Nachgewiesen mit getComputedStyle: display block statt flex, font-size 14px
statt 12.5px. Nach erzwungenem Neuladen derselben Datei stimmte beides, und
der Hinweis stand rechtsbuendig auf derselben Kante wie bei den Einzelfeldern.

Regel daraus, in AGENTS.md festgehalten: die Manifest-Version wird als LETZTES
erhoeht, nach der letzten Aenderung an Panel oder Stylesheet.

Verifiziert im Browser: alle acht Tuer- und Fensterfelder gruen, Erwartungswert
je Position rechtsbuendig.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 19:03:57 +02:00
tobias 7e2854fe8d Setup: Erwartungswert je Listenposition, gruen auch dort (2026.9.1.15)
Tuer- und Fenstersensoren haben vier Positionen, aber nur einen Beispielnamen
(front_left_door). Der beschreibt die erste; die anderen drei koennen ihn nie
enthalten. Folge: nur die erste Position konnte gruen werden, und im Kopf
stand ein Erwartungswert, der fuer drei von vier Zeilen nicht galt.

Neu "beispiele" je Position im Feld-Katalog und feldBeispiel(feld, idx) im
Panel. Der Erwartungswert steht jetzt an jeder Position statt im Kopf -
rechtsbuendig, in der kleinen grauen Schrift des Positionslabels. Auch die
Sortierung der Vorschlagsliste nutzt ihn, sonst stuende in allen vier Zeilen
dieselbe Entitaet oben.

Typografie: .setup-feld-hinweis ist fuer die Kopfzeile gebaut (14px, --fg,
rechtsbuendig, max-width 55%). Im 12,5px-Positionslabel wirkt das falsch herum.
Eine Regel im Kontext .setup-unterfeld-label .setup-feld-hinweis laesst Groesse
und Farbe erben; die Rechtsbuendigkeit kommt aus justify-content: space-between
am Label, wie eine Ebene darueber bei .setup-feld-kopf.

Verifiziert: alle acht Positionen tragen ihren eigenen Beispielnamen,
py_compile und Panel als Modul sauber, audi_ha_test ohne Fehler gestartet.
Die Darstellung selbst habe ich nicht im Browser gesehen - das Panel liess
sich in der Testinstanz nicht ansteuern.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 19:00:16 +02:00
tobias 09c7461892 Rueckblick misst auf Meter, Streckenwahl gemeinsam (2026.9.1.14)
Der erste Wurf (2026.9.1.12) verwarf jedes Importfenster mit distanz == 0. Zu
grob: seit der GNSS-Verfeinerung messen wir Meter, und eine Fahrt von 400
Metern steht im CAN-Wert als Null. Der Eigentuemer hat es auf den Punkt
gebracht - "0,0 moechte ich nicht, ab 0,1 schon".

Der Rueckblick liest jetzt ebenfalls den GNSS-Zaehler und entscheidet auf eine
Nachkommastelle.

Dabei stand die Toleranzlogik kurz doppelt im Code - in screening.py und in
historienimport.py. Genau die Doppelung, die dieses Projekt bei
UNPLAUSIBLE_KMH und MINDESTDAUER_S schon einmal teuer bezahlt hat. Sie liegt
jetzt gemeinsam in verlauf.py:

  strecke_waehlen(grob, fein) - die feine Zahl gilt, solange sie in der
  Rundungsunschaerfe des Ankers liegt, oder wenn es keinen Anker gibt.

  ist_gefahren(distanz) - ab 0,1 km ja. Eine unbekannte Strecke gilt als
  gefahren; nur die gemessene Null ist ein Nein.

VERIFIZIERT: 13 Faelle gegen beide Funktionen, darunter die zwei echten
Fahrten, die 400-Meter-Fahrt, die 100-Meter-Untergrenze, 40 Meter, fehlender
Anker, fehlender GNSS-Wert, gedrifteter Zaehler und beide Seiten der Grenze.
Import live: 1 Fenster uebersprungen, null 0-km-Fahrten im Bestand. Livepfad
live: vier alte Fahrten korrekt abgelehnt.

NICHT live gezeigt: ein Fenster mit 0,1-0,9 km, das der Import nun behaelt -
der Recorder enthaelt kein geschlossenes Fenster dieser Groesse.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 18:47:51 +02:00
tobias be95b40d79 Rueckblick legt keine Fahrten ohne gefahrene Strecke mehr an (2026.9.1.12)
Der Eigentuemer hatte auf der realen Instanz alle Nullfahrten geloescht - und
bei JEDEM Import kam eine davon wieder: 31.08.2026, 14:48-15:21, 33 Minuten,
209177 -> 209177 km, vmax 3 km/h. MINDESTDAUER_S liess sie durch, weil sie mit
33 Minuten lang genug war; eine Pruefung auf die Strecke gab es im Rueckblick
nicht.

Steht der Kilometerstand an beiden Enden gleich, hat sich das Fahrzeug nicht
bewegt - Zuendung an, aber nicht gefahren. Solche Fenster werden jetzt
uebersprungen und als eigene Zahl gemeldet, damit der Import nicht
stillschweigend etwas weglaesst.

Nur die GEMESSENE Null wird verworfen, nicht die unbekannte: liegt kein
Kilometerstand vor, bleibt distanz None und die Fahrt wird angelegt. Der Preis,
bewusst getragen: eine echte Fahrt unter einem Kilometer, bei der der Zaehler
nicht umsprang, geht verloren - sie waere aber genau der Nulleintrag, den der
Eigentuemer nicht im Bestand haben will.

Beide Oberflaechen melden die neue Zahl (Paritaetsregel).

VERIFIZIERT im Testcontainer ueber den echten Dienst: 1 Fahrt angelegt,
2 Fenster als "nicht gefahren" uebersprungen, null Fahrten mit 0 km im Bestand.
py_compile, Panel als Modul, tsc --noEmit sauber, 165/165 gruen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 18:36:33 +02:00
tobias 02187ee129 Setup: gruene Bestaetigung statt Zuruecksetzen-Knopf (2026.9.1.11)
Passt die zugeordnete Entitaet zum Beispielnamen der Rolle (FELDER-Schluessel
"beispiel"), faerbt sich das Feld leicht gruen. Dieselbe Pruefung, die
entitaetScore() ohnehin mit 8 gewichtet - hier nur fuers Auge, damit man eine
richtig belegte Rolle sieht, ohne den langen Entity-Namen Zeichen fuer Zeichen
mit dem Beispiel daneben zu vergleichen.

Bewusst nur ein Hinweis, keine Bedingung: Installationen mit anderer
Datenquelle haben andere Namen und sind trotzdem richtig zugeordnet. Deshalb
gruen als Bestaetigung, aber kein Rot und keine Warnung, wenn es fehlt. Und
nicht waehrend gesucht wird - dann steht die Sucheingabe im Feld, nicht die
Zuordnung.

Die Farbe kommt per color-mix aus dem vorhandenen --ok-Ton und dem Feldgrund
statt aus einem eigenen Gruen, damit sie in beiden Themen ruhig bleibt.

"Auf Standard zurueckstellen" entfernt (Wunsch des Eigentuemers): Knopf,
Symbol, Zuhoerer, CSS und der nur dafuer vorhandene Helfer setupStandardwert().
Keine verwaisten Verweise mehr im Quelltext.

Panel als Modul geparst, Bundle und Manifest auf 2026.9.1.11.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 18:21:52 +02:00
tobias af83805c34 Fahrtstrecke auf 100 m: CAN als Anker, GNSS als Nachkommastelle (2026.9.1.10)
Zwei echte Fahrten am 01.09.2026 haben die Datenform geliefert, auf die
2026.9.1.5 gewartet hat.

strecke_aus_zaehler() summiert die Zuwaechse des geraeteeigenen
Kilometerzaehlers und behandelt einen Ruecksprung als Ruecksetzung. Weder der
rohe Zaehler noch die Teilstrecken taugen allein: der Zaehler verliert bei
einer Ruecksetzung alles Vorherige (Fahrt 1: 0,415 km), die Teilstrecken
verlieren einzelne Datensaetze (Fahrt 2: 4 von 50 null, 0,2 km zu wenig). An
jedem Datensatz sind beide identisch - sie gehen nur verschieden kaputt.

_gnss_verfeinern() ersetzt die grobe Strecke nur, wenn die feine innerhalb der
Rundungsunschaerfe des Ankers liegt (GNSS_TOLERANZ_KM = 1.0; beide Enden des
CAN-Werts sind auf ganze Kilometer gerundet). Sonst behaelt der Fahrzeugwert
recht.

VERIFIZIERT gegen die echten Fahrten:
- Fahrt 1 (mit Ruecksetzung): 6,958 km, Google Maps sagt 7,1
- Fahrt 2 (sauber): 6,816 km
- live: 'Strecke auf 6.896 km verfeinert (Kilometerstand sagte 7.0 km)'
- drei aeltere Fahrten korrekt abgelehnt (209178 km gegen 21 km Anker)
- sechs Randfaelle: zwei Ruecksetzungen, ein Punkt, leer, unlesbare Werte

Nebenbei bestaetigt: NACHLAUF_S = 900 (Zuendung aus 16:33:39, Trip-Signal aus
16:48:44, gespeichertes Ende 16:33:36); der Rueckwaertssprung-Schutz griff live
beim Fahrzeugwechsel (-147361 km verworfen, Fahrt blieb offen); die
RPM-Zuendung prellt nicht; das Bewegungstor meldet wieder Stillstand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 17:31:33 +02:00
tobias 0124254ea9 Nachlauf-Abzug nur, wenn das Trip-Element ihn erzeugt (2026.9.1.9)
_echtes_ende() zog NACHLAUF_S bedingungslos ab. Richtig, solange die Fahrten
vom Trip-Signal ausgeloest werden - aber verlauf.fahrtsignal() faellt auf die
Zuendung zurueck, wenn TRIP_SENSOR nicht zugeordnet ist, und die hat keinen
Nachlauf. Jede Fahrt haette fuenfzehn Minuten verloren, jede kuerzere waere
ganz verschwunden.

Genau die Konstellation, in die eine bestehende Installation nach dem Update
ohne Zutun laeuft: die neue Rolle ist leer, bis jemand sie zuordnet. Gefunden
beim Durchdenken des Update-Wegs fuer die reale Instanz, nicht durch einen
Fehlerbericht.

Nachgewiesen: TRIP_SENSOR geleert, Zuendung fuenf Minuten an -> Fahrt mit
300 s angelegt statt verworfen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 12:24:50 +02:00