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:
@@ -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 {
|
||||
|
||||
@@ -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. */}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user