Selbst-Update-Sicherung, Service-Prognose ohne Servicebuch und Sicherheit neu gestaltet
- aktualisierung.py: Backup-Ordner verliert sein manifest.json beim Sichern, sonst laedt HA ihn nach einem Neustart als zweite audi_dashboard-Integration (Ursache des raetselhaften "gleicher Pfad"-Umbenennungs-Fehlers). - service.ts/audi-dashboard-app.js: Uebersicht faellt jetzt wie Service schon auf die Fahrzeugmeldung zurueck, wenn kein Servicebucheintrag existiert; eigenes (kuerzeres) Oelwechsel-Intervall wird jetzt auch ohne Servicebuch- eintrag naeherungsweise aus der Herstellermeldung hochgerechnet, statt weiterhin unveraendert die Herstellervorgabe zu zeigen. - Sicherheit neu gestaltet: vier Sammelzeilen (Fahrzeug verriegelt / Tueren und Klappen geschlossen / Fenster und Dach geschlossen / Kein Licht) statt einer flachen Liste, mit neuen Tuerschloss-, Dach- und Standlicht-Sensor- rollen, Kreis-Haekchen-Symbolen und einer Detailseite je Tuer/Motorhaube/ Kofferraum in beiden Frontends. Details, Verifikation und Sensor-Rollen in AGENTS.md (Abschnitte J/M/N). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1288,14 +1288,53 @@ function kmProTagAusFahrten() {
|
||||
function meldungsPrognose(art) {
|
||||
const fm = fahrzeugMeldungRoh(art);
|
||||
const zeitDatum = fm.ts && gueltigesDatum(new Date(fm.ts)) ? new Date(fm.ts) : null;
|
||||
if (fm.km == null) return zeitDatum ? { datum: zeitDatum, durchZeitlimit: true } : null;
|
||||
if (fm.km == null) return zeitDatum ? { datum: zeitDatum, durchZeitlimit: true, restKm: null } : null;
|
||||
const restKm = Math.abs(fm.km);
|
||||
if (restKm <= 0) return { datum: heute(), durchZeitlimit: false };
|
||||
if (restKm <= 0) return { datum: heute(), durchZeitlimit: false, restKm };
|
||||
const rate = kmProTagAusFahrten();
|
||||
if (!rate) return zeitDatum ? { datum: zeitDatum, durchZeitlimit: true } : null;
|
||||
if (!rate) return zeitDatum ? { datum: zeitDatum, durchZeitlimit: true, restKm } : null;
|
||||
const kmDatum = new Date(heute().getTime() + (restKm / rate) * dTag);
|
||||
const durchZeitlimit = !!(zeitDatum && zeitDatum < kmDatum);
|
||||
return { datum: durchZeitlimit ? zeitDatum : kmDatum, durchZeitlimit };
|
||||
return { datum: durchZeitlimit ? zeitDatum : kmDatum, durchZeitlimit, restKm };
|
||||
}
|
||||
|
||||
/* Eigenes (kürzeres) Intervall ganz ohne Servicebucheintrag: die
|
||||
Fahrzeugmeldung kennt nur das Herstellerintervall, nicht die in
|
||||
"Einrichten" gewählte kürzere Vorgabe. Ein angenommener letzter Service
|
||||
wird aus der gemeldeten Fälligkeit und der bekannten Herstellerintervall-
|
||||
Länge zurückgerechnet ("Fälligkeitspunkt minus Herstellerintervall"), und
|
||||
das eigene Intervall ab demselben angenommenen Punkt neu hochgerechnet -
|
||||
eine Näherung (der wahre letzte Service kann näher oder ferner liegen),
|
||||
aber ohne sie würde die eigene Intervallwahl bis zum ersten
|
||||
Servicebucheintrag komplett ignoriert (siehe oelMeldungsPrognose()). */
|
||||
function meldungsPrognoseEigenesIntervall(fm, herstellerKm, herstellerMonate, eigenKm, eigenMonate) {
|
||||
if (fm.km == null || !herstellerKm || eigenKm == null) return null;
|
||||
const restKm = Math.max(0, Math.abs(fm.km) - herstellerKm + eigenKm);
|
||||
let zeitDatum = null;
|
||||
if (fm.ts && gueltigesDatum(new Date(fm.ts)) && herstellerMonate != null && eigenMonate != null) {
|
||||
zeitDatum = new Date(fm.ts);
|
||||
zeitDatum.setMonth(zeitDatum.getMonth() + (eigenMonate - herstellerMonate));
|
||||
}
|
||||
if (restKm <= 0) return { datum: heute(), durchZeitlimit: false, restKm };
|
||||
const rate = kmProTagAusFahrten();
|
||||
if (!rate) return zeitDatum ? { datum: zeitDatum, durchZeitlimit: true, restKm } : null;
|
||||
const kmDatum = new Date(heute().getTime() + (restKm / rate) * dTag);
|
||||
const durchZeitlimit = !!(zeitDatum && zeitDatum < kmDatum);
|
||||
return { datum: durchZeitlimit ? zeitDatum : kmDatum, durchZeitlimit, restKm };
|
||||
}
|
||||
|
||||
/* Fällt ganz ohne Servicebucheintrag auf die Fahrzeugmeldung zurück - bei
|
||||
Herstellervorgabe unverändert (die Meldung IST die Herstellervorgabe), bei
|
||||
eigenem (kürzerem) Intervall über meldungsPrognoseEigenesIntervall()
|
||||
hochgerechnet, damit die Intervallwahl in "Einrichten" auch ganz ohne
|
||||
Servicebucheintrag etwas bewirkt. Fehlen die Herstellerwerte selbst (kein
|
||||
Sensor dafür), fällt das sanft auf die reine Meldung zurück. */
|
||||
function oelMeldungsPrognose() {
|
||||
const oel = CAR.oel;
|
||||
if (oel.modus === "hersteller") return meldungsPrognose("Ölwechsel");
|
||||
const fm = fahrzeugMeldungRoh("Ölwechsel");
|
||||
const eigen = meldungsPrognoseEigenesIntervall(fm, oel.herstellerKm, oel.herstellerMonate, oel.km, oel.monate);
|
||||
return eigen || meldungsPrognose("Ölwechsel");
|
||||
}
|
||||
|
||||
function oelwechselPrognose() {
|
||||
@@ -1437,29 +1476,82 @@ function vHome() {
|
||||
${teaser()}`;
|
||||
}
|
||||
|
||||
/* Kreis-Häkchen (ok) / Kreis-Kreuz (nicht ok) / Kreis-Fragezeichen (unbekannt)
|
||||
statt eines einfarbigen Punkts - Zustand ist so schon am Symbol selbst
|
||||
erkennbar, nicht nur an der Farbe. Nur für die Sicherheit-Ansicht (vSicherheit()/
|
||||
vTuerenKlappen()) gedacht, der übrige Bestand behält den einfachen .dot. */
|
||||
function statusKreis(ok) {
|
||||
const klasse = ok === null ? "warn" : ok ? "" : "bad";
|
||||
const glyph = ok === null
|
||||
? '<path d="M5.7 6a2.3 2.3 0 1 1 2.7 2.27c-.5.09-.7.5-.7 1.03v.2M8 11.6v.01" stroke="#fff" stroke-width="1.5" stroke-linecap="round" fill="none"/>'
|
||||
: ok
|
||||
? '<path d="M3 8.3 6.2 11.5 13 4.5" stroke="#fff" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"/>'
|
||||
: '<path d="M4 4l8 8M12 4l-8 8" stroke="#fff" stroke-width="1.8" stroke-linecap="round"/>';
|
||||
return `<span class="statuskreis ${klasse}" aria-hidden="true"><svg viewBox="0 0 16 16" width="13" height="13">${glyph}</svg></span>`;
|
||||
}
|
||||
|
||||
/* Gruppiert die flache CAR.sicherheitscheck-Liste (siehe _sicherheitscheck()
|
||||
in veroeffentlichung.py) nach ihrem "gruppe"-Feld. ok ist null, sobald ein
|
||||
einziger Punkt der Gruppe unbekannt ist, oder wenn die Gruppe ganz ohne
|
||||
Einträge dasteht (kein Sensor dieser Rolle im Setup-Menü zugeordnet) -
|
||||
nie true durch eine leere Liste erschlichen. */
|
||||
function sicherheitsGruppe(gruppe) {
|
||||
const eintraege = CAR.sicherheitscheck.filter((e) => e.gruppe === gruppe);
|
||||
const ok = eintraege.length === 0 || eintraege.some((e) => e.ok === null)
|
||||
? null
|
||||
: eintraege.every((e) => e.ok);
|
||||
return { eintraege, ok };
|
||||
}
|
||||
|
||||
function vSicherheit() {
|
||||
const eintraege = CAR.sicherheitscheck;
|
||||
const g = CAR.gesichert;
|
||||
const offen = eintraege.filter((e) => e.ok === false).length;
|
||||
const unbekannt = eintraege.filter((e) => e.ok === null).length;
|
||||
const sammelText = g === null
|
||||
? `${unbekannt} von ${eintraege.length} Punkten liefern derzeit keinen Wert`
|
||||
: g ? "Fahrzeug ist sicher abgestellt" : `${offen} Punkt${offen === 1 ? "" : "e"} prüfen`;
|
||||
/* C6: Draufsicht oben, grüne Sammelmeldung, dann die Prüfliste - dieselbe
|
||||
Reihenfolge wie der Fahrzeugstatus in der myAudi-App. */
|
||||
const verriegelt = sicherheitsGruppe("verriegelt");
|
||||
const tuerenKlappen = sicherheitsGruppe("tueren_klappen");
|
||||
const fensterDach = sicherheitsGruppe("fenster_dach");
|
||||
const licht = sicherheitsGruppe("licht");
|
||||
const sammelText = g === null ? "Zustand nicht vollständig bekannt" : g ? "Fahrzeug ist sicher abgestellt" : "Bitte prüfen";
|
||||
// Jede Zeile nennt beim Nutzer-Zustand ("ok") genau die Formulierung, die
|
||||
// der Nutzer vorgegeben hat - beim Licht ist das "Kein Licht", nicht
|
||||
// "Licht ausgeschaltet" (das beschreibt nur die Prüfung, nicht den Wert).
|
||||
const zeile = (label, ok, labelBad, labelWarn, route) => {
|
||||
const text = ok === null ? labelWarn : ok ? label : labelBad;
|
||||
const klickbar = !!route;
|
||||
return `<${klickbar ? "button" : "div"} class="row sicherheitszeile"${klickbar ? ` data-go="${route}:x"` : ""}>
|
||||
${statusKreis(ok)}
|
||||
<span class="sicherheitszeile-text">${esc(text)}</span>
|
||||
${klickbar ? '<svg class="chev" viewBox="0 0 6 10"><polyline points="1,1 5,5 1,9"/></svg>' : ""}
|
||||
</${klickbar ? "button" : "div"}>`;
|
||||
};
|
||||
/* C6: Draufsicht oben, grüne Sammelmeldung, dann die vier Sammelzeilen -
|
||||
dieselbe Reihenfolge wie der Fahrzeugstatus in der myAudi-App. */
|
||||
return `
|
||||
<div class="szene bleed"><div class="carbox">${bildMitPlatzhalter(`/local/bilder/draufsicht.webp?v=${bildVersion}`, "draufsicht.webp", "Draufsicht", "carfix klein")}</div></div>
|
||||
<div class="tile" style="display:flex;align-items:center;gap:12px;padding:16px 20px">
|
||||
<span class="dot ${g === null ? "warn" : g ? "" : "bad"}"></span>
|
||||
${statusKreis(g)}
|
||||
<span style="font-size:15px;color:var(--fg)">${esc(sammelText)}</span>
|
||||
</div>
|
||||
<div class="tile"><span class="label">Geprüfte Punkte</span>
|
||||
<div class="tile sicherheitsliste">
|
||||
${zeile("Fahrzeug verriegelt", verriegelt.ok, "Fahrzeug nicht verriegelt", "Verriegelung unbekannt")}
|
||||
${zeile("Türen und Klappen geschlossen", tuerenKlappen.ok, "Türen oder Klappen offen", "Türen/Klappen unbekannt", "tuerenklappen")}
|
||||
${zeile("Fenster und Dach geschlossen", fensterDach.ok, "Fenster oder Dach offen", "Fenster/Dach unbekannt")}
|
||||
${zeile("Kein Licht", licht.ok, "Licht eingeschaltet", "Licht unbekannt")}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/* Detailansicht zu "Türen und Klappen geschlossen" - jede Tür einzeln,
|
||||
Motorhaube, Kofferraum. Einzige der vier Sammelzeilen mit eigener
|
||||
Detailseite (siehe vSicherheit()), im Stil der früheren flachen
|
||||
"Geprüfte Punkte"-Liste. */
|
||||
function vTuerenKlappen() {
|
||||
const eintraege = sicherheitsGruppe("tueren_klappen").eintraege;
|
||||
return `
|
||||
<div class="tile"><span class="label">Türen und Klappen</span>
|
||||
<dl class="rows" style="margin-top:10px">
|
||||
${eintraege.length ? eintraege.map((e, i, arr) => `<div class="row" ${i === arr.length - 1 ? 'style="border-bottom:0"' : ""}>
|
||||
<dt>${esc(e.label)}</dt>
|
||||
<dd style="display:flex;align-items:center;justify-content:flex-end;gap:8px">
|
||||
<span class="dot ${e.ok === null ? "warn" : e.ok ? "" : "bad"}"></span>
|
||||
${e.ok === null ? "unbekannt" : e.ok ? "in Ordnung" : "prüfen"}
|
||||
${e.ok === null ? "unbekannt" : e.ok ? "zu" : "offen"}
|
||||
</dd>
|
||||
</div>`).join("") : `<div class="row" style="border-bottom:0"><dt style="color:var(--fg3)">Keine Daten verfügbar</dt></div>`}
|
||||
</dl>
|
||||
@@ -1837,10 +1929,15 @@ function vService() {
|
||||
const eigenOel = t.art === "Ölwechsel" ? oelwechselPrognose() : null;
|
||||
const eigenInsp = t.art === "Inspektion" ? inspektionPrognose() : null;
|
||||
// Ganz ohne Servicebucheintrag (eigenOel/eigenInsp beide null) übernimmt
|
||||
// die Fahrzeugmeldung selbst die Rolle der Prognosequelle, hochgerechnet
|
||||
// mit der echten Fahrleistung aus dem Fahrtenlog statt einer
|
||||
// Servicebuch-Rate - siehe meldungsPrognose().
|
||||
const meldungProg = hatMeldung && !eigenOel && !eigenInsp ? meldungsPrognose(t.art) : null;
|
||||
// die Fahrzeugmeldung selbst die Rolle der Prognosequelle - bei
|
||||
// Herstellervorgabe unverändert, beim Ölwechsel mit eigenem (kürzerem)
|
||||
// Intervall über oelMeldungsPrognose() auf dieses umgerechnet, statt es
|
||||
// bis zum ersten Servicebucheintrag zu ignorieren (siehe deren
|
||||
// Kommentar). Hochgerechnet wird dabei mit der echten Fahrleistung aus
|
||||
// dem Fahrtenlog statt einer Servicebuch-Rate.
|
||||
const meldungProg = hatMeldung && !eigenOel && !eigenInsp
|
||||
? (t.art === "Ölwechsel" ? oelMeldungsPrognose() : meldungsPrognose(t.art))
|
||||
: null;
|
||||
// Bei Herstellervorgabe rechnet die eigene Prognose ohnehin schon mit dem
|
||||
// Herstellerintervall - die Restkilometerzahl daneben waere nur eine
|
||||
// zweite, verwirrende km-Angabe neben dem Fahrzeug-Wert oben. Nur beim
|
||||
@@ -1850,7 +1947,11 @@ function vService() {
|
||||
? `voraussichtlich am ${dedat(eigenOel.datum)}`
|
||||
: `${de(eigenOel.restKm)} km, voraussichtlich am ${dedat(eigenOel.datum)}`)
|
||||
: eigenInsp ? `voraussichtlich am ${dedat(eigenInsp.datum)}`
|
||||
: meldungProg ? `voraussichtlich am ${dedat(meldungProg.datum)}` : "";
|
||||
: meldungProg
|
||||
? (t.art === "Ölwechsel" && CAR.oel.modus !== "hersteller" && meldungProg.restKm != null
|
||||
? `${de(meldungProg.restKm)} km, voraussichtlich am ${dedat(meldungProg.datum)}`
|
||||
: `voraussichtlich am ${dedat(meldungProg.datum)}`)
|
||||
: "";
|
||||
const wert = hatMeldung
|
||||
? `${fm.km != null ? de(fm.km) + " km" : ""}${fm.km != null && fmDatum ? " / " : ""}${fmDatum || ""}`
|
||||
: (eigenOel ? de(eigenOel.restKm) + " km · " + dedat(eigenOel.datum) : (anzeige.km !== null ? de(anzeige.km) + " km · " : "") + dedat(anzeige.datum));
|
||||
@@ -1961,9 +2062,26 @@ function termine() {
|
||||
oelTermin.zuerst = oelTermin.datum && oelProg.datum >= oelTermin.datum ? "zeit" : "km";
|
||||
oelTermin.ziel = oelTermin.zuerst === "km" ? oelProg.datum : oelTermin.datum;
|
||||
}
|
||||
const inspTermin = bauen("Inspektion", letzterEintrag("inspektion"), 30000, 24);
|
||||
// Ganz ohne Servicebucheintrag (kein basis) übernimmt die Fahrzeugmeldung
|
||||
// selbst die Rolle der Prognosequelle - genau wie vService()'s eigenes
|
||||
// meldungProg das schon tut (siehe dessen Kommentar dort). Ohne diesen
|
||||
// Fallback hier zeigte "Übersicht" trotz korrekt gemeldeter Fälligkeit nur
|
||||
// "kein Eintrag im Servicebuch", während "Service" dieselbe Meldung
|
||||
// längst richtig anzeigte - zwei Ansichten, ein Datenstand, zwei
|
||||
// widersprüchliche Aussagen.
|
||||
[oelTermin, inspTermin].forEach((termin) => {
|
||||
if (termin.basis) return;
|
||||
const m = termin.art === "Ölwechsel" ? oelMeldungsPrognose() : meldungsPrognose(termin.art);
|
||||
if (!m) return;
|
||||
termin.km = m.restKm != null && CAR.odoBekannt ? CAR.odo + m.restKm : null;
|
||||
termin.kmDatum = m.datum;
|
||||
termin.zuerst = "km";
|
||||
termin.ziel = m.datum;
|
||||
});
|
||||
return [
|
||||
oelTermin,
|
||||
bauen("Inspektion", letzterEintrag("inspektion"), 30000, 24),
|
||||
inspTermin,
|
||||
bauen("Hauptuntersuchung", letzterEintrag("hauptuntersuchung"), null, 24,
|
||||
CAR.erstzulassung && CAR.erstzulassung.includes("/") ? `01.${CAR.erstzulassung.replace("/", ".")}` : null),
|
||||
];
|
||||
@@ -3404,6 +3522,7 @@ const ZURUECK = {
|
||||
Zurück-Pfeil. */
|
||||
sicherheit: "home",
|
||||
standort: "home",
|
||||
tuerenklappen: "sicherheit",
|
||||
};
|
||||
const ICON = {
|
||||
sonne: '<circle cx="10" cy="10" r="4"/><path d="M10 1v2M10 17v2M1 10h2M17 10h2M3.9 3.9l1.4 1.4M14.7 14.7l1.4 1.4M16.1 3.9l-1.4 1.4M5.3 14.7l-1.4 1.4"/>',
|
||||
@@ -3748,6 +3867,7 @@ function render() {
|
||||
else if (route.name === "einst") { head = ["App", "Einstellungen"]; v.innerHTML = vEinst(); }
|
||||
else if (route.name === "ident") { head = ["Fahrzeug", "Identität und Technik"]; v.innerHTML = vIdent(); }
|
||||
else if (route.name === "sicherheit") { head = ["Fahrzeug", "Sicherheit"]; v.innerHTML = vSicherheit(); }
|
||||
else if (route.name === "tuerenklappen") { head = ["Fahrzeug", "Türen und Klappen"]; v.innerHTML = vTuerenKlappen(); }
|
||||
else if (route.name === "vers") { head = ["Fahrzeug", "Versicherung/Steuer"]; v.innerHTML = vVers(); }
|
||||
else if (route.name === "beitrag") { head = ["Versicherung", "Beitrag anpassen"]; v.innerHTML = vBeitrag(); }
|
||||
else if (route.name === "vertragsdetails") { head = ["Versicherung", "Vertragsdetails"]; v.innerHTML = vVertragsdetails(); }
|
||||
|
||||
Reference in New Issue
Block a user