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
|
`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
|
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
|
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
|
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.
|
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
|
`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
|
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.
|
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) —
|
- [ ] 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
|
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`
|
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)
|
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
|
@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
|
"""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)
|
start_ts = datetime.datetime.fromisoformat(ts_start)
|
||||||
ende_ts = datetime.datetime.fromisoformat(ts_end)
|
ende_ts = datetime.datetime.fromisoformat(ts_end)
|
||||||
dauer_s = int((ende_ts - start_ts).total_seconds())
|
dauer_s = int((ende_ts - start_ts).total_seconds())
|
||||||
|
hand = _strecke_ergaenzen(_handfelder(start_ort, ziel_ort, odo_start, odo_end, distanz))
|
||||||
fahrt = {
|
fahrt = {
|
||||||
"trip_id": profil.neue_id("t"),
|
"trip_id": profil.neue_id("t"),
|
||||||
"ts_start": start_ts.isoformat(),
|
"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",
|
"status": "offen",
|
||||||
"edited_fields": [],
|
"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)
|
profil.fahrt_anhaengen(fahrt)
|
||||||
frontend_veroeffentlichung.fahrten_veroeffentlichen()
|
frontend_veroeffentlichung.fahrten_veroeffentlichen()
|
||||||
fahrtabschluss_logik.screening_durchfuehren(einstellungen.KM_SENSOR)
|
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
|
@service
|
||||||
def audi_dashboard_fahrt_loeschen(trip_id=None):
|
def audi_dashboard_fahrt_loeschen(trip_id=None):
|
||||||
"""Löscht eine Fahrt unwiderruflich (Löschfunktion in der Oberfläche,
|
"""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 und Motorhaube - keine Quelle mehr vorhanden.
|
||||||
HECKKLAPPE_SENSOR = ""
|
HECKKLAPPE_SENSOR = ""
|
||||||
HECKKLAPPENSCHLOSS_SENSOR = ""
|
|
||||||
HAUBE_SENSOR = ""
|
HAUBE_SENSOR = ""
|
||||||
|
|
||||||
# Vom Fahrzeug selbst gemeldete Service-Fälligkeit (ergänzt die App-eigene,
|
# Vom Fahrzeug selbst gemeldete Service-Fälligkeit (ergänzt die App-eigene,
|
||||||
|
|||||||
@@ -85,10 +85,6 @@ FELDER = [
|
|||||||
"hinweis": "\"aus\"/off = zu.",
|
"hinweis": "\"aus\"/off = zu.",
|
||||||
"domains": ["binary_sensor"], "device_classes": ["door", "opening"], "units": [], "liste": False, "pflicht": False,
|
"domains": ["binary_sensor"], "device_classes": ["door", "opening"], "units": [], "liste": False, "pflicht": False,
|
||||||
"stichworte": ["heckklappe", "tailgate", "kofferraum", "trunk"]},
|
"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",
|
{"key": "HAUBE_SENSOR", "label": "Motorhaube", "gruppe": "sicherheit",
|
||||||
"hinweis": "\"aus\"/off = zu.",
|
"hinweis": "\"aus\"/off = zu.",
|
||||||
"domains": ["binary_sensor"], "device_classes": ["door", "opening"], "units": [], "liste": False, "pflicht": False,
|
"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"})
|
eintraege.append({"label": f"Fenster {pos}", "ok": None if w is None else w == "off"})
|
||||||
for label, sensor in [
|
for label, sensor in [
|
||||||
("Heckklappe", einstellungen.HECKKLAPPE_SENSOR),
|
("Heckklappe", einstellungen.HECKKLAPPE_SENSOR),
|
||||||
("Heckklappenschloss", einstellungen.HECKKLAPPENSCHLOSS_SENSOR),
|
|
||||||
("Motorhaube", einstellungen.HAUBE_SENSOR),
|
("Motorhaube", einstellungen.HAUBE_SENSOR),
|
||||||
]:
|
]:
|
||||||
w = zustand_oder_none(sensor)
|
w = zustand_oder_none(sensor)
|
||||||
|
|||||||
@@ -138,16 +138,20 @@ def offene_fahrten():
|
|||||||
return sorted(offen, key=lambda f: f.get("ts_start", ""))
|
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
|
"""Ersetzt ausgewählte Felder eines Datensatzes anhand seiner ID und
|
||||||
schreibt den gesamten Bestand neu. Manuell geänderte Felder
|
schreibt den gesamten Bestand neu. Manuell geänderte Felder
|
||||||
(edited_fields, analog zu §7.7 Regel 3) werden dabei nie überschrieben.
|
(edited_fields, analog zu §7.7 Regel 3) werden dabei nie überschrieben.
|
||||||
Gemeinsame Grundlage für Fahrten und Tankvorgänge - beide Archive
|
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
|
geaendert = False
|
||||||
for d in zeilen:
|
for d in zeilen:
|
||||||
if d.get(id_feld) == id_wert:
|
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():
|
for feld, wert in aenderungen.items():
|
||||||
if feld not in geschuetzt:
|
if feld not in geschuetzt:
|
||||||
d[feld] = wert
|
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)
|
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):
|
def fahrt_loeschen(trip_id):
|
||||||
fahrten = fahrten_lesen()
|
fahrten = fahrten_lesen()
|
||||||
uebrig = [f for f in fahrten if f.get("trip_id") != trip_id]
|
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 };
|
return { ts: null, km: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
/* App-eigene Ölwechsel-Prognose, eigenständig statt über die allgemeine
|
/* App-eigene Service-Prognose, eigenständig statt über die allgemeine
|
||||||
tagesleistung()/prognose()-Mischrechnung (die für Ölwechsel zu träge ist,
|
tagesleistung()/prognose()-Mischrechnung (die dafür zu träge ist, weil sie
|
||||||
weil sie den langfristigen Durchschnitt seit dem allerersten Servicebuch-
|
den langfristigen Durchschnitt seit dem allerersten Servicebucheintrag mit
|
||||||
eintrag mit einfließen lässt): die Berechnung beginnt bewusst komplett neu
|
einfließen lässt): die Berechnung beginnt bewusst komplett neu ab dem Datum
|
||||||
ab dem Datum des letzten tatsächlich durchgeführten Ölwechsels.
|
der letzten tatsächlich durchgeführten Arbeit dieser Art.
|
||||||
|
|
||||||
Logik (wie vorgegeben): km und Tage seit dem letzten Ölwechsel ergeben die
|
Logik (wie vorgegeben): km und Tage seit diesem Eintrag ergeben die aktuelle
|
||||||
aktuelle Fahrleistung pro Tag; hochgerechnet auf die restlichen km bis zum
|
Fahrleistung pro Tag; hochgerechnet auf die restlichen km bis zum
|
||||||
eingestellten Intervall (Kundeneigene Einstellung, siehe CAR.oel) ergibt
|
eingestellten Intervall ergibt das ein Datum - gedeckelt auf spätestens das
|
||||||
das ein Datum - gedeckelt auf spätestens das Intervall-Zeitlimit (z. B. 1
|
Intervall-Zeitlimit (z. B. 1 Jahr) nach dem letzten Eintrag, falls seither
|
||||||
Jahr) nach dem letzten Ölwechsel, falls seither wenig gefahren wurde. */
|
wenig gefahren wurde.
|
||||||
function oelwechselPrognose() {
|
|
||||||
|
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;
|
if (!CAR.odoBekannt) return null;
|
||||||
const letzter = letzterEintrag("öl");
|
const letzter = letzterEintrag(stichwort);
|
||||||
if (!letzter) return null;
|
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 zielKm = letzter.km + intervallKm;
|
||||||
const restKm = Math.max(0, zielKm - CAR.odo);
|
const restKm = Math.max(0, zielKm - CAR.odo);
|
||||||
const kappDatum = monatePlus(letzter.datum, intervallMonate);
|
const kappDatum = monatePlus(letzter.datum, intervallMonate);
|
||||||
@@ -1151,13 +1151,13 @@ function oelwechselPrognose() {
|
|||||||
const tageSeit = (heute() - dat(letzter.datum)) / dTag;
|
const tageSeit = (heute() - dat(letzter.datum)) / dTag;
|
||||||
const kmSeit = CAR.odo - letzter.km;
|
const kmSeit = CAR.odo - letzter.km;
|
||||||
if (tageSeit <= 0 || kmSeit <= 0) {
|
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
|
// direkt danach) - ohne Rate keine seriöse Km-Prognose, also das
|
||||||
// Zeitlimit als einzig belastbare Grenze zeigen.
|
// Zeitlimit als einzig belastbare Grenze zeigen.
|
||||||
datum = kappDatum;
|
datum = kappDatum;
|
||||||
} else {
|
} else {
|
||||||
const kmProTagSeitOel = kmSeit / tageSeit;
|
const kmProTagSeither = kmSeit / tageSeit;
|
||||||
const tageBisZiel = restKm / kmProTagSeitOel;
|
const tageBisZiel = restKm / kmProTagSeither;
|
||||||
const berechnet = new Date(heute().getTime() + tageBisZiel * dTag);
|
const berechnet = new Date(heute().getTime() + tageBisZiel * dTag);
|
||||||
datum = berechnet < kappDatum ? berechnet : kappDatum;
|
datum = berechnet < kappDatum ? berechnet : kappDatum;
|
||||||
}
|
}
|
||||||
@@ -1165,6 +1165,27 @@ function oelwechselPrognose() {
|
|||||||
return { km: zielKm, restKm, datum };
|
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) {
|
function terminText(t) {
|
||||||
const restKm = t.km !== null && CAR.odoBekannt ? t.km - CAR.odo : null;
|
const restKm = t.km !== null && CAR.odoBekannt ? t.km - CAR.odo : null;
|
||||||
const restKmAbs = restKm !== null ? Math.abs(restKm) : 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
|
// (Sensor nicht verfügbar), fällt sie auf dieselbe App-eigene
|
||||||
// Servicebuch-Schätzung zurück wie Hauptuntersuchung.
|
// Servicebuch-Schätzung zurück wie Hauptuntersuchung.
|
||||||
const fm = t.art === "Hauptuntersuchung" ? { ts: null, km: null } : fahrzeugMeldungRoh(t.art);
|
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) {
|
if (fm.ts == null && fm.km == null) {
|
||||||
const x = terminText(t);
|
const x = terminText(t);
|
||||||
return `<div class="row" ${letzte}>
|
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 haupt = fm.ts ? dedat(new Date(fm.ts)) : "";
|
||||||
const neben = fm.km != null ? "in " + de(fm.km) + " km" : "";
|
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}>
|
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>`;
|
<dd>${haupt}${neben ? `<span style="display:block">${neben}</span>` : ""}</dd></div>`;
|
||||||
}).join("")}
|
}).join("")}
|
||||||
</dl></button>
|
</dl></button>
|
||||||
@@ -1628,11 +1652,12 @@ function vService() {
|
|||||||
const anzeige = iv.hersteller || iv.eigen;
|
const anzeige = iv.hersteller || iv.eigen;
|
||||||
const fm = fahrzeugMeldungRoh(t.art);
|
const fm = fahrzeugMeldungRoh(t.art);
|
||||||
const fmDatum = fm.ts ? dedat(new Date(fm.ts)) : null;
|
const fmDatum = fm.ts ? dedat(new Date(fm.ts)) : null;
|
||||||
// Forecast-Zeile nur bei Ölwechsel (Kundeneigenes Intervall aus
|
// Forecast-Zeile aus dem kundeneigenen Intervall in "Einrichten" mit
|
||||||
// "Einrichten", eigenständige Reset-nach-Service-Logik, siehe
|
// eigenständiger Reset-nach-Service-Logik (servicePrognose()): beim
|
||||||
// oelwechselPrognose()) - bei Inspektion und Hauptuntersuchung
|
// Ölwechsel mit Restkilometern, bei der Inspektion auf Wunsch nur mit
|
||||||
// bewusst keine Forecast-Zeile mehr.
|
// dem Datum. Hauptuntersuchung bleibt bewusst ohne.
|
||||||
const eigenOel = t.art === "Ölwechsel" ? oelwechselPrognose() : null;
|
const eigenOel = t.art === "Ölwechsel" ? oelwechselPrognose() : null;
|
||||||
|
const eigenInsp = t.art === "Inspektion" ? inspektionPrognose() : null;
|
||||||
return `<div class="row" ${letzte}>
|
return `<div class="row" ${letzte}>
|
||||||
<dt>${t.art}
|
<dt>${t.art}
|
||||||
${t.vereinbart ? `<br><span style="color:var(--red);font-size:11px">Werkstatt am ${dedat(new Date(t.vereinbart))}</span>` : ""}
|
${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 ? `
|
${fm.km != null || fmDatum ? `
|
||||||
<span class="werte">${fm.km != null ? de(fm.km) + " km" : ""}${fm.km != null && fmDatum ? " / " : ""}${fmDatum || ""}</span>
|
<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>` : ""}
|
${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>
|
<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>
|
</dd>
|
||||||
</div>`; }).join("")}
|
</div>`; }).join("")}
|
||||||
@@ -2440,17 +2467,69 @@ function vReifen() {
|
|||||||
${satz("winter")}`;
|
${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() {
|
function vTripsFormular() {
|
||||||
// eigene Funktion, damit sowohl der volle als auch der Leer-Zustand
|
// eigene Funktion, damit sowohl der volle als auch der Leer-Zustand
|
||||||
// (§11 Abnahmekriterium 1: eine Fahrt muss erfasst werden können) dasselbe
|
// (§11 Abnahmekriterium 1: eine Fahrt muss erfasst werden können) dasselbe
|
||||||
// Formular zeigen - vorher fehlte es im Leer-Zustand komplett, der Knopf
|
// Formular zeigen - vorher fehlte es im Leer-Zustand komplett, der Knopf
|
||||||
// wechselte zu "Abbrechen", aber es erschienen keine Felder.
|
// wechselte zu "Abbrechen", aber es erschienen keine Felder.
|
||||||
return `${fahrtOffen ? `<div class="tile"><span class="label">Neue Fahrt</span>
|
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>
|
${fahrtFelder("neuFahrt", null)}
|
||||||
<div class="feld"><label>Startzeit</label><input type="time" id="neuFahrtStart" value="08:00"></div>
|
<button class="aktion primaer" data-fahrtspeichern>Speichern</button></div>` : ""}
|
||||||
<div class="feld" style="border-bottom:0"><label>Dauer in Minuten</label><input type="number" id="neuFahrtDauer" placeholder="0"></div>
|
<button class="aktion ${fahrtOffen ? "" : "primaer"}" data-fahrt>${fahrtOffen ? "Abbrechen" : "Fahrt erfassen"}</button>`;
|
||||||
<button class="aktion" data-fahrtspeichern>Speichern</button></div>` : ""}
|
|
||||||
<button class="aktion primaer" data-fahrt>${fahrtOffen ? "Abbrechen" : "Fahrt erfassen"}</button>`;
|
|
||||||
}
|
}
|
||||||
function vTrips() {
|
function vTrips() {
|
||||||
const jahre = [...new Set(TRIPS.map((t) => new Date(t.ts_start).getFullYear()))].sort((a, b) => b - a);
|
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) {
|
function vTrip(id) {
|
||||||
const t = TRIPS.find((x) => x.trip_id === id);
|
const t = TRIPS.find((x) => x.trip_id === id);
|
||||||
if (!t) return `<div class="tile">Fahrt nicht gefunden</div>`;
|
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 `
|
return `
|
||||||
${mapBox()}
|
${mapBox()}
|
||||||
<div class="tile flat" style="margin:18px 0 14px">
|
<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>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>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>
|
<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>`;
|
<button class="aktion loeschen" data-tripdel="${t.trip_id}">Fahrt löschen</button>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2637,6 +2728,7 @@ function vStat() {
|
|||||||
let neuOffen = false;
|
let neuOffen = false;
|
||||||
let fahrtOffen = false;
|
let fahrtOffen = false;
|
||||||
let tankBearbeitenOffen = false;
|
let tankBearbeitenOffen = false;
|
||||||
|
let fahrtBearbeitenOffen = false;
|
||||||
// Ergebnis eines geparsten, noch nicht gespeicherten Belegs: { praefix, daten }
|
// Ergebnis eines geparsten, noch nicht gespeicherten Belegs: { praefix, daten }
|
||||||
// - überlagert in tankFelder() die Beleg-Felder des jeweils offenen Formulars.
|
// - überlagert in tankFelder() die Beleg-Felder des jeweils offenen Formulars.
|
||||||
let belegEntwurf = null;
|
let belegEntwurf = null;
|
||||||
@@ -3324,6 +3416,9 @@ const go = (name, id) => {
|
|||||||
entwurfIndex = null;
|
entwurfIndex = null;
|
||||||
}
|
}
|
||||||
sheet = 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
|
/* B3: echter Verlauf statt statischer ZURUECK-Tabelle. Ein Tabwechsel setzt
|
||||||
den Stapel zurück, weil Tabs Wurzeln sind, keine Ebenen. */
|
den Stapel zurück, weil Tabs Wurzeln sind, keine Ebenen. */
|
||||||
if (istTab(name)) routeStack = [];
|
if (istTab(name)) routeStack = [];
|
||||||
@@ -3333,6 +3428,7 @@ const go = (name, id) => {
|
|||||||
};
|
};
|
||||||
function zurueck() {
|
function zurueck() {
|
||||||
sheet = null;
|
sheet = null;
|
||||||
|
fahrtBearbeitenOffen = false;
|
||||||
const ziel = routeStack.pop() || { name: ZURUECK[route.name] || "home" };
|
const ziel = routeStack.pop() || { name: ZURUECK[route.name] || "home" };
|
||||||
navRichtung = "raus";
|
navRichtung = "raus";
|
||||||
route = ziel; render();
|
route = ziel; render();
|
||||||
@@ -3821,12 +3917,18 @@ function ereignisseVerdrahten() {
|
|||||||
}
|
}
|
||||||
if (e.target.closest("[data-fahrt]")) { fahrtOffen = !fahrtOffen; render(); return; }
|
if (e.target.closest("[data-fahrt]")) { fahrtOffen = !fahrtOffen; render(); return; }
|
||||||
if (e.target.closest("[data-fahrtspeichern]")) {
|
if (e.target.closest("[data-fahrtspeichern]")) {
|
||||||
const datum = $("#neuFahrtDatum").value, start = $("#neuFahrtStart").value, dauer = parseInt($("#neuFahrtDauer").value, 10) || 0;
|
serviceRufen("audi_dashboard_fahrt_manuell_anlegen", fahrtFormularWerte("neuFahrt"));
|
||||||
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" });
|
|
||||||
fahrtOffen = false; render(); return;
|
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]")) {
|
if (e.target.closest("[data-tankspeichern]")) {
|
||||||
const w = tankFormularWerte("neuTank");
|
const w = tankFormularWerte("neuTank");
|
||||||
if (w.liter > 0 && w.kosten > 0) {
|
if (w.liter > 0 && w.kosten > 0) {
|
||||||
@@ -3987,7 +4089,14 @@ function ereignisseVerdrahten() {
|
|||||||
const a = e.target.closest("[data-acc]");
|
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; }
|
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]");
|
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ü-
|
// Standort (Vollbild) - Karten-/Menü-Bedienung manipuliert SMAP/das Menü-
|
||||||
// DOM direkt statt über render() (siehe Kopfkommentar Standort-Abschnitt).
|
// DOM direkt statt über render() (siehe Kopfkommentar Standort-Abschnitt).
|
||||||
if (e.target.closest("[data-standort-umschalten]")) { standortMenuUmschalten(); return; }
|
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
|
Deshalb jetzt ohne festes Abbruchlimit: nach der anfänglichen schnellen
|
||||||
Serie (siehe NACHLADE_INTERVALL_MS) läuft es mit einem größeren, aber
|
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
|
endlosen Intervall weiter, bis Daten da sind oder das Element abgehängt
|
||||||
wird (disconnectedCallback stoppt hier nichts explizit, aber ein
|
wird (disconnectedCallback stoppt den Timer, connectedCallback stößt ihn
|
||||||
getimeter Aufruf ohne HASS/callWS bricht oben ohnehin ab). */
|
ü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 = {};
|
const NACHGELADEN = {};
|
||||||
let nachladeTimer = null;
|
let nachladeTimer = null;
|
||||||
let nachladeVersuche = 0;
|
let nachladeVersuche = 0;
|
||||||
@@ -4219,7 +4335,7 @@ const NACHLADE_LANGSAM_MS = 10000;
|
|||||||
const NACHLADE_SCHNELLE_VERSUCHE = 15;
|
const NACHLADE_SCHNELLE_VERSUCHE = 15;
|
||||||
|
|
||||||
function nachladeAnstossen() {
|
function nachladeAnstossen() {
|
||||||
if (nachladeTimer || !HASS || !HASS.callWS) return;
|
if (nachladeTimer) return;
|
||||||
nachladeVersuche++;
|
nachladeVersuche++;
|
||||||
// Erster Versuch fast sofort statt erst nach vollem Intervall - der
|
// Erster Versuch fast sofort statt erst nach vollem Intervall - der
|
||||||
// Wettlauf mit pyscripts beim_start() (siehe Kopfkommentar) ist meist
|
// Wettlauf mit pyscripts beim_start() (siehe Kopfkommentar) ist meist
|
||||||
@@ -4232,26 +4348,40 @@ function nachladeAnstossen() {
|
|||||||
: NACHLADE_LANGSAM_MS;
|
: NACHLADE_LANGSAM_MS;
|
||||||
nachladeTimer = setTimeout(async () => {
|
nachladeTimer = setTimeout(async () => {
|
||||||
nachladeTimer = null;
|
nachladeTimer = null;
|
||||||
try {
|
// HASS kann hier noch fehlen: panel_custom erzeugt das Element und setzt
|
||||||
const states = await HASS.callWS({ type: "get_states" });
|
// hass nicht immer in dieser Reihenfolge. Frueher brach die Schleife in so
|
||||||
const finden = (id) => states.find((s) => s.entity_id === id);
|
// einem Fall oben ganz ab (Bedingung !HASS || !HASS.callWS) und wurde nie
|
||||||
NACHGELADEN.profil = finden("pyscript.audi_dashboard_profil") || NACHGELADEN.profil;
|
// wieder angestossen - genau der dauerhaft haengende "Lädt …"-Bildschirm.
|
||||||
NACHGELADEN.fahrten = finden("pyscript.audi_dashboard_fahrten") || NACHGELADEN.fahrten;
|
// Jetzt wird die Runde nur uebersprungen, der Takt laeuft weiter.
|
||||||
NACHGELADEN.tank = finden("pyscript.audi_dashboard_tankvorgaenge") || NACHGELADEN.tank;
|
if (HASS && HASS.callWS) {
|
||||||
NACHGELADEN.status = finden("pyscript.audi_dashboard_fahrzeugstatus") || NACHGELADEN.status;
|
try {
|
||||||
NACHGELADEN.batt = finden("pyscript.audi_dashboard_batterieverlauf") || NACHGELADEN.batt;
|
const states = await HASS.callWS({ type: "get_states" });
|
||||||
// Ohne diese Zeile bleibt der Setup-Katalog nach einem HA-Neustart leer,
|
const finden = (id) => states.find((s) => s.entity_id === id);
|
||||||
// bis zufaellig ein Push kommt - siehe setupBereit().
|
NACHGELADEN.profil = finden("pyscript.audi_dashboard_profil") || NACHGELADEN.profil;
|
||||||
NACHGELADEN.entitaeten = finden("pyscript.audi_dashboard_entitaeten") || NACHGELADEN.entitaeten;
|
NACHGELADEN.fahrten = finden("pyscript.audi_dashboard_fahrten") || NACHGELADEN.fahrten;
|
||||||
} catch (e) {
|
NACHGELADEN.tank = finden("pyscript.audi_dashboard_tankvorgaenge") || NACHGELADEN.tank;
|
||||||
// wird beim naechsten Versuch erneut probiert
|
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);
|
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);
|
}, verzoegerung);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function datenLaden(erzwingen) {
|
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 profilState = HASS.states["pyscript.audi_dashboard_profil"] || NACHGELADEN.profil;
|
||||||
const fahrtenState = HASS.states["pyscript.audi_dashboard_fahrten"] || NACHGELADEN.fahrten;
|
const fahrtenState = HASS.states["pyscript.audi_dashboard_fahrten"] || NACHGELADEN.fahrten;
|
||||||
const tankState = HASS.states["pyscript.audi_dashboard_tankvorgaenge"] || NACHGELADEN.tank;
|
const tankState = HASS.states["pyscript.audi_dashboard_tankvorgaenge"] || NACHGELADEN.tank;
|
||||||
@@ -4355,6 +4485,9 @@ class AudiDashboardPanel extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
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
|
// Alle drei Karten, nicht nur MAP: sonst bleibt beim Abhaengen des Panels
|
||||||
// (Standort- oder Uebersichtsseite offen) eine Leaflet-Instanz samt ihrem
|
// (Standort- oder Uebersichtsseite offen) eine Leaflet-Instanz samt ihrem
|
||||||
// window-Resize-Horcher und laufenden Kachelabrufen am Leben.
|
// window-Resize-Horcher und laufenden Kachelabrufen am Leben.
|
||||||
|
|||||||
@@ -68,11 +68,21 @@ html,body{height:100%}
|
|||||||
|
|
||||||
/* ---------------- Navigationsleiste ---------------- */
|
/* ---------------- Navigationsleiste ---------------- */
|
||||||
/* Sichere Bereiche des Geräterahmens: Statusleiste oben, Home-Indikator unten */
|
/* 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}
|
.head{display:flex;flex-direction:column}
|
||||||
.eyebrow{display:none}
|
.eyebrow{display:none}
|
||||||
.title{font-size:22px;font-weight:400;letter-spacing:-.01em;line-height:1.2}
|
.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}
|
.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
|
/* Rot bleibt Akzent/destruktiv, nicht Navigationsfarbe (dieselbe Regel wie
|
||||||
bei den Aktions-Buttons und dem aktiven Menü-Icon) - derselbe neutrale Ton
|
bei den Aktions-Buttons und dem aktiven Menü-Icon) - derselbe neutrale Ton
|
||||||
wie die Kopfzeile selbst. */
|
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. */
|
auf breiten Bildschirmen klaffte da eine sichtbare Luecke. */
|
||||||
.topbar{grid-column:2;grid-row:1;padding:38px 44px 8px;max-width:860px}
|
.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}
|
.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) .head{align-items:flex-start}
|
||||||
.phone:has(.back.on) .title{font-size:26px;text-align:left}
|
.phone:has(.back.on) .title{font-size:26px;text-align:left}
|
||||||
.title{font-size:34px}
|
.title{font-size:34px}
|
||||||
@@ -304,8 +308,14 @@ main#view{--seitenrand:16px;padding:0 var(--seitenrand) 30px;scroll-behavior:smo
|
|||||||
.rings{display:none !important}
|
.rings{display:none !important}
|
||||||
|
|
||||||
.duo{grid-template-columns:repeat(3,minmax(0,1fr))}
|
.duo{grid-template-columns:repeat(3,minmax(0,1fr))}
|
||||||
.carfix,.bildbox.carfix{height:260px}
|
/* Nur die Box selbst hoeher setzen, nicht jedes Element mit .carfix: die
|
||||||
.carfix.klein,.bildbox.carfix.klein{height:220px}
|
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)}
|
.bildgrid{grid-template-columns:repeat(4,1fr)}
|
||||||
.quad{gap:14px}
|
.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)
|
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
|
@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
|
"""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)
|
start_ts = datetime.datetime.fromisoformat(ts_start)
|
||||||
ende_ts = datetime.datetime.fromisoformat(ts_end)
|
ende_ts = datetime.datetime.fromisoformat(ts_end)
|
||||||
dauer_s = int((ende_ts - start_ts).total_seconds())
|
dauer_s = int((ende_ts - start_ts).total_seconds())
|
||||||
|
hand = _strecke_ergaenzen(_handfelder(start_ort, ziel_ort, odo_start, odo_end, distanz))
|
||||||
fahrt = {
|
fahrt = {
|
||||||
"trip_id": profil.neue_id("t"),
|
"trip_id": profil.neue_id("t"),
|
||||||
"ts_start": start_ts.isoformat(),
|
"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",
|
"status": "offen",
|
||||||
"edited_fields": [],
|
"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)
|
profil.fahrt_anhaengen(fahrt)
|
||||||
frontend_veroeffentlichung.fahrten_veroeffentlichen()
|
frontend_veroeffentlichung.fahrten_veroeffentlichen()
|
||||||
fahrtabschluss_logik.screening_durchfuehren(einstellungen.KM_SENSOR)
|
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
|
@service
|
||||||
def audi_dashboard_fahrt_loeschen(trip_id=None):
|
def audi_dashboard_fahrt_loeschen(trip_id=None):
|
||||||
"""Löscht eine Fahrt unwiderruflich (Löschfunktion in der Oberfläche,
|
"""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 und Motorhaube - keine Quelle mehr vorhanden.
|
||||||
HECKKLAPPE_SENSOR = ""
|
HECKKLAPPE_SENSOR = ""
|
||||||
HECKKLAPPENSCHLOSS_SENSOR = ""
|
|
||||||
HAUBE_SENSOR = ""
|
HAUBE_SENSOR = ""
|
||||||
|
|
||||||
# Vom Fahrzeug selbst gemeldete Service-Fälligkeit (ergänzt die App-eigene,
|
# Vom Fahrzeug selbst gemeldete Service-Fälligkeit (ergänzt die App-eigene,
|
||||||
|
|||||||
@@ -85,10 +85,6 @@ FELDER = [
|
|||||||
"hinweis": "\"aus\"/off = zu.",
|
"hinweis": "\"aus\"/off = zu.",
|
||||||
"domains": ["binary_sensor"], "device_classes": ["door", "opening"], "units": [], "liste": False, "pflicht": False,
|
"domains": ["binary_sensor"], "device_classes": ["door", "opening"], "units": [], "liste": False, "pflicht": False,
|
||||||
"stichworte": ["heckklappe", "tailgate", "kofferraum", "trunk"]},
|
"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",
|
{"key": "HAUBE_SENSOR", "label": "Motorhaube", "gruppe": "sicherheit",
|
||||||
"hinweis": "\"aus\"/off = zu.",
|
"hinweis": "\"aus\"/off = zu.",
|
||||||
"domains": ["binary_sensor"], "device_classes": ["door", "opening"], "units": [], "liste": False, "pflicht": False,
|
"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"})
|
eintraege.append({"label": f"Fenster {pos}", "ok": None if w is None else w == "off"})
|
||||||
for label, sensor in [
|
for label, sensor in [
|
||||||
("Heckklappe", einstellungen.HECKKLAPPE_SENSOR),
|
("Heckklappe", einstellungen.HECKKLAPPE_SENSOR),
|
||||||
("Heckklappenschloss", einstellungen.HECKKLAPPENSCHLOSS_SENSOR),
|
|
||||||
("Motorhaube", einstellungen.HAUBE_SENSOR),
|
("Motorhaube", einstellungen.HAUBE_SENSOR),
|
||||||
]:
|
]:
|
||||||
w = zustand_oder_none(sensor)
|
w = zustand_oder_none(sensor)
|
||||||
|
|||||||
@@ -138,16 +138,20 @@ def offene_fahrten():
|
|||||||
return sorted(offen, key=lambda f: f.get("ts_start", ""))
|
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
|
"""Ersetzt ausgewählte Felder eines Datensatzes anhand seiner ID und
|
||||||
schreibt den gesamten Bestand neu. Manuell geänderte Felder
|
schreibt den gesamten Bestand neu. Manuell geänderte Felder
|
||||||
(edited_fields, analog zu §7.7 Regel 3) werden dabei nie überschrieben.
|
(edited_fields, analog zu §7.7 Regel 3) werden dabei nie überschrieben.
|
||||||
Gemeinsame Grundlage für Fahrten und Tankvorgänge - beide Archive
|
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
|
geaendert = False
|
||||||
for d in zeilen:
|
for d in zeilen:
|
||||||
if d.get(id_feld) == id_wert:
|
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():
|
for feld, wert in aenderungen.items():
|
||||||
if feld not in geschuetzt:
|
if feld not in geschuetzt:
|
||||||
d[feld] = wert
|
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)
|
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):
|
def fahrt_loeschen(trip_id):
|
||||||
fahrten = fahrten_lesen()
|
fahrten = fahrten_lesen()
|
||||||
uebrig = [f for f in fahrten if f.get("trip_id") != trip_id]
|
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 };
|
return { ts: null, km: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
/* App-eigene Ölwechsel-Prognose, eigenständig statt über die allgemeine
|
/* App-eigene Service-Prognose, eigenständig statt über die allgemeine
|
||||||
tagesleistung()/prognose()-Mischrechnung (die für Ölwechsel zu träge ist,
|
tagesleistung()/prognose()-Mischrechnung (die dafür zu träge ist, weil sie
|
||||||
weil sie den langfristigen Durchschnitt seit dem allerersten Servicebuch-
|
den langfristigen Durchschnitt seit dem allerersten Servicebucheintrag mit
|
||||||
eintrag mit einfließen lässt): die Berechnung beginnt bewusst komplett neu
|
einfließen lässt): die Berechnung beginnt bewusst komplett neu ab dem Datum
|
||||||
ab dem Datum des letzten tatsächlich durchgeführten Ölwechsels.
|
der letzten tatsächlich durchgeführten Arbeit dieser Art.
|
||||||
|
|
||||||
Logik (wie vorgegeben): km und Tage seit dem letzten Ölwechsel ergeben die
|
Logik (wie vorgegeben): km und Tage seit diesem Eintrag ergeben die aktuelle
|
||||||
aktuelle Fahrleistung pro Tag; hochgerechnet auf die restlichen km bis zum
|
Fahrleistung pro Tag; hochgerechnet auf die restlichen km bis zum
|
||||||
eingestellten Intervall (Kundeneigene Einstellung, siehe CAR.oel) ergibt
|
eingestellten Intervall ergibt das ein Datum - gedeckelt auf spätestens das
|
||||||
das ein Datum - gedeckelt auf spätestens das Intervall-Zeitlimit (z. B. 1
|
Intervall-Zeitlimit (z. B. 1 Jahr) nach dem letzten Eintrag, falls seither
|
||||||
Jahr) nach dem letzten Ölwechsel, falls seither wenig gefahren wurde. */
|
wenig gefahren wurde.
|
||||||
function oelwechselPrognose() {
|
|
||||||
|
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;
|
if (!CAR.odoBekannt) return null;
|
||||||
const letzter = letzterEintrag("öl");
|
const letzter = letzterEintrag(stichwort);
|
||||||
if (!letzter) return null;
|
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 zielKm = letzter.km + intervallKm;
|
||||||
const restKm = Math.max(0, zielKm - CAR.odo);
|
const restKm = Math.max(0, zielKm - CAR.odo);
|
||||||
const kappDatum = monatePlus(letzter.datum, intervallMonate);
|
const kappDatum = monatePlus(letzter.datum, intervallMonate);
|
||||||
@@ -1151,13 +1151,13 @@ function oelwechselPrognose() {
|
|||||||
const tageSeit = (heute() - dat(letzter.datum)) / dTag;
|
const tageSeit = (heute() - dat(letzter.datum)) / dTag;
|
||||||
const kmSeit = CAR.odo - letzter.km;
|
const kmSeit = CAR.odo - letzter.km;
|
||||||
if (tageSeit <= 0 || kmSeit <= 0) {
|
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
|
// direkt danach) - ohne Rate keine seriöse Km-Prognose, also das
|
||||||
// Zeitlimit als einzig belastbare Grenze zeigen.
|
// Zeitlimit als einzig belastbare Grenze zeigen.
|
||||||
datum = kappDatum;
|
datum = kappDatum;
|
||||||
} else {
|
} else {
|
||||||
const kmProTagSeitOel = kmSeit / tageSeit;
|
const kmProTagSeither = kmSeit / tageSeit;
|
||||||
const tageBisZiel = restKm / kmProTagSeitOel;
|
const tageBisZiel = restKm / kmProTagSeither;
|
||||||
const berechnet = new Date(heute().getTime() + tageBisZiel * dTag);
|
const berechnet = new Date(heute().getTime() + tageBisZiel * dTag);
|
||||||
datum = berechnet < kappDatum ? berechnet : kappDatum;
|
datum = berechnet < kappDatum ? berechnet : kappDatum;
|
||||||
}
|
}
|
||||||
@@ -1165,6 +1165,27 @@ function oelwechselPrognose() {
|
|||||||
return { km: zielKm, restKm, datum };
|
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) {
|
function terminText(t) {
|
||||||
const restKm = t.km !== null && CAR.odoBekannt ? t.km - CAR.odo : null;
|
const restKm = t.km !== null && CAR.odoBekannt ? t.km - CAR.odo : null;
|
||||||
const restKmAbs = restKm !== null ? Math.abs(restKm) : 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
|
// (Sensor nicht verfügbar), fällt sie auf dieselbe App-eigene
|
||||||
// Servicebuch-Schätzung zurück wie Hauptuntersuchung.
|
// Servicebuch-Schätzung zurück wie Hauptuntersuchung.
|
||||||
const fm = t.art === "Hauptuntersuchung" ? { ts: null, km: null } : fahrzeugMeldungRoh(t.art);
|
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) {
|
if (fm.ts == null && fm.km == null) {
|
||||||
const x = terminText(t);
|
const x = terminText(t);
|
||||||
return `<div class="row" ${letzte}>
|
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 haupt = fm.ts ? dedat(new Date(fm.ts)) : "";
|
||||||
const neben = fm.km != null ? "in " + de(fm.km) + " km" : "";
|
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}>
|
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>`;
|
<dd>${haupt}${neben ? `<span style="display:block">${neben}</span>` : ""}</dd></div>`;
|
||||||
}).join("")}
|
}).join("")}
|
||||||
</dl></button>
|
</dl></button>
|
||||||
@@ -1628,11 +1652,12 @@ function vService() {
|
|||||||
const anzeige = iv.hersteller || iv.eigen;
|
const anzeige = iv.hersteller || iv.eigen;
|
||||||
const fm = fahrzeugMeldungRoh(t.art);
|
const fm = fahrzeugMeldungRoh(t.art);
|
||||||
const fmDatum = fm.ts ? dedat(new Date(fm.ts)) : null;
|
const fmDatum = fm.ts ? dedat(new Date(fm.ts)) : null;
|
||||||
// Forecast-Zeile nur bei Ölwechsel (Kundeneigenes Intervall aus
|
// Forecast-Zeile aus dem kundeneigenen Intervall in "Einrichten" mit
|
||||||
// "Einrichten", eigenständige Reset-nach-Service-Logik, siehe
|
// eigenständiger Reset-nach-Service-Logik (servicePrognose()): beim
|
||||||
// oelwechselPrognose()) - bei Inspektion und Hauptuntersuchung
|
// Ölwechsel mit Restkilometern, bei der Inspektion auf Wunsch nur mit
|
||||||
// bewusst keine Forecast-Zeile mehr.
|
// dem Datum. Hauptuntersuchung bleibt bewusst ohne.
|
||||||
const eigenOel = t.art === "Ölwechsel" ? oelwechselPrognose() : null;
|
const eigenOel = t.art === "Ölwechsel" ? oelwechselPrognose() : null;
|
||||||
|
const eigenInsp = t.art === "Inspektion" ? inspektionPrognose() : null;
|
||||||
return `<div class="row" ${letzte}>
|
return `<div class="row" ${letzte}>
|
||||||
<dt>${t.art}
|
<dt>${t.art}
|
||||||
${t.vereinbart ? `<br><span style="color:var(--red);font-size:11px">Werkstatt am ${dedat(new Date(t.vereinbart))}</span>` : ""}
|
${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 ? `
|
${fm.km != null || fmDatum ? `
|
||||||
<span class="werte">${fm.km != null ? de(fm.km) + " km" : ""}${fm.km != null && fmDatum ? " / " : ""}${fmDatum || ""}</span>
|
<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>` : ""}
|
${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>
|
<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>
|
</dd>
|
||||||
</div>`; }).join("")}
|
</div>`; }).join("")}
|
||||||
@@ -2440,17 +2467,69 @@ function vReifen() {
|
|||||||
${satz("winter")}`;
|
${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() {
|
function vTripsFormular() {
|
||||||
// eigene Funktion, damit sowohl der volle als auch der Leer-Zustand
|
// eigene Funktion, damit sowohl der volle als auch der Leer-Zustand
|
||||||
// (§11 Abnahmekriterium 1: eine Fahrt muss erfasst werden können) dasselbe
|
// (§11 Abnahmekriterium 1: eine Fahrt muss erfasst werden können) dasselbe
|
||||||
// Formular zeigen - vorher fehlte es im Leer-Zustand komplett, der Knopf
|
// Formular zeigen - vorher fehlte es im Leer-Zustand komplett, der Knopf
|
||||||
// wechselte zu "Abbrechen", aber es erschienen keine Felder.
|
// wechselte zu "Abbrechen", aber es erschienen keine Felder.
|
||||||
return `${fahrtOffen ? `<div class="tile"><span class="label">Neue Fahrt</span>
|
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>
|
${fahrtFelder("neuFahrt", null)}
|
||||||
<div class="feld"><label>Startzeit</label><input type="time" id="neuFahrtStart" value="08:00"></div>
|
<button class="aktion primaer" data-fahrtspeichern>Speichern</button></div>` : ""}
|
||||||
<div class="feld" style="border-bottom:0"><label>Dauer in Minuten</label><input type="number" id="neuFahrtDauer" placeholder="0"></div>
|
<button class="aktion ${fahrtOffen ? "" : "primaer"}" data-fahrt>${fahrtOffen ? "Abbrechen" : "Fahrt erfassen"}</button>`;
|
||||||
<button class="aktion" data-fahrtspeichern>Speichern</button></div>` : ""}
|
|
||||||
<button class="aktion primaer" data-fahrt>${fahrtOffen ? "Abbrechen" : "Fahrt erfassen"}</button>`;
|
|
||||||
}
|
}
|
||||||
function vTrips() {
|
function vTrips() {
|
||||||
const jahre = [...new Set(TRIPS.map((t) => new Date(t.ts_start).getFullYear()))].sort((a, b) => b - a);
|
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) {
|
function vTrip(id) {
|
||||||
const t = TRIPS.find((x) => x.trip_id === id);
|
const t = TRIPS.find((x) => x.trip_id === id);
|
||||||
if (!t) return `<div class="tile">Fahrt nicht gefunden</div>`;
|
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 `
|
return `
|
||||||
${mapBox()}
|
${mapBox()}
|
||||||
<div class="tile flat" style="margin:18px 0 14px">
|
<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>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>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>
|
<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>`;
|
<button class="aktion loeschen" data-tripdel="${t.trip_id}">Fahrt löschen</button>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2637,6 +2728,7 @@ function vStat() {
|
|||||||
let neuOffen = false;
|
let neuOffen = false;
|
||||||
let fahrtOffen = false;
|
let fahrtOffen = false;
|
||||||
let tankBearbeitenOffen = false;
|
let tankBearbeitenOffen = false;
|
||||||
|
let fahrtBearbeitenOffen = false;
|
||||||
// Ergebnis eines geparsten, noch nicht gespeicherten Belegs: { praefix, daten }
|
// Ergebnis eines geparsten, noch nicht gespeicherten Belegs: { praefix, daten }
|
||||||
// - überlagert in tankFelder() die Beleg-Felder des jeweils offenen Formulars.
|
// - überlagert in tankFelder() die Beleg-Felder des jeweils offenen Formulars.
|
||||||
let belegEntwurf = null;
|
let belegEntwurf = null;
|
||||||
@@ -3324,6 +3416,9 @@ const go = (name, id) => {
|
|||||||
entwurfIndex = null;
|
entwurfIndex = null;
|
||||||
}
|
}
|
||||||
sheet = 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
|
/* B3: echter Verlauf statt statischer ZURUECK-Tabelle. Ein Tabwechsel setzt
|
||||||
den Stapel zurück, weil Tabs Wurzeln sind, keine Ebenen. */
|
den Stapel zurück, weil Tabs Wurzeln sind, keine Ebenen. */
|
||||||
if (istTab(name)) routeStack = [];
|
if (istTab(name)) routeStack = [];
|
||||||
@@ -3333,6 +3428,7 @@ const go = (name, id) => {
|
|||||||
};
|
};
|
||||||
function zurueck() {
|
function zurueck() {
|
||||||
sheet = null;
|
sheet = null;
|
||||||
|
fahrtBearbeitenOffen = false;
|
||||||
const ziel = routeStack.pop() || { name: ZURUECK[route.name] || "home" };
|
const ziel = routeStack.pop() || { name: ZURUECK[route.name] || "home" };
|
||||||
navRichtung = "raus";
|
navRichtung = "raus";
|
||||||
route = ziel; render();
|
route = ziel; render();
|
||||||
@@ -3821,12 +3917,18 @@ function ereignisseVerdrahten() {
|
|||||||
}
|
}
|
||||||
if (e.target.closest("[data-fahrt]")) { fahrtOffen = !fahrtOffen; render(); return; }
|
if (e.target.closest("[data-fahrt]")) { fahrtOffen = !fahrtOffen; render(); return; }
|
||||||
if (e.target.closest("[data-fahrtspeichern]")) {
|
if (e.target.closest("[data-fahrtspeichern]")) {
|
||||||
const datum = $("#neuFahrtDatum").value, start = $("#neuFahrtStart").value, dauer = parseInt($("#neuFahrtDauer").value, 10) || 0;
|
serviceRufen("audi_dashboard_fahrt_manuell_anlegen", fahrtFormularWerte("neuFahrt"));
|
||||||
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" });
|
|
||||||
fahrtOffen = false; render(); return;
|
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]")) {
|
if (e.target.closest("[data-tankspeichern]")) {
|
||||||
const w = tankFormularWerte("neuTank");
|
const w = tankFormularWerte("neuTank");
|
||||||
if (w.liter > 0 && w.kosten > 0) {
|
if (w.liter > 0 && w.kosten > 0) {
|
||||||
@@ -3987,7 +4089,14 @@ function ereignisseVerdrahten() {
|
|||||||
const a = e.target.closest("[data-acc]");
|
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; }
|
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]");
|
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ü-
|
// Standort (Vollbild) - Karten-/Menü-Bedienung manipuliert SMAP/das Menü-
|
||||||
// DOM direkt statt über render() (siehe Kopfkommentar Standort-Abschnitt).
|
// DOM direkt statt über render() (siehe Kopfkommentar Standort-Abschnitt).
|
||||||
if (e.target.closest("[data-standort-umschalten]")) { standortMenuUmschalten(); return; }
|
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
|
Deshalb jetzt ohne festes Abbruchlimit: nach der anfänglichen schnellen
|
||||||
Serie (siehe NACHLADE_INTERVALL_MS) läuft es mit einem größeren, aber
|
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
|
endlosen Intervall weiter, bis Daten da sind oder das Element abgehängt
|
||||||
wird (disconnectedCallback stoppt hier nichts explizit, aber ein
|
wird (disconnectedCallback stoppt den Timer, connectedCallback stößt ihn
|
||||||
getimeter Aufruf ohne HASS/callWS bricht oben ohnehin ab). */
|
ü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 = {};
|
const NACHGELADEN = {};
|
||||||
let nachladeTimer = null;
|
let nachladeTimer = null;
|
||||||
let nachladeVersuche = 0;
|
let nachladeVersuche = 0;
|
||||||
@@ -4219,7 +4335,7 @@ const NACHLADE_LANGSAM_MS = 10000;
|
|||||||
const NACHLADE_SCHNELLE_VERSUCHE = 15;
|
const NACHLADE_SCHNELLE_VERSUCHE = 15;
|
||||||
|
|
||||||
function nachladeAnstossen() {
|
function nachladeAnstossen() {
|
||||||
if (nachladeTimer || !HASS || !HASS.callWS) return;
|
if (nachladeTimer) return;
|
||||||
nachladeVersuche++;
|
nachladeVersuche++;
|
||||||
// Erster Versuch fast sofort statt erst nach vollem Intervall - der
|
// Erster Versuch fast sofort statt erst nach vollem Intervall - der
|
||||||
// Wettlauf mit pyscripts beim_start() (siehe Kopfkommentar) ist meist
|
// Wettlauf mit pyscripts beim_start() (siehe Kopfkommentar) ist meist
|
||||||
@@ -4232,26 +4348,40 @@ function nachladeAnstossen() {
|
|||||||
: NACHLADE_LANGSAM_MS;
|
: NACHLADE_LANGSAM_MS;
|
||||||
nachladeTimer = setTimeout(async () => {
|
nachladeTimer = setTimeout(async () => {
|
||||||
nachladeTimer = null;
|
nachladeTimer = null;
|
||||||
try {
|
// HASS kann hier noch fehlen: panel_custom erzeugt das Element und setzt
|
||||||
const states = await HASS.callWS({ type: "get_states" });
|
// hass nicht immer in dieser Reihenfolge. Frueher brach die Schleife in so
|
||||||
const finden = (id) => states.find((s) => s.entity_id === id);
|
// einem Fall oben ganz ab (Bedingung !HASS || !HASS.callWS) und wurde nie
|
||||||
NACHGELADEN.profil = finden("pyscript.audi_dashboard_profil") || NACHGELADEN.profil;
|
// wieder angestossen - genau der dauerhaft haengende "Lädt …"-Bildschirm.
|
||||||
NACHGELADEN.fahrten = finden("pyscript.audi_dashboard_fahrten") || NACHGELADEN.fahrten;
|
// Jetzt wird die Runde nur uebersprungen, der Takt laeuft weiter.
|
||||||
NACHGELADEN.tank = finden("pyscript.audi_dashboard_tankvorgaenge") || NACHGELADEN.tank;
|
if (HASS && HASS.callWS) {
|
||||||
NACHGELADEN.status = finden("pyscript.audi_dashboard_fahrzeugstatus") || NACHGELADEN.status;
|
try {
|
||||||
NACHGELADEN.batt = finden("pyscript.audi_dashboard_batterieverlauf") || NACHGELADEN.batt;
|
const states = await HASS.callWS({ type: "get_states" });
|
||||||
// Ohne diese Zeile bleibt der Setup-Katalog nach einem HA-Neustart leer,
|
const finden = (id) => states.find((s) => s.entity_id === id);
|
||||||
// bis zufaellig ein Push kommt - siehe setupBereit().
|
NACHGELADEN.profil = finden("pyscript.audi_dashboard_profil") || NACHGELADEN.profil;
|
||||||
NACHGELADEN.entitaeten = finden("pyscript.audi_dashboard_entitaeten") || NACHGELADEN.entitaeten;
|
NACHGELADEN.fahrten = finden("pyscript.audi_dashboard_fahrten") || NACHGELADEN.fahrten;
|
||||||
} catch (e) {
|
NACHGELADEN.tank = finden("pyscript.audi_dashboard_tankvorgaenge") || NACHGELADEN.tank;
|
||||||
// wird beim naechsten Versuch erneut probiert
|
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);
|
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);
|
}, verzoegerung);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function datenLaden(erzwingen) {
|
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 profilState = HASS.states["pyscript.audi_dashboard_profil"] || NACHGELADEN.profil;
|
||||||
const fahrtenState = HASS.states["pyscript.audi_dashboard_fahrten"] || NACHGELADEN.fahrten;
|
const fahrtenState = HASS.states["pyscript.audi_dashboard_fahrten"] || NACHGELADEN.fahrten;
|
||||||
const tankState = HASS.states["pyscript.audi_dashboard_tankvorgaenge"] || NACHGELADEN.tank;
|
const tankState = HASS.states["pyscript.audi_dashboard_tankvorgaenge"] || NACHGELADEN.tank;
|
||||||
@@ -4355,6 +4485,9 @@ class AudiDashboardPanel extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
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
|
// Alle drei Karten, nicht nur MAP: sonst bleibt beim Abhaengen des Panels
|
||||||
// (Standort- oder Uebersichtsseite offen) eine Leaflet-Instanz samt ihrem
|
// (Standort- oder Uebersichtsseite offen) eine Leaflet-Instanz samt ihrem
|
||||||
// window-Resize-Horcher und laufenden Kachelabrufen am Leben.
|
// window-Resize-Horcher und laufenden Kachelabrufen am Leben.
|
||||||
|
|||||||
@@ -68,11 +68,21 @@ html,body{height:100%}
|
|||||||
|
|
||||||
/* ---------------- Navigationsleiste ---------------- */
|
/* ---------------- Navigationsleiste ---------------- */
|
||||||
/* Sichere Bereiche des Geräterahmens: Statusleiste oben, Home-Indikator unten */
|
/* 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}
|
.head{display:flex;flex-direction:column}
|
||||||
.eyebrow{display:none}
|
.eyebrow{display:none}
|
||||||
.title{font-size:22px;font-weight:400;letter-spacing:-.01em;line-height:1.2}
|
.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}
|
.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
|
/* Rot bleibt Akzent/destruktiv, nicht Navigationsfarbe (dieselbe Regel wie
|
||||||
bei den Aktions-Buttons und dem aktiven Menü-Icon) - derselbe neutrale Ton
|
bei den Aktions-Buttons und dem aktiven Menü-Icon) - derselbe neutrale Ton
|
||||||
wie die Kopfzeile selbst. */
|
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. */
|
auf breiten Bildschirmen klaffte da eine sichtbare Luecke. */
|
||||||
.topbar{grid-column:2;grid-row:1;padding:38px 44px 8px;max-width:860px}
|
.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}
|
.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) .head{align-items:flex-start}
|
||||||
.phone:has(.back.on) .title{font-size:26px;text-align:left}
|
.phone:has(.back.on) .title{font-size:26px;text-align:left}
|
||||||
.title{font-size:34px}
|
.title{font-size:34px}
|
||||||
@@ -304,8 +308,14 @@ main#view{--seitenrand:16px;padding:0 var(--seitenrand) 30px;scroll-behavior:smo
|
|||||||
.rings{display:none !important}
|
.rings{display:none !important}
|
||||||
|
|
||||||
.duo{grid-template-columns:repeat(3,minmax(0,1fr))}
|
.duo{grid-template-columns:repeat(3,minmax(0,1fr))}
|
||||||
.carfix,.bildbox.carfix{height:260px}
|
/* Nur die Box selbst hoeher setzen, nicht jedes Element mit .carfix: die
|
||||||
.carfix.klein,.bildbox.carfix.klein{height:220px}
|
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)}
|
.bildgrid{grid-template-columns:repeat(4,1fr)}
|
||||||
.quad{gap:14px}
|
.quad{gap:14px}
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"version": 1786920185}
|
{"version": 1787000000}
|
||||||
|
|||||||
Reference in New Issue
Block a user