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:
@@ -7941,3 +7941,249 @@ matching SHA-256 and `bundle-version 2026.9.2.7` in the manifest.
|
||||
build result.** Four of the five defects produced either a successful archive or a silent
|
||||
misconfiguration; only unzipping the `.ipa` and reading entitlements, binaries and plists exposed
|
||||
them.
|
||||
|
||||
---
|
||||
|
||||
## BP. Rollende Zeiträume, Gesten, CI-Symbole und das Screening fasst nach (2026.9.2.8-.12)
|
||||
|
||||
Vier Meldungen des Eigentümers an der realen Instanz, alle vom 02.09.2026.
|
||||
|
||||
### „Monat kleiner als Woche" — die Kalendergrenzen waren nicht ineinander enthalten
|
||||
|
||||
Gemeldet für **Gefahrene Distanz** und **Fahrzeit**: die Monatsspalte stand unter der Wochenspalte.
|
||||
Kein Rechenfehler, sondern die Definition. Am 02.09.2026 begann die Kalenderwoche am Montag, dem
|
||||
**31.08.**, der Kalendermonat aber erst am **01.09.** — die Woche zählte damit zwei Tage mit, die
|
||||
der Monat nicht kennt. Zwischen Monat und Jahr passiert dasselbe an jedem Jahreswechsel: eine Fahrt
|
||||
vom 30.12. gehört zur Woche, nicht zum Jahr.
|
||||
|
||||
Umgestellt auf **rollende Fenster**: Tag = 1, Woche = 7, Monat = 30, Jahr = 365 Tage zurück. So gilt
|
||||
1 ⊂ 7 ⊂ 30 ⊂ 365 an jedem Tag des Jahres, ein unmöglicher Vergleich kann gar nicht mehr entstehen.
|
||||
Der Preis, vom Eigentümer so entschieden: **„Jahr" ist nicht mehr das Kalenderjahr**, sondern die
|
||||
letzten 365 Tage — das gilt auch für die Prozentwerte in „Tag und Nacht" und „Art der Fahrten", die
|
||||
auf derselben Menge rechnen. „Absolut" bleibt unverändert alles je Erfasste.
|
||||
|
||||
`ZEITRAUM_TAGE` steht wortgleich in beiden Codebasen (`daten/statistik.ts`, `periodStart()` im
|
||||
Panel). Zwei neue Tests halten die Regel fest: die Verschachtelung an drei kritischen Tagen
|
||||
(02.09., 01.01., Monatserster) und die Fahrt vom Monatsletzten, die in Woche **und** Monat zählt.
|
||||
|
||||
### Langzeitverbrauch: Herleitung aus der Ansicht, nicht aus der Rechnung
|
||||
|
||||
Aus der Kachel entfernt: „Getankt insgesamt", „Strecke bis zum letzten Tankstop" samt Basiszeile
|
||||
„Kilometerstand an den Tankvorgängen", „Letzter Tankstop", der Unplausibel-Hinweis und der
|
||||
Erklärabsatz. **`langzeitverbrauch()` rechnet unverändert weiter** — `liter`, `km`, `basis` und
|
||||
`unplausibel` stehen weiter im Rückgabewert, nur zeigt sie niemand mehr an. Es bleiben der Wert
|
||||
selbst in der Kopfzeile und die Vierergruppe „Näherung je Fahrt, je Zeitraum".
|
||||
|
||||
### Die Draufsicht war der einzige Bildplatz ohne Zugang
|
||||
|
||||
Sie steht auf der Fahrzeugseite über den Sicherheitszeilen, `bilder.py` kennt `draufsicht.webp`
|
||||
seit jeher in `ERLAUBTE_DATEINAMEN` — aber in `BILDER`/`BILDPLAETZE` fehlte sie, und genau diese
|
||||
Liste baut die Auswahl in **Einstellungen → Bild der Übersicht**. Hochladen ließ sich das Foto
|
||||
damit über keine der beiden Oberflächen. Jetzt ein Platz wie jeder andere (achter Eintrag), womit
|
||||
er zugleich in der Galerie unter „Mein Audi" auftaucht — dieselbe Liste, dasselbe Verhalten.
|
||||
|
||||
### „Zuletzt": das Datum statt der Tankstelle
|
||||
|
||||
Unter „Letzter Tankvorgang" stand der Name der Tankstelle. Dort steht jetzt das **Datum** — die
|
||||
Angabe, die auf der Startseite zählt; der Name steht auf der Detailseite. Der Gesamtpreis unter den
|
||||
Litern war in beiden Oberflächen bereits da (`fuel_total_eur`) und erscheint, sobald der Beleg eine
|
||||
Summe trägt.
|
||||
|
||||
### Das Screening lief nur, während das Fahrzeug fuhr
|
||||
|
||||
Der eigentliche Fund hinter „Start/Ziel: wird ermittelt …" auf der realen Instanz.
|
||||
`screening.durchfuehren()` hing an **genau einem** Auslöser: `_kilometerstand_geaendert()`. Steht das
|
||||
Fahrzeug, ändert sich der Kilometerstand nicht, und alles, was das Screening nachträgt — Ortsnamen,
|
||||
Durchschnitt, Höchstgeschwindigkeit, Verbrauch, Radzähler — bleibt bis zur nächsten Fahrt liegen.
|
||||
Nach einem Update ist das am deutlichsten: die neuen Felder der frisch installierten Fassung füllen
|
||||
sich erst, wenn jemand losfährt.
|
||||
|
||||
Zwei neue Auslöser, beide klein: **einmal in `_nach_start()`** (dort kommt eine frisch
|
||||
aktualisierte Integration an) und ein **Zehn-Minuten-Takt** (`TAKT_SCREENING`). Beides ist billig —
|
||||
das Screening füllt nur leere Felder, und das Ortsbudget von acht Anfragen je Durchlauf
|
||||
(`geokodierung.py`) bleibt weit unter Nominatims einer Anfrage je Sekunde. Nachgewiesen im
|
||||
Testcontainer: Ortsfelder einer Fahrt geleert, ohne jede Fahrzeugbewegung vom Takt wieder gefüllt.
|
||||
|
||||
### Was an der gemeldeten Fahrt vom 01.09. NICHT zu reparieren ist
|
||||
|
||||
Dieselbe Fahrt in beiden Instanzen, gemessen:
|
||||
|
||||
| | reale Instanz | Testinstanz |
|
||||
|---|---|---|
|
||||
| Strecke | 7,0 km | 6,896 km |
|
||||
| Dauer | 28 Min | 12,9 Min |
|
||||
| Durchschnitt | 15,1 km/h | 32,6 km/h |
|
||||
| Verbrauch | 11,4 l/100 km | 8,7 l/100 km |
|
||||
|
||||
Die Dauerdifferenz beträgt **15,1 Minuten** — der **Ignition-OFF-Timeout von 900 s** des FMM003
|
||||
(Abschnitt AS). Die reale Instanz hat die Fahrt am 01.09. um 16:20 mit einer Fassung **vor**
|
||||
`cef11b2`/`0124254` aufgezeichnet, die den Nachlauf noch mitzählte. Alles Weitere folgt daraus: der
|
||||
Durchschnitt ist 7,0 km ÷ 28 Min, und der Verbrauch misst die Tankstandsdifferenz über ein 15
|
||||
Minuten längeres Fenster.
|
||||
|
||||
**Ein Update repariert das nicht rückwirkend.** `ts_end` und `duration_s` stehen so in der
|
||||
`fahrten.jsonl`, wie die damals laufende Fassung sie geschrieben hat; das Screening füllt nur leere
|
||||
Felder und rechnet keine Fahrtgrenzen neu. Wer den Tag richtig haben will, muss ihn über den
|
||||
Rückblick (`historienimport`) neu aufbauen.
|
||||
|
||||
### Nachtrag .9: kein Dauertakt, sondern Nachfassen
|
||||
|
||||
Der Zehn-Minuten-Takt aus .8 ist auf Rückfrage des Eigentümers wieder entfallen — zu Recht. Im
|
||||
Normalbetrieb bringt er nichts: beim Fahrtende hat der Lauf das volle Budget von acht Anfragen, die
|
||||
neue Fahrt braucht zwei, danach stehen Start und Ziel fest in der Fahrt und `_orte_screenen()` füllt
|
||||
ohnehin nur leere Felder. Jeder weitere Durchlauf über dieselbe Fahrt ist Leerlauf.
|
||||
|
||||
Aufgebraucht wird das Budget in genau zwei Lagen: nach einem **Rückblick-Import**, der viele Fahrten
|
||||
auf einmal anlegt, und **einmalig nach einem Update**, das ein neues Feld einführt. Dafür jetzt
|
||||
`screening._nachfassen_planen()`: Ist am Ende eines Laufs das Ortsbudget leer **und** ist mindestens
|
||||
ein Ort dazugekommen, läuft in 60 Sekunden ein weiterer. Beides muss gelten — Budget leer heißt „es
|
||||
liegt noch Arbeit an", Fortschritt heißt „die Gegenstelle antwortet". Kommt ein Lauf mit übrigem
|
||||
Budget durch, wird nichts eingeplant und die Kette endet von selbst; antwortet Nominatim gerade
|
||||
nicht, endet sie ebenfalls, statt im Minutentakt gegen eine Wand zu laufen. `beenden()` bestellt
|
||||
einen offenen Nachlauf ab.
|
||||
|
||||
`Geokodierung.neu` zählt dafür die in einem Lauf **gespeicherten** Ergebnisse — auch das ehrliche
|
||||
„dort ist nichts", aber keinen Netzfehler, denn der wird bewusst nicht gemerkt.
|
||||
|
||||
**Live nachgewiesen** im Testcontainer: alle 13 Koordinaten und der Zwischenspeicher geleert, dann
|
||||
Neustart. Lauf 1 löste genau 8 auf (12:49:58–12:50:18), 60 s Pause, Lauf 2 die restlichen 5
|
||||
(12:51:14) — danach keine weitere Zeile im Log. Ohne jede Fahrzeugbewegung.
|
||||
|
||||
### Nachtrag .9: die Draufsicht steht schmaler, das Tankdatum ist weg
|
||||
|
||||
Der Bildbereich auf der Fahrzeugseite lief randlos über die volle Breite (auf einem Telefon rund
|
||||
374 px, mit 16:8 also 187 px hoch) und drückte die Sicherheitszeilen nach unten. Auf Ansage des
|
||||
Eigentümers **schmaler statt flacher**: 60 % Breite, mittig, die Höhe geht im selben Verhältnis auf
|
||||
rund 112 px zurück (`.szene.bleed.schmal` / `.dm-szene--schmal`). Flacher hätte bei einem Foto, das
|
||||
nicht sehr breit ist, oben und unten geschnitten.
|
||||
|
||||
Aus der Kopfzeile des Langzeitverbrauchs ist zusätzlich das „bis 01.09.2026" verschwunden — der
|
||||
letzte Rest der Herleitung, die aus dem Rumpf schon raus war.
|
||||
|
||||
### Nachtrag .9: zwei Gesten, und was davon es schon gab
|
||||
|
||||
**Zum Löschen nach links wischen, Räder → Archiv.** Der Löschknopf saß in der aufgeklappten Zeile,
|
||||
hinter sieben Eingabefeldern. Das Panel hatte die Mechanik längst (`swipeZeile()`, seit den Fahrten-
|
||||
und Tankvorgangslisten), nur war das Radarchiv nie angeschlossen — jetzt mit `rfarch:<id>` samt
|
||||
eigenem `LOESCH_TEXT`/`LOESCH_HINWEIS` und einem Zweig in `eintragLoeschen()`. Umschlossen ist nur
|
||||
die **Kopfzeile**, nicht der aufgeklappte Rumpf: sonst verschöbe ein waagerechtes Ziehen in einem
|
||||
Eingabefeld die ganze Zeile.
|
||||
|
||||
Der App fehlte die Mechanik ganz; sie ist als `WischLoeschen` in `bausteine.tsx` nachgebaut.
|
||||
Bewusst **nicht** die volle iOS-Mechanik: kein Löschen durch Durchwischen, die Geste legt nur den
|
||||
Knopf frei, und die Sicherheitsabfrage bleibt — ein archivierter Satz ist unwiederbringlich. Der
|
||||
Löschknopf in der aufgeklappten Zeile bleibt ebenfalls, am Rechner ist er der kürzere Weg.
|
||||
|
||||
**Vom linken Rand nach rechts wischen = zurück.** Auch das hatte das Panel schon
|
||||
(`randwischenVerdrahten()`, 26 px Rand, 62 px Auslösung); der App fehlte es, dort war der Pfeil oben
|
||||
links der einzige Weg zurück. `useWischZurueck()` in `Shell.tsx` bringt sie mit **denselben Zahlen**.
|
||||
|
||||
Zwei Dinge, die dabei nicht offensichtlich sind:
|
||||
|
||||
- Die App hat keinen Verlaufsstapel (siehe `navigation.ts`), und iOS' eigene Zurück-Geste hängt am
|
||||
Stapel eines `UINavigationController`. In einer einzelnen WebView gibt es sie deshalb nicht
|
||||
geschenkt — sie muss von Hand gebaut werden.
|
||||
- Ziehen zum Aktualisieren und Randwischen greifen auf **dasselbe** `<main>`. Eine schräge Geste am
|
||||
oberen Rand löste sonst beides gleichzeitig aus. Ein gemeinsames `gesteBelegt`-Ref entscheidet:
|
||||
läuft der Randwisch, hält sich das Ziehen heraus.
|
||||
|
||||
Neu in **beiden**: die Seite folgt beim Randwischen dem Finger und federt unter der Schwelle zurück.
|
||||
Vorher passierte bis zum Loslassen nichts Sichtbares — wer zu kurz zog, sah nicht einmal, dass er
|
||||
die Geste begonnen hatte.
|
||||
|
||||
### Nachtrag .10: vier Befunde vom Bildschirmfoto des Eigentümers (02.09.2026)
|
||||
|
||||
**Das Zahnrad lag auf dem ersten Knopf.** Nachgerechnet an den Zahlen der Kachel: Innenabstand 22px,
|
||||
Überschrift (`.ads-eyebrow`/`.label`) 12px hoch, danach folgt der erste Knopf ohne eigenen Abstand —
|
||||
also ab y≈34. Das Zahnrad ist mit `top:11px` und 44px Trefferfläche von y=11 bis y=55 da. Es ragte
|
||||
damit **21px in den Kachelinhalt** und fing die obere Hälfte von „Inland · <Nummer>" ab, sichtbar
|
||||
wie funktional. Beide Oberflächen hatten dieselbe Geometrie, also denselben Fehler.
|
||||
|
||||
Nicht das Zahnrad verkleinert — 44px sind die Mindestgröße einer Trefferfläche —, sondern der
|
||||
Kopfzeile die Höhe gegeben, die es braucht: `min-height:34px` und `padding-right:48px` auf der
|
||||
ersten Überschrift einer Zahnrad-Kachel, Zahnrad auf `top/right:8px`. Damit endet es bei y=48, der
|
||||
Inhalt beginnt bei y=56.
|
||||
|
||||
**Die Vorschau in den Einstellungen schnitt Fotos an.** Sie stand im festen Kasten (Panel 16:9, App
|
||||
4:3) mit `object-fit:cover`; ein Foto anderen Formats verlor oben und unten je ein Stück, bei der
|
||||
Frontansicht am deutlichsten. Wer dort prüft, ob das richtige Bild hochgeladen ist, muss es ganz
|
||||
sehen — jetzt bestimmt das Bild die Höhe (`carfix klein` im Panel, `.dm-einstellbild` in der App),
|
||||
dieselbe Entscheidung wie beim Fahrzeugfoto der Übersicht am 31.08.
|
||||
|
||||
**Der Titel einer Unterseite stand mittig**, als einziger Titel der ganzen Oberfläche — iOS' Regel
|
||||
für Navigationsleisten, aber der Sprung fällt auf, wenn alles andere links steht. `text-align:center`
|
||||
ist in beiden Oberflächen entfallen; der Zurück-Pfeil ist ein eigenes Flex-Element davor, der Titel
|
||||
läuft ihm also nicht unter.
|
||||
|
||||
**Die Draufsicht ist freigestellt.** Das Foto lag auf einem gleichmäßigen Studiogrund (#DDDDDD) mit
|
||||
Schatten — auf dunklem Grund ein heller Kasten mitten auf der Seite. Freigestellt mit einem
|
||||
Flutfüller vom Bildrand her über „farblos und hell" (Sättigung ≤16, Helligkeit ≥150), zwei
|
||||
Saumdurchgängen gegen den grauen Rand an der Kante, und anschließendem engen Zuschnitt:
|
||||
1451×710 → **1405×618**, 15,7 % der Fläche durchsichtig. Der Zuschnitt ist der eigentliche Gewinn
|
||||
für die Abstände — die kommen jetzt aus dem Layout (`padding: 22px 0 26px`), nicht aus leerem
|
||||
Bildrand.
|
||||
|
||||
Das Werkzeug dafür steht **nicht** im Repo: es war ein einmaliger Lauf über ein Nutzerfoto (Skripte
|
||||
im Scratchpad, ein PNG-Leser/-Schreiber auf `zlib` plus Flutfüller). Fotos sind Nutzerdaten und
|
||||
liegen unter `/config/www/bilder/`, nie im Repository.
|
||||
|
||||
### Nachtrag .11: der zentrierte Titel war richtig, und das Zahnrad heißt settings-s
|
||||
|
||||
**Zurückgenommen: die Titel der Unterseiten stehen wieder mittig.** Die Änderung aus .10 („wie alle
|
||||
anderen auch") war ein Fehlschluss. Apples HIG kennt **zwei** Titelformen, und beide sind hier im
|
||||
Einsatz: den großen, linksbündigen auf oberster Ebene (Hauptbereiche) und den kleinen, **zentrierten**
|
||||
in der Navigationsleiste einer Detailseite. Die „anderen" Titel waren die der Hauptbereiche — sie
|
||||
folgen der anderen Regel. Nicht erneut ändern, ohne die beiden Formen auseinanderzuhalten; der
|
||||
Kommentar an `.dm-kopf--unterseite` sagt das jetzt.
|
||||
|
||||
**Einheitlichkeit geprüft, nicht behauptet:** alle **21** Unterseiten der App haben einen Eintrag in
|
||||
`ZURUECK` *und* in `TITEL` (Skript über `navigation.ts`, keine Lücke). Die Kopfzeile entsteht in
|
||||
`Shell.tsx` aus `unterseite = !istHauptbereich(route.name)` — eine Seite kann also gar nicht
|
||||
abweichen. Im Flächenlayout gibt es die Kopfzeile nicht, dort steht eine Brotkrume; der zentrierte
|
||||
Titel erscheint nur auf dem Telefon, wo der Zurück-Pfeil immer da ist.
|
||||
|
||||
**Das Kachel-Zahnrad ist `settings-s` aus der Audi-Icon-Bibliothek** (24er-Raster) — nachgewiesen,
|
||||
nicht angenommen: der Pfad in `SymbolZahnradVoll` ist zeichengleich mit
|
||||
`design/assets/icons/settings.svg`. Im Panel stand derselbe Pfad **sechsmal wortgleich im Markup**,
|
||||
obwohl es für genau diesen Zweck den `CI`-Satz gibt; er ist jetzt `CI.settingsS` und wird über
|
||||
`ciSVG(CI.settingsS, 17)` gesetzt. Die 48er-Variante hieße `settings-l` und wird nirgends gebraucht,
|
||||
das Zahnrad steht immer auf 17px.
|
||||
|
||||
**Offen und bewusst nicht angefasst:** `SymbolZahnrad` (Seitenleiste des Flächenlayouts) ist ein
|
||||
selbst gezeichnetes Kontursymbol und stammt **nicht** aus dem CI-Satz. Es auf `settings-s`
|
||||
umzustellen würde es neben die übrigen Leistensymbole stellen, die alle Kontur sind — das CI-Symbol
|
||||
ist flächiger und wöge dort schwerer. Entscheidung des Eigentümers.
|
||||
|
||||
### Nachtrag .12: das gezeichnete Zahnrad ist weg, die Zapfsäule ist fuel-station-s
|
||||
|
||||
**Das gezeichnete Zahnrad war noch in Gebrauch** — nur nicht dort, wo man hinsieht. In der App
|
||||
stand es in der Seitenleiste des Flächenlayouts (`Shell.tsx`), im Panel als `.profilbtn` in der
|
||||
Kopfzeile, per CSS auf Telefonbreite `display:none` und erst ab dem großen Umbruch `inline-flex`.
|
||||
Auf einem Telefon also nie sichtbar — daher der Eindruck, es sei längst raus. Beide zeigen jetzt
|
||||
`settings-s`, `SymbolZahnrad` ist gelöscht. Im Panel musste die Regel `.profilbtn svg` mit: sie
|
||||
stand auf `fill:none; stroke:currentColor`, was um jede Fläche des CI-Symbols eine Kontur gezogen
|
||||
hätte — dieselbe Falle, die `.voll`/`RahmenFlaeche` in der App vermeidet.
|
||||
|
||||
**Die Zapfsäule des Tanken-Reiters ist jetzt `fuel-station-s`** (aus der Bibliothek des Eigentümers
|
||||
nachgereicht; im Repo lag sie nicht — `design/assets/icons/gas-station.svg` ist eine andere
|
||||
Variante mit Buchstaben unter der Säule). Damit ist von den fünf Reitersymbolen nur noch
|
||||
„Übersicht" gezeichnet.
|
||||
|
||||
**Größe live gemessen statt geschätzt** (`getBBox()` im Browser, wie schon bei `polls-s`):
|
||||
|
||||
| Symbol | Breite | Höhe |
|
||||
|---|---|---|
|
||||
| Übersicht | 21,78 | 12,46 |
|
||||
| Mein Audi | 24,00 | 17,22 |
|
||||
| Fahrten | 22,00 | 21,44 |
|
||||
| Statistik (CI, Faktor 0,77) | 16,94 | 18,48 |
|
||||
| Tanken, gezeichnet (vorher) | 14,00 | 15,50 |
|
||||
| **fuel-station-s roh** | **19,00** | **22,00** |
|
||||
| **fuel-station-s, Faktor 0,84** | **15,96** | **18,48** |
|
||||
|
||||
Roh wäre es das höchste Symbol der Leiste gewesen — höher als „Fahrten". Faktor **0,84** um die
|
||||
Rastermitte (`translate(1.5,1.92) scale(0.84)`) bringt es auf **genau dieselbe senkrechte Spanne wie
|
||||
Statistik**: y 2,76 bis 21,24. Ränder nachgemessen, nicht nur die Größe — die Lehre aus dem
|
||||
Übersicht-Symbol vom selben Tag.
|
||||
|
||||
`fuel-station-l` (48er-Raster) wird nicht gebraucht: die Reiter zeichnen mit 21 bzw. 26px.
|
||||
|
||||
+127
-6
@@ -8,7 +8,12 @@
|
||||
*/
|
||||
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import type { PointerEvent as ReactPointerEvent, ReactNode, RefObject } from "react"
|
||||
import type {
|
||||
MutableRefObject,
|
||||
PointerEvent as ReactPointerEvent,
|
||||
ReactNode,
|
||||
RefObject,
|
||||
} from "react"
|
||||
|
||||
import { TabBar } from "@audi-dash/ui"
|
||||
|
||||
@@ -24,7 +29,7 @@ import {
|
||||
SymbolStatistik,
|
||||
SymbolTanken,
|
||||
SymbolUebersicht,
|
||||
SymbolZahnrad,
|
||||
SymbolZahnradVoll,
|
||||
SymbolZurueck,
|
||||
} from "./symbole"
|
||||
|
||||
@@ -45,9 +50,93 @@ export interface ShellProps {
|
||||
/** Ziehen zum Aktualisieren, 1:1 nach der ptr-Logik des Panels. */
|
||||
const PTR_SCHWELLE = 56
|
||||
|
||||
/*
|
||||
* Vom linken Rand nach rechts wischen = zurück.
|
||||
*
|
||||
* Warum von Hand: die App ist EINE WebView mit einer flachen Route (siehe
|
||||
* navigation.ts), kein Verlaufsstapel. iOS' eigene Zurück-Geste hängt am
|
||||
* Navigationsstapel eines UINavigationControllers - den gibt es hier nicht,
|
||||
* also gibt es die Geste auch nicht geschenkt. Bis zum 02.09.2026 war der
|
||||
* Pfeil oben links der einzige Weg zurück (gemeldet vom Eigentümer).
|
||||
*
|
||||
* RAND: die Geste muss am linken Rand beginnen. Das ist keine Nachahmung um
|
||||
* ihrer selbst willen, sondern die einzige Art, sie von allem anderen
|
||||
* Waagerechten sauber zu trennen - dem Wischen an einer Listenzeile
|
||||
* (WischLoeschen), dem seitlichen Schieben in der Fotogalerie.
|
||||
*
|
||||
* SCHWELLE: erst ab dieser Strecke wird zurückgegangen. Darunter federt die
|
||||
* Seite zurück - eine angefangene Geste ist damit widerrufbar, indem man den
|
||||
* Finger zurückführt, genau wie bei iOS.
|
||||
*/
|
||||
/* Dieselben Zahlen wie randwischenVerdrahten() im Panel (26px Rand, 62px
|
||||
Auslösung) - die Geste soll sich in beiden Oberflächen gleich anfühlen. */
|
||||
const ZURUECK_RAND = 26
|
||||
const ZURUECK_SCHWELLE = 62
|
||||
const ZURUECK_WEG = 140
|
||||
|
||||
function useWischZurueck(
|
||||
inhalt: RefObject<HTMLElement | null>,
|
||||
zurueck: (() => void) | undefined,
|
||||
belegt: MutableRefObject<boolean>,
|
||||
) {
|
||||
const [zug, setzeZug] = useState<number | null>(null)
|
||||
const start = useRef<{ x: number; y: number } | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const el = inhalt.current
|
||||
// Der Aussteiger sitzt IM Effekt, nicht davor - sonst hinge die
|
||||
// Hook-Reihenfolge davon ab, ob es ein Zurück-Ziel gibt.
|
||||
if (!el || !zurueck) return undefined
|
||||
|
||||
const beiStart = (e: TouchEvent) => {
|
||||
if (e.touches.length !== 1) return
|
||||
const t = e.touches[0]!
|
||||
if (t.clientX > ZURUECK_RAND) return
|
||||
start.current = { x: t.clientX, y: t.clientY }
|
||||
}
|
||||
const beiBewegung = (e: TouchEvent) => {
|
||||
const s = start.current
|
||||
if (!s || e.touches.length !== 1) return
|
||||
const t = e.touches[0]!
|
||||
const dx = t.clientX - s.x
|
||||
const dy = t.clientY - s.y
|
||||
if (dx <= 0 || Math.abs(dx) < Math.abs(dy) * 1.5) {
|
||||
// Senkrecht oder rückwärts gemeint: die Geste gehört uns nicht.
|
||||
if (Math.abs(dy) > 10) start.current = null
|
||||
return
|
||||
}
|
||||
// Solange wir ziehen, hält sich das Ziehen-zum-Aktualisieren heraus.
|
||||
belegt.current = true
|
||||
setzeZug(Math.min(ZURUECK_WEG, dx))
|
||||
e.preventDefault()
|
||||
}
|
||||
const beiEnde = () => {
|
||||
const gezogen = zug ?? 0
|
||||
start.current = null
|
||||
belegt.current = false
|
||||
setzeZug(null)
|
||||
if (gezogen >= ZURUECK_SCHWELLE) zurueck()
|
||||
}
|
||||
|
||||
el.addEventListener("touchstart", beiStart, { passive: true })
|
||||
el.addEventListener("touchmove", beiBewegung, { passive: false })
|
||||
el.addEventListener("touchend", beiEnde)
|
||||
el.addEventListener("touchcancel", beiEnde)
|
||||
return () => {
|
||||
el.removeEventListener("touchstart", beiStart)
|
||||
el.removeEventListener("touchmove", beiBewegung)
|
||||
el.removeEventListener("touchend", beiEnde)
|
||||
el.removeEventListener("touchcancel", beiEnde)
|
||||
}
|
||||
})
|
||||
|
||||
return zug
|
||||
}
|
||||
|
||||
function useZiehenAktualisieren(
|
||||
inhalt: RefObject<HTMLElement | null>,
|
||||
beiAktualisieren: (() => Promise<void>) | undefined,
|
||||
belegt: MutableRefObject<boolean>,
|
||||
) {
|
||||
const [zug, setzeZug] = useState(0)
|
||||
const [laedt, setzeLaedt] = useState(false)
|
||||
@@ -68,7 +157,7 @@ function useZiehenAktualisieren(
|
||||
}
|
||||
|
||||
const starten = (y: number) => {
|
||||
if (laedt) return
|
||||
if (laedt || belegt.current) return
|
||||
if ((inhalt.current?.scrollTop ?? 0) > 0) return
|
||||
start.current = y
|
||||
setzeZug(0)
|
||||
@@ -77,6 +166,11 @@ function useZiehenAktualisieren(
|
||||
preventDefault() rufen. */
|
||||
const bewegen = (y: number): boolean => {
|
||||
if (start.current === null) return false
|
||||
if (belegt.current) {
|
||||
start.current = null
|
||||
setzeZug(0)
|
||||
return false
|
||||
}
|
||||
const dy = y - start.current
|
||||
if (dy > 0 && (inhalt.current?.scrollTop ?? 0) <= 0) {
|
||||
// Widerstand wie bei iOS: halbe Strecke, gedeckelt bei 90px.
|
||||
@@ -168,7 +262,20 @@ export function Shell({
|
||||
|
||||
const unterseite = !istHauptbereich(route.name)
|
||||
const inhaltRef = useRef<HTMLElement | null>(null)
|
||||
const ptr = useZiehenAktualisieren(inhaltRef, breit ? undefined : beiAktualisieren)
|
||||
// Eine Geste zur Zeit: läuft der Wisch nach rechts, hält sich das Ziehen
|
||||
// zum Aktualisieren heraus. Ohne das zeigte eine schräge Geste am oberen
|
||||
// Seitenrand beides gleichzeitig.
|
||||
const gesteBelegt = useRef(false)
|
||||
const zurueckZug = useWischZurueck(
|
||||
inhaltRef,
|
||||
zurueckZiel && !breit ? () => geheZu(zurueckZiel) : undefined,
|
||||
gesteBelegt,
|
||||
)
|
||||
const ptr = useZiehenAktualisieren(
|
||||
inhaltRef,
|
||||
breit ? undefined : beiAktualisieren,
|
||||
gesteBelegt,
|
||||
)
|
||||
|
||||
// Der Text tickt mit, wie standAlterTicken() im Panel (Sekundentakt).
|
||||
const [, neuZeichnen] = useState(0)
|
||||
@@ -273,7 +380,7 @@ export function Shell({
|
||||
}
|
||||
onClick={() => geheZu("einst")}
|
||||
>
|
||||
<SymbolZahnrad />
|
||||
<SymbolZahnradVoll />
|
||||
<span>Einstellungen</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -314,7 +421,21 @@ export function Shell({
|
||||
: "Ziehen zum Aktualisieren"}
|
||||
</span>
|
||||
</div>
|
||||
<main className="dm-scroll dm-inhalt" ref={inhaltRef} {...ptr.handler}>
|
||||
<main
|
||||
className="dm-scroll dm-inhalt"
|
||||
ref={inhaltRef}
|
||||
{...ptr.handler}
|
||||
/* Die Seite folgt dem Finger, damit die Geste sichtbar widerrufbar
|
||||
bleibt: zurückführen unter die Schwelle, und sie federt zurück.
|
||||
Ohne Zug steht hier kein transform - eine stehende Transformation
|
||||
erzeugt einen eigenen Malkontext und hätte position:fixed im Inhalt
|
||||
an sich gebunden. */
|
||||
style={
|
||||
zurueckZug === null
|
||||
? undefined
|
||||
: { transform: `translateX(${zurueckZug}px)`, transition: "none" }
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</main>
|
||||
<TabBar
|
||||
|
||||
@@ -46,25 +46,46 @@ function alsTag(d: Date): string {
|
||||
}
|
||||
|
||||
describe("zeitraumStart", () => {
|
||||
it("beginnt das Jahr am 1. Januar", () => {
|
||||
expect(alsTag(zeitraumStart("jahr", JETZT))).toBe("2026-01-01")
|
||||
it("rechnet das Jahr 365 Tage zurück", () => {
|
||||
expect(alsTag(zeitraumStart("jahr", JETZT))).toBe("2025-08-11")
|
||||
})
|
||||
|
||||
it("beginnt den Monat am Ersten", () => {
|
||||
expect(alsTag(zeitraumStart("monat", JETZT))).toBe("2026-08-01")
|
||||
it("rechnet den Monat 30 Tage zurück", () => {
|
||||
expect(alsTag(zeitraumStart("monat", JETZT))).toBe("2026-07-12")
|
||||
})
|
||||
|
||||
it("beginnt die Woche am Montag, nicht am Sonntag", () => {
|
||||
// Der 11.08.2026 ist ein Dienstag, die Woche beginnt am 10.08.
|
||||
const start = zeitraumStart("woche", JETZT)
|
||||
expect(start.getDay()).toBe(1)
|
||||
expect(start.getDate()).toBe(10)
|
||||
it("rechnet die Woche 7 Tage zurück", () => {
|
||||
expect(alsTag(zeitraumStart("woche", JETZT))).toBe("2026-08-04")
|
||||
})
|
||||
|
||||
it("beginnt den Tag um Mitternacht", () => {
|
||||
it("rechnet den Tag 24 Stunden zurück", () => {
|
||||
const start = zeitraumStart("tag", JETZT)
|
||||
expect(start.getHours()).toBe(0)
|
||||
expect(start.getDate()).toBe(11)
|
||||
expect(alsTag(start)).toBe("2026-08-10")
|
||||
expect(start.getHours()).toBe(12)
|
||||
})
|
||||
|
||||
/* Der Grund für die Umstellung, als Test: am 02.09.2026 begann die
|
||||
Kalenderwoche am Montag, dem 31.08., der Kalendermonat aber erst am
|
||||
01.09. Eine Fahrt vom 31.08. zählte damit zur Woche und nicht zum
|
||||
Monat - "Monat kleiner als Woche", vom Eigentümer an der realen Instanz
|
||||
gemeldet. Rollende Fenster sind ineinander enthalten, immer. */
|
||||
it("hält Tag, Woche, Monat und Jahr ineinander verschachtelt", () => {
|
||||
for (const tag of ["2026-09-02T09:00:00", "2026-01-01T09:00:00", "2026-03-01T00:30:00"]) {
|
||||
const jetzt = new Date(tag)
|
||||
const [j, m, w, t] = (["jahr", "monat", "woche", "tag"] as const).map((art) =>
|
||||
zeitraumStart(art, jetzt).getTime(),
|
||||
)
|
||||
expect(j!).toBeLessThanOrEqual(m!)
|
||||
expect(m!).toBeLessThanOrEqual(w!)
|
||||
expect(w!).toBeLessThanOrEqual(t!)
|
||||
}
|
||||
})
|
||||
|
||||
it("zählt eine Fahrt vom Monatsletzten in Woche UND Monat", () => {
|
||||
const jetzt = new Date("2026-09-02T09:00:00")
|
||||
const fahrten = [fahrt({ trip_id: "a", ts_start: "2026-08-31T08:00:00" })]
|
||||
expect(fahrtenSeit(fahrten, "woche", jetzt)).toHaveLength(1)
|
||||
expect(fahrtenSeit(fahrten, "monat", jetzt)).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -91,7 +112,9 @@ describe("fahrtenSeit und tankSeit", () => {
|
||||
fahrt({ trip_id: "heute", ts_start: "2026-08-11T09:00:00" }),
|
||||
fahrt({ trip_id: "diesewoche", ts_start: "2026-08-10T09:00:00" }),
|
||||
fahrt({ trip_id: "diesenmonat", ts_start: "2026-08-02T09:00:00" }),
|
||||
fahrt({ trip_id: "letztesjahr", ts_start: "2025-12-30T09:00:00" }),
|
||||
// Im Kalenderjahr 2026 nicht enthalten, in den letzten 365 Tagen schon -
|
||||
// seit der Umstellung auf rollende Fenster zählt sie deshalb mit.
|
||||
fahrt({ trip_id: "dezember", ts_start: "2025-12-30T09:00:00" }),
|
||||
fahrt({ trip_id: "ohnestrecke", ts_start: "2026-08-11T10:00:00", distance_km: null }),
|
||||
]
|
||||
|
||||
@@ -101,7 +124,7 @@ describe("fahrtenSeit und tankSeit", () => {
|
||||
"heute",
|
||||
"diesewoche",
|
||||
])
|
||||
expect(fahrtenSeit(fahrten, "jahr", JETZT)).toHaveLength(3)
|
||||
expect(fahrtenSeit(fahrten, "jahr", JETZT)).toHaveLength(4)
|
||||
})
|
||||
|
||||
it("lässt Fahrten ohne Strecke außen vor — sie würden jede Summe verfälschen", () => {
|
||||
|
||||
@@ -31,16 +31,32 @@ export const ZEITRAUM_NAME: Record<Zeitraum, string> = {
|
||||
absolut: "Absolut",
|
||||
}
|
||||
|
||||
/** Beginn des Zeitraums. Die Woche startet montags, nicht sonntags. */
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export const ZEITRAUM_TAGE: Record<Exclude<Zeitraum, "absolut">, number> = {
|
||||
jahr: 365,
|
||||
monat: 30,
|
||||
woche: 7,
|
||||
tag: 1,
|
||||
}
|
||||
|
||||
/** Beginn des Zeitraums: so viele Tage zurück, wie ZEITRAUM_TAGE sagt. */
|
||||
export function zeitraumStart(art: Zeitraum, jetzt = new Date()): Date {
|
||||
// Vor jeder denkbaren Aufzeichnung - damit fällt nichts heraus.
|
||||
if (art === "absolut") return new Date(0)
|
||||
if (art === "jahr") return new Date(jetzt.getFullYear(), 0, 1)
|
||||
if (art === "monat") return new Date(jetzt.getFullYear(), jetzt.getMonth(), 1)
|
||||
const d = new Date(jetzt)
|
||||
d.setHours(0, 0, 0, 0)
|
||||
if (art === "woche") d.setDate(d.getDate() - ((d.getDay() + 6) % 7))
|
||||
return d
|
||||
return new Date(jetzt.getTime() - ZEITRAUM_TAGE[art] * 24 * 60 * 60 * 1000)
|
||||
}
|
||||
|
||||
export function fahrtenSeit(fahrten: readonly Fahrt[], art: Zeitraum, jetzt = new Date()): Fahrt[] {
|
||||
|
||||
@@ -400,7 +400,7 @@ export function Einstellungen({
|
||||
{/* Vorher stand hier ein Raster aller sieben Miniaturen und der Hinweis,
|
||||
Fotos seien nur über Home Assistant zu pflegen — das Panel zeigt
|
||||
genau die gewählte Ansicht groß und lädt direkt hoch. */}
|
||||
<div style={{ marginTop: "var(--sp-3)" }}>
|
||||
<div className="dm-einstellbild" style={{ marginTop: "var(--sp-3)" }}>
|
||||
<BildMitMenue
|
||||
datei={gewaehltesBild.datei}
|
||||
beschreibung={gewaehltesBild.beschreibung}
|
||||
|
||||
@@ -46,9 +46,10 @@ export function Fahrzeugstatus({ geheZu }: { geheZu: (name: SeitenName) => void
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Draufsicht, wie im Panel (.szene.bleed, "carfix klein" = 16:8) - war
|
||||
hier komplett unportiert (Parität-Audit 2026-08-30). */}
|
||||
<div className="dm-szene">
|
||||
{/* Draufsicht, wie im Panel (.szene.bleed.schmal, "carfix klein") - war
|
||||
hier komplett unportiert (Parität-Audit 2026-08-30). Seit dem
|
||||
02.09.2026 eingerückt statt randlos, siehe .dm-szene--schmal. */}
|
||||
<div className="dm-szene dm-szene--schmal">
|
||||
<Bild datei="draufsicht.webp" beschreibung="Draufsicht" name="Draufsicht" />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import { useDaten } from "../daten/DatenKontext"
|
||||
import type { Reifensatz, ReifenArchivEintrag } from "../daten/profilAdapter"
|
||||
import { datum, de, deOderStrich } from "../format"
|
||||
import { SymbolEdit, SymbolPaket, SymbolZahnradVoll } from "../symbole"
|
||||
import { BildMitMenue, Wertzeile, Werteliste, bestaetigen } from "./bausteine"
|
||||
import { BildMitMenue, WischLoeschen, Wertzeile, Werteliste, bestaetigen } from "./bausteine"
|
||||
import { Datumsfeld } from "./Datumsfeld"
|
||||
import { kalenderDateiLaden } from "./kalender"
|
||||
|
||||
@@ -374,6 +374,7 @@ function ArchivZeile({ eintrag }: { eintrag: ReifenArchivEintrag }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<WischLoeschen beiLoeschen={() => void loeschen()}>
|
||||
<Accordion
|
||||
title={eintrag.satz === "winter" ? "Winterräder" : "Sommerräder"}
|
||||
summary={
|
||||
@@ -499,5 +500,6 @@ function ArchivZeile({ eintrag }: { eintrag: ReifenArchivEintrag }) {
|
||||
</>
|
||||
)}
|
||||
</Accordion>
|
||||
</WischLoeschen>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import { Fig, ProgressBar, Tile } from "@audi-dash/ui"
|
||||
import { useDaten } from "../daten/DatenKontext"
|
||||
import { ZEITRAUM_NAME, ZEITRAUM_SPALTEN, jahresauswertung } from "../daten/statistik"
|
||||
import type { Zeitraum } from "../daten/statistik"
|
||||
import { datum, de, deOderStrich } from "../format"
|
||||
import { de, deOderStrich } from "../format"
|
||||
import { Leerzustand, Wertzeile, Werteliste } from "./bausteine"
|
||||
|
||||
/** `statTile()` im Panel: Kopf mit Zahl und Nebenwert, aufklappbarer Rumpf. */
|
||||
@@ -150,39 +150,21 @@ export function Statistik() {
|
||||
titel="Langzeitverbrauch"
|
||||
wert={deOderStrich(a.langzeit.wert, 1)}
|
||||
einheit="l/100 km"
|
||||
neben={a.langzeit.bis ? `bis ${datum(a.langzeit.bis)}` : "–"}
|
||||
/* Das Datum des letzten Tankstops ist am 02.09.2026 auf Ansage des
|
||||
Eigentümers auch aus der Kopfzeile verschwunden - es war der letzte
|
||||
Rest der Herleitung, die aus dem Rumpf schon raus ist. */
|
||||
neben=""
|
||||
>
|
||||
<Werteliste
|
||||
kinder={
|
||||
<>
|
||||
<Wertzeile label="Getankt insgesamt" wert={`${de(a.langzeit.liter, 1)} l`} />
|
||||
<Wertzeile
|
||||
label="Strecke bis zum letzten Tankstop"
|
||||
wert={`${de(a.langzeit.km)} km`}
|
||||
zusatz={a.langzeit.basis !== "–" ? a.langzeit.basis : undefined}
|
||||
/>
|
||||
<Wertzeile
|
||||
label="Letzter Tankstop"
|
||||
wert={a.langzeit.bis ? datum(a.langzeit.bis) : "–"}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
{a.langzeit.unplausibel && (
|
||||
<span className="ads-eyebrow dm-warnton" style={{ marginTop: "16px" }}>
|
||||
Aus Litern und Strecke ergibt sich derzeit kein plausibler Wert. Sobald Tankvorgänge und
|
||||
Kilometerstände vollständig vorliegen, erscheint er hier.
|
||||
</span>
|
||||
)}
|
||||
{/* 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 liter/km/basis nach wie
|
||||
vor. Es bleiben der Wert selbst und die Näherung je Zeitraum. */}
|
||||
<Vierergruppe
|
||||
titel="Näherung je Fahrt, je Zeitraum [l/100 km]"
|
||||
werte={jeZeitraum((z) => deOderStrich(a.jeZeitraum[z].verbrauch, 1))}
|
||||
/>
|
||||
<span className="ads-eyebrow" style={{ marginTop: "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>
|
||||
</StatKachel>
|
||||
|
||||
<StatKachel
|
||||
|
||||
@@ -253,7 +253,10 @@ export function Uebersicht({ geheZu }: { geheZu: (name: SeitenName, id?: string)
|
||||
{letzteTankung && (
|
||||
<Blattzeile
|
||||
label="Letzter Tankvorgang"
|
||||
labelZusatz={letzteTankung.station_name ?? ""}
|
||||
// Bis zum 02.09.2026 stand hier der Name der Tankstelle. Das
|
||||
// Datum ist die Angabe, die auf der Startseite zählt — wann war
|
||||
// ich zuletzt tanken; der Name steht auf der Detailseite.
|
||||
labelZusatz={datum(letzteTankung.ts)}
|
||||
wert={letzteTankung.liters != null ? `${de(letzteTankung.liters, 2)} l` : ""}
|
||||
wertZusatz={
|
||||
letzteTankung.fuel_total_eur != null
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* gebraucht wird, aber zu speziell für das Design-System ist.
|
||||
*/
|
||||
|
||||
import { useRef, useState, type ReactNode } from "react"
|
||||
import { useRef, useState, type ReactNode, type TouchEvent as ReactTouchEvent } from "react"
|
||||
|
||||
import { ActionButton, ActionSheet, Tile } from "@audi-dash/ui"
|
||||
|
||||
@@ -307,6 +307,113 @@ export function StatusKreis({ ok }: { ok: boolean | null }) {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Zum Löschen nach links wischen — die iOS-übliche Geste für Listenzeilen.
|
||||
*
|
||||
* Warum überhaupt: der Löschknopf des archivierten Radsatzes sitzt IN der
|
||||
* aufgeklappten Zeile. Wer löschen will, muss also erst aufklappen, an sieben
|
||||
* Eingabefeldern vorbeiscrollen und unten den Knopf finden. Auf Ansage des
|
||||
* Eigentümers am 02.09.2026.
|
||||
*
|
||||
* Bewusst NICHT die volle iOS-Mechanik: kein Löschen durch Durchwischen, die
|
||||
* Geste legt nur den Knopf frei. Ein archivierter Satz ist unwiederbringlich,
|
||||
* und ein Wisch ist zu leicht versehentlich gemacht. Die Sicherheitsabfrage
|
||||
* dahinter bleibt ebenfalls.
|
||||
*
|
||||
* Die Geste greift erst, wenn sie waagerecht deutlich stärker ist als
|
||||
* senkrecht (Faktor 1,5) — sonst nähme sie jeder Scrollbewegung die Führung
|
||||
* weg, die zufällig ein paar Pixel seitlich verläuft.
|
||||
*/
|
||||
const WISCH_BREITE = 88
|
||||
const WISCH_START = 10
|
||||
|
||||
export function WischLoeschen({
|
||||
beiLoeschen,
|
||||
beschriftung = "Löschen",
|
||||
children,
|
||||
}: {
|
||||
beiLoeschen: () => void
|
||||
beschriftung?: string
|
||||
/** Heißt `children`, weil JSX-Verschachtelung genau diesen Namen erwartet. */
|
||||
children: ReactNode
|
||||
}) {
|
||||
const [offen, setzeOffen] = useState(false)
|
||||
const [zug, setzeZug] = useState<number | null>(null)
|
||||
const start = useRef<{ x: number; y: number } | null>(null)
|
||||
const entschieden = useRef(false)
|
||||
|
||||
const versatz = zug ?? (offen ? -WISCH_BREITE : 0)
|
||||
|
||||
const beiStart = (e: ReactTouchEvent<HTMLDivElement>) => {
|
||||
if (e.touches.length !== 1) return
|
||||
start.current = { x: e.touches[0]!.clientX, y: e.touches[0]!.clientY }
|
||||
entschieden.current = false
|
||||
}
|
||||
|
||||
const beiBewegung = (e: ReactTouchEvent<HTMLDivElement>) => {
|
||||
const s = start.current
|
||||
if (!s || e.touches.length !== 1) return
|
||||
const dx = e.touches[0]!.clientX - s.x
|
||||
const dy = e.touches[0]!.clientY - s.y
|
||||
if (!entschieden.current) {
|
||||
if (Math.abs(dx) < WISCH_START) return
|
||||
// Senkrecht gemeint: die Geste gehört der Liste, nicht uns.
|
||||
if (Math.abs(dx) < Math.abs(dy) * 1.5) {
|
||||
start.current = null
|
||||
return
|
||||
}
|
||||
entschieden.current = true
|
||||
}
|
||||
const basis = offen ? -WISCH_BREITE : 0
|
||||
setzeZug(Math.max(-WISCH_BREITE, Math.min(0, basis + dx)))
|
||||
}
|
||||
|
||||
const beiEnde = () => {
|
||||
if (zug !== null) setzeOffen(zug < -WISCH_BREITE / 2)
|
||||
start.current = null
|
||||
setzeZug(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="dm-wisch">
|
||||
<button
|
||||
type="button"
|
||||
className="dm-wisch__loeschen"
|
||||
tabIndex={offen ? 0 : -1}
|
||||
aria-hidden={!offen}
|
||||
onClick={() => {
|
||||
setzeOffen(false)
|
||||
beiLoeschen()
|
||||
}}
|
||||
>
|
||||
{beschriftung}
|
||||
</button>
|
||||
<div
|
||||
className="dm-wisch__inhalt"
|
||||
style={{
|
||||
transform: `translateX(${versatz}px)`,
|
||||
transition: zug === null ? "transform .18s ease" : "none",
|
||||
}}
|
||||
onTouchStart={beiStart}
|
||||
onTouchMove={beiBewegung}
|
||||
onTouchEnd={beiEnde}
|
||||
onTouchCancel={beiEnde}
|
||||
/* Ist der Knopf offen, schließt die nächste Berührung ihn wieder,
|
||||
statt die Zeile aufzuklappen - sonst wäre der einzige Weg zurück,
|
||||
erneut zu wischen. */
|
||||
onClickCapture={(e) => {
|
||||
if (!offen) return
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setzeOffen(false)
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** Fasst ok über eine Gruppe zusammen: unbekannt, sobald ein Punkt unbekannt
|
||||
ist oder die Gruppe ganz ohne Einträge dasteht (kein Sensor dieser Rolle
|
||||
zugeordnet) - nie true durch eine leere Liste erschlichen. */
|
||||
|
||||
@@ -19,7 +19,7 @@ export interface Bildplatz {
|
||||
datei: string
|
||||
}
|
||||
|
||||
/** Die sieben Plätze der Fotogalerie unter „Mein Audi". */
|
||||
/** Die acht Plätze der Fotogalerie unter „Mein Audi". */
|
||||
export const BILDPLAETZE: Bildplatz[] = [
|
||||
{ name: "Seitenansicht", beschreibung: "Auto Seite", datei: "seitenansicht.webp" },
|
||||
{ name: "Front schräg", beschreibung: "Auto Front schräg", datei: "front-schraeg.webp" },
|
||||
@@ -28,6 +28,11 @@ export const BILDPLAETZE: Bildplatz[] = [
|
||||
{ name: "Cockpit", beschreibung: "Innenraum Cockpit", datei: "cockpit.webp" },
|
||||
{ name: "Scheinwerfer", beschreibung: "Scheinwerfer", datei: "scheinwerfer.webp" },
|
||||
{ name: "Sitze", beschreibung: "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", beschreibung: "Auto von oben", datei: "draufsicht.webp" },
|
||||
]
|
||||
|
||||
/**
|
||||
@@ -55,7 +60,6 @@ export const WEITERE_BILDER = [
|
||||
"seitenansicht-winter.webp",
|
||||
"rad-sommer.webp",
|
||||
"rad-winter.webp",
|
||||
"draufsicht.webp",
|
||||
] as const
|
||||
|
||||
export const ERLAUBTE_DATEINAMEN: readonly string[] = [
|
||||
|
||||
@@ -48,6 +48,17 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Mittig, und zwar mit Absicht: Apples Human Interface Guidelines kennen
|
||||
genau zwei Titelformen - den großen, linksbündigen Titel auf oberster
|
||||
Ebene und den kleinen, ZENTRIERTEN in der Navigationsleiste einer
|
||||
Detailseite. Beides steht hier nebeneinander: Hauptbereiche groß und
|
||||
links, Unterseiten 17px und mittig.
|
||||
|
||||
Am 02.09.2026 kurzzeitig auf linksbündig gestellt (auf Ansage, "wie alle
|
||||
anderen auch") und auf Rückfrage des Eigentümers am selben Tag wieder
|
||||
zurückgenommen: die anderen Titel sind die der Hauptbereiche, und die
|
||||
folgen der anderen Regel. Nicht erneut ändern, ohne diese beiden Formen
|
||||
auseinanderzuhalten. */
|
||||
.dm-kopf--unterseite .dm-kopf__titel {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -323,6 +323,86 @@ input[type="date"].dm-eingabe {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Die Vorschau in den Einstellungen zeigt das ganze Foto.
|
||||
|
||||
Vorher galt der Regelkasten 4:3 mit object-fit:cover - ein Foto anderen
|
||||
Formats wurde oben und unten beschnitten, bei der Frontansicht besonders
|
||||
deutlich (gemeldet 02.09.2026). Wer prüfen will, ob das richtige Bild
|
||||
hochgeladen ist, muss es ganz sehen. Dieselbe Entscheidung wie beim
|
||||
Fahrzeugfoto der Übersicht (.carfix.klein im Panel): das Bild bestimmt die
|
||||
Höhe, nicht der Kasten. Nur der Platzhalter behält ein festes Verhältnis,
|
||||
er hat ja keines von sich aus. */
|
||||
.dm-einstellbild .ads-bildbox {
|
||||
aspect-ratio: auto;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.dm-einstellbild .ads-bildbox img {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.dm-einstellbild .ads-bildbox:has(.ads-platzhalter) {
|
||||
aspect-ratio: 4 / 3;
|
||||
}
|
||||
|
||||
/* Die Draufsicht auf der Fahrzeugseite ist eine Beigabe, keine Szene: sie
|
||||
steht über den Sicherheitszeilen und soll die Zeilen nicht nach unten
|
||||
drängen. Statt randlos über die volle Breite (rund 374px auf einem Telefon,
|
||||
mit 16:8 also 187px hoch) steht sie eingerückt und mittig — 60% Breite, die
|
||||
Höhe geht im selben Verhältnis mit auf rund 112px zurück. Auf Ansage des
|
||||
Eigentümers am 02.09.2026 („Draufsicht-Foto Bereich ist zu groß"):
|
||||
schmaler, nicht flacher — flacher hätte bei einem Foto, das nicht sehr
|
||||
breit ist, oben und unten geschnitten. Gleichlautend .szene.bleed.schmal
|
||||
im Panel. Der :has()-Zweig oben setzt die Ränder bereits auf 0, wenn kein
|
||||
Foto da ist; hier gilt es unabhängig davon. */
|
||||
.dm-szene--schmal {
|
||||
/* Der Abstand zum Fahrzeug kommt jetzt aus dem Layout, nicht aus leerem
|
||||
Bildrand: das Foto ist seit dem 02.09.2026 freigestellt und eng
|
||||
beschnitten. Oben und unten Luft wie im Vorbild des Eigentümers. */
|
||||
margin: 0;
|
||||
padding: 22px 0 26px;
|
||||
}
|
||||
|
||||
.dm-szene--schmal .ads-bildbox {
|
||||
width: 60%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Zum Löschen nach links wischen (WischLoeschen in bausteine.tsx).
|
||||
|
||||
Der Knopf liegt fest hinter dem Inhalt, verschoben wird nur der Inhalt
|
||||
darüber — deshalb braucht er einen DECKENDEN Grund, sonst schiene das Rot
|
||||
hindurch. --tile ist der Grund der Kachel, in der die Zeile sitzt.
|
||||
|
||||
touch-action: pan-y sagt dem Browser, dass die waagerechte Achse uns
|
||||
gehört: senkrecht scrollt er weiter selbst, waagerecht wartet er auf uns,
|
||||
statt die Geste nach ein paar Pixeln zu übernehmen. */
|
||||
.dm-wisch {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dm-wisch__loeschen {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 88px;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
background: var(--bad);
|
||||
color: #fff;
|
||||
font: inherit;
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dm-wisch__inhalt {
|
||||
position: relative;
|
||||
background: var(--tile);
|
||||
touch-action: pan-y;
|
||||
}
|
||||
|
||||
.dm-szene .ads-bild,
|
||||
.dm-szene img {
|
||||
width: 100%;
|
||||
@@ -411,10 +491,25 @@ button.dm-serviceblock:active {
|
||||
padding-right: 44px;
|
||||
}
|
||||
|
||||
/* Das Zahnrad ist 44px hoch (Trefferfläche), die Überschrift darunter nur
|
||||
12px. Ohne reservierte Zeile ragte es also 20px in den Kachelinhalt und lag
|
||||
sichtbar auf dem ersten Knopf - auf der Versicherungsseite auf "Inland ·
|
||||
<Nummer>", von dort auch gemeldet (02.09.2026). Es fing dessen obere Hälfte
|
||||
ab: der Knopf ließ sich am oberen Rand nicht wählen.
|
||||
|
||||
Statt das Zahnrad zu verkleinern (44px sind die Mindestgröße einer
|
||||
Trefferfläche) bekommt die Kopfzeile die Höhe, die es braucht. Gleiche
|
||||
Regel im Panel (.tile:has(> .zahnrad)). */
|
||||
.dm-kachel--zahnrad > .ads-eyebrow:first-child,
|
||||
.dm-kachel--zahnrad > .ads-title:first-child {
|
||||
min-height: 34px;
|
||||
padding-right: 48px;
|
||||
}
|
||||
|
||||
.dm-zahnrad {
|
||||
position: absolute;
|
||||
top: 11px;
|
||||
right: 11px;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: inline-flex;
|
||||
|
||||
@@ -154,17 +154,26 @@ export function SymbolPaket({ groesse }: SymbolProps) {
|
||||
)
|
||||
}
|
||||
|
||||
/* fuel-station-s aus dem Audi-CI-Satz (24er-Raster), verbatim - derselbe
|
||||
Pfad wie CI.fuelStationS im Panel. Ersetzt seit dem 02.09.2026 eine
|
||||
handgezeichnete Zapfsäule.
|
||||
|
||||
Der Faktor 0,84 um die Rastermitte hat denselben Grund wie bei
|
||||
SymbolStatistik: das CI-Symbol ist randlos gezeichnet und stünde
|
||||
ungeskaliert mit 22 von 24 Einheiten Höhe als höchstes Symbol der Leiste
|
||||
da. Skaliert sind es 15,96 x 18,48 - dieselbe senkrechte Spanne wie
|
||||
Statistik (y 2,76 bis 21,24), live per getBBox() gemessen, Ränder
|
||||
eingeschlossen.
|
||||
|
||||
Die 48er-Variante hieße fuel-station-l und wird nicht gebraucht: die
|
||||
Reiter zeichnen mit 21 bzw. 26px, also im 24er-Raster. */
|
||||
export function SymbolTanken({ groesse }: SymbolProps) {
|
||||
return (
|
||||
<Rahmen
|
||||
<RahmenFlaeche
|
||||
groesse={groesse}
|
||||
kinder={
|
||||
<g>
|
||||
<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" />
|
||||
<g transform="translate(1.5,1.92) scale(0.84)">
|
||||
<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" />
|
||||
</g>
|
||||
}
|
||||
/>
|
||||
@@ -175,23 +184,24 @@ export function SymbolZurueck({ groesse }: SymbolProps) {
|
||||
return <Rahmen groesse={groesse} kinder={<path d="M15 5l-7 7 7 7" />} />
|
||||
}
|
||||
|
||||
export function SymbolZahnrad({ groesse }: SymbolProps) {
|
||||
return (
|
||||
<Rahmen
|
||||
groesse={groesse}
|
||||
kinder={
|
||||
<g>
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
<path d="M12 2v3M12 19v3M2 12h3M19 12h3M4.9 4.9l2.1 2.1M17 17l2.1 2.1M19.1 4.9L17 7M7 17l-2.1 2.1" />
|
||||
</g>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/* Hier stand bis zum 02.09.2026 SymbolZahnrad: ein selbst gezeichnetes
|
||||
Zahnrad (Kreis mit acht geraden Speichen), das nur in der Seitenleiste des
|
||||
Flächenlayouts auftauchte - auf einem Telefon also nie. Ersetzt durch
|
||||
settings-s aus dem CI-Satz, dasselbe Symbol wie in den Kachel-Ecken. Es
|
||||
stand ohnehin schon neben drei CI-Flächensymbolen (Mein Audi, Fahrten,
|
||||
Statistik); das gezeichnete war der Ausreißer, nicht umgekehrt. */
|
||||
|
||||
/* Das Zahnrad der Kachel-Ecken (`.zahnrad` im Panel) — ein gefülltes Symbol
|
||||
aus dem Audi-CI-Satz, verbatim übernommen. Nicht zu verwechseln mit
|
||||
SymbolZahnrad oben, dem Kontur-Symbol der Seitenleiste. */
|
||||
/* settings-s aus dem Audi-CI-Satz (24er-Raster), verbatim - dieselbe Datei
|
||||
wie design/assets/icons/settings.svg und derselbe Pfad wie CI.settingsS im
|
||||
Panel. Steht in der rechten oberen Ecke einer Kachel (`.zahnrad` dort).
|
||||
|
||||
Die 48er-Variante hieße settings-l; sie wird nirgends gebraucht, das
|
||||
Zahnrad wird immer 17px dargestellt.
|
||||
|
||||
"Voll" im Namen meint die Fläche des CI-Symbols (ein gefüllter Pfad mit
|
||||
Aussparungen), nicht eine gefüllte Variante der Bibliothek. Nicht zu
|
||||
Bibliothek. Seit dem 02.09.2026 auch in der Seitenleiste des
|
||||
Flächenlayouts - ein zweites, gezeichnetes Zahnrad gibt es nicht mehr. */
|
||||
export function SymbolZahnradVoll({ groesse }: SymbolProps) {
|
||||
return (
|
||||
<RahmenFlaeche
|
||||
|
||||
@@ -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"}
|
||||
Binary file not shown.
@@ -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"],
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user