# AGENTS.md — Project state, review findings, open items, and working rules

**Last updated: 2026-08-11** (after building the app and the native iOS hull — phases 1, 2, 4–11 of `UMSETZUNGSPLAN.md`). This file is the entry point for every new agent
session: what this repo is, what is finished, what is missing, and how to work here. Detail lives in
the linked documents — this file points, it does not duplicate.

> **Maintenance rule (binding):** whenever you change this repo in a way that affects anything
> recorded here — completing an open item, making or reversing a decision, adding/removing a
> component, discovering a new gap — **update this file in the same session** (tick the checkbox,
> adjust the status table, bump the "Last updated" date). This file must always be written in
> **English**, even though the rest of the project is German.

**How this file is loaded:** Claude Code does not read `AGENTS.md` natively; the root `CLAUDE.md`
imports it via `@AGENTS.md` (official recommended pattern). Other agents (Codex, Cursor, Copilot)
read `AGENTS.md` directly. Edit content here, not in `CLAUDE.md`.

---

## Working rules

### Karpathy guidelines (adopted as project standard)

From [`multica-ai/andrej-karpathy-skills`](https://github.com/multica-ai/andrej-karpathy-skills),
behavioral rules derived from Andrej Karpathy's observations on LLM coding failures. They bias
toward caution over speed — for trivial tasks, use judgment.

1. **Think before coding.** Don't assume. Don't hide confusion. Surface tradeoffs. State
   assumptions explicitly; if multiple interpretations exist, present them — don't pick silently.
   If a simpler approach exists, say so. Push back when warranted. If something is unclear, stop
   and ask.
2. **Simplicity first.** Minimum code that solves the problem. Nothing speculative: no features
   beyond what was asked, no abstractions for single-use code, no unrequested configurability, no
   error handling for impossible scenarios. If 200 lines could be 50, rewrite.
3. **Surgical changes.** Touch only what you must. Don't "improve" adjacent code, comments, or
   formatting; don't refactor what isn't broken; match existing style. Remove only orphans **your**
   change created; mention (don't delete) pre-existing dead code. Every changed line should trace
   directly to the request.
4. **Goal-driven execution.** Define success criteria, loop until verified. "Fix the bug" → write a
   test that reproduces it, then make it pass. For multi-step tasks, state a brief plan with a
   verify step per item.

### Claude Code practices (current guidance, 2026)

- Keep this file **concise and specific** — instructions concrete enough to verify ("run X before
  Y"), markdown headers/bullets, no dense prose. Context files reduce adherence as they grow;
  target roughly ≤200 lines of actual instruction.
- Don't record here what the code or git history already shows (directory dumps, dependency
  lists) — record pitfalls, rationale, decisions, and conventions that differ from defaults.
- Conflicting instructions get picked arbitrarily — when updating, **remove** superseded rules
  rather than stacking corrections.
- Verify loading with `/context` (file must appear under **Memory files**); edit via `/memory`.

---

## 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, barely built)
5. `AUDIT_2026-08-10.md` — accessibility/platform audit of the panel (partly done, rest below)
6. `bauauftrag.md` — **historical only**; code has diverged (see SPECIFICATION.md §7)

## The three projects in this repo

| Area | What | Status |
|---|---|---|
| `homeassistant/` | HA panel (`panel_custom`): pyscript backend + vanilla-JS frontend | ✅ **finished, in use** — to be replaced by the app |
| `testumgebung/` | Script that rebuilds a throwaway Home Assistant with the real backend | ✅ new, reproducible |
| `design-system/` | React component library `@audi-dash/ui`, brand-free, feeds Claude Design | ✅ done as a kit (20 components, 1,690 lines) |
| `companion-app/` | **DataMetric360** — successor app (web, PWA, native iOS/Android via Capacitor, HA iframe); will **replace** the panel | ✅ **all 21 screens built and tested**; runs natively on iOS with real data |
| `design/` | Export of the Claude Design draft for DataMetric360 | 🚧 interim and now behind the code — screens were derived from the old panel instead (owner's decision) |

Root files: `dashboard-muster*.html` = original static prototype (superseded, reference only),
`bauauftrag.md`/`.html` = original build brief (historical), `DESIGN_BRIEF_DATAMETRIC360.md` = the
prompt for the Claude Design project.

**`homeassistant/` in one paragraph:** 10 pyscript scripts + 4 modules (`pyscript/modules/`); trip
detection via the iPhone WLAN sensor with pause tolerance, two-stage trip completion via HA history
screening (odometer often updates only on the next trip), fill-up detection on fuel-level rise,
Shell PDF parser (`data/shell_beleg_parser.py`, subprocess, the only tested part of the repo), tire
km counter, backup, self-update, image management. Frontend: one file `www/audi-dashboard-app.js`
(3,130 lines, custom element, no framework/bundler), 5 tabs + ~19 detail routes, cache-busting via
`audi-dashboard-version.json` + loader stub. Entity IDs configured centrally in
`pyscript/modules/einstellungen.py` (the one file edited before install). Deploy: `update.ps1`
(robocopy to Samba share) or — still inactive — self-update from git.

**`companion-app/` — what exists:** the full app. Data layer (`src/api/`: REST, WebSocket with
reconnect backoff, persistent offline write queue, credential storage), domain logic
(`src/daten/`: profile adapter, statistics, service forecast, data context), all 21 screens
(`src/screens/`), Audi assets (`src/assets/audi/`), PWA manifest and icons. Verified by 90 unit
and render tests plus 9 checks against a live Home Assistant. `npm run dev` in the repo root
starts it; `testumgebung/aufsetzen.sh` provides the server side.

**DataMetric360 architecture (short — details in `COMPANION_APP_ARCHITECTURE.md`):**
- Future data source: **Teltonika FMM003** on the CAN bus, fully replacing the iPhone WLAN sensor
  and the VAG integration.
- **Current data path (since 2026-08-11): Codec JSON → MQTT/TLS → Mosquitto add-on → HA.** Traccar
  is **dropped** (the decision log deliberately remains in the architecture doc, marked ÜBERHOLT).
- 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)

### HA panel — known gaps (most also in SPECIFICATION.md §7)

- **GPS is dead schema:** `start_lat/lon`, addresses, `route`, `avg_speed_kmh` never populated.
  The trip-detail map draws a **fabricated** line via `fakeTrack()`
  (`audi-dashboard-app.js:414`) — not a real track.
- **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:** `BATTERIE_SENSOR = ""` (entire battery-history feature is a no-op) and
  `UPDATE_REPO_URL = ""` (self-update inactive), both in `pyscript/modules/einstellungen.py`.
  `www/bilder/` has no vehicle photos (all slots show placeholders); `steuer.faellig` unset.
- **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`.
- `INSTALL.md` step 4 names variables that no longer exist (`DOORS_SENSOR`, `WINDOWS_SENSOR`,
  `LOCK_ENTITY`, `BATTERY_VOLTAGE_SENSOR`) — actual names: `TUER_SENSOREN`/`FENSTER_SENSOREN`/
  `TUERSCHLOSS_SENSOREN`/`BATTERIE_SENSOR`.
- `homeassistant/README.md:110` mentions the removed 97% full-tank rule (removal documented in
  `belegverarbeitung.py:18-20`); the README file list omits 5 pyscript files.
- Obsolete comment `belegverarbeitung.py:41` ("TODO: Datei ablegen" — file has long existed).

### Audit leftovers — two of three now fixed in the new app

- ✅ Swipe-to-delete without a gesture-free fallback — **fixed in the app**: every list row also
  carries an always-visible "…" menu (`companion-app/src/screens/Zeilenmenue.tsx`). Still open in
  the old panel, which is being replaced anyway.
- ✅ Leaflet from a CDN — **fixed in the app**: bundled from node_modules as a lazy chunk.
- 🟡 Popup close-by-tap-outside in the old panel: still unchecked (the app uses the library's
  Popup, which is keyboard-reachable).
- Audit's own note: these three may be better done in DataMetric360 than retrofitted — the panel
  gets replaced anyway.

### companion-app / design-system ✅ RESOLVED 2026-08-11

- **Not wired together** — fixed: an npm workspace in the repo root links `@audi-dash/ui` into the
  app as a real dependency.
- Both packages: no `node_modules`, no `dist` — smoke tests need `npm install` first
  (design-system additionally `npm run build`; `scripts/smoke.mjs` imports from `../dist/`).
- No unit tests, no Storybook (substitute: SSR smoke over 21 cases in design-system).
- `design/` export (2026-08-11) has two known, already-commissioned fixes not yet in the export:
  shows **RS 6 instead of RS 4**, and **16 sub-pages** the HA panel already has are missing
  (list in `design/README.md`).

---

### Found while building (2026-08-11) — all fixed

Three defects that only surfaced by running against a real Home Assistant, not by reading code:

1. **The data layer declared field names the backend never sends.** `Fahrzeugstatus` had
   `tank_prozent`/`sicher_abgestellt`/`sicherheit`; the backend writes
   `tankprozent`/`gesichert`/`sicherheitscheck`. Every screen would have read `undefined` without
   anything failing. `technik`/`ausstattung` were typed as objects but are arrays.
2. **The profile adapter handed out live references into the raw profile.** Editing a form would
   have silently mutated the baseline and broken the promise never to overwrite the
   backend-maintained tire odometer. Sections are copied now.
3. **`design-system` still carried the pre-audit `--fg3: #657081`** (3.0:1 on `--tile`, fails WCAG
   AA) that the panel had already fixed to `#8a94a3`. The new app would have inherited a
   already-solved contrast defect.

Also: two TypeScript parameter properties in the data layer broke Node's strip-only mode, which is
what the smoke scripts run on — rewritten as plain fields.

Three more that only screenshots revealed — nothing failed, the pixels were simply wrong:

4. **The vehicle block on the home screen was invisible.** `overflow: hidden` sets a flex item's
   automatic minimum size to 0, so once the page was taller than the screen, flex-shrink squashed
   the block to zero height and took model name, badge and plate with it. Fixed with
   `.dm-inhalt > * { flex: none }`.
5. **The model name appeared twice** — once as the badge image, once as text beside it.
6. **Filenames overflowed the gallery thumbnails.**

And one in the backend, found by watching the log against a current Home Assistant: the odometer
screening called `urlopen` directly, which HA aborts as a blocking call since 2026.8. Every trip
stayed without a distance, visible only as a warning. Now runs through `task.executor`.

**CORS is a real constraint for this app.** `cors_allowed_origins` did not take effect on HA
2026.8 (preflight 403 even same-origin). Two consequences, both handled: the web build is served
from Home Assistant itself (`/local/dm360/`, same origin — the planned deployment anyway), and the
native hull enables `CapacitorHttp`, which routes fetch through native HTTP where CORS does not
apply. Verify this again at commissioning if the app ever moves to a separate hostname.

**A self-written QR encoder was discarded.** It disagreed with a reference implementation on 1239
of 3249 modules — the code would have been unreadable. `homeassistant/www/dm360-qr.html` now uses a
vendored MIT library served from Home Assistant itself, which satisfies the actual requirement
(no network call, token never leaves the local network) and round-trips correctly.

## Pending: this branch has diverged from `main` (noted 2026-08-13)

`ha_install.md` (root) plans the move from pyscript to a **native HA integration** with a config
flow — UI-only setup, no YAML, and updates via a self-reporting `UpdateEntity` against the Gitea
repo (HACS is GitHub-only, so it is not an option). All APIs in it were verified against the
running 2026.8.1 instance. It also records which review findings that move eliminates by design.

A full review of `main`'s 18 new commits is in `REVIEW_main_2026-08-13.md` — 15 findings, the
three most serious in the new setup menu (saving with an unloaded catalogue wipes the whole
mapping; "reset" has no effect on 15 of 17 fields; all four list positions get the same sensor,
which makes "securely parked" report safe while three doors were never checked).

`main` has moved 18 commits ahead of `umsetzung-datametric360` (FMM003 switch, sensor-mapping setup
menu, iOS/large-screen overlay from Claude Design). **Deliberate decision: do not merge yet** — the
owner keeps working on `main` first.

Three files conflict (`AGENTS.md`, `homeassistant/INSTALL.md`,
`homeassistant/pyscript/fahrterkennung.py`); four more are touched by both sides but merge cleanly.

**One hazard that a clean merge will not catch.** This branch changed `profil_lesen()` in
`homeassistant/pyscript/modules/profil.py` to return `None` when the profile file is missing or
corrupt, and guarded all seven call sites that existed here. `profil.py` is untouched on `main`, so
it merges silently — but `main`'s call sites (`fahrterkennung.py`, `tankerkennung.py`,
`reifenzaehler.py`, `modules/frontend_veroeffentlichung.py`) do **not** guard against `None` and
would raise `AttributeError` on a missing profile instead of logging a clear error.

When merging: take `main`'s FMM003 version of `fahrterkennung.py`, then re-apply the `None` guard to
every remaining `profil.profil_lesen()` call site.

## Open items

Execution order, exact steps, and acceptance criteria for every item below live in
`UMSETZUNGSPLAN.md` (phases 1–13). Additional decisions of 2026-08-11: **no Electron** (Capacitor
wraps the web app for iPhone; a PWA home-screen install is the accepted intermediate step) and
**no separate backend** (the app talks to the HA REST/WebSocket API directly).

### A) Build DataMetric360 (the big block)

- [x] Fix the Claude Design draft (RS 4, not RS 6) and extend it by the 16 missing sub-pages;
      then re-export to `design/`
- [x] `companion-app`: set up Vite + React + Capacitor scaffold; wire `@audi-dash/ui` as a real
      dependency (possibly add a workspace/monorepo root)
- [x] Implement the screens from the design draft on top of the existing `DataMetricApi` layer
- [x] Add Audi assets (fonts/rings/badges) at implementation time from `homeassistant/www/` —
      **never** into `design-system/`
- [x] Secure storage for the LLAT (iOS Keychain / Android Keystore via Capacitor plugin; the
      `ablageSetzen()` hook already exists)
- [x] Onboarding: manual token paste (required); QR scan only if it stays simple (QR generated
      locally under HA `/local/`, architecture §3)
- [x] Authenticated smoke tests of the data layer (reads, service calls, queue round-trip)
      against `audi_ha_test` with a real token
- [x] Offline UX per design brief (offline marker, visible pending queue)

### B) Infrastructure / commissioning (partly waits for FMM003 hardware)

- [ ] Switch `datametric360.app` nameservers at all-inkl to Cloudflare ("full setup") —
      prerequisite for the tunnel; domain carries nothing else, so this is consequence-free
- [ ] Decide hostname split (app on apex + API on `api.` subdomain, or vice versa)
- [ ] Choose reverse proxy (Nginx Proxy Manager vs. Traefik) — **can be done before hardware**
      against the existing HA API
- [x] Define the reverse-proxy path allowlist (depends on final entity/service names)
- [ ] Install/wire the FMM003; record firmware version (Codec JSON is firmware-dependent)
- [ ] Generate TLS certificates for Mosquitto + device (small private CA); FMM003 refuses MQTT
      without them
- [ ] Decide broker reachability for the vehicle: port-forward 8883 vs. VPS broker with
      Mosquitto bridge over Tailscale
- [ ] Capture the first real Codec JSON message (`mosquitto_sub`/MQTT Explorer) and build the
      field mapping from it — **do not guess beforehand** (explicit decision)
- [ ] Move trip detection to FMM003 ignition (reuse the `fahrterkennung.py` pattern, keep pause
      tolerance); then remove `sensor.iphone_wifi_connection` from `einstellungen.py`

### C) Maintain the existing HA panel (low priority — being replaced)

- [x] Fix documentation drift (statistics claim, INSTALL variable names, README gaps, obsolete
      TODO comment) — text-only changes
- [x] Harden `profil_lesen()` against missing/corrupt `fahrzeugprofil.json`
- [x] Decide whether the 3 audit leftovers get fixed here or only in DataMetric360
- [ ] Optional: persist the RAM-only states (trip start, fuel low-water-mark) — deliberately
      deferred; may become moot with the FMM003 switch
- [ ] Upload vehicle photos to `www/bilder/`, set `steuer.faellig` (operational data, not code)

### D) Once the panel is superseded

- [ ] **Archive** `audi-dashboard-app.js`, don't delete (settled decision, architecture §1)

---

## Working conventions (observed — keep them)

- German is the project language: identifiers, comments, commits, UI texts. Exceptions:
  `design-system/` uses English props/JSDoc (Claude Design audience) — and **this file**, which is
  English by rule.
- Decisions are logged, including rejected ones (see the Traccar section of the architecture
  doc); superseded sections stay in place marked "ÜBERHOLT" rather than being deleted.
- Numbers in `de-DE` format; font weights 300/400 only, never ≥600 (only those cuts exist).
- Real vehicle/movement data stays local (`data/` contents and receipt PDFs are gitignored).
- Security principle: HA is never publicly exposed; only narrowly scoped surfaces (MQTT broker,
  proxy allowlist) may be exposed, each by explicit decision.
