Befundliste des Besitzers: drei echte Fehler, dazu Feinschliff (2026.8.30.9)

Fahrzeug-Pin auf der Standortkarte fehlte: die Marker-Effekte lesen Refs,
die erst im asynchronen Leaflet-Import gesetzt werden. Beim ersten Lauf leer,
Effekt steigt aus - und weil die Koordinaten sich danach nicht mehr aendern,
lief er nie wieder. Ein Bereitschaftsschalter in den Abhaengigkeiten behebt
das fuer Marker, Kachelebene und eigene Position zugleich.

Kachelpfeil auf "Mein Audi" sass auf halber Kachelhoehe statt oben neben der
Ueberschrift; Auswahlfelder hatten nach appearance:none ueberhaupt keinen
Aufklapp-Pfeil mehr.

SmartDeal: das Datumsfeld war fehlerhaft und die Abfrage, bis wann der Rabatt
gilt, fehlte ganz. Das Panel fragt beim Einschalten und zeigt danach nur an -
genau daran haengt, dass der Rabatt von selbst auslaufen kann. Portiert.

Adressen statt Koordinaten: Nominatim drosselt uns inzwischen. Beide Apps
fielen korrekt zurueck, merkten sich Treffer aber nur bis zum Neuladen. Jetzt
dauerhaft im selben Cache wie die Tankstellensuche.

Ziehen zum Aktualisieren auf dem Geraet: dem Scrollbehaelter fehlte
overscroll-behavior, das native Overscroll nahm die Geste weg - derselbe
Befund, den das Panel am 2026-08-17 hatte.

Ausserdem: Platzhalter wieder in voller Groesse, FIN statt Fahrgestellnummer,
Details als Hinweiszeile, Chevron an der Batteriespannung, Reifenzeile
"aktuell montiert:", Zahnrad statt Stift am Anzugsmoment, Radkilometer in der
Zahlenschrift, Layer-Icon zentriert, share-s beim Teilen, Schliessen-Knopf nur
bei aufgezogener Karte.

Nicht nachstellbar und zurueckgemeldet: "Fahrten/Tanken, grauer Hintergrund" -
Panel und App sind dort deckungsgleich.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-30 23:12:33 +02:00
parent f38907991d
commit a08922e9a0
16 changed files with 396 additions and 110 deletions
+72 -1
View File
@@ -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:
+55 -3
View File
@@ -23,15 +23,27 @@ export interface Koordinate {
const CACHE_SCHLUESSEL = "dm360.geocache"
function cacheLesen(): Record<string, Koordinate | null> {
function cacheLesen(): Record<string, Koordinate | string | null> {
try {
const roh = localStorage.getItem(CACHE_SCHLUESSEL)
return roh ? (JSON.parse(roh) as Record<string, Koordinate | null>) : {}
return roh ? (JSON.parse(roh) as Record<string, Koordinate | string | null>) : {}
} 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<string, unknown>) : {}
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<string | null> {
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<string, string>
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<Koordinate | null> {
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 {
+63 -22
View File
@@ -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<string | null>(null)
const [updatePruefenLaeuft, setzeUpdatePruefenLaeuft] = useState(false)
@@ -401,35 +405,36 @@ export function Einstellungen({
>
<Switch
checked={fahrzeug.smartdeal.aktiv}
onChange={(an) =>
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"
/>
</Feld>
{fahrzeug.smartdeal.aktiv ? (
<>
<Feld label="Gültig bis" last>
<input
className="dm-eingabe"
type="date"
value={fahrzeug.smartdeal.laeuftAb ?? ""}
onChange={(e) =>
void profilSpeichern({
fahrzeug: {
...fahrzeug,
smartdeal: { ...fahrzeug.smartdeal, laeuftAb: e.target.value || null },
},
})
}
/>
</Feld>
</>
<Feld label="Gültig bis" last>
{/* 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. */}
<span>
{fahrzeug.smartdeal.laeuftAb ? datum(fahrzeug.smartdeal.laeuftAb) : ""}
</span>
</Feld>
) : (
<span className="ads-eyebrow" style={{ marginTop: "14px" }}>
Ersparnis und Bildmarke werden ausgeblendet
@@ -437,6 +442,42 @@ export function Einstellungen({
)}
</Tile>
<Popup
open={sdAbfrage}
onClose={() => setzeSdAbfrage(false)}
variant="form"
anchor="center"
>
<span className="ads-eyebrow">SmartDeal</span>
<Feld label="Gültig bis" last>
<input
className="dm-eingabe"
type="date"
value={sdEntwurf}
onChange={(e) => setzeSdEntwurf(e.target.value)}
/>
</Feld>
<div className="dm-sd-knoepfe">
<ActionButton onClick={() => setzeSdAbfrage(false)}>Abbrechen</ActionButton>
<ActionButton
variant="primary"
disabled={!sdEntwurf}
onClick={() => {
if (!sdEntwurf) return
setzeSdAbfrage(false)
void profilSpeichern({
fahrzeug: {
...fahrzeug,
smartdeal: { aktiv: true, laeuftAb: sdEntwurf },
},
})
}}
>
Bestätigen
</ActionButton>
</div>
</Popup>
{/* „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. */}
+10 -9
View File
@@ -140,8 +140,11 @@ export function MeinAudi({ geheZu }: { geheZu: (name: SeitenName, id?: string) =
<Werteliste
kinder={
<>
<Wertzeile label="Fahrgestellnummer" wert={fahrzeug.fin || ""} />
<Wertzeile label="Details" wert={fahrzeug.details || ""} />
<Wertzeile label="FIN" wert={fahrzeug.fin || ""} />
{/* Die technischen Daten stehen eine Ebene tiefer; hier nur der
Hinweis, dass es sie gibt, statt einer abgeschnittenen
Motorzeile. */}
<Wertzeile label="Details" wert="Informationen über das Auto" hinweis />
{/* Leerer Wert statt Strich, wie vAudi(): das Panel gibt hier
den Rohwert aus. */}
<Wertzeile label="Erstzulassung" wert={fahrzeug.erstzulassung} />
@@ -167,10 +170,11 @@ export function MeinAudi({ geheZu }: { geheZu: (name: SeitenName, id?: string) =
/>
<button type="button" className="dm-wertzeile dm-wertzeile--knopf" onClick={() => geheZu("battverlauf")}>
<dt>Batteriespannung</dt>
{/* Ohne Chevron: das Panel gibt der Zeile nur den
Zeiger-Cursor, kein eigenes Pfeilsymbol. */}
<dd>
{fahrzeug.batteriespannung != null ? `${de(fahrzeug.batteriespannung, 1)} V` : "unbekannt"}
<svg className="dm-wertzeile__chevron" viewBox="0 0 6 10" aria-hidden="true">
<polyline points="1,1 5,5 1,9" />
</svg>
</dd>
</button>
</>
@@ -233,15 +237,12 @@ export function MeinAudi({ geheZu }: { geheZu: (name: SeitenName, id?: string) =
<Werteliste
kinder={
<>
<Wertzeile
label={`montiert: ${fahrzeug.reifen.aktiv}räder`}
wert={(aktiverSatz["mass"] as string) || ""}
/>
<Wertzeile label="aktuell montiert:" wert={`${fahrzeug.reifen.aktiv}räder`} />
<Wertzeile label="Laufleistung" wert={deOderStrich(aktiverSatz.km) + " km"} />
<Wertzeile
label="Wechseltermin"
wert={datum(fahrzeug.reifen.wechsel["datum"] as string)}
zusatz={fahrzeug.reifen.wechsel["vereinbart"] ? "vereinbart" : ""}
zusatz={fahrzeug.reifen.wechsel["vereinbart"] ? "vereinbart" : undefined}
/>
</>
}
+3 -3
View File
@@ -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() {
<div className="dm-radkopf-row">
<div className="dm-radkopf">
<span className="dm-tile-headline">{satzLabel}</span>
<div className="dm-reifenzeile">
<div className="ads-fig dm-reifenzeile">
<IconButton
aria-label={`${satzLabel} Kilometerstand ändern`}
onClick={() => {
@@ -275,7 +275,7 @@ export function Reifen() {
setzeNmOffen((o) => !o)
}}
>
<SymbolEdit groesse={16} />
<SymbolZahnradVoll groesse={17} />
</IconButton>
</div>
{nmOffen && (
+56 -47
View File
@@ -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 `<svg viewBox="0 0 24 24" width="${px}" height="${px}" fill="none" aria-hidden="true"><path fill="currentColor" d="${d}"/></svg>`
}
/* Wie ciIcon, aber mit der Korrektur fuer das Layer-Symbol (siehe dort). */
function ciIconZentriert(d: string, px: number): string {
return `<svg viewBox="0 0 24 24" width="${px}" height="${px}" fill="none" aria-hidden="true"><g transform="translate(1.44 1.44) scale(0.88) translate(-0.55 0.2)"><path fill="currentColor" d="${d}"/></g></svg>`
}
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<string | null> {
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<string, string>; 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) }}
/>
<button
type="button"
@@ -506,16 +510,21 @@ export function Standort() {
aria-expanded={blattOffen}
aria-label={blattOffen ? "Fahrzeugdetails einklappen" : "Fahrzeugdetails aufklappen"}
/>
<button
type="button"
className="dm-standortmenu__schliessen"
onClick={() => setzeBlattOffen(false)}
aria-label="Schließen"
>
<svg viewBox="0 0 20 20" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round">
<path d="M5 5l10 10M15 5 5 15" />
</svg>
</button>
{/* Nur im aufgezogenen Zustand: im halb verdeckten Ruhezustand hat
Schließen nichts zu schließen - der Knopf sah bedienbar aus und
tat nichts. */}
{blattOffen && (
<button
type="button"
className="dm-standortmenu__schliessen"
onClick={() => setzeBlattOffen(false)}
aria-label="Schließen"
>
<svg viewBox="0 0 20 20" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round">
<path d="M5 5l10 10M15 5 5 15" />
</svg>
</button>
)}
<div className="dm-standortmenu__kopf">
<div className="dm-standortmenu__name">{einstellungen.fahrzeugtitel}</div>
<div className="dm-standortmenu__distanz">{distanzAnzeige}</div>
@@ -559,7 +568,7 @@ export function Standort() {
Route
</a>
<button type="button" className="dm-standort-pille" onClick={teilen} disabled={!bekannt}>
<span dangerouslySetInnerHTML={{ __html: ciIcon(CI.upload, 15) }} />
<span dangerouslySetInnerHTML={{ __html: ciIcon(CI.shareS, 15) }} />
Teilen
</button>
</div>
+6 -1
View File
@@ -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 (
<div className="dm-wertzeile">
<dt>{label}</dt>
<dd>
<dd className={hinweis ? "dm-wertzeile__hinweis" : undefined}>
{wert}
{zusatz && <small>{zusatz}</small>}
</dd>
+10
View File
@@ -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 {
+44 -9
View File
@@ -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;
}
@@ -1 +1 @@
{"version":"2026.8.30.8","sha256":"2754d426c7145d000fd2194d4dd85db096c2fcae008eb518584f189becfb0538","bytes":256098,"gebaut":"2026-08-30T16:30:51Z"}
{"version":"2026.8.30.9","sha256":"63b67b2116bdc2970ebcb9c02dc8c2d1ec1f0156fd70c59f88db101307054474","bytes":256695,"gebaut":"2026-08-30T21:09:06Z"}
@@ -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: '<path fill="currentColor" d="M2.8 2H5v1H3v2H2V2.8a.76.76 0 0 1 .43-.71.9.9 0 0 1 .35-.09zv.5zM9 3H7V2h2zm4 0h-2V2h2zm4 0h-2V2h2zm4 0h-2V2h2.2v.5V2h.02a1 1 0 0 1 .1.01c.07.01.16.03.25.08a.76.76 0 0 1 .43.71V5h-1zM6 6h12v12H6zm1 1v10h10V7zM3 7v2H2V7zm19 0v2h-1V7zM3 11v2H2v-2zm19 0v2h-1v-2zM3 15v2H2v-2zm19 0v2h-1v-2zM3 21v-2H2v2.2a.76.76 0 0 0 .43.71.9.9 0 0 0 .35.09h.01l.01-.5v.5H5v-1zm19-2v2.2a.76.76 0 0 1-.43.71.9.9 0 0 1-.35.09h-.01l-.01-.49V22H19v-1h2v-2zM9 22H7v-1h2zm4 0h-2v-1h2zm4 0h-2v-1h2z"/>',
planRoute: '<path fill="currentColor" d="m18.3 7-3.65-3.65.7-.7 4.86 4.85-4.86 4.85-.7-.7L18.29 8H9.5C8.67 8 8 8.67 8 9.5V21H7V9.5A2.5 2.5 0 0 1 9.5 7z"/>',
upload: '<path fill="currentColor" d="M13 20h-1V9.7l-3.65 3.65-.7-.7 4.85-4.86 4.85 4.86-.7.7L13 9.71zM6 5V4h13v1z"/>',
/* 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: '<path fill="currentColor" d="m23.5 3.8 6.85 6.85-.7.7L24 5.71V29h-1V5.7l-5.65 5.65-.7-.7 6.85-6.86ZM11 16h8v1h-7v23h23V17h-7v-1h8v25H11z"/>',
shareS: '<path fill="currentColor" d="m11.5 1.8 3.85 3.85-.7.7L12 3.71V15h-1V3.7L8.35 6.36l-.7-.7zM5 8h4v1H6v11h11V9h-3.02V8H18v13H5z"/>',
parking: '<path fill="currentColor" d="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"/>',
/* 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() {
<div class="kartensteuerung">
<button type="button" data-standort-layer aria-pressed="${SMAP_STIL === "satellit"}"
aria-label="${SMAP_STIL === "satellit" ? "Satellitenansicht, zur Straßenkarte wechseln" : "Straßenkarte, zur Satellitenansicht wechseln"}">
${ciSVG(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. -->
${ciSVG(CI.mapLayer, 22).replace("<path", '<g transform="translate(1.44 1.44) scale(0.88) translate(-0.55 0.2)"><path').replace("</svg>", "</g></svg>")}
</button>
<button type="button" data-standort-fahrzeug aria-label="Fahrzeug auf der Karte suchen">
${ciSVG(CI.carSearch, 22)}
@@ -1151,9 +1179,13 @@ function vStandort() {
<button type="button" class="standortmenu-griff" data-standort-umschalten
aria-expanded="${STANDORTMENU_OFFEN}" aria-controls="standortmenu"
aria-label="Fahrzeugdetails ${STANDORTMENU_OFFEN ? "einklappen" : "aufklappen"}"></button>
${STANDORTMENU_OFFEN ? `
<!-- Nur im aufgezogenen Zustand. Im halb verdeckten Ruhezustand hat
Schließen nichts zu schließen - der Knopf sah bedienbar aus und
tat nichts. -->
<button type="button" class="standortmenu-schliessen" data-standort-schliessen aria-label="Schließen">
<svg viewBox="0 0 20 20" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M5 5l10 10M15 5 5 15"/></svg>
</button>
</button>` : ""}
<div class="standortmenu-kopf">
<div class="standortmenu-name">${esc(CONFIG.fahrzeugtitel)}</div>
<div class="standortmenu-distanz" id="standortDistanz">${distanz}</div>
@@ -1177,7 +1209,7 @@ function vStandort() {
${ciSVG(CI.planRoute, 15)}Route
</a>
<button type="button" class="standort-pille" data-standort-teilen ${bekannt ? "" : "disabled"}>
${ciSVG(CI.upload, 15)}Teilen
${ciSVG(CI.shareS, 15)}Teilen
</button>
</div>
</div>
@@ -1642,8 +1674,10 @@ function vAudi() {
<span class="label">Fahrzeug</span>
<svg class="go" viewBox="0 0 6 10"><polyline points="1,1 5,5 1,9"/></svg>
<dl class="rows" style="margin-top:10px">
<div class="row"><dt>Fahrgestellnummer</dt><dd>${esc(CAR.fin)}</dd></div>
<div class="row"><dt>Details</dt><dd>${esc(CAR.details)}</dd></div>
<div class="row"><dt>FIN</dt><dd>${esc(CAR.fin)}</dd></div>
<!-- Die technischen Daten stehen eine Ebene tiefer (Identität und Technik);
hier nur der Hinweis darauf, statt einer abgeschnittenen Motorzeile. -->
<div class="row"><dt>Details</dt><dd class="row-hinweis">Informationen über das Auto</dd></div>
<div class="row"><dt>Erstzulassung</dt><dd>${esc(CAR.erstzulassung)}</dd></div>
<div class="row"><dt>Ausführung</dt><dd>${CONFIG.ausfuehrung ? esc(CONFIG.ausfuehrung) : "noch offen"}</dd></div>
<div class="row" style="border-bottom:0"><dt>Ausstattung</dt>
@@ -1703,10 +1737,10 @@ function vAudi() {
<span class="label">Reifen</span>
<svg class="go" viewBox="0 0 6 10"><polyline points="1,1 5,5 1,9"/></svg>
<dl class="rows" style="margin-top:10px">
<div class="row"><dt>montiert: ${r.aktiv}räder</dt><dd>${esc(a.mass)}</dd></div>
<div class="row"><dt>aktuell montiert:</dt><dd>${r.aktiv}räder</dd></div>
<div class="row"><dt>Laufleistung</dt><dd>${a.km != null ? de(a.km) + " km" : "unbekannt"}</dd></div>
<div class="row" style="border-bottom:0"><dt>Wechseltermin</dt>
<dd>${dedat(new Date(r.wechsel.datum))}<small>${r.wechsel.vereinbart ? "vereinbart" : ""}</small></dd></div>
<dd>${dedat(new Date(r.wechsel.datum))}${r.wechsel.vereinbart ? "<small>vereinbart</small>" : ""}</dd></div>
</dl></button>`;
}
@@ -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; }
@@ -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"],
@@ -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;
+9 -1
View File
@@ -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);
}