# Überschrittene Servicetermine — Beschreibung zum Nachziehen Stand 07.09.2026, Fassung `2026.9.6.9`, Commit `33c2ffa` (und Vorläufer). Gedacht für ein Spin-off dieser App: alles, was gebraucht wird, um dieselbe Markierung dort noch einmal zu bauen — einschließlich der Gründe, damit die Entscheidungen nicht neu erraten werden müssen. Betroffen sind **zwei Codebasen**, die nach der Paritätsregel immer denselben funktionalen Stand haben: das Home-Assistant-Panel (`custom_components/audi_dashboard/frontend/`) und die Companion-App (`companion-app/src/`). Wer nur eine davon nachzieht, hat einen latenten Fehler. --- ## 1. Was die Markierung leistet Ein Servicetermin, dessen Fälligkeitstag erreicht oder überschritten ist, wird an drei Stellen hervorgehoben: | Ort | Darstellung | |---|---| | Service-Blatt | die betroffene Termingruppe blass gelb hinterlegt, Marke „überschritten" im Kopf | | „Mein Audi", Service-Kachel | **nur die betroffene Zeile** blass gelb, Marke links vom Wert | | Übersicht, Kachel „Nächster Service" | Kennzahl in kräftigem Orange, Marke rechts daneben | Zwei Randbedingungen, die der Eigentümer ausdrücklich gesetzt hat: * Auf der Übersicht darf die Marke die Kachel **nicht höher machen** — sie steht deshalb in derselben Zeile wie die Kennzahl, nicht darunter. * In „Mein Audi" wird **nicht die ganze Kachel** hinterlegt, sondern nur das überschrittene Feld. Eine Zwischenfassung hatte die Kachel als Ganzes getönt; das wurde zurückgewiesen. --- ## 2. Die Fachregel: entschieden wird am Datum `terminUeberschritten(...kandidaten)` — im Panel in `audi-dashboard-app.js`, in der App exportiert aus `src/daten/service.ts`. **Regel:** Nimm das **erste lesbare Datum** der übergebenen Kandidaten und melde `true`, sobald es nicht mehr in der Zukunft liegt. Der Fälligkeitstag selbst zählt mit. Drei Punkte, die beim Nachbau leicht falsch gemacht werden: 1. **Nicht an den Restkilometern entscheiden.** Die Kilometerseite trägt je nach Quelle ein anderes Vorzeichen: die Fahrzeugsensoren melden negativ, solange noch Strecke bleibt (gemessen: `oil_change_distance` = −28600 bei Fälligkeit 2028), und die Prognosefunktion klemmt ihre Restkilometer mit `Math.max(0, …)`. Welches Vorzeichen ein überschrittener Fahrzeugwert trägt, ist an den vorliegenden Daten **nicht beobachtbar**. Der km-Fall fällt ohnehin unter die Datumsregel, weil die Prognose das Datum auf heute setzt, sobald die Restkilometer auf 0 laufen. 2. **Das erste lesbare Datum nehmen, nicht das früheste.** Es ist genau das Datum, das an der Stelle auch angezeigt wird — so kann die Markierung nicht von dem abweichen, was daneben steht. 3. **Volle ISO-Zeitstempel mitlesen.** Die Fahrzeugmeldung liefert `"2027-10-21T00:00:00+00:00"`, und sie ist bei Ölwechsel und Inspektion die Hauptquelle. Der verankerte Textparser lehnt Zeitstempel ab; ohne einen eigenen Zweig dafür gälte die Hauptquelle stillschweigend **nie** als überschritten. Im Panel macht das `zeitstempelAlsDatum()` mit einem auf `JJJJ-MM-TT` plus `T` verankerten Ausdruck; in der App kann `alsZeitpunkt()` das bereits. Der Fehler wurde vom Test gefangen, nicht im Betrieb — er wäre still geblieben. Bewusst eng gefasst statt einem allgemeinen `new Date()`: sonst deutete der Rückfall auch Zeichenketten, die der Textparser gerade abgelehnt hat. --- ## 3. Die Farben: jedes Element an seiner eigenen WCAG-Schwelle Das ist der Teil, der zwei Anläufe gebraucht hat, und der Grund dafür ist übertragbar: **kräftiges Orange ist bei 4,5:1 nicht zu haben.** Apples systemOrange `#FF9500` kommt auf Weiß auf 2,20:1; jedes Orange, das 4,5:1 schafft, ist so dunkel, dass es als Braun gelesen wird. Zwei Anläufe mit einer einzigen Textfarbe (`#A15800`, dann Apples `#C55300`) wurden beide als „braun statt gelb" zurückgewiesen — zu Recht. Die Lösung ist nicht ein besserer Kompromisston, sondern **die Trennung der Rollen**, jede an ihrer eigenen Schwelle: | Rolle | Größe | Schwelle | Tag | Nacht | |---|---|---|---|---| | `--warn` (Fläche, 12 % hinterlegt) | — | — | `#FF9F0A` | `#FFD60A` | | `--warn-fig` (Kennzahl) | 32 px = Großtext | 3:1 | `#DD6000` | `#FF9F0A` | | `--warn-marke-grund` (Pille) | — | — | `#C55300` | `#FFA056` | | `--warn-marke-schrift` | 10,5 px = Kleintext | 4,5:1 | `#FFFFFF` | `#171B21` | | `--warn-text` (Schrift auf hellem Grund) | klein | 4,5:1 | `#C55300` | `#FFA056` | Gemessen (WCAG 2.1, Hülle = 12 % `--warn` auf dem jeweiligen Kachelgrund): * `#DD6000` → 3,65:1 auf Weiß, 3,34:1 auf der Hülle (Schwelle 3:1) ✔ * Weiß auf `#C55300` → 4,55:1 (Schwelle 4,5:1) ✔ * `#FF9F0A` auf `#171B21` → 8,41:1 ✔ * `#171B21` auf `#FFA056` → 8,57:1 ✔ **Der entscheidende Kniff bei der Marke:** keine dünne Schrift, sondern eine **gefüllte Pille**. Derselbe Ton liest sich als Fläche eindeutig als Orange, während er als 1 px starker Buchstabenstrich bräunlich wirkt. Weiße Schrift auf `#C55300` erfüllt die 4,5:1 exakt so, wie es umgekehrt nicht ginge. Tokens liegen in `frontend/audi-dashboard-ios.css` (Panel) und `design-system/src/tokens/tokens.css` (App) — beide Sätze müssen gleich sein. **Zur Frage HIG oder Audi CI:** die Fläche ist Apple (iOS-Palette, 2026-08-13 übernommen); das Audi-CI-Gelb `#ffaa00` wurde damals abgelöst und liefert als Schrift nur 1,91:1. Die Schriftwerte sind **gerechnet, nicht übernommen** — Apples eigener Kontrastwert `#A16A00` liefert auf der 12 %-Hülle nur 4,19:1. Nebenbefund: Apples HIG-Seite führt inzwischen die neue („unified") Palette; einen einzelnen Wert daraus zu übernehmen wäre inkonsistent gewesen. --- ## 4. CSS Gleiche Regeln in beiden Codebasen, nur andere Klassennamen (Panel `audi-dashboard.css` / App `src/stile/screens.css`): | Panel | App | |---|---| | `.termingruppe.ueberschritten` | `.dm-termingruppe--ueberschritten` | | `.row.ueberschritten` | `.dm-wertzeile--ueberschritten` | | `.ueberschritten-marke` | `.dm-ueberschritten-marke` | | `.serviceblock.ueberschritten .fig` | `.dm-serviceblock--ueberschritten .ads-fig` | | `.fig-zeile` | `.dm-servicefig` | Die Tönung: ```css background: color-mix(in srgb, var(--warn) 12%, transparent); border-radius: var(--r-tile); padding-left: 12px; padding-right: 12px; margin-left: -12px; margin-right: -12px; ``` Die negativen Außenabstände sind der Punkt: sie lassen die Tönung bis an den Kachelrand laufen, obwohl die Zeile selbst im Innenabstand der Kachel sitzt. Prüfbar am gemessenen Breitenunterschied — getönte Zeile 752 px gegenüber 728 px der übrigen, also genau 2 × 12 px. Die Pille: ```css font-size: 10.5px; font-weight: 600; color: var(--warn-marke-schrift); background: var(--warn-marke-grund); border-radius: var(--r-pill); padding: 3px 8px; white-space: nowrap; flex: 0 0 auto; display: inline-block; vertical-align: middle; ``` Platzierung: `.fig-zeile .ueberschritten-marke { margin-left: 10px }` (Übersicht, rechts der Kennzahl in derselben Zeile) und `.row .ueberschritten-marke { margin-right: 8px }` („Mein Audi", links des Werts). Dazu `.termingruppe.ueberschritten, .termingruppe.ueberschritten + .termingruppe { border-top: 0 }` — sonst schneidet die Trennlinie durch die getönte Fläche. --- ## 5. Markup **Übersicht** — die Marke muss in dieselbe Zeile wie die Kennzahl, sonst wächst die Kachel. Die Zeile wird deshalb **immer** gebaut, auch ohne Marke: ```js const figMarkup = `