Audit Panel/iOS: sechs Befunde behoben, Zuendung aufs Trip-Signal (2026.8.31.5)

ZUENDUNG_SENSOR liegt jetzt auf dem Trip-Signal des FMM003 statt auf dem rohen,
prellenden Zuendungseingang. Damit kommt die Pausentoleranz vom Geraet - eine
Fahrt beginnt erst bei Zuendung UND Bewegung UND Start Speed.

MINDESTDAUER_S wandert nach verlauf.py und gilt jetzt auch im Livepfad. Vorher
verwarf nur der Import Zuendphasen unter 60 s; live entstanden daraus am 31.08.
elf Fahrten mit 0 km, zwei davon mit null Sekunden Dauer.

Das Panel erfand einen Literpreis von 0,00 EUR, wenn die Kosten noch nicht
feststanden (null/liter ist 0 in JavaScript) - neue Hilfsfunktion literpreis()
prueft beides, wie die App seit jeher. schnittpreis() teilte ohne Nennerpruefung
und konnte "NaN" oder "unendlich" anzeigen.

Der Setup-Dialog ist wirklich modal: die Tab-Leiste hatte einen eigenen Zuhoerer
vor der Sperre und liess einen Tabwechsel bei offenem Fenster zu.

Verwaister Code entfernt: drei Funktionen und vier Regeln im Panel, 20 Regeln in
der App.

Offen bleibt allein die leere Erstzulassung - eine Angabe, keine Aenderung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-31 20:05:59 +02:00
parent 90efcc5b00
commit 9b0e390dfd
11 changed files with 183 additions and 215 deletions
@@ -124,11 +124,29 @@ const $ = (s) => ROOT.querySelector(s);
/* ---------------------------------------------------------------- Format */
const de = (n, d = 0) => Number(n ?? 0).toLocaleString("de-DE", { minimumFractionDigits: d, maximumFractionDigits: d });
/* Preis je Liter, oder null.
Die Kosten muessen ausdruecklich geprueft werden, nicht nur die Liter: bei
einem automatisch erkannten Tankvorgang steht der Preis noch nicht fest, und
`null / 11.5` ist in JavaScript `0`. Die Liste, der Einzelbeleg und die
CSV-Ausgabe zeigten daraufhin "0,00 €/l" - einen Preis, den niemand bezahlt
hat. Die App prueft an derselben Stelle seit jeher auf beides. */
const literpreis = (f) => (f && f.liters && f.fuel_total_eur != null ? f.fuel_total_eur / f.liters : null);
const eur = (n) => de(n, 2);
const esc = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]));
const sum = (a, f) => a.reduce((s, x) => s + f(x), 0);
/* Durchschnittspreis IMMER mengengewichtet: Summe Kosten / Summe Liter */
const schnittpreis = (list) => sum(list, (f) => f.fuel_total_eur ?? f.summe ?? 0) / sum(list, (f) => f.liters ?? f.liter ?? 0);
/* Ohne die Nennerpruefung liefert ein Zeitraum, in dem kein Tankvorgang Liter
traegt, 0/0 = NaN - und einer mit Kosten aber ohne Liter eine Division durch
null. de() reicht beides ungefiltert durch, die Kopfzeile las dann
"Ø NaN €/l" bzw. "Ø ∞ €/l". Einzelne solche Datensaetze gibt es real; es
braucht nur einen Monat, der ausschliesslich daraus besteht. Die App hat
diesen Schutz samt Test seit jeher (format.ts). */
const schnittpreis = (list) => {
const liter = sum(list, (f) => f.liters ?? f.liter ?? 0);
if (liter <= 0) return 0;
return sum(list, (f) => f.fuel_total_eur ?? f.summe ?? 0) / liter;
};
/* -------------------------------------------------------- Adapter-Schicht
Übersetzt die Fahrzeugprofil-Struktur aus dem Backend (fahrzeug/
@@ -333,7 +351,6 @@ function bildInfo(i) {
}
return { label: b.label, datei: b.datei, src: `/local/bilder/${b.datei}?v=${bildVersion}` };
}
function bildSrc(i) { return bildInfo(i).src; }
function carImg(modus) {
if (modus === "fest") {
const info = bildInfo(startIndex());
@@ -460,19 +477,6 @@ function ics(titel, datum, ort, notiz) {
a.download = titel.replace(/[^\wäöüÄÖÜß ]/g, "").replace(/\s+/g, "-") + ".ics";
document.body.appendChild(a); a.click(); a.remove();
}
function ring(pct) {
const r = 64, c = 2 * Math.PI * r, on = (c * pct) / 100;
return `<div class="ringwrap">
<svg width="150" height="150" viewBox="0 0 150 150">
<circle cx="75" cy="75" r="${r}" fill="none" stroke="var(--line)" stroke-width="2"/>
<circle cx="75" cy="75" r="${r}" fill="none" stroke="var(--red)" stroke-width="2"
stroke-linecap="round" stroke-dasharray="${on} ${c}" transform="rotate(-90 75 75)"/>
</svg>
<div class="mid">
<div class="fig" style="font-size:40px">${pct}<span class="unit">%</span></div>
<span class="label" style="margin-top:8px">${de(CONFIG.tankvolumen * pct / 100, 1)} Liter</span>
</div></div>`;
}
/* ------------------------------------------------------------------ Karte
Leaflet weiterhin per CDN geladen (siehe Kopfkommentar). */
@@ -3925,7 +3929,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, 2)}` : "Preis je Liter: wird berechnet";
? `Preis je Liter: ${de(literpreis(q), 2)}` : "Preis je Liter: wird berechnet";
return `
<div class="feld"><label>Datum</label><input type="date" id="${praefix}Datum" value="${datum}"></div>
<div class="feld"><label>Uhrzeit</label><input type="time" id="${praefix}Zeit" value="${zeit}"></div>
@@ -4006,7 +4010,7 @@ function vFuel() {
<span style="color:var(--fg)">${eur(sum(fm, (f) => f.fuel_total_eur || 0))} </span>
<span>Ø ${de(schnittpreis(fm), 2)} /l</span></span></button>
<div class="acc-b ${mi === 0 ? "on" : ""}" id="fm${j}${m}">`;
fm.forEach((f) => { const preis = f.liters ? f.fuel_total_eur / f.liters : null; h += swipeZeile(`fill:${f.tank_id}`, `<button class="leaf" data-go="fill:${f.tank_id}">
fm.forEach((f) => { const preis = literpreis(f); h += swipeZeile(`fill:${f.tank_id}`, `<button class="leaf" data-go="fill:${f.tank_id}">
<span class="k">${dedat(new Date(f.ts))}<small>${esc(f.station_name || "")}</small></span>
<span class="v">${f.liters != null ? de(f.liters, 2) + " l" : ""}<small>${preis != null ? de(preis, 2) + " €/l" : ""}</small></span>
<svg class="chev" viewBox="0 0 6 10"><polyline points="1,1 5,5 1,9"/></svg></button>`); });
@@ -4021,7 +4025,7 @@ function vFuel() {
function vFill(id) {
const f = FILLS.find((x) => x.tank_id === id);
if (!f) return `<div class="tile">Tankvorgang nicht gefunden</div>`;
const berechneterPreis = f.liters ? f.fuel_total_eur / f.liters : null;
const berechneterPreis = literpreis(f);
if (tankBearbeitenOffen) {
return `
@@ -4323,7 +4327,7 @@ function datensatzExportieren(art) {
TRIPS.map((t) => [t.trip_id, t.ts_start, t.ts_end, t.distance_km != null ? de(t.distance_km) : "", t.art, t.status]));
} else if (art === "tanken") {
csvDatei("tankvorgaenge.csv", ["ID", "Zeitpunkt", "Station", "Liter", "€/l", "Kosten €"],
FILLS.map((f) => [f.tank_id, 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) : ""]));
FILLS.map((f) => [f.tank_id, f.ts, f.station_name || "", f.liters != null ? de(f.liters, 2) : "", literpreis(f) != null ? de(literpreis(f), 2) : "", f.fuel_total_eur != null ? eur(f.fuel_total_eur) : ""]));
} else if (art === "service") {
csvDatei("wartungsplan.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) : ""]));
@@ -4940,7 +4944,17 @@ function ereignisseVerdrahten() {
.replace(/audiRingsClip/g, "audiRingsClipNav");
tabbar.prepend(marke);
}
ROOT.getElementById("tabbar").addEventListener("click", (e) => { const b = e.target.closest(".tab"); if (b) go(b.dataset.tab); });
/* Der eigene Zuhoerer der Tab-Leiste haengt vor der Sperre des grossen
click-Handlers (`if (setupOffen) return`), umging sie deshalb und liess
einen Tabwechsel zu, waehrend das Setup-Fenster offen stand - obwohl es
aria-modal="true" traegt. Das Fenster schwebte danach ueber einer anderen
Seite, mitsamt der nicht gespeicherten Zuordnung. Dieselbe Sperre also
auch hier. */
ROOT.getElementById("tabbar").addEventListener("click", (e) => {
if (setupOffen || sheet) return;
const b = e.target.closest(".tab");
if (b) go(b.dataset.tab);
});
ROOT.getElementById("back").addEventListener("click", () => zurueck());
ROOT.getElementById("ringsbtn").addEventListener("click", () => go("einst"));
/* In Telefonbreite sind die Ringe oben rechts der Zugang zu den
@@ -5807,10 +5821,6 @@ function ereignisseVerdrahten() {
}
/* ------------------------------------------------------------ Datenladen */
function zustandsDaten(entityId) {
const st = HASS && HASS.states[entityId];
return st && st.attributes ? st.attributes.daten : undefined;
}
let letzterProfilState = null, letzterFahrtenState = null, letzterTankState = null, letzterStatusState = null, letzterBelegState = null, letzterBattState = null, letzterVersionState = null;