companion-app: catch up six days of panel drift before the native build

The phone-app codebase (companion-app/) hadn't been touched since
2026-08-11 - every panel fix and feature since then (HIG audit rounds,
receipt upload, trip editing, Inspektion forecast, ...) existed only in
the HA panel. Found while auditing what's needed for a real iPhone
build; user decision was to port everything now rather than ship a
stale app.

Six real, verified gaps (not blind copies of panel CSS/markup, which
doesn't transfer to the @audi-dash/ui component set):

- Battery voltage cutoff was still 13.2V, not the panel's 12.8V fix.
- Dead wlan_name field (WLAN trip detection was fully removed from the
  backend 2026-08-12) - removed from the adapter and the settings screen
  instead of leaving a form field that silently does nothing.
- No Inspektion forecast - added inspektionPrognose() alongside the
  existing oelwechselPrognose(), sharing a refactored core.
- Arbeitsweg pill used the design-system's "work" variant, which is
  documented as recoloring to red - stopped passing it, same fix as the
  panel.
- Trip creation only took Beginn/Ende/Art; extended with Startort/
  Zielort/Kilometerstand/Distanz via a new shared FahrtFelder.tsx.
- FahrtDetail.tsx was read-only - added an edit mode using the same
  shared fields, backed by a new DataMetricApi.fahrtAktualisieren()
  calling the backend service built earlier this session.

Explicitly checked and found not applicable: price rounding (already
2 decimals here), pull-to-refresh CSS (no native gesture to fix),
the panel's drag/paste receipt dialog (solves a desktop-browser problem
this native app doesn't have - the plain file picker already gets iOS's
native Files integration), and the purely cosmetic panel CSS fixes.

npm install run at the repo root (node_modules was incomplete/stale),
package-lock.json reflects the real dependency tree. Verified with
npm run typecheck (clean), npm run test (95/95, up from 90 - added
interaction tests for the new form and inspektionPrognose), and
npm run build (succeeds).

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-08-17 12:16:40 +02:00
co-authored by Claude Opus 5
parent 188dc8247e
commit edf3845375
16 changed files with 442 additions and 94 deletions
+52 -26
View File
@@ -1,13 +1,14 @@
/**
* Eigene Ölwechsel-Prognose. Portiert aus `oelwechselPrognose()` im alten
* Panel.
* Eigene Service-Prognosen (Ölwechsel, Inspektion). Portiert aus
* `oelwechselPrognose()` im alten Panel; die Inspektion kam am 2026-08-17
* hinzu (§ AGENTS.md, "Inspektion: voraussichtliches Datum ergänzen").
*
* Die Rechnung beginnt bewusst neu ab dem letzten tatsächlich durchgeführten
* Ölwechsel — der allgemeine Langzeitdurchschnitt seit dem allerersten
* Servicebucheintrag wäre dafür zu träge. Aus km und Tagen seit dem letzten
* Wechsel ergibt sich die aktuelle Fahrleistung pro Tag; hochgerechnet auf die
* verbleibenden km bis zum eingestellten Intervall ergibt das ein Datum,
* gedeckelt auf spätestens das Zeitlimit des Intervalls.
* Eintrag dieser Art — der allgemeine Langzeitdurchschnitt seit dem
* allerersten Servicebucheintrag wäre dafür zu träge. Aus km und Tagen seit
* dem letzten Eintrag ergibt sich die aktuelle Fahrleistung pro Tag;
* hochgerechnet auf die verbleibenden km bis zum Intervall ergibt das ein
* Datum, gedeckelt auf spätestens das Zeitlimit des Intervalls.
*/
import type { Fahrzeug } from "./profilAdapter"
@@ -36,33 +37,31 @@ function monatePlus(isoDatum: string, monate: number): string {
return d.toISOString().slice(0, 10)
}
/** Der jüngste Servicebucheintrag, dessen Art auf einen Ölwechsel hindeutet. */
export function letzterOelwechsel(buch: readonly Buchhaltung[]): Buchhaltung | null {
/** Der jüngste Servicebucheintrag, dessen Art auf das gesuchte Stichwort
passt (z. B. "öl"/"oel" oder "inspektion"). */
function letzterEintrag(buch: readonly Buchhaltung[], stichwort: RegExp): Buchhaltung | null {
const treffer = buch
.filter((e) => typeof e.art === "string" && /öl|oel/i.test(e.art) && e.datum && e.km != null)
.filter((e) => typeof e.art === "string" && stichwort.test(e.art) && e.datum && e.km != null)
.sort((a, b) => String(b.datum).localeCompare(String(a.datum)))
return treffer[0] ?? null
}
export function oelwechselPrognose(
/** Der jüngste Servicebucheintrag, dessen Art auf einen Ölwechsel hindeutet. */
export function letzterOelwechsel(buch: readonly Buchhaltung[]): Buchhaltung | null {
return letzterEintrag(buch, /öl|oel/i)
}
/** Gemeinsamer Kern für Ölwechsel- und Inspektionsprognose - beide rechnen
identisch, nur mit anderem Intervall und anderer Suche im Servicebuch. */
function servicePrognose(
fahrzeug: Fahrzeug,
buch: readonly Buchhaltung[],
jetzt = new Date(),
letzter: Buchhaltung | null,
intervallKm: number,
intervallMonate: number,
jetzt: Date,
): Oelprognose | null {
if (!fahrzeug.odoBekannt) return null
const letzter = letzterOelwechsel(buch)
if (!letzter?.datum || letzter.km == null) return null
const oel = fahrzeug.oel
const intervallKm =
oel.modus === "hersteller"
? (oel.herstellerKm ?? 0)
: Math.min(oel.km ?? Infinity, oel.herstellerKm ?? Infinity)
const intervallMonate =
oel.modus === "hersteller"
? (oel.herstellerMonate ?? 0)
: Math.min(oel.monate ?? Infinity, oel.herstellerMonate ?? Infinity)
if (!Number.isFinite(intervallKm) || intervallKm <= 0) return null
const zielKm = letzter.km + intervallKm
@@ -76,7 +75,7 @@ export function oelwechselPrognose(
const tageSeit = (jetzt.getTime() - new Date(letzter.datum).getTime()) / TAG_MS
const kmSeit = fahrzeug.odo - letzter.km
// Ohne belastbare Fahrleistung (direkt nach dem Wechsel) gibt es keine
// Ohne belastbare Fahrleistung (direkt nach dem Eintrag) gibt es keine
// seriöse Kilometerprognose — dann gilt das Zeitlimit.
if (tageSeit <= 0 || kmSeit <= 0) {
return { datum: kappDatum, restKm, kmProTag: 0, durchZeitlimit: true }
@@ -94,3 +93,30 @@ export function oelwechselPrognose(
durchZeitlimit,
}
}
export function oelwechselPrognose(
fahrzeug: Fahrzeug,
buch: readonly Buchhaltung[],
jetzt = new Date(),
): Oelprognose | null {
const oel = fahrzeug.oel
const intervallKm =
oel.modus === "hersteller"
? (oel.herstellerKm ?? 0)
: Math.min(oel.km ?? Infinity, oel.herstellerKm ?? Infinity)
const intervallMonate =
oel.modus === "hersteller"
? (oel.herstellerMonate ?? 0)
: Math.min(oel.monate ?? Infinity, oel.herstellerMonate ?? Infinity)
return servicePrognose(fahrzeug, letzterOelwechsel(buch), intervallKm, intervallMonate, jetzt)
}
/** Inspektion: feste 30.000 km / 24 Monate, wie auch das Fahrzeug selbst und
das alte Panel rechnen - kein einstellbares Intervall wie beim Ölwechsel. */
export function inspektionPrognose(
fahrzeug: Fahrzeug,
buch: readonly Buchhaltung[],
jetzt = new Date(),
): Oelprognose | null {
return servicePrognose(fahrzeug, letzterEintrag(buch, /inspektion/i), 30000, 24, jetzt)
}