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:
@@ -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,
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -28,6 +28,7 @@ function fahrzeug(odo: number, modus = "hersteller"): Fahrzeug {
|
||||
sicherheitscheck: [],
|
||||
zuendung: null,
|
||||
geparktSeit: null,
|
||||
faehrtSeit: null,
|
||||
standortLat: null,
|
||||
standortLon: null,
|
||||
standortZeit: null,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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); }
|
||||
|
||||
Reference in New Issue
Block a user