diff --git a/AGENTS.md b/AGENTS.md index c474ece..073c03c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,8 @@ # AGENTS.md — Project state, review findings, open items, and working rules -**Last updated: 2026-08-30** (design-audit follow-through - the five priority items applied to +**Last updated: 2026-08-30** (owner findings batch - vehicle pin, tile chevrons, select arrows, +SmartDeal dialog, address cache and pull-to-refresh; manifest `2026.8.30.9`, see section AO. +Before that: design-audit follow-through - the five priority items applied to both codebases, plus the location preview's wrong pin; manifest `2026.8.30.8`, see section AN. Before that: third parity round — the panel had never actually rendered Audi Type (@font-face inside a shadow root is ignored), "Daten bearbeiten" now works in both, and the @@ -5821,6 +5823,75 @@ pushed from elsewhere, no overlap). --- + +## AO. Owner findings batch, 2026.8.30.9 - three of them real bugs with a single cause each + +Seventeen findings in one go, all applied to both codebases unless noted. + +**The three that were genuine defects, not drift:** + +1. **No vehicle pin on the Standort map (app).** The marker effects read `leafletRef`/`kartenRef`, + which are only filled inside the asynchronous `import("leaflet")` callback. On mount they were + still null, the effects returned early - and since their dependencies (the coordinates) never + changed afterwards, they never ran again. A `karteBereit` flag, set once the map exists and added + to those dependency lists, brings them back. Same class of bug reached the tile-layer and + own-position effects; all four were fixed together. +2. **The chevron on the Mein-Audi tiles sat on half the tile's height (app).** `.ads-tile__go` was a + flex child with `margin-left`, so on a tile with several value rows it pointed into the middle of + a row instead of at the title that says where it leads. Now absolute at top 22px / right 18px - + what the panel has done since 2026-08-13. Measured before: 132 / 106 / 106 / 98px from the top. +3. **Select fields had no drop-down arrow at all (app).** `appearance:none` in `Feld.css` removed the + native arrow without putting anything back, so a picker was indistinguishable from a plain text + field. Reported at "Ansicht" and at the backup interval; the fix in `.ads-feld select` covers + every select in the app and copies the panel's own chevron image and metrics one to one. + +**SmartDeal: the date field was broken *and* the question behind it was missing.** The app carried an +inline date input labelled "Gültig bis", the panel asks for the date in a dialog the moment the +switch is turned on and afterwards only displays it. The panel's behaviour is the one that makes +sense - `smartdeal.aktiv` is computed from that date (`laeuft_ab >= heute` in both codebases), so +without it the discount can never expire on its own. The dialog is now ported; the inline field is +gone, so there is exactly one way to set the value. + +**Nominatim has started refusing us.** "Suddenly only GPS coordinates where an address used to be" is +not a display bug: the reverse lookup comes back without a CORS header (that is what throttling looks +like from a browser) and both apps fall back to coordinates as designed. What was missing is memory - +the panel kept a single point in RAM, the app the same, both gone on reload, with the reasoning "the +vehicle position changes constantly anyway". That holds for driving, not for parking: the car stands +in the same few places. Both now cache resolved addresses in the same persistent store the fuel-station +lookup already used (`rev:`-prefixed key, rounded to ~110 m); only real hits are stored, so a throttled +answer is retried rather than remembered as "no address here". + +**Pull-to-refresh did not work on the device.** The gesture logic was fine - the scroll container had +no `overscroll-behavior`, so iOS's own rubber-band took the gesture before the threshold was reached. +Exactly the finding the panel had on 2026-08-17 and the app never received; `.dm-scroll` now carries +`overscroll-behavior-y: contain` and `touch-action: pan-y`. Not reproducible on a desktop, because +there is no native overscroll to compete with. + +**The rest, briefly:** the Übersicht placeholder is back at full 16:8 size (the audit had narrowed it +to 72px; a strip does not show what belongs there); "Fahrgestellnummer" is now "FIN" and "Details" +shows "Informationen über das Auto" as a comment-weight hint rather than a truncated engine line; the +Batteriespannung row got its chevron back (an earlier note claiming the panel marks it by cursor only +was wrong - the panel has a `.chev` there); the Reifen box leads with "aktuell montiert: / +Sommerräder" and drops the dash under the change date; Anzugsmoment uses the gear, not the pencil (the +pencil belongs to the kilometre correction); the wheel odometer is back in the figure typeface at +32px; the map-layer icon is the only one of the four controls not centred in its own grid (2.1 left +vs 1.1 right, measured via `getBBox`) and is now nudged onto centre and scaled to 0.88 so it stops +outweighing its outline neighbours; Teilen uses the real `share-s` from the CI set; and the sheet's +close button only exists while the sheet is raised - half-covered it looked operable and did nothing. + +**One finding could not be reproduced:** "Fahrten/Tankfüllung: Auflistung/grauer Hintergrund ist +falsch". Panel and app are identical there - same backgrounds (transparent rows on a white tile), +same paddings, same indents, same text. Reported back rather than guessed at. + +Verified: `node --check`, `design-system` rebuilt, `tsc --noEmit` clean, 147/147 green, deployed to +`audi_ha_test` and confirmed against both frontends at 375x812 - chevrons at 22px on all four tiles, +the Zustand row's chevron, the select arrow, the full-size placeholder, the SmartDeal dialog driven +end to end (state restored afterwards), the Reifen page's gear/date/figure, and on Standort: one +marker, the centred layer icon, the share glyph, and the close button appearing exactly when the sheet +is raised. + +--- + ## Working conventions (observed — keep them) - German is the project language: identifiers, comments, commits, UI texts. Exceptions: diff --git a/companion-app/src/daten/geokodierung.ts b/companion-app/src/daten/geokodierung.ts index 44a32c9..d36807e 100644 --- a/companion-app/src/daten/geokodierung.ts +++ b/companion-app/src/daten/geokodierung.ts @@ -23,15 +23,27 @@ export interface Koordinate { const CACHE_SCHLUESSEL = "dm360.geocache" -function cacheLesen(): Record { +function cacheLesen(): Record { try { const roh = localStorage.getItem(CACHE_SCHLUESSEL) - return roh ? (JSON.parse(roh) as Record) : {} + return roh ? (JSON.parse(roh) as Record) : {} } catch { return {} } } +/** Adresstext im selben Ablagefach merken (Schlüssel mit "rev:"-Vorsatz). */ +function adresseMerken(schluessel: string, adresse: string): void { + try { + const roh = localStorage.getItem(CACHE_SCHLUESSEL) + const cache = roh ? (JSON.parse(roh) as Record) : {} + cache[schluessel] = adresse + localStorage.setItem(CACHE_SCHLUESSEL, JSON.stringify(cache)) + } catch { + // Privatmodus oder volle Ablage — dann eben ohne Cache. + } +} + function cacheSchreiben(suche: string, treffer: Koordinate | null): void { try { const cache = cacheLesen() @@ -42,11 +54,51 @@ function cacheSchreiben(suche: string, treffer: Koordinate | null): void { } } +/** + * Koordinaten → Adresse. Der Cache ist hier genauso wichtig wie bei der + * Vorwärtsrichtung, aus einem anderen Grund: Nominatim drosselt und sperrt + * bei häufigen Abrufen, und die Antwort kommt dann ohne CORS-Kopf zurück — + * im Browser ununterscheidbar von einem Netzfehler. Ohne dauerhaften Cache + * stand an einer längst aufgelösten Stelle plötzlich wieder nur ein + * Koordinatenpaar. Auf drei Nachkommastellen gerundet (~110 m), damit ein + * paar Meter Abweichung keinen neuen Abruf auslösen. + */ +export async function koordinatenAufloesen(lat: number, lon: number): Promise { + const schluessel = `rev:${lat.toFixed(3)},${lon.toFixed(3)}` + const cache = cacheLesen() + const gemerkt = cache[schluessel] + if (typeof gemerkt === "string") return gemerkt + + try { + const antwort = await fetch( + `https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${lat}&lon=${lon}&zoom=18&addressdetails=1`, + { headers: { "Accept-Language": "de" } }, + ) + const daten = (await antwort.json()) as { + address?: Record + display_name?: string + } + const a = daten.address ?? {} + const strasse = [a["road"], a["house_number"]].filter(Boolean).join(" ") + const ort = [a["postcode"], a["city"] ?? a["town"] ?? a["village"]].filter(Boolean).join(" ") + const adresse = [strasse, ort].filter(Boolean).join(", ") || daten.display_name || null + // Nur echte Treffer merken: eine gedrosselte Antwort ist kein "hier gibt + // es keine Adresse", die soll beim nächsten Mal erneut versucht werden. + if (adresse) adresseMerken(schluessel, adresse) + return adresse + } catch { + return null + } +} + export async function adresseAufloesen(adresse: string | null | undefined): Promise { const suche = (adresse ?? "").trim() if (!suche) return null const cache = cacheLesen() - if (Object.prototype.hasOwnProperty.call(cache, suche)) return cache[suche] ?? null + if (Object.prototype.hasOwnProperty.call(cache, suche)) { + const gemerkt = cache[suche] + return typeof gemerkt === "object" ? gemerkt : null + } let treffer: Koordinate | null = null try { diff --git a/companion-app/src/screens/Einstellungen.tsx b/companion-app/src/screens/Einstellungen.tsx index dbe5806..c32d03d 100644 --- a/companion-app/src/screens/Einstellungen.tsx +++ b/companion-app/src/screens/Einstellungen.tsx @@ -6,7 +6,7 @@ import { useEffect, useRef, useState } from "react" -import { ActionButton, Feld, Seg, Switch, Tile } from "@audi-dash/ui" +import { ActionButton, Feld, Popup, Seg, Switch, Tile } from "@audi-dash/ui" import { DIENST_DOMAIN, zugangLesen, zugangVerwerfen } from "../api" import type { Profil } from "../api" @@ -14,7 +14,7 @@ import { eigeneVersion } from "../daten/appVersion" import { useDaten } from "../daten/DatenKontext" import { OtaFehler, buendelAnwenden, buendelPasst, otaMoeglich } from "../daten/ota" import type { Einstellungen as EinstellungenWerte } from "../daten/profilAdapter" -import { datumZeit, isoTag } from "../format" +import { datum, datumZeit, isoTag } from "../format" import { useBoden, useTheme } from "../theme" import { BildMitMenue, Wertzeile, Werteliste, bestaetigen } from "./bausteine" import { BILDPLAETZE, SEITE_WINTER, markenlogoUrl } from "./bilder" @@ -59,6 +59,10 @@ export function Einstellungen({ const [laeuft, setzeLaeuft] = useState(false) const [gespeichert, setzeGespeichert] = useState(false) const [importOffen, setzeImportOffen] = useState(false) + /* Abfrage "bis wann gilt der Rabatt", die das Einschalten des SmartDeals + ausloest (sdpopup im Panel). */ + const [sdAbfrage, setzeSdAbfrage] = useState(false) + const [sdEntwurf, setzeSdEntwurf] = useState("") const [otaLaeuft, setzeOtaLaeuft] = useState(false) const [otaFehler, setzeOtaFehler] = useState(null) const [updatePruefenLaeuft, setzeUpdatePruefenLaeuft] = useState(false) @@ -401,35 +405,36 @@ export function Einstellungen({ > + onChange={(an) => { + /* Einschalten fragt zuerst, bis wann der Rabatt gilt - ohne + dieses Datum kann die App ihn nie von selbst auslaufen + lassen, und genau daran haengt smartdeal.aktiv im + Profiladapter. Ausschalten braucht keine Rueckfrage. + Gleiches Verhalten wie data-sd/sdpopup im Panel. */ + if (an) { + setzeSdEntwurf(fahrzeug.smartdeal.laeuftAb ?? "") + setzeSdAbfrage(true) + return + } void profilSpeichern({ fahrzeug: { ...fahrzeug, - smartdeal: { ...fahrzeug.smartdeal, aktiv: an }, + smartdeal: { ...fahrzeug.smartdeal, aktiv: false }, }, }) - } + }} aria-label="SmartDeal aktiv" /> {fahrzeug.smartdeal.aktiv ? ( - <> - - - void profilSpeichern({ - fahrzeug: { - ...fahrzeug, - smartdeal: { ...fahrzeug.smartdeal, laeuftAb: e.target.value || null }, - }, - }) - } - /> - - + + {/* Reine Anzeige, wie im Panel: geaendert wird das Datum ueber den + Schalter, der die Abfrage oeffnet - so gibt es nur einen Weg + dorthin und keine zwei Eingabestellen fuer denselben Wert. */} + + {fahrzeug.smartdeal.laeuftAb ? datum(fahrzeug.smartdeal.laeuftAb) : "–"} + + ) : ( Ersparnis und Bildmarke werden ausgeblendet @@ -437,6 +442,42 @@ export function Einstellungen({ )} + setzeSdAbfrage(false)} + variant="form" + anchor="center" + > + SmartDeal + + setzeSdEntwurf(e.target.value)} + /> + +
+ setzeSdAbfrage(false)}>Abbrechen + { + if (!sdEntwurf) return + setzeSdAbfrage(false) + void profilSpeichern({ + fahrzeug: { + ...fahrzeug, + smartdeal: { aktiv: true, laeuftAb: sdEntwurf }, + }, + }) + }} + > + Bestätigen + +
+
+ {/* „Nacht beginnt"/„Nacht endet" sind entfallen: das Panel fuehrt sie in den Einstellungen nicht, die Nachtgrenze steht dort nur als Erlaeuterung auf der Statistik-Seite. */} diff --git a/companion-app/src/screens/MeinAudi.tsx b/companion-app/src/screens/MeinAudi.tsx index f54bf91..5237aa0 100644 --- a/companion-app/src/screens/MeinAudi.tsx +++ b/companion-app/src/screens/MeinAudi.tsx @@ -140,8 +140,11 @@ export function MeinAudi({ geheZu }: { geheZu: (name: SeitenName, id?: string) = - - + + {/* Die technischen Daten stehen eine Ebene tiefer; hier nur der + Hinweis, dass es sie gibt, statt einer abgeschnittenen + Motorzeile. */} + {/* Leerer Wert statt Strich, wie vAudi(): das Panel gibt hier den Rohwert aus. */} @@ -167,10 +170,11 @@ export function MeinAudi({ geheZu }: { geheZu: (name: SeitenName, id?: string) = /> @@ -233,15 +237,12 @@ export function MeinAudi({ geheZu }: { geheZu: (name: SeitenName, id?: string) = - + } diff --git a/companion-app/src/screens/Reifen.tsx b/companion-app/src/screens/Reifen.tsx index 0127e53..6d582c1 100644 --- a/companion-app/src/screens/Reifen.tsx +++ b/companion-app/src/screens/Reifen.tsx @@ -15,7 +15,7 @@ import { DIENST_DOMAIN } from "../api" import { useDaten } from "../daten/DatenKontext" import type { Reifensatz, ReifenArchivEintrag } from "../daten/profilAdapter" import { datum, de, deOderStrich } from "../format" -import { SymbolEdit, SymbolPaket } from "../symbole" +import { SymbolEdit, SymbolPaket, SymbolZahnradVoll } from "../symbole" import { Wertzeile, Werteliste, bestaetigen } from "./bausteine" import { Bild } from "./Bild" import { kalenderDateiLaden } from "./kalender" @@ -126,7 +126,7 @@ export function Reifen() {
{satzLabel} -
+
{ @@ -275,7 +275,7 @@ export function Reifen() { setzeNmOffen((o) => !o) }} > - +
{nmOffen && ( diff --git a/companion-app/src/screens/Standort.tsx b/companion-app/src/screens/Standort.tsx index 61f16ab..99c3c3c 100644 --- a/companion-app/src/screens/Standort.tsx +++ b/companion-app/src/screens/Standort.tsx @@ -11,6 +11,7 @@ import { useEffect, useRef, useState, type PointerEvent as ReactPointerEvent } from "react" import { useDaten } from "../daten/DatenKontext" +import { koordinatenAufloesen } from "../daten/geokodierung" import { de } from "../format" import type { Fahrzeug } from "../daten/profilAdapter" import { SymbolTanken } from "../symbole" @@ -37,8 +38,10 @@ const CI = { 'M2.8 2H5v1H3v2H2V2.8a.76.76 0 0 1 .43-.71.9.9 0 0 1 .35-.09zv.5zM9 3H7V2h2zm4 0h-2V2h2zm4 0h-2V2h2zm4 0h-2V2h2.2v.5V2h.02a1 1 0 0 1 .1.01c.07.01.16.03.25.08a.76.76 0 0 1 .43.71V5h-1zM6 6h12v12H6zm1 1v10h10V7zM3 7v2H2V7zm19 0v2h-1V7zM3 11v2H2v-2zm19 0v2h-1v-2zM3 15v2H2v-2zm19 0v2h-1v-2zM3 21v-2H2v2.2a.76.76 0 0 0 .43.71.9.9 0 0 0 .35.09h.01l.01-.5v.5H5v-1zm19-2v2.2a.76.76 0 0 1-.43.71.9.9 0 0 1-.35.09h-.01l-.01-.49V22H19v-1h2v-2zM9 22H7v-1h2zm4 0h-2v-1h2zm4 0h-2v-1h2z', planRoute: 'm18.3 7-3.65-3.65.7-.7 4.86 4.85-4.86 4.85-.7-.7L18.29 8H9.5C8.67 8 8 8.67 8 9.5V21H7V9.5A2.5 2.5 0 0 1 9.5 7z', - upload: - 'M13 20h-1V9.7l-3.65 3.65-.7-.7 4.85-4.86 4.85 4.86-.7.7L13 9.71zM6 5V4h13v1z', + // share-s aus dem Audi-CI-Satz (vom Nutzer geliefert, verbatim). Loest + // beim Teilen das upload-Icon ab, das dort nur ersatzweise stand. + shareS: + 'm11.5 1.8 3.85 3.85-.7.7L12 3.71V15h-1V3.7L8.35 6.36l-.7-.7zM5 8h4v1H6v11h11V9h-3.02V8H18v13H5z', parking: 'M0 2.5A2.5 2.5 0 0 1 2.5 0H23v20.5a2.5 2.5 0 0 1-2.5 2.5H0zM2.5 1C1.67 1 1 1.67 1 2.5V22h19.5c.83 0 1.5-.67 1.5-1.5V1zm11.59 6.35C13.6 7 13.02 7 12.5 7H8v6h4.5c.52 0 1.11-.01 1.59-.35.44-.3.91-1 .91-2.65 0-1.66-.47-2.34-.91-2.65M8 14v3H7V6h5.53c.5 0 1.38 0 2.13.53C15.47 7.09 16 8.16 16 10s-.53 2.9-1.34 3.47a3.6 3.6 0 0 1-2.13.53z', // poi-car-l: Umriss (Ring) + Vollton-Silhouette dahinter, siehe @@ -61,6 +64,11 @@ function ciIcon(d: string, px: number): string { return `` } +/* Wie ciIcon, aber mit der Korrektur fuer das Layer-Symbol (siehe dort). */ +function ciIconZentriert(d: string, px: number): string { + return `` +} + function fahrzeugMarkerHtml(px = 38): string { // Groessenwechsel wie fahrzeugMarkerSVG() im Panel: ab 34px die grosse // Zeichnung (48er-Raster), darunter die kleine (24er). Die Vorschaukarte der @@ -118,34 +126,15 @@ function standortZustand( } } -/* Reverse-Geocoding über Nominatim (OpenStreetMap), wie standortAdresseAufloesen() - im Panel — Cache-Schlüssel auf 3 Nachkommastellen (~110 m) gerundet, damit - erneutes Öffnen an derselben Stelle keinen zweiten Abruf auslöst. Modulweiter - Cache (nicht in localStorage wie die Tankstellen-Adresssuche im Panel — die - Fahrzeugposition ändert sich laufend, ein dauerhafter Cache brächte nichts). */ -let adressCacheKey: string | null = null -let adressCacheWert: string | null = null -async function standortAdresseAufloesen(lat: number, lon: number): Promise { - const key = `${lat.toFixed(3)},${lon.toFixed(3)}` - if (adressCacheKey === key) return adressCacheWert - try { - const res = await fetch( - `https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${lat}&lon=${lon}&zoom=18&addressdetails=1`, - { headers: { "Accept-Language": "de" } }, - ) - const daten = (await res.json()) as { address?: Record; display_name?: string } - const a = daten.address ?? {} - const strasse = [a["road"], a["house_number"]].filter(Boolean).join(" ") - const ort = [a["postcode"], a["city"] ?? a["town"] ?? a["village"]].filter(Boolean).join(" ") - adressCacheWert = [strasse, ort].filter(Boolean).join(", ") || daten.display_name || null - } catch { - // Bewusst NICHT den Schlüssel setzen — ein Netzfehler soll beim nächsten - // Öffnen erneut versucht werden, nicht dauerhaft als "erledigt" gelten. - return null - } - adressCacheKey = key - return adressCacheWert -} +/* Die Auflösung liegt jetzt in daten/geokodierung.ts, gemeinsam mit der + Vorwärtsrichtung und mit dauerhaftem Cache. Der frühere Cache hier hielt + nur einen einzigen Punkt und nur bis zum Neuladen — mit der Begründung, die + Fahrzeugposition ändere sich ohnehin laufend. Das stimmt fürs Fahren, nicht + fürs Parken: das Auto steht meist an denselben Stellen, und wenn Nominatim + gerade drosselt (Antwort ohne CORS-Kopf, im Browser nicht von einem + Netzfehler zu unterscheiden), stand an einer längst bekannten Stelle wieder + nur ein Koordinatenpaar. */ +const standortAdresseAufloesen = koordinatenAufloesen interface EigenePosition { lat: number @@ -284,6 +273,14 @@ export function Standort() { else setzeBlattOffen((o) => !o) } + /* Die Effekte weiter unten lesen leafletRef/kartenRef, die aber erst im + asynchron geladenen import()-Callback gesetzt werden. Beim ersten Lauf + waren sie leer, die Effekte stiegen aus - und weil ihre Abhaengigkeiten + (die Koordinaten) sich danach nicht mehr aenderten, liefen sie nie + wieder. Genau daran fehlte der Fahrzeug-Pin. Dieser Schalter bringt sie + erneut zum Laufen, sobald die Karte wirklich steht. */ + const [karteBereit, setzeKarteBereit] = useState(false) + const bekannt = fahrzeug?.standortLat != null && fahrzeug?.standortLon != null const fahrzeugPos = bekannt ? { lat: fahrzeug!.standortLat!, lon: fahrzeug!.standortLon! } : null @@ -298,6 +295,7 @@ export function Standort() { const karte = L.map(element, { attributionControl: false, zoomControl: false }) kartenRef.current = karte kachelRef.current = L.tileLayer(KACHEL_URL, { attribution: KACHEL_ZUSATZ, maxZoom: 19 }).addTo(karte) + setzeKarteBereit(true) if (fahrzeugPos) karte.setView([fahrzeugPos.lat, fahrzeugPos.lon], 15) else karte.setView([51.16, 10.45], 5) setTimeout(() => karte.invalidateSize(), 60) @@ -322,7 +320,7 @@ export function Standort() { kachelRef.current = satellit ? L.tileLayer(SAT_KACHEL_URL, { attribution: SAT_ZUSATZ, maxZoom: 19 }).addTo(karte) : L.tileLayer(KACHEL_URL, { attribution: KACHEL_ZUSATZ, maxZoom: 19 }).addTo(karte) - }, [satellit]) + }, [satellit, karteBereit]) // Fahrzeugmarker zeichnen/aktualisieren. useEffect(() => { @@ -344,7 +342,7 @@ export function Standort() { marker.on("click", () => setzeBlattOffen(true)) fahrzeugMarkerRef.current = marker } - }, [fahrzeugPos?.lat, fahrzeugPos?.lon]) + }, [fahrzeugPos?.lat, fahrzeugPos?.lon, karteBereit]) // Eigene Position zeichnen/aktualisieren. useEffect(() => { @@ -367,7 +365,7 @@ export function Standort() { zIndexOffset: -100, }).addTo(karte) } - }, [eigenePosition?.lat, eigenePosition?.lon]) + }, [eigenePosition?.lat, eigenePosition?.lon, karteBereit]) // Anfangsausschnitt: beide Punkte zeigen, sobald beide bekannt sind. useEffect(() => { @@ -386,7 +384,7 @@ export function Standort() { // (Fahrzeug bewegt sich) soll den Nutzer nicht aus einer manuellen // Kartenposition herausreißen. // eslint-disable-next-line react-hooks/exhaustive-deps - }, [!!fahrzeugPos, !!eigenePosition]) + }, [!!fahrzeugPos, !!eigenePosition, karteBereit]) // Adresse nachladen, sobald/wann immer sich die Fahrzeugposition ändert. useEffect(() => { @@ -401,7 +399,7 @@ export function Standort() { return () => { abgebrochen = true } - }, [fahrzeugPos?.lat, fahrzeugPos?.lon]) + }, [fahrzeugPos?.lat, fahrzeugPos?.lon, karteBereit]) if (!fahrzeug || !einstellungen) return null @@ -441,7 +439,13 @@ export function Standort() { onClick={() => setzeSatellit((s) => !s)} aria-pressed={satellit} aria-label={satellit ? "Satellitenansicht, zur Straßenkarte wechseln" : "Straßenkarte, zur Satellitenansicht wechseln"} - dangerouslySetInnerHTML={{ __html: ciIcon(CI.mapLayer, 22) }} + /* Das Layer-Symbol ist als einziges der vier Kartenknoepfe nicht mittig in + seinem eigenen 24er-Raster (Rand links 2,1 / rechts 1,1, gemessen ueber + getBBox) und fuellt als gefuellte Form deutlich mehr Flaeche als die drei + Kontur-Nachbarn. Die Verschiebung setzt seinen Mittelpunkt genau auf + (12,12), der Faktor 0,88 nimmt ihm das Uebergewicht - dieselbe Behandlung + wie beim Statistik-Tabsymbol. */ + dangerouslySetInnerHTML={{ __html: ciIconZentriert(CI.mapLayer, 22) }} /> + {/* Nur im aufgezogenen Zustand: im halb verdeckten Ruhezustand hat + Schließen nichts zu schließen - der Knopf sah bedienbar aus und + tat nichts. */} + {blattOffen && ( + + )}
{einstellungen.fahrzeugtitel}
{distanzAnzeige}
@@ -559,7 +568,7 @@ export function Standort() { Route
diff --git a/companion-app/src/screens/bausteine.tsx b/companion-app/src/screens/bausteine.tsx index d6c7f82..4234314 100644 --- a/companion-app/src/screens/bausteine.tsx +++ b/companion-app/src/screens/bausteine.tsx @@ -226,15 +226,20 @@ export function Wertzeile({ label, wert, zusatz, + hinweis, }: { label: ReactNode wert: ReactNode zusatz?: ReactNode + /** Setzt den Wert wie einen Kommentar (klein, gedaempft) statt wie eine + Angabe - fuer Zeilen, deren rechte Seite nur beschreibt, was hinter der + Kachel liegt, statt selbst ein Messwert zu sein. */ + hinweis?: boolean }) { return (
{label}
-
+
{wert} {zusatz && {zusatz}}
diff --git a/companion-app/src/stile/grundlage.css b/companion-app/src/stile/grundlage.css index fbfb882..a96a359 100644 --- a/companion-app/src/stile/grundlage.css +++ b/companion-app/src/stile/grundlage.css @@ -59,6 +59,16 @@ body { overflow-x: hidden; -webkit-overflow-scrolling: touch; scrollbar-width: none; + /* Ohne das greift auf echten Touch-Geräten das native Overscroll des + Browsers, sobald am oberen Rand weiter nach unten gezogen wird - es nimmt + der eigenen "Ziehen zum Aktualisieren"-Geste die Kontrolle weg, obwohl + deren Logik für sich genommen richtig arbeitet. Genau derselbe Befund wie + im Panel am 2026-08-17 (main#view, overscroll-behavior-y:contain); am + Rechner ist er nicht nachstellbar, weil dort kein natives Overscroll + mitspielt. touch-action:pan-y sagt dem Browser zusätzlich, dass die + senkrechte Geste hier zu uns gehört. */ + overscroll-behavior-y: contain; + touch-action: pan-y; } .dm-scroll::-webkit-scrollbar { diff --git a/companion-app/src/stile/screens.css b/companion-app/src/stile/screens.css index 335c85e..c2755cd 100644 --- a/companion-app/src/stile/screens.css +++ b/companion-app/src/stile/screens.css @@ -50,8 +50,13 @@ input[type="date"].dm-eingabe { -webkit-appearance: none; appearance: none; - min-width: 0; - width: fit-content; + /* Kein width:fit-content mehr. Das stammte aus der Zeit, als .ads-feld input + eine feste min-width von 132px hatte - die ist beim Paritaetsabgleich auf + min-width:0 umgestellt worden, seither zwang fit-content das Feld in eine + Breite, die je Datum und Stelle anders ausfiel. Jetzt bemisst es sich wie + jedes andere Eingabefeld (flex:0 1 auto aus @audi-dash/ui), genau wie + .feld input im Panel. */ + text-align: right; } /* Eine Zeile aus einer bearbeitbaren Liste (z. B. Selbstbeteiligung im @@ -309,13 +314,13 @@ input[type="date"].dm-eingabe { 172px leerer Platzhalter vor der Reichweite, der wichtigsten Zahl der Seite (`.bildbox.bild-fehlt.carfix.klein{height:120px}` und `.szene.bleed:has(.bild-fehlt)` im Panel). */ -/* 72px statt 120px: ohne hinterlegtes Foto stand hier eine leere Flaeche - direkt ueber der Reichweite, der wichtigsten Zahl der Seite. Der - Platzhalter bleibt sichtbar - er ist der einzige Hinweis, dass es diesen - Bildplatz gibt - nur eben schmal (Designpruefung 2026-08-30). */ +/* Der Platzhalter belegt denselben Kasten wie ein echtes Foto (16:8). Er war + zwischenzeitlich auf 120 und dann 72px gestaucht, um die Reichweite + hoeher zu ruecken - auf Ansage des Besitzers steht er wieder in voller + Groesse, weil ein Streifen nicht erkennen laesst, was dort hingehoert. */ .dm-szene .ads-bildbox:has(.ads-platzhalter) { - aspect-ratio: auto; - height: 72px; + aspect-ratio: 16 / 8; + height: auto; } .dm-galerie-wechsel .ads-bildbox:has(.ads-platzhalter) { @@ -940,9 +945,24 @@ button.dm-serviceblock:active { align-items: center; gap: 6px; } +/* Gezeichneter Pfeil wie .chev im Panel - 6x10, Strichstaerke 1,4. Vorher + stand hier eine Schriftgroesse, weil an dieser Stelle einmal ein + Schriftzeichen als Pfeil diente. */ .dm-wertzeile__chevron { + width: 6px; + height: 10px; + flex: 0 0 auto; + color: var(--fg3); + fill: none; + stroke: currentColor; + stroke-width: 1.4; +} + +/* Wert als Kommentar statt als Angabe (siehe Wertzeile hinweis). Groesse und + Farbe wie jede andere Nebenzeile der App. */ +.dm-wertzeile__hinweis { + font-size: 12.5px; color: var(--fg3); - font-size: 13px; } .dm-fussnote { @@ -1360,11 +1380,15 @@ button.dm-listenzeile { color: var(--fg); display: block; } +/* Der Kilometerstand des Radsatzes ist eine Kennzahl und traegt deshalb die + Zahlenschrift (.ads-fig, im Panel .fig mit font-size:32px) - er stand hier + bisher in der normalen Textschrift und Groesse. */ .dm-reifenzeile { display: flex; align-items: center; gap: 8px; margin-top: 14px; + font-size: 32px; } .dm-reifenzeile .ads-icon-button { width: 24px; @@ -1812,3 +1836,14 @@ button.dm-listenzeile { height: 210px; background: var(--tile-2); } + +/* Knopfzeile der SmartDeal-Abfrage - Abbrechen links, Bestaetigen rechts, + wie die .seg-Zeile im sdpopup des Panels. */ +.dm-sd-knoepfe { + display: flex; + gap: var(--sp-2); + margin-top: 14px; +} +.dm-sd-knoepfe > * { + flex: 1; +} diff --git a/custom_components/audi_dashboard/frontend/app/bundle.json b/custom_components/audi_dashboard/frontend/app/bundle.json index 10080ec..28a6083 100644 --- a/custom_components/audi_dashboard/frontend/app/bundle.json +++ b/custom_components/audi_dashboard/frontend/app/bundle.json @@ -1 +1 @@ -{"version":"2026.8.30.8","sha256":"2754d426c7145d000fd2194d4dd85db096c2fcae008eb518584f189becfb0538","bytes":256098,"gebaut":"2026-08-30T16:30:51Z"} \ No newline at end of file +{"version":"2026.8.30.9","sha256":"63b67b2116bdc2970ebcb9c02dc8c2d1ec1f0156fd70c59f88db101307054474","bytes":256695,"gebaut":"2026-08-30T21:09:06Z"} \ 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 6ae35b0..9f9b0ba 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 b936db9..97d226b 100644 --- a/custom_components/audi_dashboard/frontend/audi-dashboard-app.js +++ b/custom_components/audi_dashboard/frontend/audi-dashboard-app.js @@ -789,6 +789,20 @@ function standortErfassenFallsNoetig() { async function standortAdresseAufloesen(lat, lon) { const key = lat.toFixed(3) + "," + lon.toFixed(3); if (STANDORT_ADRESSE_KEY === key) return STANDORT_ADRESSE; + /* Dauerhafter Cache im selben Fach wie die Tankstellensuche, Schluessel mit + "rev:"-Vorsatz. Bis 2026-08-30 lag hier nur ein einziger Punkt im + Arbeitsspeicher, weg beim naechsten Neuladen - mit der Begruendung, die + Fahrzeugposition aendere sich ohnehin laufend. Das stimmt fuers Fahren, + nicht fuers Parken: das Auto steht meist an denselben Stellen, und wenn + Nominatim gerade drosselt, stand an einer laengst bekannten Stelle + wieder nur ein Koordinatenpaar. */ + const cacheKey = "rev:" + key; + const cache = geoCacheLesen(); + if (typeof cache[cacheKey] === "string") { + STANDORT_ADRESSE = cache[cacheKey]; + STANDORT_ADRESSE_KEY = key; + return STANDORT_ADRESSE; + } try { const res = await fetch(`https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${lat}&lon=${lon}&zoom=18&addressdetails=1`, { headers: { "Accept-Language": "de" } }); const daten = await res.json(); @@ -804,6 +818,9 @@ async function standortAdresseAufloesen(lat, lon) { return null; } STANDORT_ADRESSE_KEY = key; + // Nur echte Treffer merken - eine gedrosselte Antwort ist kein "hier gibt es + // keine Adresse" und soll beim naechsten Mal erneut versucht werden. + if (STANDORT_ADRESSE) geoCacheSchreiben(cacheKey, STANDORT_ADRESSE); return STANDORT_ADRESSE; } function standortAdresseLaden() { @@ -1081,6 +1098,11 @@ const CI = { selectAll: '', planRoute: '', upload: '', + /* share-l/-s aus dem Audi-CI-Satz (vom Nutzer als SVG geliefert, verbatim + uebernommen) - l im 48er-, s im 24er-Raster wie poi-l/poi-s. Loest beim + Teilen das upload-Icon ab, das dort nur ersatzweise stand. */ + shareL: '', + shareS: '', parking: '', /* battery-12v-l/-s aus dem Audi-CI-Icon-Satz (vom Nutzer als SVG geliefert, verbatim uebernommen) - l im 48er-, s im 24er-Grid wie poi-l/poi-s. */ @@ -1131,7 +1153,13 @@ function vStandort() {
+ ${STANDORTMENU_OFFEN ? ` + + ` : ""}
${esc(CONFIG.fahrzeugtitel)}
${distanz}
@@ -1177,7 +1209,7 @@ function vStandort() { ${ciSVG(CI.planRoute, 15)}Route
@@ -1642,8 +1674,10 @@ function vAudi() { Fahrzeug
-
Fahrgestellnummer
${esc(CAR.fin)}
-
Details
${esc(CAR.details)}
+
FIN
${esc(CAR.fin)}
+ +
Details
Informationen über das Auto
Erstzulassung
${esc(CAR.erstzulassung)}
Ausführung
${CONFIG.ausfuehrung ? esc(CONFIG.ausfuehrung) : "noch offen"}
Ausstattung
@@ -1703,10 +1737,10 @@ function vAudi() { Reifen
-
montiert: ${r.aktiv}räder
${esc(a.mass)}
+
aktuell montiert:
${r.aktiv}räder
Laufleistung
${a.km != null ? de(a.km) + " km" : "unbekannt"}
Wechseltermin
-
${dedat(new Date(r.wechsel.datum))}${r.wechsel.vereinbart ? "vereinbart" : "–"}
+
${dedat(new Date(r.wechsel.datum))}${r.wechsel.vereinbart ? "vereinbart" : ""}
`; } diff --git a/custom_components/audi_dashboard/frontend/audi-dashboard.css b/custom_components/audi_dashboard/frontend/audi-dashboard.css index 24b88a6..c0957ae 100644 --- a/custom_components/audi_dashboard/frontend/audi-dashboard.css +++ b/custom_components/audi_dashboard/frontend/audi-dashboard.css @@ -444,6 +444,11 @@ button.sicherheitszeile:active { background: var(--tile-2); } .sicherheitsliste .sicherheitszeile:last-child { border-bottom: 0; } .sicherheitszeile-text { flex: 1; font-size: 14px; color: var(--fg); } .sicherheitszeile .chev { margin-left: auto; } +/* Wert als Kommentar statt als Angabe - fuer Zeilen, deren rechte Seite nur + beschreibt, was hinter der Kachel liegt (Mein Audi, "Details"). Groesse + und Farbe wie jede andere Nebenzeile. */ +.row dd.row-hinweis { font-size: 12.5px; color: var(--fg3); } + .row-fussnote { flex: 1 0 100%; text-align: left; @@ -632,11 +637,11 @@ button.leaf:active { background: var(--tile-2); } 260 px gestrichelte Leerfläche vor der Reichweite, der wichtigsten Zahl der App. Mit Foto gilt wieder das volle Seitenverhältnis. */ .bildbox.bild-fehlt.carfix { aspect-ratio: auto; height: 132px; } -/* 72px statt 120px: ohne hinterlegtes Foto stand hier eine leere Flaeche - direkt ueber der Reichweite, der wichtigsten Zahl der Seite. Der - Platzhalter bleibt sichtbar - er ist der einzige Hinweis, dass es diesen - Bildplatz gibt - nur eben schmal (Designpruefung 2026-08-30). */ -.bildbox.bild-fehlt.carfix.klein { height: 72px; } +/* Der Platzhalter belegt denselben Kasten wie ein echtes Foto (16:8). Er war + zwischenzeitlich auf 120 und dann 72px gestaucht, um die Reichweite + hoeher zu ruecken - auf Ansage des Besitzers steht er wieder in voller + Groesse, weil ein Streifen nicht erkennen laesst, was dort hingehoert. */ +.bildbox.bild-fehlt.carfix.klein { aspect-ratio: 16 / 8; height: auto; } .szene.bleed:has(.bild-fehlt)::before { display: none; } .szene.bleed:has(.bild-fehlt) { margin-left: 0; margin-right: 0; } .szene.bleed:has(.bild-fehlt) .carbox { padding: 0; } diff --git a/custom_components/audi_dashboard/manifest.json b/custom_components/audi_dashboard/manifest.json index e4e04a2..54dda8a 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.8.30.8", + "version": "2026.8.30.9", "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"], diff --git a/design-system/src/components/Feld/Feld.css b/design-system/src/components/Feld/Feld.css index 32cce57..e3f5f6a 100644 --- a/design-system/src/components/Feld/Feld.css +++ b/design-system/src/components/Feld/Feld.css @@ -60,6 +60,21 @@ color: var(--fg); } +/* Aufklapp-Pfeil. appearance:none oben entfernt den nativen Pfeil des + Browsers, ersatzlos - das Auswahlfeld war dadurch von einem reinen + Textfeld nicht mehr zu unterscheiden (gemeldet an Ansicht und + Auto-Backup). Dasselbe Bild und dieselben Masse wie .feld select in + audi-dashboard-ios.css, dort seit 2026-08-16 nach Apples Vorgabe fuer + Pull-down-Knoepfe. Farbe fest auf #8E8E93 - das ist in beiden Themes die + Farbe aller uebrigen Chevrons. */ +.ads-feld select { + padding-right: 28px; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 4.5l3.5 3.5 3.5-3.5' fill='none' stroke='%238E8E93' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 9px center; + background-size: 10px 10px; +} + .ads-mit-einheit { display: inline-flex; align-items: center; diff --git a/design-system/src/components/Tile/Tile.css b/design-system/src/components/Tile/Tile.css index 8abc819..1bd9283 100644 --- a/design-system/src/components/Tile/Tile.css +++ b/design-system/src/components/Tile/Tile.css @@ -35,10 +35,18 @@ button.ads-tile--button:hover { min-width: 0; } +/* Der Pfeil steht oben neben der Ueberschrift, nicht auf halber Kachelhoehe. + Als Flex-Kind mit margin-left sass er mittig - bei einer Kachel mit + mehreren Wertezeilen zeigt er dann irgendwo in die Zeilenmitte statt auf + den Titel, der sagt, wohin es geht. Das Panel loest das seit 2026-08-13 + genau so: .go absolut, top 22px / right 18px (audi-dashboard.css und die + .tilebtn-Regel der iOS-Auflage). */ .ads-tile__go { + position: absolute; + top: 22px; + right: 18px; width: 6px; height: 10px; color: var(--fg3); flex: 0 0 auto; - margin-left: var(--sp-3); }