Ueberschrittene Servicetermine werden markiert

Wunsch des Eigentuemers: faellige oder ueberschrittene Termine dezent
gelb/orange hinterlegen und mit "ueberschritten" markieren - Service-Seite und
Mein Audi; auf der Uebersicht zusaetzlich die Kennzahl im Warnton.

Erkannt wird am DATUM, nicht an den Restkilometern. Gemessen: die Sensoren
melden -28600 bzw. -22000 bei Faelligkeit 2028 bzw. 2027, negativ heisst hier
also "noch so weit hin". Welches Vorzeichen ein tatsaechlich ueberschrittener
Wert traegt, ist an den vorliegenden Daten nicht beobachtbar - geraten wird es
nicht. Der km-Fall faellt ohnehin unter die Datumsregel: servicePrognose()
setzt datum auf heute, sobald restKm auf 0 laeuft. Der Faelligkeitstag zaehlt
mit ("bereits faellig bzw. ueberschritten").

Dabei eine Luecke im Panel gefunden, vom Test und nicht im Betrieb:
datumAusText() ist verankert und nimmt keine vollen Zeitstempel - die
Fahrzeugmeldung ist aber genau das und bei Oelwechsel und Inspektion die
Hauptquelle. Sie waere still nie als ueberschritten erkannt worden.
zeitstempelAlsDatum() schliesst sie; die App hatte die Luecke nie.

Farbe nach Rolle geteilt wie --bad/--bad-flaeche: --warn ist die Flaeche,
--warn-text die Schrift. Grund: #FF9F0A als Text auf Weiss sind 2.06:1. Auf dem
12% hinterlegten Grund misst --warn-text 4.90:1 (Tag, #A15800) bzw. 9.30:1
(Nacht).

Geprueft: 15 Faelle gegen die echten Panel-Funktionen, 7 in der App, 14
Paritaetsfaelle Panel gegen App, 3 Rendertests ueber alle drei Anzeigestellen.
Gegenprobe: mit ausgehaengter Entscheidung fallen 9 Tests. 327/327, tsc sauber,
Panel als Modul geparst, Manifest und Buendel auf 2026.9.6.6, Container sauber
gestartet.

Offen: optisch nicht gesehen - der Testbestand enthaelt keinen ueberschrittenen
Termin. In OFFEN.md vermerkt.
This commit is contained in:
2026-09-07 13:39:52 +02:00
parent 4d721b6602
commit 27196f9e78
19 changed files with 2874 additions and 2409 deletions
+96 -1
View File
@@ -1,6 +1,6 @@
# AGENTS.md — Project state, review findings, open items, and working rules # AGENTS.md — Project state, review findings, open items, and working rules
**Last updated: 2026-09-06** (Die Batteriehistorie zeichnete stehende Werte als frische Messungen auf - der Vorschlag aus CB war messbar falsch, Abschnitt DI; davor: die ausgelieferte .ipa registriert zwei eigene Plugins nicht, Abschnitt DH). Davor: 2026-09-05, Audit-Durchgang, Abschnitt DC. (Tankstelle zweizeilig und antippbar, die Belegkarte sagt bei **Last updated: 2026-09-07** (Überschrittene Servicetermine werden markiert, Abschnitt DJ). Davor: 2026-09-06 (Die Batteriehistorie zeichnete stehende Werte als frische Messungen auf - der Vorschlag aus CB war messbar falsch, Abschnitt DI; davor: die ausgelieferte .ipa registriert zwei eigene Plugins nicht, Abschnitt DH). Davor: 2026-09-05, Audit-Durchgang, Abschnitt DC. (Tankstelle zweizeilig und antippbar, die Belegkarte sagt bei
fehlender Position die Wahrheit statt einen toten Knopf zu zeigen, der Regler bekommt einen fehlender Position die Wahrheit statt einen toten Knopf zu zeigen, der Regler bekommt einen
Speichern-Knopf, und die Share-Erweiterung trug eine andere Versionsnummer als die App; Speichern-Knopf, und die Share-Erweiterung trug eine andere Versionsnummer als die App;
dazu die Kopfmarke statt der Firmierung des Betreibers und ein dazu die Kopfmarke statt der Firmierung des Betreibers und ein
@@ -12708,3 +12708,98 @@ Daten des Eigentümers, und welche davon bleiben, entscheidet er. Sollte er
später mehr als tageweises Löschen brauchen (mehrere Tage auf einmal, oder eine später mehr als tageweises Löschen brauchen (mehrere Tage auf einmal, oder eine
Kennzeichnung verdächtiger Einträge), ist das ein eigener Auftrag - heute ist Kennzeichnung verdächtiger Einträge), ist das ein eigener Auftrag - heute ist
nichts davon angefragt. nichts davon angefragt.
## DJ. Überschrittene Servicetermine werden markiert (2026.9.6.6)
Wunsch des Eigentümers: fällige oder überschrittene Servicetermine hervorheben -
dezent gelb/orange transparent hinterlegt und mit „überschritten" markiert, auf
der Service-Seite und in „Mein Audi"; auf der Übersicht zusätzlich mit der
Kennzahl selbst im Warnton.
### Woran „überschritten" erkannt wird - und woran nicht
**Am Datum, nicht an den Restkilometern.** Das ist keine Bequemlichkeit: die
Kilometerseite trägt je nach Quelle ein anderes Vorzeichen. Am 07.09.2026 an den
echten Sensoren gemessen:
| Sensor | Wert | Fälligkeit laut Datum |
|---|---|---|
| `oil_change_distance` | **-28600** | 2028-07-23 |
| `inspection_distance` | **-22000** | 2027-10-21 |
Negativ heißt hier also „noch so weit hin", nicht „überschritten" - `Math.abs()`
in `meldungsPrognose()` ist damit richtig. Welches Vorzeichen ein *tatsächlich*
überschrittener Fahrzeugwert trägt, ist an den vorliegenden Daten **nicht
beobachtbar**; geraten wird es nicht. Dazu klemmt `servicePrognose()` ihre
Restkilometer ohnehin mit `Math.max(0, ...)`, die Information wäre dort also
auch verworfen.
Das Datum ist eindeutig, und der km-Fall fällt darunter: `servicePrognose()`
setzt `datum` auf **heute**, sobald `restKm` auf 0 läuft. Die Regel lautet
deshalb: **überschritten, sobald das Datum nicht mehr in der Zukunft liegt** -
der Fälligkeitstag selbst zählt mit, denn gefragt war „bereits fällig bzw.
überschritten".
Genommen wird das **erste lesbare** der übergebenen Daten - also das, das an
dieser Stelle auch angezeigt wird. So kann die Markierung nicht von dem
abweichen, was daneben steht.
### Eine Lücke im Panel, die der Test gefangen hat
`datumAusText()` im Panel ist verankert (`^…$`) und nimmt **keine** vollen
Zeitstempel. Die Fahrzeugmeldung (`fm.ts`) ist aber genau das - und bei Ölwechsel
und Inspektion die Hauptquelle. Ohne Korrektur wäre sie stillschweigend nie als
überschritten erkannt worden. Aufgefallen ist das im isolierten Test gegen die
echten Panel-Funktionen, nicht im Betrieb; `zeitstempelAlsDatum()` schließt sie,
bewusst eng gefasst (`/^\d{4}-\d{2}-\d{2}T/`) statt mit einem allgemeinen
`new Date()`, das auch deutete, was `datumAusText()` gerade abgelehnt hat.
Die App hatte diese Lücke nicht: ihr `ISO`-Muster akzeptiert `[T ]` seit jeher.
Damit sind beide Datumsleser jetzt gleich weit.
### Farbe: --warn ist die Fläche, --warn-text die Schrift
Gemessen statt gewählt. `--warn` ist im Tagmodus `#FF9F0A` - als **Text** auf
Weiß sind das **2,06:1**, weit unter der 4,5:1-Grenze, die dieses Projekt sich
selbst gesetzt hat (siehe Abschnitt CP, wo `--fg3` aus demselben Grund
abgedunkelt wurde). Deshalb dieselbe Rollenteilung wie bei `--bad` /
`--bad-flaeche`:
| | Fläche (12 % hinterlegt) | Schrift `--warn-text` | Kontrast auf dem hinterlegten Grund |
|---|---|---|---|
| Tag | `#FF9F0A` → Grund `#FFF3E2` | **`#A15800`** | **4,90:1** |
| Nacht | `#FFD60A` → Grund `#33311E` | `#FFD60A` | **9,30:1** |
Nachts trägt `--warn` selbst genug Kontrast, tagsüber nicht. `#B36200` wäre
näher am reinen Orange, fällt auf dem hinterlegten Grund aber auf 4,11:1 und
damit unter die Grenze - deshalb `#A15800`.
### Geprüft
* **15 Fälle** gegen die echten Panel-Funktionen (aus der Panel-Quelle
geschnitten, nicht nachgebaut), darunter beide Zeitstempel-Richtungen und die
Tagesgrenze.
* **7 Fälle** in `service.test.ts` für die App; gegengeprüft, dass die
Tagesgrenze wirklich gemessen wird (`<=` auf `<` gedreht → genau der
„heute zählt mit"-Test fällt).
* **14 Paritätsfälle** in `paritaet_ueberschritten.test.ts`: Panel und App
urteilen über dieselben Eingaben gleich, geprüft gegen die aus der Panel-Quelle
geschnittene Funktion.
* **3 Rendertests** über alle drei Anzeigestellen, mit einem Profil, dessen
Hauptuntersuchung in der Vergangenheit liegt. Der Regelbestand der
Beispieldaten taugte dafür nicht (Wartungsplan 2026-02-10 + 24 Monate);
`beispielApi()` nimmt dafür jetzt ein abweichendes Profil entgegen - additiv,
alle bisherigen Aufrufer unverändert.
* Gegenprobe über alles: mit ausgehängter Entscheidung (`terminUeberschritten`
liefert immer `false`) fallen **9 Tests**, darunter alle drei Rendertests.
* 327/327 App-Tests, `tsc` sauber, beide Panel-Dateien als **Modul** geparst,
Manifest und Bündel gleichauf auf `2026.9.6.6`, Testcontainer sauber gestartet.
### Was offen bleibt
**Optisch nicht gesehen.** Die Markierung ist über Rendertests und gemessene
Kontraste belegt, nicht über ein Bild - der Testbestand enthält keinen
überschrittenen Termin (Ölwechsel 2028, Inspektion 2027, HU 2028), und für einen
Blick ins laufende Panel fehlte die angemeldete Sitzung. Nach der Regel dieses
Projekts (Abschnitt S: ein sauberer Testlauf beweist kein Layout) steht das
ausdrücklich als offen, nicht als erledigt.
+11
View File
@@ -98,6 +98,17 @@ Beispielzahlen in der Statistik und ein überholter TODO-Kommentar.
--- ---
## 3b. Optisch noch nicht gesehen
**Die Markierung überschrittener Servicetermine** (`2026.9.6.6`, Abschnitt DJ)
ist über Rendertests und gemessene Kontraste belegt, aber nie am Bild geprüft -
der Testbestand hat keinen überschrittenen Termin, und für einen Blick ins
laufende Panel fehlte die angemeldete Sitzung.
*Was zu tun ist:* einmal ansehen, sobald ein Termin wirklich fällig ist - oder
mit einer angemeldeten Sitzung und einem vorübergehend zurückdatierten
Wartungsplan-Eintrag (hinterher zurücksetzen).
## 4. Gemeldet, aber nicht reproduzierbar ## 4. Gemeldet, aber nicht reproduzierbar
Beide mehrfach gesucht und **nicht gefunden**. Sie bleiben stehen, bis sie Beide mehrfach gesucht und **nicht gefunden**. Sie bleiben stehen, bis sie
@@ -0,0 +1,70 @@
import { readFileSync } from "node:fs"
import { describe, expect, it } from "vitest"
import { terminUeberschritten } from "./service"
/* Beide Codebasen müssen denselben Termin gleich beurteilen - sonst steht auf
der Übersicht "überschritten" und auf derselben Seite im Panel nicht. Geprüft
wird gegen die ECHTE Panel-Funktion, aus der Panel-Quelle herausgeschnitten,
nicht gegen einen Nachbau. Muster wie in paritaet_hu.test.ts. */
const quelle = readFileSync(
"../custom_components/audi_dashboard/frontend/audi-dashboard-app.js", "utf8")
const block = (kopf: string) => {
const i = quelle.indexOf(kopf)
if (i < 0) throw new Error("fehlt: " + kopf)
let tiefe = 0
for (let k = quelle.indexOf("{", i); k < quelle.length; k++) {
if (quelle[k] === "{") tiefe++
else if (quelle[k] === "}" && !--tiefe) return quelle.slice(i, k + 1)
}
throw new Error("unvollstaendig: " + kopf)
}
const zeile = (muster: RegExp) => {
const m = muster.exec(quelle)
if (!m) throw new Error("fehlt: " + muster)
return m[0]
}
const panel = new Function(`
${zeile(/^const DATUM_ISO = .*$/m)}
${zeile(/^const DATUM_TAG = .*$/m)}
${zeile(/^const DATUM_MONAT = .*$/m)}
${zeile(/^const DATUM_ISO_MONAT = .*$/m)}
${zeile(/^const DATUM_ZEITSTEMPEL = .*$/m)}
${zeile(/^const gueltigesDatum = .*$/m)}
${block("function datumBauen")}
${block("function datumAusText")}
${block("function zeitstempelAlsDatum")}
${block("function terminUeberschritten")}
return { terminUeberschritten };
`)() as { terminUeberschritten: (...k: (string | Date | null)[]) => boolean }
const tag = (versatz: number) => {
const x = new Date()
x.setDate(x.getDate() + versatz)
return `${String(x.getDate()).padStart(2, "0")}.${String(x.getMonth() + 1).padStart(2, "0")}.${x.getFullYear()}`
}
describe("terminUeberschritten: Panel und App urteilen gleich", () => {
const faelle: { name: string; args: (string | null)[] }[] = [
{ name: "gestern", args: [tag(-1)] },
{ name: "heute", args: [tag(0)] },
{ name: "morgen", args: [tag(1)] },
{ name: "ISO vorbei", args: ["2024-03-01"] },
{ name: "ISO Zukunft", args: ["2028-07-23"] },
{ name: "Fahrzeug-Zeitstempel vorbei", args: ["2025-10-21T00:00:00+00:00"] },
{ name: "Fahrzeug-Zeitstempel Zukunft", args: ["2027-10-21T00:00:00+00:00"] },
{ name: "monatsgenau vorbei", args: ["03/2025"] },
{ name: "monatsgenau Zukunft", args: ["03/2099"] },
{ name: "kein Datum", args: [null] },
{ name: "leer", args: [""] },
{ name: "unlesbar", args: ["irgendwas"] },
{ name: "erstes lesbares: null, dann gestern", args: [null, tag(-1)] },
{ name: "erstes lesbares: morgen vor gestern", args: [tag(1), tag(-1)] },
]
for (const f of faelle) {
it(f.name, () => {
expect(terminUeberschritten(...f.args)).toBe(panel.terminUeberschritten(...f.args))
})
}
})
+48 -11
View File
@@ -1,16 +1,6 @@
import { describe, expect, it } from "vitest" import { describe, expect, it } from "vitest"
import { import { hauptuntersuchungFaellig, inspektionPrognose, kmProTagAusFahrten, letzterOelwechsel, meldungsPrognose, meldungsPrognoseEigenesIntervall, naechsterService, oelMeldungsPrognose, oelwechselPrognose, terminUeberschritten } from "./service"
hauptuntersuchungFaellig,
inspektionPrognose,
kmProTagAusFahrten,
letzterOelwechsel,
meldungsPrognose,
meldungsPrognoseEigenesIntervall,
naechsterService,
oelMeldungsPrognose,
oelwechselPrognose,
} from "./service"
import type { Fahrzeug } from "./profilAdapter" import type { Fahrzeug } from "./profilAdapter"
function fahrzeug(odo: number, modus = "hersteller"): Fahrzeug { function fahrzeug(odo: number, modus = "hersteller"): Fahrzeug {
@@ -400,3 +390,50 @@ describe("Hauptuntersuchung", () => {
expect(hauptuntersuchungFaellig([], "keine Ahnung")).toBeNull() expect(hauptuntersuchungFaellig([], "keine Ahnung")).toBeNull()
}) })
}) })
describe("terminUeberschritten", () => {
const tag = (versatz: number) => {
const x = new Date()
x.setDate(x.getDate() + versatz)
return `${String(x.getDate()).padStart(2, "0")}.${String(x.getMonth() + 1).padStart(2, "0")}.${x.getFullYear()}`
}
it("gestern ist überschritten", () => {
expect(terminUeberschritten(tag(-1))).toBe(true)
})
it("heute zählt mit - 'bereits fällig' ist gemeint", () => {
expect(terminUeberschritten(tag(0))).toBe(true)
})
it("morgen nicht", () => {
expect(terminUeberschritten(tag(1))).toBe(false)
})
it("erkennt den vollen Zeitstempel des Fahrzeugs", () => {
// Genau die Form, die oelwechselFaelligTs/inspektionFaelligTs tragen. Im
// Panel war das die Lücke: dessen Datumsleser ist verankert und nahm sie
// nicht an, die Fahrzeugmeldung wäre dort still nie überschritten gewesen.
expect(terminUeberschritten("2025-10-21T00:00:00+00:00")).toBe(true)
expect(terminUeberschritten("2027-10-21T00:00:00+00:00")).toBe(false)
})
it("versteht monatsgenaue Angaben (Hauptuntersuchung)", () => {
expect(terminUeberschritten("03/2025")).toBe(true)
expect(terminUeberschritten("03/2099")).toBe(false)
})
it("ohne lesbares Datum wird nichts behauptet", () => {
expect(terminUeberschritten(null)).toBe(false)
expect(terminUeberschritten("")).toBe(false)
expect(terminUeberschritten("irgendwas")).toBe(false)
expect(terminUeberschritten(undefined)).toBe(false)
})
it("nimmt das erste LESBARE Datum, nicht das erste beliebige", () => {
// So kann die Markierung nicht von dem abweichen, was daneben steht.
expect(terminUeberschritten(null, tag(-1))).toBe(true)
expect(terminUeberschritten(tag(1), tag(-1))).toBe(false)
})
})
+36
View File
@@ -401,3 +401,39 @@ export function hauptuntersuchungFaellig(
const ziel = monatePlus(isoTag(z.zeit), HU_MONATE_ERSTE) const ziel = monatePlus(isoTag(z.zeit), HU_MONATE_ERSTE)
return z.genau === "monat" ? ziel.slice(0, 7) : ziel return z.genau === "monat" ? ziel.slice(0, 7) : ziel
} }
/** Ist dieser Servicetermin bereits fällig oder überschritten?
*
* Der Fälligkeitstag selbst zählt mit („bereits fällig bzw. überschritten",
* Ansage des Eigentümers 07.09.2026).
*
* Entschieden wird am DATUM, nicht an den Restkilometern - und das ist keine
* Bequemlichkeit. Die Kilometerseite trägt je nach Quelle ein anderes
* Vorzeichen: die Sensoren des Fahrzeugs melden negativ, solange noch Strecke
* bleibt (am 07.09.2026 gemessen: oil_change_distance -28600 bei Fälligkeit
* 2028), und servicePrognose() klemmt ihre Restkilometer ohnehin mit
* Math.max(0, ...). Welches Vorzeichen ein überschrittener Fahrzeugwert trägt,
* lässt sich an den vorliegenden Daten nicht beobachten - geraten wird es
* nicht. Der km-Fall fällt ohnehin unter die Datumsregel: servicePrognose()
* setzt datum auf heute, sobald restKm auf 0 läuft.
*
* Genommen wird das ERSTE lesbare Datum der übergebenen Kandidaten - also das,
* das an dieser Stelle auch angezeigt wird. So kann die Markierung nicht von
* dem abweichen, was daneben steht. Wortgleich mit terminUeberschritten() im
* Panel. */
export function terminUeberschritten(
...kandidaten: (string | Date | null | undefined)[]
): boolean {
for (const kandidat of kandidaten) {
if (kandidat === null || kandidat === undefined || kandidat === "") continue
const z = alsZeitpunkt(kandidat)
if (!z) continue
const d = z.zeit
const h = new Date()
return (
new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime() <=
new Date(h.getFullYear(), h.getMonth(), h.getDate()).getTime()
)
}
return false
}
+15 -4
View File
@@ -18,10 +18,10 @@ import { useRef, useState, type PointerEvent as ReactPointerEvent } from "react"
import { Tile } from "@audi-dash/ui" import { Tile } from "@audi-dash/ui"
import { useDaten } from "../daten/DatenKontext" import { useDaten } from "../daten/DatenKontext"
import { hauptuntersuchungFaellig, inspektionPrognose, oelwechselPrognose } from "../daten/service" import { hauptuntersuchungFaellig, inspektionPrognose, oelwechselPrognose, terminUeberschritten } from "../daten/service"
import { datum, de, deOderStrich, eur } from "../format" import { datum, de, deOderStrich, eur } from "../format"
import type { SeitenName } from "../navigation" import type { SeitenName } from "../navigation"
import { Wertzeile, Werteliste } from "./bausteine" import { UeberschrittenMarke, Werteliste, Wertzeile } from "./bausteine"
import { Bild } from "./Bild" import { Bild } from "./Bild"
import { GALERIE } from "./bilder" import { GALERIE } from "./bilder"
import type { ServicebuchEintrag } from "./Service" import type { ServicebuchEintrag } from "./Service"
@@ -115,13 +115,14 @@ export function MeinAudi({ geheZu }: { geheZu: (name: SeitenName, id?: string) =
const huFaellig = hauptuntersuchungFaellig(buch, fahrzeug.erstzulassung) const huFaellig = hauptuntersuchungFaellig(buch, fahrzeug.erstzulassung)
const oelProg = oelwechselPrognose(fahrzeug, buch) const oelProg = oelwechselPrognose(fahrzeug, buch)
const inspProg = inspektionPrognose(fahrzeug, buch) const inspProg = inspektionPrognose(fahrzeug, buch)
const termine: { art: string; prognose: string | null; haupt: string; neben: string | null }[] = [ const termine: { art: string; prognose: string | null; haupt: string; neben: string | null; ueber: boolean }[] = [
{ {
art: "Ölwechsel", art: "Ölwechsel",
prognose: oelProg prognose: oelProg
? `${de(oelProg.restKm)} km, voraussichtlich am ${datum(oelProg.datum)}` ? `${de(oelProg.restKm)} km, voraussichtlich am ${datum(oelProg.datum)}`
: null, : null,
haupt: fahrzeug.oelwechselFaelligTs ? datum(fahrzeug.oelwechselFaelligTs) : "", haupt: fahrzeug.oelwechselFaelligTs ? datum(fahrzeug.oelwechselFaelligTs) : "",
ueber: terminUeberschritten(fahrzeug.oelwechselFaelligTs, oelProg?.datum),
neben: neben:
fahrzeug.oelwechselFaelligKm != null fahrzeug.oelwechselFaelligKm != null
? `in ${de(Math.abs(fahrzeug.oelwechselFaelligKm))} km` ? `in ${de(Math.abs(fahrzeug.oelwechselFaelligKm))} km`
@@ -131,6 +132,7 @@ export function MeinAudi({ geheZu }: { geheZu: (name: SeitenName, id?: string) =
art: "Inspektion", art: "Inspektion",
prognose: inspProg ? `voraussichtlich am ${datum(inspProg.datum)}` : null, prognose: inspProg ? `voraussichtlich am ${datum(inspProg.datum)}` : null,
haupt: fahrzeug.inspektionFaelligTs ? datum(fahrzeug.inspektionFaelligTs) : "", haupt: fahrzeug.inspektionFaelligTs ? datum(fahrzeug.inspektionFaelligTs) : "",
ueber: terminUeberschritten(fahrzeug.inspektionFaelligTs, inspProg?.datum),
neben: neben:
fahrzeug.inspektionFaelligKm != null fahrzeug.inspektionFaelligKm != null
? `in ${de(Math.abs(fahrzeug.inspektionFaelligKm))} km` ? `in ${de(Math.abs(fahrzeug.inspektionFaelligKm))} km`
@@ -142,6 +144,7 @@ export function MeinAudi({ geheZu }: { geheZu: (name: SeitenName, id?: string) =
art: "Hauptuntersuchung", art: "Hauptuntersuchung",
prognose: null, prognose: null,
haupt: huFaellig ? datum(huFaellig) : "", haupt: huFaellig ? datum(huFaellig) : "",
ueber: terminUeberschritten(huFaellig),
neben: null, neben: null,
}, },
] ]
@@ -270,7 +273,15 @@ export function MeinAudi({ geheZu }: { geheZu: (name: SeitenName, id?: string) =
)} )}
</> </>
} }
wert={t.haupt} wert={
t.ueber ? (
<>
{t.haupt} <UeberschrittenMarke />
</>
) : (
t.haupt
)
}
{...(t.neben ? { zusatz: t.neben } : {})} {...(t.neben ? { zusatz: t.neben } : {})}
/> />
)) ))
+18 -9
View File
@@ -14,15 +14,11 @@ import { ActionButton, Feld, Fig, Tile } from "@audi-dash/ui"
import { kartendienstUrl } from "../daten/tankstelle" import { kartendienstUrl } from "../daten/tankstelle"
import { useDaten } from "../daten/DatenKontext" import { useDaten } from "../daten/DatenKontext"
import { hauptuntersuchungFaellig, inspektionPrognose, letzterInspektion, letzterOelwechsel, meldungsPrognose, oelMeldungsPrognose, oelwechselPrognose, serviceKandidaten } from "../daten/service" import { hauptuntersuchungFaellig, inspektionPrognose, letzterInspektion, letzterOelwechsel, meldungsPrognose, oelMeldungsPrognose, oelwechselPrognose, serviceKandidaten, terminUeberschritten } from "../daten/service"
import { datum, de, eur, isoTag } from "../format" import { datum, de, eur, isoTag } from "../format"
import type { SeitenName } from "../navigation" import type { SeitenName } from "../navigation"
import { SymbolInspektion, SymbolOelwechsel } from "../symbole" import { SymbolInspektion, SymbolOelwechsel } from "../symbole"
import { import { Blattzeile, Leerzustand, UeberschrittenMarke, Zahnradknopf } from "./bausteine"
Blattzeile,
Leerzustand,
Zahnradknopf,
} from "./bausteine"
import { LOESCH_HINWEIS, LOESCH_TEXT, bestaetigen } from "./bestaetigung" import { LOESCH_HINWEIS, LOESCH_TEXT, bestaetigen } from "./bestaetigung"
import { Datumsfeld } from "./Datumsfeld" import { Datumsfeld } from "./Datumsfeld"
import { terminUebernehmen } from "./kalender" import { terminUebernehmen } from "./kalender"
@@ -127,14 +123,25 @@ export function Service({ geheZu }: { geheZu: (name: SeitenName, id?: string) =>
const huDatum = huFaellig ? new Date(huFaellig) : null const huDatum = huFaellig ? new Date(huFaellig) : null
const huGueltig = !!huDatum && !Number.isNaN(huDatum.getTime()) const huGueltig = !!huDatum && !Number.isNaN(huDatum.getTime())
// Dasselbe Datum, das gleich als Wert erscheint - siehe
// terminUeberschritten(). Die Fahrzeugmeldung geht vor, weil sie oben steht.
const oelUeber = terminUeberschritten(
oelHatMeldung ? fahrzeug.oelwechselFaelligTs : prognose?.datum,
)
const inspUeber = terminUeberschritten(
inspHatMeldung ? fahrzeug.inspektionFaelligTs : inspektionsprognose?.datum,
)
const huUeber = huGueltig && terminUeberschritten(huFaellig)
return ( return (
<> <>
<Tile> <Tile>
<div className="dm-termingruppe"> <div className={`dm-termingruppe${oelUeber ? " dm-termingruppe--ueberschritten" : ""}`}>
<div className="dm-tgkopf"> <div className="dm-tgkopf">
<SymbolOelwechsel groesse={26} /> <SymbolOelwechsel groesse={26} />
<span>Ölwechsel</span> <span>Ölwechsel</span>
{oelUeber && <UeberschrittenMarke />}
</div> </div>
{oelHatEintrag || oelHatMeldung ? ( {oelHatEintrag || oelHatMeldung ? (
<> <>
@@ -147,10 +154,11 @@ export function Service({ geheZu }: { geheZu: (name: SeitenName, id?: string) =>
)} )}
</div> </div>
<div className="dm-termingruppe"> <div className={`dm-termingruppe${inspUeber ? " dm-termingruppe--ueberschritten" : ""}`}>
<div className="dm-tgkopf"> <div className="dm-tgkopf">
<SymbolInspektion groesse={20} /> <SymbolInspektion groesse={20} />
<span>Inspektion</span> <span>Inspektion</span>
{inspUeber && <UeberschrittenMarke />}
</div> </div>
{inspHatEintrag || inspHatMeldung ? ( {inspHatEintrag || inspHatMeldung ? (
<> <>
@@ -163,9 +171,10 @@ export function Service({ geheZu }: { geheZu: (name: SeitenName, id?: string) =>
)} )}
</div> </div>
<div className="dm-termingruppe"> <div className={`dm-termingruppe${huUeber ? " dm-termingruppe--ueberschritten" : ""}`}>
<div className="dm-tgkopf"> <div className="dm-tgkopf">
<span>Hauptuntersuchung</span> <span>Hauptuntersuchung</span>
{huUeber && <UeberschrittenMarke />}
</div> </div>
{huGueltig ? ( {huGueltig ? (
<div className="dm-tgwert">{datum(huFaellig)}</div> <div className="dm-tgwert">{datum(huFaellig)}</div>
+15 -3
View File
@@ -11,11 +11,11 @@ import { Fig, ProgressBar, StatusRow, Tile } from "@audi-dash/ui"
import { useState } from "react" import { useState } from "react"
import { useDaten } from "../daten/DatenKontext" import { useDaten } from "../daten/DatenKontext"
import { naechsterService } from "../daten/service" import { naechsterService, terminUeberschritten } from "../daten/service"
import { datum, de, deOderStrich, eur, streckeText } from "../format" import { datum, de, deOderStrich, eur, streckeText } from "../format"
import type { SeitenName } from "../navigation" import type { SeitenName } from "../navigation"
import { BILDPLAETZE, bildInfo } from "./bilder" import { BILDPLAETZE, bildInfo } from "./bilder"
import { Blattzeile, Leerzustand } from "./bausteine" import { Blattzeile, Leerzustand, UeberschrittenMarke } from "./bausteine"
// Dieselbe Ortsangabe wie in der Fahrtenliste - eine Quelle, damit "Zuletzt" // Dieselbe Ortsangabe wie in der Fahrtenliste - eine Quelle, damit "Zuletzt"
// und die Liste nicht auseinanderlaufen koennen. // und die Liste nicht auseinanderlaufen koennen.
import { strecke } from "./Fahrten" import { strecke } from "./Fahrten"
@@ -81,6 +81,9 @@ export function Uebersicht({ geheZu }: { geheZu: (name: SeitenName, id?: string)
const buch = (fahrzeug.service["buch"] ?? []) as ServicebuchEintrag[] const buch = (fahrzeug.service["buch"] ?? []) as ServicebuchEintrag[]
const service = naechsterService(fahrzeug, buch, fahrten) const service = naechsterService(fahrzeug, buch, fahrten)
// Auf der Uebersicht wechselt zusaetzlich die Kennzahl die Farbe - sie ist
// hier die Aussage, nicht nur ihr Rahmen (Ansage des Eigentuemers).
const serviceUeber = !!service && terminUeberschritten(service.datum)
const letzteFahrt = fahrten const letzteFahrt = fahrten
.slice() .slice()
@@ -238,7 +241,11 @@ export function Uebersicht({ geheZu }: { geheZu: (name: SeitenName, id?: string)
"differentiate controls from content" gilt auch für die "differentiate controls from content" gilt auch für die
Abwesenheit von Inhalt. */} Abwesenheit von Inhalt. */}
{service ? ( {service ? (
<button type="button" className="dm-serviceblock" onClick={() => geheZu("service")}> <button
type="button"
className={`dm-serviceblock${serviceUeber ? " dm-serviceblock--ueberschritten" : ""}`}
onClick={() => geheZu("service")}
>
<span className="ads-eyebrow">Nächster Service:</span> <span className="ads-eyebrow">Nächster Service:</span>
{/* Gezeichneter Pfeil wie .go im Panel, kein Schriftzeichen: das {/* Gezeichneter Pfeil wie .go im Panel, kein Schriftzeichen: das
">" war ein 15px-Glyph der Textschrift und damit erkennbar ">" war ein 15px-Glyph der Textschrift und damit erkennbar
@@ -265,6 +272,11 @@ export function Uebersicht({ geheZu }: { geheZu: (name: SeitenName, id?: string)
fig fig
) )
})()} })()}
{serviceUeber && (
<div className="dm-servicemarke">
<UeberschrittenMarke />
</div>
)}
{/* Im Panel ist auch diese Nebenzeile ein `.label` (13px, --fg3), {/* Im Panel ist auch diese Nebenzeile ein `.label` (13px, --fg3),
nicht die kleinere Fußnote (11,5px/300), die hier stand. */} nicht die kleinere Fußnote (11,5px/300), die hier stand. */}
<span className="ads-eyebrow" style={{ marginTop: "8px" }}> <span className="ads-eyebrow" style={{ marginTop: "8px" }}>
+7
View File
@@ -537,3 +537,10 @@ export function Markenlogo({
/> />
) )
} }
/** „überschritten" neben einem fälligen Servicetermin. Eigener Baustein, damit
* die drei Anzeigestellen (Service, Mein Audi, Übersicht) nicht auseinander
* laufen. Gegenstück zu UEBERSCHRITTEN_MARKE im Panel. */
export function UeberschrittenMarke() {
return <span className="dm-ueberschritten-marke">überschritten</span>
}
+44 -3
View File
@@ -12,13 +12,19 @@ import { Shell } from "../Shell"
import type { SeitenName } from "../navigation" import type { SeitenName } from "../navigation"
import { TITEL } from "../navigation" import { TITEL } from "../navigation"
import { SeiteFuer } from "./register" import { SeiteFuer } from "./register"
import { beispielApi } from "../tests/beispieldaten" import { beispielApi, beispielProfil } from "../tests/beispieldaten"
import type { Profil } from "../api/types"
import { bildschirmbreiteSetzen } from "../tests/aufbau" import { bildschirmbreiteSetzen } from "../tests/aufbau"
const ALLE_SEITEN = Object.keys(TITEL) as SeitenName[] const ALLE_SEITEN = Object.keys(TITEL) as SeitenName[]
function zeige(name: SeitenName, id?: string, geheZu: (name: SeitenName, id?: string) => void = () => {}) { function zeige(
const api = beispielApi() name: SeitenName,
id?: string,
geheZu: (name: SeitenName, id?: string) => void = () => {},
profil?: Profil,
) {
const api = profil ? beispielApi(profil) : beispielApi()
return render( return render(
<div className="ads-root" data-theme="nacht"> <div className="ads-root" data-theme="nacht">
<DatenAnbieter api={api}> <DatenAnbieter api={api}>
@@ -194,3 +200,38 @@ describe("Inhalte kommen wirklich aus den Daten", () => {
expect(artAuswahl.value).toBe("arbeitsweg") expect(artAuswahl.value).toBe("arbeitsweg")
}) })
}) })
/* Überschrittene Servicetermine werden markiert - an allen drei Stellen, die
der Eigentümer genannt hat (07.09.2026). Die Beispieldaten tragen
erstzulassung 2023-04-01, die Hauptuntersuchung liegt damit auf 04/2026 und
ist überschritten; getestet wird also am vorhandenen Bestand, nicht an einer
eigens gebauten Fixtur. */
describe("Überschrittene Servicetermine", () => {
/* Der Regelbestand hat keinen ueberschrittenen Termin (Wartungsplan
2026-02-10 + 24 Monate). Hier deshalb ein Profil, dessen letzter Eintrag
lange zurueckliegt - die Hauptuntersuchung faellt damit in die
Vergangenheit. */
const ueberfaellig: Profil = {
...beispielProfil,
fahrzeug: { ...beispielProfil.fahrzeug, erstzulassung: "2019-04-01" },
service: { ...beispielProfil.service, buch: [] },
}
it("markiert die Gruppe auf der Service-Seite", async () => {
const { container } = zeige("service", undefined, () => {}, ueberfaellig)
await waitFor(() => expect(screen.getAllByText("überschritten").length).toBeGreaterThan(0))
expect(container.querySelector(".dm-termingruppe--ueberschritten")).not.toBeNull()
})
it("markiert die Zeile in Mein Audi", async () => {
zeige("audi", undefined, () => {}, ueberfaellig)
await waitFor(() => expect(screen.getAllByText("überschritten").length).toBeGreaterThan(0))
})
it("markiert den Serviceblock auf der Übersicht und färbt die Kennzahl", async () => {
const { container } = zeige("home", undefined, () => {}, ueberfaellig)
await waitFor(() => expect(screen.getAllByText("überschritten").length).toBeGreaterThan(0))
expect(container.querySelector(".dm-serviceblock--ueberschritten")).not.toBeNull()
})
})
File diff suppressed because it is too large Load Diff
+5 -2
View File
@@ -162,7 +162,10 @@ export const beispielTankvorgaenge: Tankvorgang[] = [
] ]
/** Datenschicht-Ersatz für Tests: liefert Beispieldaten, schreibt ins Leere. */ /** Datenschicht-Ersatz für Tests: liefert Beispieldaten, schreibt ins Leere. */
export function beispielApi(): DataMetricApi { /** Die Beispieldaten mit einem abweichenden Profil - fuer Tests, die einen
* Zustand brauchen, den der Regelbestand nicht hergibt (etwa einen bereits
* ueberschrittenen Servicetermin). Ohne Argument bleibt alles wie bisher. */
export function beispielApi(profil: Profil = beispielProfil): DataMetricApi {
const horcher = { verbindung: [] as ((z: string) => void)[] } const horcher = { verbindung: [] as ((z: string) => void)[] }
return { return {
rest: { rest: {
@@ -196,7 +199,7 @@ export function beispielApi(): DataMetricApi {
}, },
anzahl: 0, anzahl: 0,
}, },
profilLesen: async () => beispielProfil, profilLesen: async () => profil,
fahrtenLesen: async () => beispielFahrten, fahrtenLesen: async () => beispielFahrten,
tankvorgaengeLesen: async () => beispielTankvorgaenge, tankvorgaengeLesen: async () => beispielTankvorgaenge,
fahrzeugstatusLesen: async () => beispielStatus, fahrzeugstatusLesen: async () => beispielStatus,
@@ -1 +1 @@
{"version":"2026.9.6.5","sha256":"d7373ee38b4b6e97f5d20e5225bca6981d21f60fc34d7b2658b8571650b52726","bytes":378720,"gebaut":"2026-09-06T21:29:15Z"} {"version":"2026.9.6.6","sha256":"c9e3d8f25998f38b70e38441a8c0e09d76bbe340845575e9ae769fe12c2fc45e","bytes":379100,"gebaut":"2026-09-07T11:37:53Z"}
@@ -741,6 +741,52 @@ function datumAusText(wert) {
return null; return null;
} }
/* Ist dieser Servicetermin bereits faellig oder ueberschritten? Der
Faelligkeitstag selbst zaehlt mit ("bereits faellig bzw. ueberschritten",
Ansage des Eigentuemers 07.09.2026).
Entschieden wird am DATUM, nicht an den Restkilometern - und das ist keine
Bequemlichkeit. Die Kilometerseite traegt je nach Quelle ein anderes
Vorzeichen: die Sensoren des Fahrzeugs melden negativ, solange noch Strecke
bleibt (am 07.09.2026 gemessen: oil_change_distance -28600 bei Faelligkeit
2028), und servicePrognose() klemmt ihre Restkilometer ohnehin mit
Math.max(0, ...). Welches Vorzeichen ein ueberschrittener Fahrzeugwert
traegt, laesst sich an den vorliegenden Daten nicht beobachten - geraten wird
es nicht. Der km-Fall faellt ohnehin unter die Datumsregel: servicePrognose()
setzt datum auf heute, sobald restKm auf 0 laeuft.
Genommen wird das ERSTE lesbare Datum der uebergebenen Kandidaten - also das,
das an dieser Stelle auch angezeigt wird. So kann die Markierung nicht von
dem abweichen, was daneben steht. */
/* Ein voller ISO-Zeitstempel ("2027-10-21T00:00:00+00:00"), wie ihn die
Sensoren des Fahrzeugs liefern. Bewusst eng gefasst statt einem allgemeinen
new Date(): sonst deutete der Rueckfall auch Zeichenketten, die
datumAusText() gerade abgelehnt hat. */
const DATUM_ZEITSTEMPEL = /^\d{4}-\d{2}-\d{2}T/;
function zeitstempelAlsDatum(wert) {
if (typeof wert !== "string" || !DATUM_ZEITSTEMPEL.test(wert)) return null;
const d = new Date(wert);
return gueltigesDatum(d) ? { zeit: d, genau: "tag" } : null;
}
function terminUeberschritten(...kandidaten) {
for (const kandidat of kandidaten) {
if (kandidat == null || kandidat === "") continue;
// datumAusText() ist verankert und nimmt bewusst keine Zeitstempel - die
// Fahrzeugmeldung (fm.ts) ist aber genau das, und sie ist bei Oelwechsel
// und Inspektion die Hauptquelle. Ohne diesen Zweig gaelte sie stillschweigend
// nie als ueberschritten; am 07.09.2026 vom Test gefangen, nicht im Betrieb.
const d = datumAusText(kandidat) || zeitstempelAlsDatum(kandidat);
if (!d) continue;
const z = d.zeit, h = new Date();
return new Date(z.getFullYear(), z.getMonth(), z.getDate()).getTime()
<= new Date(h.getFullYear(), h.getMonth(), h.getDate()).getTime();
}
return false;
}
const UEBERSCHRITTEN_MARKE = '<span class="ueberschritten-marke">überschritten</span>';
/* Zeigt eine monatsgenaue Angabe als Monat statt als erfundenen Ersten. */ /* Zeigt eine monatsgenaue Angabe als Monat statt als erfundenen Ersten. */
const demonat = (d) => (gueltigesDatum(d) const demonat = (d) => (gueltigesDatum(d)
? d.toLocaleDateString("de-DE", { month: "long", year: "numeric" }) ? d.toLocaleDateString("de-DE", { month: "long", year: "numeric" })
@@ -2434,11 +2480,15 @@ function vHome() {
const figMarkup = artIcon const figMarkup = artIcon
? `<div class="fig-zeile">${ciSVG(artIcon, 31)}<div class="fig" style="font-size:${zahlGroesse}px">${hauptwert}</div></div>` ? `<div class="fig-zeile">${ciSVG(artIcon, 31)}<div class="fig" style="font-size:${zahlGroesse}px">${hauptwert}</div></div>`
: `<div class="fig" style="font-size:${zahlGroesse}px;margin-top:8px">${hauptwert}</div>`; : `<div class="fig" style="font-size:${zahlGroesse}px;margin-top:8px">${hauptwert}</div>`;
// Auf der Uebersicht wechselt zusaetzlich die Kennzahl selbst die Farbe -
// sie ist hier die Aussage, nicht nur ihr Rahmen (Ansage des Eigentuemers).
const ueber = terminUeberschritten(datum, t.datum);
return `<div class="tile" style="margin-bottom:22px"> return `<div class="tile" style="margin-bottom:22px">
<button class="serviceblock" data-go="service:x"> <button class="serviceblock${ueber ? " ueberschritten" : ""}" data-go="service:x">
<span class="label">Nächster Service:</span> <span class="label">Nächster Service:</span>
<svg class="go" viewBox="0 0 6 10"><polyline points="1,1 5,5 1,9"/></svg> <svg class="go" viewBox="0 0 6 10"><polyline points="1,1 5,5 1,9"/></svg>
${figMarkup} ${figMarkup}
${ueber ? `<div class="servicemarke">${UEBERSCHRITTEN_MARKE}</div>` : ""}
<span class="label" style="margin-top:8px">${nebenzeile}</span> <span class="label" style="margin-top:8px">${nebenzeile}</span>
</button> </button>
${kmZeile} ${kmZeile}
@@ -2610,15 +2660,17 @@ function vAudi() {
const eigen = prognoseZeile(t.art); const eigen = prognoseZeile(t.art);
if (fm.ts == null && fm.km == null) { if (fm.ts == null && fm.km == null) {
const x = terminText(t); const x = terminText(t);
const ueber = terminUeberschritten(t.ziel, t.datum);
return `<div class="row" ${letzte}> return `<div class="row" ${letzte}>
<dt>${t.art}${t.art === "Inspektion" ? eigen : ""}</dt> <dt>${t.art}${t.art === "Inspektion" ? eigen : ""}</dt>
<dd>${x.haupt}${x.neben ? `<small style="text-align:left">${x.neben}</small>` : ""}</dd></div>`; <dd>${x.haupt}${ueber ? UEBERSCHRITTEN_MARKE : ""}${x.neben ? `<small style="text-align:left">${x.neben}</small>` : ""}</dd></div>`;
} }
const haupt = fm.ts ? dedat(new Date(fm.ts)) : ""; const haupt = fm.ts ? dedat(new Date(fm.ts)) : "";
const neben = fm.km != null ? "in " + de(fm.km) + " km" : ""; const neben = fm.km != null ? "in " + de(fm.km) + " km" : "";
const ueber = terminUeberschritten(fm.ts, t.ziel, t.datum);
return `<div class="row" ${letzte}> return `<div class="row" ${letzte}>
<dt>${t.art}${eigen}</dt> <dt>${t.art}${eigen}</dt>
<dd>${haupt}${neben ? `<span style="display:block">${neben}</span>` : ""}</dd></div>`; <dd>${haupt}${ueber ? UEBERSCHRITTEN_MARKE : ""}${neben ? `<span style="display:block">${neben}</span>` : ""}</dd></div>`;
}).join("")} }).join("")}
</dl></button> </dl></button>
<button class="tile tilebtn" data-go="vers:x"> <button class="tile tilebtn" data-go="vers:x">
@@ -3024,10 +3076,13 @@ function vService() {
Wartungsplan" gezeigt, waehrend die Uebersicht denselben Termin Wartungsplan" gezeigt, waehrend die Uebersicht denselben Termin
auswies - gemeldet vom Eigentuemer, zwei Bildschirme, zwei auswies - gemeldet vom Eigentuemer, zwei Bildschirme, zwei
Antworten. */ Antworten. */
if (!iv && !hatMeldung && t.datum) return `<div class="termingruppe"> if (!iv && !hatMeldung && t.datum) {
<div class="tg-kopf">${kopf}</div> const ueber = terminUeberschritten(t.datum);
return `<div class="termingruppe${ueber ? " ueberschritten" : ""}">
<div class="tg-kopf">${kopf}${ueber ? UEBERSCHRITTEN_MARKE : ""}</div>
<div class="tg-wert">${datumText(t.datum, t.genau)}</div> <div class="tg-wert">${datumText(t.datum, t.genau)}</div>
</div>`; </div>`;
}
if (!iv && !hatMeldung) return `<div class="termingruppe"> if (!iv && !hatMeldung) return `<div class="termingruppe">
<div class="tg-kopf">${kopf}</div> <div class="tg-kopf">${kopf}</div>
<div class="tg-wert leer">kein Eintrag im Wartungsplan</div> <div class="tg-wert leer">kein Eintrag im Wartungsplan</div>
@@ -3074,9 +3129,13 @@ function vService() {
// als Herstellervorgabe; nur der App-eigene Eigen-Intervall-Fallback // als Herstellervorgabe; nur der App-eigene Eigen-Intervall-Fallback
// (anzeige.eigen/eigenOel bei modus "eigen") ist es nicht. // (anzeige.eigen/eigenOel bei modus "eigen") ist es nicht.
const herstellervorgabe = hatMeldung || (iv && iv.hersteller); const herstellervorgabe = hatMeldung || (iv && iv.hersteller);
return `<div class="termingruppe"> // Dasselbe Datum, das gleich als `wert` erscheint - siehe
// terminUeberschritten().
const ueber = terminUeberschritten(
hatMeldung ? fm.ts : (eigenOel ? eigenOel.datum : (anzeige ? anzeige.datum : null)));
return `<div class="termingruppe${ueber ? " ueberschritten" : ""}">
<div class="tg-kopf"> <div class="tg-kopf">
${kopf} ${kopf}${ueber ? UEBERSCHRITTEN_MARKE : ""}
</div> </div>
${herstellervorgabe ? `<div class="tg-vorgabe">Herstellervorgabe</div>` : ""} ${herstellervorgabe ? `<div class="tg-vorgabe">Herstellervorgabe</div>` : ""}
<div class="tg-wert">${wert}</div> <div class="tg-wert">${wert}</div>
@@ -25,7 +25,7 @@
--line:rgba(255,255,255,.13); --line:rgba(255,255,255,.13);
--line-strong:rgba(255,255,255,.24); --line-strong:rgba(255,255,255,.24);
--fg:#FFFFFF; --fg2:#BFC4CC; --fg3:#8E8E93; --fg:#FFFFFF; --fg2:#BFC4CC; --fg3:#8E8E93;
--ok:#30D158; --warn:#FFD60A; --bad:#FF453A; --bad-flaeche:#D70015; --ok:#30D158; --warn:#FFD60A; --bad:#FF453A; --bad-flaeche:#D70015; --warn-text:#FFD60A;
/* Ohne diese Angabe rendert der Browser native Steuerelemente (Dropdown- /* Ohne diese Angabe rendert der Browser native Steuerelemente (Dropdown-
Aufklapp-Liste bei <select>, Scrollbars) in seiner Standard-Hellansicht, Aufklapp-Liste bei <select>, Scrollbars) in seiner Standard-Hellansicht,
egal wie dunkel die App selbst ist - das liess z. B. "Modell" beim egal wie dunkel die App selbst ist - das liess z. B. "Modell" beim
@@ -45,7 +45,7 @@
auf 4,75:1 bzw. 5,30:1, ohne den ruhigen Gesamteindruck zu aendern. auf 4,75:1 bzw. 5,30:1, ohne den ruhigen Gesamteindruck zu aendern.
Der Nachtwert bleibt #8E8E93 - dort sind es ohnehin 5,86:1. */ Der Nachtwert bleibt #8E8E93 - dort sind es ohnehin 5,86:1. */
--fg:#000000; --fg2:#3C3C43; --fg3:#6B6B70; --fg:#000000; --fg2:#3C3C43; --fg3:#6B6B70;
--ok:#34C759; --warn:#FF9F0A; --bad:#D70015; --bad-flaeche:#D70015; --ok:#34C759; --warn:#FF9F0A; --bad:#D70015; --bad-flaeche:#D70015; --warn-text:#A15800;
--ios-sep:rgba(60,60,67,.16); --ios-sep:rgba(60,60,67,.16);
--ios-fill:rgba(120,120,128,.16); --ios-fill:rgba(120,120,128,.16);
--tile-deckend:#FFFFFF; --tile-deckend:#FFFFFF;
@@ -21,6 +21,7 @@
--fg3: #8a94a3; --fg3: #8a94a3;
--ok: #15da15; --ok: #15da15;
--warn: #ffaa00; --warn: #ffaa00;
--warn-text: #ffaa00;
--bad: #fd2c4e; --bad: #fd2c4e;
--shade: rgba(255,255,255,.05); --shade: rgba(255,255,255,.05);
--font-stack: "Audi Type", Helvetica, Arial, sans-serif; --font-stack: "Audi Type", Helvetica, Arial, sans-serif;
@@ -50,6 +51,7 @@
--fg3: #666666; --fg3: #666666;
--ok: #0DA20D; --ok: #0DA20D;
--warn: #ffaa00; --warn: #ffaa00;
--warn-text: #ffaa00;
--bad: #eb0d3f; --bad: #eb0d3f;
--shade: rgba(0,0,0,.04); --shade: rgba(0,0,0,.04);
} }
@@ -653,6 +655,32 @@ button.leaf:active { background: var(--tile-2); }
.tg-wert.leer { font-family: var(--font-stack); font-weight: 400; font-size: 15px; color: var(--fg3); margin-top: 8px; } .tg-wert.leer { font-family: var(--font-stack); font-weight: 400; font-size: 15px; color: var(--fg3); margin-top: 8px; }
.tg-prognose { font-size: 12.5px; color: var(--fg2); margin-top: 6px; } .tg-prognose { font-size: 12.5px; color: var(--fg2); margin-top: 6px; }
/* Ueberschrittene Servicetermine: dezent hinterlegt statt umgefaerbt, damit die
Zeile lesbar bleibt und der Hinweis trotzdem auffaellt (Ansage des
Eigentuemers 07.09.2026). --warn ist die Flaeche, --warn-text die Schrift -
dieselbe Rollenteilung wie bei --bad/--bad-flaeche, und aus demselben Grund:
#FF9F0A als Text auf Weiss sind 2.06:1. Auf dem hier hinterlegten Grund
(#FFF3E2 tags, #33311E nachts) misst --warn-text 4.90:1 bzw. 9.30:1. */
.termingruppe.ueberschritten {
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 Trennlinie zur Vorgaengerzeile wuerde ueber die abgerundete Flaeche
laufen - die uebernimmt hier die Abgrenzung. */
.termingruppe.ueberschritten, .termingruppe.ueberschritten + .termingruppe { border-top: 0; }
.ueberschritten-marke {
font-size: 10.5px; color: var(--warn-text);
border: 1px solid var(--warn-text); border-radius: var(--r-pill);
padding: 3px 8px; white-space: nowrap; flex: 0 0 auto;
}
/* Auf der Uebersicht wechselt zusaetzlich die Schrift der Kennzahl - dort ist
sie die Aussage, nicht nur ihr Rahmen. */
.serviceblock.ueberschritten .fig, .serviceblock.ueberschritten .fig .unit { color: var(--warn-text); }
.servicemarke { display: inline-flex; margin-top: 8px; }
/* In der Zeilenliste von "Mein Audi" reicht die Marke neben dem Wert. */
.row .ueberschritten-marke { margin-left: 8px; vertical-align: middle; }
.badge { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; } .badge { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.badge img { height: 18px; width: auto; display: none; } .badge img { height: 18px; width: auto; display: none; }
:host([data-theme=nacht]) .badge img.negative { display: block; } :host([data-theme=nacht]) .badge img.negative { display: block; }
@@ -1,7 +1,7 @@
{ {
"domain": "audi_dashboard", "domain": "audi_dashboard",
"name": "Audi Dashboard", "name": "Audi Dashboard",
"version": "2026.9.6.5", "version": "2026.9.6.6",
"documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md", "documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md",
"issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues", "issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues",
"codeowners": [ "codeowners": [
+2
View File
@@ -38,6 +38,7 @@
--fg3: #8E8E93; --fg3: #8E8E93;
--ok: #30D158; --ok: #30D158;
--warn: #FFD60A; --warn: #FFD60A;
--warn-text: #FFD60A;
--bad: #FF453A; --bad: #FF453A;
/* Rot als FLAECHE, nicht als Schrift. /* Rot als FLAECHE, nicht als Schrift.
@@ -84,6 +85,7 @@
--fg3: #6B6B70; --fg3: #6B6B70;
--ok: #34C759; --ok: #34C759;
--warn: #FF9F0A; --warn: #FF9F0A;
--warn-text: #A15800;
/* #FF3B30 (Apples systemRed) auf #F2F2F7 ergab 3,18 - unter den 4,5 fuer /* #FF3B30 (Apples systemRed) auf #F2F2F7 ergab 3,18 - unter den 4,5 fuer
16px-Schrift. #D70015 ist Apples systemRed fuer erhoehten Kontrast und 16px-Schrift. #D70015 ist Apples systemRed fuer erhoehten Kontrast und
kommt auf 4,83 (auf Weiss 5,38). Gemessen am 04.09.2026 am kommt auf 4,83 (auf Weiss 5,38). Gemessen am 04.09.2026 am