Tankstelle zweizeilig und antippbar, Belegkarte ohne toten Knopf (2026.9.4.5)

Sieben Meldungen des Eigentuemers, beide Codebasen.

Karte im Einzelbeleg: "Position laedt nicht" und "der Button funktioniert
nicht" waren dieselbe Stelle - ohne aufloesbaren Ort malte das Panel einen
beliebigen Ausschnitt und liess den Zentrieren-Knopf fuer immer disabled.
Beide zeigen jetzt die Auskunft statt einer Karte und unterscheiden dabei
"steht nicht im Beleg" von "findet niemand". Dazu ein echter Paritaetsmangel:
die App schlug nur station_address nach, das Panel faellt seit jeher auf den
Namen zurueck - von 16 Testbelegen tragen vier eine Anschrift.

Der Suchbegriff darf Name und Anschrift NICHT zusammensetzen: gemessen findet
Nominatim "Pascalstr. 8, 85057 Ingolstadt", mit dem Betreibernamen davor
nichts. Jetzt zwei Kandidaten nacheinander (Anschrift, dann Name); fuer den
Kartendienst dagegen beides zusammen, weil Apple und Google Betriebsnamen
kennen.

Tankstelle steht ueber die volle Kachelbreite, zweizeilig (Tankstelle und
Strasse / Ort) und fuehrt beim Tippen in den Standard-Kartendienst - Apple
Karten auf Apple-Geraeten, sonst Google Maps. Dieselbe Wahl jetzt auch fuer
Fahrzeugstandort und Autohaus.

SmartDeal-Ersparnis brach um, weil die dt-Spalte 150,00px breit war und ihr
Inhalt 150,20px - zwei Zehntel Pixel, Zeile dadurch 44,8 statt 26,4px hoch.

Der Regler "Fahrt beenden" schreibt nicht mehr beim Loslassen: derselbe Wert
stellt den Schlaf-Timeout des Dongles, das soll bewusst ausgeloest werden.
Speichern/Verwerfen erscheinen nur bei echter Aenderung.

Share-Erweiterung: an der ausgelieferten .ipa gemessen trug sie 1.0, die App
2026.9.3.3 - iOS registriert eine Erweiterung mit abweichender Versionsnummer
nicht, sie fehlt dann im Teilen-Blatt. ios-signieren.sh schreibt die Version
jetzt in beide Ziele und liest die fertige .ipa gegen. Zwischenablage: dritter
Weg ueber UIPasteboard.itemProviders, und ein fehlender nativer Teil wird als
solcher gemeldet statt als "Kein Zugriff".

Am Geraet gesetzt: 1003 Network Ping Timeout 0 -> 60 s, 1004 ACK Type
TCP/IP -> AVL. Beide liegen bei flespi als pending bereit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-04 10:10:55 +02:00
parent 7f386d19ea
commit 43f25e2e0d
17 changed files with 1000 additions and 60 deletions
+168 -1
View File
@@ -1,6 +1,9 @@
# AGENTS.md — Project state, review findings, open items, and working rules
**Last updated: 2026-09-04** (Geraetezeit statt Ankunftszeit - die Wurzel hinter der 3-km-Fahrt:
**Last updated: 2026-09-04** (Tankstelle zweizeilig und antippbar, die Belegkarte sagt bei
fehlender Position die Wahrheit statt einen toten Knopf zu zeigen, der Regler bekommt einen
Speichern-Knopf, und die Share-Erweiterung trug eine andere Versionsnummer als die App - `2026.9.4.5`,
Abschnitt CC. Davor: Geraetezeit statt Ankunftszeit - die Wurzel hinter der 3-km-Fahrt:
das Fahrtfenster stand in Geraetezeit, der Verlauf war nach Ankunftszeit sortiert. Dazu die neun
Paritaetsbefunde und die verunreinigte Batteriehistorie, `2026.9.4.1`, Abschnitt CB. Davor: Der Regler stellt den Schlaf-Timeout des Dongles, flespi lesend und
schreibend, die eigene Warteschlange wieder entfallen, `2026.9.3.10`, Abschnitt BZ. Davor: Zugaenge: die Dienste-Token aus App und Panel bedienbar, der Wert kommt nie
@@ -9462,3 +9465,167 @@ resting voltage, the SOC, the SOH trend and the fallback shown in "Mein Audi / Z
the same mechanism as `wert_in_naehe()` with its 300 s window for the outside temperature. Without a
vehicle, nothing gets recorded. The 13 existing entries are the owner's data; removal is via the
swipe gesture in the measurement list.
## CC. Tankstelle zweizeilig und antippbar, die Belegkarte sagt die Wahrheit, der Regler bekommt einen Knopf (2026.9.4.5)
Sieben Meldungen des Eigentuemers, dazu die beiden Dongle-Parameter aus dem Vortag.
### Der Dongle steht jetzt auf 1003 = 60 und 1004 = AVL
Am Geraet nachgesehen statt angenommen: `network_ping.timeout` stand auf **0** und
`ack_type.mode` auf **0 (TCP/IP)** — die Werte, die am 03.09. die tote TCP-Sitzung
unbemerkt liessen (Abschnitt CB). Beide ueber `PUT /gw/devices/{id}/settings/{name}`
mit `{"properties": …, "address": "connection"}` geschrieben, dieselbe Form wie
`schlaf_timeout_schreiben()`. flespi haelt sie als **`pending`** vor und stellt sie
zu, sobald sich das Geraet meldet; `current` steht bis dahin unveraendert auf 0/0.
`records_params` (1000 = 60 s, 1001 = 30 s, 1002 = Oldest) bleibt wie es war.
### "Einzelbeleg/Map laedt die Position nicht" und "der Button funktioniert nicht"
**Dieselbe Stelle, ein Befund.** Reproduziert an einem Beleg ohne aufloesbare
Anschrift: das Panel zeichnete dann einen beliebigen Kartenausschnitt und liess den
Zentrieren-Knopf fuer immer `disabled`; die App zeigte ihren Leerzustand — und
suchte gar nicht erst, weil `TankstellenKarte` ausschliesslich `station_address`
nachschlug. Von 16 Tankvorgaengen der Testinstanz tragen **vier** eine Anschrift;
das Panel faellt seit jeher auf den Namen zurueck (`f.station_address ||
f.station_name`), die App nicht.
Beide zeigen jetzt bei fehlender Position **keine Karte**, sondern die Auskunft —
und unterscheiden dabei "steht nicht im Beleg" von "findet niemand". Im Panel
entscheidet das `BELEG_KARTE`, ein Merker, den `initMap()` setzt, wenn der
Nachschlag erfolglos zurueckkommt; `go()` und das Speichern eines Tankvorgangs
raeumen ihn wieder ab, damit eine korrigierte Anschrift erneut gesucht wird.
**Der Suchbegriff war der zweite Fallstrick, und er hat mich einmal erwischt.**
Die erste Fassung setzte Name UND Anschrift zusammen, mit der Begruendung "eine
Hausnummer ohne Betrieb trifft in Gewerbegebieten das Nachbargebaeude". Live
gemessen ist das genau falsch herum: `"Pascalstr. 8, 85057 Ingolstadt"` findet
Nominatim, `"A. Zrenner GmbH, Pascalstr. 8, 85057 Ingolstadt"` **nicht** — der
Betreibername steht in keiner Karte. Sichtbar wurde es sofort als "Keine Position"
bei einem Beleg, dessen Anschrift sich vorher aufloesen liess.
Deshalb jetzt zwei getrennte Begriffe:
* `tankstelleKandidaten()`**erst die Anschrift, dann der Name**, nacheinander
versucht. Der Name allein ist wertvoll (`"Shell, SÖLDEN"` findet Nominatim), nur
eben nicht als Praefix.
* `tankstelleZiel()` — Name UND Anschrift, aber nur fuer den Kartendienst: Apple
Karten und Google Maps kennen Betriebsnamen und sind darin deutlich besser als
Nominatims Freitextsuche.
### Die Tankstelle steht ueber die volle Breite, in zwei Zeilen, und fuehrt in die Karten
Vorgabe des Eigentuemers: erste Zeile Tankstelle und Strasse, zweite Zeile der Ort,
Tippen oeffnet den Standard-Kartendienst. Vorher eine gewoehnliche Wertzeile mit
120px Wertespalte — "Hermann Mogler Mineraloelg. GmbH, Heidenheimerstr.6, 89551
Koenigsbronn" stand dort vier- bis fuenfzeilig.
Geteilt wird am **letzten** Komma (der Belegparser setzt `"<Strasse>, <PLZ Ort>"`
zusammen), damit ein Zusatz bei der Strasse bleibt; ohne Komma bleibt alles in der
ersten Zeile statt einen Ort zu erfinden. `kartendienstUrl()` waehlt nach Geraet:
`maps.apple.com` auf Apple-Geraeten, sonst `google.com/maps/dir`. Dieselbe Wahl
gilt jetzt auch fuer den Fahrzeugstandort und das Autohaus — drei Verweise, ein
Dienst.
**Zwei CSS-Fallen dabei, beide erst am laufenden Panel sichtbar:**
1. `.tankstellenzeile dt` (0,1,1) verliert gegen `.row dt` im **iOS-Overlay**, das
spaeter geladen wird — die Schriftgroesse blieb 16px. Die Regeln heissen deshalb
`.row.tankstellenzeile …` (0,2,1) und gewinnen unabhaengig von der Reihenfolge.
2. `.rows-fest dt { flex: 0 0 150px }` — in einer **Spalte** ist die Flex-Basis die
HOEHE. Der Titel "Tankstelle" war damit 150px hoch, die Zeile 214px. `flex: 0 0
auto` in derselben Regel hebt es auf. Gemessen 82px danach.
**Merkposten, zum dritten Mal in dieser Datei:** eine Regel, die im Overlay einen
Namensvetter hat, braucht den Wert des Overlays oder hoehere Spezifitaet — und eine
Flex-Angabe bedeutet in einer Spalte etwas anderes als in einer Zeile.
### "Wozu der Zeilenumbruch bei SmartDeal-Ersparnis?"
Kein Platzmangel, sondern ein Rundungsfehler, am Einzelbeleg bei 375px gemessen:
die dt-Spalte war **150,00px** breit, ihr eigener Inhalt **150,20px** — der Text
hinter dem Markenlogo rutschte in eine zweite Zeile, die Zeile wurde 44,8px statt
26,4px hoch. Zwei Zehntel Pixel. `.marke { white-space: nowrap }` in beiden
Codebasen, und die Wertespalte gibt in genau dieser Zeile ihre feste Mindestbreite
auf (`.rows-fest .row-marke dd { min-width: 0 }`); ohne das stuende sie um 10px
ueber. Live bestaetigt: 26,4px, eine Zeile.
### Der Regler schreibt nicht mehr beim Loslassen
Vorgabe des Eigentuemers, und sie ist richtig: derselbe Wert stellt den
Schlaf-Timeout des Dongles — eine Aenderung am Fahrzeug ueber zwei Netze, die
bewusst ausgeloest werden soll und nicht als Nebenwirkung einer Wischgeste.
Apples Richtlinien lassen einen Regler sonst sofort wirken; die Ausnahme ist genau
dieser Fall, und das Muster dafuer ist der bestaetigende Knopf. Er erscheint **nur**,
wenn der Regler wirklich woanders steht (ein dauerhaft sichtbarer, meist
wirkungsloser Knopf waere Beiwerk), daneben "Verwerfen" als schlichter Text —
dieselbe Anordnung wie in der Speicherleiste der App am Seitenende.
Panel: `PAUSENZEIT_ENTWURF` neben `CONFIG.pausenzeit`; der `input`-Zweig zieht nur
Fuellung und Anzeige nach (ein `render()` mitten in der Geste erzeugt den Regler neu
und der Daumen springt aus der Hand), der `change`-Zweig setzt den Entwurf und
zeichnet neu, geschrieben wird erst auf Knopfdruck. App: eigener
`pausenzeitEntwurf`-Zustand **neben** dem Seitenentwurf, und `speichern(zusatz)`
nimmt den Wert als Argument entgegen — `setState` wirkt erst beim naechsten
Durchlauf, der Seitenentwurf trueg im selben Rendern noch den alten.
Live in beiden geprueft (Regler bewegt, Knopf erscheint, "Verwerfen" stellt 15 Min.
wieder her). **"Speichern" bewusst NICHT gedrueckt** — das schriebe einen neuen
Schlaf-Timeout an den echten Dongle.
### Teilen-Erweiterung und Zwischenablage am iPhone
Die ausgelieferte `App.ipa` vom 03.09. wurde entpackt und geprueft, statt zu raten.
**Fast alles ist in Ordnung:** die `.appex` traegt ein echtes arm64-Programm
(110.320 Bytes), App und Erweiterung haben beide `group.app.datametric360` in ihren
Entitlements, das URL-Schema `datametric360` steht im Info.plist, und
`BelegZwischenablagePlugin` ist im App-Programm enthalten.
**Ein Befund, und er erklaert genau "die Erweiterung erscheint nicht":** die
Versionsnummern stimmen nicht ueberein — App `2026.9.3.3`, Erweiterung `1.0`.
Apples App Extension Programming Guide verlangt Gleichheit, und iOS registriert
eine Erweiterung mit abweichender Nummer nicht. `ios-signieren.sh` schrieb die
Version bis dahin nur in `ios/App/App/Info.plist`; `native/teilen/Info.plist`
traegt die 1.0 fest. Jetzt bekommen **beide** Ziele dieselbe
`CFBundleShortVersionString` und `CFBundleVersion`.
Dazu, aus der Lehre von Abschnitt AK (von fuenf Fehlern in der nativen Huelle haben
vier einen erfolgreichen Archivlauf erzeugt): das Skript liest die **fertige .ipa**
gegen — Programm in der `.appex` vorhanden, Versionen gleich, App-Gruppe auf beiden
Zielen — und warnt, statt es dem naechsten Fehlerbericht zu ueberlassen.
Fuer die Zwischenablage kommt ein dritter Weg dazu: `UIPasteboard.itemProviders`.
Manche Apps legen eine Datei nur als **Zusage** ab — dann gibt
`data(forPasteboardType:)` nil zurueck und `urls` bleibt leer, nur der Anbieter
kennt sie. Und `zwischenablage.ts` fragt jetzt `Capacitor.isPluginAvailable()`:
fehlt der native Teil (eine App, die nur ueber OTA gewachsen ist), sagt die Meldung
das, statt "Kein Zugriff auf die Zwischenablage" — die schickt in die falsche
Richtung.
**Was das nicht kann:** ohne Mac laesst sich keine dieser Aenderungen ausprobieren.
Der native Teil kommt ausschliesslich ueber einen Xcode-Lauf aufs Geraet, nie ueber
das OTA-Buendel.
**Bewusst nicht angefasst:** die `NSExtensionActivationRule`. Sie ist heute
grosszuegig (jede Datei, jeder Anhang) und laesst die App im Teilen-Blatt
erscheinen; ein strenges Praedikat waere sauberer, koennte sie aber ungeprueft ganz
verschwinden lassen.
### Fahrt `t-66c4209276e7` bleibt bei 3,0 km
Ausdrueckliche Ansage des Eigentuemers ("Fahrt nicht neu rechnen"). Das Screening
fuellt grundsaetzlich nur leere Felder — der gespeicherte Wert bleibt damit von
selbst stehen.
### Testinstanz: was von dieser Sitzung dort liegenbleibt
* **`f-8852746248fa` traegt jetzt `discount: 12.9`** — von Hand gesetzt, um die
SmartDeal-Zeile ueberhaupt sichtbar zu machen (alle Testbelege hatten `null`).
Sinnvoll fuer die naechste Sichtpruefung, aber es ist eine Aenderung an den Daten
des Containers.
* **`/config/www/dmapp/`** haelt das gebaute Webbuendel; unter
`http://localhost:18123/local/dmapp/index.html` laeuft die App **gleichursprunglich**
zur Testinstanz. Das ist der Weg, die App live zu pruefen, ohne an CORS zu
scheitern: `cors_allowed_origins` in der `configuration.yaml` blieb wirkungslos
(Preflight weiterhin 403), der eingetragene Block ist deshalb wieder entfernt.
+27 -2
View File
@@ -18,12 +18,19 @@
// auch `com.adobe.pdf`. Der Preis ist derselbe wie im Web iOS zeigt beim
// Zugriff seine eigene Einfügen-Rückfrage, und das ist richtig so.
//
// ZWEI FORMEN, IN DENEN EIN PDF IN DER ZWISCHENABLAGE LIEGT
// DREI FORMEN, IN DENEN EIN PDF IN DER ZWISCHENABLAGE LIEGT
// ---------------------------------------------------------
// 1. Als Daten unter `com.adobe.pdf` so legen es die meisten Apps ab.
// 2. Als Datei-URL Mail kopiert einen Anhang oft nur als Verweis auf eine
// temporäre Kopie. Ohne den zweiten Weg bliebe genau der Fall übrig, für
// den der Knopf gedacht ist.
// 3. Als Zusage (`NSItemProvider`) die Datei existiert dann noch gar nicht,
// die kopierende App liefert sie erst auf Anforderung. `data(forPasteboardType:)`
// gibt dabei nil zurück und `urls` bleibt leer; nur der Anbieter kennt sie.
// Das ist Apples eigener, empfohlener Weg und der einzige, der auch eine
// Datei aus einer Sandbox einer anderen App zuverlässig herausgibt Weg 2
// scheitert dort am Lesezugriff. Er ist asynchron, deshalb steht er hier
// zuletzt: die beiden schnellen Wege sollen nicht auf ihn warten.
//
// DIESE DATEI IST VERSIONIERT, ios/ NICHT.
// `npx cap add ios` erzeugt den nativen Ordner neu und würde alles darin
@@ -69,6 +76,24 @@ public class BelegZwischenablagePlugin: CAPPlugin, CAPBridgedPlugin {
}
}
call.resolve([:])
// Dritter Weg: die Datei liegt nur als Zusage vor.
let anbieter = ablage.itemProviders.first {
$0.hasItemConformingToTypeIdentifier(UTType.pdf.identifier)
}
guard let anbieter else {
return call.resolve([:])
}
anbieter.loadDataRepresentation(forTypeIdentifier: UTType.pdf.identifier) { daten, _ in
guard let daten, !daten.isEmpty else {
// Kein Fehler: die Oberfläche sagt zu einem leeren Ergebnis
// ohnehin das Passende (zwischenablage.ts).
return call.resolve([:])
}
let name = anbieter.suggestedName.map { $0.hasSuffix(".pdf") ? $0 : $0 + ".pdf" }
call.resolve([
"base64": daten.base64EncodedString(),
"name": name ?? "beleg.pdf",
])
}
}
}
+50 -1
View File
@@ -172,7 +172,25 @@ VERSION="$(node -p "require('../custom_components/audi_dashboard/manifest.json')
if ! /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION" "$PLIST" 2>/dev/null; then
/usr/libexec/PlistBuddy -c "Add :CFBundleShortVersionString string $VERSION" "$PLIST"
fi
echo " $VERSION"
BUILDNR="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' "$PLIST" 2>/dev/null || echo 1)"
# Die Share-Erweiterung MUSS dieselben beiden Nummern tragen wie die App.
#
# Apples App Extension Programming Guide verlangt das ausdruecklich, und iOS
# haelt sich daran: stimmen CFBundleShortVersionString und CFBundleVersion
# nicht ueberein, registriert das System die Erweiterung nicht - sie taucht im
# Teilen-Blatt dann gar nicht erst auf, ohne jede Fehlermeldung. Genau das war
# in der .ipa vom 03.09.2026 der Fall: App 2026.9.3.3, Erweiterung 1.0 (an der
# ausgelieferten Datei nachgemessen, nicht vermutet - native/teilen/Info.plist
# traegt die 1.0 fest, und dieser Schritt schrieb bis dahin nur $PLIST).
ERW_PLIST="ios/App/DataMetric360Share/Info.plist"
if [ -f "$ERW_PLIST" ]; then
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION" "$ERW_PLIST"
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $BUILDNR" "$ERW_PLIST"
echo " $VERSION ($BUILDNR), App und Erweiterung"
else
echo " $VERSION ($BUILDNR) - Erweiterung nicht gefunden, nur die App"
fi
# Zwischengespeicherte Bereitstellungsprofile dieser App wegraeumen.
#
@@ -252,6 +270,37 @@ xcodebuild -exportArchive \
mkdir -p "$APP/auslieferung"
cp "$AUSGABE/App.ipa" "$APP/auslieferung/App.ipa"
# Am FERTIGEN Buendel gegenlesen, nicht am Projekt.
#
# Abschnitt AK der AGENTS.md: von fuenf Fehlern in der nativen Huelle haben
# vier einen erfolgreichen Archivlauf erzeugt und sind erst am entpackten
# .ipa aufgefallen. Geprueft wird hier genau das, was still schiefgehen kann:
# eine Erweiterung ohne Programm, ohne App-Gruppe, oder mit abweichender
# Versionsnummer (dann registriert iOS sie nicht und sie fehlt im
# Teilen-Blatt).
echo "==> Fertige .ipa gegenlesen"
PRUEF="$(mktemp -d)"
if unzip -q "$APP/auslieferung/App.ipa" -d "$PRUEF"; then
ERW="$PRUEF/Payload/App.app/PlugIns/DataMetric360Share.appex"
if [ ! -x "$ERW/DataMetric360Share" ] && [ ! -f "$ERW/DataMetric360Share" ]; then
echo " WARNUNG: die Erweiterung hat kein Programm - sie wird nichts tun."
fi
ERW_V="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$ERW/Info.plist" 2>/dev/null || echo '?')"
APP_V="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$PRUEF/Payload/App.app/Info.plist" 2>/dev/null || echo '?')"
if [ "$ERW_V" != "$APP_V" ]; then
echo " WARNUNG: Erweiterung $ERW_V, App $APP_V - iOS registriert die"
echo " Erweiterung dann nicht, sie fehlt im Teilen-Blatt."
else
echo " Erweiterung und App tragen beide $APP_V"
fi
for ZIEL in "$PRUEF/Payload/App.app" "$ERW"; do
if ! codesign -d --entitlements :- "$ZIEL" 2>/dev/null | grep -q "group.app.datametric360"; then
echo " WARNUNG: $(basename "$ZIEL") ohne App-Gruppe - Teilen kann nichts uebergeben."
fi
done
fi
rm -rf "$PRUEF"
# Auslieferung ueber die Luft gleich mitschreiben.
#
# manifest.plist und die Installationsseite hingen bisher an einem zweiten,
+151
View File
@@ -0,0 +1,151 @@
/**
* Tests zur Tankstellen-Anzeige und zum Kartendienst.
*
* Die Anschriften stammen aus echten Belegen der Testinstanz (04.09.2026) -
* genau an ihnen ist aufgefallen, dass die Wertespalte zu schmal war und die
* App die Tankstelle ohne Anschrift gar nicht erst nachschlug.
*/
import { afterEach, describe, expect, it, vi } from "vitest"
import {
adresseTeilen,
kartendienstUrl,
tankstelleKandidaten,
tankstelleSuchtext,
tankstelleZeilen,
tankstelleZiel,
} from "./tankstelle"
function alsGeraet(kennung: string) {
vi.stubGlobal("navigator", { userAgent: kennung, platform: "" })
}
afterEach(() => {
vi.unstubAllGlobals()
})
describe("adresseTeilen", () => {
it("trennt Straße und Ort am Komma", () => {
expect(adresseTeilen("Pascalstr. 8, 85057 Ingolstadt")).toEqual({
strasse: "Pascalstr. 8",
ort: "85057 Ingolstadt",
})
})
it("trennt am LETZTEN Komma - Zusätze bleiben bei der Straße", () => {
expect(adresseTeilen("Nürnberger Str.74, Gewerbepark, 91522 Ansbach")).toEqual({
strasse: "Nürnberger Str.74, Gewerbepark",
ort: "91522 Ansbach",
})
})
it("erfindet ohne Komma keinen Ort", () => {
expect(adresseTeilen("Heidenheimerstr.6")).toEqual({ strasse: "Heidenheimerstr.6", ort: "" })
})
it("verträgt leer und null", () => {
expect(adresseTeilen(null)).toEqual({ strasse: "", ort: "" })
expect(adresseTeilen(" ")).toEqual({ strasse: "", ort: "" })
})
})
describe("tankstelleZeilen", () => {
it("setzt Name und Straße in die erste Zeile, den Ort in die zweite", () => {
expect(tankstelleZeilen("A. Zrenner GmbH", "Pascalstr. 8, 85057 Ingolstadt")).toEqual({
oben: "A. Zrenner GmbH, Pascalstr. 8",
unten: "85057 Ingolstadt",
})
})
it("kommt ohne Anschrift aus", () => {
expect(tankstelleZeilen("Testtankstelle", null)).toEqual({
oben: "Testtankstelle",
unten: "",
})
})
it("kommt ohne Namen aus", () => {
expect(tankstelleZeilen("", "Pascalstr. 8, 85057 Ingolstadt")).toEqual({
oben: "Pascalstr. 8",
unten: "85057 Ingolstadt",
})
})
it("sagt „unbekannt“, wenn der Beleg nichts hergibt", () => {
expect(tankstelleZeilen(null, null)).toEqual({ oben: "unbekannt", unten: "" })
})
})
describe("tankstelleKandidaten", () => {
it("stellt die Anschrift voran und nimmt den Namen NICHT dazu", () => {
// Gemessen am 04.09.2026: Nominatim findet die Anschrift, mit dem
// Betreibernamen davor findet es nichts.
expect(tankstelleKandidaten("A. Zrenner GmbH", "Pascalstr. 8, 85057 Ingolstadt")).toEqual([
"Pascalstr. 8, 85057 Ingolstadt",
"A. Zrenner GmbH",
])
})
it("laesst leere Teile weg", () => {
expect(tankstelleKandidaten("Shell, SÖLDEN", null)).toEqual(["Shell, SÖLDEN"])
expect(tankstelleKandidaten(null, null)).toEqual([])
})
})
describe("tankstelleZiel", () => {
it("nimmt fuer den Kartendienst Name und Anschrift zusammen", () => {
expect(tankstelleZiel("A. Zrenner GmbH", "Pascalstr. 8, 85057 Ingolstadt")).toBe(
"A. Zrenner GmbH, Pascalstr. 8, 85057 Ingolstadt",
)
})
})
describe("tankstelleSuchtext", () => {
it("ist der erste Kandidat, also die Anschrift", () => {
expect(tankstelleSuchtext("A. Zrenner GmbH", "Pascalstr. 8, 85057 Ingolstadt")).toBe(
"Pascalstr. 8, 85057 Ingolstadt",
)
})
it("faellt auf den Namen zurueck, wenn keine Anschrift im Beleg steht", () => {
// Genau der Fall, in dem die App bis zum 04.09.2026 gar nichts nachschlug,
// das Panel dagegen schon (f.station_address || f.station_name).
expect(tankstelleSuchtext("Shell, SÖLDEN", null)).toBe("Shell, SÖLDEN")
})
it("ist leer, wenn beides fehlt", () => {
expect(tankstelleSuchtext(null, "")).toBe("")
})
})
describe("kartendienstUrl", () => {
it("nimmt auf dem iPhone Apple Karten", () => {
alsGeraet("Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X)")
expect(kartendienstUrl("A. Zrenner GmbH", "Pascalstr. 8, 85057 Ingolstadt")).toBe(
"https://maps.apple.com/?daddr=A.%20Zrenner%20GmbH%2C%20Pascalstr.%208%2C%2085057%20Ingolstadt",
)
})
it("zielt mit Koordinaten auf den Punkt und beschriftet ihn", () => {
alsGeraet("Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X)")
expect(
kartendienstUrl("A. Zrenner GmbH", "Pascalstr. 8, 85057 Ingolstadt", {
lat: 48.7869101,
lon: 11.39767,
}),
).toBe("https://maps.apple.com/?daddr=48.7869101%2C11.39767&q=A.%20Zrenner%20GmbH")
})
it("nimmt sonst Google Maps", () => {
alsGeraet("Mozilla/5.0 (X11; Linux x86_64)")
expect(kartendienstUrl(null, "Pascalstr. 8, 85057 Ingolstadt")).toBe(
"https://www.google.com/maps/dir/?api=1&destination=Pascalstr.%208%2C%2085057%20Ingolstadt",
)
})
it("bleibt null, wenn nichts bekannt ist", () => {
alsGeraet("Mozilla/5.0 (iPhone)")
expect(kartendienstUrl(null, null)).toBeNull()
})
})
+137
View File
@@ -0,0 +1,137 @@
/**
* Tankstelle eines Belegs: Anzeige in zwei Zeilen und der Weg in den
* Kartendienst des Geräts.
*
* WARUM ZWEI ZEILEN
* -----------------
* Bis zum 04.09.2026 stand die Tankstelle als gewöhnliche Wertezeile rechts in
* der schmalen Wertespalte: Name oben, die komplette Anschrift als Kleintext
* darunter. Gemessen an der echten Kachel blieben dafür 120 px - "Hermann
* Mogler Mineralölg. GmbH, Heidenheimerstr.6, 89551 Königsbronn" brach dort in
* vier bis fünf Zeilen um. Die Aufteilung hier ist die vom Eigentümer
* verlangte: erste Zeile Tankstelle und Straße, zweite Zeile der Ort.
*
* WIE DIE ANSCHRIFT GETEILT WIRD
* ------------------------------
* Der Belegparser setzt sie als `"<Straße>, <PLZ Ort>"` zusammen
* (shell_beleg_parser.py). Geteilt wird deshalb am LETZTEN Komma: ein Name wie
* "Str. 5, Gewerbepark, 85057 Ingolstadt" behält so seinen Zusatz bei der
* Straße statt beim Ort. Ohne Komma bleibt alles in der ersten Zeile - lieber
* ein zu langer erster Eintrag als ein erfundener Ort.
*
* KARTENDIENST STATT EINES FESTEN ANBIETERS
* -----------------------------------------
* "Klick auf die Tankstelle soll den Standard-Kartendienst öffnen" (Eigentümer,
* 04.09.2026). Auf Apple-Geräten ist das Karten, sonst Google Maps - beides
* über die jeweilige Web-Adresse, die das System an die installierte App
* weiterreicht. Ein `geo:`-Verweis wäre der sauberere Weg, aber Safari kennt
* ihn nicht, und ein Verweis, der auf dem Hauptgerät ins Leere läuft, ist
* keiner.
*/
export interface Koordinate {
lat: number
lon: number
}
/** `"Pascalstr. 8, 85057 Ingolstadt"` → Straße und Ort. */
export function adresseTeilen(adresse: string | null | undefined): {
strasse: string
ort: string
} {
const roh = (adresse ?? "").trim()
if (!roh) return { strasse: "", ort: "" }
const komma = roh.lastIndexOf(",")
if (komma < 0) return { strasse: roh, ort: "" }
return { strasse: roh.slice(0, komma).trim(), ort: roh.slice(komma + 1).trim() }
}
/**
* Die beiden Zeilen der Tankstellen-Angabe. `oben` ist nie leer: steht weder
* Name noch Anschrift im Beleg, heißt es dort "unbekannt" - dieselbe Auskunft,
* die die Zeile vorher gab.
*/
export function tankstelleZeilen(
name: string | null | undefined,
adresse: string | null | undefined,
): { oben: string; unten: string } {
const { strasse, ort } = adresseTeilen(adresse)
const oben = [(name ?? "").trim(), strasse].filter(Boolean).join(", ")
return { oben: oben || "unbekannt", unten: ort }
}
/**
* Suchbegriffe für die Geokodierung, in der Reihenfolge, in der sie versucht
* werden: erst die Anschrift, dann der Name.
*
* NICHT beides zusammen. Am 04.09.2026 gemessen: `"Pascalstr. 8, 85057
* Ingolstadt"` findet Nominatim, `"A. Zrenner GmbH, Pascalstr. 8, 85057
* Ingolstadt"` nicht — der Betreibername steht in keiner Karte und lässt die
* Freitextsuche ins Leere laufen. Genau daran ist die erste Fassung dieser
* Funktion gescheitert, sichtbar als „Keine Position zur Tankstelle" bei einem
* Beleg, dessen Anschrift sich vorher hat auflösen lassen.
*
* Der Name allein ist trotzdem wertvoll: `"Shell, SÖLDEN"` findet Nominatim
* (46.9756/11.0111) — deshalb der zweite Versuch statt gar keinem.
*/
export function tankstelleKandidaten(
name: string | null | undefined,
adresse: string | null | undefined,
): string[] {
return [(adresse ?? "").trim(), (name ?? "").trim()].filter(Boolean)
}
/** Der erste Suchbegriff - leer heißt: der Beleg nennt gar nichts. */
export function tankstelleSuchtext(
name: string | null | undefined,
adresse: string | null | undefined,
): string {
return tankstelleKandidaten(name, adresse)[0] ?? ""
}
/**
* Das Ziel für den Kartendienst. Hier gehören Name UND Anschrift zusammen:
* Apple Karten und Google Maps kennen Betriebsnamen und treffen damit die
* Zapfsäule statt des Nachbargebäudes - beide sind darin deutlich besser als
* Nominatims Freitextsuche.
*/
export function tankstelleZiel(
name: string | null | undefined,
adresse: string | null | undefined,
): string {
return [(name ?? "").trim(), (adresse ?? "").trim()].filter(Boolean).join(", ")
}
/** Apple-Gerät? Dort ist Karten der Standarddienst. */
function apfelgeraet(): boolean {
if (typeof navigator === "undefined") return false
const kennung = `${navigator.userAgent || ""} ${navigator.platform || ""}`
return /iPhone|iPad|iPod|Macintosh|Mac OS X/i.test(kennung)
}
/**
* Verweis in den Kartendienst - `null`, wenn nichts bekannt ist, wohin.
*
* Mit Koordinaten wird auf sie gezielt und der Name nur als Beschriftung
* mitgegeben; ohne sie muss die Anschrift als Suchtext genügen.
*/
export function kartendienstUrl(
name: string | null | undefined,
adresse: string | null | undefined,
pos?: Koordinate | null,
): string | null {
const beschriftung = (name ?? "").trim()
const ziel = pos ? `${pos.lat},${pos.lon}` : tankstelleZiel(name, adresse)
if (!ziel) return null
if (apfelgeraet()) {
const q = pos && beschriftung ? `&q=${encodeURIComponent(beschriftung)}` : ""
return `https://maps.apple.com/?daddr=${encodeURIComponent(ziel)}${q}`
}
return `https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent(ziel)}`
}
/** Wie oben, aber für einen reinen Punkt (Fahrzeugstandort, Autohaus). */
export function kartendienstUrlZuPunkt(pos: Koordinate | null | undefined): string | null {
if (!pos) return null
return kartendienstUrl(null, null, pos)
}
+13 -2
View File
@@ -67,12 +67,23 @@ function base64ZuDatei(base64: string, name: string): File {
*/
export async function pdfAusZwischenablage(): Promise<File> {
if (Capacitor.isNativePlatform()) {
/* Der native Teil steckt in der .ipa, nicht im OTA-Bündel: eine App, die
nur über die Aktualisierung in der App gewachsen ist, hat ihn nicht.
Genau das war bis zum 04.09.2026 nicht zu erkennen - der Fehlschlag kam
mit demselben Text wie eine abgelehnte Rückfrage ("Kein Zugriff"), und
der schickt in die falsche Richtung. `isPluginAvailable` fragt die
Brücke, ob die Klasse im Programm ueberhaupt vorhanden ist. */
if (!Capacitor.isPluginAvailable("BelegZwischenablage")) {
throw new ZwischenablageFehler(
"Diese App-Fassung enthält den nativen Teil noch nicht - er kommt nur mit " +
'einer neu installierten App, nicht über die Aktualisierung. Bis dahin "Datei auswählen".',
)
}
let ergebnis: { base64?: string; name?: string }
try {
ergebnis = await Nativ.pdfHolen()
} catch {
// Abgelehnte Einfügen-Rückfrage oder ein Gerät ohne das Plugin (eine
// .ipa von vor dem 02.09.2026).
// Abgelehnte Einfügen-Rückfrage des Systems.
throw new ZwischenablageFehler(
'Kein Zugriff auf die Zwischenablage - bitte "Datei auswählen".',
)
+41 -6
View File
@@ -58,6 +58,12 @@ export function Einstellungen({
// eigener Entwurf, der beim Speichern mitgeschrieben wird.
const [erstzulassung, setzeErstzulassung] = useState<string | null>(null)
const [entwurf, setzeEntwurf] = useState<EinstellungenWerte | null>(null)
/* Der Regler "Fahrt beenden" fuehrt einen EIGENEN Entwurf, nicht den der
Seite. Grund: sein Wert stellt zugleich den Schlaf-Timeout des Dongles -
eine Aenderung am Fahrzeug, die ueber zwei Netze geht. Sie soll bewusst
ausgeloest werden, nicht als Nebenwirkung des Loslassens (Vorgabe des
Eigentuemers, 04.09.2026). `null` heisst: nichts angefasst. */
const [pausenzeitEntwurf, setzePausenzeitEntwurf] = useState<number | null>(null)
const [laeuft, setzeLaeuft] = useState(false)
const [gespeichert, setzeGespeichert] = useState(false)
const [importOffen, setzeImportOffen] = useState(false)
@@ -187,6 +193,8 @@ export function Einstellungen({
if (!einstellungen || !fahrzeug) return null
const werte = entwurf ?? einstellungen
const pausenzeit = pausenzeitEntwurf ?? werte.pausenzeitMin
const pausenzeitGeaendert = pausenzeitEntwurf !== null && pausenzeitEntwurf !== werte.pausenzeitMin
const erstzulassungWert = erstzulassung ?? fahrzeug.erstzulassung
// Bei Winterraedern zeigt die Seitenansicht das Winterfoto - wie bildInfo()
// im Panel, das hier dieselbe Vorschau liefert.
@@ -209,17 +217,22 @@ export function Einstellungen({
const aendern = (teil: Partial<EinstellungenWerte>) =>
setzeEntwurf({ ...werte, ...teil })
const speichern = async () => {
if (!entwurf) return
/* `zusatz` kommt vom Regler der Kachel "Fahrt beenden": der fuehrt einen
eigenen Entwurf und uebergibt ihn erst beim Druck auf Speichern. Ohne den
Umweg wuerde `entwurf` im selben Rendern noch den alten Wert tragen -
setState wirkt erst beim naechsten Durchlauf. */
const speichern = async (zusatz?: Partial<EinstellungenWerte>) => {
if (!entwurf && !zusatz) return
setzeLaeuft(true)
setzeGespeichert(false)
try {
await profilSpeichern({
einstellungen: entwurf,
einstellungen: { ...werte, ...(zusatz ?? {}) },
...(erstzulassung !== null ? { fahrzeug: { ...fahrzeug, erstzulassung } } : {}),
})
setzeEntwurf(null)
setzeErstzulassung(null)
setzePausenzeitEntwurf(null)
setzeGespeichert(true)
} finally {
setzeLaeuft(false)
@@ -462,11 +475,33 @@ export function Einstellungen({
min={1}
max={60}
schritt={1}
wert={werte.pausenzeitMin}
onChange={(min) => aendern({ pausenzeitMin: min })}
anzeige={werte.pausenzeitMin + " Min."}
wert={pausenzeit}
onChange={setzePausenzeitEntwurf}
anzeige={pausenzeit + " Min."}
aria-label="Minuten bis zum automatischen Fahrtende"
/>
{/* Erscheint erst, wenn der Regler wirklich woanders steht - ein
dauerhaft sichtbarer, meist wirkungsloser Knopf waere Beiwerk. Die
Anordnung folgt der Speicherleiste am Seitenende: bestaetigende
Aktion als voller Knopf, der Ausweg daneben als schlichter Text. */}
{pausenzeitGeaendert && (
<div className="dm-speicherleiste dm-speicherleiste--kachel">
<ActionButton
onClick={() => void speichern({ pausenzeitMin: pausenzeitEntwurf as number })}
disabled={laeuft}
>
{laeuft ? "Speichere …" : "Speichern"}
</ActionButton>
<button
type="button"
className="dm-textknopf"
onClick={() => setzePausenzeitEntwurf(null)}
disabled={laeuft}
>
Verwerfen
</button>
</div>
)}
{/* Fusszeile statt Info-Knopf: Apples eigenes Muster fuer erklaerenden
Text unter einer Einstellung ist der Footer der Gruppe. */}
<span className="ads-eyebrow" style={{ marginTop: "14px" }}>
+2 -1
View File
@@ -12,6 +12,7 @@ import { useRef, useState } from "react"
import { ActionButton, Feld, Fig, Tile } from "@audi-dash/ui"
import { kartendienstUrl } from "../daten/tankstelle"
import { useDaten } from "../daten/DatenKontext"
import {
inspektionPrognose,
@@ -213,7 +214,7 @@ export function Service({ geheZu }: { geheZu: (name: SeitenName, id?: string) =>
className="ads-action dm-waehlknopf"
target="_blank"
rel="noopener"
href={`https://maps.google.com/?q=${encodeURIComponent(autohaus.adresse ?? "")}`}
href={kartendienstUrl(autohaus.name, autohaus.adresse) ?? undefined}
>
In Karten öffnen
</a>
+4 -3
View File
@@ -12,6 +12,7 @@ import { useEffect, useRef, useState, type PointerEvent as ReactPointerEvent } f
import { useDaten } from "../daten/DatenKontext"
import { koordinatenAufloesen } from "../daten/geokodierung"
import { kartendienstUrlZuPunkt } from "../daten/tankstelle"
import { de } from "../format"
import type { Fahrzeug } from "../daten/profilAdapter"
import { SymbolTanken } from "../symbole"
@@ -450,9 +451,9 @@ export function Standort() {
const koordinaten = fahrzeugPos ? `${de(fahrzeugPos.lat, 4)}, ${de(fahrzeugPos.lon, 4)}` : ""
const adresseAnzeige = !bekannt ? "Kein GPS-Signal vom Fahrzeug" : (adresse ?? koordinaten)
const zustand = standortZustand(fahrzeug)
const kartenlink = fahrzeugPos
? `https://www.google.com/maps/dir/?api=1&destination=${fahrzeugPos.lat},${fahrzeugPos.lon}`
: null
// Kartendienst des Geraets statt fest Google Maps: auf dem iPhone ist das
// Karten. Dieselbe Wahl wie beim Tankstellen-Verweis (daten/tankstelle.ts).
const kartenlink = kartendienstUrlZuPunkt(fahrzeugPos)
const teilen = () => {
if (!kartenlink) return
+56 -5
View File
@@ -14,6 +14,7 @@ import { ActionButton, Feld, Fig, Tile } from "@audi-dash/ui"
import type { Tankvorgang } from "../api"
import { ENTITAETEN } from "../api"
import { useDaten } from "../daten/DatenKontext"
import { kartendienstUrl, tankstelleZeilen } from "../daten/tankstelle"
import { datum, de, deOderStrich, eur, uhrzeit } from "../format"
import type { SeitenName } from "../navigation"
import { KRAFTSTOFFSORTEN, Leerzustand, Wertzeile, Werteliste, bestaetigen } from "./bausteine"
@@ -122,11 +123,7 @@ export function TankDetail({ id, geheZu }: { id: string | undefined; geheZu: (na
wert={`${eur(tank.discount)}`}
/>
)}
<Wertzeile
label="Tankstelle"
wert={tank.station_name ?? "unbekannt"}
zusatz={tank.station_address ?? ""}
/>
<Tankstellenzeile name={tank.station_name} adresse={tank.station_address} />
</>
}
/>
@@ -141,6 +138,60 @@ export function TankDetail({ id, geheZu }: { id: string | undefined; geheZu: (na
)
}
/**
* Tankstelle des Belegs: eigene Zeile über die volle Kachelbreite statt der
* engen Wertespalte, zweizeilig (Tankstelle und Straße / Ort) und antippbar -
* der Tipp führt in den Kartendienst des Geräts. Deckungsgleich mit
* `.tankstellenzeile` im Panel; die Aufteilung stammt vom Eigentümer
* (04.09.2026).
*
* Ohne Ziel bleibt es eine gewöhnliche Zeile: ein Verweis, der nirgendwohin
* führt, wäre schlechter als keiner.
*/
function Tankstellenzeile({
name,
adresse,
}: {
name: string | null | undefined
adresse: string | null | undefined
}) {
const { oben, unten } = tankstelleZeilen(name, adresse)
const url = kartendienstUrl(name, adresse)
const inhalt = (
<>
<span className="dm-tankstellenzeile__ort">
{oben}
{unten && <small>{unten}</small>}
</span>
{url && (
<svg className="dm-blatt__chevron" viewBox="0 0 6 10" aria-hidden="true">
<polyline points="1,1 5,5 1,9" />
</svg>
)}
</>
)
return (
<div className="dm-tankstellenzeile">
<dt>Tankstelle</dt>
<dd>
{url ? (
<a
className="dm-tankstellenzeile__inhalt"
href={url}
target="_blank"
rel="noopener"
aria-label={`${oben} in Karten öffnen`}
>
{inhalt}
</a>
) : (
<span className="dm-tankstellenzeile__inhalt">{inhalt}</span>
)}
</dd>
</div>
)
}
/** "YYYY-MM-DDTHH:mm" in Ortszeit für ein <input type="datetime-local">. */
function alsDatetimeLocal(iso: string): string {
const d = new Date(iso)
+43 -11
View File
@@ -3,15 +3,29 @@
*
* Die Koordinaten stehen nicht im Datensatz der Beleg trägt nur eine
* Anschrift, und das Backend füllt `station_lat`/`station_lon` nie. Sie werden
* deshalb aus der Adresse aufgelöst (`adresseAufloesen()`), genau wie im
* Panel. Ohne Adresse oder ohne Treffer bleibt der ehrliche Leerzustand
* stehen, statt irgendeinen Kartenausschnitt zu zeigen.
* deshalb aus der Anschrift aufgelöst (`adresseAufloesen()`), genau wie im
* Panel.
*
* GESUCHT WIRD AUCH NACH DEM NAMEN
* --------------------------------
* Bis zum 04.09.2026 wurde hier ausschliesslich `station_address` nachgeschlagen.
* Nur trägt die die Mehrzahl der Belege gar nicht: von 16 Tankvorgängen der
* Testinstanz haben vier eine Anschrift, alle übrigen bestenfalls einen Namen.
* Für die blieb die Karte dauerhaft leer ("Einzelbeleg/Map lädt die Position
* der Tankstelle nicht", Eigentümer 04.09.2026) - während das Panel längst
* `f.station_address || f.station_name` nachschlug. Hier steht jetzt dasselbe,
* über `tankstelleSuchtext()`: Anschrift bevorzugt, sonst der Name.
*
* Ohne beides bleibt der ehrliche Leerzustand stehen, statt irgendeinen
* Kartenausschnitt zu zeigen - und er sagt jetzt auch, WAS fehlt: eine
* Anschrift, die es nicht gibt, ist etwas anderes als eine, die niemand findet.
*/
import { useEffect, useState } from "react"
import { adresseAufloesen } from "../daten/geokodierung"
import type { Koordinate } from "../daten/geokodierung"
import { tankstelleKandidaten } from "../daten/tankstelle"
import { Leerzustand } from "./bausteine"
import { Karte } from "./Karte"
@@ -24,19 +38,33 @@ export function TankstellenKarte({
}) {
const [position, setzePosition] = useState<Koordinate | null>(null)
const [geprueft, setzeGeprueft] = useState(false)
// Als eine Zeichenkette, damit der Effekt nicht bei jedem Rendern neu läuft -
// ein frisches Array wäre bei jedem Durchlauf ein anderer Wert.
const suche = tankstelleKandidaten(name, adresse).join("\n")
useEffect(() => {
let abgebrochen = false
setzeGeprueft(false)
void adresseAufloesen(adresse).then((treffer) => {
if (abgebrochen) return
setzePosition(treffer)
setzeGeprueft(true)
})
const versuchen = async () => {
for (const begriff of suche ? suche.split("\n") : []) {
const treffer = await adresseAufloesen(begriff)
if (abgebrochen) return
if (treffer) {
setzePosition(treffer)
setzeGeprueft(true)
return
}
}
if (!abgebrochen) {
setzePosition(null)
setzeGeprueft(true)
}
}
void versuchen()
return () => {
abgebrochen = true
}
}, [adresse])
}, [suche])
// Wie im Panel (tankstelleAufKarteZeigen): die CI-Nadel mit Name und
// Anschrift im Popup, dazu der Knopf, der wieder darauf zentriert.
@@ -45,11 +73,15 @@ export function TankstellenKarte({
// Solange die Auflösung läuft: eine leere Kartenfläche, wie im Panel (die
// Karte steht dort schon, die Nadel kommt nach). Bewusst nicht die
// Ladeanzeige der Seite - die meldet „der Bildschirm ist noch nicht da".
if (!geprueft) return <div className="dm-kartenplatz" />
if (!geprueft && suche) return <div className="dm-kartenplatz" />
return (
<Leerzustand
titel="Keine Position zur Tankstelle"
text="Zu dieser Anschrift ließ sich kein Kartenpunkt finden. Sobald der Beleg eine auflösbare Adresse trägt, erscheint die Tankstelle hier."
text={
suche
? "Zu dieser Anschrift ließ sich kein Kartenpunkt finden. Trag im Beleg unter „Werte bearbeiten“ eine genauere Anschrift ein, dann erscheint die Tankstelle hier."
: "Dieser Beleg nennt weder Tankstelle noch Anschrift. Trag sie unter „Werte bearbeiten“ nach, dann erscheint die Tankstelle hier."
}
/>
)
}
+71
View File
@@ -897,6 +897,55 @@ button.dm-serviceblock:active {
text-align: right;
}
/* Tankstelle im Einzelbeleg (Vorgabe des Eigentuemers, 04.09.2026).
Vorher eine gewoehnliche Wertzeile: Name rechts in der Wertespalte, die
ganze Anschrift als Kleintext darunter. Die Spalte ist rund 120px breit -
"Hermann Mogler Mineraloelg. GmbH, Heidenheimerstr.6, 89551 Koenigsbronn"
stand dort vier- bis fuenfzeilig. Hier bekommt sie die volle Kachelbreite
unter der Beschriftung, zwei Zeilen (Tankstelle und Strasse / Ort), und
fuehrt beim Tippen in den Kartendienst - deshalb der Chevron rechts.
Deckungsgleich mit .tankstellenzeile im Panel. */
.dm-tankstellenzeile {
display: flex;
flex-direction: column;
gap: 4px;
padding: 11px 0;
border-bottom: 0;
}
.dm-tankstellenzeile dt {
font-size: 13px;
color: var(--fg2);
}
.dm-tankstellenzeile dd {
margin: 0;
}
.dm-tankstellenzeile__inhalt {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--sp-3);
color: inherit;
text-decoration: none;
}
a.dm-tankstellenzeile__inhalt:active {
opacity: 0.6;
}
.dm-tankstellenzeile__ort {
display: block;
font-size: 16px;
color: var(--fg);
line-height: 1.35;
min-width: 0;
}
.dm-tankstellenzeile__ort small {
display: block;
margin-top: 2px;
font-size: 13px;
color: var(--fg3);
}
.dm-abschnitt {
display: flex;
align-items: baseline;
@@ -1531,6 +1580,20 @@ button.dm-listenzeile {
border-top: 1px solid var(--line);
}
/* Dieselbe Leiste innerhalb einer Kachel (Regler "Fahrt beenden"): nicht
klebend, kein eigener Untergrund, keine Trennlinie - sie gehoert zur Kachel
und nicht an den Bildschirmrand. */
.dm-speicherleiste--kachel {
position: static;
margin-top: var(--sp-3);
padding: 0;
background: none;
border-top: 0;
}
.dm-speicherleiste--kachel > :first-child {
flex: 1 1 auto;
}
.dm-szene__zusatz {
min-width: 0;
}
@@ -1913,10 +1976,18 @@ button.dm-listenzeile {
/* `.marke` im Panel: Bildmarke und Text nebeneinander, z. B. vor
SmartDeal-Ersparnis" auf dem Einzelbeleg. */
/* white-space:nowrap: ohne die brach "SmartDeal-Ersparnis" neben dem
Markenlogo um. Der Grund ist kein Platzmangel, sondern ein Rundungsfehler -
am Panel gemessen (04.09.2026): die Spalte war 150,00px breit, ihr eigener
Inhalt 150,20px, also brach der Text nach dem Logo in eine zweite Zeile und
die Zeile wurde 44,8px statt 26,4px hoch. Die Zeile hat reichlich Platz, sie
darf ihn nur nicht abgeschnitten bekommen. (Im Panel kommt dazu, dass die
Wertespalte dort eine feste Mindestbreite hat - siehe .row-marke.) */
.dm-marke {
display: inline-flex;
align-items: center;
gap: 6px;
white-space: nowrap;
}
.dm-marke img {
@@ -1 +1 @@
{"version":"2026.9.4.1","sha256":"473807fbe06bc94882a3b53fa76d167a64a15d9470c0a6f993c7042a8bf009b6","bytes":270085,"gebaut":"2026-09-04T00:09:13Z"}
{"version":"2026.9.4.5","sha256":"ffea00a94ac842d8b6b233ebd91a1a97252c4b7b6a952c336ca7a6218eceef1e","bytes":270888,"gebaut":"2026-09-04T07:57:27Z"}
@@ -644,6 +644,11 @@ function ics(titel, datum, ort, notiz) {
let MAP = null, TILE = null, LEAFLET_LADEND = null;
// Zielpunkt des Zentrieren-Knopfs im Einzelbeleg (siehe mapBoxMitZentrieren).
let BELEG_STATION_POS = null;
/* Merker, ob zu EINEM Beleg überhaupt eine Position zu finden war. Der Abruf
ist asynchron, die Ansicht aber schon gezeichnet - ohne diesen Merker
stünde bei einem erfolglosen Nachschlag dauerhaft eine Karte ohne Nadel da
(samt Knopf, der nie scharf wird). `{ id, zustand: "keine" }`, sonst null. */
let BELEG_KARTE = null;
/* 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
@@ -843,18 +848,29 @@ async function initMap() {
} else if (route.name === "fill") {
BELEG_STATION_POS = null; // gehört zum vorherigen Beleg, sonst zentriert der Knopf falsch
const f = FILLS.find((x) => x.tank_id === route.id);
const suche = f ? tankstelleSuchtext(f.station_name, f.station_address) : "";
if (f && f.station_lat != null) {
tankstelleAufKarteZeigen(MAP, { lat: f.station_lat, lon: f.station_lon }, f);
} else if (f && (f.station_address || f.station_name)) {
/* Ohne Koordinaten die Anschrift vom Beleg nachschlagen. Bis die Antwort
da ist, bleibt der neutrale Ausschnitt stehen - und die Karte könnte
inzwischen zu einer anderen Ansicht gehören, deshalb vor dem Zeichnen
prüfen, ob wir noch auf demselben Beleg sind. */
} else if (suche) {
/* Ohne Koordinaten die Anschrift vom Beleg nachschlagen - und wenn der
Beleg keine trägt, den Namen (dieselbe Reihenfolge wie in der App,
tankstelleSuchtext). Bis die Antwort da ist, bleibt der neutrale
Ausschnitt stehen - und die Karte könnte inzwischen zu einer anderen
Ansicht gehören, deshalb vor dem Zeichnen prüfen, ob wir noch auf
demselben Beleg sind. */
MAP.setView(kartenMittelpunkt(), 11);
const karte = MAP, belegId = route.id;
adresseAufloesen(f.station_address || f.station_name).then((pos) => {
if (!pos || MAP !== karte || route.name !== "fill" || route.id !== belegId) return;
tankstelleAufKarteZeigen(karte, pos, f);
tankstellePosition(f.station_name, f.station_address).then((pos) => {
if (MAP !== karte || route.name !== "fill" || route.id !== belegId) return;
if (pos) return tankstelleAufKarteZeigen(karte, pos, f);
/* Kein Treffer: statt eines beliebigen Ausschnitts mit totem
Zentrieren-Knopf die ehrliche Auskunft. Genau die beiden Befunde
des Eigentümers vom 04.09.2026 - "Map lädt die Position der
Tankstelle nicht" und "der Button auf der Map funktioniert nicht"
waren dieselbe Stelle: eine Karte ohne Nadel und ein Knopf, der
nie scharf wurde. */
BELEG_KARTE = { id: belegId, zustand: "keine" };
render();
});
} else {
MAP.setView(kartenMittelpunkt(), 11);
@@ -1210,6 +1226,81 @@ async function adresseAufloesen(adresse) {
return treffer;
}
/* ------------------------------------------------------- Tankstelle
Anzeige in zwei Zeilen und der Weg in den Kartendienst des Geraets.
Wortgleich mit companion-app/src/daten/tankstelle.ts - dort steht die
ausfuehrliche Begruendung.
Kurz: die Anschrift kommt vom Belegparser als "<Strasse>, <PLZ Ort>";
geteilt wird am LETZTEN Komma, damit ein Zusatz bei der Strasse bleibt.
Erste Zeile Tankstelle und Strasse, zweite Zeile der Ort (Vorgabe des
Eigentuemers, 04.09.2026). Ohne Komma bleibt alles in der ersten Zeile -
lieber ein langer erster Eintrag als ein erfundener Ort. */
function adresseTeilen(adresse) {
const roh = (adresse || "").trim();
if (!roh) return { strasse: "", ort: "" };
const komma = roh.lastIndexOf(",");
if (komma < 0) return { strasse: roh, ort: "" };
return { strasse: roh.slice(0, komma).trim(), ort: roh.slice(komma + 1).trim() };
}
function tankstelleZeilen(name, adresse) {
const { strasse, ort } = adresseTeilen(adresse);
const oben = [(name || "").trim(), strasse].filter(Boolean).join(", ");
return { oben: oben || "unbekannt", unten: ort };
}
/* Suchbegriffe fuer die Geokodierung, in der Reihenfolge des Versuchs: erst
die Anschrift, dann der Name. NICHT beides zusammen - am 04.09.2026
gemessen: "Pascalstr. 8, 85057 Ingolstadt" findet Nominatim, mit dem
Betreibernamen davor nicht. Der Name ALLEIN ist trotzdem wertvoll
("Shell, SOELDEN" findet es), deshalb der zweite Versuch. */
function tankstelleKandidaten(name, adresse) {
return [(adresse || "").trim(), (name || "").trim()].filter(Boolean);
}
/** Der erste Suchbegriff - leer heisst: der Beleg nennt gar nichts. */
function tankstelleSuchtext(name, adresse) {
return tankstelleKandidaten(name, adresse)[0] || "";
}
/* Ziel fuer den Kartendienst. Hier gehoeren Name UND Anschrift zusammen:
Apple Karten und Google Maps kennen Betriebsnamen und treffen damit die
Zapfsaeule statt des Nachbargebaeudes. */
function tankstelleZiel(name, adresse) {
return [(name || "").trim(), (adresse || "").trim()].filter(Boolean).join(", ");
}
/* Die Kandidaten der Reihe nach nachschlagen, erster Treffer gewinnt. */
async function tankstellePosition(name, adresse) {
for (const begriff of tankstelleKandidaten(name, adresse)) {
const pos = await adresseAufloesen(begriff);
if (pos) return pos;
}
return null;
}
/** Apple-Geraet? Dort ist Karten der Standarddienst. */
function apfelgeraet() {
const kennung = `${navigator.userAgent || ""} ${navigator.platform || ""}`;
return /iPhone|iPad|iPod|Macintosh|Mac OS X/i.test(kennung);
}
function kartendienstUrl(name, adresse, pos) {
const beschriftung = (name || "").trim();
const ziel = pos ? `${pos.lat},${pos.lon}` : tankstelleZiel(name, adresse);
if (!ziel) return null;
if (apfelgeraet()) {
const q = pos && beschriftung ? `&q=${encodeURIComponent(beschriftung)}` : "";
return `https://maps.apple.com/?daddr=${encodeURIComponent(ziel)}${q}`;
}
return `https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent(ziel)}`;
}
function kartendienstUrlZuPunkt(pos) {
return pos ? kartendienstUrl(null, null, pos) : null;
}
/* Setzt Nadel und Ausschnitt, sobald die Koordinaten da sind. Getrennt von
initMap(), weil der Abruf asynchron ist und die Karte bis dahin schon
steht. */
@@ -3107,13 +3198,24 @@ function vEinst() {
Dieselben Felder gibt es unter Einstellungen Geräte &amp; Dienste Audi Dashboard
Konfigurieren.</span>
</div>` : ""}
<div class="tile"><span class="label">Fahrt beenden</span>
${(() => {
/* Der Regler schreibt NICHT mehr beim Loslassen. Sein Wert stellt zugleich
den Schlaf-Timeout des Dongles - eine Änderung am Fahrzeug, die über zwei
Netze geht; die soll bewusst ausgelöst werden und nicht als Nebenwirkung
einer Wischgeste entstehen (Vorgabe des Eigentümers, 04.09.2026). Der
Knopf erscheint erst, wenn der Regler wirklich woanders steht - ein
dauerhaft sichtbarer, meist wirkungsloser Knopf wäre Beiwerk. */
const minuten = PAUSENZEIT_ENTWURF ?? CONFIG.pausenzeit;
const geaendert = PAUSENZEIT_ENTWURF !== null && PAUSENZEIT_ENTWURF !== CONFIG.pausenzeit;
return `<div class="tile"><span class="label">Fahrt beenden</span>
<div class="schieber" style="margin-top:14px">
<input type="range" min="1" max="60" step="1" value="${CONFIG.pausenzeit}"
style="--fuell:${((CONFIG.pausenzeit - 1) / 59) * 100}%" data-pausenzeit
<input type="range" min="1" max="60" step="1" value="${minuten}"
style="--fuell:${((minuten - 1) / 59) * 100}%" data-pausenzeit
aria-label="Minuten bis zum automatischen Fahrtende">
<output>${CONFIG.pausenzeit} Min.</output>
<output>${minuten} Min.</output>
</div>
${geaendert ? `<button class="aktion primaer" style="margin-top:14px" data-pausenzeitspeichern>Speichern</button>
<button class="aktion" data-pausenzeitverwerfen>Verwerfen</button>` : ""}
<span class="label" style="margin-top:14px;color:var(--fg2);letter-spacing:0;
text-transform:none;font-size:12.5px;line-height:1.6">
Hier können Sie einstellen, wie viele Minuten nach Ausschalten der Zündung die
@@ -3121,7 +3223,8 @@ function vEinst() {
des Dongles: so lange bleibt er nach dem Abstellen wach und wartet darauf,
dass es gleich weitergeht. Danach schläft er das spart Strom, kostet beim
Fortsetzen aber ein paar Sekunden fürs Aufwachen.</span>
</div>
</div>`;
})()}
${DONGLE && DONGLE.aktiv ? `<div class="tile"><span class="label">Dongle</span>
${DONGLE.stand && DONGLE.stand.fehler
? `<span class="label" style="margin-top:8px;color:var(--bad);letter-spacing:0;
@@ -3296,6 +3399,11 @@ function vEinst() {
let nmOffen = false;
let einrichtenOffen = false;
/* Entwurf des Reglers "Fahrt beenden" (Minuten) - null heisst: nicht angefasst.
Er liegt bewusst NEBEN CONFIG.pausenzeit und nicht darin: erst der Knopf
"Speichern" uebernimmt ihn, weil derselbe Wert den Schlaf-Timeout des
Dongles stellt. Siehe die Kachel in vEinst(). */
let PAUSENZEIT_ENTWURF = null;
let bildMenuOffen = null;
let smartdealPopupOffen = false;
// "sommer"|"winter", welcher Reifensatz gerade sein km-Korrektur-Feld offen
@@ -4403,6 +4511,48 @@ function vFuel() {
${vFuelFormular()}`;
}
/* Karte des Einzelbelegs - oder die Auskunft, dass es zu dieser Tankstelle
keine Position gibt. Welches von beidem, entscheidet BELEG_KARTE: den
Merker setzt initMap(), sobald der Nachschlag erfolglos zurueckkommt.
Deckungsgleich mit TankstellenKarte.tsx in der App, samt der Unterscheidung
zwischen "steht nicht im Beleg" und "findet niemand". */
function belegKarte(f) {
const suche = tankstelleSuchtext(f.station_name, f.station_address);
const ohne =
!suche || (BELEG_KARTE && BELEG_KARTE.id === f.tank_id && BELEG_KARTE.zustand === "keine");
if (!ohne) return mapBoxMitZentrieren();
return `<div class="tile"><div class="leer">
<strong>Keine Position zur Tankstelle</strong>
<p>${
suche
? "Zu dieser Anschrift ließ sich kein Kartenpunkt finden. Tragen Sie im Beleg unter „Werte bearbeiten“ eine genauere Anschrift ein, dann erscheint die Tankstelle hier."
: "Dieser Beleg nennt weder Tankstelle noch Anschrift. Tragen Sie sie unter „Werte bearbeiten“ nach, dann erscheint die Tankstelle hier."
}</p>
</div></div>`;
}
/* Tankstelle des Belegs: eigene Zeile ueber die volle Kachelbreite statt der
engen Wertespalte, zweizeilig und antippbar.
Vorher eine gewoehnliche .row: Name rechts in der Wertespalte (rows-fest,
also 120px), die ganze Anschrift als Kleintext darunter - "Hermann Mogler
Mineraloelg. GmbH, Heidenheimerstr.6, 89551 Koenigsbronn" stand dort
vier- bis fuenfzeilig. Der Tipp fuehrt jetzt in den Kartendienst des
Geraets; ohne Name und ohne Anschrift bleibt es eine stumme Zeile, denn
ein Verweis, der nirgendwohin fuehrt, waere schlechter als keiner. */
function tankstellenZeile(f) {
const { oben, unten } = tankstelleZeilen(f.station_name, f.station_address);
const url = kartendienstUrl(f.station_name, f.station_address);
const inhalt =
`<span class="tankstellenzeile-ort">${esc(oben)}${unten ? `<small>${esc(unten)}</small>` : ""}</span>` +
(url ? `<svg class="chev" viewBox="0 0 6 10" aria-hidden="true"><polyline points="1,1 5,5 1,9"/></svg>` : "");
return `<div class="row tankstellenzeile"><dt>Tankstelle</dt><dd>${
url
? `<a href="${esc(url)}" target="_blank" rel="noopener" aria-label="${esc(oben)} in Karten öffnen">${inhalt}</a>`
: `<span>${inhalt}</span>`
}</dd></div>`;
}
function vFill(id) {
const f = FILLS.find((x) => x.tank_id === id);
if (!f) return `<div class="tile">Tankvorgang nicht gefunden</div>`;
@@ -4420,7 +4570,7 @@ function vFill(id) {
}
return `
${mapBoxMitZentrieren()}
${belegKarte(f)}
<div class="tile flat" style="margin:18px 0 14px">
<span class="label">Getankt</span>
<div class="fig" style="font-size:52px;margin-top:10px">${f.liters != null ? de(f.liters, 2) : ""}<span class="unit">l</span></div>
@@ -4433,8 +4583,8 @@ function vFill(id) {
<div class="row"><dt>Getankte Liter</dt><dd>${f.liters != null ? de(f.liters, 2) + " l" : ""}</dd></div>
<div class="row"><dt>Kosten</dt><dd>${f.fuel_total_eur != null ? eur(f.fuel_total_eur) + " " : ""}<small>real, nach Rabatt</small></dd></div>
<div class="row"><dt>Preis je Liter</dt><dd>${berechneterPreis != null ? de(berechneterPreis, 2) + " " : ""}<small>Kosten ÷ Liter</small></dd></div>
${CAR.smartdeal.aktiv && f.discount != null ? `<div class="row"><dt><span class="marke">${markenlogo()} SmartDeal-Ersparnis</span></dt><dd>${eur(f.discount)} €</dd></div>` : ""}
<div class="row" style="border-bottom:0"><dt>Tankstelle</dt><dd>${esc(f.station_name || "unbekannt")}<small>${esc(f.station_address || "")}</small></dd></div>
${CAR.smartdeal.aktiv && f.discount != null ? `<div class="row row-marke"><dt><span class="marke">${markenlogo()} SmartDeal-Ersparnis</span></dt><dd>${eur(f.discount)} €</dd></div>` : ""}
${tankstellenZeile(f)}
</dl>
${belegFehler && belegFehler.context === f.tank_id ? `<span class="label" style="margin-top:14px;color:var(--red)">${esc(belegFehler.meldung)}</span>` : ""}
<button class="aktion" data-tankbearbeiten>Werte bearbeiten</button>
@@ -5140,6 +5290,10 @@ const go = (name, id) => {
// no-op, falls die Daten längst da sind (datenLaden() rendert dann
// ohnehin nichts doppelt Sichtbares).
if (!DATEN_GELADEN) datenLaden(false);
// Der Merker "zu diesem Beleg gibt es keine Position" gilt nur so lange, wie
// man auf ihm steht: nach einer Korrektur der Anschrift soll erneut gesucht
// werden, nicht die alte Absage stehen bleiben.
BELEG_KARTE = null;
// Ein unbestätigter Servicebuch-Entwurf (siehe data-sbneu) verlässt die
// Anwendung nie ungespeichert - wird die Eintragsseite ohne Klick auf
// "Speichern" verlassen, verwerfen wir ihn hier statt ihn liegen zu lassen
@@ -5838,6 +5992,9 @@ function ereignisseVerdrahten() {
distanz: w.distanz, kraftstoff: w.kraftstoff,
receipt_key: beleg ? beleg.receipt_key : null, receipt_file: beleg ? beleg.receipt_file : null,
});
// Die Anschrift kann sich gerade geändert haben - dann soll die Karte
// erneut suchen statt bei der alten Absage zu bleiben (siehe BELEG_KARTE).
BELEG_KARTE = null;
tankBearbeitenOffen = false; belegEntwurf = null; belegFehler = null; render(); return;
}
const belegBtn = e.target.closest("[data-beleg]");
@@ -5988,6 +6145,15 @@ function ereignisseVerdrahten() {
}
return;
}
/* Regler "Fahrt beenden": erst hier wird geschrieben. profilSpeichern()
stellt ueber denselben Weg auch den Schlaf-Timeout des Dongles - genau
deshalb hat der Regler einen eigenen Knopf bekommen. */
if (e.target.closest("[data-pausenzeitspeichern]")) {
if (PAUSENZEIT_ENTWURF !== null) CONFIG.pausenzeit = PAUSENZEIT_ENTWURF;
PAUSENZEIT_ENTWURF = null;
profilSpeichern(); render(); return;
}
if (e.target.closest("[data-pausenzeitverwerfen]")) { PAUSENZEIT_ENTWURF = null; render(); return; }
const satz = e.target.closest("[data-satz]");
if (satz) { serviceRufen("reifen_wechseln", { satz: satz.dataset.satz.toLowerCase() }); CAR.reifen.aktiv = satz.dataset.satz; render(); return; }
const rfArch = e.target.closest("[data-reifenarchivieren]");
@@ -6106,12 +6272,12 @@ function ereignisseVerdrahten() {
ROOT.addEventListener("input", (e) => {
if (e.target.dataset.pausenzeit !== undefined) {
// Beim Ziehen nur die Anzeige und die Fuellung nachziehen, NICHT
// render() und nicht speichern: ein Re-Render mitten in der Geste
// erzeugt den Regler neu und der Daumen springt aus der Hand.
// Gespeichert wird beim Loslassen (change-Zweig weiter unten) -
// dieselbe Trennung wie beim Suchfeld direkt darunter.
// render(): ein Re-Render mitten in der Geste erzeugt den Regler neu und
// der Daumen springt aus der Hand. Dieselbe Trennung wie beim Suchfeld
// direkt darunter. Gespeichert wird ueberhaupt nicht mehr hier, sondern
// erst auf Knopfdruck (data-pausenzeitspeichern).
const minuten = parseInt(e.target.value, 10);
CONFIG.pausenzeit = minuten;
PAUSENZEIT_ENTWURF = minuten;
e.target.style.setProperty("--fuell", (minuten / 60) * 100 + "%");
const feld = e.target.parentElement.querySelector("output");
if (feld) feld.textContent = minuten === 0 ? "sofort" : minuten + " Min.";
@@ -6316,7 +6482,9 @@ function ereignisseVerdrahten() {
if (e.target.dataset.oelm !== undefined) { CAR.oel.monate = parseInt(e.target.value, 10); profilSpeichern(); return; }
if (e.target.dataset.setupNurpassend !== undefined) { setupNurPassend = e.target.checked; render(); return; }
if (e.target.dataset.startbild !== undefined) { CONFIG.startbild = e.target.value; profilSpeichern(); render(); return; }
if (e.target.dataset.pausenzeit !== undefined) { CONFIG.pausenzeit = parseInt(e.target.value, 10); profilSpeichern(); return; }
// Loslassen: nur den Entwurf festhalten und neu zeichnen, damit der
// Speichern-Knopf erscheint. Geschrieben wird erst dort.
if (e.target.dataset.pausenzeit !== undefined) { PAUSENZEIT_ENTWURF = parseInt(e.target.value, 10); render(); return; }
if (e.target.dataset.svsel !== undefined) { svAuswahl = parseInt(e.target.value, 10); render(); return; }
});
}
@@ -427,6 +427,29 @@ button.tile, .tilebtn { transition: background .15s, transform .1s; }
.row dd { margin: 0; font-size: 14px; color: var(--fg); text-align: right; }
.row dd small { display: block; font-size: 12.5px; color: var(--fg3); margin-top: 3px; }
.rows-fest dd { min-width: 120px; }
/* Tankstelle im Einzelbeleg (Vorgabe des Eigentuemers, 04.09.2026).
Vorher eine gewoehnliche Zeile: Name rechts in der 120px-Wertespalte, die
ganze Anschrift als Kleintext darunter - "Hermann Mogler Mineraloelg. GmbH,
Heidenheimerstr.6, 89551 Koenigsbronn" stand dort vier- bis fuenfzeilig.
Hier bekommt sie die volle Kachelbreite unter der Beschriftung, zwei Zeilen
(Tankstelle und Strasse / Ort), und fuehrt beim Tippen in den Kartendienst -
deshalb der Chevron rechts. Deckungsgleich mit .dm-tankstellenzeile in der
App. */
.row.tankstellenzeile { flex-direction: column; align-items: stretch; gap: 4px; border-bottom: 0; }
/* flex:0 0 auto hebt .rows-fest dt{flex:0 0 150px} auf: in einer Spalte
ist die Flex-Basis die HOEHE - der Titel waere sonst 150px hoch. */
.row.tankstellenzeile dt { font-size: 13px; color: var(--fg2); flex: 0 0 auto; }
.row.tankstellenzeile dd { min-width: 0; text-align: left; flex: 0 0 auto; }
.row.tankstellenzeile dd > a,
.row.tankstellenzeile dd > span {
display: flex; align-items: center; justify-content: space-between;
gap: var(--sp-3); color: inherit; text-decoration: none;
}
.row.tankstellenzeile dd > a:active { opacity: .6; }
.tankstellenzeile-ort { display: block; font-size: 14px; color: var(--fg); line-height: 1.35; min-width: 0; }
.tankstellenzeile-ort small { display: block; margin-top: 2px; font-size: 12.5px; color: var(--fg3); }
/* .row-fussnote ist mit dem Umbau der Service-Seite auf .termingruppe
entfallen (2026-08-18) - dort gibt es keine dt/dd-Spalten mehr, die um
Platz konkurrieren. Die beiden Regeln dafuer standen seither ohne
@@ -695,7 +718,16 @@ img.carfix.klein { aspect-ratio: auto; height: auto; }
.dots i { width: 5px; height: 5px; border-radius: 50%; background: var(--line-strong); }
.dots i.on { background: var(--red); }
.marke { display: inline-flex; align-items: center; gap: 6px; }
/* white-space:nowrap: ohne die brach "SmartDeal-Ersparnis" neben dem
Markenlogo um. Kein Platzmangel, sondern ein Rundungsfehler - am
Einzelbeleg gemessen (04.09.2026, 375px breit): die dt-Spalte war 150,00px
breit, ihr eigener Inhalt 150,20px, also rutschte der Text hinter dem Logo
in eine zweite Zeile und die Zeile wurde 44,8px statt 26,4px hoch. Die
Zeile hat reichlich Platz (307px fuer 150 + 16 + 55), sie darf ihn nur nicht
abgeschnitten bekommen - dafuer gibt die Wertespalte in genau dieser Zeile
ihre feste Mindestbreite auf (.row-marke). */
.marke { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
.rows-fest .row-marke dd { min-width: 0; }
.marke-logo { display: inline-flex; align-items: center; }
/* 19,2px = 16px + 20%. Der Eigentuemer wollte das Markenlogo in JEDER
Darstellung um ein Fuenftel groesser; es steht ueberall neben Text der
+13 -4
View File
@@ -1,14 +1,23 @@
{
"domain": "audi_dashboard",
"name": "Audi Dashboard",
"version": "2026.9.4.1",
"version": "2026.9.4.5",
"documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md",
"issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues",
"codeowners": ["@paul"],
"codeowners": [
"@paul"
],
"config_flow": true,
"integration_type": "service",
"single_config_entry": true,
"iot_class": "local_polling",
"dependencies": ["http", "frontend", "panel_custom", "recorder"],
"requirements": ["pypdf>=4.0.0"]
"dependencies": [
"http",
"frontend",
"panel_custom",
"recorder"
],
"requirements": [
"pypdf>=4.0.0"
]
}