Rollende Zeitraeume, zwei Gesten, CI-Symbole und das Screening faellt nach (2026.9.2.8-.12)

Gemeldet vom Eigentuemer an der realen Instanz, in einem Zug abgearbeitet.

STATISTIK. "Monat kleiner als Woche" war kein Rechenfehler, sondern die
Definition: am 02.09. begann die Kalenderwoche am 31.08., der Monat erst am
01.09. Umgestellt auf rollende Fenster (1/7/30/365 Tage), damit 1 ⊂ 7 ⊂ 30 ⊂
365 an jedem Tag gilt. "Jahr" meint damit nicht mehr das Kalenderjahr - so
entschieden. Zwei neue Tests halten die Verschachtelung fest.

Aus dem Langzeitverbrauch sind Herleitung und Erklaerabsatz aus der ANSICHT
genommen, nicht aus der Rechnung; das Datum des letzten Tankstops ebenfalls.

UEBERSICHT. Unter "Letzter Tankvorgang" steht das Datum statt der Tankstelle.

BILDER. Die Draufsicht war der einzige sichtbare Bildplatz ohne Zugang -
bilder.py kannte den Dateinamen, BILDER/BILDPLAETZE nicht. Jetzt ein Platz wie
jeder andere. Die Vorschau in den Einstellungen schneidet Fotos nicht mehr an,
und der Bildbereich der Fahrzeugseite steht eingerueckt statt randlos.

ZAHNRAD. Es lag mit seiner 44px-Trefferflaeche 21px im Kachelinhalt und fing
die obere Haelfte des ersten Knopfes ab (Versicherung, "Inland"). Die Kopfzeile
bekommt jetzt die Hoehe, die es braucht.

SCREENING. Lief ausschliesslich bei einer Aenderung des Kilometerstands, also
nur waehrend der Fahrt - im Stand blieben Ortsnamen und Verbrauch liegen. Jetzt
zusaetzlich einmal nach dem Start, und danach faellt es selbst nach, solange
ein Lauf sein Ortsbudget aufbraucht UND dabei etwas aufloest. Kein Dauertakt:
die Kette endet, sobald ein Lauf mit uebrigem Budget durchkommt.

GESTEN. Zum Loeschen nach links wischen, jetzt auch im Radarchiv (das Panel
hatte die Mechanik, das Archiv war nie angeschlossen; die App hatte sie gar
nicht). Vom linken Rand nach rechts wischen = zurueck, jetzt auch in der App,
mit denselben Zahlen wie im Panel und in beiden mit sichtbarer Rueckmeldung.

CI-SYMBOLE. Das Kachel-Zahnrad ist settings-s - im Panel stand der Pfad
sechsmal wortgleich im Markup, jetzt einmal als CI.settingsS. Das
handgezeichnete Zahnrad der Seitenleiste ist ersetzt und geloescht. Die
Zapfsaeule des Tanken-Reiters ist fuel-station-s, Faktor 0,84 um die
Rastermitte - live per getBBox() gemessen, damit sie auf dieselbe senkrechte
Spanne kommt wie Statistik.

Zurueckgenommen: die Titel der Unterseiten stehen wieder mittig. Zentriert ist
Apples HIG fuer Navigationsleisten; die linksbuendigen Titel sind die der
Hauptbereiche und folgen der anderen Regel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-02 15:47:32 +02:00
parent e28abdce25
commit 38e7731a3d
23 changed files with 911 additions and 135 deletions
@@ -1 +1 @@
{"version":"2026.9.2.7","sha256":"55a0da035cc4143bf5a52fb02112f5d89cec606b73a63a1911e1a56f6b63df38","bytes":265969,"gebaut":"2026-09-02T10:27:19Z"}
{"version":"2026.9.2.12","sha256":"2d28d29f526a130188efe816fc5cb8054f3f1afaf86f61eb371dc617753de29c","bytes":266536,"gebaut":"2026-09-02T13:40:40Z"}
@@ -340,6 +340,11 @@ const BILDER = [
{ name: "Cockpit", label: "Innenraum Cockpit", datei: "cockpit.webp" },
{ name: "Scheinwerfer", label: "Scheinwerfer", datei: "scheinwerfer.webp" },
{ name: "Sitze", label: "Innenraum Sitze", datei: "sitze.webp" },
// Steht auf der Seite "Fahrzeug" über den Sicherheitszeilen. Bis zum
// 02.09.2026 war sie der einzige sichtbare Bildplatz, der in den
// Einstellungen NICHT auswählbar war - hochladen ließ sie sich damit gar
// nicht (gemeldet vom Eigentümer). bilder.py kannte den Dateinamen längst.
{ name: "Draufsicht", label: "Auto von oben", datei: "draufsicht.webp" },
];
let bild = 0;
const SEITE_WINTER = { label: "Auto Seite mit Winterrädern", datei: "seitenansicht-winter.webp" };
@@ -1274,6 +1279,16 @@ function standortKachel() {
/* Symbole aus dem Audi-CI-Satz (assets/icons/) - hier eingebettet, weil die
Panel-Datei ohne zusaetzliche Abrufe auskommen soll. */
const CI = {
/* fuel-station-s aus dem Audi-CI-Satz (24er-Raster) - die Zapfsaeule des
Tanken-Reiters. Ersetzt seit dem 02.09.2026 eine handgezeichnete Saeule.
Die 48er-Variante hiesse fuel-station-l und wird nicht gebraucht: die
Reiter zeichnen mit 21 bzw. 26px, also im 24er-Raster. */
fuelStationS: '<path fill="currentColor" d="M4 3.5A2.5 2.5 0 0 1 6.5 1h6A2.5 2.5 0 0 1 15 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.5H15v11h1v1H3v-1h1zM5 22h9V3.5c0-.83-.67-1.5-1.5-1.5h-6C5.67 2 5 2.67 5 3.5zm1-12h7v1H6z"/>',
/* settings-s aus dem Audi-CI-Satz (24er-Raster) - das Zahnrad in der
rechten oberen Ecke einer Kachel. Stand bis zum 02.09.2026 sechsmal
wortgleich im Markup; die 48er-Variante hiesse settings-l und wird hier
nirgends gebraucht, das Zahnrad wird immer 17px dargestellt. */
settingsS: '<path fill="currentColor" d="M9.4 2h4.2v2.84c.37.13.75.27 1.11.44L16.7 3.3l3 3.01-1.98 1.99c.17.36.31.74.44 1.11H21v4.3h-2.84c-.13.37-.27.75-.44 1.11l2 2-3.03 2.89-1.98-1.98c-.36.17-.74.31-1.11.44V21H9.4v-2.84a9 9 0 0 1-1.11-.44L6.3 19.7l-3-3.01 1.98-1.99a9 9 0 0 1-.44-1.11H2V9.4h2.84c.13-.37.27-.75.44-1.11L3.3 6.3l3.01-3L8.3 5.28c.36-.17.74-.31 1.11-.44V2Zm1 1v2.56l-.34.11c-.61.2-1.16.4-1.6.66l-.34.2L6.3 4.71 4.7 6.3l1.83 1.82-.2.34c-.27.44-.45.99-.66 1.6l-.11.34H3v2.2h2.56l.11.34c.2.61.4 1.16.66 1.6l.2.34-1.82 1.82 1.59 1.6 1.82-1.83.34.2c.44.27.99.45 1.6.66l.34.11V20h2.2v-2.56l.34-.11a8.3 8.3 0 0 0 1.6-.66l.34-.2 1.83 1.83 1.57-1.5-1.81-1.82.2-.34c.27-.44.45-.99.66-1.6l.11-.34H20v-2.3h-2.56l-.11-.34a8.2 8.2 0 0 0-.66-1.6l-.2-.34 1.82-1.82-1.59-1.6-1.82 1.83-.34-.2a8.2 8.2 0 0 0-1.6-.66l-.34-.11V3zm-2.9 8.5c0-2.18 1.82-4 4-4s4 1.82 4 4-1.82 4-4 4-4-1.82-4-4m4-3c-1.62 0-3 1.38-3 3s1.38 3 3 3 3-1.38 3-3-1.38-3-3-3"/>',
mapLayer: '<path fill="currentColor" d="m11.7 19.4-8.97-5.66.54-.85 8.96 5.67c.17.1.37.1.54 0l8.96-5.67.54.85-8.97 5.66a1.5 1.5 0 0 1-1.6 0m.53-3.84c.17.1.37.1.54 0l8.3-5.25-8.3-5.23a.5.5 0 0 0-.54 0l-8.3 5.23zm-.53.84-9.64-6.09 9.64-6.08a1.5 1.5 0 0 1 1.6 0l9.64 6.08-9.64 6.1c-.49.3-1.11.3-1.6 0Z"/>',
carSearch: '<path fill="currentColor" d="M1 7.8a6.16 6.16 0 0 0 6.1 6.1h.4A5.7 5.7 0 0 0 13 7.91V7.9a6.1 6.1 0 0 0-5.91-6A5.93 5.93 0 0 0 1 7.8m-1 0C0 3.92 3.23.8 7.11.9a7.1 7.1 0 0 1 6.89 7c.06 2.33-1 4.44-2.73 5.71L14.8 19H20v1.5c0 .28.22.5.5.5h2a.5.5 0 0 0 .5-.5v-7.8l-1.9-1.9-.04-.06a55 55 0 0 0-2.84-4.71c-1.42-.1-2.49-.17-3.2-.2l.04-1c.77.04 1.93.1 3.48.22l.24.02.14.2a55 55 0 0 1 2.99 4.93L24 12.3v8.2c0 .83-.67 1.5-1.5 1.5h-2a1.5 1.5 0 0 1-1.5-1.5V20h-3.55l1.67 2.54-.84.54L14.26 20 5 19.96v.54c0 .83-.67 1.5-1.5 1.5h-2A1.5 1.5 0 0 1 0 20.5v-7.08h1v7.08c0 .28.22.5.5.5h2a.5.5 0 0 0 .5-.5v-1.54l9.6.02-3.17-4.83c-.87.45-1.85.72-2.92.75H7.1c-3.88 0-7-3.13-7.1-7.09zm5.47-2.75c1.86-.12 3.37-.2 4.52-.24l.03 1c-1.08.03-2.5.1-4.24.22L4.51 8c-.27.43-.6 1-1 1.7l-.87-.48c.4-.72.74-1.3 1.03-1.76q.6-.96 1.41-2.19l.14-.2z"/>',
gps: '<path fill="currentColor" d="M12.5 3.51V6.5h-1V3.51a8.46 8.46 0 0 0-7.99 7.99H6.5v1H3.51a8.46 8.46 0 0 0 7.99 7.99V17.5h1v2.99a8.46 8.46 0 0 0 7.99-7.99H17.5v-1h2.99a8.46 8.46 0 0 0-7.99-7.99m0-1a9.46 9.46 0 0 1 8.99 8.99h2.01v1h-2.01a9.46 9.46 0 0 1-8.99 8.99v2.01h-1v-2.01a9.46 9.46 0 0 1-8.99-8.99H.5v-1h2.01a9.46 9.46 0 0 1 8.99-8.99V.5h1z"/>',
@@ -1486,7 +1501,7 @@ function teaser() {
<span class="v">${streckeText(t.distance_km, "—")}<small>${fahrtVerbrauch(t) != null ? de(fahrtVerbrauch(t), 1) + " l/100 km" : "—"}</small></span>
<svg class="chev" viewBox="0 0 6 10"><polyline points="1,1 5,5 1,9"/></svg></button>` : ""}
${f ? `<button class="leaf" style="padding-left:0;border-bottom:0" data-go="fill:${f.tank_id}">
<span class="k">Letzter Tankvorgang<small>${esc(f.station_name || "")}</small></span>
<span class="k">Letzter Tankvorgang<small>${f.ts ? dedat(new Date(f.ts)) : ""}</small></span>
<span class="v">${f.liters != null ? de(f.liters, 2) + " l" : ""}<small>${f.fuel_total_eur != null ? eur(f.fuel_total_eur) + " €" : ""}</small></span>
<svg class="chev" viewBox="0 0 6 10"><polyline points="1,1 5,5 1,9"/></svg></button>` : ""}</div>`;
}
@@ -1848,7 +1863,7 @@ function vSicherheit() {
/* C6: Draufsicht oben, grüne Sammelmeldung, dann die vier Sammelzeilen -
dieselbe Reihenfolge wie der Fahrzeugstatus in der myAudi-App. */
return `
<div class="szene bleed"><div class="carbox">${bildMitPlatzhalter(`/local/bilder/draufsicht.webp?v=${bildVersion}`, "draufsicht.webp", "Draufsicht", "carfix klein")}</div></div>
<div class="szene bleed schmal"><div class="carbox">${bildMitPlatzhalter(`/local/bilder/draufsicht.webp?v=${bildVersion}`, "draufsicht.webp", "Draufsicht", "carfix klein")}</div></div>
<div class="tile" style="display:flex;align-items:center;gap:12px;padding:16px 20px">
${statusKreis(g)}
<span style="font-size:15px;color:var(--fg)">${esc(sammelText)}</span>
@@ -2404,7 +2419,7 @@ function vService() {
<div class="tile">
<span class="label">Autohaus</span>
<button class="zahnrad" data-go="werkstatt:x" aria-label="Angaben ändern">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M9.4 2h4.2v2.84c.37.13.75.27 1.11.44L16.7 3.3l3 3.01-1.98 1.99c.17.36.31.74.44 1.11H21v4.3h-2.84c-.13.37-.27.75-.44 1.11l2 2-3.03 2.89-1.98-1.98c-.36.17-.74.31-1.11.44V21H9.4v-2.84a9 9 0 0 1-1.11-.44L6.3 19.7l-3-3.01 1.98-1.99a9 9 0 0 1-.44-1.11H2V9.4h2.84c.13-.37.27-.75.44-1.11L3.3 6.3l3.01-3L8.3 5.28c.36-.17.74-.31 1.11-.44V2Zm1 1v2.56l-.34.11c-.61.2-1.16.4-1.6.66l-.34.2L6.3 4.71 4.7 6.3l1.83 1.82-.2.34c-.27.44-.45.99-.66 1.6l-.11.34H3v2.2h2.56l.11.34c.2.61.4 1.16.66 1.6l.2.34-1.82 1.82 1.59 1.6 1.82-1.83.34.2c.44.27.99.45 1.6.66l.34.11V20h2.2v-2.56l.34-.11a8.3 8.3 0 0 0 1.6-.66l.34-.2 1.83 1.83 1.57-1.5-1.81-1.82.2-.34c.27-.44.45-.99.66-1.6l.11-.34H20v-2.3h-2.56l-.11-.34a8.2 8.2 0 0 0-.66-1.6l-.2-.34 1.82-1.82-1.59-1.6-1.82 1.83-.34-.2a8.2 8.2 0 0 0-1.6-.66l-.34-.11V3zm-2.9 8.5c0-2.18 1.82-4 4-4s4 1.82 4 4-1.82 4-4 4-4-1.82-4-4m4-3c-1.62 0-3 1.38-3 3s1.38 3 3 3 3-1.38 3-3-1.38-3-3-3"/></svg>
${ciSVG(CI.settingsS, 17)}
</button>
<div style="font-size:15px;margin-top:12px">${esc(sv.autohaus.name)}</div>
<span class="label" style="margin-top:8px">${esc(sv.autohaus.adresse)}</span>
@@ -2655,7 +2670,7 @@ function vVers() {
return `
<div class="tile"><span class="label">Versicherung - Schaden Notruf 24/7</span>
<button class="zahnrad" data-go="notruf:x" aria-label="Rufnummern ändern">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M9.4 2h4.2v2.84c.37.13.75.27 1.11.44L16.7 3.3l3 3.01-1.98 1.99c.17.36.31.74.44 1.11H21v4.3h-2.84c-.13.37-.27.75-.44 1.11l2 2-3.03 2.89-1.98-1.98c-.36.17-.74.31-1.11.44V21H9.4v-2.84a9 9 0 0 1-1.11-.44L6.3 19.7l-3-3.01 1.98-1.99a9 9 0 0 1-.44-1.11H2V9.4h2.84c.13-.37.27-.75.44-1.11L3.3 6.3l3.01-3L8.3 5.28c.36-.17.74-.31 1.11-.44V2Zm1 1v2.56l-.34.11c-.61.2-1.16.4-1.6.66l-.34.2L6.3 4.71 4.7 6.3l1.83 1.82-.2.34c-.27.44-.45.99-.66 1.6l-.11.34H3v2.2h2.56l.11.34c.2.61.4 1.16.66 1.6l.2.34-1.82 1.82 1.59 1.6 1.82-1.83.34.2c.44.27.99.45 1.6.66l.34.11V20h2.2v-2.56l.34-.11a8.3 8.3 0 0 0 1.6-.66l.34-.2 1.83 1.83 1.57-1.5-1.81-1.82.2-.34c.27-.44.45-.99.66-1.6l.11-.34H20v-2.3h-2.56l-.11-.34a8.2 8.2 0 0 0-.66-1.6l-.2-.34 1.82-1.82-1.59-1.6-1.82 1.83-.34-.2a8.2 8.2 0 0 0-1.6-.66l-.34-.11V3zm-2.9 8.5c0-2.18 1.82-4 4-4s4 1.82 4 4-1.82 4-4 4-4-1.82-4-4m4-3c-1.62 0-3 1.38-3 3s1.38 3 3 3 3-1.38 3-3-1.38-3-3-3"/></svg>
${ciSVG(CI.settingsS, 17)}
</button>
<a class="aktion" href="tel:${esc(v.service.replace(/\s/g, ""))}"
style="display:block;text-decoration:none;margin-top:14px;text-align:center">
@@ -2666,7 +2681,7 @@ function vVers() {
</div>
<div class="tile"><span class="label">Jahresbeitrag ab ${dedat(new Date(v.gueltigAb))}</span>
<button class="zahnrad" data-go="beitrag:x" aria-label="Beitrag anpassen">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M9.4 2h4.2v2.84c.37.13.75.27 1.11.44L16.7 3.3l3 3.01-1.98 1.99c.17.36.31.74.44 1.11H21v4.3h-2.84c-.13.37-.27.75-.44 1.11l2 2-3.03 2.89-1.98-1.98c-.36.17-.74.31-1.11.44V21H9.4v-2.84a9 9 0 0 1-1.11-.44L6.3 19.7l-3-3.01 1.98-1.99a9 9 0 0 1-.44-1.11H2V9.4h2.84c.13-.37.27-.75.44-1.11L3.3 6.3l3.01-3L8.3 5.28c.36-.17.74-.31 1.11-.44V2Zm1 1v2.56l-.34.11c-.61.2-1.16.4-1.6.66l-.34.2L6.3 4.71 4.7 6.3l1.83 1.82-.2.34c-.27.44-.45.99-.66 1.6l-.11.34H3v2.2h2.56l.11.34c.2.61.4 1.16.66 1.6l.2.34-1.82 1.82 1.59 1.6 1.82-1.83.34.2c.44.27.99.45 1.6.66l.34.11V20h2.2v-2.56l.34-.11a8.3 8.3 0 0 0 1.6-.66l.34-.2 1.83 1.83 1.57-1.5-1.81-1.82.2-.34c.27-.44.45-.99.66-1.6l.11-.34H20v-2.3h-2.56l-.11-.34a8.2 8.2 0 0 0-.66-1.6l-.2-.34 1.82-1.82-1.59-1.6-1.82 1.83-.34-.2a8.2 8.2 0 0 0-1.6-.66l-.34-.11V3zm-2.9 8.5c0-2.18 1.82-4 4-4s4 1.82 4 4-1.82 4-4 4-4-1.82-4-4m4-3c-1.62 0-3 1.38-3 3s1.38 3 3 3 3-1.38 3-3-1.38-3-3-3"/></svg>
${ciSVG(CI.settingsS, 17)}
</button>
<div class="fig" style="font-size:46px;margin-top:12px">${eur(v.beitrag)}<span class="unit"></span></div>
<span class="label" style="margin-top:14px">${esc(v.zahlweise)} · enthält ${eur(v.steueranteil)} Versicherungsteuer (${esc(v.steuersatz)})</span>
@@ -2681,7 +2696,7 @@ function vVers() {
</div>
<div class="tile"><span class="label">Vertrag</span>
<button class="zahnrad" data-go="vertrag:x" aria-label="Vertrag bearbeiten">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M9.4 2h4.2v2.84c.37.13.75.27 1.11.44L16.7 3.3l3 3.01-1.98 1.99c.17.36.31.74.44 1.11H21v4.3h-2.84c-.13.37-.27.75-.44 1.11l2 2-3.03 2.89-1.98-1.98c-.36.17-.74.31-1.11.44V21H9.4v-2.84a9 9 0 0 1-1.11-.44L6.3 19.7l-3-3.01 1.98-1.99a9 9 0 0 1-.44-1.11H2V9.4h2.84c.13-.37.27-.75.44-1.11L3.3 6.3l3.01-3L8.3 5.28c.36-.17.74-.31 1.11-.44V2Zm1 1v2.56l-.34.11c-.61.2-1.16.4-1.6.66l-.34.2L6.3 4.71 4.7 6.3l1.83 1.82-.2.34c-.27.44-.45.99-.66 1.6l-.11.34H3v2.2h2.56l.11.34c.2.61.4 1.16.66 1.6l.2.34-1.82 1.82 1.59 1.6 1.82-1.83.34.2c.44.27.99.45 1.6.66l.34.11V20h2.2v-2.56l.34-.11a8.3 8.3 0 0 0 1.6-.66l.34-.2 1.83 1.83 1.57-1.5-1.81-1.82.2-.34c.27-.44.45-.99.66-1.6l.11-.34H20v-2.3h-2.56l-.11-.34a8.2 8.2 0 0 0-.66-1.6l-.2-.34 1.82-1.82-1.59-1.6-1.82 1.83-.34-.2a8.2 8.2 0 0 0-1.6-.66l-.34-.11V3zm-2.9 8.5c0-2.18 1.82-4 4-4s4 1.82 4 4-1.82 4-4 4-4-1.82-4-4m4-3c-1.62 0-3 1.38-3 3s1.38 3 3 3 3-1.38 3-3-1.38-3-3-3"/></svg>
${ciSVG(CI.settingsS, 17)}
</button>
<dl class="rows" style="margin-top:8px">
<div class="row"><dt>Gesellschaft</dt><dd>${esc(v.gesellschaft)}</dd></div>
@@ -2827,7 +2842,7 @@ function vSteuer() {
return `
<div class="tile" style="margin-top:26px"><span class="label">Kfz-Steuer</span>
<button class="zahnrad" data-go="steuer:x" aria-label="Kfz-Steuer bearbeiten">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M9.4 2h4.2v2.84c.37.13.75.27 1.11.44L16.7 3.3l3 3.01-1.98 1.99c.17.36.31.74.44 1.11H21v4.3h-2.84c-.13.37-.27.75-.44 1.11l2 2-3.03 2.89-1.98-1.98c-.36.17-.74.31-1.11.44V21H9.4v-2.84a9 9 0 0 1-1.11-.44L6.3 19.7l-3-3.01 1.98-1.99a9 9 0 0 1-.44-1.11H2V9.4h2.84c.13-.37.27-.75.44-1.11L3.3 6.3l3.01-3L8.3 5.28c.36-.17.74-.31 1.11-.44V2Zm1 1v2.56l-.34.11c-.61.2-1.16.4-1.6.66l-.34.2L6.3 4.71 4.7 6.3l1.83 1.82-.2.34c-.27.44-.45.99-.66 1.6l-.11.34H3v2.2h2.56l.11.34c.2.61.4 1.16.66 1.6l.2.34-1.82 1.82 1.59 1.6 1.82-1.83.34.2c.44.27.99.45 1.6.66l.34.11V20h2.2v-2.56l.34-.11a8.3 8.3 0 0 0 1.6-.66l.34-.2 1.83 1.83 1.57-1.5-1.81-1.82.2-.34c.27-.44.45-.99.66-1.6l.11-.34H20v-2.3h-2.56l-.11-.34a8.2 8.2 0 0 0-.66-1.6l-.2-.34 1.82-1.82-1.59-1.6-1.82 1.83-.34-.2a8.2 8.2 0 0 0-1.6-.66l-.34-.11V3zm-2.9 8.5c0-2.18 1.82-4 4-4s4 1.82 4 4-1.82 4-4 4-4-1.82-4-4m4-3c-1.62 0-3 1.38-3 3s1.38 3 3 3 3-1.38 3-3-1.38-3-3-3"/></svg>
${ciSVG(CI.settingsS, 17)}
</button>
<dl class="rows" style="margin-top:10px">
<div class="row"><dt>Betrag</dt><dd>${eur(t.betrag)} <small>${t.zeitraum === "jährlich" ? "im Jahr" : esc(t.zeitraum)}</small></dd></div>
@@ -2937,7 +2952,10 @@ function vEinst() {
const info = bildInfo(startIndex());
return `<div style="margin-top:12px">
<div data-bildklick="${esc(info.datei)}">
${bildMitPlatzhalter(info.src, info.datei, info.label, "carfix", null, true)}
${/* "klein" = das Bild bestimmt die Hoehe, statt im 16:9-Kasten
beschnitten zu werden (siehe .carfix.klein). Wer hier prueft,
ob das richtige Foto hochgeladen ist, muss es ganz sehen. */""
}${bildMitPlatzhalter(info.src, info.datei, info.label, "carfix klein", null, true)}
</div>
<input type="file" accept="image/*" hidden data-bildupload="${esc(info.datei)}">
${bildMenuOffen === info.datei ? `
@@ -3399,7 +3417,7 @@ function vReifen() {
return `
<div class="tile"><span class="label">Anzugsmoment der Radschrauben</span>
<button class="zahnrad" data-nmauf aria-label="Anzugsmoment ändern">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M9.4 2h4.2v2.84c.37.13.75.27 1.11.44L16.7 3.3l3 3.01-1.98 1.99c.17.36.31.74.44 1.11H21v4.3h-2.84c-.13.37-.27.75-.44 1.11l2 2-3.03 2.89-1.98-1.98c-.36.17-.74.31-1.11.44V21H9.4v-2.84a9 9 0 0 1-1.11-.44L6.3 19.7l-3-3.01 1.98-1.99a9 9 0 0 1-.44-1.11H2V9.4h2.84c.13-.37.27-.75.44-1.11L3.3 6.3l3.01-3L8.3 5.28c.36-.17.74-.31 1.11-.44V2Zm1 1v2.56l-.34.11c-.61.2-1.16.4-1.6.66l-.34.2L6.3 4.71 4.7 6.3l1.83 1.82-.2.34c-.27.44-.45.99-.66 1.6l-.11.34H3v2.2h2.56l.11.34c.2.61.4 1.16.66 1.6l.2.34-1.82 1.82 1.59 1.6 1.82-1.83.34.2c.44.27.99.45 1.6.66l.34.11V20h2.2v-2.56l.34-.11a8.3 8.3 0 0 0 1.6-.66l.34-.2 1.83 1.83 1.57-1.5-1.81-1.82.2-.34c.27-.44.45-.99.66-1.6l.11-.34H20v-2.3h-2.56l-.11-.34a8.2 8.2 0 0 0-.66-1.6l-.2-.34 1.82-1.82-1.59-1.6-1.82 1.83-.34-.2a8.2 8.2 0 0 0-1.6-.66l-.34-.11V3zm-2.9 8.5c0-2.18 1.82-4 4-4s4 1.82 4 4-1.82 4-4 4-4-1.82-4-4m4-3c-1.62 0-3 1.38-3 3s1.38 3 3 3 3-1.38 3-3-1.38-3-3-3"/></svg>
${ciSVG(CI.settingsS, 17)}
</button>
<div class="fig" style="font-size:34px;margin-top:12px">${de(r.anzugsmoment)}<span class="unit">Nm</span></div>
${nmOffen ? `
@@ -3438,12 +3456,17 @@ function vReifenArchiv() {
return `<div class="tile"><span class="label">Archiv</span>
${archiv.map((a, i) => {
const satzLabel = a.satz === "winter" ? "Winterräder" : "Sommerräder";
return `<button class="acc-h" aria-expanded="false" data-acc="rfarch${i}"><span class="mark"></span>
/* Nach links wischen zum Löschen, wie in der Fahrten- und
Tankvorgangsliste. Der Löschknopf in der aufgeklappten Zeile bleibt:
am Rechner ist er der kürzere Weg. Umschlossen ist nur die
Kopfzeile, nicht der aufgeklappte Rumpf - sonst verschöbe ein
waagerechtes Ziehen in einem Eingabefeld die ganze Zeile. */
return swipeZeile(`rfarch:${a.id}`, `<button class="acc-h" aria-expanded="false" data-acc="rfarch${i}"><span class="mark"></span>
<span class="k">${satzLabel}</span>
<span class="v" style="display:flex;flex-direction:column;align-items:flex-end;gap:3px">
<span style="color:var(--fg);font-size:15px">${a.km != null ? de(a.km) + " km" : ""}</span>
<small>Ersetzt am ${dedat(new Date(a.ersetzt_am))}</small>
</span></button>
</span></button>`) + `
<div class="acc-b" id="rfarch${i}">
<div class="feld"><label>Ersetzt am</label><input type="date" value="${esc(a.ersetzt_am ?? "")}" data-rfa="${a.id}|ersetzt_am"></div>
<div class="feld"><label>Gefahrene Strecke</label>
@@ -3682,18 +3705,24 @@ function statTile(id, label, fig, unit, extra, body) {
km im selben Zeitraum), da einzelne Fahrten keinen eigenen Verbrauchswert
tragen - erst über einen Zeitraum gemittelt ergibt das einen sinnvollen
l/100km-Wert. */
/* Länge der rollenden Zeiträume in Tagen.
Bis zum 02.09.2026 waren die Spannen kalendarisch: Jahr ab 1. Januar, Monat
ab dem Ersten, Woche ab Montag, Tag ab Mitternacht. Das ergab regelmäßig
einen unmöglichen Vergleich am 02.09.2026 stand in der realen Instanz
"Monat" UNTER "Woche", weil die Woche am Montag, dem 31.08., begann und
damit zwei Tage mitzählte, die der Monat nicht kennt. Zwischen Monat und
Jahr passiert dasselbe am Jahreswechsel.
Rollende Fenster können das nicht: 1 7 30 365 gilt an jedem Tag des
Jahres. Der Preis ist, dass "Jahr" nicht mehr das Kalenderjahr meint,
sondern die letzten 365 Tage so entschieden vom Eigentümer am 02.09.2026.
Gleichlautend in companion-app/src/daten/statistik.ts (ZEITRAUM_TAGE). */
const ZEITRAUM_TAGE = { jahr: 365, monat: 30, woche: 7, tag: 1 };
function periodStart(art) {
// Alles, was je erfasst wurde - kein Zeitraum, sondern die Summe.
if (art === "absolut") return new Date(0);
const jetzt = new Date();
if (art === "jahr") return new Date(jetzt.getFullYear(), 0, 1);
if (art === "monat") return new Date(jetzt.getFullYear(), jetzt.getMonth(), 1);
if (art === "woche") {
const d = new Date(jetzt); d.setHours(0, 0, 0, 0);
d.setDate(d.getDate() - ((d.getDay() + 6) % 7));
return d;
}
const d = new Date(jetzt); d.setHours(0, 0, 0, 0); return d;
return new Date(Date.now() - ZEITRAUM_TAGE[art] * 24 * 60 * 60 * 1000);
}
const fahrtenSeit = (art) => TRIPS.filter((t) => t.distance_km != null && new Date(t.ts_start) >= periodStart(art));
const tankSeit = (art) => FILLS.filter((f) => f.liters != null && new Date(f.ts) >= periodStart(art));
@@ -3797,6 +3826,12 @@ function vStat() {
const nachtKm = kmJahr - tagKm;
const arbeitFahrten = fJahr.filter((t) => t.art === "arbeitsweg"), privatFahrten = fJahr.filter((t) => t.art !== "arbeitsweg");
const arbeitKm = sum(arbeitFahrten, (t) => t.distance_km), privatKm = kmJahr - arbeitKm;
/* Die Herleitung des Langzeitwerts - getankte Liter, Bezugsstrecke, letzter
Tankstop, der Unplausibel-Hinweis und der Erklärabsatz - ist am
02.09.2026 auf Ansage des Eigentümers aus der ANSICHT genommen worden,
nicht aus der Rechnung: langzeitverbrauch() rechnet unverändert weiter
und liefert lv.liter/lv.km/lv.basis nach wie vor. In der Kachel bleiben
der Wert selbst und die Näherung je Zeitraum. */
const lv = langzeitverbrauch();
return `
${statTile("s1", "Distanz und Fahrzeit", de(kmAbs), "km", de(hAbs) + " h",
@@ -3805,15 +3840,8 @@ function vStat() {
${statTile("s2", "Getankte Menge und Kosten", de(lAbs), "l", de(eAbs) + " €",
quad("Getankte Liter [l]", [[de(lJahr), "Jahr"], [de(sum(tankSeit("monat"), (f) => f.liters)), "Monat"], [de(sum(tankSeit("woche"), (f) => f.liters)), "Woche"], [de(sum(tankSeit("tag"), (f) => f.liters)), "Tag"]]) +
quad("Kraftstoffkosten [€]", [[de(eJahr), "Jahr"], [de(sum(tankSeit("monat"), (f) => f.fuel_total_eur || 0)), "Monat"], [de(sum(tankSeit("woche"), (f) => f.fuel_total_eur || 0)), "Woche"], [de(sum(tankSeit("tag"), (f) => f.fuel_total_eur || 0)), "Tag"]]))}
${statTile("s3", "Langzeitverbrauch", lv.wert, "l/100 km", lv.bis ? "bis " + dedat(lv.bis) : "",
`<dl class="rows">
<div class="row"><dt>Getankt insgesamt</dt><dd>${de(lv.liter, 1)} l</dd></div>
<div class="row"><dt>Strecke bis zum letzten Tankstop</dt><dd>${de(lv.km)} km<small>${lv.basis}</small></dd></div>
<div class="row" style="border-bottom:0"><dt>Letzter Tankstop</dt><dd>${lv.bis ? dedat(lv.bis) : ""}</dd></div>
</dl>
${lv.unplausibel ? `<span class="label" style="margin-top:16px;color:var(--warn)">Aus Litern und Strecke ergibt sich derzeit kein plausibler Wert. Sobald Tankvorgänge und Kilometerstände vollständig vorliegen, erscheint er hier.</span>` : ""}
${quad("Näherung je Fahrt, je Zeitraum [l/100 km]", [[schnittFahrtVerbrauch(fJahr), "Jahr"], [schnittFahrtVerbrauch(fMonat), "Monat"], [schnittFahrtVerbrauch(fWoche), "Woche"], [schnittFahrtVerbrauch(fTag), "Tag"]])}
<span class="label" style="margin-top:16px">Der Langzeitwert rechnet alle je getankten Liter gegen die Strecke bis zum letzten Tankstop. Die Werte je Zeitraum sind eine Näherung aus der Tankfüllstand-Differenz der einzelnen Fahrten, keine Meldung des Fahrzeugs.</span>`)}
${statTile("s3", "Langzeitverbrauch", lv.wert, "l/100 km", "",
quad("Näherung je Fahrt, je Zeitraum [l/100 km]", [[schnittFahrtVerbrauch(fJahr), "Jahr"], [schnittFahrtVerbrauch(fMonat), "Monat"], [schnittFahrtVerbrauch(fWoche), "Woche"], [schnittFahrtVerbrauch(fTag), "Tag"]]))}
${statTile("s4", "Tag und Nacht", kmJahr > 0 ? de(Math.round((tagKm / kmJahr) * 100)) : "", "% bei Tag", de(tagKm) + " km",
`<div class="bar"><i style="flex:${tagKm};background:var(--fg)"></i><i style="flex:${nachtKm};background:var(--fg3)"></i></div>
<div class="legend"><span><i style="background:var(--fg)"></i>Tag · ${de(tagKm)} km</span>
@@ -3891,6 +3919,7 @@ const LOESCH_TEXT = {
sbuch: "Wartungsplan-Eintrag löschen?",
bild: "Foto löschen?",
batt: "Messwert löschen?",
rfarch: "Archivierten Radsatz löschen?",
};
const LOESCH_HINWEIS = {
trip: "Die Fahrt wird aus dem Fahrtenbuch entfernt. Das lässt sich nicht widerrufen.",
@@ -3898,6 +3927,7 @@ const LOESCH_HINWEIS = {
sbuch: "Der Eintrag wird aus dem Wartungsplan 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.",
rfarch: "Der archivierte Radsatz wird mit seiner Laufleistung entfernt. Das lässt sich nicht widerrufen.",
};
function eintragLoeschen(art, id) {
if (art === "trip") {
@@ -3912,6 +3942,11 @@ function eintragLoeschen(art, id) {
} else if (art === "batt") {
serviceRufen("batterieverlauf_loeschen", { datum: id });
BATTERIEVERLAUF = BATTERIEVERLAUF.filter((e) => e.datum !== id);
} else if (art === "rfarch") {
// Eigener Dienst statt profilSpeichern - wie schon beim Löschknopf in der
// aufgeklappten Zeile (siehe data-reifenarchivloeschen).
serviceRufen("reifen_archiv_loeschen", { id });
CAR.reifen.archiv = (CAR.reifen.archiv || []).filter((a) => a.id !== id);
}
}
@@ -4221,7 +4256,15 @@ const ICONS = {
(.tab svg 26px/22px) automatisch im richtigen Verhältnis, ohne zwei
Stellen pflegen zu müssen. */
stat: `<g transform="translate(2.76,2.76) scale(0.77)">${CI.pollsS}</g>`,
fuel: '<path d="M4.5 20V6.2c0-.9.8-1.7 1.7-1.7h5.6c1 0 1.7.8 1.7 1.7V20"/><path d="M3 20h12.5"/><path d="M4.5 11h10.5"/><path d="M17 20v-8.5a2 2 0 0 0-2-2"/><path d="M17 11.5V7.8l-2-2.3"/>',
/* Dieselbe Behandlung wie "stat" oben, aus demselben Grund: das CI-Symbol
ist randlos gezeichnet und stuende ungeskaliert mit 22 von 24 Einheiten
Hoehe als hoechstes Symbol der Leiste da - hoeher als "Fahrten" (21,44)
und deutlich hoeher als die gezeichnete Saeule davor (15,50).
Faktor 0,84 um die Rastermitte bringt es auf 15,96 x 18,48 und damit auf
exakt dieselbe senkrechte Spanne wie "stat" (y 2,76 bis 21,24). Live
gemessen per getBBox(), Raender eingeschlossen - nicht geschaetzt. */
fuel: `<g transform="translate(1.5,1.92) scale(0.84)">${CI.fuelStationS}</g>`,
};
const TABS = [
{ id: "home", label: "Übersicht", eyebrow: "", title: "Übersicht" },
@@ -4231,7 +4274,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", "stat", "trips"]);
const ICONS_VOLL = new Set(["audi", "stat", "trips", "fuel"]);
let route = { name: "home" };
const ZURUECK = {
trip: "trips", fill: "fuel", service: "audi", reifen: "audi", ident: "audi",
@@ -4895,8 +4938,22 @@ function zurueck() {
route = ziel; render();
}
/* B3: Randwischen zurück - dieselbe Bewegung wie ein Tipp auf den Pfeil. */
/* B3: Randwischen zurück - dieselbe Bewegung wie ein Tipp auf den Pfeil.
Seit dem 02.09.2026 folgt die Seite dabei dem Finger (randZeigen): ohne
diese Rückmeldung passierte bis zum Loslassen nichts sichtbares, und wer
die Geste zu kurz zog, sah nicht, dass er sie überhaupt begonnen hatte.
Unter der Auslöseschwelle federt sie zurück - die Geste ist damit
widerrufbar, indem man den Finger zurückführt. Gleichlautend in der App
(useWischZurueck in Shell.tsx). */
let randX0 = null, randY0 = null;
function randZeigen(dx) {
const ansicht = $("#view");
if (!ansicht) return;
if (dx === null) { ansicht.style.transition = ""; ansicht.style.transform = ""; return; }
ansicht.style.transition = "none";
ansicht.style.transform = `translateX(${Math.min(140, dx)}px)`;
}
function randwischenVerdrahten() {
const phone = ROOT.querySelector(".phone");
phone.addEventListener("pointerdown", (e) => {
@@ -4905,11 +4962,19 @@ function randwischenVerdrahten() {
const links = phone.getBoundingClientRect().left;
if (e.clientX - links <= 26 && !istTab(route.name)) { randX0 = e.clientX; randY0 = e.clientY; }
});
phone.addEventListener("pointerup", (e) => {
if (randX0 !== null && e.clientX - randX0 > 62 && Math.abs(e.clientY - randY0) < 60) zurueck();
randX0 = null;
phone.addEventListener("pointermove", (e) => {
if (randX0 === null || !e.isPrimary) return;
const dx = e.clientX - randX0;
// Senkrecht gemeint: die Geste gehört dem Scrollen, nicht uns.
if (Math.abs(e.clientY - randY0) > 60) { randX0 = null; randZeigen(null); return; }
if (dx > 0) randZeigen(dx);
});
phone.addEventListener("pointercancel", () => { randX0 = null; });
phone.addEventListener("pointerup", (e) => {
const ausloesen = randX0 !== null && e.clientX - randX0 > 62 && Math.abs(e.clientY - randY0) < 60;
randX0 = null; randZeigen(null);
if (ausloesen) zurueck();
});
phone.addEventListener("pointercancel", () => { randX0 = null; randZeigen(null); });
}
/* -------------------------------------------------------------- Aktionen
@@ -6273,7 +6338,7 @@ class AudiDashboardPanel extends HTMLElement {
<div class="sync" id="sync"><svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="m18.65 4.95 2.56 2.55-2.56 2.55-.7-.7L19.29 8H7.2A5.24 5.24 0 0 0 2 13.38V14H1v-.6A6.24 6.24 0 0 1 7.2 7h12.1l-1.34-1.35.7-.7ZM23 11.59A6.24 6.24 0 0 1 16.8 18H4.7l1.35 1.35-.7.7-2.56-2.55 2.56-2.55.7.7L4.71 17H16.8a5.24 5.24 0 0 0 5.2-5.38V11h1v.6Z"/></svg><span></span></div></div>
<span class="kopfgegengewicht" aria-hidden="true"></span>
<button class="themebtn" id="theme" hidden aria-hidden="true"><svg id="themeicon" viewBox="0 0 20 20" width="15" height="15"></svg></button>
<button class="profilbtn" id="ringsbtn" aria-label="Einstellungen"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.6 1.6 0 0 0 .33 1.76l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.6 1.6 0 0 0-1.76-.33 1.6 1.6 0 0 0-1 1.47V21a2 2 0 1 1-4 0v-.1a1.6 1.6 0 0 0-1-1.47 1.6 1.6 0 0 0-1.77.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.6 1.6 0 0 0 4.6 15a1.6 1.6 0 0 0-1.47-1H3a2 2 0 1 1 0-4h.1a1.6 1.6 0 0 0 1.47-1 1.6 1.6 0 0 0-.33-1.77l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.6 1.6 0 0 0 9 4.6 1.6 1.6 0 0 0 10 3.13V3a2 2 0 1 1 4 0v.1a1.6 1.6 0 0 0 1 1.47 1.6 1.6 0 0 0 1.77-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.6 1.6 0 0 0 19.4 9v.04a1.6 1.6 0 0 0 1.47 1H21a2 2 0 1 1 0 4h-.1a1.6 1.6 0 0 0-1.5 1Z"/></svg></button>
<button class="profilbtn" id="ringsbtn" aria-label="Einstellungen">${ciSVG(CI.settingsS, 19)}</button>
</div>
<div class="ptr" id="ptr"><svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M20.06 8.36c-1.21-3.33-4.3-6.46-8.5-6.46-3.69 0-6.97 2-8.55 5.28a9.47 9.47 0 0 0 4.23 12.77A9.56 9.56 0 0 0 20 15.72l-.89-.44a8.56 8.56 0 0 1-11.43 3.77A8.47 8.47 0 0 1 3.9 7.62a8.38 8.38 0 0 1 7.65-4.72c3.8 0 6.64 2.94 7.66 6.1h-5.66v1h7.5V2.5h-1v5.86Z"/></svg><span>Ziehen zum Aktualisieren</span></div>
<main id="view"><div class="tile" style="margin-top:20px;display:flex;align-items:center;gap:10px"><span class="lade-spinner" aria-hidden="true"></span><span class="label" style="margin:0">Lädt </span></div></main>
@@ -120,6 +120,9 @@ html,body{height:100%}
.phone:has(.back.on) .kopfgegengewicht{display:block;order:10;flex:none;width:34px;height:34px;margin-right:-8px}
.phone:has(.back.on) .topbar{padding:54px 16px 6px;gap:8px}
.phone:has(.back.on) .head{align-items:center}
/* Zentriert nach Apples HIG (kleiner Titel in der Navigationsleiste einer
Detailseite; der große, linksbündige gilt nur auf oberster Ebene). Siehe
.dm-kopf--unterseite in der App - dort steht die ganze Begründung. */
.phone:has(.back.on) .title{font-size:17px;font-weight:400;letter-spacing:-.01em;text-align:center}
.phone:has(.back.on) .sync{margin-top:2px;font-size:12.5px}
@@ -212,7 +212,11 @@
border: none; background: var(--tile); color: var(--fg2);
cursor: pointer; padding: 0; flex: 0 0 auto;
}
.profilbtn svg { width: 19px; height: 19px; fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }
/* Flaeche statt Kontur, seit das Zahnrad settings-s aus dem CI-Satz ist
(02.09.2026): der Pfad bringt sein eigenes fill="currentColor" mit, und ein
geerbter stroke wuerde zusaetzlich um jede Flaeche gezeichnet - dieselbe
Falle wie bei .voll/RahmenFlaeche in der App. */
.profilbtn svg { width: 19px; height: 19px; fill: currentColor; stroke: none; }
.profilbtn:hover { color: var(--fg); }
.profilbtn:active { background: var(--tile-2); color: var(--fg); transform: scale(.94); }
@@ -331,7 +335,7 @@ button.tile, .tilebtn { transition: background .15s, transform .1s; }
}
.zahnrad {
position: absolute;
top: 11px; right: 11px;
top: 8px; right: 8px;
width: 44px; height: 44px;
display: inline-flex; align-items: center; justify-content: center;
border: none; border-radius: 50%;
@@ -341,9 +345,14 @@ button.tile, .tilebtn { transition: background .15s, transform .1s; }
.zahnrad:hover { background: var(--tile-2); color: var(--fg); }
.zahnrad:active { background: var(--tile-2); color: var(--fg); transform: scale(.92); }
.zahnrad svg { width: 17px; height: 17px; }
/* Titelzeile nie unter dem Zahnrad/Chevron hindurchlaufen lassen */
/* Titelzeile nie unter dem Zahnrad/Chevron hindurchlaufen lassen - und die
Zeile hoch genug für die 44px-Trefferfläche des Zahnrads machen. Ohne die
Mindesthöhe ragte es in den Kachelinhalt und lag auf dem ersten Knopf
(Versicherung, "Inland · <Nummer>"; gemeldet 02.09.2026). Gleiche Regel in
der App (.dm-kachel--zahnrad). */
.tile:has(> .zahnrad) > .label:first-child,
.tile:has(> .go) > .label:first-child { padding-right: 44px; }
.tile:has(> .go) > .label:first-child { padding-right: 48px; }
.tile:has(> .zahnrad) > .label:first-child { min-height: 34px; }
/* C1: Abschnittstitel gemischt geschrieben, 14 px, ohne Sperrung - wie in der
aktuellen myAudi-App. Versalien trägt nur noch .eyebrow über dem Seitentitel. */
@@ -651,6 +660,17 @@ img.carfix.klein { aspect-ratio: auto; height: auto; }
hoeher zu ruecken - auf Ansage des Besitzers steht er wieder in voller
Groesse, weil ein Streifen nicht erkennen laesst, was dort hingehoert. */
.bildbox.bild-fehlt.carfix.klein { aspect-ratio: 16 / 8; height: auto; }
/* Die Draufsicht auf der Fahrzeugseite ist eine Beigabe, keine Szene: sie
steht ueber den Sicherheitszeilen und soll die Zeilen nicht nach unten
draengen. Statt randlos ueber die volle Breite (rund 374px auf einem
Telefon, mit 16:8 also 187px hoch) steht sie eingerueckt und mittig -
60% Breite, die Hoehe geht im selben Verhaeltnis mit auf rund 112px
zurueck. Auf Ansage des Eigentuemers am 02.09.2026 ("Draufsicht-Foto
Bereich ist zu gross"): schmaler, nicht flacher - flacher haette bei
einem Foto, das nicht sehr breit ist, oben und unten geschnitten. */
.szene.bleed.schmal { margin: 0; padding: 22px 0 26px; }
.szene.bleed.schmal .carbox { width: 60%; margin: 0 auto; padding: 0; }
.szene.bleed:has(.bild-fehlt)::before { display: none; }
.szene.bleed:has(.bild-fehlt) { margin-left: 0; margin-right: 0; }
.szene.bleed:has(.bild-fehlt) .carbox { padding: 0; }
@@ -124,10 +124,20 @@ class Geokodierung:
self._sperre = asyncio.Lock()
self._zuletzt = 0.0
self.rest = PRO_DURCHLAUF
self.neu = 0
def durchlauf_beginnen(self) -> None:
"""Setzt das Anfragenbudget fuer einen Screening-Lauf zurueck."""
"""Setzt Anfragenbudget und Fortschrittszaehler fuer einen Lauf zurueck.
`neu` zaehlt die in diesem Lauf DAZUGEKOMMENEN Ergebnisse - nur
gespeicherte, also auch das ehrliche "dort ist nichts", aber kein
Netzfehler. Das Screening entscheidet daran, ob ein zweiter Lauf noch
etwas bringen kann (screening._nachfassen_planen): Budget aufgebraucht
UND Fortschritt heisst "es liegt noch Arbeit an"; Budget aufgebraucht
OHNE Fortschritt heisst "die Gegenstelle antwortet gerade nicht" - und
dann waere Nachfassen im Minutentakt genau das Falsche."""
self.rest = PRO_DURCHLAUF
self.neu = 0
# ------------------------------------------------------ Zwischenspeicher
@@ -200,6 +210,7 @@ class Geokodierung:
return None
speicher[key] = ergebnis
self.neu += 1
await self._hass.async_add_executor_job(self._sichern, dict(speicher))
return ergebnis
@@ -131,6 +131,9 @@ class Koordinator:
self.tiefststand_liter: float | None = None
# Seit wann das Fahrzeug steht (Zeit des Geraets). None = unbekannt.
self.geparkt_seit_ts: datetime.datetime | None = None
# Ein eingeplanter zweiter Screening-Lauf, siehe screening.py
# (_nachfassen_planen). None = keiner offen.
self.screening_nachlauf: CALLBACK_TYPE | None = None
# Ergebnis des letzten "Auf Update prüfen"/"Update installieren" -
# nur auf Tastendruck gesetzt (siehe dienste.py), nie im normalen
# Veröffentlichungs-Takt neu berechnet, damit keine automatischen
@@ -186,8 +189,16 @@ class Koordinator:
# vollständig - siehe die Begründung von trigger_neu_binden() oben.
await identitaet.fin_ableiten_und_uebernehmen(self.hass, self.zuordnung, self.ablage)
await self.profil_veroeffentlichen()
# Genau hier kommt eine frisch aktualisierte Integration an, und was
# ihre neuen Felder nachtragen koennen, soll nicht bis zur naechsten
# Fahrt warten. Reicht das Ortsbudget eines Laufs nicht, plant das
# Screening selbst einen zweiten ein (screening._nachfassen_planen).
await screening.durchfuehren(self)
async def beenden(self) -> None:
if self.screening_nachlauf is not None:
self.screening_nachlauf()
self.screening_nachlauf = None
for abmelden in (*self._takte, *self._beobachter):
abmelden()
self._takte.clear()
@@ -1,7 +1,7 @@
{
"domain": "audi_dashboard",
"name": "Audi Dashboard",
"version": "2026.9.2.7",
"version": "2026.9.2.12",
"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"],
+46 -1
View File
@@ -18,7 +18,9 @@ verlauf.py ausschließt.
Läuft nach jedem Fahrtende und zusätzlich bei jeder Änderung des
Kilometerstand-Sensors - unabhängig vom Fahrtende-Ereignis selbst, eben weil
der Wert auch erst mit der nächsten Fahrt eintreffen kann.
der Wert auch erst mit der nächsten Fahrt eintreffen kann. Dazu einmal nach
dem Start von Home Assistant, und danach so lange erneut, wie ein Lauf mehr
Ortsauflösungen bräuchte, als sein Budget hergibt (_nachfassen_planen).
VORBEHALT: Der GPS-Fallback (km_quelle="gps") ist weiterhin nicht umgesetzt,
da die Adressauflösung ein eigenes Arbeitspaket ist. Fahrten ohne passenden
@@ -31,6 +33,8 @@ import datetime
import logging
from typing import TYPE_CHECKING
from homeassistant.helpers.event import async_call_later
from . import reifen, verbrauchskorrektur
from .verlauf import (
UNPLAUSIBLE_KMH,
@@ -51,6 +55,10 @@ if TYPE_CHECKING:
_LOGGER = logging.getLogger(__name__)
# Wie lange nach einem Lauf gewartet wird, dessen Ortsbudget nicht gereicht
# hat. Siehe _nachfassen_planen().
NACHFASSEN_S = 60
# Wie weit vor/zurück gesucht wird, ausgehend von Fahrtbeginn bzw. -ende.
# 48h deckt auch länger stehende Fahrzeuge ab, ohne bei jedem Screening die
# komplette Historie zu laden.
@@ -214,6 +222,43 @@ async def durchfuehren(k: Koordinator) -> None:
await _lueckenlos_verketten(k)
await k.fahrten_veroeffentlichen()
_nachfassen_planen(k)
def _nachfassen_planen(k: Koordinator) -> None:
"""Plant einen zweiten Lauf, wenn dieser hier nicht gereicht hat.
Das Ortsbudget sind acht Anfragen je Lauf (PRO_DURCHLAUF in
geokodierung.py, damit Nominatims Bitte um keinen Vorratsabruf gewahrt
bleibt). Eine einzelne Fahrt braucht zwei davon - im Normalbetrieb ist das
Budget also nie das Ende. Aufgebraucht wird es genau in zwei Lagen: nach
einem Rueckblick-Import aus Home Assistant, der viele Fahrten auf einmal
anlegt, und einmalig nach einem Update, das ein neues Feld einfuehrt.
Genau dafuer und nur dafuer ist dieses Nachfassen da - kein Dauertakt.
Bedingung ist BEIDES: das Budget ist leer (es liegt also noch Arbeit an)
UND es ist in diesem Lauf mindestens ein Ort dazugekommen (die Gegenstelle
antwortet also). Kommt ein Lauf mit uebrigem Budget durch, ist alles
aufgeloest und es wird nichts mehr eingeplant - die Kette endet von
selbst. Antwortet Nominatim gerade nicht, endet sie ebenfalls, statt im
Minutentakt gegen eine Wand zu laufen; der naechste natuerliche Ausloeser
(Fahrtende, Kilometerstand, Neustart) versucht es wieder.
"""
if k.screening_nachlauf is not None:
return
if k.geokodierung.rest > 0 or k.geokodierung.neu <= 0:
return
async def _spaeter(_jetzt) -> None:
k.screening_nachlauf = None
await durchfuehren(k)
_LOGGER.debug(
"Ortsbudget aufgebraucht, %s Orte neu - weiterer Lauf in %s s",
k.geokodierung.neu,
NACHFASSEN_S,
)
k.screening_nachlauf = async_call_later(k.hass, NACHFASSEN_S, _spaeter)
async def _fahrt_screenen(k: Koordinator, km_sensor: str, fahrt: dict) -> None: