Commit Graph

75 Commits

Author SHA1 Message Date
tobias bfb7dec1e9 TANK_DISTANZ_SENSOR + Datensatz sichern/laden mit echtem CSV-Import (Panel)
- Neuer optionaler Sensor TANK_DISTANZ_SENSOR ersetzt die eigene
  Kilometerstand-Subtraktion fuer die Tankvorgang-Strecke, wo zugeordnet
  (Live-Erkennung, manuelle Erfassung, historischer Import)
- "Fahrzeugprofil" und "Daten ausgeben" zu einer Kachel zusammengelegt:
  "Datensatz sichern" (4 Exporte wie bisher) / "Datensatz laden" (neu:
  echter CSV-Import fuer Fahrten/Tankvorgaenge/Wartungsplan)
- Import gleicht per ID ab (Fahrten/Tankvorgaenge) bzw. Datum+Art
  (Wartungsplan, hat keine eigene ID) und aktualisiert nur die in der CSV
  enthaltenen Spalten - alles andere am Datensatz bleibt unangetastet
- Nebenbei gefunden und behoben: belege.tankvorgang_aktualisieren()
  ueberschrieb bisher immer alle Felder, auch mit None, wenn irgendeins
  geaendert wurde - fuer den CSV-Import gefaehrlich, jetzt nur noch
  tatsaechlich uebergebene Felder
- Ebenfalls gefunden und behoben: Panel las das Import-Ergebnis per
  HASS.states direkt nach dem Dienstaufruf - ein Wettlauf mit dem
  state_changed-Push. Nutzt jetzt denselben HASS.callWS(get_states)-Weg
  wie der bestehende historie_importieren-Ablauf.

companion-app-Portierung von "Datensatz sichern/laden" steht noch aus.

Version 2026.8.28.3, live im Testcontainer verifiziert (alle drei
CSV-Datensatztypen: anlegen + aktualisieren per ID/Datum+Art getestet,
Testdaten danach geloescht).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-28 10:19:06 +02:00
tobias edefdd66d8 Reifen/Version/Wartungsplan: erste Runde des Sammel-Feedbacks behoben
- Archiv-Knopf jetzt in Zeile mit "Montiert" (stale margin-top entfernt),
  bekommt einen sichtbaren Hintergrund
- companion-app: Wechseltermin-Datumsfeld (appearance-Reset gegen
  natives Safari-Chrome, Breite auf Inhalt geschrumpft)
- Mein Audi/Reifen-Box: "montiert: Sommerräder" links statt Marke/Modell,
  "offen" durch "-" ersetzt
- "Servicebuch" ueberall in "Wartungsplan" umbenannt (nur Anzeigetext,
  beide Codebasen)
- Version-Kachel: tote Fahrzeugdaten-/Position-/Dashboard-Zeilen entfernt
  (inkl. der nie aktualisierten CONFIG.version-Konstante)

Version 2026.8.28.1, live im Testcontainer verifiziert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-28 09:43:40 +02:00
tobias bdd8cb89a3 Batterie-Messwertliste: Zeilen sehen nicht mehr klickbar aus (Cursor/Presshighlight)
.leaf (Panel) und .dm-listenzeile (companion-app) galten fuer echte
Navigations-Buttons UND fuer die reine <div>-Messwertliste gleichermassen -
Hand-Cursor und Tipp-Rueckmeldung suggerierten dort faelschlich Klickbarkeit,
obwohl die Liste nur Wischen-zum-Loeschen unterstuetzt. Beide Regeln jetzt
auf button.leaf/button.dm-listenzeile beschraenkt - echte Zeilen-Buttons
(Fahrten, Tankvorgaenge, Servicebuch) bleiben unveraendert klickbar.

Version 2026.8.27.22, live verifiziert (Cursor auto statt pointer bei der
Messwertliste, pointer weiterhin bei echten Zeilen-Buttons).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-28 00:47:57 +02:00
tobias f4222b715f Batteriespannung-Fallback: zeigt letzte Ruhespannungsmessung ueber Neustarts hinweg
Der RAM-only Fallback fuer die Zustand-Kachel wurde bei jedem Neustart
geleert und zeigte "unbekannt", solange seit dem Neustart keine echte
Live-Messung eintraf (Dongle bei ausgeschaltetem Fahrzeug erwartungsgemaess
immer offline). spannung_cache_vorladen() laedt ihn jetzt beim Start aus
der gespeicherten Historie vor, begrenzt auf den vereinbarten Bereich
10,0-13,0 V (SPANNUNG_MIN_V bis AGM_RUHE_MAX_V) - eine Generatorspannung
(z.B. 15,4 V) soll dort nie als Batteriespannung erscheinen.

Version 2026.8.27.21, im Testcontainer verifiziert (12,149 V statt
"unbekannt", trotz Dongle weiterhin 0 V live).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-28 00:23:16 +02:00
tobias 90fff7b305 Vollaudit: sechs echte Fehler in Backend, Panel und companion-app behoben
- companion-app Batterie.tsx: Generatorspannung zeigte sich faelschlich als
  Ruhespannung/verzerrte den SOH-Trend, Panel-Filter (AGM_RUHE_MAX_V) fehlte
- Manuell angelegte Fahrten/Tankvorgaenge trugen naive Zeitstempel und wurden
  von der Import-Dublettenpruefung stillschweigend uebersprungen; neue
  gemeinsame zeit_normalisiert() in verlauf.py schliesst die Luecke
- Batteriespannungsverlauf fehlte im Backup (sicherung.py)
- Tankvorgangs-Import verlor stillschweigend aeltere Tankvorgaenge vor Beginn
  der Litersensor-Historie; laeuft jetzt zweigleisig (Prozent + Liter)
- Panel-Statistikseite behauptete faelschlich, der Verbrauch je Fahrt komme
  vom Fahrzeug (OBD) - ist eine Naeherung aus dem Tankfuellstand
- companion-app Statistik.tsx: Arbeitsweg-Segment war noch rot statt neutral

Version 2026.8.27.19, OTA-Buendel neu gebaut, im Testcontainer verifiziert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-28 00:05:58 +02:00
tobias e1ddf2cda1 AGENTS.md: dokumentiert den Update-Vorfall - Selbst-Update hat unveroeffentlichte Sitzungsarbeit ueberschrieben
Der Nutzer fuehrte ein Selbst-Update aus, das Gitea's letzten committeten Stand
(80d73e8) zog - alle Fixes dieser Sitzung waren bis dahin nur per docker cp an
audi_ha_test ausgeliefert, nie committet. Manifest fiel auf 2026.8.27.8 zurueck,
alle Symptome (Batteriespannung "unbekannt", Diagramm-Maximalwert 13,99V) passten
exakt zum Vor-Sitzungs-Stand. Container wiederhergestellt, jetzt committet -
Lehre fuer kuenftige Sitzungen im Dokument festgehalten.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 23:24:24 +02:00
tobias 172db54e7a Statistik-Icon-Groesse, Batteriediagramm-Ueberarbeitung, Temperatur-Kopplung mit 300s-Fenster, letzte gueltige Spannung als Fallback
- Statistik-Tab-Icon per zentrierter Stauchung an die Fuellflaeche der
  anderen Tab-Icons angeglichen (Panel + companion-app), auf Nutzerwunsch
  anschliessend 10% groesser.
- Batteriediagramm neu gezeichnet: dynamische viewBox (1 Einheit = 1 Pixel,
  kein preserveAspectRatio-Verzerren mehr), ein Punkt je Ruhespannungsmessung
  statt Datumsbeschriftung, Tageshoechstwert nur noch im Tooltip. Generator-
  spannungen (> AGM_RUHE_MAX_V = 13,0 V, vom Nutzer festgelegt) bleiben aus
  dem Diagramm aussen vor; der SOC-Kurve-Eckwert "100% / voll" bleibt bei den
  ursprünglich recherchierten 12,8 V. companion-app auf dieselbe lineare
  SOC-Interpolation umgestellt (vorher eine abweichende Stufenkurve).
- Außentemperatur wird jetzt auch beim Batterieverlauf-Import nachgetragen
  (vorher nur live) - Kopplung an die naechstgelegene Messung innerhalb
  NEBENWERT_MAX_ABSTAND_S (verlauf.py), vom Nutzer nach Live-Daten-Analyse
  auf 300s festgelegt. Merge-Logik in ablage.py ergaenzt: ein erneuter Import
  kann eine zuvor fehlende Temperatur jetzt tatsaechlich nachtragen, auch
  wenn sich der Tagesminimalwert selbst nicht aendert.
- "Mein Audi"/Zustand zeigt bei unplausibler Live-Messung (z. B. Dongle
  offline) die zuletzt gemessene Spannung statt "unbekannt".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 23:23:12 +02:00
tobias 80d73e8afd Fahrten: Verbrauch-Näherung, 0V-Zustandsanzeige gefiltert, Plausibilitätsgrenze für die Live-Vervollständigung
Verbrauch (l/100km) war bislang totes Schema - kein Codepfad füllte
verbrauch_l_100km je. Jetzt aus der Literstand-Differenz (TANK_LITER_SENSOR)
über die Distanz genähert, live und beim Import, in beiden Frontends.

Die "Zustand"-Kachel zeigte die Batteriespannung ungefiltert direkt vom
Sensor, unabhängig von der Plausibilitätsgrenze der Verlaufsaufzeichnung -
ein Sensorausreißer (0V) zeigte sich dort weiterhin, obwohl die Messwertliste
ihn längst verwarf. Dieselbe Grenze gilt jetzt auch für diesen Anzeigepfad.

Reale Fahrtendaten zeigten eine Fahrt mit 22km in 67s (~1180 km/h) - die
Live-Vervollständigung (screening.py) hatte anders als der Import keine
Plausibilitätsprüfung der Durchschnittsgeschwindigkeit. Jetzt gemeinsam in
verlauf.py (UNPLAUSIBLE_KMH/durchschnitt_kmh) für beide Pfade. Dabei einen
zweiten echten Bug gefunden: _fahrt_screenen() zog sein Ergebnis nie ins
In-Memory-Objekt nach (nur in die Ablage) - eine im selben Durchlauf gerade
erst ermittelte Distanz blieb für spätere Schritte (z. B. Verbrauch) bis zum
nächsten Screening unsichtbar. Beide Stellen jetzt behoben.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 21:01:57 +02:00
tobias cf57c5f1a5 Batteriespannungs-Diagramm: feste 170px-Höhe statt aspect-ratio, Y-Achse geklemmt
Die aspect-ratio-Lösung für die volle Spaltenbreite ließ auf großen Bildschirmen
die gesamte Koordinatenfläche mitwachsen - Achsenbeschriftungen (fester font-size
in SVG-Einheiten) wurden dadurch auf ~26px zu groß, das Diagramm unnötig hoch.
Zurück auf eine feste 170px-Höhe: der Y-Maßstab bleibt immer 1:1, nur die
X-Achse dehnt sich auf die volle Breite - normale Schriftgröße, kompaktes
Diagramm, trotzdem volle Spaltenbreite.

bvSkalaY()/y() klemmen jetzt auf [0,1] statt roh zu extrapolieren - ein Punkt
außerhalb von yMin/yMax (z. B. ein Ausreißer auf einer noch nicht aktualisierten
Instanz) zeichnet sich sonst weit außerhalb der Fläche und reißt die Linie über
den Rand hinaus, statt sichtbar am Achsenrand zu liegen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 18:33:31 +02:00
tobias b8e560e75b Batteriespannung: Diagramm-Y-Achse fest 10-15V, Plausibilitätsgrenze auf 10V angehoben
Die Y-Achse beider Batteriespannungs-Diagramme (Panel und companion-app) war bisher
aus den sichtbaren Punkten berechnet - jetzt fest 10-15V, auf Nutzerwunsch (zunächst
8-15V, dann auf 10-15V korrigiert). SPANNUNG_MIN_V (batterie.py) auf denselben Wert
angehoben, damit die Plausibilitätsgrenze zur Achse passt - historienimport.py
übernimmt den Wert automatisch, da er von dort importiert statt dupliziert wird.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 18:06:12 +02:00
tobias f337820fc8 Batteriespannung: 8V-Plausibilitätsgrenze; Fahrten: echte Start-/Zielposition und Streckenlinie
Batteriespannung: Werte unter 8V (Sensor-/Verbindungsfehler, nicht physikalisch
plausibel für eine 12V-Bleibatterie) werden nicht mehr erfasst - live und beim
Import. "Neue Räder anlegen"-Knopf sitzt jetzt auf derselben Zeile wie "Montiert".

Fahrten: start_lat/-lon, end_lat/-lon und route wurden bisher nur beim
rückwirkenden Import befüllt, nie bei live erkannten Fahrten (dem Normalfall) -
Screening trägt sie jetzt aus dem GPS-Verlauf nach, unabhängig vom
Kilometerstand-Status. fakeTrack() (eine erfundene Linie) ist entfernt, die
Karte zeichnet jetzt die echte Route oder eine ehrliche Gerade zwischen den
bekannten Punkten. CARTOs anonyme Kartenkacheln verlangen inzwischen einen
API-Schlüssel ("API key required" auf jeder Karte in beiden Apps) - auf
schlüssellose OpenStreetMap-Kacheln umgestellt. Start/Ziel zeigen jetzt Datum
und Uhrzeit über der Adresse; die fehlerhafte "Status"-Zeile ist entfernt.
Batteriespannungs-Diagramm nutzt auf großen Bildschirmen die volle
Spaltenbreite statt einer festen 400px-Deckelung.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 17:23:32 +02:00
tobias 17149937a6 Batteriespannungs-Messwertliste, Wisch-Löschen, Reifensatz-Archiv, CI-Icons
Fünf vom Nutzer angeforderte Erweiterungen in einer Runde:

- Batteriespannungs-Kachel bekommt eine Messwertliste (Datum, Uhrzeit,
  Spannung, optional Außentemperatur über den neuen AUSSENTEMP_SENSOR),
  erreichbar über ein neues list-s-Symbol in der Diagramm-Kachel. Werte
  über AGM_RUHE_MAX_V (12,8 V) sind keine Ruhespannung, sondern
  Lichtmaschinenspannung - die Zeile markiert das jetzt mit
  "Generatorspannung" statt es unkommentiert als Messwert auszugeben.
- Jede Zeile der neuen Liste wischbar zum Löschen (neuer Dienst
  batterieverlauf_loeschen), über dieselbe Wisch-Mechanik wie Fahrten/
  Tankvorgänge in beiden Oberflächen.
- Batteriespannungs-Diagramm im Panel war auf großen Bildschirmen (≥860px,
  Spaltenlayout) stark in die Breite gezogen (preserveAspectRatio="none"
  bei fester Höhe) - gedeckelt wie die dort bereits vorhandenen Popups.
- Statistik-Tab-Symbol in beiden Oberflächen durch das Audi-CI-Symbol
  polls-s ersetzt.
- Neue Möglichkeit, einen Reifensatz zu archivieren ("Neue Räder
  anlegen"): der aktuelle Stand (km, Marke, Modell, DOT, Maße, Solldruck,
  Kommentar) wandert in ein Archiv, der laufende Satz beginnt bei 0 km neu.
  Archiv als aufklappbare Übersicht mit voller Bearbeitungsmöglichkeit,
  über drei neue, bewusst nicht über profilSchreiben laufende Dienste
  (reifen_archivieren/-aktualisieren/-loeschen) - aus demselben Grund wie
  reifen_wechseln: kein im Browser gehaltener Stand darf einen
  zwischenzeitlich fortgeschriebenen km-Wert überschreiben.

Live im Testcontainer geprüft (erstmals über :18123 statt :8123 erreichbar)
und dabei zwei echte Layout-Fehler gefunden, die kein Compile-/Testlauf
sehen konnte: die Archiv-Eingabefelder waren durch ein fälschlich
verwendetes .mitEinheit (feste 96px-Breite, eigentlich für Zahl+Einheit
gedacht) abgeschnitten ("Continenta" statt "Continental"), und die
Kilometerzahl in der eingeklappten Archiv-Zeile war klein an das Datum
gequetscht statt wie der Satzname lesbar. Beides behoben, live erneut
bestätigt.

Backend: py_compile clean, services.yaml ergänzt. Panel: node --check
clean. companion-app: tsc/Testsuite (146/146)/Build/OTA-Bündel alle grün.
Manifest 2026.8.25.2 → .8, jeder Neustart im Testcontainer sauber.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 11:44:24 +02:00
tobias 70253e83a5 Fahrterkennung: FMM003-Funklöcher reißen keine Fahrt mehr auseinander
Die Zündungs-Entität stammt vom FMM003-Tracker; verliert er unterwegs
kurz die Verbindung, meldet Home Assistant "unavailable" statt eines
echten Zündungszustands. Die Live-Erkennung wertete das bisher wie
"aus" - eine Funklücke über der Pausenzeit teilte eine durchgehende
Fahrt in zwei. zuendung_geaendert() ignoriert "unavailable"/"unknown"
jetzt vollständig und leitet "fährt gerade" aus dem eigenen
Zwischenstand (fahrt_start_ts) statt aus dem letzten Rohwert ab.

historienimport.py bekommt zusätzlich eine Plausibilitätsprüfung: eine
errechnete Durchschnittsgeschwindigkeit über 300 km/h deutet auf einen
Kilometerstand-Ausreißer an der Fahrtgrenze hin, nicht auf eine echte
Fahrt - die Strecke wird dann verworfen statt eine unmögliche Fahrt
anzuzeigen.

Tankerkennung: optionaler zweiter Sensor für das Tankvolumen in Litern
(TANK_LITER_SENSOR, direkt vom CAN) neben dem bisherigen
Prozent-Füllstand. Ist er zugeordnet, übernimmt er die automatische
Tankerkennung vollständig - genauer als der Umweg über das im
Fahrzeugprofil hinterlegte Tankvolumen, und der erkannte Anstieg
liefert gleich eine grobe Vorbelegung für die getankte Menge statt
eines leeren Feldes. Ohne den Sensor bleibt alles beim Alten. Der
historische Import zieht mit derselben Präferenz nach.

Manifest auf 2026.8.25.2, beide Änderungsrunden live im Testcontainer
verifiziert (py_compile, Neustart, sauberes Setup-Log).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 22:04:26 +02:00
tobias 4fccf21676 Selbst-Update: Ergebnis erscheint ohne Neuladen, Panel übersteht Neustart
Zwei Meldungen des Besitzers ("Update installieren" braucht manuelles
Neuladen, um den Neustart-Knopf zu zeigen; die "HA startet neu..."-
Anzeige ebenso) auf denselben Grund zurückgeführt: datenLaden() rief
render() nur bei Änderungen an profil/fahrten/tank/status/batt auf,
nie bei einer reinen Änderung des Versions-Sensors. Dazu ein bereits
im Code kommentiertes, bisher nur per Tab-Klick umgangenes Rennen
beim echten HA-Neustart gefunden und behoben: panel_custom baut das
Element neu auf, aber nichts zeichnete es automatisch, bis der Nutzer
irgendwo klickte. companion-app erhielt den passenden Fix für die
Neustart-Anzeige (Panel-Bug 1 betraf sie nicht, siehe AGENTS.md).

Alle drei Fixes live im Testcontainer über einen echten
homeassistant.restart bestätigt, nicht nur simuliert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 19:39:40 +02:00
tobias 0354ab8768 Update-Knopf: Seitensprung im Großbild-Layout behoben
Auf Update prüfen/Update installieren ließ die Seite im Seitenleisten-Layout
sichtbar nach oben springen - render()s eigene Scroll-Wiederherstellung
reichte hier nicht, vermutlich weil die Zustandsänderung des
Update-Sensors zusätzlich über HAs eigenes set-hass-Durchreichen an das
eingebettete Panel einen zweiten Render-Pfad anstößt. Statt die genaue
Ursache weiter zu jagen: fester Scroll-Merkwert vor dem Klick, per
requestAnimationFrame nach jedem eigenen render()-Aufruf dieses Ablaufs
erneut erzwungen. Live bei 1280x800 verifiziert: kein Sprung mehr.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 18:18:51 +02:00
tobias 7276d06a18 Sicherheit-Feinschliff, Nächster-Service-Sortierfehler behoben, Fahrzeugstatus umbenannt
- Türschlösser auf ein Sensor reduziert (Fahrertür genügt, Zentralverriegelung
  schließt alle Türen gemeinsam) - Kofferraum-/Motorhauben-Schloss entfernt.
- "Sicherheit" heißt im Panel jetzt "Fahrzeugstatus", wie in der companion-app.
- naechsterTermin() (Panel): sortierte null als Epoch 1970 und ließ dadurch
  einen datenlosen Termin (Hauptuntersuchung ohne Eintrag/Erstzulassung) immer
  vor einem echten, berechneten Termin (Ölwechsel) gewinnen - Ursache dafür,
  dass Übersicht nach dem Leeren des Servicebuchs nichts mehr zeigte.
- "Licht ausgeschaltet" statt "Kein Licht" (Formulierung zurückgenommen).
- Große Bildschirme: Unterseiten (Standort, Service, ...) zeigten ihren Titel
  in 26px statt der sonst überall genutzten 17px - wirkte neben dem
  durchgängig leichten Fließtext wie Fettschrift, obwohl font-weight nirgends
  wechselt. Jetzt dieselbe Standardgröße wie im Telefon-Layout.

Details und Verifikation in AGENTS.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 17:00:33 +02:00
tobias 5a048488ea Selbst-Update-Sicherung, Service-Prognose ohne Servicebuch und Sicherheit neu gestaltet
- aktualisierung.py: Backup-Ordner verliert sein manifest.json beim Sichern,
  sonst laedt HA ihn nach einem Neustart als zweite audi_dashboard-Integration
  (Ursache des raetselhaften "gleicher Pfad"-Umbenennungs-Fehlers).
- service.ts/audi-dashboard-app.js: Uebersicht faellt jetzt wie Service schon
  auf die Fahrzeugmeldung zurueck, wenn kein Servicebucheintrag existiert;
  eigenes (kuerzeres) Oelwechsel-Intervall wird jetzt auch ohne Servicebuch-
  eintrag naeherungsweise aus der Herstellermeldung hochgerechnet, statt
  weiterhin unveraendert die Herstellervorgabe zu zeigen.
- Sicherheit neu gestaltet: vier Sammelzeilen (Fahrzeug verriegelt / Tueren
  und Klappen geschlossen / Fenster und Dach geschlossen / Kein Licht) statt
  einer flachen Liste, mit neuen Tuerschloss-, Dach- und Standlicht-Sensor-
  rollen, Kreis-Haekchen-Symbolen und einer Detailseite je Tuer/Motorhaube/
  Kofferraum in beiden Frontends.

Details, Verifikation und Sensor-Rollen in AGENTS.md (Abschnitte J/M/N).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 15:29:37 +02:00
tobias 146f809e9b Selbst-Update: Ladezustand und echter Neustart-Knopf
Der erste echte Live-Test des Selbst-Updates gegen das private Gitea-Repo
lief erfolgreich (mehrfach im Test-Container bestätigt) - deckte aber zwei
UX-Lücken auf, die der Owner direkt gemeldet hat:

- Prüfen/Installieren gaben während der 15-25 Sekunden dauernden
  Netzwerkaktion keine sichtbare Rückmeldung - nicht von einem Hänger zu
  unterscheiden. Jetzt ein Ladeindikator (Panel: wiederverwendetes
  .lade-spinner; companion-app: neues .dm-spinner-Äquivalent, gab es dort
  noch gar nicht).
- Nach erfolgreicher Installation stand nur ein Hinweistext da, kein Weg
  zum eigentlich nötigen nächsten Schritt. Der Knopf wechselt jetzt zu
  "Installation abschließen - Jetzt neu starten" und stößt
  homeassistant.restart direkt an.

Das separat gemeldete "Lädt"-Hängenbleiben war kein Bug: reproduziert durch
Live-Test des neuen Neustart-Knopfs - eine echte HA-Verbindungsunterbrechung
während eines Neustarts zeigt exakt denselben, bereits bestehenden
Bootstrap-Ladebildschirm. Vermutlich derselbe Effekt durch den
Docker-Neustart früher in dieser Sitzung.

Verifiziert im Test-Container per Browser-Automatisierung: Neustart-Knopf
ausgelöst, Verbindungsabbruch beobachtet, per docker logs bestätigt, dass
Home Assistant tatsächlich neu gestartet ist und die neue Version aktiv
wurde.

Details in AGENTS.md, Abschnitt L.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 12:44:47 +02:00
tobias 7365180550 vCard-Import fürs Autohaus, Selbst-Update-Logging und UI-Feinschliff
- Service -> Autohaus -> Zahnrad hat jetzt einen "vCard importieren"-Knopf
  (öffnet den Dateiauswahldialog für .vcf). Liest FN/ORG, TEL, EMAIL, ADR
  und befüllt das Formular - in beiden Frontends, jeweils passend zum
  bestehenden Speicherverhalten (Panel speichert sofort, companion-app
  erst über den vorhandenen "Speichern"-Knopf).
- update_pruefen/update_installieren loggten bisher nur bei einem Fehler -
  ein erfolgreicher Aufruf war im Log nicht von "noch nicht ausgeführt" zu
  unterscheiden. Jetzt auch eine Erfolgsmeldung.
- Panel: der feste Hinweis auf den fehlenden Gitea-Token stand dauerhaft in
  der Integration-Update-Kachel, auch wenn ein Token längst hinterlegt war.
  Erscheint jetzt nur noch als Teil einer echten Fehlermeldung.

Details in AGENTS.md, Abschnitte J (Nachtrag) und K.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 12:30:24 +02:00
tobias fb251154c7 Ölwechsel/Inspektion-Anzeige repariert, Selbst-Update der Integration gebaut
Zwei getrennte Themen in einem Commit, beide in derselben Sitzung entstanden:

1. Ölwechsel/Inspektion wurden komplett ausgeblendet ("kein Eintrag im
   Servicebuch"), sobald kein Servicebucheintrag vorlag - selbst wenn der
   zugeordnete Sensor eine gültige Fälligkeit meldete. In beiden Frontends
   prüfte die Anzeige nur den Servicebuch-Zweig, bevor sie die
   Fahrzeugmeldung überhaupt las. Jetzt steht die Fahrzeugmeldung für sich;
   fehlt zusätzlich ein Servicebucheintrag, übernimmt eine neue,
   fahrtenlog-basierte Prognose (kmProTagAusFahrten()/meldungsPrognose())
   die Hochrechnung statt der Servicebuch-Rate - deckelt auf die vom
   Fahrzeug selbst gemeldete Zeitgrenze, falls zu wenig gefahren wird.

2. install.ps1 als Update-Weg wird von Windows Smart App Control blockiert,
   ohne Umgehungsmöglichkeit. Die Integration lädt sich jetzt auf
   Tastendruck selbst von Gitea (aktualisierung.py), verifiziert das
   Manifest vor jedem Tausch und tauscht per os.rename mit automatischem
   Rollback bei Fehlern - install.ps1 bleibt nur noch für die
   Erstinstallation nötig. Zugangstoken über einen neuen OptionsFlow in
   entry.options, nie in configuration.yaml.

Nebenbei: mehrere seit der HACS-Ausschluss-Entscheidung liegen gebliebene
falsche HACS-Referenzen in Code-Kommentaren und einem UI-Text korrigiert.

Details, Sicherheitsbegründung und Verifikationsstand in AGENTS.md,
Abschnitte I und J.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 12:01:01 +02:00
tobias e999d629c5 install.ps1 fragt bei fehlgeschlagener Verbindung selbst nach Zugangsdaten
Test-Path auf einem UNC-Pfad fragt nie nach einem Passwort - ohne eine
bereits bestehende SMB-Sitzung liefert es bei einem geschuetzten Share
einfach "false", ganz ohne Fehlermeldung. Das Skript verliess sich bisher
darauf, dass eine Sitzung schon von Hand aufgebaut wurde (Explorer oder
"net use"), und meldete sonst "Ziel nicht erreichbar" - selbst wenn Pfad
und Zugangsdaten korrekt waren. Jetzt fragt es bei fehlender Erreichbarkeit
selbst nach Benutzername/Kennwort und baut die Sitzung per "net use" auf,
bevor es endgueltig abbricht.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 10:27:51 +02:00
tobias 2ea01ce2ce install.ps1 warnt vor einem veralteten OTA-Bündel
Beim Prüfen, ob die GitHub-Entfernung eine Versionserhöhung brauchte, fiel
eine echte Lücke auf: nichts hielt frontend/app/bundle.json auf demselben
Stand wie manifest.json. Bei der nächsten echten Versionserhöhung ohne
"npm run ota" hätte das zu einem stillen Widerspruch geführt - die
Hinweisleiste hätte "veraltet" gemeldet (liest die Version live), die
Update-Seite "aktuell" (vergleicht gegen das eingefrorene, dann falsche
Bündel) -, ohne dass ein Update über OTA erreichbar gewesen wäre, bis
jemand den Widerspruch bemerkt.

install.ps1 liest jetzt direkt nach der Manifest-Version auch
frontend/app/bundle.json und vergleicht. Bei Abweichung: deutliche Warnung
plus erster Eintrag in der Restliste. Fehlt das Bündel ganz, passiert
nichts - OTA ist optional, das ist der normale Zustand.

Beide Pfade an einem Mock-Zielordner geprüft: passende Versionen melden
"OTA-Bündel passt zur Integration" ohne Restliste-Eintrag; eine testweise
erhöhte Manifest-Version (danach byte-genau zurückgesetzt, gegen HEAD
gegengeprüft) erzeugt die Warnung und landet als Restliste-Punkt 1.

VERSIONIERUNG.md: "Was du tun musst" nennt den OTA-Neubau jetzt als
eigenen nummerierten Schritt, statt ihn ganz auszulassen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 09:32:36 +02:00
tobias ba182cb180 GitHub-Spiegel entfernt - HACS kann nicht helfen, Gitea bleibt die einzige Quelle
Der am 2026-08-24 angelegte Spiegel nach github.com/T130B/DM360 hatte keinen
Zweck erfüllt: HACS lehnt private Repositories laut eigener Dokumentation
kategorisch ab, unabhängig davon, ob sie auf Gitea oder GitHub liegen. Ein
GitHub-Konto zusätzlich zu pflegen kaufte also nichts. Auf Wunsch entfernt -
entwickelt wird ausschließlich auf der eigenen Gitea-Instanz.

- hacs.json entfernt: tote Konfiguration ohne GitHub-Ziel, das sie bedienen
  könnte.
- manifest.json: documentation/issue_tracker/codeowners zeigen jetzt auf
  gitea.nothaft.cloud/paul/audi-app statt auf den abgeschafften Spiegel.
- install.ps1: Kopfkommentar nennt keinen aktiven Spiegel mehr.
- AGENTS.md: die HACS-Ablehnung als endgültig festgehalten (nicht nur "heute
  privat"), samt der Begründung, warum ein erneuter GitHub-Anlauf nichts
  ändern würde - falls das je wieder aufkommt.

Am laufenden Testcontainer geprüft: Integration lädt mit dem neuen Manifest
fehlerfrei neu, keine Fehler im Protokoll.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 09:26:50 +02:00
tobias 1354ca6b06 OTA-Updates für die iOS-App; HACS-Fehlannahme korrigiert
HACS kann laut eigener Dokumentation grundsätzlich nicht mit privaten
GitHub-Repositories arbeiten (hacs.xyz/docs/faq/private_repositories) - keine
Ausnahme für Tokens oder verbundene Konten. Meine frühere Annahme, HACS käme
damit zurecht, wenn es unter dem richtigen Konto angemeldet ist, war falsch.
Da das Repository aus Lizenzgründen privat bleiben muss (Audi-Hausschrift,
Typenschilder), ist install.ps1 damit nicht die Rückfallebene, sondern der
einzige Installationsweg - README, INSTALL.md, ANLEITUNG.md, install.ps1 und
VERSIONIERUNG.md korrigiert.

Oberflächen-Updates für die iOS-App laufen jetzt ohne Xcode:
@capgo/capacitor-updater eingebaut, ein Update-Abschnitt in den
Einstellungen lädt ein neues Bündel und tauscht die Oberfläche aus. Kein
Selbstlauf (autoUpdate: false) - nur auf Tastendruck, nie während der
Benutzung.

Das Bündel liegt in der Integration selbst
(custom_components/audi_dashboard/frontend/app/), nicht unter /local/: so
reist es bei jeder Installation automatisch mit, ohne zweiten
Auslieferungsweg. Gebaut von companion-app/scripts/ota-paket.ps1 (neuer
Befehl: npm run ota), gemeldet über sensor.audi_dashboard_app_version
(neues Feld daten.buendel).

Ein echter Bug beim Bauen gefunden: [IO.Compression.ZipFile]::CreateFrom-
Directory schreibt unter Windows PowerShell 5.1 Backslashes in die
Zip-Einträge - iOS hätte das Archiv falsch entpackt. Behoben, indem die
Einträge von Hand mit "/" geschrieben werden.

Rückfallebene: notifyAppReady() läuft erst, wenn React nachweislich
gerendert hat (App.tsx). Kommt diese Meldung nicht, rollt das Plugin nach
20 Sekunden von selbst auf das vorherige Bündel zurück.

Am laufenden Testcontainer verifiziert: die ausgelieferte Zip hasht exakt
auf den in bundle.json hinterlegten Wert, 13 Einträge, index.html in der
Wurzel, keine Backslashes, keine Beschädigung. tsc sauber, 117/117 Tests
(5 davon neu für buendelPasst() - dabei eine echte Lücke gefunden: die
Funktion hätte bei unbekannter eigener Version fälschlich ein Update
angeboten, jetzt genauso vorsichtig wie versionVergleichen).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 09:20:14 +02:00
tobias d8b12da36d pyscript-Backend zur echten HA-Integration umgebaut (HACS-fähig)
Das Backend liegt jetzt als custom_components/audi_dashboard/ vor - eine
normale Home-Assistant-Integration mit Config-Flow, einer sensor-Plattform
und 18 Diensten. Damit ist die App über HACS installierbar; bis das Repo auf
GitHub gespiegelt ist (HACS spricht ausschließlich mit GitHub), installiert
homeassistant/installationspaket/install.ps1 denselben Ordner ohne HACS.

Fünf Installationsschritte entfallen ersatzlos: der pyscript:-Block, der
panel_custom:-Block, das Kopieren der Oberfläche nach www/, das langlebige
Zugriffstoken (der Verlauf wird direkt über die recorder-API gelesen) und
"pip install pypdf" (steht in manifest.json). Das Fahrzeugprofil legt die
Integration beim ersten Start aus ihrer Vorlage an.

Drei alte Schwächen sind dabei mit erledigt:
- Die Nutzlast landet nicht mehr in der Recorder-Datenbank
  (_unrecorded_attributes - das kann nur eine echte Entität).
- Eine laufende Fahrt überlebt einen Neustart (Store statt Arbeitsspeicher);
  fiel sie während eines Ausfalls ins Ende, schließt
  nach_neustart_fortsetzen() sie beim letzten aufgezeichneten Zeitpunkt.
- Sensor-Zuordnungen wirken sofort - die Zustandsbeobachter werden neu
  gebunden, der Neustart-Hinweis und der Neustart-Dienst sind weg.

Namensvertrag geändert, beide Oberflächen mitgezogen:
pyscript.audi_dashboard_x -> sensor.audi_dashboard_x,
pyscript.audi_dashboard_y -> audi_dashboard.y. Eine Companion-App vom alten
Stand findet nach dem Umstieg nichts mehr und muss neu gebaut werden; das
Panel liegt in der Integration und kann nicht driften.

Der selbstgebaute Updater entfällt - HACS ist die Update-Mechanik, die Home
Assistant kennt. Die Versionierung schrumpft auf eine Quelle: manifest.json.

Geprüft am laufenden Testcontainer (Container byteweise identisch mit dem
Repo): alle 18 Dienste, Panel, Config-Entry neu laden, Historienimport,
echter Shell-Beleg in-process, Neuinstallation im Wegwerf-Container blank mit
automatisch nachinstalliertem pypdf. Companion-App: tsc sauber, 112/112
Tests, beide Rauchtests gegen das laufende Backend grün. Belegparser 8/8.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 23:53:56 +02:00
tobias 99cef7c393 Fahrzustand aus der Zuendung statt aus dem Fahrtstatus, App-Version vergleichbar
BUG "Fahrzeug faehrt" aenderte sich nie. standortZustand() leitete den
Fahrzustand aus TRIPS[0].status === "offen" ab - zwei Fehler
uebereinander, die sich gegenseitig verstaerkt haben.

Erstens heisst "offen" nicht "unterwegs", sondern "Daten noch
unvollstaendig": _fahrt_beenden() legt die Fahrt mit diesem Status an,
wenn sie ENDET, und das Kilometerstand-Screening fuellt sie spaeter. Eine
Fahrt, die nie eine Strecke bekam - etwa weil damals kein KM_SENSOR
zugeordnet war -, bleibt fuer immer "offen". Zweitens ist TRIPS[0] die
AELTESTE Fahrt, nicht die neueste: fahrten_veroeffentlichen() reicht
profil.fahrten_lesen() unveraendert in Dateireihenfolge weiter, und die
ist aufsteigend. Zusammen hat die aelteste jemals unvollstaendig
gebliebene Fahrt das Fahrzeug dauerhaft als fahrend angezeigt; an der
Testinstanz war das eine seit 18 Tagen beendete Fahrt.

Nicht den Index geflickt, sondern die Quelle korrigiert: das Backend
veroeffentlicht jetzt "zuendung" im Fahrzeugstatus, gelesen aus dem
ohnehin zugeordneten ZUENDUNG_SENSOR - demselben Signal, das auch
fahrterkennung.py als massgeblich nimmt. Anzeige und Erfassung koennen
dadurch gar nicht mehr auseinanderlaufen. Ohne zugeordneten Sensor
(null) steht "Fahrzustand unbekannt" statt einer Behauptung.

companion-app hatte denselben Fehler spiegelverkehrt: liveZustandLesen()
las "zuendung" (gab es nie) und "lat"/"lon" (das Backend liefert
standort_lat/standort_lon), und der Typ Fahrzeug kannte keines dieser
Felder. Die Live-Ansicht meldete deshalb dauerhaft "Das Fahrzeug steht"
und zeigte nie eine Position. Felder in Fahrzeugstatus/Fahrzeug und im
Adapter ergaenzt, damit der Typ diese Fehlerklasse kuenftig faengt -
wovor sein eigener Kopfkommentar seit einem frueheren Vorfall warnt.

BUG "Standortzugriff verweigert" war irrefuehrend. Die Zeile steht
direkt unter dem Fahrzeugnamen, wo sonst der Abstand zum Auto steht,
handelt aber vom Standort DIESES Geraets - sie las sich, als sei der
Standort des Autos nicht abrufbar. Jetzt "GPS offline" wie gewuenscht.
Die verweigerte Freigabe behaelt eine eigene Meldung ("GPS-Freigabe
fehlt"): sie ist der einzige Fall mit anderer Abhilfe, und "GPS offline"
wuerde dort zur Signalsuche statt zum Freigabeschalter schicken.

VERSIONIERUNG: die Zahl, die alle fuer "die Version" hielten, ist keine.
Der Integer in audi-dashboard-version.json ist ein Cache-Brecher, den
install.ps1/update.ps1 bei jedem Deploy mit UtcNow neu setzen -
unabhaengig davon, ob sich Code geaendert hat. Zwei Builds derselben
Quelle bekommen verschiedene Zahlen. Er kann die Frage "ist das derselbe
Stand?" grundsaetzlich nicht beantworten.

Deshalb beide Aufgaben getrennt: neue Datei VERSION im Projektstamm
(2026.08.23.1) als Identitaet, von Hand erhoeht; der Integer bleibt
unveraendert der Cache-Brecher. VERSION fliesst in beide Seiten - als
zweites Feld "app" in audi-dashboard-version.json (alle drei
Deploy-Skripte uebernehmen es jetzt; sie haben die Datei bisher komplett
ueberschrieben und haetten es still zerstoert) und ueber vite define als
__APP_VERSION__ in den Companion-Build. Das Backend veroeffentlicht
pyscript.audi_dashboard_app_version, die App vergleicht und meldet eine
Abweichung in der Hinweisleiste - deren erklaerter Grundsatz "nie eine
stille Veraltung" genau dieser Fall ist, nur dass hier nicht die Anzeige
veraltet, sondern die App selbst.

Bewusst nur Gleichheitsvergleich, nie groesser/kleiner: die Version ist
eine Kennung, keine Zahl; Sortieren waere scheingenau und wuerde bei
einem Formatwechsel still falsch antworten. Fehlt eine der beiden
Seiten, wird nicht verglichen und nichts gemeldet - ein aelteres Backend
oder ein Start ohne Netz darf keinen Fehlalarm ausloesen. Der
vite-Build bricht dagegen hart ab, wenn VERSION fehlt, statt eine App zu
erzeugen, die ihre eigene Veraltung nicht erkennen kann. Das Panel
braucht nichts davon: es laedt bei jedem Seitenaufruf neu.

Geprueft: Backend meldet zuendung: False und app_version 2026.08.23.1 im
Testcontainer, Panel zeigt statt "Fahrzeug faehrt" jetzt "Geparkt seit
13 Tg. 13 Std." und statt der alten Meldung "GPS-Freigabe fehlt";
VERSION landet nachweislich im Build (im Bundle gegriffen) und der Build
bricht ohne die Datei ab (gegengeprueft); tsc sauber, Tests 112/112,
vite build sauber, HA-Start ohne Fehler.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 21:59:14 +02:00
tobias 41f2a644d3 HACS-Entscheidung und die dabei aufgefallene Auslieferungsluecke festgehalten
HACS scheidet fuer den aktuellen Aufbau aus, gegen die installierte
Version 2.0.5 geprueft statt aus dem Gedaechtnis: HACS kann
ausschliesslich GitHub (fest verdrahtet, dieses Repo liegt auf Gitea),
und keine der sechs Kategorien installiert nach /config/pyscript/ - die
Zielpfade sind custom_components/, www/community/, python_scripts/,
themes/, custom_templates/ und appdaemon/apps/. Die App braucht aber
pyscript/, audi_dashboard/ und Eintraege in der configuration.yaml.

Einziger Weg waere die Konvertierung des pyscript-Backends zu einer
echten custom_component - die nebenbei panel_custom, das www-Kopieren,
die data-Umbenennung, allow_all_imports/hass_is_global und die
pyscript-Abhaengigkeit selbst erledigen wuerde. Umfang gemessen: 2.911
Zeilen, 21 Dienste, 15 State-Trigger, 10 Zeit-Trigger. Der Besitzer hat
entschieden, das bis nach der ersten echten Inbetriebnahme
zurueckzustellen.

Dabei aufgefallen und ebenfalls festgehalten: die Paritaetsregel sichert
Quell-Paritaet, nicht Auslieferungs-Paritaet. Das Panel holt sich
audi-dashboard-version.json bei jedem Seitenaufruf und ist damit sofort
aktuell; die Companion-App ist eine Capacitor-Huelle mit gebuendelten
Assets, package.json sagt 0.1.0 und in src/ prueft nichts jemals eine
Version. Die iOS-App kann also wochenlang hinterherhinken, ohne dass es
irgendwo sichtbar wird. Loesungsskizze im Abschnitt notiert, noch nicht
gebaut - die Entscheidung ueber den Auslieferungsweg steht aus.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 21:36:30 +02:00
tobias 3b99f1f332 Vergangene Daten aus dem HA-Verlauf importierbar, Auslieferstand bereinigt
Datenaufbewahrung (recorder_snippet.yaml, neu): Home Assistant loescht
Sensor-Verlaeufe standardmaessig nach 10 Tagen - das war die stille
Obergrenze dafuer, wie weit sich ueberhaupt je etwas rekonstruieren
laesst, denn geloeschte recorder-Zeilen sind endgueltig weg. Der Block
hebt das auf 365 Tage. Die Bestaende der App selbst (fahrten.jsonl,
tankvorgaenge.jsonl, batteriespannung.jsonl, fahrzeugprofil.json) waren
davon nie betroffen, die kennen ohnehin keine Purge-Logik. Platzbedarf
an der Testinstanz gemessen statt geschaetzt: 90.858 Zeilen in 17 Tagen,
hochgerechnet grob 1,9 Mio. Zeilen und 1-1,5 GB im Jahr, davon 96 % aus
der EU-Data-Act-Integration - dafuer liegt eine auskommentierte
exclude:-Liste bei. Bewusst kein include:-Block, der wuerde jeder
anderen Integration den Verlauf nehmen und dabei kaum Platz sparen.

Historienimport (pyscript/historienimport.py, neu): neuer Dienst
audi_dashboard_historie_importieren(start, ende) liest denselben
Verlauf, den die Live-Trigger in Echtzeit sehen, und leitet rueckwirkend
dieselben Datensaetze ab - Fahrten aus dem Zuendungsverlauf (inklusive
der Pausenregel, kurze Unterbrechungen verschmelzen zu einer Fahrt),
Tankvorgaenge nach derselben Tiefststand-Logik und denselben Schwellen
wie tankerkennung.py, Tagesmin/-max der Batteriespannung. Mehrfach
ausfuehrbar: ueberschneidet sich eine Fahrt mit einer bereits erfassten,
wird sie uebersprungen statt doppelt angelegt. Erzeugte Datensaetze
tragen source: "import".

Gelesen wird ueber HAs eigene recorder-API (get_significant_states),
nicht per direktem SQL auf home-assistant_v2.db: das Schema ist
HA-intern und aendert sich zwischen Versionen, die Funktion ist die
stabile Schnittstelle. Preis dafuer ist hass_is_global: true im
pyscript-Block; ohne die Zeile laeuft alles andere unveraendert weiter,
nur der Import meldet, dass er den Verlauf nicht lesen kann.

Oberflaeche in beiden Codebasen: Knopf "Daten importieren aus Home
Assistant" unter Einstellungen -> Einrichten, dahinter ein Fenster mit
Von/Bis (Vorbelegung: letzte 30 Tage), "Importieren" als Hauptaktion und
"Abbrechen" als zweite Wahl. Das Fenster bleibt offen und zeigt das
Ergebnis, statt optimistisch zu schliessen - hier ist das Ergebnis der
Zweck des Aufrufs. Fortschritt ueber die Entitaet
pyscript.audi_dashboard_import_status, weil pyscript-Dienste sofort
zurueckkehren. In companion-app bewusst NICHT ueber die
Offline-Warteschlange: ein spaeter aus dem Nichts abgefeuerter
Importlauf waere fuer den Nutzer nicht nachvollziehbar.

Beim Testen gefunden und mitgefixt: profil.batterieverlauf_tageswert_
aktualisieren() haengte in Einfuegereihenfolge an. Solange nur die
Live-Aufzeichnung schrieb, war das dasselbe wie sortiert (sie traegt
immer den heutigen Tag ein) - der Import traegt vergangene Tage nach,
die waeren hinter den neueren gelandet und das Diagramm haette zeitlich
rueckwaerts gelaufen. Sortiert jetzt nach Datum, wie der eigene
Docstring es ohnehin versprach.

Auslieferstand bereinigt: einstellungen.py brachte zwei Entity-IDs einer
laengst abgeraeumten Testinstanz mit (testzone_fmm003_...). Beim Testen
unsichtbar, weil die Testinstanz sie ueber entitaeten.json ueberschreibt
- auf einer Neuinstallation waeren sie die wirksamen Werte gewesen. Und
schlimmer als nur wirkungslos: fahrterkennung.py registriert seinen
@state_trigger nur, wenn das Feld belegt ist, ausdruecklich als Schutz
gegen eine leere Entity-ID. Ein gesetzter, aber nicht existierender Wert
hebelt genau diesen Schutz aus. Beide Felder jetzt leer wie alle
anderen; Kopfkommentar der Datei ueberarbeitet, er behauptete noch, die
EU-Data-Act-Integration werde nicht mehr verwendet.

install.ps1 abgesichert, da die erste echte Installation auf HA OS
ansteht: die feste configuration.yaml.bak wurde bei jedem Lauf
ueberschrieben, ausgerechnet das Original war nach dem zweiten Lauf weg
- Sicherungen jetzt zeitgestempelt. Nach dem Schreiben wird die Datei
zurueckgelesen und geprueft (Laenge, genau ein Markerblock, bisheriger
Inhalt unveraendert); bei der kleinsten Abweichung rollt das Skript
automatisch zurueck. Die Sicherheitseigenschaften stehen jetzt im Kopf
der Datei, statt dass man ihnen glauben muss.

Standort-Blatt: "Teilen" war im Tagmodus unsichtbar - .standort-pille
nutzte var(--tile), das Blatt darunter var(--tile-deckend), und die
iOS-Auflage setzt im Tagmodus beide auf #FFFFFF. Gemessener Kontrast
1,00:1, weiss auf weiss. Beide Pillen folgen jetzt der Knopfsprache des
Panels (.aktion / .aktion.primaer): Umriss fuer die Nebenaktion, var(--fg)
gefuellt fuer "Route". Danach 17-21:1 Textkontrast in beiden Themes.
Nebenbei ist damit --line-strong - eine Linienfarbe - nicht laenger als
Knopffuellung im Einsatz.

Geprueft: Import zweimal ueber die echte Oberflaeche im Browser gegen
eine in die recorder-DB eingespielte Kunsthistorie (der Testcontainer
laeuft nicht, waehrend das Auto faehrt, echte Fahrten liegen dort also
nicht vor) - drei Fahrten wie erwartet, die 5-Minuten-Unterbrechung
korrekt zu einer 50-Minuten-Fahrt verschmolzen, die 30-Sekunden-Zuendung
verworfen, Strecken kilometergenau; zweiter Lauf legte 0 an und meldete
3 als vorhanden. Neuinstallation in einem Wegwerf-Container: nur
Profilvorlage und Parser, keine Fahrten-/Tank-/Batteriedatei, null
Fehler im Log. install.ps1 gegen Attrappen: -Pruefen schreibt nichts,
echter Lauf laesst automations.yaml bytegleich (SHA-256) und fremde
Bloecke stehen, Ergebnis parst als gueltiges HA-YAML, zweiter Lauf
idempotent, bei fremdem pyscript:/panel_custom: bleibt die Datei
bytegleich. tsc --noEmit sauber, companion-app-Tests 106/106, vite build
sauber, HA-Configcheck und Start ohne Fehler.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 20:42:54 +02:00
tobias f115af50ab Versicherung/Steuer: Wortlaut-Feinschliff und Vertrag komplett editierbar
Mein-Audi-Kachel "Versicherung/Steuer" (vAudi()) und Kfz-Steuer-Detail
(vSteuer()): "Zusammen" heisst jetzt "Summe", die Fusszeile "feste
Kosten im Jahr" darunter entfaellt ersatzlos. Die Kfz-Steuer-Zeile
zeigt "im Jahr" statt des rohen Datenwerts "jaehrlich", deckungsgleich
mit der Versicherungs-Zeile darueber - nur wenn steuer.zeitraum
tatsaechlich "jaehrlich" ist. Der gespeicherte Wert und die
"jaehrlich"/"halbjaehrlich"-Auswahl in vSteuerBearbeiten() bleiben
unveraendert, das war ein reiner Anzeige-Fix, keine Datenmodell-
Aenderung. companion-app hatte an keiner der beiden Stellen ein
Gegenstueck (kein "Zusammen"-Tile, keine Nur-Lese-Anzeige des
Zeitraums) - dokumentiert als vorbestehende strukturelle Luecke statt
kommentarlos uebersprungen.

Vertrag-Kachel: war komplett nur lesbar (Gesellschaft, Umfang,
Vertragsnummer, Selbstbeteiligung, Schadenfreiheitsklasse). Neue
vVertragBearbeiten()-Ansicht (Panel, Route "vertrag") ueber ein neues
Zahnrad auf der Vertrag-Kachel - alle Felder editierbar,
Selbstbeteiligung zusaetzlich mit Hinzufuegen/Loeschen pro Zeile (neue
.zeile-loeschen-Klasse, 44x44pt-Tippflaeche wie .km-edit). Die
aktuelle Schadenfreiheitsklasse bleibt bewusst nur ueber "Beitrag
anpassen" editierbar (Querverweis statt Duplikat), nur "zuvor" (sfAlt)
ist neu in der Vertrag-Ansicht.

companion-app: neue Vertrag()-Seite (Route "vertrag", eigene
NaviKachel auf der Versicherungs-Seite) nach dem dortigen etablierten
Muster (lokaler Entwurf-State + expliziter Speichern-Knopf statt
Panel-Autosave pro Feld - gleiches Verhalten, eigenes Idiom). Die
bisherige Nur-Lese-Selbstbeteiligung in Vertragsdetails() entfaellt,
da sie jetzt (editierbar) in Vertrag() lebt. Schadenfreiheitsklasse
wird in companion-app bewusst nicht ergaenzt: das Feld war dort noch
nie sichtbar, auch nicht lesend - eine komplette neue UI-Sektion dafuer
waere kein "Zeile editierbar machen" mehr, sondern ein neues Feature;
als Luecke dokumentiert statt still uebergangen.

tsc --noEmit sauber, companion-app-Tests 100/100 (inkl. dem
Alle-Seiten-Rendertest, der jetzt auch "vertrag" abdeckt), vite build
erfolgreich. Panel live im Docker-Testcontainer geprueft: Zeile
hinzufuegen/bearbeiten/loeschen, Daten bleiben nach Re-Render erhalten.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 16:50:58 +02:00
tobias 23b6defa68 Service-Termine: CI-Icons statt Wortlaut, Anstehende-Termine neu gegliedert
Naechster-Service-Kachel (Uebersicht): das "bis zum Oelwechsel"/
"bis zur Inspektion" vor der Kennzahl entfaellt zugunsten der echten
Audi-CI-Icons oil-change/inspection (vom Nutzer als SVG geliefert,
verbatim uebernommen wie die uebrigen CI-Icons). Hauptuntersuchung hat
kein eigenes Icon (kein Restkilometer-Ziel) und zeigt stattdessen ihr
Datum in derselben 40px-Groesse wie die km-Zahlen, mit dem Wort
"Hauptuntersuchung" darunter statt "bis zur Hauptuntersuchung" - vorher
war das Datumsfeld kleiner (30px), um Umbruch zu vermeiden; am
laufenden Panel nachgemessen, dass 40px keinen Umbruch verursacht.
"vsl." wird ausgeschrieben zu "voraussichtlich am". bisText()/ART_BIS
(Panel) und bisText()/BIS_TEXT (companion-app) wurden durch die
Aenderung ueberfluessig und als Orphans entfernt.

Anstehende-Termine-Box (Service-Seite): aus der dt/dd-Liste werden drei
sichtbar getrennte Bloecke (Kopfzeile mit Icon + Name + optionalem
Werkstatt-Chip, optionale Herstellervorgabe-Zeile, grosse Kennzahl,
optionale Prognose-Fusszeile) - Apple HIG (Hierarchie ueber
Position/Groesse, Inhalt nicht mit Nebensaechlichem ueberladen) plus
Audi-CI (Flaeche mit Haarlinien statt Karten). Durchlief drei
Mockup-Runden als Artefakt vor der Umsetzung, zwei davon vom Nutzer
zurueckgewiesen ("Major Information is missing" bzw. Kommentare zu
Icon-Groesse/Herstellervorgabe/Ausrichtung).

Herstellervorgabe-Logik korrigiert: eine Fahrzeugmeldung stammt immer
aus dem werkseigenen Wartungsprogramm des Bordcomputers, unabhaengig
vom in "Einrichten" gewaehlten App-Intervall - zaehlt also immer als
Herstellervorgabe, nicht nur wenn der App-Modus zufaellig "hersteller"
ist. Die Prognose-Fusszeile zeigt die Restkilometerzahl nur noch beim
eigenen (kuerzeren) Intervall, bei Herstellervorgabe nur noch das
Datum. "Eigenes" in der Ölwechsel-Intervall-Auswahl umbenannt zu
"Individuell".

companion-app-Portierung ist eine echte Neustrukturierung: die
bisherige "Naechster Oelwechsel"-Kachel mischte Oelwechsel-,
Inspektions- und Hauptuntersuchungs-Zeilen in einer gemeinsamen
Werteliste, jetzt "Anstehende Termine" mit denselben drei Bloecken wie
im Panel. Neuer Export letzterInspektion() in service.ts. Zwei
dokumentierte, vorbestehende Luecken bleiben bestehen statt neu
kaschiert zu werden: kein Werkstatt-Chip (companion-app hat keine
"Termin vereinbaren"-Funktion), Herstellervorgabe kommt aus
fahrzeug.oel.modus statt einem intervalle()-Aequivalent (das es dort
nicht gibt).

npm run typecheck sauber, npm run test 99/99, npm run build erfolgreich.
Panel deployed als Version 1787013003, synchron in installationspaket/,
live im Docker-Testcontainer per DOM-Abfrage geprueft.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 00:03:16 +02:00
tobias 2848b5286f Parity-Regel: Uebersicht-Service-Block in companion-app portiert
Nutzer stellte klar: Panel und companion-app sollen immer denselben
funktionalen Stand haben, auch wenn eine Anfrage nur das Panel nennt -
das ist reiner Testkomfort (Docker-Instanz laesst sich am schnellsten
ansehen), keine Scope-Entscheidung. Regel als verbindlicher Absatz in
AGENTS.md verankert, direkt unter der bestehenden Maintenance-Regel.

Erste Anwendung im selben Zug: die eben im Panel gebaute
"Naechster Service"-Kachel nach companion-app/ portiert.

- daten/service.ts: neues naechsterService() als Gegenstueck zu
  naechsterTermin() im Panel - waehlt ueber Oelwechsel-/Inspektions-
  Prognose und die von Hand gepflegte Hauptuntersuchung hinweg den
  zeitlich naechsten Termin. bisText() liefert denselben Artikel wie
  ART_BIS im Panel ("bis zum Oelwechsel" / "bis zur Inspektion").
- screens/Uebersicht.tsx: die bisherigen Kacheln "Reichweite"/
  "Kilometerstand" nebeneinander plus eine separate "Service"-Kachel
  mit Werteliste weichen einer Reichweiten-Kachel plus einer
  Service-Kachel mit .dm-serviceblock (Knopf, nur der obere Teil) und
  .dm-servicezeile (reine Anzeige) darunter - dieselbe Control/Content-
  Trennung wie im Panel.
- stile/screens.css: .dm-serviceblock/.dm-servicezeile ergaenzt.

Tests: 4 neue in service.test.ts (naechsterService waehlt das frueher
faellige Datum, nimmt die Hauptuntersuchung auf, liefert nichts ohne
Servicebuch/HU, bisText-Artikel je Art), 1 neuer in screens.test.tsx,
der mit einem vi.fn() als geheZu wirklich belegt, dass ein Klick auf
den Serviceblock navigiert und ein Klick auf die Kilometerstand-Zeile
es nicht tut - dafuer bekam zeige() in screens.test.tsx erst einen
injizierbaren geheZu-Parameter (vorher hart auf () => {} verdrahtet).

npm run typecheck sauber, npm run test 100/100 (von 95), npm run build
erfolgreich.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 20:44:31 +02:00
tobias 600c92fd0f Service-Kennzahl als Kachel mit Hintergrund statt flach
Nutzerwunsch nach dem ersten Stand: der Block soll als Box mit Hintergrund
erscheinen. .tile.flat -> .tile; die Haarlinie trennt jetzt innerhalb der
Kachel, statt die Kachel zu ersetzen.

Der Chevron bleibt unveraendert auf right:0 relativ zum Knopf - da der
Knopf innerhalb des Kachel-Paddings sitzt, landet er damit automatisch auf
demselben Innenabstand wie ein normales .go in einer gepolsterten Kachel,
ohne Sonderregel.

Neu: .serviceblock:active{opacity:.55} als Tipp-Rueckmeldung. Ein
Hintergrundwechsel wie bei .tilebtn geht hier nicht, weil der Knopf schon
auf der Kachelflaeche liegt und nur ihr oberer Teil ist.

Beide Themes geprueft: Kachelhintergrund und Haarlinie kommen aus Tokens
(Tag rgb(255,255,255) / Nacht rgba(255,255,255,.055)), der Knopf selbst
bleibt transparent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 20:36:01 +02:00
tobias e56f2040e8 Uebersicht: fuehrende Service-Kennzahl statt zwei gleichrangiger Kacheln
Kilometerstand und Service standen als zwei identisch aussehende Kacheln
nebeneinander - 166x113px, gleiche 29px-Zahl, gleiche Farbe -, obwohl nur
die rechte ein Knopf war (erkennbar allein an einem 12px-Chevron) und die
beiden Zahlen Grundverschiedenes meinen: ein gemessener Ist-Wert gegen
einen taeglich schrumpfenden Countdown.

Neu, nach Abstimmung mit dem Nutzer und belegt an beiden Regelwerken
(Apple HIG "Layout"/"Widgets" live gelesen, Audi-CI aus dem
projekteigenen AUDI_CI_ARCHIV):

  Naechster Service:                  >
  9.029 km
  bis zum Oelwechsel · vsl. 15.04.2027
  ------------------------------------
  Kilometerstand             20.845 km

Das .duo-Raster entfaellt; der Block liegt in einer .tile.flat, also in
derselben Ebene wie die Reichweite darueber, statt in einem eigenen
Kachelcontainer. Der Chevron sitzt nur am .serviceblock-Knopf, nicht ueber
der ganzen Flaeche - damit bleibt erkennbar, welcher Teil bedienbar ist
und welcher reine Anzeige (HIG: "differentiate controls from content").

"Naechster Service" meint jede Serviceart; naechsterTermin() waehlte
ohnehin schon ueber Oelwechsel/Inspektion/Hauptuntersuchung aus, neu ist
die Beschriftung samt ART_BIS fuer den richtigen Artikel ("bis zum
Oelwechsel", aber "bis zur Inspektion"). Ohne Restkilometer
(Hauptuntersuchung) rueckt das Datum selbst ins Zahlenfeld, dann bei 30px
statt 40px.

Von der Aenderung verwaist und mitentfernt: mmjjjj() und die
.duo-Regeln in beiden Stylesheets.

Live geprueft bei 375px und 1280px: Block und Wertezeile fluchten mit der
Kachelkante, Chevron buendig rechts, Tippflaeche 88px; Klick auf den Block
oeffnet "Service", Klick auf die Kilometerstand-Zeile navigiert nicht.
Keine neuen Konsolenfehler.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 20:34:10 +02:00
tobias 7319501ee6 Service-Seite: Prognose-Fussnote ueber volle Breite statt umgebrochen
Gemeldet: das Datum der Oelwechsel-Prognose steht in einer eigenen Zeile.
Nachgemessen: das <small> in der rechten Spalte war 29px hoch bei 217px
Breite (zwei Zeilen); .row ist ein Flex aus dt/dd, und dd bekommt nur den
Rest neben dem Titel - fuer "9.029 km, voraussichtlich am 14.04.2027"
reicht das bei 12,5px nicht.

Statt an Schriftgroesse oder Text zu drehen, haengt die Fussnote jetzt als
eigenes Element (.row-fussnote) neben dt/dd und bricht per flex:1 0 100%
auf eine eigene Zeile ueber die volle Kachelbreite um. flex-wrap:wrap ist
per :has(.row-fussnote) auf genau diese Zeilen eingegrenzt, damit die
uebrigen .row-Verwendungen zweispaltig bleiben.

Die verschachtelte fm-/nicht-fm-Verzweigung im Markup faellt dabei zu
einem hatMeldung-Zweig fuer den Wert plus einem separaten fussnote-String
zusammen; die Anzeigelogik selbst ist unveraendert.

Live geprueft bei 375px und 1280px: beide Fussnoten einzeilig (14px),
307px bzw. 636px breit; alle 17 .row auf "Mein Audi" weiterhin
zweispaltig; keine neuen Konsolenfehler.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 19:08:06 +02:00
tobias 7fc378f427 Übersicht: Wert und Kontext trennen statt "9,0 tkm/ 04/27"
Auf Nachfrage, wie Apple HIG und die Audi-CI die beiden Kacheln loesen
wuerden - beide laufen auf dieselbe Antwort hinaus.

Die Oelwechsel-Kachel presste zwei verschiedene Groessen in ein
Zahlenfeld: Restkilometer und Prognosedatum, getrennt durch einen
Schraegstrich. Zwei Einheiten, davon eine erfundene ("tkm" schreibt
niemand - die Mein-Audi-Servicebox nannte dieselbe Zahl schon immer
"9.029 km"), zwei Schraegstriche mit zwei Bedeutungen, und auf 375px
zweizeilig umgebrochen.

Apple HIG "Layout" ("don't obscure essential information by crowding it
with nonessential details") und "Typography" (Hierarchie ueber Groesse
und Farbe) verlangen die Trennung; die Audi-CI definiert .fig
ausdruecklich als "large numeric figures" - eine Zahl, nicht zwei.

- Wert gross (9.029 km), Kontext leise darunter (vsl. 04/2027)
- Echte Einheit statt "tkm"
- Kachel ist jetzt ein Knopf mit Chevron zur Service-Seite; vorher eine
  Sackgasse, obwohl es die Seite gibt
- mmjj() dadurch verwaist, ersetzt durch mmjjjj() mit vollem Jahr
- HU-/Inspektionszweig faellt mit dem Oelwechselzweig zusammen, weil sie
  nach der Trennung dieselbe Form haben

companion-app hatte diesen Fehler nicht (dort standen die Termine schon
als Label/Wert/Zusatz). Dort stattdessen der benachbarte
Gruppierungsfehler behoben: die Service-Zeilen hingen innerhalb der
Kilometerstand-Kachel und fuehrten nirgendwohin - jetzt eigene,
antippbare Kachel zur Service-Seite.

Live geprueft im Panel (Tag und Nacht, 375px): beide Kacheln einzeilig,
Tippen oeffnet Service. companion-app: typecheck sauber, 95/95 Tests,
Build erfolgreich.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 13:54:27 +02:00
tobias edf3845375 companion-app: catch up six days of panel drift before the native build
The phone-app codebase (companion-app/) hadn't been touched since
2026-08-11 - every panel fix and feature since then (HIG audit rounds,
receipt upload, trip editing, Inspektion forecast, ...) existed only in
the HA panel. Found while auditing what's needed for a real iPhone
build; user decision was to port everything now rather than ship a
stale app.

Six real, verified gaps (not blind copies of panel CSS/markup, which
doesn't transfer to the @audi-dash/ui component set):

- Battery voltage cutoff was still 13.2V, not the panel's 12.8V fix.
- Dead wlan_name field (WLAN trip detection was fully removed from the
  backend 2026-08-12) - removed from the adapter and the settings screen
  instead of leaving a form field that silently does nothing.
- No Inspektion forecast - added inspektionPrognose() alongside the
  existing oelwechselPrognose(), sharing a refactored core.
- Arbeitsweg pill used the design-system's "work" variant, which is
  documented as recoloring to red - stopped passing it, same fix as the
  panel.
- Trip creation only took Beginn/Ende/Art; extended with Startort/
  Zielort/Kilometerstand/Distanz via a new shared FahrtFelder.tsx.
- FahrtDetail.tsx was read-only - added an edit mode using the same
  shared fields, backed by a new DataMetricApi.fahrtAktualisieren()
  calling the backend service built earlier this session.

Explicitly checked and found not applicable: price rounding (already
2 decimals here), pull-to-refresh CSS (no native gesture to fix),
the panel's drag/paste receipt dialog (solves a desktop-browser problem
this native app doesn't have - the plain file picker already gets iOS's
native Files integration), and the purely cosmetic panel CSS fixes.

npm install run at the repo root (node_modules was incomplete/stale),
package-lock.json reflects the real dependency tree. Verified with
npm run typecheck (clean), npm run test (95/95, up from 90 - added
interaction tests for the new form and inspektionPrognose), and
npm run build (succeeds).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 12:16:40 +02:00
tobias 188dc8247e AGENTS.md: record that companion-app has drifted 6 days behind the panel
Found while auditing what's needed for a real iPhone build: src/screens/
hasn't changed since 2026-08-11, so none of the panel work since then
(HIG audit rounds, receipt upload, trip editing, Inspektion forecast,
Heckklappenschloss removal, ...) exists in the phone app yet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 11:55:41 +02:00
tobias 126033e4dd Fahrt-Formular: Ankunftszeit statt Dauer, Art gross, neutrale Pille
Fuenf gemeldete Punkte plus ein Theme-Bug beim Nachpruefen:

- "Dauer" ist kein Eingabefeld mehr - stattdessen Ankunftszeit, die Dauer
  wird aus Start/Ankunft berechnet und live angezeigt (fahrtZeitraum()/
  dauerText()), inklusive Fahrten ueber Mitternacht.
- Art-Werte werden grossgeschrieben angezeigt (Privat/Arbeitsweg), der
  gespeicherte Wert bleibt klein (artText()-Helfer).
- Arbeitsweg-Pille ist nicht mehr rot - sieht jetzt aus wie Privat.
- "tippen zum Umschalten" unter der Pille entfernt.
- Bild-Platzhalter (Einstellungen) war im Tag-Theme unsichtbar: --tile wird
  dort vom iOS-Overlay auf reines Weiss gesetzt, identisch mit Canvas/
  Kacheln. Auf --ios-fill umgestellt, in beiden Themes sichtbar grau.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 11:49:55 +02:00
tobias 2d76f10c1c Fahrten bearbeitbar, Inspektionsprognose, Bootstrap-Takt reisst nicht mehr ab
Sechs gemeldete Punkte plus ein Fund beim Nachlesen des Ladevorgangs:

- HECKKLAPPENSCHLOSS_SENSOR ueberall entfernt (einstellungen.py,
  entitaeten.py-Katalog, _sicherheitscheck()). HECKKLAPPE_SENSOR bleibt.
- Bild-Platzhalter in den Einstellungen war halb abgeschnitten: die bare
  .carfix-Regel der Desktop-Container-Query traf auch den Platzhalter-Div,
  dessen inset:0 von der festen Hoehe uebersteuert wurde. Auf .bildbox
  eingegrenzt.
- Inspektion bekommt eine eigene "voraussichtlich am ..."-Zeile (nur Datum).
  oelwechselPrognose() dafuer auf den gemeinsamen Kern servicePrognose()
  zurueckgefuehrt.
- "Neue Fahrt" bietet jetzt alle Felder, die die Einzelfahrt anzeigt, und die
  Einzelfahrt laesst sich bearbeiten - gemeinsames fahrtFelder()/
  fahrtFormularWerte() nach dem Muster von tankFelder(). Backend: Service
  audi_dashboard_fahrt_aktualisieren, profil.fahrt_bearbeiten() (edited_fields
  schuetzt gegen die Automatik, nicht gegen den Nutzer). Die Art-Pille
  speichert jetzt ebenfalls statt nur lokal umzuschalten.
- Titelzeile steht auf dem Handy buendig zu den Kacheln: .back:not(.on) gibt
  seine Breite und den Gap auf allen Breiten frei, .topbar links 16px.
- Ladebildschirm: nachladeAnstossen() und datenLaden() stiegen bei fehlendem
  HASS aus, ohne eine naechste Runde zu planen - genau die Sackgasse, aus der
  bisher nur ein Menueklick half. Beide planen jetzt weiter, der Timer wird
  beim Abhaengen gestoppt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 11:40:36 +02:00
tobias 91dfd4807a Drop the Strg+V hint: PC gets drag-and-drop and file picker only
Ctrl+V does not reach the dialog either - Chrome only delivers a paste
gesture when there is a paste target, and the dialog has no input field.
So the shortcut is no longer mentioned anywhere; the computer now offers
exactly two routes, dragging and the file picker, and the phone keeps the
clipboard button. The paste listener stays as a silent bonus for browsers
that do deliver the event, and the now-orphaned tastenkuerzelEinfuegen()
is gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 00:44:05 +02:00
tobias e5e82b3634 Fix clipboard paste on PC, round price per litre to 2 decimals
The "paste from clipboard" button could never work on a desktop:
clipboard.read() only ever exposes text, HTML and images by spec, so a
file copied in Explorer is not offered through it - and the dialog then
wrongly blamed the file for not being a PDF. It is now hidden on
pointer-coarse devices only, where it does work; on a PC the drop zone
names Strg+V, which is the path that carries the actual file.

Detection also relied solely on the MIME type, but a file copied or
dragged from Explorer can arrive with an empty type and was rejected -
istPdf() now falls back to the extension.

Price per litre now shows 2 decimals everywhere (list, receipt detail,
both entry forms, CSV) instead of 3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 00:38:55 +02:00
tobias 5aaf1cd958 Fix pull-to-refresh on touch devices, add drag/paste receipt upload
The pull-to-refresh gesture logic was already correct (verified by driving
real pointer events through it); what was missing was overscroll-behavior
on the scroll container, so on real touch hardware the browser's own
native overscroll could take the gesture over before it hit the threshold.

"Beleg hochladen" now opens a dialog instead of the native file picker:
drag a PDF in at the computer, paste it from the clipboard on the phone
(copied out of a mail), file picker kept as a third route. All three
sources share one upload path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 00:29:05 +02:00
tobias 8a751fe441 Run a full Apple HIG audit; fix silent save failures and popup polish
Read Tab Bars, Sidebars, Sheets, Alerts, Action Sheets, Toolbars, Buttons,
Pickers, Loading, Feedback, Privacy, and Undo/Redo off developer.apple.com
and cross-checked each against the actual code. profilSpeichern() (the
shared save path for ~25 fields) had no error handling at any call site,
so a failed backend write was an unhandled promise rejection with zero
user feedback - fixed centrally, plus the same for serviceRufen(). Also
added a spinner to the "Ladt ..." bootstrap screen and gave the
Anzugsmoment/km-correction popups the same primary-button styling the
Setup popup already used.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 23:53:02 +02:00
tobias 6bf7435434 Add real Audi CI battery/edit icons, manual km-correction for tire sets
Replaced the hand-drawn placeholder battery icon with the real Audi CI
battery-12v-l/-s (user supplied the actual SVGs) next to "Ruhespannung",
matching the existing poi-l/poi-s size-variant convention.

Added a km-correction control to the Sommerräder/Winterräder tiles: a
pencil icon (Audi CI edit-s) opens an inline field to manually correct
the tire-set's accumulated km. The tricky part was persistence -
configCarZuProfil() deliberately never writes km back (a stale browser
copy could otherwise clobber a since-elapsed automatic increment), so
this needed its own backend service, audi_dashboard_reifen_km_setzen,
that overwrites only the stored counter and leaves referenz_odo_km
alone - the existing odometer-delta tracking in reifenzaehler.py then
continues accumulating from the corrected value on its own, no other
backend change required.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 23:16:08 +02:00
tobias 8b3e5b2b8f Rename Sommer/Winter to Sommerräder/Winterräder, reflow Reifen tile headline
The tile headline now reads "Sommerräder"/"Winterräder" and renders as an
actual headline (new .tile-headline class, 20px/var(--fg)) instead of the
small muted .label styling used for other in-tile section titles.

Headline + km now sit above "Montiert" instead of the wheel photo
overlaying it: new .radkopf-row (flex) puts headline/km and the photo
side by side at the tile's top; Montiert dropped position:absolute (no
longer needed once the photo stopped sharing its space) and is now a
normal-flow pill below the headline block.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 21:00:47 +02:00
tobias ec55e5808e Fix real tab-bar-icons-vanish bug, dropdown popup color, wheel/Montiert swap
The reported "whole tab bar disappears when hiding labels" bug was real,
just one level deeper than first checked. .tabbar.ohne .tab span{display:
none} targeted the label span, but the icon is also wrapped in a <span
class="tabpille"> - a bare "span" type selector doesn't care about class,
so the icon's own wrapper collapsed too. Confirmed by measuring the <svg>
directly (0x0 bounding rect) after a live screenshot showed all 5 tab
buttons empty. Fixed with :not(.tabpille) in both stylesheets.

The Modell dropdown was still bright despite the color-scheme hint from
the last commit: a <select>'s opened option list is rendered by the
browser/OS as its own surface, entirely outside the page's paint tree -
confirmed when a screenshot call hung 30s trying to capture it open.
color-scheme only gets partial credit there; explicit background-color/
color on <option> is what Chrome/Firefox/Edge actually honor for the
popup rows. Added that, verified via getComputedStyle since the open
popup itself can't be screenshotted by this tooling.

Swapped the wheel photo (now right-aligned via margin-left:auto on the
now-correctly-square .bildbox.radbild) and Montiert (already left) per
request.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 20:52:46 +02:00
tobias 17b25d492a Fix wheel-photo squareness bug, native select popups, image-cycle crash
.bildbox.radbild lost to a same-specificity, later-declared .bildbox rule,
so the wheel photo stayed a full-width 4:3 box despite the earlier crop
fix - only which slice showed ever changed. Fixed with a compound selector
and moved "Montiert" to top-left to match the now-correctly-sized photo.

Native <select> popups had no color-scheme hint and rendered in the
browser's default light palette regardless of the app's dark theme -
this is what made "Modell" and other dropdowns flash bright white.

bildWeiter() (Mein Audi image-cycle tap) referenced a CSS class that was
never emitted (.platzhalter-datei vs. the real .platzhalter-aktion),
throwing and aborting before the page-dot indicator update - the photo
advanced but the dots never moved.

Merged the separate "Fahrzeugbilder" upload grid into "Bild der
Übersicht": pick a view from the existing dropdown, tap its preview to
upload/replace/delete - same generic popup plumbing, no new mechanism.
Removed the now-dead BILDER_UPLOAD_SLOTS/.bildgrid/.bildslot/.carfix.mini.

Back arrow changed from red to the neutral headline color, matching every
other navigation-color decision in this project.

Investigated but could not reproduce: user-reported "whole tab bar
disappears" when hiding labels. Traced the exact commit that added the
icon+label highlight and confirmed it's correctly scoped to :not(.ohne);
live DOM/computed-style testing across toggle, navigation, and both
mobile/desktop widths showed the tab bar staying visible throughout.
Left unchanged pending a repro from the user.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 20:40:14 +02:00
tobias 367985ba44 UI polish batch + fix genuinely-invisible map pins, Apple HIG audit
Battery voltage stat cutoff 13.2V -> 12.8V (values above that are already
alternator output, not the battery); fixed the battery chart's Y-axis
"stretching" on zoom by computing it once from the full dataset; removed
two explanatory paragraphs from the battery detail view; Tankfuellung now
shows "X % / X l"; removed the "noch etwa X l im Tank" suffix; fuel bar
color now matches the headline instead of a red/yellow/green gradient;
active/primary buttons are grey again, not red (destructive stays red per
HIG); select fields gained an Apple-HIG pull-down chevron (appearance:none
had removed the native one with nothing replacing it); selected menu icon
is white on desktop, matching mobile; Reifenfoto crops from the right edge;
"Montiert" button rebuilt as a compact top-right pill with a 44px invisible
tap target; tab-bar active highlight now covers icon+label, not just the
icon; removed two leftover explanatory texts from Einstellungen.

Root-caused the map pins looking "transparent": the CI poi-car/poi icons
are pure contour forms (nonzero-fill-rule ring + thin detail lines, only
13-40% of their own bounding box actually filled). Fixed by extracting each
icon's own first sub-path (the true outer balloon silhouette, verified by
rasterizing it in isolation) as a solid-filled layer behind the original
icon, applied to both the vehicle and station markers.

Read Color/Typography/Layout/Buttons/Materials on developer.apple.com and
applied the user-approved subset (11px text floor, 44px tap targets);
declined items (font weight, materials) documented with rationale.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 19:05:54 +02:00
tobias 899b770cfa Einzelbeleg: CI poi pin from the receipt address, plus a centre button
The station marker was a red circleMarker. It is now the CI poi pin
(poi-l/poi-s via tankstellenMarkerSVG) - the plain pin rather than poi-car, so
the vehicle and the station stay distinguishable on a map.

While testing it turned out nothing in the backend ever writes station_lat /
station_lon, so that marker had never been reachable in practice. The receipt
address is now geocoded in the frontend via Nominatim /search
(adresseAufloesen, the forward counterpart to the existing reverse lookup),
with results cached in localStorage - negative ones too, since a station
address does not move and Nominatim's terms ask for caching. Verified live:
"Shell, SÖLDEN" resolves to 46.9756 / 11.0111.

The map card gained a centre button (CI.gps, which is byte-identical to the
delivered gps-s.svg), disabled until the coordinates arrive. .mapbox needed
position:relative so the control anchors to the card instead of an ancestor.
Only in the Einzelbeleg, as requested - the Standort fullscreen already has
its own centre controls.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 13:15:20 +02:00
tobias fb38c297bd Station name as "brand, street, city", grey box on all editable fields
The "grey box around the icons" report turned out not to be a defect: the box
on "10.000 km"/"1 Jahr"/"15" is the wanted pattern, and every value the user
can change should carry it. All .feld inputs and selects now get the grey iOS
fill; inputs with their own visual language (checkbox/radio/range/file) keep
the transparent base rule, so the switches are unaffected.

Fuel stations are now displayed as "Shell, Pascalstr. 8, Ingolstadt" instead of
the bare operator name. New _marke()/_ist_strasse()/_tankstelle() helpers are
shared by both parser paths so Shell and non-Shell receipts format the same.
The brand is only matched against the receipt header - searching the whole text
would let "Total" hit a totals line. Missing parts are dropped instead of
leaving empty comma slots, and without a known brand the operator name takes
its place. station_address still carries the full street and postcode.
test_bekannte_stationen updated to the new format; suite stays green.

installationspaket/ is versioned from now on (user request). It contains no
real vehicle data - only the example profile with empty FIN/plate placeholders.
Keep it in sync whenever pyscript/ or www/ changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 12:52:31 +02:00