Einzelfahrt: Orte aus den Koordinaten, "Min." statt "min" (2026.9.1.20)

START UND ZIEL STANDEN AUF "UNBEKANNT"

start_address/end_address stehen in HANDFELDER - das Backend fuellt sie nie von
selbst, sie kommen nur aus einer von Hand angelegten oder bearbeiteten Fahrt.
Die Koordinaten dagegen traegt das Screening ein: 12 von 14 Fahrten im
Testbestand hatten Start- und Zielposition, aber genau eine hatte eine Adresse.

Beide Oberflaechen loesen sie jetzt beim Oeffnen der Einzelfahrt auf, ueber
denselben Cache wie die Standortansicht - ein Abruf je Ort, danach nie wieder.
Bewusst NICHT in der Liste: ein Vorratsabruf fuer alle Fahrten waere genau das,
worum Nominatim in seinen Nutzungsbedingungen bittet, es nicht zu tun.

Bis zur Antwort steht "wird ermittelt ...", ohne Koordinaten oder ohne Treffer
"unbekannt". Das Panel prueft vor dem Eintragen, ob noch dieselbe Fahrt offen
ist - wer waehrend des Abrufs weiterblaettert, soll nicht die Adresse der
vorigen Fahrt in der neuen sehen.

Live: Start "Schottenau, 85072 Eichstaett", Ziel "Am Anger 7a, 85111
Adelschlag".

"MIN." STATT "MIN"

Deutsche Abkuerzung, wie die Oberflaeche sie an anderer Stelle laengst
verwendet ("Geparkt seit 2 Tg. 16 Std. 12 Min."). In dauerText() bzw. dauer(),
also ueberall wo eine Dauer erscheint. Zwei Testerwartungen mitgezogen.

Verifiziert: Panel als Modul, tsc --noEmit und vite build sauber, 165/165
Tests gruen, im Browser nachgesehen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-01 19:38:09 +02:00
parent 97a1482bb5
commit 0f680780a4
8 changed files with 147 additions and 14 deletions
+45 -2
View File
@@ -1,7 +1,7 @@
# AGENTS.md — Project state, review findings, open items, and working rules
**Last updated: 2026-09-01** (Strecken gestaffelt: Meter, Nachkommastelle, ganze Kilometer,
`2026.9.1.18`, Abschnitt BI. Davor: Audit ueber Panel, App und Backend, Abschnitt BH. Davor: Setup mit gruener Bestaetigung und Erwartungswert je
**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
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`;
@@ -7027,6 +7027,49 @@ nachgesehen: die Fahrt vom 01.09. steht jetzt mit **6,9 km** statt „7 km".
Die angepasste Erwartung ist selbst ein Beleg: der Test verlangte `„43 km"` für eine Summe von
**42,5 km** — die alte Anzeige rundete also schon dort, wo es etwas zu zeigen gab.
## BJ. Einzelfahrt: Orte statt „unbekannt", Kleingedrucktes weg, „Min." (2026.9.1.19/.20)
### Start und Ziel standen auf „unbekannt", obwohl die Position bekannt war
`start_address`/`end_address` stehen in `HANDFELDER` — das Backend füllt sie **nie** von
selbst, sie kommen nur aus einer von Hand angelegten oder bearbeiteten Fahrt. Die **Koordinaten**
dagegen trägt das Screening ein: 12 von 14 Fahrten im Testbestand hatten Start- und Zielposition,
aber genau eine hatte eine Adresse.
Beide Oberflächen lösen die Koordinaten jetzt beim Öffnen der Einzelfahrt auf — über **denselben
Cache**, den die Standortansicht benutzt (`rev:`-Schlüssel, drei Nachkommastellen ≈ 110 m). Ein
Abruf je Ort, danach nie wieder.
**Bewusst nicht in der Liste.** Ein Vorratsabruf für alle Fahrten wäre genau das, worum Nominatim
in seinen Nutzungsbedingungen bittet, es nicht zu tun. Dort steht deshalb weiterhin „Fahrt ohne
Ortsangabe", bis jemand die Fahrt einmal geöffnet hat.
Beide Fassungen zeigen bis zur Antwort „wird ermittelt …" und fallen auf „unbekannt" zurück, wenn
es keine Koordinaten gibt oder Nominatim nichts findet. Das Panel prüft vor dem Eintragen, ob noch
dieselbe Fahrt offen ist — wer während des Abrufs weiterblättert, soll nicht die Adresse der
vorigen Fahrt in der neuen sehen.
Live nachgewiesen an der Fahrt vom 01.09.: `Start · Schottenau, 85072 Eichstätt`
`Ziel · Am Anger 7a, 85111 Adelschlag`.
### Drei Erläuterungen entfernt
Auf Wunsch des Eigentümers entfallen die Kleingedruckten unter Durchschnitts- und
Höchstgeschwindigkeit sowie Verbrauch. Die Herleitungen stehen weiterhin im Quelltext
(`durchschnitt_kmh()`, `hoechstwert_im_fenster()`, `verbrauch_aus_literstaenden()`) — es
verschwindet die Anzeige, nicht das Wissen. Die Ortsangaben unter Start und Ziel bleiben: das sind
Daten, keine Erläuterung.
### „min" → „Min."
Deutsche Abkürzung, wie sie die Oberfläche an anderer Stelle längst verwendet
(`„Geparkt seit 2 Tg. 16 Std. 12 Min."`). In `dauerText()` bzw. `dauer()`, also überall wo eine
Dauer erscheint. Zwei Testerwartungen mitgezogen.
Verifiziert: Panel als Modul geparst, `tsc --noEmit` und `vite build` sauber, 165/165 Tests grün,
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`).
## Working conventions (observed — keep them)
- German is the project language: identifiers, comments, commits, UI texts. Exceptions:
+2 -2
View File
@@ -24,11 +24,11 @@ describe("Zahlen im deutschen Format", () => {
describe("dauer", () => {
it("zeigt unter einer Stunde nur Minuten", () => {
expect(dauer(2700)).toBe("45 min")
expect(dauer(2700)).toBe("45 Min.")
})
it("zeigt darüber Stunden und Minuten zweistellig", () => {
expect(dauer(8040)).toBe("2 h 14 min")
expect(dauer(8040)).toBe("2 h 14 Min.")
})
it("zeigt einen Strich statt „0 min", () => {
+4 -2
View File
@@ -125,8 +125,10 @@ export function dauer(sekunden: number | null | undefined): string {
const minutenGesamt = Math.round(sekunden / 60)
const stunden = Math.floor(minutenGesamt / 60)
const minuten = minutenGesamt % 60
if (stunden === 0) return `${minuten} min`
return `${stunden} h ${String(minuten).padStart(2, "0")} min`
// "Min." statt "min": deutsche Abkuerzung, wie sie die Oberflaeche sonst
// auch verwendet ("Geparkt seit 2 Tg. 16 Std. 12 Min.").
if (stunden === 0) return `${minuten} Min.`
return `${stunden} h ${String(minuten).padStart(2, "0")} Min.`
}
/**
+48 -2
View File
@@ -15,6 +15,7 @@ import { ActionButton, Feld, Fig, Pill, Tile } from "@audi-dash/ui"
import type { Fahrt } from "../api"
import { useDaten } from "../daten/DatenKontext"
import { koordinatenAufloesen } from "../daten/geokodierung"
import { datum, dauer, de, streckeTeile, uhrzeit } from "../format"
import type { SeitenName } from "../navigation"
import { Leerzustand, Wertzeile, Werteliste, bestaetigen } from "./bausteine"
@@ -41,6 +42,51 @@ export function FahrtDetail({ id, geheZu }: { id: string | undefined; geheZu: (n
// render()).
const [artVorgriff, setzeArtVorgriff] = useState<"privat" | "arbeitsweg" | null>(null)
/* Ortsangaben zu Start und Ziel.
Das Backend fuellt start_address/end_address NIE von selbst - sie stehen in
HANDFELDER und kommen nur aus einer von Hand angelegten oder bearbeiteten
Fahrt. Die Koordinaten dagegen traegt das Screening ein. Deshalb stand hier
bisher "unbekannt", obwohl die Position bekannt war (vom Eigentuemer
gemeldet, 01.09.2026).
Aufgeloest wird erst beim Oeffnen der Fahrt und ueber denselben Cache wie
die Standortansicht - ein Abruf je Ort, danach nie wieder. Ein Vorratsabruf
fuer alle Fahrten der Liste waere genau das, worum Nominatim in seinen
Nutzungsbedingungen bittet, es nicht zu tun. */
const [orte, setzeOrte] = useState<{ start: string | null; ziel: string | null }>({
start: null,
ziel: null,
})
const startLat = fahrt?.start_lat
const startLon = fahrt?.start_lon
const endLat = fahrt?.end_lat
const endLon = fahrt?.end_lon
useEffect(() => {
let abgemeldet = false
setzeOrte({ start: null, ziel: null })
const holen = async (lat?: number | null, lon?: number | null) =>
lat == null || lon == null ? null : await koordinatenAufloesen(lat, lon)
void (async () => {
const start = await holen(startLat, startLon)
if (!abgemeldet) setzeOrte((o) => ({ ...o, start }))
const ziel = await holen(endLat, endLon)
if (!abgemeldet) setzeOrte((o) => ({ ...o, ziel }))
})()
return () => {
abgemeldet = true
}
}, [startLat, startLon, endLat, endLon])
const ortText = (
adresse: string | null | undefined,
aufgeloest: string | null,
lat?: number | null,
lon?: number | null,
) => adresse ?? aufgeloest ?? (lat == null || lon == null ? "unbekannt" : "wird ermittelt …")
const startOrt = ortText(fahrt?.start_address, orte.start, startLat, startLon)
const zielOrt = ortText(fahrt?.end_address, orte.ziel, endLat, endLon)
// Faellt der Vorgriff mit dem echten Wert zusammen, wird er ueberfluessig.
// Bleibt er stehen, wuerde eine spaetere Aenderung von aussen (Panel,
// zweites Geraet) von einem laengst erledigten Tipp ueberdeckt.
@@ -132,12 +178,12 @@ export function FahrtDetail({ id, geheZu }: { id: string | undefined; geheZu: (n
<Wertzeile
label="Start"
wert={`${datum(fahrt.ts_start)} · ${uhrzeit(fahrt.ts_start)} Uhr`}
zusatz={fahrt.start_address ?? "unbekannt"}
zusatz={startOrt}
/>
<Wertzeile
label="Ziel"
wert={`${datum(fahrt.ts_end)} · ${uhrzeit(fahrt.ts_end)} Uhr`}
zusatz={fahrt.end_address ?? "unbekannt"}
zusatz={zielOrt}
/>
<Wertzeile
label="Startkilometer"
@@ -1 +1 @@
{"version":"2026.9.1.19","sha256":"d23e4f474bbacba0f7e6a1314a18d5284fa45a8e0e2469c36746d2214e669438","bytes":265816,"gebaut":"2026-09-01T17:30:48Z"}
{"version":"2026.9.1.20","sha256":"20cc80f881d429303da2558b3171aabe414851aacbe2fee9253f7026c376043b","bytes":266021,"gebaut":"2026-09-01T17:35:23Z"}
@@ -947,6 +947,42 @@ function standortVerfolgungBeenden() {
USER_WATCH = null;
}
/* Der Ort einer Fahrtgrenze, solange er noch nicht aufgelöst ist.
Der Backend füllt start_address/end_address NIE von selbst - sie stehen in
HANDFELDER und kommen nur aus einer von Hand angelegten oder bearbeiteten
Fahrt. Die Koordinaten dagegen trägt das Screening ein. Deshalb stand auf
der Einzelfahrt bisher "unbekannt", obwohl die Position bekannt war
(vom Eigentümer gemeldet, 01.09.2026).
Aufgelöst wird erst beim Öffnen der Fahrt und über denselben Cache wie die
Standortansicht - ein Abruf je Ort, danach nie wieder. Ein Vorratsabruf für
alle Fahrten der Liste wäre genau das, worum Nominatim in seinen
Nutzungsbedingungen bittet, es nicht zu tun. */
function fahrtOrtText(adresse, lat, lon) {
if (adresse) return esc(adresse);
if (lat == null || lon == null) return "unbekannt";
return "wird ermittelt …";
}
/* Trägt die aufgelösten Orte nach dem Zeichnen ein. Die Fahrt-Kennung wird
mitgeführt: wer während des Abrufs weiterblättert, soll nicht die Adresse
der vorigen Fahrt in der neuen sehen. */
async function fahrtOrteNachtragen(t) {
const id = t.trip_id;
const paare = [
["start", t.start_address, t.start_lat, t.start_lon],
["ziel", t.end_address, t.end_lat, t.end_lon],
];
for (const [feld, adresse, lat, lon] of paare) {
if (adresse || lat == null || lon == null) continue;
const ort = await standortAdresseAufloesen(lat, lon);
const ziel = ROOT.querySelector(`[data-fahrtort="${feld}"]`);
if (!ziel || route.name !== "trip" || route.id !== id) return;
ziel.textContent = ort || "unbekannt";
}
}
/* Reverse-Geocoding über die öffentliche Nominatim-API (OpenStreetMap) -
wie die Kartenkacheln oben (TILE_URL) ohne eigenen Schlüssel; bei diesem
Aufrufvolumen (ein Fahrzeug, nur bei geöffneter Standortansicht) im Rahmen
@@ -3469,7 +3505,9 @@ function dauerText(sekunden) {
if (sekunden == null || !isFinite(sekunden)) return "";
const min = Math.max(0, Math.round(sekunden / 60));
const std = Math.floor(min / 60);
return std ? `${std} h ${String(min % 60).padStart(2, "0")} min` : `${min} min`;
// "Min." statt "min": deutsche Abkuerzung, wie sie die Oberflaeche sonst
// auch verwendet ("Geparkt seit 2 Tg. 16 Std. 12 Min.").
return std ? `${std} h ${String(min % 60).padStart(2, "0")} Min.` : `${min} Min.`;
}
/* Start und Ankunft eines Formulars als echte Zeitpunkte. Liegt die Ankunft
@@ -3626,8 +3664,8 @@ function vTrip(id) {
style="cursor:pointer;font-family:inherit">${artText(t.art)}</button></div>
</div>
<div class="tile"><dl class="rows">
<div class="row"><dt>Start</dt><dd>${dezeitpunkt(new Date(t.ts_start))}<small>${esc(t.start_address || "unbekannt")}</small></dd></div>
<div class="row"><dt>Ziel</dt><dd>${dezeitpunkt(new Date(t.ts_end))}<small>${esc(t.end_address || "unbekannt")}</small></dd></div>
<div class="row"><dt>Start</dt><dd>${dezeitpunkt(new Date(t.ts_start))}<small data-fahrtort="start">${fahrtOrtText(t.start_address, t.start_lat, t.start_lon)}</small></dd></div>
<div class="row"><dt>Ziel</dt><dd>${dezeitpunkt(new Date(t.ts_end))}<small data-fahrtort="ziel">${fahrtOrtText(t.end_address, t.end_lat, t.end_lon)}</small></dd></div>
<div class="row"><dt>Startkilometer</dt><dd>${t.odo_start != null ? de(t.odo_start) + " km" : "noch offen"}</dd></div>
<div class="row"><dt>Endkilometer</dt><dd>${t.odo_end != null ? de(t.odo_end) + " km" : "noch offen"}</dd></div>
<div class="row"><dt>Dauer</dt><dd>${dauerText(t.duration_s)}</dd></div>
@@ -4717,7 +4755,11 @@ function render() {
const gleicheAnsicht = letzteAnsicht === route.name + "|" + (route.id || "");
let head;
if (route.name === "home") { head = ["", "Übersicht"]; v.innerHTML = vHome(); }
else if (route.name === "trip") { head = ["Fahrt", "Einzelfahrt"]; v.innerHTML = vTrip(route.id); }
else if (route.name === "trip") {
head = ["Fahrt", "Einzelfahrt"]; v.innerHTML = vTrip(route.id);
const t = TRIPS.find((x) => x.trip_id === route.id);
if (t) fahrtOrteNachtragen(t);
}
else if (route.name === "fill") { head = ["Tankvorgang", "Einzelbeleg"]; v.innerHTML = vFill(route.id); }
else if (route.name === "service") { head = ["Wartung", "Service"]; v.innerHTML = vService(); }
else if (route.name === "reifen") { head = ["Fahrzeug", "Reifen"]; v.innerHTML = vReifen(); }
@@ -1,7 +1,7 @@
{
"domain": "audi_dashboard",
"name": "Audi Dashboard",
"version": "2026.9.1.19",
"version": "2026.9.1.20",
"documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md",
"issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues",
"codeowners": ["@paul"],