HACS scheidet fuer den aktuellen Aufbau aus, gegen die installierte Version 2.0.5 geprueft statt aus dem Gedaechtnis: HACS kann ausschliesslich GitHub (fest verdrahtet, dieses Repo liegt auf Gitea), und keine der sechs Kategorien installiert nach /config/pyscript/ - die Zielpfade sind custom_components/, www/community/, python_scripts/, themes/, custom_templates/ und appdaemon/apps/. Die App braucht aber pyscript/, audi_dashboard/ und Eintraege in der configuration.yaml. Einziger Weg waere die Konvertierung des pyscript-Backends zu einer echten custom_component - die nebenbei panel_custom, das www-Kopieren, die data-Umbenennung, allow_all_imports/hass_is_global und die pyscript-Abhaengigkeit selbst erledigen wuerde. Umfang gemessen: 2.911 Zeilen, 21 Dienste, 15 State-Trigger, 10 Zeit-Trigger. Der Besitzer hat entschieden, das bis nach der ersten echten Inbetriebnahme zurueckzustellen. Dabei aufgefallen und ebenfalls festgehalten: die Paritaetsregel sichert Quell-Paritaet, nicht Auslieferungs-Paritaet. Das Panel holt sich audi-dashboard-version.json bei jedem Seitenaufruf und ist damit sofort aktuell; die Companion-App ist eine Capacitor-Huelle mit gebuendelten Assets, package.json sagt 0.1.0 und in src/ prueft nichts jemals eine Version. Die iOS-App kann also wochenlang hinterherhinken, ohne dass es irgendwo sichtbar wird. Loesungsskizze im Abschnitt notiert, noch nicht gebaut - die Entscheidung ueber den Auslieferungsweg steht aus. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
163 KiB
AGENTS.md — Project state, review findings, open items, and working rules
Last updated: 2026-08-23 (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; 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;
2026-08-18: the owner rejected a follow-up mockup for the "Mein Audi" service preview and
"Anstehende Termine" boxes ("Lost information") - no code from that proposal was built; instead the
existing Nächster-Service box on the Übersicht had its "bis zum/zur ..." wording replaced by the
real Audi CI oil-change/inspection icons (user-supplied SVGs) placed left of the figure, in both
codebases, per the owner's explicit request); 2026-08-19: two small wording fixes on the panel's
"Mein Audi" Versicherung/Steuer summary tile (vAudi()) and its "Kfz-Steuer" detail tile
(vSteuer()) — "Zusammen" renamed to "Summe" and the "feste Kosten im Jahr" caption under the sum
dropped entirely, and the Kfz-Steuer amount's period caption now reads "im Jahr" instead of the raw
data value "jährlich" (matching the Versicherung row above it) whenever steuer.zeitraum === "jährlich"; the underlying stored value and the "jährlich"/"halbjährlich" edit-form dropdown
(vSteuerBearbeiten()) were deliberately left untouched, since this was a display-label fix, not a
data-model rename. No companion-app port was needed for the "Zusammen" row: companion-app's "Mein
Audi" page (MeinAudi.tsx) only links to the Versicherung/Steuer screen via a NaviKachel, it never
rendered a combined Versicherung+Steuer sum in the first place, so there was nothing to change there
(pre-existing structural gap, not part of this fix). Companion's Steuer() edit screen has no
read-only "im Jahr"/"jährlich" display either — its only "jährlich" is a fallback default string
inside an editable-sentence, structurally different from the panel's read-only tiles — so it was left
as-is too. Later the same day: the "Vertrag" tile on that same "Mein Audi" Versicherung/Steuer page
was previously read-only end-to-end (Gesellschaft, Umfang, Vertragsnummer, Selbstbeteiligung,
Schadenfreiheitsklasse) — made every line editable, plus add/delete for the Selbstbeteiligung list
specifically (the only true repeating list among those fields). Panel: new vVertragBearbeiten()
view (route vertrag, back to vers) reached via a new zahnrad button on the "Vertrag" tile in
vVers(); Selbstbeteiligung rows get a small inline "×" delete button (new .zeile-loeschen CSS,
same 44×44pt invisible-::before tap-target trick as .km-edit) and a "+ Position hinzufügen"
.aktion button that pushes ["", ""]; Schadenfreiheitsklasse "zuvor" (sfAlt) is edited here too
(the current SF stays editable only via the pre-existing "Beitrag anpassen" screen, cross-
referenced with a hint span, not duplicated). Companion: new Vertrag() screen (route vertrag,
its own NaviKachel on the Versicherung page) follows the codebase's own established edit-screen idiom
(local entwurf state + explicit "Speichern" button) rather than the panel's per-field autosave —
same behavior, different idiom, per the parity rule. Selbstbeteiligung edit/add/delete moved here
from the old read-only display inside Vertragsdetails(), which is removed (was showing the same
list twice would have been confusing). Schadenfreiheitsklasse is not added to companion's new
screen: companion never displayed teile[].sf/sfAlt anywhere before this change (not even
read-only) — building a whole new UI section for a field the app never surfaced is out of scope for
"make the existing lines editable"; documented here as a pre-existing gap, not silently dropped.
Verified live in the Docker test container (panel: add/edit/delete a Selbstbeteiligung position,
values persisted through profilSpeichern()+re-render) and via tsc --noEmit + the companion-app test
suite (100/100 incl. the render-every-page smoke test, which now also covers vertrag) + vite build, since companion-app needs a live backend connection the local dev server doesn't have.
2026-08-23: retroactive data import — see section F below for the full entry. In short: HA's
recorder keeps sensor history only 10 days by default, which silently capped how far back anything
could ever be reconstructed; recorder_snippet.yaml raises that to a year, and a new
pyscript/historienimport.py + "Daten importieren aus Home Assistant" button (both codebases)
rebuilds trips, refuels and battery history for any past window out of that recorder history.
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.
Parity rule (binding, stated by the owner 2026-08-17): the HA panel (
homeassistant/www/audi-dashboard-app.js) and the companion app (companion-app/) must always be at the same functional state. When a request names only the panel, that is not a scoping decision — the owner simply tests there because the Docker instance makes it quick to see. Port every user-visible change to both in the same session, and say so in the report. The two are independent implementations (vanilla JS + shadow DOM vs. React +@audi-dash/ui), so a port is a re-implementation, not a copy: keep the behaviour and wording identical, let each side use its own idiom, and skip only what genuinely cannot apply (documented per case — e.g. panel-only CSS bugs, or HA-panel-only concepts like the entity Setup menu). If a port is impossible or unreasonably large, say so explicitly instead of leaving it silently undone. This rule exists because the two had already drifted six days apart once (see section A).
How this file is loaded: Claude Code does not read AGENTS.md natively; the root CLAUDE.md
imports it via @AGENTS.md (official recommended pattern). Other agents (Codex, Cursor, Copilot)
read AGENTS.md directly. Edit content here, not in CLAUDE.md.
Working rules
Karpathy guidelines (adopted as project standard)
From multica-ai/andrej-karpathy-skills,
behavioral rules derived from Andrej Karpathy's observations on LLM coding failures. They bias
toward caution over speed — for trivial tasks, use judgment.
- 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.
- 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.
- 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.
- 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/*.mdfile 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:
- This file (overview + open items)
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 phaseSPECIFICATION.md— authoritative for the HA panel, incl. §7 "Known Gaps"COMPANION_APP_ARCHITECTURE.md— authoritative for DataMetric360 (decided, phases 1–10 built)AUDIT_2026-08-10.md— accessibility/platform audit of the panel (partly done, rest below)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
hassobject). - External access: Cloudflare Tunnel + reverse proxy with a path allowlist; HA itself stays
unreachable. Domain
datametric360.appregistered (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_kmhon individual trips never populated. The trip-detail map draws a fabricated line viafakeTrack()(audi-dashboard-app.js:414) — not a real track. (Unrelated: the Übersicht's live Standort-Kachel is no longer in this state —STANDORT_TRACKERnow points at a realdevice_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) inpyscript/modules/einstellungen.py.www/bilder/has no vehicle photos (all slots show placeholders);steuer.faelligunset. (BATTERIE_SENSORwas the same kind of gap until 2026-08-12 — now wired to the FMM003'sexternal_power_voltage, see section B/C below. Several other fields are newly inactive since the same date for a different reason — theTommiG1/HA_VAG-EU-Data-Actintegration 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()inmodules/profil.pydoes not handle a missing/corruptfahrzeugprofil.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 commentaudi-dashboard-app.js:13-15claim the statistics view shows sample numbers — false;vStat()computes real values fromTRIPS/FILLS.homeassistant/README.md:110mentions the removed 97% full-tank rule (removal documented inbelegverarbeitung.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/uiinto the app as a real dependency. - Both packages: no
node_modules, nodist— smoke tests neednpm installfirst (design-system additionallynpm run build;scripts/smoke.mjsimports 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 indesign/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:
- The data layer declared field names the backend never sends.
Fahrzeugstatushadtank_prozent/sicher_abgestellt/sicherheit; the backend writestankprozent/gesichert/sicherheitscheck. Every screen would have readundefinedwithout anything failing.technik/ausstattungwere typed as objects but are arrays. - 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.
design-systemstill 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:
- The vehicle block on the home screen was invisible.
overflow: hiddensets 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 }. - The model name appeared twice — once as the badge image, once as text beside it.
- 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)
- Fix the Claude Design draft (RS 4, not RS 6) and extend it by the 16 missing sub-pages;
then re-export to
design/ companion-app: set up Vite + React + Capacitor scaffold; wire@audi-dash/uias a real dependency (possibly add a workspace/monorepo root)- Implement the screens from the design draft on top of the existing
DataMetricApilayer - Add Audi assets (fonts/rings/badges) at implementation time from
homeassistant/www/— never intodesign-system/ - Secure storage for the LLAT (iOS Keychain / Android Keystore via Capacitor plugin; the
ablageSetzen()hook already exists) - Onboarding: manual token paste (required); QR scan only if it stays simple (QR generated
locally under HA
/local/, architecture §3) - Authenticated smoke tests of the data layer (reads, service calls, queue round-trip)
against
audi_ha_testwith a real token - Offline UX per design brief (offline marker, visible pending queue)
B) Infrastructure / commissioning (partly waits for FMM003 hardware)
- Switch
datametric360.appnameservers at all-inkl to Cloudflare ("full setup") — prerequisite for the tunnel; domain carries nothing else, so this is consequence-free - Decide hostname split (app on apex + API on
api.subdomain, or vice versa) - Choose reverse proxy (Nginx Proxy Manager vs. Traefik) — can be done before hardware against the existing HA API
- Define the reverse-proxy path allowlist (depends on final entity/service names)
- Install/wire the FMM003; record firmware version (Codec JSON is firmware-dependent)
- Generate TLS certificates for Mosquitto + device (small private CA) — done 2026-08-11, 10-year
validity; Mosquitto configured (
certfile/keyfile/cafile/require_certificate: true). Remaining: upload root/client cert/key to the FMM003 Security tab — filenames must end in.pem/.pem.crt/.pem.key(Configurator rejects plain.crt/.key, content-agnostic check) - Decide broker reachability for the vehicle — reopened 2026-08-11 evening: port-forward
8883 on the Speedport Smart 4 Plus looked correctly configured (rule present, right internal
IP, right port) and internal reachability was confirmed (
homeassistant.local:8883open 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; aflespicustom 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 hostnamedatametric360.duckdns.orgreliably updating; Let's Encrypt for HA's own local UI works (root cause of the earlier DNS-01 failures was a strayaliasesentry 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), notconfiguration.yaml's oldhttp:block, which was removed after HA started migrating/ignoring it. The Mosquitto TLS setup itself (cert chain,require_certificate: true,certfile/keyfile/cafile) is verified correct and can be reused as-is if a self-hosted broker is ever revisited. - MQTT Client Type on the FMM003 ("Custom server" not selectable in practice, "AWS IoT Custom" pointed at a self-hosted broker was confirmed working by two independent community reports) — moot for now given the flespi pivot above: flespi uses the device's native Codec8/TCP channel, not MQTT at all, so Server Settings should switch to Protocol: TCP against the flespi channel host/port instead, and Codec set to "Codec 8 Extended". Revisit this item only if a self-hosted broker is picked back up later.
- Capture the first real Codec JSON message (
mosquitto_sub/MQTT Explorer) and build the field mapping from it — do not guess beforehand (explicit decision) - Move trip detection to FMM003 ignition — done 2026-08-12.
fahrterkennung.pyrewritten: trigger is noweinstellungen.ZUENDUNG_SENSOR(binary_sensor.testzone_fmm003_engine_ignition_or_acc_status, on = trip running) instead of the iPhone WLAN sensor; sametask.unique()pause-tolerance pattern kept unchanged.sensor.iphone_wifi_connectionand thefahrzeug.wlan_nameprofile field are fully removed (backend and frontend). At the same time, all entity IDs sourced from the now-abandonedTommiG1/HA_VAG-EU-Data-ActHACS integration were blanked ineinstellungen.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 existingzustand_oder_none()convention, rather than being deleted;fahrtabschluss.py/tankerkennung.py/reifenzaehler.pynow guard their@state_trigger(f"{einstellungen.KM_SENSOR}")-style decorators withif einstellungen.KM_SENSOR:(untested previously whether pyscript tolerates an empty trigger string — not worth relying on).STANDORT_TRACKERnow points at the realdevice_tracker.testzone_fmm003andBATTERIE_SENSORatsensor.testzone_fmm003_external_power_voltage(the vehicle's 12V bus voltage as measured by the tracker — NOTsensor.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'sEntiaeten.csvexport (OneDrive, not in this repo). Verified inaudi_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 nodevice_tracker.testzone_fmm003registered, so Standort still shows "Kein GPS-Signal" there — expected, the real HA instance has it).INSTALL.mdstep 4 and its troubleshooting table have since been rewritten (2026-08-12) to point at the Setup menu instead of manualeinstellungen.pyedits, and to use the current FMM003 field names (ZUENDUNG_SENSOR/STANDORT_TRACKER/BATTERIE_SENSOR) instead ofWLAN_SENSOR/TommiG1 — not live-tested against a real install (INSTALL.md itself was never run end-to-end in this project, per its own opening note), but consistent with the actual code.
C) Maintain the existing HA panel (low priority — being replaced)
- Wide-screen/desktop layout (
@container (min-width:860px)inaudi-dashboard-ios.css, turns the same.tabbarinto a 264px side-nav via CSS only, no JS/markup duplication) — done 2026-08-11 from the Claude Design "DataMetric360 Board" draft (claude.ai/designprojectc28a8d4d-ec4e-4178-9e49-ab5b90c02097). Found and fixed a real regression while verifying it live inaudi_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@containerblock, which broke the normal mobile layout too — HA doesn't reliably propagate a real height down throughpanel_custom, soheight:100%collapsedmain#viewto ~0px (tabbar rendered right under the header, content invisible). Fix: kept the outer wrapper viewport-anchored (min(880px, calc(100dvh - 24px)), matchingaudi-dashboard.css's original reasoning) at all widths, and only drop the 880px cap (calc(100dvh - 24px), still viewport-anchored, not parent-relative) inside@container (min-width:860px). Verified live at both narrow (mobile tab bar, full content) and wide (1144px container, grid side-nav, navigation clicks, settings back-arrow) — screenshots taken, no regressions found. - Desktop-Sidebar-Markenklon (
.navmarke) nachgezogen — done 2026-08-12. The user compared the deployed app against the live Claude Design "Board" (the two-viewport comparison page, which genuinely iframes this project's ownaudi-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 viaDesignSync get_file+ byte-diff:audi-dashboard-app.jswas already fully in sync (identical), butaudi-dashboard-ios.csshad drifted — the Design project had since added a.navmarkesidebar 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 inaudi_ha_testat both 402px and 1400px: rings now sit above the nav list on desktop, gear icon alone in the content header, mobile view unaffected, no console errors beyond the pre-existing missing-vehicle-photo 404 gap. - Standort-Kachel (live vehicle GPS position on the Übersicht, above "Zuletzt") — done
2026-08-12. New tile with a real (non-fake) Leaflet mini-map, opens a fullscreen "Standort"
route on tap; fullscreen has 4 floating controls (map style, center-on-vehicle,
center-on-user, fit-both) and a draggable myAudi-style bottom sheet (peek/expand/close by
drag or tap, opens on vehicle-marker click) showing distance-to-user, address (client-side
reverse geocoding via the public Nominatim API — same "public API, no key, low single-
vehicle volume" reasoning as the existing Leaflet-CDN gap below), fährt/steht/"Letzter
Parkplatz" state, fuel/range, and Route (Google Maps deep link) / Teilen (Web Share API)
actions. Backend: new
STANDORT_TRACKERentity-ID setting ineinstellungen.py(device_ tracker with lat/lon attributes), published via a new_standort()helper infrontend_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 inaudi_ha_testwith a manually createddevice_tracker.test_fahrzeugtest 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/.tilestyling); Claude Design's job is polish, not structure. One known rough edge: reverse-geocoded address didn't resolve within a few seconds in theaudi_ha_testcontainer (no crash, stays on "Adresse wird ermittelt …" indefinitely) — likely just that container's outbound network to Nominatim specifically; unconfirmed whether this reproduces on the real HA instance. - Setup menu (Einstellungen → Fahrzeug einrichten → last item when expanded) — done
2026-08-12. Lets the user map every sensor role the app uses (~18 roles, 3 of them 4-item
door/window/lock lists) to real HA entities from a searchable dropdown, instead of hand-
editing
einstellungen.py. New backend modulepyscript/modules/entitaeten.py: aFELDERcatalog (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 asprofil.py) andoverrides_anwenden(), which doessetattr(einstellungen, key, value)on the already-importedeinstellungenmodule — every existing consumer doesimport einstellungen+ live attribute access (verified: none usefrom einstellungen import X), so this needs no changes to any consumer file. New serviceaudi_dashboard_entitaeten_schreiben(frontend_api.py) and publicationentitaeten_veroeffentlichen()(frontend_veroeffentlichung.py) follow the project's existing read-via-state/write-via-service pattern. Frontend: new popup (vSetupPopup(), modeled on the existing.sheetpattern) with a from-scratch searchable-combobox component (none existed in the repo before) — client-side only, built directly fromHASS.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_triggerbakes 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 oninput, matching the same fix the existingdata-tankpreisfeldhandler already uses for the identical reason; (2) the popup background used--tile-2, which Design had since changed to a translucentrgba(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.standortmenufor the same reason), withvar(--canvas)as a defensive fallback. - Fresh-install packaging +
update.ps1fix — done 2026-08-12. While preparing a short quick-install checklist, found thatupdate.ps1never copiedaudi-dashboard-ios.cssorwww/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 everyupdate.ps1run 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-mergefiles fromwww/(already gitignored at repo root, never committed, just local clutter from an earlier merge). Newinstallationspaket/— 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 shortANLEITUNG.mdchecklist for copying to a fresh HA instance;INSTALL.mdstays the authoritative, detailed reference. Regenerate on demand, don't keep it permanently in sync — it's a deployment snapshot, not a second source of truth. - Setup-menu polish: 5 additions on top of the existing Setup popup — done 2026-08-12.
Backend (
pyscript/modules/entitaeten.py): a_STANDARDWERTEsnapshot taken at module-load time (beforeoverrides_anwenden()ever runs), exposed viaaktueller_stand()asstandardwerte— needed becausesetattr()on the liveeinstellungenmodule is permanent for the process, so "reset to default" has to write the original value back explicitly rather than just omitting the override. New serviceaudi_dashboard_neustart(frontend_api.py) wrappinghomeassistant.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, readsHASS.states[e.id].state); (2) inline warning under a field if its currently assigned entity isunavailable/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 fromstandardwerte), 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 inaudi_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 intodata/entitaeten.json(byte-inspected) and into the publishedpyscript.audi_dashboard_fahrzeugstatusstate; restart-needed sheet named exactly the changed trigger-bound field(s) and no others. Did not actually trigger a restart during verification (would have restarted the shared test container) — the service call itself was left unexercised beyond confirming it registers without error at pyscript load time. - Decide the iOS-vs-Audi-CI conflict from
DESIGN_REVIEW_2026-08-13.md§A — decided 2026-08-13, owner's call: iOS look made official (Option 1 of the two the review offered).SPECIFICATION.md§2 rewritten to describe the iOS values (colors, 16px radius) as canonical;bauauftrag.md§8 now counts as superseded (left untouched, per this project's convention of never editing that file — see its own "historical only" note above). The pre-decision Audi-CI spec text is preserved verbatim inAUDI_CI_ARCHIV_2026-08-13.md, which also documents why reverting later needs no restore:audi-dashboard-ios.cssis a purely additive overlay loaded afteraudi-dashboard.cssin_aufbauen()(audi-dashboard-app.js~line 3990) — the Audi-CI base values are still fully intact in the unmodified base stylesheet, untouched by this decision. Reverting is just: stop loading the overlay<link>. - Fix the two implementation bugs from the same review, independent of the direction decision
above — done 2026-08-13. §B, red-semantics inversion:
audi-dashboard-ios.css.aktionno longer fills every action red; base.aktionis now a neutral--ios-fillsecondary button, a new.aktion.primaerrule is the only one filled with--ios-tint(red), matching the.aktion/.primaer/.loeschensemantics 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-fillinstead of a red tint, red stays only as the accent text/icon color; two chart-bar segments inaudi-dashboard-app.jsthat used--redfor a neutral data category ("Arbeitsweg" trips, first segment of the insurance contribution bar) now use--fglike their sibling segments. Radius inconsistency:.setup-popupand.standortmenuinaudi-dashboard.cssnow referencevar(--r-tile)instead of a hardcoded20pxliteral, so both track whichever design (iOS 16px or, if reverted, Audi-CI 20px) is actually active. Verified live inaudi_ha_test: Setup-popup Abbrechen/Speichern and the "Einrichten"/"Setup"/"Fertig" buttons in Einstellungen render with the corrected fills at both mobile and desktop widths, switches render green, no console errors. - Clean up remaining "EU Data Act" residue from the FMM003 pivot (2026-08-13) — the
Einstellungen "Version" tile in
audi-dashboard-app.jshardcodedFahrzeugdaten: EU Data Act / Abruf alle 15 MinutenandPosition: iPhone / Companion App, both stale: vehicle data and position now come exclusively from the FMM003 (ZUENDUNG_SENSOR,BATTERIE_SENSOR,STANDORT_TRACKERineinstellungen.py), not the abandoned HACS integration or the iPhone. Both rows now readFMM003. Also fixedtestumgebung/konfiguration.yaml+testumgebung/README.md: theinput_text.test_wlan/iPhone WiFi Connectiontemplate sensor simulated WLAN-based trip detection, fully removed 2026-08-12 (fahrzeug.wlan_ssidno longer exists anywhere) — its "Fahrt auslösen" example was non-functional. Replaced withinput_boolean.test_zuendungdriving abinary_sensor.testzone_fmm003_engine_ignition_or_acc_statustemplate sensor matching the realZUENDUNG_SENSORentity ID, so trip detection stays testable. The remaining KM/Tank/Range/door/window/lock template sensors correctly still describe the abandonedTommiG1/HA_VAG-EU-Data-Actintegration (pereinstellungen.py) and were left in place — those fields are blank by default but stay manually mappable via the Setup menu, so the fixtures remain useful for testing that path; comments updated to make this distinction explicit. - Fix oversized toggle switches ("Slider") across the app (2026-08-13) —
.feld labelinaudi-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 labelhas 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 inaudi_ha_testat mobile (375px) and desktop widths — switches render at the correct ~51px iOS size, no console errors. - Fix desktop (≥860px container) layout audit findings (2026-08-13, see
DESIGN_AUDIT_2026-08-13.mdfor 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 asmain#view) had nomax-width:860pxwhilemain#viewdid — the track itself isminmax(0,1fr)and stretches to the full remaining window width, so.head{flex:1}pushed.profilbtnto the track's far edge, ~450px past wheremain#viewvisually ends. Fixed by giving.topbar/.phone:has(.back.on) .topbar/.ptrthe samemax-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.standortmenuare allposition:absolutewithleft/rightrelative to the full.phonebox (sidebar included), not the content column, and had no desktop max-width either —.setup-popupmeasured 1644px wide on a 1920px window (10px margins only). Fixed withmax-width:560px(.setup-popup/.sdpopup/.sheet) or640px(.standortmenu) plusmargin-left/right:autoin the same container query — deliberately nottransform:translateX(-50%), which would fight these elements' own transform-based entry animations. Centering is relative to the full.phonewidth (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 inaudi_ha_testat 375px and 1920px, no console errors. - Fix 4 more user-reported design findings (2026-08-13, same session, see
DESIGN_AUDIT_2026-08-13.md"Runde 2" for full detail with before/after measurements): (1) page title sat ~30px right of the content tiles below it on wide screens —.backreserves 44px+gap in.topbareven when inactive (opacity:0, notdisplay:none), fixed with.back:not(.on){width:0;...}in the desktop container query, root pages only (detail pages with.back.onunaffected, 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 basetop: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 selectand colors it the same grey as any other secondary text; added.feld select{color:var(--ios-tint)}so picker fields read as editable the way iOS conventionally signals it (accent color), free-text.feld inputs deliberately left alone. (4) the expand/collapse.markchevron 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;.markwas 6×10px (non-square, sized for the unrelated SVG-path.chev/.goicons), 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.5xtransform:scale()zoom on the live swipe-row DOM) but could not be reproduced — documented in the audit file as open, pending a screenshot from the real device. - Fix five user-reported bugs (2026-08-16):
(1) Türschloss-/Haubenschloss-Erkennung entfernt — user asked for these two checks to be
permanently removed (not just left blank/unmapped like the rest of the retired VAG-
integration fields). Removed
TUERSCHLOSS_SENSOREN/HAUBENSCHLOSS_SENSORfromentitaeten.py'sFELDERcatalog (no longer offered in the Setup menu) and fromeinstellungen.py(attributes deleted, not just blanked), and removed their entries from_sicherheitscheck()infrontend_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-orphanedinput_boolean.test_entriegelthelper and the fourtest_audi_lock_*template sensors fromtestumgebung/konfiguration.yaml(door-open/window fixtures forTUER_SENSOREN/FENSTER_SENSORENare untouched), and updatedtestumgebung/README.mdaccordingly. (2) "Doppelt zugeordnet" (and every other confirm-sheet) was see-through —.sheet-gruppe/.sheet-abbrecheninaudi-dashboard.cssused--tile-2, which the iOS overlay's night theme redefines asrgba(255,255,255,.10)(translucent, meant for tile-on-tile layering) — the same root cause already fixed for.setup-popup/.standortmenuon 2026-08-13, just not carried over to the generic confirm-sheet used bybestaetigen()/hinweis(). Switched both tovar(--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-deckendis 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()infrontend_veroeffentlichung.pyonly ever readlatitude/longitudeas attributes of a singleSTANDORT_TRACKERdevice_tracker entity (HA's usual convention) — but some integrations (confirmed live inaudi_ha_test's own simulated FMM003 device, which exposes separate "Latitude coordinate value"/"Longitude coordinate value" sensors) publish them as two independentsensorentities instead, which_standort()had no way to consume. AddedSTANDORT_LAT_SENSOR/STANDORT_LON_SENSOR(new optional fields ineinstellungen.py+entitaeten.py's Setup-menu catalog, group "standort") as a second path:_standort()now triesSTANDORT_TRACKERfirst (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 thenachladeAnstossenheader comment,audi-dashboard-app.js): on a slow/cold backend start,render()no-ops untilDATEN_GELADENflips true, and the previously observed manual fix was a HA panel remount. Root cause of why the click helps was not fully reproducible inaudi_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 callsdatenLaden(false)proactively ifDATEN_GELADENis 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()inhomeassistant/data/shell_beleg_parser.py, wired as a fallback inmain()(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 populateddiscountas 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 useshashlib.sha1(not the built-inhash(), 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 inaudi_ha_test(version1786752000): 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 toinstallationspaket/. - Follow-up round the same day (2026-08-16), three more items:
(1) Removed
STANDORT_TRACKERentirely — 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 fromeinstellungen.pyandentitaeten.py's Setup catalog, and simplified_standort()infrontend_veroeffentlichung.pydown 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 staleSTANDORT_TRACKERentries already sitting indata/entitaeten.json— confirmed live: this container's own leftover override file still hadSTANDORT_TRACKERfrom earlier testing (plus staleTUERSCHLOSS_SENSOREN/HAUBENSCHLOSS_SENSORfrom 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 persistedSTANDORT_LAT_SENSOR/STANDORT_LON_SENSOR. Redid the Setup save live (mapped both tosensor.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>→ syntheticFile/DataTransferdispatch →FileReader→hass.callService) via browser instrumentation: the happy path works completely (backend parses, publishespyscript.audi_dashboard_beleg_ergebnis, frontend fills the form) —belegverarbeitung.py's existingtry/exceptaround_parser_aufrufen()already publishes a visiblefehlerfor parser failures specifically. The actual gap:base64.b64decode()and_pdf_speichern()inaudi_dashboard_beleg_hochladen()were outside that try block — any failure there (corrupted upload, disk full, ...) raised an unhandled exception with zero UI feedback, sinceserviceRufen()'s generic.catch()only doesconsole.error(). Widened the try block to cover all three steps. Also stopped usingserviceRufen()for this one call site specifically (audi-dashboard-app.js, theeingabe.onchangehandler) — it now callsHASS.callService()directly with its own.catch()that setsbelegFehlerand 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 — addedCI.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) toaudi-dashboard-app.js, replacing the single-colorpoiCarteardrop previously used byfahrzeugMarkerSVG(). Updated both marker call sites'iconAnchorto the new pin's actual tip position and simplified.fahrzeug-pininaudi-dashboard.css(no morecolor: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 inaudi_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 toaudi_ha_test(version1786758000) and synced toinstallationspaket/. - 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()appendedleaflet.min.csstodocument.head, but the panel renders insidethis.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 intoROOTand awaiting it before the map is built; the script tag stays indocument.head(it must,window.Lis global). The CSS injection is deliberately re-checked on everyleafletLaden()call rather than guarded by thewindow.Lcheck, because a HA panel remount produces a fresh shadow root whilewindow.Lis 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 CARTOdark_allin Nacht mode; replaced by a single themelessTILE_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 CIpoi-caricons (poi-car-l.svg/poi-car-s.svgfrom the delivered icon set) instead of the hand-built two-tone pin from the previous round:CI.poiCarL(48-grid) at ≥34 px, the existingCI.poiCar(24-grid) below, anchors recomputed per variant. Since these are single-color outline forms incurrentColor,.fahrzeug-pinregained 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-deletebehind 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 awischtclass set on the first movement of the gesture (and the existingswipedclass 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)}withbackground: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 headingSUMME-EURand 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 carriesSHELL TANKSTELLE, 6450 SÖLDENon one line): it finds the first postcode line in the header and takes the name from the line above.Preis/Lwithout 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 toaudi_ha_test(version1786765000) and synced toinstallationspaket/. - 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 selectinaudi-dashboard-ios.cssnow all getbackground: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 inputrule became redundant and was removed (its width/alignment still come fromaudi-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 inshell_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_addresskeeps the full street + postcode + city as before. The regression suite'stest_bekannte_stationenexpectations 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 fromhomeassistant/.gitignore. It carries no real vehicle data, onlydata/fahrzeugprofil.example.jsonwith empty FIN/Kennzeichen placeholders; verified before un-ignoring. Keep it in sync wheneverpyscript/orwww/changes — that was already the working rule, but it is now visible in the repo when it is forgotten. Deployed toaudi_ha_test(version1786766000). - Fifth round the same day (2026-08-16) — one-click installer and the Einzelbeleg map:
(1)
installationspaket/Installieren.cmd+install.ps1automate steps 1–3 ofANLEITUNG.md. The.cmdwrapper exists because double-clicking a.ps1opens 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 overwritesfahrzeugprofil.json,fahrten.jsonl,tankvorgaenge.jsonl,entitaeten.json,ha_token.txt; backsconfiguration.yamlup to.bakand replaces its own marker-delimited block instead of appending twice; refuses to touch the file at all whenpyscript:/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 noconfiguration.yaml.-Pruefenis a dry run.install.ps1is 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 redcircleMarkeris now the CIpoipin (CI.poiL/CI.poiSfrompoi-l.svg/poi-s.svg, viatankstellenMarkerSVG()) — deliberately the plain pin, notpoi-car, so vehicle and station stay distinguishable. Found while testing: nothing in the backend ever writesstation_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 existingstandortAdresseAufloesen()), with results cached inlocalStorage(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 deliveredgps-s.svg), disabled until the coordinates arrive..mapboxgainedposition:relativeso the control anchors to the card. Only there, per explicit request — the Standort fullscreen already has its own centre controls. Deployed toaudi_ha_test(version1786769000). Note for testing map behaviour in this app:initMap()runs on everyrender(), 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 onL.Map.prototype.flyToinstead. - 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(andpoiL/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'sdattribute 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 newCI.poiCarLSil/poiCarSil/poiLSil/poiSSilconstants, fixed-color-filled (notcurrentColor), andfahrzeugMarkerSVG()/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.autoCSS 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_Vinaudi-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()recomputedyMin/yMaxfrom 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 invBatterieverlauf()(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-unusedliterlocal was removed with it. (7) Active/primary buttons are grey again, not red —.aktion.primaerinaudi-dashboard-ios.cssfilled with--ios-tint(red) since the 2026-08-13 iOS-overlay decision; explicit user request to drop that and match the base stylesheet's neutralvar(--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 hadappearance:none(native arrow removed) but no replacement indicator at all — a real gap, not cosmetic. Added a CSSbackground-imagechevron (light/dark variants) in both stylesheets; baseaudi-dashboard.csskept 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'sget_page_textworked and was used instead) and cross-checked against actual measurements in this codebase, then asked the user viaAskUserQuestionwhich 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::beforewith 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…") fromvEinst(), both per explicit request. (11) Fuel bar color now matches the headline (var(--fg)) instead of a red/yellow/green status gradient invHome(); thebalkenFarbelocal was removed with it. (12) Selected menu icon is white (headline color), not red, in the desktop side-nav (.tab.oninside the@container (min-width:860px)block inaudi-dashboard-ios.css) — the mobile tab bar already usedvar(--fg)there, this brought the desktop variant in line. (13) Reifenfoto (Sommerrad/Winterrad) now crops from the right edge, not the center —.radbildgainedobject-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.montiertclass, 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 (bothvar(--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.ongets the highlight background directly (whole button), with the inner.tabpillebackground suppressed in that case; the icon-only mode (.tabbar.ohne) keeps the original tight icon-pill unchanged. Deployed toaudi_ha_test(version1786901000); pin fix visually confirmed live (solid teardrop pin with white car glyph, clearly over the map's "Obergurgl" label text) and the tab-bar highlight confirmed covering both icon and "Übersicht" label. - Seventh round the same day (2026-08-16) — more user-reported layout/interaction bugs:
(1) Wheel photo (Sommerrad/Winterrad) was never actually a square, despite the fifth round's
crop-direction fix. Root cause:
bildMitPlatzhalter()puts theradbildclass on both the wrapping.bildboxdiv 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.bildboxis declared later in the file, so it silently won — the box stayed a full-width 4:3 rectangle the whole time,object-position:right centerjust 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);.radbildalone now only carriesobject-position. Verified live:.bildbox.radbildnow 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 acolor-schemehint. 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 unlesscolor-schemesays otherwise — this is what made "Modell" and every other dropdown "flash" bright white against the dark app on open. Addedcolor-scheme: darkunder:host([data-theme="nacht"])andcolor-scheme: lightunder:host([data-theme="tag"])inaudi-dashboard-ios.css. (4) Fixed a real JS crash in the "Mein Audi" image-cycle click handler.bildWeiter()(fires on tapping the hero photo) triedbox.querySelector(".platzhalter-datei").textContent = ...— that class was never emitted bybildMitPlatzhalter()(the actual class is.platzhalter- aktion, and in gallery mode it deliberately shows a static "Foto hinzufügen" prompt, not a filename).querySelectorreturnednull, the assignment threw, and the exception aborted the function before reaching the line that updates the.dotspage 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.dotsonclass 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 genericdata-bildklick/bildMenuOffenpopup plumbing already used by the wheel-photo tiles, not a new mechanism.bildInfo()'s existing winter-side-view special case (swaps inseitenansicht-winter.webpwhile 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-unusedBILDER_UPLOAD_SLOTSconstant and the.bildgrid/.bildslot/.bildslot-label/.carfix.miniCSS were removed as dead code; a new.bildmenu.ansichtpositions 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) inaudi-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 toaudi_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 thecolor-schemefix 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-schemeonly gets partial credit there; the reliable lever is that Chrome/Firefox/Edge do honor explicitbackground-color/colorset 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 viagetComputedStyleon 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 staysleft-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#tabbarand.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 selectorspandoes not care about class, so the rule hid the icon's wrapper too.getBoundingClientRect()on the<svg>read{w:0,h:0}andgetComputedStyle(.tabpille).displayread"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 bothaudi-dashboard.cssandaudi-dashboard-ios.css(the base file's copy was already shadowed by the ios one at matching specificity, but fixed too rather than left as a landmine). Verified live via screenshot: all 5 tab icons render correctly in icon-only mode, active tab still shows its filled pill. - Eighth round the same day (2026-08-16) — Reifen tile relabel + reflow, per explicit request:
"Sommer"/"Winter" → "Sommerräder"/"Winterräder" (only the section headline text in
vReifen()'ssatz(); the singular "Sommerrad"/"Winterrad" photo label and thedata-satzvalue compared againstCAR.reifen.aktivelsewhere 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-headlineclass (20px,var(--fg), same typographic family as the page.titleone step down) replacing the small, muted.labelstyling 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;.montiertdroppedposition: absolute(no longer needs to float over the photo now that the photo isn't in that space) and became a normal-flow pill withmargin-topunder the headline block — simpler than the previous overlay hack, same 44×44pt invisible tap target via::before..bildbox.radbild'smargin- 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 toaudi_ha_test, confirmed live via screenshot for both Sommerräder (mounted) and Winterräder (unmounted) tiles. - 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 inbild-fehltstate, and.bildbox.bild-fehlt img{display:none}makes the<img id="carimg">itself un-hit-testable — a real tap lands on the.platzhalterdiv instead, which the old handler (e.target.id === "carimg") never matched, so nothing happened. First fix wrapped the gallery image in adata-bildgaleriecontainer 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 ofbild-fehltstate) but always callsbildWeiter(), 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-kopfflex row, same geometry as the header's.syncicon+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 realbattery-12v-l.svg/battery-12v-s.svgfiles 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 asCI.battery12vL/CI.battery12vS(verbatim paths,lon the 48-grid,son the 24-grid, matching the existingpoi-l/poi-ssize-variant convention); only thesvariant is used here viaciSVG()since this is a small 15px inline label icon. Deployed toaudi_ha_test, confirmed live via screenshot. Also this round (user then supplied the realedit-l.svg/edit-s.svgtoo): added a manual km-correction control to the Reifen tiles, per explicit request — a small pencil icon (Audi CIedit-s, newCI.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 stripskmbefore 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 mutatingCAR.reifen[k].kmand callingprofilSpeichern()like every other Reifen field does would silently drop the edit. Added a dedicated backend service instead,audi_dashboard_reifen_km_setzen(satz, km)inreifenzaehler.py, that only overwrites the stored counter itself and leavesreifen.referenz_odo_km(the last-processed odometer reference) untouched - the existingkilometerstand_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. NewkmOffenstate (which satz's correction field is open, mirroring the existingnmOffen/bildMenuOffensingle-open-popup pattern) plusdata-kmauf/data-kmspeichernhandlers, styled like the existing.montiert/.zahnradcontrols (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. - 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-motionsupport, confirm-sheet button ordering all already correct). Three real findings surfaced, all fixed and user-approved viaAskUserQuestionbefore 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, ...) — wasasyncwith notry/catchat 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 insideprofilSpeichern()itself intry/catch, reusing the existinghinweis()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-patchinghass.callServiceto reject inside the running app (viahost.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-spinnerCSS, 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 globalprefers-reduced-motionoverride (*, *::before, *::afterblock already forcesanimation-duration:.01msapp-wide) with no extra code. (3) Anzugsmoment and km-correction popups now use.aktion.primaeron 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 inaudi_ha_test(version1786916894): spinner CSS confirmed viagetComputedStyle(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 toinstallationspaket/. - 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
PointerEvents through the exactpointerdown/pointermove/pointeruphandlers inereignisseVerdrahten()(theptrY0/ptrDraglogic) — 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 nooverscroll-behaviorset 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 withoverscroll-behavior-y: containonmain#viewplusoverscroll-behavior: noneon the outer:host > divwrapper, so a gesture leavingmain#viewnever 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 viagetComputedStylethatoverscroll-behavior-ycomputes tocontain; no visual regressions, no new console errors. Synced toinstallationspaket/. 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. - 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 (
belegPopupstate,vBelegPopup(), rendered into the existing#overlayalongsidesheetMarkup()/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 intobelegDateiVerarbeiten()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:dragovermustpreventDefault()or the browser just opens the PDF itself and nodropever fires; the drag/drop/paste listeners hang ondocument, not on the drop zone, because the zone lives in the shadow DOM and a drag crossing its edge would otherwise be lost — and becauserender()replaces the overlay'sinnerHTML, so per-element listeners wouldn't survive. Phone pasting usesnavigator.clipboard.read()(iOS shows its own paste prompt) with the keyboardpasteevent as the desktop path. Non-PDFs are rejected with an in-dialog message instead of being uploaded. Verified live inaudi_ha_testby instrumentinghass.callServiceand driving realDragEvent/ClipboardEvent/KeyboardEvents: 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 toinstallationspaket/. 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: daspaste-Ereignis trägt die Datei inclipboardData.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", pertastenkuerzelEinfuegen()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 leeremtypean und wurde dadurch abgelehnt. Neue HilfsfunktionistPdf()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". Derpaste-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. - 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)aufde(..., 2)umgestellt: Jahres- und Monatsschnitt in der Tankliste, Zeilenwert je Tankvorgang, "Preis je Liter" im Einzelbeleg, die Live-Vorschau in beiden Tankformularen (statisch und imdata-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 - dereneur()rundet ohnehin schon auf 2 Stellen. Live geprüft: keinede(..., 3)-Aufrufe mehr in der Datei, Liste und Einzelbeleg zeigen "2,07 €/l", "1,99 €/l" usw. - Twelfth round (2026-08-17) — six user-reported items, one of them a real dead-end in the
bootstrap:
(1)
HECKKLAPPENSCHLOSS_SENSORremoved for good, the same treatment Türschloss/ Haubenschloss got on 2026-08-16: gone fromeinstellungen.py(attribute deleted, not blanked), fromentitaeten.py's Setup catalog, and from_sicherheitscheck()infrontend_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 132pxoverflow:hiddenbox. 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, whoseposition: absolute;inset:0is over-constrained by an explicit height (the height wins,bottom:0is ignored). Scoped both rules to.bildbox.carfix; image and placeholder fill the box via their own100%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 KernservicePrognose(stichwort, km, monate)zurückgeführt;inspektionPrognose()nutzt dieselben 30.000 km / 24 Monate wieintervalle()/termine(). Angezeigt in der Service-Kachel auf "Mein Audi" (auch ohne Fahrzeugmeldung, anders als beim Ölwechsel - dort nenntterminText()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 gemeinsamesfahrtFelder()/fahrtFormularWerte()(Datum, Startzeit, Dauer, Art, Start-/Zielort, Start-/Endkilometer, Distanz) nach demselben Muster wietankFelder()/tankFormularWerte(), benutzt vom Erfassungsformular und vom neuen "Werte bearbeiten" auf der Einzelfahrt-Seite (fahrtBearbeitenOffen, VorbildvFill()). Backend:audi_dashboard_fahrt_manuell_anlegenum dieselben Felder erweitert, neuer Serviceaudi_dashboard_fahrt_aktualisieren. Zwei Punkte, die dabei zählen: von Hand gesetzte Felder landen inedited_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 jetztprofil.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, annext((f for f in …), None)real gesehen) - Listen-Abstraktionen dagegen schon. Live end-to-end geprüft: anlegen, bearbeiten, erneut bearbeiten, jeweils infahrten.jsonlnachgesehen 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:.backist auf Wurzelseiten unsichtbar (opacity:0), belegt aber Breite plus Gap. Die Regel.back:not(.on){width:0;…}gilt jetzt auf allen Breiten und hebt permargin-right:-4pxauch den Gap auf;.topbarbekam links 16px statt 20px, passend zu--seitenrandvonmain#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. - 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.callWSohne neuen Timer aus, unddatenLaden()bei!HASSebenso - danach lief nichts mehr von selbst, und der einzige Ausweg war ein zufälliger hass-Push oder ein Menüklick (go()ruftdatenLaden()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 inaudi_ha_testgezeigt (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. - 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 byfahrtZeitraum()/dauerText()and kept live via a newdata-fahrtzeitfeldinput handler (same "patch the one node, don't call render()" pattern the existingdata-tankpreisfeldhandler 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()derivests_start/ts_endfrom 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 recalculatedduration_s(7200s for a 10:00→12:00 edit, checked infahrten.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 newartText()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-unusedworkclass 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--tileis 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,.24in Nacht) - the same token.feld select/.aktionalready 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 toaudi_ha_test(version1787009000) and synced toinstallationspaket/; console showed only the pre-existing service-worker/404 noise from the missing vehicle photos, nothing new. - Service-Seite: Prognose-Fussnote bricht nicht mehr um (2026-08-17) — Nutzermeldung, das Datum
der Ölwechsel-Prognose stehe in einer eigenen Zeile. Nachgemessen statt geschätzt: das
<small>in der rechten Spalte war 29px hoch bei 217px Breite (zwei Zeilen), die Inspektions-Fussnote darunter mit kürzerem Text 14px (eine). Ursache ist nicht die Textlänge allein, sondern der Platz:.rowist ein Flex mitdt/dd, undddbekommt nur den Rest neben dem Titel - für "9.029 km, voraussichtlich am 14.04.2027" bei 12,5px reicht das nicht. Statt an Schriftgröße oder Text zu drehen, wandert die Fussnote jetzt als eigenes Element (.row-fussnote) nebendt/ddund umbricht perflex:1 0 100%auf eine eigene Zeile über die volle Kachelbreite - dieselbe Stelle, an der sie vorher schon optisch stand, nur ohne die künstliche Breitenbegrenzung. Dasflex-wrap:wrapist bewusst mit:has(.row-fussnote)auf genau diese Zeilen eingegrenzt, damit keine der übrigen.row-Verwendungen (17 Zeilen allein auf "Mein Audi") ihr zweispaltiges Layout verliert. Die vorher verschachtelte fm-/nicht-fm-Verzweigung im Markup ist dabei zu einemhatMeldung-Zweig für den Wert plus einem separat berechnetenfussnote-String zusammengefallen - die Anzeigelogik selbst (Ölwechsel-Fussnote nur bei Fahrzeugmeldung, Inspektion immer) ist unverändert. Live geprüft bei 375px und 1280px: beide Fussnoten 14px hoch (einzeilig), 307px bzw. 636px breit, linksbündig auf der Kachelkante; alle 17.rowauf "Mein Audi" weiterhin zweispaltig; keine neuen Konsolenfehler. Deployed als Version1787003000, synchron ininstallationspaket/. - 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 inhomeassistant/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/uicomponents) and found six real, mechanically distinct gaps, all fixed: 1.AGM_RUHE_MAX_Vwas still 13.2 inBatterie.tsx- lowered to 12.8, matching the panel's 2026-08-16 fix (same rationale: above that is already alternator voltage). 2. Deadwlan_namefield -profilAdapter.tsstill read/wrote it andEinstellungen.tsxshowed 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.tsonly hadoelwechselPrognose(). Refactored the shared math into aservicePrognose()core (keptoelwechselPrognose()'s public signature/behavior identical - all 9 existing tests still pass unchanged) and addedinspektionPrognose()(fixed 30.000 km / 24 Monate, no customizable interval like the oil change has), wired intoService.tsxas a new "Eigene Prognose (Inspektion)" row. 3 new tests. 4. Arbeitsweg pill was red -Pill variant="work"inFahrten.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 sharedsrc/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.tsxwas entirely read-only - no edit, no Art toggle after creation. Added a "Bearbeiten" toggle reusingFahrtFelder.tsx, backed by a newDataMetricApi .fahrtAktualisieren()(mirrors the existingtankvorgangAktualisieren()pattern) calling the backend'saudi_dashboard_fahrt_aktualisierenservice (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'seur()already always renders 2 decimals - the 3-decimal bug the panel had never existed here); pull-to-refreshoverscroll-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 inTankDetail.tsxgets 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 forinspektionPrognose()),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 frommain.tsx) and only the nativeios//android/platform folders themselves (deliberately gitignored, regenerated vianpx cap add) are the missing piece, and that step needs a Mac. - Ü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 kmgroß,vsl. 04/2027als leisere Zweitzeile), echte Einheit statt „tkm", und die Kachel ist jetzt einbutton.tile.tilebtnmit.go-Chevron nachservice:x— vorher eine Sackgasse, obwohl die Service-Seite existiert.mmjj()wurde dadurch verwaist (nur noch das volle Jahr wird gebraucht) und ist durchmmjjjj()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, antippbareTile variant="button" chevron→service. Geprüft: typecheck sauber, 95/95 Tests, Build erfolgreich. - Übersicht: Kilometerstand/Service statt zwei gleichrangiger Kacheln (2026-08-17) — Nutzer
mochte das Kachelpaar nicht und fragte, wie Apple HIG bzw. die Audi-CI es lösen würden. Beide
Regelwerke wurden dafür belegt, nicht aus dem Gedächtnis zitiert: die HIG-Seiten „Layout“
(„Differentiate controls from content“; Wichtigkeit über Position und Größe) und „Widgets“
(dynamische statt statischer Inhalte; „sparse layouts can make the widget seem unnecessary“)
live von developer.apple.com gelesen, die CI-Regeln aus dem projekteigenen
AUDI_CI_ARCHIV_2026-08-13.md(flache Flächen mit Haarlinien statt gestapelter Karten;.fig/Audi Type Wide als eigene typografische Klasse für Zahlen; Hierarchie nur über Größe und Farbe, da nur die Schnitte 300/400 existieren). Gemessener Befund: beide Kacheln 166×113px, gleiche 29px-Zahl, gleiche Farbe — obwohl nur die rechte ein Knopf war (erkennbar allein an einem 12px-Chevron) und die beiden Zahlen Grundverschiedenes meinen (Ist-Wert vs. täglich schrumpfender Countdown). Beide Schulen sagen dazu dasselbe, sie unterscheiden sich nur im Mittel. Ergebnis nach drei Iterationen mit dem Nutzer (Zwischenstände jeweils als Mockup gezeigt, bevor Code angefasst wurde): das.duo-Raster entfällt, stattdessen eine führende Kennzahl in einer normalen.tilemit Hintergrund (erst flach entworfen, auf Nutzerwunsch dann doch als Box) — Zeile „Nächster Service:“, Chevron oben rechts, 40px-Zahl, Unterzeile „bis zum Ölwechsel · vsl. 15.04.2027“, Haarlinie, darunter der Kilometerstand als.servicezeile. Der Chevron sitzt bewusst nur am.serviceblock-Knopf, nicht über der ganzen Fläche: damit ist der obere Teil das Bedienelement und die Kilometerstand-Zeile reine Anzeige (live geprüft — Klick auf den Block öffnet „Service“, Klick auf die Zeile navigiert nicht). „Nächster Service“ meint auf Nutzerwunsch jede Serviceart;naechsterTermin()wählte ohnehin schon über Ölwechsel/Inspektion/Hauptuntersuchung hinweg aus, neu ist nur die Beschriftung samtART_BIS-Zuordnung für den richtigen Artikel („bis zum Ölwechsel“, aber „bis zur Inspektion“). Fällt kein Restkilometerwert an (Hauptuntersuchung hat kein km-Ziel), rückt das Datum selbst ins Zahlenfeld, dann bei 30px statt 40px, damit „15.04.2027“ auf 375px nicht umbricht. Von der Änderung verwaist und deshalb mitentfernt:mmjjjj()(nur hier benutzt) und die.duo-Regeln in beiden Stylesheets. Bewusst offen gelassen: der Statuspunkt aus der HIG-Variante („alles ruhig“ vs. „bald fällig“) — der Nutzer hat ihn nicht übernommen. Bewusst nicht entschieden von mir: das volle Tagesdatum statt „04/2027“ war Nutzerwunsch; angemerkt wurde dabei, dass die Prognose an einem einzigen Arbeitstag von 12.04. über 14.04. auf 15.04.2027 wanderte (oelwechselPrognose()extrapoliert aus km/Tag, jede Odometer-Aktualisierung verschiebt sie), das Tagesdatum also mehr Genauigkeit verspricht, als der Wert hat. Live geprüft bei 375px und 1280px: Block und Wertezeile fluchten mit der Kachelkante (16→359 bzw. 564→1236), Chevron bündig rechts, Tippfläche 88px hoch, keine neuen Konsolenfehler. Deployed als Version1787007000(zwischenzeitlich auf Nutzerwunsch von.tile.flatauf eine normale.tilemit Hintergrund umgestellt —.serviceblock:active {opacity:.55}als Tipp-Rückmeldung ergänzt, da der übliche.tilebtn-Hintergrundwechsel hier nicht geht), synchron ininstallationspaket/. Entwurf und Begründung als Artefakt: „Zwei Kacheln, drei Antworten“. - Nächster-Service-Box: CI-Icon statt "bis zum/zur ..."-Wortlaut (2026-08-18) — der Nutzer
lehnte zunächst einen Folge-Entwurf ab, der dieselbe HIG/CI-Systematik auf die "Mein Audi"-
Service-Vorschau und "Anstehende Termine" auf der Service-Seite anwenden sollte ("I don't like
it" → per Rückfrage: "Lost information", also zu viel weggelassen) — dieser Vorschlag wurde
nicht umgesetzt, keine Codeänderung daraus übernommen. Stattdessen ein präziser, neuer
Wunsch für die bereits bestehende Nächster-Service-Box auf der Übersicht: das Wort
("bis zum Ölwechsel"/"bis zur Inspektion") vor der Kennzahl entfällt zugunsten der echten Audi-
CI-Icons
inspection-l/-s/oil-change-l/-s(vom Nutzer als SVG geliefert, ausC:\Users\tobia\Downloads\Icons 2026-08-09 13_04_03\, verbatim übernommen wie die übrigen CI-Icons dieses Projekts — l im 48er-, s im 24er-Grid), links der Zahl platziert; auf Nutzerwunsch anschließend nochmal um 30% vergrößert (24px → 31px). Umgesetzt in beiden Codebasen: Panel bekamCI.oilChangeL/-S/CI.inspectionL/-S(neben den bestehenden CI- Icon-Konstanten) und ein neues.fig-zeile-Wrapper-Div (display:flex;align-items:center) um Icon +.figinvHome()'s Serviceblock; die Nebenzeile zeigt jetzt nur nochvsl. DATUM. Companion-app bekam die identischen Pfade als neueSymbolOelwechsel/SymbolInspektion-Komponenten insymbole.tsx(eigenerRahmenFlaeche-Wrapper, da diese zwei — anders als die generischen Tab-Icons dort — gefüllte Flächen-Icons sind, kein stroke-Wrapper) und eine neue.dm-servicefig-CSS-Klasse inscreens.css;Uebersicht.tsxwählt das Symbol über eine kleineSERVICE_SYMBOL-Tabelle nachservice.art. Wichtig: die Icons blieben bewusst außerhalb vondesign-system/(Lizenzregel oben — echte Audi-CI-Assets dürfen nur in Panel/companion-app, nicht in das markenfreie@audi-dash/ui) — die Verdrahtung liegt deshalb direkt im jeweiligen Screen, nicht in der gemeinsamenFig-Komponente. Die Hauptuntersuchung hat kein eigenes Icon (kein Restkilometer-Ziel,restKmist dort immernull) und behältbisText()als einzige Kennzeichnung — das deckt sich in beiden Codebasen genau mit der Bedingung, unter der ein Icon überhaupt existiert.npm run typecheck/npm run test(100/100)/npm run buildsauber. Live im Docker-Testcontainer geprüft (Version1787012001, synchron ininstallationspaket/): Ölwechsel-Icon rendert links der "8.999 km"- Kennzahl, Unterzeile zeigt nur noch "vsl. 18.04.2027", keine neuen Konsolenfehler. - Nächster-Service-Box: Hauptuntersuchung-Datum in Zahlengröße + Art-Name statt "bis zur ..."
(2026-08-18) — Folgewunsch zum Icon-Umbau: das Datum im Zahlenfeld war für die
Hauptuntersuchung (kein Restkilometer-Ziel) bisher kleiner gesetzt (30px statt 40px), extra
damit lange Daten wie "15.04.2027" auf 375px nicht umbrechen; der Nutzer wollte stattdessen
dieselbe 40px-Größe wie bei Ölwechsel/Inspektion, plus darunter statt "bis zur
Hauptuntersuchung" nur noch das Wort "Hauptuntersuchung" selbst. Vor der Umsetzung am
laufenden Panel nachgemessen (nicht angenommen): "15.04.2027" misst bei 40px/Audi Type Wide
182px, der Kachel-Container hat 322px — die ursprüngliche Wrap-Sorge, die zur 30px-Regel
geführt hatte, bestätigt sich bei den tatsächlichen Container-Maßen nicht.
zahlGroesseist jetzt fest 40 (PanelvHome()und companion-appUebersicht.tsx,Fig size={40}unabhängig vonrestKm). DabisText()/ART_BIS(Panel) undbisText()/BIS_TEXT(companion-appservice.ts) dadurch ihre einzige Aufrufstelle verloren, wurden beide als Orphans entfernt (inkl. des zugehörigen companion-app-Tests inservice.test.ts— von 100 auf 99 Tests, kein Funktionsverlust, nur der jetzt toten Funktion beraubt). Live im Docker-Testcontainer per DOM-Patch simuliert (Testfixture hat aktuell Ölwechsel als nächstgelegenen Termin, nicht die Hauptuntersuchung) und bei 390px Breite screenshotet: "06.08.2028" einzeilig bei 40px, "Hauptuntersuchung" darunter, kein Icon (wie vorgesehen — es gibt kein CI-Icon für die Hauptuntersuchung).npm run typecheck/npm run test(99/99)/npm run buildin companion-app sauber. Deployed als Panel-Version1787012002, synchron ininstallationspaket/. - "Anstehende Termine"-Box (Service-Seite) umgebaut: drei sichtbar getrennte Blöcke
(2026-08-18) — auf Wunsch des Nutzers ein neuer Entwurf (Apple HIG + Audi CI, wie beim
Übersicht-Umbau), mit expliziten Vorgaben: Herstellervorgabe bleibt für Ölwechsel und
Inspektion möglich, "voraussichtlich ..." bleibt für beide erhalten, alle drei Arten werden
sichtbar getrennt, die Hauptuntersuchung bekommt bewusst weder Herstellervorgabe noch
Prognosezeile. Durchlief drei Mockup-Runden als Artefakt ("Drei Termine, drei Blöcke") vor der
Umsetzung: Runde 1 abgelehnt ("Major Information is missing"). Runde 2 ergänzte ein
vollständiges Feld-Inventar (Tabelle: jedes Element aus
vService()mit Fundstelle im Vorschlag), integrierte den vereinbarten Werkstatttermin in den Hauptvergleich statt als Anhang, zeigte den "kein Eintrag im Servicebuch"-Zustand. Runde 3 (auf drei Inline-Kommentare des Nutzers im Artefakt): Ölwechsel-Icon von 20px auf 26px angehoben (füllt in seinem 24×24-Raster nur eine 24×10-Fläche statt der fast vollen Höhe des Inspektion-Icons — pergetBBox()nachgemessen, kein exakter Ausgleich möglich ohne das Icon unverhältnismäßig breit zu machen), Herstellervorgabe-Zeile auch am Ölwechsel-Beispiel gezeigt (vorher nur bei Inspektion), der Hauptuntersuchungs-Block von einem Icon-Platzhalter-Abstand auf echt linksbündig geändert. Danach vom Nutzer freigegeben ("Ok, implement it"). Umsetzung, beide Codebasen: Panel (vService(),audi-dashboard-app.js):<dl class="rows">durch drei<div class="termingruppe">ersetzt (Kopfzeile mit Icon + Art-Name + optionalem.tg-chipfür einen vereinbarten Werkstatttermin, optionale.tg-vorgabe-Zeile,.tg-wertals 22px/Gewicht-300-Zahl, optionale.tg-prognose-Fußzeile) — Auswahllogik für Wert/ Fußzeile/Herstellervorgabe unverändert aus dem bisherigen Code übernommen, nur die Markup- Struktur geändert.ART_ICON/ART_ICON_GROESSE(26px Ölwechsel, 20px Inspektion) vonvHome()auf Modulebene gehoben, da jetzt von zwei Stellen genutzt —vHome()s eigene Icon-Größe (31px, beide gleich) bewusst unverändert gelassen, das ist eine offene, dem Nutzer gestellte Rückfrage. Der Werkstatt-Chip nutztvar(--red)nur als Text-/Rahmenfarbe, keine Füllung — deckt sich mit der bereits bestehenden Projektregel "Rot ist Akzent, nie Fläche" (siehe iOS-Overlay-Fix vom 2026-08-13). Deployed als Version1787013000, synchron ininstallationspaket/, live im Docker-Testcontainer per direkter DOM-Abfrage geprüft (JS statt Screenshot, da die Browser-Pane in dieser Sitzung zeitweise nicht kompositierte): alle drei Blöcke rendern mit korrektem Text, Icons in 26px/20px, Hauptuntersuchung ohne Icon, keine neuen Konsolenfehler. companion-app (Service.tsx): dieselbe Drei-Blöcke-Struktur, aber eine echte Neustrukturierung, kein 1:1-Kopieren — die bisherige Kachel hieß "Nächster Ölwechsel" und mischte Ölwechsel-, Inspektions- und Hauptuntersuchungs-Zeilen fachfremd in einer gemeinsamenWerteliste; jetzt "Anstehende Termine" wie im Panel, mit denselben drei.dm-termingruppe- Blöcken. NeueSymbolOelwechsel/SymbolInspektion-Komponenten insymbole.tsx(bereits für die Übersicht angelegt, hier wiederverwendet). Neuer ExportletzterInspektion()inservice.ts(Gegenstück zuletzterOelwechsel()) für den "kein Eintrag"-Zustand der Inspektion. Zwei bewusste, dokumentierte Vereinfachungen gegenüber dem Panel (vorbestehende Lücken, nicht durch diese Änderung verursacht): (1) companion-app hat keine "Termin vereinbaren"-Funktion und keinevereinbart-Datenfelder für Service-Termine — der Werkstatt- Chip entfällt deshalb ersatzlos, es gibt nichts anzuzeigen. (2) companion-app hat kein Gegenstück zuintervalle()/dem generischen Herstellerintervall-Fallback des Panels; die Herstellervorgabe-Zeile wird stattdessen direkt ausfahrzeug.oel.modusabgeleitet (Ölwechsel: nur beimodus === "hersteller", Inspektion: immer, da sie kein einstellbares Intervall hat — deckt sich inhaltlich mit dem Panel-Verhalten, auch ohne dieselbe Funktion zu teilen). Beide Punkte sind funktional gleichwertig zum bisherigen companion-app-Stand, nicht neu eingeführte Lücken.npm run typechecksauber,npm run test99/99,npm run builderfolgreich; die bestehenden "Alle Seiten rendern mit Beispieldaten"-Tests rendern die Service-Seite weiterhin ohne Absturz. Kein Live-Screenshot in dieser Sitzung (bräuchte einen laufenden Dev-Server samt Backend-Anbindung) — strukturell verifiziert über Typecheck/Tests/Build. Zwei Nachbesserungen direkt nach der ersten Live-Prüfung, beide vom Nutzer gemeldet: (1) "Herstellervorgabe" fehlte beim Ölwechsel, obwohl der angezeigte Wert (29.100 km / 25.07.2028) vom Fahrzeug selbst gemeldet wird (hatMeldung/fahrzeug.oelwechselFaelligTs). Root Cause: die Zeile war bisher ausschließlich aniv.hersteller(Panel) bzw.fahrzeug.oel.modus === "hersteller"(companion-app) gekoppelt — dem App-eigenen Intervall-Modus aus "Einrichten". Der Bordcomputer kennt diesen App-Modus aber gar nicht; eine Fahrzeugmeldung stammt immer aus dem werkseigenen Wartungsprogramm und ist damit per Definition Herstellervorgabe, unabhängig vom gewählten App-Modus. Das Testprofil hat den Ölwechsel aktuell aufmodus: "eigen"stehen (10.000 km/12 Monate statt 30.000 km/24 Monate) — das war korrektes Verhalten der alten Bedingung, nur die Bedingung selbst war zu eng. Fix:herstellervorgabe = hatMeldung || iv.hersteller(Panel) bzw.oelHatMeldung || fahrzeug.oel.modus === "hersteller"(companion-app) — dieselbe alte Bedingung bleibt für den Fall ohne Fahrzeugmeldung erhalten (App-eigener Fallback auf das Herstellerintervall), kommt jetzt aber zusätzlich bei einer echten Fahrzeugmeldung zum Tragen. (2) Die Prognose-Fußzeile zeigte "8.999 km, voraussichtlich am 18.04.2027" auch dann, wenn in "Einrichten" tatsächlich Herstellervorgabe gewählt ist — dort rechnet die eigene Prognose ohnehin schon mit dem Herstellerintervall, die Restkilometerzahl wäre also nur eine zweite, verwirrende km-Angabe neben dem Fahrzeug-Wert direkt darüber. Fix: die "X km,"- Vorsilbe entfällt, sobaldCAR.oel.modus/fahrzeug.oel.modus === "hersteller"ist — nur beim eigenen (kürzeren) Intervall bleibt sie stehen, weil sie dort echte Zusatzinformation ist (matcht damit exakt, wie die Inspektion-Fußzeile schon immer aussah, da sie kein einstellbares Intervall hat). Mit dem Testprofil (modus: "eigen") bleibt die Kilometerzahl also weiterhin sichtbar — korrekt, das ist genau der Fall, für den sie gedacht ist; umschalten auf Herstellervorgabe in "Einrichten" würde sie ausblenden. Beide Fixes deployed als Panel-Version1787013001, synchron ininstallationspaket/, live per DOM-Abfrage geprüft: Ölwechsel-Block zeigt jetzt "Herstellervorgabe", keine neuen Konsolenfehler. companion-app:npm run typechecksauber,npm run test99/99,npm run builderfolgreich. - Parity rule eingeführt (2026-08-17): der Nutzer stellte klar, dass Panel und
companion-app/immer denselben funktionalen Stand haben sollen — auch wenn eine Anfrage nur das Panel nennt, das ist reiner Testkomfort (Docker-Instanz), keine Scope-Entscheidung. Regel als verbindlicher Absatz direkt unter der Maintenance-Regel oben in dieser Datei verankert. Als erste Anwendung sofort im selben Zug die Übersicht-Neugestaltung von oben nachcompanion-app/portiert (sieheUebersicht.tsx):naechsterService()indaten/service.tsneu — Gegenstück zunaechsterTermin()im Panel, wählt über Ölwechsel-/Inspektions-Prognose und die von Hand gepflegte Hauptuntersuchung hinweg den zeitlich nächsten Termin,bisText()liefert denselben Artikel wieART_BISim Panel. Die beiden bisherigen Kacheln "Reichweite"/"Kilometerstand" nebeneinander plus eine separate "Service"-Kachel mit Werteliste wichen einer Reichweiten-Kachel plus einer Service-Kachel mit.dm-serviceblock(Knopf, nur der obere Teil) und.dm-servicezeile(reine Anzeige) darunter — dieselbe Control/Content-Trennung wie im Panel.npm run typecheckblieb sauber,npm run testauf 100/100 (von 95 — vier neue Tests fürnaechsterService()/bisText()inservice.test.ts, einer inscreens.test.tsx, der mit einemvi.fn()alsgeheZuwirklich belegt, dass ein Klick auf den Serviceblock navigiert und ein Klick auf die Kilometerstand-Zeile es nicht tut — dafür musstezeige()inscreens.test.tsxerst einen injizierbarengeheZu-Parameter bekommen, vorher war er hart auf() => {}verdrahtet),npm run builderfolgreich. - 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.mdandREVIEW_main_2026-08-13.mdlist further drift not yet fixed (WLAN references still inSPECIFICATION.md,homeassistant/README.md, the profile template) - Harden
profil_lesen()against missing/corruptfahrzeugprofil.json— done on theumsetzung-datametric360branch (2026-08-11), brought over by the 2026-08-13 merge. ReturnsNoneon 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 forNone— verified by grep across the merged tree, no caller left unguarded (this was flagged as the merge's main risk inREVIEW_main_2026-08-13.md§"Was den Branch betrifft"). - Decide whether the 3 audit leftovers get fixed here or only in DataMetric360 — decided
2026-08-11 (
UMSETZUNGSPLAN.mdPhase 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/, setsteuer.faellig(operational data, not code)
D) Once the panel is superseded
- Archive
audi-dashboard-app.js, don't delete (settled decision, architecture §1)
E) Versicherungs-Details overhaul (planned, not started — noted 2026-08-19)
The owner wants to revisit "Versicherung/Steuer" more broadly next session, beyond the line-editing work already done (see section C in the changelog above: "Vertrag" tile made fully editable with add/delete for Selbstbeteiligung). Three separate asks, none scoped or started yet:
- Store the insurer's own Vertragsdetails PDF against the policy (upload/attach + view later) —
needs a decision on storage location (mirrors the existing vehicle-photo/backup file handling
under
www/bilder/? a new profile-attachment concept?) and where it's surfaced (new tile on "Vertragsdetails", or the main "Vertrag" tile) - A free-text field for the owner's own manual notes/summary on the insurance (not derived from
any structured field) — likely a
notiz-style textarea, panel already has a.notizCSS class pattern used elsewhere (seefeld.notizinaudi-dashboard.css) that could apply here - Redesign the "Versicherung/Steuer" Übersicht/summary box — owner explicitly said they don't yet know what they want here; needs a requirements conversation (what should be surfaced at a glance vs. moved to detail screens) before any mockup, not just a straight implementation
Not planned/scoped in either codebase yet — start with clarifying questions on the PDF storage model and the summary-box redesign goals before touching code, per this project's mockup-first convention for open-ended redesign asks (see the "Anstehende Termine" 3-round mockup process, section C).
F) Retroactive data import from the HA recorder (built 2026-08-23)
The retention answer, because it was the owner's actual question. Two different lifetimes were being conflated:
- The app's own records live forever already.
fahrten.jsonl,tankvorgaenge.jsonl,batteriespannung.jsonlandfahrzeugprofil.jsonunder/config/audi_dashboard/have no purge path anywhere inprofil.py— nothing trims them, ever. That was already true before this change. - HA's raw sensor history did not.
recorder.purge_keep_daysdefaults to 10 days, and the test instance had norecorder:block at all. That default was the real ceiling: it caps how far back any reconstruction can ever reach, because deleted recorder rows are gone permanently.
homeassistant/recorder_snippet.yaml (new) raises it to 365 days with auto_repack: true. Measured
on the test instance rather than guessed: 90.858 state rows over 17 days ≈ 5.300/day, 64,7 MB DB →
roughly 1,9 M rows and 1–1,5 GB for a year. 96 % of that traffic is the cupra_eu_data_act
integration (~1.737 states per entity per 17 days across ~50 entities), 1 % flespi/FMM003, 2 % the
rest — so the file carries a commented-out exclude: list targeting the EU-Data-Act side for anyone
who needs to shrink it. Deliberately no include: block: an include would stop HA recording
everything else entirely, and since 97 % of this instance is vehicle data it would save almost
nothing while costing every other integration its history.
The import itself. pyscript/historienimport.py (new) exposes
audi_dashboard_historie_importieren(start, ende). It reads the same sensors the live triggers
watch and derives the same records they would have produced:
- trips from the ignition sensor — contiguous "on" stretches, then merged where the gap is shorter
than
fahrten_pausenzeit_min, which reproduces the live pause rule (task.unique()+task.sleep()infahrterkennung.py) after the fact; odometer, GPS start/end filled from the other histories; stretches under 60 s dropped as ignition-without-a-drive - refuels from the tank sensor using the same low-water-mark logic and the same thresholds as
tankerkennung.py(5 l / 9 pp, whichever is more sensitive) - daily min/max battery voltage, same shape as
batterieverlauf.py
Re-running is safe by design: trips whose window overlaps an existing trip are skipped (whatever
their origin), refuels dedupe on a 90-minute window, battery days merge in profil's own
min/max update. Created records carry source: "import" alongside the existing ha/manual/auto.
How the history is read, and why. Via HA's own
homeassistant.components.recorder.history.get_significant_states through task.executor(), not by
opening home-assistant_v2.db with sqlite3 directly. Direct SQL was the tempting option
(allow_all_imports is already on, no config change needed) but the recorder schema is HA-internal
and changes between versions; the function is the interface HA itself uses. The cost is one new
config line, hass_is_global: true in the pyscript block (configuration_snippet.yaml), without
which the hass name does not exist in pyscript — everything else in the app works fine without it,
only the import reports that it cannot read the history. significant_changes_only=False matters:
the default mode drops exactly the small numeric steps that odometer distance and refuel detection
are built from.
Frontend, both codebases. Panel: vImportPopup() reusing the existing .beleg-popup overlay
pattern, opened from a new button under Einstellungen → Einrichten, with Von/Bis
datetime-local fields defaulting to the last 30 days, "Importieren" primary and "Abbrechen"
second. The dialog stays open and shows the result rather than closing optimistically — the one
place in the app where that is right, because the result is the point of the action. Progress
comes from polling pyscript.audi_dashboard_import_status (same pattern as the update status),
since pyscript services return immediately while the work continues in the background.
Companion: screens/HistorienImport.tsx using the design system's Popup variant="form", plus
api.historieImportieren() / api.importStatusLesen(). Deliberately not routed through the
offline queue like every other write: the import is a long-running operation whose result is the
reason you invoked it, and a queued run firing later from a dead session would surprise the user —
offline, it simply should not be offered.
One real bug found and fixed while testing: profil.batterieverlauf_tageswert_aktualisieren()
appended in insertion order. That was invisible while only the live path wrote it (it always adds
today, i.e. always the newest), but the import adds past days — they landed after newer ones and
the frontend chart, which draws in file order, would have run backwards. It now sorts by date,
restoring the invariant its own docstring already promised.
Verification. The test container is not running while the car is driven (owner's note), so its
recorder holds no real trips. Instead a synthetic history was seeded straight into the recorder DB —
two drives, one with a 5-minute mid-drive ignition gap, one 30-second ignition burst, a two-step
refuel, and two days of voltage — and the import reproduced it exactly: 3 trips (the gap correctly
merged into one 50-minute/40 km trip), 1 rejected as too short, 1 refuel, 2 battery days, distances
matching the seeded odometer to the kilometre. A second run over the same window created 0 and
reported 3 as already present, confirming dedup. Both runs were driven through the real UI in the
browser, not by calling the service directly. tsc --noEmit clean, companion tests 106/106 (6 new
for the result-sentence pluralisation), vite build clean, HA config check and startup clean.
Still open / worth knowing: the import has only ever run against seeded data, never against a
real multi-week recorder history on the production instance — the first real run is the honest test,
especially for how long a full year takes (the UI waits up to two minutes before saying it continues
in the background). Address reverse-geocoding is not attempted for imported trips (start_address/
end_address stay null, same as live-detected ones before the user edits them).
H) HACS: deferred by decision, and the deployed-parity problem it exposed (2026-08-23)
Decision: no HACS conversion for now. Owner asked whether the app could be installed via HACS. Checked against the actually installed HACS 2.0.5 rather than from memory — it cannot, for two independent reasons:
- HACS is GitHub-only.
github.com/api.github.comare hardcoded throughout; no Gitea, GitLab or self-hosted support. This repo lives ongitea.nothaft.cloud. - No HACS category installs to
/config/pyscript/. The six categories resolve tocustom_components/<domain>/,www/community/<name>/,python_scripts/,themes/,custom_templates/andappdaemon/apps/<name>/(verified inrepositories/*.py→localpath). The app needs/config/pyscript/and/config/audi_dashboard/andconfiguration.yamlentries — none of which HACS can do.
The only path is converting the pyscript backend into a real custom integration
(custom_components/audi_dashboard/), which would also kill panel_custom:, the www/ copying,
the data/→audi_dashboard/ rename, allow_all_imports/hass_is_global and the pyscript
dependency itself. Scope measured: 2.911 lines, 21 services, 15 state triggers, 10 time triggers,
39 task.executor, 12 state.set. Owner chose to defer it until after the app has actually run on
the real HA — sensible: converting first means debugging the rewrite and the real environment at the
same time. Do not start this without the owner raising it again.
The open question this surfaced — deployed parity of the iOS app. The parity rule (binding, above) guarantees source parity: both codebases change in the same session. It guarantees nothing about what is running. Today those two are wired completely differently:
- the panel fetches
/local/audi-dashboard-version.jsonwithcache: "no-store"on every page load and reloads its assets when the number changed — current within one page load, no user action - the companion app is a Capacitor shell with
webDir: "dist"and bundled assets, sideloaded through Xcode.package.jsonsays0.1.0and nothing insrc/ever checks a version. It stays on whatever was bundled at signing time, silently, indefinitely
So the iOS app can be weeks behind the panel with nothing anywhere making that visible. That is a real gap, not a hypothetical one, and it gets worse once the app is genuinely in daily use.
Proposed shape (discussed 2026-08-23, not yet built — needs the owner's call on the delivery
layer): one shared version stamp fed into both builds instead of today's unrelated
audi-dashboard-version.json integer and 0.1.0; the backend publishing the version it shipped
with, so both frontends can compare their own build stamp against it and show a visible hint when
behind (drift becomes loud instead of silent); and for delivery, OTA web-bundle updates so the iOS
shell can follow without an Xcode rebuild. Pointing Capacitor's server.url at HA was considered
and rejected: it would make the app as current as the panel, but the shell then cannot boot without
reaching HA, which guts the deliberately built offline queue (api/warteschlange.ts). Note the
whole mechanism survives the later integration conversion unchanged — that end state serves panel
assets and companion bundle from the same component, so one HACS update moves both.
G) Fresh-install audit + installer hardening (2026-08-23, before the first real deployment)
Owner is about to install on the real HA OS instance and asked for two things: that the one-click package produce a genuinely blank app, and that it carry no risk to the existing HA system.
The one real contamination bug. pyscript/modules/einstellungen.py shipped with two hardcoded
entity IDs from a long-gone test instance — ZUENDUNG_SENSOR = "binary_sensor.testzone_fmm003_engine_ignition_or_acc_status" and BATTERIE_SENSOR = "sensor.testzone_fmm003_external_power_voltage" (note testzone_, older even than the current test
container's testcar_b9_…). Invisible during all testing, because the test instance's own
entitaeten.json overrides both. On a fresh install with no overrides they would have been the
effective values — and worse than merely dangling: fahrterkennung.py guards trigger registration
with if einstellungen.ZUENDUNG_SENSOR:, explicitly built so an unset sensor registers no trigger at
all. A set-but-nonexistent value defeats exactly that guard. Both are now "" like every other field,
and the file header was rewritten — it still claimed the EU-Data-Act integration was gone, which
stopped being true when cupra_eu_data_act appeared.
Everything else in the package was already clean, verified rather than assumed: no fahrten.jsonl,
no tankvorgaenge.jsonl, no batteriespannung.jsonl, no entitaeten.json, no real tokens (the
ha_token hits are path constants), and fahrzeugprofil.example.json carries only model specs —
FIN, Kennzeichen, Erstzulassung, HU, insurance numbers and all counters are empty or zero.
Verified by actually doing it, not by reading the installer: a throwaway ghcr.io/home-assistant/ home-assistant:stable container, the package copied in exactly as install.ps1 does it, pyscript
config entry added, restarted. Result: /config/audi_dashboard/ held only fahrzeugprofil.json,
shell_beleg_parser.py and an empty belege/; no trip, fuel or battery file existed at all; zero
ERROR lines in the log.
Installer safety, audited and then hardened. The audit found the good news first: install.ps1
contains no Remove-Item anywhere, no robocopy /MIR, never touches .storage/, never reads or
writes automations.yaml/scripts.yaml/secrets.yaml, and never restarts HA. The only file it
changes outside its own folders is configuration.yaml — which is also the only way it could stop HA
booting. Three gaps closed there:
- the fixed
configuration.yaml.bakwas overwritten on every run, so a second run destroyed the pristine original. Backups are now timestamped, and the plain.bakis written only once - after writing, the file is read back and checked (still at least as long as before, marker block present exactly once, prior content byte-identical); any deviation rolls back automatically from the timestamped backup and moves the step to the manual to-do list
- the header now states the safety properties explicitly, so they can be checked rather than trusted
Tested end to end against mock config dirs: -Pruefen wrote nothing; a real run preserved a foreign
light: group and left automations.yaml byte-identical (SHA-256 compared) while appending the
block; the output parses as valid HA YAML with all foreign keys intact; a second run left exactly one
block and preserved fahrzeugprofil.json; and against a config that already had its own pyscript:
and panel_custom: the file came back byte-identical, as designed.
The genuine remaining risk is disk space, not the installer. recorder_snippet.yaml's 365 days
is ~1–1,5 GB, and on HA OS with an SD card or small eMMC a full disk stops HA from starting — nothing
to do with this app. The snippet header and the installer's to-do list now say so plainly, tell the
user to check Einstellungen → System → Speicher first, and recommend purge_keep_days: 90 when space
is tight (enough for the "catch up on the past" purpose, since the import moves data into the app's
own permanent .jsonl files anyway).
Also fixed this round (panel-only): the "Teilen" pill on the location sheet was invisible in
day mode — .standort-pille used background: var(--tile) while the sheet under it uses
var(--tile-deckend), and the iOS overlay sets both to #FFFFFF in day mode. Measured contrast
was 1.00:1, white on white. Both pills now follow the app's own button language (.aktion /
.aktion.primaer): outline for the secondary, var(--fg) fill for "Route". Text contrast measured
17–21:1 in both themes afterwards. This also retired --line-strong — a line colour — being used
as a button fill. No companion-app port: the location sheet's share/route pills do not exist there,
which the parity rule exempts as panel-only.
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-DEformat; 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.