Fahrten bearbeitbar, Inspektionsprognose, Bootstrap-Takt reisst nicht mehr ab
Sechs gemeldete Punkte plus ein Fund beim Nachlesen des Ladevorgangs: - HECKKLAPPENSCHLOSS_SENSOR ueberall entfernt (einstellungen.py, entitaeten.py-Katalog, _sicherheitscheck()). HECKKLAPPE_SENSOR bleibt. - Bild-Platzhalter in den Einstellungen war halb abgeschnitten: die bare .carfix-Regel der Desktop-Container-Query traf auch den Platzhalter-Div, dessen inset:0 von der festen Hoehe uebersteuert wurde. Auf .bildbox eingegrenzt. - Inspektion bekommt eine eigene "voraussichtlich am ..."-Zeile (nur Datum). oelwechselPrognose() dafuer auf den gemeinsamen Kern servicePrognose() zurueckgefuehrt. - "Neue Fahrt" bietet jetzt alle Felder, die die Einzelfahrt anzeigt, und die Einzelfahrt laesst sich bearbeiten - gemeinsames fahrtFelder()/ fahrtFormularWerte() nach dem Muster von tankFelder(). Backend: Service audi_dashboard_fahrt_aktualisieren, profil.fahrt_bearbeiten() (edited_fields schuetzt gegen die Automatik, nicht gegen den Nutzer). Die Art-Pille speichert jetzt ebenfalls statt nur lokal umzuschalten. - Titelzeile steht auf dem Handy buendig zu den Kacheln: .back:not(.on) gibt seine Breite und den Gap auf allen Breiten frei, .topbar links 16px. - Ladebildschirm: nachladeAnstossen() und datenLaden() stiegen bei fehlendem HASS aus, ohne eine naechste Runde zu planen - genau die Sackgasse, aus der bisher nur ein Menueklick half. Beide planen jetzt weiter, der Timer wird beim Abhaengen gestoppt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -34,7 +34,12 @@ correct (verified via live pointer-event simulation), the real gap was a missing
|
||||
`overscroll-behavior` on the scroll container letting the browser's own native overscroll compete
|
||||
with it on real touch devices; same day: the fuel-receipt upload got a proper dialog — drag the PDF
|
||||
in at the computer, paste it from the clipboard on the phone, file picker still available as a third
|
||||
route — see section C). This file is the entry point for every new agent
|
||||
route — see section C; a twelfth round the same day removed `HECKKLAPPENSCHLOSS_SENSOR`, fixed a
|
||||
half-cut-off image placeholder, gave the Inspektion its own "voraussichtlich am …" forecast, made
|
||||
trips fully editable (new `audi_dashboard_fahrt_aktualisieren`, "Neue Fahrt" now offers every field
|
||||
the detail page shows), aligned the page title with the tiles on phone width, and closed two
|
||||
dead-ends in the bootstrap retry loop behind the reported hanging "Lädt …" screen).
|
||||
This file is the entry point for every new agent
|
||||
session: what this repo is, what is finished, what is missing, and how to work here. Detail lives in
|
||||
the linked documents — this file points, it does not duplicate.
|
||||
|
||||
@@ -1211,6 +1216,68 @@ wraps the web app for iPhone; a PWA home-screen install is the accepted intermed
|
||||
`companion-app` (Nachfolger-App) brauchte keine Änderung - deren `eur()` rundet ohnehin schon
|
||||
auf 2 Stellen. Live geprüft: keine `de(..., 3)`-Aufrufe mehr in der Datei, Liste und
|
||||
Einzelbeleg zeigen "2,07 €/l", "1,99 €/l" usw.
|
||||
- [x] Twelfth round (2026-08-17) — six user-reported items, one of them a real dead-end in the
|
||||
bootstrap:
|
||||
(1) **`HECKKLAPPENSCHLOSS_SENSOR` removed for good**, the same treatment Türschloss/
|
||||
Haubenschloss got on 2026-08-16: gone from `einstellungen.py` (attribute deleted, not
|
||||
blanked), from `entitaeten.py`'s Setup catalog, and from `_sicherheitscheck()` in
|
||||
`frontend_veroeffentlichung.py`. `HECKKLAPPE_SENSOR` (the lid itself) stays. Verified live:
|
||||
"Geprüfte Punkte" no longer lists it, the Setup catalog no longer offers it.
|
||||
(2) **Bild-Platzhalter in Einstellungen war halb abgeschnitten** — reproduced by measurement,
|
||||
not by eye: the placeholder rendered 260px tall inside a 132px `overflow:hidden` box. Cause:
|
||||
`bildMitPlatzhalter()` puts the size class (`carfix`) on the box, the `<img>` **and** the
|
||||
placeholder `<div>`, and the desktop container query said `.carfix,.bildbox.carfix
|
||||
{height:260px}` — the bare selector therefore also hit the placeholder, whose `position:
|
||||
absolute;inset:0` is over-constrained by an explicit height (the height wins, `bottom:0` is
|
||||
ignored). Scoped both rules to `.bildbox.carfix`; image and placeholder fill the box via
|
||||
their own `100%` anyway. Verified: placeholder now measures exactly the box height.
|
||||
(3) **Inspektion bekommt eine "voraussichtlich am …"-Zeile** wie der Ölwechsel, aber auf
|
||||
Wunsch nur mit dem Datum. `oelwechselPrognose()` wurde dafür auf einen gemeinsamen Kern
|
||||
`servicePrognose(stichwort, km, monate)` zurückgeführt; `inspektionPrognose()` nutzt
|
||||
dieselben 30.000 km / 24 Monate wie `intervalle()`/`termine()`. Angezeigt in der
|
||||
Service-Kachel auf "Mein Audi" (auch ohne Fahrzeugmeldung, anders als beim Ölwechsel - dort
|
||||
nennt `terminText()` den generischen Termin schon "voraussichtlich", die Zahl stünde sonst
|
||||
doppelt) und auf der Service-Seite. Live geprüft: beide Stellen zeigen "voraussichtlich am
|
||||
20.10.2027", Ölwechsel unverändert.
|
||||
(4)+(5) **"Neue Fahrt" hat jetzt alle Felder der Einzelfahrt, und die Einzelfahrt lässt sich
|
||||
bearbeiten.** Neues gemeinsames `fahrtFelder()`/`fahrtFormularWerte()` (Datum, Startzeit,
|
||||
Dauer, Art, Start-/Zielort, Start-/Endkilometer, Distanz) nach demselben Muster wie
|
||||
`tankFelder()`/`tankFormularWerte()`, benutzt vom Erfassungsformular und vom neuen
|
||||
"Werte bearbeiten" auf der Einzelfahrt-Seite (`fahrtBearbeitenOffen`, Vorbild `vFill()`).
|
||||
Backend: `audi_dashboard_fahrt_manuell_anlegen` um dieselben Felder erweitert, neuer Service
|
||||
`audi_dashboard_fahrt_aktualisieren`. Zwei Punkte, die dabei zählen: von Hand gesetzte Felder
|
||||
landen in `edited_fields`, damit das Kilometerstand-Screening (§7.2) sie nicht später
|
||||
überschreibt - und weil `_datensatz_aktualisieren()` genau diese Liste schützt, hätte ein
|
||||
zweites Bearbeiten sonst nichts mehr bewirkt; dafür gibt es jetzt `profil.fahrt_bearbeiten()`
|
||||
(`schutz=False`), der Schutz gilt gegen die Automatik, nicht gegen den Menschen. Fehlt die
|
||||
Distanz, ergibt sie sich aus den beiden Kilometerständen (`km_quelle: "manuell"`, Status
|
||||
"vollständig"). Gleich mitgezogen: die Art-Pille auf der Einzelfahrt schaltete bisher nur
|
||||
lokal um und war nach dem nächsten Backend-Push wieder weg - sie ruft jetzt denselben
|
||||
Service. **Merkposten pyscript:** Generator-Ausdrücke gibt es dort nicht
|
||||
(`NotImplementedError: not implemented ast ast_generatorexp`, an `next((f for f in …), None)`
|
||||
real gesehen) - Listen-Abstraktionen dagegen schon. Live end-to-end geprüft: anlegen,
|
||||
bearbeiten, erneut bearbeiten, jeweils in `fahrten.jsonl` nachgesehen und nach Reload wieder
|
||||
im Formular.
|
||||
(6) **Titelzeile stand auf dem Handy 34px rechts von den Kacheln.** Derselbe Befund wie im
|
||||
Design-Audit 2026-08-13, dort aber nur in der Desktop-Container-Query behoben: `.back` ist
|
||||
auf Wurzelseiten unsichtbar (`opacity:0`), belegt aber Breite plus Gap. Die Regel
|
||||
`.back:not(.on){width:0;…}` gilt jetzt auf allen Breiten und hebt per `margin-right:-4px`
|
||||
auch den Gap auf; `.topbar` bekam links 16px statt 20px, passend zu `--seitenrand` von
|
||||
`main#view`. Gemessen: Titel und Kachel beginnen auf dem Handy beide bei 16px, auf dem
|
||||
Desktop beide bei 564px (dort blieben vorher 4px Rest). Detailseiten (`.back.on`) unberührt.
|
||||
- [x] Ladebildschirm: der Takt darf nicht abreißen (2026-08-17) — Nutzerbericht "nach dem Neuladen
|
||||
dreht sich nur 'Lädt …', erst ein Klick auf einen Menüpunkt zeigt die App". Beim Lesen des
|
||||
Bootstraps fanden sich zwei echte Sackgassen, die genau dieses Verhalten erklären:
|
||||
`nachladeAnstossen()` stieg bei `!HASS || !HASS.callWS` **ohne** neuen Timer aus, und
|
||||
`datenLaden()` bei `!HASS` ebenso - danach lief nichts mehr von selbst, und der einzige
|
||||
Ausweg war ein zufälliger hass-Push oder ein Menüklick (`go()` ruft `datenLaden()` mit auf,
|
||||
genau die Abmilderung vom 2026-08-16). Beide planen jetzt die nächste Runde, statt
|
||||
auszusteigen; `disconnectedCallback()` stoppt den Timer dafür sauber, `connectedCallback()`
|
||||
stößt ihn wieder an. **Nicht reproduziert:** weder ein einfacher Reload noch ein
|
||||
Container-Neustart mit sofortigem Reload haben den Hänger in `audi_ha_test` gezeigt (die App
|
||||
war jedes Mal binnen ~2s da) - die Sackgassen sind im Code belegt, dass sie *die* Ursache
|
||||
beim Nutzer waren, ist damit aber nicht bewiesen. Tritt es erneut auf: Browser-Konsole
|
||||
erfragen, bevor weiter geraten wird.
|
||||
- [ ] Fix remaining documentation drift (statistics claim, README gaps, obsolete TODO comment) —
|
||||
text-only changes; INSTALL.md's WLAN/TommiG1 drift and stale variable names were fixed
|
||||
2026-08-12 (see section B); `DESIGN_REVIEW_2026-08-13.md` and `REVIEW_main_2026-08-13.md`
|
||||
|
||||
@@ -122,13 +122,43 @@ def _fahrt_beenden(start_ts, ende_ts):
|
||||
fahrtabschluss_logik.screening_durchfuehren(einstellungen.KM_SENSOR)
|
||||
|
||||
|
||||
def _handfelder(start_ort, ziel_ort, odo_start, odo_end, distanz):
|
||||
"""Die von Hand befüllbaren Felder einer Fahrt auf ihre Datensatz-Namen
|
||||
abgebildet, ohne die leer gelassenen. Der Aufrufer schreibt die Schlüssel
|
||||
zusätzlich nach edited_fields - das schützt sie vor dem automatischen
|
||||
Kilometerstand-Screening (§7.2), das sonst später darüberschreiben würde."""
|
||||
werte = {
|
||||
"start_address": start_ort or None,
|
||||
"end_address": ziel_ort or None,
|
||||
"odo_start": odo_start,
|
||||
"odo_end": odo_end,
|
||||
"distance_km": distanz,
|
||||
}
|
||||
return {k: v for k, v in werte.items() if v is not None}
|
||||
|
||||
|
||||
def _strecke_ergaenzen(felder):
|
||||
"""Fehlt die Distanz, ergibt sie sich aus den beiden Kilometerständen -
|
||||
dieselbe Rechnung wie im Screening (fahrtabschluss_logik)."""
|
||||
if "distance_km" not in felder and "odo_start" in felder and "odo_end" in felder:
|
||||
felder["distance_km"] = round(felder["odo_end"] - felder["odo_start"], 1)
|
||||
return felder
|
||||
|
||||
|
||||
@service
|
||||
def audi_dashboard_fahrt_manuell_anlegen(ts_start=None, ts_end=None, art="privat"):
|
||||
def audi_dashboard_fahrt_manuell_anlegen(ts_start=None, ts_end=None, art="privat",
|
||||
start_ort=None, ziel_ort=None,
|
||||
odo_start=None, odo_end=None, distanz=None):
|
||||
"""Manuelle Fahrterfassung aus der Oberfläche (§5.3), unabhängig von der
|
||||
automatischen Zündungs-Erkennung. Aufruf als pyscript.audi_dashboard_fahrt_manuell_anlegen."""
|
||||
automatischen Zündungs-Erkennung. Dieselben Felder wie beim Bearbeiten
|
||||
einer bestehenden Fahrt (siehe audi_dashboard_fahrt_aktualisieren); alles
|
||||
außer Start- und Endzeitpunkt ist optional und bleibt leer, wenn es das
|
||||
Screening später selbst ergänzen soll. Aufruf als
|
||||
pyscript.audi_dashboard_fahrt_manuell_anlegen."""
|
||||
start_ts = datetime.datetime.fromisoformat(ts_start)
|
||||
ende_ts = datetime.datetime.fromisoformat(ts_end)
|
||||
dauer_s = int((ende_ts - start_ts).total_seconds())
|
||||
hand = _strecke_ergaenzen(_handfelder(start_ort, ziel_ort, odo_start, odo_end, distanz))
|
||||
fahrt = {
|
||||
"trip_id": profil.neue_id("t"),
|
||||
"ts_start": start_ts.isoformat(),
|
||||
@@ -152,11 +182,54 @@ def audi_dashboard_fahrt_manuell_anlegen(ts_start=None, ts_end=None, art="privat
|
||||
"status": "offen",
|
||||
"edited_fields": [],
|
||||
}
|
||||
fahrt.update(hand)
|
||||
fahrt["edited_fields"] = sorted(hand)
|
||||
if fahrt["distance_km"] is not None:
|
||||
fahrt["km_quelle"] = "manuell"
|
||||
fahrt["status"] = "vollständig"
|
||||
profil.fahrt_anhaengen(fahrt)
|
||||
frontend_veroeffentlichung.fahrten_veroeffentlichen()
|
||||
fahrtabschluss_logik.screening_durchfuehren(einstellungen.KM_SENSOR)
|
||||
|
||||
|
||||
@service
|
||||
def audi_dashboard_fahrt_aktualisieren(trip_id=None, ts_start=None, ts_end=None, art=None,
|
||||
start_ort=None, ziel_ort=None,
|
||||
odo_start=None, odo_end=None, distanz=None):
|
||||
"""Bearbeitet eine bestehende Fahrt (Einzelfahrt-Seite), egal ob sie
|
||||
automatisch erkannt oder von Hand angelegt wurde. Aufruf als
|
||||
pyscript.audi_dashboard_fahrt_aktualisieren."""
|
||||
if not trip_id:
|
||||
log.warning("audi_dashboard: fahrt_aktualisieren ohne trip_id aufgerufen")
|
||||
return
|
||||
aenderungen = _strecke_ergaenzen(_handfelder(start_ort, ziel_ort, odo_start, odo_end, distanz))
|
||||
if art:
|
||||
aenderungen["art"] = art
|
||||
if ts_start and ts_end:
|
||||
start_ts = datetime.datetime.fromisoformat(ts_start)
|
||||
ende_ts = datetime.datetime.fromisoformat(ts_end)
|
||||
aenderungen["ts_start"] = start_ts.isoformat()
|
||||
aenderungen["ts_end"] = ende_ts.isoformat()
|
||||
aenderungen["duration_s"] = int((ende_ts - start_ts).total_seconds())
|
||||
if aenderungen.get("distance_km") is not None:
|
||||
aenderungen["km_quelle"] = "manuell"
|
||||
aenderungen["status"] = "vollständig"
|
||||
# Von Hand gesetzte Felder bleiben von Hand gesetzt: die Liste wächst mit,
|
||||
# damit ein späteres Screening sie nicht wieder überschreibt.
|
||||
# Bewusst eine Listen-Abstraktion statt next(<Generator>): pyscript kann
|
||||
# keine Generator-Ausdrücke auswerten ("not implemented ast
|
||||
# ast_generatorexp"), Listen-Abstraktionen dagegen schon.
|
||||
passend = [f for f in profil.fahrten_lesen() if f.get("trip_id") == trip_id]
|
||||
handfelder = {"start_address", "end_address", "odo_start", "odo_end", "distance_km"}
|
||||
bisher = set(passend[0].get("edited_fields", [])) if passend else set()
|
||||
aenderungen["edited_fields"] = sorted(bisher | (handfelder & set(aenderungen)))
|
||||
if profil.fahrt_bearbeiten(trip_id, aenderungen):
|
||||
frontend_veroeffentlichung.fahrten_veroeffentlichen()
|
||||
log.info(f"audi_dashboard: Fahrt {trip_id} aktualisiert")
|
||||
else:
|
||||
log.warning(f"audi_dashboard: Fahrt {trip_id} nicht gefunden")
|
||||
|
||||
|
||||
@service
|
||||
def audi_dashboard_fahrt_loeschen(trip_id=None):
|
||||
"""Löscht eine Fahrt unwiderruflich (Löschfunktion in der Oberfläche,
|
||||
|
||||
@@ -67,7 +67,6 @@ FENSTER_SENSOREN = []
|
||||
|
||||
# Heckklappe und Motorhaube - keine Quelle mehr vorhanden.
|
||||
HECKKLAPPE_SENSOR = ""
|
||||
HECKKLAPPENSCHLOSS_SENSOR = ""
|
||||
HAUBE_SENSOR = ""
|
||||
|
||||
# Vom Fahrzeug selbst gemeldete Service-Fälligkeit (ergänzt die App-eigene,
|
||||
|
||||
@@ -85,10 +85,6 @@ FELDER = [
|
||||
"hinweis": "\"aus\"/off = zu.",
|
||||
"domains": ["binary_sensor"], "device_classes": ["door", "opening"], "units": [], "liste": False, "pflicht": False,
|
||||
"stichworte": ["heckklappe", "tailgate", "kofferraum", "trunk"]},
|
||||
{"key": "HECKKLAPPENSCHLOSS_SENSOR", "label": "Heckklappenschloss", "gruppe": "sicherheit",
|
||||
"hinweis": "\"on\" = entriegelt, \"aus\"/off = verriegelt.",
|
||||
"domains": ["binary_sensor"], "device_classes": ["lock"], "units": [], "liste": False, "pflicht": False,
|
||||
"stichworte": ["heckklappe", "tailgate", "schloss", "lock"]},
|
||||
{"key": "HAUBE_SENSOR", "label": "Motorhaube", "gruppe": "sicherheit",
|
||||
"hinweis": "\"aus\"/off = zu.",
|
||||
"domains": ["binary_sensor"], "device_classes": ["door", "opening"], "units": [], "liste": False, "pflicht": False,
|
||||
|
||||
@@ -138,7 +138,6 @@ def _sicherheitscheck():
|
||||
eintraege.append({"label": f"Fenster {pos}", "ok": None if w is None else w == "off"})
|
||||
for label, sensor in [
|
||||
("Heckklappe", einstellungen.HECKKLAPPE_SENSOR),
|
||||
("Heckklappenschloss", einstellungen.HECKKLAPPENSCHLOSS_SENSOR),
|
||||
("Motorhaube", einstellungen.HAUBE_SENSOR),
|
||||
]:
|
||||
w = zustand_oder_none(sensor)
|
||||
|
||||
@@ -138,16 +138,20 @@ def offene_fahrten():
|
||||
return sorted(offen, key=lambda f: f.get("ts_start", ""))
|
||||
|
||||
|
||||
def _datensatz_aktualisieren(zeilen, id_feld, id_wert, aenderungen, schreiben):
|
||||
def _datensatz_aktualisieren(zeilen, id_feld, id_wert, aenderungen, schreiben, schutz=True):
|
||||
"""Ersetzt ausgewählte Felder eines Datensatzes anhand seiner ID und
|
||||
schreibt den gesamten Bestand neu. Manuell geänderte Felder
|
||||
(edited_fields, analog zu §7.7 Regel 3) werden dabei nie überschrieben.
|
||||
Gemeinsame Grundlage für Fahrten und Tankvorgänge - beide Archive
|
||||
funktionieren nach demselben Muster."""
|
||||
funktionieren nach demselben Muster.
|
||||
|
||||
schutz=False hebt genau diese Sperre auf - nötig für Eingaben aus der
|
||||
Oberfläche: edited_fields schützt gegen die automatische Ergänzung, nicht
|
||||
gegen den Menschen, der das Feld gerade selbst korrigiert."""
|
||||
geaendert = False
|
||||
for d in zeilen:
|
||||
if d.get(id_feld) == id_wert:
|
||||
geschuetzt = set(d.get("edited_fields", []))
|
||||
geschuetzt = set(d.get("edited_fields", [])) if schutz else set()
|
||||
for feld, wert in aenderungen.items():
|
||||
if feld not in geschuetzt:
|
||||
d[feld] = wert
|
||||
@@ -162,6 +166,13 @@ def fahrt_aktualisieren(trip_id, aenderungen):
|
||||
return _datensatz_aktualisieren(fahrten_lesen(), "trip_id", trip_id, aenderungen, fahrten_schreiben)
|
||||
|
||||
|
||||
def fahrt_bearbeiten(trip_id, aenderungen):
|
||||
"""Wie fahrt_aktualisieren(), aber für Eingaben aus der Oberfläche: eine
|
||||
von Hand gesetzte Angabe sticht auch dann, wenn dasselbe Feld schon einmal
|
||||
von Hand gesetzt wurde (siehe schutz-Parameter oben)."""
|
||||
return _datensatz_aktualisieren(fahrten_lesen(), "trip_id", trip_id, aenderungen, fahrten_schreiben, schutz=False)
|
||||
|
||||
|
||||
def fahrt_loeschen(trip_id):
|
||||
fahrten = fahrten_lesen()
|
||||
uebrig = [f for f in fahrten if f.get("trip_id") != trip_id]
|
||||
|
||||
@@ -1123,24 +1123,24 @@ function fahrzeugMeldungRoh(art) {
|
||||
return { ts: null, km: null };
|
||||
}
|
||||
|
||||
/* App-eigene Ölwechsel-Prognose, eigenständig statt über die allgemeine
|
||||
tagesleistung()/prognose()-Mischrechnung (die für Ölwechsel zu träge ist,
|
||||
weil sie den langfristigen Durchschnitt seit dem allerersten Servicebuch-
|
||||
eintrag mit einfließen lässt): die Berechnung beginnt bewusst komplett neu
|
||||
ab dem Datum des letzten tatsächlich durchgeführten Ölwechsels.
|
||||
/* App-eigene Service-Prognose, eigenständig statt über die allgemeine
|
||||
tagesleistung()/prognose()-Mischrechnung (die dafür zu träge ist, weil sie
|
||||
den langfristigen Durchschnitt seit dem allerersten Servicebucheintrag mit
|
||||
einfließen lässt): die Berechnung beginnt bewusst komplett neu ab dem Datum
|
||||
der letzten tatsächlich durchgeführten Arbeit dieser Art.
|
||||
|
||||
Logik (wie vorgegeben): km und Tage seit dem letzten Ölwechsel ergeben die
|
||||
aktuelle Fahrleistung pro Tag; hochgerechnet auf die restlichen km bis zum
|
||||
eingestellten Intervall (Kundeneigene Einstellung, siehe CAR.oel) ergibt
|
||||
das ein Datum - gedeckelt auf spätestens das Intervall-Zeitlimit (z. B. 1
|
||||
Jahr) nach dem letzten Ölwechsel, falls seither wenig gefahren wurde. */
|
||||
function oelwechselPrognose() {
|
||||
Logik (wie vorgegeben): km und Tage seit diesem Eintrag ergeben die aktuelle
|
||||
Fahrleistung pro Tag; hochgerechnet auf die restlichen km bis zum
|
||||
eingestellten Intervall ergibt das ein Datum - gedeckelt auf spätestens das
|
||||
Intervall-Zeitlimit (z. B. 1 Jahr) nach dem letzten Eintrag, falls seither
|
||||
wenig gefahren wurde.
|
||||
|
||||
Gemeinsame Grundlage für Ölwechsel und Inspektion (siehe die beiden
|
||||
Aufrufer darunter) - beide rechnen identisch, nur mit anderem Intervall. */
|
||||
function servicePrognose(stichwort, intervallKm, intervallMonate) {
|
||||
if (!CAR.odoBekannt) return null;
|
||||
const letzter = letzterEintrag("öl");
|
||||
const letzter = letzterEintrag(stichwort);
|
||||
if (!letzter) return null;
|
||||
const oel = CAR.oel;
|
||||
const intervallKm = oel.modus === "hersteller" ? oel.herstellerKm : Math.min(oel.km, oel.herstellerKm);
|
||||
const intervallMonate = oel.modus === "hersteller" ? oel.herstellerMonate : Math.min(oel.monate, oel.herstellerMonate);
|
||||
const zielKm = letzter.km + intervallKm;
|
||||
const restKm = Math.max(0, zielKm - CAR.odo);
|
||||
const kappDatum = monatePlus(letzter.datum, intervallMonate);
|
||||
@@ -1151,13 +1151,13 @@ function oelwechselPrognose() {
|
||||
const tageSeit = (heute() - dat(letzter.datum)) / dTag;
|
||||
const kmSeit = CAR.odo - letzter.km;
|
||||
if (tageSeit <= 0 || kmSeit <= 0) {
|
||||
// Noch keine belastbare Fahrleistung seit dem Ölwechsel bekannt (z. B.
|
||||
// Noch keine belastbare Fahrleistung seit dem Eintrag bekannt (z. B.
|
||||
// direkt danach) - ohne Rate keine seriöse Km-Prognose, also das
|
||||
// Zeitlimit als einzig belastbare Grenze zeigen.
|
||||
datum = kappDatum;
|
||||
} else {
|
||||
const kmProTagSeitOel = kmSeit / tageSeit;
|
||||
const tageBisZiel = restKm / kmProTagSeitOel;
|
||||
const kmProTagSeither = kmSeit / tageSeit;
|
||||
const tageBisZiel = restKm / kmProTagSeither;
|
||||
const berechnet = new Date(heute().getTime() + tageBisZiel * dTag);
|
||||
datum = berechnet < kappDatum ? berechnet : kappDatum;
|
||||
}
|
||||
@@ -1165,6 +1165,27 @@ function oelwechselPrognose() {
|
||||
return { km: zielKm, restKm, datum };
|
||||
}
|
||||
|
||||
function oelwechselPrognose() {
|
||||
const oel = CAR.oel;
|
||||
return servicePrognose("öl",
|
||||
oel.modus === "hersteller" ? oel.herstellerKm : Math.min(oel.km, oel.herstellerKm),
|
||||
oel.modus === "hersteller" ? oel.herstellerMonate : Math.min(oel.monate, oel.herstellerMonate));
|
||||
}
|
||||
/* Inspektion: dieselben 30.000 km / 24 Monate wie in intervalle()/termine().
|
||||
Angezeigt wird davon bewusst nur das Datum ("voraussichtlich am …") - die
|
||||
Restkilometer stehen bei der Inspektion schon in der Zeile daneben. */
|
||||
function inspektionPrognose() { return servicePrognose("inspektion", 30000, 24); }
|
||||
|
||||
/* Kleine graue Zweitzeile unter dem Termin-Titel, in Service-Kachel und
|
||||
Service-Seite identisch aufgebaut. Leer, solange kein passender
|
||||
Servicebucheintrag vorliegt. */
|
||||
function prognoseZeile(art) {
|
||||
const p = art === "Ölwechsel" ? oelwechselPrognose() : art === "Inspektion" ? inspektionPrognose() : null;
|
||||
if (!p) return "";
|
||||
const km = art === "Ölwechsel" ? de(p.restKm) + " km, " : "";
|
||||
return `<br><span style="color:var(--fg3);font-size:11px">${km}voraussichtlich am ${dedat(p.datum)}</span>`;
|
||||
}
|
||||
|
||||
function terminText(t) {
|
||||
const restKm = t.km !== null && CAR.odoBekannt ? t.km - CAR.odo : null;
|
||||
const restKmAbs = restKm !== null ? Math.abs(restKm) : null;
|
||||
@@ -1318,22 +1339,25 @@ function vAudi() {
|
||||
// (Sensor nicht verfügbar), fällt sie auf dieselbe App-eigene
|
||||
// Servicebuch-Schätzung zurück wie Hauptuntersuchung.
|
||||
const fm = t.art === "Hauptuntersuchung" ? { ts: null, km: null } : fahrzeugMeldungRoh(t.art);
|
||||
// Zusätzlich zur realen Meldung klein die App-eigene Prognose
|
||||
// (Servicebuch + tatsächliche Fahrleistung seit der letzten Arbeit,
|
||||
// siehe servicePrognose()). Steht links unter dem Kachel-Titel statt
|
||||
// rechts bei "am"/"in", die beide gleichwertig (gleiche Schriftgröße
|
||||
// wie "am") untereinander in der rechten Spalte stehen. Die Inspektion
|
||||
// bekommt sie auf Wunsch auch ohne Fahrzeugmeldung und ohne den
|
||||
// km-Anteil - beim Ölwechsel stünde die Zahl dort doppelt, weil
|
||||
// terminText() den generischen Termin schon "voraussichtlich" nennt.
|
||||
const eigen = prognoseZeile(t.art);
|
||||
if (fm.ts == null && fm.km == null) {
|
||||
const x = terminText(t);
|
||||
return `<div class="row" ${letzte}>
|
||||
<dt>${t.art}</dt><dd>${x.haupt}${x.neben ? `<small style="text-align:left">${x.neben}</small>` : ""}</dd></div>`;
|
||||
<dt>${t.art}${t.art === "Inspektion" ? eigen : ""}</dt>
|
||||
<dd>${x.haupt}${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" : "";
|
||||
// Zusätzlich zur realen Meldung bei Ölwechsel klein die App-eigene
|
||||
// Prognose (Servicebuch + tatsächliche Fahrleistung seit dem letzten
|
||||
// Wechsel, siehe oelwechselPrognose()) - bei Inspektion bewusst nicht,
|
||||
// wie gefordert. Steht links unter dem Kachel-Titel statt rechts bei
|
||||
// "am"/"in", die beide gleichwertig (gleiche Schriftgröße wie "am")
|
||||
// untereinander in der rechten Spalte stehen.
|
||||
const eigen = t.art === "Ölwechsel" ? oelwechselPrognose() : null;
|
||||
return `<div class="row" ${letzte}>
|
||||
<dt>${t.art}${eigen ? `<br><span style="color:var(--fg3);font-size:11px">${de(eigen.restKm)} km, voraussichtlich am ${dedat(eigen.datum)}</span>` : ""}</dt>
|
||||
<dt>${t.art}${eigen}</dt>
|
||||
<dd>${haupt}${neben ? `<span style="display:block">${neben}</span>` : ""}</dd></div>`;
|
||||
}).join("")}
|
||||
</dl></button>
|
||||
@@ -1628,11 +1652,12 @@ function vService() {
|
||||
const anzeige = iv.hersteller || iv.eigen;
|
||||
const fm = fahrzeugMeldungRoh(t.art);
|
||||
const fmDatum = fm.ts ? dedat(new Date(fm.ts)) : null;
|
||||
// Forecast-Zeile nur bei Ölwechsel (Kundeneigenes Intervall aus
|
||||
// "Einrichten", eigenständige Reset-nach-Service-Logik, siehe
|
||||
// oelwechselPrognose()) - bei Inspektion und Hauptuntersuchung
|
||||
// bewusst keine Forecast-Zeile mehr.
|
||||
// Forecast-Zeile aus dem kundeneigenen Intervall in "Einrichten" mit
|
||||
// eigenständiger Reset-nach-Service-Logik (servicePrognose()): beim
|
||||
// Ölwechsel mit Restkilometern, bei der Inspektion auf Wunsch nur mit
|
||||
// dem Datum. Hauptuntersuchung bleibt bewusst ohne.
|
||||
const eigenOel = t.art === "Ölwechsel" ? oelwechselPrognose() : null;
|
||||
const eigenInsp = t.art === "Inspektion" ? inspektionPrognose() : null;
|
||||
return `<div class="row" ${letzte}>
|
||||
<dt>${t.art}
|
||||
${t.vereinbart ? `<br><span style="color:var(--red);font-size:11px">Werkstatt am ${dedat(new Date(t.vereinbart))}</span>` : ""}
|
||||
@@ -1642,8 +1667,10 @@ function vService() {
|
||||
${fm.km != null || fmDatum ? `
|
||||
<span class="werte">${fm.km != null ? de(fm.km) + " km" : ""}${fm.km != null && fmDatum ? " / " : ""}${fmDatum || ""}</span>
|
||||
${eigenOel ? `<small style="text-align:left">${de(eigenOel.restKm)} km, voraussichtlich am ${dedat(eigenOel.datum)}</small>` : ""}
|
||||
${eigenInsp ? `<small style="text-align:left">voraussichtlich am ${dedat(eigenInsp.datum)}</small>` : ""}
|
||||
` : `
|
||||
<span class="werte">${eigenOel ? de(eigenOel.restKm) + " km · " + dedat(eigenOel.datum) : (anzeige.km !== null ? de(anzeige.km) + " km · " : "") + dedat(anzeige.datum)}</span>
|
||||
${eigenInsp ? `<small style="text-align:left">voraussichtlich am ${dedat(eigenInsp.datum)}</small>` : ""}
|
||||
`}
|
||||
</dd>
|
||||
</div>`; }).join("")}
|
||||
@@ -2440,17 +2467,69 @@ function vReifen() {
|
||||
${satz("winter")}`;
|
||||
}
|
||||
|
||||
/* Gemeinsames Formular für "Neue Fahrt" und das Bearbeiten einer bestehenden
|
||||
Fahrt - dieselben Felder an beiden Stellen, damit sie nicht auseinander-
|
||||
laufen können (gleiches Muster wie tankFelder()/tankFormularWerte()). Die
|
||||
Felder decken genau das ab, was die Einzelfahrt-Seite anzeigt; Verbrauch,
|
||||
Status und km-Quelle bleiben außen vor, die meldet das Fahrzeug bzw. leitet
|
||||
das Backend ab. */
|
||||
const FAHRT_ARTEN = ["privat", "arbeitsweg"];
|
||||
|
||||
/* Datum für ein <input type="date"> in Ortszeit. Bewusst nicht über
|
||||
toISOString(), das rechnet nach UTC um und liefert kurz nach Mitternacht
|
||||
den Vortag. */
|
||||
function datumsfeld(d) {
|
||||
const zwei = (n) => String(n).padStart(2, "0");
|
||||
return `${d.getFullYear()}-${zwei(d.getMonth() + 1)}-${zwei(d.getDate())}`;
|
||||
}
|
||||
function zeitfeld(d) {
|
||||
const zwei = (n) => String(n).padStart(2, "0");
|
||||
return `${zwei(d.getHours())}:${zwei(d.getMinutes())}`;
|
||||
}
|
||||
|
||||
function fahrtFelder(praefix, t) {
|
||||
const start = t && t.ts_start ? new Date(t.ts_start) : new Date();
|
||||
const dauer = t && t.duration_s != null ? Math.round(t.duration_s / 60) : "";
|
||||
const art = t && t.art ? t.art : FAHRT_ARTEN[0];
|
||||
const wert = (v) => (v == null ? "" : v);
|
||||
return `
|
||||
<div class="feld"><label for="${praefix}Datum">Datum</label><input type="date" id="${praefix}Datum" value="${datumsfeld(start)}"></div>
|
||||
<div class="feld"><label for="${praefix}Start">Startzeit</label><input type="time" id="${praefix}Start" value="${t ? zeitfeld(start) : "08:00"}"></div>
|
||||
<div class="feld"><label for="${praefix}Dauer">Dauer</label>
|
||||
<span class="mitEinheit"><input type="number" min="0" id="${praefix}Dauer" value="${dauer}" placeholder="0"><span>min</span></span></div>
|
||||
<div class="feld"><label for="${praefix}Art">Art</label>
|
||||
<select id="${praefix}Art">${FAHRT_ARTEN.map((a) => `<option ${a === art ? "selected" : ""}>${a}</option>`).join("")}</select></div>
|
||||
<div class="feld"><label for="${praefix}Startort">Startort</label><input id="${praefix}Startort" value="${esc(wert(t && t.start_address))}" placeholder="unbekannt"></div>
|
||||
<div class="feld"><label for="${praefix}Zielort">Zielort</label><input id="${praefix}Zielort" value="${esc(wert(t && t.end_address))}" placeholder="unbekannt"></div>
|
||||
<div class="feld"><label for="${praefix}OdoStart">Startkilometer</label><input type="number" id="${praefix}OdoStart" value="${wert(t && t.odo_start)}" placeholder="wird ergänzt"></div>
|
||||
<div class="feld"><label for="${praefix}OdoEnde">Endkilometer</label><input type="number" id="${praefix}OdoEnde" value="${wert(t && t.odo_end)}" placeholder="wird ergänzt"></div>
|
||||
<div class="feld" style="border-bottom:0"><label for="${praefix}Distanz">Gefahrene Distanz</label>
|
||||
<span class="mitEinheit"><input type="number" step="0.1" id="${praefix}Distanz" value="${wert(t && t.distance_km)}" placeholder="aus km-Stand"><span>km</span></span></div>`;
|
||||
}
|
||||
|
||||
function fahrtFormularWerte(praefix) {
|
||||
const zahl = (id) => { const v = $(`#${praefix}${id}`).value; return v === "" ? null : parseFloat(v.replace(",", ".")); };
|
||||
const text = (id) => $(`#${praefix}${id}`).value.trim() || null;
|
||||
const dauer = parseInt($(`#${praefix}Dauer`).value, 10) || 0;
|
||||
const tsStart = new Date(`${$(`#${praefix}Datum`).value}T${$(`#${praefix}Start`).value}:00`);
|
||||
return {
|
||||
ts_start: tsStart.toISOString(),
|
||||
ts_end: new Date(tsStart.getTime() + dauer * 60000).toISOString(),
|
||||
art: $(`#${praefix}Art`).value,
|
||||
start_ort: text("Startort"), ziel_ort: text("Zielort"),
|
||||
odo_start: zahl("OdoStart"), odo_end: zahl("OdoEnde"), distanz: zahl("Distanz"),
|
||||
};
|
||||
}
|
||||
|
||||
function vTripsFormular() {
|
||||
// eigene Funktion, damit sowohl der volle als auch der Leer-Zustand
|
||||
// (§11 Abnahmekriterium 1: eine Fahrt muss erfasst werden können) dasselbe
|
||||
// Formular zeigen - vorher fehlte es im Leer-Zustand komplett, der Knopf
|
||||
// wechselte zu "Abbrechen", aber es erschienen keine Felder.
|
||||
return `${fahrtOffen ? `<div class="tile"><span class="label">Neue Fahrt</span>
|
||||
<div class="feld"><label>Datum</label><input type="date" id="neuFahrtDatum" value="${new Date().toISOString().slice(0, 10)}"></div>
|
||||
<div class="feld"><label>Startzeit</label><input type="time" id="neuFahrtStart" value="08:00"></div>
|
||||
<div class="feld" style="border-bottom:0"><label>Dauer in Minuten</label><input type="number" id="neuFahrtDauer" placeholder="0"></div>
|
||||
<button class="aktion" data-fahrtspeichern>Speichern</button></div>` : ""}
|
||||
<button class="aktion primaer" data-fahrt>${fahrtOffen ? "Abbrechen" : "Fahrt erfassen"}</button>`;
|
||||
${fahrtFelder("neuFahrt", null)}
|
||||
<button class="aktion primaer" data-fahrtspeichern>Speichern</button></div>` : ""}
|
||||
<button class="aktion ${fahrtOffen ? "" : "primaer"}" data-fahrt>${fahrtOffen ? "Abbrechen" : "Fahrt erfassen"}</button>`;
|
||||
}
|
||||
function vTrips() {
|
||||
const jahre = [...new Set(TRIPS.map((t) => new Date(t.ts_start).getFullYear()))].sort((a, b) => b - a);
|
||||
@@ -2491,6 +2570,16 @@ function vTrips() {
|
||||
function vTrip(id) {
|
||||
const t = TRIPS.find((x) => x.trip_id === id);
|
||||
if (!t) return `<div class="tile">Fahrt nicht gefunden</div>`;
|
||||
|
||||
if (fahrtBearbeitenOffen) {
|
||||
return `
|
||||
<div class="tile"><span class="label">Fahrt bearbeiten</span>
|
||||
${fahrtFelder("bearbFahrt", t)}
|
||||
<button class="aktion primaer" data-fahrtaktualisieren="${t.trip_id}">Speichern</button>
|
||||
<button class="aktion" data-fahrtbearbeiten>Abbrechen</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
return `
|
||||
${mapBox()}
|
||||
<div class="tile flat" style="margin:18px 0 14px">
|
||||
@@ -2509,7 +2598,9 @@ function vTrip(id) {
|
||||
<div class="row"><dt>Dauer</dt><dd>${Math.round(t.duration_s / 60)} min</dd></div>
|
||||
<div class="row"><dt>Verbrauch</dt><dd>${fahrtVerbrauch(t) != null ? de(fahrtVerbrauch(t), 1) + " l/100 km" : "liegt nicht vor"}<small>vom Fahrzeug gemeldet</small></dd></div>
|
||||
<div class="row"><dt>Status</dt><dd>${t.status}<small>Quelle: ${t.km_quelle || "–"}</small></dd></div>
|
||||
</dl></div>
|
||||
</dl>
|
||||
<button class="aktion" data-fahrtbearbeiten>Werte bearbeiten</button>
|
||||
</div>
|
||||
<button class="aktion loeschen" data-tripdel="${t.trip_id}">Fahrt löschen</button>`;
|
||||
}
|
||||
|
||||
@@ -2637,6 +2728,7 @@ function vStat() {
|
||||
let neuOffen = false;
|
||||
let fahrtOffen = false;
|
||||
let tankBearbeitenOffen = false;
|
||||
let fahrtBearbeitenOffen = false;
|
||||
// Ergebnis eines geparsten, noch nicht gespeicherten Belegs: { praefix, daten }
|
||||
// - überlagert in tankFelder() die Beleg-Felder des jeweils offenen Formulars.
|
||||
let belegEntwurf = null;
|
||||
@@ -3324,6 +3416,9 @@ const go = (name, id) => {
|
||||
entwurfIndex = null;
|
||||
}
|
||||
sheet = null;
|
||||
// Ein offenes Bearbeiten-Formular gehört zu genau der Fahrt, auf der es
|
||||
// geöffnet wurde - sonst begrüßt die nächste Fahrt einen mit Eingabefeldern.
|
||||
fahrtBearbeitenOffen = false;
|
||||
/* B3: echter Verlauf statt statischer ZURUECK-Tabelle. Ein Tabwechsel setzt
|
||||
den Stapel zurück, weil Tabs Wurzeln sind, keine Ebenen. */
|
||||
if (istTab(name)) routeStack = [];
|
||||
@@ -3333,6 +3428,7 @@ const go = (name, id) => {
|
||||
};
|
||||
function zurueck() {
|
||||
sheet = null;
|
||||
fahrtBearbeitenOffen = false;
|
||||
const ziel = routeStack.pop() || { name: ZURUECK[route.name] || "home" };
|
||||
navRichtung = "raus";
|
||||
route = ziel; render();
|
||||
@@ -3821,12 +3917,18 @@ function ereignisseVerdrahten() {
|
||||
}
|
||||
if (e.target.closest("[data-fahrt]")) { fahrtOffen = !fahrtOffen; render(); return; }
|
||||
if (e.target.closest("[data-fahrtspeichern]")) {
|
||||
const datum = $("#neuFahrtDatum").value, start = $("#neuFahrtStart").value, dauer = parseInt($("#neuFahrtDauer").value, 10) || 0;
|
||||
const tsStart = new Date(`${datum}T${start}:00`);
|
||||
const tsEnd = new Date(tsStart.getTime() + dauer * 60000);
|
||||
serviceRufen("audi_dashboard_fahrt_manuell_anlegen", { ts_start: tsStart.toISOString(), ts_end: tsEnd.toISOString(), art: "privat" });
|
||||
serviceRufen("audi_dashboard_fahrt_manuell_anlegen", fahrtFormularWerte("neuFahrt"));
|
||||
fahrtOffen = false; render(); return;
|
||||
}
|
||||
if (e.target.closest("[data-fahrtbearbeiten]")) {
|
||||
fahrtBearbeitenOffen = !fahrtBearbeitenOffen; render(); return;
|
||||
}
|
||||
const fahrtAkt = e.target.closest("[data-fahrtaktualisieren]");
|
||||
if (fahrtAkt) {
|
||||
serviceRufen("audi_dashboard_fahrt_aktualisieren",
|
||||
{ trip_id: fahrtAkt.dataset.fahrtaktualisieren, ...fahrtFormularWerte("bearbFahrt") });
|
||||
fahrtBearbeitenOffen = false; render(); return;
|
||||
}
|
||||
if (e.target.closest("[data-tankspeichern]")) {
|
||||
const w = tankFormularWerte("neuTank");
|
||||
if (w.liter > 0 && w.kosten > 0) {
|
||||
@@ -3987,7 +4089,14 @@ function ereignisseVerdrahten() {
|
||||
const a = e.target.closest("[data-acc]");
|
||||
if (a) { const b = ROOT.getElementById(a.dataset.acc); a.setAttribute("aria-expanded", b.classList.toggle("on")); return; }
|
||||
const art = e.target.closest("[data-art]");
|
||||
if (art) { const tr = TRIPS.find((x) => x.trip_id === art.dataset.art); tr.art = tr.art === "arbeitsweg" ? "privat" : "arbeitsweg"; render(); return; }
|
||||
if (art) {
|
||||
const tr = TRIPS.find((x) => x.trip_id === art.dataset.art);
|
||||
tr.art = tr.art === "arbeitsweg" ? "privat" : "arbeitsweg";
|
||||
// Bis 2026-08-17 blieb das Umschalten rein lokal und war nach dem
|
||||
// naechsten Backend-Push wieder weg - jetzt wandert es mit.
|
||||
serviceRufen("audi_dashboard_fahrt_aktualisieren", { trip_id: tr.trip_id, art: tr.art });
|
||||
render(); return;
|
||||
}
|
||||
// Standort (Vollbild) - Karten-/Menü-Bedienung manipuliert SMAP/das Menü-
|
||||
// DOM direkt statt über render() (siehe Kopfkommentar Standort-Abschnitt).
|
||||
if (e.target.closest("[data-standort-umschalten]")) { standortMenuUmschalten(); return; }
|
||||
@@ -4209,8 +4318,15 @@ function belegErgebnisVerarbeiten(daten) {
|
||||
Deshalb jetzt ohne festes Abbruchlimit: nach der anfänglichen schnellen
|
||||
Serie (siehe NACHLADE_INTERVALL_MS) läuft es mit einem größeren, aber
|
||||
endlosen Intervall weiter, bis Daten da sind oder das Element abgehängt
|
||||
wird (disconnectedCallback stoppt hier nichts explizit, aber ein
|
||||
getimeter Aufruf ohne HASS/callWS bricht oben ohnehin ab). */
|
||||
wird (disconnectedCallback stoppt den Timer, connectedCallback stößt ihn
|
||||
über datenLaden() wieder an).
|
||||
|
||||
2026-08-17: Der Takt darf an keiner Stelle abreißen. Vorher stieg sowohl
|
||||
nachladeAnstossen() (bei fehlendem HASS/callWS) als auch datenLaden() (bei
|
||||
fehlendem HASS) still aus, ohne eine nächste Runde zu planen - danach half
|
||||
nur noch ein zufälliger hass-Push oder ein Klick ins Menü (go() ruft
|
||||
datenLaden() mit auf). Genau das war das gemeldete "Lädt … bis man auf
|
||||
einen Menüpunkt klickt". */
|
||||
const NACHGELADEN = {};
|
||||
let nachladeTimer = null;
|
||||
let nachladeVersuche = 0;
|
||||
@@ -4219,7 +4335,7 @@ const NACHLADE_LANGSAM_MS = 10000;
|
||||
const NACHLADE_SCHNELLE_VERSUCHE = 15;
|
||||
|
||||
function nachladeAnstossen() {
|
||||
if (nachladeTimer || !HASS || !HASS.callWS) return;
|
||||
if (nachladeTimer) return;
|
||||
nachladeVersuche++;
|
||||
// Erster Versuch fast sofort statt erst nach vollem Intervall - der
|
||||
// Wettlauf mit pyscripts beim_start() (siehe Kopfkommentar) ist meist
|
||||
@@ -4232,26 +4348,40 @@ function nachladeAnstossen() {
|
||||
: NACHLADE_LANGSAM_MS;
|
||||
nachladeTimer = setTimeout(async () => {
|
||||
nachladeTimer = null;
|
||||
try {
|
||||
const states = await HASS.callWS({ type: "get_states" });
|
||||
const finden = (id) => states.find((s) => s.entity_id === id);
|
||||
NACHGELADEN.profil = finden("pyscript.audi_dashboard_profil") || NACHGELADEN.profil;
|
||||
NACHGELADEN.fahrten = finden("pyscript.audi_dashboard_fahrten") || NACHGELADEN.fahrten;
|
||||
NACHGELADEN.tank = finden("pyscript.audi_dashboard_tankvorgaenge") || NACHGELADEN.tank;
|
||||
NACHGELADEN.status = finden("pyscript.audi_dashboard_fahrzeugstatus") || NACHGELADEN.status;
|
||||
NACHGELADEN.batt = finden("pyscript.audi_dashboard_batterieverlauf") || NACHGELADEN.batt;
|
||||
// Ohne diese Zeile bleibt der Setup-Katalog nach einem HA-Neustart leer,
|
||||
// bis zufaellig ein Push kommt - siehe setupBereit().
|
||||
NACHGELADEN.entitaeten = finden("pyscript.audi_dashboard_entitaeten") || NACHGELADEN.entitaeten;
|
||||
} catch (e) {
|
||||
// wird beim naechsten Versuch erneut probiert
|
||||
// HASS kann hier noch fehlen: panel_custom erzeugt das Element und setzt
|
||||
// hass nicht immer in dieser Reihenfolge. Frueher brach die Schleife in so
|
||||
// einem Fall oben ganz ab (Bedingung !HASS || !HASS.callWS) und wurde nie
|
||||
// wieder angestossen - genau der dauerhaft haengende "Lädt …"-Bildschirm.
|
||||
// Jetzt wird die Runde nur uebersprungen, der Takt laeuft weiter.
|
||||
if (HASS && HASS.callWS) {
|
||||
try {
|
||||
const states = await HASS.callWS({ type: "get_states" });
|
||||
const finden = (id) => states.find((s) => s.entity_id === id);
|
||||
NACHGELADEN.profil = finden("pyscript.audi_dashboard_profil") || NACHGELADEN.profil;
|
||||
NACHGELADEN.fahrten = finden("pyscript.audi_dashboard_fahrten") || NACHGELADEN.fahrten;
|
||||
NACHGELADEN.tank = finden("pyscript.audi_dashboard_tankvorgaenge") || NACHGELADEN.tank;
|
||||
NACHGELADEN.status = finden("pyscript.audi_dashboard_fahrzeugstatus") || NACHGELADEN.status;
|
||||
NACHGELADEN.batt = finden("pyscript.audi_dashboard_batterieverlauf") || NACHGELADEN.batt;
|
||||
// Ohne diese Zeile bleibt der Setup-Katalog nach einem HA-Neustart leer,
|
||||
// bis zufaellig ein Push kommt - siehe setupBereit().
|
||||
NACHGELADEN.entitaeten = finden("pyscript.audi_dashboard_entitaeten") || NACHGELADEN.entitaeten;
|
||||
} catch (e) {
|
||||
// wird beim naechsten Versuch erneut probiert
|
||||
}
|
||||
}
|
||||
datenLaden(true);
|
||||
// Rueckversicherung: datenLaden() stoesst die naechste Runde selbst an,
|
||||
// aber nur, wenn es nicht vorher aussteigt. Der Takt darf an keiner
|
||||
// Stelle abreissen, solange noch keine Daten da sind.
|
||||
if (!DATEN_GELADEN) nachladeAnstossen();
|
||||
}, verzoegerung);
|
||||
}
|
||||
|
||||
async function datenLaden(erzwingen) {
|
||||
if (!HASS) return;
|
||||
if (!HASS) {
|
||||
if (!DATEN_GELADEN) nachladeAnstossen();
|
||||
return;
|
||||
}
|
||||
const profilState = HASS.states["pyscript.audi_dashboard_profil"] || NACHGELADEN.profil;
|
||||
const fahrtenState = HASS.states["pyscript.audi_dashboard_fahrten"] || NACHGELADEN.fahrten;
|
||||
const tankState = HASS.states["pyscript.audi_dashboard_tankvorgaenge"] || NACHGELADEN.tank;
|
||||
@@ -4355,6 +4485,9 @@ class AudiDashboardPanel extends HTMLElement {
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
// Nachlade-Takt anhalten, solange das Panel nicht im Dokument haengt -
|
||||
// connectedCallback() stoesst ihn ueber datenLaden() wieder an.
|
||||
if (nachladeTimer) { clearTimeout(nachladeTimer); nachladeTimer = null; }
|
||||
// Alle drei Karten, nicht nur MAP: sonst bleibt beim Abhaengen des Panels
|
||||
// (Standort- oder Uebersichtsseite offen) eine Leaflet-Instanz samt ihrem
|
||||
// window-Resize-Horcher und laufenden Kachelabrufen am Leben.
|
||||
|
||||
@@ -68,11 +68,21 @@ html,body{height:100%}
|
||||
|
||||
/* ---------------- Navigationsleiste ---------------- */
|
||||
/* Sichere Bereiche des Geräterahmens: Statusleiste oben, Home-Indikator unten */
|
||||
.topbar{align-items:center;gap:4px;padding:56px 20px 4px}
|
||||
/* Linker Innenabstand = --seitenrand von main#view (16px), damit der Titel
|
||||
exakt auf der Kante der Kacheln darunter sitzt. */
|
||||
.topbar{align-items:center;gap:4px;padding:56px 20px 4px 16px}
|
||||
.head{display:flex;flex-direction:column}
|
||||
.eyebrow{display:none}
|
||||
.title{font-size:22px;font-weight:400;letter-spacing:-.01em;line-height:1.2}
|
||||
.back{width:34px;height:34px;margin-left:-8px;justify-content:center}
|
||||
/* Design-Audit 2026-08-13, auf allen Breiten nachgezogen 2026-08-17: .back ist
|
||||
auch inaktiv (opacity:0) noch ein Flex-Element plus Gap - auf Wurzelseiten
|
||||
ohne Zurueck-Pfeil schob das die Kopfzeile (.head/.title) sichtbar weiter
|
||||
nach rechts, als main#view darunter beginnt. Das negative margin-right hebt
|
||||
zusaetzlich den Gap auf, sonst blieben 4px Versatz. Auf Detailseiten
|
||||
(.back.on) bleibt der Platz bewusst reserviert, das ist dort ein echter,
|
||||
bedienbarer Knopf. */
|
||||
.back:not(.on){width:0;min-width:0;margin:0 -4px 0 0;overflow:hidden}
|
||||
/* Rot bleibt Akzent/destruktiv, nicht Navigationsfarbe (dieselbe Regel wie
|
||||
bei den Aktions-Buttons und dem aktiven Menü-Icon) - derselbe neutrale Ton
|
||||
wie die Kopfzeile selbst. */
|
||||
@@ -264,12 +274,6 @@ main#view{--seitenrand:16px;padding:0 var(--seitenrand) 30px;scroll-behavior:smo
|
||||
auf breiten Bildschirmen klaffte da eine sichtbare Luecke. */
|
||||
.topbar{grid-column:2;grid-row:1;padding:38px 44px 8px;max-width:860px}
|
||||
.phone:has(.back.on) .topbar{grid-column:2;grid-row:1;padding:34px 44px 8px;justify-content:flex-start;max-width:860px}
|
||||
/* Design-Audit 2026-08-13: .back ist auch inaktiv (opacity:0) noch ein
|
||||
44px breites Flex-Element plus Gap - auf Wurzelseiten ohne Zurueck-Pfeil
|
||||
schob das die Kopfzeile (.head/.title) sichtbar weiter nach rechts als
|
||||
main#view darunter beginnt. Auf Detailseiten (.back.on) bleibt der Platz
|
||||
bewusst reserviert, das ist dort ein echter, bedienbarer Knopf. */
|
||||
.back:not(.on){width:0;min-width:0;margin:0;overflow:hidden}
|
||||
.phone:has(.back.on) .head{align-items:flex-start}
|
||||
.phone:has(.back.on) .title{font-size:26px;text-align:left}
|
||||
.title{font-size:34px}
|
||||
@@ -304,8 +308,14 @@ main#view{--seitenrand:16px;padding:0 var(--seitenrand) 30px;scroll-behavior:smo
|
||||
.rings{display:none !important}
|
||||
|
||||
.duo{grid-template-columns:repeat(3,minmax(0,1fr))}
|
||||
.carfix,.bildbox.carfix{height:260px}
|
||||
.carfix.klein,.bildbox.carfix.klein{height:220px}
|
||||
/* Nur die Box selbst hoeher setzen, nicht jedes Element mit .carfix: die
|
||||
Klasse haengt auch am <img> und am Platzhalter-<div> (siehe
|
||||
bildMitPlatzhalter()). Der Platzhalter ist position:absolute;inset:0 -
|
||||
eine feste Hoehe schlaegt dort das bottom:0 und machte ihn 260px hoch in
|
||||
einer 132px hohen, overflow:hidden-Box (leerer Zustand), also sichtbar
|
||||
abgeschnitten. Bild und Platzhalter fuellen die Box ohnehin per 100%. */
|
||||
.bildbox.carfix{height:260px}
|
||||
.bildbox.carfix.klein{height:220px}
|
||||
.bildgrid{grid-template-columns:repeat(4,1fr)}
|
||||
.quad{gap:14px}
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version": 1786920185}
|
||||
{"version": 1787000000}
|
||||
|
||||
@@ -122,13 +122,43 @@ def _fahrt_beenden(start_ts, ende_ts):
|
||||
fahrtabschluss_logik.screening_durchfuehren(einstellungen.KM_SENSOR)
|
||||
|
||||
|
||||
def _handfelder(start_ort, ziel_ort, odo_start, odo_end, distanz):
|
||||
"""Die von Hand befüllbaren Felder einer Fahrt auf ihre Datensatz-Namen
|
||||
abgebildet, ohne die leer gelassenen. Der Aufrufer schreibt die Schlüssel
|
||||
zusätzlich nach edited_fields - das schützt sie vor dem automatischen
|
||||
Kilometerstand-Screening (§7.2), das sonst später darüberschreiben würde."""
|
||||
werte = {
|
||||
"start_address": start_ort or None,
|
||||
"end_address": ziel_ort or None,
|
||||
"odo_start": odo_start,
|
||||
"odo_end": odo_end,
|
||||
"distance_km": distanz,
|
||||
}
|
||||
return {k: v for k, v in werte.items() if v is not None}
|
||||
|
||||
|
||||
def _strecke_ergaenzen(felder):
|
||||
"""Fehlt die Distanz, ergibt sie sich aus den beiden Kilometerständen -
|
||||
dieselbe Rechnung wie im Screening (fahrtabschluss_logik)."""
|
||||
if "distance_km" not in felder and "odo_start" in felder and "odo_end" in felder:
|
||||
felder["distance_km"] = round(felder["odo_end"] - felder["odo_start"], 1)
|
||||
return felder
|
||||
|
||||
|
||||
@service
|
||||
def audi_dashboard_fahrt_manuell_anlegen(ts_start=None, ts_end=None, art="privat"):
|
||||
def audi_dashboard_fahrt_manuell_anlegen(ts_start=None, ts_end=None, art="privat",
|
||||
start_ort=None, ziel_ort=None,
|
||||
odo_start=None, odo_end=None, distanz=None):
|
||||
"""Manuelle Fahrterfassung aus der Oberfläche (§5.3), unabhängig von der
|
||||
automatischen Zündungs-Erkennung. Aufruf als pyscript.audi_dashboard_fahrt_manuell_anlegen."""
|
||||
automatischen Zündungs-Erkennung. Dieselben Felder wie beim Bearbeiten
|
||||
einer bestehenden Fahrt (siehe audi_dashboard_fahrt_aktualisieren); alles
|
||||
außer Start- und Endzeitpunkt ist optional und bleibt leer, wenn es das
|
||||
Screening später selbst ergänzen soll. Aufruf als
|
||||
pyscript.audi_dashboard_fahrt_manuell_anlegen."""
|
||||
start_ts = datetime.datetime.fromisoformat(ts_start)
|
||||
ende_ts = datetime.datetime.fromisoformat(ts_end)
|
||||
dauer_s = int((ende_ts - start_ts).total_seconds())
|
||||
hand = _strecke_ergaenzen(_handfelder(start_ort, ziel_ort, odo_start, odo_end, distanz))
|
||||
fahrt = {
|
||||
"trip_id": profil.neue_id("t"),
|
||||
"ts_start": start_ts.isoformat(),
|
||||
@@ -152,11 +182,54 @@ def audi_dashboard_fahrt_manuell_anlegen(ts_start=None, ts_end=None, art="privat
|
||||
"status": "offen",
|
||||
"edited_fields": [],
|
||||
}
|
||||
fahrt.update(hand)
|
||||
fahrt["edited_fields"] = sorted(hand)
|
||||
if fahrt["distance_km"] is not None:
|
||||
fahrt["km_quelle"] = "manuell"
|
||||
fahrt["status"] = "vollständig"
|
||||
profil.fahrt_anhaengen(fahrt)
|
||||
frontend_veroeffentlichung.fahrten_veroeffentlichen()
|
||||
fahrtabschluss_logik.screening_durchfuehren(einstellungen.KM_SENSOR)
|
||||
|
||||
|
||||
@service
|
||||
def audi_dashboard_fahrt_aktualisieren(trip_id=None, ts_start=None, ts_end=None, art=None,
|
||||
start_ort=None, ziel_ort=None,
|
||||
odo_start=None, odo_end=None, distanz=None):
|
||||
"""Bearbeitet eine bestehende Fahrt (Einzelfahrt-Seite), egal ob sie
|
||||
automatisch erkannt oder von Hand angelegt wurde. Aufruf als
|
||||
pyscript.audi_dashboard_fahrt_aktualisieren."""
|
||||
if not trip_id:
|
||||
log.warning("audi_dashboard: fahrt_aktualisieren ohne trip_id aufgerufen")
|
||||
return
|
||||
aenderungen = _strecke_ergaenzen(_handfelder(start_ort, ziel_ort, odo_start, odo_end, distanz))
|
||||
if art:
|
||||
aenderungen["art"] = art
|
||||
if ts_start and ts_end:
|
||||
start_ts = datetime.datetime.fromisoformat(ts_start)
|
||||
ende_ts = datetime.datetime.fromisoformat(ts_end)
|
||||
aenderungen["ts_start"] = start_ts.isoformat()
|
||||
aenderungen["ts_end"] = ende_ts.isoformat()
|
||||
aenderungen["duration_s"] = int((ende_ts - start_ts).total_seconds())
|
||||
if aenderungen.get("distance_km") is not None:
|
||||
aenderungen["km_quelle"] = "manuell"
|
||||
aenderungen["status"] = "vollständig"
|
||||
# Von Hand gesetzte Felder bleiben von Hand gesetzt: die Liste wächst mit,
|
||||
# damit ein späteres Screening sie nicht wieder überschreibt.
|
||||
# Bewusst eine Listen-Abstraktion statt next(<Generator>): pyscript kann
|
||||
# keine Generator-Ausdrücke auswerten ("not implemented ast
|
||||
# ast_generatorexp"), Listen-Abstraktionen dagegen schon.
|
||||
passend = [f for f in profil.fahrten_lesen() if f.get("trip_id") == trip_id]
|
||||
handfelder = {"start_address", "end_address", "odo_start", "odo_end", "distance_km"}
|
||||
bisher = set(passend[0].get("edited_fields", [])) if passend else set()
|
||||
aenderungen["edited_fields"] = sorted(bisher | (handfelder & set(aenderungen)))
|
||||
if profil.fahrt_bearbeiten(trip_id, aenderungen):
|
||||
frontend_veroeffentlichung.fahrten_veroeffentlichen()
|
||||
log.info(f"audi_dashboard: Fahrt {trip_id} aktualisiert")
|
||||
else:
|
||||
log.warning(f"audi_dashboard: Fahrt {trip_id} nicht gefunden")
|
||||
|
||||
|
||||
@service
|
||||
def audi_dashboard_fahrt_loeschen(trip_id=None):
|
||||
"""Löscht eine Fahrt unwiderruflich (Löschfunktion in der Oberfläche,
|
||||
|
||||
@@ -67,7 +67,6 @@ FENSTER_SENSOREN = []
|
||||
|
||||
# Heckklappe und Motorhaube - keine Quelle mehr vorhanden.
|
||||
HECKKLAPPE_SENSOR = ""
|
||||
HECKKLAPPENSCHLOSS_SENSOR = ""
|
||||
HAUBE_SENSOR = ""
|
||||
|
||||
# Vom Fahrzeug selbst gemeldete Service-Fälligkeit (ergänzt die App-eigene,
|
||||
|
||||
@@ -85,10 +85,6 @@ FELDER = [
|
||||
"hinweis": "\"aus\"/off = zu.",
|
||||
"domains": ["binary_sensor"], "device_classes": ["door", "opening"], "units": [], "liste": False, "pflicht": False,
|
||||
"stichworte": ["heckklappe", "tailgate", "kofferraum", "trunk"]},
|
||||
{"key": "HECKKLAPPENSCHLOSS_SENSOR", "label": "Heckklappenschloss", "gruppe": "sicherheit",
|
||||
"hinweis": "\"on\" = entriegelt, \"aus\"/off = verriegelt.",
|
||||
"domains": ["binary_sensor"], "device_classes": ["lock"], "units": [], "liste": False, "pflicht": False,
|
||||
"stichworte": ["heckklappe", "tailgate", "schloss", "lock"]},
|
||||
{"key": "HAUBE_SENSOR", "label": "Motorhaube", "gruppe": "sicherheit",
|
||||
"hinweis": "\"aus\"/off = zu.",
|
||||
"domains": ["binary_sensor"], "device_classes": ["door", "opening"], "units": [], "liste": False, "pflicht": False,
|
||||
|
||||
@@ -138,7 +138,6 @@ def _sicherheitscheck():
|
||||
eintraege.append({"label": f"Fenster {pos}", "ok": None if w is None else w == "off"})
|
||||
for label, sensor in [
|
||||
("Heckklappe", einstellungen.HECKKLAPPE_SENSOR),
|
||||
("Heckklappenschloss", einstellungen.HECKKLAPPENSCHLOSS_SENSOR),
|
||||
("Motorhaube", einstellungen.HAUBE_SENSOR),
|
||||
]:
|
||||
w = zustand_oder_none(sensor)
|
||||
|
||||
@@ -138,16 +138,20 @@ def offene_fahrten():
|
||||
return sorted(offen, key=lambda f: f.get("ts_start", ""))
|
||||
|
||||
|
||||
def _datensatz_aktualisieren(zeilen, id_feld, id_wert, aenderungen, schreiben):
|
||||
def _datensatz_aktualisieren(zeilen, id_feld, id_wert, aenderungen, schreiben, schutz=True):
|
||||
"""Ersetzt ausgewählte Felder eines Datensatzes anhand seiner ID und
|
||||
schreibt den gesamten Bestand neu. Manuell geänderte Felder
|
||||
(edited_fields, analog zu §7.7 Regel 3) werden dabei nie überschrieben.
|
||||
Gemeinsame Grundlage für Fahrten und Tankvorgänge - beide Archive
|
||||
funktionieren nach demselben Muster."""
|
||||
funktionieren nach demselben Muster.
|
||||
|
||||
schutz=False hebt genau diese Sperre auf - nötig für Eingaben aus der
|
||||
Oberfläche: edited_fields schützt gegen die automatische Ergänzung, nicht
|
||||
gegen den Menschen, der das Feld gerade selbst korrigiert."""
|
||||
geaendert = False
|
||||
for d in zeilen:
|
||||
if d.get(id_feld) == id_wert:
|
||||
geschuetzt = set(d.get("edited_fields", []))
|
||||
geschuetzt = set(d.get("edited_fields", [])) if schutz else set()
|
||||
for feld, wert in aenderungen.items():
|
||||
if feld not in geschuetzt:
|
||||
d[feld] = wert
|
||||
@@ -162,6 +166,13 @@ def fahrt_aktualisieren(trip_id, aenderungen):
|
||||
return _datensatz_aktualisieren(fahrten_lesen(), "trip_id", trip_id, aenderungen, fahrten_schreiben)
|
||||
|
||||
|
||||
def fahrt_bearbeiten(trip_id, aenderungen):
|
||||
"""Wie fahrt_aktualisieren(), aber für Eingaben aus der Oberfläche: eine
|
||||
von Hand gesetzte Angabe sticht auch dann, wenn dasselbe Feld schon einmal
|
||||
von Hand gesetzt wurde (siehe schutz-Parameter oben)."""
|
||||
return _datensatz_aktualisieren(fahrten_lesen(), "trip_id", trip_id, aenderungen, fahrten_schreiben, schutz=False)
|
||||
|
||||
|
||||
def fahrt_loeschen(trip_id):
|
||||
fahrten = fahrten_lesen()
|
||||
uebrig = [f for f in fahrten if f.get("trip_id") != trip_id]
|
||||
|
||||
@@ -1123,24 +1123,24 @@ function fahrzeugMeldungRoh(art) {
|
||||
return { ts: null, km: null };
|
||||
}
|
||||
|
||||
/* App-eigene Ölwechsel-Prognose, eigenständig statt über die allgemeine
|
||||
tagesleistung()/prognose()-Mischrechnung (die für Ölwechsel zu träge ist,
|
||||
weil sie den langfristigen Durchschnitt seit dem allerersten Servicebuch-
|
||||
eintrag mit einfließen lässt): die Berechnung beginnt bewusst komplett neu
|
||||
ab dem Datum des letzten tatsächlich durchgeführten Ölwechsels.
|
||||
/* App-eigene Service-Prognose, eigenständig statt über die allgemeine
|
||||
tagesleistung()/prognose()-Mischrechnung (die dafür zu träge ist, weil sie
|
||||
den langfristigen Durchschnitt seit dem allerersten Servicebucheintrag mit
|
||||
einfließen lässt): die Berechnung beginnt bewusst komplett neu ab dem Datum
|
||||
der letzten tatsächlich durchgeführten Arbeit dieser Art.
|
||||
|
||||
Logik (wie vorgegeben): km und Tage seit dem letzten Ölwechsel ergeben die
|
||||
aktuelle Fahrleistung pro Tag; hochgerechnet auf die restlichen km bis zum
|
||||
eingestellten Intervall (Kundeneigene Einstellung, siehe CAR.oel) ergibt
|
||||
das ein Datum - gedeckelt auf spätestens das Intervall-Zeitlimit (z. B. 1
|
||||
Jahr) nach dem letzten Ölwechsel, falls seither wenig gefahren wurde. */
|
||||
function oelwechselPrognose() {
|
||||
Logik (wie vorgegeben): km und Tage seit diesem Eintrag ergeben die aktuelle
|
||||
Fahrleistung pro Tag; hochgerechnet auf die restlichen km bis zum
|
||||
eingestellten Intervall ergibt das ein Datum - gedeckelt auf spätestens das
|
||||
Intervall-Zeitlimit (z. B. 1 Jahr) nach dem letzten Eintrag, falls seither
|
||||
wenig gefahren wurde.
|
||||
|
||||
Gemeinsame Grundlage für Ölwechsel und Inspektion (siehe die beiden
|
||||
Aufrufer darunter) - beide rechnen identisch, nur mit anderem Intervall. */
|
||||
function servicePrognose(stichwort, intervallKm, intervallMonate) {
|
||||
if (!CAR.odoBekannt) return null;
|
||||
const letzter = letzterEintrag("öl");
|
||||
const letzter = letzterEintrag(stichwort);
|
||||
if (!letzter) return null;
|
||||
const oel = CAR.oel;
|
||||
const intervallKm = oel.modus === "hersteller" ? oel.herstellerKm : Math.min(oel.km, oel.herstellerKm);
|
||||
const intervallMonate = oel.modus === "hersteller" ? oel.herstellerMonate : Math.min(oel.monate, oel.herstellerMonate);
|
||||
const zielKm = letzter.km + intervallKm;
|
||||
const restKm = Math.max(0, zielKm - CAR.odo);
|
||||
const kappDatum = monatePlus(letzter.datum, intervallMonate);
|
||||
@@ -1151,13 +1151,13 @@ function oelwechselPrognose() {
|
||||
const tageSeit = (heute() - dat(letzter.datum)) / dTag;
|
||||
const kmSeit = CAR.odo - letzter.km;
|
||||
if (tageSeit <= 0 || kmSeit <= 0) {
|
||||
// Noch keine belastbare Fahrleistung seit dem Ölwechsel bekannt (z. B.
|
||||
// Noch keine belastbare Fahrleistung seit dem Eintrag bekannt (z. B.
|
||||
// direkt danach) - ohne Rate keine seriöse Km-Prognose, also das
|
||||
// Zeitlimit als einzig belastbare Grenze zeigen.
|
||||
datum = kappDatum;
|
||||
} else {
|
||||
const kmProTagSeitOel = kmSeit / tageSeit;
|
||||
const tageBisZiel = restKm / kmProTagSeitOel;
|
||||
const kmProTagSeither = kmSeit / tageSeit;
|
||||
const tageBisZiel = restKm / kmProTagSeither;
|
||||
const berechnet = new Date(heute().getTime() + tageBisZiel * dTag);
|
||||
datum = berechnet < kappDatum ? berechnet : kappDatum;
|
||||
}
|
||||
@@ -1165,6 +1165,27 @@ function oelwechselPrognose() {
|
||||
return { km: zielKm, restKm, datum };
|
||||
}
|
||||
|
||||
function oelwechselPrognose() {
|
||||
const oel = CAR.oel;
|
||||
return servicePrognose("öl",
|
||||
oel.modus === "hersteller" ? oel.herstellerKm : Math.min(oel.km, oel.herstellerKm),
|
||||
oel.modus === "hersteller" ? oel.herstellerMonate : Math.min(oel.monate, oel.herstellerMonate));
|
||||
}
|
||||
/* Inspektion: dieselben 30.000 km / 24 Monate wie in intervalle()/termine().
|
||||
Angezeigt wird davon bewusst nur das Datum ("voraussichtlich am …") - die
|
||||
Restkilometer stehen bei der Inspektion schon in der Zeile daneben. */
|
||||
function inspektionPrognose() { return servicePrognose("inspektion", 30000, 24); }
|
||||
|
||||
/* Kleine graue Zweitzeile unter dem Termin-Titel, in Service-Kachel und
|
||||
Service-Seite identisch aufgebaut. Leer, solange kein passender
|
||||
Servicebucheintrag vorliegt. */
|
||||
function prognoseZeile(art) {
|
||||
const p = art === "Ölwechsel" ? oelwechselPrognose() : art === "Inspektion" ? inspektionPrognose() : null;
|
||||
if (!p) return "";
|
||||
const km = art === "Ölwechsel" ? de(p.restKm) + " km, " : "";
|
||||
return `<br><span style="color:var(--fg3);font-size:11px">${km}voraussichtlich am ${dedat(p.datum)}</span>`;
|
||||
}
|
||||
|
||||
function terminText(t) {
|
||||
const restKm = t.km !== null && CAR.odoBekannt ? t.km - CAR.odo : null;
|
||||
const restKmAbs = restKm !== null ? Math.abs(restKm) : null;
|
||||
@@ -1318,22 +1339,25 @@ function vAudi() {
|
||||
// (Sensor nicht verfügbar), fällt sie auf dieselbe App-eigene
|
||||
// Servicebuch-Schätzung zurück wie Hauptuntersuchung.
|
||||
const fm = t.art === "Hauptuntersuchung" ? { ts: null, km: null } : fahrzeugMeldungRoh(t.art);
|
||||
// Zusätzlich zur realen Meldung klein die App-eigene Prognose
|
||||
// (Servicebuch + tatsächliche Fahrleistung seit der letzten Arbeit,
|
||||
// siehe servicePrognose()). Steht links unter dem Kachel-Titel statt
|
||||
// rechts bei "am"/"in", die beide gleichwertig (gleiche Schriftgröße
|
||||
// wie "am") untereinander in der rechten Spalte stehen. Die Inspektion
|
||||
// bekommt sie auf Wunsch auch ohne Fahrzeugmeldung und ohne den
|
||||
// km-Anteil - beim Ölwechsel stünde die Zahl dort doppelt, weil
|
||||
// terminText() den generischen Termin schon "voraussichtlich" nennt.
|
||||
const eigen = prognoseZeile(t.art);
|
||||
if (fm.ts == null && fm.km == null) {
|
||||
const x = terminText(t);
|
||||
return `<div class="row" ${letzte}>
|
||||
<dt>${t.art}</dt><dd>${x.haupt}${x.neben ? `<small style="text-align:left">${x.neben}</small>` : ""}</dd></div>`;
|
||||
<dt>${t.art}${t.art === "Inspektion" ? eigen : ""}</dt>
|
||||
<dd>${x.haupt}${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" : "";
|
||||
// Zusätzlich zur realen Meldung bei Ölwechsel klein die App-eigene
|
||||
// Prognose (Servicebuch + tatsächliche Fahrleistung seit dem letzten
|
||||
// Wechsel, siehe oelwechselPrognose()) - bei Inspektion bewusst nicht,
|
||||
// wie gefordert. Steht links unter dem Kachel-Titel statt rechts bei
|
||||
// "am"/"in", die beide gleichwertig (gleiche Schriftgröße wie "am")
|
||||
// untereinander in der rechten Spalte stehen.
|
||||
const eigen = t.art === "Ölwechsel" ? oelwechselPrognose() : null;
|
||||
return `<div class="row" ${letzte}>
|
||||
<dt>${t.art}${eigen ? `<br><span style="color:var(--fg3);font-size:11px">${de(eigen.restKm)} km, voraussichtlich am ${dedat(eigen.datum)}</span>` : ""}</dt>
|
||||
<dt>${t.art}${eigen}</dt>
|
||||
<dd>${haupt}${neben ? `<span style="display:block">${neben}</span>` : ""}</dd></div>`;
|
||||
}).join("")}
|
||||
</dl></button>
|
||||
@@ -1628,11 +1652,12 @@ function vService() {
|
||||
const anzeige = iv.hersteller || iv.eigen;
|
||||
const fm = fahrzeugMeldungRoh(t.art);
|
||||
const fmDatum = fm.ts ? dedat(new Date(fm.ts)) : null;
|
||||
// Forecast-Zeile nur bei Ölwechsel (Kundeneigenes Intervall aus
|
||||
// "Einrichten", eigenständige Reset-nach-Service-Logik, siehe
|
||||
// oelwechselPrognose()) - bei Inspektion und Hauptuntersuchung
|
||||
// bewusst keine Forecast-Zeile mehr.
|
||||
// Forecast-Zeile aus dem kundeneigenen Intervall in "Einrichten" mit
|
||||
// eigenständiger Reset-nach-Service-Logik (servicePrognose()): beim
|
||||
// Ölwechsel mit Restkilometern, bei der Inspektion auf Wunsch nur mit
|
||||
// dem Datum. Hauptuntersuchung bleibt bewusst ohne.
|
||||
const eigenOel = t.art === "Ölwechsel" ? oelwechselPrognose() : null;
|
||||
const eigenInsp = t.art === "Inspektion" ? inspektionPrognose() : null;
|
||||
return `<div class="row" ${letzte}>
|
||||
<dt>${t.art}
|
||||
${t.vereinbart ? `<br><span style="color:var(--red);font-size:11px">Werkstatt am ${dedat(new Date(t.vereinbart))}</span>` : ""}
|
||||
@@ -1642,8 +1667,10 @@ function vService() {
|
||||
${fm.km != null || fmDatum ? `
|
||||
<span class="werte">${fm.km != null ? de(fm.km) + " km" : ""}${fm.km != null && fmDatum ? " / " : ""}${fmDatum || ""}</span>
|
||||
${eigenOel ? `<small style="text-align:left">${de(eigenOel.restKm)} km, voraussichtlich am ${dedat(eigenOel.datum)}</small>` : ""}
|
||||
${eigenInsp ? `<small style="text-align:left">voraussichtlich am ${dedat(eigenInsp.datum)}</small>` : ""}
|
||||
` : `
|
||||
<span class="werte">${eigenOel ? de(eigenOel.restKm) + " km · " + dedat(eigenOel.datum) : (anzeige.km !== null ? de(anzeige.km) + " km · " : "") + dedat(anzeige.datum)}</span>
|
||||
${eigenInsp ? `<small style="text-align:left">voraussichtlich am ${dedat(eigenInsp.datum)}</small>` : ""}
|
||||
`}
|
||||
</dd>
|
||||
</div>`; }).join("")}
|
||||
@@ -2440,17 +2467,69 @@ function vReifen() {
|
||||
${satz("winter")}`;
|
||||
}
|
||||
|
||||
/* Gemeinsames Formular für "Neue Fahrt" und das Bearbeiten einer bestehenden
|
||||
Fahrt - dieselben Felder an beiden Stellen, damit sie nicht auseinander-
|
||||
laufen können (gleiches Muster wie tankFelder()/tankFormularWerte()). Die
|
||||
Felder decken genau das ab, was die Einzelfahrt-Seite anzeigt; Verbrauch,
|
||||
Status und km-Quelle bleiben außen vor, die meldet das Fahrzeug bzw. leitet
|
||||
das Backend ab. */
|
||||
const FAHRT_ARTEN = ["privat", "arbeitsweg"];
|
||||
|
||||
/* Datum für ein <input type="date"> in Ortszeit. Bewusst nicht über
|
||||
toISOString(), das rechnet nach UTC um und liefert kurz nach Mitternacht
|
||||
den Vortag. */
|
||||
function datumsfeld(d) {
|
||||
const zwei = (n) => String(n).padStart(2, "0");
|
||||
return `${d.getFullYear()}-${zwei(d.getMonth() + 1)}-${zwei(d.getDate())}`;
|
||||
}
|
||||
function zeitfeld(d) {
|
||||
const zwei = (n) => String(n).padStart(2, "0");
|
||||
return `${zwei(d.getHours())}:${zwei(d.getMinutes())}`;
|
||||
}
|
||||
|
||||
function fahrtFelder(praefix, t) {
|
||||
const start = t && t.ts_start ? new Date(t.ts_start) : new Date();
|
||||
const dauer = t && t.duration_s != null ? Math.round(t.duration_s / 60) : "";
|
||||
const art = t && t.art ? t.art : FAHRT_ARTEN[0];
|
||||
const wert = (v) => (v == null ? "" : v);
|
||||
return `
|
||||
<div class="feld"><label for="${praefix}Datum">Datum</label><input type="date" id="${praefix}Datum" value="${datumsfeld(start)}"></div>
|
||||
<div class="feld"><label for="${praefix}Start">Startzeit</label><input type="time" id="${praefix}Start" value="${t ? zeitfeld(start) : "08:00"}"></div>
|
||||
<div class="feld"><label for="${praefix}Dauer">Dauer</label>
|
||||
<span class="mitEinheit"><input type="number" min="0" id="${praefix}Dauer" value="${dauer}" placeholder="0"><span>min</span></span></div>
|
||||
<div class="feld"><label for="${praefix}Art">Art</label>
|
||||
<select id="${praefix}Art">${FAHRT_ARTEN.map((a) => `<option ${a === art ? "selected" : ""}>${a}</option>`).join("")}</select></div>
|
||||
<div class="feld"><label for="${praefix}Startort">Startort</label><input id="${praefix}Startort" value="${esc(wert(t && t.start_address))}" placeholder="unbekannt"></div>
|
||||
<div class="feld"><label for="${praefix}Zielort">Zielort</label><input id="${praefix}Zielort" value="${esc(wert(t && t.end_address))}" placeholder="unbekannt"></div>
|
||||
<div class="feld"><label for="${praefix}OdoStart">Startkilometer</label><input type="number" id="${praefix}OdoStart" value="${wert(t && t.odo_start)}" placeholder="wird ergänzt"></div>
|
||||
<div class="feld"><label for="${praefix}OdoEnde">Endkilometer</label><input type="number" id="${praefix}OdoEnde" value="${wert(t && t.odo_end)}" placeholder="wird ergänzt"></div>
|
||||
<div class="feld" style="border-bottom:0"><label for="${praefix}Distanz">Gefahrene Distanz</label>
|
||||
<span class="mitEinheit"><input type="number" step="0.1" id="${praefix}Distanz" value="${wert(t && t.distance_km)}" placeholder="aus km-Stand"><span>km</span></span></div>`;
|
||||
}
|
||||
|
||||
function fahrtFormularWerte(praefix) {
|
||||
const zahl = (id) => { const v = $(`#${praefix}${id}`).value; return v === "" ? null : parseFloat(v.replace(",", ".")); };
|
||||
const text = (id) => $(`#${praefix}${id}`).value.trim() || null;
|
||||
const dauer = parseInt($(`#${praefix}Dauer`).value, 10) || 0;
|
||||
const tsStart = new Date(`${$(`#${praefix}Datum`).value}T${$(`#${praefix}Start`).value}:00`);
|
||||
return {
|
||||
ts_start: tsStart.toISOString(),
|
||||
ts_end: new Date(tsStart.getTime() + dauer * 60000).toISOString(),
|
||||
art: $(`#${praefix}Art`).value,
|
||||
start_ort: text("Startort"), ziel_ort: text("Zielort"),
|
||||
odo_start: zahl("OdoStart"), odo_end: zahl("OdoEnde"), distanz: zahl("Distanz"),
|
||||
};
|
||||
}
|
||||
|
||||
function vTripsFormular() {
|
||||
// eigene Funktion, damit sowohl der volle als auch der Leer-Zustand
|
||||
// (§11 Abnahmekriterium 1: eine Fahrt muss erfasst werden können) dasselbe
|
||||
// Formular zeigen - vorher fehlte es im Leer-Zustand komplett, der Knopf
|
||||
// wechselte zu "Abbrechen", aber es erschienen keine Felder.
|
||||
return `${fahrtOffen ? `<div class="tile"><span class="label">Neue Fahrt</span>
|
||||
<div class="feld"><label>Datum</label><input type="date" id="neuFahrtDatum" value="${new Date().toISOString().slice(0, 10)}"></div>
|
||||
<div class="feld"><label>Startzeit</label><input type="time" id="neuFahrtStart" value="08:00"></div>
|
||||
<div class="feld" style="border-bottom:0"><label>Dauer in Minuten</label><input type="number" id="neuFahrtDauer" placeholder="0"></div>
|
||||
<button class="aktion" data-fahrtspeichern>Speichern</button></div>` : ""}
|
||||
<button class="aktion primaer" data-fahrt>${fahrtOffen ? "Abbrechen" : "Fahrt erfassen"}</button>`;
|
||||
${fahrtFelder("neuFahrt", null)}
|
||||
<button class="aktion primaer" data-fahrtspeichern>Speichern</button></div>` : ""}
|
||||
<button class="aktion ${fahrtOffen ? "" : "primaer"}" data-fahrt>${fahrtOffen ? "Abbrechen" : "Fahrt erfassen"}</button>`;
|
||||
}
|
||||
function vTrips() {
|
||||
const jahre = [...new Set(TRIPS.map((t) => new Date(t.ts_start).getFullYear()))].sort((a, b) => b - a);
|
||||
@@ -2491,6 +2570,16 @@ function vTrips() {
|
||||
function vTrip(id) {
|
||||
const t = TRIPS.find((x) => x.trip_id === id);
|
||||
if (!t) return `<div class="tile">Fahrt nicht gefunden</div>`;
|
||||
|
||||
if (fahrtBearbeitenOffen) {
|
||||
return `
|
||||
<div class="tile"><span class="label">Fahrt bearbeiten</span>
|
||||
${fahrtFelder("bearbFahrt", t)}
|
||||
<button class="aktion primaer" data-fahrtaktualisieren="${t.trip_id}">Speichern</button>
|
||||
<button class="aktion" data-fahrtbearbeiten>Abbrechen</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
return `
|
||||
${mapBox()}
|
||||
<div class="tile flat" style="margin:18px 0 14px">
|
||||
@@ -2509,7 +2598,9 @@ function vTrip(id) {
|
||||
<div class="row"><dt>Dauer</dt><dd>${Math.round(t.duration_s / 60)} min</dd></div>
|
||||
<div class="row"><dt>Verbrauch</dt><dd>${fahrtVerbrauch(t) != null ? de(fahrtVerbrauch(t), 1) + " l/100 km" : "liegt nicht vor"}<small>vom Fahrzeug gemeldet</small></dd></div>
|
||||
<div class="row"><dt>Status</dt><dd>${t.status}<small>Quelle: ${t.km_quelle || "–"}</small></dd></div>
|
||||
</dl></div>
|
||||
</dl>
|
||||
<button class="aktion" data-fahrtbearbeiten>Werte bearbeiten</button>
|
||||
</div>
|
||||
<button class="aktion loeschen" data-tripdel="${t.trip_id}">Fahrt löschen</button>`;
|
||||
}
|
||||
|
||||
@@ -2637,6 +2728,7 @@ function vStat() {
|
||||
let neuOffen = false;
|
||||
let fahrtOffen = false;
|
||||
let tankBearbeitenOffen = false;
|
||||
let fahrtBearbeitenOffen = false;
|
||||
// Ergebnis eines geparsten, noch nicht gespeicherten Belegs: { praefix, daten }
|
||||
// - überlagert in tankFelder() die Beleg-Felder des jeweils offenen Formulars.
|
||||
let belegEntwurf = null;
|
||||
@@ -3324,6 +3416,9 @@ const go = (name, id) => {
|
||||
entwurfIndex = null;
|
||||
}
|
||||
sheet = null;
|
||||
// Ein offenes Bearbeiten-Formular gehört zu genau der Fahrt, auf der es
|
||||
// geöffnet wurde - sonst begrüßt die nächste Fahrt einen mit Eingabefeldern.
|
||||
fahrtBearbeitenOffen = false;
|
||||
/* B3: echter Verlauf statt statischer ZURUECK-Tabelle. Ein Tabwechsel setzt
|
||||
den Stapel zurück, weil Tabs Wurzeln sind, keine Ebenen. */
|
||||
if (istTab(name)) routeStack = [];
|
||||
@@ -3333,6 +3428,7 @@ const go = (name, id) => {
|
||||
};
|
||||
function zurueck() {
|
||||
sheet = null;
|
||||
fahrtBearbeitenOffen = false;
|
||||
const ziel = routeStack.pop() || { name: ZURUECK[route.name] || "home" };
|
||||
navRichtung = "raus";
|
||||
route = ziel; render();
|
||||
@@ -3821,12 +3917,18 @@ function ereignisseVerdrahten() {
|
||||
}
|
||||
if (e.target.closest("[data-fahrt]")) { fahrtOffen = !fahrtOffen; render(); return; }
|
||||
if (e.target.closest("[data-fahrtspeichern]")) {
|
||||
const datum = $("#neuFahrtDatum").value, start = $("#neuFahrtStart").value, dauer = parseInt($("#neuFahrtDauer").value, 10) || 0;
|
||||
const tsStart = new Date(`${datum}T${start}:00`);
|
||||
const tsEnd = new Date(tsStart.getTime() + dauer * 60000);
|
||||
serviceRufen("audi_dashboard_fahrt_manuell_anlegen", { ts_start: tsStart.toISOString(), ts_end: tsEnd.toISOString(), art: "privat" });
|
||||
serviceRufen("audi_dashboard_fahrt_manuell_anlegen", fahrtFormularWerte("neuFahrt"));
|
||||
fahrtOffen = false; render(); return;
|
||||
}
|
||||
if (e.target.closest("[data-fahrtbearbeiten]")) {
|
||||
fahrtBearbeitenOffen = !fahrtBearbeitenOffen; render(); return;
|
||||
}
|
||||
const fahrtAkt = e.target.closest("[data-fahrtaktualisieren]");
|
||||
if (fahrtAkt) {
|
||||
serviceRufen("audi_dashboard_fahrt_aktualisieren",
|
||||
{ trip_id: fahrtAkt.dataset.fahrtaktualisieren, ...fahrtFormularWerte("bearbFahrt") });
|
||||
fahrtBearbeitenOffen = false; render(); return;
|
||||
}
|
||||
if (e.target.closest("[data-tankspeichern]")) {
|
||||
const w = tankFormularWerte("neuTank");
|
||||
if (w.liter > 0 && w.kosten > 0) {
|
||||
@@ -3987,7 +4089,14 @@ function ereignisseVerdrahten() {
|
||||
const a = e.target.closest("[data-acc]");
|
||||
if (a) { const b = ROOT.getElementById(a.dataset.acc); a.setAttribute("aria-expanded", b.classList.toggle("on")); return; }
|
||||
const art = e.target.closest("[data-art]");
|
||||
if (art) { const tr = TRIPS.find((x) => x.trip_id === art.dataset.art); tr.art = tr.art === "arbeitsweg" ? "privat" : "arbeitsweg"; render(); return; }
|
||||
if (art) {
|
||||
const tr = TRIPS.find((x) => x.trip_id === art.dataset.art);
|
||||
tr.art = tr.art === "arbeitsweg" ? "privat" : "arbeitsweg";
|
||||
// Bis 2026-08-17 blieb das Umschalten rein lokal und war nach dem
|
||||
// naechsten Backend-Push wieder weg - jetzt wandert es mit.
|
||||
serviceRufen("audi_dashboard_fahrt_aktualisieren", { trip_id: tr.trip_id, art: tr.art });
|
||||
render(); return;
|
||||
}
|
||||
// Standort (Vollbild) - Karten-/Menü-Bedienung manipuliert SMAP/das Menü-
|
||||
// DOM direkt statt über render() (siehe Kopfkommentar Standort-Abschnitt).
|
||||
if (e.target.closest("[data-standort-umschalten]")) { standortMenuUmschalten(); return; }
|
||||
@@ -4209,8 +4318,15 @@ function belegErgebnisVerarbeiten(daten) {
|
||||
Deshalb jetzt ohne festes Abbruchlimit: nach der anfänglichen schnellen
|
||||
Serie (siehe NACHLADE_INTERVALL_MS) läuft es mit einem größeren, aber
|
||||
endlosen Intervall weiter, bis Daten da sind oder das Element abgehängt
|
||||
wird (disconnectedCallback stoppt hier nichts explizit, aber ein
|
||||
getimeter Aufruf ohne HASS/callWS bricht oben ohnehin ab). */
|
||||
wird (disconnectedCallback stoppt den Timer, connectedCallback stößt ihn
|
||||
über datenLaden() wieder an).
|
||||
|
||||
2026-08-17: Der Takt darf an keiner Stelle abreißen. Vorher stieg sowohl
|
||||
nachladeAnstossen() (bei fehlendem HASS/callWS) als auch datenLaden() (bei
|
||||
fehlendem HASS) still aus, ohne eine nächste Runde zu planen - danach half
|
||||
nur noch ein zufälliger hass-Push oder ein Klick ins Menü (go() ruft
|
||||
datenLaden() mit auf). Genau das war das gemeldete "Lädt … bis man auf
|
||||
einen Menüpunkt klickt". */
|
||||
const NACHGELADEN = {};
|
||||
let nachladeTimer = null;
|
||||
let nachladeVersuche = 0;
|
||||
@@ -4219,7 +4335,7 @@ const NACHLADE_LANGSAM_MS = 10000;
|
||||
const NACHLADE_SCHNELLE_VERSUCHE = 15;
|
||||
|
||||
function nachladeAnstossen() {
|
||||
if (nachladeTimer || !HASS || !HASS.callWS) return;
|
||||
if (nachladeTimer) return;
|
||||
nachladeVersuche++;
|
||||
// Erster Versuch fast sofort statt erst nach vollem Intervall - der
|
||||
// Wettlauf mit pyscripts beim_start() (siehe Kopfkommentar) ist meist
|
||||
@@ -4232,26 +4348,40 @@ function nachladeAnstossen() {
|
||||
: NACHLADE_LANGSAM_MS;
|
||||
nachladeTimer = setTimeout(async () => {
|
||||
nachladeTimer = null;
|
||||
try {
|
||||
const states = await HASS.callWS({ type: "get_states" });
|
||||
const finden = (id) => states.find((s) => s.entity_id === id);
|
||||
NACHGELADEN.profil = finden("pyscript.audi_dashboard_profil") || NACHGELADEN.profil;
|
||||
NACHGELADEN.fahrten = finden("pyscript.audi_dashboard_fahrten") || NACHGELADEN.fahrten;
|
||||
NACHGELADEN.tank = finden("pyscript.audi_dashboard_tankvorgaenge") || NACHGELADEN.tank;
|
||||
NACHGELADEN.status = finden("pyscript.audi_dashboard_fahrzeugstatus") || NACHGELADEN.status;
|
||||
NACHGELADEN.batt = finden("pyscript.audi_dashboard_batterieverlauf") || NACHGELADEN.batt;
|
||||
// Ohne diese Zeile bleibt der Setup-Katalog nach einem HA-Neustart leer,
|
||||
// bis zufaellig ein Push kommt - siehe setupBereit().
|
||||
NACHGELADEN.entitaeten = finden("pyscript.audi_dashboard_entitaeten") || NACHGELADEN.entitaeten;
|
||||
} catch (e) {
|
||||
// wird beim naechsten Versuch erneut probiert
|
||||
// HASS kann hier noch fehlen: panel_custom erzeugt das Element und setzt
|
||||
// hass nicht immer in dieser Reihenfolge. Frueher brach die Schleife in so
|
||||
// einem Fall oben ganz ab (Bedingung !HASS || !HASS.callWS) und wurde nie
|
||||
// wieder angestossen - genau der dauerhaft haengende "Lädt …"-Bildschirm.
|
||||
// Jetzt wird die Runde nur uebersprungen, der Takt laeuft weiter.
|
||||
if (HASS && HASS.callWS) {
|
||||
try {
|
||||
const states = await HASS.callWS({ type: "get_states" });
|
||||
const finden = (id) => states.find((s) => s.entity_id === id);
|
||||
NACHGELADEN.profil = finden("pyscript.audi_dashboard_profil") || NACHGELADEN.profil;
|
||||
NACHGELADEN.fahrten = finden("pyscript.audi_dashboard_fahrten") || NACHGELADEN.fahrten;
|
||||
NACHGELADEN.tank = finden("pyscript.audi_dashboard_tankvorgaenge") || NACHGELADEN.tank;
|
||||
NACHGELADEN.status = finden("pyscript.audi_dashboard_fahrzeugstatus") || NACHGELADEN.status;
|
||||
NACHGELADEN.batt = finden("pyscript.audi_dashboard_batterieverlauf") || NACHGELADEN.batt;
|
||||
// Ohne diese Zeile bleibt der Setup-Katalog nach einem HA-Neustart leer,
|
||||
// bis zufaellig ein Push kommt - siehe setupBereit().
|
||||
NACHGELADEN.entitaeten = finden("pyscript.audi_dashboard_entitaeten") || NACHGELADEN.entitaeten;
|
||||
} catch (e) {
|
||||
// wird beim naechsten Versuch erneut probiert
|
||||
}
|
||||
}
|
||||
datenLaden(true);
|
||||
// Rueckversicherung: datenLaden() stoesst die naechste Runde selbst an,
|
||||
// aber nur, wenn es nicht vorher aussteigt. Der Takt darf an keiner
|
||||
// Stelle abreissen, solange noch keine Daten da sind.
|
||||
if (!DATEN_GELADEN) nachladeAnstossen();
|
||||
}, verzoegerung);
|
||||
}
|
||||
|
||||
async function datenLaden(erzwingen) {
|
||||
if (!HASS) return;
|
||||
if (!HASS) {
|
||||
if (!DATEN_GELADEN) nachladeAnstossen();
|
||||
return;
|
||||
}
|
||||
const profilState = HASS.states["pyscript.audi_dashboard_profil"] || NACHGELADEN.profil;
|
||||
const fahrtenState = HASS.states["pyscript.audi_dashboard_fahrten"] || NACHGELADEN.fahrten;
|
||||
const tankState = HASS.states["pyscript.audi_dashboard_tankvorgaenge"] || NACHGELADEN.tank;
|
||||
@@ -4355,6 +4485,9 @@ class AudiDashboardPanel extends HTMLElement {
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
// Nachlade-Takt anhalten, solange das Panel nicht im Dokument haengt -
|
||||
// connectedCallback() stoesst ihn ueber datenLaden() wieder an.
|
||||
if (nachladeTimer) { clearTimeout(nachladeTimer); nachladeTimer = null; }
|
||||
// Alle drei Karten, nicht nur MAP: sonst bleibt beim Abhaengen des Panels
|
||||
// (Standort- oder Uebersichtsseite offen) eine Leaflet-Instanz samt ihrem
|
||||
// window-Resize-Horcher und laufenden Kachelabrufen am Leben.
|
||||
|
||||
@@ -68,11 +68,21 @@ html,body{height:100%}
|
||||
|
||||
/* ---------------- Navigationsleiste ---------------- */
|
||||
/* Sichere Bereiche des Geräterahmens: Statusleiste oben, Home-Indikator unten */
|
||||
.topbar{align-items:center;gap:4px;padding:56px 20px 4px}
|
||||
/* Linker Innenabstand = --seitenrand von main#view (16px), damit der Titel
|
||||
exakt auf der Kante der Kacheln darunter sitzt. */
|
||||
.topbar{align-items:center;gap:4px;padding:56px 20px 4px 16px}
|
||||
.head{display:flex;flex-direction:column}
|
||||
.eyebrow{display:none}
|
||||
.title{font-size:22px;font-weight:400;letter-spacing:-.01em;line-height:1.2}
|
||||
.back{width:34px;height:34px;margin-left:-8px;justify-content:center}
|
||||
/* Design-Audit 2026-08-13, auf allen Breiten nachgezogen 2026-08-17: .back ist
|
||||
auch inaktiv (opacity:0) noch ein Flex-Element plus Gap - auf Wurzelseiten
|
||||
ohne Zurueck-Pfeil schob das die Kopfzeile (.head/.title) sichtbar weiter
|
||||
nach rechts, als main#view darunter beginnt. Das negative margin-right hebt
|
||||
zusaetzlich den Gap auf, sonst blieben 4px Versatz. Auf Detailseiten
|
||||
(.back.on) bleibt der Platz bewusst reserviert, das ist dort ein echter,
|
||||
bedienbarer Knopf. */
|
||||
.back:not(.on){width:0;min-width:0;margin:0 -4px 0 0;overflow:hidden}
|
||||
/* Rot bleibt Akzent/destruktiv, nicht Navigationsfarbe (dieselbe Regel wie
|
||||
bei den Aktions-Buttons und dem aktiven Menü-Icon) - derselbe neutrale Ton
|
||||
wie die Kopfzeile selbst. */
|
||||
@@ -264,12 +274,6 @@ main#view{--seitenrand:16px;padding:0 var(--seitenrand) 30px;scroll-behavior:smo
|
||||
auf breiten Bildschirmen klaffte da eine sichtbare Luecke. */
|
||||
.topbar{grid-column:2;grid-row:1;padding:38px 44px 8px;max-width:860px}
|
||||
.phone:has(.back.on) .topbar{grid-column:2;grid-row:1;padding:34px 44px 8px;justify-content:flex-start;max-width:860px}
|
||||
/* Design-Audit 2026-08-13: .back ist auch inaktiv (opacity:0) noch ein
|
||||
44px breites Flex-Element plus Gap - auf Wurzelseiten ohne Zurueck-Pfeil
|
||||
schob das die Kopfzeile (.head/.title) sichtbar weiter nach rechts als
|
||||
main#view darunter beginnt. Auf Detailseiten (.back.on) bleibt der Platz
|
||||
bewusst reserviert, das ist dort ein echter, bedienbarer Knopf. */
|
||||
.back:not(.on){width:0;min-width:0;margin:0;overflow:hidden}
|
||||
.phone:has(.back.on) .head{align-items:flex-start}
|
||||
.phone:has(.back.on) .title{font-size:26px;text-align:left}
|
||||
.title{font-size:34px}
|
||||
@@ -304,8 +308,14 @@ main#view{--seitenrand:16px;padding:0 var(--seitenrand) 30px;scroll-behavior:smo
|
||||
.rings{display:none !important}
|
||||
|
||||
.duo{grid-template-columns:repeat(3,minmax(0,1fr))}
|
||||
.carfix,.bildbox.carfix{height:260px}
|
||||
.carfix.klein,.bildbox.carfix.klein{height:220px}
|
||||
/* Nur die Box selbst hoeher setzen, nicht jedes Element mit .carfix: die
|
||||
Klasse haengt auch am <img> und am Platzhalter-<div> (siehe
|
||||
bildMitPlatzhalter()). Der Platzhalter ist position:absolute;inset:0 -
|
||||
eine feste Hoehe schlaegt dort das bottom:0 und machte ihn 260px hoch in
|
||||
einer 132px hohen, overflow:hidden-Box (leerer Zustand), also sichtbar
|
||||
abgeschnitten. Bild und Platzhalter fuellen die Box ohnehin per 100%. */
|
||||
.bildbox.carfix{height:260px}
|
||||
.bildbox.carfix.klein{height:220px}
|
||||
.bildgrid{grid-template-columns:repeat(4,1fr)}
|
||||
.quad{gap:14px}
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version": 1786920185}
|
||||
{"version": 1787000000}
|
||||
|
||||
Reference in New Issue
Block a user