diff --git a/AGENTS.md b/AGENTS.md
index d025278..1940ad8 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1425,6 +1425,45 @@ wraps the web app for iPhone; a PWA home-screen install is the accepted intermed
Ü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 `.tile.flat` (gleiche Ebene wie die Reichweite darüber, kein
+ eigener Kachelhintergrund) — Zeile „Nächster Service:“, Chevron bündig 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 `1787006000`, synchron in `installationspaket/`.
+ Entwurf und Begründung als Artefakt: „Zwei Kacheln, drei Antworten“.
+ **Noch offen:** die Änderung existiert nur im Panel, nicht in `companion-app/` — dort steht
+ die Übersicht weiter mit dem alten Aufbau (siehe den Drift-Eintrag weiter oben; die beiden
+ Codebasen laufen sonst wieder auseinander).
- [ ] 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`
diff --git a/homeassistant/installationspaket/www/audi-dashboard-app.js b/homeassistant/installationspaket/www/audi-dashboard-app.js
index 918b144..12a974a 100644
--- a/homeassistant/installationspaket/www/audi-dashboard-app.js
+++ b/homeassistant/installationspaket/www/audi-dashboard-app.js
@@ -324,14 +324,9 @@ function heute() { return new Date(); }
const dTag = 86400000;
/* Ein unlesbares Datum ergibt einen Strich, keine Rechenreste. dat() erwartet
TT.MM.JJJJ; steht im Profil etwas anderes (es wird laut INSTALL.md von Hand
- gepflegt, z. B. versehentlich im ISO-Format), lieferte mmjjjj() sonst
- "NaN/NaN" - ein Wert, der wie eine Angabe aussieht und keine ist. */
+ gepflegt, z. B. versehentlich im ISO-Format), lieferte dedat() sonst
+ "NaN.NaN.NaN" - ein Wert, der wie eine Angabe aussieht und keine ist. */
const gueltigesDatum = (d) => d instanceof Date && !Number.isNaN(d.getTime());
-/* Volles Jahr, weil das Datum überall als eigene Zeile steht und nicht mehr
- in ein Zahlenfeld gequetscht wird (früher mmjj() mit "04/27"). */
-const mmjjjj = (d) => (gueltigesDatum(d)
- ? String(d.getMonth() + 1).padStart(2, "0") + "/" + d.getFullYear()
- : "–");
const dedat = (d) => (gueltigesDatum(d)
? d.toLocaleDateString("de-DE", { day: "2-digit", month: "2-digit", year: "numeric" })
: "–");
@@ -1094,6 +1089,15 @@ function teaser() {
function naechsterTermin() { return termine().slice().sort((a, b) => a.ziel - b.ziel)[0]; }
+/* "der Ölwechsel", aber "die Inspektion"/"die Hauptuntersuchung" - ohne diese
+ Zuordnung stünde auf der Übersicht "bis zum Inspektion". */
+const ART_BIS = {
+ "Ölwechsel": "bis zum Ölwechsel",
+ "Inspektion": "bis zur Inspektion",
+ "Hauptuntersuchung": "bis zur Hauptuntersuchung",
+};
+const bisText = (art) => ART_BIS[art] || `bis zum nächsten Service (${art})`;
+
function intervalle(t) {
const oel = CAR.oel, b = t.basis;
if (!b) return null;
@@ -1245,34 +1249,55 @@ function vHome() {
Reichweite
-
-
Kilometerstand
-
${CAR.odoBekannt ? de(CAR.odo) : "–"}km
- ${(() => { if (!t) return `
Service
kein Eintrag
`;
- const artLabel = t.art === "Hauptuntersuchung" ? "Haupt- untersuchung" : esc(t.art);
- const restKm = t.km !== null && CAR.odoBekannt ? Math.abs(t.km - CAR.odo) : null;
- const datum = t.kmDatum || t.datum;
- /* Ein Wert pro Zahlenfeld, der Rest als leisere Zweitzeile darunter.
- Vorher stand hier "9,0 tkm/ 04/27" - zwei Einheiten in einem Slot,
- eine davon erfunden ("tkm" schreibt niemand, und die Mein-Audi-Kachel
- nannte dieselbe Zahl schon immer "9.029 km"), auf 375px zweizeilig
- umgebrochen. Apple HIG "Layout" ("don't obscure [essential
- information] by crowding it with nonessential details") und
- "Typography" (Hierarchie über Größe/Farbe) verlangen die Trennung -
- deckungsgleich mit der Audi-CI, wo .fig genau EINE große Zahl trägt.
- Kachel ist jetzt ein Knopf: hinter ihr liegt die Service-Seite, das
- war bislang eine Sackgasse ohne Hinweis. */
- const hauptwert = restKm !== null
- ? `${de(restKm)}km`
- : (datum ? mmjjjj(datum) : "–");
- const nebenzeile = restKm !== null && datum ? `vsl. ${mmjjjj(datum)}` : "";
- return ``; })()}
-
+ ${(() => {
+ /* Kilometerstand und Service standen bis 2026-08-17 als zwei gleich große
+ Kacheln nebeneinander - gleiches Maß, gleiche Zahlengröße, gleiche Farbe,
+ obwohl nur eine davon ein Knopf war und die beiden Zahlen Grundver-
+ schiedenes meinen: der Kilometerstand ist ein gemessener Ist-Wert, der
+ sich kaum ändert, der Service-Wert ein Countdown, der täglich kleiner
+ wird. Zwei gleich große Zahlen nebeneinander behaupteten damit eine
+ Gleichrangigkeit, die inhaltlich nicht besteht.
+
+ Jetzt: eine führende Kennzahl, darunter der Kilometerstand als ruhige
+ Wertezeile, getrennt durch eine Haarlinie - Audi-CI (Flächen und
+ Haarlinien statt gestapelter Karten, Hierarchie über Größe und Farbe)
+ kombiniert mit Apple HIGs "differentiate controls from content": der
+ Chevron sitzt nur am Serviceblock, damit erkennbar bleibt, welcher Teil
+ auf die Service-Seite führt und welcher reine Anzeige ist. Der Block
+ liegt bewusst in derselben Ebene wie die Reichweite darüber (.tile flat,
+ kein eigener Kachelhintergrund). */
+ const kmZeile = `
+ Kilometerstand
+
${CAR.odoBekannt ? de(CAR.odo) : "–"}km
+
`;
+ if (!t) return `
+ Nächster Service:
+
kein Eintrag im Servicebuch
+ ${kmZeile}
+
`;
+ // naechsterTermin() waehlt bereits ueber alle drei Arten hinweg (Oelwechsel,
+ // Inspektion, Hauptuntersuchung) den zeitlich naechsten aus.
+ const restKm = t.km !== null && CAR.odoBekannt ? Math.abs(t.km - CAR.odo) : null;
+ const datum = t.kmDatum || t.datum;
+ const hauptwert = restKm !== null
+ ? `${de(restKm)}km`
+ : (datum ? dedat(datum) : "–");
+ // Ohne Restkilometer steht das Datum selbst im Zahlenfeld - dann kleiner,
+ // damit "15.04.2027" auf 375px nicht umbricht.
+ const zahlGroesse = restKm !== null ? 40 : 30;
+ const nebenzeile = restKm !== null && datum
+ ? `${bisText(t.art)} · vsl. ${dedat(datum)}`
+ : bisText(t.art);
+ return `
+
+ ${kmZeile}
+
`;
+ })()}
${standortKachel()}
${teaser()}`;
}
diff --git a/homeassistant/installationspaket/www/audi-dashboard-ios.css b/homeassistant/installationspaket/www/audi-dashboard-ios.css
index 2b24bc9..a97c020 100644
--- a/homeassistant/installationspaket/www/audi-dashboard-ios.css
+++ b/homeassistant/installationspaket/www/audi-dashboard-ios.css
@@ -307,7 +307,6 @@ main#view{--seitenrand:16px;padding:0 var(--seitenrand) 30px;scroll-behavior:smo
deshalb hier mit !important. */
.rings{display:none !important}
- .duo{grid-template-columns:repeat(3,minmax(0,1fr))}
/* Nur die Box selbst hoeher setzen, nicht jedes Element mit .carfix: die
Klasse haengt auch am und am Platzhalter-
(siehe
bildMitPlatzhalter()). Der Platzhalter ist position:absolute;inset:0 -
diff --git a/homeassistant/installationspaket/www/audi-dashboard-version.json b/homeassistant/installationspaket/www/audi-dashboard-version.json
index dc1f246..dcb0a42 100644
--- a/homeassistant/installationspaket/www/audi-dashboard-version.json
+++ b/homeassistant/installationspaket/www/audi-dashboard-version.json
@@ -1 +1 @@
-{"version": 1787003000}
+{"version": 1787006000}
diff --git a/homeassistant/installationspaket/www/audi-dashboard.css b/homeassistant/installationspaket/www/audi-dashboard.css
index 7fced30..dabe736 100644
--- a/homeassistant/installationspaket/www/audi-dashboard.css
+++ b/homeassistant/installationspaket/www/audi-dashboard.css
@@ -463,8 +463,26 @@ button.tile, .tilebtn { transition: background .15s, transform .1s; }
/* Der Außenabstand sitzt am Raster, nicht an den Kacheln - sonst wird er im
2×2-Raster (vHome) zum zusätzlichen internen Zeilenabstand und der Abstand
innerhalb der Gruppe wäre größer als der zur nächsten. */
-.duo { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-3); margin-bottom: var(--sp-5); }
-.duo .tile { margin-bottom: 0; }
+/* Übersicht: "Nächster Service" als flache, tippbare Kennzahl, darunter der
+ Kilometerstand als Wertezeile (siehe Kommentar in vHome()). Der Block sitzt
+ in einer .tile.flat, hat also keinen eigenen Kachelhintergrund - deshalb
+ sitzt der Chevron bündig am oberen rechten Rand statt auf --sp-5 eingerückt
+ wie sonst in einer gepolsterten Kachel. */
+.serviceblock {
+ position: relative;
+ display: block; width: 100%;
+ border: 0; background: none; padding: 0;
+ font-family: inherit; text-align: left; color: inherit; cursor: pointer;
+}
+.serviceblock > .go { top: 3px; right: 0; }
+/* Beschriftung nie unter dem Chevron hindurchlaufen lassen */
+.serviceblock > .label:first-child { padding-right: 24px; }
+.servicezeile {
+ display: flex; align-items: baseline; justify-content: space-between;
+ gap: var(--sp-4);
+ border-top: 1px solid var(--line);
+ margin-top: 14px; padding-top: 13px;
+}
.badge { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.badge img { height: 18px; width: auto; display: none; }
diff --git a/homeassistant/www/audi-dashboard-app.js b/homeassistant/www/audi-dashboard-app.js
index 918b144..12a974a 100644
--- a/homeassistant/www/audi-dashboard-app.js
+++ b/homeassistant/www/audi-dashboard-app.js
@@ -324,14 +324,9 @@ function heute() { return new Date(); }
const dTag = 86400000;
/* Ein unlesbares Datum ergibt einen Strich, keine Rechenreste. dat() erwartet
TT.MM.JJJJ; steht im Profil etwas anderes (es wird laut INSTALL.md von Hand
- gepflegt, z. B. versehentlich im ISO-Format), lieferte mmjjjj() sonst
- "NaN/NaN" - ein Wert, der wie eine Angabe aussieht und keine ist. */
+ gepflegt, z. B. versehentlich im ISO-Format), lieferte dedat() sonst
+ "NaN.NaN.NaN" - ein Wert, der wie eine Angabe aussieht und keine ist. */
const gueltigesDatum = (d) => d instanceof Date && !Number.isNaN(d.getTime());
-/* Volles Jahr, weil das Datum überall als eigene Zeile steht und nicht mehr
- in ein Zahlenfeld gequetscht wird (früher mmjj() mit "04/27"). */
-const mmjjjj = (d) => (gueltigesDatum(d)
- ? String(d.getMonth() + 1).padStart(2, "0") + "/" + d.getFullYear()
- : "–");
const dedat = (d) => (gueltigesDatum(d)
? d.toLocaleDateString("de-DE", { day: "2-digit", month: "2-digit", year: "numeric" })
: "–");
@@ -1094,6 +1089,15 @@ function teaser() {
function naechsterTermin() { return termine().slice().sort((a, b) => a.ziel - b.ziel)[0]; }
+/* "der Ölwechsel", aber "die Inspektion"/"die Hauptuntersuchung" - ohne diese
+ Zuordnung stünde auf der Übersicht "bis zum Inspektion". */
+const ART_BIS = {
+ "Ölwechsel": "bis zum Ölwechsel",
+ "Inspektion": "bis zur Inspektion",
+ "Hauptuntersuchung": "bis zur Hauptuntersuchung",
+};
+const bisText = (art) => ART_BIS[art] || `bis zum nächsten Service (${art})`;
+
function intervalle(t) {
const oel = CAR.oel, b = t.basis;
if (!b) return null;
@@ -1245,34 +1249,55 @@ function vHome() {
Reichweite
-
-
Kilometerstand
-
${CAR.odoBekannt ? de(CAR.odo) : "–"}km
- ${(() => { if (!t) return `
Service
kein Eintrag
`;
- const artLabel = t.art === "Hauptuntersuchung" ? "Haupt- untersuchung" : esc(t.art);
- const restKm = t.km !== null && CAR.odoBekannt ? Math.abs(t.km - CAR.odo) : null;
- const datum = t.kmDatum || t.datum;
- /* Ein Wert pro Zahlenfeld, der Rest als leisere Zweitzeile darunter.
- Vorher stand hier "9,0 tkm/ 04/27" - zwei Einheiten in einem Slot,
- eine davon erfunden ("tkm" schreibt niemand, und die Mein-Audi-Kachel
- nannte dieselbe Zahl schon immer "9.029 km"), auf 375px zweizeilig
- umgebrochen. Apple HIG "Layout" ("don't obscure [essential
- information] by crowding it with nonessential details") und
- "Typography" (Hierarchie über Größe/Farbe) verlangen die Trennung -
- deckungsgleich mit der Audi-CI, wo .fig genau EINE große Zahl trägt.
- Kachel ist jetzt ein Knopf: hinter ihr liegt die Service-Seite, das
- war bislang eine Sackgasse ohne Hinweis. */
- const hauptwert = restKm !== null
- ? `${de(restKm)}km`
- : (datum ? mmjjjj(datum) : "–");
- const nebenzeile = restKm !== null && datum ? `vsl. ${mmjjjj(datum)}` : "";
- return ``; })()}
-
+ ${(() => {
+ /* Kilometerstand und Service standen bis 2026-08-17 als zwei gleich große
+ Kacheln nebeneinander - gleiches Maß, gleiche Zahlengröße, gleiche Farbe,
+ obwohl nur eine davon ein Knopf war und die beiden Zahlen Grundver-
+ schiedenes meinen: der Kilometerstand ist ein gemessener Ist-Wert, der
+ sich kaum ändert, der Service-Wert ein Countdown, der täglich kleiner
+ wird. Zwei gleich große Zahlen nebeneinander behaupteten damit eine
+ Gleichrangigkeit, die inhaltlich nicht besteht.
+
+ Jetzt: eine führende Kennzahl, darunter der Kilometerstand als ruhige
+ Wertezeile, getrennt durch eine Haarlinie - Audi-CI (Flächen und
+ Haarlinien statt gestapelter Karten, Hierarchie über Größe und Farbe)
+ kombiniert mit Apple HIGs "differentiate controls from content": der
+ Chevron sitzt nur am Serviceblock, damit erkennbar bleibt, welcher Teil
+ auf die Service-Seite führt und welcher reine Anzeige ist. Der Block
+ liegt bewusst in derselben Ebene wie die Reichweite darüber (.tile flat,
+ kein eigener Kachelhintergrund). */
+ const kmZeile = `
+ Kilometerstand
+
${CAR.odoBekannt ? de(CAR.odo) : "–"}km
+
`;
+ if (!t) return `
+ Nächster Service:
+
kein Eintrag im Servicebuch
+ ${kmZeile}
+
`;
+ // naechsterTermin() waehlt bereits ueber alle drei Arten hinweg (Oelwechsel,
+ // Inspektion, Hauptuntersuchung) den zeitlich naechsten aus.
+ const restKm = t.km !== null && CAR.odoBekannt ? Math.abs(t.km - CAR.odo) : null;
+ const datum = t.kmDatum || t.datum;
+ const hauptwert = restKm !== null
+ ? `${de(restKm)}km`
+ : (datum ? dedat(datum) : "–");
+ // Ohne Restkilometer steht das Datum selbst im Zahlenfeld - dann kleiner,
+ // damit "15.04.2027" auf 375px nicht umbricht.
+ const zahlGroesse = restKm !== null ? 40 : 30;
+ const nebenzeile = restKm !== null && datum
+ ? `${bisText(t.art)} · vsl. ${dedat(datum)}`
+ : bisText(t.art);
+ return `
+
+ ${kmZeile}
+
`;
+ })()}
${standortKachel()}
${teaser()}`;
}
diff --git a/homeassistant/www/audi-dashboard-ios.css b/homeassistant/www/audi-dashboard-ios.css
index 2b24bc9..a97c020 100644
--- a/homeassistant/www/audi-dashboard-ios.css
+++ b/homeassistant/www/audi-dashboard-ios.css
@@ -307,7 +307,6 @@ main#view{--seitenrand:16px;padding:0 var(--seitenrand) 30px;scroll-behavior:smo
deshalb hier mit !important. */
.rings{display:none !important}
- .duo{grid-template-columns:repeat(3,minmax(0,1fr))}
/* Nur die Box selbst hoeher setzen, nicht jedes Element mit .carfix: die
Klasse haengt auch am und am Platzhalter-
(siehe
bildMitPlatzhalter()). Der Platzhalter ist position:absolute;inset:0 -
diff --git a/homeassistant/www/audi-dashboard-version.json b/homeassistant/www/audi-dashboard-version.json
index dc1f246..dcb0a42 100644
--- a/homeassistant/www/audi-dashboard-version.json
+++ b/homeassistant/www/audi-dashboard-version.json
@@ -1 +1 @@
-{"version": 1787003000}
+{"version": 1787006000}
diff --git a/homeassistant/www/audi-dashboard.css b/homeassistant/www/audi-dashboard.css
index 7fced30..dabe736 100644
--- a/homeassistant/www/audi-dashboard.css
+++ b/homeassistant/www/audi-dashboard.css
@@ -463,8 +463,26 @@ button.tile, .tilebtn { transition: background .15s, transform .1s; }
/* Der Außenabstand sitzt am Raster, nicht an den Kacheln - sonst wird er im
2×2-Raster (vHome) zum zusätzlichen internen Zeilenabstand und der Abstand
innerhalb der Gruppe wäre größer als der zur nächsten. */
-.duo { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-3); margin-bottom: var(--sp-5); }
-.duo .tile { margin-bottom: 0; }
+/* Übersicht: "Nächster Service" als flache, tippbare Kennzahl, darunter der
+ Kilometerstand als Wertezeile (siehe Kommentar in vHome()). Der Block sitzt
+ in einer .tile.flat, hat also keinen eigenen Kachelhintergrund - deshalb
+ sitzt der Chevron bündig am oberen rechten Rand statt auf --sp-5 eingerückt
+ wie sonst in einer gepolsterten Kachel. */
+.serviceblock {
+ position: relative;
+ display: block; width: 100%;
+ border: 0; background: none; padding: 0;
+ font-family: inherit; text-align: left; color: inherit; cursor: pointer;
+}
+.serviceblock > .go { top: 3px; right: 0; }
+/* Beschriftung nie unter dem Chevron hindurchlaufen lassen */
+.serviceblock > .label:first-child { padding-right: 24px; }
+.servicezeile {
+ display: flex; align-items: baseline; justify-content: space-between;
+ gap: var(--sp-4);
+ border-top: 1px solid var(--line);
+ margin-top: 14px; padding-top: 13px;
+}
.badge { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.badge img { height: 18px; width: auto; display: none; }