Tankstellen-Nadel, doppeltes Tanken-Symbol, Tankstellenname, packageClassList

Fuenf Meldungen des Eigentuemers, vier davon behoben:

1. Tankstellen-Nadel war bei 32 px ein dunkler Fleck (poi zeichnet nur
   einen Ring). Gefuellter Punkt in currentColor zwischen Silhouette und
   Symbol - die Geometrie stand im Pfad. Rot wurde ausdruecklich
   abgelehnt, die Umsetzung aendert keine Farbe.

2. Tanken-Symbol im Standort-Blatt wurde gefuellt UND umrandet, weil das
   CI-Symbol zusaetzlich stroke-Attribute mitbekam. Jetzt ueber ciSVG
   wie das Parkplatz-Symbol daneben; in der App dafuer imRaster an
   SymbolTanken (der 0,84-Faktor gilt nur in der Reiterleiste).

3. Tankstellenname: der Bestand hat zwei Bauformen - aeltere Belege
   tragen den Betreiber in station_name und die Anschrift daneben.
   Neu tankstelleTeile() liest beide Felder, entfernt Firmierungen,
   kuerzt Strassen und trennt die Hausnummer auch ohne Leerzeichen.
   Uebersicht: Marke, Strasse, Ort. Einzelbeleg: zwei Zeilen.
   adresseTeilen() hat keinen Aufrufer mehr und ist entfernt.

4. Zwischenablage und Kalendereintrag hatten denselben Grund: Capacitor 8
   registriert nur, was in packageClassList steht, und cap sync traegt
   dort ausschliesslich npm-Pakete ein. Beide app-eigenen Plugins lagen
   im Programm und wurden nie registriert. ios-teilen-einrichten.mjs
   traegt sie jetzt nach, ios-signieren.sh bricht ab, wenn sie im
   fertigen .ipa fehlen.

5. Geteilter Beleg nur mit Datum und Uhrzeit bleibt offen - der Weg
   nutzt denselben belegLesen()-Pfad wie eine gewaehlte Datei, es fehlt
   der konkrete Beleg oder die Protokollzeile.

tsc sauber, 246 Tests, vite build sauber, Panel als Modul geparst,
audi_ha_test auf 2026.9.4.20 ohne Traceback. Panel und App liefern fuer
die Tankstelle byteweise dasselbe.
This commit is contained in:
2026-09-04 16:34:46 +02:00
parent cefb235f6d
commit a99ae432fc
14 changed files with 543 additions and 78 deletions
+152
View File
@@ -10574,3 +10574,155 @@ laeuft ja auf `.17` und bekommt `.19` erst, wenn die Instanz sie ausliefert. Der
Weg bleibt: **erst die Integration auf der realen Instanz aktualisieren**
(Einstellungen -> Version -> pruefen, installieren, neu starten), danach bietet
die App den Schritt `.17` -> `.19` an, und der geht vorwaerts.
## CM. Fuenf Meldungen: die Nadel, zwei Symbole, der Tankstellenname - und der Grund, aus dem zwei eigene Plugins nie liefen (2026.9.4.20)
### 1. Die Tankstellen-Nadel war ein schwarzer Fleck
Gerendert und angesehen statt beurteilt: die Silhouette ist `#182634`, und das
`poi`-Symbol darueber zeichnet nur einen duennen **Ring**. Bei echter
Kartengroesse (32 px) verschwindet der Ring, uebrig bleibt eine dunkle Flaeche -
waehrend die Fahrzeug-Nadel daneben durch das gefuellte Auto-Piktogramm sofort
liest.
Vier Varianten nebeneinander gestellt (dunkel / Audi-Rot / weiss mit Kontur /
weiss mit dunklem Punkt), jeweils gross und bei 32 px. **Rot vom Eigentuemer
ausdruecklich abgelehnt** - am Code war zu dem Zeitpunkt nichts geaendert.
Umgesetzt ist die Variante, die **keine Farbe anfasst**: ein gefuellter Punkt in
`currentColor` zwischen Silhouette und Symbol. Seine Geometrie steht im Pfad
ohnehin (`poi-s`: 11.5/8.5 r3.5, `poi-l`: 23.5/17.5 r7.5) - erfunden ist daran
nichts. Beide Codebasen.
### 2. Das Tanken-Symbol im Standort-Blatt wurde doppelt gezeichnet
Gemeldet als "dicker und dunkler als das Parkplatz-Icon". Der Grund steht im
Markup: `ICONS.fuel` **ist** laengst das gefuellte CI-Symbol `fuel-station-s`,
bekam an dieser einen Stelle aber zusaetzlich
`stroke="currentColor" stroke-width="1.5"` mit - die Flaeche wurde also gefuellt
UND umrandet. Exakt die Falle, die am 02.09.2026 schon `.profilbtn svg` mit dem
Zahnrad hatte.
Jetzt derselbe Weg wie fuer `CI.parking` daneben (`ciSVG(...)`, ohne
Strichattribute). Dazu die Groesse: gemessen fuellt `parking` das Raster mit
23x23, `fuel-station-s` roh 19x22, mit dem 0,84-Faktor der Reiterleiste nur noch
16,0x18,5. Neben dem randfuellenden Parkplatz-Symbol passt die **ungeskalierte**
Fassung. In der App bekam `SymbolTanken` dafuer ein `imRaster` - der 0,84-Faktor
ist die Kalibrierung fuer die Reiterleiste und gilt nur dort.
### 3. Tankstellenname: der Bestand hat zwei Bauformen
Gemeldet: im Beleg vom 10.11.2025 steht in der Uebersicht noch die Firmierung
("...GmbH"). An den echten Datensaetzen nachgesehen - und die Ursache ist die
Datenlage, nicht die Anzeige:
| | `station_name` | `station_address` |
|---|---|---|
| bis 2025 | `TC Sengül GmbH` (der **Betreiber**) | `Nürnberger Str.74, 91522 Ansbach` |
| ab 08.2026 | `Shell, Pascalstr. 8, Ingolstadt` | `None` |
Die Markenerkennung lief nur ueber `station_name`. Bei den aelteren Belegen
steht die Marke dort gar nicht - also blieb die Firmierung stehen, waehrend die
Anschrift daneben ungenutzt blieb.
Neu `tankstelleTeile()` (beide Codebasen, wortgleich): liest **beide** Felder,
zerlegt in Marke / Strasse / Hausnummer / Postleitzahl / Ort. Firmierungen
(GmbH, mbH, OHG, KG, AG, e. K., Inh., & Co) fallen heraus, Strassen werden
gekuerzt (`Nürnberger Straße` wird `Nürnberger Str.`), und die Hausnummer wird
auch ohne Leerzeichen davor abgetrennt - so drucken die Kassen es (`Str.74`).
Daraus die beiden vom Eigentuemer verlangten Formen:
| Beleg | Uebersicht | Einzelbeleg |
|---|---|---|
| `TC Sengül GmbH` + Anschrift | `Nürnberger Str., Ansbach` | `Nürnberger Str. 74` / `91522 Ansbach` |
| `Shell, Pascalstr. 8, Ingolstadt` | `Shell, Pascalstr., Ingolstadt` | `Shell, Pascalstr. 8` / `Ingolstadt` |
| `Shell, SÖLDEN` | `Shell, SÖLDEN` | `Shell` / `SÖLDEN` |
Geschrieben wie eine Anschrift: das Komma trennt nur die Marke ab, zwischen
Strasse und Nummer und zwischen Postleitzahl und Ort steht keins.
**Ein Rueckfall bleibt bewusst:** ergibt die Zerlegung gar nichts - nur eine
Firmierung, keine Anschrift -, steht weiter der rohe Name da. Ihn dann
wegzulassen hiesse, eine bekannte Tankstelle "unbekannt" zu nennen.
`adresseTeilen()` hat damit keinen Aufrufer mehr und ist in beiden Codebasen
entfernt (samt seiner Tests).
### 4. Zwei eigene Plugins waren im Programm - und trotzdem tot
Gemeldet: die Zwischenablage meldet "die App muss erst aktualisiert werden",
obwohl frisch installiert. Und, kurz darauf: der Kalendereintrag erzeugt weiter
nur eine ics-Datei.
**Beides derselbe Grund**, gefunden durch Lesen der Capacitor-Quellen statt
durch Vermuten. `CapacitorBridge.swift`, `registerPlugins()` liest
`packageClassList` aus der `capacitor.config.json` des Buendels und loest jeden
Eintrag ueber `NSClassFromString` auf.
**Capacitor 8 sucht Plugin-Klassen NICHT im Programm.** Registriert wird
ausschliesslich, was in dieser Liste steht. Und die erzeugt `cap sync` aus den
installierten **npm-Paketen** - app-eigene Plugins sind keine. In der
ausgelieferten `.ipa` nachgesehen, dort stehen genau sechs Fremd-Plugins:
SecureStorage, AppPlugin, FilesystemPlugin, PreferencesPlugin, SharePlugin,
CapacitorUpdaterPlugin.
`BelegZwischenablagePlugin` und `KalenderTerminPlugin` fehlen - beide liegen
nachweislich im Programm (5 Treffer im Binaerprogramm), werden aber nie
registriert. Folgen, beide gemeldet:
* `Capacitor.isPluginAvailable("BelegZwischenablage")` ist `false` - daher die
Meldung ueber die angeblich noetige Aktualisierung.
* `KalenderTermin` scheitert mit `UNIMPLEMENTED`, der Rueckfall auf Datei plus
Teilen-Blatt greift, und der kann den Kalender **prinzipiell nie erreichen**
(Abschnitt BS). Daher "nur eine ics-Datei, kein Eintrag".
`scripts/ios-teilen-einrichten.mjs` traegt die Klassen jetzt nach jedem
`cap sync` in `ios/App/App/capacitor.config.json` nach. Die Namen liest es aus
den Swift-Dateien selbst (der objc-Name), damit sie nicht an zwei Stellen
gepflegt werden; der Schritt ist idempotent (zweiter Lauf ergaenzt 0 Eintraege,
geprueft).
Dazu ein **Waechter am fertigen `.ipa`**: fehlt eine der Klassen in
`packageClassList`, bricht `ios-signieren.sh` ab. Gegen die ausgelieferte
Fassung geprueft - er schlaegt dort korrekt an. Dieselbe Linie wie bei der
`CFBundleVersion` (Abschnitt CD).
Die Fehlermeldung in `zwischenablage.ts` nannte eine **geratene Ursache** ("kommt
nur mit einer neu installierten App") - die war nachweislich falsch, der
Eigentuemer hatte frisch installiert. Sie nennt jetzt nur noch die Tatsache.
**Merkposten, zum zweiten Mal nach dem Preferences-Plugin (Abschnitt BR):** bei
einem Plugin, das eine Systemgrenze ueberbrueckt, den Quelltext der Bruecke
lesen, nicht die Dokumentation.
### 5. Geteilter Beleg: nur Datum und Uhrzeit - noch offen
Der Weg selbst ist geprueft und in Ordnung: ein ueber das Teilen-Blatt
hereingereichter Beleg laeuft durch **exakt denselben** `belegLesen()`-Pfad wie
eine von Hand gewaehlte Datei (`Tanken.tsx`), inklusive Dateiname aus dem Pfad.
Es gibt dort keinen zweiten, aermeren Zweig.
Damit bleibt die Auswertung selbst, und die haengt am konkreten PDF - das hier
nicht vorliegt. **Nicht geraten**: um es zu entscheiden, braucht es den Beleg
(unter `tests/belegparser/belege/`, das ist gitignored) oder die Protokollzeile
der Instanz zu diesem Hochladen.
### Verifiziert
`tsc --noEmit` sauber, **246 Tests** (von 244; 6 neue fuer die Zerlegung an den
echten Belegen, eine Erwartung bewusst geaendert), `vite build` sauber, Panel als
**Modul** geparst, `node --check` und `bash -n` auf beiden Skripten,
`audi_ha_test` auf `2026.9.4.20` sauber gestartet, kein Traceback.
**Panel und App liefern fuer die Tankstelle byteweise dasselbe** - die
Panel-Funktionen dafuer aus dem Buendel geschnitten und in Node gegen dieselben
sieben Faelle laufen lassen, nicht nachgebaut.
**Live in der App**: in der Tankliste steht nirgends mehr "GmbH", der gemeldete
Beleg liest sich `Nürnberger Str., Ansbach`, und der Einzelbeleg zeigt
`Nürnberger Str. 74` / `91522 Ansbach`.
**Nicht geprueft, weil es ohne Mac nicht geht:** dass die Registrierung auf dem
Geraet greift. Das zeigt erst der naechste Xcode-Lauf - und der bricht jetzt ab,
wenn sie fehlt.
+15
View File
@@ -335,6 +335,21 @@ if unzip -q "$AUSGABE/App.ipa" -d "$PRUEF"; then
echo " WARNUNG: $(basename "$ZIEL") ohne App-Gruppe - Teilen kann nichts uebergeben."
fi
done
# Eigene Plugins muessen in packageClassList stehen. Capacitor 8 sucht
# Plugin-Klassen NICHT im Programm (registerPlugins() in
# CapacitorBridge.swift liest nur diese Liste) - eine einkompilierte
# Klasse, die dort fehlt, existiert fuer die Bruecke nicht. Genau so kam
# die .ipa vom 04.09.2026 heraus: Zwischenablage und Kalendertermin waren
# im Programm und trotzdem tot.
KONF="$PRUEF/Payload/App.app/capacitor.config.json"
for KLASSE in $(grep -ho "@objc([A-Za-z0-9_]*)" "$APP/native"/*.swift | sed "s/@objc(//;s/)//"); do
if ! grep -q "$KLASSE" "$KONF" 2>/dev/null; then
echo " FEHLER: $KLASSE fehlt in packageClassList - das Plugin bliebe stumm." >&2
rm -rf "$PRUEF"
exit 1
fi
done
echo " packageClassList: eigene Plugins eingetragen"
fi
rm -rf "$PRUEF"
@@ -97,6 +97,44 @@ for (const datei of PLUGIN_DATEIEN) {
}
melden(`Quellen nach ios/App/${ZIEL_NAME}/ kopiert`)
/* ------------------------------------------ Eigene Plugins registrieren */
/* Capacitor 8 sucht Plugin-Klassen NICHT im Programm. `registerPlugins()`
in CapacitorBridge.swift liest ausschliesslich `packageClassList` aus der
capacitor.config.json des Buendels und loest jeden Eintrag ueber
NSClassFromString auf - was dort nicht steht, existiert fuer die Bruecke
nicht, auch wenn die Klasse einkompiliert ist.
Genau daran sind beide eigenen Plugins gescheitert: `cap sync` erzeugt die
Liste aus den installierten npm-Paketen, und unsere Plugins sind keine.
In der ausgelieferten .ipa vom 04.09.2026 standen dort nur die sechs
Fremd-Plugins - die Zwischenablage meldete deshalb 'diese App-Fassung
enthaelt den nativen Teil noch nicht' (Nutzerbefund), obwohl
BelegZwischenablagePlugin nachweislich im Programm lag, und der
Kalendertermin fiel still auf das Teilen-Blatt zurueck, das den Kalender
gar nicht erreichen kann (siehe AGENTS.md Abschnitt BS).
Die Klassennamen kommen aus den Swift-Dateien selbst (@objc(...)), damit
sie nicht an zwei Stellen gepflegt werden muessen. */
const konfigPfad = join(iosApp, "App", "capacitor.config.json")
if (!existsSync(konfigPfad)) {
abbruch(`${konfigPfad} nicht gefunden - erst 'npx cap sync ios' laufen lassen.`)
}
const eigeneKlassen = PLUGIN_DATEIEN.map((datei) => {
const quelle = readFileSync(join(APP, "native", datei), "utf8")
const treffer = /@objc\(([A-Za-z0-9_]+)\)/.exec(quelle)
if (!treffer) abbruch(`${datei}: kein @objc(...)-Name gefunden`)
return treffer[1]
})
const konfig = JSON.parse(readFileSync(konfigPfad, "utf8"))
konfig.packageClassList = konfig.packageClassList ?? []
const fehlten = eigeneKlassen.filter((k) => !konfig.packageClassList.includes(k))
if (fehlten.length) {
konfig.packageClassList.push(...fehlten)
writeFileSync(konfigPfad, JSON.stringify(konfig, null, 2) + String.fromCharCode(10))
}
melden(`packageClassList: ${eigeneKlassen.join(", ")} eingetragen`)
/* ------------------------------------------------------------- URL-Schema */
/* Ohne dieses Schema kann die Erweiterung die App nicht holen - sie legt den
+60 -27
View File
@@ -9,11 +9,12 @@
import { afterEach, describe, expect, it, vi } from "vitest"
import {
adresseTeilen,
kartendienstUrl,
markeErkennen,
tankstelleKandidaten,
tankstelleSuchtext,
tankstelleKurz,
tankstelleTeile,
tankstelleZeilen,
tankstelleZiel,
} from "./tankstelle"
@@ -26,31 +27,6 @@ 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("markeErkennen", () => {
it("findet die Kopfmarke im gespeicherten Namen", () => {
expect(markeErkennen("Shell, SÖLDEN")).toBe("Shell")
@@ -92,7 +68,8 @@ describe("tankstelleZeilen", () => {
it("zeigt ohne Anschrift den gespeicherten Namen unveraendert", () => {
// Sonst hiesse eine bekannte Tankstelle plötzlich "unbekannt".
expect(tankstelleZeilen("Shell, SÖLDEN", null)).toEqual({ oben: "Shell, SÖLDEN", unten: "" })
// Geaendert am 04.09.2026: die Marke gehoert nach oben, der Ort nach unten.
expect(tankstelleZeilen("Shell, SÖLDEN", null)).toEqual({ oben: "Shell", unten: "SÖLDEN" })
expect(tankstelleZeilen("Testtankstelle", null)).toEqual({ oben: "Testtankstelle", unten: "" })
})
@@ -216,3 +193,59 @@ describe("kartendienstUrl", () => {
)
})
})
/* Die Vorgabe des Eigentuemers vom 04.09.2026, an den ECHTEN Datensaetzen der
Instanz geprueft - beide Bauformen des Bestandes:
bis 2025: name = Firmierung des Betreibers, Anschrift daneben
ab 08.2026: name = "Marke, Strasse, Ort", keine Anschrift
Uebersicht: Marke, Strasse ohne Hausnummer, Ort ohne Postleitzahl
Einzelbeleg: oben Marke + Strasse mit Hausnummer, unten Postleitzahl + Ort */
describe("tankstelleKurz und tankstelleZeilen an echten Belegen", () => {
it("holt Strasse und Ort aus der Anschrift, wenn im Namen nur die Firmierung steht", () => {
// Genau der gemeldete Beleg vom 10.11.2025.
const n = "TC Sengül GmbH"
const a = "Nürnberger Str.74, 91522 Ansbach"
expect(tankstelleKurz(n, a)).toBe("Nürnberger Str., Ansbach")
expect(tankstelleZeilen(n, a)).toEqual({ oben: "Nürnberger Str. 74", unten: "91522 Ansbach" })
})
it("zerlegt auch den zusammengesetzten Namen der neueren Belege", () => {
const n = "Shell, Pascalstr. 8, Ingolstadt"
expect(tankstelleKurz(n, null)).toBe("Shell, Pascalstr., Ingolstadt")
expect(tankstelleZeilen(n, null)).toEqual({ oben: "Shell, Pascalstr. 8", unten: "Ingolstadt" })
})
it("kuerzt die ausgeschriebene Strasse", () => {
const n = "Aral, Nürnberger Straße 74, 91522 Ansbach"
expect(tankstelleKurz(n, null)).toBe("Aral, Nürnberger Str., Ansbach")
})
it("laesst die Firmierung ueberall weg", () => {
expect(tankstelleKurz("Hermann Mogler Mineralölg. GmbH", "Heidenheimerstr.6, 89551 Königsbronn")).toBe(
"Heidenheimerstr., Königsbronn",
)
})
it("behaelt den rohen Namen, wenn sich sonst nichts gewinnen laesst", () => {
// Sonst hiesse eine bekannte Tankstelle ploetzlich "unbekannt".
expect(tankstelleKurz("Testtankstelle", null)).toBe("Testtankstelle")
expect(tankstelleKurz("A. Zrenner GmbH", null)).toBe("A. Zrenner GmbH")
expect(tankstelleKurz(null, null)).toBe("unbekannt")
})
it("trennt Hausnummer auch ohne Leerzeichen davor", () => {
// So drucken die Kassen es: "Str.74", nicht "Str. 74".
expect(tankstelleTeile(null, "Heidenheimerstr.6, 89551 Königsbronn")).toMatchObject({
strasse: "Heidenheimerstr.",
hausnummer: "6",
plz: "89551",
ort: "Königsbronn",
})
expect(tankstelleTeile(null, "Am Anger 7a, 85111 Adelschlag")).toMatchObject({
strasse: "Am Anger",
hausnummer: "7a",
})
})
})
+142 -20
View File
@@ -72,38 +72,160 @@ export function markeErkennen(name: string | null | undefined): string | null {
return null
}
/** `"Pascalstr. 8, 85057 Ingolstadt"` → Straße und Ort. */
export function adresseTeilen(adresse: string | null | undefined): {
/* Kennzeichen einer Firmierung. Ein Teil, der so etwas enthaelt, ist der
BETREIBER und gehoert nach Vorgabe des Eigentuemers nicht in die Anzeige -
"TC Sengül GmbH" oder "Hermann Mogler Mineralölg. GmbH" stehen auf keinem
Preisschild und in keiner Karte. */
const FIRMA = /\b(gmbh|mbh|ohg|kg|ag|e\.\s?k\.|inh\.|& co)\b/i
/* "91522 Ansbach" - vier Stellen wegen Oesterreich (der Belegparser hat
denselben Fall, siehe shell_beleg_parser.py). */
const PLZ_ORT = /^(\d{4,5})\s+(.+)$/
/* Strasse mit Hausnummer am Ende. Greedy bis zum letzten Nicht-Ziffernzeichen,
damit auch die zusammengeschriebene Form der Belege trifft:
"Nürnberger Str.74" -> "Nürnberger Str." + "74". */
const STRASSE_NR = /^(.*\D)\s*(\d+\s*[a-zA-Z]?)$/
/**
* "Nürnberger Straße" → "Nürnberger Str.", "Heidenheimerstraße" →
* "Heidenheimerstr.". Vorgabe des Eigentuemers fuer die Uebersicht; die Belege
* liefern beide Schreibweisen, je nach Kasse.
*
* Die Grossschreibung des Treffers entscheidet, ob "Str." oder "str." daraus
* wird - sonst stuende mitten im Wort ein grosses S.
*/
export function strasseKuerzen(strasse: string): string {
return strasse.replace(/stra(?:ß|ss)e\b\.?/gi, (treffer) =>
treffer[0] === treffer[0]?.toUpperCase() ? "Str." : "str.",
)
}
export interface Tankstellenteile {
/** Kopfmarke, sofern erkennbar. Nie die Firmierung des Betreibers. */
marke: string
/** Ohne Hausnummer, in der kurzen Schreibweise. */
strasse: string
hausnummer: string
plz: 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() }
/** Der gespeicherte Name, unveraendert - der letzte Rueckfall. */
roh: string
}
/**
* Die beiden Zeilen der Tankstellen-Angabe (Vorgabe des Eigentümers):
* oben Kopfmarke und Straße mit Hausnummer, unten Postleitzahl und Ort.
* Zerlegt, was ein Beleg ueber die Tankstelle hergibt.
*
* Ohne erkennbare Marke steht oben nur die Straße - die Firmierung des
* Betreibers tritt NICHT an ihre Stelle, sie ist nach außen unsichtbar.
* Trägt der Beleg gar keine Anschrift, bleibt der gespeicherte Name die
* einzige Auskunft und wird unverändert gezeigt ("Shell, SÖLDEN"); ihn dann
* wegzulassen hieße, eine bekannte Tankstelle "unbekannt" zu nennen.
* WARUM BEIDE FELDER GELESEN WERDEN
* ---------------------------------
* Der Bestand hat zwei Bauformen, am 04.09.2026 an den echten Datensaetzen
* nachgesehen:
*
* `oben` ist nie leer: nennt der Beleg nichts, heißt es dort "unbekannt".
* bis 2025: station_name = "TC Sengül GmbH" (der Betreiber!)
* station_address = "Nürnberger Str.74, 91522 Ansbach"
* ab 08.2026: station_name = "Shell, Pascalstr. 8, Ingolstadt"
* station_address = null
*
* Die Markenerkennung lief bis hierher nur ueber `station_name`. Bei den
* aelteren Belegen steht die Marke dort gar nicht - deshalb blieb in der
* Uebersicht die Firmierung stehen ("...GmbH", Nutzerbefund 04.09.2026),
* waehrend die Anschrift daneben ungenutzt blieb. Gelesen werden deshalb
* beide Felder: die Anschrift zuerst (sie ist die strukturierte Quelle), der
* Name danach fuer Marke und alles, was die Anschrift nicht hergibt.
*/
export function tankstelleTeile(
name: string | null | undefined,
adresse: string | null | undefined,
): Tankstellenteile {
const teile: Tankstellenteile = {
marke: markeErkennen(name) ?? "",
strasse: "",
hausnummer: "",
plz: "",
ort: "",
roh: (name ?? "").trim(),
}
const quellen: string[] = []
for (const stueck of (adresse ?? "").split(",")) quellen.push(stueck)
for (const stueck of (name ?? "").split(",")) {
const t = stueck.trim()
// Die Marke steht schon fest; als Ort taugt sie nicht.
if (teile.marke && t.toLowerCase() === teile.marke.toLowerCase()) continue
quellen.push(t)
}
for (const stueck of quellen) {
const t = stueck.trim()
if (!t || FIRMA.test(t)) continue
const plzOrt = PLZ_ORT.exec(t)
if (plzOrt) {
if (!teile.plz) teile.plz = plzOrt[1] ?? ""
if (!teile.ort) teile.ort = (plzOrt[2] ?? "").trim()
continue
}
const mitNummer = STRASSE_NR.exec(t)
if (mitNummer) {
if (!teile.strasse) {
teile.strasse = strasseKuerzen((mitNummer[1] ?? "").trim())
teile.hausnummer = (mitNummer[2] ?? "").replace(/\s+/g, "")
}
continue
}
// Weder Postleitzahl noch Hausnummer: der Ort. Eine Strasse ohne Nummer
// laesst sich davon nicht sicher unterscheiden - im Zweifel der Ort, weil
// er in der Uebersicht die wichtigere Angabe ist.
if (!teile.ort) teile.ort = t
}
return teile
}
/**
* Die Zeile der Uebersicht: **Marke, Strasse ohne Hausnummer, Ort ohne
* Postleitzahl** (Vorgabe des Eigentuemers, 04.09.2026).
*
* Ergibt die Zerlegung gar nichts - etwa wenn nur eine Firmierung ohne
* Anschrift gespeichert ist - bleibt der rohe Name stehen. Ihn dann
* wegzulassen hiesse, eine bekannte Tankstelle "unbekannt" zu nennen.
*/
export function tankstelleKurz(
name: string | null | undefined,
adresse: string | null | undefined,
): string {
const t = tankstelleTeile(name, adresse)
const zeile = [t.marke, t.strasse, t.ort].filter(Boolean).join(", ")
return zeile || t.roh || "unbekannt"
}
/**
* Die beiden Zeilen im Einzelbeleg (Vorgabe des Eigentuemers, 04.09.2026):
* oben **Marke, Strasse mit Hausnummer**, unten **Postleitzahl und Ort**.
*
* Geschrieben wie eine Anschrift, also "Shell, Pascalstr. 8" und "85057
* Ingolstadt" - das Komma trennt die Marke ab, zwischen Strasse und Nummer
* und zwischen Postleitzahl und Ort steht keins.
*
* `oben` ist nie leer: gibt der Beleg nur einen Ort her, rueckt der nach oben,
* und kennt er gar nichts, heisst es dort "unbekannt".
*/
export function tankstelleZeilen(
name: string | null | undefined,
adresse: string | null | undefined,
): { oben: string; unten: string } {
const { strasse, ort } = adresseTeilen(adresse)
if (!strasse && !ort) return { oben: (name ?? "").trim() || "unbekannt", unten: "" }
const oben = [markeErkennen(name) ?? "", strasse].filter(Boolean).join(", ")
return { oben: oben || "unbekannt", unten: ort }
const t = tankstelleTeile(name, adresse)
const strasse = [t.strasse, t.hausnummer].filter(Boolean).join(" ")
let oben = [t.marke, strasse].filter(Boolean).join(", ")
let unten = [t.plz, t.ort].filter(Boolean).join(" ")
if (!oben && !unten) return { oben: t.roh || "unbekannt", unten: "" }
if (!oben) {
oben = unten
unten = ""
}
return { oben, unten }
}
/**
+15 -8
View File
@@ -67,16 +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. */
/* Meldet sich der native Teil nicht bei der Bruecke, hilft nur die
Dateiauswahl - der Web-Weg kann ein PDF grundsaetzlich nicht liefern
(Begruendung im Modulkopf).
Bis zum 04.09.2026 stand hier die Vermutung, die App sei nur ueber die
Aktualisierung gewachsen. Sie war falsch: der Eigentuemer hatte frisch
installiert, und BelegZwischenablagePlugin lag nachweislich im
Programm. Capacitor 8 sucht Plugin-Klassen aber nicht im Programm,
sondern registriert ausschliesslich, was in packageClassList der
capacitor.config.json steht - und `cap sync` traegt dort nur
npm-Pakete ein, keine app-eigenen Plugins. Behoben in
scripts/ios-teilen-einrichten.mjs; hier steht deshalb nur noch die
Tatsache und nicht mehr eine geratene Ursache. */
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".',
'Diese App-Fassung kann die Zwischenablage nicht abfragen - bitte ' +
'"Datei auswählen".',
)
}
let ergebnis: { base64?: string; name?: string }
+8
View File
@@ -38,9 +38,17 @@ const CI_POI = '<path fill="currentColor" d="M4 8.5C4 4.32 7.32 1 11.5 1S19 4.32
const CI_GPS = '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'
function nadelSvg(px = 32): string {
// Der Ring im poi-Icon ist bei Kartengroesse zu duenn, um sich von der
// dunklen Silhouette abzuheben - die Nadel las sich als schwarzer Fleck
// (Nutzerbefund 04.09.2026). Der gefuellte Punkt schliesst genau den
// Kreis, der im Pfad ohnehin steht (poi-s: 11.5/8.5 r3.5, poi-l:
// 23.5/17.5 r7.5) - erfunden ist daran nichts, und keine Farbe aendert
// sich: currentColor ist dasselbe Weiss wie die uebrigen Konturlinien.
const punkt = '<circle cx="11.5" cy="8.5" r="2.9" fill="currentColor"/>'
return (
`<svg viewBox="0 0 24 24" width="${px}" height="${px}" fill="none" aria-hidden="true">` +
CI_POI_SIL +
punkt +
CI_POI +
"</svg>"
)
+1 -1
View File
@@ -598,7 +598,7 @@ export function Standort() {
)}
</div>
<div className="dm-standortmenu__werte">
<SymbolTanken groesse={16} />
<SymbolTanken groesse={16} imRaster />
<span>{de(fahrzeug.tankPct)} %</span>
<span>{fahrzeug.reichweite != null ? `${de(fahrzeug.reichweite)} km` : "Reichweite unbekannt"}</span>
</div>
+2 -1
View File
@@ -24,6 +24,7 @@ import {
import { Blattzeile, KRAFTSTOFFSORTEN, Leerzustand } from "./bausteine"
import { BelegPopup } from "./BelegPopup"
import { markenlogoUrl } from "./bilder"
import { tankstelleKurz } from "../daten/tankstelle"
import { LOESCH_HINWEIS } from "./bestaetigung"
import { Zeilenmenue } from "./Zeilenmenue"
@@ -124,7 +125,7 @@ export function Tanken({ geheZu }: { geheZu: (name: SeitenName, id?: string) =>
kinder={
<Blattzeile
label={datum(tank.ts)}
labelZusatz={tank.station_name ?? ""}
labelZusatz={tankstelleKurz(tank.station_name, tank.station_address)}
wert={tank.liters != null ? `${de(tank.liters, 2)} l` : ""}
wertZusatz={preis != null ? `${de(preis, 2)} €/l` : ""}
beiKlick={() => geheZu("fill", tank.tank_id)}
+13 -2
View File
@@ -167,14 +167,25 @@ export function SymbolPaket({ groesse }: SymbolProps) {
Die 48er-Variante hieße fuel-station-l und wird nicht gebraucht: die
Reiter zeichnen mit 21 bzw. 26px, also im 24er-Raster. */
export function SymbolTanken({ groesse }: SymbolProps) {
/* Der 0,84-Faktor ist die Kalibrierung fuer die Reiterleiste: dort steht das
Symbol neben vier anderen und waere roh das hoechste (Befund 02.09.2026,
19x22 gegen 24er-Raster). Ausserhalb der Leiste stimmt er nicht - im
Standort-Blatt steht daneben das randfuellende Parkplatz-Symbol (23x23),
und die geskalierte Fassung (16,0x18,5) wirkte daneben zu klein.
`imRaster` laesst das Symbol seine eigene Groesse behalten. */
export function SymbolTanken({ groesse, imRaster = false }: SymbolProps & { imRaster?: boolean }) {
const pfad = (
<path fill="currentColor" d="M4 3.5A2.5 2.5 0 0 1 6.5 1h6A2.5 2.5 0 0 1 15 3.5V10h.5a2.5 2.5 0 0 1 2.5 2.5v5a1.5 1.5 0 0 0 3 0V8.07a16.4 16.4 0 0 0-2.89-5.76l.78-.62a17.4 17.4 0 0 1 3.1 6.17l.01.07v9.57a2.5 2.5 0 0 1-5 0v-5c0-.83-.67-1.5-1.5-1.5H15v11h1v1H3v-1h1zM5 22h9V3.5c0-.83-.67-1.5-1.5-1.5h-6C5.67 2 5 2.67 5 3.5zm1-12h7v1H6z" />
)
return (
<RahmenFlaeche
groesse={groesse}
kinder={
imRaster ? pfad : (
<g transform="translate(1.5,1.92) scale(0.84)">
<path fill="currentColor" d="M4 3.5A2.5 2.5 0 0 1 6.5 1h6A2.5 2.5 0 0 1 15 3.5V10h.5a2.5 2.5 0 0 1 2.5 2.5v5a1.5 1.5 0 0 0 3 0V8.07a16.4 16.4 0 0 0-2.89-5.76l.78-.62a17.4 17.4 0 0 1 3.1 6.17l.01.07v9.57a2.5 2.5 0 0 1-5 0v-5c0-.83-.67-1.5-1.5-1.5H15v11h1v1H3v-1h1zM5 22h9V3.5c0-.83-.67-1.5-1.5-1.5h-6C5.67 2 5 2.67 5 3.5zm1-12h7v1H6z" />
{pfad}
</g>
)
}
/>
)
@@ -1 +1 @@
{"version":"2026.9.4.19","sha256":"58969a387bfa72c3d452c04b689e8ec01440c9a2a89b7a097d43fe0418092ae6","bytes":322568,"gebaut":"2026-09-04T14:06:53Z"}
{"version":"2026.9.4.20","sha256":"95213bb0998806d4b59b4d520d2dd93a4f1c7ce48991e6f77b38e9dbca8247a8","bytes":322945,"gebaut":"2026-09-04T14:28:33Z"}
@@ -981,7 +981,16 @@ function tankstellenMarkerSVG(px = 32) {
const box = gross ? 48 : 24;
const sil = gross ? CI.poiLSil : CI.poiSSil;
const icon = gross ? CI.poiL : CI.poiS;
return `<svg viewBox="0 0 ${box} ${box}" width="${px}" height="${px}" fill="none" aria-hidden="true">${sil}${icon}</svg>`;
// Der Ring im poi-Icon ist bei Kartengroesse zu duenn, um sich von der
// dunklen Silhouette abzuheben - die Nadel las sich als schwarzer Fleck
// (Nutzerbefund 04.09.2026). Der gefuellte Punkt schliesst genau den
// Kreis, der im Pfad ohnehin steht (poi-s: 11.5/8.5 r3.5, poi-l:
// 23.5/17.5 r7.5) - erfunden ist daran nichts, und keine Farbe aendert
// sich: currentColor ist dasselbe Weiss wie die uebrigen Konturlinien.
const punkt = gross
? '<circle cx="23.5" cy="17.5" r="6.2" fill="currentColor"/>'
: '<circle cx="11.5" cy="8.5" r="2.9" fill="currentColor"/>';
return `<svg viewBox="0 0 ${box} ${box}" width="${px}" height="${px}" fill="none" aria-hidden="true">${sil}${punkt}${icon}</svg>`;
}
/* Fahrzustand fürs Menü: kommt aus der Zündung (CAR.zuendung, vom Backend aus
@@ -1322,23 +1331,86 @@ function markeErkennen(name) {
return null;
}
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() };
/* Kennzeichen einer Firmierung. Ein Teil, der so etwas enthaelt, ist der
BETREIBER und gehoert nach Vorgabe des Eigentuemers nicht in die Anzeige. */
const FIRMA_MUSTER = /\b(gmbh|mbh|ohg|kg|ag|e\.\s?k\.|inh\.|& co)\b/i;
const PLZ_ORT_MUSTER = /^(\d{4,5})\s+(.+)$/;
/* Greedy bis zum letzten Nicht-Ziffernzeichen, damit auch die
zusammengeschriebene Form der Belege trifft: "Nürnberger Str.74". */
const STRASSE_NR_MUSTER = /^(.*\D)\s*(\d+\s*[a-zA-Z]?)$/;
/* "Nürnberger Straße" -> "Nürnberger Str.". Die Grossschreibung des Treffers
entscheidet, sonst stuende mitten im Wort ein grosses S. */
function strasseKuerzen(strasse) {
return strasse.replace(/stra(?:ß|ss)e\b\.?/gi, (t) => (t[0] === t[0].toUpperCase() ? "Str." : "str."));
}
/* Oben Kopfmarke und Strasse mit Hausnummer, unten Postleitzahl und Ort.
Ohne erkennbare Marke steht oben nur die Strasse - die Firmierung des
Betreibers tritt NICHT an ihre Stelle. Ohne Anschrift bleibt der
gespeicherte Name die einzige Auskunft und wird unveraendert gezeigt. */
/* Zerlegt, was ein Beleg ueber die Tankstelle hergibt.
Der Bestand hat zwei Bauformen, am 04.09.2026 an den echten Datensaetzen
nachgesehen:
bis 2025: station_name = "TC Sengül GmbH" (der Betreiber!)
station_address = "Nürnberger Str.74, 91522 Ansbach"
ab 08.2026: station_name = "Shell, Pascalstr. 8, Ingolstadt"
station_address = null
Die Markenerkennung lief bis hierher nur ueber den Namen - bei den aelteren
Belegen steht die Marke dort gar nicht, deshalb blieb in der Uebersicht die
Firmierung stehen ("...GmbH", Nutzerbefund 04.09.2026), waehrend die
Anschrift daneben ungenutzt blieb. Gelesen werden jetzt beide Felder. */
function tankstelleTeile(name, adresse) {
const teile = { marke: markeErkennen(name) || "", strasse: "", hausnummer: "", plz: "", ort: "", roh: (name || "").trim() };
const quellen = (adresse || "").split(",");
for (const stueck of (name || "").split(",")) {
const t = stueck.trim();
if (teile.marke && t.toLowerCase() === teile.marke.toLowerCase()) continue;
quellen.push(t);
}
for (const stueck of quellen) {
const t = stueck.trim();
if (!t || FIRMA_MUSTER.test(t)) continue;
const plzOrt = PLZ_ORT_MUSTER.exec(t);
if (plzOrt) {
if (!teile.plz) teile.plz = plzOrt[1];
if (!teile.ort) teile.ort = plzOrt[2].trim();
continue;
}
const mitNummer = STRASSE_NR_MUSTER.exec(t);
if (mitNummer) {
if (!teile.strasse) {
teile.strasse = strasseKuerzen(mitNummer[1].trim());
teile.hausnummer = mitNummer[2].replace(/\s+/g, "");
}
continue;
}
/* Weder Postleitzahl noch Hausnummer: der Ort. Eine Strasse ohne Nummer
laesst sich davon nicht sicher unterscheiden - im Zweifel der Ort, weil
er in der Uebersicht die wichtigere Angabe ist. */
if (!teile.ort) teile.ort = t;
}
return teile;
}
/* Zeile der Uebersicht: Marke, Strasse ohne Hausnummer, Ort ohne Postleitzahl
(Vorgabe des Eigentuemers, 04.09.2026). Ergibt die Zerlegung nichts, bleibt
der rohe Name stehen - ihn wegzulassen hiesse, eine bekannte Tankstelle
"unbekannt" zu nennen. */
function tankstelleKurz(name, adresse) {
const t = tankstelleTeile(name, adresse);
return [t.marke, t.strasse, t.ort].filter(Boolean).join(", ") || t.roh || "unbekannt";
}
/* Die beiden Zeilen im Einzelbeleg: oben Marke und Strasse mit Hausnummer,
unten Postleitzahl und Ort - geschrieben wie eine Anschrift, das Komma
trennt nur die Marke ab. `oben` ist nie leer. */
function tankstelleZeilen(name, adresse) {
const { strasse, ort } = adresseTeilen(adresse);
if (!strasse && !ort) return { oben: (name || "").trim() || "unbekannt", unten: "" };
const oben = [markeErkennen(name) || "", strasse].filter(Boolean).join(", ");
return { oben: oben || "unbekannt", unten: ort };
const t = tankstelleTeile(name, adresse);
const strasse = [t.strasse, t.hausnummer].filter(Boolean).join(" ");
let oben = [t.marke, strasse].filter(Boolean).join(", ");
let unten = [t.plz, t.ort].filter(Boolean).join(" ");
if (!oben && !unten) return { oben: t.roh || "unbekannt", unten: "" };
if (!oben) { oben = unten; unten = ""; }
return { oben, unten };
}
/* Suchbegriffe fuer die Geokodierung, in der Reihenfolge des Versuchs: erst
@@ -1768,7 +1840,13 @@ function vStandort() {
: `${ciSVG(CI.parking)}<span>${zustand.seitTs != null ? "Geparkt seit " + dauerSeitText(zustand.seitTs) : "Fahrzeug steht, Parkdauer unbekannt"}</span>`}
</div>
<div class="standortmenu-werte">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${ICONS.fuel}</svg>
<!-- CI-Symbole sind Flaechen und tragen ihr fill selbst. Die
Strichattribute von oben zogen zusaetzlich eine 1,5er Kontur um
jede Flaeche - das Symbol wurde gefuellt UND umrandet gezeichnet
und wirkte neben dem Parkplatz-Symbol dicker und dunkler
(Nutzerbefund 04.09.2026). Dieselbe Falle wie beim Zahnrad in
.profilbtn svg. Jetzt derselbe Weg wie fuer CI.parking daneben. -->
${ciSVG(CI.fuelStationS, 16)}
<span>${de(CAR.tankPct)} %</span>
<span>${CAR.reichweite != null ? de(CAR.reichweite) + " km" : "Reichweite unbekannt"}</span>
</div>
@@ -4674,7 +4752,7 @@ function vFuel() {
<span>Ø ${de(schnittpreis(fm), 2)} /l</span></span></button>
<div class="acc-b ${mi === 0 ? "on" : ""}" id="fm${j}${m}">`;
fm.forEach((f) => { const preis = literpreis(f); h += swipeZeile(`fill:${f.tank_id}`, `<button class="leaf" data-go="fill:${f.tank_id}">
<span class="k">${dedat(new Date(f.ts))}<small>${esc(f.station_name || "")}</small></span>
<span class="k">${dedat(new Date(f.ts))}<small>${esc(tankstelleKurz(f.station_name, f.station_address))}</small></span>
<span class="v">${f.liters != null ? de(f.liters, 2) + " l" : ""}<small>${preis != null ? de(preis, 2) + " €/l" : ""}</small></span>
<svg class="chev" viewBox="0 0 6 10"><polyline points="1,1 5,5 1,9"/></svg></button>`); });
h += `</div></div>`;
@@ -1,7 +1,7 @@
{
"domain": "audi_dashboard",
"name": "Audi Dashboard",
"version": "2026.9.4.19",
"version": "2026.9.4.20",
"documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md",
"issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues",
"codeowners": [