7276d06a18
- Türschlösser auf ein Sensor reduziert (Fahrertür genügt, Zentralverriegelung schließt alle Türen gemeinsam) - Kofferraum-/Motorhauben-Schloss entfernt. - "Sicherheit" heißt im Panel jetzt "Fahrzeugstatus", wie in der companion-app. - naechsterTermin() (Panel): sortierte null als Epoch 1970 und ließ dadurch einen datenlosen Termin (Hauptuntersuchung ohne Eintrag/Erstzulassung) immer vor einem echten, berechneten Termin (Ölwechsel) gewinnen - Ursache dafür, dass Übersicht nach dem Leeren des Servicebuchs nichts mehr zeigte. - "Licht ausgeschaltet" statt "Kein Licht" (Formulierung zurückgenommen). - Große Bildschirme: Unterseiten (Standort, Service, ...) zeigten ihren Titel in 26px statt der sonst überall genutzten 17px - wirkte neben dem durchgängig leichten Fließtext wie Fettschrift, obwohl font-weight nirgends wechselt. Jetzt dieselbe Standardgröße wie im Telefon-Layout. Details und Verifikation in AGENTS.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2704 lines
226 KiB
Markdown
2704 lines
226 KiB
Markdown
# 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
|
||
`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`](https://github.com/multica-ai/andrej-karpathy-skills),
|
||
behavioral rules derived from Andrej Karpathy's observations on LLM coding failures. They bias
|
||
toward caution over speed — for trivial tasks, use judgment.
|
||
|
||
1. **Think before coding.** Don't assume. Don't hide confusion. Surface tradeoffs. State
|
||
assumptions explicitly; if multiple interpretations exist, present them — don't pick silently.
|
||
If a simpler approach exists, say so. Push back when warranted. If something is unclear, stop
|
||
and ask.
|
||
2. **Simplicity first.** Minimum code that solves the problem. Nothing speculative: no features
|
||
beyond what was asked, no abstractions for single-use code, no unrequested configurability, no
|
||
error handling for impossible scenarios. If 200 lines could be 50, rewrite.
|
||
3. **Surgical changes.** Touch only what you must. Don't "improve" adjacent code, comments, or
|
||
formatting; don't refactor what isn't broken; match existing style. Remove only orphans **your**
|
||
change created; mention (don't delete) pre-existing dead code. Every changed line should trace
|
||
directly to the request.
|
||
4. **Goal-driven execution.** Define success criteria, loop until verified. "Fix the bug" → write a
|
||
test that reproduces it, then make it pass. For multi-step tasks, state a brief plan with a
|
||
verify step per item.
|
||
|
||
### Claude Code practices (current guidance, 2026)
|
||
|
||
- Keep this file **concise and specific** — instructions concrete enough to verify ("run X before
|
||
Y"), markdown headers/bullets, no dense prose. Context files reduce adherence as they grow;
|
||
target roughly ≤200 lines of actual instruction.
|
||
- Don't record here what the code or git history already shows (directory dumps, dependency
|
||
lists) — record pitfalls, rationale, decisions, and conventions that differ from defaults.
|
||
- Conflicting instructions get picked arbitrarily — when updating, **remove** superseded rules
|
||
rather than stacking corrections.
|
||
- Verify loading with `/context` (file must appear under **Memory files**); edit via `/memory`.
|
||
- **This file (plus the session's task list) is the source of truth for what's done — not a leftover
|
||
plan-mode file.** A `~/.claude/plans/*.md` file surfaced by the system as "not yet complete" can be
|
||
stale: plan files aren't marked complete when their work finishes. Before claiming a feature is
|
||
unbuilt (e.g. "the Setup menu isn't implemented yet"), check the `[x]`/`[ ]` items below and/or the
|
||
task list — trust those over an old plan file's own claim about its status. (Caused a real mistake
|
||
2026-08-12: told the user the Setup menu wasn't built yet, when it had shipped hours earlier in
|
||
the same project — see the "Setup menu" `[x]` entry further down.)
|
||
|
||
---
|
||
|
||
## What this project is
|
||
|
||
Private replacement for the Audi *connect plug & play* app (discontinued end of 2026), for exactly
|
||
one vehicle (**Audi RS 4 Avant competition**), single user, self-hosted on Home Assistant, access
|
||
via Tailscale only. Read-only toward the vehicle — no remote control. Covers: vehicle status, trip
|
||
log, fuel log with Shell receipt parsing, service forecasting, insurance/tax, tire management.
|
||
|
||
**License rule (hard):** Audi Type fonts, four-rings SVG, and model-badge SVGs are cleared **only
|
||
for this one private, unpublished installation**. Therefore `design-system/` is brand-free (it gets
|
||
uploaded to Claude Design), while the HA panel and the future sideload-only app may use the real
|
||
assets. Never mix these the other way around.
|
||
|
||
**Reading order for a new session:**
|
||
1. This file (overview + open items)
|
||
2. `UMSETZUNGSPLAN.md` — the step-by-step execution plan for all open items (13 phases with
|
||
commands and acceptance criteria); when working on an open item, follow the plan's phase
|
||
3. `SPECIFICATION.md` — authoritative for the HA panel, incl. §7 "Known Gaps"
|
||
4. `COMPANION_APP_ARCHITECTURE.md` — authoritative for DataMetric360 (decided, phases 1–10 built)
|
||
5. `AUDIT_2026-08-10.md` — accessibility/platform audit of the panel (partly done, rest below)
|
||
6. `bauauftrag.md` — **historical only**; code has diverged (see SPECIFICATION.md §7)
|
||
|
||
## The three projects in this repo
|
||
|
||
| Area | What | Status |
|
||
|---|---|---|
|
||
| `custom_components/audi_dashboard/` | The HA integration: backend, panel, OTA app bundle. HACS is not used and cannot be — private repo, categorical refusal on HACS's side, no GitHub mirror any more; `install.ps1` is the permanent, only install path (section H) | ✅ **finished, in use** — replaced the pyscript backend on 2026-08-23 (section H) |
|
||
| `homeassistant/` | Install package, recorder snippet, guides — **no code any more** | ✅ |
|
||
| `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.
|
||
|
||
**`custom_components/audi_dashboard/` in one paragraph:** a normal HA integration with a config
|
||
flow, one `sensor` platform and 18 services. `koordinator.py` owns the runtime state and the
|
||
wiring; the domain modules beside it carry the logic: trip detection via the FMM003 ignition
|
||
sensor with pause tolerance (`fahrterkennung.py`), two-stage trip completion via recorder history
|
||
(`screening.py` — the odometer often updates only on the next trip), fill-up detection on
|
||
fuel-level rise (`tankerkennung.py`), Shell PDF parser (`shell_beleg_parser.py`, in-process via
|
||
`pypdf`, the only tested part of the repo), tire km counter, backup, image management, retroactive
|
||
history import. `ablage.py` is the only place that touches files. Frontend: `frontend/audi-dashboard-app.js`
|
||
(~4,900 lines, custom element, no framework/bundler), 5 tabs + ~19 detail routes, served by the
|
||
integration itself under `/audi_dashboard_static/` with the manifest version as the cache breaker.
|
||
Entity IDs are assigned **in the UI** (Settings → Fahrzeug einrichten → Setup), stored as overrides
|
||
in `/config/audi_dashboard/entitaeten.json`; `einstellungen.py` holds only the built-in defaults
|
||
(all empty). Deploy: HACS, or `homeassistant/installationspaket/install.ps1` where HACS can't reach
|
||
the repo.
|
||
|
||
**`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, OTA update flow — see
|
||
section H), all 21 screens (`src/screens/`), Audi assets (`src/assets/audi/`), PWA manifest and
|
||
icons. Verified by 117 unit and render tests (`npm test`) plus the two smoke suites against a live
|
||
Home Assistant (`npm run smoke`, `npm run smoke:auth`). `npm run dev` in the repo root starts it;
|
||
`testumgebung/aufsetzen.sh` provides the server side.
|
||
|
||
**DataMetric360 architecture (short — details in `COMPANION_APP_ARCHITECTURE.md`):**
|
||
- Future data source: **Teltonika FMM003** on the CAN bus, fully replacing the iPhone WLAN sensor
|
||
and the VAG integration.
|
||
- **Current data path (since 2026-08-12): FMM003 → flespi (native Codec8/TCP, IMEI auth) → HA.**
|
||
Both earlier directions are dropped: Traccar first, then the self-hosted MQTT/TLS broker — the
|
||
router's port-forward turned out to be unusable. The decision logs deliberately remain in the
|
||
architecture doc, marked ÜBERHOLT. Details and open steps in section B below.
|
||
- Frontend access: HA REST + WebSocket with a long-lived access token (no `hass` object).
|
||
- External access: Cloudflare Tunnel + reverse proxy with a path allowlist; HA itself stays
|
||
unreachable. Domain **`datametric360.app`** registered (all-inkl, 2026-08-11).
|
||
- Distribution: **sideload only** (license reason) — hence real Audi assets are allowed there.
|
||
- Trip detection moves to FMM003 ignition; the pause-tolerance feature
|
||
(`fahrten_pausenzeit_min`) is a deliberate product decision and must be preserved.
|
||
|
||
---
|
||
|
||
## Review findings (2026-08-11)
|
||
|
||
### Design/CI review of `main` (2026-08-13) — see `DESIGN_REVIEW_2026-08-13.md`
|
||
|
||
Code review of all UI layers plus headless-browser screenshots of the panel (mock `hass`),
|
||
checked against the repo's own Audi-CI rules. Headlines: the iOS overlay
|
||
(`audi-dashboard-ios.css`) overrides the documented palette/radii, adds shadows/blur and turns
|
||
red into a fill color **without any recorded decision** (visible e.g. as a red-filled "Abbrechen"
|
||
next to a black "Speichern" in the setup popup); `design-system/` still carries pre-audit state
|
||
(old low-contrast `--fg3 #657081`, no focus styles at all, 13.5px inputs, 34px icon buttons);
|
||
the `design/` export's RS-6 error extends into the technical sample data (73 l tank, 285/30 R22,
|
||
FIN series 4G). Four browser-verified rendering defects: switches stretched by
|
||
`.feld label{flex:1 1 auto}` (state display becomes ambiguous), double "offen" in the trip list,
|
||
empty oil-change tile when the service book is empty, and fragmented Leaflet rendering in all
|
||
three maps (✅ **fixed 2026-08-16** — root cause was Leaflet's stylesheet being loaded into
|
||
`document.head`, which never reaches the panel's shadow root; see the third-round entry in
|
||
"Open items"). Positives: the 300/400 font-weight rule and de-DE
|
||
formatting hold everywhere; `design-system/` is verifiably brand-free.
|
||
|
||
### HA panel — known gaps (most also in SPECIFICATION.md §7)
|
||
|
||
- **GPS is dead schema for trip records specifically:** `start_lat/lon`, addresses, `route`,
|
||
`avg_speed_kmh` on individual trips never populated. The trip-detail map draws a **fabricated**
|
||
line via `fakeTrack()` (`audi-dashboard-app.js:414`) — not a real track. (Unrelated: the
|
||
Übersicht's live Standort-Kachel is no longer in this state — `STANDORT_TRACKER` now points at
|
||
a real `device_tracker`, see section B/C. This item is only about per-trip route data, which
|
||
nothing currently populates.)
|
||
- **No GPS fallback in trip completion:** trips without an odometer match stay `status="offen"`
|
||
forever (`modules/fahrtabschluss_logik.py:16-20`).
|
||
- **RAM-only state:** running trip (`_fahrt_start_ts`) and fuel low-water-mark
|
||
(`_tiefststand_pct`) do not survive an HA restart. Deliberately deferred hardening.
|
||
- **Inactive features:** `UPDATE_REPO_URL = ""` (self-update inactive) in
|
||
`pyscript/modules/einstellungen.py`. `www/bilder/` has no vehicle photos (all slots show
|
||
placeholders); `steuer.faellig` unset. (`BATTERIE_SENSOR` was the same kind of gap until
|
||
2026-08-12 — now wired to the FMM003's `external_power_voltage`, see section B/C below. Several
|
||
*other* fields are newly inactive since the same date for a different reason — the
|
||
`TommiG1/HA_VAG-EU-Data-Act` integration they depended on was dropped: `KM_SENSOR`,
|
||
`TANK_SENSOR`, `RANGE_SENSOR`, `REFRESH_BUTTON`, all door/window/lock sensors, oil-change/
|
||
inspection sensors. All are configurable again via the new Setup menu, see section C.)
|
||
- **Robustness:** `profil_lesen()` in `modules/profil.py` does not handle a missing/corrupt
|
||
`fahrzeugprofil.json` — all callers throw.
|
||
- **Tests:** only the receipt parser is tested (`data/tests/test_shell_beleg_parser.py`, 10 real
|
||
receipts) — and its test PDFs are gitignored, so a fresh clone can't run it. Backend and
|
||
frontend: no tests, no CI.
|
||
- **Leaflet via CDN:** trip map needs public internet in addition to the Tailscale tunnel.
|
||
|
||
### Documentation drift ✅ FIXED 2026-08-11 (kept as a record of what was wrong)
|
||
|
||
- `homeassistant/README.md:99`, `INSTALL.md:204`, and the header comment
|
||
`audi-dashboard-app.js:13-15` claim the statistics view shows sample numbers — **false**;
|
||
`vStat()` computes real values from `TRIPS`/`FILLS`.
|
||
- `homeassistant/README.md:110` mentions the removed 97% full-tank rule (removal documented in
|
||
`belegverarbeitung.py:18-20`); the README file list omits 5 pyscript files.
|
||
- Obsolete comment `belegverarbeitung.py:41` ("TODO: Datei ablegen" — file has long existed).
|
||
|
||
### Audit leftovers — two of three now fixed in the new app
|
||
|
||
- ✅ Swipe-to-delete without a gesture-free fallback — **fixed in the app**: every list row also
|
||
carries an always-visible "…" menu (`companion-app/src/screens/Zeilenmenue.tsx`). Still open in
|
||
the old panel, which is being replaced anyway.
|
||
- ✅ Leaflet from a CDN — **fixed in the app**: bundled from node_modules as a lazy chunk.
|
||
- 🟡 Popup close-by-tap-outside in the old panel: still unchecked (the app uses the library's
|
||
Popup, which is keyboard-reachable).
|
||
- Audit's own note: these three may be better done in DataMetric360 than retrofitted — the panel
|
||
gets replaced anyway.
|
||
|
||
### companion-app / design-system ✅ RESOLVED 2026-08-11
|
||
|
||
- **Not wired together** — fixed: an npm workspace in the repo root links `@audi-dash/ui` into the
|
||
app as a real dependency.
|
||
- Both packages: no `node_modules`, no `dist` — smoke tests need `npm install` first
|
||
(design-system additionally `npm run build`; `scripts/smoke.mjs` imports from `../dist/`).
|
||
- No unit tests, no Storybook (substitute: SSR smoke over 21 cases in design-system).
|
||
- `design/` export (2026-08-11) has two known, already-commissioned fixes not yet in the export:
|
||
shows **RS 6 instead of RS 4**, and **16 sub-pages** the HA panel already has are missing
|
||
(list in `design/README.md`).
|
||
|
||
---
|
||
|
||
### Found while building (2026-08-11) — all fixed
|
||
|
||
Three defects that only surfaced by running against a real Home Assistant, not by reading code:
|
||
|
||
1. **The data layer declared field names the backend never sends.** `Fahrzeugstatus` had
|
||
`tank_prozent`/`sicher_abgestellt`/`sicherheit`; the backend writes
|
||
`tankprozent`/`gesichert`/`sicherheitscheck`. Every screen would have read `undefined` without
|
||
anything failing. `technik`/`ausstattung` were typed as objects but are arrays.
|
||
2. **The profile adapter handed out live references into the raw profile.** Editing a form would
|
||
have silently mutated the baseline and broken the promise never to overwrite the
|
||
backend-maintained tire odometer. Sections are copied now.
|
||
3. **`design-system` still carried the pre-audit `--fg3: #657081`** (3.0:1 on `--tile`, fails WCAG
|
||
AA) that the panel had already fixed to `#8a94a3`. The new app would have inherited a
|
||
already-solved contrast defect.
|
||
|
||
Also: two TypeScript parameter properties in the data layer broke Node's strip-only mode, which is
|
||
what the smoke scripts run on — rewritten as plain fields.
|
||
|
||
Three more that only screenshots revealed — nothing failed, the pixels were simply wrong:
|
||
|
||
4. **The vehicle block on the home screen was invisible.** `overflow: hidden` sets a flex item's
|
||
automatic minimum size to 0, so once the page was taller than the screen, flex-shrink squashed
|
||
the block to zero height and took model name, badge and plate with it. Fixed with
|
||
`.dm-inhalt > * { flex: none }`.
|
||
5. **The model name appeared twice** — once as the badge image, once as text beside it.
|
||
6. **Filenames overflowed the gallery thumbnails.**
|
||
|
||
And one in the backend, found by watching the log against a current Home Assistant: the odometer
|
||
screening called `urlopen` directly, which HA aborts as a blocking call since 2026.8. Every trip
|
||
stayed without a distance, visible only as a warning. Now runs through `task.executor`.
|
||
|
||
**CORS is a real constraint for this app.** `cors_allowed_origins` did not take effect on HA
|
||
2026.8 (preflight 403 even same-origin). Two consequences, both handled: the web build is served
|
||
from Home Assistant itself (`/local/dm360/`, same origin — the planned deployment anyway), and the
|
||
native hull enables `CapacitorHttp`, which routes fetch through native HTTP where CORS does not
|
||
apply. Verify this again at commissioning if the app ever moves to a separate hostname.
|
||
|
||
**A self-written QR encoder was discarded.** It disagreed with a reference implementation on 1239
|
||
of 3249 modules — the code would have been unreadable. `homeassistant/www/dm360-qr.html` now uses a
|
||
vendored MIT library served from Home Assistant itself, which satisfies the actual requirement
|
||
(no network call, token never leaves the local network) and round-trips correctly.
|
||
|
||
## Pending: this branch has diverged from `main` (noted 2026-08-13)
|
||
|
||
`ha_install.md` (root) planned the move from pyscript to a **native HA integration** with a config
|
||
flow — UI-only setup, no YAML. **That move happened on 2026-08-23; see section H.** The plan
|
||
document is now historical: it still argued for a self-reporting `UpdateEntity` against the Gitea
|
||
repo because HACS is GitHub-only, and that part was dropped — the built-in updater was removed
|
||
rather than reimplemented, and updates go through HACS (or the install script where HACS cannot
|
||
reach the repo). All APIs in it were verified against the running 2026.8.1 instance. It also
|
||
records which review findings that move eliminates by design.
|
||
|
||
A full review of `main`'s 18 new commits is in `REVIEW_main_2026-08-13.md` — 15 findings, the
|
||
three most serious in the new setup menu (saving with an unloaded catalogue wipes the whole
|
||
mapping; "reset" has no effect on 15 of 17 fields; all four list positions get the same sensor,
|
||
which makes "securely parked" report safe while three doors were never checked).
|
||
|
||
`main` has moved 18 commits ahead of `umsetzung-datametric360` (FMM003 switch, sensor-mapping setup
|
||
menu, iOS/large-screen overlay from Claude Design). **Deliberate decision: do not merge yet** — the
|
||
owner keeps working on `main` first.
|
||
|
||
Three files conflict (`AGENTS.md`, `homeassistant/INSTALL.md`,
|
||
`homeassistant/pyscript/fahrterkennung.py`); four more are touched by both sides but merge cleanly.
|
||
|
||
**One hazard that a clean merge will not catch.** This branch changed `profil_lesen()` in
|
||
`homeassistant/pyscript/modules/profil.py` to return `None` when the profile file is missing or
|
||
corrupt, and guarded all seven call sites that existed here. `profil.py` is untouched on `main`, so
|
||
it merges silently — but `main`'s call sites (`fahrterkennung.py`, `tankerkennung.py`,
|
||
`reifenzaehler.py`, `modules/frontend_veroeffentlichung.py`) do **not** guard against `None` and
|
||
would raise `AttributeError` on a missing profile instead of logging a clear error.
|
||
|
||
When merging: take `main`'s FMM003 version of `fahrterkennung.py`, then re-apply the `None` guard to
|
||
every remaining `profil.profil_lesen()` call site.
|
||
|
||
## Open items
|
||
|
||
Execution order, exact steps, and acceptance criteria for every item below live in
|
||
`UMSETZUNGSPLAN.md` (phases 1–13). Additional decisions of 2026-08-11: **no Electron** (Capacitor
|
||
wraps the web app for iPhone; a PWA home-screen install is the accepted intermediate step) and
|
||
**no separate backend** (the app talks to the HA REST/WebSocket API directly).
|
||
|
||
### A) Build DataMetric360 (the big block)
|
||
|
||
- [x] Fix the Claude Design draft (RS 4, not RS 6) and extend it by the 16 missing sub-pages;
|
||
then re-export to `design/`
|
||
- [x] `companion-app`: set up Vite + React + Capacitor scaffold; wire `@audi-dash/ui` as a real
|
||
dependency (possibly add a workspace/monorepo root)
|
||
- [x] Implement the screens from the design draft on top of the existing `DataMetricApi` layer
|
||
- [x] Add Audi assets (fonts/rings/badges) at implementation time from `homeassistant/www/` —
|
||
**never** into `design-system/`
|
||
- [x] Secure storage for the LLAT (iOS Keychain / Android Keystore via Capacitor plugin; the
|
||
`ablageSetzen()` hook already exists)
|
||
- [x] Onboarding: manual token paste (required); QR scan only if it stays simple (QR generated
|
||
locally under HA `/local/`, architecture §3)
|
||
- [x] Authenticated smoke tests of the data layer (reads, service calls, queue round-trip)
|
||
against `audi_ha_test` with a real token
|
||
- [x] Offline UX per design brief (offline marker, visible pending queue)
|
||
|
||
### B) Infrastructure / commissioning (partly waits for FMM003 hardware)
|
||
|
||
- [ ] Switch `datametric360.app` nameservers at all-inkl to Cloudflare ("full setup") —
|
||
prerequisite for the tunnel; domain carries nothing else, so this is consequence-free
|
||
- [ ] Decide hostname split (app on apex + API on `api.` subdomain, or vice versa)
|
||
- [ ] Choose reverse proxy (Nginx Proxy Manager vs. Traefik) — **can be done before hardware**
|
||
against the existing HA API
|
||
- [x] Define the reverse-proxy path allowlist (depends on final entity/service names)
|
||
- [ ] Install/wire the FMM003; record firmware version (Codec JSON is firmware-dependent)
|
||
- [x] Generate TLS certificates for Mosquitto + device (small private CA) — done 2026-08-11, 10-year
|
||
validity; Mosquitto configured (`certfile`/`keyfile`/`cafile`/`require_certificate: true`).
|
||
Remaining: upload root/client cert/key to the FMM003 Security tab — filenames must end in
|
||
`.pem`/`.pem.crt`/`.pem.key` (Configurator rejects plain `.crt`/`.key`, content-agnostic check)
|
||
- [ ] Decide broker reachability for the vehicle — **reopened 2026-08-11 evening**: port-forward
|
||
8883 on the Speedport Smart 4 Plus looked correctly configured (rule present, right internal
|
||
IP, right port) and internal reachability was confirmed (`homeassistant.local:8883` open from
|
||
the LAN, Mosquitto TLS listener genuinely up, TLS cert chain end-to-end verified byte-for-byte
|
||
against the FMM003's uploaded client cert), but the port stayed **closed from outside**
|
||
(confirmed via external port checker, both before and after a router reboot that changed the
|
||
dynamic WAN IP — DNS/DuckDNS matched correctly each time, so not a DNS or CGNAT issue).
|
||
Community reports (ComputerBase, Telekom Hilft) describe this as a known, Telekom-acknowledged
|
||
firmware bug on this router model; a full disable-the-firewall workaround doesn't exist on this
|
||
model either. Port-forward rule has been removed again. **New direction: route the FMM003
|
||
through flespi instead** (native Teltonika/Codec8 channel, IMEI-based auth, no certs, no
|
||
inbound port needed at all — flespi has a stable public endpoint; HA pulls data back out via
|
||
flespi's REST API or MQTT, outbound-only). Free flespi tier (10 devices/2 channels) is enough
|
||
for one vehicle. Next step: user creates the flespi account + Teltonika channel; a `flespi`
|
||
custom integration is already present in this HA instance (unconfigured) — check what it
|
||
needs once flespi-side setup exists.
|
||
Unrelated but still valid from the same session: DuckDNS hostname
|
||
`datametric360.duckdns.org` reliably updating; Let's Encrypt for HA's own local UI works (root
|
||
cause of the earlier DNS-01 failures was a stray `aliases` entry in the DuckDNS add-on config,
|
||
not DNS/network — see COMPANION_APP_ARCHITECTURE.md §5 item 2a). HA's SSL config now lives in
|
||
Settings → System → Network (UI), not `configuration.yaml`'s old `http:` block, which was
|
||
removed after HA started migrating/ignoring it. The Mosquitto TLS setup itself (cert chain,
|
||
`require_certificate: true`, `certfile`/`keyfile`/`cafile`) is verified correct and can be
|
||
reused as-is if a self-hosted broker is ever revisited.
|
||
- [ ] MQTT Client Type on the FMM003 ("Custom server" not selectable in practice, "AWS IoT Custom"
|
||
pointed at a self-hosted broker was confirmed working by two independent community reports) —
|
||
**moot for now** given the flespi pivot above: flespi uses the device's native Codec8/TCP
|
||
channel, not MQTT at all, so Server Settings should switch to Protocol: TCP against the flespi
|
||
channel host/port instead, and Codec set to "Codec 8 Extended". Revisit this item only if a
|
||
self-hosted broker is picked back up later.
|
||
- [ ] Capture the first real Codec JSON message (`mosquitto_sub`/MQTT Explorer) and build the
|
||
field mapping from it — **do not guess beforehand** (explicit decision)
|
||
- [x] Move trip detection to FMM003 ignition — done 2026-08-12. `fahrterkennung.py` rewritten:
|
||
trigger is now `einstellungen.ZUENDUNG_SENSOR`
|
||
(`binary_sensor.testzone_fmm003_engine_ignition_or_acc_status`, on = trip running) instead
|
||
of the iPhone WLAN sensor; same `task.unique()` pause-tolerance pattern kept unchanged.
|
||
`sensor.iphone_wifi_connection` and the `fahrzeug.wlan_name` profile field are fully removed
|
||
(backend and frontend). At the same time, all entity IDs sourced from the now-abandoned
|
||
`TommiG1/HA_VAG-EU-Data-Act` HACS integration were blanked in `einstellungen.py`
|
||
(`KM_SENSOR`, `TANK_SENSOR`, `RANGE_SENSOR`, `REFRESH_BUTTON`, door/window/lock sensors,
|
||
oil-change/inspection sensors) — the consuming features (Übersicht tiles, Reifenzähler,
|
||
automatic Tankerkennung, Fahrtabschluss-Screening) are untouched and degrade gracefully to
|
||
"unbekannt"/inactive per the existing `zustand_oder_none()` convention, rather than being
|
||
deleted; `fahrtabschluss.py`/`tankerkennung.py`/`reifenzaehler.py` now guard their
|
||
`@state_trigger(f"{einstellungen.KM_SENSOR}")`-style decorators with
|
||
`if einstellungen.KM_SENSOR:` (untested previously whether pyscript tolerates an empty
|
||
trigger string — not worth relying on). `STANDORT_TRACKER` now points at the real
|
||
`device_tracker.testzone_fmm003` and `BATTERIE_SENSOR` at
|
||
`sensor.testzone_fmm003_external_power_voltage` (the vehicle's 12V bus voltage as measured
|
||
by the tracker — NOT `sensor.testzone_fmm003_battery_voltage`, which is the tracker's own
|
||
internal backup cell and unrelated to the car). Real entity list for this device: the user's
|
||
`Entiaeten.csv` export (OneDrive, not in this repo). Verified in `audi_ha_test`: pyscript
|
||
reload (`pyscript.reload`) produced no errors/warnings for any of the changed files; the
|
||
Übersicht correctly shows "Status unbekannt" and em-dash placeholders for the now-blank VAG
|
||
fields instead of crashing (that container itself has no `device_tracker.testzone_fmm003`
|
||
registered, so Standort still shows "Kein GPS-Signal" there — expected, the real HA instance
|
||
has it). `INSTALL.md` step 4 and its troubleshooting table have since been rewritten
|
||
(2026-08-12) to point at the Setup menu instead of manual `einstellungen.py` edits, and to
|
||
use the current FMM003 field names (`ZUENDUNG_SENSOR`/`STANDORT_TRACKER`/`BATTERIE_SENSOR`)
|
||
instead of `WLAN_SENSOR`/TommiG1 — not live-tested against a real install (INSTALL.md itself
|
||
was never run end-to-end in this project, per its own opening note), but consistent with the
|
||
actual code.
|
||
|
||
### C) Maintain the existing HA panel (low priority — being replaced)
|
||
|
||
- [x] Wide-screen/desktop layout (`@container (min-width:860px)` in `audi-dashboard-ios.css`,
|
||
turns the same `.tabbar` into a 264px side-nav via CSS only, no JS/markup duplication) — done
|
||
2026-08-11 from the Claude Design "DataMetric360 Board" draft (`claude.ai/design` project
|
||
`c28a8d4d-ec4e-4178-9e49-ab5b90c02097`). Found and fixed a real regression while verifying it
|
||
live in `audi_ha_test`: the overlay's `:host > div{height:100%}` (meant to drop the old
|
||
412px-cap for the sidebar layout) was applied unconditionally instead of only inside the
|
||
`@container` block, which broke the **normal mobile layout too** — HA doesn't reliably
|
||
propagate a real height down through `panel_custom`, so `height:100%` collapsed `main#view` to
|
||
~0px (tabbar rendered right under the header, content invisible). Fix: kept the outer wrapper
|
||
viewport-anchored (`min(880px, calc(100dvh - 24px))`, matching `audi-dashboard.css`'s original
|
||
reasoning) at all widths, and only drop the 880px cap (`calc(100dvh - 24px)`, still
|
||
viewport-anchored, not parent-relative) inside `@container (min-width:860px)`. Verified live at
|
||
both narrow (mobile tab bar, full content) and wide (1144px container, grid side-nav,
|
||
navigation clicks, settings back-arrow) — screenshots taken, no regressions found.
|
||
- [x] Desktop-Sidebar-Markenklon (`.navmarke`) nachgezogen — done 2026-08-12. The user compared the
|
||
deployed app against the live Claude Design "Board" (the two-viewport comparison page, which
|
||
genuinely iframes this project's own `audi-dashboard-app.js`/`-ios.css`, not a static mockup)
|
||
and asked why the desktop Audi-rings placement differed: Design showed the rings pinned above
|
||
the sidebar nav list, ours showed them in the content header next to the title. Root cause,
|
||
confirmed via `DesignSync get_file` + byte-diff: `audi-dashboard-app.js` was already fully in
|
||
sync (identical), but `audi-dashboard-ios.css` had drifted — the Design project had since
|
||
added a `.navmarke` sidebar clone of the rings (only visible in the `@container
|
||
(min-width:860px)` block; `.rings{display:none!important}` hides the header copy there so the
|
||
mark appears once) plus a filled-pill active-tab style (`.tabpille`), neither pulled back
|
||
locally. Diffed the two full files first to confirm every change was additive/CSS-only, then
|
||
replaced the local file wholesale. Verified in `audi_ha_test` at both 402px and 1400px: rings
|
||
now sit above the nav list on desktop, gear icon alone in the content header, mobile view
|
||
unaffected, no console errors beyond the pre-existing missing-vehicle-photo 404 gap.
|
||
- [x] Standort-Kachel (live vehicle GPS position on the Übersicht, above "Zuletzt") — done
|
||
2026-08-12. New tile with a real (non-fake) Leaflet mini-map, opens a fullscreen "Standort"
|
||
route on tap; fullscreen has 4 floating controls (map style, center-on-vehicle,
|
||
center-on-user, fit-both) and a draggable myAudi-style bottom sheet (peek/expand/close by
|
||
drag or tap, opens on vehicle-marker click) showing distance-to-user, address (client-side
|
||
reverse geocoding via the public Nominatim API — same "public API, no key, low single-
|
||
vehicle volume" reasoning as the existing Leaflet-CDN gap below), fährt/steht/"Letzter
|
||
Parkplatz" state, fuel/range, and Route (Google Maps deep link) / Teilen (Web Share API)
|
||
actions. Backend: new `STANDORT_TRACKER` entity-ID setting in `einstellungen.py` (device_
|
||
tracker with lat/lon attributes), published via a new `_standort()` helper in
|
||
`frontend_veroeffentlichung.py`. The built-in default points at the author's test device
|
||
(`device_tracker.testzone_fmm003`); until the FMM003 actually delivers positions (section B),
|
||
that entity does not exist and the tile shows "Kein GPS-Signal vom Fahrzeug". Verified end-to-end in `audi_ha_test` with a manually
|
||
created `device_tracker.test_fahrzeug` test entity (tile, fullscreen map, and menu all
|
||
confirmed rendering/populating correctly); the device's own location (browser Geolocation
|
||
API) and the Route/Teilen deep links work independently of the vehicle-GPS gap. Pushed to
|
||
the Claude Design project (`c28a8d4d-ec4e-4178-9e49-ab5b90c02097`, "DataMetric360 Board")
|
||
for visual refinement — the JS/CSS ship functionally complete but visually plain
|
||
(placeholder vehicle-marker glyph, default `.aktion`/`.tile` styling); Claude Design's job is
|
||
polish, not structure. One known rough edge: reverse-geocoded address didn't resolve within
|
||
a few seconds in the `audi_ha_test` container (no crash, stays on "Adresse wird ermittelt …"
|
||
indefinitely) — likely just that container's outbound network to Nominatim specifically;
|
||
unconfirmed whether this reproduces on the real HA instance.
|
||
- [x] Setup menu (Einstellungen → Fahrzeug einrichten → last item when expanded) — done
|
||
2026-08-12. Lets the user map every sensor role the app uses (~18 roles, 3 of them 4-item
|
||
door/window/lock lists) to real HA entities from a searchable dropdown, instead of hand-
|
||
editing `einstellungen.py`. New backend module `pyscript/modules/entitaeten.py`: a
|
||
`FELDER` catalog (label, hint, expected domain/device_class/unit, keywords, whether it's
|
||
one of the trigger-bound fields) plus JSON override I/O (`data/entitaeten.json`, same
|
||
atomic-write pattern as `profil.py`) and `overrides_anwenden()`, which does
|
||
`setattr(einstellungen, key, value)` on the already-imported `einstellungen` module — every
|
||
existing consumer does `import einstellungen` + live attribute access (verified: none use
|
||
`from einstellungen import X`), so this needs no changes to any consumer file. New service
|
||
`audi_dashboard_entitaeten_schreiben` (`frontend_api.py`) and publication
|
||
`entitaeten_veroeffentlichen()` (`frontend_veroeffentlichung.py`) follow the project's
|
||
existing read-via-state/write-via-service pattern. Frontend: new popup (`vSetupPopup()`,
|
||
modeled on the existing `.sheet` pattern) with a from-scratch searchable-combobox component
|
||
(none existed in the repo before) — client-side only, built directly from `HASS.states`, no
|
||
new backend read service; a global "Nur passende Sensoren anzeigen" switch hard-filters
|
||
candidates by domain/unit, a keyword+domain+unit score ranks and auto-suggests for empty
|
||
fields. Deliberately does NOT auto-apply changes for the three trigger-bound fields
|
||
(`ZUENDUNG_SENSOR`/`KM_SENSOR`/`TANK_SENSOR` — `@state_trigger` bakes the entity ID in at
|
||
module-load time) — the UI shows an inline "wirkt erst nach Neustart" hint instead of
|
||
attempting a risky self-`pyscript.reload()` from inside a running service call. Two real
|
||
bugs found and fixed during live verification, both now also pushed to the Claude Design
|
||
project: (1) a full popup re-render on every keystroke fought the search input for focus —
|
||
fixed by patching just the candidate-list DOM node on `input`, matching the same fix the
|
||
existing `data-tankpreisfeld` handler already uses for the identical reason; (2) the popup
|
||
background used `--tile-2`, which Design had since changed to a translucent
|
||
`rgba(255,255,255,.1)` in the night theme (for the frosted-glass tile look elsewhere) — made
|
||
the whole popup partially see-through against the app content behind it. Fixed by switching
|
||
to `--tile-deckend` (Design's own opaque-surface token, already used by `.standortmenu` for
|
||
the same reason), with `var(--canvas)` as a defensive fallback.
|
||
- [x] Fresh-install packaging + `update.ps1` fix — done 2026-08-12. While preparing a short
|
||
quick-install checklist, found that `update.ps1` never copied `audi-dashboard-ios.css` or
|
||
`www/badges/*` — both are loaded by the running app (the ios overlay is injected by the app
|
||
itself via JS, badges by the model picker since item #51), so every `update.ps1` run since
|
||
those were added silently left the deployed copy stale on those two. Fixed (script now copies
|
||
both). Also removed two stray `*.bak-before-audit-merge` files from `www/` (already gitignored
|
||
at repo root, never committed, just local clutter from an earlier merge). New
|
||
`installationspaket/` — a generated, gitignored bundle (pyscript/, www/ minus the two backup
|
||
files, data/ with only the example profile + receipt parser, `configuration_snippet.yaml`,
|
||
`update.ps1`) plus a short `ANLEITUNG.md` checklist for copying to a fresh HA instance;
|
||
`INSTALL.md` stays the authoritative, detailed reference. Regenerate on demand, don't keep it
|
||
permanently in sync — it's a deployment snapshot, not a second source of truth.
|
||
- [x] Setup-menu polish: 5 additions on top of the existing Setup popup — done 2026-08-12.
|
||
Backend (`pyscript/modules/entitaeten.py`): a `_STANDARDWERTE` snapshot taken at module-load
|
||
time (before `overrides_anwenden()` ever runs), exposed via `aktueller_stand()` as
|
||
`standardwerte` — needed because `setattr()` on the live `einstellungen` module is permanent
|
||
for the process, so "reset to default" has to write the original value back explicitly rather
|
||
than just omitting the override. New service `audi_dashboard_neustart` (`frontend_api.py`)
|
||
wrapping `homeassistant.restart()`, called only from an explicit user-confirmed button, never
|
||
automatically. Frontend (`audi-dashboard-app.js`): (1) live current-value preview next to each
|
||
candidate in the search dropdown (`entitaetZeilenMarkup`, reads `HASS.states[e.id].state`);
|
||
(2) inline warning under a field if its currently assigned entity is `unavailable`/`unknown`/
|
||
missing (`entitaetStatusWarnung`); (3) duplicate-assignment check on save
|
||
(`setupDuplikate`) — blocks with a confirm-sheet ("trotzdem speichern?") rather than silently
|
||
allowing the same entity in two roles; (4) per-field reset button (only shown when the current
|
||
value differs from `standardwerte`), reverts to the built-in default; (5) after saving, if any
|
||
of the 3 trigger-bound fields actually changed (`setupGeaenderteTriggerFelder`, compares
|
||
against a snapshot taken when the popup opened), a confirm-sheet offers "Jetzt neu starten"
|
||
calling the new service. Verified live in `audi_ha_test`: value preview and unavailable-warning
|
||
confirmed correct (`ZUENDUNG_SENSOR`'s configured entity genuinely doesn't exist in this test
|
||
container → "Entität nicht gefunden." shown as designed); duplicate-check correctly caught both
|
||
a deliberately-forced KM/TANK collision and pre-existing door/window/lock auto-suggestion
|
||
collisions (a real side effect of this container's limited matching entities, not a bug);
|
||
reset button appeared/disappeared correctly as values diverged from/matched the default;
|
||
save round-tripped through the real service into `data/entitaeten.json` (byte-inspected) and
|
||
into the published `pyscript.audi_dashboard_fahrzeugstatus` state; restart-needed sheet named
|
||
exactly the changed trigger-bound field(s) and no others. Did not actually trigger a restart
|
||
during verification (would have restarted the shared test container) — the service call itself
|
||
was left unexercised beyond confirming it registers without error at pyscript load time.
|
||
- [x] Decide the iOS-vs-Audi-CI conflict from `DESIGN_REVIEW_2026-08-13.md` §A — decided
|
||
2026-08-13, owner's call: **iOS look made official** (Option 1 of the two the review offered).
|
||
`SPECIFICATION.md` §2 rewritten to describe the iOS values (colors, 16px radius) as canonical;
|
||
`bauauftrag.md` §8 now counts as superseded (left untouched, per this project's convention of
|
||
never editing that file — see its own "historical only" note above). The pre-decision Audi-CI
|
||
spec text is preserved verbatim in [`AUDI_CI_ARCHIV_2026-08-13.md`](AUDI_CI_ARCHIV_2026-08-13.md),
|
||
which also documents *why reverting later needs no restore*: `audi-dashboard-ios.css` is a
|
||
purely additive overlay loaded after `audi-dashboard.css` in `_aufbauen()`
|
||
(`audi-dashboard-app.js` ~line 3990) — the Audi-CI base values are still fully intact in the
|
||
unmodified base stylesheet, untouched by this decision. Reverting is just: stop loading the
|
||
overlay `<link>`.
|
||
- [x] Fix the two implementation bugs from the same review, independent of the direction decision
|
||
above — done 2026-08-13. **§B, red-semantics inversion:** `audi-dashboard-ios.css` `.aktion`
|
||
no longer fills every action red; base `.aktion` is now a neutral `--ios-fill` secondary
|
||
button, a new `.aktion.primaer` rule is the only one filled with `--ios-tint` (red), matching
|
||
the `.aktion`/`.primaer`/`.loeschen` semantics the base stylesheet already defined (outline
|
||
secondary / filled primary / destructive). Also fixed as part of the same finding: switch
|
||
on-state now uses `--ok` (iOS system green) instead of red; the desktop-sidebar active-tab
|
||
background (`.tab.on`) is now the neutral `--ios-fill` instead of a red tint, red stays only
|
||
as the accent text/icon color; two chart-bar segments in `audi-dashboard-app.js` that used
|
||
`--red` for a neutral data category ("Arbeitsweg" trips, first segment of the insurance
|
||
contribution bar) now use `--fg` like their sibling segments. **Radius inconsistency:**
|
||
`.setup-popup` and `.standortmenu` in `audi-dashboard.css` now reference `var(--r-tile)`
|
||
instead of a hardcoded `20px` literal, so both track whichever design (iOS 16px or, if
|
||
reverted, Audi-CI 20px) is actually active. Verified live in `audi_ha_test`: Setup-popup
|
||
Abbrechen/Speichern and the "Einrichten"/"Setup"/"Fertig" buttons in Einstellungen render with
|
||
the corrected fills at both mobile and desktop widths, switches render green, no console
|
||
errors.
|
||
- [x] Clean up remaining "EU Data Act" residue from the FMM003 pivot (2026-08-13) — the
|
||
Einstellungen "Version" tile in `audi-dashboard-app.js` hardcoded `Fahrzeugdaten: EU Data
|
||
Act / Abruf alle 15 Minuten` and `Position: iPhone / Companion App`, both stale: vehicle data
|
||
and position now come exclusively from the FMM003 (`ZUENDUNG_SENSOR`, `BATTERIE_SENSOR`,
|
||
`STANDORT_TRACKER` in `einstellungen.py`), not the abandoned HACS integration or the iPhone.
|
||
Both rows now read `FMM003`. Also fixed `testumgebung/konfiguration.yaml` +
|
||
`testumgebung/README.md`: the `input_text.test_wlan` / `iPhone WiFi Connection` template
|
||
sensor simulated WLAN-based trip detection, fully removed 2026-08-12 (`fahrzeug.wlan_ssid` no
|
||
longer exists anywhere) — its "Fahrt auslösen" example was non-functional. Replaced with
|
||
`input_boolean.test_zuendung` driving a `binary_sensor.testzone_fmm003_engine_ignition_or_acc_status`
|
||
template sensor matching the real `ZUENDUNG_SENSOR` entity ID, so trip detection stays
|
||
testable. The remaining KM/Tank/Range/door/window/lock template sensors correctly still
|
||
describe the abandoned `TommiG1/HA_VAG-EU-Data-Act` integration (per `einstellungen.py`) and
|
||
were left in place — those fields are blank by default but stay manually mappable via the
|
||
Setup menu, so the fixtures remain useful for testing that path; comments updated to make
|
||
this distinction explicit.
|
||
- [x] Fix oversized toggle switches ("Slider") across the app (2026-08-13) — `.feld label` in
|
||
`audi-dashboard.css` (`flex: 1 1 auto`, meant to let the *text* label grow so the control
|
||
lands at the row's right edge) unintentionally also matched `<label class="switch">`, since
|
||
the toggle switch is itself implemented as a `<label>` — its own `.switch { flex-shrink: 0;
|
||
width: 46px }` rule lost because `.feld label` has equal-or-higher specificity and comes
|
||
later in the file. Every switch inside a `.feld` (Einstellungen, Tanken, Reifen, ...)
|
||
stretched to fill the row (~260-300px measured live, instead of 46-51px). Fixed by excluding
|
||
switches from the selector: `.feld label:not(.switch)`. Verified live in `audi_ha_test` at
|
||
mobile (375px) and desktop widths — switches render at the correct ~51px iOS size, no
|
||
console errors.
|
||
- [x] Fix desktop (≥860px container) layout audit findings (2026-08-13, see
|
||
`DESIGN_AUDIT_2026-08-13.md` for full detail) — user-reported: (1) the settings gear button
|
||
sat far past the right edge of the visible content column on wide screens because `.topbar`
|
||
(grid-column:2, same track as `main#view`) had no `max-width:860px` while `main#view` did —
|
||
the track itself is `minmax(0,1fr)` and stretches to the full remaining window width, so
|
||
`.head{flex:1}` pushed `.profilbtn` to the track's far edge, ~450px past where `main#view`
|
||
visually ends. Fixed by giving `.topbar`/`.phone:has(.back.on) .topbar`/`.ptr` the same
|
||
`max-width:860px`. (2) In phone width the Audi rings (`#marke`/`.rings`) are, per existing
|
||
code comment, the *only* access to Einstellungen (the gear icon is hidden there) but were
|
||
42×24px — well under the 44px minimum touch target every sibling icon-button uses. Bumped to
|
||
58×32px. (3) Found while verifying (1): `.setup-popup`, `.sdpopup`, `.sheet`, and
|
||
`.standortmenu` are all `position:absolute` with `left`/`right` relative to the *full*
|
||
`.phone` box (sidebar included), not the content column, and had no desktop max-width either
|
||
— `.setup-popup` measured 1644px wide on a 1920px window (10px margins only). Fixed with
|
||
`max-width:560px` (`.setup-popup`/`.sdpopup`/`.sheet`) or `640px` (`.standortmenu`) plus
|
||
`margin-left/right:auto` in the same container query — deliberately not
|
||
`transform:translateX(-50%)`, which would fight these elements' own transform-based entry
|
||
animations. Centering is relative to the full `.phone` width (sidebar included), not the
|
||
860px content column exactly — a ~138px rightward offset from true content-center remains at
|
||
1920px; fixing that precisely would require moving these overlays into grid-column 2 in the
|
||
markup, deferred as a larger structural change. All three verified live in `audi_ha_test` at
|
||
375px and 1920px, no console errors.
|
||
- [x] Fix 4 more user-reported design findings (2026-08-13, same session, see
|
||
`DESIGN_AUDIT_2026-08-13.md` "Runde 2" for full detail with before/after measurements):
|
||
(1) page title sat ~30px right of the content tiles below it on wide screens — `.back`
|
||
reserves 44px+gap in `.topbar` even when inactive (`opacity:0`, not `display:none`), fixed
|
||
with `.back:not(.on){width:0;...}` in the desktop container query, root pages only (detail
|
||
pages with `.back.on` unaffected, verified back-navigation still works). (2) the chevron on
|
||
the 5 `.go`-tiles (Fahrzeug/Service/Versicherung/Reifen/Schutzbrief on Mein Audi/Versicherung)
|
||
was vertically centered on the *entire* multi-row tile (`top:50%` in the iOS overlay) instead
|
||
of next to the title, overlapping wrapped row text — removed the override, falls back to the
|
||
base `top:var(--sp-5)` next-to-title position. (3) the Ölwechsel-Intervall `<select>` values
|
||
("10.000 km"/"1 Jahr") looked like inert text — iOS overlay strips border/background from
|
||
`.feld select` and colors it the same grey as any other secondary text; added
|
||
`.feld select{color:var(--ios-tint)}` so picker fields read as editable the way iOS
|
||
conventionally signals it (accent color), free-text `.feld input`s deliberately left alone.
|
||
(4) the expand/collapse `.mark` chevron on group headers (Fahrten "2026"/"August") looked
|
||
like a checkmark — it uses the classic "2 borders + rotate(45deg)" CSS chevron technique,
|
||
which requires a square box; `.mark` was 6×10px (non-square, sized for the unrelated SVG-path
|
||
`.chev`/`.go` icons), made it 8×8px. Two more reported findings ("grey box around all menu
|
||
icons", "red partial frame on Fahrten/Tanken rows") were investigated (computed-style
|
||
diffing across all 5 tabs in both themes, a temporary 2.5x `transform:scale()` zoom on the
|
||
live swipe-row DOM) but could not be reproduced — documented in the audit file as open,
|
||
pending a screenshot from the real device.
|
||
- [x] Fix five user-reported bugs (2026-08-16):
|
||
(1) **Türschloss-/Haubenschloss-Erkennung entfernt** — user asked for these two checks to be
|
||
permanently removed (not just left blank/unmapped like the rest of the retired VAG-
|
||
integration fields). Removed `TUERSCHLOSS_SENSOREN`/`HAUBENSCHLOSS_SENSOR` from
|
||
`entitaeten.py`'s `FELDER` catalog (no longer offered in the Setup menu) and from
|
||
`einstellungen.py` (attributes deleted, not just blanked), and removed their entries from
|
||
`_sicherheitscheck()` in `frontend_veroeffentlichung.py` (no longer listed under "Geprüfte
|
||
Punkte"). `HECKKLAPPENSCHLOSS_SENSOR` (tailgate lock) was deliberately left untouched — user
|
||
named only the two door/hood locks. Also removed the now-orphaned
|
||
`input_boolean.test_entriegelt` helper and the four `test_audi_lock_*` template sensors from
|
||
`testumgebung/konfiguration.yaml` (door-open/window fixtures for `TUER_SENSOREN`/
|
||
`FENSTER_SENSOREN` are untouched), and updated `testumgebung/README.md` accordingly.
|
||
(2) **"Doppelt zugeordnet" (and every other confirm-sheet) was see-through** — `.sheet-gruppe`/
|
||
`.sheet-abbrechen` in `audi-dashboard.css` used `--tile-2`, which the iOS overlay's night
|
||
theme redefines as `rgba(255,255,255,.10)` (translucent, meant for tile-on-tile layering) —
|
||
the same root cause already fixed for `.setup-popup`/`.standortmenu` on 2026-08-13, just not
|
||
carried over to the generic confirm-sheet used by `bestaetigen()`/`hinweis()`. Switched both
|
||
to `var(--tile-deckend, var(--canvas))`, matching the existing fallback pattern already used
|
||
elsewhere in this file (base CSS must not go blank if the purely-additive iOS overlay is ever
|
||
reverted, since `--tile-deckend` is an overlay-only token).
|
||
(3) **FMM003 coordinates arrive as two separate sensors, not one device_tracker** — user
|
||
reported "coordinates are split into latitude and longitude". Root cause: `_standort()` in
|
||
`frontend_veroeffentlichung.py` only ever read `latitude`/`longitude` as *attributes of a
|
||
single `STANDORT_TRACKER` device_tracker entity* (HA's usual convention) — but some
|
||
integrations (confirmed live in `audi_ha_test`'s own simulated FMM003 device, which exposes
|
||
separate "Latitude coordinate value"/"Longitude coordinate value" sensors) publish them as
|
||
two independent `sensor` entities instead, which `_standort()` had no way to consume. Added
|
||
`STANDORT_LAT_SENSOR`/`STANDORT_LON_SENSOR` (new optional fields in `einstellungen.py` +
|
||
`entitaeten.py`'s Setup-menu catalog, group "standort") as a second path: `_standort()` now
|
||
tries `STANDORT_TRACKER` first (unchanged behavior when it works), and falls back to reading
|
||
the two plain sensors' states directly when the tracker is unset or has no coordinates.
|
||
(4) **App can show only "Lädt …" until a tab is clicked** — known, already-mitigated race
|
||
(see the `nachladeAnstossen` header comment, `audi-dashboard-app.js`): on a slow/cold backend
|
||
start, `render()` no-ops until `DATEN_GELADEN` flips true, and the previously observed
|
||
manual fix was a HA panel remount. Root cause of *why* the click helps was not fully
|
||
reproducible in `audi_ha_test` (this is a genuine uncertainty, flagged rather than guessed
|
||
around) — but as a safe, low-risk hardening, `go()` (the tab/menu click handler) now calls
|
||
`datenLaden(false)` proactively if `DATEN_GELADEN` is still false, so any click that happens
|
||
to occur after data has actually become available is guaranteed to pick it up immediately
|
||
instead of depending on timing.
|
||
(5) **Fuel-receipt parser only understood Shell's exact layout** — user asked for a generic
|
||
algorithm for other/unknown fuel stations: find the address, find the total ("Gesamt"/
|
||
"Absolut"), find the liters ("Menge"/"Amount"), divide to get the paid price/liter, compare
|
||
against a printed "Preis/Liter" — a mismatch means a discount, found on the receipt as a
|
||
minus-marked amount. Implemented exactly this as `_parsen_generisch()` in
|
||
`homeassistant/data/shell_beleg_parser.py`, wired as a fallback in `main()` (Shell-specific
|
||
`_parsen()` tried first, unchanged — verified the existing 10-receipt regression suite still
|
||
passes byte-for-byte; `_parsen_generisch()` verified against a synthetic non-Shell receipt
|
||
with a known discount, all fields and the discount math correct). No separate "SmartDeal"
|
||
flag was needed — the frontend already treats any populated `discount` as SmartDeal-eligible
|
||
(gated only by the user's own "SmartDeal aktiv" switch, not by station name), so a generically
|
||
parsed Shell receipt behaves identically to one parsed via the strict path. Receipt-key
|
||
generation uses `hashlib.sha1` (not the built-in `hash()`, which is randomly salted per
|
||
process and would have broken the §7.7 duplicate-detection dedup across the parser's
|
||
per-receipt subprocess invocations).
|
||
All five deployed and verified live in `audi_ha_test` (version `1786752000`): Sicherheit-
|
||
Liste and Setup-Katalog confirmed to no longer offer Türschloss/Haubenschloss; the new
|
||
Breitengrad/Längengrad Setup fields confirmed present and correctly auto-suggesting the
|
||
container's real split lat/lon sensors; "Doppelt zugeordnet" confirmed rendering on an opaque
|
||
card by deliberately triggering it (this container's limited entity set causes a real
|
||
duplicate auto-suggestion); pyscript reload clean, no new console errors beyond the
|
||
pre-existing placeholder-image 404s. Synced to `installationspaket/`.
|
||
- [x] Follow-up round the same day (2026-08-16), three more items:
|
||
(1) **Removed `STANDORT_TRACKER` entirely** — user confirmed the combined device_tracker
|
||
field is no longer needed (flespi only ever provides the split lat/lon sensors added earlier
|
||
that day). Deleted the field from `einstellungen.py` and `entitaeten.py`'s Setup catalog, and
|
||
simplified `_standort()` in `frontend_veroeffentlichung.py` down to the single split-sensor
|
||
path (no more two-branch fallback). `overrides_schreiben()`'s existing "only known keys
|
||
survive" behavior (see its docstring) meant no explicit migration was needed for stale
|
||
`STANDORT_TRACKER` entries already sitting in `data/entitaeten.json` — confirmed live: this
|
||
container's own leftover override file still had `STANDORT_TRACKER` from earlier testing
|
||
(plus stale `TUERSCHLOSS_SENSOREN`/`HAUBENSCHLOSS_SENSOR` from before finding (1) of the
|
||
previous entry), all silently dropped on the next Setup save, which is exactly why the GPS
|
||
tile was still showing "Kein GPS-Signal" after the user's own remap attempt earlier — that
|
||
attempt happened to save while the old combined field was still in the catalog, so nothing
|
||
ever actually persisted `STANDORT_LAT_SENSOR`/`STANDORT_LON_SENSOR`. Redid the Setup save
|
||
live (mapped both to `sensor.testcar_b9_fmm003_testintegratoin_lat/longitude_coordinate_value`)
|
||
and confirmed the Übersicht/fullscreen Standort map switched from the "no signal" placeholder
|
||
to a real rendered map at the correct coordinates (South Tyrol terrain, "Burgleralm" label
|
||
visible).
|
||
(2) **Fuel-receipt "silently does nothing", root cause found** — reproduced the exact
|
||
real upload flow (button click → real `<input type=file>` → synthetic `File`/`DataTransfer`
|
||
dispatch → `FileReader` → `hass.callService`) via browser instrumentation: the happy path
|
||
works completely (backend parses, publishes `pyscript.audi_dashboard_beleg_ergebnis`, frontend
|
||
fills the form) — `belegverarbeitung.py`'s existing `try/except` around `_parser_aufrufen()`
|
||
already publishes a visible `fehler` for parser failures specifically. The actual gap:
|
||
`base64.b64decode()` and `_pdf_speichern()` in `audi_dashboard_beleg_hochladen()` were
|
||
*outside* that try block — any failure there (corrupted upload, disk full, ...) raised an
|
||
unhandled exception with zero UI feedback, since `serviceRufen()`'s generic `.catch()` only
|
||
does `console.error()`. Widened the try block to cover all three steps. Also stopped using
|
||
`serviceRufen()` for this one call site specifically (`audi-dashboard-app.js`, the
|
||
`eingabe.onchange` handler) — it now calls `HASS.callService()` directly with its own
|
||
`.catch()` that sets `belegFehler` and re-renders, so a rejected service call (network,
|
||
timeout — anything that bypasses the backend's own state-publish) is visible too, not just
|
||
logged. Verified against the real 10-receipt Shell suite (still passing) both before and
|
||
after this change.
|
||
(3) **Vehicle map marker replaced with a two-tone pin+car icon** — added `CI.pinCar` (a
|
||
balloon/pin path + white badge circle + a simplified car-front glyph built from rects, not
|
||
circles, after an isolated-DOM-injection test round showed a circles-for-headlights version
|
||
read as a face/animal rather than a car) to `audi-dashboard-app.js`, replacing the single-color
|
||
`poiCar` teardrop previously used by `fahrzeugMarkerSVG()`. Updated both marker call sites'
|
||
`iconAnchor` to the new pin's actual tip position and simplified `.fahrzeug-pin` in
|
||
`audi-dashboard.css` (no more `color:var(--red)`/white-halo-filter hack, since the new icon
|
||
carries its own fixed colors). Verified the icon markup renders correctly via an isolated
|
||
DOM-injection test (bypassing Leaflet); could **not** get a final on-map screenshot in
|
||
`audi_ha_test` — the Standort map tiles themselves only ever partially load in this container
|
||
(reproduced on a fully fresh reload, unrelated to this change), consistent with a previously
|
||
documented network limitation of this specific sandbox (see the Standort-Kachel entry above:
|
||
reverse-geocoding via Nominatim had the same kind of container-specific network issue).
|
||
Structurally verified only; needs a look on a real device/network to confirm final visual
|
||
placement.
|
||
All three deployed to `audi_ha_test` (version `1786758000`) and synced to `installationspaket/`.
|
||
- [x] Third round the same day (2026-08-16) — **the "fragmented Leaflet rendering" defect is solved
|
||
at its root**, plus four smaller items:
|
||
(1) **Leaflet CSS never reached the shadow root.** `leafletLaden()` appended
|
||
`leaflet.min.css` to `document.head`, but the panel renders inside `this.shadowRoot` — and
|
||
document stylesheets do not cross a shadow boundary. Inside the panel, `.leaflet-tile
|
||
{position:absolute}` and friends therefore never applied: the tiles laid themselves out as
|
||
ordinary in-flow `<img>` elements (two per row, ~1040 px of stacked height inside a 190 px
|
||
box), so only a thin strip of map was ever visible, and the marker pane ended up ~1009 px
|
||
below the visible area — which is exactly why the new vehicle pin appeared "missing". This
|
||
also explains the earlier misdiagnosis as a container/network limitation: every tile request
|
||
succeeded (`complete:true`, `naturalWidth:512`), the geometry was the problem. Fixed by
|
||
injecting the stylesheet into `ROOT` and awaiting it before the map is built; the script tag
|
||
stays in `document.head` (it must, `window.L` is global). The CSS injection is deliberately
|
||
re-checked on every `leafletLaden()` call rather than guarded by the `window.L` check, because
|
||
a HA panel remount produces a fresh shadow root while `window.L` is already set. Verified live:
|
||
both the Übersicht preview map and the fullscreen Standort map now fill their containers and
|
||
show the pin at the vehicle position.
|
||
(2) **Maps are now always light.** `TILES[theme]` switched to CARTO `dark_all` in Nacht mode;
|
||
replaced by a single themeless `TILE_URL` (`light_all`), matching how Google Maps & co. keep
|
||
the standard road map light regardless of app theme.
|
||
(3) **Vehicle marker now uses the real CI `poi-car` icons** (`poi-car-l.svg` / `poi-car-s.svg`
|
||
from the delivered icon set) instead of the hand-built two-tone pin from the previous round:
|
||
`CI.poiCarL` (48-grid) at ≥34 px, the existing `CI.poiCar` (24-grid) below, anchors recomputed
|
||
per variant. Since these are single-color outline forms in `currentColor`, `.fahrzeug-pin`
|
||
regained a stacked white drop-shadow halo so the outline stays readable on map tiles.
|
||
(4) **Red partial frame on Fahrten/Tanken list rows** (open since the 2026-08-13 design audit,
|
||
finding 3) — reproduced and confirmed by recoloring the delete button live. Row heights are
|
||
fractional (66.28 px), so `.swipe-content`'s edges miss the device-pixel grid and the red
|
||
`.swipe-delete` behind it bled through as a hairline around every row. Fixed by only painting
|
||
the delete button while a swipe is actually happening: `.swipe-delete{visibility:hidden}` plus
|
||
a `wischt` class set on the first movement of the gesture (and the existing `swiped` class for
|
||
the open row). Verified the full gesture still works — button appears at the first pixel of
|
||
drag and the row settles open at −84 px.
|
||
(5) **Select fields now show the grey background box** (design audit finding 1) — replaced
|
||
`.feld select{color:var(--ios-tint)}` with `background:var(--ios-fill);padding:8px 10px`,
|
||
matching `.mitEinheit input` ("Pause bis [15] Minuten"), as the user asked. Verified on
|
||
Modell / Ölwechsel-Intervall / Bildposition; free text fields stay plain, so the box now
|
||
genuinely marks "there is a choice here".
|
||
(6) **Generic receipt parser: total and station name** — a real Austrian non-Shell receipt
|
||
returned the right litres but 6,00 € and no station. Two causes, both fixed: the free-running
|
||
total pattern matched the *column heading* `SUMME-EUR` and read the next line's article number
|
||
as the amount, and bon printers letter-space headings (`G E S A M T BETRAG EUR: 92,60`), which
|
||
the keyword never matched. Total detection is now line-based — keyword checked against the
|
||
whitespace-stripped line, amount must be a real money value (`_GELD`, decimals required), tax
|
||
lines (`MWST`/`UST`/`VAT`/`STEUER`/`NETTO`) excluded, largest remaining candidate wins. The
|
||
address heuristic no longer demands three fixed lines with a 5-digit postcode (AT uses 4
|
||
digits, and this receipt carries `SHELL TANKSTELLE, 6450 SÖLDEN` on one line): it finds the
|
||
first postcode line in the header and takes the name from the line above. `Preis/L` without a
|
||
spelled-out "Liter" now matches too. Result on that receipt: 42,13 l · 92,60 € · 2,198 €/l ·
|
||
no discount · `AUTO B. FRISCHMANN GMBH`. The 10-receipt Shell regression suite still passes.
|
||
All deployed to `audi_ha_test` (version `1786765000`) and synced to `installationspaket/`.
|
||
- [x] Fourth round the same day (2026-08-16):
|
||
(1) **The "grey box around the icons" report is resolved — it was never a defect.** After five
|
||
fruitless reproduction attempts across two sessions, the user clarified what they actually
|
||
meant: the grey box seen on "10.000 km", "1 Jahr", "15" is the *desired* pattern, and **every
|
||
value the user can change should carry it** to signal editability. `.feld input`/`.feld select`
|
||
in `audi-dashboard-ios.css` now all get `background:var(--ios-fill);padding:8px 10px`;
|
||
inputs with their own visual language (`checkbox`/`radio`/`range`/`file`) are excluded and keep
|
||
the transparent base rule, so the three switches stay untouched. The separate
|
||
`.mitEinheit input` rule became redundant and was removed (its width/alignment still come from
|
||
`audi-dashboard.css`). Verified live: FIN, Kennzeichen, Erstzulassung, Ausführung, Modell,
|
||
both Ölwechsel selects, Ansicht, Pause bis and Autom. Backup all boxed; the three checkboxes
|
||
not. **Do not re-open this as a bug.**
|
||
(2) **Station display name is now "Marke, Straße, Ort"** (e.g. `Shell, Pascalstr. 8,
|
||
Ingolstadt`) instead of the bare operator name (`A. Zrenner GmbH`) — the brand and place say
|
||
more when scrolling the Tankvorgänge list. New `_marke()`/`_ist_strasse()`/`_tankstelle()`
|
||
helpers in `shell_beleg_parser.py`, shared by both parser paths so Shell and non-Shell
|
||
receipts format identically. The brand is matched only against the first 8 lines (the receipt
|
||
header) — searching the whole text would let "Total" hit a totals line. Missing parts are
|
||
dropped rather than left as empty comma slots (the Austrian receipt prints no street:
|
||
`Shell, SÖLDEN`), and without a recognizable brand the operator name takes the brand's place.
|
||
`station_address` keeps the full street + postcode + city as before. The regression suite's
|
||
`test_bekannte_stationen` expectations were updated to the new format (intended change, not a
|
||
break); all 8 tests / 164 subtests pass.
|
||
(3) **`installationspaket/` is versioned from now on** (user request) — removed from
|
||
`homeassistant/.gitignore`. It carries no real vehicle data, only
|
||
`data/fahrzeugprofil.example.json` with empty FIN/Kennzeichen placeholders; verified before
|
||
un-ignoring. **Keep it in sync whenever `pyscript/` or `www/` changes** — that was already the
|
||
working rule, but it is now visible in the repo when it is forgotten.
|
||
Deployed to `audi_ha_test` (version `1786766000`).
|
||
- [x] Fifth round the same day (2026-08-16) — one-click installer and the Einzelbeleg map:
|
||
(1) **`installationspaket/Installieren.cmd` + `install.ps1`** automate steps 1–3 of
|
||
`ANLEITUNG.md`. The `.cmd` wrapper exists because double-clicking a `.ps1` opens it in an
|
||
editor and the execution policy blocks it; the bypass is per-call, the machine policy is not
|
||
touched. Safe to re-run: never overwrites `fahrzeugprofil.json`, `fahrten.jsonl`,
|
||
`tankvorgaenge.jsonl`, `entitaeten.json`, `ha_token.txt`; backs `configuration.yaml` up to
|
||
`.bak` and replaces its own marker-delimited block instead of appending twice; refuses to
|
||
touch the file at all when `pyscript:`/`panel_custom:` are already there from another source
|
||
(duplicate top-level keys are invalid YAML — HA would not start); aborts before writing if
|
||
the target has no `configuration.yaml`. `-Pruefen` is a dry run. **`install.ps1` is stored
|
||
UTF-8 *with* BOM on purpose** — PowerShell 5.1 reads scripts as ANSI otherwise and mangles
|
||
every umlaut; the files it *writes* stay BOM-less (YAML). Verified against a fake config
|
||
tree: fresh install, re-run idempotency, data preservation, foreign-key refusal,
|
||
wrong-directory abort, and the merged YAML parses.
|
||
(2) **Tankstellenposition im Einzelbeleg.** The red `circleMarker` is now the CI `poi` pin
|
||
(`CI.poiL`/`CI.poiS` from `poi-l.svg`/`poi-s.svg`, via `tankstellenMarkerSVG()`) — deliberately
|
||
the plain pin, not `poi-car`, so vehicle and station stay distinguishable.
|
||
**Found while testing: nothing in the backend ever writes `station_lat`/`station_lon`**, so
|
||
this marker had never actually been reachable. Fixed by geocoding the receipt address in the
|
||
frontend (`adresseAufloesen()`, Nominatim `/search` — the forward counterpart to the existing
|
||
`standortAdresseAufloesen()`), with results cached in `localStorage` (`audi_dashboard_geocache`)
|
||
including negative results, since a station address does not move and Nominatim's terms ask
|
||
for caching. Confirmed live: `Shell, SÖLDEN` → 46.9756/11.0111.
|
||
(3) **Zentrieren-Knopf** in the Einzelbeleg map card (`mapBoxMitZentrieren()`, `CI.gps` —
|
||
byte-identical to the delivered `gps-s.svg`), disabled until the coordinates arrive. `.mapbox`
|
||
gained `position:relative` so the control anchors to the card. Only there, per explicit
|
||
request — the Standort fullscreen already has its own centre controls.
|
||
Deployed to `audi_ha_test` (version `1786769000`).
|
||
Note for testing map behaviour in this app: `initMap()` runs on **every** `render()`, and the
|
||
app re-renders on each backend push (~30 s), rebuilding the map and re-centring it. Panning
|
||
the map from the console and checking later therefore proves nothing — spy on
|
||
`L.Map.prototype.flyTo` instead.
|
||
- [x] Sixth round the same day (2026-08-16) — a large batch of user-reported UI polish items, plus
|
||
the map pins' real visibility root cause:
|
||
(1) **Map pins were genuinely near-invisible, not just a styling nit.** The user reported the
|
||
vehicle's map pin looked "transparent"; reproduced empirically (not by eye) by rasterizing the
|
||
live marker's SVG to a canvas and sampling alpha — only ~13% of the icon's own bounding box had
|
||
any ink, and the exact center pixel was fully transparent. Root cause: `CI.poiCarL`/`poiCar`
|
||
(and `poiL`/`poiS`, used for the Einzelbeleg station pin) are pure **contour** forms — the
|
||
balloon outline is drawn as two nested, oppositely-wound paths that cancel to a 1px ring under
|
||
the default nonzero fill rule, plus thin interior detail lines; there is no filled area at all.
|
||
A first fix (solid navy circle behind just the icon's round "head", via `::before`) was
|
||
explicitly rejected by the user ("I want the whole icon solid, not just the car" / "make the
|
||
needle opaque") — correct, since it only covered ~36% of the icon and left the pointed tip as a
|
||
bare outline. Real fix: each icon's `d` attribute is multiple sub-paths (split at top-level
|
||
M/m); the **first** sub-path alone is already the complete, correct outer balloon silhouette
|
||
(verified by rasterizing it in isolation — solid center pixel, 36-41% fill ratio, matching a
|
||
normal teardrop-in-square ratio). Extracted that first sub-path verbatim (no redrawing) into
|
||
four new `CI.poiCarLSil`/`poiCarSil`/`poiLSil`/`poiSSil` constants, fixed-color-filled (not
|
||
`currentColor`), and `fahrzeugMarkerSVG()`/`tankstellenMarkerSVG()` now render it as the bottom
|
||
layer behind the original detailed icon; `.fahrzeug-pin`'s color switched from dark navy to
|
||
white so the original outline/detail lines read against the new solid fill, exactly like the
|
||
white-glyph-on-solid-teardrop convention of Google/Apple Maps pins. Applies uniformly to both
|
||
marker types now (station pin included, per explicit user request), so the earlier per-variant
|
||
`.auto` CSS modifier and its circle-position math were removed again as dead weight.
|
||
(2) **Battery voltage statistic cutoff lowered 13.2V → 12.8V** (`AGM_RUHE_MAX_V` in
|
||
`audi-dashboard-app.js`) — user's point: anything above ~12.8V (full-charge resting voltage per
|
||
the existing in-code research citation) is already alternator output, not the battery's own
|
||
state, so admitting up to 13.2V let charging-tail values distort the charge-state stats.
|
||
(3) **Battery chart Y-axis no longer "stretches."** `bvZeichnen()` recomputed `yMin`/`yMax` from
|
||
whatever points were currently zoomed into (min 0.4V span + 15% pad) on every redraw, so the
|
||
axis rescaled on every pan/zoom gesture. Now computed once from the full dataset in
|
||
`vBatterieverlauf()` (`bvYDomain`) and reused unchanged across zoom/pan.
|
||
(4) Removed two explanatory paragraphs from the Batteriespannung detail view ("Näherung auf
|
||
Basis…" and "Ziehen zum Verschieben…") per explicit request.
|
||
(5) **Tankfüllung now shows "X % / X l"** (`vAudi()`) instead of a literal-litre primary value
|
||
with a redundant "100 % von 58 l" sub-line.
|
||
(6) Removed the "· noch etwa X l im Tank" suffix from the Übersicht's "Reichweite" label
|
||
(`vHome()`); the now-unused `liter` local was removed with it.
|
||
(7) **Active/primary buttons are grey again, not red** — `.aktion.primaer` in
|
||
`audi-dashboard-ios.css` filled with `--ios-tint` (red) since the 2026-08-13 iOS-overlay
|
||
decision; explicit user request to drop that and match the base stylesheet's neutral
|
||
`var(--fg)`/`var(--canvas)` fill. Destructive actions (`.aktion.loeschen`) intentionally stay
|
||
red — Apple's own HIG reserves red specifically for destructive actions, so this one use is
|
||
correct per the guideline, not just left over.
|
||
(8) **`<select>` fields now carry an Apple-HIG pull-down chevron.** They already had
|
||
`appearance:none` (native arrow removed) but no replacement indicator at all — a real gap, not
|
||
cosmetic. Added a CSS `background-image` chevron (light/dark variants) in both stylesheets;
|
||
base `audi-dashboard.css` kept independently correct (own light/dark variant via
|
||
`:host([data-theme=tag])`) per this repo's "purely additive overlay" convention for the ios
|
||
file.
|
||
(9) **Apple HIG research round.** Read Color/Typography/Layout/Buttons/Materials directly off
|
||
developer.apple.com (the WebFetch tool returned only page titles for this JS-rendered site: the
|
||
in-app Browser tool's `get_page_text` worked and was used instead) and cross-checked against
|
||
actual measurements in this codebase, then asked the user via `AskUserQuestion` which findings
|
||
to apply. Accepted: text below the 11pt iOS legibility minimum (battery-chart axis labels were
|
||
8px, the header's "Sync" line was 10.5px) raised to 11px; the new compact "Montiert" pill's
|
||
*invisible* hit area (not its visual size) expanded to the 44×44pt minimum via a `::before`
|
||
with negative insets, same technique applied to future compact controls. Declined, deliberately:
|
||
switching the app's few weight-300 ("Light") text spots to 400 — Audi Type only ships 300/400,
|
||
and the user chose to keep Audi CI's own weight over HIG's general "avoid Light" guidance;
|
||
making popups/sheets translucent to match the Materials guidance — the app already restricts
|
||
translucency to the nav layer only (tab bar), which *is* the HIG-correct pattern, and popups
|
||
were deliberately made opaque on 2026-08-16 earlier the same day to fix a real see-through bug,
|
||
so translucency there would be a regression, not an improvement.
|
||
(10) Removed "Aus dem Fahrzeug gelesen" and the Einrichten intro paragraph ("Fahrgestellnummer,
|
||
Kennzeichen, Erstzulassung…") from `vEinst()`, both per explicit request.
|
||
(11) **Fuel bar color now matches the headline** (`var(--fg)`) instead of a red/yellow/green
|
||
status gradient in `vHome()`; the `balkenFarbe` local was removed with it.
|
||
(12) **Selected menu icon is white (headline color), not red**, in the desktop side-nav
|
||
(`.tab.on` inside the `@container (min-width:860px)` block in `audi-dashboard-ios.css`) — the
|
||
mobile tab bar already used `var(--fg)` there, this brought the desktop variant in line.
|
||
(13) **Reifenfoto (Sommerrad/Winterrad) now crops from the right edge**, not the center —
|
||
`.radbild` gained `object-position: right center`; the photo is typically wider than the 96px
|
||
square slot and the wheel itself tends to sit toward the right of the source photo.
|
||
(14) **"Montiert" button rebuilt as a compact top-right pill** (new `.montiert` class,
|
||
replacing the old full-width `.seg`-wrapped button) per explicit layout instructions: top edge
|
||
aligned with the wheel photo's top (`top:var(--sp-5)`, same offset the photo already has from
|
||
the tile's own padding), right inset equal to the tile's standard left content inset (both
|
||
`var(--sp-5)`) so left/right margins match. Markup reordered so the button renders before
|
||
`.radkopf` (position is absolute either way, but keeps DOM order sensible).
|
||
(15) **Tab-bar active highlight now covers icon *and* label when the label is visible**, not
|
||
just a tight pill behind the icon — `.tabbar:not(.ohne) .tab.on` gets the highlight background
|
||
directly (whole button), with the inner `.tabpille` background suppressed in that case; the
|
||
icon-only mode (`.tabbar.ohne`) keeps the original tight icon-pill unchanged.
|
||
Deployed to `audi_ha_test` (version `1786901000`); pin fix visually confirmed live (solid
|
||
teardrop pin with white car glyph, clearly over the map's "Obergurgl" label text) and the
|
||
tab-bar highlight confirmed covering both icon and "Übersicht" label.
|
||
- [x] Seventh round the same day (2026-08-16) — more user-reported layout/interaction bugs:
|
||
(1) **Wheel photo (Sommerrad/Winterrad) was never actually a square, despite the fifth round's
|
||
crop-direction fix.** Root cause: `bildMitPlatzhalter()` puts the `radbild` class on *both* the
|
||
wrapping `.bildbox` div and the `<img>`; the old `.radbild{width:96px;height:96px;...}` rule and
|
||
`.bildbox{width:100%;aspect-ratio:4/3;...}` have equal specificity (one class each), and
|
||
`.bildbox` is declared later in the file, so it silently won — the box stayed a full-width 4:3
|
||
rectangle the whole time, `object-position:right center` just changed which slice of that wide
|
||
box showed. Fixed with a `.bildbox.radbild{width:96px;height:96px;aspect-ratio:1/1}` compound
|
||
selector (two classes, unambiguously higher specificity than plain `.bildbox`); `.radbild` alone
|
||
now only carries `object-position`. Verified live: `.bildbox.radbild` now measures exactly
|
||
96×96px.
|
||
(2) **"Montiert" moved from top-right to top-left**, per explicit request — this was the visible
|
||
symptom of (1): the pill was anchored top-right assuming it overlaid the (accidentally) full-
|
||
width photo; once the photo is a real 96px square on the left, top-left is where it actually
|
||
needs to sit to stay over the picture.
|
||
(3) **Native `<select>` popups now get a `color-scheme` hint.** The closed `<select>` box itself
|
||
was already themed correctly, but the *opened* native option list ignores app CSS and falls back
|
||
to the browser/OS default palette unless `color-scheme` says otherwise — this is what made
|
||
"Modell" and every other dropdown "flash" bright white against the dark app on open. Added
|
||
`color-scheme: dark` under `:host([data-theme="nacht"])` and `color-scheme: light` under
|
||
`:host([data-theme="tag"])` in `audi-dashboard-ios.css`.
|
||
(4) **Fixed a real JS crash in the "Mein Audi" image-cycle click handler.** `bildWeiter()` (fires
|
||
on tapping the hero photo) tried `box.querySelector(".platzhalter-datei").textContent = ...` —
|
||
that class was never emitted by `bildMitPlatzhalter()` (the actual class is `.platzhalter-
|
||
aktion`, and in gallery mode it deliberately shows a static "Foto hinzufügen" prompt, not a
|
||
filename). `querySelector` returned `null`, the assignment threw, and the exception aborted the
|
||
function *before* reaching the line that updates the `.dots` page indicator — so the photo
|
||
itself advanced (that line ran first) but the dots never moved, reading as "doesn't work."
|
||
Removed the dead line entirely; verified live that the `.dots` `on` class now correctly moves
|
||
with each click and no exception is thrown.
|
||
(5) **Merged the separate "Fahrzeugbilder" upload grid into "Bild der Übersicht"**, per explicit
|
||
request: pick a view from the existing "Ansicht" dropdown, then tap the (now also click-to-
|
||
upload) preview below it to upload/replace/delete that specific photo — reusing the same
|
||
generic `data-bildklick`/`bildMenuOffen` popup plumbing already used by the wheel-photo tiles,
|
||
not a new mechanism. `bildInfo()`'s existing winter-side-view special case (swaps in
|
||
`seitenansicht-winter.webp` while winter tires are marked mounted) means the winter variant
|
||
stays uploadable through the same control, gated by current tire season instead of a permanently
|
||
visible separate slot. The now-unused `BILDER_UPLOAD_SLOTS` constant and the `.bildgrid`/
|
||
`.bildslot`/`.bildslot-label`/`.carfix.mini` CSS were removed as dead code; a new
|
||
`.bildmenu.ansicht` positions the ersetzen/löschen popup anchored to the tile's *bottom* edge
|
||
(not a fixed top offset like the other two menu variants) since this preview's height varies
|
||
with the tile's own content instead of being a fixed small thumbnail.
|
||
(6) **Back arrow changed from red (`--ios-tint`) to the neutral headline color (`--fg`)** in
|
||
`audi-dashboard-ios.css`, matching every other navigation-color decision made this project (red
|
||
stays reserved for destructive actions per Apple's HIG).
|
||
Initially investigated but could not reproduce (see below for the real fix, found later the same
|
||
day): user reported that hiding tab labels ("Beschriftung in der Menüleiste" off) makes the
|
||
*entire* tab bar disappear. First pass traced the CSS cascade and the commit that added the sixth
|
||
round's icon+label highlight and found it correctly scoped to `:not(.ohne)`; live DOM/computed-
|
||
style testing (toggle, click the label, navigate away/back, both container widths) checked
|
||
`#tabbar`'s and each `.tab`'s own box — all stayed visible, sized, and classed correctly — so the
|
||
bug looked unreproducible and was provisionally left alone.
|
||
Deployed to `audi_ha_test`; wheel-photo squareness, Montiert position, back-arrow color, and the
|
||
image-cycle dots all confirmed live via direct DOM/computed-style checks (no visual screenshot
|
||
tool available this session — see running note below).
|
||
Follow-up same day: user reported the `color-scheme` fix alone wasn't enough — dropdowns were
|
||
"still very bright." Correct: a `<select>`'s *opened* option list is rendered by the browser/OS
|
||
as a separate surface outside the page's render tree entirely — confirmed firsthand when a
|
||
screenshot tool call actually hung for 30s trying to capture it, and even a real click-to-open
|
||
attempt afterward produced no visible change in any screenshot. `color-scheme` only gets partial
|
||
credit there; the reliable lever is that Chrome/Firefox/Edge *do* honor explicit
|
||
`background-color`/`color` set directly on `<option>` elements for the popup rows. Added
|
||
`.feld select option{background-color:var(--canvas);color:var(--fg)}` — deliberately `--canvas`,
|
||
not `--tile-2` (translucent in the night theme, would let the browser's own light backdrop show
|
||
through). Verified via `getComputedStyle` on the actual `<option>` nodes
|
||
(`background-color: rgb(12,16,20)`, `color: rgb(255,255,255)` in night theme) since the popup
|
||
itself is provably outside what any screenshot in this tool can capture — user should confirm
|
||
visually on their own device.
|
||
Also: "Montiert" and the wheel photo were requested swapped — photo now sits on the tile's right
|
||
edge (`.bildbox.radbild{margin-left:auto}` inside its full-width wrapper), Montiert stays
|
||
`left`-anchored, unchanged from the seventh round.
|
||
**Found the real tab-bar bug right after, from the user's own screenshot** ("in this view i dont
|
||
see any menu items" — a live repro of icon-only mode, this time actually screenshotted since the
|
||
browser pane had recovered). The earlier live-testing pass checked `#tabbar` and `.tab`'s own
|
||
boxes and both looked fine — the actual defect was one level deeper and only visible by measuring
|
||
the icon itself: `.tabbar.ohne .tab span{display:none}` (in both stylesheets) was meant to hide
|
||
only the trailing label `<span>${t.label}</span>`, but the icon is *also* wrapped in a `<span
|
||
class="tabpille">` — a bare type selector `span` does not care about class, so the rule hid the
|
||
icon's wrapper too. `getBoundingClientRect()` on the `<svg>` read `{w:0,h:0}` and
|
||
`getComputedStyle(.tabpille).display` read `"none"` once actually checked — that was the missing
|
||
measurement in the first pass. This is exactly the reported "whole menu bar disappears": in icon-
|
||
only mode every tab lost both its label *and* its icon, leaving five empty buttons in an otherwise
|
||
correctly-visible bar. Fixed with `.tabbar.ohne .tab span:not(.tabpille){display:none}` in both
|
||
`audi-dashboard.css` and `audi-dashboard-ios.css` (the base file's copy was already shadowed by
|
||
the ios one at matching specificity, but fixed too rather than left as a landmine). Verified live
|
||
via screenshot: all 5 tab icons render correctly in icon-only mode, active tab still shows its
|
||
filled pill.
|
||
- [x] Eighth round the same day (2026-08-16) — Reifen tile relabel + reflow, per explicit request:
|
||
"Sommer"/"Winter" → **"Sommerräder"/"Winterräder"** (only the section headline text in
|
||
`vReifen()`'s `satz()`; the singular "Sommerrad"/"Winterrad" photo label and the `data-satz`
|
||
value compared against `CAR.reifen.aktiv` elsewhere were deliberately left untouched — the first
|
||
is grammatically correct as-is for a single wheel photo, the second is an internal state key, not
|
||
display text, and changing it would break the mount-toggle matching in the `[data-satz]` click
|
||
handler). That headline is now an actual **headline** — new `.tile-headline` class (20px,
|
||
`var(--fg)`, same typographic family as the page `.title` one step down) replacing the small,
|
||
muted `.label` styling every other in-tile section title uses.
|
||
Reflowed so the headline+km sit **above** "Montiert" instead of the wheel photo overlaying it:
|
||
new `.radkopf-row` (flex, `justify-content:space-between`) puts the headline/km block and the
|
||
wheel photo side by side at the tile's top, both top-aligned; `.montiert` dropped `position:
|
||
absolute` (no longer needs to float over the photo now that the photo isn't in that space) and
|
||
became a normal-flow pill with `margin-top` under the headline block — simpler than the previous
|
||
overlay hack, same 44×44pt invisible tap target via `::before`. `.bildbox.radbild`'s `margin-
|
||
left:auto` (added last round to right-align it under its old block-flow parent) was removed since
|
||
`.radkopf-row`'s flex layout does that job now. Deployed to `audi_ha_test`, confirmed live via
|
||
screenshot for both Sommerräder (mounted) and Winterräder (unmounted) tiles.
|
||
- [x] Ninth round the same day (2026-08-16) — "Mein Audi" image-cycle tap, still broken after the
|
||
seventh round's `bildWeiter()` crash fix: reproduced with a genuine mouse click (not a JS
|
||
`.click()` call) via screenshot and found the actual cause was one level upstream. This docker
|
||
test container has zero uploaded car photos, so every gallery image is in `bild-fehlt` state,
|
||
and `.bildbox.bild-fehlt img{display:none}` makes the `<img id="carimg">` itself un-hit-testable
|
||
— a real tap lands on the `.platzhalter` div instead, which the old handler
|
||
(`e.target.id === "carimg"`) never matched, so nothing happened. First fix wrapped the gallery
|
||
image in a `data-bildgalerie` container that opened the upload picker when empty and advanced
|
||
when a photo was loaded; the user then clarified uploads should stay exclusively in Einstellungen
|
||
("Bild der Übersicht") — Mein Audi should only ever cycle. Settled on: the wrapper stays (needed
|
||
so taps land on something clickable regardless of `bild-fehlt` state) but always calls
|
||
`bildWeiter()`, no upload branch. Verified live with a real click (screenshot → click → recheck
|
||
`.dots`): the active dot correctly advances, no console errors beyond this environment's
|
||
pre-existing service-worker/404 noise.
|
||
Also this round: replaced the "Farbskala Ladezustand (AGM, Ruhespannung)" text-only label above
|
||
the battery voltage color scale with a small battery icon + "Ruhespannung" (new `.bv-skala-kopf`
|
||
flex row, same geometry as the header's `.sync` icon+text pattern). First attempt hand-drew a
|
||
generic outline battery glyph since no "battery" icon exists in this repo's local CI subset
|
||
(`design/assets/icons/`); the user then supplied the real `battery-12v-l.svg`/`battery-12v-s.svg`
|
||
files from the actual Audi CI icon set (not something this session has fetch access to — asked
|
||
the user rather than guessing a URL or fabricating an icon and calling it authentic). Added both
|
||
as `CI.battery12vL`/`CI.battery12vS` (verbatim paths, `l` on the 48-grid, `s` on the 24-grid,
|
||
matching the existing `poi-l`/`poi-s` size-variant convention); only the `s` variant is used here
|
||
via `ciSVG()` since this is a small 15px inline label icon.
|
||
Deployed to `audi_ha_test`, confirmed live via screenshot.
|
||
Also this round (user then supplied the real `edit-l.svg`/`edit-s.svg` too): added a manual
|
||
km-correction control to the Reifen tiles, per explicit request — a small pencil icon (Audi CI
|
||
`edit-s`, new `CI.editS`/`CI.editL`) to the left of each Sommerräder/Winterräder km figure, "the
|
||
counter shall add continuously to these km even if the user changes the km". The tricky part
|
||
was persistence: `configCarZuProfil()` *deliberately* strips `km` before every profile write
|
||
(see its own comment - km is "vom Backend fortgeschrieben", a stale browser copy must never
|
||
clobber a since-elapsed automatic increment), so simply mutating `CAR.reifen[k].km` and calling
|
||
`profilSpeichern()` like every other Reifen field does would silently drop the edit. Added a
|
||
dedicated backend service instead, `audi_dashboard_reifen_km_setzen(satz, km)` in
|
||
`reifenzaehler.py`, that only overwrites the stored counter itself and leaves
|
||
`reifen.referenz_odo_km` (the last-processed odometer reference) untouched - the existing
|
||
`kilometerstand_geaendert()` trigger always reads the counter fresh off disk before adding the
|
||
next delta, so continuing to accumulate from the corrected value required no other backend
|
||
change at all. New `kmOffen` state (which satz's correction field is open, mirroring the
|
||
existing `nmOffen`/`bildMenuOffen` single-open-popup pattern) plus `data-kmauf`/
|
||
`data-kmspeichern` handlers, styled like the existing `.montiert`/`.zahnrad` controls (44×44pt
|
||
invisible tap target via `::before`). Verified end-to-end: edited Sommerräder from 150→4200 km,
|
||
confirmed the pyscript service ran via the container's HA log, then did a full page reload
|
||
(forces a real backend re-fetch, not just optimistic local UI state) and the corrected value
|
||
was still there.
|
||
- [x] Tenth round the same day (2026-08-16) — a real full Apple HIG audit, not just the sixth
|
||
round's 5-page pass: read Tab Bars, Sidebars, Sheets, Alerts, Action Sheets, Toolbars/
|
||
Navigation Bars, Buttons, Pickers, Loading, Feedback, Privacy, and Undo/Redo directly off
|
||
developer.apple.com and cross-checked each against the actual code (not just CSS) — most of
|
||
the app already held up (5-tab bar, destructive-button placement, 44pt tap targets,
|
||
`prefers-reduced-motion` support, confirm-sheet button ordering all already correct). Three
|
||
real findings surfaced, all fixed and user-approved via `AskUserQuestion` before implementing:
|
||
(1) **A second, larger silent-failure gap than the one already fixed for receipt upload.**
|
||
`profilSpeichern()` — the shared save path for roughly 25 editable fields across the app
|
||
(Anzugsmoment, service book, SmartDeal, tax, insurance, oil change, pause time, backup
|
||
interval, start image, ...) — was `async` with no `try`/`catch` at any of its ~25 call sites,
|
||
so a failed backend write was an unhandled promise rejection with zero user feedback; the
|
||
popup had already closed and the UI already updated optimistically, so the change looked
|
||
saved when it silently wasn't. Wrapped the one call inside `profilSpeichern()` itself in
|
||
`try`/`catch`, reusing the existing `hinweis()` error-sheet pattern — one central fix instead
|
||
of ~25 call-site patches. `serviceRufen()` (the separate wrapper used by km-correction, manual
|
||
trip/fuel entry, tire-set switching, deletions, restart, ...) got the identical treatment.
|
||
Verified live by monkey-patching `hass.callService` to reject inside the running app (via
|
||
`host.hass.callService = () => Promise.reject(...)`), then triggering a real Anzugsmoment save
|
||
through the actual UI: before the fix, the popup silently closed with a stale "Uncaught (in
|
||
promise)" console entry and no user-visible sign anything went wrong; after the fix, a
|
||
"Speichern fehlgeschlagen" sheet appears with the real error text. A follow-up full reload
|
||
confirmed the failed test write never reached the backend (value reverted to the real stored
|
||
120 Nm) — the fix only adds feedback, it doesn't change persistence behavior.
|
||
(2) **"Lädt …" bootstrap screen got a progress indicator** (new `.lade-spinner` CSS, a plain
|
||
rotating ring) instead of bare static text, per HIG Loading guidance to show something moving
|
||
while content loads — notable here because this exact screen has a documented history of
|
||
genuinely hanging, so a spinner also helps a user tell "still loading" apart from "stuck".
|
||
Respects the existing global `prefers-reduced-motion` override (`*, *::before, *::after`
|
||
block already forces `animation-duration:.01ms` app-wide) with no extra code.
|
||
(3) **Anzugsmoment and km-correction popups now use `.aktion.primaer` on Speichern**, matching
|
||
the Setup and SmartDeal popups, which already distinguished their primary action — these two
|
||
were the only popups in the app stacking two visually-identical plain buttons.
|
||
All three verified live in `audi_ha_test` (version `1786916894`): spinner CSS confirmed via
|
||
`getComputedStyle` (16×16px, `border-radius:50%`, `animation-name:lade-spin`) since a genuine
|
||
race-condition-timed screenshot proved too fast to catch reliably; Speichern/Abbrechen
|
||
hierarchy confirmed via screenshot on both popups; error-sheet fix confirmed via the
|
||
monkey-patch test above. Synced to `installationspaket/`.
|
||
- [x] Eleventh round (2026-08-17) — "why can't I pull to refresh on Übersicht", investigated as a
|
||
real bug report rather than assumed working: live-tested the gesture by dispatching real
|
||
`PointerEvent`s through the exact `pointerdown`/`pointermove`/`pointerup` handlers in
|
||
`ereignisseVerdrahten()` (the `ptrY0`/`ptrDrag` logic) — every step fired correctly (drag
|
||
threshold, "Loslassen zum Aktualisieren" label swap, spinner, `datenAktualisieren()` call,
|
||
collapse), so the app's own JS/CSS was never the defect. Root cause instead: `main#view` (the
|
||
actual scroll container) had no `overscroll-behavior` set at all, so on a **real** touchscreen
|
||
the browser's own native overscroll/pull gesture can intercept or compete with the custom drag
|
||
before it reaches the threshold — this can't reproduce via desktop testing or synthetic pointer
|
||
events, only real touch hardware, which is exactly why the live test above passed while the
|
||
user's real phone didn't work. Fixed with `overscroll-behavior-y: contain` on `main#view` plus
|
||
`overscroll-behavior: none` on the outer `:host > div` wrapper, so a gesture leaving `main#view`
|
||
never chains up into the underlying Home Assistant page either. Re-ran the same synthetic
|
||
pointer-event test after deploying — gesture still fires identically end-to-end, confirmed via
|
||
`getComputedStyle` that `overscroll-behavior-y` computes to `contain`; no visual regressions,
|
||
no new console errors. Synced to `installationspaket/`. Not yet confirmed fixed on the user's
|
||
actual device — the code-level fix is the standard remedy for this exact symptom class, but ask
|
||
if it's still reproducing before assuming this is fully closed.
|
||
- [x] Beleg-Upload: Ziehen und Einfügen (2026-08-17) — "Beleg hochladen" opened the native file
|
||
picker directly, which is the wrong affordance for both of the user's actual routes: at the
|
||
computer the PDF should be draggable in, on the phone it arrives as a PDF copied out of a mail
|
||
and needs pasting. Replaced the immediate picker with a small dialog (`belegPopup` state,
|
||
`vBelegPopup()`, rendered into the existing `#overlay` alongside `sheetMarkup()`/
|
||
`vSetupPopup()`): a dashed drop area plus "Aus Zwischenablage einfügen" and "Datei auswählen"
|
||
(the old path, kept as the third route). The upload itself was extracted out of the click
|
||
handler into `belegDateiVerarbeiten()` so all three sources share one code path — including the
|
||
deliberate non-`serviceRufen()` error handling whose reasoning is unchanged (see its comment).
|
||
Notes worth keeping: `dragover` **must** `preventDefault()` or the browser just opens the PDF
|
||
itself and no `drop` ever fires; the drag/drop/paste listeners hang on `document`, not on the
|
||
drop zone, because the zone lives in the shadow DOM and a drag crossing its edge would
|
||
otherwise be lost — and because `render()` replaces the overlay's `innerHTML`, so per-element
|
||
listeners wouldn't survive. Phone pasting uses `navigator.clipboard.read()` (iOS shows its own
|
||
paste prompt) with the keyboard `paste` event as the desktop path. Non-PDFs are rejected with
|
||
an in-dialog message instead of being uploaded. Verified live in `audi_ha_test` by
|
||
instrumenting `hass.callService` and driving real `DragEvent`/`ClipboardEvent`/`KeyboardEvent`s:
|
||
drop → zone highlights (`darueber`), dialog closes, service called with the right filename and
|
||
base64 payload; paste → same; a dropped PNG → "Das war kein PDF." and no service call; Escape →
|
||
closes. Synced to `installationspaket/`.
|
||
**Nachbesserung am selben Tag, nach Fehlermeldung des Nutzers** ("copy paste from pc brings
|
||
error no pdf but it is the latest bill") — zwei echte Fehler, beide bestätigt:
|
||
(1) `navigator.clipboard.read()` (der "Aus Zwischenablage einfügen"-Knopf) gibt laut
|
||
Spezifikation **nur Text, HTML und Bilder** heraus. Eine im Explorer/Finder kopierte PDF-Datei
|
||
taucht dort grundsätzlich nicht auf - der Knopf konnte am Rechner also gar nicht funktionieren
|
||
und behauptete dann fälschlich, es liege kein PDF vor. Der Weg, der dort funktioniert, ist
|
||
Strg+V: das `paste`-Ereignis trägt die Datei in `clipboardData.files`. Die Meldung nennt jetzt
|
||
genau das, statt die Datei zu beschuldigen; die Ablagefläche sagt es von vornherein
|
||
("oder mit Strg+V einfügen", per `tastenkuerzelEinfuegen()` auf Mac Cmd+V).
|
||
(2) Die Erkennung hing allein am MIME-Typ (`type === "application/pdf"`) - eine aus dem
|
||
Explorer kopierte oder gezogene Datei kommt je nach Browser und Quelle aber mit **leerem**
|
||
`type` an und wurde dadurch abgelehnt. Neue Hilfsfunktion `istPdf()` prüft zusätzlich die
|
||
Dateiendung; live nachgestellt mit genau diesem Fall (`new File([...], "Tankbeleg_August.pdf",
|
||
{})`, `type === ""`) - vorher abgelehnt, jetzt korrekt hochgeladen.
|
||
(3) Auf Rückfrage des Nutzers ("why is it that way? computer has copy paste function") noch
|
||
einen Schritt weiter: Der Einfügen-Knopf wird am Rechner jetzt **gar nicht mehr angeboten**
|
||
(`zwischenablageKnopfSinnvoll()`, Prüfung auf `(pointer: coarse)`), weil er dort prinzipiell
|
||
nicht funktionieren kann - ein Knopf, der zwangsläufig in eine Fehlermeldung läuft, ist
|
||
schlechter als keiner. "Datei auswählen" wird dort zur primären Aktion; auf dem Telefon bleibt
|
||
alles wie zuvor. Der Unterschied dahinter, kurz: Strg+V ist eine bewusste Nutzergeste an die
|
||
Seite, deshalb bekommt die Seite den vollen Inhalt inklusive Dateien - `clipboard.read()` ist
|
||
die Seite, die von sich aus die Zwischenablage ausliest, und das wird aus Datenschutzgründen
|
||
stark eingeschränkt (sonst könnte jede Seite still mitlesen, was zuletzt kopiert wurde).
|
||
Live geprüft: Desktop zeigt nur noch "Datei auswählen [primaer]"/"Abbrechen", Mobil (375px,
|
||
`pointer: coarse`) weiterhin alle drei Knöpfe.
|
||
(4) **Auch Strg+V half am Rechner nicht** - vom Nutzer getestet und zurückgemeldet ("ctrl+v
|
||
doesnt work either, pc doesnt know where to paste"). Stimmt: Chrome stellt eine Einfüge-Geste
|
||
nur zu, wenn es ein Einfüge-Ziel gibt (Eingabefeld/contenteditable), und das Fenster hat
|
||
keines. Der Hinweis ist deshalb überall entfernt; am Rechner heißt es jetzt schlicht "PDF
|
||
hierher ziehen / oder unten auswählen", am Telefon "Beleg einfügen / das aus der Mail
|
||
kopierte PDF". Der `paste`-Listener bleibt als stille Zugabe für Browser, die das Ereignis
|
||
doch zustellen, wird aber nirgends mehr beworben; `tastenkuerzelEinfuegen()` wurde damit
|
||
verwaist und ist entfernt. **Am Rechner also bewusst nur zwei Wege: Ziehen und Dateiauswahl.**
|
||
- [x] Preis je Liter überall auf 2 Nachkommastellen (2026-08-17, Nutzerwunsch: "€/l for gas is
|
||
common to be displayed as x,xx €/l") — alle sieben Stellen von `de(..., 3)` auf `de(..., 2)`
|
||
umgestellt: Jahres- und Monatsschnitt in der Tankliste, Zeilenwert je Tankvorgang, "Preis je
|
||
Liter" im Einzelbeleg, die Live-Vorschau in beiden Tankformularen (statisch und im
|
||
`data-tankpreisfeld`-Handler) sowie die CSV-Spalte "€/l". Im CSV kein Informationsverlust: der
|
||
Wert ist aus den ebenfalls exportierten Spalten Liter und Kosten jederzeit neu berechenbar.
|
||
`companion-app` (Nachfolger-App) brauchte keine Änderung - deren `eur()` rundet ohnehin schon
|
||
auf 2 Stellen. Live geprüft: keine `de(..., 3)`-Aufrufe mehr in der Datei, Liste und
|
||
Einzelbeleg zeigen "2,07 €/l", "1,99 €/l" usw.
|
||
- [x] Twelfth round (2026-08-17) — six user-reported items, one of them a real dead-end in the
|
||
bootstrap:
|
||
(1) **`HECKKLAPPENSCHLOSS_SENSOR` removed for good**, the same treatment Türschloss/
|
||
Haubenschloss got on 2026-08-16: gone from `einstellungen.py` (attribute deleted, not
|
||
blanked), from `entitaeten.py`'s Setup catalog, and from `_sicherheitscheck()` in
|
||
`frontend_veroeffentlichung.py`. `HECKKLAPPE_SENSOR` (the lid itself) stays. Verified live:
|
||
"Geprüfte Punkte" no longer lists it, the Setup catalog no longer offers it.
|
||
(2) **Bild-Platzhalter in Einstellungen war halb abgeschnitten** — reproduced by measurement,
|
||
not by eye: the placeholder rendered 260px tall inside a 132px `overflow:hidden` box. Cause:
|
||
`bildMitPlatzhalter()` puts the size class (`carfix`) on the box, the `<img>` **and** the
|
||
placeholder `<div>`, and the desktop container query said `.carfix,.bildbox.carfix
|
||
{height:260px}` — the bare selector therefore also hit the placeholder, whose `position:
|
||
absolute;inset:0` is over-constrained by an explicit height (the height wins, `bottom:0` is
|
||
ignored). Scoped both rules to `.bildbox.carfix`; image and placeholder fill the box via
|
||
their own `100%` anyway. Verified: placeholder now measures exactly the box height.
|
||
(3) **Inspektion bekommt eine "voraussichtlich am …"-Zeile** wie der Ölwechsel, aber auf
|
||
Wunsch nur mit dem Datum. `oelwechselPrognose()` wurde dafür auf einen gemeinsamen Kern
|
||
`servicePrognose(stichwort, km, monate)` zurückgeführt; `inspektionPrognose()` nutzt
|
||
dieselben 30.000 km / 24 Monate wie `intervalle()`/`termine()`. Angezeigt in der
|
||
Service-Kachel auf "Mein Audi" (auch ohne Fahrzeugmeldung, anders als beim Ölwechsel - dort
|
||
nennt `terminText()` den generischen Termin schon "voraussichtlich", die Zahl stünde sonst
|
||
doppelt) und auf der Service-Seite. Live geprüft: beide Stellen zeigen "voraussichtlich am
|
||
20.10.2027", Ölwechsel unverändert.
|
||
(4)+(5) **"Neue Fahrt" hat jetzt alle Felder der Einzelfahrt, und die Einzelfahrt lässt sich
|
||
bearbeiten.** Neues gemeinsames `fahrtFelder()`/`fahrtFormularWerte()` (Datum, Startzeit,
|
||
Dauer, Art, Start-/Zielort, Start-/Endkilometer, Distanz) nach demselben Muster wie
|
||
`tankFelder()`/`tankFormularWerte()`, benutzt vom Erfassungsformular und vom neuen
|
||
"Werte bearbeiten" auf der Einzelfahrt-Seite (`fahrtBearbeitenOffen`, Vorbild `vFill()`).
|
||
Backend: `audi_dashboard_fahrt_manuell_anlegen` um dieselben Felder erweitert, neuer Service
|
||
`audi_dashboard_fahrt_aktualisieren`. Zwei Punkte, die dabei zählen: von Hand gesetzte Felder
|
||
landen in `edited_fields`, damit das Kilometerstand-Screening (§7.2) sie nicht später
|
||
überschreibt - und weil `_datensatz_aktualisieren()` genau diese Liste schützt, hätte ein
|
||
zweites Bearbeiten sonst nichts mehr bewirkt; dafür gibt es jetzt `profil.fahrt_bearbeiten()`
|
||
(`schutz=False`), der Schutz gilt gegen die Automatik, nicht gegen den Menschen. Fehlt die
|
||
Distanz, ergibt sie sich aus den beiden Kilometerständen (`km_quelle: "manuell"`, Status
|
||
"vollständig"). Gleich mitgezogen: die Art-Pille auf der Einzelfahrt schaltete bisher nur
|
||
lokal um und war nach dem nächsten Backend-Push wieder weg - sie ruft jetzt denselben
|
||
Service. **Merkposten pyscript:** Generator-Ausdrücke gibt es dort nicht
|
||
(`NotImplementedError: not implemented ast ast_generatorexp`, an `next((f for f in …), None)`
|
||
real gesehen) - Listen-Abstraktionen dagegen schon. Live end-to-end geprüft: anlegen,
|
||
bearbeiten, erneut bearbeiten, jeweils in `fahrten.jsonl` nachgesehen und nach Reload wieder
|
||
im Formular.
|
||
(6) **Titelzeile stand auf dem Handy 34px rechts von den Kacheln.** Derselbe Befund wie im
|
||
Design-Audit 2026-08-13, dort aber nur in der Desktop-Container-Query behoben: `.back` ist
|
||
auf Wurzelseiten unsichtbar (`opacity:0`), belegt aber Breite plus Gap. Die Regel
|
||
`.back:not(.on){width:0;…}` gilt jetzt auf allen Breiten und hebt per `margin-right:-4px`
|
||
auch den Gap auf; `.topbar` bekam links 16px statt 20px, passend zu `--seitenrand` von
|
||
`main#view`. Gemessen: Titel und Kachel beginnen auf dem Handy beide bei 16px, auf dem
|
||
Desktop beide bei 564px (dort blieben vorher 4px Rest). Detailseiten (`.back.on`) unberührt.
|
||
- [x] Ladebildschirm: der Takt darf nicht abreißen (2026-08-17) — Nutzerbericht "nach dem Neuladen
|
||
dreht sich nur 'Lädt …', erst ein Klick auf einen Menüpunkt zeigt die App". Beim Lesen des
|
||
Bootstraps fanden sich zwei echte Sackgassen, die genau dieses Verhalten erklären:
|
||
`nachladeAnstossen()` stieg bei `!HASS || !HASS.callWS` **ohne** neuen Timer aus, und
|
||
`datenLaden()` bei `!HASS` ebenso - danach lief nichts mehr von selbst, und der einzige
|
||
Ausweg war ein zufälliger hass-Push oder ein Menüklick (`go()` ruft `datenLaden()` mit auf,
|
||
genau die Abmilderung vom 2026-08-16). Beide planen jetzt die nächste Runde, statt
|
||
auszusteigen; `disconnectedCallback()` stoppt den Timer dafür sauber, `connectedCallback()`
|
||
stößt ihn wieder an. **Nicht reproduziert:** weder ein einfacher Reload noch ein
|
||
Container-Neustart mit sofortigem Reload haben den Hänger in `audi_ha_test` gezeigt (die App
|
||
war jedes Mal binnen ~2s da) - die Sackgassen sind im Code belegt, dass sie *die* Ursache
|
||
beim Nutzer waren, ist damit aber nicht bewiesen. Tritt es erneut auf: Browser-Konsole
|
||
erfragen, bevor weiter geraten wird.
|
||
- [x] Thirteenth round (2026-08-17, same day) — five more user-reported items on the trip form and
|
||
one theme bug found while verifying it:
|
||
(1) **"Dauer" replaced by "Ankunftszeit"; duration is no longer editable.** `fahrtFelder()`
|
||
now has Startzeit + Ankunftszeit inputs and a plain (non-input) duration display next to
|
||
them, computed by `fahrtZeitraum()`/`dauerText()` and kept live via a new
|
||
`data-fahrtzeitfeld` input handler (same "patch the one node, don't call render()" pattern
|
||
the existing `data-tankpreisfeld` handler uses, for the same reason: a full re-render would
|
||
steal focus from the field being typed in). An arrival time not later than the start time is
|
||
treated as crossing midnight (`+1 Tag`), matching how `_fahrt_beenden()` already handles a
|
||
real overnight trip. `fahrtFormularWerte()` derives `ts_start`/`ts_end` from the same
|
||
calculation - the backend never sees a separate duration field, so it can't disagree with
|
||
the times. Verified live: 09:00→10:45 shows "1 Std. 45 Min.", 23:30→00:15 shows "45 Min."
|
||
(crossed midnight correctly), and editing an existing trip's arrival time on the Einzelfahrt
|
||
page persisted the recalculated `duration_s` (7200s for a 10:00→12:00 edit, checked in
|
||
`fahrten.jsonl`).
|
||
(2) **Art values now display capitalized** ("Privat"/"Arbeitsweg") wherever shown to the user
|
||
- the trip-edit `<select>`, the trip-list row, and the Einzelfahrt pill - via a new
|
||
`artText()` helper; the stored value stays lowercase (`"privat"`/`"arbeitsweg"`), since it's
|
||
compared in several places (the pill, the "Arbeitsweg" statistic, the CSV export) and already
|
||
exists that way in every trip recorded so far. The CSV export itself was deliberately left
|
||
showing the raw value - a data file, not UI copy.
|
||
(3) **Arbeitsweg pill no longer red.** `.pill.work` (`color:var(--red)` / a translucent red
|
||
fill in the iOS overlay) is gone from both stylesheets along with the now-unused `work` class
|
||
on the button; the pill uses the same neutral styling regardless of Art, matching the request
|
||
that Arbeitsweg should look like Privat, not draw attention to itself.
|
||
(4) **"tippen zum Umschalten" hint removed** from under the Einzelfahrt pill, per explicit
|
||
request - the pill is still clickable, just without the caption.
|
||
(5) **Found while re-verifying the placeholder fix from round twelve: it broke daylight mode.**
|
||
`.bildbox.bild-fehlt .platzhalter{background:var(--tile)}` (added round twelve) reads fine in
|
||
Nacht theme, where `--tile` is a faint white overlay on a near-black canvas - but the iOS
|
||
overlay's Tag-theme block redefines `--tile:#FFFFFF`, identical to the canvas/most tiles
|
||
around it, so the placeholder box became invisible in daylight mode (measured live:
|
||
`background-color: rgb(255,255,255)` on both the placeholder and its surroundings). Switched
|
||
to `--ios-fill` (`rgba(120,120,128,.16)` in Tag theme, `.24` in Nacht) - the same token
|
||
`.feld select`/`.aktion` already use to mark "this is a surface", visibly grey in both themes.
|
||
Verified live in Tag theme: `background-color: rgba(120,120,128,.16)`, clearly a distinct box
|
||
against the white canvas (see screenshot taken during verification).
|
||
All five deployed to `audi_ha_test` (version `1787009000`) and synced to `installationspaket/`;
|
||
console showed only the pre-existing service-worker/404 noise from the missing vehicle photos,
|
||
nothing new.
|
||
- [x] 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: `.row` ist ein Flex mit `dt`/`dd`, und `dd` bekommt 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`) neben `dt`/`dd` und umbricht per `flex: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. Das `flex-wrap:wrap` ist 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 einem `hatMeldung`-Zweig für den Wert plus
|
||
einem separat berechneten `fussnote`-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 `.row` auf "Mein Audi" weiterhin zweispaltig; keine neuen
|
||
Konsolenfehler. Deployed als Version `1787003000`, synchron in `installationspaket/`.
|
||
- [x] **Gap found while auditing "what's needed for a real iPhone app" (2026-08-17), then closed
|
||
the same day:** `companion-app/src/screens/` hadn't been touched since 2026-08-11 — every
|
||
panel fix and feature from the six days since (HIG audit rounds, receipt upload, trip
|
||
editing, Inspektion forecast, ...) existed only in `homeassistant/www/audi-dashboard-app.js`.
|
||
User decision: port everything now, before any native build work (Android build itself
|
||
deprioritized/uncertain per the user - Paul has an Apple Developer Program and will help with
|
||
iOS signing). Audited both codebases field-by-field (not by diffing the panel's CSS/markup,
|
||
which doesn't transfer to `@audi-dash/ui` components) and found six real, mechanically
|
||
distinct gaps, all fixed:
|
||
1. **`AGM_RUHE_MAX_V` was still 13.2** in `Batterie.tsx` - lowered to 12.8, matching the
|
||
panel's 2026-08-16 fix (same rationale: above that is already alternator voltage).
|
||
2. **Dead `wlan_name` field** - `profilAdapter.ts` still read/wrote it and `Einstellungen.tsx`
|
||
showed it as an editable "WLAN des Fahrzeugs" field, even though WLAN-based trip detection
|
||
was fully removed from the backend on 2026-08-12 (replaced by FMM003 ignition). Editing it
|
||
had zero effect. Removed from the type, both directions of the adapter, the settings
|
||
screen, and both test fixtures.
|
||
3. **No Inspektion forecast** - `service.ts` only had `oelwechselPrognose()`. Refactored the
|
||
shared math into a `servicePrognose()` core (kept `oelwechselPrognose()`'s public
|
||
signature/behavior identical - all 9 existing tests still pass unchanged) and added
|
||
`inspektionPrognose()` (fixed 30.000 km / 24 Monate, no customizable interval like the oil
|
||
change has), wired into `Service.tsx` as a new "Eigene Prognose (Inspektion)" row. 3 new
|
||
tests.
|
||
4. **Arbeitsweg pill was red** - `Pill variant="work"` in `Fahrten.tsx`/`FahrtDetail.tsx`;
|
||
`@audi-dash/ui`'s own source comment says the variant literally "recolors border+text to
|
||
the accent red". Stopped passing the variant in both spots - same fix as the panel, no
|
||
change needed to the shared design-system component itself.
|
||
5. **Trip creation only took Beginn/Ende/Art** - the panel's fuller field set (Startort,
|
||
Zielort, Start-/Endkilometer, Distanz) didn't exist here. New shared
|
||
`src/screens/FahrtFelder.tsx` (type, defaults, backend-payload conversion, and the
|
||
presentational field group) used by both the create form (`Fahrten.tsx`) and a **new**
|
||
edit capability.
|
||
6. **`FahrtDetail.tsx` was entirely read-only** - no edit, no Art toggle after creation. Added
|
||
a "Bearbeiten" toggle reusing `FahrtFelder.tsx`, backed by a new `DataMetricApi
|
||
.fahrtAktualisieren()` (mirrors the existing `tankvorgangAktualisieren()` pattern) calling
|
||
the backend's `audi_dashboard_fahrt_aktualisieren` service (built this session on the panel
|
||
side, so the backend contract already existed and needed no changes).
|
||
**Explicitly investigated and found NOT to apply** (worth recording so it isn't re-attempted):
|
||
price-per-litre rounding (companion-app's `eur()` already always renders 2 decimals - the
|
||
3-decimal bug the panel had never existed here); pull-to-refresh `overscroll-behavior` (no
|
||
native pull gesture implementation here to fix); the panel's drag/paste/file-picker receipt
|
||
dialog (solves a **desktop browser clipboard** problem that doesn't exist in a Capacitor
|
||
native app - the plain `<input type=file>` already in `TankDetail.tsx` gets iOS's native
|
||
Files/Photos picker for free, which already covers "a PDF saved from Mail"); every
|
||
Setup-menu/entity-mapping feature (HA-panel-only concept, the companion app consumes the
|
||
backend's already-resolved published state and has no equivalent screen, nor should it);
|
||
the many purely cosmetic HIG-audit CSS fixes (icon fills, chevrons, tab-bar highlight
|
||
bugs, ...) - those target the panel's own hand-written markup/CSS, not
|
||
`@audi-dash/ui`'s components, and don't transfer.
|
||
Verified after every change: `npm run typecheck` (clean throughout), `npm run test` (95/95
|
||
passing, up from 90 - added interaction tests for the new create-form fields and the
|
||
edit-toggle pre-fill, plus 3 for `inspektionPrognose()`), `npm run build` (succeeds). Also
|
||
corrected a stale README claim ("Capacitor-Hülle und sichere Ablage" both listed as open) -
|
||
the secure-storage adapter is actually fully wired (`ablageNativ.ts`, activated from
|
||
`main.tsx`) and only the native `ios/`/`android/` platform folders themselves (deliberately
|
||
gitignored, regenerated via `npx cap add`) are the missing piece, and that step needs a Mac.
|
||
- [x] Übersicht: Kilometerstand-/Service-Kacheln neu gestaltet (2026-08-17) — Nutzer fragte, wie
|
||
Apple HIG und die Audi-CI das lösen würden. Befund: die Ölwechsel-Kachel presste zwei
|
||
verschiedene Größen in **ein** Zahlenfeld (`teile.join("/ ")` → `9,0 tkm/ 04/27`) — zwei
|
||
Einheiten, davon eine erfundene (**„tkm" schreibt niemand**, und die Mein-Audi-Servicebox
|
||
nannte dieselbe Zahl schon immer „9.029 km", d. h. dieselbe Tatsache war an zwei Stellen
|
||
unterschiedlich formatiert), zwei Schrägstriche mit zwei Bedeutungen, und auf 375 px
|
||
zweizeilig umgebrochen. Beide Regelwerke laufen hier auf dieselbe Auflösung hinaus, live
|
||
nachgelesen: Apple HIG *Layout* („Make essential information easy to find by giving it
|
||
sufficient space… don't obscure it by crowding it with nonessential details") und
|
||
*Typography* („Adjust font weight, size, and color… to help people visualize hierarchy");
|
||
die Audi-CI definiert `.fig` (Audi Type Wide 300) ausdrücklich als *„large numeric
|
||
figures"* — **eine** Zahl, nicht zwei. Umgesetzt: Wert und Kontext getrennt
|
||
(`9.029 km` groß, `vsl. 04/2027` als leisere Zweitzeile), echte Einheit statt „tkm", und
|
||
die Kachel ist jetzt ein `button.tile.tilebtn` mit `.go`-Chevron nach `service:x` — vorher
|
||
eine Sackgasse, obwohl die Service-Seite existiert. `mmjj()` wurde dadurch verwaist (nur
|
||
noch das volle Jahr wird gebraucht) und ist durch `mmjjjj()` ersetzt. Die HU-/Inspektions-
|
||
Zweige sind mit dem Ölwechsel-Zweig zusammengefallen, weil sie nach der Trennung dieselbe
|
||
Form haben. Live geprüft (Tag und Nacht, 375 px): beide Kacheln einzeilig, Tippen öffnet
|
||
Service mit aktivem Zurück-Pfeil.
|
||
**companion-app hatte diesen Fehler nicht** — dort standen die Termine schon als
|
||
Label/Wert/Zusatz-Zeilen. Stattdessen dort der benachbarte Gruppierungsfehler behoben: die
|
||
zwei Service-Zeilen hingen *innerhalb* der Kilometerstand-Kachel (zwei Termine unter einer
|
||
Überschrift, die nichts mit ihnen zu tun hat) und führten nirgendwohin. Jetzt eigene,
|
||
antippbare `Tile variant="button" chevron` → `service`. Geprüft: typecheck sauber, 95/95
|
||
Tests, Build erfolgreich.
|
||
- [x] Ü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 `.tile` mit 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 samt `ART_BIS`-Zuordnung für den richtigen Artikel („bis zu**m** Ölwechsel“, aber
|
||
„bis zu**r** 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 Version `1787007000` (zwischenzeitlich auf Nutzerwunsch von
|
||
`.tile.flat` auf eine normale `.tile` mit Hintergrund umgestellt — `.serviceblock:active
|
||
{opacity:.55}` als Tipp-Rückmeldung ergänzt, da der übliche `.tilebtn`-Hintergrundwechsel
|
||
hier nicht geht), synchron in `installationspaket/`. Entwurf und Begründung als Artefakt:
|
||
„Zwei Kacheln, drei Antworten“.
|
||
- [x] 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, aus
|
||
`C:\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 bekam `CI.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 + `.fig` in `vHome()`'s Serviceblock; die Nebenzeile zeigt jetzt nur noch
|
||
`vsl. DATUM`. Companion-app bekam die identischen Pfade als neue `SymbolOelwechsel`/
|
||
`SymbolInspektion`-Komponenten in `symbole.tsx` (eigener `RahmenFlaeche`-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 in `screens.css`; `Uebersicht.tsx`
|
||
wählt das Symbol über eine kleine `SERVICE_SYMBOL`-Tabelle nach `service.art`. **Wichtig:** die
|
||
Icons blieben bewusst außerhalb von `design-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 gemeinsamen `Fig`-Komponente. Die
|
||
Hauptuntersuchung hat kein eigenes Icon (kein Restkilometer-Ziel, `restKm` ist dort immer
|
||
`null`) und behält `bisText()` 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 build` sauber. Live im Docker-Testcontainer geprüft (Version
|
||
`1787012001`, synchron in `installationspaket/`): Ölwechsel-Icon rendert links der "8.999 km"-
|
||
Kennzahl, Unterzeile zeigt nur noch "vsl. 18.04.2027", keine neuen Konsolenfehler.
|
||
- [x] 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. `zahlGroesse` ist
|
||
jetzt fest 40 (Panel `vHome()` und companion-app `Uebersicht.tsx`, `Fig size={40}`
|
||
unabhängig von `restKm`). Da `bisText()`/`ART_BIS` (Panel) und `bisText()`/`BIS_TEXT`
|
||
(companion-app `service.ts`) dadurch ihre einzige Aufrufstelle verloren, wurden beide als
|
||
Orphans entfernt (inkl. des zugehörigen companion-app-Tests in `service.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 build` in companion-app sauber. Deployed als Panel-Version `1787012002`,
|
||
synchron in `installationspaket/`.
|
||
- [x] "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 — per `getBBox()` 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-chip` für einen vereinbarten Werkstatttermin, optionale `.tg-vorgabe`-Zeile, `.tg-wert`
|
||
als 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) von
|
||
`vHome()` 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 nutzt `var(--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 Version `1787013000`, synchron in
|
||
`installationspaket/`, 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 gemeinsamen
|
||
`Werteliste`; jetzt "Anstehende Termine" wie im Panel, mit denselben drei `.dm-termingruppe`-
|
||
Blöcken. Neue `SymbolOelwechsel`/`SymbolInspektion`-Komponenten in `symbole.tsx` (bereits für
|
||
die Übersicht angelegt, hier wiederverwendet). Neuer Export `letzterInspektion()` in
|
||
`service.ts` (Gegenstück zu `letzterOelwechsel()`) 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 keine `vereinbart`-Datenfelder für Service-Termine — der Werkstatt-
|
||
Chip entfällt deshalb ersatzlos, es gibt nichts anzuzeigen. (2) companion-app hat kein
|
||
Gegenstück zu `intervalle()`/dem generischen Herstellerintervall-Fallback des Panels; die
|
||
Herstellervorgabe-Zeile wird stattdessen direkt aus `fahrzeug.oel.modus` abgeleitet (Ölwechsel:
|
||
nur bei `modus === "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 typecheck` sauber, `npm run test` 99/99, `npm run build` erfolgreich; 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 an `iv.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 auf `modus: "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, sobald `CAR.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-Version
|
||
`1787013001`, synchron in `installationspaket/`, live per DOM-Abfrage geprüft: Ölwechsel-Block
|
||
zeigt jetzt "Herstellervorgabe", keine neuen Konsolenfehler. companion-app: `npm run typecheck`
|
||
sauber, `npm run test` 99/99, `npm run build` erfolgreich.
|
||
- [x] **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 nach
|
||
`companion-app/` portiert (siehe `Uebersicht.tsx`): `naechsterService()` in `daten/service.ts`
|
||
neu — Gegenstück zu `naechsterTermin()` im Panel, wählt über Ölwechsel-/Inspektions-Prognose
|
||
und die von Hand gepflegte Hauptuntersuchung hinweg den zeitlich nächsten Termin, `bisText()`
|
||
liefert denselben Artikel wie `ART_BIS` im 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 typecheck` blieb sauber, `npm run test` auf
|
||
100/100 (von 95 — vier neue Tests für `naechsterService()`/`bisText()` in `service.test.ts`,
|
||
einer in `screens.test.tsx`, der mit einem `vi.fn()` als `geheZu` wirklich belegt, dass ein
|
||
Klick auf den Serviceblock navigiert und ein Klick auf die Kilometerstand-Zeile es nicht tut
|
||
— dafür musste `zeige()` in `screens.test.tsx` erst einen injizierbaren `geheZu`-Parameter
|
||
bekommen, vorher war er hart auf `() => {}` verdrahtet), `npm run build` erfolgreich.
|
||
- [ ] Fix remaining documentation drift (statistics claim, README gaps, obsolete TODO comment) —
|
||
text-only changes; INSTALL.md's WLAN/TommiG1 drift and stale variable names were fixed
|
||
2026-08-12 (see section B); `DESIGN_REVIEW_2026-08-13.md` and `REVIEW_main_2026-08-13.md`
|
||
list further drift not yet fixed (WLAN references still in `SPECIFICATION.md`,
|
||
`homeassistant/README.md`, the profile template)
|
||
- [x] Harden `profil_lesen()` against missing/corrupt `fahrzeugprofil.json` — done on the
|
||
`umsetzung-datametric360` branch (2026-08-11), brought over by the 2026-08-13 merge. Returns
|
||
`None` on a missing or malformed file instead of raising; all 5 call sites
|
||
(`backup.py` ×2, `fahrterkennung.py`, `frontend_veroeffentlichung.py`, `reifenzaehler.py`,
|
||
`tankerkennung.py`) already guard for `None` — verified by grep across the merged tree, no
|
||
caller left unguarded (this was flagged as the merge's main risk in `REVIEW_main_2026-08-13.md`
|
||
§"Was den Branch betrifft").
|
||
- [x] Decide whether the 3 audit leftovers get fixed here or only in DataMetric360 — decided
|
||
2026-08-11 (`UMSETZUNGSPLAN.md` Phase 2): swipe-delete keyboard fallback, popup keyboard
|
||
access, and self-hosting Leaflet stay unfixed in this panel and are addressed only in
|
||
DataMetric360.
|
||
- [ ] Optional: persist the RAM-only states (trip start, fuel low-water-mark) — deliberately
|
||
deferred; may become moot with the FMM003 switch
|
||
- [ ] Upload vehicle photos to `www/bilder/`, set `steuer.faellig` (operational data, not code)
|
||
|
||
### D) Once the panel is superseded
|
||
|
||
- [ ] **Archive** `audi-dashboard-app.js`, don't delete (settled decision, architecture §1)
|
||
|
||
### 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 `.notiz` CSS class
|
||
pattern used elsewhere (see `feld.notiz` in `audi-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.jsonl` and `fahrzeugprofil.json` under `/config/audi_dashboard/` have no purge
|
||
path anywhere in `profil.py` — nothing trims them, ever. That was already true before this change.
|
||
- **HA's raw sensor history did not.** `recorder.purge_keep_days` defaults to **10 days**, and the
|
||
test instance had no `recorder:` 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()` in `fahrterkennung.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: the pyscript backend became a real integration (2026-08-23)
|
||
|
||
Owner asked for it directly ("build the hacs") after deferring it earlier the same day. The
|
||
deferral reasoning still held — converting first means debugging the rewrite and the real
|
||
environment at once — but the owner overrode it, so the conversion happened.
|
||
|
||
**What was actually blocking HACS.** Two independent things, both checked against the installed
|
||
HACS 2.0.5 rather than from memory:
|
||
|
||
- **No HACS category installs to `/config/pyscript/`.** The six categories resolve to
|
||
`custom_components/<domain>/`, `www/community/<name>/`, `python_scripts/`, `themes/`,
|
||
`custom_templates/` and `appdaemon/apps/<name>/` (verified in `repositories/*.py` → `localpath`).
|
||
The app needed `/config/pyscript/` **and** `/config/audi_dashboard/` **and** `configuration.yaml`
|
||
entries. This is what the conversion solved.
|
||
- **HACS is GitHub-only, and does not work here — permanently, not just today.** `github.com`/
|
||
`api.github.com` are hardcoded throughout; the repo lives on `gitea.nothaft.cloud`. Briefly
|
||
mirrored to `github.com/T130B/DM360` on 2026-08-24, but that changes nothing: **HACS
|
||
categorically refuses private repositories**, confirmed against its own docs
|
||
(hacs.xyz/docs/faq/private_repositories: "Private GitHub repositories can not be used with HACS
|
||
at all... HACS can only get publicly available information") — no token, no signed-in-account
|
||
exception. An earlier note in this file claimed the opposite ("HACS must be signed in with the
|
||
account that owns it"); that was wrong and is corrected here. The repo is **private on purpose**
|
||
(it carries the Audi typeface and the model badges, licensed for this one private install only),
|
||
so making it public to satisfy HACS is not on the table — and since the mirror bought nothing
|
||
(HACS still can't see a private repo, mirrored or not), the owner decommissioned it the same day.
|
||
**Development is Gitea-only from here on; there is no GitHub presence.** `hacs.json` was removed
|
||
(dead weight with no GitHub target to serve), and `manifest.json`'s `documentation`/
|
||
`issue_tracker`/`codeowners` point at the Gitea repo instead.
|
||
`homeassistant/installationspaket/install.ps1` is not a fallback — it is **the only installation
|
||
path**, and the docs (`README.md`, `homeassistant/INSTALL.md`, `installationspaket/ANLEITUNG.md`,
|
||
`VERSIONIERUNG.md`, the script's own header) say so. Don't reintroduce a GitHub mirror or
|
||
`hacs.json` without the owner raising it again — the private/public constraint that killed this
|
||
path hasn't changed and isn't expected to.
|
||
|
||
**The shape.** `custom_components/audi_dashboard/` at repo root — not for HACS's benefit (see
|
||
above, HACS is off the table), just where `install.ps1` and Home Assistant itself expect a
|
||
`custom_components` entry to live. Config flow, one
|
||
`sensor` platform, 18 services, ~2,900 lines of pyscript ported to ~2,400 lines of integration.
|
||
`koordinator.py` holds the runtime state and does the wiring that `@service`/`@state_trigger`/
|
||
`@time_trigger` used to do implicitly; the domain modules keep their old names and structure so the
|
||
diff stays readable. `modules/` was flattened (it only existed because pyscript treats that folder
|
||
specially). Three renames earned their keep: `profil.py` → `ablage.py` (it handles trips and
|
||
fill-ups too, the old name was actively wrong), `entitaeten.py` → `zuordnung.py` (avoids collision
|
||
with HA "entities"), `frontend_veroeffentlichung.py` → `veroeffentlichung.py`.
|
||
|
||
**Five install steps disappeared, and none of them by hand-waving:**
|
||
|
||
| Was | Now |
|
||
|---|---|
|
||
| `pyscript:` block with `allow_all_imports` + `hass_is_global` | gone — no YAML at all |
|
||
| `panel_custom:` block | the integration registers its own panel |
|
||
| copy the frontend to `/config/www/` | served from `frontend/` under `/audi_dashboard_static/` |
|
||
| long-lived token in `audi_dashboard/ha_token.txt` | recorder API read directly (`verlauf.py`) |
|
||
| `pip install pypdf` in the container | `requirements` in `manifest.json`, HA installs it |
|
||
| copy `fahrzeugprofil.example.json` by hand | seeded from `vorlage/` on first setup |
|
||
|
||
Verified on a genuinely fresh throwaway container, not reasoned about: 12 entities, zero trips,
|
||
zero fill-ups, zero battery days, profile created from the template, and **pypdf 6.16.2 installed
|
||
by HA itself**.
|
||
|
||
**The naming contract changed, and both frontends had to follow.** `pyscript.audi_dashboard_x` →
|
||
`sensor.audi_dashboard_x`, `pyscript.audi_dashboard_y` → `audi_dashboard.y`. The prefix only ever
|
||
existed because pyscript owned the domain. Both frontends now carry the names in one table each
|
||
(`const.py` ↔ `frontend/audi-dashboard-app.js` `E`/`DOMAIN` ↔ `companion-app/src/api/types.ts`
|
||
`ENTITAETEN`/`DIENST_DOMAIN`). **A companion app from before this change finds nothing after the
|
||
upgrade** — it must be rebuilt in the same step. The panel can't drift: it ships inside the
|
||
integration.
|
||
|
||
**Three long-standing weaknesses fixed, because the conversion made them cheap:**
|
||
|
||
1. **The payload no longer hits the recorder database.** `_unrecorded_attributes = {"daten"}` on the
|
||
entity. Only a real entity belonging to an integration can do this — pyscript's bare `state.set()`
|
||
can't, so every publish wrote the complete trip archive to the DB, every 60 seconds. Confirmed by
|
||
querying the history API: only `icon` and `friendly_name` come back. Note the mechanism moved:
|
||
exclusion now comes from `state_info["unrecorded_attributes"]` on the Entity, **not** from a
|
||
per-domain `recorder.py` platform hook (checked in `db_schema.py` of the installed 2026.8.0).
|
||
2. **A running trip survives a restart.** `fahrt_start_ts` and the tank low-water mark live in a
|
||
`Store` instead of only in memory. This was a documented data-loss bug.
|
||
3. **Trigger rebinding is live.** `@state_trigger` baked the entity ID in at module load, so
|
||
changing ZUENDUNG/KM/TANK_SENSOR needed an HA restart — the setup dialog had a warning and a
|
||
restart button for exactly that. `trigger_neu_binden()` replaces all three; the warning, the
|
||
button and the `neustart` service are gone.
|
||
|
||
**Fixing (2) exposed a new failure mode, and it was found by testing rather than reasoning.** If the
|
||
ignition goes off *while HA is down*, nobody sees the transition — the resumed trip would stay open
|
||
forever and the next real trip would be appended to its start. `nach_neustart_fortsetzen()` closes
|
||
it at the last recorded ignition timestamp (not "now" — HA may have been down for days), and runs
|
||
via `async_at_started` so a not-yet-restored source can't be mistaken for "off". Verified live: an
|
||
orphaned trip was closed at 21:12:43, the last value the recorder held.
|
||
|
||
**The self-updater is gone.** `updateverwaltung.py` cloned a git repo into a staging folder and
|
||
copied `pyscript/` and `www/` over the running code — necessary only as long as the app had no
|
||
install shape Home Assistant knows. HACS is that shape: it checks, downloads, installs, shows a
|
||
changelog and can roll back. A second, homemade update mechanism beside it is more attack surface
|
||
than value. The Settings tile now shows the installed version and points at HACS.
|
||
|
||
**Versioning collapsed to one number.** `manifest.json` `version` is the single source: HA lists it,
|
||
HACS compares it, the integration publishes it as `sensor.audi_dashboard_app_version`, it hangs on
|
||
the panel URL as `?v=…`, and vite reads it for `__APP_VERSION__` (hard-failing if absent). The
|
||
`VERSION` file and the Unix-seconds cache-buster in `audi-dashboard-version.json` are both gone —
|
||
two sources for one fact would have drifted, and the comparison would have gone silently wrong. The
|
||
new cache breaker is strictly better: it changes only on real changes. `VERSIONIERUNG.md` rewritten.
|
||
|
||
**Installer got smaller and much safer.** It copies one folder and touches nothing else. The
|
||
`configuration.yaml` manipulation — the one way an installer can stop HA from starting, and the
|
||
reason the old script needed timestamped backups, a conflict refusal and a read-back rollback — is
|
||
gone with the YAML it used to write. What remains: refuses to delete a folder whose `manifest.json`
|
||
doesn't claim `domain: audi_dashboard`, reads the manifest back after copying. Tested on a mock
|
||
config dir: dry run, first install, update run, and the refusal path.
|
||
|
||
**Removed from the repo** (recoverable via `git log`): `homeassistant/pyscript/`,
|
||
`homeassistant/www/audi-dashboard-*` + `badges/`, `homeassistant/data/`, `homeassistant/install.sh`,
|
||
`homeassistant/update.ps1`, the `installationspaket/` copy of everything (the duplicate-and-sync
|
||
arrangement is gone — the installer reads the real folder), `configuration_snippet.yaml` (nothing
|
||
left to add), `ANLEITUNG.pdf` (stale and not regenerable here; the `.md` beside it is current).
|
||
`data/tests/` → `tests/belegparser/`, pointing at the integration's parser copy; 8/8 still pass.
|
||
|
||
**Do not resurrect the pyscript tree.** If something is missing, port it — running both backends
|
||
writes two sets of trips into the same files.
|
||
|
||
**Don't re-litigate HACS.** Confirmed dead-end, not a gap — see the H) intro above. Nothing about
|
||
Gitea-vs-mirrored-GitHub changes it: HACS refuses private repos regardless of where they're
|
||
mirrored, and the private/public constraint (licensed Audi assets in the repo) isn't going away.
|
||
`install.ps1` is the permanent, only install path, not a stand-in.
|
||
|
||
**Stale HACS claims found and fixed in code comments/UI text (2026-08-24).** The docs-level cleanup
|
||
above happened the same day as this section was written, but a full-repo grep afterward (prompted by
|
||
the owner spotting live UI text still claiming "Updates laufen über HACS") turned up leftovers the
|
||
first pass missed, because it focused on the standalone docs (`README.md`, `INSTALL.md`, etc.) and
|
||
not source comments or UI strings. Fixed: the panel's Einstellungen → Version tile literally told the
|
||
user HACS would notice and install updates itself — replaced with the actual mechanism
|
||
(`install.ps1`, panel ships inside the integration and can't go stale on its own). Also fixed,
|
||
all now pointing at `install.ps1`/"jeder Auslieferung" instead of "HACS"/"HACS-Update": comments in
|
||
`audi-dashboard-app.js` (the version-tracking block and the retired-updater history note),
|
||
`companion-app/vite.config.ts` (manifest-version header comment), `const.py` (`STATIK_URL`,
|
||
`BUENDEL_ORDNER`, `BILDER_ORDNER` comments), `bilder.py` (module docstring), `__init__.py`
|
||
(`cache_headers=False` rationale), and a diagram branch in `VERSIONIERUNG.md` that stated "HACS
|
||
vergleicht sie gegen das Repository und meldet Updates" as flat fact, contradicting the boxed note
|
||
one screen above it. Left alone: `install.ps1`'s own header (already correctly explains the
|
||
exclusion), `ota-paket.ps1`'s comment (already correct), and one debugging note in
|
||
`audi-dashboard-app.js` (~line 4720) that recalls what was actually running in a specific test
|
||
container during a past incident — a historical fact about that container, not a claim about how
|
||
this project works, so not in scope for this cleanup. Also out of scope, a different topic entirely:
|
||
mentions of the *retired vehicle-data-source* HACS integration `TommiG1/HA_VAG-EU-Data-Act` in older
|
||
planning docs (`SPECIFICATION.md`, `COMPANION_APP_ARCHITECTURE.md`, `bauauftrag.md/html`,
|
||
`testumgebung/`) — that HACS integration is unrelated to whether HACS can install *this* app; it was
|
||
FMM003's predecessor as a data source, already documented as retired elsewhere in those same files.
|
||
Manifest bumped to `2026.8.24.3` and `npm run ota` rerun (the panel text change is user-visible).
|
||
|
||
### The deployed-parity problem, and what was built for it (2026-08-23)
|
||
|
||
The parity rule (binding, above) guarantees *source* parity: both codebases change in the same
|
||
session. It guarantees nothing about what is *running*. After the conversion the two sides are
|
||
wired differently in a new way:
|
||
|
||
- the panel ships **inside** the integration — one `install.ps1` run (the only delivery path — HACS
|
||
is a dead end here, see section H) moves backend and panel together, and it cannot go stale at all
|
||
- the companion app is a Capacitor shell with `webDir: "dist"` and **bundled** assets, sideloaded
|
||
through Xcode. It stays on whatever was bundled at signing time, silently, indefinitely
|
||
|
||
The version comparison above is what makes that visible: the app compares its compiled-in stamp
|
||
against `sensor.audi_dashboard_app_version` and says so in the existing `Hinweisleiste`, whose
|
||
stated principle is already "nie eine stille Veraltung".
|
||
|
||
Deliberate choices worth keeping: the comparison is **equality only**, never greater/less — the
|
||
version is an identifier, not a number, and ordering it would be false precision that breaks
|
||
silently on a format change. Either side missing yields `"unbekannt"` and **no** warning, so an
|
||
older backend or an offline start cannot produce a false alarm. And the vite build **fails hard** if
|
||
the manifest version is absent rather than emitting an app that cannot detect its own staleness.
|
||
|
||
**OTA delivery: built and verified end to end, 2026-08-24.** `@capgo/capacitor-updater` 8.51.14
|
||
installed (MPL-2.0, peer `@capacitor/core: ^8.0.0` against our `^8.5.0`). Manual mode, not
|
||
`autoUpdate` (that stays `false` on purpose — an update must never swap the UI out from under
|
||
someone mid-entry): `companion-app/src/daten/ota.ts` wraps `download({url, version, checksum})` +
|
||
`set({id})`, wired to a button in the Einstellungen screen (`otaUpdateVerfuegbar` /
|
||
`otaAusloesen`). `notifyAppReady()` runs from a `useEffect` in `AngemeldeteApp` (`App.tsx`) —
|
||
deliberately there and not in `main.tsx`, so a crash before React actually renders never gets
|
||
confirmed and the plugin's `appReadyTimeout` rollback (`capacitor.config.ts`, 20s, more generous
|
||
than the 10s default because first load also fetches profile/trips/refuels) takes over.
|
||
|
||
The bundle itself lives **inside the integration**, not under `/local/` as first sketched:
|
||
`custom_components/audi_dashboard/frontend/app/{bundle.zip,bundle.json}`, built by
|
||
`companion-app/scripts/ota-paket.ps1`, served at `/audi_dashboard_static/app/bundle.zip`. Reason
|
||
for the relocation: `/local/` would have needed its own delivery step that `install.ps1` doesn't
|
||
touch and could be forgotten; inside the integration folder it travels with every install/update
|
||
automatically, same as the panel. `sensor.audi_dashboard_app_version`'s `daten` gained a `buendel`
|
||
field (`koordinator._buendel_lesen()`) carrying `{version, sha256, bytes, gebaut, url}` — `null`
|
||
when no bundle is published, a normal state, not an error.
|
||
|
||
One real bug the build caught: `[IO.Compression.ZipFile]::CreateFromDirectory` on Windows
|
||
PowerShell 5.1 (.NET Framework, not Core) writes **backslashes** as path separators for nested
|
||
entries — a spec-violating zip that iOS would not have unpacked correctly. Caught by inspecting the
|
||
actual zip contents after the first build, not assumed; fixed by writing entries by hand via
|
||
`ZipFile::Open` + `CreateEntryFromFile` with `\` replaced by `/`. The plugin verifies SHA-256 over
|
||
the *downloaded zip itself* before unpacking (`CapgoUpdater.swift` `calcChecksum`, confirmed by
|
||
reading the plugin source, not the README) — `ota-paket.ps1` hashes the same file it writes, so the
|
||
two can't drift.
|
||
|
||
Verified against the running test container: entry reload picks up code changes but **not** a
|
||
manifest version bump (HA caches the `Integration` object — needs a full restart, same lesson as
|
||
the pyscript-removal step in section G); after a real restart, `sensor.audi_dashboard_app_version`
|
||
published the new manifest version and a complete `buendel` block; the zip served over HTTP hashed
|
||
identically to `bundle.json`'s `sha256`; `unzip -l`/`-t` confirmed 13 entries, `index.html` at the
|
||
root, no backslashes, no corruption.
|
||
|
||
**A second, quieter bug surfaced 2026-08-24 when a manifest-only metadata change (the GitHub-removal
|
||
commit) prompted the question of whether it needed a version bump.** It didn't — but the question
|
||
exposed that nothing enforced `frontend/app/bundle.json`'s version staying in sync with
|
||
`manifest.json`'s on a *real* bump. Trace the failure: bump manifest to V2, ship via `install.ps1`,
|
||
forget `npm run ota` → backend publishes `app: "V2"` live, but `bundle.json` (frozen at build time)
|
||
still says V1, and the already-installed app is still V1. `Hinweisleiste` compares app-vs-server and
|
||
correctly says "you're outdated, update" (V1 ≠ V2) — but the Update screen's `buendelPasst()`
|
||
compares app-vs-*bundle* and says "you're current" (V1 = V1, the bundle's stale number). Two
|
||
contradictory messages, and no way to actually reach V2 via OTA until someone notices and reruns
|
||
`npm run ota` — the exact kind of silent-until-discovered coordination bug this project has fixed
|
||
repeatedly elsewhere (trip status, tire counter, `edited_fields`). Fixed at the one chokepoint every
|
||
delivery already passes through: `install.ps1` now reads `frontend/app/bundle.json`'s version right
|
||
after reading the manifest's, warns loudly and adds the mismatch as the **first** Restliste item if
|
||
they differ (a missing bundle file is not a mismatch — OTA is optional, silence is correct then).
|
||
Verified both ways on a mock config dir: matching versions print "OTA-Bündel passt zur Integration"
|
||
and add nothing to the Restliste; a deliberately bumped manifest (tested via a temporary edit,
|
||
reverted byte-for-byte after, diffed against `HEAD` to confirm) produces the warning and lands it as
|
||
Restliste item 1. `VERSIONIERUNG.md`'s "Was du tun musst" now lists the OTA rebuild as its own
|
||
numbered step (2, before shipping) instead of omitting it entirely.
|
||
|
||
Capacitor's `server.url` pointed 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, gutting the deliberately
|
||
built offline queue (`api/warteschlange.ts`).
|
||
|
||
**`install.ps1` couldn't authenticate to the Samba share, and gave a misleading error (found and
|
||
fixed 2026-08-24, during the owner's first real deployment attempt).** The owner's Samba add-on was
|
||
running and reachable — confirmed via its own log (`smbd`/`nmbd` started cleanly) and via
|
||
`Test-Path`/`net use` from an interactive PowerShell session, which worked. Yet `install.ps1` kept
|
||
reporting "Ziel nicht erreichbar" for the exact same path. Root cause: `Test-Path` on a UNC path
|
||
**never prompts for credentials** — without an already-authenticated SMB session to that server, a
|
||
password-protected share just returns `false`, with no error and no password prompt. The script had
|
||
no credential handling at all; it only ever worked by accident, piggybacking on a session the user
|
||
happened to have already established by hand (Explorer, or a manual `net use`). Once that session
|
||
dropped — confirmed happening organically here when the Samba add-on itself restarted mid-session —
|
||
the next run failed again with the same unhelpful message, even though path and password were both
|
||
correct. Two side findings along the way, also worth remembering: (1) `Test-Path` against a UNC path
|
||
can fail silently for reasons other than credentials, e.g. process elevation — a `net use` session
|
||
established in a non-elevated shell is invisible to a process launched via "Run as administrator",
|
||
because they're different logon contexts; `Installieren.cmd` needs no elevation and should always be
|
||
run plainly. (2) Add-ons on Home Assistant OS often bind to their **own IP**, separate from HA
|
||
Core's — the Samba add-on here answered on a different address than the one shown on
|
||
Settings → System → Network, confirmed via the add-on's own broadcast log line and by successfully
|
||
loading the HA frontend on that same IP.
|
||
|
||
Fixed at the point of failure: if the target path isn't reachable, `install.ps1` now prompts for a
|
||
username (defaulting to `homeassistant`, the Samba add-on's account) and password, clears any stale
|
||
connection to the same path first (`net use $Ziel /delete`), and authenticates
|
||
(`net use $Ziel /user:... ...`) before retrying `Test-Path`. Only a genuine auth/path failure after
|
||
that throws. No more manual `net use` pre-step required. Verified the script still parses
|
||
(`PSParser]::Tokenize`) after the edit; end-to-end verification pending the owner's next real run.
|
||
|
||
### G) Fresh-install audit + installer hardening (2026-08-23, before the first real deployment)
|
||
|
||
> **Historical from here on.** This describes the pyscript-era package, which section H replaced
|
||
> later the same day. The *findings* still matter — the blank-delivery rule and the safety
|
||
> properties carried over into the new installer — but the files it names (`pyscript/`, `www/`,
|
||
> `data/`, the `configuration.yaml` block) no longer exist. Kept as the record of what was wrong
|
||
> and why the current shape looks the way it does.
|
||
|
||
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.bak` was overwritten on every run, so a second run destroyed the
|
||
pristine original. Backups are now timestamped, and the plain `.bak` is 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).
|
||
|
||
**Two user-reported bugs fixed 2026-08-23, both worse than they looked:**
|
||
|
||
*"Fahrzeug fährt" never changed.* `standortZustand()` inferred the driving state from
|
||
`TRIPS[0].status === "offen"` — two independent errors stacked. First, `"offen"` does not mean
|
||
"driving"; it means **data incomplete** (distance still missing, see `fahrtabschluss_logik.py`) — a
|
||
trip created by `_fahrt_beenden()` *when it ends*. A trip that never got its odometer, e.g. because
|
||
no `KM_SENSOR` was mapped at the time, stays `"offen"` forever. Second, `TRIPS[0]` is the **oldest**
|
||
trip, not the newest: `fahrten_veroeffentlichen()` passes `profil.fahrten_lesen()` through in file
|
||
order, which is ascending. Together: the oldest ever-incomplete trip pinned the display to "fährt"
|
||
permanently. The test instance had one open for 18 days.
|
||
|
||
Fixed at the source rather than by patching the index: the backend now publishes `zuendung` in
|
||
`fahrzeugstatus` from the already-mapped `ZUENDUNG_SENSOR` — the same signal `fahrterkennung.py`
|
||
treats as authoritative, so display and detection cannot disagree by construction. `null` (no sensor
|
||
mapped) renders "Fahrzustand unbekannt" rather than claiming "steht".
|
||
|
||
The companion had the mirror-image bug: `liveZustandLesen()` read `status["zuendung"]` (never
|
||
published) and `status["lat"]`/`status["lon"]` (backend publishes `standort_lat`/`standort_lon`), and
|
||
`Fahrzeug` declared none of those fields — so `LiveFahrt` showed "Das Fahrzeug steht" permanently and
|
||
never a position. Fields added to `Fahrzeugstatus`/`Fahrzeug` and the adapter, so the type now
|
||
catches this class of bug — which its own header comment already warned about from a previous
|
||
occurrence.
|
||
|
||
*"Standortzugriff verweigert" was misleading.* It sits directly under the vehicle name where the
|
||
distance-to-car goes, but describes **this device's** browser geolocation — it read as if the car's
|
||
location were unavailable. Now "GPS offline", as the owner asked. Permission denial keeps its own
|
||
string ("GPS-Freigabe fehlt"): it is the one case with a different remedy, and "GPS offline" would
|
||
send the user looking at signal instead of at a permission toggle.
|
||
|
||
**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.
|
||
|
||
### I) Ölwechsel/Inspektion could vanish entirely with a mapped sensor and no Servicebuch (found and fixed 2026-08-24)
|
||
|
||
Owner reported: "no inspektion and ölwechsel shown even if there is the right sensor and
|
||
herstellervorgabe." Both were true — sensor mapped correctly, "Herstellervorgabe" selected — and the
|
||
row still just said "kein Eintrag im Servicebuch" (no Servicebuch entry).
|
||
|
||
**Root cause, identical in both codebases.** The Ölwechsel/Inspektion display combines two
|
||
independent sources on purpose (see the header comment in `companion-app/src/screens/Service.tsx`):
|
||
the vehicle's own reported due date/km (`fahrzeugMeldungRoh()` / `fahrzeug.oelwechselFaelligTs` etc.,
|
||
sourced from the mapped HA sensor) is meant to stand *alongside* the app's own Servicebuch-based
|
||
forecast, not depend on it. But the actual gating checked only the Servicebuch side:
|
||
- Panel (`frontend/audi-dashboard-app.js`, `vService()`): `const iv = intervalle(t); if (!iv) return
|
||
...„kein Eintrag im Servicebuch"...` — and `intervalle()` returns `null` whenever there's no
|
||
Servicebuch basis, full stop. The vehicle-reported `fm`/`hatMeldung` was computed *after* this early
|
||
return, so a mapped sensor with valid data never even got read for a fahrzeug with an empty
|
||
Servicebuch — exactly the state of a freshly set-up car.
|
||
- companion-app (`src/screens/Service.tsx`): same shape, `{oelHatEintrag ? ... : "kein Eintrag im
|
||
Servicebuch"}` — gated purely on a Servicebuch entry existing, ignoring `oelHatMeldung`/
|
||
`inspHatMeldung` computed one line above it. `src/daten/service.ts`'s `naechsterService()` (feeds
|
||
the Home-screen "Nächster Service" tile) had the same gap one level up: it only ever built
|
||
candidates from `oelwechselPrognose()`/`inspektionPrognose()` (both Servicebuch-only), never
|
||
falling back to `fahrzeug.oelwechselFaelligTs`/`inspektionFaelligTs`.
|
||
|
||
**Fixed by reordering, not by adding a new data path** — the sensor data was already being read
|
||
correctly by the backend and delivered to both frontends; only the display-gating ignored it.
|
||
Panel: `fm`/`hatMeldung` now computed *before* the early-return check, which now reads `if (!iv &&
|
||
!hatMeldung)`; `anzeige`/`herstellervorgabe` guarded for `iv` possibly being `null` (both remain safe
|
||
because the code paths that dereference `iv` only run when `hatMeldung` is false, and the new guard
|
||
guarantees `iv` is non-null whenever that's the case). companion-app: `oelHatEintrag ||
|
||
oelHatMeldung` / `inspHatEintrag || inspHatMeldung` as the render condition; `naechsterService()`
|
||
gained an `else if (fahrzeug.oelwechselFaelligTs)` / `...inspektionFaelligTs` fallback branch per
|
||
service type, added only when the Servicebuch-based prognose is `null` — a real Servicebuch entry
|
||
still wins when both exist (own forecast is finer-grained, factoring actual driven km/day). The
|
||
Hauptuntersuchung path is unaffected either way: `fahrzeugMeldungRoh("Hauptuntersuchung")` always
|
||
returns `{ts: null, km: null}`, so `hatMeldung` is always `false` for it — same behavior as before.
|
||
|
||
Verified: `tsc --noEmit` clean, all 120 companion-app tests pass (including 3 new regression tests in
|
||
`service.test.ts` covering the meldung-only fallback for both service types and confirming a real
|
||
Servicebuch prognose still takes priority when both are present), panel JS re-checked with `node
|
||
--check`. `manifest.json` bumped to `2026.8.24.2` and `npm run ota` rerun (own rule from section H,
|
||
enforced by `install.ps1` since the same day) since this touched both the panel and the companion-app.
|
||
|
||
### J) Self-update: the integration pulls its own updates from Gitea (built 2026-08-24)
|
||
|
||
**Why.** `install.ps1` (section H) stopped being a reliable update path: Windows Smart App Control
|
||
blocks its execution, and unlike SmartScreen there is no "run anyway" override — once fully
|
||
enabled, Smart App Control can only be turned off by reinstalling Windows. The owner's actual daily
|
||
pain wasn't the block itself but the whole cycle it forced: open Gitea, download a zip, unpack it,
|
||
run the (now-blocked) installer, re-enter the Samba password every time (see section H's Samba-auth
|
||
fix earlier the same day). Owner chose, from three options laid out (free workarounds first / code-sign
|
||
the installer / have Home Assistant pull its own updates), the third: **the integration now updates
|
||
itself.** `install.ps1` remains necessary for the *first* install only — the integration has to
|
||
already be running before it can update itself — which is the rare case; the daily-update tedium is
|
||
what this closes.
|
||
|
||
**Safety design — stricter than install.ps1, because it runs inside what it replaces.**
|
||
`install.ps1` is an external process against a foreign HA instance; this feature's code lives
|
||
*inside* `custom_components/audi_dashboard/` and replaces that very folder while its own service
|
||
call is still executing. `aktualisierung.py`'s module docstring carries the full reasoning; the
|
||
shape:
|
||
1. Download and verification happen entirely in a sibling staging folder
|
||
(`custom_components/audi_dashboard_update_staging/`) — the live folder is never touched until the
|
||
download is confirmed complete and plausible.
|
||
2. The swap is `os.rename`, not delete-then-copy: the live folder is renamed to
|
||
`audi_dashboard_backup` (kept, not deleted — reversible) before the staging folder is renamed into
|
||
its place. If the second rename fails, the first is undone automatically rather than leaving a
|
||
half-replaced folder.
|
||
3. The downloaded `manifest.json` is checked for `domain == "audi_dashboard"` and a `version` field
|
||
before either rename runs — matches install.ps1's own "won't touch a folder that isn't
|
||
provably ours" rule (section G/H).
|
||
4. **No automatic reload or restart.** A reload triggered from inside the same service call that's
|
||
still running would risk being interrupted by its own reload. The UI shows a "restart Home
|
||
Assistant" message instead — the same pattern install.ps1's Restliste already uses.
|
||
5. Nothing outside `custom_components/` is touched — same invariant as install.ps1.
|
||
6. The check step (a Gitea network call) runs only on explicit button press, never on the
|
||
coordinator's normal publish tick — same "no unsolicited background action" stance as the OTA
|
||
`autoUpdate: false` decision.
|
||
|
||
Renaming the very folder the running code was imported from is safe on Linux (this project runs on
|
||
Home Assistant OS / Docker): Python holds no open file handles on `.py` sources after import
|
||
completes, and `os.rename` only operates on path strings, not live handles.
|
||
|
||
**Token storage.** No prior mechanism existed for secrets in this integration — `config_flow.py`'s
|
||
`entry.data` was always `{}`, no `OptionsFlow` existed. Added `AudiDashboardOptionsFlow`
|
||
(Settings → Devices & Services → Audi Dashboard → Configure), one masked field
|
||
(`CONF_GITEA_TOKEN` in `const.py`), stored in `entry.options` — HA's own encrypted config-entry
|
||
store, never `configuration.yaml`. A read-only-scoped Gitea token is enough; the form text says so.
|
||
|
||
**What was built:**
|
||
- `custom_components/audi_dashboard/aktualisierung.py` — `version_pruefen()` (one lightweight Gitea
|
||
Contents-API call for just `manifest.json`, no repo download) and `update_installieren()`
|
||
(downloads the full repo archive via Gitea's archive endpoint — Gitea has no single-folder
|
||
download — delegates to the blocking `entpacken_pruefen_tauschen()` via
|
||
`hass.async_add_executor_job`). The blocking function takes all paths as parameters rather than
|
||
reading module constants directly, specifically so it's testable against a temp directory without
|
||
touching the real integration folder.
|
||
- Two new services (`update_pruefen`, `update_installieren`), registered in `dienste.py` following
|
||
the existing `SCHEMATA`/`behandler`/`_als_dienst()` pattern; both zero-field, both catch
|
||
`aktualisierung.AktualisierungsFehler` and turn it into a user-readable message instead of an
|
||
unhandled exception.
|
||
- `koordinator.py`: `alles_veroeffentlichen()`'s `E_APP_VERSION` publish was factored out into its
|
||
own `app_version_veroeffentlichen()` method and extended with `integration_update`. This holds the
|
||
*last* check/install result (`self.letzte_update_pruefung`) — deliberately **not** recomputed on
|
||
every publish tick, only ever set by the two new service handlers, so normal operation triggers zero
|
||
Gitea calls.
|
||
- Both frontends got a new "Integration-Update" tile (companion-app: `Einstellungen.tsx` +
|
||
`IntegrationUpdateAngabe` type in `api/types.ts` + `integrationUpdate` in `DatenKontext.tsx`; panel:
|
||
new tile next to the existing Version tile in `audi-dashboard-app.js`, backed by a new
|
||
`INTEGRATION_UPDATE` global read from `daten.integration_update`, wired to the existing
|
||
`data-*`/`serviceRufen()`/`ereignisseVerdrahten()` click-dispatch pattern) — not an extension of the
|
||
existing OTA "App-Update" tile, which is a different concept (the Capacitor iOS shell going stale)
|
||
that doesn't apply to a browser-rendered panel.
|
||
|
||
**Verified, honestly scoped by what this session's sandbox could actually run:**
|
||
- The safety-critical pure logic (`entpacken_pruefen_tauschen()`) has 7 unit tests
|
||
(`tests/aktualisierung/test_aktualisierung.py`, `unittest`, same style as
|
||
`tests/belegparser/test_shell_beleg_parser.py`) covering: successful swap with the old version
|
||
preserved in the backup folder, `__pycache__` cleanup, wrong domain rejected, missing `version`
|
||
field rejected, missing `manifest.json` rejected, corrupt zip rejected, archive missing the
|
||
expected path rejected — and every rejection case asserts the live folder is byte-for-byte
|
||
untouched. Run inside the project's existing throwaway `ghcr.io/home-assistant/home-assistant:stable`
|
||
test container (`audi_ha_test`) via `docker exec`, since no Python interpreter exists in the
|
||
session sandbox itself.
|
||
- The complete integration (config flow, both services, koordinator changes, both frontends) was
|
||
deployed into that same container and restarted from a clean state, twice, ending on
|
||
`Audi Dashboard 2026.8.24.4 eingerichtet` with zero errors or tracebacks — confirms the
|
||
`OptionsFlow`/service registration/coordinator wiring is structurally correct against a real,
|
||
current (2026.8.0) Home Assistant.
|
||
- companion-app: `tsc --noEmit` clean, all 127 tests pass.
|
||
- **Not verified**: an actual successful download+swap against the real private Gitea repo, since
|
||
that needs the owner's own token — entering it is the owner's action via the HA UI, not something
|
||
to hand to an assistant in chat. The failure paths (missing token, bad token, unreachable Gitea) are
|
||
covered by the unit tests and code review, not by a live call against Gitea. First real end-to-end
|
||
use is the owner's to do once a token is configured — worth a first careful try on the test
|
||
container before the real instance, same caution as every other install.ps1-era change in this
|
||
project.
|
||
|
||
**Docs updated to stop calling install.ps1 "the only way":** `README.md`, `VERSIONIERUNG.md` — both
|
||
now say install.ps1 is the only way *to install*; the self-update is now the preferred way *to
|
||
update* an already-running instance.
|
||
|
||
**Two fixes found during the owner's own first live test (2026-08-24, same day).** First attempt
|
||
against the test container ("nothing changed") turned out to be a real user error caught by log
|
||
inspection, not a bug: the check was clicked before the token had actually been saved in the
|
||
Options Flow — `dienste.py`'s log showed exactly why (`Kein Gitea-Token hinterlegt`), confirming the
|
||
error path works. But this surfaced a genuine observability gap: `update_pruefen`/
|
||
`update_installieren` only ever logged on *failure* — a successful check was silent, so "nothing in
|
||
the log" was ambiguous between "not clicked yet" and "worked fine." Fixed by adding an `_LOGGER.info`
|
||
line on the success path of both services. Separately, the owner asked for the panel's idle-state
|
||
text ("Ein Gitea-Zugriffstoken muss zuvor... eingetragen sein") to only appear as part of an actual
|
||
error, not as a permanent reminder cluttering the tile before the first check — removed from the
|
||
idle branch; the same information already surfaces via `integration_update.fehler` when it's
|
||
actually true. companion-app never had this line in the first place, so no parity fix needed there.
|
||
|
||
**A real bug found the same day, from a genuinely confusing error report.** After a successful
|
||
install-then-restart cycle, a later "Update installieren" attempt failed with:
|
||
`Konnte den bestehenden Ordner nicht sichern: [Errno 2] No such file or directory:
|
||
'/config/custom_components/audi_dashboard_backup' -> '/config/custom_components/audi_dashboard_backup'`
|
||
— note both sides of the `os.rename` are the *same path*, which shouldn't be possible given
|
||
`INTEGRATIONSORDNER`/`_BACKUP_ORDNER` are built from clearly different string literals. Root cause,
|
||
found via `docker logs`: the log line's logger name was `custom_components.audi_dashboard_backup.dienste`,
|
||
not `custom_components.audi_dashboard.dienste`. Home Assistant scans **every** folder directly under
|
||
`custom_components/` for a `manifest.json` and loads whatever `domain` it declares — by folder
|
||
content, not folder name. The `audi_dashboard_backup` folder left behind by a successful swap is a
|
||
byte-for-byte copy of the integration, manifest included, so across a restart HA loaded it as a
|
||
*second* `audi_dashboard` integration. Python then imported its `dienste.py` as its own module
|
||
(`custom_components.audi_dashboard_backup.dienste`), whose `__file__`-derived `INTEGRATIONSORDNER`
|
||
*is* the backup folder — making `_BACKUP_ORDNER` collapse onto the same path for that rogue copy.
|
||
This duplicate-domain state also explained two other symptoms reported the same session: the panel
|
||
intermittently doing nothing visible on "Auf Update prüfen"/"Update installieren" (whichever
|
||
integration's service registration won the race), and the frontend getting stuck on the bootstrap
|
||
"Lädt …" screen after a restart (confirmed live: with the stray `audi_dashboard_backup` folder still
|
||
present, `audi_dashboard` appeared **twice** in the loader's warnings and the panel never finished
|
||
loading; deleting the stray folder and restarting fixed both immediately).
|
||
|
||
**Fix**, in `entpacken_pruefen_tauschen()`: right after the backup rename succeeds, its
|
||
`manifest.json` is itself renamed to `manifest.json.bak` — structurally invalid as an integration, so
|
||
HA's scanner skips it. On the rollback path (second rename fails), the `.bak` is restored to
|
||
`manifest.json` *before* the backup folder is renamed back into place, so a restored live folder is
|
||
never left without a valid manifest. Covered by two new unit tests (manifest absent from the backup
|
||
folder after a successful swap; manifest correctly restored after a forced rollback via a mocked
|
||
`os.rename`) — `tests/aktualisierung/test_aktualisierung.py` now has 8 tests, all passing. Verified
|
||
live: deployed the fix, deleted the stray backup folder from the test container, restarted — single
|
||
`audi_dashboard` loader warning, clean `eingerichtet` line, frontend loads normally. Manifest bumped
|
||
to `2026.8.24.7` (see section M below, same version bump covers both fixes).
|
||
|
||
### K) vCard import for the workshop contact (built 2026-08-24)
|
||
|
||
Service → Autohaus (gear icon) now has a "vCard importieren" button next to the existing form
|
||
fields, opening a file picker for a `.vcf` contact exported from a phone/desktop address book. Reads
|
||
`FN`/`ORG` (name, `FN` preferred), `TEL`, `EMAIL`, and `ADR` (the seven-part vCard address —
|
||
PO-Box/extended/street/locality/region/postal-code/country — joined into one readable line, since
|
||
the form's address field is a single string). Deliberately not a full vCard parser: only the four
|
||
fields the Autohaus form already has. Unfolds RFC 6350 §3.2 folded lines (CRLF + one WSP removed
|
||
entirely, not replaced with a space) and un-escapes `\,`/`\;`/`\n`/`\\` before use. Multiple
|
||
`TEL`/`EMAIL`/`ADR` lines: first one wins.
|
||
|
||
- Panel: `vcardParsen()` in `audi-dashboard-app.js`, wired into `vWerkstatt()`'s new hidden
|
||
`data-vcard-import` file input via the existing central `change` listener — same
|
||
parse-then-`hinweis()`-on-error pattern as the existing JSON profile/backup import handlers.
|
||
Updates `CAR.service.autohaus` fields directly and calls the existing `profilSpeichern()` (saves
|
||
immediately, matching this field's existing per-keystroke-save behavior).
|
||
Same isolated-function shape as `entpacken_pruefen_tauschen()` in section J's self-updater: parse
|
||
logic takes no dependency on `HASS`/global state, so it's directly testable.
|
||
- companion-app: new `src/screens/vcard.ts` (`vcardParsen()`, 7 unit tests in `vcard.test.ts`
|
||
covering folding, escaping, ORG fallback, first-wins on repeats, and the two "nothing readable"
|
||
error cases), wired into `Werkstatt()` in `Service.tsx`. Unlike the panel, this only populates the
|
||
local `entwurf` draft state — the existing explicit "Speichern" button still does the actual save,
|
||
matching this screen's existing edit-then-confirm pattern rather than the panel's live-save one.
|
||
Empty fields from the vCard never overwrite non-empty draft values.
|
||
|
||
Verified: `tsc --noEmit` clean, all 134 companion-app tests pass (127 + 7 new), panel JS re-checked
|
||
with `node --check`. Manifest bumped to `2026.8.24.5`, `npm run ota` rerun (both frontends changed).
|
||
|
||
### L) Self-update UX: loading feedback and a real restart button (2026-08-24, same day)
|
||
|
||
**The self-update feature (section J) got its first genuine live test against the real private
|
||
Gitea repo this same day**, and it worked — confirmed directly in the test container's log across
|
||
several real attempts (`Update installiert: Version 2026.8.24.5`, no manual file copying involved).
|
||
That's the first real end-to-end confirmation beyond the unit tests and structural container-restart
|
||
checks section J could offer on its own.
|
||
|
||
That same live test exposed two real UX gaps, both raised directly by the owner:
|
||
|
||
1. **No feedback during or after the ~15–25 second install** (confirmed from the log's own
|
||
timestamps between a "Prüfung" and the matching "installiert" line). The button just sat there
|
||
with no state change — indistinguishable from a hang, and after success, the version still read
|
||
"2026.8.24.4 installiert" because that field only updates after a real restart, which nothing in
|
||
the UI was telling the user to do next beyond a paragraph of text.
|
||
2. **A separate, initially confusing "Lädt …" hang the owner saw and asked about.** Traced by
|
||
reproducing it directly: `.lade-spinner`/"Lädt …" is this project's own **existing** top-level
|
||
bootstrap loading screen (shown whenever `DATEN_GELADEN` is false — i.e., no live connection to
|
||
Home Assistant yet), not anything related to the update button specifically. Clicking the new
|
||
"Jetzt neu starten" button and watching it live in a browser reproduced the *exact* same screen —
|
||
because a `homeassistant.restart` call genuinely does drop the frontend's connection until HA
|
||
finishes rebooting, and this is the correct, expected screen for that. The most likely explanation
|
||
for the owner's *original* sighting (before this button existed): the Docker Desktop restart from
|
||
earlier in the same session dropped the connection the same way. Not a bug — but the fact that it
|
||
was indistinguishable from one was the actual, valid complaint: nothing signposted that a restart
|
||
was in progress versus something being stuck.
|
||
|
||
**Fixed with a three-state UI, researched against Apple HIG's guidance on indeterminate progress**
|
||
(unknown-duration network operations get a running indicator, not just a disabled control; on
|
||
completion the UI leads straight to the next required action instead of leaving the user to find it
|
||
themselves):
|
||
- **Prüfen/Installieren, in progress** — a small spinner + "Prüfe …"/"Installiere …", reusing the
|
||
panel's own pre-existing `.lade-spinner` (originally built, per its own comment, for the exact same
|
||
"don't let 'still loading' look like 'stuck'" reason at app bootstrap) and a new equivalent
|
||
`.dm-spinner`/`.dm-ladezeile` pair added to companion-app's `screens.css` (that side had no spinner
|
||
at all before, only a button-text swap).
|
||
- **On success** — the tile switches directly to "Installation abschließen - Jetzt neu starten",
|
||
which calls `homeassistant.restart` (a core HA service any admin could already trigger via
|
||
Settings → System → Restart — this button doesn't grant a new capability, it surfaces an existing
|
||
one at the point it's actually needed). Its own click shows "Home Assistant startet neu …";
|
||
no `.finally`-style cleanup after that call succeeds, since the connection normally drops before
|
||
the promise would ever resolve — only a genuine rejection (service refused, etc.) reaches the
|
||
catch block.
|
||
- **On a real request-level failure** (not the already-handled "no token"/"bad token" case from
|
||
section J, which returns normally with `fehler` set — this is Gitea unreachable, the websocket
|
||
itself failing, or any other exception that never made it back from Home Assistant at all) — this
|
||
was previously silent in companion-app (a `try/finally` with no `catch` re-throws into an
|
||
unhandled promise rejection). Added explicit `catch` blocks on all three actions in both frontends,
|
||
each surfacing its own message rather than one shared silently-stale error field.
|
||
|
||
- Panel: `INTEGRATION_UPDATE_LAEUFT` (`"pruefen" | "installieren" | "neustart" | null`), the
|
||
`data-update` click handler rewritten from `serviceRufen()` (fire-and-forget) to direct
|
||
`HASS.callService()` promise chains so the loading flag can wrap them; `vEinst()`'s tile rewritten
|
||
with the three states above.
|
||
- companion-app: `updateNeustartLaeuft`/`updateAktionFehler` state and `updateNeustartAusloesen()` in
|
||
`Einstellungen.tsx`; new `homeAssistantNeuStarten()` in `api/index.ts` (`dienstAufrufen("homeassistant",
|
||
"restart")` — note the domain isn't `DIENST_DOMAIN`, this is a core HA service, not one of this
|
||
integration's own).
|
||
|
||
Verified live end-to-end in the test container via browser automation: triggered the real restart
|
||
button, watched the connection drop into the bootstrap "Lädt …" screen exactly as reasoned above,
|
||
confirmed via `docker logs` that Home Assistant actually rebooted
|
||
(`Audi Dashboard 2026.8.24.6 eingerichtet` after restart, version now genuinely current — not just
|
||
the pre-restart file swap). companion-app: `tsc --noEmit` clean, full suite still green. Manifest
|
||
bumped to `2026.8.24.6` and `npm run ota` rerun per the standing rule in `VERSIONIERUNG.md` — both
|
||
frontends changed (new spinner/restart-button behavior is real, visible UI, not internal-only).
|
||
|
||
### M) Übersicht/Service mismatch on a blank install: two real bugs (2026-08-24, same day)
|
||
|
||
The owner tested on a **blank installation** (no Servicebuch entries yet) and found two bugs:
|
||
|
||
1. **Service showed the vehicle's own km/date; Übersicht showed something else (or nothing).** Root
|
||
cause was worse on the panel than in companion-app: `termine()` (panel) built its candidate list
|
||
purely from `letzterEintrag()` (a real Servicebuch entry) — with none, `km`/`datum`/`ziel` all
|
||
stayed `null`, so `vHome()` (Übersicht) rendered a broken/empty service block, while `vService()`
|
||
already had its own separate `meldungsPrognose()` fallback and displayed correctly. companion-app's
|
||
`naechsterService()` did already call `meldungsPrognose()` as a fallback, but paired the *raw*
|
||
signed `fahrzeug.oelwechselFaelligKm` with a *computed* projected date, which could read
|
||
inconsistently with `Service.tsx`'s own primary line. Fixed by adding the same meldung-based
|
||
fallback to `termine()` (new block right after `oelTermin`/`inspTermin` are built: `if
|
||
(termin.basis) return;` then reads `oelMeldungsPrognose()`/`meldungsPrognose()`), and by having
|
||
`meldungsPrognose()` always return `restKm` (`Math.abs(faelligKm)`) so both codebases use one
|
||
consistent value instead of recomputing it differently at each call site. Verified live: emptied
|
||
the test container's `service.buch` array (backed up first, restored after), confirmed Service and
|
||
Übersicht showed byte-identical numbers (8.687 km / 28.03.2027) where Übersicht previously would
|
||
have shown nothing usable at all.
|
||
|
||
2. **The Ölwechsel forecast ignored the owner's own (shorter) interval from "Einrichten" until a
|
||
Servicebuch entry existed** — it silently kept showing the manufacturer's own countdown instead,
|
||
with no restKm shown either. Root cause: `meldungsPrognose()`/`fahrzeugMeldungRoh()` only ever
|
||
relay the vehicle sensor's own manufacturer-interval countdown; nothing about them reads
|
||
`oel.modus`/`oel.km`/`oel.monate`. Without a Servicebuch entry there's no known "last real oil
|
||
change" to anchor a custom-interval projection from — the owner chose (asked directly, see
|
||
conversation) to estimate one anyway: back-calculate an assumed last-service point as
|
||
`(manufacturer's own due point) − (manufacturer's own interval length)`, then project the owner's
|
||
shorter interval from that same assumed point. An approximation, not exact — it can never be as
|
||
good as a real Servicebuch entry — but it's the only way to show a number that respects the
|
||
owner's chosen interval before the first entry exists, instead of silently ignoring the choice.
|
||
Implemented once in `service.ts`
|
||
(`meldungsPrognoseEigenesIntervall()` + `oelMeldungsPrognose()`, the latter dispatching to the
|
||
existing `meldungsPrognose()` unchanged when `modus === "hersteller"`) and mirrored 1:1 in the
|
||
panel (same two functions, same math, `Date`-based month arithmetic instead of `service.ts`'s
|
||
ISO-string `monatePlus()` since the panel's own `monatePlus()` only accepts `TT.MM.JJJJ` strings
|
||
and the vehicle meldung's timestamp is ISO). Both `Service.tsx`/`vService()`'s footnote for this
|
||
fallback case now show `restKm` alongside the date when the owner's interval is shorter than the
|
||
manufacturer's, exactly like the existing Servicebuch-based prognose already does. Inspektion is
|
||
unaffected — it has no owner-selectable interval, always the manufacturer's own.
|
||
|
||
Only Ölwechsel has a selectable interval, so this only applies there. If `oel.herstellerKm`/
|
||
`herstellerMonate` are themselves unknown, both functions degrade to the plain (unchanged)
|
||
`meldungsPrognose()` rather than guessing further.
|
||
|
||
Verified: companion-app — 4 new tests for `meldungsPrognoseEigenesIntervall()`/`oelMeldungsPrognose()`
|
||
(restKm/date math, the "already overdue by the owner's interval" clamp-to-zero case, no-fahrten
|
||
fallback, graceful degradation without hersteller values), full suite green at 142/142, `tsc --noEmit`
|
||
clean. Panel: `node --check` clean; live-verified in the test container using the exact same
|
||
blank-install simulation as bug 1 above — with `service.buch` emptied and the container's real
|
||
`oel` settings (`modus: "eigen"`, `10.000 km`/`12 Monate`, manufacturer `30.000 km`/`24 Monate`), the
|
||
footnote showed **8.700 km, voraussichtlich am 23.07.2027** — the derived number from
|
||
`28.700 − 30.000 + 10.000`, using the vehicle's real raw meldung — closely matching (not identical to,
|
||
as expected for an approximation) the real Servicebuch-based value of 8.687 km / 28.03.2027 seen with
|
||
the Servicebuch restored. Restored the container's real profile afterward; no live data was left
|
||
altered.
|
||
|
||
Manifest bumped to `2026.8.24.7` (covers this section and section J's manifest-neutering fix above —
|
||
one version bump, both landed the same day); superseded by `2026.8.24.8` in section N below the same
|
||
day. `npm run ota` rerun once, covering both this section's and section N's frontend changes together
|
||
(see section N).
|
||
|
||
### N) Sicherheit neu gestaltet: verriegelt / Türen & Klappen / Fenster & Dach / Licht (2026-08-24, same day)
|
||
|
||
The owner asked for a rework of the "Sicherheit" screen (`vSicherheit()`/`Fahrzeugstatus.tsx`), which
|
||
until now showed one flat list of every door/window/heckklappe/haube check with no grouping and no
|
||
lock, roof, or light status at all. Requested: four named collector rows — **Fahrzeug verriegelt**
|
||
(new lock sensors, state "abgeschlossen"), **Türen und Klappen geschlossen** (with an arrow to a
|
||
detail view of every door, Motorhaube, and Kofferraum individually), **Fenster und Dach geschlossen**
|
||
(Dach = Schiebedach), **Licht ausgeschaltet** (Standlicht, showing the text **"Kein Licht"** for the
|
||
good state, not a generic "in Ordnung"). The owner also supplied a concrete mockup: green
|
||
checkmark-circles instead of the old plain-color dot, only the second row carrying a chevron.
|
||
|
||
Went through plan mode given the cross-codebase, backend+frontend scope. Before planning, checked the
|
||
real entities available in the test container's `core.entity_registry` (`cupra_eu_data_act` platform)
|
||
to ground the sensor-role design in what actually exists rather than guessing: per-door
|
||
`binary_sensor.*_door_lock` (`device_class: lock`), `..._tailgate_lock`, `..._hood_lock`, `..._sunroof`
|
||
(`device_class: window`), `..._parking_lights` (`device_class: light`). HA's own convention for all of
|
||
these is `off` = the good state (locked/closed/no light) — the exact same `state == "off"` → `ok=True`
|
||
check this integration already uses for door/window sensors. No new check pattern needed, only new
|
||
sensor roles.
|
||
|
||
**Backend:**
|
||
- `einstellungen.py` — five new `Sensorzuordnung` fields: `TUERSCHLOSS_SENSOREN` (4 positions, mirrors
|
||
`TUER_SENSOREN`), `HECKKLAPPE_SCHLOSS_SENSOR`, `HAUBE_SCHLOSS_SENSOR` (paired with the existing
|
||
open-sensors), `DACH_SENSOR`, `LICHT_SENSOR`. Matching `FELDER` catalog entries under the existing
|
||
`"sicherheit"` group — the Setup-Menü is entirely `FELDER`-driven (confirmed: its group headers come
|
||
straight from this list), so the new roles appeared there with zero extra frontend work.
|
||
- `veroeffentlichung.py`'s `_sicherheitscheck()` — extended with the five new checks, each entry now
|
||
additionally tagged `"gruppe"`: `"verriegelt"` (all 6 locks), `"tueren_klappen"` (4 doors + Motorhaube
|
||
+ the renamed "Kofferraum" — was "Heckklappe"; the underlying `HECKKLAPPE_SENSOR` field itself is
|
||
unchanged, so an existing assignment isn't silently broken by the relabel), `"fenster_dach"` (4
|
||
windows + Dach), `"licht"` (Standlicht alone). `gesichert` is untouched — `all()` over the full flat
|
||
list is mathematically identical to "AND of all four group results," so no logic change, just more
|
||
entries in the same list.
|
||
- **Consequence flagged to the owner**: because every entry in the flat list (including brand-new,
|
||
unassigned ones) counts toward `gesichert`, any existing installation shows "Zustand nicht
|
||
vollständig bekannt" for the overall status until the three new roles are assigned in Setup — same
|
||
"unknown beats assumed-good" principle this project has followed throughout, but worth knowing before
|
||
it looks like a regression.
|
||
|
||
**Frontend (both codebases):** a small grouping helper (`sicherheitsGruppe()` in the panel,
|
||
`gruppenErgebnis()` + filter in companion-app) turns the flat list into a group's aggregate `ok`
|
||
(`null` the moment one entry is unknown, or if the group has zero entries at all — never `true` via an
|
||
empty-array vacuous truth). Both `vSicherheit()`/`Fahrzeugstatus.tsx` rewritten to the four-row layout;
|
||
each row's own label text changes with its state (e.g. "Kein Licht" / "Licht eingeschaltet" / "Licht
|
||
unbekannt") rather than pairing a fixed label with a separate status word — matches the mockup, which
|
||
shows no secondary value text at all. New checkmark-circle indicator (`statusKreis()` in the panel,
|
||
`StatusKreis` component in companion-app's `bausteine.tsx`) — green check / red cross / amber
|
||
question-mark SVG in a filled circle, replacing the old plain dot for this screen only; the rest of the
|
||
app keeps `.dot`/`.dm-punkt` unchanged. New route `"tuerenklappen"` in both routing tables
|
||
(`ZURUECK`/`navigation.ts`), reachable only from the "Türen und Klappen geschlossen" row; its detail
|
||
page is the old flat-list style filtered to just that group (`vTuerenKlappen()`, new
|
||
`screens/TuerenKlappen.tsx`).
|
||
|
||
**Verified:**
|
||
- Backend: `python3 -m py_compile` on both changed modules, clean restart in the test container with a
|
||
single `audi_dashboard` loader warning (no duplicate-domain regression from section J/M's fix).
|
||
- companion-app: `tsc --noEmit` clean, full suite green at 145/145 (added: grouping/unknown-propagation
|
||
test, chevron-navigates-only-this-row test, detail-page-shows-only-its-group test; updated the one
|
||
pre-existing test and the `beispieldaten.ts`/`profilAdapter.test.ts` fixtures for the now-required
|
||
`gruppe` field on `SicherheitsPunkt`).
|
||
- Panel: `node --check` clean.
|
||
- Live end-to-end in the browser against the test container: before assigning the new sensor roles, all
|
||
three new rows correctly showed "unbekannt" (amber) and the overall status read "Zustand nicht
|
||
vollständig bekannt" — confirming the flagged consequence above is real and correctly wired, not
|
||
theoretical. Opened Setup, scrolled to "Sicherheit": all five new fields were **already
|
||
auto-suggested** to the exact real entities found earlier (the existing stichworte/device_class
|
||
matching worked without any manual searching) — saved (one pre-existing, unrelated "doppelt
|
||
zugeordnet" warning about `Front left door`/`Front left window`, present before this change, dismissed
|
||
via the existing "Trotzdem speichern" override). After saving: "Fahrzeug nicht verriegelt" showed red
|
||
(the demo lock entities report unlocked — correct, not a bug), "Türen und Klappen geschlossen" and
|
||
"Fenster und Dach geschlossen" showed green, "Licht ausgeschaltet" showed green. Clicked the chevron on
|
||
row 2: navigated to "Türen und Klappen" showing all four doors, Kofferraum, and Motorhaube individually,
|
||
all green "zu" — confirmed the detail page and the "Kofferraum" relabel both work.
|
||
|
||
Manifest bumped to `2026.8.24.8`. `npm run ota` rerun (`scripts/ota-paket.ps1 -Bauen`): picked up
|
||
`2026.8.24.8` automatically from the manifest, built clean, `bundle.zip` (233.244 Bytes) + sha256 +
|
||
`bundle.json` written — covers both this section's and section M's frontend changes, since neither had
|
||
triggered a bundle rebuild yet. `Installieren.cmd` picks the bundle up automatically; no separate step
|
||
needed for a self-update-based deployment.
|
||
|
||
**Same-day follow-up from live owner feedback (2026.8.24.9):**
|
||
- **"Kein Licht" reverted to "Licht ausgeschaltet"** — the owner's original wording was right; my
|
||
interpretation of their earlier parenthetical note was wrong. Fixed in both frontends.
|
||
- **Locks simplified to one sensor**: dropped `HECKKLAPPE_SCHLOSS_SENSOR`/`HAUBE_SCHLOSS_SENSOR`
|
||
entirely (owner: central locking means individual locks can't be operated separately, so checking
|
||
them adds nothing), and `TUERSCHLOSS_SENSOREN` (4 positions) collapsed to a single
|
||
`TUERSCHLOSS_SENSOR` — the driver's door lock stands for the whole vehicle, same reasoning. `"Fahrzeug
|
||
verriegelt"` is now one sensor check, not six. No frontend code changes needed for this — both
|
||
`sicherheitsGruppe()`/`gruppenErgebnis()` only ever consume the flat, `gruppe`-tagged list generically,
|
||
never reference specific sensor-role names.
|
||
- **Real, previously-undiscovered bug found via the owner reporting "Nächster Service still empty" on a
|
||
genuinely blank instance** (Servicebuch cleared AND `erstzulassung` also blank - the owner's own test
|
||
container ended up in exactly this state): `naechsterTermin()`'s comparator, `(a, b) => a.ziel -
|
||
b.ziel`, silently coerces a `null` `ziel` to `0` (the Unix epoch) in JS arithmetic. A candidate with
|
||
genuinely no data (no Servicebuch entry, no vehicle meldung, and for Hauptuntersuchung specifically no
|
||
`erstzulassung` to fall back to) therefore sorted as "nearest" ahead of a *different* candidate that
|
||
had a real, computed date — because epoch-1970 always beats any real future date arithmetically. This
|
||
starved out Ölwechsel/Inspektion even when section M's meldung-fallback had correctly computed a real
|
||
number for them; the tile just showed "–" under "Hauptuntersuchung" instead. Not a regression from
|
||
section M — this comparator predates it and was always latent, just needed a candidate with a
|
||
genuinely absent `ziel` to surface. Fixed by filtering out `ziel === null` candidates before sorting,
|
||
returning `null` only when literally none of the three have a usable date (matching `vHome()`'s
|
||
existing, previously-dead `if (!t)` "kein Eintrag im Servicebuch" branch, which can now actually be
|
||
reached). companion-app's `naechsterService()` was checked and confirmed unaffected: it only ever
|
||
pushes a candidate when a real computed value exists (no null-`datum` candidates ever enter the array)
|
||
and sorts ISO date *strings* via `localeCompare()`, not raw arithmetic — structurally immune to this
|
||
class of bug.
|
||
- **Root cause of the browser not showing my fixes at first**: `audi-dashboard-app.js` is fetched by the
|
||
frontend with a cache-busting `?v=<manifest version>` query string. Editing the file on disk without
|
||
also bumping the manifest version left the URL unchanged, so the browser kept serving a cached copy
|
||
(confirmed via `performance.getEntriesByType('resource')` showing `transferSize: 0`) even across a
|
||
forced navigation and a hard-reload keystroke. Bumping the version forced a real refetch. Worth
|
||
remembering for any future panel-JS-only fix: a version bump isn't just the project's documentation
|
||
rule, it's load-bearing for actually seeing the change in a running browser tab.
|
||
|
||
Manifest bumped to `2026.8.24.9` for this follow-up round.
|
||
|
||
**A second same-day follow-up round (2026.8.24.10 / .11), more live owner feedback:**
|
||
- **Locks simplified further to a single sensor**: `TUERSCHLOSS_SENSOREN` (4 positions) collapsed to
|
||
one `TUERSCHLOSS_SENSOR` — owner: the central locking system locks/unlocks every door together, an
|
||
individual lock can't be operated separately, so the driver's door alone represents the whole
|
||
vehicle's lock state. "Fahrzeug verriegelt" is now exactly one sensor check. No frontend changes
|
||
needed (same reason as the Kofferraum/Motorhauben-Schloss removal above: both grouping helpers only
|
||
ever consume the flat, `gruppe`-tagged list, never a specific sensor-role name).
|
||
- **`vSicherheit()`'s route title renamed from "Sicherheit" to "Fahrzeugstatus"** in the panel, matching
|
||
what companion-app's `navigation.ts` already called this page (`TITEL.sicherheit`) — the two frontends
|
||
had drifted apart on this one label. The `FELDER` catalog's `"sicherheit"` *sensor-role group* heading
|
||
(Setup-Menü section, a different concept entirely — sensor categorization, not a page name) is
|
||
untouched.
|
||
- **A real, previously-undiscovered bug in `naechsterTermin()` (panel), found from the owner reporting
|
||
"I deleted everything in Servicebuch, why isn't Ölwechsel shown on Übersicht?"**: the sort comparator
|
||
`(a, b) => a.ziel - b.ziel` coerces a `null` `ziel` to `0` (the Unix epoch) under JS arithmetic. On
|
||
this owner's test container, both the Servicebuch *and* `erstzulassung` ended up empty at once, so the
|
||
Hauptuntersuchung candidate's `ziel` was genuinely `null` (no entry, no `erstzulassung` fallback
|
||
either) — and epoch-1970 sorts before any real future date, so HU won "nearest" over Ölwechsel even
|
||
though section M's meldung-fallback had correctly computed a real number for Ölwechsel. The tile
|
||
rendered "–" under "Hauptuntersuchung" instead of the real Ölwechsel figure. Not a regression from
|
||
section M — this comparator predates it and was always latent, just needed a candidate with a
|
||
genuinely absent `ziel` to expose it. Fixed by filtering out `ziel === null` candidates before
|
||
sorting, returning `null` only when literally none of the three have a usable date (the existing
|
||
`if (!t)` "kein Eintrag im Servicebuch" branch in `vHome()` can now actually be reached — it never
|
||
could be before, since `termine()` always returns exactly three entries and the old comparator always
|
||
produced *some* truthy result). companion-app's `naechsterService()` was checked and confirmed
|
||
structurally immune: it only ever pushes a candidate when a real computed value exists, and sorts ISO
|
||
date *strings* with `localeCompare()`, never raw arithmetic on a possibly-`null` value.
|
||
- **"Licht ausgeschaltet" reverted back from "Kein Licht"** — the owner's original wording was right;
|
||
my read of their earlier parenthetical was wrong. Fixed in both frontends' idle/ok-state label.
|
||
- **Cache-busting gotcha hit again, worse this time**: after the `naechsterTermin()` fix, the browser
|
||
kept showing the *old* broken behavior even after a forced navigation **and** a hard-reload keystroke
|
||
(`ctrl+shift+r`) — `performance.getEntriesByType('resource')` still showed `transferSize: 0` for
|
||
`audi-dashboard-app.js` afterward, meaning even a hard reload didn't bypass this particular cache.
|
||
Only bumping the manifest version (changing the `?v=` query string HA embeds when registering the
|
||
panel's script URL) forced a real refetch. Confirmed this isn't just a documentation nicety: **any**
|
||
panel-JS-only edit needs a version bump to actually be visible in a running browser tab, full stop —
|
||
a hard reload is not a reliable substitute.
|
||
- **Wide/desktop layout headline fixed** (`audi-dashboard-ios.css`, `@container (min-width:860px)`
|
||
block): sub-pages reached via the back arrow (Standort, Service, Fahrzeugstatus, Identität und
|
||
Technik, everything with `.back.on`) were rendering their title at `font-size:26px` — the owner
|
||
described this as "bold" in wide/desktop mode versus "not bold, centered" on the phone layout. There
|
||
is no actual `font-weight` bump anywhere in this stylesheet (`.title` is `font-weight:400`
|
||
everywhere, confirmed by grepping the whole file) — the 26px size alone, against an app that mostly
|
||
uses weight-300 body text (see `VERSIONIERUNG.md`/working-conventions "font weights 300/400 only"),
|
||
reads as heavy at a glance even at the *same* weight as every other title. This also violates the iOS
|
||
Human Interface Guidelines pattern this stylesheet otherwise deliberately follows: a "large title"
|
||
belongs to root/tab pages (`.title{font-size:34px}`, unscoped by `.back.on`, left untouched — the
|
||
owner didn't flag `Mein Audi`'s own big top-level title as wrong), while pushed/detail pages keep a
|
||
small, standard title regardless of screen size. Fixed by dropping the back-button-page override to
|
||
`font-size:17px` — the exact same value already used for these same pages in the narrow/phone layout
|
||
(`.phone:has(.back.on) .title` at the file's un-scoped, phone-default rule) — for genuine
|
||
cross-breakpoint consistency, not just a smaller number. Left `text-align:left` as-is: that's a
|
||
property of the wide layout's sidebar+column grid (`.topbar{justify-content:flex-start}`,
|
||
`.head{align-items:flex-start}`, both still scoped to `.back.on` and untouched), consistent with how
|
||
the *also-left-aligned* top-level large titles behave in this same layout — normal desktop-app
|
||
convention, not the trait the owner was actually flagging. Verified live at an actual `1280×800`
|
||
viewport (the container-query breakpoint needs a real wide render, not just the browser tool's
|
||
`desktop` preset, which turned out to stay narrow) — before the fix, "Service"/"Standort" rendered
|
||
visibly larger than their phone-layout counterparts; after, both match.
|
||
|
||
Manifest bumped to `2026.8.24.11` across this round. `npm run ota` rerun once more at the end to keep
|
||
the OTA bundle's embedded version aligned with the manifest (no further companion-app source changes
|
||
after the "Licht ausgeschaltet" text fix earlier in this same round, so this rebuild only updates the
|
||
stamped version number, not the bundle contents).
|
||
|
||
---
|
||
|
||
## Working conventions (observed — keep them)
|
||
|
||
- German is the project language: identifiers, comments, commits, UI texts. Exceptions:
|
||
`design-system/` uses English props/JSDoc (Claude Design audience) — and **this file**, which is
|
||
English by rule.
|
||
- Decisions are logged, including rejected ones (see the Traccar section of the architecture
|
||
doc); superseded sections stay in place marked "ÜBERHOLT" rather than being deleted.
|
||
- Numbers in `de-DE` format; font weights 300/400 only, never ≥600 (only those cuts exist).
|
||
- Real vehicle/movement data stays local: it lives in Home Assistant under
|
||
`/config/audi_dashboard/`, never in this repo. Only the placeholder template
|
||
(`custom_components/audi_dashboard/vorlage/fahrzeugprofil.json`) is versioned; the real receipt
|
||
PDFs under `tests/belegparser/belege/` are gitignored.
|
||
- **There is no second copy of the code to keep in sync any more.** Until 2026-08-23,
|
||
`installationspaket/` duplicated `pyscript/`, `www/` and `data/`, and every change had to be
|
||
mirrored by hand — the older entries below still say so. The installer now reads
|
||
`custom_components/audi_dashboard/` directly. Do not reintroduce a copy.
|
||
- Security principle: HA is never publicly exposed; only narrowly scoped surfaces (MQTT broker,
|
||
proxy allowlist) may be exposed, each by explicit decision.
|