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
@@ -377,6 +377,10 @@ const dedat = (d) => (gueltigesDatum(d)
? d.toLocaleDateString("de-DE", { day: "2-digit", month: "2-digit", year: "numeric" })
: "");
function dat(s) { const [d, m, j] = s.split(".").map(Number); return new Date(j, m - 1, d); }
/* Datum + Uhrzeit in einer Zeile, für Start-/Zielzeitpunkt der Einzelfahrt. */
const dezeitpunkt = (d) => (gueltigesDatum(d)
? `${dedat(d)} · ${d.toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" })} Uhr`
: "");
const MISCHDAUER = 180;
function tagesleistung() {
@@ -449,9 +453,14 @@ let BELEG_STATION_POS = null;
/* Bewusst themeunabhaengig hell: Kartendienste (Google Maps & Co.) zeigen die
Standard-Strassenkarte immer im hellen Stil, weil Strassen, Beschriftungen
und Wegfarben darauf lesbar bleiben. Die frueher bei Nacht eingeblendeten
dark_all-Kacheln liessen die Karte im Panel schwarz wirken. */
const TILE_URL = "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png";
const ATTR = "&copy; OpenStreetMap &middot; CARTO";
dark_all-Kacheln liessen die Karte im Panel schwarz wirken.
CARTOs anonyme basemaps.cartocdn.com-Kacheln (vorher hier) verlangen
inzwischen einen eigenen API-Schlüssel - ohne ihn zeigten die Kacheln nur
noch "API key required". Die Standard-OpenStreetMap-Kacheln sind weiterhin
ohne Schlüssel nutzbar und decken diesen Bedarf (ein Fahrzeug, geringes
Kartenaufkommen) ohne eigenen Dienst. */
const TILE_URL = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png";
const ATTR = "&copy; OpenStreetMap contributors";
/* Satellitenansicht der Standort-Vollbildkarte (Layer-Knopf) - wie die
CARTO-Straßenkacheln oben ohne eigenen Schlüssel nutzbar. */
const SAT_TILE_URL = "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}";
@@ -527,15 +536,6 @@ function kartenMittelpunkt() {
return pos && pos.length === 2 ? pos : [51.1657, 10.4515];
}
function fakeTrack(a, b, n = 13) {
const pts = []; let seed = Math.round((a[0] + b[1]) * 1000);
const rnd = () => { seed = (seed * 1103515245 + 12345) & 0x7fffffff; return seed / 0x7fffffff - 0.5; };
for (let i = 0; i <= n; i++) {
const t = i / n, k = Math.sin(t * Math.PI);
pts.push([a[0] + (b[0] - a[0]) * t + rnd() * 0.05 * k, a[1] + (b[1] - a[1]) * t + rnd() * 0.05 * k]);
}
pts[0] = a; pts[n] = b; return pts;
}
function tiles() {
if (!MAP || !window.L) return;
const url = TILE_URL;
@@ -552,14 +552,20 @@ async function initMap() {
if (route.name === "trip") {
const t = TRIPS.find((x) => x.trip_id === route.id);
if (t && t.start_lat != null && t.end_lat != null) {
const track = fakeTrack([t.start_lat, t.start_lon], [t.end_lat, t.end_lon]);
const line = window.L.polyline(track, { color: rot, weight: 2.5, opacity: 0.9 }).addTo(MAP);
// Echte Streckenlinie aus dem GPS-Verlauf (route, screening.py), wenn
// vorhanden - sonst eine ehrliche gerade Linie zwischen Start und Ziel,
// keine erfundene Kurve mehr (siehe companion-app/Karte.tsx, dieselbe
// Haltung: keine Route zu zeigen, die es so nicht gab).
const track = Array.isArray(t.route) && t.route.length >= 2
? t.route
: [[t.start_lat, t.start_lon], [t.end_lat, t.end_lon]];
const line = window.L.polyline(track, { color: rot, weight: 2.5, opacity: 0.9, dashArray: Array.isArray(t.route) && t.route.length >= 2 ? null : "4 6" }).addTo(MAP);
track.slice(1, -1).forEach((pt) => window.L.circleMarker(pt, { radius: 2.5, color: rot, weight: 0, fillColor: rot, fillOpacity: 0.85 }).addTo(MAP));
window.L.circleMarker([t.start_lat, t.start_lon], { radius: 6, color: "#fff", weight: 2, fillColor: "#000", fillOpacity: 1 }).addTo(MAP).bindPopup("Start · " + (t.start_address || ""));
window.L.circleMarker([t.end_lat, t.end_lon], { radius: 6, color: rot, weight: 2, fillColor: rot, fillOpacity: 1 }).addTo(MAP).bindPopup("Ziel · " + (t.end_address || ""));
MAP.fitBounds(line.getBounds(), { padding: [26, 26] });
} else {
MAP.setView(kartenMittelpunkt(), 11); // kein Track ohne GPS-Daten (§7a-Backend-Lücke)
MAP.setView(kartenMittelpunkt(), 11); // kein Track ohne GPS-Daten
}
} else if (route.name === "fill") {
BELEG_STATION_POS = null; // gehört zum vorherigen Beleg, sonst zentriert der Knopf falsch
@@ -1804,7 +1810,7 @@ function vBatterieverlauf() {
</div>
${genug ? `
<div id="bvChart" style="margin-top:14px;touch-action:none;position:relative">
<svg id="bvSvg" viewBox="0 0 ${BV_W} ${BV_H}" preserveAspectRatio="none" style="width:100%;height:170px;display:block"></svg>
<svg id="bvSvg" viewBox="0 0 ${BV_W} ${BV_H}" preserveAspectRatio="none" style="width:100%;aspect-ratio:${BV_W}/${BV_H};display:block"></svg>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:10px">
<span class="label" id="bvRange" style="margin-top:0"></span>
@@ -2886,8 +2892,8 @@ function vReifen() {
<button type="button" data-bildersetzen="${radDatei}">Foto ersetzen</button>
<button type="button" class="loeschen" data-bildloeschen="${radDatei}">Löschen</button>
</div>` : ""}
<button data-satz="${k === "sommer" ? "Sommer" : "Winter"}" class="montiert ${aktiv ? "on" : ""}">Montiert</button>
<div style="display:flex;justify-content:flex-end;margin-top:10px">
<div style="display:flex;justify-content:space-between;align-items:center">
<button data-satz="${k === "sommer" ? "Sommer" : "Winter"}" class="montiert ${aktiv ? "on" : ""}">Montiert</button>
<button class="iconbtn" data-reifenarchivieren="${k}" aria-label="Neue Räder anlegen">${ciSVG(CI.packageS, 18)}</button>
</div>
<div style="margin-top:12px">
@@ -3150,13 +3156,12 @@ function vTrip(id) {
style="cursor:pointer;font-family:inherit">${artText(t.art)}</button></div>
</div>
<div class="tile"><dl class="rows">
<div class="row"><dt>Startort</dt><dd>${esc(t.start_address || "unbekannt")}<small>${dedat(new Date(t.ts_start))}</small></dd></div>
<div class="row"><dt>Zielort</dt><dd>${esc(t.end_address || "unbekannt")}</dd></div>
<div class="row"><dt>Start</dt><dd>${dezeitpunkt(new Date(t.ts_start))}<small>${esc(t.start_address || "unbekannt")}</small></dd></div>
<div class="row"><dt>Ziel</dt><dd>${dezeitpunkt(new Date(t.ts_end))}<small>${esc(t.end_address || "unbekannt")}</small></dd></div>
<div class="row"><dt>Startkilometer</dt><dd>${t.odo_start != null ? de(t.odo_start) + " km" : "noch offen"}</dd></div>
<div class="row"><dt>Endkilometer</dt><dd>${t.odo_end != null ? de(t.odo_end) + " km" : "noch offen"}</dd></div>
<div class="row"><dt>Dauer</dt><dd>${dauerText(t.duration_s)}</dd></div>
<div class="row"><dt>Verbrauch</dt><dd>${fahrtVerbrauch(t) != null ? de(fahrtVerbrauch(t), 1) + " l/100 km" : "liegt nicht vor"}<small>vom Fahrzeug gemeldet</small></dd></div>
<div class="row"><dt>Status</dt><dd>${t.status}<small>Quelle: ${t.km_quelle || ""}</small></dd></div>
</dl>
<button class="aktion" data-fahrtbearbeiten>Werte bearbeiten</button>
</div>