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:
@@ -1,6 +1,8 @@
|
||||
# AGENTS.md — Project state, review findings, open items, and working rules
|
||||
|
||||
**Last updated: 2026-08-31** (Neustart-Festhänger der App, OTA-Prüfsumme, Statistik-Kopfzahl,
|
||||
**Last updated: 2026-08-31** (Audit über Panel und iOS-App: sieben Befunde, sechs behoben,
|
||||
`ZUENDUNG_SENSOR` auf das Trip-Signal des FMM003 umgelegt; Manifest `2026.8.31.5`, siehe
|
||||
Abschnitt AV. Davor am selben Tag: Neustart-Festhänger der App, OTA-Prüfsumme, Statistik-Kopfzahl,
|
||||
Setup-Filter und die gemessene Wahrheit über die Dongle-Konfiguration; Manifest `2026.8.31.4`,
|
||||
siehe Abschnitte AT und AU. Davor: 2026-08-30 (owner findings batch - vehicle pin, tile chevrons, select arrows,
|
||||
SmartDeal dialog, address cache and pull-to-refresh; manifest `2026.8.30.19`, see sections AO, AP and AQ.
|
||||
@@ -6189,6 +6191,109 @@ mitten in der Fahrt eine Lücke von **2739 s** ohne einen einzigen Satz. Dagegen
|
||||
Einstellung am Gerät; das ist der in Abschnitt P/W beschriebene Fall, den die
|
||||
Plausibilitätsgrenze (`UNPLAUSIBLE_KMH`) abfängt.
|
||||
|
||||
## AV. Audit Panel/iOS und die sieben Befunde daraus (2026.8.31.5)
|
||||
|
||||
Bericht mit allen Messwerten:
|
||||
https://claude.ai/code/artifact/59b55b39-0d49-483d-a3b7-c29ca59af7bf
|
||||
|
||||
Der Eigentümer hat „alles" angewiesen, danach ausdrücklich auch die bis dahin
|
||||
vertagte Sensor-Umstellung. Alle sieben Befunde sind erledigt bis auf Befund 07, der
|
||||
eine Angabe des Eigentümers braucht.
|
||||
|
||||
### Die Zündung liegt jetzt auf dem Trip-Signal des Geräts
|
||||
|
||||
`ZUENDUNG_SENSOR` zeigt in `audi_ha_test` nicht mehr auf
|
||||
`engine_ignition_or_acc_status` (den rohen, prellenden Eingang), sondern auf
|
||||
`binary_sensor.…_trip_status_true_if_trip_started_false_if_stopped`. Begründung
|
||||
vollständig in Abschnitt AU. Kurz: das Trip-Signal beginnt erst, wenn Zündung **und**
|
||||
Bewegung **und** `Start Speed` zusammenkommen, und endet erst 900 s nach Zündung aus —
|
||||
die Pausentoleranz kommt damit vom Gerät, ohne dass wir einen zweiten Zeitgeber bauen.
|
||||
|
||||
Geschrieben über den echten Dienst (`entitaeten_schreiben`), nicht durch Hantieren an
|
||||
der Datei: nur diese eine Rolle hat sich geändert, alle 23 Einträge stehen noch.
|
||||
**Merkposten Werkzeug:** `hass.callService(...)` aus dem Browser-Werkzeug heraus
|
||||
scheiterte reproduzierbar mit der nichtssagenden Meldung `3`, `hass.callApi("POST",
|
||||
"services/…")` mit demselben Rumpf ging durch. Bei so einer Meldung also den REST-Weg
|
||||
nehmen, statt den Dienst zu verdächtigen.
|
||||
|
||||
**Der Preis, bewusst in Kauf genommen:** das Fahrtende liegt 15 Minuten hinter dem
|
||||
tatsächlichen — genau wie mit der früheren eigenen Pausentoleranz, also kein Rückschritt.
|
||||
Wer es genauer will, müsste das Ende nachträglich auf den letzten Punkt mit
|
||||
Geschwindigkeit > 0 zurückrechnen; eigenes Arbeitspaket, hier nicht angefangen.
|
||||
|
||||
### Live-Erkennung und Import bewerten dasselbe Fenster wieder gleich
|
||||
|
||||
`MINDESTDAUER_S = 60` lebt jetzt in `verlauf.py` neben `UNPLAUSIBLE_KMH` und aus
|
||||
demselben Grund. `fahrterkennung.fahrt_beenden()` prüft sie, bevor eine Fahrt entsteht;
|
||||
der laufende Start wird trotzdem zurückgesetzt, sonst hinge er für immer.
|
||||
|
||||
Vorher stand die Zahl allein in `historienimport.py` — dessen eigener Kommentar den
|
||||
Widerspruch sogar benannte („die Live-Erkennung legt sie zwar an"). Am 31.08. sind daraus
|
||||
an einem Tag **elf Fahrten mit 0 km** entstanden, zwei davon mit null Sekunden Dauer, alle
|
||||
als „vollständig" im Bestand. Die Umstellung oben nimmt die Ursache, diese Grenze fängt
|
||||
den Rest ab; beides zusammen, nicht eines statt des anderen.
|
||||
|
||||
**Nicht angefasst: die elf bereits gespeicherten Fahrten.** Das ist die echte Historie des
|
||||
Eigentümers, und deren Bereinigung ist seine Entscheidung — dieselbe Linie wie in
|
||||
Abschnitt W.
|
||||
|
||||
### Das Panel erfand einen Literpreis von 0,00 €
|
||||
|
||||
`f.liters ? f.fuel_total_eur / f.liters : null` — bei einem automatisch erkannten
|
||||
Tankvorgang stehen die Kosten noch nicht fest, und `null / 11.5` ist in JavaScript `0`.
|
||||
Liste, Einzelbeleg und CSV zeigten daraufhin einen Preis, den niemand bezahlt hat. Neue
|
||||
Hilfsfunktion `literpreis(f)` prüft beides, wie die App an derselben Stelle seit jeher;
|
||||
vier Aufrufstellen umgestellt. Live bestätigt: die drei Zeilen vom 27./29.08. zeigen jetzt
|
||||
nur noch die Liter.
|
||||
|
||||
### „Ø NaN €/l" und „Ø ∞ €/l" waren erreichbar
|
||||
|
||||
`schnittpreis()` teilte ohne Nennerprüfung: ein Zeitraum ohne Liter ergibt `0/0`, einer
|
||||
mit Kosten aber ohne Liter eine Division durch null, und `de()` reicht beides durch.
|
||||
Einzelne solche Datensätze gibt es real; es braucht nur einen Monat, der ausschließlich
|
||||
daraus besteht. Jetzt `if (liter <= 0) return 0`, wie `format.ts` es seit jeher tut.
|
||||
|
||||
### Der Setup-Dialog ist wirklich modal
|
||||
|
||||
Die Tab-Leiste hat einen eigenen, früher registrierten Zuhörer und umging damit die Sperre
|
||||
`if (setupOffen) return` des großen click-Handlers — obwohl der Dialog
|
||||
`aria-modal="true"` trägt. Er schwebte anschließend über einer anderen Seite, mitsamt der
|
||||
nicht gespeicherten Zuordnung. Dieselbe Sperre (plus `sheet`) steht jetzt auch dort.
|
||||
Live geprüft: Tabwechsel bei offenem Dialog bleibt wirkungslos, nach „Abbrechen" gehen
|
||||
alle fünf Tabs und eine Detailseite wieder.
|
||||
|
||||
### Verwaister Code entfernt
|
||||
|
||||
Panel: `bildSrc()`, `ring()`, `zustandsDaten()` (ohne Aufrufstelle) sowie
|
||||
`.bildgrid`, `.platzhalter-datei` und die beiden `.row-fussnote`-Regeln — Reste dreier
|
||||
abgelöster Entwürfe. App: 20 `.dm-*`-Regeln, darunter `dm-detail__zahl`, seit der
|
||||
Umstellung auf `Fig` ohne Markup.
|
||||
|
||||
**Wie geprüft wurde, damit die Liste stimmt:** ein Skript liest jede Regel und sucht ihre
|
||||
Verwender im Quelltext beider Codebasen. Zwei Fallen dabei, beide real aufgetreten: Namen
|
||||
aus deutschen Kommentaren sehen wie Klassen aus, und `ads-dot--${status}` wird zur
|
||||
Laufzeit zusammengesetzt und taucht in keiner Suche auf — sechs Design-System-Klassen
|
||||
standen deshalb zunächst falsch auf der Liste und wurden vor dem Melden aussortiert.
|
||||
|
||||
### Offen: Befund 07, Erstzulassung
|
||||
|
||||
`fahrzeug.erstzulassung` ist leer, deshalb steht bei der Hauptuntersuchung in beiden
|
||||
Oberflächen „kein Eintrag im Wartungsplan" — ohne dieses Datum gibt es keinen Anker,
|
||||
weder aus dem Wartungsplan noch aus einer Fahrzeugmeldung. Kein Codefehler; einzutragen
|
||||
unter Einstellungen → Fahrzeug einrichten. Bewusst nicht erfunden.
|
||||
|
||||
### Was am Panel und an der App in Ordnung war
|
||||
|
||||
Vier von fünf Tabs textgleich (die fünfte Abweichung war Befund 02 selbst), Service und
|
||||
Fahrzeugstatus ebenfalls, 153 `esc()`-Aufrufe ohne Lücke bei Freitext, beide
|
||||
Frontend-Dateien als **Modul** geparst (siehe Abschnitt AQ), 165/165 Tests grün, in der
|
||||
Konsole beider Oberflächen nur das bekannte ServiceWorker-Rauschen.
|
||||
|
||||
Verifiziert: `py_compile` auf den drei Backend-Dateien, `node --check` gegen die
|
||||
Modul-Grammatik, `tsc --noEmit` sauber, 165/165, `vite build` sauber,
|
||||
`audi_ha_test` auf `2026.8.31.5` sauber gestartet, und jede der fünf Codeänderungen
|
||||
einzeln live nachgewiesen.
|
||||
|
||||
## Working conventions (observed — keep them)
|
||||
|
||||
- German is the project language: identifiers, comments, commits, UI texts. Exceptions:
|
||||
|
||||
@@ -533,72 +533,7 @@ button.dm-serviceblock:active {
|
||||
border: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dm-bildmenu-fang {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.dm-bildmenu {
|
||||
position: absolute;
|
||||
z-index: 1001;
|
||||
bottom: var(--sp-5);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
min-width: 160px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 0;
|
||||
border-radius: 14px;
|
||||
padding: var(--sp-2);
|
||||
background: rgba(40, 44, 52, .92);
|
||||
-webkit-backdrop-filter: saturate(180%) blur(22px);
|
||||
backdrop-filter: saturate(180%) blur(22px);
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, .5);
|
||||
}
|
||||
|
||||
[data-theme="tag"] .dm-bildmenu {
|
||||
background: rgba(250, 250, 252, .92);
|
||||
}
|
||||
|
||||
.dm-bildmenu--rad {
|
||||
bottom: auto;
|
||||
top: 90px;
|
||||
left: 22px;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.dm-bildmenu button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
background: none;
|
||||
border: 0;
|
||||
border-bottom: 1px solid var(--line);
|
||||
color: var(--fg);
|
||||
font-family: inherit;
|
||||
font-size: 16px;
|
||||
padding: 13px 16px;
|
||||
min-height: 46px;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dm-bildmenu button:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.dm-bildmenu button:active {
|
||||
background: var(--shade);
|
||||
}
|
||||
|
||||
.dm-bildmenu__loeschen {
|
||||
color: var(--bad);
|
||||
}
|
||||
|
||||
/* Name des Autohauses: 15px in --fg, wie im Panel. */
|
||||
}/* Name des Autohauses: 15px in --fg, wie im Panel. */
|
||||
.dm-autohaus__name {
|
||||
font-size: 15px;
|
||||
margin-top: var(--sp-3);
|
||||
@@ -802,39 +737,13 @@ button.dm-serviceblock:active {
|
||||
font-size: 15px;
|
||||
font-weight: 300;
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
/* "Letzte Fahrt"/"Letzte Tankung" auf der Übersicht - deckungsgleich mit
|
||||
.leaf .v/.leaf .v small im Panel (15px/400/var(--fg2), Zusatzzeile
|
||||
12.5px/var(--fg3), KEINE Wide-Schrift/Hero-Zahl). War hier 20px/300 ohne
|
||||
Farbangabe - eine erfundene "große Zahl"-Behandlung, die dieser Kachel im
|
||||
Panel nie zustand (Parität-Audit 2026-08-30). Nur hier verwendet (die
|
||||
anderen NaviKachel-Aufrufe in MeinAudi.tsx/Service.tsx/Versicherung.tsx
|
||||
nutzen .dm-navikachel__wert direkt, unangetastet). */
|
||||
.dm-navikachel__zahl {
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
color: var(--fg2);
|
||||
}
|
||||
.dm-navikachel__wert .dm-navikachel__zahl + small {
|
||||
font-size: 12.5px;
|
||||
}
|
||||
|
||||
.dm-navikachel__wert small,
|
||||
}.dm-navikachel__wert small,
|
||||
.dm-wertzeile small {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
font-size: 12.5px;
|
||||
color: var(--fg3);
|
||||
}
|
||||
|
||||
.dm-aktionen {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: var(--sp-2) 0 var(--sp-4);
|
||||
}
|
||||
|
||||
.dm-textknopf {
|
||||
}.dm-textknopf {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--fg2);
|
||||
@@ -1207,37 +1116,7 @@ button.dm-listenzeile {
|
||||
gap: var(--sp-3);
|
||||
margin: var(--sp-2) 0 var(--sp-3);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.dm-detail__zahl {
|
||||
font-size: 40px;
|
||||
font-weight: 300;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
/* Einzelfahrt/Einzelbeleg: das Panel zeichnet die Distanz- bzw. Liter-Zahl in
|
||||
vTrip()/dem Tankvorgang-Detail eigens bei 52px (style="font-size:52px"),
|
||||
größer als die übrigen Detailseiten mit ihrer gemeinsamen 40px-.fig - kein
|
||||
Versehen, sondern die Hauptzahl der jeweiligen Seite. */
|
||||
.dm-detail__zahl--gross {
|
||||
font-size: 52px;
|
||||
}
|
||||
|
||||
.dm-detail__einheit {
|
||||
font-size: 15px;
|
||||
color: var(--fg2);
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.dm-detail__marken {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Deckend wie .mapbox im Panel (audi-dashboard-ios.css). --tile-2 ist im
|
||||
}/* Deckend wie .mapbox im Panel (audi-dashboard-ios.css). --tile-2 ist im
|
||||
Nachtmodus ein zweiter Schleier auf der ohnehin durchscheinenden Kachel -
|
||||
die Flaeche hinter den Kartenkacheln wurde dadurch heller als vorgesehen. */
|
||||
.dm-karte {
|
||||
@@ -1336,13 +1215,7 @@ button.dm-listenzeile {
|
||||
height: 4px;
|
||||
border-radius: 50%;
|
||||
background: var(--fg3);
|
||||
}
|
||||
|
||||
.dm-liste__serie {
|
||||
color: var(--fg2);
|
||||
}
|
||||
|
||||
/* 170px hoch wie BV_H im Panel — 120px ließen die Kurve sichtbar flacher
|
||||
}/* 170px hoch wie BV_H im Panel — 120px ließen die Kurve sichtbar flacher
|
||||
wirken als dort. */
|
||||
.dm-diagramm {
|
||||
width: 100%;
|
||||
@@ -1458,32 +1331,7 @@ button.dm-listenzeile {
|
||||
.dm-archivbtn:hover,
|
||||
.dm-archivbtn:active {
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
.dm-waehlen {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--sp-2);
|
||||
margin-top: var(--sp-3);
|
||||
}
|
||||
|
||||
.dm-waehlen__knopf {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 10px 16px;
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: var(--r-pill);
|
||||
font-size: 13px;
|
||||
font-weight: 300;
|
||||
color: var(--fg);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.dm-waehlen__knopf:hover {
|
||||
background: var(--shade);
|
||||
}
|
||||
|
||||
/* Bleibt beim Scrollen unten stehen, damit ungespeicherte Aenderungen nicht
|
||||
}/* Bleibt beim Scrollen unten stehen, damit ungespeicherte Aenderungen nicht
|
||||
aus dem Blick geraten. */
|
||||
.dm-speicherleiste {
|
||||
position: sticky;
|
||||
@@ -1911,15 +1759,7 @@ button.dm-listenzeile {
|
||||
border-color: var(--fg);
|
||||
border-style: solid;
|
||||
background: var(--shade);
|
||||
}
|
||||
|
||||
.dm-beleg-knoepfe {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-2);
|
||||
margin-top: var(--sp-3);
|
||||
}
|
||||
/* Solldruck: vier Zeichen ("2,40"), nicht die volle Feldbreite.
|
||||
}/* Solldruck: vier Zeichen ("2,40"), nicht die volle Feldbreite.
|
||||
Deckungsgleich mit .druckfeld in audi-dashboard.css (58px). Die Klasse
|
||||
stand hier schon im Markup, hatte aber keine Regel - die Felder nahmen
|
||||
deshalb die ganze Zeile ein. */
|
||||
|
||||
@@ -26,7 +26,7 @@ from typing import TYPE_CHECKING
|
||||
from . import screening
|
||||
from .ablage import neue_id
|
||||
from .veroeffentlichung import zustand_oder_none
|
||||
from .verlauf import verlauf_lesen, zeit_normalisiert
|
||||
from .verlauf import MINDESTDAUER_S, verlauf_lesen, zeit_normalisiert
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .koordinator import Koordinator
|
||||
@@ -170,6 +170,20 @@ async def _letztes_lebenszeichen(
|
||||
async def fahrt_beenden(
|
||||
k: Koordinator, start_ts: datetime.datetime, ende_ts: datetime.datetime
|
||||
) -> None:
|
||||
dauer_s = int((ende_ts - start_ts).total_seconds())
|
||||
if dauer_s < MINDESTDAUER_S:
|
||||
# Zündung an, aber nicht gefahren - oder ein prellender
|
||||
# Zündungseingang. Der Import verwirft solche Fenster seit jeher;
|
||||
# hier taten wir es nicht, und der Bestand füllte sich mit
|
||||
# Nulleinträgen (siehe MINDESTDAUER_S in verlauf.py). Der laufende
|
||||
# Start wird trotzdem zurückgesetzt, sonst hinge er für immer.
|
||||
await k.fahrt_start_setzen(None)
|
||||
_LOGGER.info(
|
||||
"Zündung war nur %s s an - keine Fahrt angelegt (Grenze %s s)",
|
||||
dauer_s,
|
||||
MINDESTDAUER_S,
|
||||
)
|
||||
return
|
||||
fahrt = leere_fahrt(start_ts, ende_ts, "ha")
|
||||
await k.ablage.fahrt_anhaengen(fahrt)
|
||||
await k.fahrt_start_setzen(None)
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version":"2026.8.31.4","sha256":"48d4965f198c06dbcad882c76eca656de6cc8c18a17c7ff24a39c5c9a1e8c5a9","bytes":266032,"gebaut":"2026-08-31T13:30:52Z"}
|
||||
{"version":"2026.8.31.5","sha256":"3f297e72a5b215e54d1eb17d134d6d66ef65d1d29a5793220ff2456b6c6cc5c8","bytes":265697,"gebaut":"2026-08-31T18:03:21Z"}
|
||||
Binary file not shown.
@@ -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) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[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;
|
||||
|
||||
|
||||
@@ -142,7 +142,6 @@ main#view{--seitenrand:16px;padding:0 var(--seitenrand) 30px;scroll-behavior:smo
|
||||
.row dt{font-size:16px;color:var(--fg)}
|
||||
.row dd{font-size:16px;color:var(--fg2)}
|
||||
.row dd small{font-size:12.5px;margin-top:2px}
|
||||
.row-fussnote{font-size:12.5px}
|
||||
.rows-fest dt{flex:0 0 150px}
|
||||
.chev{stroke:var(--ios-grau);stroke-width:1.6}
|
||||
|
||||
@@ -352,7 +351,6 @@ main#view{--seitenrand:16px;padding:0 var(--seitenrand) 30px;scroll-behavior:smo
|
||||
/* Feste 220px hier wuerden das Foto wieder beschneiden - die Hoehe
|
||||
bestimmt seit dem 2026-08-31 das Bild selbst (siehe audi-dashboard.css). */
|
||||
.bildbox.carfix.klein{height:auto}
|
||||
.bildgrid{grid-template-columns:repeat(4,1fr)}
|
||||
.quad{gap:14px}
|
||||
|
||||
/* Design-Audit 2026-08-13: .setup-popup/.sdpopup/.sheet/.standortmenu sind
|
||||
@@ -392,7 +390,6 @@ main#view{--seitenrand:16px;padding:0 var(--seitenrand) 30px;scroll-behavior:smo
|
||||
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 */
|
||||
.row dd .dot{width:8px;height:8px}
|
||||
|
||||
@@ -418,12 +418,10 @@ button.tile, .tilebtn { transition: background .15s, transform .1s; }
|
||||
.row dd { margin: 0; font-size: 14px; color: var(--fg); text-align: right; }
|
||||
.row dd small { display: block; font-size: 12.5px; color: var(--fg3); margin-top: 3px; }
|
||||
.rows-fest dd { min-width: 120px; }
|
||||
/* Fussnote unter einer Zeile, ueber die volle Kachelbreite statt in der
|
||||
rechten Spalte: dort blieben nur ~217px, in die eine Prognose wie
|
||||
"9.029 km, voraussichtlich am 12.04.2027" zwangslaeufig umbrach.
|
||||
:has() grenzt das Umbrechen auf genau die Zeilen ein, die eine Fussnote
|
||||
fuehren - alle anderen .row bleiben unveraendert einzeilig. */
|
||||
.row:has(.row-fussnote) { flex-wrap: wrap; }
|
||||
/* .row-fussnote ist mit dem Umbau der Service-Seite auf .termingruppe
|
||||
entfallen (2026-08-18) - dort gibt es keine dt/dd-Spalten mehr, die um
|
||||
Platz konkurrieren. Die beiden Regeln dafuer standen seither ohne
|
||||
passendes Markup da. */
|
||||
/* Sammelzeilen der Sicherheit-Ansicht (vSicherheit()): Kreis-Häkchen + Text
|
||||
+ optionaler Chevron statt dt/dd. Erbt Abstand/Trennlinie von .row, ersetzt
|
||||
aber dessen space-between-Layout und setzt die Button-Vorgaben zurück, wenn
|
||||
@@ -449,13 +447,6 @@ button.sicherheitszeile:active { background: var(--tile-2); }
|
||||
und Farbe wie jede andere Nebenzeile. */
|
||||
.row dd.row-hinweis { font-size: 12.5px; color: var(--fg3); }
|
||||
|
||||
.row-fussnote {
|
||||
flex: 1 0 100%;
|
||||
text-align: left;
|
||||
font-size: 12.5px;
|
||||
color: var(--fg3);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.leaf {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
|
||||
@@ -45,6 +45,7 @@ from .batterie import SPANNUNG_MIN_V
|
||||
from .fahrterkennung import leere_fahrt
|
||||
from .tankerkennung import LITER_SCHWELLE, leerer_tankvorgang, schwelle_prozent
|
||||
from .verlauf import (
|
||||
MINDESTDAUER_S,
|
||||
durchschnitt_kmh,
|
||||
hoechstwert_im_fenster,
|
||||
route_aus_verlauf,
|
||||
@@ -68,10 +69,8 @@ _LOGGER = logging.getLogger(__name__)
|
||||
# Import und Live-Erkennung am Rand überschneiden.
|
||||
TANK_DUBLETTE_MIN = 90
|
||||
|
||||
# Fahrten, die kürzer sind, sind Zündung-an-ohne-Fahrt (Radio, Tür öffnen mit
|
||||
# Zündung, Diagnose) - die Live-Erkennung legt sie zwar an, im Rückblick
|
||||
# fluten sie den Bestand aber mit Nulleinträgen. Bewusst konservativ.
|
||||
MINDESTDAUER_S = 60
|
||||
# MINDESTDAUER_S liegt seit dem 31.08.2026 in verlauf.py - die Live-Erkennung
|
||||
# prüft seitdem dieselbe Grenze, siehe die Begründung dort.
|
||||
|
||||
# UNPLAUSIBLE_KMH selbst lebt in verlauf.py - gemeinsam mit screening.py, das
|
||||
# dieselbe Prüfung seit 2026.8.27 auch für die Live-Vervollständigung nutzt
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "audi_dashboard",
|
||||
"name": "Audi Dashboard",
|
||||
"version": "2026.8.31.4",
|
||||
"version": "2026.8.31.5",
|
||||
"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"],
|
||||
|
||||
@@ -118,6 +118,18 @@ def zahl(wert: object) -> float | None:
|
||||
# bewerten.
|
||||
UNPLAUSIBLE_KMH = 300
|
||||
|
||||
# Kuerzer als das ist Zuendung ohne Fahrt: Radio, Tuer oeffnen, Diagnose - oder,
|
||||
# seit die Pausentoleranz entfallen ist, ein prellender Zuendungseingang. Am
|
||||
# 31.08.2026 sind daraus an einem einzigen Tag elf Fahrten mit 0 km entstanden,
|
||||
# zwei davon mit null Sekunden Dauer.
|
||||
#
|
||||
# Die Zahl stand vorher nur in historienimport.py, dessen eigener Kommentar den
|
||||
# Widerspruch sogar benannte ("die Live-Erkennung legt sie zwar an"): derselbe
|
||||
# Vorgang wurde beim Import verworfen und live gespeichert. Sie lebt deshalb
|
||||
# jetzt hier, gemeinsam mit UNPLAUSIBLE_KMH und aus demselben Grund - beide
|
||||
# Wege duerfen denselben Vorgang nie unterschiedlich bewerten.
|
||||
MINDESTDAUER_S = 60
|
||||
|
||||
|
||||
def durchschnitt_kmh(distanz_km: float | None, dauer_s: float | None) -> float | None:
|
||||
"""km/h aus Strecke und Dauer, oder None wenn eine der beiden fehlt oder
|
||||
|
||||
Reference in New Issue
Block a user