Sicherheit-Feinschliff, Nächster-Service-Sortierfehler behoben, Fahrzeugstatus umbenannt

- 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>
This commit is contained in:
2026-08-24 17:00:33 +02:00
parent 5a048488ea
commit 7276d06a18
9 changed files with 143 additions and 41 deletions
+106 -4
View File
@@ -2568,10 +2568,9 @@ page is the old flat-list style filtered to just that group (`vTuerenKlappen()`,
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, **"Kein Licht" showed green** — the exact wording asked
for. 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.
"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 +
@@ -2579,6 +2578,109 @@ Manifest bumped to `2026.8.24.8`. `npm run ota` rerun (`scripts/ota-paket.ps1 -B
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)
+1 -1
View File
@@ -81,7 +81,7 @@ export function Fahrzeugstatus({ geheZu }: { geheZu: (name: SeitenName) => void
<div className="dm-sicherheitszeile">
<StatusKreis ok={licht} />
<span className="dm-sicherheitszeile__text">
{licht === null ? "Licht unbekannt" : licht ? "Kein Licht" : "Licht eingeschaltet"}
{licht === null ? "Licht unbekannt" : licht ? "Licht ausgeschaltet" : "Licht eingeschaltet"}
</span>
</div>
</div>
@@ -81,12 +81,11 @@ class Sensorzuordnung:
HECKKLAPPE_SENSOR: str = ""
HAUBE_SENSOR: str = ""
# Schlösser (getrennt von den Öffnungssensoren oben - zu heißt nicht
# verriegelt). Je vier Türschlösser, dazu Heckklappe/Motorhaube einzeln,
# wie bei den Öffnungssensoren.
TUERSCHLOSS_SENSOREN: list[str] = field(default_factory=list)
HECKKLAPPE_SCHLOSS_SENSOR: str = ""
HAUBE_SCHLOSS_SENSOR: str = ""
# Türschloss (getrennt von den Öffnungssensoren oben - zu heißt nicht
# verriegelt). Nur die Fahrertür: die Zentralverriegelung schließt immer
# alle Türen gemeinsam, ein einzelnes Türschloss lässt sich nicht separat
# bedienen - ihr Zustand gilt fürs ganze Fahrzeug (Nutzer-Entscheidung).
TUERSCHLOSS_SENSOR: str = ""
# Schiebedach (offen/zu) und Standlicht (an/aus) - ergänzen die
# Fenster-Gruppe bzw. stehen für sich, siehe _sicherheitscheck() in
@@ -173,18 +172,11 @@ FELDER: list[dict] = [
"hinweis": "\"aus\"/off = zu.",
"domains": ["binary_sensor"], "device_classes": ["door", "opening"], "units": [], "liste": False, "pflicht": False,
"stichworte": ["haube", "hood", "bonnet", "motorhaube"]},
{"key": "TUERSCHLOSS_SENSOREN", "label": "Türschlösser", "gruppe": "sicherheit",
"hinweis": "\"aus\"/off = verriegelt. Für \"Fahrzeug verriegelt\" wichtig.",
"domains": ["binary_sensor"], "device_classes": ["lock"], "units": [], "liste": True, "positionen": POSITIONEN,
"pflicht": False, "stichworte": ["türschloss", "schloss", "lock", "verriegelt"]},
{"key": "HECKKLAPPE_SCHLOSS_SENSOR", "label": "Kofferraum-Schloss", "gruppe": "sicherheit",
"hinweis": "\"aus\"/off = verriegelt.",
"domains": ["binary_sensor"], "device_classes": ["lock"], "units": [], "liste": False, "pflicht": False,
"stichworte": ["kofferraum", "heckklappe", "tailgate", "schloss", "lock"]},
{"key": "HAUBE_SCHLOSS_SENSOR", "label": "Motorhauben-Schloss", "gruppe": "sicherheit",
"hinweis": "\"aus\"/off = verriegelt.",
"domains": ["binary_sensor"], "device_classes": ["lock"], "units": [], "liste": False, "pflicht": False,
"stichworte": ["haube", "hood", "schloss", "lock"]},
{"key": "TUERSCHLOSS_SENSOR", "label": "Türschloss (Fahrertür)", "gruppe": "sicherheit",
"hinweis": "\"aus\"/off = verriegelt. Die Zentralverriegelung schließt alle Türen gemeinsam - "
"die Fahrertür genügt für \"Fahrzeug verriegelt\".",
"domains": ["binary_sensor"], "device_classes": ["lock"], "units": [], "liste": False,
"pflicht": False, "stichworte": ["türschloss", "schloss", "lock", "verriegelt", "fahrertür"]},
{"key": "DACH_SENSOR", "label": "Schiebedach", "gruppe": "sicherheit",
"hinweis": "\"aus\"/off = zu.",
"domains": ["binary_sensor"], "device_classes": ["window", "door", "opening"], "units": [], "liste": False, "pflicht": False,
@@ -1 +1 @@
{"version":"2026.8.24.8","sha256":"3eddc79605bc979da46476cf000c2fb501b8bc510148da60b56b0b49b9b603c6","bytes":233244,"gebaut":"2026-08-24T12:43:13Z"}
{"version":"2026.8.24.11","sha256":"c1a85f9a15b434121f4cd19e2729cfbe60ca89757163954f07152483b8b209af","bytes":233245,"gebaut":"2026-08-24T14:58:21Z"}
@@ -1177,7 +1177,16 @@ function teaser() {
<svg class="chev" viewBox="0 0 6 10"><polyline points="1,1 5,5 1,9"/></svg></button>` : ""}</div>`;
}
function naechsterTermin() { return termine().slice().sort((a, b) => a.ziel - b.ziel)[0]; }
// Kandidaten ohne ziel (kein Servicebucheintrag, keine Fahrzeugmeldung, bei
// der Hauptuntersuchung auch keine verwertbare Erstzulassung) müssen VOR dem
// Sortieren raus: (a,b) => a.ziel - b.ziel zwingt null auf 0 (Epoch 1970) und
// liess einen datenlosen Termin dadurch immer als "naechster" gewinnen -
// selbst gegen einen anderen Termin mit echtem, berechnetem Datum. null nur
// dann zurückgeben, wenn wirklich kein einziger Termin ein ziel hat.
function naechsterTermin() {
const mitZiel = termine().filter((t) => t.ziel !== null);
return mitZiel.length ? mitZiel.slice().sort((a, b) => a.ziel - b.ziel)[0] : null;
}
// CI-Icon je Serviceart statt eines Wort-Zusatzes ("bis zum Ölwechsel" o.ä.) -
// nur Ölwechsel/Inspektion haben eines, die Hauptuntersuchung nicht. Verwendet
@@ -1510,9 +1519,6 @@ function vSicherheit() {
const fensterDach = sicherheitsGruppe("fenster_dach");
const licht = sicherheitsGruppe("licht");
const sammelText = g === null ? "Zustand nicht vollständig bekannt" : g ? "Fahrzeug ist sicher abgestellt" : "Bitte prüfen";
// Jede Zeile nennt beim Nutzer-Zustand ("ok") genau die Formulierung, die
// der Nutzer vorgegeben hat - beim Licht ist das "Kein Licht", nicht
// "Licht ausgeschaltet" (das beschreibt nur die Prüfung, nicht den Wert).
const zeile = (label, ok, labelBad, labelWarn, route) => {
const text = ok === null ? labelWarn : ok ? label : labelBad;
const klickbar = !!route;
@@ -1534,7 +1540,7 @@ function vSicherheit() {
${zeile("Fahrzeug verriegelt", verriegelt.ok, "Fahrzeug nicht verriegelt", "Verriegelung unbekannt")}
${zeile("Türen und Klappen geschlossen", tuerenKlappen.ok, "Türen oder Klappen offen", "Türen/Klappen unbekannt", "tuerenklappen")}
${zeile("Fenster und Dach geschlossen", fensterDach.ok, "Fenster oder Dach offen", "Fenster/Dach unbekannt")}
${zeile("Kein Licht", licht.ok, "Licht eingeschaltet", "Licht unbekannt")}
${zeile("Licht ausgeschaltet", licht.ok, "Licht eingeschaltet", "Licht unbekannt")}
</div>`;
}
@@ -3866,7 +3872,7 @@ function render() {
else if (route.name === "reifen") { head = ["Fahrzeug", "Reifen"]; v.innerHTML = vReifen(); }
else if (route.name === "einst") { head = ["App", "Einstellungen"]; v.innerHTML = vEinst(); }
else if (route.name === "ident") { head = ["Fahrzeug", "Identität und Technik"]; v.innerHTML = vIdent(); }
else if (route.name === "sicherheit") { head = ["Fahrzeug", "Sicherheit"]; v.innerHTML = vSicherheit(); }
else if (route.name === "sicherheit") { head = ["Fahrzeug", "Fahrzeugstatus"]; v.innerHTML = vSicherheit(); }
else if (route.name === "tuerenklappen") { head = ["Fahrzeug", "Türen und Klappen"]; v.innerHTML = vTuerenKlappen(); }
else if (route.name === "vers") { head = ["Fahrzeug", "Versicherung/Steuer"]; v.innerHTML = vVers(); }
else if (route.name === "beitrag") { head = ["Versicherung", "Beitrag anpassen"]; v.innerHTML = vBeitrag(); }
@@ -275,7 +275,14 @@ main#view{--seitenrand:16px;padding:0 var(--seitenrand) 30px;scroll-behavior:smo
.topbar{grid-column:2;grid-row:1;padding:38px 44px 8px;max-width:860px}
.phone:has(.back.on) .topbar{grid-column:2;grid-row:1;padding:34px 44px 8px;justify-content:flex-start;max-width:860px}
.phone:has(.back.on) .head{align-items:flex-start}
.phone:has(.back.on) .title{font-size:26px;text-align:left}
/* Unterseiten (mit Zurueck-Pfeil, z. B. Standort/Service) behalten die
normale Titelgroesse - nur die obersten Tab-Seiten bekommen den grossen
"Large Title"-Auftritt (.title{font-size:34px} unten). Vorher wurden
Unterseiten hier auf 26px vergroessert, was neben dem sonst durchgaengig
300er-Fliesstext optisch wie Fettschrift wirkte, obwohl font-weight
ueberall 400 bleibt - keine echte Auszeichnung, nur eine Grosse, die zu
einer Unterseite nicht passt. */
.phone:has(.back.on) .title{font-size:17px;text-align:left}
.title{font-size:34px}
.ptr{grid-column:2;grid-row:2;max-width:860px}
main#view{--seitenrand:44px;grid-column:2;grid-row:3;padding:8px var(--seitenrand) 48px;max-width:860px}
@@ -1,7 +1,7 @@
{
"domain": "audi_dashboard",
"name": "Audi Dashboard",
"version": "2026.8.24.8",
"version": "2026.8.24.11",
"documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md",
"issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues",
"codeowners": ["@paul"],
@@ -124,15 +124,10 @@ def _sicherheitscheck(hass: HomeAssistant, werte: Sensorzuordnung) -> list[dict]
):
w = zustand_oder_none(hass, sensor)
eintraege.append({"label": label, "ok": None if w is None else w == "off", "gruppe": "tueren_klappen"})
for pos, sensor in zip(POSITIONEN, werte.TUERSCHLOSS_SENSOREN):
w = zustand_oder_none(hass, sensor)
eintraege.append({"label": f"Türschloss {pos}", "ok": None if w is None else w == "off", "gruppe": "verriegelt"})
for label, sensor in (
("Kofferraum-Schloss", werte.HECKKLAPPE_SCHLOSS_SENSOR),
("Motorhauben-Schloss", werte.HAUBE_SCHLOSS_SENSOR),
):
w = zustand_oder_none(hass, sensor)
eintraege.append({"label": label, "ok": None if w is None else w == "off", "gruppe": "verriegelt"})
# Nur die Fahrertür: die Zentralverriegelung schließt alle Türen gemeinsam,
# ihr Zustand gilt fürs ganze Fahrzeug (siehe TUERSCHLOSS_SENSOR).
w = zustand_oder_none(hass, werte.TUERSCHLOSS_SENSOR)
eintraege.append({"label": "Türschloss", "ok": None if w is None else w == "off", "gruppe": "verriegelt"})
w = zustand_oder_none(hass, werte.DACH_SENSOR)
eintraege.append({"label": "Dach", "ok": None if w is None else w == "off", "gruppe": "fenster_dach"})
w = zustand_oder_none(hass, werte.LICHT_SENSOR)