Wartungsplan-Schema companion-app<->Panel angeglichen; Setup-Feldkopf zeigt zugeordneten Sensor
companion-app nannte Servicebuch-Feldnamen betrieb/notiz, waehrend Panel und Backend (profil["service"]["buch"]) werkstatt/kosten/arbeiten verwenden - ohne Uebersetzung dazwischen zeigten in einer Oberflaeche angelegte Eintraege in der anderen leere Werkstatt-/Kosten-Werte. werkstatt/kosten/ arbeiten als kanonisch uebernommen (aeltere, bereits etablierte Panel- Konvention); ServicebuchEintrag, das Bearbeitungsformular (inkl. neuem Kosten-Feld) und die Ansicht in Service.tsx angepasst, den Workaround-Cast im CSV-Export (DatensatzPopup.tsx) sowie die Testfixture entsprechend bereinigt. Setup-Popup (Panel, Item 11 aus der letzten Sammel-Rueckmeldung): die statische Beschreibung unter jedem Feldnamen ist jetzt die Kennung und der aktuelle Wert des tatsaechlich zugeordneten Sensors. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4001,15 +4001,11 @@ Archiv button and "Montiert" share one Y-center exactly, with a visible backgrou
|
||||
derived calculation, not read from any vehicle sensor. Owner pointed out a real FMM003 sensor,
|
||||
`sensor.testzone_fmm003_calculated_distance_covered_after_the_latest_refueling` (device-side, presumably
|
||||
more accurate/authoritative than our own subtraction), that reports this directly and "can be used instead
|
||||
of the calculation" - not yet wired in, would mirror the `TANK_LITER_SENSOR` precedent (section Q): new
|
||||
optional sensor role, preferred over the calculation when mapped, calculation stays as the fallback when
|
||||
unmapped. Owner to confirm before this is built.
|
||||
- **Item 9: done on the panel, see section AC below - companion-app port still pending** (parity rule).
|
||||
- **Item 11**: Setup-Menü sensor role fields - replace the current description text under each field's
|
||||
headline (e.g. "on = Fahrt läuft", "Optional, vom CAN...") with the live sensor's current output value and
|
||||
unit inline next to the headline (`[on/off]`, `[%]`, `[l]`, `[km]`...), and show the matched entity's ID
|
||||
suffix (the part after `fmm003_...`) as "the expected sensor" - not yet investigated in the code
|
||||
(`entitaeten.py`'s `FELDER` catalog / the Setup popup's row-rendering in the panel).
|
||||
of the calculation" - **done, see section AC below** (`TANK_DISTANZ_SENSOR`).
|
||||
- **Item 9: done, both codebases** - panel in section AC, companion-app port in section AD.
|
||||
- **Item 11: done, panel only (no companion-app equivalent, see section AF)** - Setup-Menü sensor role
|
||||
fields now show the mapped sensor's ID suffix and live value next to the headline instead of a static
|
||||
description.
|
||||
|
||||
## AC. Item 8 built (TANK_DISTANZ_SENSOR); Item 9 built on the panel: "Datensatz sichern/laden" (2026.8.28.3)
|
||||
|
||||
@@ -4147,6 +4143,80 @@ data" smoke test, which covers `Einstellungen.tsx`), `vite build` succeeds. **No
|
||||
running companion-app session with backend auth this round, the same structural limitation noted repeatedly
|
||||
elsewhere in this file for companion-app-only changes.
|
||||
|
||||
## AE. Wartungsplan schema mismatch between panel and companion-app fixed (2026-08-28)
|
||||
|
||||
The pre-existing schema mismatch flagged in section AD above (companion-app's `ServicebuchEintrag` naming
|
||||
`betrieb`/`notiz` while the actual stored data — `profil["service"]["buch"]`, written by both the panel and
|
||||
the backend — uses `werkstatt`/`kosten`/`arbeiten`, with no translation layer anywhere) is now fixed.
|
||||
Decided `werkstatt`/`kosten`/`arbeiten` as canonical, per section AD's own reasoning: the panel's naming is
|
||||
the older, already-established convention, and real installations' stored data already uses it.
|
||||
|
||||
`companion-app/src/screens/Service.tsx`: `ServicebuchEintrag` renamed to `werkstatt?: string` /
|
||||
`kosten?: number | null` / `arbeiten?: string`. The add/edit form's "Betrieb"/"Notiz" fields became
|
||||
"Werkstatt" / a new "Kosten" (€) field / "Durchgeführte Arbeiten" (matching the panel's own three-field
|
||||
layout and its "Durchgeführte Arbeiten" label exactly, not just the field name) — companion-app previously
|
||||
had no way to record cost on a Wartungsplan entry at all, this closes that gap too. The read-only entry
|
||||
display gained matching `Wertzeile`s (`Werkstatt`, `Kosten` — "noch offen" when unset, matching the panel's
|
||||
placeholder wording — `Durchgeführte Arbeiten`), replacing the old `Betrieb`/`Notiz` rows.
|
||||
|
||||
`DatensatzPopup.tsx`'s Wartungsplan CSV export (built the same day as the bug was found, section AC/AD) had
|
||||
worked around the mismatch with an `as Record<string, unknown>` cast and an explanatory comment reading the
|
||||
real field names directly; both are now redundant and removed — it reads `e.werkstatt`/`e.kosten` through
|
||||
the corrected type like any other field.
|
||||
|
||||
`src/tests/beispieldaten.ts`'s two Wartungsplan fixture entries updated from `betrieb` to `werkstatt`
|
||||
(no `kosten`/`arbeiten` needed adding — the existing tests don't assert on them). No dedicated
|
||||
`Service.test.ts` exists; grepped the whole `src/` tree for other `betrieb`/`notiz` consumers before and
|
||||
after — `Einstellungen.tsx`/`Uebersicht.tsx` only pass the raw `buch` array through as `ServicebuchEintrag[]`
|
||||
without touching individual field names, so neither needed a change.
|
||||
|
||||
Verified: `tsc --noEmit` clean, full suite green at 146/146 (no new tests needed — a schema/naming
|
||||
alignment, not new behavior), `vite build` succeeds. **Not live-verified against a real backend** — same
|
||||
structural limitation as section AD immediately above (no running companion-app session with backend auth
|
||||
this round). The underlying stored data was never wrong (the panel and backend always used
|
||||
`werkstatt`/`kosten`/`arbeiten`); this was a companion-app-only frontend read/write bug, so no data migration
|
||||
is needed — an existing Wartungsplan entry will display correctly the next time companion-app is rebuilt and
|
||||
deployed with this fix.
|
||||
|
||||
## AF. Item 11: Setup-Menü zeigt jetzt den zugeordneten Sensor statt einer statischen Beschreibung (2026.8.28.4)
|
||||
|
||||
Panel only - the Setup menu (sensor-role mapping) is an HA-panel-only concept, no companion-app equivalent
|
||||
exists to port this to (companion-app consumes the backend's already-resolved state, it has no Setup UI at
|
||||
all - see the parity rule's own carve-out for this exact case).
|
||||
|
||||
Every field row in Setup used to show a static, developer-written description under the label ("on = Fahrt
|
||||
läuft, erkennt Fahrtbeginn/-ende.", "Optional, vom CAN..."). Owner: replace it with something read from the
|
||||
*actual* currently-mapped sensor instead - its technical ID (just the part after `fmm003_`, e.g.
|
||||
`testintegratoin_engine_ignition_or_acc_status` instead of the full
|
||||
`binary_sensor.testcar_b9_fmm003_testintegratoin_engine_ignition_or_acc_status`) and its live output value
|
||||
with unit (`[on/off]`, `[%]`, `[l]`, `[km]`...), positioned next to the headline rather than stacked below it.
|
||||
|
||||
New `entitaetIdKurz(entityId)` (strips the domain prefix, then finds `"fmm003_"` case-insensitively and
|
||||
returns everything after it - falls back to the domain-free ID if `"fmm003_"` isn't present, for other
|
||||
brands/integrations) and `entitaetWertKurz(entityId)` (current `state` + `unit_of_measurement`, `null` for
|
||||
`unavailable`/`unknown`/unmapped - the exact same zustand logic `entitaetZeilenMarkup()` already used for
|
||||
the search-result list, just for one entity instead of a whole candidate list). `setupErwarteterSensor()`
|
||||
combines both into one string (`"<id-suffix> · <value> <unit>"`), used in `setupFeldzeile()`'s header in
|
||||
place of the old `feld.hinweis` read. List-type fields (four positions, e.g. door/window sensors) show
|
||||
nothing in the shared header - there's no single "the" sensor to reference there, each position has its own
|
||||
mapping and its own combo below.
|
||||
|
||||
`feld.hinweis` itself is untouched in the backend `FELDER` catalog (`einstellungen.py`) - still valid data,
|
||||
simply no longer read at this one call site; not removed, since removing it wasn't asked and nothing else
|
||||
depends on knowing it's now unused.
|
||||
|
||||
CSS: `.setup-feld-kopf` was `flex-direction: column` (label above the old description) - changed to a row
|
||||
(`justify-content: space-between`) so the new content sits beside the label as requested, with
|
||||
`text-overflow: ellipsis` on the right-hand span since a long ID-suffix-plus-value string won't always fit.
|
||||
|
||||
Verified live in `audi_ha_test` (not just read from source - this file's own section S lesson): four real
|
||||
rows read via the shadow DOM after opening Setup for real (`ZUENDUNG_SENSOR` →
|
||||
`"testintegratoin_engine_ignition_or_acc_status · off"`, `KM_SENSOR` → `"...mileage_read_from_can · 21302
|
||||
km"`, etc.) - label and the new span measured on the identical Y-coordinate (`sameRow: true`), confirming
|
||||
the row layout actually changed, not just the text content. Confirmed a list field (`TUER_SENSOREN`,
|
||||
"Türen") renders with no header span at all, as designed. `node --check` clean, manifest bumped to
|
||||
`2026.8.28.4`, `audi_ha_test` restarted and confirmed clean via log.
|
||||
|
||||
---
|
||||
|
||||
## Working conventions (observed — keep them)
|
||||
|
||||
@@ -79,25 +79,16 @@ function exportieren(
|
||||
)
|
||||
return
|
||||
}
|
||||
// ACHTUNG: liest werkstatt/kosten, nicht ServicebuchEintrag.betrieb/notiz -
|
||||
// das ist absichtlich. Der tatsächlich gespeicherte Datensatz
|
||||
// (profil["service"]["buch"], vom Backend/Panel geschrieben) trägt
|
||||
// werkstatt/kosten/arbeiten; ServicebuchEintrag hier im Frontend nennt
|
||||
// sie betrieb/notiz und wird nirgends dazwischen übersetzt (Service.tsx
|
||||
// liest fahrzeug.service["buch"] ungemappt durch) - ein
|
||||
// Schema-Auseinanderlaufen zwischen den beiden Oberflächen, das beim Bau
|
||||
// dieses CSV-Exports auffiel, aber ein eigenes Thema ist (siehe AGENTS.md).
|
||||
// Der Export muss die echten Feldnamen lesen, sonst zeigt jede exportierte
|
||||
// Zeile eine leere Werkstatt/Kosten-Spalte.
|
||||
csvHerunterladen(
|
||||
"wartungsplan",
|
||||
["Datum", "km", "Art", "Werkstatt", "Kosten €"],
|
||||
wartungsplan.map((e) => {
|
||||
const roh = e as Record<string, unknown>
|
||||
const werkstatt = typeof roh.werkstatt === "string" ? roh.werkstatt : ""
|
||||
const kosten = typeof roh.kosten === "number" ? roh.kosten : null
|
||||
return [e.datum ?? "", e.km != null ? de(e.km) : "", e.art ?? "", werkstatt, kosten != null ? eur(kosten) : ""]
|
||||
}),
|
||||
wartungsplan.map((e) => [
|
||||
e.datum ?? "",
|
||||
e.km != null ? de(e.km) : "",
|
||||
e.art ?? "",
|
||||
e.werkstatt ?? "",
|
||||
e.kosten != null ? eur(e.kosten) : "",
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
oelMeldungsPrognose,
|
||||
oelwechselPrognose,
|
||||
} from "../daten/service"
|
||||
import { datum, de, isoTag } from "../format"
|
||||
import { datum, de, eur, isoTag } from "../format"
|
||||
import type { SeitenName } from "../navigation"
|
||||
import { SymbolInspektion, SymbolOelwechsel } from "../symbole"
|
||||
import { Leerzustand, NaviKachel, Wertzeile, Werteliste, bestaetigen } from "./bausteine"
|
||||
@@ -32,8 +32,9 @@ export interface ServicebuchEintrag {
|
||||
datum?: string
|
||||
km?: number
|
||||
art?: string
|
||||
betrieb?: string
|
||||
notiz?: string
|
||||
werkstatt?: string
|
||||
kosten?: number | null
|
||||
arbeiten?: string
|
||||
[weitere: string]: unknown
|
||||
}
|
||||
|
||||
@@ -373,18 +374,31 @@ export function Servicebuch() {
|
||||
onChange={(e) => setzeEntwurf((alt) => ({ ...alt, km: Number(e.target.value) }))}
|
||||
/>
|
||||
</Feld>
|
||||
<Feld label="Betrieb">
|
||||
<Feld label="Werkstatt">
|
||||
<input
|
||||
className="dm-eingabe"
|
||||
value={entwurf.betrieb ?? ""}
|
||||
onChange={(e) => setzeEntwurf((alt) => ({ ...alt, betrieb: e.target.value }))}
|
||||
value={entwurf.werkstatt ?? ""}
|
||||
onChange={(e) => setzeEntwurf((alt) => ({ ...alt, werkstatt: e.target.value }))}
|
||||
/>
|
||||
</Feld>
|
||||
<Feld label="Notiz" last>
|
||||
<Feld label="Kosten" unit="€">
|
||||
<input
|
||||
className="dm-eingabe"
|
||||
value={entwurf.notiz ?? ""}
|
||||
onChange={(e) => setzeEntwurf((alt) => ({ ...alt, notiz: e.target.value }))}
|
||||
inputMode="decimal"
|
||||
value={entwurf.kosten ?? ""}
|
||||
onChange={(e) =>
|
||||
setzeEntwurf((alt) => ({
|
||||
...alt,
|
||||
kosten: e.target.value === "" ? null : Number(e.target.value),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</Feld>
|
||||
<Feld label="Durchgeführte Arbeiten" last>
|
||||
<input
|
||||
className="dm-eingabe"
|
||||
value={entwurf.arbeiten ?? ""}
|
||||
onChange={(e) => setzeEntwurf((alt) => ({ ...alt, arbeiten: e.target.value }))}
|
||||
/>
|
||||
</Feld>
|
||||
<div style={{ marginTop: 14 }}>
|
||||
@@ -428,8 +442,9 @@ export function Servicebuch() {
|
||||
label="Kilometerstand"
|
||||
wert={eintrag.km != null ? `${de(eintrag.km)} km` : "—"}
|
||||
/>
|
||||
<Wertzeile label="Betrieb" wert={eintrag.betrieb || "—"} />
|
||||
{eintrag.notiz && <Wertzeile label="Notiz" wert={eintrag.notiz} />}
|
||||
<Wertzeile label="Werkstatt" wert={eintrag.werkstatt || "—"} />
|
||||
<Wertzeile label="Kosten" wert={eintrag.kosten != null ? eur(eintrag.kosten) : "noch offen"} />
|
||||
{eintrag.arbeiten && <Wertzeile label="Durchgeführte Arbeiten" wert={eintrag.arbeiten} />}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -104,8 +104,8 @@ export const beispielProfil: Profil = {
|
||||
vereinbart: {},
|
||||
autohaus: { name: "Audi Zentrum", adresse: "Musterweg 1", tel: "089 1234", mail: "a@b.de" },
|
||||
buch: [
|
||||
{ datum: "2026-02-10", km: 40000, art: "Ölwechsel", betrieb: "Audi Zentrum" },
|
||||
{ datum: "2025-03-05", km: 25000, art: "Inspektion", betrieb: "Audi Zentrum" },
|
||||
{ datum: "2026-02-10", km: 40000, art: "Ölwechsel", werkstatt: "Audi Zentrum" },
|
||||
{ datum: "2025-03-05", km: 25000, art: "Inspektion", werkstatt: "Audi Zentrum" },
|
||||
],
|
||||
},
|
||||
technik: [
|
||||
|
||||
@@ -4061,6 +4061,42 @@ function sheetMarkup() {
|
||||
/* ------------------------------------------------------------ Setup-Popup */
|
||||
const LUPE_SVG = `<svg viewBox="0 0 20 20" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><circle cx="8.5" cy="8.5" r="6"/><path d="m17 17-4.3-4.3"/></svg>`;
|
||||
|
||||
/* Nur der Teil der Entity-ID nach "fmm003_" - die tatsächliche, technische
|
||||
Kennung des zugeordneten Sensors, kürzer und aussagekräftiger als der
|
||||
volle "binary_sensor.testcar_b9_fmm003_..."-Pfad. Ohne "fmm003_" im Namen
|
||||
(z. B. eine andere Marke/Integration) bleibt der Domain-freie Rest
|
||||
stehen, statt nichts anzuzeigen. */
|
||||
function entitaetIdKurz(entityId) {
|
||||
if (!entityId) return null;
|
||||
const ohneDomain = entityId.includes(".") ? entityId.split(".").slice(1).join(".") : entityId;
|
||||
const idx = ohneDomain.toLowerCase().indexOf("fmm003_");
|
||||
return idx >= 0 ? ohneDomain.slice(idx + "fmm003_".length) : ohneDomain;
|
||||
}
|
||||
|
||||
/* Aktueller Wert + Einheit einer Entität, oder null ohne brauchbaren
|
||||
Zustand (nicht zugeordnet, unavailable/unknown) - dieselbe Zustandslogik
|
||||
wie in entitaetZeilenMarkup() unten, hier für eine einzelne Entität statt
|
||||
einer ganzen Kandidatenliste. */
|
||||
function entitaetWertKurz(entityId) {
|
||||
if (!entityId || !HASS.states[entityId]) return null;
|
||||
const zustand = HASS.states[entityId].state;
|
||||
if (zustand === "unavailable" || zustand === "unknown") return null;
|
||||
const einheit = HASS.states[entityId].attributes && HASS.states[entityId].attributes.unit_of_measurement;
|
||||
return `${zustand}${einheit ? " " + einheit : ""}`;
|
||||
}
|
||||
|
||||
/* Ersetzt den bisherigen statischen Beschreibungstext im Setup ("on = Fahrt
|
||||
läuft", "Optional, vom CAN...") - Nutzerwunsch: statt einer generischen
|
||||
Erklärung zeigt die Kopfzeile jetzt die Kennung UND die aktuelle Meldung
|
||||
des tatsächlich zugeordneten Sensors, direkt neben der Feld-Überschrift.
|
||||
Ohne Zuordnung (leer) gibt es nichts zu zeigen. */
|
||||
function setupErwarteterSensor(entityId) {
|
||||
const kurz = entitaetIdKurz(entityId);
|
||||
if (!kurz) return "";
|
||||
const wert = entitaetWertKurz(entityId);
|
||||
return wert != null ? `${kurz} · ${wert}` : kurz;
|
||||
}
|
||||
|
||||
function entitaetZeilenMarkup(feldId, key, suchtext, aktuellerWert) {
|
||||
const kandidaten = entitaetKandidaten(key, suchtext, aktuellerWert);
|
||||
if (!kandidaten.length) return `<div class="entitaet-leer">Keine passende Entität gefunden</div>`;
|
||||
@@ -4096,9 +4132,13 @@ function entitaetCombo(key, idx) {
|
||||
}
|
||||
|
||||
function setupFeldzeile(feld) {
|
||||
// Bei einem Listenfeld (vier Positionen wie Tür-/Fenstersensoren) gibt es
|
||||
// keinen EINEN zugeordneten Sensor für die Kopfzeile - jede Position hat
|
||||
// ihre eigene Zuordnung, siehe die Unterfelder unten.
|
||||
const erwartet = feld.liste ? "" : setupErwarteterSensor(setupZuordnung[feld.key]);
|
||||
const kopf = `<div class="setup-feld-kopf">
|
||||
<span class="setup-feld-label">${esc(feld.label)}</span>
|
||||
<span class="setup-feld-hinweis">${esc(feld.hinweis)}</span>
|
||||
${erwartet ? `<span class="setup-feld-hinweis">${esc(erwartet)}</span>` : ""}
|
||||
</div>`;
|
||||
if (feld.liste) {
|
||||
return `<div class="setup-feldzeile">${kopf}
|
||||
|
||||
@@ -1255,9 +1255,16 @@ button.leaf:active { background: var(--tile-2); }
|
||||
.setup-gruppe-titel { font-size: 11px; letter-spacing: .1em; text-transform: uppercase; color: var(--fg3); margin-bottom: 6px; }
|
||||
.setup-feldzeile { padding: 12px 0; border-bottom: 1px solid var(--line); }
|
||||
.setup-feldzeile:last-child { border-bottom: none; }
|
||||
.setup-feld-kopf { display: flex; flex-direction: column; gap: 2px; margin-bottom: 8px; }
|
||||
.setup-feld-label { font-size: 14.5px; color: var(--fg); }
|
||||
.setup-feld-hinweis { font-size: 12.5px; color: var(--fg2); line-height: 1.4; }
|
||||
/* Nutzerwunsch: der zugeordnete Sensor (Kennung + aktueller Wert, siehe
|
||||
setupErwarteterSensor() in audi-dashboard-app.js) steht NEBEN der
|
||||
Feld-Überschrift, nicht mehr wie die frühere statische Beschreibung
|
||||
darunter - deshalb Zeile statt Spalte. */
|
||||
.setup-feld-kopf { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; margin-bottom: 8px; }
|
||||
.setup-feld-label { font-size: 14.5px; color: var(--fg); flex: 0 1 auto; }
|
||||
.setup-feld-hinweis {
|
||||
font-size: 12.5px; color: var(--fg2); line-height: 1.4; text-align: right;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 55%;
|
||||
}
|
||||
.setup-unterfelder { display: flex; flex-direction: column; gap: 12px; }
|
||||
.setup-unterfeld { display: flex; flex-direction: column; gap: 4px; }
|
||||
.setup-unterfeld-label { font-size: 12px; color: var(--fg3); }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "audi_dashboard",
|
||||
"name": "Audi Dashboard",
|
||||
"version": "2026.8.28.3",
|
||||
"version": "2026.8.28.4",
|
||||
"documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md",
|
||||
"issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues",
|
||||
"codeowners": ["@paul"],
|
||||
|
||||
Reference in New Issue
Block a user