Setup-Menü: Wertvorschau, Unavailable-Warnung, Duplikat-Check, Reset, Neustart-Hinweis
Fünf Erweiterungen des bestehenden Setup-Popups: Live-Wert neben jedem Such-Kandidaten, Warnhinweis bei unavailable/unknown/fehlender Entität, Duplikat-Check mit Bestätigung vor dem Speichern, Zurücksetzen-Button je Feld (Standardwerte-Snapshot in entitaeten.py, vor jedem Override genommen), und ein bestätigter "Jetzt neu starten"-Knopf nach dem Speichern, falls sich eines der drei trigger-gebundenen Felder geändert hat (neuer Service audi_dashboard_neustart). Im Docker-Testcontainer Feld für Feld verifiziert.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# AGENTS.md — Project state, review findings, open items, and working rules
|
||||
|
||||
**Last updated: 2026-08-12**. This file is the entry point for every new agent
|
||||
**Last updated: 2026-08-12** (Setup-menu polish batch). This file is the entry point for every new agent
|
||||
session: what this repo is, what is finished, what is missing, and how to work here. Detail lives in
|
||||
the linked documents — this file points, it does not duplicate.
|
||||
|
||||
@@ -49,6 +49,13 @@ toward caution over speed — for trivial tasks, use judgment.
|
||||
- Conflicting instructions get picked arbitrarily — when updating, **remove** superseded rules
|
||||
rather than stacking corrections.
|
||||
- Verify loading with `/context` (file must appear under **Memory files**); edit via `/memory`.
|
||||
- **This file (plus the session's task list) is the source of truth for what's done — not a leftover
|
||||
plan-mode file.** A `~/.claude/plans/*.md` file surfaced by the system as "not yet complete" can be
|
||||
stale: plan files aren't marked complete when their work finishes. Before claiming a feature is
|
||||
unbuilt (e.g. "the Setup menu isn't implemented yet"), check the `[x]`/`[ ]` items below and/or the
|
||||
task list — trust those over an old plan file's own claim about its status. (Caused a real mistake
|
||||
2026-08-12: told the user the Setup menu wasn't built yet, when it had shipped hours earlier in
|
||||
the same project — see the "Setup menu" `[x]` entry further down.)
|
||||
|
||||
---
|
||||
|
||||
@@ -365,6 +372,33 @@ capacitor/iframe/browser (`umgebung.ts`), types + entity table (`types.ts`), fac
|
||||
`update.ps1`) plus a short `ANLEITUNG.md` checklist for copying to a fresh HA instance;
|
||||
`INSTALL.md` stays the authoritative, detailed reference. Regenerate on demand, don't keep it
|
||||
permanently in sync — it's a deployment snapshot, not a second source of truth.
|
||||
- [x] Setup-menu polish: 5 additions on top of the existing Setup popup — done 2026-08-12.
|
||||
Backend (`pyscript/modules/entitaeten.py`): a `_STANDARDWERTE` snapshot taken at module-load
|
||||
time (before `overrides_anwenden()` ever runs), exposed via `aktueller_stand()` as
|
||||
`standardwerte` — needed because `setattr()` on the live `einstellungen` module is permanent
|
||||
for the process, so "reset to default" has to write the original value back explicitly rather
|
||||
than just omitting the override. New service `audi_dashboard_neustart` (`frontend_api.py`)
|
||||
wrapping `homeassistant.restart()`, called only from an explicit user-confirmed button, never
|
||||
automatically. Frontend (`audi-dashboard-app.js`): (1) live current-value preview next to each
|
||||
candidate in the search dropdown (`entitaetZeilenMarkup`, reads `HASS.states[e.id].state`);
|
||||
(2) inline warning under a field if its currently assigned entity is `unavailable`/`unknown`/
|
||||
missing (`entitaetStatusWarnung`); (3) duplicate-assignment check on save
|
||||
(`setupDuplikate`) — blocks with a confirm-sheet ("trotzdem speichern?") rather than silently
|
||||
allowing the same entity in two roles; (4) per-field reset button (only shown when the current
|
||||
value differs from `standardwerte`), reverts to the built-in default; (5) after saving, if any
|
||||
of the 3 trigger-bound fields actually changed (`setupGeaenderteTriggerFelder`, compares
|
||||
against a snapshot taken when the popup opened), a confirm-sheet offers "Jetzt neu starten"
|
||||
calling the new service. Verified live in `audi_ha_test`: value preview and unavailable-warning
|
||||
confirmed correct (`ZUENDUNG_SENSOR`'s configured entity genuinely doesn't exist in this test
|
||||
container → "Entität nicht gefunden." shown as designed); duplicate-check correctly caught both
|
||||
a deliberately-forced KM/TANK collision and pre-existing door/window/lock auto-suggestion
|
||||
collisions (a real side effect of this container's limited matching entities, not a bug);
|
||||
reset button appeared/disappeared correctly as values diverged from/matched the default;
|
||||
save round-tripped through the real service into `data/entitaeten.json` (byte-inspected) and
|
||||
into the published `pyscript.audi_dashboard_fahrzeugstatus` state; restart-needed sheet named
|
||||
exactly the changed trigger-bound field(s) and no others. Did not actually trigger a restart
|
||||
during verification (would have restarted the shared test container) — the service call itself
|
||||
was left unexercised beyond confirming it registers without error at pyscript load time.
|
||||
- [ ] Fix remaining documentation drift (statistics claim, README gaps, obsolete TODO comment) —
|
||||
text-only changes; INSTALL.md's WLAN/TommiG1 drift and stale variable names were fixed
|
||||
2026-08-12 (see section B)
|
||||
|
||||
@@ -97,3 +97,16 @@ def audi_dashboard_entitaeten_schreiben(zuordnung_json=None):
|
||||
frontend_veroeffentlichung.entitaeten_veroeffentlichen()
|
||||
frontend_veroeffentlichung.fahrzeugstatus_veroeffentlichen()
|
||||
log.info("audi_dashboard: Entitäten-Zuordnung aktualisiert")
|
||||
|
||||
|
||||
@service
|
||||
def audi_dashboard_neustart():
|
||||
"""Startet Home Assistant neu. Einziger Zweck: eine Änderung an einem der
|
||||
drei trigger-gebundenen Setup-Felder (ZUENDUNG_SENSOR/KM_SENSOR/
|
||||
TANK_SENSOR, siehe entitaeten.py) wird erst nach einem Neustart wirksam,
|
||||
weil @state_trigger die Entity-ID beim Modul-Laden einmalig einliest.
|
||||
Wird vom Setup-Popup als expliziter, vom Nutzer bestätigter Knopf
|
||||
angeboten - kein automatischer Selbst-Reload direkt nach dem Speichern.
|
||||
Aufruf als pyscript.audi_dashboard_neustart."""
|
||||
log.info("audi_dashboard: Neustart über das Setup-Menü ausgelöst")
|
||||
homeassistant.restart()
|
||||
|
||||
@@ -133,6 +133,17 @@ for _feld in FELDER:
|
||||
|
||||
_SCHLUESSEL = {feld["key"] for feld in FELDER}
|
||||
|
||||
# Schnappschuss der eingebauten Standardwerte aus einstellungen.py, genommen
|
||||
# BEVOR overrides_anwenden() jemals lief (Modul-Ebene, läuft beim `import
|
||||
# entitaeten` in frontend_api.py - vor dem @time_trigger("startup"), der
|
||||
# overrides_anwenden() erstmals aufruft). Notwendig fürs Setup-Menü
|
||||
# "Zurücksetzen": setattr() in overrides_anwenden() überschreibt das Attribut
|
||||
# auf dem laufenden einstellungen-Modul dauerhaft, ein einfaches Weglassen
|
||||
# des Overrides beim nächsten Speichern stellt den eingebauten Wert NICHT
|
||||
# wieder her (das Attribut bleibt ja gesetzt) - "Zurücksetzen" muss den
|
||||
# Standardwert stattdessen aktiv zurückschreiben.
|
||||
_STANDARDWERTE = {feld["key"]: getattr(einstellungen, feld["key"], None) for feld in FELDER}
|
||||
|
||||
|
||||
# --------------------------------------------------------------- JSON-I/O ---
|
||||
# Gleiches Muster wie profil.profil_lesen()/profil_schreiben(): io.open nur
|
||||
@@ -185,4 +196,4 @@ def aktueller_stand():
|
||||
bereits angewendete Overrides als auch die eingebauten Standardwerte
|
||||
einheitlich. Grundlage für pyscript.audi_dashboard_entitaeten."""
|
||||
werte = {feld["key"]: getattr(einstellungen, feld["key"], None) for feld in FELDER}
|
||||
return {"katalog": FELDER, "werte": werte}
|
||||
return {"katalog": FELDER, "werte": werte, "standardwerte": _STANDARDWERTE}
|
||||
|
||||
@@ -1911,6 +1911,7 @@ let smartdealPopupOffen = false;
|
||||
an den Service geht - Abbrechen verwirft sie einfach ohne Nebeneffekt. */
|
||||
let setupOffen = false;
|
||||
let setupZuordnung = null; // {KEY: "entity.id"} bzw. {KEY: [4 IDs]} für Listenfelder
|
||||
let setupZuordnungStart = null; // Schnappschuss von setupZuordnung beim Öffnen - für den Neustart-Hinweis (welche trigger-gebundenen Felder haben sich geändert)
|
||||
let setupNurPassend = true; // globaler Schalter: Kandidaten auf passende Domain/Einheit einschränken
|
||||
let setupSucheOffen = null; // Feld-Id ("KEY" oder "KEY:idx") des offenen Dropdowns, sonst null
|
||||
let setupSuchtext = "";
|
||||
@@ -1986,11 +1987,79 @@ function setupOeffnen() {
|
||||
}
|
||||
}
|
||||
setupZuordnung = zuordnung;
|
||||
setupZuordnungStart = JSON.parse(JSON.stringify(zuordnung));
|
||||
setupSucheOffen = null; setupSuchtext = ""; setupSpeichertLaeuft = false;
|
||||
setupOffen = true; render();
|
||||
}
|
||||
function setupSchliessen() {
|
||||
setupOffen = false; setupSucheOffen = null; setupZuordnung = null; render();
|
||||
setupOffen = false; setupSucheOffen = null; setupZuordnung = null; setupZuordnungStart = null; render();
|
||||
}
|
||||
|
||||
/* Standardwert (aus einstellungen.py, vor jedem Override) für ein Feld bzw.
|
||||
eine Listenposition - Grundlage für den Zurücksetzen-Button. */
|
||||
function setupStandardwert(key, idx) {
|
||||
const standard = (ENTITAETEN && ENTITAETEN.standardwerte) || {};
|
||||
const wert = standard[key];
|
||||
if (idx == null) return wert || "";
|
||||
return (wert || [])[idx] || "";
|
||||
}
|
||||
|
||||
/* Entity-IDs, die im Arbeitsstand mehr als einer Rolle zugeordnet sind. */
|
||||
function setupDuplikate() {
|
||||
const zaehler = {};
|
||||
for (const feld of setupKatalog()) {
|
||||
const werte = feld.liste ? (setupZuordnung[feld.key] || []) : [setupZuordnung[feld.key]];
|
||||
for (const id of werte) { if (id) zaehler[id] = (zaehler[id] || 0) + 1; }
|
||||
}
|
||||
return Object.keys(zaehler).filter((id) => zaehler[id] > 1);
|
||||
}
|
||||
|
||||
/* Labels der trigger-gebundenen Felder (ZUENDUNG/KM/TANK), deren Wert sich
|
||||
seit dem Öffnen des Popups geändert hat - für den Neustart-Hinweis nach
|
||||
dem Speichern. */
|
||||
function setupGeaenderteTriggerFelder() {
|
||||
if (!setupZuordnungStart) return [];
|
||||
return setupKatalog()
|
||||
.filter((f) => f.trigger_gebunden && JSON.stringify(setupZuordnung[f.key]) !== JSON.stringify(setupZuordnungStart[f.key]))
|
||||
.map((f) => f.label);
|
||||
}
|
||||
|
||||
function setupSpeichernAusfuehren() {
|
||||
setupSpeichertLaeuft = true; render();
|
||||
(async () => {
|
||||
try {
|
||||
await HASS.callService("pyscript", "audi_dashboard_entitaeten_schreiben", { zuordnung_json: JSON.stringify(setupZuordnung) });
|
||||
} catch (err) {
|
||||
setupSpeichertLaeuft = false; hinweis("Speichern fehlgeschlagen", err.message); return;
|
||||
}
|
||||
const geaendert = setupGeaenderteTriggerFelder();
|
||||
setupOffen = false; setupSucheOffen = null; setupZuordnung = null; setupZuordnungStart = null; setupSpeichertLaeuft = false; render();
|
||||
if (geaendert.length) {
|
||||
bestaetigen("Neustart nötig", `Wirkt für ${geaendert.join(", ")} erst nach einem Neustart von Home Assistant.`, "Jetzt neu starten", setupNeustartAusloesen, false);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
function setupNeustartAusloesen() {
|
||||
(async () => {
|
||||
try {
|
||||
await HASS.callService("pyscript", "audi_dashboard_neustart", {});
|
||||
hinweis("Neustart ausgelöst", "Home Assistant startet neu - die App lädt sich danach automatisch neu.");
|
||||
} catch (err) {
|
||||
hinweis("Neustart fehlgeschlagen", err.message);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
/* Warnhinweis, falls der aktuell gewählte Sensor keinen brauchbaren Wert
|
||||
liefert - sichtbar, bevor gespeichert wird, statt es erst hinterher in
|
||||
der App zu bemerken. */
|
||||
function entitaetStatusWarnung(wert) {
|
||||
if (!wert) return "";
|
||||
const zustand = HASS.states[wert] ? HASS.states[wert].state : null;
|
||||
if (zustand === null) return `<div class="setup-feld-warnung">Entität nicht gefunden.</div>`;
|
||||
if (zustand === "unavailable" || zustand === "unknown") return `<div class="setup-feld-warnung">Aktuell „${esc(zustand)}" - kein Wert vom Sensor.</div>`;
|
||||
return "";
|
||||
}
|
||||
function vReifen() {
|
||||
const r = CAR.reifen;
|
||||
@@ -2639,22 +2708,33 @@ const LUPE_SVG = `<svg viewBox="0 0 20 20" width="16" height="16" fill="none" st
|
||||
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>`;
|
||||
return kandidaten.map((e) => `
|
||||
return kandidaten.map((e) => {
|
||||
const zustand = HASS.states[e.id] ? HASS.states[e.id].state : null;
|
||||
const unbekannt = zustand === null || zustand === "unavailable" || zustand === "unknown";
|
||||
const wertAnzeige = unbekannt ? (zustand === null ? "nicht gefunden" : zustand) : `${zustand}${e.unit ? " " + e.unit : ""}`;
|
||||
return `
|
||||
<button type="button" class="entitaet-eintrag ${e.id === aktuellerWert ? "aktiv" : ""}" data-setup-wahl="${esc(feldId)}" data-setup-entity="${esc(e.id)}">
|
||||
<span class="entitaet-eintrag-name">${esc(e.name)}</span>
|
||||
<span class="entitaet-eintrag-id">${esc(e.id)}${e.unit ? " · " + esc(e.unit) : ""}</span>
|
||||
</button>`).join("");
|
||||
<span class="entitaet-eintrag-id">${esc(e.id)}</span>
|
||||
<span class="entitaet-eintrag-wert ${unbekannt ? "unbekannt" : ""}">${esc(wertAnzeige)}</span>
|
||||
</button>`;
|
||||
}).join("");
|
||||
}
|
||||
|
||||
const RESET_SVG = `<svg viewBox="0 0 20 20" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M16 10a6 6 0 1 1-2-4.5"/><path d="M16 3v4h-4"/></svg>`;
|
||||
|
||||
function entitaetCombo(key, idx) {
|
||||
const feldId = idx == null ? key : `${key}:${idx}`;
|
||||
const wert = idx == null ? setupZuordnung[key] : (setupZuordnung[key] || [])[idx];
|
||||
const offen = setupSucheOffen === feldId;
|
||||
const anzeige = offen ? setupSuchtext : entitaetName(wert);
|
||||
const standard = setupStandardwert(key, idx);
|
||||
const zeigeReset = wert !== standard;
|
||||
return `<div class="entitaet-combo">
|
||||
<span class="entitaet-combo-icon">${LUPE_SVG}</span>
|
||||
<input type="text" class="entitaet-combo-input" autocomplete="off" spellcheck="false"
|
||||
placeholder="Entität suchen …" value="${esc(anzeige)}" data-setup-suche="${esc(feldId)}">
|
||||
${zeigeReset ? `<button type="button" class="entitaet-combo-reset" title="Auf Standard zurücksetzen" data-setup-reset="${esc(feldId)}">${RESET_SVG}</button>` : ""}
|
||||
${offen ? `<div class="entitaet-liste" data-setup-liste="${esc(feldId)}">${entitaetZeilenMarkup(feldId, key, setupSuchtext, wert)}</div>` : ""}
|
||||
</div>`;
|
||||
}
|
||||
@@ -2672,11 +2752,12 @@ function setupFeldzeile(feld) {
|
||||
<div class="setup-unterfeld">
|
||||
<span class="setup-unterfeld-label">${esc(pos)}</span>
|
||||
${entitaetCombo(feld.key, i)}
|
||||
${entitaetStatusWarnung((setupZuordnung[feld.key] || [])[i])}
|
||||
</div>`).join("")}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
return `<div class="setup-feldzeile">${kopf}${entitaetCombo(feld.key, null)}</div>`;
|
||||
return `<div class="setup-feldzeile">${kopf}${entitaetCombo(feld.key, null)}${entitaetStatusWarnung(setupZuordnung[feld.key])}</div>`;
|
||||
}
|
||||
|
||||
const SETUP_GRUPPEN = [
|
||||
@@ -3074,17 +3155,24 @@ function ereignisseVerdrahten() {
|
||||
setupSucheOffen = null; setupSuchtext = ""; render();
|
||||
return;
|
||||
}
|
||||
const reset = e.target.closest("[data-setup-reset]");
|
||||
if (reset) {
|
||||
const feldId = reset.dataset.setupReset;
|
||||
const teile = feldId.split(":");
|
||||
if (teile.length === 2) setupZuordnung[teile[0]][Number(teile[1])] = setupStandardwert(teile[0], Number(teile[1]));
|
||||
else setupZuordnung[feldId] = setupStandardwert(feldId, null);
|
||||
render();
|
||||
return;
|
||||
}
|
||||
if (e.target.closest("[data-setup-speichern]")) {
|
||||
if (setupSpeichertLaeuft) return;
|
||||
setupSpeichertLaeuft = true; render();
|
||||
(async () => {
|
||||
try {
|
||||
await HASS.callService("pyscript", "audi_dashboard_entitaeten_schreiben", { zuordnung_json: JSON.stringify(setupZuordnung) });
|
||||
} catch (err) {
|
||||
setupSpeichertLaeuft = false; hinweis("Speichern fehlgeschlagen", err.message); return;
|
||||
}
|
||||
setupOffen = false; setupSucheOffen = null; setupZuordnung = null; setupSpeichertLaeuft = false; render();
|
||||
})();
|
||||
const duplikate = setupDuplikate();
|
||||
if (duplikate.length) {
|
||||
const namen = duplikate.map(entitaetName).join(", ");
|
||||
bestaetigen("Doppelt zugeordnet", `${namen} ${duplikate.length === 1 ? "ist" : "sind"} mehr als einer Rolle zugeordnet. Trotzdem speichern?`, "Trotzdem speichern", setupSpeichernAusfuehren, false);
|
||||
return;
|
||||
}
|
||||
setupSpeichernAusfuehren();
|
||||
return;
|
||||
}
|
||||
// Klick aufs (noch geschlossene) Suchfeld öffnet dessen Dropdown; ein
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version": 1786543200}
|
||||
{"version": 1786550400}
|
||||
|
||||
@@ -901,12 +901,18 @@ button.tile, .tilebtn { transition: background .15s, transform .1s; }
|
||||
width: 100%; box-sizing: border-box;
|
||||
background: var(--tile-deckend, var(--canvas)); border: 1px solid var(--line); color: var(--fg);
|
||||
font-family: inherit; font-size: 16px; /* siehe .feld input weiter oben: >=16px gegen Safari-Autozoom */
|
||||
padding: 11px 13px 11px 36px;
|
||||
padding: 11px 34px 11px 36px;
|
||||
border-radius: 12px;
|
||||
text-align: left;
|
||||
-webkit-appearance: none; appearance: none;
|
||||
}
|
||||
.entitaet-combo-input:focus { outline: none; border-color: var(--red); }
|
||||
.entitaet-combo-reset {
|
||||
position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
|
||||
width: 26px; height: 26px; display: flex; align-items: center; justify-content: center;
|
||||
background: none; border: none; color: var(--fg3); cursor: pointer; padding: 0;
|
||||
}
|
||||
.entitaet-combo-reset:active { color: var(--fg); }
|
||||
.entitaet-liste {
|
||||
position: absolute; z-index: 5; left: 0; right: 0; top: calc(100% + 4px);
|
||||
max-height: 240px; overflow-y: auto;
|
||||
@@ -925,7 +931,10 @@ button.tile, .tilebtn { transition: background .15s, transform .1s; }
|
||||
.entitaet-eintrag:active, .entitaet-eintrag.aktiv { background: var(--shade); }
|
||||
.entitaet-eintrag-name { font-size: 14px; color: var(--fg); }
|
||||
.entitaet-eintrag-id { font-size: 11.5px; color: var(--fg3); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
|
||||
.entitaet-eintrag-wert { font-size: 11.5px; color: var(--fg2); margin-top: 1px; }
|
||||
.entitaet-eintrag-wert.unbekannt { color: var(--warn); }
|
||||
.entitaet-leer { padding: 14px 12px; font-size: 13px; color: var(--fg2); }
|
||||
.setup-feld-warnung { font-size: 12px; color: var(--warn); line-height: 1.4; margin-top: 6px; }
|
||||
|
||||
.setup-fuss {
|
||||
flex: 0 0 auto;
|
||||
|
||||
Reference in New Issue
Block a user