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 <noreply@anthropic.com>
This commit is contained in:
2026-09-03 15:00:38 +02:00
parent d30bb0e9f0
commit 5ccbfec92d
29 changed files with 1444 additions and 51 deletions
+9
View File
@@ -332,6 +332,15 @@ export class DataMetricApi {
/** Startet den Import. Kehrt zurück, sobald das Backend den Auftrag
angenommen hat — nicht, wenn er fertig ist; dafür importStatusLesen(). */
/** Beendet die laufende Fahrt sofort, mit vorlaeufigem Ende.
Wie historieImportieren() bewusst NICHT ueber die Warteschlange: im
Funkloch abgesetzt und Stunden spaeter nachgeholt wuerde sie eine Fahrt
schliessen, die laengst regulaer zu Ende ist. */
fahrtJetztBeenden(): Promise<unknown> {
return this.rest.dienstAufrufen(DIENST_DOMAIN, "fahrt_jetzt_beenden", {});
}
historieImportieren(start: string, ende: string): Promise<unknown> {
return this.rest.dienstAufrufen(DIENST_DOMAIN, "historie_importieren", {
start,
+3
View File
@@ -68,6 +68,9 @@ export interface Fahrt {
* vom Fahrzeug selbst für diese eine Fahrt gemeldeter Wert - siehe
* verbrauch_aus_literstaenden() im Backend. */
verbrauch_l_100km?: number | null;
/** Das Ende steht nur vorlaeufig fest (Knopf "Fahrt beenden", das Geraet
hat sein Zuendungs-Aus noch nicht gemeldet). */
ende_vorlaeufig?: boolean;
pausen?: unknown[];
}
+11
View File
@@ -23,6 +23,9 @@ export interface Einstellungen {
backupIntervall: string
letztesBackup: string | null
ausfuehrung: string
/** Minuten nach dem Zuendungs-Aus, bis die Fahrt automatisch endet.
0 heisst: sofort. Siehe fahrterkennung._pausenzeit_s im Backend. */
pausenzeitMin: number
}
export interface Reifensatz {
@@ -69,6 +72,8 @@ export interface Fahrzeug {
zugeordnet", also unbekannt — nicht „steht". */
zuendung: boolean | null
geparktSeit: string | null
/** Beginn der laufenden Fahrt, sonst null - siehe faehrt_seit im Backend. */
faehrtSeit: string | null
standortLat: number | null
standortLon: number | null
standortZeit: string | null
@@ -142,6 +147,10 @@ export function profilZuEinstellungen(profil: Profil): Einstellungen {
backupIntervall: text(einst, "backup_intervall", "aus"),
letztesBackup: text(einst, "letztes_backup") || null,
ausfuehrung: text(fahrzeug, "ausfuehrung"),
// Der Wert hat die Zeit ueberlebt, in der die Einstellung nicht angeboten
// wurde (31.08. bis 03.09.2026) - deshalb steht er in vielen Profilen
// schon, und der Standard greift nur bei neuen.
pausenzeitMin: zahl(einst, "fahrten_pausenzeit_min") ?? 15,
}
}
@@ -173,6 +182,7 @@ export function profilZuFahrzeug(profil: Profil, status: Fahrzeugstatus): Fahrze
sicherheitscheck: status.sicherheitscheck ?? [],
zuendung: status.zuendung ?? null,
geparktSeit: (status.geparkt_seit as string | null) ?? null,
faehrtSeit: (status.faehrt_seit as string | null) ?? null,
standortLat: status.standort_lat ?? null,
standortLon: status.standort_lon ?? null,
standortZeit: status.standort_zeit ?? null,
@@ -240,6 +250,7 @@ export function zusammenfuehren(
e["backup_intervall"] = einstellungen.backupIntervall
e["nacht_von"] = einstellungen.nachtVon
e["nacht_bis"] = einstellungen.nachtBis
e["fahrten_pausenzeit_min"] = einstellungen.pausenzeitMin
e["smartdeal"] = { aktiv: fahrzeug.smartdeal.aktiv, laeuft_ab: fahrzeug.smartdeal.laeuftAb }
e["oelwechsel_intervall"] = {
modus: fahrzeug.oel.modus,
+1
View File
@@ -28,6 +28,7 @@ function fahrzeug(odo: number, modus = "hersteller"): Fahrzeug {
sicherheitscheck: [],
zuendung: null,
geparktSeit: null,
faehrtSeit: null,
standortLat: null,
standortLon: null,
standortZeit: null,
+20 -1
View File
@@ -6,7 +6,7 @@
import { useEffect, useRef, useState } from "react"
import { ActionButton, Feld, Seg, Sheet, Switch, Tile } from "@audi-dash/ui"
import { ActionButton, Feld, Seg, Sheet, Slider, Switch, Tile } from "@audi-dash/ui"
import { ApiFehler, DIENST_DOMAIN, zugangLesen, zugangVerwerfen } from "../api"
import type { Profil } from "../api"
@@ -348,6 +348,25 @@ export function Einstellungen({
</ActionButton>
</Tile>
<Tile>
<span className="ads-eyebrow">Fahrt beenden</span>
<Slider
min={0}
max={60}
schritt={5}
wert={werte.pausenzeitMin}
onChange={(min) => aendern({ pausenzeitMin: min })}
anzeige={werte.pausenzeitMin === 0 ? "sofort" : werte.pausenzeitMin + " Min."}
aria-label="Minuten bis zum automatischen Fahrtende"
/>
{/* Fusszeile statt Info-Knopf: Apples eigenes Muster fuer erklaerenden
Text unter einer Einstellung ist der Footer der Gruppe. */}
<span className="ads-eyebrow" style={{ marginTop: "14px" }}>
Hier können Sie einstellen, wie viele Minuten nach Ausschalten der Zündung die
Fahrt automatisch beendet werden soll.
</span>
</Tile>
<Tile>
<span className="ads-eyebrow">Darstellung</span>
<Seg
+3 -3
View File
@@ -107,9 +107,9 @@ function dauerSeitText(ts: number | null): string {
/* Fahrzustand aus der Zündung + letzter beendeter Fahrt, deckungsgleich mit
standortZustand() im Panel. */
/* 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 (Kachel „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
/* Seit wann das Fahrzeug steht: aus dem Backend, hergeleitet aus dem
+52 -11
View File
@@ -8,6 +8,8 @@
import { Fig, ProgressBar, StatusRow, Tile } from "@audi-dash/ui"
import { useState } from "react"
import { useDaten } from "../daten/DatenKontext"
import { naechsterService } from "../daten/service"
import { datum, de, deOderStrich, eur, streckeText } from "../format"
@@ -34,10 +36,21 @@ const SERVICE_SYMBOL: Partial<Record<string, typeof SymbolOelwechsel>> = {
export function Uebersicht({ geheZu }: { geheZu: (name: SeitenName, id?: string) => void }) {
const { fahrzeug, einstellungen, fahrten, tankvorgaenge } = useDaten()
const { fahrzeug, einstellungen, fahrten, tankvorgaenge, api, neuLaden } = useDaten()
const [beendenLaeuft, setzeBeendenLaeuft] = useState(false)
if (!fahrzeug || !einstellungen) return null
const fahrtBeenden = async () => {
setzeBeendenLaeuft(true)
try {
await api.fahrtJetztBeenden()
await neuLaden()
} finally {
setzeBeendenLaeuft(false)
}
}
const winterraeder = fahrzeug.reifen.aktiv === "Winter"
/* Der gespeicherte Wert ist der NAME der Ansicht, nicht der Dateiname -
siehe die Begruendung in Einstellungen.tsx. Der Vergleich gegen datei
@@ -233,22 +246,50 @@ export function Uebersicht({ geheZu }: { geheZu: (name: SeitenName, id?: string)
Tankvorgang". Fehlen beide Datensätze, lässt das Panel die Kachel
ganz weg; die Leerzustände hier bleiben als eigene Konvention der
App erhalten (siehe Kapitel 1 des Paritätsaudits). */}
{letzteFahrt || letzteTankung ? (
{fahrzeug.faehrtSeit || letzteFahrt || letzteTankung ? (
<Tile className="dm-teaser">
<span className="ads-eyebrow">Zuletzt</span>
{letzteFahrt && (
{fahrzeug.faehrtSeit ? (
/* Waehrend der Fahrt gibt es weder Strecke noch Verbrauch - also
steht rechts nichts, auch kein Strich. Ein Strich behauptete,
ein Wert fehle; richtig ist, dass es ihn noch nicht gibt. */
<Blattzeile
label="Letzte Fahrt"
labelZusatz={strecke(letzteFahrt) ?? ""}
wert={streckeText(letzteFahrt.distance_km, "—")}
wertZusatz={
letzteFahrt.verbrauch_l_100km != null
? `${de(letzteFahrt.verbrauch_l_100km, 1)} l/100 km`
: "—"
label="Aktuelle Fahrt"
labelZusatz={<span className="dm-laeuft">läuft</span>}
aktion={
<button
type="button"
className="dm-zeilenknopf"
onClick={() => void fahrtBeenden()}
disabled={beendenLaeuft}
>
{beendenLaeuft ? "Wird beendet …" : "Fahrt beenden"}
</button>
}
beiKlick={() => geheZu("trip", letzteFahrt.trip_id)}
letzte={!letzteTankung}
/>
) : (
letzteFahrt && (
<Blattzeile
label="Letzte Fahrt"
labelZusatz={strecke(letzteFahrt) ?? ""}
wert={
(letzteFahrt.ende_vorlaeufig ? "~ " : "") +
streckeText(letzteFahrt.distance_km, "—")
}
wertZusatz={
letzteFahrt.ende_vorlaeufig ? (
<span className="dm-vorlaeufig">vorläufig</span>
) : letzteFahrt.verbrauch_l_100km != null ? (
`${de(letzteFahrt.verbrauch_l_100km, 1)} l/100 km`
) : (
"—"
)
}
beiKlick={() => geheZu("trip", letzteFahrt.trip_id)}
letzte={!letzteTankung}
/>
)
)}
{letzteTankung && (
<Blattzeile
+13 -5
View File
@@ -200,13 +200,18 @@ export function Blattzeile({
labelZusatz,
wert,
wertZusatz,
aktion,
beiKlick,
letzte,
}: {
label: ReactNode
labelZusatz?: ReactNode
wert: ReactNode
/** Ohne Wert entfaellt die Wertespalte ganz - fuer Zeilen, die (noch)
nichts anzuzeigen haben, statt einen Strich zu setzen. */
wert?: ReactNode
wertZusatz?: ReactNode
/** Steht anstelle der Wertespalte, z. B. ein Knopf. */
aktion?: ReactNode
/** Ohne Klickziel wird die Zeile ein reines <div>, wie im Panel. */
beiKlick?: () => void
/** Unterste Zeile einer Kachel — dort entfällt die Trennlinie. */
@@ -219,10 +224,13 @@ export function Blattzeile({
{label}
{labelZusatz !== undefined && <small>{labelZusatz}</small>}
</span>
<span className="dm-blatt__v">
{wert}
{wertZusatz !== undefined && <small>{wertZusatz}</small>}
</span>
{wert !== undefined && (
<span className="dm-blatt__v">
{wert}
{wertZusatz !== undefined && <small>{wertZusatz}</small>}
</span>
)}
{aktion}
{beiKlick && (
<svg className="dm-blatt__chevron" viewBox="0 0 6 10" aria-hidden="true">
<polyline points="1,1 5,5 1,9" />
+27
View File
@@ -1965,3 +1965,30 @@ button.dm-listenzeile {
width: auto;
vertical-align: middle;
}
/* Knopf in einer Listenzeile - heute nur "Fahrt beenden" in der
Zuletzt-Kachel. Zwilling von .zeilenknopf im Panel: 36 px sichtbar,
44 px Trefferhoehe ueber das Pseudoelement, damit die Zeile nicht hoeher
wird als ihre Nachbarin. */
.dm-zeilenknopf {
position: relative;
flex: 0 0 auto;
font-family: inherit;
font-size: 13px;
letter-spacing: .01em;
color: var(--fg);
background: none;
border: 1px solid var(--line-strong);
border-radius: var(--r-pill);
padding: 8px 15px;
min-height: 36px;
cursor: pointer;
}
.dm-zeilenknopf:hover { background: var(--shade); }
.dm-zeilenknopf:active { opacity: .55; }
.dm-zeilenknopf:disabled { opacity: .4; cursor: default; }
.dm-zeilenknopf::before { content: ""; position: absolute; inset: -4px 0; }
/* Gruen wie der Sicherheits-Haken: es laeuft gerade etwas. */
.dm-blatt__k small .dm-laeuft, .dm-laeuft { color: var(--ok); }
.dm-vorlaeufig { color: var(--fg3); }