Batteriespannung: 8V-Plausibilitätsgrenze; Fahrten: echte Start-/Zielposition und Streckenlinie

Batteriespannung: Werte unter 8V (Sensor-/Verbindungsfehler, nicht physikalisch
plausibel für eine 12V-Bleibatterie) werden nicht mehr erfasst - live und beim
Import. "Neue Räder anlegen"-Knopf sitzt jetzt auf derselben Zeile wie "Montiert".

Fahrten: start_lat/-lon, end_lat/-lon und route wurden bisher nur beim
rückwirkenden Import befüllt, nie bei live erkannten Fahrten (dem Normalfall) -
Screening trägt sie jetzt aus dem GPS-Verlauf nach, unabhängig vom
Kilometerstand-Status. fakeTrack() (eine erfundene Linie) ist entfernt, die
Karte zeichnet jetzt die echte Route oder eine ehrliche Gerade zwischen den
bekannten Punkten. CARTOs anonyme Kartenkacheln verlangen inzwischen einen
API-Schlüssel ("API key required" auf jeder Karte in beiden Apps) - auf
schlüssellose OpenStreetMap-Kacheln umgestellt. Start/Ziel zeigen jetzt Datum
und Uhrzeit über der Adresse; die fehlerhafte "Status"-Zeile ist entfernt.
Batteriespannungs-Diagramm nutzt auf großen Bildschirmen die volle
Spaltenbreite statt einer festen 400px-Deckelung.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-27 17:23:32 +02:00
parent 17149937a6
commit f337820fc8
13 changed files with 328 additions and 77 deletions
+5 -5
View File
@@ -4,10 +4,10 @@
Die Fachtypen (Fahrt, Tankvorgang, ...) sind aus SPECIFICATION.md §5
"Data files" abgeleitet - also aus dem, was das Backend tatsächlich in
fahrten.jsonl / tankvorgaenge.jsonl schreibt, nicht aus Wunschdenken.
Felder, die laut §7 im Schema stehen, aber von keinem Codepfad je gefüllt
werden (start_lat, avg_speed_kmh, route, ...), sind hier bewusst als
optional markiert - sie können null/undefined sein und die Oberfläche darf
sich nie darauf verlassen. */
Mehrere Felder (start_lat/-lon, end_lat/-lon, route, avg_speed_kmh) sind
nur befüllt, wenn ein GPS-Sensor zugeordnet ist (screening.py/
historienimport.py) - ohne Zuordnung bleiben sie null. Deshalb hier bewusst
optional markiert; die Oberfläche darf sich nie auf ihre Existenz verlassen. */
/** Roher Zustand einer HA-Entität, wie ihn /api/states liefert. */
export interface HassState<A = Record<string, unknown>> {
@@ -55,7 +55,7 @@ export interface Fahrt {
end_lon?: number | null;
start_address?: string | null;
end_address?: string | null;
route?: unknown | null;
route?: [number, number][] | null;
pausen?: unknown[];
}
+17 -10
View File
@@ -110,6 +110,7 @@ export function FahrtDetail({ id }: { id: string | undefined }) {
{...(fahrt.end_lat != null && fahrt.end_lon != null
? { ziel: { lat: fahrt.end_lat, lon: fahrt.end_lon } }
: {})}
{...(fahrt.route && fahrt.route.length >= 2 ? { route: fahrt.route } : {})}
/>
) : (
<Leerzustand
@@ -117,16 +118,22 @@ export function FahrtDetail({ id }: { id: string | undefined }) {
text="Das Fahrzeug liefert derzeit keine Positionsdaten. Sobald es das tut, erscheint hier die gefahrene Strecke auf der Karte."
/>
)}
{(fahrt.start_address || fahrt.end_address) && (
<Werteliste
kinder={
<>
<Wertzeile label="Start" wert={fahrt.start_address ?? "—"} />
<Wertzeile label="Ziel" wert={fahrt.end_address ?? "—"} />
</>
}
/>
)}
<Werteliste
kinder={
<>
<Wertzeile
label="Start"
wert={`${datum(fahrt.ts_start)} · ${uhrzeit(fahrt.ts_start)} Uhr`}
zusatz={fahrt.start_address ?? "unbekannt"}
/>
<Wertzeile
label="Ziel"
wert={`${datum(fahrt.ts_end)} · ${uhrzeit(fahrt.ts_end)} Uhr`}
zusatz={fahrt.end_address ?? "unbekannt"}
/>
</>
}
/>
</Tile>
</>
)
+24 -19
View File
@@ -13,27 +13,30 @@ import { useEffect, useRef } from "react"
import "leaflet/dist/leaflet.css"
import { useTheme } from "../theme"
export interface Punkt {
lat: number
lon: number
}
const KACHELN = {
nacht: {
url: "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png",
zusatz: "© OpenStreetMap, © CARTO",
},
tag: {
url: "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png",
zusatz: "© OpenStreetMap, © CARTO",
},
} as const
// Themeunabhaengig hell wie im Panel (Kartendienste zeigen die
// Standard-Strassenkarte immer hell) - und seit CARTOs anonyme
// basemaps.cartocdn.com-Kacheln einen eigenen API-Schluessel verlangen
// (ohne ihn nur noch "API key required"-Kacheln), die einzige hier weiterhin
// ohne Schluessel nutzbare Quelle.
const KACHEL_URL = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
const KACHEL_ZUSATZ = "© OpenStreetMap contributors"
export function Karte({ start, ziel }: { start: Punkt; ziel?: Punkt }) {
export function Karte({
start,
ziel,
route,
}: {
start: Punkt
ziel?: Punkt
/** Echte Streckenlinie aus dem GPS-Verlauf (Backend: screening.py/historienimport.py). */
route?: [number, number][]
}) {
const behaelter = useRef<HTMLDivElement | null>(null)
const [theme] = useTheme()
useEffect(() => {
const element = behaelter.current
@@ -47,8 +50,7 @@ export function Karte({ start, ziel }: { start: Punkt; ziel?: Punkt }) {
void import("leaflet").then((L) => {
if (abgebrochen) return
karte = L.map(element, { attributionControl: true, scrollWheelZoom: false })
const kachel = KACHELN[theme]
L.tileLayer(kachel.url, { attribution: kachel.zusatz, maxZoom: 19 }).addTo(karte)
L.tileLayer(KACHEL_URL, { attribution: KACHEL_ZUSATZ, maxZoom: 19 }).addTo(karte)
const punkte: [number, number][] = [[start.lat, start.lon]]
L.circleMarker([start.lat, start.lon], { radius: 6, color: "#F50537" })
@@ -60,8 +62,11 @@ export function Karte({ start, ziel }: { start: Punkt; ziel?: Punkt }) {
L.circleMarker([ziel.lat, ziel.lon], { radius: 6, color: "#F50537", fill: true })
.bindTooltip("Ziel")
.addTo(karte)
// Verbindungslinie NUR zwischen echten Punkten, kein erfundener Verlauf.
L.polyline(punkte, { color: "#F50537", weight: 2, dashArray: "4 6" }).addTo(karte)
// Echte Route, wenn vorhanden - sonst eine Verbindungslinie NUR zwischen
// den beiden echten Punkten, kein erfundener Verlauf dazwischen.
const linie = route && route.length >= 2 ? route : punkte
L.polyline(linie, { color: "#F50537", weight: 2, dashArray: route && route.length >= 2 ? undefined : "4 6" }).addTo(karte)
if (route && route.length >= 2) punkte.push(...route)
}
if (punkte.length > 1) {
@@ -75,7 +80,7 @@ export function Karte({ start, ziel }: { start: Punkt; ziel?: Punkt }) {
abgebrochen = true
karte?.remove()
}
}, [start, ziel, theme])
}, [start, ziel, route])
return <div ref={behaelter} className="dm-karte" role="img" aria-label="Karte der Fahrt" />
}