Fix broken map rendering, split GPS cleanup, receipt parser and design backlog

Leaflet's stylesheet was appended to document.head, so it never reached the
panel's shadow root: .leaflet-tile{position:absolute} never applied, tiles laid
out as in-flow images (~1040px inside a 190px box) and the marker pane ended up
far below the visible area. That is the real cause of the long-standing
"fragmented Leaflet rendering" finding and of the invisible vehicle pin - every
tile request had actually succeeded. The stylesheet now goes into the shadow
root and is awaited before the map is built.

Also in this round:
- Map tiles are always light (Google-Maps-style), no dark variant at night.
- Vehicle marker uses the real CI poi-car icons (poi-car-l >=34px, poi-car-s
  below), with a white halo so the outline stays readable on tiles.
- Removed the obsolete combined STANDORT_TRACKER field; only the split
  lat/lon sensors remain.
- Receipt upload: widened the try block so base64/save failures surface, and
  the frontend call site now reports a rejected service call.
- Generic receipt parser: total detection is line-based (letter-spaced
  headings, no more matching the SUMME-EUR column header, tax lines excluded),
  address heuristic handles 4-digit postcodes and single-line address blocks,
  and "Preis/L" matches without a spelled-out "Liter".
- Design backlog: red hairline frame on list rows (delete button bled through
  at fractional row heights) and select fields now use the grey background box.

Shell 10-receipt regression suite still passes; all changes verified live in
the audi_ha_test container.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-16 12:46:48 +02:00
parent 25d1cebd12
commit 39da68cd87
11 changed files with 328 additions and 133 deletions
+11 -5
View File
@@ -103,14 +103,20 @@ def audi_dashboard_beleg_hochladen(pdf_base64=None, dateiname=None, tank_id=None
# task.executor entfällt hier bewusst: _pdf_speichern/_parser_aufrufen sind
# in dieser pyscript-Datei definiert, task.executor akzeptiert aber nur
# echte externe Python-Funktionen (siehe Kopfkommentar in profil.py).
inhalt_bytes = base64.b64decode(pdf_base64)
dateiname = dateiname or f"beleg_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf"
pdf_pfad = _pdf_speichern(inhalt_bytes, dateiname)
#
# Alles bis zum geparsten Ergebnis in einem try: eine unbehandelte
# Exception hier (kaputtes Base64, volle Festplatte, ...) wäre für die
# Oberfläche unsichtbar geblieben - serviceRufen() im Frontend loggt einen
# rejizierten Service-Aufruf nur in die Browser-Konsole, zeigt aber nichts
# an (siehe audi-dashboard-app.js). Bisher war nur der Parser-Aufruf
# selbst so abgesichert; b64decode/_pdf_speichern fehlten.
try:
inhalt_bytes = base64.b64decode(pdf_base64)
dateiname = dateiname or f"beleg_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf"
pdf_pfad = _pdf_speichern(inhalt_bytes, dateiname)
geparst = _parser_aufrufen(pdf_pfad)
except Exception as exc:
log.error(f"audi_dashboard: Beleg konnte nicht geparst werden: {exc}")
log.error(f"audi_dashboard: Beleg konnte nicht verarbeitet werden: {exc}")
frontend_veroeffentlichung.beleg_ergebnis_veroeffentlichen({"fehler": "Beleg konnte nicht gelesen werden", "tank_id": tank_id})
return
@@ -77,16 +77,10 @@ OELWECHSEL_STRECKE_SENSOR = ""
NAECHSTE_INSPEKTION_SENSOR = ""
INSPEKTION_STRECKE_SENSOR = ""
# Live-GPS-Position des Fahrzeugs (Übersicht -> Standort-Kachel). Erwartet
# eine device_tracker.*-Entity mit "latitude"/"longitude"-Attributen (HA-
# Konvention, siehe _standort() in frontend_veroeffentlichung.py) - jetzt
# vom FMM003 geliefert.
STANDORT_TRACKER = "device_tracker.testzone_fmm003"
# Alternative zu STANDORT_TRACKER: manche Integrationen (z. B. flespi) liefern
# Breiten-/Längengrad nicht als Attribute einer einzelnen device_tracker-
# Entity, sondern als zwei eigene sensor-Entities. _standort() nutzt diese
# beiden nur, wenn STANDORT_TRACKER leer ist oder keine Koordinaten liefert.
# Live-GPS-Position des Fahrzeugs (Übersicht -> Standort-Kachel). Breiten-/
# Längengrad als zwei eigene sensor-Entities (flespi liefert Koordinaten so,
# nicht als Attribute einer device_tracker-Entity - siehe _standort() in
# frontend_veroeffentlichung.py).
STANDORT_LAT_SENSOR = ""
STANDORT_LON_SENSOR = ""
+4 -8
View File
@@ -109,18 +109,14 @@ FELDER = [
"hinweis": "Meldet die Integration oft als negativen Countdown - die App zeigt nur den Betrag.",
"domains": ["sensor"], "device_classes": ["distance"], "units": ["km", "mi"], "liste": False, "pflicht": False,
"stichworte": ["inspektion", "inspection", "distance", "strecke"]},
{"key": "STANDORT_TRACKER", "label": "GPS-Standort", "gruppe": "standort",
"hinweis": "Braucht latitude/longitude-Attribute (HA-Konvention für device_tracker).",
"domains": ["device_tracker"], "device_classes": [], "units": [], "liste": False, "pflicht": False,
"stichworte": ["standort", "gps", "tracker", "position", "ortung"]},
{"key": "STANDORT_LAT_SENSOR", "label": "GPS-Standort: Breitengrad", "gruppe": "standort",
"hinweis": "Nur nötig, falls die Integration Breiten-/Längengrad als zwei eigene Sensoren statt als Attribute einer GPS-Standort-Entity liefert (z. B. flespi).",
"hinweis": "Breitengrad als eigener Sensor (z. B. flespi).",
"domains": ["sensor"], "device_classes": [], "units": [], "liste": False, "pflicht": False,
"stichworte": ["breitengrad", "latitude", "lat"]},
"stichworte": ["breitengrad", "latitude", "lat", "standort", "gps", "position", "ortung"]},
{"key": "STANDORT_LON_SENSOR", "label": "GPS-Standort: Längengrad", "gruppe": "standort",
"hinweis": "Nur nötig, falls die Integration Breiten-/Längengrad als zwei eigene Sensoren statt als Attribute einer GPS-Standort-Entity liefert (z. B. flespi).",
"hinweis": "Längengrad als eigener Sensor (z. B. flespi).",
"domains": ["sensor"], "device_classes": [], "units": [], "liste": False, "pflicht": False,
"stichworte": ["längengrad", "longitude", "lon", "lng"]},
"stichworte": ["längengrad", "longitude", "lon", "lng", "standort", "gps", "position", "ortung"]},
]
# Mehrere @state_trigger(f"...")-Dekoratoren (fahrterkennung.py,
@@ -88,59 +88,36 @@ def _abs_zahl(wert):
def _standort():
"""Live-GPS-Position des Fahrzeugs (Übersicht -> Standort-Kachel). Zwei
mögliche Quellen, in dieser Reihenfolge versucht: (1) STANDORT_TRACKER,
eine device_tracker-Entity mit latitude/longitude/gps_accuracy-Attributen
(HA-Konvention) - der Normalfall; (2) STANDORT_LAT_SENSOR/
STANDORT_LON_SENSOR, zwei eigene sensor-Entities für Breiten-/Längengrad -
manche Integrationen (z. B. flespi) liefern Koordinaten so, statt als
Attribute einer einzelnen Tracker-Entity. Fehlt beides oder ist (noch)
nicht verfügbar, liefert diese Funktion durchgehend None statt eines
geratenen Werts - genau das zustand_oder_none()-Muster von oben, hier als
eigene Funktion, weil zusätzlich zum Zustand noch Attribute und der
Aktualisierungszeitpunkt gebraucht werden: state.get() liefert dafür ein
StateVal (str-Unterklasse) mit .last_updated als datetime-Attribut - kein
eigenständiges state.last_updated(), das gibt es in pyscript nicht."""
"""Live-GPS-Position des Fahrzeugs (Übersicht -> Standort-Kachel) - liest
STANDORT_LAT_SENSOR/STANDORT_LON_SENSOR, zwei eigene sensor-Entities für
Breiten-/Längengrad (flespi liefert Koordinaten so, nicht als Attribute
einer einzelnen device_tracker-Entity). Fehlt eine der beiden Entity-IDs
oder ist der Zustand (noch) nicht verfügbar, liefert diese Funktion
durchgehend None statt eines geratenen Werts - genau das
zustand_oder_none()-Muster von oben, hier als eigene Funktion, weil
zusätzlich zum Zustand noch der Aktualisierungszeitpunkt gebraucht wird:
state.get() liefert dafür ein StateVal (str-Unterklasse) mit
.last_updated als datetime-Attribut - kein eigenständiges
state.last_updated(), das gibt es in pyscript nicht."""
leer = {"lat": None, "lon": None, "genauigkeit_m": None, "zeit": None}
tracker_id = einstellungen.STANDORT_TRACKER
if tracker_id:
try:
wert = state.get(tracker_id)
attribute = state.getattr(tracker_id)
except NameError:
attribute = None
if attribute and attribute.get("latitude") is not None:
zeit = None
try:
zeit = wert.last_updated.isoformat()
except AttributeError:
pass
return {
"lat": _zu_zahl(attribute.get("latitude")),
"lon": _zu_zahl(attribute.get("longitude")),
"genauigkeit_m": _zu_zahl(attribute.get("gps_accuracy")),
"zeit": zeit,
}
lat_id, lon_id = einstellungen.STANDORT_LAT_SENSOR, einstellungen.STANDORT_LON_SENSOR
if lat_id and lon_id:
try:
lat_wert = state.get(lat_id)
lon_wert = state.get(lon_id)
except NameError:
return leer
lat, lon = _zu_zahl(lat_wert), _zu_zahl(lon_wert)
if lat is None or lon is None:
return leer
zeit = None
try:
zeit = lat_wert.last_updated.isoformat()
except AttributeError:
pass
return {"lat": lat, "lon": lon, "genauigkeit_m": None, "zeit": zeit}
return leer
if not lat_id or not lon_id:
return leer
try:
lat_wert = state.get(lat_id)
lon_wert = state.get(lon_id)
except NameError:
return leer
lat, lon = _zu_zahl(lat_wert), _zu_zahl(lon_wert)
if lat is None or lon is None:
return leer
zeit = None
try:
zeit = lat_wert.last_updated.isoformat()
except AttributeError:
pass
return {"lat": lat, "lon": lon, "genauigkeit_m": None, "zeit": zeit}
POSITIONEN = ["vorne links", "vorne rechts", "hinten links", "hinten rechts"]