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
+112 -3
View File
@@ -6,8 +6,14 @@ oversized toggle switches, and fixed a desktop-layout audit (settings button / r
overflowing past the capped content column) — see `DESIGN_AUDIT_2026-08-13.md`; 2026-08-16: fixed
five user-reported bugs — removed Türschloss/Haubenschloss checks, fixed a translucent confirm-
sheet, added a flespi-style split lat/lon location source, hardened the "Lädt …" bootstrap race,
and added a generic (station-independent) fuel-receipt parser fallback — see section C). This file
is the entry point for every new agent
and added a generic (station-independent) fuel-receipt parser fallback; later the same day: removed
the now-unused combined `STANDORT_TRACKER` field entirely (flespi never provides it), closed a real
silent-failure gap in the receipt-upload error path, and replaced the vehicle's map marker with a
two-tone pin+car icon — see section C; a third round the same day found and fixed the long-standing
"fragmented Leaflet rendering" defect at its root — the Leaflet stylesheet was loaded into
`document.head` and therefore never reached the panel's shadow root — plus light-only map tiles,
the CI `poi-car` marker, the red frame on list rows, the select-field grey box, and two more
generic-receipt-parser gaps). This file is the entry point for every new agent
session: what this repo is, what is finished, what is missing, and how to work here. Detail lives in
the linked documents — this file points, it does not duplicate.
@@ -150,7 +156,9 @@ the `design/` export's RS-6 error extends into the technical sample data (73 l t
FIN series 4G). Four browser-verified rendering defects: switches stretched by
`.feld label{flex:1 1 auto}` (state display becomes ambiguous), double "offen" in the trip list,
empty oil-change tile when the service book is empty, and fragmented Leaflet rendering in all
three maps (verify on a real device). Positives: the 300/400 font-weight rule and de-DE
three maps (**fixed 2026-08-16** — root cause was Leaflet's stylesheet being loaded into
`document.head`, which never reaches the panel's shadow root; see the third-round entry in
"Open items"). Positives: the 300/400 font-weight rule and de-DE
formatting hold everywhere; `design-system/` is verifiably brand-free.
### HA panel — known gaps (most also in SPECIFICATION.md §7)
@@ -666,6 +674,107 @@ wraps the web app for iPhone; a PWA home-screen install is the accepted intermed
card by deliberately triggering it (this container's limited entity set causes a real
duplicate auto-suggestion); pyscript reload clean, no new console errors beyond the
pre-existing placeholder-image 404s. Synced to `installationspaket/`.
- [x] Follow-up round the same day (2026-08-16), three more items:
(1) **Removed `STANDORT_TRACKER` entirely** — user confirmed the combined device_tracker
field is no longer needed (flespi only ever provides the split lat/lon sensors added earlier
that day). Deleted the field from `einstellungen.py` and `entitaeten.py`'s Setup catalog, and
simplified `_standort()` in `frontend_veroeffentlichung.py` down to the single split-sensor
path (no more two-branch fallback). `overrides_schreiben()`'s existing "only known keys
survive" behavior (see its docstring) meant no explicit migration was needed for stale
`STANDORT_TRACKER` entries already sitting in `data/entitaeten.json` — confirmed live: this
container's own leftover override file still had `STANDORT_TRACKER` from earlier testing
(plus stale `TUERSCHLOSS_SENSOREN`/`HAUBENSCHLOSS_SENSOR` from before finding (1) of the
previous entry), all silently dropped on the next Setup save, which is exactly why the GPS
tile was still showing "Kein GPS-Signal" after the user's own remap attempt earlier — that
attempt happened to save while the old combined field was still in the catalog, so nothing
ever actually persisted `STANDORT_LAT_SENSOR`/`STANDORT_LON_SENSOR`. Redid the Setup save
live (mapped both to `sensor.testcar_b9_fmm003_testintegratoin_lat/longitude_coordinate_value`)
and confirmed the Übersicht/fullscreen Standort map switched from the "no signal" placeholder
to a real rendered map at the correct coordinates (South Tyrol terrain, "Burgleralm" label
visible).
(2) **Fuel-receipt "silently does nothing", root cause found** — reproduced the exact
real upload flow (button click → real `<input type=file>` → synthetic `File`/`DataTransfer`
dispatch → `FileReader``hass.callService`) via browser instrumentation: the happy path
works completely (backend parses, publishes `pyscript.audi_dashboard_beleg_ergebnis`, frontend
fills the form) — `belegverarbeitung.py`'s existing `try/except` around `_parser_aufrufen()`
already publishes a visible `fehler` for parser failures specifically. The actual gap:
`base64.b64decode()` and `_pdf_speichern()` in `audi_dashboard_beleg_hochladen()` were
*outside* that try block — any failure there (corrupted upload, disk full, ...) raised an
unhandled exception with zero UI feedback, since `serviceRufen()`'s generic `.catch()` only
does `console.error()`. Widened the try block to cover all three steps. Also stopped using
`serviceRufen()` for this one call site specifically (`audi-dashboard-app.js`, the
`eingabe.onchange` handler) — it now calls `HASS.callService()` directly with its own
`.catch()` that sets `belegFehler` and re-renders, so a rejected service call (network,
timeout — anything that bypasses the backend's own state-publish) is visible too, not just
logged. Verified against the real 10-receipt Shell suite (still passing) both before and
after this change.
(3) **Vehicle map marker replaced with a two-tone pin+car icon** — added `CI.pinCar` (a
balloon/pin path + white badge circle + a simplified car-front glyph built from rects, not
circles, after an isolated-DOM-injection test round showed a circles-for-headlights version
read as a face/animal rather than a car) to `audi-dashboard-app.js`, replacing the single-color
`poiCar` teardrop previously used by `fahrzeugMarkerSVG()`. Updated both marker call sites'
`iconAnchor` to the new pin's actual tip position and simplified `.fahrzeug-pin` in
`audi-dashboard.css` (no more `color:var(--red)`/white-halo-filter hack, since the new icon
carries its own fixed colors). Verified the icon markup renders correctly via an isolated
DOM-injection test (bypassing Leaflet); could **not** get a final on-map screenshot in
`audi_ha_test` — the Standort map tiles themselves only ever partially load in this container
(reproduced on a fully fresh reload, unrelated to this change), consistent with a previously
documented network limitation of this specific sandbox (see the Standort-Kachel entry above:
reverse-geocoding via Nominatim had the same kind of container-specific network issue).
Structurally verified only; needs a look on a real device/network to confirm final visual
placement.
All three deployed to `audi_ha_test` (version `1786758000`) and synced to `installationspaket/`.
- [x] Third round the same day (2026-08-16) — **the "fragmented Leaflet rendering" defect is solved
at its root**, plus four smaller items:
(1) **Leaflet CSS never reached the shadow root.** `leafletLaden()` appended
`leaflet.min.css` to `document.head`, but the panel renders inside `this.shadowRoot` — and
document stylesheets do not cross a shadow boundary. Inside the panel, `.leaflet-tile
{position:absolute}` and friends therefore never applied: the tiles laid themselves out as
ordinary in-flow `<img>` elements (two per row, ~1040 px of stacked height inside a 190 px
box), so only a thin strip of map was ever visible, and the marker pane ended up ~1009 px
below the visible area — which is exactly why the new vehicle pin appeared "missing". This
also explains the earlier misdiagnosis as a container/network limitation: every tile request
succeeded (`complete:true`, `naturalWidth:512`), the geometry was the problem. Fixed by
injecting the stylesheet into `ROOT` and awaiting it before the map is built; the script tag
stays in `document.head` (it must, `window.L` is global). The CSS injection is deliberately
re-checked on every `leafletLaden()` call rather than guarded by the `window.L` check, because
a HA panel remount produces a fresh shadow root while `window.L` is already set. Verified live:
both the Übersicht preview map and the fullscreen Standort map now fill their containers and
show the pin at the vehicle position.
(2) **Maps are now always light.** `TILES[theme]` switched to CARTO `dark_all` in Nacht mode;
replaced by a single themeless `TILE_URL` (`light_all`), matching how Google Maps & co. keep
the standard road map light regardless of app theme.
(3) **Vehicle marker now uses the real CI `poi-car` icons** (`poi-car-l.svg` / `poi-car-s.svg`
from the delivered icon set) instead of the hand-built two-tone pin from the previous round:
`CI.poiCarL` (48-grid) at ≥34 px, the existing `CI.poiCar` (24-grid) below, anchors recomputed
per variant. Since these are single-color outline forms in `currentColor`, `.fahrzeug-pin`
regained a stacked white drop-shadow halo so the outline stays readable on map tiles.
(4) **Red partial frame on Fahrten/Tanken list rows** (open since the 2026-08-13 design audit,
finding 3) — reproduced and confirmed by recoloring the delete button live. Row heights are
fractional (66.28 px), so `.swipe-content`'s edges miss the device-pixel grid and the red
`.swipe-delete` behind it bled through as a hairline around every row. Fixed by only painting
the delete button while a swipe is actually happening: `.swipe-delete{visibility:hidden}` plus
a `wischt` class set on the first movement of the gesture (and the existing `swiped` class for
the open row). Verified the full gesture still works — button appears at the first pixel of
drag and the row settles open at 84 px.
(5) **Select fields now show the grey background box** (design audit finding 1) — replaced
`.feld select{color:var(--ios-tint)}` with `background:var(--ios-fill);padding:8px 10px`,
matching `.mitEinheit input` ("Pause bis [15] Minuten"), as the user asked. Verified on
Modell / Ölwechsel-Intervall / Bildposition; free text fields stay plain, so the box now
genuinely marks "there is a choice here".
(6) **Generic receipt parser: total and station name** — a real Austrian non-Shell receipt
returned the right litres but 6,00 € and no station. Two causes, both fixed: the free-running
total pattern matched the *column heading* `SUMME-EUR` and read the next line's article number
as the amount, and bon printers letter-space headings (`G E S A M T BETRAG EUR: 92,60`), which
the keyword never matched. Total detection is now line-based — keyword checked against the
whitespace-stripped line, amount must be a real money value (`_GELD`, decimals required), tax
lines (`MWST`/`UST`/`VAT`/`STEUER`/`NETTO`) excluded, largest remaining candidate wins. The
address heuristic no longer demands three fixed lines with a 5-digit postcode (AT uses 4
digits, and this receipt carries `SHELL TANKSTELLE, 6450 SÖLDEN` on one line): it finds the
first postcode line in the header and takes the name from the line above. `Preis/L` without a
spelled-out "Liter" now matches too. Result on that receipt: 42,13 l · 92,60 € · 2,198 €/l ·
no discount · `AUTO B. FRISCHMANN GMBH`. The 10-receipt Shell regression suite still passes.
All deployed to `audi_ha_test` (version `1786765000`) and synced to `installationspaket/`.
- [ ] Fix remaining documentation drift (statistics claim, README gaps, obsolete TODO comment) —
text-only changes; INSTALL.md's WLAN/TommiG1 drift and stale variable names were fixed
2026-08-12 (see section B); `DESIGN_REVIEW_2026-08-13.md` and `REVIEW_main_2026-08-13.md`
+5 -2
View File
@@ -177,8 +177,11 @@ Oberfläche „unbekannt"/em-dash statt eines Werts, kein Absturz:
(`RANGE_SENSOR`) — bis zu einer neuen Datenquelle unbelegt, siehe
`AGENTS.md` (die frühere `TommiG1/HA_VAG-EU-Data-Act`-Integration liefert
diese nicht mehr)
- Standort (`STANDORT_TRACKER`, ein `device_tracker`) und Batteriespannung
(`BATTERIE_SENSOR`) — vom FMM003, z. B. `device_tracker.<gerätename>`
- Standort (`STANDORT_LAT_SENSOR`/`STANDORT_LON_SENSOR`, zwei eigene
`sensor`-Entities für Breiten-/Längengrad — flespi liefert Koordinaten so,
nicht als `latitude`/`longitude`-Attribute eines `device_tracker`) und
Batteriespannung (`BATTERIE_SENSOR`) — vom FMM003, z. B.
`sensor.<gerätename>_latitude_coordinate_value`/`..._longitude_coordinate_value`
bzw. `sensor.<gerätename>_external_power_voltage` (**nicht**
`..._battery_voltage` — das ist die interne Pufferzelle des Trackers,
nicht die Fahrzeugbatterie)
+59 -12
View File
@@ -91,6 +91,14 @@ def _iso(tag, monat, jahr, stunde, minute, sekunde="0"):
_ZAHL = r"[\d.,]+"
# Echte Geldangabe: Nachkommastellen sind Pflicht, damit im stationsunabhängigen
# Parser (_parsen_generisch) keine Artikel-/Belegnummer als Betrag durchgeht.
_GELD = r"\d{1,3}(?:[ .]\d{3})*,\d{2}"
_GESAMT_WOERTER = ("GESAMT", "ABSOLUT", "ENDBETRAG", "ZUZAHLEN", "TOTAL", "SUMME")
# Steuer- und Nettozeilen tragen dieselben Schlüsselwörter ("MWST GESAMT"),
# nennen aber nie den bezahlten Rechnungsbetrag.
_STEUER_WOERTER = ("MWST", "UST", "VAT", "STEUER", "NETTO")
def _parsen(text):
def suchen(muster, *, pflicht=True, flags=re.MULTILINE):
@@ -225,20 +233,57 @@ def _parsen_generisch(text):
def suchen(muster, flags=re.IGNORECASE | re.MULTILINE):
return re.search(muster, text, flags)
# ---- Adresse: Name-/Straßen-/PLZ-Ort-Zeile hintereinander - wie bei
# Shell (_parsen() oben), aber ohne Ankerzeile "Obj.-Nr." danach, also
# direkt an der PLZ-Ort-Form (5 Ziffern + Text) erkannt. Nicht Pflicht.
adresse = suchen(r"([^\n]{2,60})\n([^\n]{2,60})\n(\d{5}\s+[^\n]{2,40})")
station_name = adresse.group(1).strip() if adresse else None
station_address = (
f"{adresse.group(2).strip()}, {adresse.group(3).strip()}" if adresse else None
)
zeilen = [z.rstrip() for z in text.splitlines()]
# ---- Adresse -----------------------------------------------------------
# Die PLZ-Ort-Angabe steht nicht zuverlässig auf einer eigenen Zeile (ein
# österreichischer Beleg führt sie als "SHELL TANKSTELLE, 6450 SÖLDEN"
# zusammen mit der Filialbezeichnung) und PLZ sind vier- *oder*
# fünfstellig (AT/DE). Deshalb nicht mehr drei feste Zeilen hintereinander
# verlangen, sondern im Belegkopf die erste Zeile mit PLZ+Ort suchen; der
# Name ist die erste Textzeile darüber.
plz_zeile = None
for i, zeile in enumerate(zeilen[:12]):
if re.search(r"\b\d{4,5}\s+[A-Za-zÄÖÜäöüß]", zeile):
plz_zeile = i
break
station_name = None
station_address = None
if plz_zeile is not None:
station_address = zeilen[plz_zeile].strip()
for zeile in reversed(zeilen[:plz_zeile]):
if zeile.strip():
station_name = zeile.strip()
break
if station_name is None:
for zeile in zeilen[:5]:
if zeile.strip():
station_name = zeile.strip()
break
# ---- Gesamtbetrag ------------------------------------------------------
gesamt = suchen(r"(?:Gesamt\w*|Absolut\w*|Endbetrag|Summe|Total)\D{0,20}?(" + _ZAHL + r")\s*(?:€|EUR)?")
if not gesamt:
# Zeilenweise statt über den ganzen Text, aus zwei Gründen, die beide an
# einem echten Beleg aufgefallen sind: Bondrucker sperren Überschriften
# gern buchstabenweise ("G E S A M T BETRAG EUR: 92,60"), und ein frei
# laufendes Muster fand vorher die Spaltenüberschrift "SUMME-EUR" und
# las die Artikelnummer der Folgezeile als Betrag. Deshalb: Schlüsselwort
# auf der leerzeichenfreien Zeile prüfen, Betrag nur als echte Geldangabe
# (mit Nachkommastellen) akzeptieren und Steuerzeilen ausschließen.
kandidaten = []
for zeile in zeilen:
kompakt = re.sub(r"\s+", "", zeile).upper()
if not any(wort in kompakt for wort in _GESAMT_WOERTER):
continue
if any(wort in kompakt for wort in _STEUER_WOERTER):
continue
betraege = re.findall(_GELD, zeile)
if betraege:
kandidaten.append(_de_zahl(betraege[-1]))
if not kandidaten:
raise ValueError("Kein Gesamtbetrag gefunden (weder 'Gesamt' noch 'Absolut')")
fuel_total_eur = _de_zahl(gesamt.group(1))
# Mehrere Treffer (z. B. Summenzeile und Kartenzahlungszeile): der
# Rechnungsbetrag ist der größte - Teilbeträge sind nie größer.
fuel_total_eur = max(kandidaten)
# ---- Menge in Litern ----------------------------------------------------
menge = suchen(r"(?:Menge|Amount)\D{0,15}?(" + _ZAHL + r")\s*(?:l\b|L\b|Liter|Ltr\.?)")
@@ -253,8 +298,10 @@ def _parsen_generisch(text):
berechneter_preis_l = round(fuel_total_eur / liters, 3)
# ---- Aufgedruckter Preis/Liter - Abweichung verrät einen Rabatt -------
# "Preis/L" (ohne ausgeschriebenes "Liter") kommt auf echten Belegen vor,
# deshalb ist alles nach dem L optional.
aufgedruckt = suchen(
r"(?:Preis\s*/?\s*(?:je\s*)?Liter|Price\s*/?\s*L(?:iter)?|€\s*/\s*l|EUR\s*/\s*l)\D{0,10}?(" + _ZAHL + r")"
r"(?:Preis\s*/?\s*(?:je\s*)?L(?:iter|tr\.?)?|Price\s*/?\s*L(?:iter)?|€\s*/\s*l|EUR\s*/\s*l)\D{0,10}?(" + _ZAHL + r")"
)
list_price_per_l = _de_zahl(aufgedruckt.group(1)) if aufgedruckt else berechneter_preis_l
+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"]
+83 -33
View File
@@ -386,31 +386,57 @@ function ring(pct) {
/* ------------------------------------------------------------------ Karte
Leaflet weiterhin per CDN geladen (siehe Kopfkommentar). */
let MAP = null, TILE = null, LEAFLET_LADEND = null;
const TILES = {
nacht: "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png",
tag: "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png",
};
/* Bewusst themeunabhaengig hell: Kartendienste (Google Maps & Co.) zeigen die
Standard-Strassenkarte immer im hellen Stil, weil Strassen, Beschriftungen
und Wegfarben darauf lesbar bleiben. Die frueher bei Nacht eingeblendeten
dark_all-Kacheln liessen die Karte im Panel schwarz wirken. */
const TILE_URL = "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png";
const ATTR = "&copy; OpenStreetMap &middot; CARTO";
/* Satellitenansicht der Standort-Vollbildkarte (Layer-Knopf) - wie die
CARTO-Straßenkacheln oben ohne eigenen Schlüssel nutzbar. */
const SAT_TILE_URL = "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}";
const SAT_ATTR = "Tiles &copy; Esri";
function leafletLaden() {
if (window.L) return Promise.resolve();
if (LEAFLET_LADEND) return LEAFLET_LADEND;
LEAFLET_LADEND = new Promise((resolve, reject) => {
const css1 = document.createElement("link");
css1.rel = "stylesheet";
css1.href = "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css";
document.head.appendChild(css1);
const script = document.createElement("script");
script.src = "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js";
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
const LEAFLET_CSS_URL = "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css";
const LEAFLET_JS_URL = "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js";
/* Das Leaflet-Stylesheet muss in den Shadow-Root, nicht in document.head:
Dokument-Styles greifen nicht in ein Shadow-DOM hinein. Lag es im Head,
fehlte drinnen u. a. .leaflet-tile{position:absolute} - die Kacheln flossen
dann als gewoehnliche Bilder untereinander (bei 8 Kacheln ueber 1000 px
hoch), von der Karte war nur ein schmaler Streifen zu sehen und der
Fahrzeugmarker landete weit unterhalb des sichtbaren Ausschnitts. Das JS
gehoert dagegen weiterhin in den Head, damit window.L global bleibt. */
function leafletCssEinhaengen() {
if (!ROOT || ROOT.querySelector("link[data-leaflet-css]")) return Promise.resolve();
return new Promise((fertig) => {
const css = document.createElement("link");
css.rel = "stylesheet";
css.href = LEAFLET_CSS_URL;
css.dataset.leafletCss = "1";
/* Auch bei Ladefehler weitermachen statt haengen zu bleiben - dann sieht
die Karte zwar wieder falsch aus, aber die Ansicht baut sich auf. */
css.onload = fertig;
css.onerror = fertig;
ROOT.appendChild(css);
});
return LEAFLET_LADEND;
}
function leafletLaden() {
/* Bewusst bei jedem Aufruf: haengt HA das Panel neu ein, entsteht ein neuer
Shadow-Root ohne das Stylesheet, waehrend window.L laengst gesetzt ist. */
const css = leafletCssEinhaengen();
if (window.L) return css;
if (!LEAFLET_LADEND) {
LEAFLET_LADEND = new Promise((resolve, reject) => {
const script = document.createElement("script");
script.src = LEAFLET_JS_URL;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
return Promise.all([css, LEAFLET_LADEND]);
}
function mapBox() { return `<div class="mapbox"><div id="map"></div></div>`; }
@@ -437,7 +463,7 @@ function fakeTrack(a, b, n = 13) {
}
function tiles() {
if (!MAP || !window.L) return;
const url = TILES[HOST.dataset.theme];
const url = TILE_URL;
if (TILE) MAP.removeLayer(TILE);
TILE = window.L.tileLayer(url, { maxZoom: 19, attribution: ATTR }).addTo(MAP);
}
@@ -498,10 +524,15 @@ const STANDORT_PEEK_PX = 96; // sichtbarer Anteil im geschlossenen Zustand, muss
function standortBekannt() { return CAR.standortLat != null && CAR.standortLon != null; }
/* Kartenmarkierung: poi-car aus dem Audi-CI-Satz (assets/icons/poi-car.svg).
Die Spitze sitzt unten, deshalb ankert Leaflet am unteren Rand. */
/* Kartenmarkierung: poi-car aus dem CI-Satz, in den beiden gelieferten
Groessenvarianten - poi-car-l (48er-Raster, mehr Detail) ab 34 px, darunter
poi-car-s (24er-Raster). Die Nadelspitze sitzt unten, deshalb ankert Leaflet
am unteren Rand (siehe iconAnchor an den Aufrufstellen: Spitze bei y=45.25
im 48er- bzw. y=23.3 im 24er-viewBox). */
function fahrzeugMarkerSVG(px = 34) {
return `<svg viewBox="0 0 24 24" width="${px}" height="${px}" fill="none" aria-hidden="true">${CI.poiCar}</svg>`;
const gross = px >= 34;
const box = gross ? 48 : 24;
return `<svg viewBox="0 0 ${box} ${box}" width="${px}" height="${px}" fill="none" aria-hidden="true">${gross ? CI.poiCarL : CI.poiCar}</svg>`;
}
/* Fahrzustand fürs Menü: "fährt" solange die aktuelle Fahrt noch offen ist
@@ -576,7 +607,7 @@ function standortErfassenFallsNoetig() {
}
/* Reverse-Geocoding über die öffentliche Nominatim-API (OpenStreetMap) -
wie die Kartenkacheln oben (TILES) ohne eigenen Schlüssel; bei diesem
wie die Kartenkacheln oben (TILE_URL) ohne eigenen Schlüssel; bei diesem
Aufrufvolumen (ein Fahrzeug, nur bei geöffneter Standortansicht) im Rahmen
der Nominatim-Nutzungsbedingungen unproblematisch, genau wie das bereits
dokumentierte Leaflet-über-CDN-Vorgehen (siehe AGENTS.md). Cache-Schlüssel
@@ -639,14 +670,14 @@ function standortTiles() {
if (SMAP_TILE) SMAP.removeLayer(SMAP_TILE);
SMAP_TILE = SMAP_STIL === "satellit"
? window.L.tileLayer(SAT_TILE_URL, { maxZoom: 19, attribution: SAT_ATTR }).addTo(SMAP)
: window.L.tileLayer(TILES[HOST.dataset.theme], { maxZoom: 19, attribution: ATTR }).addTo(SMAP);
: window.L.tileLayer(TILE_URL, { maxZoom: 19, attribution: ATTR }).addTo(SMAP);
}
function standortMarkerZeichnen() {
if (!SMAP || !window.L) return;
if (FAHRZEUG_MARKER) { SMAP.removeLayer(FAHRZEUG_MARKER); FAHRZEUG_MARKER = null; }
if (USER_MARKER) { SMAP.removeLayer(USER_MARKER); USER_MARKER = null; }
if (standortBekannt()) {
const icon = window.L.divIcon({ className: "fahrzeug-pin-wrap", html: `<div class="fahrzeug-pin">${fahrzeugMarkerSVG(38)}</div>`, iconSize: [38, 38], iconAnchor: [18, 36] });
const icon = window.L.divIcon({ className: "fahrzeug-pin-wrap", html: `<div class="fahrzeug-pin">${fahrzeugMarkerSVG(38)}</div>`, iconSize: [38, 38], iconAnchor: [19, 36] });
FAHRZEUG_MARKER = window.L.marker([CAR.standortLat, CAR.standortLon], { icon }).addTo(SMAP);
FAHRZEUG_MARKER.on("click", () => standortMenuOeffnen());
}
@@ -693,10 +724,10 @@ async function initVorschauMap() {
zoomControl: false, attributionControl: false, dragging: false,
scrollWheelZoom: false, doubleClickZoom: false, boxZoom: false, keyboard: false, touchZoom: false,
});
TMAP_TILE = window.L.tileLayer(TILES[HOST.dataset.theme], { maxZoom: 19, attribution: ATTR }).addTo(TMAP);
TMAP_TILE = window.L.tileLayer(TILE_URL, { maxZoom: 19, attribution: ATTR }).addTo(TMAP);
if (standortBekannt()) {
TMAP.setView([CAR.standortLat, CAR.standortLon], 14);
const icon = window.L.divIcon({ className: "fahrzeug-pin-wrap", html: `<div class="fahrzeug-pin">${fahrzeugMarkerSVG(32)}</div>`, iconSize: [32, 32], iconAnchor: [15, 30] });
const icon = window.L.divIcon({ className: "fahrzeug-pin-wrap", html: `<div class="fahrzeug-pin">${fahrzeugMarkerSVG(32)}</div>`, iconSize: [32, 32], iconAnchor: [15, 31] });
window.L.marker([CAR.standortLat, CAR.standortLon], { icon }).addTo(TMAP);
} else {
TMAP.setView(kartenMittelpunkt(), 11);
@@ -781,6 +812,9 @@ const CI = {
carSearch: '<path fill="currentColor" d="M1 7.8a6.16 6.16 0 0 0 6.1 6.1h.4A5.7 5.7 0 0 0 13 7.91V7.9a6.1 6.1 0 0 0-5.91-6A5.93 5.93 0 0 0 1 7.8m-1 0C0 3.92 3.23.8 7.11.9a7.1 7.1 0 0 1 6.89 7c.06 2.33-1 4.44-2.73 5.71L14.8 19H20v1.5c0 .28.22.5.5.5h2a.5.5 0 0 0 .5-.5v-7.8l-1.9-1.9-.04-.06a55 55 0 0 0-2.84-4.71c-1.42-.1-2.49-.17-3.2-.2l.04-1c.77.04 1.93.1 3.48.22l.24.02.14.2a55 55 0 0 1 2.99 4.93L24 12.3v8.2c0 .83-.67 1.5-1.5 1.5h-2a1.5 1.5 0 0 1-1.5-1.5V20h-3.55l1.67 2.54-.84.54L14.26 20 5 19.96v.54c0 .83-.67 1.5-1.5 1.5h-2A1.5 1.5 0 0 1 0 20.5v-7.08h1v7.08c0 .28.22.5.5.5h2a.5.5 0 0 0 .5-.5v-1.54l9.6.02-3.17-4.83c-.87.45-1.85.72-2.92.75H7.1c-3.88 0-7-3.13-7.1-7.09zm5.47-2.75c1.86-.12 3.37-.2 4.52-.24l.03 1c-1.08.03-2.5.1-4.24.22L4.51 8c-.27.43-.6 1-1 1.7l-.87-.48c.4-.72.74-1.3 1.03-1.76q.6-.96 1.41-2.19l.14-.2z"/>',
gps: '<path fill="currentColor" d="M12.5 3.51V6.5h-1V3.51a8.46 8.46 0 0 0-7.99 7.99H6.5v1H3.51a8.46 8.46 0 0 0 7.99 7.99V17.5h1v2.99a8.46 8.46 0 0 0 7.99-7.99H17.5v-1h2.99a8.46 8.46 0 0 0-7.99-7.99m0-1a9.46 9.46 0 0 1 8.99 8.99h2.01v1h-2.01a9.46 9.46 0 0 1-8.99 8.99v2.01h-1v-2.01a9.46 9.46 0 0 1-8.99-8.99H.5v-1h2.01a9.46 9.46 0 0 1 8.99-8.99V.5h1z"/>',
poiCar: '<path fill="currentColor" d="M3.8 8.6C3.8 4.33 7.32.8 11.5.8s7.7 3.53 7.7 7.8c0 1.11-.5 2.58-1.17 4.1a48 48 0 0 1-2.5 4.7 90 90 0 0 1-3.54 5.47l-.06.09-.02.02V23l-.41-.29-.4.29h-.01l-.02-.03-.06-.09a70 70 0 0 1-1.05-1.53 90 90 0 0 1-2.49-3.93 48 48 0 0 1-2.5-4.72C4.29 11.18 3.8 9.71 3.8 8.6m7.7 14.1-.4.29.4.57.4-.57zm0-.87a88 88 0 0 0 3.17-4.94 47 47 0 0 0 2.45-4.6A10.3 10.3 0 0 0 18.2 8.6c0-3.73-3.08-6.8-6.7-6.8a6.82 6.82 0 0 0-6.7 6.8c0 .89.4 2.18 1.08 3.69.67 1.48 1.55 3.1 2.45 4.6a89 89 0 0 0 3.17 4.94M8.77 4h5.47L17 7.35V11a1 1 0 0 1-1 1h-1.5a1 1 0 0 1-1-1h-4a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7.32zm.46 1L7 7.68V8h2v1H7v2h1.5v-1h6v1H16V9h-2V8h2v-.29L13.76 5z"/>',
/* poi-car-l - grosse Variante desselben CI-Symbols (48er-Raster), fuer die
Fahrzeugnadel auf der Vollbildkarte. Siehe fahrzeugMarkerSVG(). */
poiCarL: '<path fill="currentColor" d="M9 17.5C9 9.52 15.52 3 23.5 3S38 9.52 38 17.5c0 2.04-.92 4.78-2.23 7.67a90 90 0 0 1-4.84 8.98 170 170 0 0 1-6.98 10.59l-.03.04-.01.01-.41-.29-.4.3-.02-.02-.03-.04a66 66 0 0 1-.56-.8 166 166 0 0 1-6.42-9.8 90 90 0 0 1-4.84-8.97C9.92 22.27 9 19.54 9 17.5m14.5 27-.4.3.4.55.4-.56zm0-.86a132 132 0 0 0 1.77-2.56 170 170 0 0 0 4.8-7.45 89 89 0 0 0 4.79-8.87c1.3-2.89 2.14-5.45 2.14-7.26C37 10.08 30.92 4 23.5 4S10 10.08 10 17.5c0 1.8.83 4.37 2.14 7.26a89 89 0 0 0 4.79 8.87 170 170 0 0 0 6.57 10.01M17.96 7.98A63.3 63.3 0 0 1 29.05 8l.2.02.13.16c.6.7 1.1 1.36 1.46 1.94l1.31 2.08L34 14.31v7.19c0 .83-.67 1.5-1.5 1.5h-2a1.5 1.5 0 0 1-1.5-1.5V21H18v.5c0 .83-.67 1.5-1.5 1.5h-2a1.5 1.5 0 0 1-1.5-1.5v-7.19l1.85-2.11 1.3-2.08c.37-.58.86-1.24 1.47-1.96l.13-.16.2-.02ZM15.48 13 14 14.69V15h3v1h-3v5.5c0 .28.22.5.5.5h2a.5.5 0 0 0 .5-.5V20h13v1.5c0 .28.22.5.5.5h2a.5.5 0 0 0 .5-.5V16h-3v-1h3v-.31L31.52 13zm15.36-1L30 10.65a16 16 0 0 0-1.25-1.67 62.4 62.4 0 0 0-10.5-.02 17 17 0 0 0-1.25 1.7L16.16 12zM19.3 15h8.42l1.4 1.4-.16.31c-.34.74-.65 1.32-.92 1.72-.14.2-.28.38-.42.5a.92.92 0 0 1-.61.27h-7a.92.92 0 0 1-.6-.27 2.4 2.4 0 0 1-.43-.5c-.27-.4-.58-.98-.92-1.72l-.15-.32zm.42 1-.6.6c.26.57.5.99.69 1.27.11.17.2.27.26.32l.01.01h6.86v-.01c.07-.05.16-.15.27-.32.2-.28.43-.7.7-1.27l-.6-.6h-7.6Z"/>',
selectAll: '<path fill="currentColor" d="M2.8 2H5v1H3v2H2V2.8a.76.76 0 0 1 .43-.71.9.9 0 0 1 .35-.09zv.5zM9 3H7V2h2zm4 0h-2V2h2zm4 0h-2V2h2zm4 0h-2V2h2.2v.5V2h.02a1 1 0 0 1 .1.01c.07.01.16.03.25.08a.76.76 0 0 1 .43.71V5h-1zM6 6h12v12H6zm1 1v10h10V7zM3 7v2H2V7zm19 0v2h-1V7zM3 11v2H2v-2zm19 0v2h-1v-2zM3 15v2H2v-2zm19 0v2h-1v-2zM3 21v-2H2v2.2a.76.76 0 0 0 .43.71.9.9 0 0 0 .35.09h.01l.01-.5v.5H5v-1zm19-2v2.2a.76.76 0 0 1-.43.71.9.9 0 0 1-.35.09h-.01l-.01-.49V22H19v-1h2v-2zM9 22H7v-1h2zm4 0h-2v-1h2zm4 0h-2v-1h2z"/>',
planRoute: '<path fill="currentColor" d="m18.3 7-3.65-3.65.7-.7 4.86 4.85-4.86 4.85-.7-.7L18.29 8H9.5C8.67 8 8 8.67 8 9.5V21H7V9.5A2.5 2.5 0 0 1 9.5 7z"/>',
upload: '<path fill="currentColor" d="M13 20h-1V9.7l-3.65 3.65-.7-.7 4.85-4.86 4.85 4.86-.7.7L13 9.71zM6 5V4h13v1z"/>',
@@ -3103,11 +3137,9 @@ function ereignisseVerdrahten() {
ROOT.getElementById("theme").addEventListener("click", () => {
themeSetzen(HOST.dataset.theme === "nacht" ? "tag" : "nacht");
themeIcon();
/* Neu zeichnen, damit auch die Leaflet-Kacheln den Stil wechseln: die
Kachel-URL wird beim Aufbau aus TILES[theme] gewaehlt und nicht
nachtraeglich aktualisiert. Ohne dies blieb die Karte bis zum naechsten
Backend-Update im alten Stil - seit der Dauerkarte auf der Uebersicht
deutlich sichtbar. */
/* Neu zeichnen, damit theme-abhaengige, in JS gesetzte Werte (z. B. die
aus --red gelesene Routenfarbe in initMap) mitziehen. Die Kacheln selbst
sind seit TILE_URL themeunabhaengig hell und brauchen das nicht mehr. */
render();
});
// Wischgeste über Pointer Events (nicht Touch Events), damit dieselbe
@@ -3153,6 +3185,9 @@ function ereignisseVerdrahten() {
}
e.preventDefault();
swipeWarGezogen = true;
// Loeschknopf erst waehrend der Geste einblenden (siehe .swipe-delete in
// audi-dashboard.css) - sonst blitzt er dauerhaft an den Zeilenraendern durch.
swipeAktivWrap.classList.add("wischt");
const basis = swipeAktivWrap.classList.contains("swiped") ? -SWIPE_BREITE : 0;
swipeDX = Math.max(-SWIPE_BREITE - 20, Math.min(0, basis + dx));
const inhalt = swipeAktivWrap.querySelector(".swipe-content");
@@ -3163,6 +3198,7 @@ function ereignisseVerdrahten() {
if (!swipeAktivWrap) return;
const inhalt = swipeAktivWrap.querySelector(".swipe-content");
inhalt.style.transition = ""; inhalt.style.transform = "";
swipeAktivWrap.classList.remove("wischt");
if (swipeGeste === "x") {
if (swipeDX <= -SWIPE_BREITE / 2) { schliesseAlleSwipes(); swipeAktivWrap.classList.add("swiped"); }
else { swipeAktivWrap.classList.remove("swiped"); }
@@ -3510,7 +3546,21 @@ function ereignisseVerdrahten() {
const leser = new FileReader();
leser.onload = () => {
const base64 = leser.result.split(",")[1];
serviceRufen("audi_dashboard_beleg_hochladen", { pdf_base64: base64, dateiname: datei.name, tank_id: tankId });
// Kein serviceRufen() hier: dessen catch loggt nur in die Browser-
// Konsole (bei allen anderen Aufrufen ok, die reagieren auf
// datenLaden() statt auf den Rückgabewert) - ein rejizierter
// Service-Aufruf (Backend nicht erreichbar, Timeout, ...) blieb
// für den Beleg-Upload sonst komplett unsichtbar in der
// Oberfläche, obwohl der Backend-seitige Parser-Fehlerpfad selbst
// schon einen Fehler veröffentlicht (siehe belegverarbeitung.py).
HASS.callService("pyscript", "audi_dashboard_beleg_hochladen",
{ pdf_base64: base64, dateiname: datei.name, tank_id: tankId }
).catch((err) => {
console.error("audi_dashboard: audi_dashboard_beleg_hochladen", err);
belegFehler = { context: kontext, meldung: "Beleg konnte nicht hochgeladen werden" };
if (!tankId) belegEntwurf = null;
render();
});
};
leser.readAsDataURL(datei);
};
+5 -4
View File
@@ -140,10 +140,11 @@ main#view{--seitenrand:16px;padding:0 var(--seitenrand) 30px;scroll-behavior:smo
padding:8px 4px;color:var(--fg2)}
/* Design-Audit 2026-08-13: ein <select> (z.B. Oelwechsel-Intervall "10.000 km"/
"1 Jahr") sah wie reiner, nicht editierbarer Text aus, seit hier Rahmen/
Flaeche entfernt wurden - anders als bei freien Texteingaben zeigt iOS bei
Picker-Feldern wie diesem ueblicherweise die Akzentfarbe statt Fliesstext-
Grau als Hinweis "hier gibt es eine Auswahl". */
.feld select{color:var(--ios-tint)}
Flaeche entfernt wurden. Der Hinweis "hier gibt es eine Auswahl" laeuft
deshalb ueber dieselbe graue Flaeche wie bei .mitEinheit input (Feld
"Pause bis [15] Minuten") - ein Kasten liest sich als bedienbar, waehrend
eingefaerbter Text auch bloss Hervorhebung sein koennte. */
.feld select{background:var(--ios-fill);padding:8px 10px}
.feld input:focus,.feld select:focus{background:var(--ios-fill);color:var(--fg)}
.mitEinheit input{background:var(--ios-fill);padding:8px 10px}
.notiz{background:var(--ios-fill);border:0;border-radius:12px;font-size:16px}
@@ -1 +1 @@
{"version": 1786752000}
{"version": 1786765000}
+17 -5
View File
@@ -667,7 +667,15 @@ button.tile, .tilebtn { transition: background .15s, transform .1s; }
/* -------------------------------------------------------------- Wischen */
.swipe-wrap { position: relative; overflow: hidden; }
.swipe-delete { position: absolute; inset: 0 0 0 auto; width: 84px; display: flex; }
/* Der rote Loeschknopf liegt dauerhaft hinter der Zeile. Zeilenhoehen sind
gebrochen (z. B. 66,28 px), dadurch fiel die Kante von .swipe-content nicht
aufs Geraetepixelraster und der rote Knopf blitzte als duenne Linie an den
Zeilenraendern durch - sichtbar als roter Teilrahmen um jede Listenzeile.
Er wird deshalb erst sichtbar, sobald wirklich gewischt wird ("wischt"
waehrend der Geste, "swiped" fuer die offen stehende Zeile). */
.swipe-delete { position: absolute; inset: 0 0 0 auto; width: 84px; display: flex; visibility: hidden; }
.swipe-wrap.wischt .swipe-delete,
.swipe-wrap.swiped .swipe-delete { visibility: visible; }
.swipe-loeschen {
width: 100%;
background: var(--bad); color: #fff; border: none;
@@ -702,12 +710,16 @@ button.tile, .tilebtn { transition: background .15s, transform .1s; }
}
.fahrzeug-pin-wrap { background: none; border: none; }
/* poi-car aus dem CI-Satz - rotes Symbol, weiss abgesetzt statt in einem
Kasten, damit die Spitze exakt auf der Position steht. */
/* poi-car aus dem CI-Satz ist eine reine Konturform in currentColor - auf
Kartenkacheln waere sie ohne Trennung zum Untergrund kaum lesbar. Die
gestapelten weissen drop-shadows ziehen deshalb einen Halo um die Kontur,
der letzte Schatten gibt Tiefe. Feste Farbe statt Theme-Variable, damit die
Nadel in Tag- und Nachtansicht gleich aussieht. */
.fahrzeug-pin {
color: var(--red);
display: flex; align-items: center; justify-content: center;
filter: drop-shadow(0 0 1px #fff) drop-shadow(0 0 2px #fff) drop-shadow(0 2px 5px rgba(0,0,0,.45));
color: #182634;
filter: drop-shadow(0 0 1px #fff) drop-shadow(0 0 1px #fff)
drop-shadow(0 0 2px #fff) drop-shadow(0 2px 4px rgba(0,0,0,.4));
}
.user-pin-wrap { background: none; border: none; }
.user-pin {