diff --git a/AGENTS.md b/AGENTS.md index 2d14b57..36d2b85 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. diff --git a/companion-app/scripts/ios-signieren.sh b/companion-app/scripts/ios-signieren.sh index 84b07db..85e41bd 100755 --- a/companion-app/scripts/ios-signieren.sh +++ b/companion-app/scripts/ios-signieren.sh @@ -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" diff --git a/companion-app/scripts/ios-teilen-einrichten.mjs b/companion-app/scripts/ios-teilen-einrichten.mjs index b8d4335..afc81f4 100644 --- a/companion-app/scripts/ios-teilen-einrichten.mjs +++ b/companion-app/scripts/ios-teilen-einrichten.mjs @@ -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 diff --git a/companion-app/src/daten/tankstelle.test.ts b/companion-app/src/daten/tankstelle.test.ts index 54a046e..1391c37 100644 --- a/companion-app/src/daten/tankstelle.test.ts +++ b/companion-app/src/daten/tankstelle.test.ts @@ -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", + }) + }) +}) diff --git a/companion-app/src/daten/tankstelle.ts b/companion-app/src/daten/tankstelle.ts index 5213e6e..7d37403 100644 --- a/companion-app/src/daten/tankstelle.ts +++ b/companion-app/src/daten/tankstelle.ts @@ -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 } } /** diff --git a/companion-app/src/daten/zwischenablage.ts b/companion-app/src/daten/zwischenablage.ts index 207f2d0..067ccfd 100644 --- a/companion-app/src/daten/zwischenablage.ts +++ b/companion-app/src/daten/zwischenablage.ts @@ -67,16 +67,23 @@ function base64ZuDatei(base64: string, name: string): File { */ export async function pdfAusZwischenablage(): Promise { 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 } diff --git a/companion-app/src/screens/Karte.tsx b/companion-app/src/screens/Karte.tsx index 3a932a6..c7389e3 100644 --- a/companion-app/src/screens/Karte.tsx +++ b/companion-app/src/screens/Karte.tsx @@ -38,9 +38,17 @@ const CI_POI = '' return ( `" ) diff --git a/companion-app/src/screens/Standort.tsx b/companion-app/src/screens/Standort.tsx index d43daf6..24df691 100644 --- a/companion-app/src/screens/Standort.tsx +++ b/companion-app/src/screens/Standort.tsx @@ -598,7 +598,7 @@ export function Standort() { )}
- + {de(fahrzeug.tankPct)} % {fahrzeug.reichweite != null ? `${de(fahrzeug.reichweite)} km` : "Reichweite unbekannt"}
diff --git a/companion-app/src/screens/Tanken.tsx b/companion-app/src/screens/Tanken.tsx index ab4696a..de55a5b 100644 --- a/companion-app/src/screens/Tanken.tsx +++ b/companion-app/src/screens/Tanken.tsx @@ -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={ geheZu("fill", tank.tank_id)} diff --git a/companion-app/src/symbole.tsx b/companion-app/src/symbole.tsx index fc7bb16..e841ada 100644 --- a/companion-app/src/symbole.tsx +++ b/companion-app/src/symbole.tsx @@ -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 = ( + + ) return ( - + {pfad} + ) } /> ) diff --git a/custom_components/audi_dashboard/frontend/app/bundle.json b/custom_components/audi_dashboard/frontend/app/bundle.json index c914aa0..14e6d47 100644 --- a/custom_components/audi_dashboard/frontend/app/bundle.json +++ b/custom_components/audi_dashboard/frontend/app/bundle.json @@ -1 +1 @@ -{"version":"2026.9.4.19","sha256":"58969a387bfa72c3d452c04b689e8ec01440c9a2a89b7a097d43fe0418092ae6","bytes":322568,"gebaut":"2026-09-04T14:06:53Z"} \ No newline at end of file +{"version":"2026.9.4.20","sha256":"95213bb0998806d4b59b4d520d2dd93a4f1c7ce48991e6f77b38e9dbca8247a8","bytes":322945,"gebaut":"2026-09-04T14:28:33Z"} \ No newline at end of file diff --git a/custom_components/audi_dashboard/frontend/app/bundle.zip b/custom_components/audi_dashboard/frontend/app/bundle.zip index 0961d06..e22428c 100644 Binary files a/custom_components/audi_dashboard/frontend/app/bundle.zip and b/custom_components/audi_dashboard/frontend/app/bundle.zip differ diff --git a/custom_components/audi_dashboard/frontend/audi-dashboard-app.js b/custom_components/audi_dashboard/frontend/audi-dashboard-app.js index 180c239..31f733b 100644 --- a/custom_components/audi_dashboard/frontend/audi-dashboard-app.js +++ b/custom_components/audi_dashboard/frontend/audi-dashboard-app.js @@ -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 ``; + // 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 + ? '' + : ''; + return ``; } /* 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)}${zustand.seitTs != null ? "Geparkt seit " + dauerSeitText(zustand.seitTs) : "Fahrzeug steht, Parkdauer unbekannt"}`}
- + + ${ciSVG(CI.fuelStationS, 16)} ${de(CAR.tankPct)} % ${CAR.reichweite != null ? de(CAR.reichweite) + " km" : "Reichweite unbekannt"}
@@ -4674,7 +4752,7 @@ function vFuel() { Ø ${de(schnittpreis(fm), 2)} €/l
`; fm.forEach((f) => { const preis = literpreis(f); h += swipeZeile(`fill:${f.tank_id}`, ``); }); h += `
`; diff --git a/custom_components/audi_dashboard/manifest.json b/custom_components/audi_dashboard/manifest.json index a836a70..44a4852 100644 --- a/custom_components/audi_dashboard/manifest.json +++ b/custom_components/audi_dashboard/manifest.json @@ -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": [