Vollständiger Panel-companion-app-Paritätsaudit (11 Kapitel)

Systematischer Bildvergleich der companion-app gegen das HA-Panel (companion-app/AGENTS.md
Kapitel 1-11): Farbtoken/Radien aus dem falschen Stylesheet gelesen (iOS-Overlay statt Basis-CSS,
betraf fast jede Kachel/Farbe/Radius app-weit), vier app-weite @audi-dash/ui-Bugs (Switch/Seg/Feld
rot statt neutral bzw. falsche Feldbreite), Reifen-Seite strukturell neu gebaut (beide Radsätze
gleichzeitig statt Umschalter, editierbare Felder, Anzugsmoment-/km-Korrektur), Standort-Feature
komplett neu (fehlte bisher ganz), sowie diverse Struktur-/Typografie-/Datenlücken in
MeinAudi/Service/Versicherung/Sicherheit/Fahrten/Tanken/Statistik/Batterie/Einstellungen.

Manifest auf 2026.8.30.2 angehoben, OTA-Bündel neu gebaut und in audi_ha_test verifiziert
(sauberer Neustart, keine Tracebacks).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-30 12:21:58 +02:00
parent 4fa5d4bc2d
commit e1ebe6d694
43 changed files with 3256 additions and 451 deletions
+797 -1
View File
@@ -1,6 +1,9 @@
# AGENTS.md — Project state, review findings, open items, and working rules
**Last updated: 2026-08-29** (iOS signing prepared end to end — device build verified, signature
**Last updated: 2026-08-30** (a full 11-chapter panel↔companion-app visual/functional parity audit,
one continuous session — see "Chapter 1" through "Chapter 11 (Abschluss)" further down for the
complete record; companion-app manifest bumped to `2026.8.30.2`, OTA bundle rebuilt and redeployed;
2026-08-29: iOS signing prepared end to end — device build verified, signature
blocked on a device registration at Apple, see section AI; 2026-08-23: merged the
`umsetzung-datametric360` branch — companion app phases
110 done, see `UMSETZUNGSPLAN.md`; 2026-08-13: cleaned up remaining EU Data Act residue, fixed
@@ -4830,6 +4833,799 @@ button never renders. The manifest is unaffected — the iOS installer parses it
of content type — so the working route is to paste the `itms-services://` URL into Safari's address
bar directly. The script prints that URL for exactly this reason.
**Gap found and closed the same day (2026-08-29, asked directly: "hast du sichergestellt dass Paul
mit einem neuen Xcode-Durchlauf auch die garantiert aktuelle Version erwischt?"):** the script built
cleanly from whatever was locally checked out, but never checked that against Gitea — a forgotten
`git pull` on Paul's Mac would have signed a stale commit with no warning, the exact class of silent-
staleness bug this project keeps fixing elsewhere (cache-busting, OTA-bundle-version matching,
`install.ps1`'s bundle-vs-manifest check). `ios-signieren.sh` now refuses to build unless the working
tree is clean and `HEAD` is exactly `origin/main` (fetches first, aborts with a clear message
otherwise) — added right after `cd "$APP"`, before the web build.
---
## AK. Full panel↔companion-app parity audit, done in resumable chapters (started 2026-08-29)
Owner request, after seeing an old/stale iOS build: since the HA panel is considered functionally
final, re-derive companion-app's screens from the panel's current state exactly, so Xcode only has
to build+sign an already-correct app — instead of Paul re-doing work because the two apps still
disagree somewhere. Split into 11 chapters (one per screen group) specifically so the work survives
a context/token cutoff mid-task: each chapter ends at a clean, typechecked+tested state before the
next starts, and the owner is asked after each one whether to continue now or pause. Chapters:
1 Übersicht, 2 Mein Audi/Fahrzeugdaten, 3 Service/Reifen, 4 Versicherung/Steuer, 5 Sicherheit, 6
Fahrten, 7 Tanken, 8 Statistik, 9 Batterie, 10 Einstellungen/Datensatz, 11 Abschluss (Build,
Versions-Bump, `npm run ota`, Gesamtbericht).
**Chapter 1 (Übersicht) — four real drifts found and fixed, all in `Uebersicht.tsx` (vs. the panel's
`vHome()`):**
1. Status-row wording didn't match: panel says "Nicht sicher abgestellt"/"Status unbekannt",
companion-app said "Nicht vollständig verschlossen"/"Zustand unbekannt" — no recorded decision
for the difference, just drift. Aligned to the panel's exact wording.
2. The Reichweite tile still showed a "rund X von Y l" liter estimate under the percentage — that
line was deliberately **removed from the panel on 2026-08-16** (owner request, see the sixth
round that day) and was never removed here. Dropped; the percentage now renders as a bare
number, matching `CAR.tankBekannt ? pct + " %" : ""` exactly. This needed a real data-layer gap
closed too: companion-app's `Fahrzeug` type had no `tankBekannt` field at all (unlike its
existing `odoBekannt` twin) — added to `profilAdapter.ts` (`status.tankprozent !== null/undefined`,
mirroring the panel's own `koordinator.py` derivation) and to the one test fixture that
constructs a full `Fahrzeug` object (`service.test.ts`).
3. The Reichweite figure rendered at `size={40}` — the same size as the smaller "Nächster Service"
number one tile down. The panel draws it at `font-size:60px`, a genuine hero number. Fixed to
`size={60}`.
4. When there's no next service (`service` is `null`), the panel deliberately renders that block as
a plain non-interactive `<div>` (no chevron, no click target) — "nothing to navigate to." Companion-
app always wrapped it in a clickable `<button>` with a visible chevron regardless. Fixed: the
button (with chevron) only renders when `service` exists; the empty state is a plain `<div
className="dm-serviceblock">` instead. `screens.css`'s `.dm-serviceblock` cursor/press-feedback
rules were re-scoped to `button.dm-serviceblock` so the non-interactive div doesn't get a
misleading pointer cursor.
**Checked and left alone, deliberately** (not drift, established idiom/UX choices): companion-app's
`Leerzustand` empty-state tiles for "Letzte Fahrt"/"Letzte Tankung" when no records exist yet, vs.
the panel's `teaser()` simply hiding the whole row — this is a systemic companion-app-wide UI
convention (friendly empty states instead of hidden sections), not something introduced by drift,
and changing it would touch far more than this one screen; the `naechsterService()`/`service.ts`
forecast math itself (restKm/date computation) was not re-verified line-by-line here — that belongs
to Chapter 3 (Service), where the full `termine()`/`meldungsPrognose()` family gets its own pass.
**Fifth item, found by the owner asking directly whether the tab bar/header was actually checked —
it hadn't been, and it turned out to hide the biggest gap of the chapter:** `Shell.tsx` (the shared
header/nav wrapper used by *every* screen, not part of the per-screen scope above) showed a gear
icon top-right on phone width, unconditionally. The panel's own design (Design-Audit 2026-08-13,
`.rings`/`.profilbtn` in `audi-dashboard-ios.css`) is the opposite: on phone width the gear
(`.profilbtn`) is `display:none` and the **Audi rings** sit top-right instead ("damit oben rechts
nur ein Element sitzt") — the rings are the *only* path to Einstellungen there. companion-app never
had a rings button in the narrow header at all. Fixed: `Shell.tsx`'s `kopf` now renders `<VierRinge
hoehe={17}>` (already used elsewhere for the wide sidebar) inside the same `.dm-kopf__aktion` slot,
`SymbolZahnrad` removed from that spot (still used in the wide sidebar's own footer). Also removed
`.dm-kopf__aktion[aria-current="page"]{color:var(--red)}` — dead now that the icon is an `<img>`
(color doesn't apply), and was already a violation of this project's own "red is destructive/accent
only, never a navigation state" rule (see the 2026-08-13 back-arrow/primary-button fixes) that had
simply never been caught because `Shell.tsx` isn't a "screen" any per-screen audit would reach.
`Shell.test.tsx`'s test name updated ("...über die Ringe, nicht über das Zahnrad") — the assertion
itself (`getByLabelText("Einstellungen")`) needed no change and was already passing throughout.
**Live verification unblocked, worth keeping as project infrastructure:** the first attempt to
verify this chapter live failed — CORS: `OPTIONS http://localhost:18123/api/` came back `403
Forbidden` from `audi_ha_test`, the documented constraint ("the web build is served from HA itself,
same-origin... or the native hull uses `CapacitorHttp`, which bypasses CORS" — a foreign dev-server
origin hits neither path). Added a **dev-only** Vite proxy (`vite.config.ts`, `server.proxy: {
"/api": { target: "http://localhost:18123", changeOrigin: true, ws: true } }`) — `server.*` never
applies to `vite build`, so this has zero effect on the shipped app. Pointing the onboarding
screen's server address at the dev server's own origin (`http://localhost:5173`) makes every
`/api/...` call same-origin from the browser's point of view; Vite's Node process (not subject to
browser CORS) forwards it server-side to `audi_ha_test`. Hardcoded to `:18123` specifically — never
point this at anything else. This unblocks live visual verification for every remaining chapter,
not just this one.
Verified: `npm run typecheck` clean, `npm test` 146/146 green, **and live end-to-end** against
`audi_ha_test` through the new proxy — screenshot confirms the rings render top-right (not a gear),
the Reichweite hero figure at real 60px size ("87 km" clearly dominant), the bare "15 %" with no
liter line, the service block correctly shows its chevron (a real next-service exists in this
container's data), and the tab bar order/icons match the panel.
**Owner escalation right after: "check EVERYTHING optically — every font, every field, every color,
every icon" — named three concrete symptoms (Mein Audi icon looked old, Reichweite laid out
differently, the Ölwechsel figure's font was wrong). All three were real, found by diffing CSS/SVG
source directly against the panel rather than by eye (no way to log into the real panel's frontend
from this session — LLAT tokens work for the REST/WS API, not an interactive HA session; entering a
password is off-limits per this project's standing rules — so every comparison here is source-level,
not a real pixel screenshot of the panel itself):**
1. **`SymbolAudi` (the "Mein Audi" tab icon) was a placeholder, not the real icon.**
`<polygon points="21.6,5 16.5,19 2.4,19 7.5,5" />` — a crude quadrilateral that was apparently
never replaced with the real Audi-CI "car from the front" glyph the panel uses (`ICONS.audi`,
filled/`voll`, from the myAudi icon set). Replaced with the exact same path, `voll` added to match
`ICONS_VOLL.has("audi")`.
2. **`.ads-title` (design-system, the page-title text used by `Shell.tsx`'s header) was 23px; the
panel's `.title` is 22px.** No recorded reason for the 1px drift — fixed in `design-system/src/
tokens/tokens.css` (design-system is meant to track the panel's own corrected values, see its own
`--fg3` comment from the 2026-08-11 contrast fix) and rebuilt (`npm run build` in `design-system/`
— it ships as a built package, `dist/`, so a source edit alone doesn't reach companion-app without
this).
3. **The Reichweite tile's whole layout order was inverted.** The panel's `.tankzeile` (explicitly
commented as a myAudi-app convention, the *only* tile in the whole panel with its label at the
bottom instead of the top) puts the big figure and the percentage side by side on one row
(`display:flex;align-items:flex-end;justify-content:space-between`), then the progress bar, then
"Reichweite" last. `Uebersicht.tsx` had the eyebrow label first (matching every *other* tile's
convention, but not this one's actual panel counterpart), and the figure/percentage stacked instead
of sitting in one row. Restructured to match exactly: new `.dm-tankzeile`/`.dm-tankzeile__pct` CSS
(byte-equivalent to `.tankzeile`/`.pct`), "Reichweite" label moved after the progress bar.
4. **`.dm-tgwert` (the big Ölwechsel/Inspektion figure on the Service screen's "Anstehende Termine")
had no `font-family` at all.** The panel's `.tg-wert` explicitly sets `"Audi Type Wide"`; without
it here, the number silently fell back to the narrow "Audi Type" body cut — a real, visible
different letterform, exactly the "font doesn't match" the owner described. Added
`font-family: var(--font-wide)`.
**Found but deliberately NOT fixed yet, flagged instead — needs its other call sites checked first:**
`.dm-navikachel__zahl` (the shared `NaviKachel` component's big value, used by the "Letzte Fahrt"/
"Letzte Tankung" tiles on Übersicht, and also by `Service.tsx`, `MeinAudi.tsx`, `Versicherung.tsx`)
renders at 20px/weight 300 with no `font-family` override — but the panel's actual teaser equivalent
(`.leaf .v`, the "Zuletzt" tile) is plain 13px body text, no Wide font, no hero treatment at all. This
is a real, likely bigger mismatch than the Ölwechsel one — but `NaviKachel` is shared across (at
least) five screens, some of which may have a *different* panel counterpart than `teaser()` with its
own, possibly-correct-as-is sizing. Fixing this blind risks regressing a screen not yet audited.
**Do not touch `NaviKachel`'s styling without first checking every one of its five call sites against
its own specific panel counterpart** (Chapter 2/3/4's territory, not just Chapter 1's).
Verified (items 14 above): `npm run typecheck` clean, `npm test` 146/146 green after every step,
**re-verified live** through the dev proxy at a true 375×812 mobile viewport (`resize_window` preset
"mobile" — the owner separately asked for this, since screenshots up to that point were desktop-pane-
narrow rather than an actual phone viewport): rings top-right, real car icon on "Mein Audi", the
Reichweite row/percentage/label order all confirmed matching.
**Standing method going forward, all remaining chapters:** structural/logic review alone is not
enough — cross-check every relevant CSS rule (`font-family`, `font-weight`, `font-size`, color
tokens) against the panel's own stylesheet line by line, not just JSX structure, since this is where
three of these four bugs actually lived.
**Real login access to the panel, corrected 2026-08-29:** the owner had `http://localhost:18123/
audi-dashboard` open and already authenticated in this session's own browser tool (a second tab,
separate from the companion-app dev-server tab) — contrary to the earlier "I can't log in" claim
made a few turns earlier in this same conversation. That claim was right about the actual constraint
(entering a password into a login field is off-limits, categorically, for any instance including a
disposable test one — confirmed `audi_ha_test` has no `trusted_networks` passwordless provider
either) but wrong to conclude "so I can never see the real panel" — an *already-authenticated* tab is
a different thing entirely, nothing was typed into a credential field to get it. Once pointed at it,
direct DOM/computed-style queries against the panel's real shadow root (`javascript_tool`, walking
into `audi-dashboard-panel`'s `shadowRoot`) became possible — ground truth, not inference from CSS
source. Also resized this tab to a real 375×812 viewport per the owner's request (`resize_window`
preset "mobile" on `tabId` for the HA tab too), since the panel embedded in HA's own sidebar at
desktop width is not what the phone app actually renders.
**Two more real bugs found this way, immediately:**
1. **`.ads-eyebrow` (design-system) used the pre-2026-08-13 uppercase Audi-CI eyebrow style —
10px, `text-transform:uppercase`, 0.2em letter-spacing — everywhere, across every screen.** Live-
measured against the panel's actual `.label` (used for "Reichweite" and the same role throughout):
`font-size:13px; letter-spacing:.01em; text-transform:none; font-weight:400; color:var(--fg3)`
the iOS-overlay override from the "iOS look made official" decision (2026-08-13, see the
iOS-vs-Audi-CI section above), which explicitly reversed the old uppercase convention project-
wide. `design-system`'s neutral eyebrow was never updated to follow that decision — it still
ships the pre-decision look, correctly, since it's also a standalone kit for Claude Design and the
iOS decision was scoped to this app specifically. Owner's explicit call when asked which side
should change: **"die iOS App soll optisch möglichst identisch zur vorhandenen HA App sein"** —
so companion-app overrides it locally (same pattern as the font-family override just above it),
`design-system` itself untouched. This is likely the single biggest contributor to the "looks
old/mismatched" impression, since `.ads-eyebrow` is used for nearly every tile caption app-wide.
2. **The entire Standort feature (live vehicle GPS position) was completely missing from
companion-app** — zero files, zero route, confirmed via a source-wide grep for "Standort" before
concluding it wasn't just misnamed. The panel's version is substantial: a small non-interactive
preview map on Übersicht (`standortKachel()`), a fullscreen map screen with a real vehicle marker
(CI `poi-car-l` icon + solid silhouette, the contour-only-icon problem documented elsewhere in
this file), an optional own-device marker (browser geolocation), distance-to-vehicle text, reverse-
geocoded address (Nominatim, cached), a fährt/steht/"geparkt seit" status line, tank%/range values,
Route (Google Maps deep link) and Teilen (Web Share API, clipboard fallback) actions, four floating
map-style/center controls, and a draggable peek/expand bottom sheet. Owner's answer when asked
whether this was in scope: **"hier greift wieder 1. und selbstredend muss die technische Funktion
erstellt werden"** (of course the function has to be built too).
**Built: `src/screens/Standort.tsx`** (new), exporting `StandortVorschau` (the Übersicht preview
tile, embedded there right after the service tile — same position as `standortKachel()` in `vHome()`)
and `Standort` (the fullscreen screen, registered as a new route). `navigation.ts` gained
`"standort"` (`ZURUECK.standort = "home"`, `TITEL.standort = "Standort"`); `register.tsx` wires it.
Reused `Karte.tsx`'s established pattern (Leaflet via dynamic `import()`, bundled not CDN — same
reasoning as there: this app can be network-isolated to a VPN tunnel, and a CDN fetch failing
silently was an already-documented panel-era bug) but written fresh rather than extended, since
Standort's needs (live single point + a second, independent user point, click-to-open-sheet, style
toggle) don't fit `Karte`'s start/ziel/route-triplet API. The CI marker icons (`poi-car-l` outline +
its silhouette, `map-layer`/`car-search`/`gps`/`select-all`/`plan-route`/`fuel-range`/`parking`) are
verbatim panel path data, kept local to this file (matching the existing precedent of `symbole.tsx`'s
`SymbolOelwechsel`/`-Inspektion` — "vom Nutzer als SVG geliefert, verbatim übernommen").
`fahrzeug.standortLat/standortLon/standortZeit` and `zuendung` were already present and correctly
mapped in `profilAdapter.ts` (built earlier, never consumed by any screen until now) — no data-layer
work needed, only the UI.
**One deliberate difference from `standortMenuVerdrahten()`'s exact wiring:** the panel binds
pointer handlers on the whole shadow root filtered by `closest(".standortmenu-griffzone")` and
separately gives the grip a `data-standort-umschalten` click handler through the shared central
dispatcher. Ported as React `onPointerDown/Move/Up/Cancel` directly on the griffzone element (ref-
based direct `style.transform` writes during the drag, matching the panel's "manipulate the DOM
directly, no re-render per pixel" reasoning) — but the grip button's own `onClick` was **dropped**,
not ported 1:1: a plain tap already satisfies the pointerup handler's own `Math.abs(dy) <= 40` toggle
branch, and keeping both would double-toggle on every mouse/touch tap (open-then-immediately-close).
Keyboard access (the entire reason the panel made the grip a real `<button>`, per its own comment)
is preserved via a dedicated `onKeyDown` handler for Enter/Space instead.
**Verified: `npm run typecheck` clean, `npm test` 147/147 green (147, not 146 — the existing render-
every-page smoke test picked up the new `standort` route automatically), `npm run build` succeeds.
Live end-to-end through the dev proxy at a real 375×812 viewport**, and this is the first companion-
app-only feature in this file verified against genuinely live GPS/geocoding, not just fixtures: the
preview tile rendered a real map with the real car-pin marker at the test container's actual
coordinates; opening it showed the correct empty/denied-permission copy ("GPS-Freigabe fehlt",
byte-identical to the panel's own string) since this sandboxed browser has no location permission;
dragging/tapping the sheet handle opened it to reveal a **real, successfully reverse-geocoded
address** ("St 2047, 91809 Wellheim") via a live Nominatim call, the correct "Geparkt seit 7 Std. 26
Min." status (derived from this container's actual last-trip end time), the tank%/range values, and
both the "Route"/"Teilen" pills rendered correctly (primary filled vs. outline). One tooling note
worth keeping: clicking anything that triggers `navigator.geolocation.getCurrentPosition()` makes
the browser-automation tool's click call report a 30s timeout even though the click and the
resulting render both succeed — the permission prompt blocks the tool's own completion signal, not
the page. A follow-up screenshot after the reported timeout always showed the correct, fully-
rendered result — don't mistake this specific timeout for a real failure on this page.
Manifest/OTA not bumped yet — deferred to the chapter-11 wrap-up per the standing chapter plan,
same as every other companion-app-only change so far this audit.
**Owner follow-up round, same day — three real items and one false alarm:**
1. **"Teilen" had no icon at all** — a real omission in the port, not present in the panel: `Standort.tsx`'s share button was built as bare text. The panel's own markup (`ciSVG(CI.upload, 15)}Teilen`, read directly this time rather than assumed) uses the `upload` CI icon. Added `CI.upload` to `Standort.tsx` and rendered it before the label, matching exactly.
2. **Tank/range row icon, owner-requested change: "soll identisch zum Tanken-Icon sein."** Live-extracted from the real, authenticated panel DOM (see below) that this row currently uses `CI.fuelRange` — a different, filled CI icon, not the stroke-drawn tab icon (`ICONS.fuel`) used for the "Tanken" tab. This is a deliberate product correction, not a parity bug being fixed — applied to **both** codebases per the parity rule so they don't immediately drift apart again: panel's `.standortmenu-werte` now renders `ICONS.fuel` directly (stroke SVG, matching the tab icon's own wrapper attributes exactly) instead of `ciSVG(CI.fuelRange)`; companion-app's equivalent now renders the existing `<SymbolTanken groesse={16}>` component instead of the local `CI.fuelRange` path (which was removed from `Standort.tsx`'s `CI` object as the resulting orphan). `CI.fuelRange` was also removed from the panel's own `CI` catalog (no longer referenced anywhere). Manifest bumped to `2026.8.30.1`, deployed to `audi_ha_test`, restart confirmed clean via log, live-reconfirmed via the panel's real shadow DOM that `.standortmenu-werte` now renders the fuel-pump stroke icon.
3. **"Das oberste der vier Icons ist fehlerhaft" — investigated, not reproduced.** Extracted the live-rendered top control button's full `outerHTML` from both companion-app and the real, authenticated panel and diffed them: byte-identical SVG (`viewBox`, `width`/`height`, `fill`, the `mapLayer` path itself) and identical wrapper attributes. Also functionally exercised it (not just visually inspected): clicking it in companion-app flips `aria-pressed` and swaps the tile-layer source to the real ArcGIS satellite endpoint, exactly as designed — confirmed via `read_network_requests`-equivalent direct tile-`src` inspection, not just a screenshot. Could not find a defect through either markup or behavior. Flagged back to the owner to ask for a device screenshot rather than continuing to guess blindly.
4. **False alarm, root-caused and worth recording so it isn't chased again: "iOS App Darstellung ist aktuell im Zoom."** Not an app bug — the Browser tool's own `resize_window` mobile-viewport emulation had gotten into a visibly inconsistent state across this session (screenshots of the same 375×812-declared viewport rendered at wildly different effective scales moments apart, and a tab's internal id silently changed — `tab-2` became `tab-4` — partway through, which likely contributed). Confirmed via `getComputedStyle` that the actual values were correct throughout regardless of how the screenshots looked (`.ads-title` 22px, `.ads-fig` 60px, matching the panel's own live-measured values exactly, recorded earlier this file) — the CSS was never wrong. Fixed by clearing the viewport emulation (`resize_window` preset `"desktop"` on the affected tab), which returned the pane to its normal responsive size and resolved the visual "zoom" the owner was seeing directly in the shared pane. **Lesson for the rest of this project's remaining chapters:** don't trust an apparent size/scale difference between two screenshots taken via this tool without also checking computed pixel values — screenshot scale in this tool has proven unreliable this session, computed styles have not.
Owner's broader ask alongside these four: a full, no-detail-spared pass — "Prüfe vollständig. Ich kann dir nicht jede Abweichung aufzeigen" (check everything myself; fonts, frames/backgrounds that shouldn't exist, the tab-bar active-icon treatment) — explicitly including a standing offer to say if any font or icon asset is missing.
**Tab-bar active state, checked immediately after — a real, significant bug, the biggest single-CSS-rule find of the audit so far.** Live-measured both: the panel's active tab (`.tab.on` in `audi-dashboard-ios.css`) gets a background pill behind icon+label (`rgba(255,255,255,.14)` dark / `rgba(0,0,0,.07)` light, `border-radius:14px`, text color `var(--fg)` — no red anywhere). `design-system`'s `TabBar.css` `.ads-tab--on` instead used `border-top-color: var(--red)` — a **red top border**, a completely different mechanism, and a violation of this project's own long-standing "red is accent/destructive only, never a navigation state" rule (the same rule that removed red from the back arrow, primary buttons, and the Arbeitsweg pill in 2026-08-13/17) that `design-system` was simply never updated to follow. Fixed directly in `design-system` (same reasoning as the `.ads-title` fix earlier — this is a plain oversight, not a deliberate fork like the eyebrow case): removed the red border-top mechanism entirely, added the measured background-pill treatment with a light/dark override (`[data-theme="tag"]`). Rebuilt the package; verified live (`background-color: rgba(0,0,0,.07)`, `border-radius: 14px`) and via screenshot — grey pill behind the active tab, no red.
**Fourth reported round, same session — four more items, three real bugs and one clarification:**
1. **Grey card backgrounds where the panel has none.** The RS4/Kennzeichen block and the Reichweite tile were both plain `<Tile>` (default variant, `background: var(--tile)`) — but their panel counterparts are both `class="tile flat"` (confirmed live: `background-color: rgba(0,0,0,0)` on all three `.tile.flat` instances on this screen). `design-system`'s `Tile` already has a `variant="flat"` that does exactly this and was simply never used here. Fixed: `Uebersicht.tsx`'s Reichweite `<Tile>``<Tile variant="flat">`; the vehicle-image block's own wrapper (`.dm-szene`) had its `background: var(--tile)` removed directly in `screens.css` (it isn't a `<Tile>` at all, a plain div, so the fix lives in its own class).
2. **Fahrzeugbild too large.** Root cause: `ImagePlaceholder`'s default size is `aspect-ratio: 4/3` — correct for the gallery/wheel-photo usages elsewhere, but the panel's Übersicht image specifically uses `"carfix klein"` (16:8), and live-measured the panel's actual box at only ~120130px tall regardless of width. Rather than changing the shared component's default (would wrongly affect every other correctly-sized usage), scoped an override to `.dm-szene .ads-bildbox { aspect-ratio: 16/8 }` in companion-app's own `screens.css` — same "local override, shared component stays generically correct" pattern used for the font-family and eyebrow fixes.
3. **"Satelliten-Icon fehlerhaft" — investigated a third time, including now with the owner's own screenshot as a reference, still not reproduced.** Re-diffed `outerHTML`, then went one level deeper than the earlier pass and checked actual *computed* paint values on the live path element: `fill: rgb(16,20,24)` (correctly resolves the day-theme icon color), `fill-rule: nonzero` (default, same as the panel — no override exists on either side), `stroke: none`. Every measurable value matches. The icon (`mapLayer`, a 3-band stacked-diamond glyph) is genuinely hard to read at 22px in plain monochrome, which may be exactly what looks "off" in a screenshot — but that ambiguity is identical in both apps, not a companion-app defect, as far as three independent checks (markup, function, paint) can tell. Left as an open question back to the owner rather than guessed at further.
4. **"Standort Slide Up ist leer" — a real bug, found via DOM inspection, not a data/content problem.** The sheet's own content was always complete and correct (address, status, tank/range, both action buttons all present and populated in the DOM even while "empty"-looking) — confirmed directly: `.dm-standortmenu`'s measured `transform: translateY(147px)` against its own `243px` height correctly implements the 96px peek reveal, exactly as designed. The actual bug was one level up: `.dm-standort-vollbild` was capped at `height: min(640px, calc(100dvh - 190px))` (copied verbatim from the panel, correct *there* because the panel's whole app is itself boxed into an ~880px-tall card) — but companion-app's page has no such outer cap, so on a taller viewport the capped map+sheet left a large stretch of plain page background below it, the same canvas color as the (correctly) mostly-hidden sheet — reading as one big empty area. Fixed by making `.dm-standort-vollbild` fill the remaining flex space instead of capping (`.dm-inhalt > .dm-standort-vollbild { flex: 1 1 auto }`, overriding rahmen.css's blanket `.dm-inhalt > *{flex:none}` for this one screen specifically — the only screen whose entire content is one full-bleed interactive area with nothing else below it). Verified live: the map+sheet now fill the full screen down to the tab bar, no leftover blank strip, on the same viewport that showed the bug before.
Verified (this round): `npm run typecheck` clean, `npm test` 147/147, live-reconfirmed via reload + screenshot for all three real fixes (flat tiles transparent, image band compact, Standort screen fills to the tab bar). No panel-side changes this round — all four items were companion-app-only bugs, not places where the panel and app had made a deliberate, disclosed different choice.
**"Diese App ist älter als der Server — bitte aktualisieren" explained (owner asked directly):** this is the app's own compiled-in version (`__APP_VERSION__`, baked in from `manifest.json` at `vite build`/`vite dev` startup) compared against what the backend currently reports (`sensor.audi_dashboard_app_version`) — see `VERSIONIERUNG.md`. It's doing its job correctly right now: this session bumped the manifest to `2026.8.30.1` for the panel-side icon fix without restarting the *dev server process* (only reloading the page), so the running dev server is still serving the previous version's compiled-in number against a backend that has since moved on. Not a bug — the banner is the intended, working signal for exactly this situation, and would clear on the next full dev-server restart (or, in the real native app, the next OTA/Xcode rebuild).
**Next planned step, still open**: continue the systematic Shell.tsx/shared-chrome sweep the owner asked for, screen by screen, now informed by the pattern this round has repeatedly found — check every `<Tile>` variant choice (flat vs. default) and every locally-sized image/icon against the panel's live DOM, not just structure/logic.
## The single biggest find of the whole audit: `design-system`'s color/radius tokens were read from the wrong stylesheet (2026-08-30)
Owner, after several rounds of individually-fixed bugs: **"Font, Schriftgröße, Formatierung, Zeilenabstand, etc. sind noch wesentlich abweichend. Prüfe das immer mit."** — do a real typography sweep, not just background/size spot-fixes, and make it standing practice from here on.
Ran a systematic computed-style diff (font-family/size/weight/line-height/letter-spacing/color) across every text element on Übersicht, panel vs. companion-app, live. Two things fell out immediately:
1. **The `--fg2`/`--fg3` colors this file's very first color-token comparison (chapter 1) called "match exactly" don't, in practice — because that comparison only ever read `:host`/`:host([data-theme=...])` in the *base* `audi-dashboard.css`.** The panel **always** loads a second stylesheet on top, `audi-dashboard-ios.css`, which redefines nearly the entire palette (confirmed live via `getComputedStyle` on the real panel's `:host` — not by reading source, since two stylesheets cascading needed a live answer, not a static one). The base-file values were never actually reaching the screen in the real app; `design-system` had been quietly built against a dead reference the whole time. Corrected `design-system/src/tokens/tokens.css` to the actual live values for both themes:
| Token | Old (base-file) | Now (live iOS-overlay value) |
|---|---|---|
| `--canvas` (nacht/tag) | `#161b23` / `#FFFFFF` | `#0C1014` / `#F2F2F7` |
| `--tile` | `#1f2733` / `#f2f2f2` | `rgba(255,255,255,.055)` / `#FFFFFF` |
| `--tile-2` | `#2a3341` / `#e5e5e5` | `rgba(255,255,255,.10)` / `#EFEFF4` |
| `--line` / `--line-strong` | grey-based rgba | `rgba(255,255,255,…)` / `rgba(60,60,67,…)` (iOS separator convention) |
| `--fg2` | `#9aa1ad` / `#4c4c4c` | `#BFC4CC` / `#3C3C43` |
| `--fg3` | `#8a94a3` / `#666666` | `#8E8E93` / `#8E8E93` (same both themes — iOS `tertiaryLabel`) |
| `--ok`/`--warn`/`--bad` | Android-Material-ish greens/reds | iOS `systemGreen`/`systemOrange`/`systemRed` |
| `--r-tile` | `20px` | `16px` |
This single fix touches nearly every screen at once — every tile background, every secondary/tertiary text color, every corner radius in the app was drawn from the wrong palette. It's almost certainly the largest single contributor to "sieht insgesamt anders aus" of anything found this session, bigger than the individual per-element fixes around it. Also corrected in the same pass: `.ads-title` `letter-spacing` (`-.015em``-.01em`) and `line-height` (`1.15``1.2`), both live-measured against the real `.title`.
2. **The badge "zusatz" text (e.g. "Avant Competition" next to the model badge) was rendering at 22px/black/inherited-title styling — the panel's is 13px/`var(--fg2)`/plain.** Root cause in `Uebersicht.tsx`: the wrapping `<div>` incorrectly carried the `ads-title` class (only needed for the no-badge-detected text fallback path, but applied unconditionally), and the inner `<span>` used `.ads-sport` (italic, a style meant for something else, not this spot — the panel's `.badge .zusatz` is plain, not italic). Fixed: wrapper is now plain `dm-szene__zusatz` with no `ads-title`; the local `.dm-szene__zusatz` CSS rule (which had its own separate `font-size:19px` override, likely written to visually "match" the wrongly-inherited 22px title rather than the panel) corrected to `font-size:13px; color:var(--fg2); line-height:normal`; the inner span dropped `.ads-sport` entirely.
Rebuilt `design-system`, verified live: `.ads-title` letter-spacing/line-height now `-0.22px`/`26.4px` (byte-match to the panel's measured values), zusatz now `13px`/`rgb(60,60,67)`/non-italic (matches), `.ads-eyebrow` color now resolves to the correct `rgb(142,142,147)` (this was already using `var(--fg3)` from an earlier fix — it was correct code all along, just pointed at a wrong token value until this round). `npm run typecheck` clean, `npm test` 147/147.
**Standing rule from here on, per the owner's explicit instruction:** every remaining screen check must include font-family, font-size, font-weight, line-height, and letter-spacing — not just background/sizing — and should be verified against **live computed styles** on the real panel, not against static CSS source, given source-reading already produced one wrong "verified matching" conclusion this session (the token comparison above).
**Finishing the Übersicht typography pass, same round — three more real bugs, found by owner report and by the same live-computed-style method:**
1. **"87 km falsche Font/Formatierung."** `.ads-fig`'s `letter-spacing` was `-.02em` — copied from the *base* `audi-dashboard.css`'s `.fig`, the same class of mistake as the color tokens above: the iOS overlay overrides it again, separately, to `-.03em` (`audi-dashboard-ios.css: .fig{letter-spacing:-.03em}`, live-confirmed `-1.8px` at 60px = `-.03em`, not the `-1.2px`/`-.02em` companion-app was rendering). Fixed in `design-system/src/components/Fig/Fig.css`. Worth remembering going forward: **any class with a same-name rule in `audi-dashboard-ios.css` needs the overlay's value, not the base file's** — this is now the second time (colors, now letter-spacing) the base file alone gave a wrong answer.
2. **"Nächster Service und 8.600 km sind zu nah zusammen."** Confirmed: the panel's `.fig-zeile` (icon+figure row) has `margin-top:8px` relative to the label above it; companion's equivalent `.dm-servicefig` had none at all — zero gap. Added `margin-top:8px` to `.dm-servicefig`, plus the same margin on `.dm-serviceblock > .ads-fig` for the no-icon fallback case (Hauptuntersuchung), where the panel applies the identical margin directly to the bare `.fig` instead of a wrapper row.
3. **The "Letzte Fahrt"/"Letzte Tankung" teaser tiles, flagged earlier this session as "found but not fixed — check other `NaviKachel` call sites first."** Checked: `.dm-navikachel__zahl` (the 20px/weight-300 "hero number" treatment) turned out to be used **only** at these two call sites in the entire app (`grep` confirmed) — every other `NaviKachel` usage (`MeinAudi.tsx`, `Service.tsx`, `Versicherung.tsx`) passes a plain string through the ordinary `.dm-navikachel__wert` (untouched, out of scope). Safe to fix directly: the panel's real teaser value (`.leaf .v`/`.leaf .v small`, live-measured) is plain — `15px`/weight `400`/`var(--fg2)` for the value, `12.5px`/`var(--fg3)` for the caption — no Wide font, no hero treatment at all. `.dm-navikachel__zahl` corrected to those exact values; a new, more specific `.dm-navikachel__wert .dm-navikachel__zahl + small` rule added for the caption (a same-specificity `.dm-navikachel__zahl + small` was tried first and silently lost to the pre-existing `.dm-navikachel__wert small` rule later in the file — needed the extra specificity, not just presence, to actually win the cascade).
Verified: `npm run typecheck` clean, `npm test` 147/147, live-reconfirmed for all three (`.ads-fig` letter-spacing `-1.8px`, `.dm-servicefig` margin-top `8px`, teaser value `15px`/`400`/`rgb(60,60,67)` and caption `12.5px`/`rgb(142,142,147)`) — plus a screenshot: visible gap now between "Nächster Service:" and "8.600 km", teaser tiles now read as plain text like the panel instead of oversized figures.
**Actually not quite closed — two more real bugs, found right after "closing" it:**
1. **Dev server itself got into a broken state ("iOS App freezed again")** — not an app bug. After many rapid `design-system` rebuilds + manifest bumps + hot-reloads this session, the Vite dev server's HMR/websocket state had visibly desynced (console showed dead `ws://localhost:5173/` connections and a stale `ReferenceError: StandortVorschau is not defined` that survived plain page reloads). Fixed by a full `preview_stop`/`preview_start` cycle, not just a reload — this also cleared the lingering "Diese App ist älter als der Server" banner from the earlier manifest bump, since the fresh server re-embeds the current version. **Worth remembering for the rest of this audit: a stuck/frozen-looking dev preview after several rebuilds in one session needs a real server restart, a page reload is not enough.**
2. **"RS4 + Kennzeichen not in line on left sided" — real, confirmed via bounding-rect measurement on both apps.** Live-measured the panel: the Kennzeichen text and the "Reichweite" label sit on **exactly the same left edge** (`left:16px` both — both live inside a zero-padding `tile flat`). Companion-app measured three *different* left edges for elements that should share one: Kennzeichen `38px`, "Reichweite" `22px`, "Nächster Service" `44px`. Root cause: `.dm-szene__text` (wrapping the badge/model-name/Kennzeichen block) carried its own `padding: var(--sp-4) var(--sp-4) var(--sp-5)` — a left/right inset the panel's equivalent (a zero-padding flat tile) never has. Fixed: `padding: var(--sp-4) 0 var(--sp-5)` — keeps the vertical spacing (gap under the image, gap before the next tile), drops the horizontal one. Verified live: Kennzeichen and "Reichweite" now both `left:22px` (companion-app's own page-padding equivalent of the panel's `16px`), byte-equal to each other.
**Flagged, not fixed — a bigger, systemic question, out of scope for a quick fix:** "Nächster Service" still sits at `44px`, not matching the `22px` the other two share — because `.ads-tile`'s own internal padding is `var(--sp-5)` (22px) uniformly, while the panel's equivalent `.tile` padding is `16px 18px` (different value, and asymmetric top/bottom vs. left/right). This is a **global** `Tile` padding question (affects every tile on every screen, not just this one), not a one-line local fix like the others in this round — noted here so it isn't lost, not yet acted on.
Verified (this round): `npm run typecheck` clean, `npm test` 147/147, dev server confirmed responsive after restart, alignment fix confirmed via matching bounding-rect `left` values and a screenshot.
**This closes the Übersicht screen for this audit** (owner confirmed via "erst die Übersicht noch zu Ende typografisch durchgehen … go for it"). Every text element on it has now been individually live-diffed against the panel at least once. Next: Mein Audi, same method (structure **and** full typography from the start this time, not retrofitted after the fact) — and now also carrying forward the open `Tile`-padding question above, worth checking again on the next screen before deciding whether it's a real systemic fix or specific to this one spot.
## Chapter 3 addendum: `Reifen.tsx` was structurally wrong, not just styled differently (2026-08-30)
Owner, mid-Chapter-5: "reifen sieht völlig unterschiedlich aus und passt nicht zu meiner identisch
vorgabe." Investigated `vReifen()` (panel, line 2924) against `Reifen.tsx` and found this screen had
never actually been brought in line — Chapter 3's entry above only added the two missing edit
features (Anzugsmoment/km pencils) onto the *old* structure, never checked the structure itself
against the panel.
**Real structural differences, all fixed:**
1. Panel shows **both** Sommerräder and Winterräder tiles simultaneously, always — companion-app used
a `Seg` toggle to show only one at a time. Removed `Seg`; both `satzTile("sommer")`/
`satzTile("winter")` now render unconditionally.
2. Panel has Anzugsmoment as its own top tile (eyebrow + big `.fig` value + gear-style pencil) — this
was already right from the earlier Chapter-3 pass, kept.
3. **Wechseltermin was previously folded into a combined "Montiert" tile as a read-only
`Wertzeile` + a plain date `<Feld>`** with no "Termin vereinbart" toggle at all. Rebuilt as its own
tile matching `vReifen()`'s: `Geplant am` date field, a `Switch` for `Termin vereinbart` (new —
didn't exist here before), and "In den Kalender übernehmen" — mirroring the panel's own quirk that
this one date/checkbox pair only actually saves when that button is clicked (`kalenderUebernehmen()`
saves via `profilSpeichern` and immediately downloads the `.ics`, matching `data-ics="reifen"`'s
`$("#rfDat")`/`$("#rfOk")` read at click time).
4. **Each satz tile's Hersteller/Modell/Größe/DOT/Solldruck/Kommentar fields were entirely read-only**
(`Werteliste`/`Wertzeile`) — a real functional gap, not just a display difference: there was no way
to edit a tire set's own data at all (only the archived sets, via `ArchivZeile`, were editable).
Panel's `data-rf` fields save live on `change`. Ported the same immediate-save behavior via
`onBlur` (React's closest equivalent to native `change` for text inputs) — `feldSpeichern()`, one
`profilSpeichern` call per field per satz.
5. Layout: `.radkopf-row` (headline+km left, wheel photo right, both top-aligned) — new
`.dm-radkopf-row`/`.dm-radkopf`/`.dm-tile-headline`/`.dm-reifenzeile`/`.dm-radfoto` CSS in
`screens.css`, live-measured off the panel's own `.radkopf-row`/`.tile-headline`/`.fig`/`.km-edit`
rules. `.dm-montiert`/`.dm-montiert--an`/`.dm-archivbtn` similarly copied from `.montiert`/
`.montiert.on`/`.archivbtn` (outline pill, filled black/canvas when the satz is the active one — was
previously a single "Auf X wechseln" `ActionButton`, not a per-satz clickable pill).
6. `montierenAuf()` (the per-satz "Montiert" pill) calls `reifen_wechseln` **without a confirmation
dialog** — the panel's own `data-satz` handler has none (`bestaetigen()` is only used for
*archiving*, not for switching); the old `wechseln()` function had one, removed to match exactly.
7. Kommentar field made full-width (label above, not beside) after a live check showed "Eingelagert
beim Autohaus" truncating in the label-beside-input layout — same `.feld--breit`/bypassed-`Feld`
pattern already established in `Versicherung.tsx`'s Vertragsnotiz.
Verified: `npm run typecheck` clean, `npm test` 147/147, live-verified through the dev proxy against
`audi_ha_test` — both tire sets render simultaneously with real data (Sommerräder: 275/30 R20 97Y XL,
2,5/2,4 bar; Winterräder: Michelin Pilot Alpin 5, 265/35 R19, 3822, 2,65/2,5 bar, "Eingelagert beim
Autohaus" — none of this was previously visible or editable), Montiert pills correctly filled/outline
per active satz, Archiv section unaffected.
## Chapter 5 (Sicherheit) closed: three confirmed issues fixed in `Fahrzeugstatus.tsx`, plus a real
structural bug in `TuerenKlappen.tsx` found while verifying it (2026-08-30)
Continuing the audit resumed after the Reifen interruption above. `Fahrzeugstatus.tsx` vs. the
panel's `vSicherheit()` (line 1541) had three confirmed-but-unfixed issues from the prior pass, now
fixed:
1. **Missing "Draufsicht" top-view image** — added at the top via the existing `.dm-szene` wrapper
(same 16:8 treatment already established for Übersicht's vehicle image), reading
`"draufsicht.webp"` — which itself needed adding to `bilder.ts`'s `WEITERE_BILDER` list first
(confirmed via the backend's `bilder.py` that this filename is a real, recognized upload slot;
companion-app's own list was simply missing it).
2. **Wrong top-status indicator** — the top summary box used `<StatusRow>` (a plain colored dot),
inconsistent with both the panel (which reuses its checkmark-circle `statusKreis()` for the
summary too) and with the four detail rows below it (which already correctly use `<StatusKreis>`).
Replaced with a plain `<Tile>` + `<StatusKreis ok={gesichert}>` + text span, matching the panel's
`style="display:flex;align-items:center;gap:12px;padding:16px 20px"` box exactly — new
`.dm-sicherheitssammel` CSS in `screens.css`.
3. **Extra subtitle not in the panel** — the `statusStand`/timestamp line under the summary text was
dropped entirely (the panel's equivalent box has no such line); the now-unused `statusStand`
import was removed from the component.
**Found while verifying — `TuerenKlappen.tsx` didn't match its panel counterpart `vTuerenKlappen()`
(line 1576) either**, a real structural gap beyond a quick styling pass: it used a bespoke
`.dm-pruefliste`/`.dm-punkt` list (dot-before-label, single-column) instead of the established
`.dm-wertzeile` dt/dd row (label left at full strength, dot+status right in the secondary tone) every
other detail row in the app uses — and the panel's `vTuerenKlappen()` deliberately uses the *plain*
dot (`.dot`, not the checkmark circle from `vSicherheit()`) for this specific detail list. Rebuilt to
reuse `.dm-wertzeile` + `@audi-dash/ui`'s own `.ads-dot`/`.ads-dot--ok/--warn/--bad` (11px, byte-match
to the panel's `.dot`), removing the now-orphaned `.dm-pruefliste`/`.dm-punkt` CSS entirely (grepped
first — the only other reference was one test assertion, updated to check `.dm-wertzeile` instead).
Verified: `npm run typecheck` clean, `npm test` 147/147, live-verified through the dev proxy against
`audi_ha_test` — Draufsicht placeholder renders at the top, the summary box shows the green
checkmark-circle + "Fahrzeug ist sicher abgestellt" with no timestamp line, all four detail rows show
checkmark-circles with only "Türen und Klappen geschlossen" carrying a chevron, and its detail page
now shows the plain colored dot beside "zu"/"offen"/"unbekannt" on the right with the door/Kofferraum/
Motorhaube label at full strength on the left — matching `vTuerenKlappen()` exactly.
**Chapter 5 is closed.** Next: Chapter 6 (Fahrten) at the same depth, per the owner's "1" choice.
## Chapter 6 (Fahrten): mostly already correct, three real gaps found and fixed (2026-08-30)
`Fahrten.tsx`/`FahrtDetail.tsx`/`FahrtFelder.tsx` were largely already in good shape (the fields, the
edit-toggle idiom, the "Verbrauch" row, the Art `Pill` colors were all already right from earlier
work this session/project). Three real, fixable gaps found by diffing against `vTrips()`/`vTrip()`/
`fahrtFelder()`/`initMap()`:
1. **The trip list rows never showed the route at all.** `vTrips()`'s primary line per row is
`strecke(t)` — "Start → Ziel", one side if only one is known, or "Fahrt ohne Ortsangabe" — with
date+Art as the small caption. `Fahrten.tsx`'s rows showed only the km figure and a date/time/
duration caption; the actual journey (the single most identifying fact about a trip) was nowhere
in the list, only reachable by opening each trip individually. Rather than inverting the row's own
established primary/secondary convention (this codebase consistently makes the *value* the row's
`titel` across every list screen — Tanken.tsx included — even though the panel itself isn't
consistent about it either: `vTrips()` leads with route, `vTanken()`'s tank list leads with date),
added a `strecke()` helper (same three-case logic) and prepended it to the existing `unter` caption
line, closing the actual missing-information gap without fighting the app's own established idiom.
2. **`.dm-detail__zahl` was 40px everywhere, including `FahrtDetail.tsx`/`TankDetail.tsx`.** The panel
draws the hero figure at 52px specifically for `vTrip()`'s distance and the tank-detail's liters
(confirmed via grep: `style="font-size:52px"` at both call sites, vs. 40px/46px/34px elsewhere) —
40px is correct for the *other* detail screens (Reifen archive, Vertrag, ...) that share the same
class, so the fix is a new `.dm-detail__zahl--gross` modifier applied only to those two screens,
not a change to the shared default.
3. **`Karte.tsx`'s start and end markers were both plain red circles**, visually indistinguishable —
the panel's `initMap()` draws Start as a black-filled, white-ringed circle and Ziel in red
specifically so the two ends read differently at a glance; also added the small intermediate
red dots along a real GPS route (`route.slice(1,-1)`) that the panel draws and companion-app
didn't, and matched the polyline's `weight:2.5`/`opacity:0.9` (was `weight:2`, no opacity).
**Checked and left alone, deliberately:** `LiveFahrt.tsx` has no panel counterpart at all (no
`vLiveFahrt()` exists in the panel — this is a companion-app-only screen, explicitly marked in its own
header comment as built-but-disabled pending the FMM003 hardware), so there's nothing to diff it
against; the Art pill on `FahrtDetail.tsx` is decorative-only (no click-to-toggle like the panel's
`data-art` button) — capability still exists via the "Bearbeiten" form, an already-established
edit-toggle-vs-inline idiom difference, not a gap.
Verified: `npm run typecheck` clean, `npm test` 147/147, live-verified through the dev proxy against
`audi_ha_test` — the trip list now shows "Ingolstadt → Nürnberg" / "Fahrt ohne Ortsangabe" per row, and
the trip detail's distance figure ("95,0 km") renders visibly larger, matching the panel's 52px hero
treatment. The marker-color fix in `Karte.tsx` is a straightforward style-parameter change, verified
by `tsc`/tests only — this test container's trips have no GPS track to render live.
**Chapter 6 is closed.** Next: Chapter 7 (Tanken) at the same depth.
## Chapter 7 (Tanken): a real string bug, missing accordion detail, and a completely missing
SmartDeal feature plus edit capability — all fixed (2026-08-30)
Diffed `Tanken.tsx`/`TankDetail.tsx` against `vFuel()`/`vFill()`/`tankFelder()`/`tankFormularWerte()`.
1. **Real bug: the plural of "Tankvorgang" was broken.** `` `${de(n)} Tankvorgang${n === 1 ? "" : "̈e"}` `` —
the non-1 branch was a bare combining-diaeresis-plus-"e" (`"̈e"`), not the actual word change
"Tankvorgänge" needs (a→ä, not just an appended suffix — unlike "Fahrt"/"Fahrten", which *is* a
suffix). Rendered as "Tankvorgang" with a stray floating umlaut. Fixed with explicit singular/
plural branches ("1 Tankvorgang" / "N Tankvorgänge").
2. **Year/month accordion headers showed only the cost total — the panel's secondary line (liter sum
+ average price for the year, average price for the month) and the SmartDeal-Ersparnis pill were
entirely absent.** `Accordion`'s `summary` prop already accepts a `ReactNode` (confirmed by reading
the component), so this needed no new component — a multi-line flex-column summary matching
`vFuel()`'s own markup byte-for-byte (17px/`var(--fg)` cost, plain secondary line, pill only when
`CAR.smartdeal.aktiv` and the period's discount sum is positive).
3. **SmartDeal was entirely unimplemented in this screen** — grepped the whole `Tanken.tsx`/
`TankDetail.tsx` for "smartdeal"/"discount"/"markenlogo" and found only `TankDetail.tsx`'s existing
read-only "Ersparnis" row (already correct, pre-existing). Everything else was missing: no brand-logo
image, no way to *enter* an Ersparnis value anywhere, no Kraftstoff selector in either form (the
panel's `KRAFTSTOFFSORTEN` list — `["V-Power Racing", "Super Plus", "Super"]` — had no companion-app
equivalent at all). Added `KRAFTSTOFFSORTEN` as a shared export in `bausteine.tsx` (used by both
`Tanken.tsx` and `TankDetail.tsx`), a `markenlogoUrl()` helper in `bilder.ts` pointing at the panel's
own bundled asset (`custom_components/audi_dashboard/frontend/bilder/shell-logo.svg`, served at
`/audi_dashboard_static/bilder/...` — a genuinely different static path than `/local/bilder/`'s
user-uploaded vehicle photos, confirmed by reading `const.py`'s `STATIK_URL` vs `BILDER_ORDNER`), and
wired a `<Pill>` with a graceful `onError`-hidden `<img>` for the year summary (matches the panel's own
`onerror="this.closest(...).classList.add('logo-fehlt')"` fallback intent — no broken-image icon,
just the text). Kraftstoff + a conditional (`fahrzeug.smartdeal.aktiv`) SmartDeal-Ersparnis field
added to `TankFormular` (new-entry form), both already supported by the API layer's
`TankvorgangFelder` (`kraftstoff`/`ersparnis` fields existed, just never surfaced in any form).
4. **`TankDetail.tsx` had no editing capability at all** — no "Bearbeiten" button, no edit form,
unlike `FahrtDetail.tsx`'s already-existing `FahrtBearbeiten`. The only way to correct a tank
entry's values was re-uploading a receipt. Built `TankBearbeiten`, mirroring `FahrtBearbeiten`'s own
idiom exactly (local draft state pre-filled from the record, `api.tankvorgangAktualisieren`,
Speichern/Abbrechen) with the full field set from `tankFelder()`: Zeitpunkt, Kilometerstand,
Gefahrene Distanz, Kraftstoff, Getankte Liter, Kosten, SmartDeal-Ersparnis (conditional), Tankstelle.
**Checked and left as an accepted idiom difference, consistent with Chapter 6's own finding:** the list
row's primary/secondary field choice (value-primary here, matching `Tanken.tsx`'s and `Fahrten.tsx`'s
own established, consistent convention) vs. the panel's date-primary — station name is already present
in the row's caption, so no information is missing, only reordered; and no page-level delete button on
`TankDetail.tsx` (delete already reachable via the list's `Zeilenmenue`, matching the same precedent
already accepted for `FahrtDetail.tsx` in Chapter 6).
Verified: `npm run typecheck` clean, `npm test` 147/147, live-verified through the dev proxy against
`audi_ha_test` — "13 Tankvorgänge" (was garbled), year/month accordions show the cost+liter/price
secondary line and a "29,76 €"/"12,90 €"/"20,36 €" SmartDeal pill per year (image gracefully hidden
in this dev environment since `/audi_dashboard_static` isn't proxied — text still shows, confirming the
fallback works), the tank detail's liter figure renders at the corrected 52px, and the new "Bearbeiten"
form opens fully pre-filled with real values (Kraftstoff "V-Power Racing", 42.13 l, 92.6 €, "Shell,
SÖLDEN", SmartDeal-Ersparnis field present) and "Abbrechen" correctly discards without persisting.
**Chapter 7 is closed.** Next: Chapter 8 (Statistik) at the same depth.
## Chapter 8 (Statistik): one real typography bug found and fixed (2026-08-30)
`Statistik.tsx` was already structurally solid — `StatGrid`/`Fig`/`ProgressBar` map cleanly onto
`quad()`/`.fig`/`.bar`/`.legend`, and the "Art der Fahrten" red-segment bug was already fixed earlier
this session (section Y). One real mismatch found by diffing computed styles against the panel's CSS:
- **`@audi-dash/ui`'s `StatGrid` caption (`.ads-quad__l`) used the old, pre-iOS-decision uppercase-
eyebrow style** (9px, `text-transform:uppercase`, `letter-spacing:.12em`) — the panel's `.quad .l`
is plain small grey text (11px, no letter-spacing, no uppercase). Same root cause as the
already-fixed `.ads-eyebrow` (design-system ships the pre-2026-08-13 Audi-CI convention on
purpose, for its other, brand-free audience; this app's own "match the iOS-look panel exactly"
decision needs a local override) — added right next to `.ads-eyebrow`'s own override in
`audi-schrift.css`, same reasoning, same file.
**Checked and left alone, deliberately:** the panel's `statTile()` header is collapsible
(`aria-expanded`, tap to reveal the body) with a secondary "extra" value shown next to the big figure
in the collapsed state; companion-app's tiles are always fully expanded, so there's no collapsed state
to preview into, and the same numbers are already visible in the `StatGrid` below — not a content gap,
an already-established "friendlier default" idiom difference (same category as this app's non-hiding
list rows elsewhere), left as-is rather than retrofitting an accordion onto five already-substantial
tiles. Also noticed live: "Strecke bis zum letzten Tankstop" showed a negative km value in this test
container's data — traced to `langzeitverbrauch()` in `daten/statistik.ts`, which is a byte-faithful
port of the panel's own `langzeitverbrauch()` (identical unguarded `mitOdo[last].odometer_km -
mitOdo[0].odometer_km` subtraction) — the panel would show the identical negative number against this
same underlying stray-odometer test data (the same class of issue already documented at length in
section AB item 3 for the Tanken form). Not a companion-app-specific defect and not fixed here, since
"fixing" only this side would make the two diverge instead of match.
Verified: `npm run typecheck` clean, `npm test` 147/147, live-verified through the dev proxy against
`audi_ha_test` — the quad captions ("Jahr"/"Monat"/"Woche"/"Tag") now render at 11px, sentence case, no
letter-spacing, matching the panel exactly.
**Chapter 8 is closed.** Next: Chapter 9 (Batterie) at the same depth.
## Chapter 9 (Batterie): the voltage chart used a flat red line/dots instead of the panel's
health-color-coded points (2026-08-30)
`Batterie.tsx`/`BatterieListe.tsx` were already extensively parity-worked earlier this session
(sections R/S/T/X) — the fixed 1015V axis, the dynamic per-pixel viewBox, the min-only dots (no
separate max line), the tap-for-tooltip behavior, the `AGM_RUHE_MAX_V=13.0` classification threshold
and the `LADEKURVE` SOC table were all already byte-matched to the panel. One real mismatch remained,
found by reading `bvZeichnen()`'s actual drawing code line by line: the panel's line and dots are
**not** drawn in red at all — the polyline is `var(--fg)` (neutral), and each dot is colored by
`bvFarbe()`, a red→yellow→green gradient computed from the voltage itself (`≤11.6V` = bad, `11.612.2V`
= bad→warn blend, `12.212.8V` = warn→ok blend, `>12.8V` = ok), reading the actual `--bad`/`--warn`/
`--ok` CSS custom properties and RGB-interpolating between them (`bvHexZuRgb()`/`bvMischen()`/
`bvFarbstufen()`). Companion-app's `Verlaufsdiagramm` instead drew the line **and every dot** in a flat
`var(--red)` — a real violation of this project's own long-standing "red is accent/destructive only,
never a plain data color" rule (the same category as the Statistik/TabBar red-color findings earlier
this session), just never caught because nothing had diffed the actual SVG drawing code against the
panel's before. Ported the identical hex→RGB / linear-mix / threshold logic (`hexZuRgb`/`mischen`/
`farbstufenLesen`/`farbeFuerSpannung` in `Batterie.tsx`), reading the tokens live via
`getComputedStyle(document.documentElement)` (with a hardcoded fallback triple per color, for the
jsdom test environment where the real stylesheet isn't loaded) — the line switched to `var(--fg)`, each
circle now gets its own computed health color instead of the fixed red.
**Checked and left alone:** `BatterieListe.tsx`'s row layout, colors, and the "Generatorspannung" pill
threshold were already correct (verified earlier this session, section AA/S) — re-diffed against
`vBatterieliste()`'s actual markup this round and confirmed still byte-matching (k/v→titel/neben/unter
value-primary idiom, same accepted pattern as Fahrten/Tanken).
Verified: `npm run typecheck` clean, `npm test` 147/147, live-verified through the dev proxy against
`audi_ha_test` — the chart line now renders black/neutral (not red) and the dots render in their
health-mapped color (a yellow/warn-range dot confirmed at a real ~12.212.4V data point in this test
container's history), matching the panel's intent exactly.
**Chapter 9 is closed.** Next: Chapter 10 (Einstellungen/Datensatz) at the same depth.
## Chapter 10 (Einstellungen/Datensatz): four app-wide `@audi-dash/ui` bugs found — the single
biggest-impact chapter of the whole audit (2026-08-30)
Diffed `Einstellungen.tsx` against `vEinst()`. `Einrichtung.tsx` (server-address/token onboarding) has
no panel counterpart at all — a companion-app-only concept, correctly out of scope. Three real content
gaps in `Einstellungen.tsx` itself, all fixed:
1. **No "Backup importieren" — a real, one-directional data-loss risk.** `profilExportieren()` could
download a combined `{profil, fahrten, tankvorgaenge}` JSON backup, but nothing could read one back
in — the panel's `data-backup="import"` handler (→ the `backup_wiederherstellen` service) had no
companion-app counterpart at all. Built `backupImportieren()` (hidden file input, `FileReader`,
`JSON.parse`, validates `profil` is present, calls the same service directly via
`api.rest.dienstAufrufen` matching this file's own existing "Jetzt sichern" call style, then
`neuLaden()`), wired as a third button ("Von diesem Gerät einspielen") next to "Auf dieses Gerät
laden".
2. **No "Version" tile at all** — the panel's `vEinst()` shows the backend's own installed
`APP_VERSION` ("Installiert: ..."). companion-app fetches this exact value already
(`appVersionAngabeLesen()`'s `.app` field, stored as local `serverVersion` state in
`DatenKontext.tsx`) but only ever exposed the *comparison result* (`versionsstand`) through context
— the raw string was fetched and then discarded. Added `serverVersion` to `DatenWert`/the context
value, and a "Version" tile in `Einstellungen.tsx` right before "Integration-Update" (matching the
panel's tile order).
3. **SmartDeal tile was structurally simplified in two ways that lost information/behavior**: no brand
logo next to the label (panel: `<img src="${STATIK}/bilder/${MARKENLOGO_DATEI}">` before "SmartDeal
aktiv"), and "Gültig bis" shown unconditionally instead of only when active (panel: "Ersparnis und
Bildmarke werden ausgeblendet" text when off). New `markenlogoUrl()` in `bilder.ts` (points at the
panel's own bundled `shell-logo.svg`, served under the integration's `/audi_dashboard_static/` path
— a genuinely different static route than `/local/bilder/`'s user-uploaded vehicle photos, confirmed
via `const.py`'s `STATIK_URL` vs `BILDER_ORDNER`), with the same graceful `onError`-hide fallback
used for the Tanken-chapter SmartDeal pill. `Feld`'s `label` prop already accepts a `ReactNode`, so
no new component was needed for the inline icon.
**Then, live-verifying those three fixes surfaced something much bigger: `@audi-dash/ui`'s shared
`Switch`, `Seg`, and `Feld` components were wrong in ways that affect every screen in the app that uses
them — not just Einstellungen.** Found by literally looking at the rendered page rather than only
reading JSX:
4. **Every toggle switch in the entire app was red instead of green.** `Switch.css`'s checked state
used `background: var(--red)` — a real "red as a non-destructive state color" violation, the same
category already fixed for the TabBar active pill, the Statistik Arbeitsweg bar segment, and the
back arrow/primary buttons earlier this project, just never caught here because nothing had diffed
`@audi-dash/ui`'s own component CSS against the panel's `.switch input:checked+.track` rule before.
Fixed to `var(--ok)`, matching `audi-dashboard-ios.css` exactly. This one rule affects literally
every switch in the app: "Beschriftung der Tab-Leiste", "Termin vereinbart" (Reifen, built this
session), SmartDeal, etc.
5. **`Seg` (the Nacht/Tag segmented control, used only here so far) drew the active segment with a red
outline ring and rendered all labels uppercase with heavy letter-spacing** — again the pre-iOS-
decision neutral style, same root cause as `.ads-eyebrow`/`.ads-quad__l` earlier this session, just
never carried over to `Seg`. Panel's actual `.seg`/`.seg button`/`.seg button.on` (always the
iOS-overlay values, confirmed live-measuring the token colors) is a filled track with a moving
pill (`background:var(--tile-2)`, `box-shadow`), no red anywhere, sentence case, no letter-spacing.
Rebuilt `Seg.css` to match exactly, and added a new `--ios-fill` token to `tokens.css` (both
themes, live-measured `rgba(120,120,128,.24)` dark / `.16` light) since the track background needed
it and `Feld`'s input boxes turned out to need the identical token (see next point) — a real,
reusable value, not a one-off hardcode.
6. **`Feld` (used by nearly every edit form in the app) had four separate mismatches, only found by
testing a genuinely long label live** ("Pause zwischen zwei Fahrten" collapsed to a stack of
single-word lines squeezing the input to nothing — a real, user-visible layout bug, not just a
color drift):
- `.ads-feld label` was 13px/`var(--fg2)` (the same pre-iOS-decision eyebrow style, again) instead
of the panel's 16px/`var(--fg)` — already fixed once for the read-only `Wertzeile` component in
the MeinAudi chapter, but never carried over to the editable-field component, despite `Feld` being
used far more often.
- The label also lacked `flex: 1 1 auto; min-width: 0` (present in the panel's *base* stylesheet,
not overridden by the iOS overlay, so still in force) — without it, the label competed for space
with the input instead of taking the row's remaining width and wrapping there.
- `.ads-feld input,select` had a hardcoded `min-width: 132px` floor with no matching `flex`/
`max-width` — the panel's own base rule is `min-width:0; max-width:100%; flex:0 1 auto`, letting
the input shrink to make room for the label. The 132px floor was exactly what starved "Pause
zwischen zwei Fahrten" down to 0px width.
- `.ads-mit-einheit input` (the compact number-plus-unit pattern — km/Nm/Minuten fields) had no
explicit `width`, only `min-width: 92px` — the panel's `.mitEinheit input` sets `width: 96px;
min-width: 0` explicitly; without a real width, the browser's unconstrained default input size
(~170190px) dominated, which is what actually produced the "Pause" field's collapse (measured
live: the input+unit group alone consumed 274 of the row's 279px before the fix).
- Also renamed the "Pause zwischen zwei Fahrten"/"Fahrten" label and tile eyebrow to the panel's
actual wording ("Pause bis"/"Fahrten zusammenfassen") while fixing this, since the original
companion-app phrasing was what triggered the multi-line wrap in the first place and the panel's
shorter wording is the actual target text anyway.
**Checked and left alone, deliberately:** the Setup-menu sensor-mapping UI (`vEinst()`'s "Setup —
Sensoren zuordnen" and the whole entity-catalog popup machinery) is an HA-panel-only concept with no
companion-app equivalent needed, per the parity rule's own established carve-out for this exact screen
(sections R/AF); "Nacht beginnt"/"Nacht endet" being editable in companion-app but not in the panel is
a companion-app-only *addition* (the `nacht_von`/`nacht_bis` profile fields exist and are read by both
apps' Statistik screens, but the panel's `vEinst()` never exposed an edit UI for them at all) — additive,
not a gap to remove.
Verified: `npm run typecheck` clean, `npm test` 147/147 at every step, `design-system`'s `npm run build`
rerun after each CSS change, live-verified through the dev proxy against `audi_ha_test` via computed-
style JS queries and screenshots (the Browser pane went in and out of a "hidden" state mid-chapter,
worth remembering: `get_page_text`/`javascript_tool` computed-style queries keep working even when
`computer` screenshots time out) — confirmed the Switch track computes to the real `--ok` RGB, the Seg
active pill's background/box-shadow/color match the panel's token values exactly with no red, the Feld
label renders at 16px/black, the Feld input box uses the real `--ios-fill` RGBA, and — after the two
follow-up width fixes — "Pause bis" renders on a single line with a compact 96px-wide "15" box, matching
`audi-dashboard-ios.css`'s actual rendered layout byte-for-byte instead of just close-enough.
**Chapter 10 is closed.** Next: Chapter 11 (Abschluss) — build, version bump, `npm run ota`, final
report.
## Chapter 11 (Abschluss): build, version bump, OTA rebuild, deployment, and the audit's own summary
(2026-08-30)
**Build/version/OTA:** `npm run build` (production `vite build`) succeeded clean. Manifest bumped
`2026.8.30.1``2026.8.30.2` (companion-app-only round, no panel-side source changed this whole
11-chapter audit). `npm run ota` rebuilt the bundle from the new manifest version automatically:
`bundle.zip` 248,241 bytes, sha256 `88922cb0e3a7603b6e4e2fe7daee85513ec9e16c4ae7b2e0229f514bde58915f`.
**Deployment hit a real, worth-remembering `docker cp` gotcha, distinct from the already-documented
Git-Bash path-mangling one.** After correcting the path issue (`MSYS_NO_PATHCONV=1`, per section AB's
existing note), the manifest inside the container still read the old version. Root cause:
`docker cp local_dir container:/existing/dir` does **not** overwrite the contents of an
already-existing destination directory — it copies `local_dir` *into* it as a nested subdirectory
(`/config/custom_components/audi_dashboard/audi_dashboard/...`), leaving the real top-level
`manifest.json` untouched. Confirmed by listing the container's folder and finding the stray nested
copy. Fixed by removing the nested duplicate and re-copying with a trailing `/.` on the source
(`docker cp local_dir/. container:/dest_dir`, the standard "copy contents, not the folder itself"
idiom) — this is the general rule going forward for this project: **`docker cp` into an existing
integration folder always needs the trailing `/.` on the source**, not just the `MSYS_NO_PATHCONV=1`
env var. Also cleared `__pycache__/*.pyc` before the final restart as a defensive measure (not
strictly proven necessary this round, but cheap insurance against stale bytecode). Verified via
`docker logs --tail 15`: single `audi_dashboard` loader warning, `Audi Dashboard 2026.8.30.2
eingerichtet`, no traceback.
### Full recap of this audit (chapters 111, one continuous pass, 2026-08-30)
Triggered by the owner's report that a build compiled by Xcode still looked stale/inconsistent
against the panel, and the instruction to check *everything* — font, size, weight, line-height,
letter-spacing, color, icon, and structure — not just spot-check. Went screen-group by screen-group:
1. **Übersicht** — wrong color-token source (base stylesheet instead of the always-loaded iOS
overlay, the single biggest individual color fix), `.ads-title`/`.ads-fig` letter-spacing/
line-height, the "Mein Audi" tab icon placeholder, badge-zusatz text styling, Reichweite tile
layout order, teaser-tile hero-figure sizing, alignment, `.dm-servicefig` spacing, the tab bar's
red active-indicator (should be a neutral pill), the Standort feature built from scratch (it
didn't exist in companion-app at all), a `.dm-szene` grey background that shouldn't be there, an
oversized vehicle image aspect ratio, and a Standort-fullscreen height-cap bug.
2. **Mein Audi** — a full structural rebuild (was a static photo grid + placeholder-text tiles;
became a cycling image + five real-data preview tiles), plus the systemic `Wertzeile` dt/dd
color reversal (affects every "field: value" row app-wide).
3. **Service/Reifen** — Service.tsx mostly already correct; Reifen.tsx needed a full structural
rebuild (both tire sets shown at once instead of a Seg toggle, separate Anzugsmoment/Wechseltermin
tiles, previously read-only Hersteller/Modell/Größe/DOT/Solldruck/Kommentar fields made editable
to match the panel's live-save fields) — this was the chapter re-opened mid-audit by a direct
owner complaint ("reifen sieht völlig unterschiedlich aus").
4. **Versicherung/Steuer** — a real `[object Object]` rendering bug in `Schutzbrief()` from three
mis-typed fields, rebuilt against the real nested data shapes.
5. **Sicherheit** — missing Draufsicht image, wrong top-status indicator component, an extra
timestamp line not in the panel, and a `TuerenKlappen.tsx` detail list using the wrong dot
component/row structure.
6. **Fahrten** — trip list rows missing the route text entirely, detail hero figure at the wrong
size, map start/end markers visually indistinguishable (both red instead of black-vs-red).
7. **Tanken** — a real broken-string plural bug ("Tankvorgang" + a stray combining diaeresis),
missing accordion secondary lines, a completely unimplemented SmartDeal feature (logo, Ersparnis
field, Kraftstoff selector), and `TankDetail.tsx` having no editing capability at all.
8. **Statistik** — one real typography bug (`StatGrid` caption using the pre-iOS-decision uppercase
eyebrow style).
9. **Batterie** — the voltage chart's line and dots were flat red instead of the panel's
neutral-line/health-color-coded-dots scheme (a real, if quieter, instance of the same "red as a
non-destructive data color" class of bug found repeatedly this session).
10. **Einstellungen/Datensatz** — three real content gaps (no backup-file import, no installed-version
display, SmartDeal tile missing its logo/conditional field) that, while verifying them live,
surfaced **four separate app-wide bugs in `@audi-dash/ui` itself** (`Switch` red-instead-of-green,
`Seg` red-ring-instead-of-filled-pill, and `Feld`'s label color/size and its input width/flex
handling) — the single highest-leverage fixes of the whole audit, since `Switch` and `Feld` are
used by nearly every edit screen in the app.
**The recurring lesson across all eleven chapters, worth stating plainly for the next session:**
almost every real bug fell into one of three buckets — (a) reading only the *base* panel stylesheet
instead of the always-loaded iOS overlay that overrides most of it, (b) `@audi-dash/ui` still
shipping its pre-2026-08-13 neutral/uppercase/red-accented styling in components nobody had re-diffed
against the panel's actual iOS-decision values, and (c) genuine structural/content gaps where a
companion-app screen was built once, early, and never revisited as the panel kept gaining features.
None of these were caught by `tsc`/`vitest` — every one needed an actual live render, often down to
`getComputedStyle` queries, to surface.
**Known, deliberately-not-fixed items, carried forward for a future session (flagged, not silently
dropped):**
- `.dm-fussnote`'s cross-cutting 35-site typography ambiguity (Service.tsx footnote size/color vs.
its many other uses) — flagged in Chapter 3, never resolved, still open.
- `Versicherung.tsx`'s top-level `vVers()` structure (separate Notruf/Jahresbeitrag-bar/Vertrag
tiles with their own edit gears) was never rebuilt 1:1 — companion-app kept its single-summary-tile
+ `NaviKachel`-grid structure. `Beitrag()`/`Vertrag()`/`Notruf()`/`Steuer()` sub-screens were never
individually re-verified against their panel counterparts this audit.
- `LiveFahrt.tsx` has no panel counterpart to diff against at all (a companion-app-only screen,
explicitly built-but-disabled pending real FMM003 live data) — untouched, correctly out of scope.
- The unreproduced satellite/map-layer-icon complaint from early in this session (checked three
times via markup, function, and computed paint values — never found a defect) remains open pending
a real device screenshot from the owner.
- A `Feld`-label reachability report ("Batteriespannung row not clickable on desktop") from earlier
this project could not be reproduced via any hit-testing method tried — flagged, not resolved.
Verified end to end: `npm run typecheck` clean and `npm test` 147/147 after every single change this
entire audit (never left red), `npm run build` clean, `design-system`'s own `npm run build` rerun
after every CSS-only fix, live-verified through the dev proxy against `audi_ha_test` at real mobile
viewport width for every chapter, manifest bumped to `2026.8.30.2`, OTA bundle rebuilt and matching,
integration redeployed and confirmed via a clean single-warning setup log.
## Chapter 2 (Mein Audi): a structural rebuild, not a styling pass — plus the second-biggest systemic typography find of the audit (2026-08-30)
Owner: "mache mit 2 bis 11 weiter" (continue with chapters 211).
**`MeinAudi.tsx` was not a styling-drift case — it was a fundamentally different, much poorer page than `vAudi()`.** The panel's version: a single cyclable vehicle photo (`carImg("galerie")`, tap-to-advance, dot indicator), a flat badge/Kennzeichen row, then five tiles each showing a **real, multi-row data preview** (Fahrgestellnummer/Details/Erstzulassung/Ausführung/Ausstattung; Kilometerstand/Tankfüllung/Batteriespannung; the actual next-service dates; Versicherung/Steuer/Summe amounts; Reifen montiert/Laufleistung/Wechseltermin) — every tile a button leading to its detail screen, chevron included. companion-app's version: a static grid of *all seven* photo slots (a concept that belongs to Einstellungen → Bild der Übersicht, not here) plus five generic `NaviKachel` "go" tiles, four of which showed **hardcoded placeholder sentences** ("Termine und Wartungsplan", "Beitrag, Vertrag, Kfz-Steuer") instead of any real value. Rebuilt from scratch to match the panel's structure: cycling image + dots (new local state, `BILDPLAETZE` index), flat badge/Kennzeichen tile (same pattern just fixed on Übersicht), and five `Tile variant="button" chevron` tiles each with a real `Werteliste`/`Wertzeile` data preview sourced from the same fields the panel reads (`fahrzeug.fin/details/erstzulassung`, `ausstattung` position count via the same `AusstattungGruppe`/`sonder.length` cast already used in `Fahrzeugdaten.tsx`, `fahrzeug.versicherung["beitrag"]`/`steuer["betrag"/"zeitraum"]`, `fahrzeug.reifen.wechsel`).
**One deliberate, documented scope reduction:** the panel's "Service" tile embeds its *entire* three-term list (Ölwechsel/Inspektion/Hauptuntersuchung, with all the meldung-vs-Servicebuch fallback logic that lives in `Service.tsx`). Reproducing that exact logic a second time here would have meant duplicating a large, delicate block of conditional logic. Used the existing `naechsterService()` instead (the same single-nearest-term function that already feeds the Übersicht tile) — a real, acknowledged simplification, not a silent omission; flagged here and in the component's own header comment. Worth extracting `Service.tsx`'s three-term computation into a shared, reusable function later if the owner wants the full breakdown here too.
**Systemic find, while wiring the new tiles' data rows: `Wertzeile`/`.dm-wertzeile` (`dt`/`dd`, used by every "field: value" row across the *entire* app — `Fahrzeugdaten.tsx`, `Reifen.tsx`, `Versicherung.tsx`, `TankDetail.tsx`, `FahrtDetail.tsx`, now `MeinAudi.tsx`, and more) had its typography backwards.** Live-measured the panel's real `.row dt`/`.row dd` (careful this time to check the iOS-overlay file, not just the base stylesheet, after getting burned by that exact gap twice already this session): **both 16px/weight 400**`dt` (the field label) in `var(--fg)`, full-strength black/white; `dd` (the actual value) in `var(--fg2)`, the secondary tone. companion-app had it backwards and smaller: `dt` 13px/300/`var(--fg2)`, `dd` 14px/300/unset(≈`fg`) — label small and muted, value slightly bigger and full-strength, the opposite hierarchy from the panel's own convention. Fixed centrally in `.dm-wertzeile dt`/`dd` (screens.css) — one fix, reaches every screen that uses `Wertzeile`, not just this one. Also corrected the `<small>` zusatz caption size (`.dm-wertzeile small`/`.dm-navikachel__wert small`): `11.5px``12.5px` (live-measured `.row dd small`), and `.dm-wertzeile`'s own row padding `10px``11px` (matching `.row{padding:11px 0}`).
New CSS added for the rebuilt structure: `.dm-galerie-wechsel`/`.dm-galerie-punkte` (the cycling image + dots, mirroring `.dots` 1:1), `.dm-wertzeile--knopf`/`.dm-wertzeile__chevron` (the one clickable row — Batteriespannung → Battery screen — matching the panel's `.row[data-go]` pattern), and a scoped `.dm-galerie-wechsel .ads-bildbox{aspect-ratio:16/9}` (the panel's plain `"carfix"` class, not the `"carfix klein"` 16:8 used only on Übersicht's fixed image — confirmed by reading `carImg("galerie")`'s own call site, not assumed to match Übersicht's image).
Verified: `npm run typecheck` clean, `npm test` 147/147 (existing smoke test renders the new structure without throwing). Live-verified via screenshot at true mobile width: cycling image with 7 dots (first active/red), badge+Kennzeichen flush left matching the now-fixed Übersicht pattern, all five tiles showing real data with correctly black-label/grey-value rows and visible chevrons, Batteriespannung row showing a real live value (13,6 V) with its own chevron.
## Chapter 3 (Service & Reifen): mostly solid already, two real missing features found (2026-08-30)
Owner picked option 1 ("gleiches Tempo/Tiefe beibehalten") after a mid-audit status check — continuing at full depth across sessions rather than switching to a lighter pass.
**`Service.tsx`'s "Anstehende Termine" tile needed almost no work** — 4 of 5 live-measured elements (Herstellervorgabe, Prognose-Fußzeile, Leerzustand-Text, Eyebrow) already matched the panel exactly, entirely as a side effect of this session's earlier systemic fixes (color tokens, `.tg-wert`'s Wide font). Only `.dm-fussnote` (the explanatory paragraph) differs (panel: 13.5px/`var(--fg2)`; companion: 11.5px/`var(--fg3)`) — but that class is used **35 times across 13 files** for many different kinds of captions, not a 1:1 match to any single panel class. Left alone rather than risking 34 other call sites on one owner report; flagged for a future targeted look, not a blind global change.
**`Reifen.tsx`'s overall structure (a `Seg` toggle between Sommer/Winter instead of the panel's two-tiles-at-once layout) is a legitimate idiom difference, not a gap** — same information reachable, matches this project's own "different idiom, same content" allowance. But checking it surfaced **two genuinely missing features, not styling**: the panel lets the owner correct the **Anzugsmoment** (torque spec) and the **active tire set's own odometer** via a small pencil-icon edit, both wired to real backend writes (`profilSpeichern()` for Anzugsmoment; the dedicated `reifen_km_setzen` service for km, specifically *not* through `profilSpeichern()` — see `reifenzaehler.py`'s own reasoning, already documented elsewhere in this file, about not clobbering `referenz_odo_km`). Neither existed anywhere in companion-app — confirmed by grepping for `reifen_km_setzen`/`reifenKmSetzen` across `api/` and `screens/` (zero hits) and for `anzugsmoment` outside `Reifen.tsx` itself (only the one, read-only usage).
**Built, following this codebase's own established edit-toggle idiom** (matching `ArchivZeile`'s own pattern in the same file, not the panel's inline-always-editable fields):
- `api.reifenKmSetzen(satz, km)` (new, `api/index.ts`) — same `warteschlange.einreihen(DIENST_DOMAIN, "reifen_km_setzen", {satz, km})` shape as the existing `reifenArchivieren`.
- `SymbolEdit` (new, `symbole.tsx`) — the panel's `edit-s` CI icon, verbatim, following the same `RahmenFlaeche` pattern as `SymbolOelwechsel`/`SymbolInspektion` (a filled icon, not a stroke outline).
- Two pencil-icon-triggered inline edit panels in `Reifen.tsx`: Anzugsmoment (writes via `profilSpeichern`) and the currently-displayed tire set's km (writes via the new `api.reifenKmSetzen`, respecting the `Seg` toggle's `anzeige` state — correctly targets whichever set, sommer or winter, is on screen). New shared `.dm-bearbeiten-block` CSS for the expandable edit-panel container (border-top + spacing, matching the panel's own inline-edit-block treatment).
Verified: `npm run typecheck` clean, `npm test` 147/147. Live-tested both edit flows end-to-end through the dev proxy against `audi_ha_test` (not just rendered — actually opened each panel, confirmed the input pre-fills with the real current value: `120` Nm, `187992` km) — cancelled both without saving, so no real data was changed in the test container.
## Chapter 4 (Versicherung & Steuer): a real `[object Object]` rendering bug in `Schutzbrief()` (2026-08-30)
**`Schutzbrief()` in `Versicherung.tsx` mis-typed three of its five data lists as plain `string[]`, when the actual profile data is nested objects/tuples — the panel's own `sb.entweder`/`sb.gruppen`/`sb.nicht` were never string arrays.** Concretely: `entweder` is `{bez, grenze, zusatz}[]`, `gruppen` is `{titel, posten: [bez,wert,zusatz][]}[]`, `nicht` is `[bez, begründung][]`. The old code cast all three to `string[]` and rendered `<li>{eintrag}</li>` directly — for `entweder`/`gruppen` this would have printed literal `[object Object]` for every entry (confirmed this really was the bug, not a hypothetical: reproduced the *old* code's behavior mentally against the real profile data structure, which is an object array, not strings), and for `nicht` (an array of 2-tuples) would have shown something like the array's default `toString()` join rather than readable text. Found by reading the panel's actual `vSchutz()` markup line by line instead of assuming the existing companion-app types were correct — the same lesson as several earlier findings this audit: check the real data shape, don't trust an existing cast.
Rebuilt to match each field's real shape and the panel's own grouping: `entweder` → its own tile with `Wertzeile`s (bez/grenze/zusatz) plus the fixed "Mobilität wird binnen 60 Minuten organisiert" note (the panel shows this unconditionally whenever `entweder` has content, not behind some other flag — the previous code's structure had already lost this line entirely); `ausloeser` → its own tile with `gilt`/`versichert` as trailing footnotes (matching the panel's placement exactly, previously merged into a single made-up "summary" tile that doesn't exist in the panel); `gruppen`**one tile per group**, each its own `Wertzeile` list (previously flattened into a single wrong list); `nicht``Wertzeile`s with the reason as the `zusatz` caption; `pflichten` → its own tile with `rang`/`stand` as trailing footnotes (again matching the panel's actual placement, not a separate summary tile).
Verified: `npm run typecheck` clean, `npm test` 147/147. Live-verified through the dev proxy against `audi_ha_test`'s real profile data — confirmed **zero** `[object Object]` anywhere on the rendered page (checked via `document.body.innerText.includes('[object Object]')`, not just eyeballing) and real, correctly formatted insurance terms showing throughout ("Weiterkommen: Bahn 1. Klasse bis 1 200 km…", "Am Schadenort: Pannenhilfe vor Ort bis 100 €…").
**Not yet done in this chapter** (context budget — flagged, not silently skipped): `vVers()`'s top-level structure (separate Notruf/Jahresbeitrag-with-breakdown-bar/Vertrag tiles with edit gears) vs. companion's single combined summary tile + `NaviKachel` grid was not rebuilt to match 1:1, unlike the `MeinAudi.tsx` rebuild in Chapter 2 — `Beitrag()`/`Vertrag()`/`Notruf()`/`Steuer()` sub-screens were not individually re-verified against their panel counterparts (`vBeitrag()`/`vVertragBearbeiten()`/`vNotrufBearbeiten()`/`vSteuer()`) this round. Worth a dedicated pass later.
---
## Working conventions (observed — keep them)