Files
audi-app/AGENTS.md
T
tobias 7fc378f427 Übersicht: Wert und Kontext trennen statt "9,0 tkm/ 04/27"
Auf Nachfrage, wie Apple HIG und die Audi-CI die beiden Kacheln loesen
wuerden - beide laufen auf dieselbe Antwort hinaus.

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

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

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

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

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 13:54:27 +02:00

1447 lines
123 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# AGENTS.md — Project state, review findings, open items, and working rules
**Last updated: 2026-08-17** (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; a tenth round the same day ran a real full Apple HIG
audit (Navigation/Presentation/Selection & Input/Patterns categories, not just the sixth round's
5-page pass) and fixed the three findings that survived: closed a second, larger silent-failure gap
in `profilSpeichern()` itself (the shared save path for ~25 fields, previously an unhandled promise
rejection with zero user feedback), added a spinner to the "Lädt …" bootstrap screen, and gave the
Anzugsmoment/km-correction popups the same primary-button styling the Setup popup already used;
2026-08-17: fixed pull-to-refresh on Übersicht not working — the JS gesture logic was already
correct (verified via live pointer-event simulation), the real gap was a missing
`overscroll-behavior` on the scroll container letting the browser's own native overscroll compete
with it on real touch devices; same day: the fuel-receipt upload got a proper dialog — drag the PDF
in at the computer, paste it from the clipboard on the phone, file picker still available as a third
route — see section C; a twelfth round the same day removed `HECKKLAPPENSCHLOSS_SENSOR`, fixed a
half-cut-off image placeholder, gave the Inspektion its own "voraussichtlich am …" forecast, made
trips fully editable (new `audi_dashboard_fahrt_aktualisieren`, "Neue Fahrt" now offers every field
the detail page shows), aligned the page title with the tiles on phone width, and closed two
dead-ends in the bootstrap retry loop behind the reported hanging "Lädt …" screen; a thirteenth
round the same day replaced the trip form's editable "Dauer" with Start-/Ankunftszeit (duration is
now computed, not entered), capitalized the Art labels shown to the user, dropped the red styling
and hint text from the Arbeitsweg pill, and fixed a daylight-mode-only regression from round twelve
where the image placeholder's grey background had turned invisible-white; separately the same day,
`companion-app/` (the actual phone-app codebase, which had silently drifted six days behind the
panel) was brought back in sync with everything the panel gained since 2026-08-11 - see the
"Gap found while auditing..." entry near the end of section C; finally, the Übersicht's
Kilometerstand/Service tiles were redesigned after the owner asked how Apple HIG and Audi CI would
solve them - both answer the same way, and the merged "9,0 tkm/ 04/27" number field is gone).
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 110 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 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)
- [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 `<link>`.
- [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 `<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.
- [x] 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.
- [x] 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 input`s 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.
- [x] 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/`.
- [x] 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 → `FileReader``hass.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/`.
- [x] 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/`.
- [x] 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`).
- [x] 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.
- [x] 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.
- [x] 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.
- [x] 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.
- [x] Ninth round the same day (2026-08-16) — "Mein Audi" image-cycle tap, still broken after the
seventh round's `bildWeiter()` crash fix: reproduced with a genuine mouse click (not a JS
`.click()` call) via screenshot and found the actual cause was one level upstream. This docker
test container has zero uploaded car photos, so every gallery image is in `bild-fehlt` state,
and `.bildbox.bild-fehlt img{display:none}` makes the `<img id="carimg">` itself un-hit-testable
— a real tap lands on the `.platzhalter` div instead, which the old handler
(`e.target.id === "carimg"`) never matched, so nothing happened. First fix wrapped the gallery
image in a `data-bildgalerie` container that opened the upload picker when empty and advanced
when a photo was loaded; the user then clarified uploads should stay exclusively in Einstellungen
("Bild der Übersicht") — Mein Audi should only ever cycle. Settled on: the wrapper stays (needed
so taps land on something clickable regardless of `bild-fehlt` state) but always calls
`bildWeiter()`, no upload branch. Verified live with a real click (screenshot → click → recheck
`.dots`): the active dot correctly advances, no console errors beyond this environment's
pre-existing service-worker/404 noise.
Also this round: replaced the "Farbskala Ladezustand (AGM, Ruhespannung)" text-only label above
the battery voltage color scale with a small battery icon + "Ruhespannung" (new `.bv-skala-kopf`
flex row, same geometry as the header's `.sync` icon+text pattern). First attempt hand-drew a
generic outline battery glyph since no "battery" icon exists in this repo's local CI subset
(`design/assets/icons/`); the user then supplied the real `battery-12v-l.svg`/`battery-12v-s.svg`
files from the actual Audi CI icon set (not something this session has fetch access to — asked
the user rather than guessing a URL or fabricating an icon and calling it authentic). Added both
as `CI.battery12vL`/`CI.battery12vS` (verbatim paths, `l` on the 48-grid, `s` on the 24-grid,
matching the existing `poi-l`/`poi-s` size-variant convention); only the `s` variant is used here
via `ciSVG()` since this is a small 15px inline label icon.
Deployed to `audi_ha_test`, confirmed live via screenshot.
Also this round (user then supplied the real `edit-l.svg`/`edit-s.svg` too): added a manual
km-correction control to the Reifen tiles, per explicit request — a small pencil icon (Audi CI
`edit-s`, new `CI.editS`/`CI.editL`) to the left of each Sommerräder/Winterräder km figure, "the
counter shall add continuously to these km even if the user changes the km". The tricky part
was persistence: `configCarZuProfil()` *deliberately* strips `km` before every profile write
(see its own comment - km is "vom Backend fortgeschrieben", a stale browser copy must never
clobber a since-elapsed automatic increment), so simply mutating `CAR.reifen[k].km` and calling
`profilSpeichern()` like every other Reifen field does would silently drop the edit. Added a
dedicated backend service instead, `audi_dashboard_reifen_km_setzen(satz, km)` in
`reifenzaehler.py`, that only overwrites the stored counter itself and leaves
`reifen.referenz_odo_km` (the last-processed odometer reference) untouched - the existing
`kilometerstand_geaendert()` trigger always reads the counter fresh off disk before adding the
next delta, so continuing to accumulate from the corrected value required no other backend
change at all. New `kmOffen` state (which satz's correction field is open, mirroring the
existing `nmOffen`/`bildMenuOffen` single-open-popup pattern) plus `data-kmauf`/
`data-kmspeichern` handlers, styled like the existing `.montiert`/`.zahnrad` controls (44×44pt
invisible tap target via `::before`). Verified end-to-end: edited Sommerräder from 150→4200 km,
confirmed the pyscript service ran via the container's HA log, then did a full page reload
(forces a real backend re-fetch, not just optimistic local UI state) and the corrected value
was still there.
- [x] Tenth round the same day (2026-08-16) — a real full Apple HIG audit, not just the sixth
round's 5-page pass: read Tab Bars, Sidebars, Sheets, Alerts, Action Sheets, Toolbars/
Navigation Bars, Buttons, Pickers, Loading, Feedback, Privacy, and Undo/Redo directly off
developer.apple.com and cross-checked each against the actual code (not just CSS) — most of
the app already held up (5-tab bar, destructive-button placement, 44pt tap targets,
`prefers-reduced-motion` support, confirm-sheet button ordering all already correct). Three
real findings surfaced, all fixed and user-approved via `AskUserQuestion` before implementing:
(1) **A second, larger silent-failure gap than the one already fixed for receipt upload.**
`profilSpeichern()` — the shared save path for roughly 25 editable fields across the app
(Anzugsmoment, service book, SmartDeal, tax, insurance, oil change, pause time, backup
interval, start image, ...) — was `async` with no `try`/`catch` at any of its ~25 call sites,
so a failed backend write was an unhandled promise rejection with zero user feedback; the
popup had already closed and the UI already updated optimistically, so the change looked
saved when it silently wasn't. Wrapped the one call inside `profilSpeichern()` itself in
`try`/`catch`, reusing the existing `hinweis()` error-sheet pattern — one central fix instead
of ~25 call-site patches. `serviceRufen()` (the separate wrapper used by km-correction, manual
trip/fuel entry, tire-set switching, deletions, restart, ...) got the identical treatment.
Verified live by monkey-patching `hass.callService` to reject inside the running app (via
`host.hass.callService = () => Promise.reject(...)`), then triggering a real Anzugsmoment save
through the actual UI: before the fix, the popup silently closed with a stale "Uncaught (in
promise)" console entry and no user-visible sign anything went wrong; after the fix, a
"Speichern fehlgeschlagen" sheet appears with the real error text. A follow-up full reload
confirmed the failed test write never reached the backend (value reverted to the real stored
120 Nm) — the fix only adds feedback, it doesn't change persistence behavior.
(2) **"Lädt …" bootstrap screen got a progress indicator** (new `.lade-spinner` CSS, a plain
rotating ring) instead of bare static text, per HIG Loading guidance to show something moving
while content loads — notable here because this exact screen has a documented history of
genuinely hanging, so a spinner also helps a user tell "still loading" apart from "stuck".
Respects the existing global `prefers-reduced-motion` override (`*, *::before, *::after`
block already forces `animation-duration:.01ms` app-wide) with no extra code.
(3) **Anzugsmoment and km-correction popups now use `.aktion.primaer` on Speichern**, matching
the Setup and SmartDeal popups, which already distinguished their primary action — these two
were the only popups in the app stacking two visually-identical plain buttons.
All three verified live in `audi_ha_test` (version `1786916894`): spinner CSS confirmed via
`getComputedStyle` (16×16px, `border-radius:50%`, `animation-name:lade-spin`) since a genuine
race-condition-timed screenshot proved too fast to catch reliably; Speichern/Abbrechen
hierarchy confirmed via screenshot on both popups; error-sheet fix confirmed via the
monkey-patch test above. Synced to `installationspaket/`.
- [x] Eleventh round (2026-08-17) — "why can't I pull to refresh on Übersicht", investigated as a
real bug report rather than assumed working: live-tested the gesture by dispatching real
`PointerEvent`s through the exact `pointerdown`/`pointermove`/`pointerup` handlers in
`ereignisseVerdrahten()` (the `ptrY0`/`ptrDrag` logic) — every step fired correctly (drag
threshold, "Loslassen zum Aktualisieren" label swap, spinner, `datenAktualisieren()` call,
collapse), so the app's own JS/CSS was never the defect. Root cause instead: `main#view` (the
actual scroll container) had no `overscroll-behavior` set at all, so on a **real** touchscreen
the browser's own native overscroll/pull gesture can intercept or compete with the custom drag
before it reaches the threshold — this can't reproduce via desktop testing or synthetic pointer
events, only real touch hardware, which is exactly why the live test above passed while the
user's real phone didn't work. Fixed with `overscroll-behavior-y: contain` on `main#view` plus
`overscroll-behavior: none` on the outer `:host > div` wrapper, so a gesture leaving `main#view`
never chains up into the underlying Home Assistant page either. Re-ran the same synthetic
pointer-event test after deploying — gesture still fires identically end-to-end, confirmed via
`getComputedStyle` that `overscroll-behavior-y` computes to `contain`; no visual regressions,
no new console errors. Synced to `installationspaket/`. Not yet confirmed fixed on the user's
actual device — the code-level fix is the standard remedy for this exact symptom class, but ask
if it's still reproducing before assuming this is fully closed.
- [x] Beleg-Upload: Ziehen und Einfügen (2026-08-17) — "Beleg hochladen" opened the native file
picker directly, which is the wrong affordance for both of the user's actual routes: at the
computer the PDF should be draggable in, on the phone it arrives as a PDF copied out of a mail
and needs pasting. Replaced the immediate picker with a small dialog (`belegPopup` state,
`vBelegPopup()`, rendered into the existing `#overlay` alongside `sheetMarkup()`/
`vSetupPopup()`): a dashed drop area plus "Aus Zwischenablage einfügen" and "Datei auswählen"
(the old path, kept as the third route). The upload itself was extracted out of the click
handler into `belegDateiVerarbeiten()` so all three sources share one code path — including the
deliberate non-`serviceRufen()` error handling whose reasoning is unchanged (see its comment).
Notes worth keeping: `dragover` **must** `preventDefault()` or the browser just opens the PDF
itself and no `drop` ever fires; the drag/drop/paste listeners hang on `document`, not on the
drop zone, because the zone lives in the shadow DOM and a drag crossing its edge would
otherwise be lost — and because `render()` replaces the overlay's `innerHTML`, so per-element
listeners wouldn't survive. Phone pasting uses `navigator.clipboard.read()` (iOS shows its own
paste prompt) with the keyboard `paste` event as the desktop path. Non-PDFs are rejected with
an in-dialog message instead of being uploaded. Verified live in `audi_ha_test` by
instrumenting `hass.callService` and driving real `DragEvent`/`ClipboardEvent`/`KeyboardEvent`s:
drop → zone highlights (`darueber`), dialog closes, service called with the right filename and
base64 payload; paste → same; a dropped PNG → "Das war kein PDF." and no service call; Escape →
closes. Synced to `installationspaket/`.
**Nachbesserung am selben Tag, nach Fehlermeldung des Nutzers** ("copy paste from pc brings
error no pdf but it is the latest bill") — zwei echte Fehler, beide bestätigt:
(1) `navigator.clipboard.read()` (der "Aus Zwischenablage einfügen"-Knopf) gibt laut
Spezifikation **nur Text, HTML und Bilder** heraus. Eine im Explorer/Finder kopierte PDF-Datei
taucht dort grundsätzlich nicht auf - der Knopf konnte am Rechner also gar nicht funktionieren
und behauptete dann fälschlich, es liege kein PDF vor. Der Weg, der dort funktioniert, ist
Strg+V: das `paste`-Ereignis trägt die Datei in `clipboardData.files`. Die Meldung nennt jetzt
genau das, statt die Datei zu beschuldigen; die Ablagefläche sagt es von vornherein
("oder mit Strg+V einfügen", per `tastenkuerzelEinfuegen()` auf Mac Cmd+V).
(2) Die Erkennung hing allein am MIME-Typ (`type === "application/pdf"`) - eine aus dem
Explorer kopierte oder gezogene Datei kommt je nach Browser und Quelle aber mit **leerem**
`type` an und wurde dadurch abgelehnt. Neue Hilfsfunktion `istPdf()` prüft zusätzlich die
Dateiendung; live nachgestellt mit genau diesem Fall (`new File([...], "Tankbeleg_August.pdf",
{})`, `type === ""`) - vorher abgelehnt, jetzt korrekt hochgeladen.
(3) Auf Rückfrage des Nutzers ("why is it that way? computer has copy paste function") noch
einen Schritt weiter: Der Einfügen-Knopf wird am Rechner jetzt **gar nicht mehr angeboten**
(`zwischenablageKnopfSinnvoll()`, Prüfung auf `(pointer: coarse)`), weil er dort prinzipiell
nicht funktionieren kann - ein Knopf, der zwangsläufig in eine Fehlermeldung läuft, ist
schlechter als keiner. "Datei auswählen" wird dort zur primären Aktion; auf dem Telefon bleibt
alles wie zuvor. Der Unterschied dahinter, kurz: Strg+V ist eine bewusste Nutzergeste an die
Seite, deshalb bekommt die Seite den vollen Inhalt inklusive Dateien - `clipboard.read()` ist
die Seite, die von sich aus die Zwischenablage ausliest, und das wird aus Datenschutzgründen
stark eingeschränkt (sonst könnte jede Seite still mitlesen, was zuletzt kopiert wurde).
Live geprüft: Desktop zeigt nur noch "Datei auswählen [primaer]"/"Abbrechen", Mobil (375px,
`pointer: coarse`) weiterhin alle drei Knöpfe.
(4) **Auch Strg+V half am Rechner nicht** - vom Nutzer getestet und zurückgemeldet ("ctrl+v
doesnt work either, pc doesnt know where to paste"). Stimmt: Chrome stellt eine Einfüge-Geste
nur zu, wenn es ein Einfüge-Ziel gibt (Eingabefeld/contenteditable), und das Fenster hat
keines. Der Hinweis ist deshalb überall entfernt; am Rechner heißt es jetzt schlicht "PDF
hierher ziehen / oder unten auswählen", am Telefon "Beleg einfügen / das aus der Mail
kopierte PDF". Der `paste`-Listener bleibt als stille Zugabe für Browser, die das Ereignis
doch zustellen, wird aber nirgends mehr beworben; `tastenkuerzelEinfuegen()` wurde damit
verwaist und ist entfernt. **Am Rechner also bewusst nur zwei Wege: Ziehen und Dateiauswahl.**
- [x] Preis je Liter überall auf 2 Nachkommastellen (2026-08-17, Nutzerwunsch: "€/l for gas is
common to be displayed as x,xx €/l") — alle sieben Stellen von `de(..., 3)` auf `de(..., 2)`
umgestellt: Jahres- und Monatsschnitt in der Tankliste, Zeilenwert je Tankvorgang, "Preis je
Liter" im Einzelbeleg, die Live-Vorschau in beiden Tankformularen (statisch und im
`data-tankpreisfeld`-Handler) sowie die CSV-Spalte "€/l". Im CSV kein Informationsverlust: der
Wert ist aus den ebenfalls exportierten Spalten Liter und Kosten jederzeit neu berechenbar.
`companion-app` (Nachfolger-App) brauchte keine Änderung - deren `eur()` rundet ohnehin schon
auf 2 Stellen. Live geprüft: keine `de(..., 3)`-Aufrufe mehr in der Datei, Liste und
Einzelbeleg zeigen "2,07 €/l", "1,99 €/l" usw.
- [x] Twelfth round (2026-08-17) — six user-reported items, one of them a real dead-end in the
bootstrap:
(1) **`HECKKLAPPENSCHLOSS_SENSOR` removed for good**, the same treatment Türschloss/
Haubenschloss got on 2026-08-16: gone from `einstellungen.py` (attribute deleted, not
blanked), from `entitaeten.py`'s Setup catalog, and from `_sicherheitscheck()` in
`frontend_veroeffentlichung.py`. `HECKKLAPPE_SENSOR` (the lid itself) stays. Verified live:
"Geprüfte Punkte" no longer lists it, the Setup catalog no longer offers it.
(2) **Bild-Platzhalter in Einstellungen war halb abgeschnitten** — reproduced by measurement,
not by eye: the placeholder rendered 260px tall inside a 132px `overflow:hidden` box. Cause:
`bildMitPlatzhalter()` puts the size class (`carfix`) on the box, the `<img>` **and** the
placeholder `<div>`, and the desktop container query said `.carfix,.bildbox.carfix
{height:260px}` — the bare selector therefore also hit the placeholder, whose `position:
absolute;inset:0` is over-constrained by an explicit height (the height wins, `bottom:0` is
ignored). Scoped both rules to `.bildbox.carfix`; image and placeholder fill the box via
their own `100%` anyway. Verified: placeholder now measures exactly the box height.
(3) **Inspektion bekommt eine "voraussichtlich am …"-Zeile** wie der Ölwechsel, aber auf
Wunsch nur mit dem Datum. `oelwechselPrognose()` wurde dafür auf einen gemeinsamen Kern
`servicePrognose(stichwort, km, monate)` zurückgeführt; `inspektionPrognose()` nutzt
dieselben 30.000 km / 24 Monate wie `intervalle()`/`termine()`. Angezeigt in der
Service-Kachel auf "Mein Audi" (auch ohne Fahrzeugmeldung, anders als beim Ölwechsel - dort
nennt `terminText()` den generischen Termin schon "voraussichtlich", die Zahl stünde sonst
doppelt) und auf der Service-Seite. Live geprüft: beide Stellen zeigen "voraussichtlich am
20.10.2027", Ölwechsel unverändert.
(4)+(5) **"Neue Fahrt" hat jetzt alle Felder der Einzelfahrt, und die Einzelfahrt lässt sich
bearbeiten.** Neues gemeinsames `fahrtFelder()`/`fahrtFormularWerte()` (Datum, Startzeit,
Dauer, Art, Start-/Zielort, Start-/Endkilometer, Distanz) nach demselben Muster wie
`tankFelder()`/`tankFormularWerte()`, benutzt vom Erfassungsformular und vom neuen
"Werte bearbeiten" auf der Einzelfahrt-Seite (`fahrtBearbeitenOffen`, Vorbild `vFill()`).
Backend: `audi_dashboard_fahrt_manuell_anlegen` um dieselben Felder erweitert, neuer Service
`audi_dashboard_fahrt_aktualisieren`. Zwei Punkte, die dabei zählen: von Hand gesetzte Felder
landen in `edited_fields`, damit das Kilometerstand-Screening (§7.2) sie nicht später
überschreibt - und weil `_datensatz_aktualisieren()` genau diese Liste schützt, hätte ein
zweites Bearbeiten sonst nichts mehr bewirkt; dafür gibt es jetzt `profil.fahrt_bearbeiten()`
(`schutz=False`), der Schutz gilt gegen die Automatik, nicht gegen den Menschen. Fehlt die
Distanz, ergibt sie sich aus den beiden Kilometerständen (`km_quelle: "manuell"`, Status
"vollständig"). Gleich mitgezogen: die Art-Pille auf der Einzelfahrt schaltete bisher nur
lokal um und war nach dem nächsten Backend-Push wieder weg - sie ruft jetzt denselben
Service. **Merkposten pyscript:** Generator-Ausdrücke gibt es dort nicht
(`NotImplementedError: not implemented ast ast_generatorexp`, an `next((f for f in …), None)`
real gesehen) - Listen-Abstraktionen dagegen schon. Live end-to-end geprüft: anlegen,
bearbeiten, erneut bearbeiten, jeweils in `fahrten.jsonl` nachgesehen und nach Reload wieder
im Formular.
(6) **Titelzeile stand auf dem Handy 34px rechts von den Kacheln.** Derselbe Befund wie im
Design-Audit 2026-08-13, dort aber nur in der Desktop-Container-Query behoben: `.back` ist
auf Wurzelseiten unsichtbar (`opacity:0`), belegt aber Breite plus Gap. Die Regel
`.back:not(.on){width:0;…}` gilt jetzt auf allen Breiten und hebt per `margin-right:-4px`
auch den Gap auf; `.topbar` bekam links 16px statt 20px, passend zu `--seitenrand` von
`main#view`. Gemessen: Titel und Kachel beginnen auf dem Handy beide bei 16px, auf dem
Desktop beide bei 564px (dort blieben vorher 4px Rest). Detailseiten (`.back.on`) unberührt.
- [x] Ladebildschirm: der Takt darf nicht abreißen (2026-08-17) — Nutzerbericht "nach dem Neuladen
dreht sich nur 'Lädt …', erst ein Klick auf einen Menüpunkt zeigt die App". Beim Lesen des
Bootstraps fanden sich zwei echte Sackgassen, die genau dieses Verhalten erklären:
`nachladeAnstossen()` stieg bei `!HASS || !HASS.callWS` **ohne** neuen Timer aus, und
`datenLaden()` bei `!HASS` ebenso - danach lief nichts mehr von selbst, und der einzige
Ausweg war ein zufälliger hass-Push oder ein Menüklick (`go()` ruft `datenLaden()` mit auf,
genau die Abmilderung vom 2026-08-16). Beide planen jetzt die nächste Runde, statt
auszusteigen; `disconnectedCallback()` stoppt den Timer dafür sauber, `connectedCallback()`
stößt ihn wieder an. **Nicht reproduziert:** weder ein einfacher Reload noch ein
Container-Neustart mit sofortigem Reload haben den Hänger in `audi_ha_test` gezeigt (die App
war jedes Mal binnen ~2s da) - die Sackgassen sind im Code belegt, dass sie *die* Ursache
beim Nutzer waren, ist damit aber nicht bewiesen. Tritt es erneut auf: Browser-Konsole
erfragen, bevor weiter geraten wird.
- [x] Thirteenth round (2026-08-17, same day) — five more user-reported items on the trip form and
one theme bug found while verifying it:
(1) **"Dauer" replaced by "Ankunftszeit"; duration is no longer editable.** `fahrtFelder()`
now has Startzeit + Ankunftszeit inputs and a plain (non-input) duration display next to
them, computed by `fahrtZeitraum()`/`dauerText()` and kept live via a new
`data-fahrtzeitfeld` input handler (same "patch the one node, don't call render()" pattern
the existing `data-tankpreisfeld` handler uses, for the same reason: a full re-render would
steal focus from the field being typed in). An arrival time not later than the start time is
treated as crossing midnight (`+1 Tag`), matching how `_fahrt_beenden()` already handles a
real overnight trip. `fahrtFormularWerte()` derives `ts_start`/`ts_end` from the same
calculation - the backend never sees a separate duration field, so it can't disagree with
the times. Verified live: 09:00→10:45 shows "1 Std. 45 Min.", 23:30→00:15 shows "45 Min."
(crossed midnight correctly), and editing an existing trip's arrival time on the Einzelfahrt
page persisted the recalculated `duration_s` (7200s for a 10:00→12:00 edit, checked in
`fahrten.jsonl`).
(2) **Art values now display capitalized** ("Privat"/"Arbeitsweg") wherever shown to the user
- the trip-edit `<select>`, the trip-list row, and the Einzelfahrt pill - via a new
`artText()` helper; the stored value stays lowercase (`"privat"`/`"arbeitsweg"`), since it's
compared in several places (the pill, the "Arbeitsweg" statistic, the CSV export) and already
exists that way in every trip recorded so far. The CSV export itself was deliberately left
showing the raw value - a data file, not UI copy.
(3) **Arbeitsweg pill no longer red.** `.pill.work` (`color:var(--red)` / a translucent red
fill in the iOS overlay) is gone from both stylesheets along with the now-unused `work` class
on the button; the pill uses the same neutral styling regardless of Art, matching the request
that Arbeitsweg should look like Privat, not draw attention to itself.
(4) **"tippen zum Umschalten" hint removed** from under the Einzelfahrt pill, per explicit
request - the pill is still clickable, just without the caption.
(5) **Found while re-verifying the placeholder fix from round twelve: it broke daylight mode.**
`.bildbox.bild-fehlt .platzhalter{background:var(--tile)}` (added round twelve) reads fine in
Nacht theme, where `--tile` is a faint white overlay on a near-black canvas - but the iOS
overlay's Tag-theme block redefines `--tile:#FFFFFF`, identical to the canvas/most tiles
around it, so the placeholder box became invisible in daylight mode (measured live:
`background-color: rgb(255,255,255)` on both the placeholder and its surroundings). Switched
to `--ios-fill` (`rgba(120,120,128,.16)` in Tag theme, `.24` in Nacht) - the same token
`.feld select`/`.aktion` already use to mark "this is a surface", visibly grey in both themes.
Verified live in Tag theme: `background-color: rgba(120,120,128,.16)`, clearly a distinct box
against the white canvas (see screenshot taken during verification).
All five deployed to `audi_ha_test` (version `1787009000`) and synced to `installationspaket/`;
console showed only the pre-existing service-worker/404 noise from the missing vehicle photos,
nothing new.
- [x] **Gap found while auditing "what's needed for a real iPhone app" (2026-08-17), then closed
the same day:** `companion-app/src/screens/` hadn't been touched since 2026-08-11 — every
panel fix and feature from the six days since (HIG audit rounds, receipt upload, trip
editing, Inspektion forecast, ...) existed only in `homeassistant/www/audi-dashboard-app.js`.
User decision: port everything now, before any native build work (Android build itself
deprioritized/uncertain per the user - Paul has an Apple Developer Program and will help with
iOS signing). Audited both codebases field-by-field (not by diffing the panel's CSS/markup,
which doesn't transfer to `@audi-dash/ui` components) and found six real, mechanically
distinct gaps, all fixed:
1. **`AGM_RUHE_MAX_V` was still 13.2** in `Batterie.tsx` - lowered to 12.8, matching the
panel's 2026-08-16 fix (same rationale: above that is already alternator voltage).
2. **Dead `wlan_name` field** - `profilAdapter.ts` still read/wrote it and `Einstellungen.tsx`
showed it as an editable "WLAN des Fahrzeugs" field, even though WLAN-based trip detection
was fully removed from the backend on 2026-08-12 (replaced by FMM003 ignition). Editing it
had zero effect. Removed from the type, both directions of the adapter, the settings
screen, and both test fixtures.
3. **No Inspektion forecast** - `service.ts` only had `oelwechselPrognose()`. Refactored the
shared math into a `servicePrognose()` core (kept `oelwechselPrognose()`'s public
signature/behavior identical - all 9 existing tests still pass unchanged) and added
`inspektionPrognose()` (fixed 30.000 km / 24 Monate, no customizable interval like the oil
change has), wired into `Service.tsx` as a new "Eigene Prognose (Inspektion)" row. 3 new
tests.
4. **Arbeitsweg pill was red** - `Pill variant="work"` in `Fahrten.tsx`/`FahrtDetail.tsx`;
`@audi-dash/ui`'s own source comment says the variant literally "recolors border+text to
the accent red". Stopped passing the variant in both spots - same fix as the panel, no
change needed to the shared design-system component itself.
5. **Trip creation only took Beginn/Ende/Art** - the panel's fuller field set (Startort,
Zielort, Start-/Endkilometer, Distanz) didn't exist here. New shared
`src/screens/FahrtFelder.tsx` (type, defaults, backend-payload conversion, and the
presentational field group) used by both the create form (`Fahrten.tsx`) and a **new**
edit capability.
6. **`FahrtDetail.tsx` was entirely read-only** - no edit, no Art toggle after creation. Added
a "Bearbeiten" toggle reusing `FahrtFelder.tsx`, backed by a new `DataMetricApi
.fahrtAktualisieren()` (mirrors the existing `tankvorgangAktualisieren()` pattern) calling
the backend's `audi_dashboard_fahrt_aktualisieren` service (built this session on the panel
side, so the backend contract already existed and needed no changes).
**Explicitly investigated and found NOT to apply** (worth recording so it isn't re-attempted):
price-per-litre rounding (companion-app's `eur()` already always renders 2 decimals - the
3-decimal bug the panel had never existed here); pull-to-refresh `overscroll-behavior` (no
native pull gesture implementation here to fix); the panel's drag/paste/file-picker receipt
dialog (solves a **desktop browser clipboard** problem that doesn't exist in a Capacitor
native app - the plain `<input type=file>` already in `TankDetail.tsx` gets iOS's native
Files/Photos picker for free, which already covers "a PDF saved from Mail"); every
Setup-menu/entity-mapping feature (HA-panel-only concept, the companion app consumes the
backend's already-resolved published state and has no equivalent screen, nor should it);
the many purely cosmetic HIG-audit CSS fixes (icon fills, chevrons, tab-bar highlight
bugs, ...) - those target the panel's own hand-written markup/CSS, not
`@audi-dash/ui`'s components, and don't transfer.
Verified after every change: `npm run typecheck` (clean throughout), `npm run test` (95/95
passing, up from 90 - added interaction tests for the new create-form fields and the
edit-toggle pre-fill, plus 3 for `inspektionPrognose()`), `npm run build` (succeeds). Also
corrected a stale README claim ("Capacitor-Hülle und sichere Ablage" both listed as open) -
the secure-storage adapter is actually fully wired (`ablageNativ.ts`, activated from
`main.tsx`) and only the native `ios/`/`android/` platform folders themselves (deliberately
gitignored, regenerated via `npx cap add`) are the missing piece, and that step needs a Mac.
- [x] Übersicht: Kilometerstand-/Service-Kacheln neu gestaltet (2026-08-17) — Nutzer fragte, wie
Apple HIG und die Audi-CI das lösen würden. Befund: die Ölwechsel-Kachel presste zwei
verschiedene Größen in **ein** Zahlenfeld (`teile.join("/ ")``9,0 tkm/ 04/27`) — zwei
Einheiten, davon eine erfundene (**„tkm" schreibt niemand**, und die Mein-Audi-Servicebox
nannte dieselbe Zahl schon immer „9.029 km", d. h. dieselbe Tatsache war an zwei Stellen
unterschiedlich formatiert), zwei Schrägstriche mit zwei Bedeutungen, und auf 375 px
zweizeilig umgebrochen. Beide Regelwerke laufen hier auf dieselbe Auflösung hinaus, live
nachgelesen: Apple HIG *Layout* („Make essential information easy to find by giving it
sufficient space… don't obscure it by crowding it with nonessential details") und
*Typography* („Adjust font weight, size, and color… to help people visualize hierarchy");
die Audi-CI definiert `.fig` (Audi Type Wide 300) ausdrücklich als *„large numeric
figures"* — **eine** Zahl, nicht zwei. Umgesetzt: Wert und Kontext getrennt
(`9.029 km` groß, `vsl. 04/2027` als leisere Zweitzeile), echte Einheit statt „tkm", und
die Kachel ist jetzt ein `button.tile.tilebtn` mit `.go`-Chevron nach `service:x` — vorher
eine Sackgasse, obwohl die Service-Seite existiert. `mmjj()` wurde dadurch verwaist (nur
noch das volle Jahr wird gebraucht) und ist durch `mmjjjj()` ersetzt. Die HU-/Inspektions-
Zweige sind mit dem Ölwechsel-Zweig zusammengefallen, weil sie nach der Trennung dieselbe
Form haben. Live geprüft (Tag und Nacht, 375 px): beide Kacheln einzeilig, Tippen öffnet
Service mit aktivem Zurück-Pfeil.
**companion-app hatte diesen Fehler nicht** — dort standen die Termine schon als
Label/Wert/Zusatz-Zeilen. Stattdessen dort der benachbarte Gruppierungsfehler behoben: die
zwei Service-Zeilen hingen *innerhalb* der Kilometerstand-Kachel (zwei Termine unter einer
Überschrift, die nichts mit ihnen zu tun hat) und führten nirgendwohin. Jetzt eigene,
antippbare `Tile variant="button" chevron``service`. Geprüft: typecheck sauber, 95/95
Tests, Build erfolgreich.
- [ ] 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)
- [x] 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").
- [x] 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.