diff --git a/AGENTS.md b/AGENTS.md index 7dd636a..2c18faa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2942,6 +2942,150 @@ confirmed mechanism the pending Sicherheit plan already documented. `screening.p `TankDetail.tsx` were deliberately left untouched: the estimate flows through existing display/edit paths without needing either to change. +## R. Five owner-requested features in one round: battery measurement list, swipe-delete, diagram-width fix, +CI icon substitutions, wheel-set archiving (2026.8.25.3/.4) + +**1. Battery measurement list.** The battery-voltage tile only ever showed a hand-drawn chart. Added a +"list" corner button (CI `list-s`) that opens a new list view — one row per day (the existing daily +min/max data, unchanged storage shape) showing date, time, voltage, and now also outside air temperature. +Backend: new optional `AUSSENTEMP_SENSOR` role (`einstellungen.py`); `batterie.py`'s 5-minute check now also +reads it and passes it to `ablage.batterieverlauf_tageswert_aktualisieren()`, which gained an `aussentemp` +parameter — stored as `min_temp_c`, **only updated when a measurement also sets that day's minimum voltage** +(the temperature is tied to that specific reading, not a daily average). Older entries and setups without +the sensor simply carry `min_temp_c: None` — no backfill, no migration. New screens: companion-app +`BatterieListe.tsx` (route `battliste`, reached from an `IconButton`/`SymbolListe` in `Batterie.tsx`'s +header) and panel `vBatterieliste()` (route `battliste`, reached via a new `.iconbtn`/`ciSVG(CI.listS)` +button in `vBatterieverlauf()`'s tile header). + +**2. Swipe-to-delete for the new list, per entry.** Reused the existing pattern in both codebases rather +than inventing a new one: companion-app's `Zeilenmenue` (wipe + accessible menu button, see its own +docstring) and the panel's `swipeZeile()`/`eintragLoeschen()`/`LOESCH_TEXT`/`LOESCH_HINWEIS` machinery +already used for trips/tank vorgaenge, extended with a `"batt"` art. New backend service +`batterieverlauf_loeschen` (field: `datum`) → `ablage.batterieverlauf_eintrag_loeschen()` (new) → +`batterie.eintrag_loeschen()` (new, wired in `dienste.py`). + +**3. Battery diagram "still stretched" on a large monitor — panel only (confirmed with the owner; the +diagram in question doesn't exist as a separate wide layout in companion-app).** Root cause found in +`audi-dashboard-ios.css`: past 860px viewport width the whole app switches to a wide two-column layout +(`@container (min-width:860px)`) where `main#view`'s content column is 772px wide (860 − 2×44px side +padding). The battery chart's SVG (`#bvSvg`, `audi-dashboard-app.js`) is drawn on a fixed `viewBox="0 0 320 +170"` with `preserveAspectRatio="none"` and `width:100%` — at 772px wide against a native 320-unit design, +that's a ~2.4× horizontal stretch with the height staying fixed at 170px, turning the chart into an +unnaturally wide, flat band. Fixed the same way the existing popups in that file already solve the identical +problem one section up: `#bvChart{max-width:400px;margin-left:auto;margin-right:auto}` inside the same +`@container` block, no changes to the SVG's own drawing logic (`BV_W`/`BV_H`/`bvZeichnen()`), which section +O of this file already spent real effort getting right for a *different* stretch bug (the Y-axis +rescaling one) — deliberately left untouched here. + +**4. CI icon substitutions — `list-l/-s`, `polls-l/-s`, `package-l/-s`, all requested by the owner +(SVG source supplied directly from their local Audi icon export folder, same as every other CI icon in this +codebase — see the recurring "vom Nutzer als SVG geliefert, verbatim uebernommen" comment convention).** +Added to the `CI` catalog in `audi-dashboard-app.js`. `polls-s` replaces the Statistik tab's hand-drawn bar +icon (`ICONS.stat`) in the panel's tab bar — needed adding `"stat"` to `ICONS_VOLL` (the existing mechanism +for filled-vs-outline icon rendering; CI paths carry their own `fill="currentColor"` but would otherwise +also inherit the tab bar's `stroke:currentColor` from `.tab svg`, doubling the line weight around the +shape). Companion-app's `symbole.tsx` had no filled-icon mechanism at all yet (its `SymbolAudi` etc. are +plain stroked outlines, unlike the panel's CI-icon-based tab bar) — added a `voll` prop to the shared +`Rahmen` wrapper (`fill`/`stroke` swap) as the equivalent mechanism, then `SymbolStatistik` (parity with +the panel), plus `SymbolListe`/`SymbolPaket` for the other two icon uses below. `list-s`/`package-s` are the +corner/inline buttons described in points 1 and 5; the `-l` (48-grid) variants are stored for parity with +every other CI icon pair in the catalog but aren't wired to a render path yet, same as the pre-existing +`editL`/`oilChangeL`/etc. + +**5. Wheel-set archiving ("Neue Räder anlegen").** A `package-s` button sits between the wheel photo and +the editable Hersteller/Modell fields on the Reifen page in both codebases (panel: inline always-editable +`data-rf` inputs, matching that page's existing convention; companion-app: read-only `Wertzeile`s with a +separate edit form, matching *its* existing convention — no new interaction pattern introduced in either). +Pressing it asks "Neue Räder anlegen?" (panel: the existing `bestaetigen()` action sheet; companion-app: the +existing `window.confirm`-based `bestaetigen()` already used by `wechseln()` on the same page) before doing +anything. On confirmation, `reifen.py`'s new `archivieren(k, satz)`: snapshots the season's *entire* current +record (km, marke, modell, dot, mass, druck_vorne/hinten, kommentar, plus a generated id and today's date as +`ersetzt_am`) into `reifen.archiv` (newest first), then resets the live record — **km to 0 (explicit owner +decision) and marke/modell/dot/kommentar to blank** (a replacement tire hasn't been characterized yet), +**but mass/druck_vorne/druck_hinten/bild carried over** (size and target pressure don't usually change for +a same-fitment replacement — a judgment call, not something the owner specified; flag if wrong). Three new +services (`reifen_archivieren`, `reifen_archiv_aktualisieren`, `reifen_archiv_loeschen`), all deliberately +**bypassing `profilSchreiben`/`profilSpeichern`** for the same reason `reifen_wechseln`/`reifen_km_setzen` +already do (see `reifenzaehler.py`'s own docstring) — a full-profile round trip from a stale browser copy +could clobber a km value the backend fortgeschrieben in the meantime. + +Overview UI: an "Archiv" tile at the bottom of the Reifen page, one row per archived set. Companion-app uses +the design system's `Accordion` (title = "Sommerräder"/"Winterräder", summary = "Ersetzt am ... · N km"), +expanding to a read-only `Werteliste` with a "Bearbeiten" button that swaps in an edit form (mirrors +`TankFormular`'s style) — plus a destructive "Löschen" button. The panel reuses its own existing +`acc-h`/`acc-b`/`data-acc` accordion mechanism (same as the trips/tankvorgaenge year/month lists) with +always-inline-editable fields (`data-rfa="id|feld"`, parallel to `data-rf`) that call +`reifen_archiv_aktualisieren` directly on change, plus a `data-reifenarchivloeschen` button. +`profilAdapter.ts` gained a `ReifenArchivEintrag` type and `Fahrzeug.reifen.archiv`; `profilZuFahrzeug()` +reads it straight off the profile (`?? []`), and `zusammenfuehren()` needed no change — it clones the raw +profile first and only ever touches `aktiv`/`anzugsmoment_nm`/`wechsel`/`saetze`, so `archiv` survives +untouched exactly like it already did for every other field this function doesn't know about. + +Verified: `py_compile` clean on all six touched backend files (`einstellungen.py`, `ablage.py`, +`batterie.py`, `const.py`, `dienste.py`, `reifen.py`) plus `services.yaml` entries added for all four new +services; `node --check` clean on the panel JS; companion-app `tsc --noEmit` clean and full suite green at +**146/146** (one new test — the existing "every registered page renders with sample data" smoke test picked +up `battliste` automatically); `npm run build` and `npm run ota` both succeeded (OTA bundle rebuilt at +`2026.8.25.4`, sha256 `9d5ebc2a62da4bbddcd766a462726bb51a213ea4095e367e88a8e5d321a9f8e4`). Manifest bumped +`2026.8.25.2` → `.3` (backend) → `.4` (frontend + OTA), both restarts in `audi_ha_test` confirmed clean via +`"Audi Dashboard ... eingerichtet"` with no tracebacks, including the periodic battery-check log line +firing without error against the new `AUSSENTEMP_SENSOR` code path. **Not verified live in a browser** — +the setup-menu entry for `AUSSENTEMP_SENSOR`, the new list screens, the icon substitutions' actual pixel +rendering, and the archive accordion/edit flow were none of them clicked through in this round (a +browser-tooling networking issue blocked reaching the test instance's UI, same blocker noted in section Q). +This entire round rests on static analysis, compiler/test-suite checks, and clean service-registration logs +— not on having seen any of it render. + +## S. Live browser access to the test instance, a "Generatorspannung" remark, and two real bugs section R's +static-only verification missed (2026.8.25.5–.8) + +**The owner logged this session into the test instance's browser at `http://localhost:18123` (not the +`:8123` used earlier - a different, working port for this environment).** This unblocked live verification +for the first time since the FMM003 investigation's `/profile/security` refusal (section P) — worth noting +for future sessions: try `:18123` before assuming the browser tooling can't reach this project's test +container. + +**Generator-voltage remark (2026.8.25.5):** `AGM_RUHE_MAX_V = 12.8` (the resting-vs-alternator-voltage +threshold, already used by the diagram's own point filter and the SOC estimate) was not applied to the new +measurement list from section R — a day whose minimum was never actually measured at rest still displayed +next to a date/time as if it were one. Companion-app: exported the constant from `Batterie.tsx`, added a +`Pill` reading "Generatorspannung" in `BatterieListe.tsx` when `min > AGM_RUHE_MAX_V`. Panel: same threshold +already in scope in `vBatterieliste()`, added a `.pill`-styled badge under the date in the `.v` column. + +**Live verification then immediately found two real defects that every static check in section R had +missed** — `py_compile`/`tsc`/`node --check`/the test suite all passed because none of them render CSS: + +1. **Archive edit fields visually truncated ("Continenta", "SportConta", "275/30 R2").** `vReifenArchiv()`'s + `feld()` helper unconditionally wrapped every input in `` — a class meant for + short number+unit pairs (km, Nm, bar) that hard-codes `input{width:96px}`. None of the fields that + actually used this helper (marke/modell/mass/dot/kommentar) needed a unit at all, so the wrapper was pure + accident, not a deliberate width choice. Fixed by dropping `mitEinheit` from that helper entirely, + matching the plain `` the live (non-archived) season fields already use one screen up. Confirmed + live: all three previously-clipped fields now render in full. +2. **Owner follow-up, also confirmed live before and after:** the archive accordion's collapsed summary + line squeezed `4.542 km` into a `` immediately after the "Ersetzt am" date, and separately asked + for the Kommentar/Notiz field specifically to get more room. Both fixed the same way in both codebases: + Kommentar/Notiz now renders label-above-input at full width (`.feld--breit` in the panel CSS; a + bypassed-`Feld` block in companion-app) instead of sharing a row with its label like every other short + field — and the collapsed summary now shows the km figure at title size/color (`.acc-h .k`/ + `.ads-acc-h__k`-equivalent styling) with the date dropped to its own smaller line below, instead of the + two being run together. + +**Lesson for this project going forward, worth remembering deliberately:** a clean compile/test pass is not +evidence a layout is correct — none of the tooling in this repo renders CSS or checks visual width. Section +R shipped a real, user-visible truncation bug behind a fully green test suite. When browser access is +available (now confirmed reachable at `:18123`), prefer at least one live screenshot of any new or changed +input-heavy screen over relying on compiler output alone. + +Verified: panel `node --check` clean at every step; companion-app `tsc --noEmit` clean, suite green at +146/146, `npm run build`/`npm run ota` succeeded. Manifest `2026.8.25.4` → `.5` (Generatorspannung) → `.6` +(mitEinheit fix) → `.7` (Kommentar width + archive header restyle, panel) → `.8` (same two, companion-app +parity + final OTA rebuild, sha256 `464c294c6c07dcb636a8ea3bd93c7e525b21722defb41f573e2b31fefade49ea`). +Every restart in `audi_ha_test` confirmed clean via log, **and this time also confirmed live in the browser** +at each step from `.6` onward — screenshots taken of the battery list (Generatorspannung pill rendering +correctly, including a real entry below the threshold correctly *not* tagged), the Statistik tab's `polls-s` +icon, and the Reifen archive section before and after both fixes. + --- ## Working conventions (observed — keep them) diff --git a/companion-app/src/api/index.ts b/companion-app/src/api/index.ts index 2219490..a592bfc 100644 --- a/companion-app/src/api/index.ts +++ b/companion-app/src/api/index.ts @@ -44,6 +44,20 @@ export interface TankvorgangFelder { fahrt_manuell_anlegen/fahrt_aktualisieren). Alles außer den Zeitpunkten ist optional: leer bleibt es liegen, bis das Kilometerstand-Screening (§7.2) oder eine spätere Bearbeitung es füllt. */ +/** Eingabefelder für einen archivierten Reifensatz - Namen wie beim Backend + (reifen.py: reifen_archiv_aktualisieren). */ +export interface ReifenArchivFelder { + ersetzt_am?: string; + km?: number | null; + marke?: string | null; + modell?: string | null; + dot?: string | null; + mass?: string | null; + druck_vorne?: string | null; + druck_hinten?: string | null; + kommentar?: string | null; +} + export interface FahrtFelder { ts_start?: string; ts_end?: string; @@ -174,6 +188,45 @@ export class DataMetricApi { ); } + /** Löscht einen Tageseintrag aus der Batteriespannungs-Messwertliste. */ + batterieverlaufEintragLoeschen(datum: string): Promise { + return this.warteschlange.einreihen( + DIENST_DOMAIN, + "batterieverlauf_loeschen", + { datum }, + "Messwert löschen", + ); + } + + /** "Neue Räder anlegen": archiviert den aktuellen Stand von `satz` und legt + einen frischen (km 0) an. */ + reifenArchivieren(satz: "sommer" | "winter"): Promise { + return this.warteschlange.einreihen( + DIENST_DOMAIN, + "reifen_archivieren", + { satz }, + "Neue Räder anlegen", + ); + } + + reifenArchivAktualisieren(id: string, felder: ReifenArchivFelder): Promise { + return this.warteschlange.einreihen( + DIENST_DOMAIN, + "reifen_archiv_aktualisieren", + { id, ...felder }, + "Archivierten Reifensatz ändern", + ); + } + + reifenArchivLoeschen(id: string): Promise { + return this.warteschlange.einreihen( + DIENST_DOMAIN, + "reifen_archiv_loeschen", + { id }, + "Archivierten Reifensatz löschen", + ); + } + /** Stößt eine sofortige Aktualisierung im Backend an (Pull-to-refresh). */ jetztAktualisieren(): Promise { return this.rest.dienstAufrufen(DIENST_DOMAIN, "jetzt_aktualisieren"); diff --git a/companion-app/src/daten/profilAdapter.ts b/companion-app/src/daten/profilAdapter.ts index 2f41692..839f992 100644 --- a/companion-app/src/daten/profilAdapter.ts +++ b/companion-app/src/daten/profilAdapter.ts @@ -36,6 +36,22 @@ export interface Reifensatz { [weitere: string]: unknown } +/** Ein per "Neue Räder anlegen" archivierter Reifensatz (reifen.py + archivieren()) - der vollständige Stand zum Zeitpunkt des Austauschs. */ +export interface ReifenArchivEintrag { + id: string + satz: "sommer" | "winter" + ersetzt_am: string + km: number | null + marke: string | null + modell: string | null + dot: string | null + mass: string | null + druck_vorne: string | null + druck_hinten: string | null + kommentar: string | null +} + export interface Fahrzeug { fin: string details: string @@ -74,6 +90,8 @@ export interface Fahrzeug { wechsel: Record sommer: Reifensatz winter: Reifensatz + /** Neueste zuerst - schon so im Profil (reifen.py archivieren()). */ + archiv: ReifenArchivEintrag[] } service: Record technik: Record @@ -179,6 +197,7 @@ export function profilZuFahrzeug(profil: Profil, status: Fahrzeugstatus): Fahrze wechsel: (reifen["wechsel"] ?? {}) as Record, sommer: saetze["sommer"] ?? {}, winter: saetze["winter"] ?? {}, + archiv: (reifen["archiv"] as ReifenArchivEintrag[] | undefined) ?? [], }, service: abschnitt(profil, "service"), technik: abschnitt(profil, "technik"), diff --git a/companion-app/src/daten/service.test.ts b/companion-app/src/daten/service.test.ts index 544cf9a..b1feb69 100644 --- a/companion-app/src/daten/service.test.ts +++ b/companion-app/src/daten/service.test.ts @@ -37,7 +37,7 @@ function fahrzeug(odo: number, modus = "hersteller"): Fahrzeug { oel: { modus, km: 15000, monate: 12, herstellerKm: 30000, herstellerMonate: 24 }, versicherung: {}, steuer: { faellig: "" }, - reifen: { aktiv: "Sommer", anzugsmoment: null, wechsel: {}, sommer: {}, winter: {} }, + reifen: { aktiv: "Sommer", anzugsmoment: null, wechsel: {}, sommer: {}, winter: {}, archiv: [] }, service: {}, technik: {}, ausstattung: [], diff --git a/companion-app/src/navigation.ts b/companion-app/src/navigation.ts index bde607d..4d0d30a 100644 --- a/companion-app/src/navigation.ts +++ b/companion-app/src/navigation.ts @@ -17,6 +17,7 @@ export type SeitenName = // Unterseiten von "Mein Audi" | "ident" | "battverlauf" + | "battliste" | "service" | "werkstatt" | "sbuch" @@ -54,6 +55,7 @@ export const ZURUECK: Partial> = { reifen: "audi", ident: "audi", battverlauf: "audi", + battliste: "battverlauf", vers: "audi", beitrag: "vers", vertrag: "vers", @@ -78,6 +80,7 @@ export const TITEL: Record = { fuel: "Tanken", ident: "Fahrzeugdaten", battverlauf: "Batterie", + battliste: "Messwerte", service: "Service", werkstatt: "Werkstatt", sbuch: "Servicebuch", diff --git a/companion-app/src/screens/Batterie.tsx b/companion-app/src/screens/Batterie.tsx index 5518454..9f9a5e9 100644 --- a/companion-app/src/screens/Batterie.tsx +++ b/companion-app/src/screens/Batterie.tsx @@ -13,11 +13,13 @@ import { useEffect, useState } from "react" -import { Tile } from "@audi-dash/ui" +import { IconButton, Tile } from "@audi-dash/ui" import { ENTITAETEN } from "../api" import { useDaten } from "../daten/DatenKontext" import { datum, de } from "../format" +import type { SeitenName } from "../navigation" +import { SymbolListe } from "../symbole" import { Leerzustand, Wertzeile, Werteliste } from "./bausteine" interface Tageswert { @@ -29,8 +31,10 @@ interface Tageswert { } /** Ab hier gilt eine Messung als nicht mehr in Ruhe aufgenommen - alles - darüber ist schon Lichtmaschinenspannung, nicht mehr Ruhespannung. */ -const AGM_RUHE_MAX_V = 12.8 + darüber ist schon Lichtmaschinenspannung, nicht mehr Ruhespannung. Auch + von BatterieListe.tsx verwendet, für die "Generatorspannung"-Kennzeichnung + dort - eine einzige Schwelle für beide Ansichten. */ +export const AGM_RUHE_MAX_V = 12.8 /** Spannung → Ladezustand für eine AGM-Batterie in Ruhe. */ const LADEKURVE: [number, number][] = [ @@ -49,7 +53,7 @@ function ladezustand(spannung: number): number | null { return 0 } -export function Batterie() { +export function Batterie({ geheZu }: { geheZu: (name: SeitenName) => void }) { const { api, fahrzeug } = useDaten() const [verlauf, setzeVerlauf] = useState(null) const [fehler, setzeFehler] = useState(false) @@ -97,7 +101,12 @@ export function Batterie() { return ( <> - Ruhespannung +
+ Ruhespannung + geheZu("battliste")}> + + +
{letzter.min != null ? de(letzter.min, 2) : "—"} diff --git a/companion-app/src/screens/BatterieListe.tsx b/companion-app/src/screens/BatterieListe.tsx new file mode 100644 index 0000000..38b936c --- /dev/null +++ b/companion-app/src/screens/BatterieListe.tsx @@ -0,0 +1,103 @@ +/** + * Batteriespannungs-Messwertliste — ein Eintrag pro Tag (dieselben Daten wie + * das Diagramm in Batterie.tsx), aber als Liste: Datum, Uhrzeit, Spannung, + * Außentemperatur. Erreichbar über das Listen-Symbol oben rechts im + * Diagramm. + * + * Jede Zeile lässt sich wie Fahrten/Tankvorgänge nach links wegwischen, um + * sie zu löschen (Zeilenmenue - Wischgeste plus sichtbares Menü, siehe dort). + * + * Liegt der Tagesminimalwert über AGM_RUHE_MAX_V (Batterie.tsx), wurde er nie + * in Ruhe gemessen (das Fahrzeug stand an diesem Tag nie lange genug still) - + * die Zeile zeigt dann "Generatorspannung" statt so zu tun, als sei es ein + * Ruhewert. + */ + +import { useEffect, useState } from "react" + +import { Pill, Tile } from "@audi-dash/ui" + +import { ENTITAETEN } from "../api" +import { useDaten } from "../daten/DatenKontext" +import { datum, datumZeit, de } from "../format" +import { Leerzustand } from "./bausteine" +import { AGM_RUHE_MAX_V } from "./Batterie" +import { Zeilenmenue } from "./Zeilenmenue" + +interface Tageswert { + datum: string + min: number | null + max: number | null + min_ts?: string + min_temp_c?: number | null +} + +export function BatterieListe() { + const { api } = useDaten() + const [verlauf, setzeVerlauf] = useState(null) + const [fehler, setzeFehler] = useState(false) + + const neuLaden = () => { + void api.rest + .datenLesen(ENTITAETEN.batterieverlauf) + .then((daten) => setzeVerlauf(Array.isArray(daten) ? daten : [])) + .catch(() => setzeFehler(true)) + } + + useEffect(neuLaden, [api]) + + if (fehler) { + return ( + + ) + } + + if (!verlauf) return
Liste wird geladen …
+ + const messwerte = verlauf.filter((t) => t.min != null).slice().reverse() + + if (messwerte.length === 0) { + return ( + + ) + } + + const loeschen = async (eintrag: Tageswert) => { + await api.batterieverlaufEintragLoeschen(eintrag.datum) + neuLaden() + } + + return ( + + {messwerte.map((t) => ( + void loeschen(t)} + kinder={ +
+ + + {de(t.min!, 2)} V + + {t.min_temp_c != null ? `${de(t.min_temp_c, 1)} °C` : "—"} + + + + {t.min_ts ? datumZeit(t.min_ts) : datum(t.datum)} + + + {t.min! > AGM_RUHE_MAX_V && Generatorspannung} +
+ } + /> + ))} +
+ ) +} diff --git a/companion-app/src/screens/Reifen.tsx b/companion-app/src/screens/Reifen.tsx index 795d3f1..777649c 100644 --- a/companion-app/src/screens/Reifen.tsx +++ b/companion-app/src/screens/Reifen.tsx @@ -9,12 +9,13 @@ import { useState } from "react" -import { ActionButton, Feld, Seg, Tile } from "@audi-dash/ui" +import { Accordion, ActionButton, Feld, IconButton, Seg, Tile } from "@audi-dash/ui" import { DIENST_DOMAIN } from "../api" import { useDaten } from "../daten/DatenKontext" -import type { Reifensatz } from "../daten/profilAdapter" +import type { Reifensatz, ReifenArchivEintrag } from "../daten/profilAdapter" import { datum, de, deOderStrich } from "../format" +import { SymbolPaket } from "../symbole" import { Wertzeile, Werteliste, bestaetigen } from "./bausteine" import { Bild } from "./Bild" import { kalenderDateiLaden } from "./kalender" @@ -45,6 +46,19 @@ export function Reifen() { } } + const archivieren = async () => { + if (!bestaetigen("Neue Räder anlegen?")) return + setzeLaeuft(true) + try { + // Eigener Dienst wie reifen_wechseln: archiviert den bisherigen Stand + // und setzt den laufenden Satz zurück (km 0, Marke/Modell/DOT/Kommentar + // leer - siehe reifen.py archivieren()). + await api.reifenArchivieren(anzeige === "Winter" ? "winter" : "sommer") + } finally { + setzeLaeuft(false) + } + } + const wechseldatumSetzen = async (neuesDatum: string) => { await profilSpeichern({ fahrzeug: { @@ -125,6 +139,11 @@ export function Reifen() { beschreibung={`${anzeige}rad`} name={`${anzeige}rad`} /> +
+ void archivieren()} disabled={laeuft}> + + +
@@ -153,6 +172,193 @@ export function Reifen() { rechnet sie immer dem gerade montierten Satz zu.

+ + {fahrzeug.reifen.archiv.length > 0 && ( + + Archiv + {fahrzeug.reifen.archiv.map((eintrag) => ( + + ))} + + )} ) } + +function ArchivZeile({ eintrag }: { eintrag: ReifenArchivEintrag }) { + const { api } = useDaten() + const [bearbeiten, setzeBearbeiten] = useState(false) + const [laeuft, setzeLaeuft] = useState(false) + const [form, setzeForm] = useState(() => ({ + ersetzt_am: eintrag.ersetzt_am, + km: String(eintrag.km ?? ""), + marke: eintrag.marke ?? "", + modell: eintrag.modell ?? "", + dot: eintrag.dot ?? "", + mass: eintrag.mass ?? "", + druck_vorne: eintrag.druck_vorne ?? "", + druck_hinten: eintrag.druck_hinten ?? "", + kommentar: eintrag.kommentar ?? "", + })) + + const speichern = async () => { + setzeLaeuft(true) + try { + const kmZahl = Number(form.km.replace(",", ".")) + await api.reifenArchivAktualisieren(eintrag.id, { + ersetzt_am: form.ersetzt_am, + km: Number.isFinite(kmZahl) ? kmZahl : null, + marke: form.marke || null, + modell: form.modell || null, + dot: form.dot || null, + mass: form.mass || null, + druck_vorne: form.druck_vorne || null, + druck_hinten: form.druck_hinten || null, + kommentar: form.kommentar || null, + }) + setzeBearbeiten(false) + } finally { + setzeLaeuft(false) + } + } + + const loeschen = async () => { + if (!bestaetigen("Diesen archivierten Reifensatz löschen?")) return + setzeLaeuft(true) + try { + await api.reifenArchivLoeschen(eintrag.id) + } finally { + setzeLaeuft(false) + } + } + + return ( + + {deOderStrich(eintrag.km)} km + Ersetzt am {datum(eintrag.ersetzt_am)} + + } + > + {bearbeiten ? ( + <> + + setzeForm({ ...form, ersetzt_am: e.target.value })} + /> + + + setzeForm({ ...form, km: e.target.value })} + /> + + + setzeForm({ ...form, marke: e.target.value })} + /> + + + setzeForm({ ...form, modell: e.target.value })} + /> + + + setzeForm({ ...form, dot: e.target.value })} + /> + + + setzeForm({ ...form, mass: e.target.value })} + /> + + + setzeForm({ ...form, druck_vorne: e.target.value })} + /> + + + setzeForm({ ...form, druck_hinten: e.target.value })} + /> + + {/* Kein hier (Label und Eingabe teilen sich sonst eine Zeile, + die Eingabe bleibt auf ihrer intrinsischen Breite - für eine + Notiz wie "Profiltiefe 5,1 mm bei der letzten Kontrolle" zu + wenig Platz, live beobachtet). Eingabe über die volle Breite, + Label darüber - Parität zum Panel (.feld--breit). */} +
+ + setzeForm({ ...form, kommentar: e.target.value })} + /> +
+
+ void speichern()} disabled={laeuft}> + {laeuft ? "Speichere …" : "Speichern"} + + setzeBearbeiten(false)} disabled={laeuft}> + Abbrechen + +
+
+ void loeschen()} disabled={laeuft}> + Löschen + +
+ + ) : ( + <> + + + + + + {Boolean(eintrag.kommentar) && } + + } + /> +
+ setzeBearbeiten(true)}>Bearbeiten +
+ + )} +
+ ) +} diff --git a/companion-app/src/screens/register.tsx b/companion-app/src/screens/register.tsx index d67d1f9..1dcb109 100644 --- a/companion-app/src/screens/register.tsx +++ b/companion-app/src/screens/register.tsx @@ -6,6 +6,7 @@ import { useDaten } from "../daten/DatenKontext" import type { Route, SeitenName } from "../navigation" import { Batterie } from "./Batterie" +import { BatterieListe } from "./BatterieListe" import { Einstellungen } from "./Einstellungen" import { FahrtDetail } from "./FahrtDetail" import { Fahrten } from "./Fahrten" @@ -69,7 +70,9 @@ export function SeiteFuer(props: SeitenProps) { case "ident": return case "battverlauf": - return + return + case "battliste": + return case "service": return case "werkstatt": diff --git a/companion-app/src/symbole.tsx b/companion-app/src/symbole.tsx index 034dc0f..eb8c2de 100644 --- a/companion-app/src/symbole.tsx +++ b/companion-app/src/symbole.tsx @@ -13,14 +13,22 @@ interface SymbolProps { groesse?: number | undefined } -function Rahmen({ groesse = 24, kinder }: SymbolProps & { kinder: ReactElement }) { +/** `voll`: Fuellflaeche statt Kontur, fuer Symbole aus dem Audi-CI-Icon-Satz, + deren Pfade ihr eigenes fill="currentColor" mitbringen - ohne stroke="none" + hier wuerde der geerbte Kontur-Stroke trotzdem um jede Flaeche gezeichnet. + Entspricht der .voll-Klasse im Panel (audi-dashboard-app.js). */ +function Rahmen({ + groesse = 24, + voll = false, + kinder, +}: SymbolProps & { voll?: boolean; kinder: ReactElement }) { return ( } /> + return ( + + } + /> + ) +} + +// CI-Symbol list-s: Sprung vom Diagramm zur Werteliste (Batteriespannung). +export function SymbolListe({ groesse }: SymbolProps) { + return ( + } + /> + ) +} + +// CI-Symbol package-s: "Neue Räder anlegen" (Reifensatz archivieren). +export function SymbolPaket({ groesse }: SymbolProps) { + return ( + + } + /> + ) } export function SymbolTanken({ groesse }: SymbolProps) { diff --git a/custom_components/audi_dashboard/ablage.py b/custom_components/audi_dashboard/ablage.py index 6ede3ab..09e8682 100644 --- a/custom_components/audi_dashboard/ablage.py +++ b/custom_components/audi_dashboard/ablage.py @@ -332,19 +332,24 @@ class Ablage: # ------------------------------------------------------ Batteriespannung async def batterieverlauf_lesen(self) -> list[dict]: - """Ein Eintrag pro Tag ({datum, min, min_ts, max, max_ts}), älteste - zuerst. min_ts/max_ts sind die Zeitstempel (ISO, UTC) der jeweiligen - Einzelmessung, für die Datum/Uhrzeit-Anzeige beim Antippen des - Diagrammpunkts - der Punkt selbst zeigt nur den Minimalwert.""" + """Ein Eintrag pro Tag ({datum, min, min_ts, max, max_ts, + min_temp_c}), älteste zuerst. min_ts/max_ts sind die Zeitstempel + (ISO, UTC) der jeweiligen Einzelmessung, für die Datum/Uhrzeit-Anzeige + beim Antippen des Diagrammpunkts - der Punkt selbst zeigt nur den + Minimalwert. min_temp_c ist die Außentemperatur zum Zeitpunkt genau + dieser Minimalmessung (None ohne zugeordneten Temperatursensor oder + bei älteren, vor dessen Einführung geschriebenen Einträgen).""" text = await self._im_executor(self._text_lesen, self.batterieverlauf_pfad) return self._zeilen_aus_text(text, self.batterieverlauf_pfad) async def batterieverlauf_tageswert_aktualisieren( - self, datum: str, ts: str, spannung: float + self, datum: str, ts: str, spannung: float, aussentemp: float | None = None ) -> None: """Trägt eine neue Messung in den Tageseintrag für `datum` ein: legt ihn beim ersten Wert des Tages an, erweitert sonst nur min/max samt - dem Zeitstempel der jeweils neuen Extremmessung.""" + dem Zeitstempel der jeweils neuen Extremmessung. min_temp_c wird nur + mitgeschrieben, wenn diese Messung auch den Minimalwert setzt - sie + gilt für genau diesen Zeitpunkt, nicht für den Tag insgesamt.""" async with self._sperre: verlauf = await self.batterieverlauf_lesen() for eintrag in verlauf: @@ -353,14 +358,16 @@ class Ablage: if spannung < eintrag["min"]: eintrag["min"] = spannung eintrag["min_ts"] = ts + eintrag["min_temp_c"] = aussentemp if spannung > eintrag["max"]: eintrag["max"] = spannung eintrag["max_ts"] = ts break else: - verlauf.append( - {"datum": datum, "min": spannung, "min_ts": ts, "max": spannung, "max_ts": ts} - ) + verlauf.append({ + "datum": datum, "min": spannung, "min_ts": ts, + "max": spannung, "max_ts": ts, "min_temp_c": aussentemp, + }) # Nach Datum sortiert schreiben, nicht in Einfügereihenfolge. # Solange nur die Live-Aufzeichnung schrieb, war beides dasselbe # (sie trägt immer den heutigen Tag ein). Der nachträgliche Import @@ -374,6 +381,19 @@ class Ablage: self._text_aus_zeilen(verlauf), ) + async def batterieverlauf_eintrag_loeschen(self, datum: str) -> bool: + async with self._sperre: + verlauf = await self.batterieverlauf_lesen() + uebrig = [e for e in verlauf if e.get("datum") != datum] + if len(uebrig) == len(verlauf): + return False + await self._im_executor( + self._text_schreiben, + self.batterieverlauf_pfad, + self._text_aus_zeilen(uebrig), + ) + return True + # ---------------------------------------------------- Sensor-Zuordnung async def zuordnung_lesen(self) -> dict: diff --git a/custom_components/audi_dashboard/batterie.py b/custom_components/audi_dashboard/batterie.py index 8193fc1..24d9abe 100644 --- a/custom_components/audi_dashboard/batterie.py +++ b/custom_components/audi_dashboard/batterie.py @@ -27,6 +27,12 @@ langen Fahrt), tauchen dort nicht in der Statistik auf. Ohne zugeordneten BATTERIE_SENSOR passiert hier bewusst nichts, statt Platzhalter-Nullwerte aufzuzeichnen. + +Optional wird bei jeder neuen Tagesminimum-Messung auch die aktuelle +Außentemperatur (AUSSENTEMP_SENSOR, CAN ambient air temperature) mit +aufgezeichnet - für die Messwertliste (Datum, Uhrzeit, Außentemperatur), nicht +für die Ladezustandsauswertung selbst. Ohne zugeordneten Temperatursensor +bleibt das Feld None, genau wie beim Batteriesensor. """ from __future__ import annotations @@ -55,10 +61,33 @@ async def pruefen(k: Koordinator) -> None: except (TypeError, ValueError): return + aussentemp = None + temp_wert = zustand_oder_none(k.hass, k.zuordnung.werte.AUSSENTEMP_SENSOR) + if temp_wert is not None: + try: + aussentemp = float(temp_wert) + except (TypeError, ValueError): + pass + jetzt = datetime.datetime.now(datetime.UTC) heute = datetime.date.today().isoformat() await k.ablage.batterieverlauf_tageswert_aktualisieren( - heute, jetzt.isoformat(), spannung + heute, jetzt.isoformat(), spannung, aussentemp ) await k.batterieverlauf_veroeffentlichen() _LOGGER.debug("Batteriespannung erfasst (%s: %s V)", heute, spannung) + + +async def eintrag_loeschen(k: Koordinator, datum: str | None) -> None: + """Löscht einen Tageseintrag aus der Messwertliste (Wischen zum Löschen). + + Entfernt nur diesen einen Tag - min/max anderer Tage bleiben unberührt, + das Diagramm zeichnet beim nächsten Aufruf einfach ohne diesen Punkt.""" + if not datum: + _LOGGER.warning("batterieverlauf_loeschen ohne datum aufgerufen") + return + if await k.ablage.batterieverlauf_eintrag_loeschen(datum): + await k.batterieverlauf_veroeffentlichen() + _LOGGER.info("Batteriespannungs-Tageseintrag %s gelöscht", datum) + else: + _LOGGER.warning("Batteriespannungs-Tageseintrag %s nicht gefunden", datum) diff --git a/custom_components/audi_dashboard/const.py b/custom_components/audi_dashboard/const.py index fe7952d..797fade 100644 --- a/custom_components/audi_dashboard/const.py +++ b/custom_components/audi_dashboard/const.py @@ -84,8 +84,12 @@ D_TANKVORGANG_AKTUALISIEREN = "tankvorgang_aktualisieren" D_TANKVORGANG_LOESCHEN = "tankvorgang_loeschen" D_BILD_HOCHLADEN = "bild_hochladen" D_BILD_LOESCHEN = "bild_loeschen" +D_BATTERIEVERLAUF_LOESCHEN = "batterieverlauf_loeschen" D_REIFEN_WECHSELN = "reifen_wechseln" D_REIFEN_KM_SETZEN = "reifen_km_setzen" +D_REIFEN_ARCHIVIEREN = "reifen_archivieren" +D_REIFEN_ARCHIV_AKTUALISIEREN = "reifen_archiv_aktualisieren" +D_REIFEN_ARCHIV_LOESCHEN = "reifen_archiv_loeschen" D_BACKUP_JETZT = "backup_jetzt" D_BACKUP_WIEDERHERSTELLEN = "backup_wiederherstellen" D_HISTORIE_IMPORTIEREN = "historie_importieren" diff --git a/custom_components/audi_dashboard/dienste.py b/custom_components/audi_dashboard/dienste.py index d8a140e..480f0e3 100644 --- a/custom_components/audi_dashboard/dienste.py +++ b/custom_components/audi_dashboard/dienste.py @@ -28,6 +28,7 @@ from homeassistant.helpers import config_validation as cv from . import ( aktualisierung, + batterie, belege, bilder, fahrterkennung, @@ -41,6 +42,7 @@ from .const import ( CONF_GITEA_TOKEN, D_BACKUP_JETZT, D_BACKUP_WIEDERHERSTELLEN, + D_BATTERIEVERLAUF_LOESCHEN, D_BELEG_HOCHLADEN, D_BILD_HOCHLADEN, D_BILD_LOESCHEN, @@ -51,6 +53,9 @@ from .const import ( D_HISTORIE_IMPORTIEREN, D_JETZT_AKTUALISIEREN, D_PROFIL_SCHREIBEN, + D_REIFEN_ARCHIV_AKTUALISIEREN, + D_REIFEN_ARCHIV_LOESCHEN, + D_REIFEN_ARCHIVIEREN, D_REIFEN_KM_SETZEN, D_REIFEN_WECHSELN, D_SCREENING_JETZT, @@ -94,6 +99,18 @@ _TANK_FELDER = { vol.Optional("receipt_file"): _TEXT, } +_REIFEN_ARCHIV_FELDER = { + vol.Optional("ersetzt_am"): _TEXT, + vol.Optional("km"): _ZAHL, + vol.Optional("marke"): _TEXT, + vol.Optional("modell"): _TEXT, + vol.Optional("dot"): _TEXT, + vol.Optional("mass"): _TEXT, + vol.Optional("druck_vorne"): _TEXT, + vol.Optional("druck_hinten"): _TEXT, + vol.Optional("kommentar"): _TEXT, +} + SCHEMATA: dict[str, vol.Schema] = { D_JETZT_AKTUALISIEREN: vol.Schema({}), D_SCREENING_JETZT: vol.Schema({}), @@ -132,6 +149,12 @@ SCHEMATA: dict[str, vol.Schema] = { vol.Required("satz"): vol.In(reifen.SAETZE), vol.Required("km"): vol.Coerce(float), }), + D_REIFEN_ARCHIVIEREN: vol.Schema({vol.Required("satz"): vol.In(reifen.SAETZE)}), + D_REIFEN_ARCHIV_AKTUALISIEREN: vol.Schema({ + vol.Required("id"): cv.string, **_REIFEN_ARCHIV_FELDER + }), + D_REIFEN_ARCHIV_LOESCHEN: vol.Schema({vol.Required("id"): cv.string}), + D_BATTERIEVERLAUF_LOESCHEN: vol.Schema({vol.Required("datum"): cv.string}), D_BACKUP_WIEDERHERSTELLEN: vol.Schema({ vol.Optional("profil_json"): _JSON, vol.Optional("fahrten_json"): _JSON, @@ -320,6 +343,16 @@ def registrieren(hass: HomeAssistant, k: Koordinator) -> None: D_REIFEN_KM_SETZEN: _als_dienst( lambda a: reifen.km_setzen(k, a.data["satz"], a.data["km"]) ), + D_REIFEN_ARCHIVIEREN: _als_dienst(lambda a: reifen.archivieren(k, a.data["satz"])), + D_REIFEN_ARCHIV_AKTUALISIEREN: _als_dienst( + lambda a: reifen.archiv_aktualisieren(k, a.data["id"], dict(a.data)) + ), + D_REIFEN_ARCHIV_LOESCHEN: _als_dienst( + lambda a: reifen.archiv_loeschen(k, a.data["id"]) + ), + D_BATTERIEVERLAUF_LOESCHEN: _als_dienst( + lambda a: batterie.eintrag_loeschen(k, a.data["datum"]) + ), D_BACKUP_JETZT: _als_dienst(lambda _a: sicherung.jetzt(k)), D_BACKUP_WIEDERHERSTELLEN: _als_dienst( lambda a: sicherung.wiederherstellen(k, dict(a.data)) diff --git a/custom_components/audi_dashboard/einstellungen.py b/custom_components/audi_dashboard/einstellungen.py index 1e3fc92..ad7ce88 100644 --- a/custom_components/audi_dashboard/einstellungen.py +++ b/custom_components/audi_dashboard/einstellungen.py @@ -81,6 +81,11 @@ class Sensorzuordnung: # und hat mit der Fahrzeugbatterie nichts zu tun). BATTERIE_SENSOR: str = "" + # Außentemperatur (CAN ambient air temperature) - optional, für die + # Batteriespannungs-Messwertliste: zusammen mit dem Tagesminimum + # aufgezeichnet, siehe batterie.py. + AUSSENTEMP_SENSOR: str = "" + # Knopf für eine sofortige Neuabfrage beim Fahrzeug. REFRESH_BUTTON: str = "" @@ -166,6 +171,11 @@ FELDER: list[dict] = [ "hinweis": "Nicht jede Integration liefert diesen Wert - bleibt er leer, zeigt \"Mein Audi\" \"unbekannt\".", "domains": ["sensor"], "device_classes": ["voltage"], "units": ["V"], "liste": False, "pflicht": False, "stichworte": ["batterie", "battery", "spannung", "volt", "12v"]}, + {"key": "AUSSENTEMP_SENSOR", "label": "Außentemperatur", "gruppe": "uebersicht", + "hinweis": "CAN-Außentemperatur - optional, ergänzt die Batteriespannungs-Messwertliste um den " + "Temperaturwert je Tag.", + "domains": ["sensor"], "device_classes": ["temperature"], "units": ["°C", "°F"], "liste": False, + "pflicht": False, "stichworte": ["außentemperatur", "aussentemperatur", "ambient", "temperatur", "temperature"]}, {"key": "REFRESH_BUTTON", "label": "Sofort-Aktualisierung (Knopf)", "gruppe": "uebersicht", "hinweis": "Der von der Integration bereitgestellte Knopf für \"Ziehen zum Aktualisieren\".", "domains": ["button"], "device_classes": [], "units": [], "liste": False, "pflicht": False, diff --git a/custom_components/audi_dashboard/frontend/app/bundle.json b/custom_components/audi_dashboard/frontend/app/bundle.json index 4e5d991..07725b2 100644 --- a/custom_components/audi_dashboard/frontend/app/bundle.json +++ b/custom_components/audi_dashboard/frontend/app/bundle.json @@ -1 +1 @@ -{"version":"2026.8.24.11","sha256":"c1a85f9a15b434121f4cd19e2729cfbe60ca89757163954f07152483b8b209af","bytes":233245,"gebaut":"2026-08-24T14:58:21Z"} \ No newline at end of file +{"version":"2026.8.25.8","sha256":"464c294c6c07dcb636a8ea3bd93c7e525b21722defb41f573e2b31fefade49ea","bytes":235525,"gebaut":"2026-08-27T09:40:45Z"} \ No newline at end of file diff --git a/custom_components/audi_dashboard/frontend/app/bundle.zip b/custom_components/audi_dashboard/frontend/app/bundle.zip index 0692755..6cd17ec 100644 Binary files a/custom_components/audi_dashboard/frontend/app/bundle.zip and b/custom_components/audi_dashboard/frontend/app/bundle.zip differ diff --git a/custom_components/audi_dashboard/frontend/audi-dashboard-app.js b/custom_components/audi_dashboard/frontend/audi-dashboard-app.js index acf72e4..e78d4e4 100644 --- a/custom_components/audi_dashboard/frontend/audi-dashboard-app.js +++ b/custom_components/audi_dashboard/frontend/audi-dashboard-app.js @@ -176,6 +176,12 @@ function profilZuCar(p, status) { wechsel: p.reifen.wechsel, sommer: reifenSatz("sommer"), winter: reifenSatz("winter"), + // Neueste zuerst, schon so im Profil (reifen.py archivieren()). Nur + // gelesen - bearbeitet/gelöscht wird über eigene Dienste + // (reifen_archiv_aktualisieren/-loeschen), nicht über profilSpeichern, + // aus demselben Grund wie km oben: kein Stand im Browser darf einen + // zwischenzeitlichen Servereintrag überschreiben. + archiv: p.reifen.archiv || [], }, service: p.service, technik: p.technik, @@ -1044,6 +1050,16 @@ const CI = { verbatim uebernommen) - l im 48er-, s im 24er-Grid wie poi-l/poi-s. */ battery12vL: '', battery12vS: '', + /* list-l/-s, polls-l/-s, package-l/-s aus dem Audi-CI-Icon-Satz (vom Nutzer + als SVG geliefert, verbatim uebernommen) - l im 48er-, s im 24er-Grid wie + poi-l/poi-s. list: Sprung vom Diagramm zur Werteliste (Batteriespannung). + polls: Statistik-Tab in der Tableiste. package: "Neue Räder anlegen". */ + listL: '', + listS: '', + pollsL: '', + pollsS: '', + packageL: '', + packageS: '', /* edit-l/-s aus dem Audi-CI-Icon-Satz (vom Nutzer als SVG geliefert, verbatim uebernommen) - l im 48er-, s im 24er-Grid wie poi-l/poi-s. */ editL: '', @@ -1782,7 +1798,10 @@ function vBatterieverlauf() { return `
- Verlauf +
+ Verlauf + +
${genug ? `
@@ -1825,6 +1844,31 @@ function vBatterieverlauf() {
`; } +/* Messwertliste zur Batteriespannungs-Kachel: ein Eintrag pro Tag (dieselben + Daten wie das Diagramm), aber als Liste mit Datum, Uhrzeit, Spannung und + Außentemperatur. Jede Zeile wischbar zum Löschen wie Fahrten/Tankvorgänge + (art "batt" in eintragLoeschen()/LOESCH_TEXT/LOESCH_HINWEIS) - kein + data-go/Chevron, es gibt keine Detailseite für einen einzelnen Messwert. + Liegt der Tagesminimalwert über AGM_RUHE_MAX_V, wurde er nie in Ruhe + gemessen (an dem Tag stand das Fahrzeug nie lange genug still) - die Zeile + markiert das mit "Generatorspannung" statt so zu tun, als sei es ein + Ruhewert (dieselbe Schwelle wie im Diagramm selbst, siehe bvPunkte oben). */ +function vBatterieliste() { + const messwerte = BATTERIEVERLAUF.filter((e) => e.min != null).slice().reverse(); + if (messwerte.length === 0) { + return `
Keine Messwerte
`; + } + return `
${messwerte.map((e) => { + const zeit = e.min_ts ? new Date(e.min_ts) : null; + const wann = zeit ? `${dedat(zeit)} · ${zeit.toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" })} Uhr` : dedat(new Date(e.datum)); + return swipeZeile(`batt:${e.datum}`, ` +
+ ${de(e.min, 2)} V${e.min_temp_c != null ? de(e.min_temp_c, 1) + " °C" : "—"} + ${wann}${e.min > AGM_RUHE_MAX_V ? `Generatorspannung` : ""} +
`); + }).join("")}
`; +} + function bvSkalaX(t) { return BV_ML + ((t - bvDomain[0]) / (bvDomain[1] - bvDomain[0])) * (BV_W - BV_ML - BV_MR); } function bvSkalaY(v, yMin, yMax) { return BV_H - BV_MB - ((v - yMin) / (yMax - yMin)) * (BV_H - BV_MT - BV_MB); } function bvDomainKlemmen(domain) { @@ -2843,7 +2887,10 @@ function vReifen() {
` : ""} -
+
+ +
+
${feld(k, "marke", "Hersteller")} ${feld(k, "modell", "Modell")} ${feld(k, "mass", "Größe")} @@ -2856,7 +2903,8 @@ function vReifen() { bar
- ${feld(k, "kommentar", "Kommentar")} +
+
`; }; @@ -2883,7 +2931,55 @@ function vReifen() {
${satz("sommer")} - ${satz("winter")}`; + ${satz("winter")} + ${vReifenArchiv()}`; +} + +/* Übersicht der per "Neue Räder anlegen" archivierten Sätze - jede Zeile + aufklappbar (dasselbe acc-h/acc-b-Muster wie die Tankvorgänge-Liste), mit + inline editierbaren Feldern wie bei den laufenden Sätzen oben, nur über + eigene Dienste statt profilSpeichern (siehe profilZuCar()). */ +function vReifenArchiv() { + const archiv = CAR.reifen.archiv || []; + if (archiv.length === 0) return ""; + // Kein .mitEinheit hier (das erzwingt eine feste 96px-Eingabebreite fürs + // Zahl+Einheit-Muster, z. B. km/Nm - für freien Text wie Hersteller/Modell/ + // Kommentar riss das den Wert ab, siehe live beobachtet: "Continenta", + // "SportConta"). Genau wie feld() in satz() oben: einfaches , das + // sich über den .feld-Flex den ganzen Rest der Zeile nimmt. + const feld = (a, schluessel, bez) => `
+
`; + return `
Archiv + ${archiv.map((a, i) => { + const satzLabel = a.satz === "winter" ? "Winterräder" : "Sommerräder"; + return ` +
+
+
+ km
+ ${feld(a, "marke", "Hersteller")} + ${feld(a, "modell", "Modell")} + ${feld(a, "mass", "Größe")} + ${feld(a, "dot", "DOT")} +
+ + + / + + bar + +
+
+
+ +
`; + }).join("")} +
`; } /* Gemeinsames Formular für "Neue Fahrt" und das Bearbeiten einer bestehenden @@ -3250,12 +3346,14 @@ const LOESCH_TEXT = { fill: "Tankvorgang löschen?", sbuch: "Servicebuch-Eintrag löschen?", bild: "Foto löschen?", + batt: "Messwert löschen?", }; const LOESCH_HINWEIS = { trip: "Die Fahrt wird aus dem Fahrtenbuch entfernt. Das lässt sich nicht widerrufen.", fill: "Der Tankvorgang und der zugehörige Beleg werden entfernt. Das lässt sich nicht widerrufen.", sbuch: "Der Eintrag wird aus dem Servicebuch entfernt. Das lässt sich nicht widerrufen.", bild: "Das Foto wird vom Server gelöscht. Der Platz bleibt bestehen und kann neu belegt werden.", + batt: "Der Tageseintrag wird aus der Messwertliste entfernt. Das lässt sich nicht widerrufen.", }; function eintragLoeschen(art, id) { if (art === "trip") { @@ -3267,6 +3365,9 @@ function eintragLoeschen(art, id) { } else if (art === "sbuch") { CAR.service.buch.splice(parseInt(id, 10), 1); profilSpeichern(); + } else if (art === "batt") { + serviceRufen("batterieverlauf_loeschen", { datum: id }); + BATTERIEVERLAUF = BATTERIEVERLAUF.filter((e) => e.datum !== id); } } @@ -3511,7 +3612,9 @@ const ICONS = { assets/icons/car.svg). Flaeche statt Kontur, deshalb die Klasse "voll". */ audi: '', trips: '', - stat: '', + /* CI-Symbol polls-s statt der vorherigen handgezeichneten Balken - dieselbe + Fuellflaeche-statt-Kontur-Behandlung wie "audi" (siehe ICONS_VOLL). */ + stat: CI.pollsS, fuel: '', }; const TABS = [ @@ -3522,7 +3625,7 @@ const TABS = [ { id: "fuel", label: "Tanken", eyebrow: "", title: "Tanken" }, ]; /* Symbole, die als Flaeche gezeichnet sind statt als Kontur */ -const ICONS_VOLL = new Set(["audi"]); +const ICONS_VOLL = new Set(["audi", "stat"]); let route = { name: "home" }; const ZURUECK = { trip: "trips", fill: "fuel", service: "audi", reifen: "audi", ident: "audi", @@ -3530,6 +3633,7 @@ const ZURUECK = { vertrag: "vers", steuer: "vers", notruf: "vers", werkstatt: "service", sbuch: "service", einst: "home", battverlauf: "audi", + battliste: "battverlauf", /* Fehlte: die Sicherheitsansicht war eine Sackgasse ohne sichtbaren Zurück-Pfeil. */ sicherheit: "home", @@ -3889,6 +3993,7 @@ function render() { else if (route.name === "schutz") { head = ["Im Ernstfall", "Schutzbrief"]; v.innerHTML = vSchutz(); } else if (route.name === "werkstatt") { head = ["Service", "Autohaus"]; v.innerHTML = vWerkstatt(); } else if (route.name === "battverlauf") { head = ["Fahrzeug", "Batteriespannung"]; v.innerHTML = vBatterieverlauf(); bvZeichnen(); } + else if (route.name === "battliste") { head = ["Batteriespannung", "Messwerte"]; v.innerHTML = vBatterieliste(); } else if (route.name === "sbuch") { head = ["Servicebuch", "Eintrag"]; v.innerHTML = vSbuch(); } else if (route.name === "standort") { head = ["Fahrzeug", "Standort"]; v.innerHTML = vStandort(); } else { const t = TABS.find((x) => x.id === route.name); head = [t.eyebrow, t.id === "audi" ? "Mein Audi" : t.title]; @@ -4724,6 +4829,25 @@ function ereignisseVerdrahten() { } const satz = e.target.closest("[data-satz]"); if (satz) { serviceRufen("reifen_wechseln", { satz: satz.dataset.satz.toLowerCase() }); CAR.reifen.aktiv = satz.dataset.satz; render(); return; } + const rfArch = e.target.closest("[data-reifenarchivieren]"); + if (rfArch) { + const k = rfArch.dataset.reifenarchivieren; + bestaetigen("Neue Räder anlegen?", + "Der aktuelle Stand wird ins Archiv gelegt, der laufende Satz beginnt neu bei 0 km.", + "Anlegen", () => { serviceRufen("reifen_archivieren", { satz: k }); }, false); + return; + } + const rfArchDel = e.target.closest("[data-reifenarchivloeschen]"); + if (rfArchDel) { + const id = rfArchDel.dataset.reifenarchivloeschen; + bestaetigen("Archivierten Reifensatz löschen?", + "Das lässt sich nicht widerrufen.", "Löschen", () => { + serviceRufen("reifen_archiv_loeschen", { id }); + CAR.reifen.archiv = CAR.reifen.archiv.filter((a) => a.id !== id); + render(); + }, true); + return; + } const k = e.target.closest("[data-ics]"); if (k) { if (k.dataset.ics === "service") { @@ -4912,6 +5036,18 @@ function ereignisseVerdrahten() { CAR.reifen[k][schluessel] = wert; profilSpeichern(); render(); return; } + const rfa = e.target.dataset.rfa; + if (rfa) { + const [id, schluessel] = rfa.split("|"); + const wert = schluessel === "km" ? parseFloat(e.target.value) || 0 : e.target.value; + const eintrag = CAR.reifen.archiv.find((a) => a.id === id); + if (eintrag) eintrag[schluessel] = wert; + // Eigener Dienst statt profilSpeichern (siehe profilZuCar()) - ändert + // nur diesen einen archivierten Satz, statt das ganze Profil (samt + // eines womöglich seither fortgeschriebenen km-Stands) zu überschreiben. + serviceRufen("reifen_archiv_aktualisieren", { id, [schluessel]: wert }); + return; + } if (e.target.dataset.schrift !== undefined) { beschriftung = e.target.checked; try { localStorage.setItem(SCHRIFT_KEY, beschriftung ? "an" : "aus"); } catch (err) { /* Privatmodus */ } diff --git a/custom_components/audi_dashboard/frontend/audi-dashboard-ios.css b/custom_components/audi_dashboard/frontend/audi-dashboard-ios.css index 68f2f0a..24588cb 100644 --- a/custom_components/audi_dashboard/frontend/audi-dashboard-ios.css +++ b/custom_components/audi_dashboard/frontend/audi-dashboard-ios.css @@ -334,6 +334,14 @@ main#view{--seitenrand:16px;padding:0 var(--seitenrand) 30px;scroll-behavior:smo Einblend-Animation dieser Elemente zu beruehren. */ .setup-popup,.sdpopup,.sheet,.beleg-popup{max-width:560px;margin-left:auto;margin-right:auto} .standortmenu{max-width:640px;margin-left:auto;margin-right:auto} + + /* Batteriespannungs-Diagramm ist auf BV_W=320 SVG-Einheiten gezeichnet + (audi-dashboard-app.js) und haelt seine Hoehe fest bei 170px - bei voller + Spaltenbreite (772px Inhaltsbreite: 860 - 2*44px Seitenrand) zog + preserveAspectRatio="none" das Diagramm zu einem extrem breiten, flachen + Band auseinander. Gedeckelt wie die Popups oben, statt die feste Hoehe + oder die BV_*-Zeichen-Logik anzufassen. */ + #bvChart{max-width:400px;margin-left:auto;margin-right:auto} } /* Fahrzeugbild randlos wie in einer Fahrzeug-App: der Platzhalter bleibt diff --git a/custom_components/audi_dashboard/frontend/audi-dashboard.css b/custom_components/audi_dashboard/frontend/audi-dashboard.css index 9c31da8..6f2b7f6 100644 --- a/custom_components/audi_dashboard/frontend/audi-dashboard.css +++ b/custom_components/audi_dashboard/frontend/audi-dashboard.css @@ -171,6 +171,17 @@ .themebtn:hover { background: var(--tile-2); color: var(--fg); } .themebtn:active { background: var(--tile-2); color: var(--fg); transform: scale(.92); } .themebtn svg { fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; } +/* Wie .themebtn (44px Trefferflaeche), aber ohne die Kontur-Stroke-Regel - + fuer CI-Symbole (ciSVG()), deren Pfade ihr eigenes fill="currentColor" + mitbringen (z. B. list-s im Kopf der Batteriespannungs-Kachel). */ +.iconbtn { + display: inline-flex; align-items: center; justify-content: center; + width: 44px; height: 44px; border-radius: 50%; + border: none; background: none; color: var(--fg3); + cursor: pointer; padding: 0; flex: 0 0 auto; +} +.iconbtn:hover { background: var(--tile-2); color: var(--fg); } +.iconbtn:active { background: var(--tile-2); color: var(--fg); transform: scale(.92); } /* D4 war urspruenglich: die Ringe sind Marke, kein Knopf. Seit die iOS-Auflage in Telefonbreite das Zahnrad ausblendet, sind sie dort aber der einzige Zugang zu den Einstellungen - und als reines div mit @@ -665,6 +676,15 @@ button.sicherheitszeile:active { background: var(--tile-2); } appearance: none; } .feld input:focus, .feld select:focus, .notiz:focus { outline: none; border-color: var(--red); } +/* Label und Eingabe untereinander statt nebeneinander, die Eingabe über die + volle Zeilenbreite - für Felder, deren Wert typischerweise länger ist als + ein Kurztext (Kommentar bei Sommer-/Winterrädern, live wie archiviert). + Sonst gilt .feld input oben: die Eingabe wächst nie über ihre intrinsische + Breite, nur das Label tut das (flex:1 1 auto) - für einen Reifenkommentar + ("Profiltiefe 5,1 mm bei der letzten Montage") reichte das nicht. */ +.feld--breit { display: block; } +.feld--breit label { display: block; margin-bottom: 8px; } +.feld--breit input { width: 100%; text-align: left; } /* Das AUFGEKLAPPTE