Fahrtenliste zeigt Orte aus dem Zwischenspeicher (2026.9.1.23)

Was die Einzelfahrt einmal aufgeloest hat, zeigt die Liste umsonst mit:
ortAusCache() liest nur den Zwischenspeicher und fragt nie das Netz. Ein Abruf
je Zeile waere genau der Vorratsabruf, um dessen Unterlassung Nominatim bittet.

In der Liste steht nur der ORT, nicht die Anschrift (Wunsch des Eigentuemers):
eine Zeile traegt Datum, Art, Strecke und Verbrauch, zwei volle Anschriften
passen dort nicht. Auf der Einzelfahrt bleibt die vollstaendige Anschrift.

Der Ortsname wird beim Aufloesen separat gemerkt ("ort:"-Schluessel), nicht aus
der Anschrift geschnitten - faellt Nominatim auf display_name zurueck, stuende
hinter dem letzten Komma das LAND.

Der Rueckfall war trotzdem noetig, und das Ausliefern hat es gezeigt: die Liste
stand weiter voller Anschriften, weil der Ortsname nur bei einem frischen Abruf
entsteht und die Adressen laengst im Speicher lagen. stadtAusAnschrift()
schneidet ihn deshalb aus einer gespeicherten Anschrift - aber nur, wenn hinter
dem letzten Komma eine Postleitzahl steht. Damit greift es bei unserem Format
"<Strasse>, <PLZ> <Ort>" und niemals bei display_name.

VERIFIZIERT: 7 Faelle gegen den Schnitt (mit/ohne Strasse, benannter Platz,
ohne PLZ, display_name-Form, null), Panel als Modul, tsc --noEmit und
vite build sauber, 165/165 Tests gruen. Im Browser: "Eichstaett -> Adelschlag"
in der Liste, volle Anschrift auf der Einzelfahrt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-01 20:36:24 +02:00
parent 0f680780a4
commit ce287ccac2
7 changed files with 193 additions and 8 deletions
+28 -1
View File
@@ -1,7 +1,8 @@
# AGENTS.md — Project state, review findings, open items, and working rules
**Last updated: 2026-09-01** (Einzelfahrt: Orte aus den Koordinaten, Kleingedrucktes weg, „Min.",
`2026.9.1.20`, Abschnitt BJ. Davor: Strecken gestaffelt, Abschnitt BI. Davor: Audit ueber Panel, App und Backend, Abschnitt BH. Davor: Setup mit gruener Bestaetigung und Erwartungswert je
`2026.9.1.23`, Abschnitt BJ - dazu Orte in der Fahrtenliste ohne Netzabruf.
Davor: Strecken gestaffelt, Abschnitt BI. Davor: Audit ueber Panel, App und Backend, Abschnitt BH. Davor: Setup mit gruener Bestaetigung und Erwartungswert je
Listenposition, Abschnitt BG. Davor: Rueckblick ohne Nullfahrten, Streckenwahl gemeinsam in `verlauf.py`,
`2026.9.1.14`, Abschnitt BF. Davor: Fahrtstrecke auf 100 m: CAN als Anker, GNSS als Nachkommastelle,
`2026.9.1.10`, Abschnitt BE. Davor: Nachlauf-Abzug nur mit zugeordnetem `TRIP_SENSOR`, `2026.9.1.9`;
@@ -7070,6 +7071,32 @@ Verifiziert: Panel als Modul geparst, `tsc --noEmit` und `vite build` sauber, 16
im Browser nachgesehen — Adressen, `Dauer 13 Min.`, und die Liste zeigt die Staffelung aus
Abschnitt BI (`6,9 km`, `21,0 km`, `104 km`, `478 km`).
### Nachtrag (2026.9.1.21.23): die Liste zeigt Orte, ohne einen einzigen Abruf
**Was schon im Speicher liegt, zeigt die Liste umsonst mit.** `ortAusCache()` liest nur den
Zwischenspeicher und fragt nie das Netz. Ein Abruf je Zeile wäre genau der Vorratsabruf, um dessen
Unterlassung Nominatim bittet — bei zweihundert Fahrten vierhundert Anfragen für einen Blick auf
die Liste.
**In der Liste nur der Ort, nicht die Anschrift** (Wunsch des Eigentümers): eine Zeile trägt Datum,
Art, Strecke und Verbrauch; „Schottenau, 85072 Eichstätt → Am Anger 7a, 85111 Adelschlag" ist länger
als der ganze Rest. Auf der Einzelfahrt bleibt die vollständige Anschrift.
Der Ortsname wird beim Auflösen **separat gemerkt** (`ort:`-Schlüssel), nicht nachträglich aus der
Anschrift geschnitten — fällt Nominatim auf `display_name` zurück, stünde hinter dem letzten Komma
das **Land**, und in der Liste stünde „Deutschland → Deutschland".
**Der Rückfall war trotzdem nötig, und das Ausliefern hat es gezeigt:** die Liste stand weiter voller
Anschriften. Grund — der Ortsname entsteht nur bei einem *frischen* Abruf, und die Adressen lagen
längst im Speicher, also kehrte die Auflösung sofort zurück und schrieb nichts. `stadtAusAnschrift()`
schneidet den Ort deshalb aus einer bereits gespeicherten Anschrift, **aber nur wenn hinter dem
letzten Komma eine Postleitzahl steht**. Damit greift es bei unserem eigenen Format
`„<Straße>, <PLZ> <Ort>"` und niemals bei `display_name`.
Geprüft: 7 Fälle gegen den Schnitt (mit/ohne Straße, benannter Platz, ohne PLZ, `display_name`-Form,
`null`). Im Browser nachgesehen: `„Eichstätt → Adelschlag"` in der Liste, volle Anschrift auf der
Einzelfahrt.
## Working conventions (observed — keep them)
- German is the project language: identifiers, comments, commits, UI texts. Exceptions:
+65 -1
View File
@@ -77,6 +77,67 @@ function cacheSchreiben(suche: string, treffer: Koordinate | null): void {
* Koordinatenpaar. Auf drei Nachkommastellen gerundet (~110 m), damit ein
* paar Meter Abweichung keinen neuen Abruf auslösen.
*/
/** Ein bereits aufgeloester Ort aus dem Zwischenspeicher - OHNE Netzabruf.
*
* Die Einzelfahrt loest Koordinaten bei Bedarf auf und legt das Ergebnis im
* selben Fach ab wie die Standortansicht. Was einmal dort steht, kann die
* Liste umsonst mitbenutzen: kein zusaetzlicher Abruf, keine Wartezeit, kein
* Flackern beim Zeichnen.
*
* Bewusst NUR aus dem Speicher. Ein Abruf je Zeile waere genau der
* Vorratsabruf, um dessen Unterlassung Nominatim in seinen
* Nutzungsbedingungen bittet - bei zweihundert Fahrten waeren das
* vierhundert Anfragen fuer einen Blick auf die Liste.
*
* Schluessel wie bei der Aufloesung: drei Nachkommastellen (~110 m). */
/** Der Ortsname allein, fuer die Fahrtenliste.
*
* In der Liste steht eine Fahrt in einer Zeile neben Datum, Art, Strecke und
* Verbrauch. Zwei volle Anschriften passen dort nicht - "Schottenau, 85072
* Eichstaett -> Am Anger 7a, 85111 Adelschlag" ist laenger als der Rest der
* Zeile zusammen. Auf der Einzelfahrt bleibt die vollstaendige Anschrift.
*
* Der Ort wird beim Aufloesen SEPARAT gemerkt (Schluessel "ort:"), nicht
* nachtraeglich aus der Anschrift geschnitten. Das Zerlegen der eigenen
* Ausgabe waere fast immer richtig und in einem Fall falsch: faellt Nominatim
* auf display_name zurueck, steht hinter dem letzten Komma das LAND. Dann
* stuende in der Liste "Deutschland -> Deutschland". */
/** Der Ortsname aus einer bereits gespeicherten Anschrift.
*
* Rueckfall fuer alles, was VOR dem Merken des Ortsnamens aufgeloest wurde:
* dort liegt nur die volle Anschrift, und ein neuer Netzabruf, nur um den Ort
* zu erfahren, waere Verschwendung.
*
* Geschnitten wird ausschliesslich, wenn hinter dem letzten Komma eine
* Postleitzahl steht - unser eigenes Format ist "<Strasse>, <PLZ> <Ort>".
* Faellt Nominatim auf display_name zurueck, fehlt die PLZ und hinter dem
* letzten Komma stuende das LAND; dann lieber die volle Anschrift zeigen als
* "Deutschland". */
export function stadtAusAnschrift(anschrift: string | null | undefined): string | null {
if (!anschrift) return null
const teil = String(anschrift).split(", ").pop()?.trim() ?? ""
const treffer = teil.match(/^\d{4,5}\s+(.+)$/)
return treffer && treffer[1] ? treffer[1].trim() : null
}
export function stadtAusCache(
lat: number | null | undefined,
lon: number | null | undefined,
): string | null {
if (lat === null || lat === undefined || lon === null || lon === undefined) return null
const wert = cacheLesen()[`ort:${lat.toFixed(3)},${lon.toFixed(3)}`]
return typeof wert === "string" && wert ? wert : null
}
export function ortAusCache(
lat: number | null | undefined,
lon: number | null | undefined,
): string | null {
if (lat === null || lat === undefined || lon === null || lon === undefined) return null
const wert = cacheLesen()[`rev:${lat.toFixed(3)},${lon.toFixed(3)}`]
return typeof wert === "string" ? wert : null
}
export async function koordinatenAufloesen(lat: number, lon: number): Promise<string | null> {
const schluessel = `rev:${lat.toFixed(3)},${lon.toFixed(3)}`
const cache = cacheLesen()
@@ -96,7 +157,8 @@ export async function koordinatenAufloesen(lat: number, lon: number): Promise<st
}
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 stadt = (a["city"] ?? a["town"] ?? a["village"] ?? "").trim()
const ort = [a["postcode"], stadt].filter(Boolean).join(" ")
/* Steht das Auto auf einem benannten Platz — Parkplatz, Parkhaus,
Rastplatz, Ladesäule —, sagt dessen Name mehr als die nächstgelegene
Hausnummer: "Parkhaus Westentor" statt "Rathausstraße 4". Nominatim
@@ -111,6 +173,8 @@ export async function koordinatenAufloesen(lat: number, lon: number): Promise<st
// 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)
// Der reine Ortsname zusätzlich, für die Fahrtenliste — siehe stadtAusCache().
if (stadt) adresseMerken(`ort:${lat.toFixed(3)},${lon.toFixed(3)}`, stadt)
return adresse
} catch {
return null
+26 -2
View File
@@ -9,6 +9,7 @@ import { Accordion, ActionButton, Feld, Tile } from "@audi-dash/ui"
import { useDaten } from "../daten/DatenKontext"
import { nachJahrUndMonat } from "../daten/statistik"
import { ortAusCache, stadtAusAnschrift, stadtAusCache } from "../daten/geokodierung"
import { datum, de, streckeText, summe } from "../format"
import type { SeitenName } from "../navigation"
import { Blattzeile, Leerzustand } from "./bausteine"
@@ -20,8 +21,31 @@ import { Zeilenmenue } from "./Zeilenmenue"
"Fahrt ohne Ortsangabe". Trug bisher in der Liste hier gar nichts bei
(nur auf der Einzelfahrt-Seite sichtbar) - im Panel ist das die
führende Information jeder Fahrtzeile. */
function strecke(fahrt: { start_address?: string | null; end_address?: string | null }): string | null {
const a = fahrt.start_address, b = fahrt.end_address
function strecke(fahrt: {
start_address?: string | null
end_address?: string | null
start_lat?: number | null
start_lon?: number | null
end_lat?: number | null
end_lon?: number | null
}): string | null {
// Was die Einzelfahrt schon einmal aufgeloest hat, zeigt die Liste umsonst
// mit - siehe ortAusCache(), kein zusaetzlicher Abruf.
// Ortsname bevorzugt; ohne ihn die volle Anschrift, damit eine vor dieser
// Aenderung aufgeloeste Fahrt nicht ploetzlich wieder ortlos dasteht.
const ortsname = (
adresse: string | null | undefined,
lat: number | null | undefined,
lon: number | null | undefined,
): string | null => {
if (adresse) return adresse
const gemerkt = stadtAusCache(lat, lon)
if (gemerkt) return gemerkt
const voll = ortAusCache(lat, lon)
return stadtAusAnschrift(voll) ?? voll
}
const a = ortsname(fahrt.start_address, fahrt.start_lat, fahrt.start_lon)
const b = ortsname(fahrt.end_address, fahrt.end_lat, fahrt.end_lon)
if (!a && !b) return "Fahrt ohne Ortsangabe"
if (!b) return a ?? null
if (!a) return b
@@ -1 +1 @@
{"version":"2026.9.1.20","sha256":"20cc80f881d429303da2558b3171aabe414851aacbe2fee9253f7026c376043b","bytes":266021,"gebaut":"2026-09-01T17:35:23Z"}
{"version":"2026.9.1.23","sha256":"c3e1376f4e314b2994bc6341c423b92d251142e0abd2abe4752e1f5f1a578d13","bytes":266223,"gebaut":"2026-09-01T18:35:21Z"}
@@ -754,6 +754,7 @@ let USER_POS_FEHLER = null;
let USER_WATCH = null; // watchPosition-Kennung, solange die Standortseite offen ist
let STANDORT_ADRESSE = null; // zuletzt aufgelöste Adresse (Cache über Koordinaten-Rundung)
let STANDORT_ADRESSE_KEY = null;
let STANDORT_STADT = ""; // reiner Ortsname zum selben Punkt
let STANDORTMENU_OFFEN = false;
let SMAP = null, SMAP_TILE = null, SMAP_STIL = null; // Vollbild-Karte (eigenständig von MAP/TMAP); SMAP_STIL: null/"strasse" oder "satellit"
let TMAP = null, TMAP_TILE = null; // Vorschau-Karte der Übersicht-Kachel
@@ -1016,7 +1017,8 @@ async function standortAdresseAufloesen(lat, lon) {
const daten = await res.json();
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(" ");
STANDORT_STADT = (a.city || a.town || a.village || "").trim();
const ort = [a.postcode, STANDORT_STADT].filter(Boolean).join(" ");
/* Steht das Auto auf einem benannten Platz - Parkplatz, Parkhaus,
Rastplatz -, sagt dessen Name mehr als die naechstgelegene Hausnummer.
Nominatim liefert ihn im Feld name; die Pruefung auf die Art der
@@ -1038,6 +1040,9 @@ async function standortAdresseAufloesen(lat, lon) {
// 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);
// Der reine Ortsname zusaetzlich, fuer die Fahrtenliste - siehe
// stadtAusCache().
if (STANDORT_STADT) geoCacheSchreiben("ort:" + key, STANDORT_STADT);
return STANDORT_ADRESSE;
}
function standortAdresseLaden() {
@@ -3720,8 +3725,73 @@ function istNachtZeit(ts) {
}
/* D3: Die Strecke führt die Zeile, das Datum steht darunter. Umbruch nur
zwischen den Orten, nie am Pfeil. */
/* Ein bereits aufgeloester Ort aus dem Zwischenspeicher - OHNE Netzabruf.
Die Einzelfahrt loest Koordinaten bei Bedarf auf und legt das Ergebnis im
selben Fach ab wie die Standortansicht. Was einmal dort steht, kann die
Liste umsonst mitbenutzen: kein zusaetzlicher Abruf, keine Wartezeit, kein
Flackern beim Zeichnen.
Bewusst NUR aus dem Speicher. Ein Abruf je Zeile waere genau der
Vorratsabruf, um dessen Unterlassung Nominatim in seinen
Nutzungsbedingungen bittet - bei zweihundert Fahrten waeren das
vierhundert Anfragen fuer einen Blick auf die Liste.
Schluessel wie bei der Aufloesung: drei Nachkommastellen (~110 m). */
function ortAusCache(lat, lon) {
if (lat == null || lon == null) return null;
const wert = geoCacheLesen()["rev:" + lat.toFixed(3) + "," + lon.toFixed(3)];
return typeof wert === "string" ? wert : null;
}
/* Der Ortsname allein, fuer die Fahrtenliste.
In der Liste steht eine Fahrt in einer Zeile neben Datum, Art, Strecke und
Verbrauch. Zwei volle Anschriften passen dort nicht - "Schottenau, 85072
Eichstaett -> Am Anger 7a, 85111 Adelschlag" ist laenger als der Rest der
Zeile zusammen. Auf der Einzelfahrt bleibt die vollstaendige Anschrift.
Der Ort wird beim Aufloesen SEPARAT gemerkt (Schluessel "ort:"), nicht
nachtraeglich aus der Anschrift geschnitten. Das Zerlegen der eigenen
Ausgabe waere fast immer richtig und in einem Fall falsch: faellt Nominatim
auf display_name zurueck, steht hinter dem letzten Komma das LAND. Dann
stuende in der Liste "Deutschland -> Deutschland". */
/* Der Ortsname aus einer bereits gespeicherten Anschrift.
Rueckfall fuer alles, was VOR dem Merken des Ortsnamens aufgeloest wurde:
dort liegt nur die volle Anschrift, und ein neuer Netzabruf, nur um den Ort
zu erfahren, waere Verschwendung.
Geschnitten wird ausschliesslich, wenn hinter dem letzten Komma eine
Postleitzahl steht - unser eigenes Format ist "<Strasse>, <PLZ> <Ort>".
Faellt Nominatim auf display_name zurueck, fehlt die PLZ und hinter dem
letzten Komma stuende das LAND; dann lieber die volle Anschrift zeigen als
"Deutschland". */
function stadtAusAnschrift(anschrift) {
if (!anschrift) return null;
const teil = String(anschrift).split(", ").pop().trim();
const treffer = teil.match(/^\d{4,5}\s+(.+)$/);
return treffer ? treffer[1].trim() : null;
}
function stadtAusCache(lat, lon) {
if (lat == null || lon == null) return null;
const wert = geoCacheLesen()["ort:" + lat.toFixed(3) + "," + lon.toFixed(3)];
return typeof wert === "string" && wert ? wert : null;
}
function strecke(t) {
const a = t.start_address, b = t.end_address;
// Ortsname bevorzugt; ohne ihn die volle Anschrift, damit eine vor dieser
// Aenderung aufgeloeste Fahrt nicht ploetzlich wieder ortlos dasteht.
const ortsname = (adresse, lat, lon) => {
if (adresse) return adresse;
const gemerkt = stadtAusCache(lat, lon);
if (gemerkt) return gemerkt;
const voll = ortAusCache(lat, lon);
return stadtAusAnschrift(voll) || voll;
};
const a = ortsname(t.start_address, t.start_lat, t.start_lon);
const b = ortsname(t.end_address, t.end_lat, t.end_lon);
if (!a && !b) return "Fahrt ohne Ortsangabe";
if (!b) return esc(a);
if (!a) return esc(b);
@@ -1,7 +1,7 @@
{
"domain": "audi_dashboard",
"name": "Audi Dashboard",
"version": "2026.9.1.20",
"version": "2026.9.1.23",
"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"],