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:
@@ -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
|
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-
|
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,
|
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
|
and added a generic (station-independent) fuel-receipt parser fallback; later the same day: removed
|
||||||
is the entry point for every new agent
|
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
|
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.
|
||||||
|
|
||||||
@@ -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
|
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,
|
`.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
|
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.
|
formatting hold everywhere; `design-system/` is verifiably brand-free.
|
||||||
|
|
||||||
### HA panel — known gaps (most also in SPECIFICATION.md §7)
|
### 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
|
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
|
duplicate auto-suggestion); pyscript reload clean, no new console errors beyond the
|
||||||
pre-existing placeholder-image 404s. Synced to `installationspaket/`.
|
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) —
|
- [ ] 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`
|
||||||
|
|||||||
@@ -177,8 +177,11 @@ Oberfläche „unbekannt"/em-dash statt eines Werts, kein Absturz:
|
|||||||
(`RANGE_SENSOR`) — bis zu einer neuen Datenquelle unbelegt, siehe
|
(`RANGE_SENSOR`) — bis zu einer neuen Datenquelle unbelegt, siehe
|
||||||
`AGENTS.md` (die frühere `TommiG1/HA_VAG-EU-Data-Act`-Integration liefert
|
`AGENTS.md` (die frühere `TommiG1/HA_VAG-EU-Data-Act`-Integration liefert
|
||||||
diese nicht mehr)
|
diese nicht mehr)
|
||||||
- Standort (`STANDORT_TRACKER`, ein `device_tracker`) und Batteriespannung
|
- Standort (`STANDORT_LAT_SENSOR`/`STANDORT_LON_SENSOR`, zwei eigene
|
||||||
(`BATTERIE_SENSOR`) — vom FMM003, z. B. `device_tracker.<gerätename>`
|
`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**
|
bzw. `sensor.<gerätename>_external_power_voltage` (**nicht**
|
||||||
`..._battery_voltage` — das ist die interne Pufferzelle des Trackers,
|
`..._battery_voltage` — das ist die interne Pufferzelle des Trackers,
|
||||||
nicht die Fahrzeugbatterie)
|
nicht die Fahrzeugbatterie)
|
||||||
|
|||||||
@@ -91,6 +91,14 @@ def _iso(tag, monat, jahr, stunde, minute, sekunde="0"):
|
|||||||
|
|
||||||
_ZAHL = r"[\d.,]+"
|
_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 _parsen(text):
|
||||||
def suchen(muster, *, pflicht=True, flags=re.MULTILINE):
|
def suchen(muster, *, pflicht=True, flags=re.MULTILINE):
|
||||||
@@ -225,20 +233,57 @@ def _parsen_generisch(text):
|
|||||||
def suchen(muster, flags=re.IGNORECASE | re.MULTILINE):
|
def suchen(muster, flags=re.IGNORECASE | re.MULTILINE):
|
||||||
return re.search(muster, text, flags)
|
return re.search(muster, text, flags)
|
||||||
|
|
||||||
# ---- Adresse: Name-/Straßen-/PLZ-Ort-Zeile hintereinander - wie bei
|
zeilen = [z.rstrip() for z in text.splitlines()]
|
||||||
# Shell (_parsen() oben), aber ohne Ankerzeile "Obj.-Nr." danach, also
|
|
||||||
# direkt an der PLZ-Ort-Form (5 Ziffern + Text) erkannt. Nicht Pflicht.
|
# ---- Adresse -----------------------------------------------------------
|
||||||
adresse = suchen(r"([^\n]{2,60})\n([^\n]{2,60})\n(\d{5}\s+[^\n]{2,40})")
|
# Die PLZ-Ort-Angabe steht nicht zuverlässig auf einer eigenen Zeile (ein
|
||||||
station_name = adresse.group(1).strip() if adresse else None
|
# österreichischer Beleg führt sie als "SHELL TANKSTELLE, 6450 SÖLDEN"
|
||||||
station_address = (
|
# zusammen mit der Filialbezeichnung) und PLZ sind vier- *oder*
|
||||||
f"{adresse.group(2).strip()}, {adresse.group(3).strip()}" if adresse else None
|
# 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 ------------------------------------------------------
|
# ---- Gesamtbetrag ------------------------------------------------------
|
||||||
gesamt = suchen(r"(?:Gesamt\w*|Absolut\w*|Endbetrag|Summe|Total)\D{0,20}?(" + _ZAHL + r")\s*(?:€|EUR)?")
|
# Zeilenweise statt über den ganzen Text, aus zwei Gründen, die beide an
|
||||||
if not gesamt:
|
# 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')")
|
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 in Litern ----------------------------------------------------
|
||||||
menge = suchen(r"(?:Menge|Amount)\D{0,15}?(" + _ZAHL + r")\s*(?:l\b|L\b|Liter|Ltr\.?)")
|
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)
|
berechneter_preis_l = round(fuel_total_eur / liters, 3)
|
||||||
|
|
||||||
# ---- Aufgedruckter Preis/Liter - Abweichung verrät einen Rabatt -------
|
# ---- 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(
|
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
|
list_price_per_l = _de_zahl(aufgedruckt.group(1)) if aufgedruckt else berechneter_preis_l
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
# task.executor entfällt hier bewusst: _pdf_speichern/_parser_aufrufen sind
|
||||||
# in dieser pyscript-Datei definiert, task.executor akzeptiert aber nur
|
# in dieser pyscript-Datei definiert, task.executor akzeptiert aber nur
|
||||||
# echte externe Python-Funktionen (siehe Kopfkommentar in profil.py).
|
# 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"
|
# Alles bis zum geparsten Ergebnis in einem try: eine unbehandelte
|
||||||
pdf_pfad = _pdf_speichern(inhalt_bytes, dateiname)
|
# 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:
|
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)
|
geparst = _parser_aufrufen(pdf_pfad)
|
||||||
except Exception as exc:
|
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})
|
frontend_veroeffentlichung.beleg_ergebnis_veroeffentlichen({"fehler": "Beleg konnte nicht gelesen werden", "tank_id": tank_id})
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -77,16 +77,10 @@ OELWECHSEL_STRECKE_SENSOR = ""
|
|||||||
NAECHSTE_INSPEKTION_SENSOR = ""
|
NAECHSTE_INSPEKTION_SENSOR = ""
|
||||||
INSPEKTION_STRECKE_SENSOR = ""
|
INSPEKTION_STRECKE_SENSOR = ""
|
||||||
|
|
||||||
# Live-GPS-Position des Fahrzeugs (Übersicht -> Standort-Kachel). Erwartet
|
# Live-GPS-Position des Fahrzeugs (Übersicht -> Standort-Kachel). Breiten-/
|
||||||
# eine device_tracker.*-Entity mit "latitude"/"longitude"-Attributen (HA-
|
# Längengrad als zwei eigene sensor-Entities (flespi liefert Koordinaten so,
|
||||||
# Konvention, siehe _standort() in frontend_veroeffentlichung.py) - jetzt
|
# nicht als Attribute einer device_tracker-Entity - siehe _standort() in
|
||||||
# vom FMM003 geliefert.
|
# frontend_veroeffentlichung.py).
|
||||||
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.
|
|
||||||
STANDORT_LAT_SENSOR = ""
|
STANDORT_LAT_SENSOR = ""
|
||||||
STANDORT_LON_SENSOR = ""
|
STANDORT_LON_SENSOR = ""
|
||||||
|
|
||||||
|
|||||||
@@ -109,18 +109,14 @@ FELDER = [
|
|||||||
"hinweis": "Meldet die Integration oft als negativen Countdown - die App zeigt nur den Betrag.",
|
"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,
|
"domains": ["sensor"], "device_classes": ["distance"], "units": ["km", "mi"], "liste": False, "pflicht": False,
|
||||||
"stichworte": ["inspektion", "inspection", "distance", "strecke"]},
|
"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",
|
{"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,
|
"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",
|
{"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,
|
"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,
|
# Mehrere @state_trigger(f"...")-Dekoratoren (fahrterkennung.py,
|
||||||
|
|||||||
@@ -88,59 +88,36 @@ def _abs_zahl(wert):
|
|||||||
|
|
||||||
|
|
||||||
def _standort():
|
def _standort():
|
||||||
"""Live-GPS-Position des Fahrzeugs (Übersicht -> Standort-Kachel). Zwei
|
"""Live-GPS-Position des Fahrzeugs (Übersicht -> Standort-Kachel) - liest
|
||||||
mögliche Quellen, in dieser Reihenfolge versucht: (1) STANDORT_TRACKER,
|
STANDORT_LAT_SENSOR/STANDORT_LON_SENSOR, zwei eigene sensor-Entities für
|
||||||
eine device_tracker-Entity mit latitude/longitude/gps_accuracy-Attributen
|
Breiten-/Längengrad (flespi liefert Koordinaten so, nicht als Attribute
|
||||||
(HA-Konvention) - der Normalfall; (2) STANDORT_LAT_SENSOR/
|
einer einzelnen device_tracker-Entity). Fehlt eine der beiden Entity-IDs
|
||||||
STANDORT_LON_SENSOR, zwei eigene sensor-Entities für Breiten-/Längengrad -
|
oder ist der Zustand (noch) nicht verfügbar, liefert diese Funktion
|
||||||
manche Integrationen (z. B. flespi) liefern Koordinaten so, statt als
|
durchgehend None statt eines geratenen Werts - genau das
|
||||||
Attribute einer einzelnen Tracker-Entity. Fehlt beides oder ist (noch)
|
zustand_oder_none()-Muster von oben, hier als eigene Funktion, weil
|
||||||
nicht verfügbar, liefert diese Funktion durchgehend None statt eines
|
zusätzlich zum Zustand noch der Aktualisierungszeitpunkt gebraucht wird:
|
||||||
geratenen Werts - genau das zustand_oder_none()-Muster von oben, hier als
|
state.get() liefert dafür ein StateVal (str-Unterklasse) mit
|
||||||
eigene Funktion, weil zusätzlich zum Zustand noch Attribute und der
|
.last_updated als datetime-Attribut - kein eigenständiges
|
||||||
Aktualisierungszeitpunkt gebraucht werden: state.get() liefert dafür ein
|
state.last_updated(), das gibt es in pyscript nicht."""
|
||||||
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}
|
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
|
lat_id, lon_id = einstellungen.STANDORT_LAT_SENSOR, einstellungen.STANDORT_LON_SENSOR
|
||||||
if lat_id and lon_id:
|
if not lat_id or not lon_id:
|
||||||
try:
|
return leer
|
||||||
lat_wert = state.get(lat_id)
|
try:
|
||||||
lon_wert = state.get(lon_id)
|
lat_wert = state.get(lat_id)
|
||||||
except NameError:
|
lon_wert = state.get(lon_id)
|
||||||
return leer
|
except NameError:
|
||||||
lat, lon = _zu_zahl(lat_wert), _zu_zahl(lon_wert)
|
return leer
|
||||||
if lat is None or lon is None:
|
lat, lon = _zu_zahl(lat_wert), _zu_zahl(lon_wert)
|
||||||
return leer
|
if lat is None or lon is None:
|
||||||
zeit = None
|
return leer
|
||||||
try:
|
zeit = None
|
||||||
zeit = lat_wert.last_updated.isoformat()
|
try:
|
||||||
except AttributeError:
|
zeit = lat_wert.last_updated.isoformat()
|
||||||
pass
|
except AttributeError:
|
||||||
return {"lat": lat, "lon": lon, "genauigkeit_m": None, "zeit": zeit}
|
pass
|
||||||
|
return {"lat": lat, "lon": lon, "genauigkeit_m": None, "zeit": zeit}
|
||||||
return leer
|
|
||||||
|
|
||||||
|
|
||||||
POSITIONEN = ["vorne links", "vorne rechts", "hinten links", "hinten rechts"]
|
POSITIONEN = ["vorne links", "vorne rechts", "hinten links", "hinten rechts"]
|
||||||
|
|||||||
@@ -386,31 +386,57 @@ function ring(pct) {
|
|||||||
/* ------------------------------------------------------------------ Karte
|
/* ------------------------------------------------------------------ Karte
|
||||||
Leaflet weiterhin per CDN geladen (siehe Kopfkommentar). */
|
Leaflet weiterhin per CDN geladen (siehe Kopfkommentar). */
|
||||||
let MAP = null, TILE = null, LEAFLET_LADEND = null;
|
let MAP = null, TILE = null, LEAFLET_LADEND = null;
|
||||||
const TILES = {
|
/* Bewusst themeunabhaengig hell: Kartendienste (Google Maps & Co.) zeigen die
|
||||||
nacht: "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png",
|
Standard-Strassenkarte immer im hellen Stil, weil Strassen, Beschriftungen
|
||||||
tag: "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png",
|
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 = "© OpenStreetMap · CARTO";
|
const ATTR = "© OpenStreetMap · CARTO";
|
||||||
/* Satellitenansicht der Standort-Vollbildkarte (Layer-Knopf) - wie die
|
/* Satellitenansicht der Standort-Vollbildkarte (Layer-Knopf) - wie die
|
||||||
CARTO-Straßenkacheln oben ohne eigenen Schlüssel nutzbar. */
|
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_TILE_URL = "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}";
|
||||||
const SAT_ATTR = "Tiles © Esri";
|
const SAT_ATTR = "Tiles © Esri";
|
||||||
|
|
||||||
function leafletLaden() {
|
const LEAFLET_CSS_URL = "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css";
|
||||||
if (window.L) return Promise.resolve();
|
const LEAFLET_JS_URL = "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js";
|
||||||
if (LEAFLET_LADEND) return LEAFLET_LADEND;
|
|
||||||
LEAFLET_LADEND = new Promise((resolve, reject) => {
|
/* Das Leaflet-Stylesheet muss in den Shadow-Root, nicht in document.head:
|
||||||
const css1 = document.createElement("link");
|
Dokument-Styles greifen nicht in ein Shadow-DOM hinein. Lag es im Head,
|
||||||
css1.rel = "stylesheet";
|
fehlte drinnen u. a. .leaflet-tile{position:absolute} - die Kacheln flossen
|
||||||
css1.href = "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css";
|
dann als gewoehnliche Bilder untereinander (bei 8 Kacheln ueber 1000 px
|
||||||
document.head.appendChild(css1);
|
hoch), von der Karte war nur ein schmaler Streifen zu sehen und der
|
||||||
const script = document.createElement("script");
|
Fahrzeugmarker landete weit unterhalb des sichtbaren Ausschnitts. Das JS
|
||||||
script.src = "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js";
|
gehoert dagegen weiterhin in den Head, damit window.L global bleibt. */
|
||||||
script.onload = resolve;
|
function leafletCssEinhaengen() {
|
||||||
script.onerror = reject;
|
if (!ROOT || ROOT.querySelector("link[data-leaflet-css]")) return Promise.resolve();
|
||||||
document.head.appendChild(script);
|
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>`; }
|
function mapBox() { return `<div class="mapbox"><div id="map"></div></div>`; }
|
||||||
@@ -437,7 +463,7 @@ function fakeTrack(a, b, n = 13) {
|
|||||||
}
|
}
|
||||||
function tiles() {
|
function tiles() {
|
||||||
if (!MAP || !window.L) return;
|
if (!MAP || !window.L) return;
|
||||||
const url = TILES[HOST.dataset.theme];
|
const url = TILE_URL;
|
||||||
if (TILE) MAP.removeLayer(TILE);
|
if (TILE) MAP.removeLayer(TILE);
|
||||||
TILE = window.L.tileLayer(url, { maxZoom: 19, attribution: ATTR }).addTo(MAP);
|
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; }
|
function standortBekannt() { return CAR.standortLat != null && CAR.standortLon != null; }
|
||||||
|
|
||||||
/* Kartenmarkierung: poi-car aus dem Audi-CI-Satz (assets/icons/poi-car.svg).
|
/* Kartenmarkierung: poi-car aus dem CI-Satz, in den beiden gelieferten
|
||||||
Die Spitze sitzt unten, deshalb ankert Leaflet am unteren Rand. */
|
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) {
|
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
|
/* 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) -
|
/* 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
|
Aufrufvolumen (ein Fahrzeug, nur bei geöffneter Standortansicht) im Rahmen
|
||||||
der Nominatim-Nutzungsbedingungen unproblematisch, genau wie das bereits
|
der Nominatim-Nutzungsbedingungen unproblematisch, genau wie das bereits
|
||||||
dokumentierte Leaflet-über-CDN-Vorgehen (siehe AGENTS.md). Cache-Schlüssel
|
dokumentierte Leaflet-über-CDN-Vorgehen (siehe AGENTS.md). Cache-Schlüssel
|
||||||
@@ -639,14 +670,14 @@ function standortTiles() {
|
|||||||
if (SMAP_TILE) SMAP.removeLayer(SMAP_TILE);
|
if (SMAP_TILE) SMAP.removeLayer(SMAP_TILE);
|
||||||
SMAP_TILE = SMAP_STIL === "satellit"
|
SMAP_TILE = SMAP_STIL === "satellit"
|
||||||
? window.L.tileLayer(SAT_TILE_URL, { maxZoom: 19, attribution: SAT_ATTR }).addTo(SMAP)
|
? 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() {
|
function standortMarkerZeichnen() {
|
||||||
if (!SMAP || !window.L) return;
|
if (!SMAP || !window.L) return;
|
||||||
if (FAHRZEUG_MARKER) { SMAP.removeLayer(FAHRZEUG_MARKER); FAHRZEUG_MARKER = null; }
|
if (FAHRZEUG_MARKER) { SMAP.removeLayer(FAHRZEUG_MARKER); FAHRZEUG_MARKER = null; }
|
||||||
if (USER_MARKER) { SMAP.removeLayer(USER_MARKER); USER_MARKER = null; }
|
if (USER_MARKER) { SMAP.removeLayer(USER_MARKER); USER_MARKER = null; }
|
||||||
if (standortBekannt()) {
|
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 = window.L.marker([CAR.standortLat, CAR.standortLon], { icon }).addTo(SMAP);
|
||||||
FAHRZEUG_MARKER.on("click", () => standortMenuOeffnen());
|
FAHRZEUG_MARKER.on("click", () => standortMenuOeffnen());
|
||||||
}
|
}
|
||||||
@@ -693,10 +724,10 @@ async function initVorschauMap() {
|
|||||||
zoomControl: false, attributionControl: false, dragging: false,
|
zoomControl: false, attributionControl: false, dragging: false,
|
||||||
scrollWheelZoom: false, doubleClickZoom: false, boxZoom: false, keyboard: false, touchZoom: 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()) {
|
if (standortBekannt()) {
|
||||||
TMAP.setView([CAR.standortLat, CAR.standortLon], 14);
|
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);
|
window.L.marker([CAR.standortLat, CAR.standortLon], { icon }).addTo(TMAP);
|
||||||
} else {
|
} else {
|
||||||
TMAP.setView(kartenMittelpunkt(), 11);
|
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"/>',
|
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"/>',
|
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"/>',
|
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"/>',
|
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"/>',
|
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"/>',
|
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", () => {
|
ROOT.getElementById("theme").addEventListener("click", () => {
|
||||||
themeSetzen(HOST.dataset.theme === "nacht" ? "tag" : "nacht");
|
themeSetzen(HOST.dataset.theme === "nacht" ? "tag" : "nacht");
|
||||||
themeIcon();
|
themeIcon();
|
||||||
/* Neu zeichnen, damit auch die Leaflet-Kacheln den Stil wechseln: die
|
/* Neu zeichnen, damit theme-abhaengige, in JS gesetzte Werte (z. B. die
|
||||||
Kachel-URL wird beim Aufbau aus TILES[theme] gewaehlt und nicht
|
aus --red gelesene Routenfarbe in initMap) mitziehen. Die Kacheln selbst
|
||||||
nachtraeglich aktualisiert. Ohne dies blieb die Karte bis zum naechsten
|
sind seit TILE_URL themeunabhaengig hell und brauchen das nicht mehr. */
|
||||||
Backend-Update im alten Stil - seit der Dauerkarte auf der Uebersicht
|
|
||||||
deutlich sichtbar. */
|
|
||||||
render();
|
render();
|
||||||
});
|
});
|
||||||
// Wischgeste über Pointer Events (nicht Touch Events), damit dieselbe
|
// Wischgeste über Pointer Events (nicht Touch Events), damit dieselbe
|
||||||
@@ -3153,6 +3185,9 @@ function ereignisseVerdrahten() {
|
|||||||
}
|
}
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
swipeWarGezogen = true;
|
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;
|
const basis = swipeAktivWrap.classList.contains("swiped") ? -SWIPE_BREITE : 0;
|
||||||
swipeDX = Math.max(-SWIPE_BREITE - 20, Math.min(0, basis + dx));
|
swipeDX = Math.max(-SWIPE_BREITE - 20, Math.min(0, basis + dx));
|
||||||
const inhalt = swipeAktivWrap.querySelector(".swipe-content");
|
const inhalt = swipeAktivWrap.querySelector(".swipe-content");
|
||||||
@@ -3163,6 +3198,7 @@ function ereignisseVerdrahten() {
|
|||||||
if (!swipeAktivWrap) return;
|
if (!swipeAktivWrap) return;
|
||||||
const inhalt = swipeAktivWrap.querySelector(".swipe-content");
|
const inhalt = swipeAktivWrap.querySelector(".swipe-content");
|
||||||
inhalt.style.transition = ""; inhalt.style.transform = "";
|
inhalt.style.transition = ""; inhalt.style.transform = "";
|
||||||
|
swipeAktivWrap.classList.remove("wischt");
|
||||||
if (swipeGeste === "x") {
|
if (swipeGeste === "x") {
|
||||||
if (swipeDX <= -SWIPE_BREITE / 2) { schliesseAlleSwipes(); swipeAktivWrap.classList.add("swiped"); }
|
if (swipeDX <= -SWIPE_BREITE / 2) { schliesseAlleSwipes(); swipeAktivWrap.classList.add("swiped"); }
|
||||||
else { swipeAktivWrap.classList.remove("swiped"); }
|
else { swipeAktivWrap.classList.remove("swiped"); }
|
||||||
@@ -3510,7 +3546,21 @@ function ereignisseVerdrahten() {
|
|||||||
const leser = new FileReader();
|
const leser = new FileReader();
|
||||||
leser.onload = () => {
|
leser.onload = () => {
|
||||||
const base64 = leser.result.split(",")[1];
|
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);
|
leser.readAsDataURL(datei);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -140,10 +140,11 @@ main#view{--seitenrand:16px;padding:0 var(--seitenrand) 30px;scroll-behavior:smo
|
|||||||
padding:8px 4px;color:var(--fg2)}
|
padding:8px 4px;color:var(--fg2)}
|
||||||
/* Design-Audit 2026-08-13: ein <select> (z.B. Oelwechsel-Intervall "10.000 km"/
|
/* 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/
|
"1 Jahr") sah wie reiner, nicht editierbarer Text aus, seit hier Rahmen/
|
||||||
Flaeche entfernt wurden - anders als bei freien Texteingaben zeigt iOS bei
|
Flaeche entfernt wurden. Der Hinweis "hier gibt es eine Auswahl" laeuft
|
||||||
Picker-Feldern wie diesem ueblicherweise die Akzentfarbe statt Fliesstext-
|
deshalb ueber dieselbe graue Flaeche wie bei .mitEinheit input (Feld
|
||||||
Grau als Hinweis "hier gibt es eine Auswahl". */
|
"Pause bis [15] Minuten") - ein Kasten liest sich als bedienbar, waehrend
|
||||||
.feld select{color:var(--ios-tint)}
|
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)}
|
.feld input:focus,.feld select:focus{background:var(--ios-fill);color:var(--fg)}
|
||||||
.mitEinheit input{background:var(--ios-fill);padding:8px 10px}
|
.mitEinheit input{background:var(--ios-fill);padding:8px 10px}
|
||||||
.notiz{background:var(--ios-fill);border:0;border-radius:12px;font-size:16px}
|
.notiz{background:var(--ios-fill);border:0;border-radius:12px;font-size:16px}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"version": 1786752000}
|
{"version": 1786765000}
|
||||||
|
|||||||
@@ -667,7 +667,15 @@ button.tile, .tilebtn { transition: background .15s, transform .1s; }
|
|||||||
|
|
||||||
/* -------------------------------------------------------------- Wischen */
|
/* -------------------------------------------------------------- Wischen */
|
||||||
.swipe-wrap { position: relative; overflow: hidden; }
|
.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 {
|
.swipe-loeschen {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: var(--bad); color: #fff; border: none;
|
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; }
|
.fahrzeug-pin-wrap { background: none; border: none; }
|
||||||
/* poi-car aus dem CI-Satz - rotes Symbol, weiss abgesetzt statt in einem
|
/* poi-car aus dem CI-Satz ist eine reine Konturform in currentColor - auf
|
||||||
Kasten, damit die Spitze exakt auf der Position steht. */
|
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 {
|
.fahrzeug-pin {
|
||||||
color: var(--red);
|
|
||||||
display: flex; align-items: center; justify-content: center;
|
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-wrap { background: none; border: none; }
|
||||||
.user-pin {
|
.user-pin {
|
||||||
|
|||||||
Reference in New Issue
Block a user