diff --git a/AGENTS.md b/AGENTS.md index 4752d97..1dea019 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1169,6 +1169,40 @@ wraps the web app for iPhone; a PWA home-screen install is the accepted intermed drop → zone highlights (`darueber`), dialog closes, service called with the right filename and base64 payload; paste → same; a dropped PNG → "Das war kein PDF." and no service call; Escape → closes. Synced to `installationspaket/`. + **Nachbesserung am selben Tag, nach Fehlermeldung des Nutzers** ("copy paste from pc brings + error no pdf but it is the latest bill") — zwei echte Fehler, beide bestätigt: + (1) `navigator.clipboard.read()` (der "Aus Zwischenablage einfügen"-Knopf) gibt laut + Spezifikation **nur Text, HTML und Bilder** heraus. Eine im Explorer/Finder kopierte PDF-Datei + taucht dort grundsätzlich nicht auf - der Knopf konnte am Rechner also gar nicht funktionieren + und behauptete dann fälschlich, es liege kein PDF vor. Der Weg, der dort funktioniert, ist + Strg+V: das `paste`-Ereignis trägt die Datei in `clipboardData.files`. Die Meldung nennt jetzt + genau das, statt die Datei zu beschuldigen; die Ablagefläche sagt es von vornherein + ("oder mit Strg+V einfügen", per `tastenkuerzelEinfuegen()` auf Mac Cmd+V). + (2) Die Erkennung hing allein am MIME-Typ (`type === "application/pdf"`) - eine aus dem + Explorer kopierte oder gezogene Datei kommt je nach Browser und Quelle aber mit **leerem** + `type` an und wurde dadurch abgelehnt. Neue Hilfsfunktion `istPdf()` prüft zusätzlich die + Dateiendung; live nachgestellt mit genau diesem Fall (`new File([...], "Tankbeleg_August.pdf", + {})`, `type === ""`) - vorher abgelehnt, jetzt korrekt hochgeladen. + (3) Auf Rückfrage des Nutzers ("why is it that way? computer has copy paste function") noch + einen Schritt weiter: Der Einfügen-Knopf wird am Rechner jetzt **gar nicht mehr angeboten** + (`zwischenablageKnopfSinnvoll()`, Prüfung auf `(pointer: coarse)`), weil er dort prinzipiell + nicht funktionieren kann - ein Knopf, der zwangsläufig in eine Fehlermeldung läuft, ist + schlechter als keiner. "Datei auswählen" wird dort zur primären Aktion; auf dem Telefon bleibt + alles wie zuvor. Der Unterschied dahinter, kurz: Strg+V ist eine bewusste Nutzergeste an die + Seite, deshalb bekommt die Seite den vollen Inhalt inklusive Dateien - `clipboard.read()` ist + die Seite, die von sich aus die Zwischenablage ausliest, und das wird aus Datenschutzgründen + stark eingeschränkt (sonst könnte jede Seite still mitlesen, was zuletzt kopiert wurde). + Live geprüft: Desktop zeigt nur noch "Datei auswählen [primaer]"/"Abbrechen", Mobil (375px, + `pointer: coarse`) weiterhin alle drei Knöpfe. +- [x] Preis je Liter überall auf 2 Nachkommastellen (2026-08-17, Nutzerwunsch: "€/l for gas is + common to be displayed as x,xx €/l") — alle sieben Stellen von `de(..., 3)` auf `de(..., 2)` + umgestellt: Jahres- und Monatsschnitt in der Tankliste, Zeilenwert je Tankvorgang, "Preis je + Liter" im Einzelbeleg, die Live-Vorschau in beiden Tankformularen (statisch und im + `data-tankpreisfeld`-Handler) sowie die CSV-Spalte "€/l". Im CSV kein Informationsverlust: der + Wert ist aus den ebenfalls exportierten Spalten Liter und Kosten jederzeit neu berechenbar. + `companion-app` (Nachfolger-App) brauchte keine Änderung - deren `eur()` rundet ohnehin schon + auf 2 Stellen. Live geprüft: keine `de(..., 3)`-Aufrufe mehr in der Datei, Liste und + Einzelbeleg zeigen "2,07 €/l", "1,99 €/l" usw. - [ ] Fix remaining documentation drift (statistics claim, README gaps, obsolete TODO comment) — text-only changes; INSTALL.md's WLAN/TommiG1 drift and stale variable names were fixed 2026-08-12 (see section B); `DESIGN_REVIEW_2026-08-13.md` and `REVIEW_main_2026-08-13.md` diff --git a/homeassistant/installationspaket/www/audi-dashboard-app.js b/homeassistant/installationspaket/www/audi-dashboard-app.js index 371acff..37afc31 100644 --- a/homeassistant/installationspaket/www/audi-dashboard-app.js +++ b/homeassistant/installationspaket/www/audi-dashboard-app.js @@ -2805,7 +2805,7 @@ function tankFelder(praefix, f) { const kraftstoff = q && q.fuel_type ? q.fuel_type : KRAFTSTOFFSORTEN[0]; const station = q && q.station_name ? q.station_name : ""; const preisText = q && q.liters && q.fuel_total_eur != null && q.liters > 0 - ? `Preis je Liter: ${de(q.fuel_total_eur / q.liters, 3)} €` : "Preis je Liter: wird berechnet"; + ? `Preis je Liter: ${de(q.fuel_total_eur / q.liters, 2)} €` : "Preis je Liter: wird berechnet"; return `
@@ -2871,7 +2871,7 @@ function vFuel() { ${j} ${eur(sum(fj, (f) => f.fuel_total_eur || 0))} € - ${de(sum(fj, (f) => f.liters || 0), 1)} l · Ø ${de(schnittpreis(fj), 3)} €/l + ${de(sum(fj, (f) => f.liters || 0), 1)} l · Ø ${de(schnittpreis(fj), 2)} €/l ${CAR.smartdeal.aktiv ? `${markenlogo()}−${eur(ersparnisJahr)} €` : ""}
`; @@ -2882,11 +2882,11 @@ function vFuel() { ${m} ${eur(sum(fm, (f) => f.fuel_total_eur || 0))} € - Ø ${de(schnittpreis(fm), 3)} €/l + Ø ${de(schnittpreis(fm), 2)} €/l
`; fm.forEach((f) => { const preis = f.liters ? f.fuel_total_eur / f.liters : null; h += swipeZeile(`fill:${f.tank_id}`, ``); }); h += `
`; }); @@ -2925,7 +2925,7 @@ function vFill(id) {
Kraftstoff
${esc(f.fuel_type || KRAFTSTOFFSORTEN[0])}
Getankte Liter
${f.liters != null ? de(f.liters, 2) + " l" : "–"}
Kosten
${f.fuel_total_eur != null ? eur(f.fuel_total_eur) + " €" : "–"}real, nach Rabatt
-
Preis je Liter
${berechneterPreis != null ? de(berechneterPreis, 3) + " €" : "–"}Kosten ÷ Liter
+
Preis je Liter
${berechneterPreis != null ? de(berechneterPreis, 2) + " €" : "–"}Kosten ÷ Liter
${CAR.smartdeal.aktiv && f.discount != null ? `
${markenlogo()} SmartDeal-Ersparnis
${eur(f.discount)} €
` : ""}
Tankstelle
${esc(f.station_name || "unbekannt")}${esc(f.station_address || "")}
@@ -3006,7 +3006,7 @@ function hinweis(titel, text) { unverändert. */ function belegDateiVerarbeiten(datei, tankId, kontext) { if (!datei) return; - if (datei.type && datei.type !== "application/pdf") { + if (!istPdf(datei)) { belegFehler = { context: kontext, meldung: "Nur PDF-Belege werden unterstützt" }; render(); return; } @@ -3025,9 +3025,16 @@ function belegDateiVerarbeiten(datei, tankId, kontext) { leser.readAsDataURL(datei); } +/* Nicht nur auf den MIME-Typ verlassen: eine aus dem Explorer/Finder kopierte + oder gezogene Datei kommt je nach Browser und Quelle mit leerem type an. */ +function istPdf(datei) { + if (!datei) return false; + return datei.type === "application/pdf" || /\.pdf$/i.test(datei.name || ""); +} + /* Nimmt das erste PDF aus einer DataTransfer-/Clipboard-Dateiliste. */ function belegAusDateiliste(dateien) { - return [...(dateien || [])].find((d) => d.type === "application/pdf") || null; + return [...(dateien || [])].find(istPdf) || null; } function belegPopupSchliessenUndLaden(datei) { @@ -3040,12 +3047,21 @@ function belegPopupSchliessenUndLaden(datei) { /* Telefon-Weg: das aus der Mail kopierte PDF liegt in der Zwischenablage. navigator.clipboard.read() ist der einzige Weg, ohne dass der Nutzer in ein - Eingabefeld tippen muss - iOS zeigt dafür seine eigene Einfügen-Abfrage. */ + Eingabefeld tippen muss - iOS zeigt dafür seine eigene Einfügen-Abfrage. + + WICHTIG (Ursache eines gemeldeten Fehlers): Am Rechner hilft dieser Knopf + bei einer im Explorer/Finder kopierten Datei NICHT. clipboard.read() gibt + laut Spezifikation nur Text, HTML und Bilder heraus - eine kopierte + PDF-Datei taucht dort gar nicht auf, egal wie frisch sie ist. Der Weg, der + dort funktioniert, ist Strg+V: das paste-Ereignis trägt die echte Datei in + clipboardData.files. Deshalb sagt die Meldung unten genau das, statt + pauschal "kein PDF" zu behaupten. */ async function belegAusZwischenablage() { const ctx = belegPopup; if (!ctx) return; + const strgVHinweis = `Am Rechner kann der Browser eine kopierte Datei nur über ${tastenkuerzelEinfuegen()} entgegennehmen - bitte so einfügen oder das PDF hineinziehen.`; if (!navigator.clipboard || !navigator.clipboard.read) { - belegPopup.fehler = "Dieser Browser kann die Zwischenablage nicht lesen - bitte ziehen oder Datei auswählen."; + belegPopup.fehler = strgVHinweis; render(); return; } try { @@ -3053,13 +3069,25 @@ async function belegAusZwischenablage() { const typ = (eintrag.types || []).find((t) => t === "application/pdf"); if (typ) { belegPopupSchliessenUndLaden(new File([await eintrag.getType(typ)], "beleg.pdf", { type: "application/pdf" })); return; } } - belegPopup.fehler = "In der Zwischenablage liegt kein PDF."; + belegPopup.fehler = strgVHinweis; } catch (err) { - belegPopup.fehler = "Kein Zugriff auf die Zwischenablage."; + belegPopup.fehler = "Kein Zugriff auf die Zwischenablage. " + strgVHinweis; } render(); } +function tastenkuerzelEinfuegen() { + return /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent) ? "Cmd+V" : "Strg+V"; +} + +/* Der Einfügen-Knopf wird nur auf Tippgeräten angeboten. Am Rechner kann er + aus Datenschutzgründen prinzipiell nicht funktionieren (clipboard.read() + gibt Dateien nie heraus, siehe oben) - ein Knopf, der dort zwangsläufig + scheitert, führt nur in eine Sackgasse. Dort bleiben Ziehen und Strg+V. */ +function zwischenablageKnopfSinnvoll() { + return window.matchMedia && window.matchMedia("(pointer: coarse)").matches; +} + function vBelegPopup() { if (!belegPopup) return ""; return `
@@ -3067,11 +3095,11 @@ function vBelegPopup() { Beleg hochladen
PDF hierher ziehen - oder aus der Zwischenablage einfügen + oder mit ${tastenkuerzelEinfuegen()} einfügen
${belegPopup.fehler ? `${esc(belegPopup.fehler)}` : ""} - - + ${zwischenablageKnopfSinnvoll() ? `` : ""} + `; } @@ -3928,7 +3956,7 @@ function ereignisseVerdrahten() { if (w === "fahrten") csvDatei("fahrten.csv", ["Start", "Ende", "km", "Art", "Status"], TRIPS.map((t) => [t.ts_start, t.ts_end, t.distance_km != null ? de(t.distance_km) : "", t.art, t.status])); else if (w === "tanken") csvDatei("tankvorgaenge.csv", ["Zeitpunkt", "Station", "Liter", "€/l", "Kosten €"], - FILLS.map((f) => [f.ts, f.station_name || "", f.liters != null ? de(f.liters, 2) : "", f.liters ? de(f.fuel_total_eur / f.liters, 3) : "", f.fuel_total_eur != null ? eur(f.fuel_total_eur) : ""])); + FILLS.map((f) => [f.ts, f.station_name || "", f.liters != null ? de(f.liters, 2) : "", f.liters ? de(f.fuel_total_eur / f.liters, 2) : "", f.fuel_total_eur != null ? eur(f.fuel_total_eur) : ""])); else csvDatei("servicebuch.csv", ["Datum", "km", "Art", "Werkstatt", "Kosten €"], CAR.service.buch.map((e2) => [e2.datum, de(e2.km), e2.art, e2.werkstatt, e2.kosten != null ? eur(e2.kosten) : ""])); return; @@ -3995,7 +4023,7 @@ function ereignisseVerdrahten() { const liter = parseFloat((ROOT.getElementById(`${praefix}Liter`).value || "0").replace(",", ".")); const kosten = parseFloat((ROOT.getElementById(`${praefix}Kosten`).value || "0").replace(",", ".")); const anzeige = ROOT.getElementById(`${praefix}PreisAnzeige`); - if (anzeige) anzeige.textContent = liter > 0 ? `Preis je Liter: ${de(kosten / liter, 3)} €` : "Preis je Liter: wird berechnet"; + if (anzeige) anzeige.textContent = liter > 0 ? `Preis je Liter: ${de(kosten / liter, 2)} €` : "Preis je Liter: wird berechnet"; }); ROOT.addEventListener("change", (e) => { diff --git a/homeassistant/installationspaket/www/audi-dashboard-version.json b/homeassistant/installationspaket/www/audi-dashboard-version.json index fa9ec75..b04b813 100644 --- a/homeassistant/installationspaket/www/audi-dashboard-version.json +++ b/homeassistant/installationspaket/www/audi-dashboard-version.json @@ -1 +1 @@ -{"version": 1786919196} +{"version": 1786919856} diff --git a/homeassistant/www/audi-dashboard-app.js b/homeassistant/www/audi-dashboard-app.js index 371acff..37afc31 100644 --- a/homeassistant/www/audi-dashboard-app.js +++ b/homeassistant/www/audi-dashboard-app.js @@ -2805,7 +2805,7 @@ function tankFelder(praefix, f) { const kraftstoff = q && q.fuel_type ? q.fuel_type : KRAFTSTOFFSORTEN[0]; const station = q && q.station_name ? q.station_name : ""; const preisText = q && q.liters && q.fuel_total_eur != null && q.liters > 0 - ? `Preis je Liter: ${de(q.fuel_total_eur / q.liters, 3)} €` : "Preis je Liter: wird berechnet"; + ? `Preis je Liter: ${de(q.fuel_total_eur / q.liters, 2)} €` : "Preis je Liter: wird berechnet"; return `
@@ -2871,7 +2871,7 @@ function vFuel() { ${j} ${eur(sum(fj, (f) => f.fuel_total_eur || 0))} € - ${de(sum(fj, (f) => f.liters || 0), 1)} l · Ø ${de(schnittpreis(fj), 3)} €/l + ${de(sum(fj, (f) => f.liters || 0), 1)} l · Ø ${de(schnittpreis(fj), 2)} €/l ${CAR.smartdeal.aktiv ? `${markenlogo()}−${eur(ersparnisJahr)} €` : ""}
`; @@ -2882,11 +2882,11 @@ function vFuel() { ${m} ${eur(sum(fm, (f) => f.fuel_total_eur || 0))} € - Ø ${de(schnittpreis(fm), 3)} €/l + Ø ${de(schnittpreis(fm), 2)} €/l
`; fm.forEach((f) => { const preis = f.liters ? f.fuel_total_eur / f.liters : null; h += swipeZeile(`fill:${f.tank_id}`, ``); }); h += `
`; }); @@ -2925,7 +2925,7 @@ function vFill(id) {
Kraftstoff
${esc(f.fuel_type || KRAFTSTOFFSORTEN[0])}
Getankte Liter
${f.liters != null ? de(f.liters, 2) + " l" : "–"}
Kosten
${f.fuel_total_eur != null ? eur(f.fuel_total_eur) + " €" : "–"}real, nach Rabatt
-
Preis je Liter
${berechneterPreis != null ? de(berechneterPreis, 3) + " €" : "–"}Kosten ÷ Liter
+
Preis je Liter
${berechneterPreis != null ? de(berechneterPreis, 2) + " €" : "–"}Kosten ÷ Liter
${CAR.smartdeal.aktiv && f.discount != null ? `
${markenlogo()} SmartDeal-Ersparnis
${eur(f.discount)} €
` : ""}
Tankstelle
${esc(f.station_name || "unbekannt")}${esc(f.station_address || "")}
@@ -3006,7 +3006,7 @@ function hinweis(titel, text) { unverändert. */ function belegDateiVerarbeiten(datei, tankId, kontext) { if (!datei) return; - if (datei.type && datei.type !== "application/pdf") { + if (!istPdf(datei)) { belegFehler = { context: kontext, meldung: "Nur PDF-Belege werden unterstützt" }; render(); return; } @@ -3025,9 +3025,16 @@ function belegDateiVerarbeiten(datei, tankId, kontext) { leser.readAsDataURL(datei); } +/* Nicht nur auf den MIME-Typ verlassen: eine aus dem Explorer/Finder kopierte + oder gezogene Datei kommt je nach Browser und Quelle mit leerem type an. */ +function istPdf(datei) { + if (!datei) return false; + return datei.type === "application/pdf" || /\.pdf$/i.test(datei.name || ""); +} + /* Nimmt das erste PDF aus einer DataTransfer-/Clipboard-Dateiliste. */ function belegAusDateiliste(dateien) { - return [...(dateien || [])].find((d) => d.type === "application/pdf") || null; + return [...(dateien || [])].find(istPdf) || null; } function belegPopupSchliessenUndLaden(datei) { @@ -3040,12 +3047,21 @@ function belegPopupSchliessenUndLaden(datei) { /* Telefon-Weg: das aus der Mail kopierte PDF liegt in der Zwischenablage. navigator.clipboard.read() ist der einzige Weg, ohne dass der Nutzer in ein - Eingabefeld tippen muss - iOS zeigt dafür seine eigene Einfügen-Abfrage. */ + Eingabefeld tippen muss - iOS zeigt dafür seine eigene Einfügen-Abfrage. + + WICHTIG (Ursache eines gemeldeten Fehlers): Am Rechner hilft dieser Knopf + bei einer im Explorer/Finder kopierten Datei NICHT. clipboard.read() gibt + laut Spezifikation nur Text, HTML und Bilder heraus - eine kopierte + PDF-Datei taucht dort gar nicht auf, egal wie frisch sie ist. Der Weg, der + dort funktioniert, ist Strg+V: das paste-Ereignis trägt die echte Datei in + clipboardData.files. Deshalb sagt die Meldung unten genau das, statt + pauschal "kein PDF" zu behaupten. */ async function belegAusZwischenablage() { const ctx = belegPopup; if (!ctx) return; + const strgVHinweis = `Am Rechner kann der Browser eine kopierte Datei nur über ${tastenkuerzelEinfuegen()} entgegennehmen - bitte so einfügen oder das PDF hineinziehen.`; if (!navigator.clipboard || !navigator.clipboard.read) { - belegPopup.fehler = "Dieser Browser kann die Zwischenablage nicht lesen - bitte ziehen oder Datei auswählen."; + belegPopup.fehler = strgVHinweis; render(); return; } try { @@ -3053,13 +3069,25 @@ async function belegAusZwischenablage() { const typ = (eintrag.types || []).find((t) => t === "application/pdf"); if (typ) { belegPopupSchliessenUndLaden(new File([await eintrag.getType(typ)], "beleg.pdf", { type: "application/pdf" })); return; } } - belegPopup.fehler = "In der Zwischenablage liegt kein PDF."; + belegPopup.fehler = strgVHinweis; } catch (err) { - belegPopup.fehler = "Kein Zugriff auf die Zwischenablage."; + belegPopup.fehler = "Kein Zugriff auf die Zwischenablage. " + strgVHinweis; } render(); } +function tastenkuerzelEinfuegen() { + return /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent) ? "Cmd+V" : "Strg+V"; +} + +/* Der Einfügen-Knopf wird nur auf Tippgeräten angeboten. Am Rechner kann er + aus Datenschutzgründen prinzipiell nicht funktionieren (clipboard.read() + gibt Dateien nie heraus, siehe oben) - ein Knopf, der dort zwangsläufig + scheitert, führt nur in eine Sackgasse. Dort bleiben Ziehen und Strg+V. */ +function zwischenablageKnopfSinnvoll() { + return window.matchMedia && window.matchMedia("(pointer: coarse)").matches; +} + function vBelegPopup() { if (!belegPopup) return ""; return `
@@ -3067,11 +3095,11 @@ function vBelegPopup() { Beleg hochladen
PDF hierher ziehen - oder aus der Zwischenablage einfügen + oder mit ${tastenkuerzelEinfuegen()} einfügen
${belegPopup.fehler ? `${esc(belegPopup.fehler)}` : ""} - - + ${zwischenablageKnopfSinnvoll() ? `` : ""} + `; } @@ -3928,7 +3956,7 @@ function ereignisseVerdrahten() { if (w === "fahrten") csvDatei("fahrten.csv", ["Start", "Ende", "km", "Art", "Status"], TRIPS.map((t) => [t.ts_start, t.ts_end, t.distance_km != null ? de(t.distance_km) : "", t.art, t.status])); else if (w === "tanken") csvDatei("tankvorgaenge.csv", ["Zeitpunkt", "Station", "Liter", "€/l", "Kosten €"], - FILLS.map((f) => [f.ts, f.station_name || "", f.liters != null ? de(f.liters, 2) : "", f.liters ? de(f.fuel_total_eur / f.liters, 3) : "", f.fuel_total_eur != null ? eur(f.fuel_total_eur) : ""])); + FILLS.map((f) => [f.ts, f.station_name || "", f.liters != null ? de(f.liters, 2) : "", f.liters ? de(f.fuel_total_eur / f.liters, 2) : "", f.fuel_total_eur != null ? eur(f.fuel_total_eur) : ""])); else csvDatei("servicebuch.csv", ["Datum", "km", "Art", "Werkstatt", "Kosten €"], CAR.service.buch.map((e2) => [e2.datum, de(e2.km), e2.art, e2.werkstatt, e2.kosten != null ? eur(e2.kosten) : ""])); return; @@ -3995,7 +4023,7 @@ function ereignisseVerdrahten() { const liter = parseFloat((ROOT.getElementById(`${praefix}Liter`).value || "0").replace(",", ".")); const kosten = parseFloat((ROOT.getElementById(`${praefix}Kosten`).value || "0").replace(",", ".")); const anzeige = ROOT.getElementById(`${praefix}PreisAnzeige`); - if (anzeige) anzeige.textContent = liter > 0 ? `Preis je Liter: ${de(kosten / liter, 3)} €` : "Preis je Liter: wird berechnet"; + if (anzeige) anzeige.textContent = liter > 0 ? `Preis je Liter: ${de(kosten / liter, 2)} €` : "Preis je Liter: wird berechnet"; }); ROOT.addEventListener("change", (e) => { diff --git a/homeassistant/www/audi-dashboard-version.json b/homeassistant/www/audi-dashboard-version.json index fa9ec75..b04b813 100644 --- a/homeassistant/www/audi-dashboard-version.json +++ b/homeassistant/www/audi-dashboard-version.json @@ -1 +1 @@ -{"version": 1786919196} +{"version": 1786919856}