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
@@ -741,6 +741,52 @@ function datumAusText(wert) {
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. */
const demonat = (d) => (gueltigesDatum(d)
? d.toLocaleDateString("de-DE", { month: "long", year: "numeric" })
@@ -2434,11 +2480,15 @@ function vHome() {
const figMarkup = artIcon
? `<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>`;
// 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">
<button class="serviceblock" data-go="service:x">
<button class="serviceblock${ueber ? " ueberschritten" : ""}" data-go="service:x">
<span class="label">Nächster Service:</span>
<svg class="go" viewBox="0 0 6 10"><polyline points="1,1 5,5 1,9"/></svg>
${figMarkup}
${ueber ? `<div class="servicemarke">${UEBERSCHRITTEN_MARKE}</div>` : ""}
<span class="label" style="margin-top:8px">${nebenzeile}</span>
</button>
${kmZeile}
@@ -2610,15 +2660,17 @@ function vAudi() {
const eigen = prognoseZeile(t.art);
if (fm.ts == null && fm.km == null) {
const x = terminText(t);
const ueber = terminUeberschritten(t.ziel, t.datum);
return `<div class="row" ${letzte}>
<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 neben = fm.km != null ? "in " + de(fm.km) + " km" : "";
const ueber = terminUeberschritten(fm.ts, t.ziel, t.datum);
return `<div class="row" ${letzte}>
<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("")}
</dl></button>
<button class="tile tilebtn" data-go="vers:x">
@@ -3024,10 +3076,13 @@ function vService() {
Wartungsplan" gezeigt, waehrend die Uebersicht denselben Termin
auswies - gemeldet vom Eigentuemer, zwei Bildschirme, zwei
Antworten. */
if (!iv && !hatMeldung && t.datum) return `<div class="termingruppe">
<div class="tg-kopf">${kopf}</div>
if (!iv && !hatMeldung && t.datum) {
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>`;
}
if (!iv && !hatMeldung) return `<div class="termingruppe">
<div class="tg-kopf">${kopf}</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
// (anzeige.eigen/eigenOel bei modus "eigen") ist es nicht.
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">
${kopf}
${kopf}${ueber ? UEBERSCHRITTEN_MARKE : ""}
</div>
${herstellervorgabe ? `<div class="tg-vorgabe">Herstellervorgabe</div>` : ""}
<div class="tg-wert">${wert}</div>