# AGENTS.md — Project state, review findings, open items, and working rules **Last updated: 2026-08-16** (merged the `umsetzung-datametric360` branch — companion app phases 1–10 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). 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`](https://github.com/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 1–10 built) 5. `AUDIT_2026-08-10.md` — accessibility/platform audit of the panel (partly done, rest below) 6. `bauauftrag.md` — **historical 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: 4. **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 }`. 5. **The model name appeared twice** — once as the badge image, once as text beside it. 6. **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 1–13). 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) - [x] Fix the Claude Design draft (RS 4, not RS 6) and extend it by the 16 missing sub-pages; then re-export to `design/` - [x] `companion-app`: set up Vite + React + Capacitor scaffold; wire `@audi-dash/ui` as a real dependency (possibly add a workspace/monorepo root) - [x] Implement the screens from the design draft on top of the existing `DataMetricApi` layer - [x] Add Audi assets (fonts/rings/badges) at implementation time from `homeassistant/www/` — **never** into `design-system/` - [x] Secure storage for the LLAT (iOS Keychain / Android Keystore via Capacitor plugin; the `ablageSetzen()` hook already exists) - [x] Onboarding: manual token paste (required); QR scan only if it stays simple (QR generated locally under HA `/local/`, architecture §3) - [x] Authenticated smoke tests of the data layer (reads, service calls, queue round-trip) against `audi_ha_test` with a real token - [x] 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 - [x] Define the reverse-proxy path allowlist (depends on final entity/service names) - [ ] Install/wire the FMM003; record firmware version (Codec JSON is firmware-dependent) - [x] 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) - [x] 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) - [x] 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. - [x] 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. - [x] 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. - [x] 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. - [x] 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. - [x] 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. - [x] 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`](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 ``. - [x] 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. - [x] 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. - [x] 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 `