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>
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>
Erste .ipa, die die Share-Erweiterung wirklich enthaelt. An der fertigen
Datei geprueft statt am Projekt:
- DataMetric360Share.appex traegt ein echtes Mach-O arm64 Programm
- App und Erweiterung tragen beide group.app.datametric360
- URL-Schema datametric360 eingetragen
- Ad-hoc-Profil mit beiden Geraeten, gueltig bis 29.08.2027
- Signatur gueltig, Erweiterung eingeschlossen
Vorher: design-system neu gebaut, Typecheck sauber, 165/165 Tests gruen.
Erster Kompilierlauf der Datei ueberhaupt - vorher hing sie in keiner
Sources-Phase und wurde nie uebersetzt. Die deutschen Anfuehrungszeichen in
der Hinweismeldung waren gemischt: oeffnend typografisch (U+201E), schliessend
aber gerade (U+0022). In einem Swift-String beendet das gerade Zeichen die
Zeichenkette, der Rest der Zeile war Unsinn.
Jetzt beidseitig typografisch. In Zeile 97 stehen nur noch die zwei geraden
Anfuehrungszeichen des Strings selbst.
An der fertigen .ipa gemessen: DataMetric360Share.appex enthielt nur
Info.plist, Signatur und Profil - keine ausfuehrbare Datei, obwohl
CFBundleExecutable eine versprach. Xcode meldet dabei nichts, der Bau gilt
als erfolgreich. Das Teilen-Blatt haette schlicht nicht funktioniert.
Ursache: addSourceFile() legt die Datei nur in die uebergebene Gruppe, wenn
man ihm eine mitgibt - an eine Sources-Phase haengt es sie dann nicht. Der
PBXBuildFile-Eintrag existierte, war aber verwaist, und alle Sources-Phasen
des Ziels blieben leer.
Die Quelle wird jetzt direkt beim Anlegen der Sources-Phase uebergeben.
Belegt am neu erzeugten Projekt: zwei Sources-Phasen, die der App mit
AppDelegate/SceneDelegate, die der Erweiterung mit ShareViewController.
An der fertigen .ipa gemessen: die Erweiterung trug
com.apple.security.application-groups, die App nicht. Der Beleg waere also
abgelegt, aber nie gefunden worden - Teilen meldet Erfolg, die Tanken-Seite
bleibt leer. Genau die Sorte stiller Ausfall, gegen die dieses Projekt sonst
ueberall anbaut.
Ursache: die pbxproj fuehrt Werte mal mit, mal ohne Anfuehrungszeichen. Was
addTarget schreibt, ist gequotet ("app.datametric360.teilen"); was Capacitor
fuer die App erzeugt, steht nackt da (app.datametric360). Der Vergleich auf
die gequotete Form lief damit fuer die App immer ins Leere, und
CODE_SIGN_ENTITLEMENTS wurde dort nie gesetzt.
Jetzt wird entquotet verglichen. Belegt am neu erzeugten Projekt: vier
CODE_SIGN_ENTITLEMENTS statt zwei - Debug und Release beider Ziele.
Erster Lauf des Einrichtungsskripts auf einem Mac. Xcode brach ab mit
"error: Unexpected duplicate tasks", zweimal ValidateEmbeddedBinary auf
dieselbe DataMetric360Share.appex.
Ursache: addTarget() legt fuer den Typ app_extension selbst schon eine
Copy-Phase nach PlugIns am ersten Ziel an. Das Skript fuegte danach eine
zweite mit demselben Ziel und derselben Datei hinzu. Die explizite Phase
entfaellt jetzt; belegt am erzeugten Projekt: genau eine Phase mit
dstSubfolderSpec 13 statt zwei.
Alles Uebrige des Skripts lief auf Anhieb: Ziel, Entitlements, URL-Schema,
Idempotenz.
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
Nach den beiden Fahrten stand der Verdacht im Raum, der GNSS-Zaehler messe
systematisch 1,5-2 Prozent zu kurz - gemessen gegen Streckenschaetzungen aus
Google Maps. Das war falsch.
Die bessere Messung kam vom Eigentuemer: zwischen zwei Spruengen des
CAN-Kilometerstands ist das Fahrzeug exakt 1,000 km gefahren. Was der
GNSS-Zaehler in derselben Spanne zaehlt, ergibt den Massstab ohne Schaetzung.
Fahrt 2, erster bis letzter Sprung: Raeder 5,000 km, GNSS 5,015 km,
Verhaeltnis 0,9970 - fuenfzehn Meter auf fuenf Kilometer.
Fahrt 1 liefert 1,0879, ist aber unbrauchbar: ihr erster Kilometersprung ist
der Datensatz mit der Ruecksetzung. Einzelne Runden streuen ohnehin von 0,849
bis 1,194 km, weil ein Sprung irgendwo in den zehn Sekunden zwischen zwei
Datensaetzen liegen kann.
Ein Korrekturfaktor ist damit vom Tisch, bevor er gebaut wurde. Die
vermeintlich fehlenden 300 m der Fahrt 2 sind keine Messabweichung: der Tacho
lief von 61817 auf 61823, die echte Strecke liegt zwischen 6 und 7 km, und
6,896 sitzt mitten darin.
Nur Dokumentation, kein Code.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
_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>
DIE APP MELDETE EINEN FEHLER, WO KEINER WAR
Nach "Jetzt neu starten" sprang der Knopf zurueck und darunter stand 502,
waehrend Home Assistant ordnungsgemaess hochfuhr. Der Nutzer musste annehmen,
der Neustart sei gescheitert, und drueckte erneut.
updateNeustartAusloesen() behandelte jeden Fehlschlag als Fehler. Ueber einen
Vorschaltserver kommt beim Neustart aber kein Abbruch zurueck, sondern eine
saubere 502/503/504 - der Proxy antwortet, Home Assistant noch nicht. Genau
der Fall, fuer den ApiFehler.istVoruebergehend seit dem 31.08. existiert; er
wurde hier nur nicht gefragt. Der Bildschirm bleibt jetzt auf "Home Assistant
startet neu ...", der bestehende Effekt raeumt ihn ab, sobald die Verbindung
wieder steht.
Im Panel war es kein Fehler (WebSocket bricht ab statt 502 zu liefern), aber
die Luecke war dieselbe, sobald ein Vorschaltserver dazwischen steht -
dieselbe Toleranz deshalb auch dort.
DAS OTA-BUENDEL STAND NOCH AUF 2026.8.31.5
Gemeldet: Kopfzeile "App ist aelter als der Server", Kachel "App ist aktuell
31.5", Integration auf 1.7. Kein Fehler - die Anzeige sagte die Wahrheit.
Sieben Versionen lang wurde nur die Integration ausgeliefert; npm run ota lief
seit dem 31.08. nicht mehr.
Merkposten: eine Aenderung an companion-app/src ist erst dann beim Nutzer,
wenn das Buendel neu gebaut wurde. Der Versionssprung allein beschreibt sonst
eine App, die es nicht gibt.
VERIFIZIERT: Manifest, bundle.json und die ausgelieferte Zip tragen alle
2026.9.1.8 und denselben SHA-256 c2f9192e...; tsc --noEmit sauber, 165/165
Tests gruen, Panel als Modul geparst, audi_ha_test fehlerfrei gestartet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Zuhoerer bekommt den Zuendungswechsel nur mit, wenn Home Assistant laeuft.
Ging die Zuendung aus, waehrend es unten war - ueber Nacht der Normalfall -,
feuerte nichts, und die Registrierung beim Hochfahren wird bewusst ignoriert
(sonst begaenne die Parkdauer bei jedem Neustart neu). Ergebnis war
"Parkdauer unbekannt", obwohl der Zeitpunkt im recorder steht.
_parkbeginn_nachholen() laeuft einmal in _nach_start und schlaegt ihn nach -
nur, wenn kein Wert gespeichert ist und die Zuendung auf "aus" steht.
Genommen wird der letzte echte Wechsel an -> aus im Fenster von sieben Tagen;
gibt es keinen, bleibt es bei "unbekannt" statt den Rand des Fensters zu
erfinden.
Wieder mit der Meldezeit desselben Datensatzes statt seiner Ankunftszeit -
dieselbe Begruendung wie in verlauf.geraetezeit(), hier rueckblickend.
VERIFIZIERT in audi_ha_test, der Test trennt genau die beiden Zeiten:
Speicher geleert, neu gestartet -> "Parkbeginn aus der Aufzeichnung
nachgeholt: 09:04:57". Das ist die Geraetezeit; angekommen war der Datensatz
um 09:44. Ein zweiter Neustart ruehrt den Wert nicht an, schreibt keine Zeile
und erzeugt keinen Fehler.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
GNSS-STRECKENROLLEN, NOCH OHNE VERBRAUCHER
11806 steht seit dem 01.09. auf GNSS. Zwei neue Sensorrollen angelegt, damit
nach der ersten echten Fahrt nur noch gemessen und nicht mehr zugeordnet
werden muss: GNSS_KM_SENSOR (total_calculated_mileage, der robustere fuer eine
Fahrtstrecke - ein verlorener Datensatz verfaelscht die Differenz nicht) und
GNSS_TEILSTRECKE_SENSOR (segment_mileage, zeigt WO Strecke fehlt, taugt als
Gegenprobe). Kein Code liest sie; die Zuordnung aendert nichts.
KM_SENSOR bleibt der Anker - er ist der Tacho des Fahrzeugs und driftet nicht.
Der alte Warnhinweis dort zeigt jetzt auf die neue Rolle statt ins Leere.
"GEPARKT SEIT" KOMMT VON DER ZUENDUNG
Die Anzeige rechnete "jetzt minus ts_end der juengsten Fahrt". Das ist keine
Aussage ueber das Parken, sondern ueber die Fahrterkennung: am 01.09. stand
dort "Geparkt seit 2 Tg. 16 Std.", waehrend am Vorabend 7 km gefahren wurden -
die Fahrt fehlte im Bestand, weil trip_status festhing.
Der Koordinator merkt sich jetzt den Zeitpunkt, zu dem die Zuendung zuletzt
ausging, in der Zeit des Geraets. Er liegt im Laufzeit-Store neben
fahrt_start_ts und ueberlebt einen Neustart. Kein Rueckfall auf die
Fahrtenliste: null heisst unbekannt und wird auch so angezeigt - ausdrueckliche
Ansage des Eigentuemers, eine falsche Zahl ist schlechter als keine.
Beim Ausliefern fiel derselbe Neustart-Fehler auf wie bei der Phantomfahrt:
der Zuhoerer las die Registrierung der Entitaet als Wechsel und stempelte den
Parkbeginn bei jedem Neustart neu. if alt is None: return behebt es. Merkposten
fuer dieses Projekt: jeder neue Zustandszuhoerer braucht diese Pruefung.
VERIFIZIERT in audi_ha_test: Neustart laesst 09:34:15 unveraendert; Zuendung an
-> null; Zuendung aus mit Geraetezeit von vor 40 Minuten -> 09:04:57 statt
jetzt. Companion-App gleichgezogen, tsc --noEmit sauber, 165/165 Tests gruen.
NOTIERT, NICHT GEBAUT: der GNSS-Zaehler driftet im Stand. Die 0,034 km waren
GPS-Rauschen, nicht Aufloesung - der Dongle lag unbewegt. Die GNSS-Zuwaechse
brauchen deshalb ein Bewegungstor, bevor sie summiert werden duerfen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Das Trip-Signal endet nicht mit der Zuendung, sondern "Ignition OFF Timeout"
spaeter - beim FMM003 900 s. Das Fahrtende lag damit systematisch 15 Minuten
zu spaet. _echtes_ende() zieht NACHLAUF_S = 900 ab.
Die Rechnung stammt vollstaendig aus der Logik des Geraets und ueberlebt
deshalb ein Funkloch: der Datensatz kommt spaeter, traegt aber seine eigene
Zeit (2026.9.1.2/.3).
Die Klemme ist der wichtigere Teil: war das Signal nicht laenger als der
Nachlauf, wurde nicht gefahren - Ende auf den Beginn geklemmt, MINDESTDAUER_S
verwirft den Vorgang. Ohne sie entstuende aus dreissig Sekunden Zuendung eine
Viertelstunde Fahrt.
_nachlauf_gegenpruefen() vergleicht das gerechnete Ende mit dem beobachteten
Zuendungswechsel und warnt ab 120 s Abweichung, ohne zu korrigieren. Wie genau
"Zuendung aus" bekannt ist, haengt am Operanden des Zuendungs-I/O-Elements; er
steht auf 3 (nach der Reihenfolge in der Teltonika-Doku "Monitoring", also
kein eigener Datensatz je Wechsel). Solange das nicht feststeht, waere ein
Umschalten auf den beobachteten Wert eine Verschlechterung.
Keine Einstellung in der Oberflaeche: die Zahl gehoert zum Geraet, nicht zum
Nutzer - dieselbe Linie wie UNPLAUSIBLE_KMH. flespi koennte sie liefern
(GET /gw/devices/{id}/settings/all), aber die Integration liest heute nur
Telemetrie; ein zweiter Zugang samt Token waere unverhaeltnismaessig.
Verifiziert in audi_ha_test: Signal 1200 s -> Fahrt 300 s; Signal 930 s ->
keine Fahrt (Bestand 12 -> 12); Gegenprobe mit 887 s Abweichung ausgeloest.
Testfahrt ueber fahrt_loeschen entfernt, Bestand unveraendert 11 Fahrten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Enthaelt 2026.9.1.2, das nie einzeln ausgeliefert wurde.
FAHRT- UND TANKZEITEN KOMMEN VOM GERAET
Fahrtbeginn war datetime.now() - die Uhrzeit, zu der Home Assistant den
Wechsel verarbeitet. Hat das Geraet gepuffert (Funkloch, Tiefgarage,
Tiefschlaf), liegt die Wahrheit beliebig weit davor. Gemessen: ein Datensatz
mit Fahrtende trug die Geraetezeit 07:29:04 und kam um 07:35:23 an.
Neue Sensorrolle MELDEZEIT_SENSOR (message_timestamp, Unix-Sekunden). Der
Wert ist UTC, keine Ortszeit - nachgemessen lag der Versatz zur UTC-Uhr von
HA bei Sekunden, nicht bei zwei Stunden; eine Zeitzonenumrechnung baute einen
Zwei-Stunden-Fehler ein.
geraetezeit() liegt in verlauf.py, weil Fahrt- und Tankerkennung denselben
Zeitstempel bestimmen muessen. tankerkennung._automatisch_anlegen() stempelte
bisher ebenfalls mit now(); ein aus einem gepufferten Datensatz erkannter
Tankvorgang trug damit die Uhrzeit des Auftauchens.
Die flespi-Integration setzt die Entitaeten eines Datensatzes nacheinander -
gemessen Zuendung 07:35:23.633, zugehoerige Meldezeit 2 ms spaeter. Wer
sofort liest, bekommt den vorigen Datensatz: beim Fahren zehn Sekunden
daneben, im Stand Stunden (On Stop Min Period = 43200 s). geraetezeit()
wartet deshalb bis zu 2 s auf den passenden Wert. Zwei Notbremsen, beide live
ausgeloest: Zeitstempel in der Zukunft und Ende vor dem Beginn.
TRIP UND ZUENDUNG SIND ZWEI ROLLEN
ZUENDUNG_SENSOR zeigte auf das Trip-Signal; im Setup stand "Zuendung/ACC" ueber
einer Trip-Entitaet und die echte Zuendung war nirgends zugeordnet. Neu:
TRIP_SENSOR loest Fahrten aus, ZUENDUNG_SENSOR liefert nur noch die Anzeige
"faehrt/steht". verlauf.fahrtsignal() ist die eine Stelle, die entscheidet -
Trip-Status wenn zugeordnet, sonst Zuendung, damit bestehende Installationen
unveraendert weiterlaufen und Livepfad und Rueckblick nicht auseinanderlaufen.
VERIFIZIERT in audi_ha_test, jeder Weg einzeln:
- Fahrtbeginn 08:30:03 (Geraetezeit) statt 08:38:27 (Ankunft)
- Fahrtende 08:04:40 -> 08:09:22 statt 08:11:04 -> 08:11:24
- Tankvorgang 08:18:45 statt 08:38:51 - 20 Minuten frueher
- Meldezeit in der Zukunft und Ende vor Beginn: Warnung, Ankunft genommen
- Zuendung schalten legt keine Fahrt mehr an, Trip-Signal schon
Der Livetest fing dabei einen NameError, den py_compile nicht sehen konnte:
der Import von geraetezeit in tankerkennung.py fehlte. Kompiliert ist nicht
verifiziert. Alle Testdatensaetze wurden ueber die eigenen Dienste wieder
entfernt; Bestand unveraendert 11 Fahrten, 13 Tankvorgaenge, keine offene Fahrt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
zuendung_geaendert() prueste den neuen Zustand, aber nicht den alten. Beim
Hochfahren taucht die Zuendungs-Entitaet neu auf; das kommt als Wechsel mit
old_state = None an. Steht das Trip-Signal ohnehin auf "an" - beim FMM003 der
Normalfall -, las der Beobachter das als "Zuendung ging gerade an" und legte
eine Fahrt an, deren Beginn schlicht der Zeitpunkt des Neustarts war. Daher
der Eintrag 30.08. 19:43 bis 31.08. 08:37, 12,9 h, 0 km.
Die Pruefung ist "if alt is None: return" - dieselbe, die
_kilometerstand_geaendert() im Koordinator seit jeher hat. Eine wirklich
laufende Fahrt geht nicht verloren: deren Beginn liegt im Store und wird von
nach_neustart_fortsetzen() aufgenommen.
Nachgewiesen: zwei Neustarts hintereinander bei trip_status = on und leerem
Zwischenstand, null "Fahrt gestartet"-Zeilen und fahrt_start_ts bleibt null.
Vorher entstand unter genau diesen Bedingungen jedes Mal eine.
_verbrauch_screenen() liest die Literstaende jetzt ebenfalls mit wert_bei()
statt naechster_wert() - dieselbe Doppelbewertung gegenueber
historienimport.py wie beim Kilometerstand, eine Ebene tiefer.
naechster_wert() bleibt bei Breiten-/Laengengrad: eine Position ist kein
Zaehler.
Auf Anweisung des Eigentuemers ausserdem die zwoelf Fahrten mit
distance_km == 0 geloescht (ueber den Dienst fahrt_loeschen, nach einem
backup_jetzt) und "Open items" in AGENTS.md aufgeraeumt: vier Karteileichen
des MQTT-Wegs entfernt, Cloudflare-Zugang und iPhone-Signatur als erledigt
gebucht, Befund 07 als Nutzereingabe geschlossen, QR-Scanner auf den halben
Umfang gekuerzt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
_fahrt_screenen() holte beide Kilometerstaende mit naechster_wert(), also dem
zeitlich naechstgelegenen Wert - gleich ob davor oder danach. Fuer einen
Zaehlerstand ist das falsch, und wert_bei()s eigener Docstring sagt es
woertlich: der Stand bei Fahrtbeginn ist der zuletzt gemeldete, nicht der
naechste, der schon Strecke enthaelt. historienimport.py hat immer wert_bei()
benutzt - dieselbe Fahrt wurde also je nach Weg unterschiedlich bewertet,
genau das, was die gemeinsame Herkunft von UNPLAUSIBLE_KMH und MINDESTDAUER_S
in verlauf.py ausschliesst.
Am echten recorder-Verlauf nachgewiesen: fuer den Fahrtbeginn 16:39:42 UTC
lieferte naechster_wert() 21325 (der Stand von 16:40:53, also 71 s nach dem
Beginn und aus einem anderen Fahrzeug), wert_bei() dagegen 209177.
_vollstaendig() prueft ausserdem nach unten. Bisher galt nur > UNPLAUSIBLE_KMH;
ein Rueckwaertssprung ergab eine negative Strecke und wurde als "vollstaendig"
gespeichert. historienimport.py verlangt odo_end >= odo_start seit jeher. Die
Pruefung sitzt an der Strecke, nicht an der Geschwindigkeit: durchschnitt_kmh()
liefert bei Dauer 0 None, eine Fahrt mit null Sekunden kaeme sonst vorbei.
Dem Fahrzeugwechsel des Dongles waehrend der Testphase steht das nicht im Weg:
verglichen werden Anfang und Ende derselben Fahrt, und jedes Fahrzeug zaehlt
innerhalb seiner eigenen Fahrt aufwaerts. In allen 23 gespeicherten Fahrten
haette die Pruefung nie ausgeloest. Greift sie doch, werden beide Werte
verworfen und die Fahrt bleibt "offen" - der naechste Screening-Lauf versucht
es erneut.
Verifiziert gegen die echte Aufzeichnung in audi_ha_test: sieben Faelle,
darunter drei Regressionen (normale Fahrt, unmoegliches Tempo, echte 0-km-Fahrt
bei stillstehendem Zaehler). py_compile sauber, 2026.8.31.6 sauber gestartet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ZUENDUNG_SENSOR liegt jetzt auf dem Trip-Signal des FMM003 statt auf dem rohen,
prellenden Zuendungseingang. Damit kommt die Pausentoleranz vom Geraet - eine
Fahrt beginnt erst bei Zuendung UND Bewegung UND Start Speed.
MINDESTDAUER_S wandert nach verlauf.py und gilt jetzt auch im Livepfad. Vorher
verwarf nur der Import Zuendphasen unter 60 s; live entstanden daraus am 31.08.
elf Fahrten mit 0 km, zwei davon mit null Sekunden Dauer.
Das Panel erfand einen Literpreis von 0,00 EUR, wenn die Kosten noch nicht
feststanden (null/liter ist 0 in JavaScript) - neue Hilfsfunktion literpreis()
prueft beides, wie die App seit jeher. schnittpreis() teilte ohne Nennerpruefung
und konnte "NaN" oder "unendlich" anzeigen.
Der Setup-Dialog ist wirklich modal: die Tab-Leiste hatte einen eigenen Zuhoerer
vor der Sperre und liess einen Tabwechsel bei offenem Fenster zu.
Verwaister Code entfernt: drei Funktionen und vier Regeln im Panel, 20 Regeln in
der App.
Offen bleibt allein die leere Erstzulassung - eine Angabe, keine Aenderung.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die App blieb nach einem Neustart von Home Assistant auf dem alten Stand
stehen, mit einer 502-Fehlerzeile, die erst ein Beenden der App wegbekam.
Zwei Ursachen: 502/503/504 galten als harter Ladefehler statt als "Server
faehrt gerade hoch", und nach dem Wiederverbinden wurde genau einmal
nachgeladen - schlug das fehl, kam nie wieder ein Versuch. Beides behoben,
vier Regressionstests dazu.
OTA: das Buendel ist von HEAD ueber die Platte bis zu den ausgelieferten
Bytes nachweislich deckungsgleich. Die Fassung haengt jetzt trotzdem als
?v= an der Adresse (der statische Pfad kommt ohne Cache-Control, aber mit
ETag), und ein Pruefsummenfehler wird erklaert statt durchgereicht.
Statistik: der Gesamtwert steht jetzt als grosse Zahl in der Kopfzeile,
darunter bleiben vier Spalten - fuenf passen auf dem Telefon nachweislich
nicht in eine Zeile.
Fahrzeugfoto ohne Beschnitt und ohne Bodenschatten, damit auch ohne den
Schalter dafuer. Setup filtert zusaetzlich nach dem Signalnamen aus dem
Katalog.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der 15-Minuten-Timer war doppelt: der FMM003 wartet laut Konfiguration
(Trip \ Odometer, Ignition OFF Timeout) selbst 900 s, die App noch einmal
15 Minuten. Zusammen eine halbe Stunde. Die Pausenregel ist damit entfallen -
in der Live-Erkennung, im Historienimport und in beiden Oberflaechen. Der
Schwellwert "ab wann ist es ein Parkplatz" hing an derselben Einstellung, hat
damit aber nichts zu tun und steht jetzt als eigene Zahl.
Hoechst- und Durchschnittsgeschwindigkeit je Fahrt. Der Durchschnitt folgt aus
Strecke und Dauer, vmax aus der neuen Rolle GESCHWINDIGKEIT_SENSOR - laut
Konfiguration liest das Geraet die Geschwindigkeit vom OBD/CAN. Es ist der
groesste gemeldete Wert, nicht die tatsaechliche Spitze: alle zehn Sekunden
ein Datensatz. So ist es auch beschriftet.
Der Langzeitverbrauch war um einen ganzen Tankinhalt zu hoch. Gemessen wird
die Strecke ZWISCHEN erstem und letztem Tankstop; verbraucht wurde darauf,
was ab dem ZWEITEN hineinkam - der erste fuellte die Strecke davor. Bei 13
Tankvorgaengen rund 8 Prozent, im Testfall 16,7 statt 8,3 l/100 km. Der Test
hatte die falsche Rechnung festgeschrieben und ist mitkorrigiert. Eine
negative Strecke (falscher Kilometerstand an einem Tankvorgang) gilt jetzt
als unplausibel statt als Null ohne Erklaerung.
Verbrauch je Fahrt bekommt Grenzen: am Sensor nachgemessen loest
can_fuel_volume 0,1 l auf, ein Schritt entspricht also 10/Strecke l/100 km
Fehler. Unter 3 km sagt die Differenz nichts mehr, ueber 60 l/100 km ist es
kein Verbrauch.
Statistik: zusaetzlich "Absolut", und die Einheit steht jetzt in eckigen
Klammern an der Rubrik statt vier Mal am Zeitraum. Fuenf Spalten passen auf
einem Telefon nicht nebeneinander, das Raster bricht um.
Radfoto in Reifen war ein reines <Bild> - Ersetzen und Loeschen von dort aus
gar nicht erreichbar. Jetzt BildMitMenue wie im Panel.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DataMetric oben, das CI-Fahrzeug in der Mitte, 360 in Audi-Rot unten, auf
Nacht-Grund. Ausgewaehlt aus zwoelf Varianten, jede zweimal gezeigt: gross
und bei 60 pt, der Groesse auf dem Homescreen - daran entschied es sich.
Vektorquelle unter native/logo/, erzeugt von scripts/logo-bauen.cjs. Das
Fahrzeug liest das Skript aus ICONS.audi im Panel, damit es nur eine Quelle
fuer diese Form gibt; die Schriften stecken als data-URI in der SVG, sonst
haengt das Ergebnis davon ab, welche Fonts auf dem rasternden Rechner
installiert sind.
Drei Ausfuehrungen fuer iOS 18: hell mit eigenem Grund, dunkel und getoent
ohne - dort legt das System seinen eigenen Grund darunter, ein
mitgeliefertes Schwarz bliebe als Kasten sichtbar. Getoent wertet iOS die
Helligkeit aus, deshalb wird die 360 dort ein helleres Grau statt Rot.
ios-signieren.sh legt die Vorlage ueber die von Capacitor erzeugte - ios/
ist gitignored, ein in Xcode eingesetztes Symbol waere beim naechsten
cap add ios wieder weg.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Drei Schritte, die bisher am Entwickler hingen, laufen jetzt im Bau mit:
- Versionsnummer aus der manifest.json ins Info.plist. Capacitors Vorlage
setzt 1.0/1 und laesst es dabei - zwei nacheinander aufgespielte .ipa waren
auf dem Geraet nicht zu unterscheiden.
- Zwischengespeicherte Bereitstellungsprofile dieser App wegraeumen.
-allowProvisioningUpdates zieht ein neues Profil nur, wenn kein passendes
herumliegt; ein vorhandenes wird stillschweigend weiterbenutzt, auch wenn
sich die Berechtigungen geaendert haben. Genau daran ist am 2026-08-29 eine
Installation gescheitert, und mit der neuen App-Gruppe der Share-Erweiterung
ist derselbe Fall wieder eingetreten. Entfernt wird nur, was zu dieser App
gehoert; --profile-behalten ueberspringt es.
- ios-luftweg.sh haengt am Ende des Baus. manifest.plist und Icons landen
damit ohne zweiten Aufruf in auslieferung/, und der Schluss nennt die
itms-services-Adresse zum Oeffnen in Safari.
Ausserdem die Reihenfolge im Skript geradegezogen: der Kommentar zur
Standort-Berechtigung stand ueber dem Share-Block statt ueber seinem eigenen
Code.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Share-Erweiterung: ein PDF aus Mail landet ueber Teilen -> DataMetric360 als
Tankbeleg. Alles Native liegt versioniert unter companion-app/native/, weil
ios/ gitignored ist und npx cap add ios es sonst wieder frisst;
scripts/ios-teilen-einrichten.mjs haengt es bei jedem Bau ins Xcode-Projekt
und ist in ios-signieren.sh eingehaengt. Der pbxproj-Teil ist auf keinem Mac
erprobt - er bricht vor dem Schreiben ab, und docs/SHARE_EXTENSION.md
beschreibt dieselben Handgriffe fuer Xcode.
"In den Kalender uebernehmen" ging in der App nie: WKWebView ignoriert das
download-Attribut, der Klick lief ins Leere. Auf dem Geraet jetzt
@capacitor/filesystem plus @capacitor/share.
useTheme/useBoden legten je Aufrufer eigenen Zustand an - fuenf Kopien. Der
Schalter aenderte nur seine eigene, data-theme in App.tsx blieb stehen, die
Tag/Nacht-Umschaltung wirkte erst nach einem Neustart. Jetzt ein Speicher je
Einstellung ueber useSyncExternalStore. Zwei Regressionstests, gegen den
alten Stand als fehlschlagend nachgewiesen.
Alle Popups nach Apple HIG: neue Bausteine Sheet und ActionSheet, blickdicht
und mit abgedunkeltem Schleier, in beiden Codebasen. Popup portalierte nach
document.body - ausserhalb von [data-theme] - und war deshalb im Tagmodus
fast unsichtbar; Portal zielt jetzt auf .ads-root.
ImagePlaceholder merkte sich "fehlgeschlagen" ohne die Adresse: nach einem
leeren Galerieplatz zeigten auch die mit Foto nur noch den Platzhalter.
Vier Regressionstests, ebenfalls gegen den alten Stand geprueft.
Bildadressen tragen jetzt einen Cache-Brecher - HA liefert /local/ mit 31
Tagen Cache-Vorgabe aus, im Panel eingefuegte Radfotos erschienen in der App
deshalb nicht.
uebersichtsbild: Panel speichert den Namen, die App verglich gegen den
Dateinamen - der Vergleich traf nie zu. Kanonisch ist der Name.
Weiter: Navigationspfeil statt gleichschenkligem Dreieck auf der Streckenlinie
(die Kerbe unterscheidet Kopf und Ende), CI-Symbol tour-s als Fahrten-Icon,
Datumsfelder zeigen ihren Wert ohne erstes Antippen, feste Beispielnamen im
Setup, Kopf-Gegengewicht fuer mittige Titel, Standort-Blatt mit festem
Fussabstand, NSLocationWhenInUseUsageDescription, watchPosition fuer die
Live-Ortung, Art-Pille wieder als Knopf, Zwischenablage fuer Tankbelege.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Aufgezogen wurde das Blatt exakt so hoch wie sein Inhalt - die Knoepfe Route
und Teilen schlossen dadurch unten buendig mit der Menueleiste ab.
Apple benennt dafuer das passende Konzept: Blaetter rasten auf "detents" ein,
Hoehen, auf denen ein Blatt natuerlich zur Ruhe kommt; large ist das voll
ausgefahrene Blatt, medium etwa die Haelfte davon (Human Interface
Guidelines, Sheets - nachgelesen, nicht aus dem Gedaechtnis zitiert). Unser
Blatt hatte im aufgezogenen Zustand ueberhaupt keine Rasthoehe, sondern nur
seine Inhaltshoehe.
Jetzt min-height: 50% im aufgezogenen Zustand. Der Ueberschuss liegt unter
den Knoepfen und haelt sie von der Leiste frei. Die eingeklappte Hoehe bleibt
unberuehrt, weil die Verschiebung mit 100% der jeweiligen Blatthoehe minus
dem Guckwert rechnet.
Gemessen: Panel 311px auf 622px Karte, App 298 auf 595 - beide exakt 50%,
Luft unter den Knoepfen 89 bzw. 76px statt zuvor 18.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Haelt fest, warum node --check den Syntaxfehler von .14 nicht finden konnte
(Annex-B-HTML-Kommentare sind in Scripts erlaubt, in Modulen nicht) und wie
die Pruefung ab sofort laufen muss. Dazu der zweite Fehler: der Panel-Tab
wurde nach dem Ausliefern nie neu geladen, alle spaeteren Live-Aussagen
liefen gegen das alte Skript.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Beim "Zuendung an"-Patch habe ich den Begruendungskommentar in den
${...}-Ausdruck gesetzt statt in den Markup-Text. Dort ist <!-- kein
HTML-Kommentar, sondern JavaScript. Folge: audi-dashboard-app.js liess sich
nicht mehr als Modul parsen, connectedCallback() brach ab, das Panel hatte
keinen Shadow Root - der Bildschirm blieb leer.
Betroffen sind .14 bis .18, also alles, was seit dem auf Gitea lag. Wer in
dieser Zeit aktualisiert und neu gestartet hat, bekam ein totes Panel.
Warum es durchrutschte - und das ist der eigentliche Befund: "node --check"
prueft eine .js-Datei als SCRIPT, und dort sind HTML-artige Kommentare per
Annex B ausdruecklich erlaubt. Der Browser laedt dieselbe Datei als MODUL,
wo sie verboten sind. Die Pruefung, auf die sich dieses Projekt bei jeder
Panel-Aenderung verlaesst, kann diesen Fehler grundsaetzlich nicht finden.
Ab sofort gilt: die Datei vor dem Ausliefern als Modul pruefen, also unter
.mjs kopieren und node --check darauf laufen lassen. Rueckwirkend ueber alle
heutigen Commits geprueft - .14 ist der erste kaputte.
Erschwerend kam dazu, dass ich den Panel-Tab nach .14 nie neu geladen habe.
Alle spaeteren "live geprueft"-Aussagen zur Panel-Seite liefen gegen das noch
im Speicher stehende alte Skript und waren damit wertlos.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die angezeigte "Verfuegbare Version" ist das gespeicherte Ergebnis der letzten
Pruefung, nicht der Live-Stand - version_pruefen() laeuft ausschliesslich auf
Tastendruck. Im Zweig "Integration ist aktuell" stand der Zeitpunkt der
Pruefung schon dabei, ausgerechnet im Verfuegbar-Zweig nicht. Eine Wochen alte
Zahl sah dort aus wie eine Tatsache.
Schlimmer: der Knopf "Auf Update pruefen" gab es nur im Zweig "ist aktuell".
Sobald einmal ein Update gefunden war, liess sich die Angabe gar nicht mehr
auffrischen - sie blieb bis zur Installation stehen, auch wenn auf Gitea
laengst eine neuere Fassung lag.
Beides in beiden Codebasen ergaenzt: Zeitstempel als Nebenzeile an der
Version, "Erneut pruefen" als zweiter Knopf unter "Update installieren".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
--tile gehoert auf den Seitenhintergrund und sonst nirgends. Was auf einer
Kachel oder einem Blatt liegt, nimmt --tile-deckend (deckende Flaeche) oder
--ios-fill (Bedienelement). Grund: --tile ist nachts absichtlich
durchscheinend und tags deckend weiss - auf einer anderen Flaeche versagt es
deshalb in genau einem Theme, tags unsichtbar, nachts als doppelter Schleier.
Genau daran sind heute drei Fehler entstanden, jeder in nur einem Theme
sichtbar.
Geprueft zur Laufzeit statt per Textsuche, weil es auf die aufgeloeste Farbe
gegen die des naechsten gefuellten Vorfahren ankommt - beides steht nicht im
Quelltext. Panel 21 Routen, App 18 Seiten, beide Themes. Einziger
verbleibender Treffer ist die Listenzeile auf der Kachel im Tagmodus, wo
beide deckend weiss sind und die Zeile bewusst mit der Kachel verschmilzt.
Die Pruefung selbst ist gegengetestet: der heute behobene Schliessen-Knopf
wurde von Hand auf den alten Wert zurueckgesetzt und sofort erkannt. Eine
Pruefung, die man nie hat scheitern sehen, belegt nichts.
Dabei gefunden und angeglichen: die Kartenflaechen der Detailseiten trugen in
der App --tile-2, im Panel --tile-deckend - nachts ein zweiter Schleier auf
der ohnehin durchscheinenden Kachel.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der runde Knopf malte var(--tile) auf ein Blatt, das selbst deckend ist - im
Tagmodus beides #FFFFFF, also weiss auf weiss. Nachts faellt es nicht auf,
weil --tile dort ein Schleier ueber dunklem Grund ist.
Derselbe Fehlertyp wie bei den Wischzeilen von heute, nur andersherum: dort
addierten sich zwei Schleier im Nachtmodus, hier verschwindet eine Flaeche im
Tagmodus. Beide Male, weil eine Flaeche auf einer Flaeche mit --tile gemalt
wurde. Jetzt --ios-fill, in diesem Projekt ohnehin das Zeichen fuer "das hier
laesst sich bedienen"; es traegt in beiden Themes. In beiden Codebasen
geaendert - das Panel hatte den Fehler genauso.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Es gibt keinen Bewegungssensor am Fahrzeug. Der einzige Anhaltspunkt ist
ZUENDUNG_SENSOR, und der meldet on auch bei blosser Zuendung oder in
Zubehoerstellung - "Fahrzeug faehrt" behauptete also mehr, als bekannt ist.
Mein zwischenzeitlicher Vorschlag, zusaetzlich eine laufende Fahrt zu
verlangen, war falsch begruendet: fahrt_start_ts in fahrterkennung.py wird aus
genau demselben Signal abgeleitet und haette nur eine zweite Huelle um
dieselbe Aussage gelegt.
Der umgekehrte Fall bleibt unangetastet: Zuendung aus heisst verlaesslich,
dass gerade nicht gefahren wird - "Fahrzeug steht" und "Geparkt seit ..."
stehen weiter. Zwei Typ-Kommentare, die dieselbe Gleichsetzung
weitertrugen, sind mitkorrigiert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>