Files
audi-app/AGENTS.md
T
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

89 KiB
Raw Blame History

AGENTS.md — Project state, review findings, open items, and working rules

Last updated: 2026-08-16 (merged the umsetzung-datametric360 branch — companion app phases 110 done, see UMSETZUNGSPLAN.md; 2026-08-13: cleaned up remaining EU Data Act residue, fixed oversized toggle switches, and fixed a desktop-layout audit (settings button / rings logo / popups overflowing past the capped content column) — see DESIGN_AUDIT_2026-08-13.md; 2026-08-16: fixed five user-reported bugs — removed Türschloss/Haubenschloss checks, fixed a translucent confirm- sheet, added a flespi-style split lat/lon location source, hardened the "Lädt …" bootstrap race, and added a generic (station-independent) fuel-receipt parser fallback; later the same day: removed the now-unused combined STANDORT_TRACKER field entirely (flespi never provides it), closed a real silent-failure gap in the receipt-upload error path, and replaced the vehicle's map marker with a two-tone pin+car icon — see section C; a third round the same day found and fixed the long-standing "fragmented Leaflet rendering" defect at its root — the Leaflet stylesheet was loaded into document.head and therefore never reached the panel's shadow root — plus light-only map tiles, the CI poi-car marker, the red frame on list rows, the select-field grey box, and two more generic-receipt-parser gaps; a sixth round the same day fixed the map pins' real visibility problem (the CI poi-car/poi icons are thin outline-only paths, only 13-40% filled — solved with a solid silhouette layer extracted from each icon's own outer contour, not a redraw), applied a batch of ~20 user-reported UI polish items against Apple's Human Interface Guidelines, some via a guided Q&A, and lowered the battery-voltage statistic's cutoff from 13.2V to 12.8V; a seventh round the same day fixed a real CSS specificity bug that kept the wheel photo a wide 4:3 box instead of the intended 96x96 square, moved the "Montiert" pill to match, added color-scheme so native <select> popups stop rendering in the browser's default light palette against the dark app, root-caused and fixed a JS crash in the "Mein Audi" image-cycle click handler, merged the separate "Fahrzeugbilder" upload grid into "Bild der Übersicht" (pick a view, tap its photo to upload), and turned the back arrow from red to the neutral headline color — see section C). This file is the entry point for every new agent session: what this repo is, what is finished, what is missing, and how to work here. Detail lives in the linked documents — this file points, it does not duplicate.

Maintenance rule (binding): whenever you change this repo in a way that affects anything recorded here — completing an open item, making or reversing a decision, adding/removing a component, discovering a new gap — update this file in the same session (tick the checkbox, adjust the status table, bump the "Last updated" date). This file must always be written in English, even though the rest of the project is German.

How this file is loaded: Claude Code does not read AGENTS.md natively; the root CLAUDE.md imports it via @AGENTS.md (official recommended pattern). Other agents (Codex, Cursor, Copilot) read AGENTS.md directly. Edit content here, not in CLAUDE.md.


Working rules

Karpathy guidelines (adopted as project standard)

From multica-ai/andrej-karpathy-skills, behavioral rules derived from Andrej Karpathy's observations on LLM coding failures. They bias toward caution over speed — for trivial tasks, use judgment.

  1. Think before coding. Don't assume. Don't hide confusion. Surface tradeoffs. State assumptions explicitly; if multiple interpretations exist, present them — don't pick silently. If a simpler approach exists, say so. Push back when warranted. If something is unclear, stop and ask.
  2. Simplicity first. Minimum code that solves the problem. Nothing speculative: no features beyond what was asked, no abstractions for single-use code, no unrequested configurability, no error handling for impossible scenarios. If 200 lines could be 50, rewrite.
  3. Surgical changes. Touch only what you must. Don't "improve" adjacent code, comments, or formatting; don't refactor what isn't broken; match existing style. Remove only orphans your change created; mention (don't delete) pre-existing dead code. Every changed line should trace directly to the request.
  4. Goal-driven execution. Define success criteria, loop until verified. "Fix the bug" → write a test that reproduces it, then make it pass. For multi-step tasks, state a brief plan with a verify step per item.

Claude Code practices (current guidance, 2026)

  • Keep this file concise and specific — instructions concrete enough to verify ("run X before Y"), markdown headers/bullets, no dense prose. Context files reduce adherence as they grow; target roughly ≤200 lines of actual instruction.
  • Don't record here what the code or git history already shows (directory dumps, dependency lists) — record pitfalls, rationale, decisions, and conventions that differ from defaults.
  • Conflicting instructions get picked arbitrarily — when updating, remove superseded rules rather than stacking corrections.
  • Verify loading with /context (file must appear under Memory files); edit via /memory.
  • This file (plus the session's task list) is the source of truth for what's done — not a leftover plan-mode file. A ~/.claude/plans/*.md file surfaced by the system as "not yet complete" can be stale: plan files aren't marked complete when their work finishes. Before claiming a feature is unbuilt (e.g. "the Setup menu isn't implemented yet"), check the [x]/[ ] items below and/or the task list — trust those over an old plan file's own claim about its status. (Caused a real mistake 2026-08-12: told the user the Setup menu wasn't built yet, when it had shipped hours earlier in the same project — see the "Setup menu" [x] entry further down.)

What this project is

Private replacement for the Audi connect plug & play app (discontinued end of 2026), for exactly one vehicle (Audi RS 4 Avant competition), single user, self-hosted on Home Assistant, access via Tailscale only. Read-only toward the vehicle — no remote control. Covers: vehicle status, trip log, fuel log with Shell receipt parsing, service forecasting, insurance/tax, tire management.

License rule (hard): Audi Type fonts, four-rings SVG, and model-badge SVGs are cleared only for this one private, unpublished installation. Therefore design-system/ is brand-free (it gets uploaded to Claude Design), while the HA panel and the future sideload-only app may use the real assets. Never mix these the other way around.

Reading order for a new session:

  1. This file (overview + open items)
  2. UMSETZUNGSPLAN.md — the step-by-step execution plan for all open items (13 phases with commands and acceptance criteria); when working on an open item, follow the plan's phase
  3. SPECIFICATION.md — authoritative for the HA panel, incl. §7 "Known Gaps"
  4. COMPANION_APP_ARCHITECTURE.md — authoritative for DataMetric360 (decided, phases 110 built)
  5. AUDIT_2026-08-10.md — accessibility/platform audit of the panel (partly done, rest below)
  6. bauauftrag.mdhistorical only; code has diverged (see SPECIFICATION.md §7)

The three projects in this repo

Area What Status
homeassistant/ HA panel (panel_custom): pyscript backend + vanilla-JS frontend finished, in use — to be replaced by the app
testumgebung/ Script that rebuilds a throwaway Home Assistant with the real backend new, reproducible
design-system/ React component library @audi-dash/ui, brand-free, feeds Claude Design done as a kit (20 components, 1,690 lines)
companion-app/ DataMetric360 — successor app (web, PWA, native iOS/Android via Capacitor, HA iframe); will replace the panel all 21 screens built and tested; runs natively on iOS with real data
design/ Export of the Claude Design draft for DataMetric360 🚧 interim and now behind the code — screens were derived from the old panel instead (owner's decision)

Root files: dashboard-muster*.html = original static prototype (superseded, reference only), bauauftrag.md/.html = original build brief (historical), DESIGN_BRIEF_DATAMETRIC360.md = the prompt for the Claude Design project.

homeassistant/ in one paragraph: 10 pyscript scripts + 5 modules (pyscript/modules/); trip detection via the FMM003 ignition sensor with pause tolerance (the iPhone WLAN sensor is gone — see section B), two-stage trip completion via HA history screening (odometer often updates only on the next trip), fill-up detection on fuel-level rise, Shell PDF parser (data/shell_beleg_parser.py, subprocess, the only tested part of the repo), tire km counter, backup, self-update, image management. Frontend: one file www/audi-dashboard-app.js (~4,000 lines, custom element, no framework/bundler), 5 tabs + ~19 detail routes, cache-busting via audi-dashboard-version.json + loader stub. Entity IDs are assigned in the UI (Settings → Fahrzeug einrichten → Setup), stored as overrides in data/entitaeten.json; pyscript/modules/einstellungen.py holds only the built-in defaults. Deploy: update.ps1 (robocopy to Samba share) or — still inactive — self-update from git.

companion-app/ — what exists: the full app. Data layer (src/api/: REST, WebSocket with reconnect backoff, persistent offline write queue, credential storage), domain logic (src/daten/: profile adapter, statistics, service forecast, data context), all 21 screens (src/screens/), Audi assets (src/assets/audi/), PWA manifest and icons. Verified by 90 unit and render tests plus 9 checks against a live Home Assistant. npm run dev in the repo root starts it; testumgebung/aufsetzen.sh provides the server side.

DataMetric360 architecture (short — details in COMPANION_APP_ARCHITECTURE.md):

  • Future data source: Teltonika FMM003 on the CAN bus, fully replacing the iPhone WLAN sensor and the VAG integration.
  • Current data path (since 2026-08-12): FMM003 → flespi (native Codec8/TCP, IMEI auth) → HA. Both earlier directions are dropped: Traccar first, then the self-hosted MQTT/TLS broker — the router's port-forward turned out to be unusable. The decision logs deliberately remain in the architecture doc, marked ÜBERHOLT. Details and open steps in section B below.
  • Frontend access: HA REST + WebSocket with a long-lived access token (no hass object).
  • External access: Cloudflare Tunnel + reverse proxy with a path allowlist; HA itself stays unreachable. Domain datametric360.app registered (all-inkl, 2026-08-11).
  • Distribution: sideload only (license reason) — hence real Audi assets are allowed there.
  • Trip detection moves to FMM003 ignition; the pause-tolerance feature (fahrten_pausenzeit_min) is a deliberate product decision and must be preserved.

Review findings (2026-08-11)

Design/CI review of main (2026-08-13) — see DESIGN_REVIEW_2026-08-13.md

Code review of all UI layers plus headless-browser screenshots of the panel (mock hass), checked against the repo's own Audi-CI rules. Headlines: the iOS overlay (audi-dashboard-ios.css) overrides the documented palette/radii, adds shadows/blur and turns red into a fill color without any recorded decision (visible e.g. as a red-filled "Abbrechen" next to a black "Speichern" in the setup popup); design-system/ still carries pre-audit state (old low-contrast --fg3 #657081, no focus styles at all, 13.5px inputs, 34px icon buttons); the design/ export's RS-6 error extends into the technical sample data (73 l tank, 285/30 R22, FIN series 4G). Four browser-verified rendering defects: switches stretched by .feld label{flex:1 1 auto} (state display becomes ambiguous), double "offen" in the trip list, empty oil-change tile when the service book is empty, and fragmented Leaflet rendering in all three maps ( fixed 2026-08-16 — root cause was Leaflet's stylesheet being loaded into document.head, which never reaches the panel's shadow root; see the third-round entry in "Open items"). Positives: the 300/400 font-weight rule and de-DE formatting hold everywhere; design-system/ is verifiably brand-free.

HA panel — known gaps (most also in SPECIFICATION.md §7)

  • GPS is dead schema for trip records specifically: start_lat/lon, addresses, route, avg_speed_kmh on individual trips never populated. The trip-detail map draws a fabricated line via fakeTrack() (audi-dashboard-app.js:414) — not a real track. (Unrelated: the Übersicht's live Standort-Kachel is no longer in this state — STANDORT_TRACKER now points at a real device_tracker, see section B/C. This item is only about per-trip route data, which nothing currently populates.)
  • No GPS fallback in trip completion: trips without an odometer match stay status="offen" forever (modules/fahrtabschluss_logik.py:16-20).
  • RAM-only state: running trip (_fahrt_start_ts) and fuel low-water-mark (_tiefststand_pct) do not survive an HA restart. Deliberately deferred hardening.
  • Inactive features: UPDATE_REPO_URL = "" (self-update inactive) in pyscript/modules/einstellungen.py. www/bilder/ has no vehicle photos (all slots show placeholders); steuer.faellig unset. (BATTERIE_SENSOR was the same kind of gap until 2026-08-12 — now wired to the FMM003's external_power_voltage, see section B/C below. Several other fields are newly inactive since the same date for a different reason — the TommiG1/HA_VAG-EU-Data-Act integration they depended on was dropped: KM_SENSOR, TANK_SENSOR, RANGE_SENSOR, REFRESH_BUTTON, all door/window/lock sensors, oil-change/ inspection sensors. All are configurable again via the new Setup menu, see section C.)
  • Robustness: profil_lesen() in modules/profil.py does not handle a missing/corrupt fahrzeugprofil.json — all callers throw.
  • Tests: only the receipt parser is tested (data/tests/test_shell_beleg_parser.py, 10 real receipts) — and its test PDFs are gitignored, so a fresh clone can't run it. Backend and frontend: no tests, no CI.
  • Leaflet via CDN: trip map needs public internet in addition to the Tailscale tunnel.

Documentation drift FIXED 2026-08-11 (kept as a record of what was wrong)

  • homeassistant/README.md:99, INSTALL.md:204, and the header comment audi-dashboard-app.js:13-15 claim the statistics view shows sample numbers — false; vStat() computes real values from TRIPS/FILLS.
  • homeassistant/README.md:110 mentions the removed 97% full-tank rule (removal documented in belegverarbeitung.py:18-20); the README file list omits 5 pyscript files.
  • Obsolete comment belegverarbeitung.py:41 ("TODO: Datei ablegen" — file has long existed).

Audit leftovers — two of three now fixed in the new app

  • Swipe-to-delete without a gesture-free fallback — fixed in the app: every list row also carries an always-visible "…" menu (companion-app/src/screens/Zeilenmenue.tsx). Still open in the old panel, which is being replaced anyway.
  • Leaflet from a CDN — fixed in the app: bundled from node_modules as a lazy chunk.
  • 🟡 Popup close-by-tap-outside in the old panel: still unchecked (the app uses the library's Popup, which is keyboard-reachable).
  • Audit's own note: these three may be better done in DataMetric360 than retrofitted — the panel gets replaced anyway.

companion-app / design-system RESOLVED 2026-08-11

  • Not wired together — fixed: an npm workspace in the repo root links @audi-dash/ui into the app as a real dependency.
  • Both packages: no node_modules, no dist — smoke tests need npm install first (design-system additionally npm run build; scripts/smoke.mjs imports from ../dist/).
  • No unit tests, no Storybook (substitute: SSR smoke over 21 cases in design-system).
  • design/ export (2026-08-11) has two known, already-commissioned fixes not yet in the export: shows RS 6 instead of RS 4, and 16 sub-pages the HA panel already has are missing (list in design/README.md).

Found while building (2026-08-11) — all fixed

Three defects that only surfaced by running against a real Home Assistant, not by reading code:

  1. The data layer declared field names the backend never sends. Fahrzeugstatus had tank_prozent/sicher_abgestellt/sicherheit; the backend writes tankprozent/gesichert/sicherheitscheck. Every screen would have read undefined without anything failing. technik/ausstattung were typed as objects but are arrays.
  2. The profile adapter handed out live references into the raw profile. Editing a form would have silently mutated the baseline and broken the promise never to overwrite the backend-maintained tire odometer. Sections are copied now.
  3. design-system still carried the pre-audit --fg3: #657081 (3.0:1 on --tile, fails WCAG AA) that the panel had already fixed to #8a94a3. The new app would have inherited a already-solved contrast defect.

Also: two TypeScript parameter properties in the data layer broke Node's strip-only mode, which is what the smoke scripts run on — rewritten as plain fields.

Three more that only screenshots revealed — nothing failed, the pixels were simply wrong:

  1. The vehicle block on the home screen was invisible. overflow: hidden sets a flex item's automatic minimum size to 0, so once the page was taller than the screen, flex-shrink squashed the block to zero height and took model name, badge and plate with it. Fixed with .dm-inhalt > * { flex: none }.
  2. The model name appeared twice — once as the badge image, once as text beside it.
  3. Filenames overflowed the gallery thumbnails.

And one in the backend, found by watching the log against a current Home Assistant: the odometer screening called urlopen directly, which HA aborts as a blocking call since 2026.8. Every trip stayed without a distance, visible only as a warning. Now runs through task.executor.

CORS is a real constraint for this app. cors_allowed_origins did not take effect on HA 2026.8 (preflight 403 even same-origin). Two consequences, both handled: the web build is served from Home Assistant itself (/local/dm360/, same origin — the planned deployment anyway), and the native hull enables CapacitorHttp, which routes fetch through native HTTP where CORS does not apply. Verify this again at commissioning if the app ever moves to a separate hostname.

A self-written QR encoder was discarded. It disagreed with a reference implementation on 1239 of 3249 modules — the code would have been unreadable. homeassistant/www/dm360-qr.html now uses a vendored MIT library served from Home Assistant itself, which satisfies the actual requirement (no network call, token never leaves the local network) and round-trips correctly.

Pending: this branch has diverged from main (noted 2026-08-13)

ha_install.md (root) plans the move from pyscript to a native HA integration with a config flow — UI-only setup, no YAML, and updates via a self-reporting UpdateEntity against the Gitea repo (HACS is GitHub-only, so it is not an option). All APIs in it were verified against the running 2026.8.1 instance. It also records which review findings that move eliminates by design.

A full review of main's 18 new commits is in REVIEW_main_2026-08-13.md — 15 findings, the three most serious in the new setup menu (saving with an unloaded catalogue wipes the whole mapping; "reset" has no effect on 15 of 17 fields; all four list positions get the same sensor, which makes "securely parked" report safe while three doors were never checked).

main has moved 18 commits ahead of umsetzung-datametric360 (FMM003 switch, sensor-mapping setup menu, iOS/large-screen overlay from Claude Design). Deliberate decision: do not merge yet — the owner keeps working on main first.

Three files conflict (AGENTS.md, homeassistant/INSTALL.md, homeassistant/pyscript/fahrterkennung.py); four more are touched by both sides but merge cleanly.

One hazard that a clean merge will not catch. This branch changed profil_lesen() in homeassistant/pyscript/modules/profil.py to return None when the profile file is missing or corrupt, and guarded all seven call sites that existed here. profil.py is untouched on main, so it merges silently — but main's call sites (fahrterkennung.py, tankerkennung.py, reifenzaehler.py, modules/frontend_veroeffentlichung.py) do not guard against None and would raise AttributeError on a missing profile instead of logging a clear error.

When merging: take main's FMM003 version of fahrterkennung.py, then re-apply the None guard to every remaining profil.profil_lesen() call site.

Open items

Execution order, exact steps, and acceptance criteria for every item below live in UMSETZUNGSPLAN.md (phases 113). Additional decisions of 2026-08-11: no Electron (Capacitor wraps the web app for iPhone; a PWA home-screen install is the accepted intermediate step) and no separate backend (the app talks to the HA REST/WebSocket API directly).

A) Build DataMetric360 (the big block)

  • Fix the Claude Design draft (RS 4, not RS 6) and extend it by the 16 missing sub-pages; then re-export to design/
  • companion-app: set up Vite + React + Capacitor scaffold; wire @audi-dash/ui as a real dependency (possibly add a workspace/monorepo root)
  • Implement the screens from the design draft on top of the existing DataMetricApi layer
  • Add Audi assets (fonts/rings/badges) at implementation time from homeassistant/www/never into design-system/
  • Secure storage for the LLAT (iOS Keychain / Android Keystore via Capacitor plugin; the ablageSetzen() hook already exists)
  • Onboarding: manual token paste (required); QR scan only if it stays simple (QR generated locally under HA /local/, architecture §3)
  • Authenticated smoke tests of the data layer (reads, service calls, queue round-trip) against audi_ha_test with a real token
  • Offline UX per design brief (offline marker, visible pending queue)

B) Infrastructure / commissioning (partly waits for FMM003 hardware)

  • Switch datametric360.app nameservers at all-inkl to Cloudflare ("full setup") — prerequisite for the tunnel; domain carries nothing else, so this is consequence-free
  • Decide hostname split (app on apex + API on api. subdomain, or vice versa)
  • Choose reverse proxy (Nginx Proxy Manager vs. Traefik) — can be done before hardware against the existing HA API
  • Define the reverse-proxy path allowlist (depends on final entity/service names)
  • Install/wire the FMM003; record firmware version (Codec JSON is firmware-dependent)
  • Generate TLS certificates for Mosquitto + device (small private CA) — done 2026-08-11, 10-year validity; Mosquitto configured (certfile/keyfile/cafile/require_certificate: true). Remaining: upload root/client cert/key to the FMM003 Security tab — filenames must end in .pem/.pem.crt/.pem.key (Configurator rejects plain .crt/.key, content-agnostic check)
  • Decide broker reachability for the vehicle — reopened 2026-08-11 evening: port-forward 8883 on the Speedport Smart 4 Plus looked correctly configured (rule present, right internal IP, right port) and internal reachability was confirmed (homeassistant.local:8883 open from the LAN, Mosquitto TLS listener genuinely up, TLS cert chain end-to-end verified byte-for-byte against the FMM003's uploaded client cert), but the port stayed closed from outside (confirmed via external port checker, both before and after a router reboot that changed the dynamic WAN IP — DNS/DuckDNS matched correctly each time, so not a DNS or CGNAT issue). Community reports (ComputerBase, Telekom Hilft) describe this as a known, Telekom-acknowledged firmware bug on this router model; a full disable-the-firewall workaround doesn't exist on this model either. Port-forward rule has been removed again. New direction: route the FMM003 through flespi instead (native Teltonika/Codec8 channel, IMEI-based auth, no certs, no inbound port needed at all — flespi has a stable public endpoint; HA pulls data back out via flespi's REST API or MQTT, outbound-only). Free flespi tier (10 devices/2 channels) is enough for one vehicle. Next step: user creates the flespi account + Teltonika channel; a flespi custom integration is already present in this HA instance (unconfigured) — check what it needs once flespi-side setup exists. Unrelated but still valid from the same session: DuckDNS hostname datametric360.duckdns.org reliably updating; Let's Encrypt for HA's own local UI works (root cause of the earlier DNS-01 failures was a stray aliases entry in the DuckDNS add-on config, not DNS/network — see COMPANION_APP_ARCHITECTURE.md §5 item 2a). HA's SSL config now lives in Settings → System → Network (UI), not configuration.yaml's old http: block, which was removed after HA started migrating/ignoring it. The Mosquitto TLS setup itself (cert chain, require_certificate: true, certfile/keyfile/cafile) is verified correct and can be reused as-is if a self-hosted broker is ever revisited.
  • MQTT Client Type on the FMM003 ("Custom server" not selectable in practice, "AWS IoT Custom" pointed at a self-hosted broker was confirmed working by two independent community reports) — moot for now given the flespi pivot above: flespi uses the device's native Codec8/TCP channel, not MQTT at all, so Server Settings should switch to Protocol: TCP against the flespi channel host/port instead, and Codec set to "Codec 8 Extended". Revisit this item only if a self-hosted broker is picked back up later.
  • Capture the first real Codec JSON message (mosquitto_sub/MQTT Explorer) and build the field mapping from it — do not guess beforehand (explicit decision)
  • Move trip detection to FMM003 ignition — done 2026-08-12. fahrterkennung.py rewritten: trigger is now einstellungen.ZUENDUNG_SENSOR (binary_sensor.testzone_fmm003_engine_ignition_or_acc_status, on = trip running) instead of the iPhone WLAN sensor; same task.unique() pause-tolerance pattern kept unchanged. sensor.iphone_wifi_connection and the fahrzeug.wlan_name profile field are fully removed (backend and frontend). At the same time, all entity IDs sourced from the now-abandoned TommiG1/HA_VAG-EU-Data-Act HACS integration were blanked in einstellungen.py (KM_SENSOR, TANK_SENSOR, RANGE_SENSOR, REFRESH_BUTTON, door/window/lock sensors, oil-change/inspection sensors) — the consuming features (Übersicht tiles, Reifenzähler, automatic Tankerkennung, Fahrtabschluss-Screening) are untouched and degrade gracefully to "unbekannt"/inactive per the existing zustand_oder_none() convention, rather than being deleted; fahrtabschluss.py/tankerkennung.py/reifenzaehler.py now guard their @state_trigger(f"{einstellungen.KM_SENSOR}")-style decorators with if einstellungen.KM_SENSOR: (untested previously whether pyscript tolerates an empty trigger string — not worth relying on). STANDORT_TRACKER now points at the real device_tracker.testzone_fmm003 and BATTERIE_SENSOR at sensor.testzone_fmm003_external_power_voltage (the vehicle's 12V bus voltage as measured by the tracker — NOT sensor.testzone_fmm003_battery_voltage, which is the tracker's own internal backup cell and unrelated to the car). Real entity list for this device: the user's Entiaeten.csv export (OneDrive, not in this repo). Verified in audi_ha_test: pyscript reload (pyscript.reload) produced no errors/warnings for any of the changed files; the Übersicht correctly shows "Status unbekannt" and em-dash placeholders for the now-blank VAG fields instead of crashing (that container itself has no device_tracker.testzone_fmm003 registered, so Standort still shows "Kein GPS-Signal" there — expected, the real HA instance has it). INSTALL.md step 4 and its troubleshooting table have since been rewritten (2026-08-12) to point at the Setup menu instead of manual einstellungen.py edits, and to use the current FMM003 field names (ZUENDUNG_SENSOR/STANDORT_TRACKER/BATTERIE_SENSOR) instead of WLAN_SENSOR/TommiG1 — not live-tested against a real install (INSTALL.md itself was never run end-to-end in this project, per its own opening note), but consistent with the actual code.

C) Maintain the existing HA panel (low priority — being replaced)

  • Wide-screen/desktop layout (@container (min-width:860px) in audi-dashboard-ios.css, turns the same .tabbar into a 264px side-nav via CSS only, no JS/markup duplication) — done 2026-08-11 from the Claude Design "DataMetric360 Board" draft (claude.ai/design project c28a8d4d-ec4e-4178-9e49-ab5b90c02097). Found and fixed a real regression while verifying it live in audi_ha_test: the overlay's :host > div{height:100%} (meant to drop the old 412px-cap for the sidebar layout) was applied unconditionally instead of only inside the @container block, which broke the normal mobile layout too — HA doesn't reliably propagate a real height down through panel_custom, so height:100% collapsed main#view to ~0px (tabbar rendered right under the header, content invisible). Fix: kept the outer wrapper viewport-anchored (min(880px, calc(100dvh - 24px)), matching audi-dashboard.css's original reasoning) at all widths, and only drop the 880px cap (calc(100dvh - 24px), still viewport-anchored, not parent-relative) inside @container (min-width:860px). Verified live at both narrow (mobile tab bar, full content) and wide (1144px container, grid side-nav, navigation clicks, settings back-arrow) — screenshots taken, no regressions found.
  • Desktop-Sidebar-Markenklon (.navmarke) nachgezogen — done 2026-08-12. The user compared the deployed app against the live Claude Design "Board" (the two-viewport comparison page, which genuinely iframes this project's own audi-dashboard-app.js/-ios.css, not a static mockup) and asked why the desktop Audi-rings placement differed: Design showed the rings pinned above the sidebar nav list, ours showed them in the content header next to the title. Root cause, confirmed via DesignSync get_file + byte-diff: audi-dashboard-app.js was already fully in sync (identical), but audi-dashboard-ios.css had drifted — the Design project had since added a .navmarke sidebar clone of the rings (only visible in the @container (min-width:860px) block; .rings{display:none!important} hides the header copy there so the mark appears once) plus a filled-pill active-tab style (.tabpille), neither pulled back locally. Diffed the two full files first to confirm every change was additive/CSS-only, then replaced the local file wholesale. Verified in audi_ha_test at both 402px and 1400px: rings now sit above the nav list on desktop, gear icon alone in the content header, mobile view unaffected, no console errors beyond the pre-existing missing-vehicle-photo 404 gap.
  • Standort-Kachel (live vehicle GPS position on the Übersicht, above "Zuletzt") — done 2026-08-12. New tile with a real (non-fake) Leaflet mini-map, opens a fullscreen "Standort" route on tap; fullscreen has 4 floating controls (map style, center-on-vehicle, center-on-user, fit-both) and a draggable myAudi-style bottom sheet (peek/expand/close by drag or tap, opens on vehicle-marker click) showing distance-to-user, address (client-side reverse geocoding via the public Nominatim API — same "public API, no key, low single- vehicle volume" reasoning as the existing Leaflet-CDN gap below), fährt/steht/"Letzter Parkplatz" state, fuel/range, and Route (Google Maps deep link) / Teilen (Web Share API) actions. Backend: new STANDORT_TRACKER entity-ID setting in einstellungen.py (device_ tracker with lat/lon attributes), published via a new _standort() helper in frontend_veroeffentlichung.py. The built-in default points at the author's test device (device_tracker.testzone_fmm003); until the FMM003 actually delivers positions (section B), that entity does not exist and the tile shows "Kein GPS-Signal vom Fahrzeug". Verified end-to-end in audi_ha_test with a manually created device_tracker.test_fahrzeug test entity (tile, fullscreen map, and menu all confirmed rendering/populating correctly); the device's own location (browser Geolocation API) and the Route/Teilen deep links work independently of the vehicle-GPS gap. Pushed to the Claude Design project (c28a8d4d-ec4e-4178-9e49-ab5b90c02097, "DataMetric360 Board") for visual refinement — the JS/CSS ship functionally complete but visually plain (placeholder vehicle-marker glyph, default .aktion/.tile styling); Claude Design's job is polish, not structure. One known rough edge: reverse-geocoded address didn't resolve within a few seconds in the audi_ha_test container (no crash, stays on "Adresse wird ermittelt …" indefinitely) — likely just that container's outbound network to Nominatim specifically; unconfirmed whether this reproduces on the real HA instance.
  • Setup menu (Einstellungen → Fahrzeug einrichten → last item when expanded) — done 2026-08-12. Lets the user map every sensor role the app uses (~18 roles, 3 of them 4-item door/window/lock lists) to real HA entities from a searchable dropdown, instead of hand- editing einstellungen.py. New backend module pyscript/modules/entitaeten.py: a FELDER catalog (label, hint, expected domain/device_class/unit, keywords, whether it's one of the trigger-bound fields) plus JSON override I/O (data/entitaeten.json, same atomic-write pattern as profil.py) and overrides_anwenden(), which does setattr(einstellungen, key, value) on the already-imported einstellungen module — every existing consumer does import einstellungen + live attribute access (verified: none use from einstellungen import X), so this needs no changes to any consumer file. New service audi_dashboard_entitaeten_schreiben (frontend_api.py) and publication entitaeten_veroeffentlichen() (frontend_veroeffentlichung.py) follow the project's existing read-via-state/write-via-service pattern. Frontend: new popup (vSetupPopup(), modeled on the existing .sheet pattern) with a from-scratch searchable-combobox component (none existed in the repo before) — client-side only, built directly from HASS.states, no new backend read service; a global "Nur passende Sensoren anzeigen" switch hard-filters candidates by domain/unit, a keyword+domain+unit score ranks and auto-suggests for empty fields. Deliberately does NOT auto-apply changes for the three trigger-bound fields (ZUENDUNG_SENSOR/KM_SENSOR/TANK_SENSOR@state_trigger bakes the entity ID in at module-load time) — the UI shows an inline "wirkt erst nach Neustart" hint instead of attempting a risky self-pyscript.reload() from inside a running service call. Two real bugs found and fixed during live verification, both now also pushed to the Claude Design project: (1) a full popup re-render on every keystroke fought the search input for focus — fixed by patching just the candidate-list DOM node on input, matching the same fix the existing data-tankpreisfeld handler already uses for the identical reason; (2) the popup background used --tile-2, which Design had since changed to a translucent rgba(255,255,255,.1) in the night theme (for the frosted-glass tile look elsewhere) — made the whole popup partially see-through against the app content behind it. Fixed by switching to --tile-deckend (Design's own opaque-surface token, already used by .standortmenu for the same reason), with var(--canvas) as a defensive fallback.
  • Fresh-install packaging + update.ps1 fix — done 2026-08-12. While preparing a short quick-install checklist, found that update.ps1 never copied audi-dashboard-ios.css or www/badges/* — both are loaded by the running app (the ios overlay is injected by the app itself via JS, badges by the model picker since item #51), so every update.ps1 run since those were added silently left the deployed copy stale on those two. Fixed (script now copies both). Also removed two stray *.bak-before-audit-merge files from www/ (already gitignored at repo root, never committed, just local clutter from an earlier merge). New installationspaket/ — a generated, gitignored bundle (pyscript/, www/ minus the two backup files, data/ with only the example profile + receipt parser, configuration_snippet.yaml, update.ps1) plus a short ANLEITUNG.md checklist for copying to a fresh HA instance; INSTALL.md stays the authoritative, detailed reference. Regenerate on demand, don't keep it permanently in sync — it's a deployment snapshot, not a second source of truth.
  • Setup-menu polish: 5 additions on top of the existing Setup popup — done 2026-08-12. Backend (pyscript/modules/entitaeten.py): a _STANDARDWERTE snapshot taken at module-load time (before overrides_anwenden() ever runs), exposed via aktueller_stand() as standardwerte — needed because setattr() on the live einstellungen module is permanent for the process, so "reset to default" has to write the original value back explicitly rather than just omitting the override. New service audi_dashboard_neustart (frontend_api.py) wrapping homeassistant.restart(), called only from an explicit user-confirmed button, never automatically. Frontend (audi-dashboard-app.js): (1) live current-value preview next to each candidate in the search dropdown (entitaetZeilenMarkup, reads HASS.states[e.id].state); (2) inline warning under a field if its currently assigned entity is unavailable/unknown/ missing (entitaetStatusWarnung); (3) duplicate-assignment check on save (setupDuplikate) — blocks with a confirm-sheet ("trotzdem speichern?") rather than silently allowing the same entity in two roles; (4) per-field reset button (only shown when the current value differs from standardwerte), reverts to the built-in default; (5) after saving, if any of the 3 trigger-bound fields actually changed (setupGeaenderteTriggerFelder, compares against a snapshot taken when the popup opened), a confirm-sheet offers "Jetzt neu starten" calling the new service. Verified live in audi_ha_test: value preview and unavailable-warning confirmed correct (ZUENDUNG_SENSOR's configured entity genuinely doesn't exist in this test container → "Entität nicht gefunden." shown as designed); duplicate-check correctly caught both a deliberately-forced KM/TANK collision and pre-existing door/window/lock auto-suggestion collisions (a real side effect of this container's limited matching entities, not a bug); reset button appeared/disappeared correctly as values diverged from/matched the default; save round-tripped through the real service into data/entitaeten.json (byte-inspected) and into the published pyscript.audi_dashboard_fahrzeugstatus state; restart-needed sheet named exactly the changed trigger-bound field(s) and no others. Did not actually trigger a restart during verification (would have restarted the shared test container) — the service call itself was left unexercised beyond confirming it registers without error at pyscript load time.
  • Decide the iOS-vs-Audi-CI conflict from DESIGN_REVIEW_2026-08-13.md §A — decided 2026-08-13, owner's call: iOS look made official (Option 1 of the two the review offered). SPECIFICATION.md §2 rewritten to describe the iOS values (colors, 16px radius) as canonical; bauauftrag.md §8 now counts as superseded (left untouched, per this project's convention of never editing that file — see its own "historical only" note above). The pre-decision Audi-CI spec text is preserved verbatim in AUDI_CI_ARCHIV_2026-08-13.md, which also documents why reverting later needs no restore: audi-dashboard-ios.css is a purely additive overlay loaded after audi-dashboard.css in _aufbauen() (audi-dashboard-app.js ~line 3990) — the Audi-CI base values are still fully intact in the unmodified base stylesheet, untouched by this decision. Reverting is just: stop loading the overlay <link>.
  • Fix the two implementation bugs from the same review, independent of the direction decision above — done 2026-08-13. §B, red-semantics inversion: audi-dashboard-ios.css .aktion no longer fills every action red; base .aktion is now a neutral --ios-fill secondary button, a new .aktion.primaer rule is the only one filled with --ios-tint (red), matching the .aktion/.primaer/.loeschen semantics the base stylesheet already defined (outline secondary / filled primary / destructive). Also fixed as part of the same finding: switch on-state now uses --ok (iOS system green) instead of red; the desktop-sidebar active-tab background (.tab.on) is now the neutral --ios-fill instead of a red tint, red stays only as the accent text/icon color; two chart-bar segments in audi-dashboard-app.js that used --red for a neutral data category ("Arbeitsweg" trips, first segment of the insurance contribution bar) now use --fg like their sibling segments. Radius inconsistency: .setup-popup and .standortmenu in audi-dashboard.css now reference var(--r-tile) instead of a hardcoded 20px literal, so both track whichever design (iOS 16px or, if reverted, Audi-CI 20px) is actually active. Verified live in audi_ha_test: Setup-popup Abbrechen/Speichern and the "Einrichten"/"Setup"/"Fertig" buttons in Einstellungen render with the corrected fills at both mobile and desktop widths, switches render green, no console errors.
  • Clean up remaining "EU Data Act" residue from the FMM003 pivot (2026-08-13) — the Einstellungen "Version" tile in audi-dashboard-app.js hardcoded Fahrzeugdaten: EU Data Act / Abruf alle 15 Minuten and Position: iPhone / Companion App, both stale: vehicle data and position now come exclusively from the FMM003 (ZUENDUNG_SENSOR, BATTERIE_SENSOR, STANDORT_TRACKER in einstellungen.py), not the abandoned HACS integration or the iPhone. Both rows now read FMM003. Also fixed testumgebung/konfiguration.yaml + testumgebung/README.md: the input_text.test_wlan / iPhone WiFi Connection template sensor simulated WLAN-based trip detection, fully removed 2026-08-12 (fahrzeug.wlan_ssid no longer exists anywhere) — its "Fahrt auslösen" example was non-functional. Replaced with input_boolean.test_zuendung driving a binary_sensor.testzone_fmm003_engine_ignition_or_acc_status template sensor matching the real ZUENDUNG_SENSOR entity ID, so trip detection stays testable. The remaining KM/Tank/Range/door/window/lock template sensors correctly still describe the abandoned TommiG1/HA_VAG-EU-Data-Act integration (per einstellungen.py) and were left in place — those fields are blank by default but stay manually mappable via the Setup menu, so the fixtures remain useful for testing that path; comments updated to make this distinction explicit.
  • Fix oversized toggle switches ("Slider") across the app (2026-08-13) — .feld label in audi-dashboard.css (flex: 1 1 auto, meant to let the text label grow so the control lands at the row's right edge) unintentionally also matched <label class="switch">, since the toggle switch is itself implemented as a <label> — its own .switch { flex-shrink: 0; width: 46px } rule lost because .feld label has equal-or-higher specificity and comes later in the file. Every switch inside a .feld (Einstellungen, Tanken, Reifen, ...) stretched to fill the row (~260-300px measured live, instead of 46-51px). Fixed by excluding switches from the selector: .feld label:not(.switch). Verified live in audi_ha_test at mobile (375px) and desktop widths — switches render at the correct ~51px iOS size, no console errors.
  • Fix desktop (≥860px container) layout audit findings (2026-08-13, see DESIGN_AUDIT_2026-08-13.md for full detail) — user-reported: (1) the settings gear button sat far past the right edge of the visible content column on wide screens because .topbar (grid-column:2, same track as main#view) had no max-width:860px while main#view did — the track itself is minmax(0,1fr) and stretches to the full remaining window width, so .head{flex:1} pushed .profilbtn to the track's far edge, ~450px past where main#view visually ends. Fixed by giving .topbar/.phone:has(.back.on) .topbar/.ptr the same max-width:860px. (2) In phone width the Audi rings (#marke/.rings) are, per existing code comment, the only access to Einstellungen (the gear icon is hidden there) but were 42×24px — well under the 44px minimum touch target every sibling icon-button uses. Bumped to 58×32px. (3) Found while verifying (1): .setup-popup, .sdpopup, .sheet, and .standortmenu are all position:absolute with left/right relative to the full .phone box (sidebar included), not the content column, and had no desktop max-width either — .setup-popup measured 1644px wide on a 1920px window (10px margins only). Fixed with max-width:560px (.setup-popup/.sdpopup/.sheet) or 640px (.standortmenu) plus margin-left/right:auto in the same container query — deliberately not transform:translateX(-50%), which would fight these elements' own transform-based entry animations. Centering is relative to the full .phone width (sidebar included), not the 860px content column exactly — a ~138px rightward offset from true content-center remains at 1920px; fixing that precisely would require moving these overlays into grid-column 2 in the markup, deferred as a larger structural change. All three verified live in audi_ha_test at 375px and 1920px, no console errors.
  • Fix 4 more user-reported design findings (2026-08-13, same session, see DESIGN_AUDIT_2026-08-13.md "Runde 2" for full detail with before/after measurements): (1) page title sat ~30px right of the content tiles below it on wide screens — .back reserves 44px+gap in .topbar even when inactive (opacity:0, not display:none), fixed with .back:not(.on){width:0;...} in the desktop container query, root pages only (detail pages with .back.on unaffected, verified back-navigation still works). (2) the chevron on the 5 .go-tiles (Fahrzeug/Service/Versicherung/Reifen/Schutzbrief on Mein Audi/Versicherung) was vertically centered on the entire multi-row tile (top:50% in the iOS overlay) instead of next to the title, overlapping wrapped row text — removed the override, falls back to the base top:var(--sp-5) next-to-title position. (3) the Ölwechsel-Intervall <select> values ("10.000 km"/"1 Jahr") looked like inert text — iOS overlay strips border/background from .feld select and colors it the same grey as any other secondary text; added .feld select{color:var(--ios-tint)} so picker fields read as editable the way iOS conventionally signals it (accent color), free-text .feld inputs deliberately left alone. (4) the expand/collapse .mark chevron on group headers (Fahrten "2026"/"August") looked like a checkmark — it uses the classic "2 borders + rotate(45deg)" CSS chevron technique, which requires a square box; .mark was 6×10px (non-square, sized for the unrelated SVG-path .chev/.go icons), made it 8×8px. Two more reported findings ("grey box around all menu icons", "red partial frame on Fahrten/Tanken rows") were investigated (computed-style diffing across all 5 tabs in both themes, a temporary 2.5x transform:scale() zoom on the live swipe-row DOM) but could not be reproduced — documented in the audit file as open, pending a screenshot from the real device.
  • Fix five user-reported bugs (2026-08-16): (1) Türschloss-/Haubenschloss-Erkennung entfernt — user asked for these two checks to be permanently removed (not just left blank/unmapped like the rest of the retired VAG- integration fields). Removed TUERSCHLOSS_SENSOREN/HAUBENSCHLOSS_SENSOR from entitaeten.py's FELDER catalog (no longer offered in the Setup menu) and from einstellungen.py (attributes deleted, not just blanked), and removed their entries from _sicherheitscheck() in frontend_veroeffentlichung.py (no longer listed under "Geprüfte Punkte"). HECKKLAPPENSCHLOSS_SENSOR (tailgate lock) was deliberately left untouched — user named only the two door/hood locks. Also removed the now-orphaned input_boolean.test_entriegelt helper and the four test_audi_lock_* template sensors from testumgebung/konfiguration.yaml (door-open/window fixtures for TUER_SENSOREN/ FENSTER_SENSOREN are untouched), and updated testumgebung/README.md accordingly. (2) "Doppelt zugeordnet" (and every other confirm-sheet) was see-through.sheet-gruppe/ .sheet-abbrechen in audi-dashboard.css used --tile-2, which the iOS overlay's night theme redefines as rgba(255,255,255,.10) (translucent, meant for tile-on-tile layering) — the same root cause already fixed for .setup-popup/.standortmenu on 2026-08-13, just not carried over to the generic confirm-sheet used by bestaetigen()/hinweis(). Switched both to var(--tile-deckend, var(--canvas)), matching the existing fallback pattern already used elsewhere in this file (base CSS must not go blank if the purely-additive iOS overlay is ever reverted, since --tile-deckend is an overlay-only token). (3) FMM003 coordinates arrive as two separate sensors, not one device_tracker — user reported "coordinates are split into latitude and longitude". Root cause: _standort() in frontend_veroeffentlichung.py only ever read latitude/longitude as attributes of a single STANDORT_TRACKER device_tracker entity (HA's usual convention) — but some integrations (confirmed live in audi_ha_test's own simulated FMM003 device, which exposes separate "Latitude coordinate value"/"Longitude coordinate value" sensors) publish them as two independent sensor entities instead, which _standort() had no way to consume. Added STANDORT_LAT_SENSOR/STANDORT_LON_SENSOR (new optional fields in einstellungen.py + entitaeten.py's Setup-menu catalog, group "standort") as a second path: _standort() now tries STANDORT_TRACKER first (unchanged behavior when it works), and falls back to reading the two plain sensors' states directly when the tracker is unset or has no coordinates. (4) App can show only "Lädt …" until a tab is clicked — known, already-mitigated race (see the nachladeAnstossen header comment, audi-dashboard-app.js): on a slow/cold backend start, render() no-ops until DATEN_GELADEN flips true, and the previously observed manual fix was a HA panel remount. Root cause of why the click helps was not fully reproducible in audi_ha_test (this is a genuine uncertainty, flagged rather than guessed around) — but as a safe, low-risk hardening, go() (the tab/menu click handler) now calls datenLaden(false) proactively if DATEN_GELADEN is still false, so any click that happens to occur after data has actually become available is guaranteed to pick it up immediately instead of depending on timing. (5) Fuel-receipt parser only understood Shell's exact layout — user asked for a generic algorithm for other/unknown fuel stations: find the address, find the total ("Gesamt"/ "Absolut"), find the liters ("Menge"/"Amount"), divide to get the paid price/liter, compare against a printed "Preis/Liter" — a mismatch means a discount, found on the receipt as a minus-marked amount. Implemented exactly this as _parsen_generisch() in homeassistant/data/shell_beleg_parser.py, wired as a fallback in main() (Shell-specific _parsen() tried first, unchanged — verified the existing 10-receipt regression suite still passes byte-for-byte; _parsen_generisch() verified against a synthetic non-Shell receipt with a known discount, all fields and the discount math correct). No separate "SmartDeal" flag was needed — the frontend already treats any populated discount as SmartDeal-eligible (gated only by the user's own "SmartDeal aktiv" switch, not by station name), so a generically parsed Shell receipt behaves identically to one parsed via the strict path. Receipt-key generation uses hashlib.sha1 (not the built-in hash(), which is randomly salted per process and would have broken the §7.7 duplicate-detection dedup across the parser's per-receipt subprocess invocations). All five deployed and verified live in audi_ha_test (version 1786752000): Sicherheit- Liste and Setup-Katalog confirmed to no longer offer Türschloss/Haubenschloss; the new Breitengrad/Längengrad Setup fields confirmed present and correctly auto-suggesting the container's real split lat/lon sensors; "Doppelt zugeordnet" confirmed rendering on an opaque card by deliberately triggering it (this container's limited entity set causes a real duplicate auto-suggestion); pyscript reload clean, no new console errors beyond the pre-existing placeholder-image 404s. Synced to installationspaket/.
  • Follow-up round the same day (2026-08-16), three more items: (1) Removed STANDORT_TRACKER entirely — user confirmed the combined device_tracker field is no longer needed (flespi only ever provides the split lat/lon sensors added earlier that day). Deleted the field from einstellungen.py and entitaeten.py's Setup catalog, and simplified _standort() in frontend_veroeffentlichung.py down to the single split-sensor path (no more two-branch fallback). overrides_schreiben()'s existing "only known keys survive" behavior (see its docstring) meant no explicit migration was needed for stale STANDORT_TRACKER entries already sitting in data/entitaeten.json — confirmed live: this container's own leftover override file still had STANDORT_TRACKER from earlier testing (plus stale TUERSCHLOSS_SENSOREN/HAUBENSCHLOSS_SENSOR from before finding (1) of the previous entry), all silently dropped on the next Setup save, which is exactly why the GPS tile was still showing "Kein GPS-Signal" after the user's own remap attempt earlier — that attempt happened to save while the old combined field was still in the catalog, so nothing ever actually persisted STANDORT_LAT_SENSOR/STANDORT_LON_SENSOR. Redid the Setup save live (mapped both to sensor.testcar_b9_fmm003_testintegratoin_lat/longitude_coordinate_value) and confirmed the Übersicht/fullscreen Standort map switched from the "no signal" placeholder to a real rendered map at the correct coordinates (South Tyrol terrain, "Burgleralm" label visible). (2) Fuel-receipt "silently does nothing", root cause found — reproduced the exact real upload flow (button click → real <input type=file> → synthetic File/DataTransfer dispatch → FileReaderhass.callService) via browser instrumentation: the happy path works completely (backend parses, publishes pyscript.audi_dashboard_beleg_ergebnis, frontend fills the form) — belegverarbeitung.py's existing try/except around _parser_aufrufen() already publishes a visible fehler for parser failures specifically. The actual gap: base64.b64decode() and _pdf_speichern() in audi_dashboard_beleg_hochladen() were outside that try block — any failure there (corrupted upload, disk full, ...) raised an unhandled exception with zero UI feedback, since serviceRufen()'s generic .catch() only does console.error(). Widened the try block to cover all three steps. Also stopped using serviceRufen() for this one call site specifically (audi-dashboard-app.js, the eingabe.onchange handler) — it now calls HASS.callService() directly with its own .catch() that sets belegFehler and re-renders, so a rejected service call (network, timeout — anything that bypasses the backend's own state-publish) is visible too, not just logged. Verified against the real 10-receipt Shell suite (still passing) both before and after this change. (3) Vehicle map marker replaced with a two-tone pin+car icon — added CI.pinCar (a balloon/pin path + white badge circle + a simplified car-front glyph built from rects, not circles, after an isolated-DOM-injection test round showed a circles-for-headlights version read as a face/animal rather than a car) to audi-dashboard-app.js, replacing the single-color poiCar teardrop previously used by fahrzeugMarkerSVG(). Updated both marker call sites' iconAnchor to the new pin's actual tip position and simplified .fahrzeug-pin in audi-dashboard.css (no more color:var(--red)/white-halo-filter hack, since the new icon carries its own fixed colors). Verified the icon markup renders correctly via an isolated DOM-injection test (bypassing Leaflet); could not get a final on-map screenshot in audi_ha_test — the Standort map tiles themselves only ever partially load in this container (reproduced on a fully fresh reload, unrelated to this change), consistent with a previously documented network limitation of this specific sandbox (see the Standort-Kachel entry above: reverse-geocoding via Nominatim had the same kind of container-specific network issue). Structurally verified only; needs a look on a real device/network to confirm final visual placement. All three deployed to audi_ha_test (version 1786758000) and synced to installationspaket/.
  • Third round the same day (2026-08-16) — the "fragmented Leaflet rendering" defect is solved at its root, plus four smaller items: (1) Leaflet CSS never reached the shadow root. leafletLaden() appended leaflet.min.css to document.head, but the panel renders inside this.shadowRoot — and document stylesheets do not cross a shadow boundary. Inside the panel, .leaflet-tile {position:absolute} and friends therefore never applied: the tiles laid themselves out as ordinary in-flow <img> elements (two per row, ~1040 px of stacked height inside a 190 px box), so only a thin strip of map was ever visible, and the marker pane ended up ~1009 px below the visible area — which is exactly why the new vehicle pin appeared "missing". This also explains the earlier misdiagnosis as a container/network limitation: every tile request succeeded (complete:true, naturalWidth:512), the geometry was the problem. Fixed by injecting the stylesheet into ROOT and awaiting it before the map is built; the script tag stays in document.head (it must, window.L is global). The CSS injection is deliberately re-checked on every leafletLaden() call rather than guarded by the window.L check, because a HA panel remount produces a fresh shadow root while window.L is already set. Verified live: both the Übersicht preview map and the fullscreen Standort map now fill their containers and show the pin at the vehicle position. (2) Maps are now always light. TILES[theme] switched to CARTO dark_all in Nacht mode; replaced by a single themeless TILE_URL (light_all), matching how Google Maps & co. keep the standard road map light regardless of app theme. (3) Vehicle marker now uses the real CI poi-car icons (poi-car-l.svg / poi-car-s.svg from the delivered icon set) instead of the hand-built two-tone pin from the previous round: CI.poiCarL (48-grid) at ≥34 px, the existing CI.poiCar (24-grid) below, anchors recomputed per variant. Since these are single-color outline forms in currentColor, .fahrzeug-pin regained a stacked white drop-shadow halo so the outline stays readable on map tiles. (4) Red partial frame on Fahrten/Tanken list rows (open since the 2026-08-13 design audit, finding 3) — reproduced and confirmed by recoloring the delete button live. Row heights are fractional (66.28 px), so .swipe-content's edges miss the device-pixel grid and the red .swipe-delete behind it bled through as a hairline around every row. Fixed by only painting the delete button while a swipe is actually happening: .swipe-delete{visibility:hidden} plus a wischt class set on the first movement of the gesture (and the existing swiped class for the open row). Verified the full gesture still works — button appears at the first pixel of drag and the row settles open at 84 px. (5) Select fields now show the grey background box (design audit finding 1) — replaced .feld select{color:var(--ios-tint)} with background:var(--ios-fill);padding:8px 10px, matching .mitEinheit input ("Pause bis [15] Minuten"), as the user asked. Verified on Modell / Ölwechsel-Intervall / Bildposition; free text fields stay plain, so the box now genuinely marks "there is a choice here". (6) Generic receipt parser: total and station name — a real Austrian non-Shell receipt returned the right litres but 6,00 € and no station. Two causes, both fixed: the free-running total pattern matched the column heading SUMME-EUR and read the next line's article number as the amount, and bon printers letter-space headings (G E S A M T BETRAG EUR: 92,60), which the keyword never matched. Total detection is now line-based — keyword checked against the whitespace-stripped line, amount must be a real money value (_GELD, decimals required), tax lines (MWST/UST/VAT/STEUER/NETTO) excluded, largest remaining candidate wins. The address heuristic no longer demands three fixed lines with a 5-digit postcode (AT uses 4 digits, and this receipt carries SHELL TANKSTELLE, 6450 SÖLDEN on one line): it finds the first postcode line in the header and takes the name from the line above. Preis/L without a spelled-out "Liter" now matches too. Result on that receipt: 42,13 l · 92,60 € · 2,198 €/l · no discount · AUTO B. FRISCHMANN GMBH. The 10-receipt Shell regression suite still passes. All deployed to audi_ha_test (version 1786765000) and synced to installationspaket/.
  • Fourth round the same day (2026-08-16): (1) The "grey box around the icons" report is resolved — it was never a defect. After five fruitless reproduction attempts across two sessions, the user clarified what they actually meant: the grey box seen on "10.000 km", "1 Jahr", "15" is the desired pattern, and every value the user can change should carry it to signal editability. .feld input/.feld select in audi-dashboard-ios.css now all get background:var(--ios-fill);padding:8px 10px; inputs with their own visual language (checkbox/radio/range/file) are excluded and keep the transparent base rule, so the three switches stay untouched. The separate .mitEinheit input rule became redundant and was removed (its width/alignment still come from audi-dashboard.css). Verified live: FIN, Kennzeichen, Erstzulassung, Ausführung, Modell, both Ölwechsel selects, Ansicht, Pause bis and Autom. Backup all boxed; the three checkboxes not. Do not re-open this as a bug. (2) Station display name is now "Marke, Straße, Ort" (e.g. Shell, Pascalstr. 8, Ingolstadt) instead of the bare operator name (A. Zrenner GmbH) — the brand and place say more when scrolling the Tankvorgänge list. New _marke()/_ist_strasse()/_tankstelle() helpers in shell_beleg_parser.py, shared by both parser paths so Shell and non-Shell receipts format identically. The brand is matched only against the first 8 lines (the receipt header) — searching the whole text would let "Total" hit a totals line. Missing parts are dropped rather than left as empty comma slots (the Austrian receipt prints no street: Shell, SÖLDEN), and without a recognizable brand the operator name takes the brand's place. station_address keeps the full street + postcode + city as before. The regression suite's test_bekannte_stationen expectations were updated to the new format (intended change, not a break); all 8 tests / 164 subtests pass. (3) installationspaket/ is versioned from now on (user request) — removed from homeassistant/.gitignore. It carries no real vehicle data, only data/fahrzeugprofil.example.json with empty FIN/Kennzeichen placeholders; verified before un-ignoring. Keep it in sync whenever pyscript/ or www/ changes — that was already the working rule, but it is now visible in the repo when it is forgotten. Deployed to audi_ha_test (version 1786766000).
  • Fifth round the same day (2026-08-16) — one-click installer and the Einzelbeleg map: (1) installationspaket/Installieren.cmd + install.ps1 automate steps 13 of ANLEITUNG.md. The .cmd wrapper exists because double-clicking a .ps1 opens it in an editor and the execution policy blocks it; the bypass is per-call, the machine policy is not touched. Safe to re-run: never overwrites fahrzeugprofil.json, fahrten.jsonl, tankvorgaenge.jsonl, entitaeten.json, ha_token.txt; backs configuration.yaml up to .bak and replaces its own marker-delimited block instead of appending twice; refuses to touch the file at all when pyscript:/panel_custom: are already there from another source (duplicate top-level keys are invalid YAML — HA would not start); aborts before writing if the target has no configuration.yaml. -Pruefen is a dry run. install.ps1 is stored UTF-8 with BOM on purpose — PowerShell 5.1 reads scripts as ANSI otherwise and mangles every umlaut; the files it writes stay BOM-less (YAML). Verified against a fake config tree: fresh install, re-run idempotency, data preservation, foreign-key refusal, wrong-directory abort, and the merged YAML parses. (2) Tankstellenposition im Einzelbeleg. The red circleMarker is now the CI poi pin (CI.poiL/CI.poiS from poi-l.svg/poi-s.svg, via tankstellenMarkerSVG()) — deliberately the plain pin, not poi-car, so vehicle and station stay distinguishable. Found while testing: nothing in the backend ever writes station_lat/station_lon, so this marker had never actually been reachable. Fixed by geocoding the receipt address in the frontend (adresseAufloesen(), Nominatim /search — the forward counterpart to the existing standortAdresseAufloesen()), with results cached in localStorage (audi_dashboard_geocache) including negative results, since a station address does not move and Nominatim's terms ask for caching. Confirmed live: Shell, SÖLDEN → 46.9756/11.0111. (3) Zentrieren-Knopf in the Einzelbeleg map card (mapBoxMitZentrieren(), CI.gps — byte-identical to the delivered gps-s.svg), disabled until the coordinates arrive. .mapbox gained position:relative so the control anchors to the card. Only there, per explicit request — the Standort fullscreen already has its own centre controls. Deployed to audi_ha_test (version 1786769000). Note for testing map behaviour in this app: initMap() runs on every render(), and the app re-renders on each backend push (~30 s), rebuilding the map and re-centring it. Panning the map from the console and checking later therefore proves nothing — spy on L.Map.prototype.flyTo instead.
  • Sixth round the same day (2026-08-16) — a large batch of user-reported UI polish items, plus the map pins' real visibility root cause: (1) Map pins were genuinely near-invisible, not just a styling nit. The user reported the vehicle's map pin looked "transparent"; reproduced empirically (not by eye) by rasterizing the live marker's SVG to a canvas and sampling alpha — only ~13% of the icon's own bounding box had any ink, and the exact center pixel was fully transparent. Root cause: CI.poiCarL/poiCar (and poiL/poiS, used for the Einzelbeleg station pin) are pure contour forms — the balloon outline is drawn as two nested, oppositely-wound paths that cancel to a 1px ring under the default nonzero fill rule, plus thin interior detail lines; there is no filled area at all. A first fix (solid navy circle behind just the icon's round "head", via ::before) was explicitly rejected by the user ("I want the whole icon solid, not just the car" / "make the needle opaque") — correct, since it only covered ~36% of the icon and left the pointed tip as a bare outline. Real fix: each icon's d attribute is multiple sub-paths (split at top-level M/m); the first sub-path alone is already the complete, correct outer balloon silhouette (verified by rasterizing it in isolation — solid center pixel, 36-41% fill ratio, matching a normal teardrop-in-square ratio). Extracted that first sub-path verbatim (no redrawing) into four new CI.poiCarLSil/poiCarSil/poiLSil/poiSSil constants, fixed-color-filled (not currentColor), and fahrzeugMarkerSVG()/tankstellenMarkerSVG() now render it as the bottom layer behind the original detailed icon; .fahrzeug-pin's color switched from dark navy to white so the original outline/detail lines read against the new solid fill, exactly like the white-glyph-on-solid-teardrop convention of Google/Apple Maps pins. Applies uniformly to both marker types now (station pin included, per explicit user request), so the earlier per-variant .auto CSS modifier and its circle-position math were removed again as dead weight. (2) Battery voltage statistic cutoff lowered 13.2V → 12.8V (AGM_RUHE_MAX_V in audi-dashboard-app.js) — user's point: anything above ~12.8V (full-charge resting voltage per the existing in-code research citation) is already alternator output, not the battery's own state, so admitting up to 13.2V let charging-tail values distort the charge-state stats. (3) Battery chart Y-axis no longer "stretches." bvZeichnen() recomputed yMin/yMax from whatever points were currently zoomed into (min 0.4V span + 15% pad) on every redraw, so the axis rescaled on every pan/zoom gesture. Now computed once from the full dataset in vBatterieverlauf() (bvYDomain) and reused unchanged across zoom/pan. (4) Removed two explanatory paragraphs from the Batteriespannung detail view ("Näherung auf Basis…" and "Ziehen zum Verschieben…") per explicit request. (5) Tankfüllung now shows "X % / X l" (vAudi()) instead of a literal-litre primary value with a redundant "100 % von 58 l" sub-line. (6) Removed the "· noch etwa X l im Tank" suffix from the Übersicht's "Reichweite" label (vHome()); the now-unused liter local was removed with it. (7) Active/primary buttons are grey again, not red.aktion.primaer in audi-dashboard-ios.css filled with --ios-tint (red) since the 2026-08-13 iOS-overlay decision; explicit user request to drop that and match the base stylesheet's neutral var(--fg)/var(--canvas) fill. Destructive actions (.aktion.loeschen) intentionally stay red — Apple's own HIG reserves red specifically for destructive actions, so this one use is correct per the guideline, not just left over. (8) <select> fields now carry an Apple-HIG pull-down chevron. They already had appearance:none (native arrow removed) but no replacement indicator at all — a real gap, not cosmetic. Added a CSS background-image chevron (light/dark variants) in both stylesheets; base audi-dashboard.css kept independently correct (own light/dark variant via :host([data-theme=tag])) per this repo's "purely additive overlay" convention for the ios file. (9) Apple HIG research round. Read Color/Typography/Layout/Buttons/Materials directly off developer.apple.com (the WebFetch tool returned only page titles for this JS-rendered site: the in-app Browser tool's get_page_text worked and was used instead) and cross-checked against actual measurements in this codebase, then asked the user via AskUserQuestion which findings to apply. Accepted: text below the 11pt iOS legibility minimum (battery-chart axis labels were 8px, the header's "Sync" line was 10.5px) raised to 11px; the new compact "Montiert" pill's invisible hit area (not its visual size) expanded to the 44×44pt minimum via a ::before with negative insets, same technique applied to future compact controls. Declined, deliberately: switching the app's few weight-300 ("Light") text spots to 400 — Audi Type only ships 300/400, and the user chose to keep Audi CI's own weight over HIG's general "avoid Light" guidance; making popups/sheets translucent to match the Materials guidance — the app already restricts translucency to the nav layer only (tab bar), which is the HIG-correct pattern, and popups were deliberately made opaque on 2026-08-16 earlier the same day to fix a real see-through bug, so translucency there would be a regression, not an improvement. (10) Removed "Aus dem Fahrzeug gelesen" and the Einrichten intro paragraph ("Fahrgestellnummer, Kennzeichen, Erstzulassung…") from vEinst(), both per explicit request. (11) Fuel bar color now matches the headline (var(--fg)) instead of a red/yellow/green status gradient in vHome(); the balkenFarbe local was removed with it. (12) Selected menu icon is white (headline color), not red, in the desktop side-nav (.tab.on inside the @container (min-width:860px) block in audi-dashboard-ios.css) — the mobile tab bar already used var(--fg) there, this brought the desktop variant in line. (13) Reifenfoto (Sommerrad/Winterrad) now crops from the right edge, not the center — .radbild gained object-position: right center; the photo is typically wider than the 96px square slot and the wheel itself tends to sit toward the right of the source photo. (14) "Montiert" button rebuilt as a compact top-right pill (new .montiert class, replacing the old full-width .seg-wrapped button) per explicit layout instructions: top edge aligned with the wheel photo's top (top:var(--sp-5), same offset the photo already has from the tile's own padding), right inset equal to the tile's standard left content inset (both var(--sp-5)) so left/right margins match. Markup reordered so the button renders before .radkopf (position is absolute either way, but keeps DOM order sensible). (15) Tab-bar active highlight now covers icon and label when the label is visible, not just a tight pill behind the icon — .tabbar:not(.ohne) .tab.on gets the highlight background directly (whole button), with the inner .tabpille background suppressed in that case; the icon-only mode (.tabbar.ohne) keeps the original tight icon-pill unchanged. Deployed to audi_ha_test (version 1786901000); pin fix visually confirmed live (solid teardrop pin with white car glyph, clearly over the map's "Obergurgl" label text) and the tab-bar highlight confirmed covering both icon and "Übersicht" label.
  • Seventh round the same day (2026-08-16) — more user-reported layout/interaction bugs: (1) Wheel photo (Sommerrad/Winterrad) was never actually a square, despite the fifth round's crop-direction fix. Root cause: bildMitPlatzhalter() puts the radbild class on both the wrapping .bildbox div and the <img>; the old .radbild{width:96px;height:96px;...} rule and .bildbox{width:100%;aspect-ratio:4/3;...} have equal specificity (one class each), and .bildbox is declared later in the file, so it silently won — the box stayed a full-width 4:3 rectangle the whole time, object-position:right center just changed which slice of that wide box showed. Fixed with a .bildbox.radbild{width:96px;height:96px;aspect-ratio:1/1} compound selector (two classes, unambiguously higher specificity than plain .bildbox); .radbild alone now only carries object-position. Verified live: .bildbox.radbild now measures exactly 96×96px. (2) "Montiert" moved from top-right to top-left, per explicit request — this was the visible symptom of (1): the pill was anchored top-right assuming it overlaid the (accidentally) full- width photo; once the photo is a real 96px square on the left, top-left is where it actually needs to sit to stay over the picture. (3) Native <select> popups now get a color-scheme hint. The closed <select> box itself was already themed correctly, but the opened native option list ignores app CSS and falls back to the browser/OS default palette unless color-scheme says otherwise — this is what made "Modell" and every other dropdown "flash" bright white against the dark app on open. Added color-scheme: dark under :host([data-theme="nacht"]) and color-scheme: light under :host([data-theme="tag"]) in audi-dashboard-ios.css. (4) Fixed a real JS crash in the "Mein Audi" image-cycle click handler. bildWeiter() (fires on tapping the hero photo) tried box.querySelector(".platzhalter-datei").textContent = ... — that class was never emitted by bildMitPlatzhalter() (the actual class is .platzhalter- aktion, and in gallery mode it deliberately shows a static "Foto hinzufügen" prompt, not a filename). querySelector returned null, the assignment threw, and the exception aborted the function before reaching the line that updates the .dots page indicator — so the photo itself advanced (that line ran first) but the dots never moved, reading as "doesn't work." Removed the dead line entirely; verified live that the .dots on class now correctly moves with each click and no exception is thrown. (5) Merged the separate "Fahrzeugbilder" upload grid into "Bild der Übersicht", per explicit request: pick a view from the existing "Ansicht" dropdown, then tap the (now also click-to- upload) preview below it to upload/replace/delete that specific photo — reusing the same generic data-bildklick/bildMenuOffen popup plumbing already used by the wheel-photo tiles, not a new mechanism. bildInfo()'s existing winter-side-view special case (swaps in seitenansicht-winter.webp while winter tires are marked mounted) means the winter variant stays uploadable through the same control, gated by current tire season instead of a permanently visible separate slot. The now-unused BILDER_UPLOAD_SLOTS constant and the .bildgrid/ .bildslot/.bildslot-label/.carfix.mini CSS were removed as dead code; a new .bildmenu.ansicht positions the ersetzen/löschen popup anchored to the tile's bottom edge (not a fixed top offset like the other two menu variants) since this preview's height varies with the tile's own content instead of being a fixed small thumbnail. (6) Back arrow changed from red (--ios-tint) to the neutral headline color (--fg) in audi-dashboard-ios.css, matching every other navigation-color decision made this project (red stays reserved for destructive actions per Apple's HIG). Initially investigated but could not reproduce (see below for the real fix, found later the same day): user reported that hiding tab labels ("Beschriftung in der Menüleiste" off) makes the entire tab bar disappear. First pass traced the CSS cascade and the commit that added the sixth round's icon+label highlight and found it correctly scoped to :not(.ohne); live DOM/computed- style testing (toggle, click the label, navigate away/back, both container widths) checked #tabbar's and each .tab's own box — all stayed visible, sized, and classed correctly — so the bug looked unreproducible and was provisionally left alone. Deployed to audi_ha_test; wheel-photo squareness, Montiert position, back-arrow color, and the image-cycle dots all confirmed live via direct DOM/computed-style checks (no visual screenshot tool available this session — see running note below). Follow-up same day: user reported the color-scheme fix alone wasn't enough — dropdowns were "still very bright." Correct: a <select>'s opened option list is rendered by the browser/OS as a separate surface outside the page's render tree entirely — confirmed firsthand when a screenshot tool call actually hung for 30s trying to capture it, and even a real click-to-open attempt afterward produced no visible change in any screenshot. color-scheme only gets partial credit there; the reliable lever is that Chrome/Firefox/Edge do honor explicit background-color/color set directly on <option> elements for the popup rows. Added .feld select option{background-color:var(--canvas);color:var(--fg)} — deliberately --canvas, not --tile-2 (translucent in the night theme, would let the browser's own light backdrop show through). Verified via getComputedStyle on the actual <option> nodes (background-color: rgb(12,16,20), color: rgb(255,255,255) in night theme) since the popup itself is provably outside what any screenshot in this tool can capture — user should confirm visually on their own device. Also: "Montiert" and the wheel photo were requested swapped — photo now sits on the tile's right edge (.bildbox.radbild{margin-left:auto} inside its full-width wrapper), Montiert stays left-anchored, unchanged from the seventh round. Found the real tab-bar bug right after, from the user's own screenshot ("in this view i dont see any menu items" — a live repro of icon-only mode, this time actually screenshotted since the browser pane had recovered). The earlier live-testing pass checked #tabbar and .tab's own boxes and both looked fine — the actual defect was one level deeper and only visible by measuring the icon itself: .tabbar.ohne .tab span{display:none} (in both stylesheets) was meant to hide only the trailing label <span>${t.label}</span>, but the icon is also wrapped in a <span class="tabpille"> — a bare type selector span does not care about class, so the rule hid the icon's wrapper too. getBoundingClientRect() on the <svg> read {w:0,h:0} and getComputedStyle(.tabpille).display read "none" once actually checked — that was the missing measurement in the first pass. This is exactly the reported "whole menu bar disappears": in icon- only mode every tab lost both its label and its icon, leaving five empty buttons in an otherwise correctly-visible bar. Fixed with .tabbar.ohne .tab span:not(.tabpille){display:none} in both audi-dashboard.css and audi-dashboard-ios.css (the base file's copy was already shadowed by the ios one at matching specificity, but fixed too rather than left as a landmine). Verified live via screenshot: all 5 tab icons render correctly in icon-only mode, active tab still shows its filled pill.
  • Eighth round the same day (2026-08-16) — Reifen tile relabel + reflow, per explicit request: "Sommer"/"Winter" → "Sommerräder"/"Winterräder" (only the section headline text in vReifen()'s satz(); the singular "Sommerrad"/"Winterrad" photo label and the data-satz value compared against CAR.reifen.aktiv elsewhere were deliberately left untouched — the first is grammatically correct as-is for a single wheel photo, the second is an internal state key, not display text, and changing it would break the mount-toggle matching in the [data-satz] click handler). That headline is now an actual headline — new .tile-headline class (20px, var(--fg), same typographic family as the page .title one step down) replacing the small, muted .label styling every other in-tile section title uses. Reflowed so the headline+km sit above "Montiert" instead of the wheel photo overlaying it: new .radkopf-row (flex, justify-content:space-between) puts the headline/km block and the wheel photo side by side at the tile's top, both top-aligned; .montiert dropped position: absolute (no longer needs to float over the photo now that the photo isn't in that space) and became a normal-flow pill with margin-top under the headline block — simpler than the previous overlay hack, same 44×44pt invisible tap target via ::before. .bildbox.radbild's margin- left:auto (added last round to right-align it under its old block-flow parent) was removed since .radkopf-row's flex layout does that job now. Deployed to audi_ha_test, confirmed live via screenshot for both Sommerräder (mounted) and Winterräder (unmounted) tiles.
  • Fix remaining documentation drift (statistics claim, README gaps, obsolete TODO comment) — text-only changes; INSTALL.md's WLAN/TommiG1 drift and stale variable names were fixed 2026-08-12 (see section B); DESIGN_REVIEW_2026-08-13.md and REVIEW_main_2026-08-13.md list further drift not yet fixed (WLAN references still in SPECIFICATION.md, homeassistant/README.md, the profile template)
  • Harden profil_lesen() against missing/corrupt fahrzeugprofil.json — done on the umsetzung-datametric360 branch (2026-08-11), brought over by the 2026-08-13 merge. Returns None on a missing or malformed file instead of raising; all 5 call sites (backup.py ×2, fahrterkennung.py, frontend_veroeffentlichung.py, reifenzaehler.py, tankerkennung.py) already guard for None — verified by grep across the merged tree, no caller left unguarded (this was flagged as the merge's main risk in REVIEW_main_2026-08-13.md §"Was den Branch betrifft").
  • Decide whether the 3 audit leftovers get fixed here or only in DataMetric360 — decided 2026-08-11 (UMSETZUNGSPLAN.md Phase 2): swipe-delete keyboard fallback, popup keyboard access, and self-hosting Leaflet stay unfixed in this panel and are addressed only in DataMetric360.
  • Optional: persist the RAM-only states (trip start, fuel low-water-mark) — deliberately deferred; may become moot with the FMM003 switch
  • Upload vehicle photos to www/bilder/, set steuer.faellig (operational data, not code)

D) Once the panel is superseded

  • Archive audi-dashboard-app.js, don't delete (settled decision, architecture §1)

Working conventions (observed — keep them)

  • German is the project language: identifiers, comments, commits, UI texts. Exceptions: design-system/ uses English props/JSDoc (Claude Design audience) — and this file, which is English by rule.
  • Decisions are logged, including rejected ones (see the Traccar section of the architecture doc); superseded sections stay in place marked "ÜBERHOLT" rather than being deleted.
  • Numbers in de-DE format; font weights 300/400 only, never ≥600 (only those cuts exist).
  • Real vehicle/movement data stays local (data/ contents and receipt PDFs are gitignored).
  • Security principle: HA is never publicly exposed; only narrowly scoped surfaces (MQTT broker, proxy allowlist) may be exposed, each by explicit decision.