diff --git a/AGENTS.md b/AGENTS.md
index c0da38a..6cea913 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1037,6 +1037,51 @@ wraps the web app for iPhone; a PWA home-screen install is the accepted intermed
left:auto` (added last round to right-align it under its old block-flow parent) was removed since
`.radkopf-row`'s flex layout does that job now. Deployed to `audi_ha_test`, confirmed live via
screenshot for both Sommerräder (mounted) and Winterräder (unmounted) tiles.
+- [x] Ninth round the same day (2026-08-16) — "Mein Audi" image-cycle tap, still broken after the
+ seventh round's `bildWeiter()` crash fix: reproduced with a genuine mouse click (not a JS
+ `.click()` call) via screenshot and found the actual cause was one level upstream. This docker
+ test container has zero uploaded car photos, so every gallery image is in `bild-fehlt` state,
+ and `.bildbox.bild-fehlt img{display:none}` makes the `` itself un-hit-testable
+ — a real tap lands on the `.platzhalter` div instead, which the old handler
+ (`e.target.id === "carimg"`) never matched, so nothing happened. First fix wrapped the gallery
+ image in a `data-bildgalerie` container that opened the upload picker when empty and advanced
+ when a photo was loaded; the user then clarified uploads should stay exclusively in Einstellungen
+ ("Bild der Übersicht") — Mein Audi should only ever cycle. Settled on: the wrapper stays (needed
+ so taps land on something clickable regardless of `bild-fehlt` state) but always calls
+ `bildWeiter()`, no upload branch. Verified live with a real click (screenshot → click → recheck
+ `.dots`): the active dot correctly advances, no console errors beyond this environment's
+ pre-existing service-worker/404 noise.
+ Also this round: replaced the "Farbskala Ladezustand (AGM, Ruhespannung)" text-only label above
+ the battery voltage color scale with a small battery icon + "Ruhespannung" (new `.bv-skala-kopf`
+ flex row, same geometry as the header's `.sync` icon+text pattern). First attempt hand-drew a
+ generic outline battery glyph since no "battery" icon exists in this repo's local CI subset
+ (`design/assets/icons/`); the user then supplied the real `battery-12v-l.svg`/`battery-12v-s.svg`
+ files from the actual Audi CI icon set (not something this session has fetch access to — asked
+ the user rather than guessing a URL or fabricating an icon and calling it authentic). Added both
+ as `CI.battery12vL`/`CI.battery12vS` (verbatim paths, `l` on the 48-grid, `s` on the 24-grid,
+ matching the existing `poi-l`/`poi-s` size-variant convention); only the `s` variant is used here
+ via `ciSVG()` since this is a small 15px inline label icon.
+ Deployed to `audi_ha_test`, confirmed live via screenshot.
+ Also this round (user then supplied the real `edit-l.svg`/`edit-s.svg` too): added a manual
+ km-correction control to the Reifen tiles, per explicit request — a small pencil icon (Audi CI
+ `edit-s`, new `CI.editS`/`CI.editL`) to the left of each Sommerräder/Winterräder km figure, "the
+ counter shall add continuously to these km even if the user changes the km". The tricky part
+ was persistence: `configCarZuProfil()` *deliberately* strips `km` before every profile write
+ (see its own comment - km is "vom Backend fortgeschrieben", a stale browser copy must never
+ clobber a since-elapsed automatic increment), so simply mutating `CAR.reifen[k].km` and calling
+ `profilSpeichern()` like every other Reifen field does would silently drop the edit. Added a
+ dedicated backend service instead, `audi_dashboard_reifen_km_setzen(satz, km)` in
+ `reifenzaehler.py`, that only overwrites the stored counter itself and leaves
+ `reifen.referenz_odo_km` (the last-processed odometer reference) untouched - the existing
+ `kilometerstand_geaendert()` trigger always reads the counter fresh off disk before adding the
+ next delta, so continuing to accumulate from the corrected value required no other backend
+ change at all. New `kmOffen` state (which satz's correction field is open, mirroring the
+ existing `nmOffen`/`bildMenuOffen` single-open-popup pattern) plus `data-kmauf`/
+ `data-kmspeichern` handlers, styled like the existing `.montiert`/`.zahnrad` controls (44×44pt
+ invisible tap target via `::before`). Verified end-to-end: edited Sommerräder from 150→4200 km,
+ confirmed the pyscript service ran via the container's HA log, then did a full page reload
+ (forces a real backend re-fetch, not just optimistic local UI state) and the corrected value
+ was still there.
- [ ] 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); `DESIGN_REVIEW_2026-08-13.md` and `REVIEW_main_2026-08-13.md`
diff --git a/homeassistant/installationspaket/pyscript/reifenzaehler.py b/homeassistant/installationspaket/pyscript/reifenzaehler.py
index 1615d5c..3ca799a 100644
--- a/homeassistant/installationspaket/pyscript/reifenzaehler.py
+++ b/homeassistant/installationspaket/pyscript/reifenzaehler.py
@@ -105,3 +105,37 @@ def audi_dashboard_reifen_wechseln(satz=None):
return
_fortschreiben_und_veroeffentlichen(_aktueller_km(), neuer_aktiv=satz)
log.info(f"audi_dashboard: aktiver Reifensatz auf '{satz}' umgeschaltet")
+
+
+@service
+def audi_dashboard_reifen_km_setzen(satz=None, km=None):
+ """Manuelle Korrektur des gefahrenen Kilometerstands eines Reifensatzes
+ (Editier-Symbol neben der km-Anzeige in der App). Überschreibt nur den
+ gespeicherten Zähler selbst - referenz_odo_km (der zuletzt verarbeitete
+ Fahrzeug-Kilometerstand) bleibt unverändert, damit die nächste
+ Fortschreibung (kilometerstand_geaendert oben) weiterhin korrekt von hier
+ aus weiterzählt, statt auf den neuen Wert einen erneuten Delta seit dem
+ alten Stand aufzuschlagen. configCarZuProfil() im Frontend schreibt km nie
+ selbst mit (siehe Kommentar dort) - genau deshalb braucht es diesen
+ eigenen Service statt eines einfachen Profil-Überschreibens."""
+ if satz not in ("sommer", "winter"):
+ log.warning(f"audi_dashboard: ungültiger Reifensatz '{satz}'")
+ return
+ try:
+ km = round(float(km), 1)
+ except (TypeError, ValueError):
+ log.warning(f"audi_dashboard: ungültiger km-Wert '{km}'")
+ return
+ if km < 0:
+ log.warning(f"audi_dashboard: negativer km-Wert '{km}' abgelehnt")
+ return
+ p = profil.profil_lesen()
+ if p is None:
+ return
+ reifen = p.setdefault("reifen", {})
+ saetze = reifen.setdefault("saetze", {})
+ saetze.setdefault(satz, {})["km"] = km
+ profil.profil_schreiben(p)
+ frontend_veroeffentlichung.profil_veroeffentlichen()
+ _zustaende_veroeffentlichen(p)
+ log.info(f"audi_dashboard: km-Stand '{satz}' manuell auf {km} gesetzt")
diff --git a/homeassistant/installationspaket/www/audi-dashboard-app.js b/homeassistant/installationspaket/www/audi-dashboard-app.js
index 9e1cf7a..8f479e1 100644
--- a/homeassistant/installationspaket/www/audi-dashboard-app.js
+++ b/homeassistant/installationspaket/www/audi-dashboard-app.js
@@ -255,8 +255,17 @@ function carImg(modus) {
return bildMitPlatzhalter(info.src, info.datei, info.label, "carfix klein");
}
const info = bildInfo(bild);
- return bildMitPlatzhalter(info.src, info.datei, info.label, "carfix", "carimg") +
- `
${BILDER.map((_, i) => ``).join("")}
`;
+ /* data-bildgalerie: ein Tipp irgendwo auf die Kachel (Foto oder Platzhalter)
+ schaltet zum naechsten Bild weiter - Hochladen/Ersetzen/Loeschen bleibt
+ bewusst allein den Einstellungen vorbehalten (Bild der Übersicht),
+ deshalb hier kein data-bildupload/data-bildklick, nur bildWeiter().
+ Der Wrapper ist noetig, weil das selbst bei fehlendem Foto per
+ .bildbox.bild-fehlt img{display:none} unsichtbar und damit unklickbar
+ ist - der bisherige Handler (id="carimg") griff dann ins Leere. */
+ return `
`;
}
/* Bildmarke des Kraftstoffanbieters (Shell). Anders als bei den Fahrzeug-
fotos ist hier kein generischer Platzhalter-Rahmen sinnvoll, weil das Logo
@@ -947,6 +956,14 @@ const CI = {
upload: '',
fuelRange: '',
parking: '',
+ /* battery-12v-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. */
+ battery12vL: '',
+ battery12vS: '',
+ /* 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: '',
+ editS: '',
};
function ciSVG(pfad, px = 16) {
return ``;
@@ -1481,7 +1498,10 @@ function vBatterieverlauf() {
` : `
${leerText}
`}
- Farbskala Ladezustand (AGM, Ruhespannung)
+
+ ${ciSVG(CI.battery12vS, 15)}
+ Ruhespannung
+
@@ -2109,6 +2129,9 @@ let nmOffen = false;
let einrichtenOffen = false;
let bildMenuOffen = null;
let smartdealPopupOffen = false;
+// "sommer"|"winter", welcher Reifensatz gerade sein km-Korrektur-Feld offen
+// hat (Editier-Symbol neben der km-Anzeige) - null wenn keins offen ist.
+let kmOffen = null;
/* --------------------------------------------------------- Setup-Popup
Sensor-zu-Entität-Zuordnung (Einstellungen -> Fahrzeug einrichten ->
@@ -2335,12 +2358,27 @@ function vReifen() {
+
+ Korrigiert nur den bisher gefahrenen Stand - weitere Fahrten mit diesem
+ Satz zählen ab hier weiter dazu.
+
+
+
` : ""}
${bildMenuOffen === radDatei ? `
@@ -3583,7 +3621,7 @@ function ereignisseVerdrahten() {
smartdealPopupOffen = false; profilSpeichern(); render();
return;
}
- if (e.target.id === "carimg") { bildWeiter(); return; }
+ if (e.target.closest("[data-bildgalerie]")) { bildWeiter(); return; }
if (e.target.closest("[data-sbneu]")) {
CAR.service.buch.unshift({ datum: dedat(heute()), km: CAR.odoBekannt ? CAR.odo : 0, art: SERVICE_ARTEN[0], werkstatt: CAR.service.autohaus.name, kosten: null, arbeiten: "" });
entwurfIndex = 0; go("sbuch", "0"); return;
@@ -3598,6 +3636,24 @@ function ereignisseVerdrahten() {
if (!isNaN(wert) && wert > 0) { CAR.reifen.anzugsmoment = wert; profilSpeichern(); }
nmOffen = false; render(); return;
}
+ const kmauf = e.target.closest("[data-kmauf]");
+ if (kmauf) { kmOffen = kmOffen === kmauf.dataset.kmauf ? null : kmauf.dataset.kmauf; render(); return; }
+ const kmspeichern = e.target.closest("[data-kmspeichern]");
+ if (kmspeichern) {
+ const satz = kmspeichern.dataset.kmspeichern;
+ const wert = parseFloat($(`#km-${satz}`).value);
+ if (!isNaN(wert) && wert >= 0) {
+ const km = Math.round(wert * 10) / 10;
+ CAR.reifen[satz].km = km;
+ // km wird bewusst nicht ueber profilSpeichern() geschrieben (siehe
+ // Kommentar in configCarZuProfil) - eigener Service, der nur den
+ // Zaehler selbst setzt und referenz_odo_km unangetastet laesst, damit
+ // die naechste automatische Fortschreibung ab diesem neuen Stand
+ // weiterzaehlt statt ihn zu ueberschreiben.
+ serviceRufen("audi_dashboard_reifen_km_setzen", { satz, km });
+ }
+ kmOffen = null; render(); return;
+ }
if (e.target.closest("[data-neu]")) {
neuOffen = !neuOffen;
if (belegEntwurf && belegEntwurf.praefix === "neuTank") belegEntwurf = null;
diff --git a/homeassistant/installationspaket/www/audi-dashboard.css b/homeassistant/installationspaket/www/audi-dashboard.css
index 82cb2e6..af29d11 100644
--- a/homeassistant/installationspaket/www/audi-dashboard.css
+++ b/homeassistant/installationspaket/www/audi-dashboard.css
@@ -340,6 +340,10 @@ button.tile, .tilebtn { transition: background .15s, transform .1s; }
color: var(--fg);
display: block;
}
+/* Icon + Text statt reinem Fließtext-Label (z.B. Batterie-Symbol vor
+ "Ruhespannung") - dieselbe Zeilen-Geometrie wie .sync in der Kopfzeile. */
+.bv-skala-kopf { display: flex; align-items: center; gap: 6px; }
+
/* A4: Messwerte erben nie mehr das uppercase der Label-Klasse. */
.wert {
font-size: 14px;
@@ -907,6 +911,21 @@ button.tile, .tilebtn { transition: background .15s, transform .1s; }
.radkopf-row { display: flex; justify-content: space-between; align-items: flex-start; gap: var(--sp-4); }
.radkopf { display: flex; flex-direction: column; }
+/* Editier-Symbol links neben der km-Anzeige (Audi-CI edit-s) - oeffnet das
+ Korrekturfeld fuer den manuell gepflegten Kilometerstand des Reifensatzes.
+ 44x44pt Tippflaeche per unsichtbarem ::before wie bei .montiert/.zahnrad,
+ optisch bleibt nur das kleine Symbol sichtbar. */
+.km-edit {
+ position: relative;
+ display: inline-flex; align-items: center; justify-content: center;
+ width: 22px; height: 22px;
+ border: none; background: none; color: var(--fg3);
+ padding: 0; cursor: pointer; flex: 0 0 auto;
+}
+.km-edit::before { content: ""; position: absolute; inset: -11px; }
+.km-edit:hover { color: var(--fg); }
+.km-edit:active { color: var(--fg); transform: scale(.92); }
+
/* Kompakte Pille statt voller Breite (Apple-HIG-Pull-down-Maß): normale
Flusslage unter der Kachel-Überschrift/km, nicht mehr über dem Foto
überlagert (das saß nur dort, solange Überschrift+km diesen Platz noch
diff --git a/homeassistant/pyscript/reifenzaehler.py b/homeassistant/pyscript/reifenzaehler.py
index 1615d5c..3ca799a 100644
--- a/homeassistant/pyscript/reifenzaehler.py
+++ b/homeassistant/pyscript/reifenzaehler.py
@@ -105,3 +105,37 @@ def audi_dashboard_reifen_wechseln(satz=None):
return
_fortschreiben_und_veroeffentlichen(_aktueller_km(), neuer_aktiv=satz)
log.info(f"audi_dashboard: aktiver Reifensatz auf '{satz}' umgeschaltet")
+
+
+@service
+def audi_dashboard_reifen_km_setzen(satz=None, km=None):
+ """Manuelle Korrektur des gefahrenen Kilometerstands eines Reifensatzes
+ (Editier-Symbol neben der km-Anzeige in der App). Überschreibt nur den
+ gespeicherten Zähler selbst - referenz_odo_km (der zuletzt verarbeitete
+ Fahrzeug-Kilometerstand) bleibt unverändert, damit die nächste
+ Fortschreibung (kilometerstand_geaendert oben) weiterhin korrekt von hier
+ aus weiterzählt, statt auf den neuen Wert einen erneuten Delta seit dem
+ alten Stand aufzuschlagen. configCarZuProfil() im Frontend schreibt km nie
+ selbst mit (siehe Kommentar dort) - genau deshalb braucht es diesen
+ eigenen Service statt eines einfachen Profil-Überschreibens."""
+ if satz not in ("sommer", "winter"):
+ log.warning(f"audi_dashboard: ungültiger Reifensatz '{satz}'")
+ return
+ try:
+ km = round(float(km), 1)
+ except (TypeError, ValueError):
+ log.warning(f"audi_dashboard: ungültiger km-Wert '{km}'")
+ return
+ if km < 0:
+ log.warning(f"audi_dashboard: negativer km-Wert '{km}' abgelehnt")
+ return
+ p = profil.profil_lesen()
+ if p is None:
+ return
+ reifen = p.setdefault("reifen", {})
+ saetze = reifen.setdefault("saetze", {})
+ saetze.setdefault(satz, {})["km"] = km
+ profil.profil_schreiben(p)
+ frontend_veroeffentlichung.profil_veroeffentlichen()
+ _zustaende_veroeffentlichen(p)
+ log.info(f"audi_dashboard: km-Stand '{satz}' manuell auf {km} gesetzt")
diff --git a/homeassistant/www/audi-dashboard-app.js b/homeassistant/www/audi-dashboard-app.js
index 9e1cf7a..8f479e1 100644
--- a/homeassistant/www/audi-dashboard-app.js
+++ b/homeassistant/www/audi-dashboard-app.js
@@ -255,8 +255,17 @@ function carImg(modus) {
return bildMitPlatzhalter(info.src, info.datei, info.label, "carfix klein");
}
const info = bildInfo(bild);
- return bildMitPlatzhalter(info.src, info.datei, info.label, "carfix", "carimg") +
- `
${BILDER.map((_, i) => ``).join("")}
`;
+ /* data-bildgalerie: ein Tipp irgendwo auf die Kachel (Foto oder Platzhalter)
+ schaltet zum naechsten Bild weiter - Hochladen/Ersetzen/Loeschen bleibt
+ bewusst allein den Einstellungen vorbehalten (Bild der Übersicht),
+ deshalb hier kein data-bildupload/data-bildklick, nur bildWeiter().
+ Der Wrapper ist noetig, weil das selbst bei fehlendem Foto per
+ .bildbox.bild-fehlt img{display:none} unsichtbar und damit unklickbar
+ ist - der bisherige Handler (id="carimg") griff dann ins Leere. */
+ return `
`;
}
/* Bildmarke des Kraftstoffanbieters (Shell). Anders als bei den Fahrzeug-
fotos ist hier kein generischer Platzhalter-Rahmen sinnvoll, weil das Logo
@@ -947,6 +956,14 @@ const CI = {
upload: '',
fuelRange: '',
parking: '',
+ /* battery-12v-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. */
+ battery12vL: '',
+ battery12vS: '',
+ /* 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: '',
+ editS: '',
};
function ciSVG(pfad, px = 16) {
return ``;
@@ -1481,7 +1498,10 @@ function vBatterieverlauf() {
` : `
${leerText}
`}
- Farbskala Ladezustand (AGM, Ruhespannung)
+
+ ${ciSVG(CI.battery12vS, 15)}
+ Ruhespannung
+
@@ -2109,6 +2129,9 @@ let nmOffen = false;
let einrichtenOffen = false;
let bildMenuOffen = null;
let smartdealPopupOffen = false;
+// "sommer"|"winter", welcher Reifensatz gerade sein km-Korrektur-Feld offen
+// hat (Editier-Symbol neben der km-Anzeige) - null wenn keins offen ist.
+let kmOffen = null;
/* --------------------------------------------------------- Setup-Popup
Sensor-zu-Entität-Zuordnung (Einstellungen -> Fahrzeug einrichten ->
@@ -2335,12 +2358,27 @@ function vReifen() {