Add real Audi CI battery/edit icons, manual km-correction for tire sets

Replaced the hand-drawn placeholder battery icon with the real Audi CI
battery-12v-l/-s (user supplied the actual SVGs) next to "Ruhespannung",
matching the existing poi-l/poi-s size-variant convention.

Added a km-correction control to the Sommerräder/Winterräder tiles: a
pencil icon (Audi CI edit-s) opens an inline field to manually correct
the tire-set's accumulated km. The tricky part was persistence -
configCarZuProfil() deliberately never writes km back (a stale browser
copy could otherwise clobber a since-elapsed automatic increment), so
this needed its own backend service, audi_dashboard_reifen_km_setzen,
that overwrites only the stored counter and leaves referenz_odo_km
alone - the existing odometer-delta tracking in reifenzaehler.py then
continues accumulating from the corrected value on its own, no other
backend change required.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-16 23:16:08 +02:00
parent 8b3e5b2b8f
commit 6bf7435434
7 changed files with 273 additions and 10 deletions
@@ -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")
@@ -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") +
`<div class="dots" id="dots">${BILDER.map((_, i) => `<i class="${i === bild ? "on" : ""}"></i>`).join("")}</div>`;
/* 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 <img> 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 `<div data-bildgalerie>
${bildMitPlatzhalter(info.src, info.datei, info.label, "carfix", "carimg")}
</div>
<div class="dots" id="dots">${BILDER.map((_, i) => `<i class="${i === bild ? "on" : ""}"></i>`).join("")}</div>`;
}
/* 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: '<path fill="currentColor" d="M13 20h-1V9.7l-3.65 3.65-.7-.7 4.85-4.86 4.85 4.86-.7.7L13 9.71zM6 5V4h13v1z"/>',
fuelRange: '<path fill="currentColor" d="M2 3.5A2.5 2.5 0 0 1 4.5 1h6A2.5 2.5 0 0 1 13 3.5V10h.5a2.5 2.5 0 0 1 2.5 2.5v5a1.5 1.5 0 0 0 3 0V8.07a16.4 16.4 0 0 0-2.89-5.76l.78-.62a17.4 17.4 0 0 1 3.1 6.17l.01.07v9.57a2.5 2.5 0 0 1-5 0v-5c0-.83-.67-1.5-1.5-1.5H13v7h-1V3.5c0-.83-.67-1.5-1.5-1.5h-6C3.67 2 3 2.67 3 3.5V22h4v1H1v-1h1zM4 10h7v1H4zm5 14v-5h1v2h13v-2h1v5h-1v-2H10v2z"/>',
parking: '<path fill="currentColor" d="M0 2.5A2.5 2.5 0 0 1 2.5 0H23v20.5a2.5 2.5 0 0 1-2.5 2.5H0zM2.5 1C1.67 1 1 1.67 1 2.5V22h19.5c.83 0 1.5-.67 1.5-1.5V1zm11.59 6.35C13.6 7 13.02 7 12.5 7H8v6h4.5c.52 0 1.11-.01 1.59-.35.44-.3.91-1 .91-2.65 0-1.66-.47-2.34-.91-2.65M8 14v3H7V6h5.53c.5 0 1.38 0 2.13.53C15.47 7.09 16 8.16 16 10s-.53 2.9-1.34 3.47a3.6 3.6 0 0 1-2.13.53z"/>',
/* 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: '<path fill="currentColor" d="M11 11h7v4h13v-4h7v4h6v23H5V15h6zm1 4h5v-3h-5zm-6 1v21h37V16zm31-1v-3h-5v3zm-3 11v4.5h-1V26h-4.5v-1H33v-4.5h1V25h4.5v1zm-15.5 0h-8v-1h8z"/>',
battery12vS: '<path fill="currentColor" d="M5 5h4v2h6V5h4v2h3v12H2V7h3zm1 2h2V6H6zM3 8v10h18V8zm15-1V6h-2v1zm-1 6v2h-1v-2h-2v-1h2v-2h1v2h2v1zm-8 0H5v-1h4z"/>',
/* 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: '<path fill="currentColor" d="M28.99 3.6c.34-.1.73-.11 1.1.04h.02l3.61 1.81c.35.18.58.5.68.84s.11.73-.04 1.1v.01l-2.01 4.06 4.02 2.02-6.72 13.44-.9-.44 6.28-12.56-3.13-1.56-13.6 27.47-6.3 4.66v-7.8L28.15 4.27c.18-.35.5-.58.84-.68Zm2.46 7.42 2-4.02a.65.65 0 0 0 0-.41c-.05-.14-.12-.22-.17-.24l-3.58-1.8a.65.65 0 0 0-.41 0c-.14.05-.22.12-.24.17L27.02 8.8zm-4.88-1.33-13.4 26.89 4.5 2.25L31.01 11.9l-4.44-2.2ZM13 37.61v4.9l3.95-2.93z"/>',
editS: '<path fill="currentColor" d="m14.14 1.06 2.4 1.1.04.02c.23.16.42.39.52.65.1.27.13.6-.06.91l-.91 1.94 2.94 1.38-2.82 5.95-.9-.42 2.38-5.05-2.02-.95L9.5 19.8 5 23.57v-5.88l7.56-16.14.02-.03c.16-.23.39-.42.65-.52a1 1 0 0 1 .9.06Zm1.08 4.2.94-2 .02-.03v-.04a.4.4 0 0 0-.13-.16l-2.4-1.1-.02-.01h-.04a.4.4 0 0 0-.16.13L12.53 4l2.7 1.27Zm-3.12-.37L6.16 17.56l2.7 1.27L14.8 6.16 12.1 4.9ZM6 18.6v2.84l2.18-1.81L6 18.59Z"/>',
};
function ciSVG(pfad, px = 16) {
return `<svg viewBox="0 0 24 24" width="${px}" height="${px}" fill="none" aria-hidden="true">${pfad}</svg>`;
@@ -1481,7 +1498,10 @@ function vBatterieverlauf() {
` : `
<span class="label" style="margin-top:14px;color:var(--fg3)">${leerText}</span>
`}
<span class="label" style="margin-top:16px">Farbskala Ladezustand (AGM, Ruhespannung)</span>
<div class="label bv-skala-kopf" style="margin-top:16px">
${ciSVG(CI.battery12vS, 15)}
<span>Ruhespannung</span>
</div>
<div style="margin-top:8px;height:8px;border-radius:var(--r-pill);
background:linear-gradient(to right, ${punktFarbe("bad")}, ${punktFarbe("warn")}, ${punktFarbe("ok")})"></div>
<div style="display:flex;justify-content:space-between;margin-top:6px">
@@ -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() {
<div class="radkopf-row">
<div class="radkopf">
<span class="tile-headline">${satzLabel}</span>
<div class="fig" style="font-size:32px;margin-top:14px">${t.km != null ? de(t.km) : ""}<span class="unit">km</span></div>
<div class="fig" style="font-size:32px;margin-top:14px;display:flex;align-items:center;gap:8px">
<button type="button" class="km-edit" data-kmauf="${k}" aria-label="${satzLabel} Kilometerstand ändern">${ciSVG(CI.editS, 16)}</button>
${t.km != null ? de(t.km) : ""}<span class="unit">km</span>
</div>
</div>
<div data-bildklick="${radDatei}">
${bildMitPlatzhalter(`/local/bilder/${radDatei}?v=${bildVersion}`, radDatei, radLabel, "radbild")}
</div>
</div>
${kmOffen === k ? `
<div style="margin-top:14px;border-top:1px solid var(--line);padding-top:16px">
<div class="feld" style="border-bottom:0"><label for="km-${k}">Neuer Wert</label>
<span class="mitEinheit"><input id="km-${k}" type="number" step="1" min="0"
value="${t.km ?? ""}"><span>km</span></span></div>
<span class="label" style="margin-top:6px;color:var(--fg2);letter-spacing:0;
text-transform:none;font-size:12.5px;line-height:1.6">
Korrigiert nur den bisher gefahrenen Stand - weitere Fahrten mit diesem
Satz zählen ab hier weiter dazu.</span>
<button class="aktion" data-kmspeichern="${k}">Speichern</button>
<button class="aktion" data-kmauf="${k}">Abbrechen</button>
</div>` : ""}
<input type="file" accept="image/*" hidden data-bildupload="${radDatei}">
${bildMenuOffen === radDatei ? `
<div class="bildmenu-catcher" data-bildmenu-schliessen></div>
@@ -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;
@@ -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