Vollständiger Panel-companion-app-Paritätsaudit (11 Kapitel)

Systematischer Bildvergleich der companion-app gegen das HA-Panel (companion-app/AGENTS.md
Kapitel 1-11): Farbtoken/Radien aus dem falschen Stylesheet gelesen (iOS-Overlay statt Basis-CSS,
betraf fast jede Kachel/Farbe/Radius app-weit), vier app-weite @audi-dash/ui-Bugs (Switch/Seg/Feld
rot statt neutral bzw. falsche Feldbreite), Reifen-Seite strukturell neu gebaut (beide Radsätze
gleichzeitig statt Umschalter, editierbare Felder, Anzugsmoment-/km-Korrektur), Standort-Feature
komplett neu (fehlte bisher ganz), sowie diverse Struktur-/Typografie-/Datenlücken in
MeinAudi/Service/Versicherung/Sicherheit/Fahrten/Tanken/Statistik/Batterie/Einstellungen.

Manifest auf 2026.8.30.2 angehoben, OTA-Bündel neu gebaut und in audi_ha_test verifiziert
(sauberer Neustart, keine Tracebacks).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
2026-08-30 12:21:58 +02:00
co-authored by Claude Sonnet 5
parent 4fa5d4bc2d
commit e1ebe6d694
43 changed files with 3256 additions and 451 deletions
+178 -4
View File
@@ -9,12 +9,13 @@
import { useEffect, useRef, useState } from "react"
import { ActionButton, Pill, Tile } from "@audi-dash/ui"
import { ActionButton, Feld, Pill, Tile } from "@audi-dash/ui"
import type { Tankvorgang } from "../api"
import { ENTITAETEN } from "../api"
import { useDaten } from "../daten/DatenKontext"
import { datum, datumZeit, de, deOderStrich, eur } from "../format"
import { Leerzustand, Wertzeile, Werteliste } from "./bausteine"
import { KRAFTSTOFFSORTEN, Leerzustand, Wertzeile, Werteliste } from "./bausteine"
interface BelegErgebnis {
ok?: boolean
@@ -27,6 +28,7 @@ interface BelegErgebnis {
export function TankDetail({ id }: { id: string | undefined }) {
const { tankvorgaenge, api } = useDaten()
const tank = tankvorgaenge.find((t) => t.tank_id === id)
const [bearbeitenOffen, setzeBearbeitenOffen] = useState(false)
if (!tank) {
return (
@@ -37,12 +39,21 @@ export function TankDetail({ id }: { id: string | undefined }) {
)
}
if (bearbeitenOffen) {
return <TankBearbeiten tank={tank} beiFertig={() => setzeBearbeitenOffen(false)} />
}
return (
<>
<Tile>
<span className="ads-eyebrow">{datumZeit(tank.ts)}</span>
<div className="dm-abschnitt" style={{ marginBottom: 0 }}>
<span className="ads-eyebrow">{datumZeit(tank.ts)}</span>
<button type="button" className="dm-textknopf" onClick={() => setzeBearbeitenOffen(true)}>
Bearbeiten
</button>
</div>
<div className="dm-detail__kopf">
<div className="dm-detail__zahl">
<div className="dm-detail__zahl dm-detail__zahl--gross">
{deOderStrich(tank.liters, 2)}
<span className="dm-detail__einheit">l</span>
</div>
@@ -92,6 +103,169 @@ export function TankDetail({ id }: { id: string | undefined }) {
)
}
/** "YYYY-MM-DDTHH:mm" in Ortszeit für ein <input type="datetime-local">. */
function alsDatetimeLocal(iso: string): string {
const d = new Date(iso)
if (Number.isNaN(d.getTime())) return ""
const versatz = d.getTimezoneOffset() * 60_000
return new Date(d.getTime() - versatz).toISOString().slice(0, 16)
}
/** Bearbeiten-Formular für einen bestehenden Tankvorgang - dieselben Felder
wie tankFelder() im Panel (Zeitpunkt, Kilometerstand, Distanz, Kraftstoff,
Liter, Kosten, SmartDeal-Ersparnis, Tankstelle). Fehlte hier bisher
vollständig - der einzige Weg, einen Wert zu korrigieren, war ein neuer
Beleg-Upload. */
function TankBearbeiten({ tank, beiFertig }: { tank: Tankvorgang; beiFertig: () => void }) {
const { api, fahrzeug } = useDaten()
const [zeitpunkt, setzeZeitpunkt] = useState(alsDatetimeLocal(tank.ts))
const [km, setzeKm] = useState(tank.odometer_km != null ? String(tank.odometer_km) : "")
const [distanz, setzeDistanz] = useState(tank.distance_km != null ? String(tank.distance_km) : "")
const [kraftstoff, setzeKraftstoff] = useState(tank.fuel_type ?? KRAFTSTOFFSORTEN[0])
const [liter, setzeLiter] = useState(tank.liters != null ? String(tank.liters) : "")
const [kosten, setzeKosten] = useState(tank.fuel_total_eur != null ? String(tank.fuel_total_eur) : "")
const [ersparnis, setzeErsparnis] = useState(tank.discount != null ? String(tank.discount) : "")
const [station, setzeStation] = useState(tank.station_name ?? "")
const [fehler, setzeFehler] = useState<string | null>(null)
const [laeuft, setzeLaeuft] = useState(false)
const smartdealAktiv = fahrzeug?.smartdeal.aktiv === true
const speichern = async () => {
setzeFehler(null)
const literZahl = Number(liter.replace(",", "."))
const kostenZahl = Number(kosten.replace(",", "."))
if (!Number.isFinite(literZahl) || literZahl <= 0) {
setzeFehler("Bitte die getankte Menge in Litern angeben.")
return
}
if (!Number.isFinite(kostenZahl) || kostenZahl <= 0) {
setzeFehler("Bitte den Gesamtbetrag angeben.")
return
}
setzeLaeuft(true)
try {
const kmZahl = Number(km.replace(",", "."))
const distanzZahl = Number(distanz.replace(",", "."))
const ersparnisZahl = Number(ersparnis.replace(",", "."))
await api.tankvorgangAktualisieren(tank.tank_id, {
ts: zeitpunkt ? new Date(zeitpunkt).toISOString() : tank.ts,
liter: literZahl,
kosten: kostenZahl,
kraftstoff,
station: station || null,
km: Number.isFinite(kmZahl) && km ? kmZahl : null,
distanz: Number.isFinite(distanzZahl) && distanz ? distanzZahl : null,
...(smartdealAktiv
? { ersparnis: Number.isFinite(ersparnisZahl) && ersparnis ? ersparnisZahl : null }
: {}),
})
beiFertig()
} catch (ursache) {
setzeFehler(ursache instanceof Error ? ursache.message : String(ursache))
} finally {
setzeLaeuft(false)
}
}
return (
<Tile>
<div className="dm-abschnitt" style={{ marginBottom: 0 }}>
<span className="ads-eyebrow">Tankvorgang bearbeiten</span>
<button type="button" className="dm-textknopf" onClick={beiFertig} disabled={laeuft}>
Abbrechen
</button>
</div>
<Feld label="Zeitpunkt">
<input
className="dm-eingabe"
type="datetime-local"
value={zeitpunkt}
onChange={(e) => setzeZeitpunkt(e.target.value)}
/>
</Feld>
<Feld label="Kilometerstand" unit="km">
<input
className="dm-eingabe"
inputMode="numeric"
placeholder="wird ergänzt"
value={km}
onChange={(e) => setzeKm(e.target.value)}
/>
</Feld>
<Feld label="Gefahrene Distanz" unit="km">
<input
className="dm-eingabe"
inputMode="decimal"
placeholder="aus km-Stand"
value={distanz}
onChange={(e) => setzeDistanz(e.target.value)}
/>
</Feld>
<Feld label="Kraftstoff">
<select
className="dm-auswahl"
value={kraftstoff}
onChange={(e) => setzeKraftstoff(e.target.value)}
>
{KRAFTSTOFFSORTEN.map((sorte) => (
<option key={sorte} value={sorte}>
{sorte}
</option>
))}
</select>
</Feld>
<Feld label="Getankte Liter" unit="l">
<input
className="dm-eingabe"
inputMode="decimal"
placeholder="0,00"
value={liter}
onChange={(e) => setzeLiter(e.target.value)}
/>
</Feld>
<Feld label="Kosten (real, nach Rabatt)" unit="€">
<input
className="dm-eingabe"
inputMode="decimal"
placeholder="0,00"
value={kosten}
onChange={(e) => setzeKosten(e.target.value)}
/>
</Feld>
{smartdealAktiv && (
<Feld label="SmartDeal-Ersparnis" unit="€">
<input
className="dm-eingabe"
inputMode="decimal"
placeholder="0,00"
value={ersparnis}
onChange={(e) => setzeErsparnis(e.target.value)}
/>
</Feld>
)}
<Feld label="Tankstelle" last>
<input
className="dm-eingabe"
placeholder="Name"
value={station}
onChange={(e) => setzeStation(e.target.value)}
/>
</Feld>
{fehler && (
<p className="dm-fehler" role="alert">
{fehler}
</p>
)}
<div style={{ marginTop: 14 }}>
<ActionButton onClick={() => void speichern()} disabled={laeuft}>
{laeuft ? "Speichere …" : "Speichern"}
</ActionButton>
</div>
</Tile>
)
}
function BelegBereich({
tankId,
belegDatei,