fb38c297bd
The "grey box around the icons" report turned out not to be a defect: the box on "10.000 km"/"1 Jahr"/"15" is the wanted pattern, and every value the user can change should carry it. All .feld inputs and selects now get the grey iOS fill; inputs with their own visual language (checkbox/radio/range/file) keep the transparent base rule, so the switches are unaffected. Fuel stations are now displayed as "Shell, Pascalstr. 8, Ingolstadt" instead of the bare operator name. New _marke()/_ist_strasse()/_tankstelle() helpers are shared by both parser paths so Shell and non-Shell receipts format the same. The brand is only matched against the receipt header - searching the whole text would let "Total" hit a totals line. Missing parts are dropped instead of leaving empty comma slots, and without a known brand the operator name takes its place. station_address still carries the full street and postcode. test_bekannte_stationen updated to the new format; suite stays green. installationspaket/ is versioned from now on (user request). It contains no real vehicle data - only the example profile with empty FIN/plate placeholders. Keep it in sync whenever pyscript/ or www/ changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
844 lines
68 KiB
Markdown
844 lines
68 KiB
Markdown
# AGENTS.md — Project state, review findings, open items, and working rules
|
||
|
||
**Last updated: 2026-08-16** (merged the `umsetzung-datametric360` branch — companion app phases
|
||
1–10 done, see `UMSETZUNGSPLAN.md`; 2026-08-13: cleaned up remaining EU Data Act residue, fixed
|
||
oversized toggle switches, and fixed a desktop-layout audit (settings button / rings logo / popups
|
||
overflowing past the capped content column) — see `DESIGN_AUDIT_2026-08-13.md`; 2026-08-16: fixed
|
||
five user-reported bugs — removed Türschloss/Haubenschloss checks, fixed a translucent confirm-
|
||
sheet, added a flespi-style split lat/lon location source, hardened the "Lädt …" bootstrap race,
|
||
and added a generic (station-independent) fuel-receipt parser fallback; later the same day: removed
|
||
the now-unused combined `STANDORT_TRACKER` field entirely (flespi never provides it), closed a real
|
||
silent-failure gap in the receipt-upload error path, and replaced the vehicle's map marker with a
|
||
two-tone pin+car icon — see section C; a third round the same day found and fixed the long-standing
|
||
"fragmented Leaflet rendering" defect at its root — the Leaflet stylesheet was loaded into
|
||
`document.head` and therefore never reached the panel's shadow root — plus light-only map tiles,
|
||
the CI `poi-car` marker, the red frame on list rows, the select-field grey box, and two more
|
||
generic-receipt-parser gaps). This file is the entry point for every new agent
|
||
session: what this repo is, what is finished, what is missing, and how to work here. Detail lives in
|
||
the linked documents — this file points, it does not duplicate.
|
||
|
||
> **Maintenance rule (binding):** whenever you change this repo in a way that affects anything
|
||
> recorded here — completing an open item, making or reversing a decision, adding/removing a
|
||
> component, discovering a new gap — **update this file in the same session** (tick the checkbox,
|
||
> adjust the status table, bump the "Last updated" date). This file must always be written in
|
||
> **English**, even though the rest of the project is German.
|
||
|
||
**How this file is loaded:** Claude Code does not read `AGENTS.md` natively; the root `CLAUDE.md`
|
||
imports it via `@AGENTS.md` (official recommended pattern). Other agents (Codex, Cursor, Copilot)
|
||
read `AGENTS.md` directly. Edit content here, not in `CLAUDE.md`.
|
||
|
||
---
|
||
|
||
## Working rules
|
||
|
||
### Karpathy guidelines (adopted as project standard)
|
||
|
||
From [`multica-ai/andrej-karpathy-skills`](https://github.com/multica-ai/andrej-karpathy-skills),
|
||
behavioral rules derived from Andrej Karpathy's observations on LLM coding failures. They bias
|
||
toward caution over speed — for trivial tasks, use judgment.
|
||
|
||
1. **Think before coding.** Don't assume. Don't hide confusion. Surface tradeoffs. State
|
||
assumptions explicitly; if multiple interpretations exist, present them — don't pick silently.
|
||
If a simpler approach exists, say so. Push back when warranted. If something is unclear, stop
|
||
and ask.
|
||
2. **Simplicity first.** Minimum code that solves the problem. Nothing speculative: no features
|
||
beyond what was asked, no abstractions for single-use code, no unrequested configurability, no
|
||
error handling for impossible scenarios. If 200 lines could be 50, rewrite.
|
||
3. **Surgical changes.** Touch only what you must. Don't "improve" adjacent code, comments, or
|
||
formatting; don't refactor what isn't broken; match existing style. Remove only orphans **your**
|
||
change created; mention (don't delete) pre-existing dead code. Every changed line should trace
|
||
directly to the request.
|
||
4. **Goal-driven execution.** Define success criteria, loop until verified. "Fix the bug" → write a
|
||
test that reproduces it, then make it pass. For multi-step tasks, state a brief plan with a
|
||
verify step per item.
|
||
|
||
### Claude Code practices (current guidance, 2026)
|
||
|
||
- Keep this file **concise and specific** — instructions concrete enough to verify ("run X before
|
||
Y"), markdown headers/bullets, no dense prose. Context files reduce adherence as they grow;
|
||
target roughly ≤200 lines of actual instruction.
|
||
- Don't record here what the code or git history already shows (directory dumps, dependency
|
||
lists) — record pitfalls, rationale, decisions, and conventions that differ from defaults.
|
||
- Conflicting instructions get picked arbitrarily — when updating, **remove** superseded rules
|
||
rather than stacking corrections.
|
||
- Verify loading with `/context` (file must appear under **Memory files**); edit via `/memory`.
|
||
- **This file (plus the session's task list) is the source of truth for what's done — not a leftover
|
||
plan-mode file.** A `~/.claude/plans/*.md` file surfaced by the system as "not yet complete" can be
|
||
stale: plan files aren't marked complete when their work finishes. Before claiming a feature is
|
||
unbuilt (e.g. "the Setup menu isn't implemented yet"), check the `[x]`/`[ ]` items below and/or the
|
||
task list — trust those over an old plan file's own claim about its status. (Caused a real mistake
|
||
2026-08-12: told the user the Setup menu wasn't built yet, when it had shipped hours earlier in
|
||
the same project — see the "Setup menu" `[x]` entry further down.)
|
||
|
||
---
|
||
|
||
## What this project is
|
||
|
||
Private replacement for the Audi *connect plug & play* app (discontinued end of 2026), for exactly
|
||
one vehicle (**Audi RS 4 Avant competition**), single user, self-hosted on Home Assistant, access
|
||
via Tailscale only. Read-only toward the vehicle — no remote control. Covers: vehicle status, trip
|
||
log, fuel log with Shell receipt parsing, service forecasting, insurance/tax, tire management.
|
||
|
||
**License rule (hard):** Audi Type fonts, four-rings SVG, and model-badge SVGs are cleared **only
|
||
for this one private, unpublished installation**. Therefore `design-system/` is brand-free (it gets
|
||
uploaded to Claude Design), while the HA panel and the future sideload-only app may use the real
|
||
assets. Never mix these the other way around.
|
||
|
||
**Reading order for a new session:**
|
||
1. This file (overview + open items)
|
||
2. `UMSETZUNGSPLAN.md` — the step-by-step execution plan for all open items (13 phases with
|
||
commands and acceptance criteria); when working on an open item, follow the plan's phase
|
||
3. `SPECIFICATION.md` — authoritative for the HA panel, incl. §7 "Known Gaps"
|
||
4. `COMPANION_APP_ARCHITECTURE.md` — authoritative for DataMetric360 (decided, phases 1–10 built)
|
||
5. `AUDIT_2026-08-10.md` — accessibility/platform audit of the panel (partly done, rest below)
|
||
6. `bauauftrag.md` — **historical only**; code has diverged (see SPECIFICATION.md §7)
|
||
|
||
## The three projects in this repo
|
||
|
||
| Area | What | Status |
|
||
|---|---|---|
|
||
| `homeassistant/` | HA panel (`panel_custom`): pyscript backend + vanilla-JS frontend | ✅ **finished, in use** — to be replaced by the app |
|
||
| `testumgebung/` | Script that rebuilds a throwaway Home Assistant with the real backend | ✅ new, reproducible |
|
||
| `design-system/` | React component library `@audi-dash/ui`, brand-free, feeds Claude Design | ✅ done as a kit (20 components, 1,690 lines) |
|
||
| `companion-app/` | **DataMetric360** — successor app (web, PWA, native iOS/Android via Capacitor, HA iframe); will **replace** the panel | ✅ **all 21 screens built and tested**; runs natively on iOS with real data |
|
||
| `design/` | Export of the Claude Design draft for DataMetric360 | 🚧 interim and now behind the code — screens were derived from the old panel instead (owner's decision) |
|
||
|
||
Root files: `dashboard-muster*.html` = original static prototype (superseded, reference only),
|
||
`bauauftrag.md`/`.html` = original build brief (historical), `DESIGN_BRIEF_DATAMETRIC360.md` = the
|
||
prompt for the Claude Design project.
|
||
|
||
**`homeassistant/` in one paragraph:** 10 pyscript scripts + 5 modules (`pyscript/modules/`); trip
|
||
detection via the FMM003 ignition sensor with pause tolerance (the iPhone WLAN sensor is gone — see
|
||
section B), two-stage trip completion via HA history screening (odometer often updates only on the
|
||
next trip), fill-up detection on fuel-level rise, Shell PDF parser (`data/shell_beleg_parser.py`,
|
||
subprocess, the only tested part of the repo), tire km counter, backup, self-update, image
|
||
management. Frontend: one file `www/audi-dashboard-app.js`
|
||
(~4,000 lines, custom element, no framework/bundler), 5 tabs + ~19 detail routes, cache-busting via
|
||
`audi-dashboard-version.json` + loader stub. Entity IDs are assigned **in the UI** (Settings →
|
||
Fahrzeug einrichten → Setup), stored as overrides in `data/entitaeten.json`;
|
||
`pyscript/modules/einstellungen.py` holds only the built-in defaults. Deploy: `update.ps1`
|
||
(robocopy to Samba share) or — still inactive — self-update from git.
|
||
|
||
**`companion-app/` — what exists:** the full app. Data layer (`src/api/`: REST, WebSocket with
|
||
reconnect backoff, persistent offline write queue, credential storage), domain logic
|
||
(`src/daten/`: profile adapter, statistics, service forecast, data context), all 21 screens
|
||
(`src/screens/`), Audi assets (`src/assets/audi/`), PWA manifest and icons. Verified by 90 unit
|
||
and render tests plus 9 checks against a live Home Assistant. `npm run dev` in the repo root
|
||
starts it; `testumgebung/aufsetzen.sh` provides the server side.
|
||
|
||
**DataMetric360 architecture (short — details in `COMPANION_APP_ARCHITECTURE.md`):**
|
||
- Future data source: **Teltonika FMM003** on the CAN bus, fully replacing the iPhone WLAN sensor
|
||
and the VAG integration.
|
||
- **Current data path (since 2026-08-12): FMM003 → flespi (native Codec8/TCP, IMEI auth) → HA.**
|
||
Both earlier directions are dropped: Traccar first, then the self-hosted MQTT/TLS broker — the
|
||
router's port-forward turned out to be unusable. The decision logs deliberately remain in the
|
||
architecture doc, marked ÜBERHOLT. Details and open steps in section B below.
|
||
- Frontend access: HA REST + WebSocket with a long-lived access token (no `hass` object).
|
||
- External access: Cloudflare Tunnel + reverse proxy with a path allowlist; HA itself stays
|
||
unreachable. Domain **`datametric360.app`** registered (all-inkl, 2026-08-11).
|
||
- Distribution: **sideload only** (license reason) — hence real Audi assets are allowed there.
|
||
- Trip detection moves to FMM003 ignition; the pause-tolerance feature
|
||
(`fahrten_pausenzeit_min`) is a deliberate product decision and must be preserved.
|
||
|
||
---
|
||
|
||
## Review findings (2026-08-11)
|
||
|
||
### Design/CI review of `main` (2026-08-13) — see `DESIGN_REVIEW_2026-08-13.md`
|
||
|
||
Code review of all UI layers plus headless-browser screenshots of the panel (mock `hass`),
|
||
checked against the repo's own Audi-CI rules. Headlines: the iOS overlay
|
||
(`audi-dashboard-ios.css`) overrides the documented palette/radii, adds shadows/blur and turns
|
||
red into a fill color **without any recorded decision** (visible e.g. as a red-filled "Abbrechen"
|
||
next to a black "Speichern" in the setup popup); `design-system/` still carries pre-audit state
|
||
(old low-contrast `--fg3 #657081`, no focus styles at all, 13.5px inputs, 34px icon buttons);
|
||
the `design/` export's RS-6 error extends into the technical sample data (73 l tank, 285/30 R22,
|
||
FIN series 4G). Four browser-verified rendering defects: switches stretched by
|
||
`.feld label{flex:1 1 auto}` (state display becomes ambiguous), double "offen" in the trip list,
|
||
empty oil-change tile when the service book is empty, and fragmented Leaflet rendering in all
|
||
three maps (✅ **fixed 2026-08-16** — root cause was Leaflet's stylesheet being loaded into
|
||
`document.head`, which never reaches the panel's shadow root; see the third-round entry in
|
||
"Open items"). Positives: the 300/400 font-weight rule and de-DE
|
||
formatting hold everywhere; `design-system/` is verifiably brand-free.
|
||
|
||
### HA panel — known gaps (most also in SPECIFICATION.md §7)
|
||
|
||
- **GPS is dead schema for trip records specifically:** `start_lat/lon`, addresses, `route`,
|
||
`avg_speed_kmh` on individual trips never populated. The trip-detail map draws a **fabricated**
|
||
line via `fakeTrack()` (`audi-dashboard-app.js:414`) — not a real track. (Unrelated: the
|
||
Übersicht's live Standort-Kachel is no longer in this state — `STANDORT_TRACKER` now points at
|
||
a real `device_tracker`, see section B/C. This item is only about per-trip route data, which
|
||
nothing currently populates.)
|
||
- **No GPS fallback in trip completion:** trips without an odometer match stay `status="offen"`
|
||
forever (`modules/fahrtabschluss_logik.py:16-20`).
|
||
- **RAM-only state:** running trip (`_fahrt_start_ts`) and fuel low-water-mark
|
||
(`_tiefststand_pct`) do not survive an HA restart. Deliberately deferred hardening.
|
||
- **Inactive features:** `UPDATE_REPO_URL = ""` (self-update inactive) in
|
||
`pyscript/modules/einstellungen.py`. `www/bilder/` has no vehicle photos (all slots show
|
||
placeholders); `steuer.faellig` unset. (`BATTERIE_SENSOR` was the same kind of gap until
|
||
2026-08-12 — now wired to the FMM003's `external_power_voltage`, see section B/C below. Several
|
||
*other* fields are newly inactive since the same date for a different reason — the
|
||
`TommiG1/HA_VAG-EU-Data-Act` integration they depended on was dropped: `KM_SENSOR`,
|
||
`TANK_SENSOR`, `RANGE_SENSOR`, `REFRESH_BUTTON`, all door/window/lock sensors, oil-change/
|
||
inspection sensors. All are configurable again via the new Setup menu, see section C.)
|
||
- **Robustness:** `profil_lesen()` in `modules/profil.py` does not handle a missing/corrupt
|
||
`fahrzeugprofil.json` — all callers throw.
|
||
- **Tests:** only the receipt parser is tested (`data/tests/test_shell_beleg_parser.py`, 10 real
|
||
receipts) — and its test PDFs are gitignored, so a fresh clone can't run it. Backend and
|
||
frontend: no tests, no CI.
|
||
- **Leaflet via CDN:** trip map needs public internet in addition to the Tailscale tunnel.
|
||
|
||
### Documentation drift ✅ FIXED 2026-08-11 (kept as a record of what was wrong)
|
||
|
||
- `homeassistant/README.md:99`, `INSTALL.md:204`, and the header comment
|
||
`audi-dashboard-app.js:13-15` claim the statistics view shows sample numbers — **false**;
|
||
`vStat()` computes real values from `TRIPS`/`FILLS`.
|
||
- `homeassistant/README.md:110` mentions the removed 97% full-tank rule (removal documented in
|
||
`belegverarbeitung.py:18-20`); the README file list omits 5 pyscript files.
|
||
- Obsolete comment `belegverarbeitung.py:41` ("TODO: Datei ablegen" — file has long existed).
|
||
|
||
### Audit leftovers — two of three now fixed in the new app
|
||
|
||
- ✅ Swipe-to-delete without a gesture-free fallback — **fixed in the app**: every list row also
|
||
carries an always-visible "…" menu (`companion-app/src/screens/Zeilenmenue.tsx`). Still open in
|
||
the old panel, which is being replaced anyway.
|
||
- ✅ Leaflet from a CDN — **fixed in the app**: bundled from node_modules as a lazy chunk.
|
||
- 🟡 Popup close-by-tap-outside in the old panel: still unchecked (the app uses the library's
|
||
Popup, which is keyboard-reachable).
|
||
- Audit's own note: these three may be better done in DataMetric360 than retrofitted — the panel
|
||
gets replaced anyway.
|
||
|
||
### companion-app / design-system ✅ RESOLVED 2026-08-11
|
||
|
||
- **Not wired together** — fixed: an npm workspace in the repo root links `@audi-dash/ui` into the
|
||
app as a real dependency.
|
||
- Both packages: no `node_modules`, no `dist` — smoke tests need `npm install` first
|
||
(design-system additionally `npm run build`; `scripts/smoke.mjs` imports from `../dist/`).
|
||
- No unit tests, no Storybook (substitute: SSR smoke over 21 cases in design-system).
|
||
- `design/` export (2026-08-11) has two known, already-commissioned fixes not yet in the export:
|
||
shows **RS 6 instead of RS 4**, and **16 sub-pages** the HA panel already has are missing
|
||
(list in `design/README.md`).
|
||
|
||
---
|
||
|
||
### Found while building (2026-08-11) — all fixed
|
||
|
||
Three defects that only surfaced by running against a real Home Assistant, not by reading code:
|
||
|
||
1. **The data layer declared field names the backend never sends.** `Fahrzeugstatus` had
|
||
`tank_prozent`/`sicher_abgestellt`/`sicherheit`; the backend writes
|
||
`tankprozent`/`gesichert`/`sicherheitscheck`. Every screen would have read `undefined` without
|
||
anything failing. `technik`/`ausstattung` were typed as objects but are arrays.
|
||
2. **The profile adapter handed out live references into the raw profile.** Editing a form would
|
||
have silently mutated the baseline and broken the promise never to overwrite the
|
||
backend-maintained tire odometer. Sections are copied now.
|
||
3. **`design-system` still carried the pre-audit `--fg3: #657081`** (3.0:1 on `--tile`, fails WCAG
|
||
AA) that the panel had already fixed to `#8a94a3`. The new app would have inherited a
|
||
already-solved contrast defect.
|
||
|
||
Also: two TypeScript parameter properties in the data layer broke Node's strip-only mode, which is
|
||
what the smoke scripts run on — rewritten as plain fields.
|
||
|
||
Three more that only screenshots revealed — nothing failed, the pixels were simply wrong:
|
||
|
||
4. **The vehicle block on the home screen was invisible.** `overflow: hidden` sets a flex item's
|
||
automatic minimum size to 0, so once the page was taller than the screen, flex-shrink squashed
|
||
the block to zero height and took model name, badge and plate with it. Fixed with
|
||
`.dm-inhalt > * { flex: none }`.
|
||
5. **The model name appeared twice** — once as the badge image, once as text beside it.
|
||
6. **Filenames overflowed the gallery thumbnails.**
|
||
|
||
And one in the backend, found by watching the log against a current Home Assistant: the odometer
|
||
screening called `urlopen` directly, which HA aborts as a blocking call since 2026.8. Every trip
|
||
stayed without a distance, visible only as a warning. Now runs through `task.executor`.
|
||
|
||
**CORS is a real constraint for this app.** `cors_allowed_origins` did not take effect on HA
|
||
2026.8 (preflight 403 even same-origin). Two consequences, both handled: the web build is served
|
||
from Home Assistant itself (`/local/dm360/`, same origin — the planned deployment anyway), and the
|
||
native hull enables `CapacitorHttp`, which routes fetch through native HTTP where CORS does not
|
||
apply. Verify this again at commissioning if the app ever moves to a separate hostname.
|
||
|
||
**A self-written QR encoder was discarded.** It disagreed with a reference implementation on 1239
|
||
of 3249 modules — the code would have been unreadable. `homeassistant/www/dm360-qr.html` now uses a
|
||
vendored MIT library served from Home Assistant itself, which satisfies the actual requirement
|
||
(no network call, token never leaves the local network) and round-trips correctly.
|
||
|
||
## Pending: this branch has diverged from `main` (noted 2026-08-13)
|
||
|
||
`ha_install.md` (root) plans the move from pyscript to a **native HA integration** with a config
|
||
flow — UI-only setup, no YAML, and updates via a self-reporting `UpdateEntity` against the Gitea
|
||
repo (HACS is GitHub-only, so it is not an option). All APIs in it were verified against the
|
||
running 2026.8.1 instance. It also records which review findings that move eliminates by design.
|
||
|
||
A full review of `main`'s 18 new commits is in `REVIEW_main_2026-08-13.md` — 15 findings, the
|
||
three most serious in the new setup menu (saving with an unloaded catalogue wipes the whole
|
||
mapping; "reset" has no effect on 15 of 17 fields; all four list positions get the same sensor,
|
||
which makes "securely parked" report safe while three doors were never checked).
|
||
|
||
`main` has moved 18 commits ahead of `umsetzung-datametric360` (FMM003 switch, sensor-mapping setup
|
||
menu, iOS/large-screen overlay from Claude Design). **Deliberate decision: do not merge yet** — the
|
||
owner keeps working on `main` first.
|
||
|
||
Three files conflict (`AGENTS.md`, `homeassistant/INSTALL.md`,
|
||
`homeassistant/pyscript/fahrterkennung.py`); four more are touched by both sides but merge cleanly.
|
||
|
||
**One hazard that a clean merge will not catch.** This branch changed `profil_lesen()` in
|
||
`homeassistant/pyscript/modules/profil.py` to return `None` when the profile file is missing or
|
||
corrupt, and guarded all seven call sites that existed here. `profil.py` is untouched on `main`, so
|
||
it merges silently — but `main`'s call sites (`fahrterkennung.py`, `tankerkennung.py`,
|
||
`reifenzaehler.py`, `modules/frontend_veroeffentlichung.py`) do **not** guard against `None` and
|
||
would raise `AttributeError` on a missing profile instead of logging a clear error.
|
||
|
||
When merging: take `main`'s FMM003 version of `fahrterkennung.py`, then re-apply the `None` guard to
|
||
every remaining `profil.profil_lesen()` call site.
|
||
|
||
## Open items
|
||
|
||
Execution order, exact steps, and acceptance criteria for every item below live in
|
||
`UMSETZUNGSPLAN.md` (phases 1–13). Additional decisions of 2026-08-11: **no Electron** (Capacitor
|
||
wraps the web app for iPhone; a PWA home-screen install is the accepted intermediate step) and
|
||
**no separate backend** (the app talks to the HA REST/WebSocket API directly).
|
||
|
||
### A) Build DataMetric360 (the big block)
|
||
|
||
- [x] Fix the Claude Design draft (RS 4, not RS 6) and extend it by the 16 missing sub-pages;
|
||
then re-export to `design/`
|
||
- [x] `companion-app`: set up Vite + React + Capacitor scaffold; wire `@audi-dash/ui` as a real
|
||
dependency (possibly add a workspace/monorepo root)
|
||
- [x] Implement the screens from the design draft on top of the existing `DataMetricApi` layer
|
||
- [x] Add Audi assets (fonts/rings/badges) at implementation time from `homeassistant/www/` —
|
||
**never** into `design-system/`
|
||
- [x] Secure storage for the LLAT (iOS Keychain / Android Keystore via Capacitor plugin; the
|
||
`ablageSetzen()` hook already exists)
|
||
- [x] Onboarding: manual token paste (required); QR scan only if it stays simple (QR generated
|
||
locally under HA `/local/`, architecture §3)
|
||
- [x] Authenticated smoke tests of the data layer (reads, service calls, queue round-trip)
|
||
against `audi_ha_test` with a real token
|
||
- [x] Offline UX per design brief (offline marker, visible pending queue)
|
||
|
||
### B) Infrastructure / commissioning (partly waits for FMM003 hardware)
|
||
|
||
- [ ] Switch `datametric360.app` nameservers at all-inkl to Cloudflare ("full setup") —
|
||
prerequisite for the tunnel; domain carries nothing else, so this is consequence-free
|
||
- [ ] Decide hostname split (app on apex + API on `api.` subdomain, or vice versa)
|
||
- [ ] Choose reverse proxy (Nginx Proxy Manager vs. Traefik) — **can be done before hardware**
|
||
against the existing HA API
|
||
- [x] Define the reverse-proxy path allowlist (depends on final entity/service names)
|
||
- [ ] Install/wire the FMM003; record firmware version (Codec JSON is firmware-dependent)
|
||
- [x] Generate TLS certificates for Mosquitto + device (small private CA) — done 2026-08-11, 10-year
|
||
validity; Mosquitto configured (`certfile`/`keyfile`/`cafile`/`require_certificate: true`).
|
||
Remaining: upload root/client cert/key to the FMM003 Security tab — filenames must end in
|
||
`.pem`/`.pem.crt`/`.pem.key` (Configurator rejects plain `.crt`/`.key`, content-agnostic check)
|
||
- [ ] Decide broker reachability for the vehicle — **reopened 2026-08-11 evening**: port-forward
|
||
8883 on the Speedport Smart 4 Plus looked correctly configured (rule present, right internal
|
||
IP, right port) and internal reachability was confirmed (`homeassistant.local:8883` open from
|
||
the LAN, Mosquitto TLS listener genuinely up, TLS cert chain end-to-end verified byte-for-byte
|
||
against the FMM003's uploaded client cert), but the port stayed **closed from outside**
|
||
(confirmed via external port checker, both before and after a router reboot that changed the
|
||
dynamic WAN IP — DNS/DuckDNS matched correctly each time, so not a DNS or CGNAT issue).
|
||
Community reports (ComputerBase, Telekom Hilft) describe this as a known, Telekom-acknowledged
|
||
firmware bug on this router model; a full disable-the-firewall workaround doesn't exist on this
|
||
model either. Port-forward rule has been removed again. **New direction: route the FMM003
|
||
through flespi instead** (native Teltonika/Codec8 channel, IMEI-based auth, no certs, no
|
||
inbound port needed at all — flespi has a stable public endpoint; HA pulls data back out via
|
||
flespi's REST API or MQTT, outbound-only). Free flespi tier (10 devices/2 channels) is enough
|
||
for one vehicle. Next step: user creates the flespi account + Teltonika channel; a `flespi`
|
||
custom integration is already present in this HA instance (unconfigured) — check what it
|
||
needs once flespi-side setup exists.
|
||
Unrelated but still valid from the same session: DuckDNS hostname
|
||
`datametric360.duckdns.org` reliably updating; Let's Encrypt for HA's own local UI works (root
|
||
cause of the earlier DNS-01 failures was a stray `aliases` entry in the DuckDNS add-on config,
|
||
not DNS/network — see COMPANION_APP_ARCHITECTURE.md §5 item 2a). HA's SSL config now lives in
|
||
Settings → System → Network (UI), not `configuration.yaml`'s old `http:` block, which was
|
||
removed after HA started migrating/ignoring it. The Mosquitto TLS setup itself (cert chain,
|
||
`require_certificate: true`, `certfile`/`keyfile`/`cafile`) is verified correct and can be
|
||
reused as-is if a self-hosted broker is ever revisited.
|
||
- [ ] MQTT Client Type on the FMM003 ("Custom server" not selectable in practice, "AWS IoT Custom"
|
||
pointed at a self-hosted broker was confirmed working by two independent community reports) —
|
||
**moot for now** given the flespi pivot above: flespi uses the device's native Codec8/TCP
|
||
channel, not MQTT at all, so Server Settings should switch to Protocol: TCP against the flespi
|
||
channel host/port instead, and Codec set to "Codec 8 Extended". Revisit this item only if a
|
||
self-hosted broker is picked back up later.
|
||
- [ ] Capture the first real Codec JSON message (`mosquitto_sub`/MQTT Explorer) and build the
|
||
field mapping from it — **do not guess beforehand** (explicit decision)
|
||
- [x] Move trip detection to FMM003 ignition — done 2026-08-12. `fahrterkennung.py` rewritten:
|
||
trigger is now `einstellungen.ZUENDUNG_SENSOR`
|
||
(`binary_sensor.testzone_fmm003_engine_ignition_or_acc_status`, on = trip running) instead
|
||
of the iPhone WLAN sensor; same `task.unique()` pause-tolerance pattern kept unchanged.
|
||
`sensor.iphone_wifi_connection` and the `fahrzeug.wlan_name` profile field are fully removed
|
||
(backend and frontend). At the same time, all entity IDs sourced from the now-abandoned
|
||
`TommiG1/HA_VAG-EU-Data-Act` HACS integration were blanked in `einstellungen.py`
|
||
(`KM_SENSOR`, `TANK_SENSOR`, `RANGE_SENSOR`, `REFRESH_BUTTON`, door/window/lock sensors,
|
||
oil-change/inspection sensors) — the consuming features (Übersicht tiles, Reifenzähler,
|
||
automatic Tankerkennung, Fahrtabschluss-Screening) are untouched and degrade gracefully to
|
||
"unbekannt"/inactive per the existing `zustand_oder_none()` convention, rather than being
|
||
deleted; `fahrtabschluss.py`/`tankerkennung.py`/`reifenzaehler.py` now guard their
|
||
`@state_trigger(f"{einstellungen.KM_SENSOR}")`-style decorators with
|
||
`if einstellungen.KM_SENSOR:` (untested previously whether pyscript tolerates an empty
|
||
trigger string — not worth relying on). `STANDORT_TRACKER` now points at the real
|
||
`device_tracker.testzone_fmm003` and `BATTERIE_SENSOR` at
|
||
`sensor.testzone_fmm003_external_power_voltage` (the vehicle's 12V bus voltage as measured
|
||
by the tracker — NOT `sensor.testzone_fmm003_battery_voltage`, which is the tracker's own
|
||
internal backup cell and unrelated to the car). Real entity list for this device: the user's
|
||
`Entiaeten.csv` export (OneDrive, not in this repo). Verified in `audi_ha_test`: pyscript
|
||
reload (`pyscript.reload`) produced no errors/warnings for any of the changed files; the
|
||
Übersicht correctly shows "Status unbekannt" and em-dash placeholders for the now-blank VAG
|
||
fields instead of crashing (that container itself has no `device_tracker.testzone_fmm003`
|
||
registered, so Standort still shows "Kein GPS-Signal" there — expected, the real HA instance
|
||
has it). `INSTALL.md` step 4 and its troubleshooting table have since been rewritten
|
||
(2026-08-12) to point at the Setup menu instead of manual `einstellungen.py` edits, and to
|
||
use the current FMM003 field names (`ZUENDUNG_SENSOR`/`STANDORT_TRACKER`/`BATTERIE_SENSOR`)
|
||
instead of `WLAN_SENSOR`/TommiG1 — not live-tested against a real install (INSTALL.md itself
|
||
was never run end-to-end in this project, per its own opening note), but consistent with the
|
||
actual code.
|
||
|
||
### C) Maintain the existing HA panel (low priority — being replaced)
|
||
|
||
- [x] Wide-screen/desktop layout (`@container (min-width:860px)` in `audi-dashboard-ios.css`,
|
||
turns the same `.tabbar` into a 264px side-nav via CSS only, no JS/markup duplication) — done
|
||
2026-08-11 from the Claude Design "DataMetric360 Board" draft (`claude.ai/design` project
|
||
`c28a8d4d-ec4e-4178-9e49-ab5b90c02097`). Found and fixed a real regression while verifying it
|
||
live in `audi_ha_test`: the overlay's `:host > div{height:100%}` (meant to drop the old
|
||
412px-cap for the sidebar layout) was applied unconditionally instead of only inside the
|
||
`@container` block, which broke the **normal mobile layout too** — HA doesn't reliably
|
||
propagate a real height down through `panel_custom`, so `height:100%` collapsed `main#view` to
|
||
~0px (tabbar rendered right under the header, content invisible). Fix: kept the outer wrapper
|
||
viewport-anchored (`min(880px, calc(100dvh - 24px))`, matching `audi-dashboard.css`'s original
|
||
reasoning) at all widths, and only drop the 880px cap (`calc(100dvh - 24px)`, still
|
||
viewport-anchored, not parent-relative) inside `@container (min-width:860px)`. Verified live at
|
||
both narrow (mobile tab bar, full content) and wide (1144px container, grid side-nav,
|
||
navigation clicks, settings back-arrow) — screenshots taken, no regressions found.
|
||
- [x] Desktop-Sidebar-Markenklon (`.navmarke`) nachgezogen — done 2026-08-12. The user compared the
|
||
deployed app against the live Claude Design "Board" (the two-viewport comparison page, which
|
||
genuinely iframes this project's own `audi-dashboard-app.js`/`-ios.css`, not a static mockup)
|
||
and asked why the desktop Audi-rings placement differed: Design showed the rings pinned above
|
||
the sidebar nav list, ours showed them in the content header next to the title. Root cause,
|
||
confirmed via `DesignSync get_file` + byte-diff: `audi-dashboard-app.js` was already fully in
|
||
sync (identical), but `audi-dashboard-ios.css` had drifted — the Design project had since
|
||
added a `.navmarke` sidebar clone of the rings (only visible in the `@container
|
||
(min-width:860px)` block; `.rings{display:none!important}` hides the header copy there so the
|
||
mark appears once) plus a filled-pill active-tab style (`.tabpille`), neither pulled back
|
||
locally. Diffed the two full files first to confirm every change was additive/CSS-only, then
|
||
replaced the local file wholesale. Verified in `audi_ha_test` at both 402px and 1400px: rings
|
||
now sit above the nav list on desktop, gear icon alone in the content header, mobile view
|
||
unaffected, no console errors beyond the pre-existing missing-vehicle-photo 404 gap.
|
||
- [x] Standort-Kachel (live vehicle GPS position on the Übersicht, above "Zuletzt") — done
|
||
2026-08-12. New tile with a real (non-fake) Leaflet mini-map, opens a fullscreen "Standort"
|
||
route on tap; fullscreen has 4 floating controls (map style, center-on-vehicle,
|
||
center-on-user, fit-both) and a draggable myAudi-style bottom sheet (peek/expand/close by
|
||
drag or tap, opens on vehicle-marker click) showing distance-to-user, address (client-side
|
||
reverse geocoding via the public Nominatim API — same "public API, no key, low single-
|
||
vehicle volume" reasoning as the existing Leaflet-CDN gap below), fährt/steht/"Letzter
|
||
Parkplatz" state, fuel/range, and Route (Google Maps deep link) / Teilen (Web Share API)
|
||
actions. Backend: new `STANDORT_TRACKER` entity-ID setting in `einstellungen.py` (device_
|
||
tracker with lat/lon attributes), published via a new `_standort()` helper in
|
||
`frontend_veroeffentlichung.py`. The built-in default points at the author's test device
|
||
(`device_tracker.testzone_fmm003`); until the FMM003 actually delivers positions (section B),
|
||
that entity does not exist and the tile shows "Kein GPS-Signal vom Fahrzeug". Verified end-to-end in `audi_ha_test` with a manually
|
||
created `device_tracker.test_fahrzeug` test entity (tile, fullscreen map, and menu all
|
||
confirmed rendering/populating correctly); the device's own location (browser Geolocation
|
||
API) and the Route/Teilen deep links work independently of the vehicle-GPS gap. Pushed to
|
||
the Claude Design project (`c28a8d4d-ec4e-4178-9e49-ab5b90c02097`, "DataMetric360 Board")
|
||
for visual refinement — the JS/CSS ship functionally complete but visually plain
|
||
(placeholder vehicle-marker glyph, default `.aktion`/`.tile` styling); Claude Design's job is
|
||
polish, not structure. One known rough edge: reverse-geocoded address didn't resolve within
|
||
a few seconds in the `audi_ha_test` container (no crash, stays on "Adresse wird ermittelt …"
|
||
indefinitely) — likely just that container's outbound network to Nominatim specifically;
|
||
unconfirmed whether this reproduces on the real HA instance.
|
||
- [x] Setup menu (Einstellungen → Fahrzeug einrichten → last item when expanded) — done
|
||
2026-08-12. Lets the user map every sensor role the app uses (~18 roles, 3 of them 4-item
|
||
door/window/lock lists) to real HA entities from a searchable dropdown, instead of hand-
|
||
editing `einstellungen.py`. New backend module `pyscript/modules/entitaeten.py`: a
|
||
`FELDER` catalog (label, hint, expected domain/device_class/unit, keywords, whether it's
|
||
one of the trigger-bound fields) plus JSON override I/O (`data/entitaeten.json`, same
|
||
atomic-write pattern as `profil.py`) and `overrides_anwenden()`, which does
|
||
`setattr(einstellungen, key, value)` on the already-imported `einstellungen` module — every
|
||
existing consumer does `import einstellungen` + live attribute access (verified: none use
|
||
`from einstellungen import X`), so this needs no changes to any consumer file. New service
|
||
`audi_dashboard_entitaeten_schreiben` (`frontend_api.py`) and publication
|
||
`entitaeten_veroeffentlichen()` (`frontend_veroeffentlichung.py`) follow the project's
|
||
existing read-via-state/write-via-service pattern. Frontend: new popup (`vSetupPopup()`,
|
||
modeled on the existing `.sheet` pattern) with a from-scratch searchable-combobox component
|
||
(none existed in the repo before) — client-side only, built directly from `HASS.states`, no
|
||
new backend read service; a global "Nur passende Sensoren anzeigen" switch hard-filters
|
||
candidates by domain/unit, a keyword+domain+unit score ranks and auto-suggests for empty
|
||
fields. Deliberately does NOT auto-apply changes for the three trigger-bound fields
|
||
(`ZUENDUNG_SENSOR`/`KM_SENSOR`/`TANK_SENSOR` — `@state_trigger` bakes the entity ID in at
|
||
module-load time) — the UI shows an inline "wirkt erst nach Neustart" hint instead of
|
||
attempting a risky self-`pyscript.reload()` from inside a running service call. Two real
|
||
bugs found and fixed during live verification, both now also pushed to the Claude Design
|
||
project: (1) a full popup re-render on every keystroke fought the search input for focus —
|
||
fixed by patching just the candidate-list DOM node on `input`, matching the same fix the
|
||
existing `data-tankpreisfeld` handler already uses for the identical reason; (2) the popup
|
||
background used `--tile-2`, which Design had since changed to a translucent
|
||
`rgba(255,255,255,.1)` in the night theme (for the frosted-glass tile look elsewhere) — made
|
||
the whole popup partially see-through against the app content behind it. Fixed by switching
|
||
to `--tile-deckend` (Design's own opaque-surface token, already used by `.standortmenu` for
|
||
the same reason), with `var(--canvas)` as a defensive fallback.
|
||
- [x] Fresh-install packaging + `update.ps1` fix — done 2026-08-12. While preparing a short
|
||
quick-install checklist, found that `update.ps1` never copied `audi-dashboard-ios.css` or
|
||
`www/badges/*` — both are loaded by the running app (the ios overlay is injected by the app
|
||
itself via JS, badges by the model picker since item #51), so every `update.ps1` run since
|
||
those were added silently left the deployed copy stale on those two. Fixed (script now copies
|
||
both). Also removed two stray `*.bak-before-audit-merge` files from `www/` (already gitignored
|
||
at repo root, never committed, just local clutter from an earlier merge). New
|
||
`installationspaket/` — a generated, gitignored bundle (pyscript/, www/ minus the two backup
|
||
files, data/ with only the example profile + receipt parser, `configuration_snippet.yaml`,
|
||
`update.ps1`) plus a short `ANLEITUNG.md` checklist for copying to a fresh HA instance;
|
||
`INSTALL.md` stays the authoritative, detailed reference. Regenerate on demand, don't keep it
|
||
permanently in sync — it's a deployment snapshot, not a second source of truth.
|
||
- [x] Setup-menu polish: 5 additions on top of the existing Setup popup — done 2026-08-12.
|
||
Backend (`pyscript/modules/entitaeten.py`): a `_STANDARDWERTE` snapshot taken at module-load
|
||
time (before `overrides_anwenden()` ever runs), exposed via `aktueller_stand()` as
|
||
`standardwerte` — needed because `setattr()` on the live `einstellungen` module is permanent
|
||
for the process, so "reset to default" has to write the original value back explicitly rather
|
||
than just omitting the override. New service `audi_dashboard_neustart` (`frontend_api.py`)
|
||
wrapping `homeassistant.restart()`, called only from an explicit user-confirmed button, never
|
||
automatically. Frontend (`audi-dashboard-app.js`): (1) live current-value preview next to each
|
||
candidate in the search dropdown (`entitaetZeilenMarkup`, reads `HASS.states[e.id].state`);
|
||
(2) inline warning under a field if its currently assigned entity is `unavailable`/`unknown`/
|
||
missing (`entitaetStatusWarnung`); (3) duplicate-assignment check on save
|
||
(`setupDuplikate`) — blocks with a confirm-sheet ("trotzdem speichern?") rather than silently
|
||
allowing the same entity in two roles; (4) per-field reset button (only shown when the current
|
||
value differs from `standardwerte`), reverts to the built-in default; (5) after saving, if any
|
||
of the 3 trigger-bound fields actually changed (`setupGeaenderteTriggerFelder`, compares
|
||
against a snapshot taken when the popup opened), a confirm-sheet offers "Jetzt neu starten"
|
||
calling the new service. Verified live in `audi_ha_test`: value preview and unavailable-warning
|
||
confirmed correct (`ZUENDUNG_SENSOR`'s configured entity genuinely doesn't exist in this test
|
||
container → "Entität nicht gefunden." shown as designed); duplicate-check correctly caught both
|
||
a deliberately-forced KM/TANK collision and pre-existing door/window/lock auto-suggestion
|
||
collisions (a real side effect of this container's limited matching entities, not a bug);
|
||
reset button appeared/disappeared correctly as values diverged from/matched the default;
|
||
save round-tripped through the real service into `data/entitaeten.json` (byte-inspected) and
|
||
into the published `pyscript.audi_dashboard_fahrzeugstatus` state; restart-needed sheet named
|
||
exactly the changed trigger-bound field(s) and no others. Did not actually trigger a restart
|
||
during verification (would have restarted the shared test container) — the service call itself
|
||
was left unexercised beyond confirming it registers without error at pyscript load time.
|
||
- [x] Decide the iOS-vs-Audi-CI conflict from `DESIGN_REVIEW_2026-08-13.md` §A — decided
|
||
2026-08-13, owner's call: **iOS look made official** (Option 1 of the two the review offered).
|
||
`SPECIFICATION.md` §2 rewritten to describe the iOS values (colors, 16px radius) as canonical;
|
||
`bauauftrag.md` §8 now counts as superseded (left untouched, per this project's convention of
|
||
never editing that file — see its own "historical only" note above). The pre-decision Audi-CI
|
||
spec text is preserved verbatim in [`AUDI_CI_ARCHIV_2026-08-13.md`](AUDI_CI_ARCHIV_2026-08-13.md),
|
||
which also documents *why reverting later needs no restore*: `audi-dashboard-ios.css` is a
|
||
purely additive overlay loaded after `audi-dashboard.css` in `_aufbauen()`
|
||
(`audi-dashboard-app.js` ~line 3990) — the Audi-CI base values are still fully intact in the
|
||
unmodified base stylesheet, untouched by this decision. Reverting is just: stop loading the
|
||
overlay `<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`).
|
||
- [ ] 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.
|