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
@@ -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;
}
/* 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>
@@ -25,7 +25,7 @@
--line:rgba(255,255,255,.13);
--line-strong:rgba(255,255,255,.24);
--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-
Aufklapp-Liste bei <select>, Scrollbars) in seiner Standard-Hellansicht,
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.
Der Nachtwert bleibt #8E8E93 - dort sind es ohnehin 5,86:1. */
--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-fill:rgba(120,120,128,.16);
--tile-deckend:#FFFFFF;
@@ -21,6 +21,7 @@
--fg3: #8a94a3;
--ok: #15da15;
--warn: #ffaa00;
--warn-text: #ffaa00;
--bad: #fd2c4e;
--shade: rgba(255,255,255,.05);
--font-stack: "Audi Type", Helvetica, Arial, sans-serif;
@@ -50,6 +51,7 @@
--fg3: #666666;
--ok: #0DA20D;
--warn: #ffaa00;
--warn-text: #ffaa00;
--bad: #eb0d3f;
--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-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 img { height: 18px; width: auto; display: none; }
:host([data-theme=nacht]) .badge img.negative { display: block; }