diff --git a/AGENTS.md b/AGENTS.md index c8bed94..22ac4da 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,7 +38,11 @@ route — see section C; a twelfth round the same day removed `HECKKLAPPENSCHLOS half-cut-off image placeholder, gave the Inspektion its own "voraussichtlich am …" forecast, made trips fully editable (new `audi_dashboard_fahrt_aktualisieren`, "Neue Fahrt" now offers every field the detail page shows), aligned the page title with the tiles on phone width, and closed two -dead-ends in the bootstrap retry loop behind the reported hanging "Lädt …" screen). +dead-ends in the bootstrap retry loop behind the reported hanging "Lädt …" screen; a thirteenth +round the same day replaced the trip form's editable "Dauer" with Start-/Ankunftszeit (duration is +now computed, not entered), capitalized the Art labels shown to the user, dropped the red styling +and hint text from the Arbeitsweg pill, and fixed a daylight-mode-only regression from round twelve +where the image placeholder's grey background had turned invisible-white). This file is the entry point for every new agent session: what this repo is, what is finished, what is missing, and how to work here. Detail lives in the linked documents — this file points, it does not duplicate. @@ -1278,6 +1282,46 @@ wraps the web app for iPhone; a PWA home-screen install is the accepted intermed war jedes Mal binnen ~2s da) - die Sackgassen sind im Code belegt, dass sie *die* Ursache beim Nutzer waren, ist damit aber nicht bewiesen. Tritt es erneut auf: Browser-Konsole erfragen, bevor weiter geraten wird. +- [x] Thirteenth round (2026-08-17, same day) — five more user-reported items on the trip form and + one theme bug found while verifying it: + (1) **"Dauer" replaced by "Ankunftszeit"; duration is no longer editable.** `fahrtFelder()` + now has Startzeit + Ankunftszeit inputs and a plain (non-input) duration display next to + them, computed by `fahrtZeitraum()`/`dauerText()` and kept live via a new + `data-fahrtzeitfeld` input handler (same "patch the one node, don't call render()" pattern + the existing `data-tankpreisfeld` handler uses, for the same reason: a full re-render would + steal focus from the field being typed in). An arrival time not later than the start time is + treated as crossing midnight (`+1 Tag`), matching how `_fahrt_beenden()` already handles a + real overnight trip. `fahrtFormularWerte()` derives `ts_start`/`ts_end` from the same + calculation - the backend never sees a separate duration field, so it can't disagree with + the times. Verified live: 09:00→10:45 shows "1 Std. 45 Min.", 23:30→00:15 shows "45 Min." + (crossed midnight correctly), and editing an existing trip's arrival time on the Einzelfahrt + page persisted the recalculated `duration_s` (7200s for a 10:00→12:00 edit, checked in + `fahrten.jsonl`). + (2) **Art values now display capitalized** ("Privat"/"Arbeitsweg") wherever shown to the user + - the trip-edit ` in Ortszeit. Bewusst nicht über toISOString(), das rechnet nach UTC um und liefert kurz nach Mitternacht @@ -2489,16 +2520,30 @@ function zeitfeld(d) { function fahrtFelder(praefix, t) { const start = t && t.ts_start ? new Date(t.ts_start) : new Date(); - const dauer = t && t.duration_s != null ? Math.round(t.duration_s / 60) : ""; + const ende = t && t.ts_end ? new Date(t.ts_end) : null; const art = t && t.art ? t.art : FAHRT_ARTEN[0]; const wert = (v) => (v == null ? "" : v); + const startzeit = t ? zeitfeld(start) : "08:00", ankunftzeit = ende ? zeitfeld(ende) : "08:30"; + // Die Dauer ist bewusst kein Eingabefeld: sie ergibt sich aus Start und + // Ankunft und würde sich sonst mit ihnen widersprechen können. Aus + // t.duration_s statt neu berechnet, damit eine über Mitternacht laufende + // bestehende Fahrt (Ankunft "kleiner" als Start) korrekt bleibt; bei einer + // neuen Fahrt (t === null) ergibt sich der Startwert aus denselben + // Vorgabezeiten wie die Felder selbst. Der data-fahrtzeitfeld-Handler + // rechnet bei jeder Änderung neu. + let dauerSekunden = t && t.duration_s != null ? t.duration_s : null; + if (dauerSekunden == null) { + const [sh, sm] = startzeit.split(":").map(Number), [eh, em] = ankunftzeit.split(":").map(Number); + dauerSekunden = ((eh * 60 + em) - (sh * 60 + sm) + (eh * 60 + em <= sh * 60 + sm ? 1440 : 0)) * 60; + } + const dauer = dauerText(dauerSekunden); return ` -
-
-
- min
+
+
+
+
${dauer}
-
+
@@ -2510,11 +2555,10 @@ function fahrtFelder(praefix, t) { function fahrtFormularWerte(praefix) { const zahl = (id) => { const v = $(`#${praefix}${id}`).value; return v === "" ? null : parseFloat(v.replace(",", ".")); }; const text = (id) => $(`#${praefix}${id}`).value.trim() || null; - const dauer = parseInt($(`#${praefix}Dauer`).value, 10) || 0; - const tsStart = new Date(`${$(`#${praefix}Datum`).value}T${$(`#${praefix}Start`).value}:00`); + const z = fahrtZeitraum(praefix); return { - ts_start: tsStart.toISOString(), - ts_end: new Date(tsStart.getTime() + dauer * 60000).toISOString(), + ts_start: z ? z.start.toISOString() : null, + ts_end: z ? z.ende.toISOString() : null, art: $(`#${praefix}Art`).value, start_ort: text("Startort"), ziel_ort: text("Zielort"), odo_start: zahl("OdoStart"), odo_end: zahl("OdoEnde"), distanz: zahl("Distanz"), @@ -2556,7 +2600,7 @@ function vTrips() {
`; tm.forEach((t) => { h += swipeZeile(`trip:${t.trip_id}`, ``); }); h += `
`; @@ -2586,9 +2630,8 @@ function vTrip(id) { Distanz
${t.distance_km != null ? de(t.distance_km) : "–"}km
-
- tippen zum Umschalten +
Startort
${esc(t.start_address || "unbekannt")}${dedat(new Date(t.ts_start))}
@@ -4128,6 +4171,15 @@ function ereignisseVerdrahten() { if (liste) liste.innerHTML = entitaetZeilenMarkup(sucheFeld, key, setupSuchtext, wert); return; } + // Gleicher Grund wie beim tankpreisfeld-Handler darunter: nur die + // Anzeige patchen, kein render() - das würde das gerade bearbeitete Feld + // neu erzeugen und den Fokus verlieren. + const zeitfeldPraefix = e.target.dataset.fahrtzeitfeld; + if (zeitfeldPraefix) { + const anzeige = ROOT.getElementById(`${zeitfeldPraefix}DauerAnzeige`); + if (anzeige) anzeige.textContent = fahrtDauerText(zeitfeldPraefix); + return; + } const praefix = e.target.dataset.tankpreisfeld; if (!praefix) return; const liter = parseFloat((ROOT.getElementById(`${praefix}Liter`).value || "0").replace(",", ".")); diff --git a/homeassistant/installationspaket/www/audi-dashboard-ios.css b/homeassistant/installationspaket/www/audi-dashboard-ios.css index bf00064..21a7e4d 100644 --- a/homeassistant/installationspaket/www/audi-dashboard-ios.css +++ b/homeassistant/installationspaket/www/audi-dashboard-ios.css @@ -196,7 +196,6 @@ main#view{--seitenrand:16px;padding:0 var(--seitenrand) 30px;scroll-behavior:smo .pill{font-size:12px;letter-spacing:.01em;text-transform:none;padding:5px 11px; border:0;background:var(--ios-fill);color:var(--fg2)} -.pill.work{background:rgba(245,5,55,.16);color:var(--ios-tint)} .status{padding:14px 0 2px} .status .t{font-size:16px} @@ -333,7 +332,12 @@ main#view{--seitenrand:16px;padding:0 var(--seitenrand) 30px;scroll-behavior:smo /* Fahrzeugbild randlos wie in einer Fahrzeug-App: der Platzhalter bleibt sichtbar, aber ohne technischen Strichrahmen. */ .szene{margin-bottom:20px} -.bildbox.bild-fehlt .platzhalter{border:0;background:var(--tile);border-radius:var(--r-tile)} +/* --tile statt --ios-fill waere hier naheliegend, faellt im Tag-Theme aber + auf reines Weiss (#FFFFFF) - ununterscheidbar von Canvas und den meisten + Kacheln drumherum, die Box verschwand. --ios-fill ist in beiden Themes ein + deutlich sichtbares transluzentes Grau, dieselbe Konvention wie + .feld select/.aktion ("das hier ist eine Flaeche"). */ +.bildbox.bild-fehlt .platzhalter{border:0;background:var(--ios-fill);border-radius:var(--r-tile)} .platzhalter-datei{display:none} /* Zeilen mit Wert plus Statuspunkt rechts (Sicherheitscheck) ruhiger setzen */ diff --git a/homeassistant/installationspaket/www/audi-dashboard-version.json b/homeassistant/installationspaket/www/audi-dashboard-version.json index dfd7c6b..6da09ce 100644 --- a/homeassistant/installationspaket/www/audi-dashboard-version.json +++ b/homeassistant/installationspaket/www/audi-dashboard-version.json @@ -1 +1 @@ -{"version": 1787000000} +{"version": 1787009000} diff --git a/homeassistant/installationspaket/www/audi-dashboard.css b/homeassistant/installationspaket/www/audi-dashboard.css index 1a46f0c..560eb72 100644 --- a/homeassistant/installationspaket/www/audi-dashboard.css +++ b/homeassistant/installationspaket/www/audi-dashboard.css @@ -676,7 +676,6 @@ button.tile, .tilebtn { transition: background .15s, transform .1s; } border-radius: var(--r-pill); padding: 6px 12px; } -.pill.work { color: var(--red); border-color: var(--red); } /* ------------------------------------------------------------ Akkordeon */ .acc-h { diff --git a/homeassistant/www/audi-dashboard-app.js b/homeassistant/www/audi-dashboard-app.js index 4974506..fdeca0f 100644 --- a/homeassistant/www/audi-dashboard-app.js +++ b/homeassistant/www/audi-dashboard-app.js @@ -2473,7 +2473,38 @@ function vReifen() { Felder decken genau das ab, was die Einzelfahrt-Seite anzeigt; Verbrauch, Status und km-Quelle bleiben außen vor, die meldet das Fahrzeug bzw. leitet das Backend ab. */ +/* Gespeichert bleibt der kleingeschriebene Schlüssel - er wird an mehreren + Stellen verglichen (Pille auf der Einzelfahrt, Statistik "Arbeitsweg", CSV) + und steht so auch in allen bereits erfassten Fahrten. Angezeigt wird er + groß, deshalb artText() statt eines geänderten Werts. */ const FAHRT_ARTEN = ["privat", "arbeitsweg"]; +const artText = (a) => (a ? a.charAt(0).toUpperCase() + a.slice(1) : ""); + +/* "1 Std. 15 Min." statt "75 min" - bei mehrstündigen Fahrten liest sich die + reine Minutenzahl schlecht. */ +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} Std. ${min % 60} Min.` : `${min} Min.`; +} + +/* Start und Ankunft eines Formulars als echte Zeitpunkte. Liegt die Ankunft + vor der Abfahrt, wurde über Mitternacht gefahren - dann zählt der Folgetag. + null, solange Datum oder eine der beiden Uhrzeiten fehlt. */ +function fahrtZeitraum(praefix) { + const datum = $(`#${praefix}Datum`).value, ab = $(`#${praefix}Start`).value, an = $(`#${praefix}Ende`).value; + if (!datum || !ab || !an) return null; + const start = new Date(`${datum}T${ab}:00`); + let ende = new Date(`${datum}T${an}:00`); + if (isNaN(start) || isNaN(ende)) return null; + if (ende <= start) ende = new Date(ende.getTime() + dTag); + return { start, ende }; +} +function fahrtDauerText(praefix) { + const z = fahrtZeitraum(praefix); + return z ? dauerText((z.ende - z.start) / 1000) : "–"; +} /* Datum für ein in Ortszeit. Bewusst nicht über toISOString(), das rechnet nach UTC um und liefert kurz nach Mitternacht @@ -2489,16 +2520,30 @@ function zeitfeld(d) { function fahrtFelder(praefix, t) { const start = t && t.ts_start ? new Date(t.ts_start) : new Date(); - const dauer = t && t.duration_s != null ? Math.round(t.duration_s / 60) : ""; + const ende = t && t.ts_end ? new Date(t.ts_end) : null; const art = t && t.art ? t.art : FAHRT_ARTEN[0]; const wert = (v) => (v == null ? "" : v); + const startzeit = t ? zeitfeld(start) : "08:00", ankunftzeit = ende ? zeitfeld(ende) : "08:30"; + // Die Dauer ist bewusst kein Eingabefeld: sie ergibt sich aus Start und + // Ankunft und würde sich sonst mit ihnen widersprechen können. Aus + // t.duration_s statt neu berechnet, damit eine über Mitternacht laufende + // bestehende Fahrt (Ankunft "kleiner" als Start) korrekt bleibt; bei einer + // neuen Fahrt (t === null) ergibt sich der Startwert aus denselben + // Vorgabezeiten wie die Felder selbst. Der data-fahrtzeitfeld-Handler + // rechnet bei jeder Änderung neu. + let dauerSekunden = t && t.duration_s != null ? t.duration_s : null; + if (dauerSekunden == null) { + const [sh, sm] = startzeit.split(":").map(Number), [eh, em] = ankunftzeit.split(":").map(Number); + dauerSekunden = ((eh * 60 + em) - (sh * 60 + sm) + (eh * 60 + em <= sh * 60 + sm ? 1440 : 0)) * 60; + } + const dauer = dauerText(dauerSekunden); return ` -
-
-
- min
+
+
+
+
${dauer}
-
+
@@ -2510,11 +2555,10 @@ function fahrtFelder(praefix, t) { function fahrtFormularWerte(praefix) { const zahl = (id) => { const v = $(`#${praefix}${id}`).value; return v === "" ? null : parseFloat(v.replace(",", ".")); }; const text = (id) => $(`#${praefix}${id}`).value.trim() || null; - const dauer = parseInt($(`#${praefix}Dauer`).value, 10) || 0; - const tsStart = new Date(`${$(`#${praefix}Datum`).value}T${$(`#${praefix}Start`).value}:00`); + const z = fahrtZeitraum(praefix); return { - ts_start: tsStart.toISOString(), - ts_end: new Date(tsStart.getTime() + dauer * 60000).toISOString(), + ts_start: z ? z.start.toISOString() : null, + ts_end: z ? z.ende.toISOString() : null, art: $(`#${praefix}Art`).value, start_ort: text("Startort"), ziel_ort: text("Zielort"), odo_start: zahl("OdoStart"), odo_end: zahl("OdoEnde"), distanz: zahl("Distanz"), @@ -2556,7 +2600,7 @@ function vTrips() {
`; tm.forEach((t) => { h += swipeZeile(`trip:${t.trip_id}`, ``); }); h += `
`; @@ -2586,9 +2630,8 @@ function vTrip(id) { Distanz
${t.distance_km != null ? de(t.distance_km) : "–"}km
-
- tippen zum Umschalten +
Startort
${esc(t.start_address || "unbekannt")}${dedat(new Date(t.ts_start))}
@@ -4128,6 +4171,15 @@ function ereignisseVerdrahten() { if (liste) liste.innerHTML = entitaetZeilenMarkup(sucheFeld, key, setupSuchtext, wert); return; } + // Gleicher Grund wie beim tankpreisfeld-Handler darunter: nur die + // Anzeige patchen, kein render() - das würde das gerade bearbeitete Feld + // neu erzeugen und den Fokus verlieren. + const zeitfeldPraefix = e.target.dataset.fahrtzeitfeld; + if (zeitfeldPraefix) { + const anzeige = ROOT.getElementById(`${zeitfeldPraefix}DauerAnzeige`); + if (anzeige) anzeige.textContent = fahrtDauerText(zeitfeldPraefix); + return; + } const praefix = e.target.dataset.tankpreisfeld; if (!praefix) return; const liter = parseFloat((ROOT.getElementById(`${praefix}Liter`).value || "0").replace(",", ".")); diff --git a/homeassistant/www/audi-dashboard-ios.css b/homeassistant/www/audi-dashboard-ios.css index bf00064..21a7e4d 100644 --- a/homeassistant/www/audi-dashboard-ios.css +++ b/homeassistant/www/audi-dashboard-ios.css @@ -196,7 +196,6 @@ main#view{--seitenrand:16px;padding:0 var(--seitenrand) 30px;scroll-behavior:smo .pill{font-size:12px;letter-spacing:.01em;text-transform:none;padding:5px 11px; border:0;background:var(--ios-fill);color:var(--fg2)} -.pill.work{background:rgba(245,5,55,.16);color:var(--ios-tint)} .status{padding:14px 0 2px} .status .t{font-size:16px} @@ -333,7 +332,12 @@ main#view{--seitenrand:16px;padding:0 var(--seitenrand) 30px;scroll-behavior:smo /* Fahrzeugbild randlos wie in einer Fahrzeug-App: der Platzhalter bleibt sichtbar, aber ohne technischen Strichrahmen. */ .szene{margin-bottom:20px} -.bildbox.bild-fehlt .platzhalter{border:0;background:var(--tile);border-radius:var(--r-tile)} +/* --tile statt --ios-fill waere hier naheliegend, faellt im Tag-Theme aber + auf reines Weiss (#FFFFFF) - ununterscheidbar von Canvas und den meisten + Kacheln drumherum, die Box verschwand. --ios-fill ist in beiden Themes ein + deutlich sichtbares transluzentes Grau, dieselbe Konvention wie + .feld select/.aktion ("das hier ist eine Flaeche"). */ +.bildbox.bild-fehlt .platzhalter{border:0;background:var(--ios-fill);border-radius:var(--r-tile)} .platzhalter-datei{display:none} /* Zeilen mit Wert plus Statuspunkt rechts (Sicherheitscheck) ruhiger setzen */ diff --git a/homeassistant/www/audi-dashboard-version.json b/homeassistant/www/audi-dashboard-version.json index dfd7c6b..6da09ce 100644 --- a/homeassistant/www/audi-dashboard-version.json +++ b/homeassistant/www/audi-dashboard-version.json @@ -1 +1 @@ -{"version": 1787000000} +{"version": 1787009000} diff --git a/homeassistant/www/audi-dashboard.css b/homeassistant/www/audi-dashboard.css index 1a46f0c..560eb72 100644 --- a/homeassistant/www/audi-dashboard.css +++ b/homeassistant/www/audi-dashboard.css @@ -676,7 +676,6 @@ button.tile, .tilebtn { transition: background .15s, transform .1s; } border-radius: var(--r-pill); padding: 6px 12px; } -.pill.work { color: var(--red); border-color: var(--red); } /* ------------------------------------------------------------ Akkordeon */ .acc-h {