Bilder: der Cache-Brecher haengt am Foto statt an der Startzeit

Beide Oberflaechen haengten Date.now() an jede Bildadresse - richtig
gegen ein veraltetes Foto (31 Tage Cache-Vorgabe unter /local/), aber die
Adresse aendert sich damit bei JEDEM Start, der Zwischenspeicher greift
zwischen zwei Starts also nie. Mit dem Vorladen waeren das 5,3 MB je
Start gewesen.

bilder.staende() liefert jetzt den Zeitstempel je Datei, veroeffentlicht
an sensor.audi_dashboard_app_version; eine Bildadresse aendert sich damit
genau dann, wenn das Foto ein anderes ist. Ein fehlendes Foto bekommt 0
(dann loest sich auch ein gespeicherter 404 von selbst auf), ein Backend
ohne Staende faellt auf das bisherige Verhalten zurueck, und Upload wie
Loeschen veroeffentlichen sofort neu.

Die App merkt sich den letzten Stand im Browserspeicher: sie malt ihren
ersten Bildschirm, bevor die Versionsangabe eintrifft - ohne das
Gedaechtnis trug genau dieser Durchlauf noch die Startzeit und holte das
Uebersichtsfoto doch wieder bei jedem Start.

Live gemessen: Neuladen ohne Aenderung 5.328.771 Bytes Inhalt bei 0 Bytes
uebertragen; ein per touch geaendertes Foto wird neu geholt (513.403
Bytes), die anderen nicht; App-Start ohne eine einzige Adresse mit
Startzeit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-04 13:29:45 +02:00
parent d6f7993504
commit 68732aa10f
12 changed files with 324 additions and 26 deletions
+67 -2
View File
@@ -9,8 +9,9 @@ installieren - der Erweiterung fehlte die CFBundleVersion; dazu der QR-Scanner
fuer den Zugangs-Token; dazu Debug-Mode, eine Version-Kachel, das Knopfpaar nach
HIG und Bilder ohne Verzoegerung; dazu eine Musterseite, die die App ohne Token
und ohne Netz rendert; dazu der QR-Code aus einem gespeicherten Bild, ein stiller
Debug-Knopf und der HA-Zugang bei den Zugaengen, `2026.9.4.12`,
Abschnitte CC bis CH. Davor: Geraetezeit statt Ankunftszeit - die Wurzel hinter der 3-km-Fahrt:
Debug-Knopf und der HA-Zugang bei den Zugaengen; der Cache-Brecher der Bilder haengt
jetzt am Foto statt am App-Start, `2026.9.4.14`,
Abschnitte CC bis CI. Davor: Geraetezeit statt Ankunftszeit - die Wurzel hinter der 3-km-Fahrt:
das Fahrtfenster stand in Geraetezeit, der Verlauf war nach Ankunftszeit sortiert. Dazu die neun
Paritaetsbefunde und die verunreinigte Batteriehistorie, `2026.9.4.1`, Abschnitt CB. Davor: Der Regler stellt den Schlaf-Timeout des Dongles, flespi lesend und
schreibend, die eigene Warteschlange wieder entfallen, `2026.9.3.10`, Abschnitt BZ. Davor: Zugaenge: die Dienste-Token aus App und Panel bedienbar, der Wert kommt nie
@@ -10101,3 +10102,67 @@ App meldet daraufhin voellig zu Recht „Der Server ist unter dieser Adresse nic
erreichbar". Einzutragen ist die **eigene** Adresse `http://localhost:5173`; der
Vite-Proxy reicht `/api` serverseitig an `:18123` weiter (Abschnitt AK). Nach der
Korrektur stand die Uebersicht mit echten Daten.
## CI. Der Cache-Brecher haengt jetzt am Foto, nicht am App-Start (2026.9.4.14)
Entscheidung des Eigentuemers auf die drei vorgelegten Wege (Abschnitt CG):
nicht weniger vorladen, sondern den wiederholten Abruf ganz abschaffen.
### Was falsch war
Beide Oberflaechen haengten `?v=${Date.now()}` an jede Bildadresse - gesetzt
beim Start und nach jedem Hochladen. Das loest zwar zuverlaessig den
eigentlichen Zweck (Home Assistant liefert `/local/` mit 31 Tagen
Cache-Vorgabe, ohne Anhaengsel bliebe ein ersetztes Foto einen Monat lang das
alte), erkauft ihn aber teuer: die Adresse ist bei **jedem** Start eine andere,
der Zwischenspeicher des Browsers kann also zwischen zwei Starts nie greifen.
Mit dem Vorladen aller Fotos waeren das **5,3 MB je Start** gewesen.
### Was jetzt gilt
`bilder.staende()` liefert den Zeitstempel **je Datei**
(`{"seitenansicht.webp": 1788161081, …}`), veroeffentlicht in den `daten` von
`sensor.audi_dashboard_app_version` - dieselbe Entitaet, die schon Buendel,
Zugaenge, Dongle und den letzten Datensatz traegt. Elf Dateizugriffe, im
Executor, weil blockierende Aufrufe in der Ereignisschleife seit 2026.8 als
Fehler gemeldet werden.
Damit aendert sich eine Bildadresse **genau dann, wenn das Foto ein anderes
ist** - und sonst nie.
Drei Feinheiten, jede aus einem konkreten Fall:
- **Ein Foto, das es nicht gibt, bekommt `v=0`.** Home Assistant liefert auch
den 404 mit Cache-Vorgabe; sobald das Foto hochgeladen ist, traegt die
Adresse seinen Zeitstempel und der gespeicherte Fehlschlag ist ueberholt.
- **Ohne Staende gilt das alte Verhalten** (`Date.now()`). Ein aelteres Backend
darf nicht dazu fuehren, dass alle Adressen auf einem festen `v=0` stehen
bleiben - das waere ein Foto, das sich nie mehr aktualisiert.
- **Die App merkt sich den letzten Stand im Browserspeicher.** Sie malt ihren
ersten Bildschirm, BEVOR die Versionsangabe eintrifft; ohne das Gedaechtnis
trug genau dieser eine Durchlauf noch die Startzeit und holte das
Uebersichtsfoto bei jedem Start erneut - live gesehen, nachdem der Rest
schon stimmte. Ein veralteter gemerkter Stand ist ungefaehrlich: sobald die
frische Angabe da ist, wechselt die Adresse und das Bild wird nachgeholt.
`bild_hochladen`/`bild_loeschen` veroeffentlichen jetzt sofort neu - sonst
saehe die Oberflaeche das neue Foto erst beim naechsten Minutentakt.
Das Vorladen laeuft nur noch einmal je **Bildstand** statt einmal je Start;
die Kennung ist die Liste der Staende.
### Live nachgewiesen, beide Haelften
Im Panel und in der App, gegen die echten Dateien des Testcontainers:
| | |
|---|---|
| veroeffentlichte Staende | deckungsgleich mit den `mtime` der sechs vorhandenen Dateien |
| Neuladen ohne Aenderung | **5.328.771 Bytes Inhalt, 0 Bytes uebertragen**, 0 ms - alles aus dem Zwischenspeicher |
| ein Foto geaendert (`touch`) | neuer Stand veroeffentlicht, neue Adresse, **513.403 Bytes** neu geholt - die anderen unangetastet |
| App-Start, zweiter Durchlauf | **0 Bytes uebertragen insgesamt**, keine einzige Adresse mit Startzeit |
Dazu 6 Regressionstests (`src/screens/bilder.test.ts`): Zeitstempel statt
Startzeit, gleich bleibend ohne Aenderung, wechselnd bei Aenderung, `0` fuer
ein fehlendes Foto, Rueckfall ohne Staende, und der gemerkte Stand ueber einen
Neustart.
+2
View File
@@ -297,6 +297,8 @@ export interface AppVersionAngabe {
zugaenge?: ZugangAngabe[];
dongle?: DongleAngabe;
letzter_datensatz?: LetzterDatensatz | null;
/** Zeitstempel je vorhandenem Fahrzeugfoto - siehe bilder.staende(). */
bildstaende?: Record<string, number> | null;
}
/* -------------------------------------------------- Entitäts-Verzeichnis */
+5
View File
@@ -34,6 +34,7 @@ import {
type Verbindungszustand,
type WartenderAuftrag,
} from "../api"
import { bildStaendeSetzen } from "../screens/bilder"
import type { Versionsstand } from "./appVersion"
import { eigeneVersion, versionVergleichen } from "./appVersion"
import type { Einstellungen, Fahrzeug } from "./profilAdapter"
@@ -172,6 +173,10 @@ export function DatenAnbieter({
setzeZugaenge(versionsangabe?.zugaenge ?? [])
setzeDongle(versionsangabe?.dongle ?? null)
setzeLetzterDatensatz(versionsangabe?.letzter_datensatz ?? null)
// Kein React-Zustand: die Bildadressen werden beim Rendern gebraucht,
// teils ausserhalb von Komponenten (bilderVorladen) - dieselbe
// Ueberlegung wie bei zugangMerken().
bildStaendeSetzen(versionsangabe?.bildstaende ?? null)
return true
} catch (fehler) {
// Ein voruebergehender Ausfall ist kein Ladefehler, solange schon
+76
View File
@@ -0,0 +1,76 @@
/**
* Der Bildstand entscheidet, wann ein Foto neu geholt wird.
*
* Genau daran hing der gemeldete Fehler: bis zum 04.09.2026 stand am Ende
* jeder Bildadresse die Startzeit der App, und damit holte jeder Start alle
* Fotos erneut (gemessen: 5,3 MB) - der Zwischenspeicher konnte zwischen zwei
* Starts gar nicht greifen. Jetzt kommt der Zeitstempel je Datei vom Backend,
* und diese Tests halten die drei Faelle fest, auf die es dabei ankommt.
*/
import { beforeEach, describe, expect, it, vi } from "vitest"
vi.mock("./zugang", () => ({
zugangLesenSynchron: () => ({ basisUrl: "http://muster.local:8123", token: "x" }),
zugangVergessen: () => {},
zugangMerken: async () => {},
}))
const { bildStaendeSetzen, bildStand, bildUrl } = await import("./bilder")
describe("Bildstand", () => {
beforeEach(() => {
bildStaendeSetzen(null)
})
it("nimmt den Zeitstempel der Datei, nicht die Startzeit", () => {
bildStaendeSetzen({ "seitenansicht.webp": 1788500000 })
expect(bildStand("seitenansicht.webp")).toBe(1788500000)
expect(bildUrl("seitenansicht.webp")).toBe(
"http://muster.local:8123/local/bilder/seitenansicht.webp?v=1788500000",
)
})
it("bleibt gleich, solange sich das Foto nicht aendert", () => {
bildStaendeSetzen({ "seitenansicht.webp": 1788500000 })
const erste = bildUrl("seitenansicht.webp")
// Zweiter Start, dieselbe Antwort des Backends.
bildStaendeSetzen({ "seitenansicht.webp": 1788500000 })
expect(bildUrl("seitenansicht.webp")).toBe(erste)
})
it("aendert sich, sobald das Foto ein anderes ist", () => {
bildStaendeSetzen({ "seitenansicht.webp": 1788500000 })
const vorher = bildUrl("seitenansicht.webp")
bildStaendeSetzen({ "seitenansicht.webp": 1788599999 })
expect(bildUrl("seitenansicht.webp")).not.toBe(vorher)
})
it("setzt 0 fuer ein Foto, das es nicht gibt", () => {
// Wichtig, weil Home Assistant auch den 404 mit Cache-Vorgabe ausliefert:
// sobald das Foto hochgeladen ist, traegt die Adresse seinen Zeitstempel
// und der gespeicherte Fehlschlag ist damit ueberholt.
bildStaendeSetzen({ "seitenansicht.webp": 1788500000 })
expect(bildStand("cockpit.webp")).toBe(0)
})
it("faellt auf die Startzeit zurueck, wenn das Backend keine Staende liefert", () => {
// Aeltere Integration: dann gilt das Verhalten von vorher, statt alle
// Adressen auf ein festes v=0 zu legen - das waere ein Foto, das sich nie
// mehr aktualisiert.
bildStaendeSetzen(null)
expect(bildStand("seitenansicht.webp")).toBeGreaterThan(1_000_000_000_000)
})
})
describe("Bildstand: gemerkt ueber den Neustart", () => {
it("malt den ersten Bildschirm schon mit dem letzten bekannten Stand", async () => {
// Die Versionsangabe trifft erst NACH dem ersten Rendern ein. Ohne das
// Gedaechtnis traegt dieser eine Durchlauf die Startzeit - und holt das
// Uebersichtsfoto bei jedem Start erneut.
bildStaendeSetzen({ "seitenansicht.webp": 1788500000 })
vi.resetModules()
const frisch = await import("./bilder")
expect(frisch.bildStand("seitenansicht.webp")).toBe(1788500000)
})
})
+68 -6
View File
@@ -93,13 +93,67 @@ export const ERLAUBTE_DATEINAMEN: readonly string[] = [
* setzt den Wert nach jedem Hochladen oder Löschen neu. Hier dasselbe: beim
* Start der App einmal, danach bei jeder Änderung.
*
* Der Preis ist ein erneuter Abruf je App-Start. Der ist gewollt — ein Foto,
* das der Nutzer nicht sieht, kostet mehr als ein Megabyte über eine
* Verbindung, die ohnehin gerade das ganze Fahrzeugprofil holt.
* Bis zum 04.09.2026 war das `Date.now()` beim App-Start - eine Adresse, die
* sich bei JEDEM Start ändert. Der Zwischenspeicher griff damit zwischen
* zwei Starts nie, und mit dem Vorladen aller Fotos wären das 5,3 MB je
* Start gewesen. Jetzt kommt der Zeitstempel jeder Datei vom Backend
* (`bilder.staende()`, veröffentlicht an `sensor.audi_dashboard_app_version`):
* er ändert sich genau dann, wenn das Foto wirklich ein anderes ist.
*
* `bildVersion` bleibt als Rückfall für ein Backend, das die Stände noch
* nicht mitliefert - dann ist das Verhalten wie zuvor.
*/
/*
* Gemerkt wird der letzte bekannte Stand im Browserspeicher, und zwar aus
* einem gemessenen Grund: die App malt ihren ersten Bildschirm, BEVOR die
* Versionsangabe (und damit die Stände) eintrifft. Ohne dieses Gedächtnis
* trägt genau dieser eine Durchlauf noch die Startzeit als Anhängsel - und
* holt das Übersichtsfoto damit bei jedem Start erneut, also exakt die
* Kosten, um die es hier geht (am 04.09.2026 live gesehen).
*
* Ein veralteter gemerkter Stand ist ungefährlich: sobald die frische
* Angabe da ist, wechselt die Adresse und das Bild wird nachgeholt.
*/
const SPEICHERSCHLUESSEL = "dm360.bildstaende"
function gemerkteStaende(): Record<string, number> | null {
try {
const roh = window.localStorage.getItem(SPEICHERSCHLUESSEL)
if (!roh) return null
const wert: unknown = JSON.parse(roh)
return wert && typeof wert === "object" ? (wert as Record<string, number>) : null
} catch {
// Kein Speicher (privates Fenster, Testumgebung) - dann eben ohne.
return null
}
}
let bildStaende: Record<string, number> | null =
typeof window === "undefined" ? null : gemerkteStaende()
let bildVersion = Date.now()
/** Nach dem Hochladen oder Löschen eines Fotos aufrufen. */
/** Die Stände aus der veröffentlichten Entität übernehmen. */
export function bildStaendeSetzen(staende: Record<string, number> | null): void {
bildStaende = staende
try {
if (staende) window.localStorage.setItem(SPEICHERSCHLUESSEL, JSON.stringify(staende))
else window.localStorage.removeItem(SPEICHERSCHLUESSEL)
} catch {
// Siehe oben - das Merken ist eine Beschleunigung, keine Bedingung.
}
}
/**
* Der Stand einer Datei. 0 heißt: dieses Foto gibt es nicht - sobald es
* hochgeladen wird, ändert sich die Adresse von selbst, und auch ein
* gespeicherter 404 löst sich damit auf.
*/
export function bildStand(dateiname: string): number {
if (!bildStaende) return bildVersion
return bildStaende[dateiname] ?? 0
}
/** Nach dem Hochladen oder Löschen eines Fotos aufrufen (nur der Rückfall). */
export function bildVersionErneuern(): void {
bildVersion = Date.now()
}
@@ -113,7 +167,7 @@ export function bildUrl(dateiname: string | null | undefined): string | undefine
if (!dateiname) return undefined
const basis = zugangLesenSynchron()?.basisUrl
if (!basis) return undefined
return `${basis}/local/bilder/${dateiname}?v=${bildVersion}`
return `${basis}/local/bilder/${dateiname}?v=${bildStand(dateiname)}`
}
/**
@@ -252,10 +306,18 @@ export async function bildVerkleinern(datei: File): Promise<Blob> {
* Nutzer wirklich braucht. Das Datenvolumen ist dasselbe wie beim Durchblaettern
* aller Seiten, nur frueher und ohne Warten.
*/
let vorgeladen: string | null = null
export function bilderVorladen(): void {
if (typeof window === "undefined" || typeof Image === "undefined") return
const namen = [...BILDPLAETZE.map((b) => b.datei), SEITE_WINTER.datei]
// Kennung über alle Stände: solange sich kein Foto geändert hat, läuft das
// Vorladen kein zweites Mal - und der Browser holt die Bilder beim nächsten
// Start aus seinem eigenen Zwischenspeicher.
const kennung = namen.map(bildStand).join(",")
if (vorgeladen === kennung) return
vorgeladen = kennung
const starten = () => {
const namen = [...BILDPLAETZE.map((b) => b.datei), SEITE_WINTER.datei]
for (const name of namen) {
const url = bildUrl(name)
if (!url) continue
@@ -46,6 +46,43 @@ ERLAUBTE_DATEINAMEN = frozenset({
})
def _staende(ordner: str) -> dict[str, int]:
"""Zeitstempel jedes vorhandenen Fotos, als Sekunden.
Er dient beiden Oberflaechen als Anhaengsel an der Bildadresse
(`?v=`). Home Assistant liefert /local/ mit 31 Tagen Cache-Vorgabe;
ohne wechselndes Anhaengsel zeigt eine Oberflaeche deshalb einen
Monat lang das Foto, das sie beim ersten Mal geholt hat.
WARUM DER ZEITSTEMPEL UND NICHT DIE STARTZEIT DER APP: bis zum
04.09.2026 haengte jede Oberflaeche `Date.now()` an - eine Adresse,
die sich bei JEDEM Start aendert. Der Zwischenspeicher des Browsers
griff damit zwischen zwei Starts nie, jeder Start holte alle Fotos
erneut (gemessen: 5,3 MB). Der Zeitstempel aendert sich dagegen
genau dann, wenn das Foto wirklich ein anderes ist - dann und nur
dann wird neu geholt.
Ein Foto, das es nicht gibt, fehlt hier: die Oberflaechen setzen
dafuer 0 ein, und sobald es hochgeladen wird, aendert sich die
Adresse von selbst - auch ein zwischengespeicherter 404 loest sich
damit auf.
"""
ergebnis: dict[str, int] = {}
for name in ERLAUBTE_DATEINAMEN:
try:
ergebnis[name] = int(os.path.getmtime(os.path.join(ordner, name)))
except OSError:
continue
return ergebnis
async def staende(hass: HomeAssistant, ordner: str) -> dict[str, int]:
# Elf Dateizugriffe sind billig, gehoeren aber trotzdem nicht in die
# Ereignisschleife - Home Assistant meldet blockierende Aufrufe dort
# seit 2026.8 als Fehler.
return await hass.async_add_executor_job(_staende, ordner)
def _pfad(ordner: str, dateiname: str | None) -> str | None:
if not dateiname or dateiname not in ERLAUBTE_DATEINAMEN:
return None
+16 -4
View File
@@ -407,13 +407,14 @@ def registrieren(hass: HomeAssistant, k: Koordinator) -> None:
D_TANKVORGANG_LOESCHEN: _als_dienst(
lambda a: belege.tankvorgang_loeschen(k, a.data["tank_id"])
),
# Nach dem Schreiben sofort neu veroeffentlichen: der Bildstand
# haengt an der Datei, und ohne diese Zeile saehe die Oberflaeche
# das neue Foto erst beim naechsten Minutentakt.
D_BILD_HOCHLADEN: _als_dienst(
lambda a: bilder.hochladen(
hass, bilder_ordner, a.data["dateiname"], a.data["daten_base64"]
)
lambda a: _bild_hochladen(k, hass, bilder_ordner, a)
),
D_BILD_LOESCHEN: _als_dienst(
lambda a: bilder.loeschen(hass, bilder_ordner, a.data["dateiname"])
lambda a: _bild_loeschen(k, hass, bilder_ordner, a)
),
D_REIFEN_WECHSELN: _als_dienst(lambda a: reifen.wechseln(k, a.data["satz"])),
D_REIFEN_KM_SETZEN: _als_dienst(
@@ -444,3 +445,14 @@ def registrieren(hass: HomeAssistant, k: Koordinator) -> None:
def abmelden(hass: HomeAssistant) -> None:
for name in SCHEMATA:
hass.services.async_remove(DOMAIN, name)
async def _bild_hochladen(k, hass, ordner, aufruf) -> None:
await bilder.hochladen(
hass, ordner, aufruf.data["dateiname"], aufruf.data["daten_base64"]
)
await k.app_version_veroeffentlichen()
async def _bild_loeschen(k, hass, ordner, aufruf) -> None:
await bilder.loeschen(hass, ordner, aufruf.data["dateiname"])
await k.app_version_veroeffentlichen()
@@ -1 +1 @@
{"version":"2026.9.4.12","sha256":"bb66727726eac2e6be98dc1132bd3b532c8c6b9525f9885020a65d99d2d7fc55","bytes":321283,"gebaut":"2026-09-04T11:05:31Z"}
{"version":"2026.9.4.14","sha256":"38298c65ce8557c805efd30f56e6978ea60857321ae008efd647cddd7b1b50d5","bytes":321436,"gebaut":"2026-09-04T11:28:44Z"}
@@ -365,11 +365,33 @@ const BILDER = [
const GALERIE = BILDER.filter((b) => !b.nurStatus);
let bild = 0;
const SEITE_WINTER = { label: "Auto Seite mit Winterrädern", datei: "seitenansicht-winter.webp" };
// Bei jedem Hochladen/Löschen eines Fahrzeugfotos hochgezählt und als
// Query-Parameter an alle bilder/-URLs gehängt (siehe bildInfo()
// unten) - ohne das würde der Browser nach einem Upload weiter das alte Bild
// oder den zwischenzeitlich gecachten 404 unter derselben URL zeigen.
/* Der Stand eines Fahrzeugfotos, als Anhaengsel an seiner Adresse.
Home Assistant liefert /local/ mit 31 Tagen Cache-Vorgabe aus; ohne
wechselndes Anhaengsel zeigt der Browser nach einem Upload weiter das alte
Bild oder den zwischenzeitlich gespeicherten 404.
Bis zum 04.09.2026 stand hier `Date.now()` - eine Adresse, die sich bei
JEDEM Start aendert. Der Zwischenspeicher griff damit zwischen zwei Starts
nie, und mit dem Vorladen aller Fotos waeren das 5,3 MB je Start gewesen.
Jetzt kommt der Zeitstempel jeder Datei vom Backend (bilder.staende(),
veroeffentlicht an sensor.audi_dashboard_app_version): er aendert sich
genau dann, wenn das Foto wirklich ein anderes ist.
bildVersion bleibt als Rueckfall fuer ein Backend, das die Staende noch
nicht mitliefert - dann ist das Verhalten wie zuvor. */
let BILD_STAENDE = null;
let bildVersion = Date.now();
function bildStand(datei) {
if (!BILD_STAENDE) return bildVersion;
// 0 heisst: dieses Foto gibt es nicht. Sobald es hochgeladen wird,
// aendert sich die Adresse von selbst - auch ein gespeicherter 404
// loest sich damit auf.
return BILD_STAENDE[datei] ?? 0;
}
function bildAdresse(datei) {
return `/local/bilder/${datei}?v=${bildStand(datei)}`;
}
/* Platzhalter statt kaputtem Bild-Symbol: bei Ladefehler (Bilder aus §7a
noch nicht vorhanden) wird die Bildfläche durch ein generisches Symbol
@@ -499,9 +521,9 @@ async function bildVerkleinern(datei) {
function bildInfo(i) {
const b = BILDER[i];
if (b.name === "Seitenansicht" && CAR.reifen.aktiv === "Winter") {
return { label: SEITE_WINTER.label, datei: SEITE_WINTER.datei, src: `/local/bilder/${SEITE_WINTER.datei}?v=${bildVersion}` };
return { label: SEITE_WINTER.label, datei: SEITE_WINTER.datei, src: bildAdresse(SEITE_WINTER.datei) };
}
return { label: b.label, datei: b.datei, src: `/local/bilder/${b.datei}?v=${bildVersion}` };
return { label: b.label, datei: b.datei, src: bildAdresse(b.datei) };
}
/* Alle Fahrzeugfotos einmal vorab holen UND dekodieren.
@@ -516,12 +538,17 @@ function bildInfo(i) {
konkurriert, und nur einmal je Bildstand. */
let bilderVorgeladen = null;
function bilderVorladen() {
if (bilderVorgeladen === bildVersion) return;
bilderVorgeladen = bildVersion;
const dateien = [...BILDER.map((b) => b.datei), SEITE_WINTER.datei];
// Kennung ueber alle Staende: solange sich kein Foto geaendert hat,
// laeuft das Vorladen kein zweites Mal - und der Browser holt die Bilder
// beim naechsten Start aus seinem eigenen Zwischenspeicher.
const kennung = dateien.map(bildStand).join(",");
if (bilderVorgeladen === kennung) return;
bilderVorgeladen = kennung;
const starten = () => {
for (const datei of [...BILDER.map((b) => b.datei), SEITE_WINTER.datei]) {
for (const datei of dateien) {
const img = new Image();
img.src = `/local/bilder/${datei}?v=${bildVersion}`;
img.src = bildAdresse(datei);
// Ein nicht hochgeladenes Foto ist der Normalfall - still schlucken.
if (img.decode) img.decode().catch(() => {});
}
@@ -2181,7 +2208,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 schmal"><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(bildAdresse("draufsicht.webp"), "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>
@@ -3842,7 +3869,7 @@ function vReifen() {
</div>
</div>
<div data-bildklick="${radDatei}">
${bildMitPlatzhalter(`/local/bilder/${radDatei}?v=${bildVersion}`, radDatei, radLabel, "radbild")}
${bildMitPlatzhalter(bildAdresse(radDatei), radDatei, radLabel, "radbild")}
</div>
</div>
${kmOffen === k ? `
@@ -6786,6 +6813,9 @@ async function datenLaden(erzwingen) {
LETZTER_DATENSATZ = versionState.attributes.daten
? versionState.attributes.daten.letzter_datensatz ?? null
: null;
BILD_STAENDE = versionState.attributes.daten
? versionState.attributes.daten.bildstaende ?? null
: null;
}
if (entitaetenState && entitaetenState.attributes) ENTITAETEN = entitaetenState.attributes.daten;
@@ -49,6 +49,7 @@ from homeassistant.helpers.storage import Store
from . import (
batterie,
bilder,
fahrterkennung,
flespi,
geraet,
@@ -64,6 +65,7 @@ from . import (
from .ablage import Ablage
from .geokodierung import Geokodierung
from .const import (
BILDER_ORDNER,
BUENDEL_INFO,
BUENDEL_ORDNER,
BUENDEL_URL,
@@ -621,6 +623,9 @@ class Koordinator:
async def app_version_veroeffentlichen(self) -> None:
buendel = await self.hass.async_add_executor_job(self._buendel_lesen)
bildstaende = await bilder.staende(
self.hass, self.hass.config.path(BILDER_ORDNER)
)
# Die App liest alle drei aus derselben Entität: welchen Stand diese
# Installation ausliefert, ob ein nachladbares OTA-Bündel danebenliegt,
# und ob eine neuere Fassung bei Gitea bereitliegt. Zusammen an einer
@@ -649,6 +654,10 @@ class Koordinator:
"letzter_datensatz": geraet.letzter_datensatz(
self.hass, self.zuordnung.werte
),
# Der Stand jedes Fahrzeugfotos, damit beide Oberflaechen
# ein Bild nur dann neu holen, wenn es wirklich ein
# anderes ist - siehe bilder.staende().
"bildstaende": bildstaende,
},
)
@@ -1,7 +1,7 @@
{
"domain": "audi_dashboard",
"name": "Audi Dashboard",
"version": "2026.9.4.12",
"version": "2026.9.4.14",
"documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md",
"issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues",
"codeowners": [