Fahrt beenden auf Knopfdruck, Schieberegler, Zuendungspause (2026.9.3.5-.7)

Vier Themen aus einer Sitzung.

1. Die Fahrt endet wieder bei uns statt im Dongle (Abschnitt BW). Der
   Ignition-OFF-Timeout des FMM003 (900 s) und sein Schlaf-Timeout (ebenfalls
   900 s) starten beide beim Zuendungs-Aus und fallen in derselben Sekunde -
   am 02.09. zweimal beobachtet, einmal ging das Trip-Ende verloren, einmal kam
   es eine Sekunde vor dem Schlaf an. Ausloeser ist jetzt die Zuendung, die
   Wartezeit laeuft in Home Assistant. ZUENDUNG_NACHLAUF_S = 180 wird in beiden
   Wegen abgezogen (ueber das Trip-Signal 1080 s, weil dessen Ende selbst an der
   verzoegerten Zuendung haengt).

2. Schieberegler, neu in beiden Codebasen. Schrittweite 5 Minuten; der Daumen
   war im Tagmodus weiss auf weiss und traegt jetzt einen Ring aus
   --line-strong - ein Token, das genau dort sichtbar ist, wo es gebraucht wird.

3. Knopf "Fahrt beenden" in der Zuletzt-Kachel (Abschnitt BX). Schliesst auf
   das letzte Lebenszeichen, nicht auf "jetzt", und kennzeichnet das Ende als
   vorlaeufig. Ein spaet eintreffendes Zuendungs-Aus zieht es nach - innerhalb
   von sechs Stunden, nur nach vorn, und nur bei einer vorlaeufigen Fahrt.
   ts_end wandert bewusst NICHT in edited_fields, sonst blockierte der Schutz
   fuer Handeingaben genau diese Korrektur.

4. Das OTA-Buendel kann auf dem Mac gar nicht entstehen (Abschnitt BV):
   npm run ota ist eine Windows-PowerShell-Datei, ios-signieren.sh baut ein
   frisches dist/ und fasst das Buendel nie an. Ausgeliefert war deshalb eine
   Oberflaeche ohne den Versionshinweis unter richtiger Nummer.

Verifiziert: Backend 27/27 (5 Signalwechsel, 14 Zuendungspause, 8 Fahrtende),
companion-app tsc sauber und 179/179, Panel als Modul geparst, audi_ha_test auf
2026.9.3.7 sauber gestartet. Live am laufenden Panel und ohne Rueckstand
belegt: Wartezeit samt Abbruch, die 180-s-Rechnung, der Regler im Tagmodus und
der Knopf von der laufenden Fahrt bis zum verworfenen Kurzvorgang - Fahrten
vorher 16, nachher 16.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-03 15:00:38 +02:00
co-authored by Claude Opus 5
parent d30bb0e9f0
commit 5ccbfec92d
29 changed files with 1444 additions and 51 deletions
@@ -200,6 +200,11 @@ function profilZuConfig(p) {
backupIntervall: p.einstellungen.backup_intervall || "aus",
letztesBackup: p.einstellungen.letztes_backup || null,
ausfuehrung: p.fahrzeug.ausfuehrung || "",
// Der Wert hat die Zeit ueberlebt, in der die Einstellung nicht
// angeboten wurde (31.08. bis 03.09.2026); der Standard greift nur bei
// neuen Profilen. Siehe fahrterkennung._pausenzeit_s im Backend.
pausenzeit: Number.isFinite(p.einstellungen.fahrten_pausenzeit_min)
? p.einstellungen.fahrten_pausenzeit_min : 15,
};
}
@@ -227,6 +232,7 @@ function profilZuCar(p, status) {
sicherheitscheck: status.sicherheitscheck || [],
zuendung: status.zuendung ?? null,
geparktSeit: status.geparkt_seit ?? null,
faehrtSeit: status.faehrt_seit ?? null,
standortLat: status.standort_lat,
standortLon: status.standort_lon,
standortGenauigkeitM: status.standort_genauigkeit_m,
@@ -284,6 +290,7 @@ function configCarZuProfil(vorherigesProfil) {
p.fahrzeug.ausfuehrung = CONFIG.ausfuehrung;
p.einstellungen.uebersichtsbild = CONFIG.startbild;
p.einstellungen.backup_intervall = CONFIG.backupIntervall;
p.einstellungen.fahrten_pausenzeit_min = CONFIG.pausenzeit;
p.einstellungen.smartdeal = { aktiv: CAR.smartdeal.aktiv, laeuft_ab: CAR.smartdeal.laeuftAb };
p.einstellungen.oelwechsel_intervall = {
modus: CAR.oel.modus, km: CAR.oel.km, monate: CAR.oel.monate,
@@ -959,9 +966,9 @@ function standortZustand() {
};
}
/* Ab wann aus "steht gerade" ein "Parkplatz" wird. Hing bis 2026-08-31 an
der Einstellung fahrten_pausenzeit_min - die ist mit der Pausenregel
entfallen (der FMM003 bringt sie selbst mit), der Schwellwert hier hat
damit aber nichts zu tun und bleibt als eigene Zahl stehen. */
der Einstellung fahrten_pausenzeit_min. Die gibt es seit dem 03.09.2026
wieder ("Fahrt beenden"), aber der Schwellwert hier hat nach wie vor nichts
damit zu tun und bleibt als eigene Zahl stehen. */
const PARKPLATZ_AB_MIN = 15;
function dauerSeitText(ts) {
if (ts == null) return "unbekannt";
@@ -1608,9 +1615,15 @@ function teaser() {
: null;
const t = juengster(TRIPS, "ts_start"), f = juengster(FILLS, "ts");
return `<div class="tile teaser"><span class="label">Zuletzt</span>
${t ? `<button class="leaf" style="padding-left:0" data-go="trip:${t.trip_id}">
${CAR.faehrtSeit ? `<div class="leaf" style="padding-left:0">
${/* Kein Knopf und keine Wertespalte: solange die Fahrt laeuft, gibt es
weder Strecke noch Verbrauch. Ein Strich behauptete, ein Wert fehle -
richtig ist, dass es ihn noch nicht gibt. */""}
<span class="k">Aktuelle Fahrt<small class="laeuft">läuft</small></span>
<button class="zeilenknopf" data-fahrtbeenden>Fahrt beenden</button></div>`
: t ? `<button class="leaf" style="padding-left:0" data-go="trip:${t.trip_id}">
<span class="k">Letzte Fahrt<small>${strecke(t)}</small></span>
<span class="v">${streckeText(t.distance_km, "—")}<small>${fahrtVerbrauch(t) != null ? de(fahrtVerbrauch(t), 1) + " l/100 km" : "—"}</small></span>
<span class="v">${t.ende_vorlaeufig ? "~ " : ""}${streckeText(t.distance_km, "—")}<small${t.ende_vorlaeufig ? " class=\"vorlaeufig\"" : ""}>${t.ende_vorlaeufig ? "vorläufig" : fahrtVerbrauch(t) != null ? de(fahrtVerbrauch(t), 1) + " l/100 km" : "—"}</small></span>
<svg class="chev" viewBox="0 0 6 10"><polyline points="1,1 5,5 1,9"/></svg></button>` : ""}
${f ? `<button class="leaf" style="padding-left:0;border-bottom:0" data-go="fill:${f.tank_id}">
<span class="k">Letzter Tankvorgang<small>${f.ts ? dedat(new Date(f.ts)) : ""}</small></span>
@@ -3063,6 +3076,18 @@ function vEinst() {
` : ""}
<button class="aktion ${einrichtenOffen ? "primaer" : ""}" style="margin-top:16px" data-einrichten-auf>${einrichtenOffen ? "Fertig" : "Einrichten"}</button>
</div>
<div class="tile"><span class="label">Fahrt beenden</span>
<div class="schieber" style="margin-top:14px">
<input type="range" min="0" max="60" step="5" value="${CONFIG.pausenzeit}"
style="--fuell:${(CONFIG.pausenzeit / 60) * 100}%" data-pausenzeit
aria-label="Minuten bis zum automatischen Fahrtende">
<output>${CONFIG.pausenzeit === 0 ? "sofort" : CONFIG.pausenzeit + " Min."}</output>
</div>
<span class="label" style="margin-top:14px;color:var(--fg2);letter-spacing:0;
text-transform:none;font-size:12.5px;line-height:1.6">
Hier können Sie einstellen, wie viele Minuten nach Ausschalten der Zündung die
Fahrt automatisch beendet werden soll.</span>
</div>
<div class="tile"><span class="label">Darstellung</span>
<div class="seg">
<button data-mode="nacht" class="${HOST.dataset.theme === "nacht" ? "on" : ""}">Nacht</button>
@@ -5946,11 +5971,29 @@ function ereignisseVerdrahten() {
// Einzelbeleg: zurück auf die Tankstelle (manipuliert MAP direkt, kein
// render() - das würde die Karte neu aufbauen und den Ausschnitt verlieren).
if (e.target.closest("#belegZentrieren")) { belegAufTankstelleZentrieren(); return; }
// Vor [data-go]: der Knopf sitzt in der Zuletzt-Kachel, deren Zeile sonst
// in die Einzelfahrt navigiert. Die Zeile ist waehrend der Fahrt zwar ein
// div ohne data-go, aber die Reihenfolge haelt das auch dann, wenn die
// Kachel spaeter einmal anders gebaut wird.
if (e.target.closest("[data-fahrtbeenden]")) { serviceRufen("fahrt_jetzt_beenden", {}); return; }
const g = e.target.closest("[data-go]");
if (g) { const [n, i] = g.dataset.go.split(":"); go(n, i); }
});
ROOT.addEventListener("input", (e) => {
if (e.target.dataset.pausenzeit !== undefined) {
// Beim Ziehen nur die Anzeige und die Fuellung nachziehen, NICHT
// render() und nicht speichern: ein Re-Render mitten in der Geste
// erzeugt den Regler neu und der Daumen springt aus der Hand.
// Gespeichert wird beim Loslassen (change-Zweig weiter unten) -
// dieselbe Trennung wie beim Suchfeld direkt darunter.
const minuten = parseInt(e.target.value, 10);
CONFIG.pausenzeit = minuten;
e.target.style.setProperty("--fuell", (minuten / 60) * 100 + "%");
const feld = e.target.parentElement.querySelector("output");
if (feld) feld.textContent = minuten === 0 ? "sofort" : minuten + " Min.";
return;
}
const sucheFeld = e.target.dataset.setupSuche;
if (sucheFeld) {
// Nur die Kandidatenliste patchen, nicht render() - ein voller
@@ -6150,6 +6193,7 @@ function ereignisseVerdrahten() {
if (e.target.dataset.oelm !== undefined) { CAR.oel.monate = parseInt(e.target.value, 10); profilSpeichern(); return; }
if (e.target.dataset.setupNurpassend !== undefined) { setupNurPassend = e.target.checked; render(); return; }
if (e.target.dataset.startbild !== undefined) { CONFIG.startbild = e.target.value; profilSpeichern(); render(); return; }
if (e.target.dataset.pausenzeit !== undefined) { CONFIG.pausenzeit = parseInt(e.target.value, 10); profilSpeichern(); return; }
if (e.target.dataset.svsel !== undefined) { svAuswahl = parseInt(e.target.value, 10); render(); return; }
});
}