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:
2026-08-24 15:29:37 +02:00
parent 146f809e9b
commit 5a048488ea
23 changed files with 866 additions and 97 deletions
@@ -94,7 +94,7 @@ describe("profilZuFahrzeug mit Fahrzeugstatus", () => {
tankprozent: 62,
reichweite_km: 385,
gesichert: true,
sicherheitscheck: [{ label: "Tür vorne links", ok: true }],
sicherheitscheck: [{ label: "Tür vorne links", ok: true, gruppe: "tueren_klappen" }],
})
expect(f.odo).toBe(48250)
expect(f.odoBekannt).toBe(true)
+94
View File
@@ -5,7 +5,9 @@ import {
kmProTagAusFahrten,
letzterOelwechsel,
meldungsPrognose,
meldungsPrognoseEigenesIntervall,
naechsterService,
oelMeldungsPrognose,
oelwechselPrognose,
} from "./service"
import type { Fahrzeug } from "./profilAdapter"
@@ -252,4 +254,96 @@ describe("meldungsPrognose", () => {
it("liefert nichts ohne jede brauchbare Angabe", () => {
expect(meldungsPrognose(null, null, fahrten, jetzt)).toBeNull()
})
it("gibt die Restkilometer mit zurück", () => {
const p = meldungsPrognose("2027-01-01T00:00:00+00:00", 5000, [], jetzt)
expect(p!.restKm).toBe(5000)
})
})
describe("meldungsPrognoseEigenesIntervall", () => {
const jetzt = new Date("2026-08-11T12:00:00")
// 30 km/Tag, siehe kmProTagAusFahrten-Tests oben.
const fahrten = [
{ ts_start: "2026-07-12T12:00:00", distance_km: 400, status: "abgeschlossen" },
{ ts_start: "2026-08-01T08:00:00", distance_km: 500, status: "abgeschlossen" },
]
it("rechnet Restkilometer und Zeitgrenze auf das eigene, kürzere Intervall um", () => {
// Herstellermeldung: 25.000 km / 24 Monate Rest bis zum 2027-08-11 -
// daraus ergibt sich ein angenommener letzter Service vor 2025-08-11.
// Eigenes Intervall (15.000 km / 12 Monate) ab demselben Punkt: Zeit-
// grenze 2026-08-11 (heute), Restkilometer 25000 - 30000 + 15000 = 10000.
const p = meldungsPrognoseEigenesIntervall(
"2027-08-11T00:00:00+00:00", 25000, 30000, 24, 15000, 12, fahrten, jetzt,
)
expect(p).not.toBeNull()
expect(p!.restKm).toBe(10000)
// 10.000 km bei 30 km/Tag läge weit in der Zukunft - die zurückgerechnete
// eigene Zeitgrenze (heute) greift zuerst.
expect(p!.durchZeitlimit).toBe(true)
expect(p!.datum).toBe("2026-08-11")
})
it("deckelt die Restkilometer bei null, wenn das eigene Intervall rechnerisch schon überschritten ist", () => {
// Erst 10.000 von 30.000 km seit der Herstellerfälligkeit gefahren -
// das kürzere eigene Intervall (15.000 km) ist rechnerisch längst fällig.
const p = meldungsPrognoseEigenesIntervall(
"2027-08-11T00:00:00+00:00", 10000, 30000, 24, 15000, 12, fahrten, jetzt,
)
expect(p).not.toBeNull()
expect(p!.restKm).toBe(0)
expect(p!.durchZeitlimit).toBe(false)
})
it("fällt ohne Fahrtenlog auf die zurückgerechnete eigene Zeitgrenze zurück", () => {
const p = meldungsPrognoseEigenesIntervall(
"2027-08-11T00:00:00+00:00", 25000, 30000, 24, 15000, 12, [], jetzt,
)
expect(p).not.toBeNull()
expect(p!.datum).toBe("2026-08-11")
expect(p!.durchZeitlimit).toBe(true)
})
it("liefert nichts ohne bekannte Herstellerintervall-Länge", () => {
expect(
meldungsPrognoseEigenesIntervall("2027-08-11T00:00:00+00:00", 25000, null, 24, 15000, 12, fahrten, jetzt),
).toBeNull()
})
})
describe("oelMeldungsPrognose", () => {
const jetzt = new Date("2026-08-11T12:00:00")
const fahrten = [
{ ts_start: "2026-07-12T12:00:00", distance_km: 400, status: "abgeschlossen" },
{ ts_start: "2026-08-01T08:00:00", distance_km: 500, status: "abgeschlossen" },
]
it("übernimmt bei Herstellervorgabe unverändert die reine Fahrzeugmeldung", () => {
const f = fahrzeug(10000, "hersteller")
f.oelwechselFaelligTs = "2027-08-11T00:00:00+00:00"
f.oelwechselFaelligKm = 25000
const p = oelMeldungsPrognose(f, fahrten, jetzt)
expect(p!.restKm).toBe(25000)
expect(p!.datum).toBe("2027-08-11")
})
it("rechnet bei eigenem Intervall auf dessen kürzere Vorgabe um", () => {
const f = fahrzeug(10000, "eigen")
f.oelwechselFaelligTs = "2027-08-11T00:00:00+00:00"
f.oelwechselFaelligKm = 25000
const p = oelMeldungsPrognose(f, fahrten, jetzt)
expect(p!.restKm).toBe(10000)
expect(p!.datum).toBe("2026-08-11")
})
it("fällt bei eigenem Intervall ohne Herstellerwerte auf die reine Meldung zurück", () => {
const f = fahrzeug(10000, "eigen")
f.oel.herstellerKm = null
f.oelwechselFaelligTs = "2027-08-11T00:00:00+00:00"
f.oelwechselFaelligKm = 25000
const p = oelMeldungsPrognose(f, fahrten, jetzt)
expect(p!.restKm).toBe(25000)
expect(p!.datum).toBe("2027-08-11")
})
})
+69 -7
View File
@@ -154,6 +154,8 @@ export interface Meldungsprognose {
hochgerechnete Kilometergrenze - der Nutzer fährt zu wenig, um die
km-Grenze rechtzeitig zu erreichen. */
durchZeitlimit: boolean
/** Restkilometer bis zur Fälligkeit, falls bekannt. */
restKm: number | null
}
/** Prognose rein aus der Fahrzeugmeldung, für den Fall ganz ohne
@@ -169,14 +171,74 @@ export function meldungsPrognose(
jetzt = new Date(),
): Meldungsprognose | null {
const zeitDatum = faelligTs ? faelligTs.slice(0, 10) : null
if (faelligKm == null) return zeitDatum ? { datum: zeitDatum, durchZeitlimit: true } : null
if (faelligKm == null) return zeitDatum ? { datum: zeitDatum, durchZeitlimit: true, restKm: null } : null
const restKm = Math.abs(faelligKm)
if (restKm <= 0) return { datum: jetzt.toISOString().slice(0, 10), durchZeitlimit: false }
if (restKm <= 0) return { datum: jetzt.toISOString().slice(0, 10), durchZeitlimit: false, restKm }
const rate = kmProTagAusFahrten(fahrten, jetzt)
if (!rate) return zeitDatum ? { datum: zeitDatum, durchZeitlimit: true } : null
if (!rate) return zeitDatum ? { datum: zeitDatum, durchZeitlimit: true, restKm } : null
const kmDatum = new Date(jetzt.getTime() + (restKm / rate) * TAG_MS).toISOString().slice(0, 10)
const durchZeitlimit = !!zeitDatum && zeitDatum < kmDatum
return { datum: durchZeitlimit ? (zeitDatum as string) : kmDatum, durchZeitlimit }
return { datum: durchZeitlimit ? (zeitDatum as string) : 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()). */
export function meldungsPrognoseEigenesIntervall(
faelligTs: string | null,
faelligKm: number | null,
herstellerKm: number | null,
herstellerMonate: number | null,
eigenKm: number | null,
eigenMonate: number | null,
fahrten: readonly Fahrtdaten[],
jetzt = new Date(),
): Meldungsprognose | null {
if (faelligKm == null || !herstellerKm || eigenKm == null) return null
const restKm = Math.max(0, Math.abs(faelligKm) - herstellerKm + eigenKm)
const zeitDatum =
faelligTs && herstellerMonate != null && eigenMonate != null
? monatePlus(faelligTs, eigenMonate - herstellerMonate)
: null
if (restKm <= 0) return { datum: jetzt.toISOString().slice(0, 10), durchZeitlimit: false, restKm }
const rate = kmProTagAusFahrten(fahrten, jetzt)
if (!rate) return zeitDatum ? { datum: zeitDatum, durchZeitlimit: true, restKm } : null
const kmDatum = new Date(jetzt.getTime() + (restKm / rate) * TAG_MS).toISOString().slice(0, 10)
const durchZeitlimit = !!zeitDatum && zeitDatum < kmDatum
return { datum: durchZeitlimit ? (zeitDatum as string) : 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. */
export function oelMeldungsPrognose(
fahrzeug: Fahrzeug,
fahrten: readonly Fahrtdaten[],
jetzt = new Date(),
): Meldungsprognose | null {
const oel = fahrzeug.oel
const roh = () => meldungsPrognose(fahrzeug.oelwechselFaelligTs, fahrzeug.oelwechselFaelligKm, fahrten, jetzt)
if (oel.modus === "hersteller") return roh()
const eigen = meldungsPrognoseEigenesIntervall(
fahrzeug.oelwechselFaelligTs,
fahrzeug.oelwechselFaelligKm,
oel.herstellerKm,
oel.herstellerMonate,
oel.km,
oel.monate,
fahrten,
jetzt,
)
return eigen ?? roh()
}
/* ------------------------------------------------------ Nächster Service */
@@ -219,8 +281,8 @@ export function naechsterService(
if (oel) {
kandidaten.push({ art: "Ölwechsel", restKm: oel.restKm, datum: oel.datum })
} else {
const m = meldungsPrognose(fahrzeug.oelwechselFaelligTs, fahrzeug.oelwechselFaelligKm, fahrten, jetzt)
if (m) kandidaten.push({ art: "Ölwechsel", restKm: fahrzeug.oelwechselFaelligKm, datum: m.datum })
const m = oelMeldungsPrognose(fahrzeug, fahrten, jetzt)
if (m) kandidaten.push({ art: "Ölwechsel", restKm: m.restKm, datum: m.datum })
}
const insp = inspektionPrognose(fahrzeug, buch, jetzt)
@@ -228,7 +290,7 @@ export function naechsterService(
kandidaten.push({ art: "Inspektion", restKm: insp.restKm, datum: insp.datum })
} else {
const m = meldungsPrognose(fahrzeug.inspektionFaelligTs, fahrzeug.inspektionFaelligKm, fahrten, jetzt)
if (m) kandidaten.push({ art: "Inspektion", restKm: fahrzeug.inspektionFaelligKm, datum: m.datum })
if (m) kandidaten.push({ art: "Inspektion", restKm: m.restKm, datum: m.datum })
}
// fahrzeug.hu wird von Hand gepflegt und kann leer oder unlesbar sein.