Debug-Mode, eine Version-Kachel, Knoepfe nach HIG, Bilder ohne Verzoegerung (2026.9.4.11)

Debug-Mode: Zugaenge und Dongle stehen nicht mehr verstreut oben in den
Einstellungen, sondern zusammen hinter einem Schalter am Seitenende - dazu
neu der letzte Datensatz des Dongles (geraet.py). "Letzter Datensatz" heisst
dabei: alle Entitaeten des Geraets, deren Aktualisierung hoechstens zwei
Sekunden nach der juengsten liegt - die flespi-Integration setzt einen
Datensatz in Millisekunden. Angezeigt mit Ankunft, Geraetezeit und dem
Rueckstand zwischen beiden.

Beim Verifizieren gefunden: app_version_veroeffentlichen() lief nur beim
Start und nach einer Aktion, und beim Start haben die Entitaeten des Geraets
noch keinen Zustand - die Kachel waere dauerhaft leer geblieben. Laeuft
jetzt im Minutentakt mit, ohne Netzaufruf.

Ausserdem:
- "Version" und "Integration-Update" sind eine Kachel
- Speichern/Verwerfen als zwei gleich breite Knoepfe, der bestaetigende
  gefuellt und rechts; "Verwerfen" war in der App blosser Text
- Bilder werden nach dem ersten Zeichnen in der Leerlaufzeit vorgeladen UND
  dekodiert. Die Verzoegerung beim Seitenwechsel kam nicht vom Netz (31 Tage
  Cache), sondern vom asynchronen Dekodieren eines frisch erzeugten <img>.

214/214 Tests, Panel als Modul geparst, live gegen audi_ha_test geprueft.
This commit is contained in:
2026-09-04 12:15:33 +02:00
parent 63a45d4ee2
commit 4ab6b9fca5
14 changed files with 695 additions and 182 deletions
@@ -1 +1 @@
{"version":"2026.9.4.8","sha256":"d6fe041f31480ff67d9b39884e9ef8b5ac5795d8575716d4638a9f557198bc49","bytes":319995,"gebaut":"2026-09-04T09:50:03Z"}
{"version":"2026.9.4.11","sha256":"937302e85e87c62326c4f7013cb026f50243b8cc5d521d752b5a3708120d2b91","bytes":320679,"gebaut":"2026-09-04T10:13:31Z"}
@@ -503,6 +503,33 @@ function bildInfo(i) {
}
return { label: b.label, datei: b.datei, src: `/local/bilder/${b.datei}?v=${bildVersion}` };
}
/* Alle Fahrzeugfotos einmal vorab holen UND dekodieren.
Gemeldet als "Bilder laden beim Umschalten der Menues verzoegert". Am Netz
liegt es nicht: die Adresse ist innerhalb einer Sitzung stabil und Home
Assistant liefert /local/ mit 31 Tagen Cache-Dauer. Verzoegert wird das Bild
selbst - ein frisch erzeugtes <img> wird standardmaessig asynchron dekodiert
und erst danach gezeichnet, und render() erzeugt bei jedem Seitenwechsel ein
neues. decode() erledigt genau diesen Schritt vorab.
Laeuft in der Leerlaufzeit, damit es nicht mit dem ersten Datenabruf
konkurriert, und nur einmal je Bildstand. */
let bilderVorgeladen = null;
function bilderVorladen() {
if (bilderVorgeladen === bildVersion) return;
bilderVorgeladen = bildVersion;
const starten = () => {
for (const datei of [...BILDER.map((b) => b.datei), SEITE_WINTER.datei]) {
const img = new Image();
img.src = `/local/bilder/${datei}?v=${bildVersion}`;
// Ein nicht hochgeladenes Foto ist der Normalfall - still schlucken.
if (img.decode) img.decode().catch(() => {});
}
};
if (window.requestIdleCallback) window.requestIdleCallback(starten, { timeout: 4000 });
else setTimeout(starten, 2000);
}
function carImg(modus) {
if (modus === "fest") {
const info = bildInfo(startIndex());
@@ -3242,28 +3269,6 @@ function vEinst() {
` : ""}
<button class="aktion ${einrichtenOffen ? "primaer" : ""}" style="margin-top:16px" data-einrichten-auf>${einrichtenOffen ? "Fertig" : "Einrichten"}</button>
</div>
${ZUGAENGE.length ? `<div class="tile"><span class="label">Zugänge</span>
${ZUGAENGE.map((z, i) => `
<button class="acc-h" aria-expanded="false" data-acc="zug${i}"><span class="mark"></span>
<span class="k">${esc(z.titel)}</span>
<span class="v" style="color:${z.gesetzt ? "var(--ok)" : "var(--fg3)"}">${
z.gesetzt ? (z.rest ? "gesetzt · …" + esc(z.rest) : "gesetzt") : "nicht gesetzt"
}</span></button>
<div class="acc-b" id="zug${i}">
<span class="label" style="margin-top:8px;color:var(--fg2);letter-spacing:0;
text-transform:none;font-size:12.5px;line-height:1.6">${esc(z.zweck)}</span>
<div class="feld" style="border-bottom:0"><label for="tok${i}">Neuer Token</label>
<input id="tok${i}" type="password" autocomplete="off" data-tokenfeld="${esc(z.dienst)}"
placeholder="${z.gesetzt ? "ersetzt den hinterlegten" : "noch keiner hinterlegt"}"></div>
<button class="aktion primaer" data-tokenspeichern="${esc(z.dienst)}">Speichern</button>
${z.gesetzt ? `<button class="aktion loeschen" data-tokenloeschen="${esc(z.dienst)}">Löschen</button>` : ""}
</div>`).join("")}
<span class="label" style="margin-top:14px;color:var(--fg2);letter-spacing:0;
text-transform:none;font-size:12.5px;line-height:1.6">
Ein gespeicherter Token wird nie wieder angezeigt nur ob einer hinterlegt ist.
Dieselben Felder gibt es unter Einstellungen Geräte &amp; Dienste Audi Dashboard
Konfigurieren.</span>
</div>` : ""}
${(() => {
/* Der Regler schreibt NICHT mehr beim Loslassen. Sein Wert stellt zugleich
den Schlaf-Timeout des Dongles - eine Änderung am Fahrzeug, die über zwei
@@ -3280,8 +3285,10 @@ function vEinst() {
aria-label="Minuten bis zum automatischen Fahrtende">
<output>${minuten} Min.</output>
</div>
${geaendert ? `<button class="aktion primaer" style="margin-top:14px" data-pausenzeitspeichern>Speichern</button>
<button class="aktion" data-pausenzeitverwerfen>Verwerfen</button>` : ""}
${geaendert ? `<div class="knopfpaar">
<button class="aktion" data-pausenzeitverwerfen>Verwerfen</button>
<button class="aktion primaer" data-pausenzeitspeichern>Speichern</button>
</div>` : ""}
<span class="label" style="margin-top:14px;color:var(--fg2);letter-spacing:0;
text-transform:none;font-size:12.5px;line-height:1.6">
Hier können Sie einstellen, wie viele Minuten nach Ausschalten der Zündung die
@@ -3291,36 +3298,6 @@ function vEinst() {
Fortsetzen aber ein paar Sekunden fürs Aufwachen.</span>
</div>`;
})()}
${DONGLE && DONGLE.aktiv ? `<div class="tile"><span class="label">Dongle</span>
${DONGLE.stand && DONGLE.stand.fehler
? `<span class="label" style="margin-top:8px;color:var(--bad);letter-spacing:0;
text-transform:none;font-size:12.5px;line-height:1.6">${esc(DONGLE.stand.fehler)}</span>`
: ""}
${DONGLE.stand && (DONGLE.stand.werte || []).length ? `<dl class="rows">
${DONGLE.stand.werte.map((w) => `<div class="row">
<dt>${esc(w.titel)} <small style="color:var(--fg3)">${esc(w.nummer)}</small></dt>
<dd${w.abweichung ? ` style="color:var(--warn)"` : ""}>${
w.geraet === null || w.geraet === undefined ? "" : esc(String(w.geraet))
}${w.einheit ? " " + esc(w.einheit) : ""}${
w.offen !== null && w.offen !== undefined
? ` <small>→ ${esc(String(w.offen))}, unterwegs</small>`
: w.abweichung ? ` <small>(wir: ${esc(String(w.unser))})</small>` : ""
}</dd>
</div>`).join("")}
</dl>` : ""}
<button class="aktion" data-dongle ${DONGLE_LAEUFT ? "disabled" : ""}>${
DONGLE_LAEUFT ? `<span class="lade-spinner"></span>Liest …` : "Jetzt lesen"
}</button>
<span class="label" style="margin-top:14px;color:var(--fg2);letter-spacing:0;
text-transform:none;font-size:12.5px;line-height:1.6">
${DONGLE.stand && DONGLE.stand.gelesen_am
// Der Punkt der Abkuerzung ist zugleich der Satzpunkt - sonst stuende
// dort "vor 1 Std..". "vor 3 Tage" hat keinen und bekommt einen.
? `Gelesen ${esc(alterText(Date.parse(DONGLE.stand.gelesen_am)).replace(/\.$/, ""))}. `
: "Noch nicht gelesen. "}Der Regler oben schreibt den Schlaf-Timeout sofort
hierher; bis das Gerät sich das nächste Mal meldet, steht die Änderung als
unterwegs" daneben.</span>
</div>` : ""}
<div class="tile"><span class="label">Darstellung</span>
<div class="seg">
<button data-mode="nacht" class="${HOST.dataset.theme === "nacht" ? "on" : ""}">Nacht</button>
@@ -3404,12 +3381,14 @@ function vEinst() {
<label class="aktion" style="display:block;text-align:center;cursor:pointer">
Backup importieren<input type="file" accept="application/json" data-backup="import" hidden></label>
</div>
<!-- Installierte Fassung und ihr Update stehen in EINER Kachel: es ist
dieselbe Sache aus zwei Blickwinkeln, und zwei Kacheln nebeneinander
zwangen zum Vergleichen zweier Zahlen ueber eine Kachelgrenze hinweg
(Vorgabe des Eigentuemers, 04.09.2026). -->
<div class="tile"><span class="label">Version</span>
<dl class="rows">
<div class="row" style="border-bottom:0"><dt>Installiert</dt><dd>${esc(APP_VERSION || "unbekannt")}<small>Home-Assistant-Integration</small></dd></div>
</dl>
</div>
<div class="tile"><span class="label">Integration-Update</span>
${(() => {
const u = INTEGRATION_UPDATE;
const laeuft = INTEGRATION_UPDATE_LAEUFT;
@@ -3460,11 +3439,112 @@ function vEinst() {
${INTEGRATION_UPDATE && INTEGRATION_UPDATE.fehler && !INTEGRATION_UPDATE_LAEUFT
? `<span class="label" style="margin-top:10px;color:var(--red)">${esc(INTEGRATION_UPDATE.fehler)}</span>`
: ""}
</div>`;
</div>
<!-- Debug-Mode: Zugaenge, Dongle und der letzte Datensatz des Geraets.
Drei Kacheln, die im Alltag nichts zu suchen haben und beim Suchen
eines Fehlers alles - deshalb zusammen, zugeklappt und ganz unten
(Vorgabe des Eigentuemers, 04.09.2026). -->
<div class="tile"><span class="label">Debug-Mode</span>
<span class="label" style="margin-top:8px;color:var(--fg2);letter-spacing:0;
text-transform:none;font-size:12.5px;line-height:1.6">
Zugangs-Token, die Einstellungen des Dongles und der letzte Datensatz, den
das Gerät gemeldet hat. Für die Fehlersuche, nicht für den Alltag.</span>
<button class="aktion ${debugOffen ? "primaer" : ""}" style="margin-top:16px" data-debug-auf>${debugOffen ? "Fertig" : "Öffnen"}</button>
</div>
${debugOffen ? `
${ZUGAENGE.length ? `<div class="tile"><span class="label">Zugänge</span>
${ZUGAENGE.map((z, i) => `
<button class="acc-h" aria-expanded="false" data-acc="zug${i}"><span class="mark"></span>
<span class="k">${esc(z.titel)}</span>
<span class="v" style="color:${z.gesetzt ? "var(--ok)" : "var(--fg3)"}">${
z.gesetzt ? (z.rest ? "gesetzt · …" + esc(z.rest) : "gesetzt") : "nicht gesetzt"
}</span></button>
<div class="acc-b" id="zug${i}">
<span class="label" style="margin-top:8px;color:var(--fg2);letter-spacing:0;
text-transform:none;font-size:12.5px;line-height:1.6">${esc(z.zweck)}</span>
<div class="feld" style="border-bottom:0"><label for="tok${i}">Neuer Token</label>
<input id="tok${i}" type="password" autocomplete="off" data-tokenfeld="${esc(z.dienst)}"
placeholder="${z.gesetzt ? "ersetzt den hinterlegten" : "noch keiner hinterlegt"}"></div>
<button class="aktion primaer" data-tokenspeichern="${esc(z.dienst)}">Speichern</button>
${z.gesetzt ? `<button class="aktion loeschen" data-tokenloeschen="${esc(z.dienst)}">Löschen</button>` : ""}
</div>`).join("")}
<span class="label" style="margin-top:14px;color:var(--fg2);letter-spacing:0;
text-transform:none;font-size:12.5px;line-height:1.6">
Ein gespeicherter Token wird nie wieder angezeigt nur ob einer hinterlegt ist.
Dieselben Felder gibt es unter Einstellungen Geräte &amp; Dienste Audi Dashboard
Konfigurieren.</span>
</div>` : ""}
${DONGLE && DONGLE.aktiv ? `<div class="tile"><span class="label">Dongle</span>
${DONGLE.stand && DONGLE.stand.fehler
? `<span class="label" style="margin-top:8px;color:var(--bad);letter-spacing:0;
text-transform:none;font-size:12.5px;line-height:1.6">${esc(DONGLE.stand.fehler)}</span>`
: ""}
${DONGLE.stand && (DONGLE.stand.werte || []).length ? `<dl class="rows">
${DONGLE.stand.werte.map((w) => `<div class="row">
<dt>${esc(w.titel)} <small style="color:var(--fg3)">${esc(w.nummer)}</small></dt>
<dd${w.abweichung ? ` style="color:var(--warn)"` : ""}>${
w.geraet === null || w.geraet === undefined ? "" : esc(String(w.geraet))
}${w.einheit ? " " + esc(w.einheit) : ""}${
w.offen !== null && w.offen !== undefined
? ` <small>→ ${esc(String(w.offen))}, unterwegs</small>`
: w.abweichung ? ` <small>(wir: ${esc(String(w.unser))})</small>` : ""
}</dd>
</div>`).join("")}
</dl>` : ""}
<button class="aktion" data-dongle ${DONGLE_LAEUFT ? "disabled" : ""}>${
DONGLE_LAEUFT ? `<span class="lade-spinner"></span>Liest …` : "Jetzt lesen"
}</button>
<span class="label" style="margin-top:14px;color:var(--fg2);letter-spacing:0;
text-transform:none;font-size:12.5px;line-height:1.6">
${DONGLE.stand && DONGLE.stand.gelesen_am
// Der Punkt der Abkuerzung ist zugleich der Satzpunkt - sonst stuende
// dort "vor 1 Std..". "vor 3 Tage" hat keinen und bekommt einen.
? `Gelesen ${esc(alterText(Date.parse(DONGLE.stand.gelesen_am)).replace(/\.$/, ""))}. `
: "Noch nicht gelesen. "}Der Regler oben schreibt den Schlaf-Timeout sofort
hierher; bis das Gerät sich das nächste Mal meldet, steht die Änderung als
unterwegs" daneben.</span>
</div>` : ""}
${(() => {
/* Der letzte Datensatz, wie er in Home Assistant angekommen ist - siehe
geraet.py. Der Abstand zwischen Gerätezeit und Ankunft ist die Angabe,
die bei gepufferten Datensätzen zählt: genau dieser Rückstand war die
Wurzel des 3-km-Fahrt-Fehlers. */
const d = LETZTER_DATENSATZ;
if (!d) return `<div class="tile"><span class="label">Letzter Datensatz</span>
<span class="label" style="margin-top:8px;color:var(--fg2);letter-spacing:0;
text-transform:none;font-size:12.5px;line-height:1.6">
Noch nichts gemeldet. Dafür braucht es die Meldezeit oder das
Fahrtsignal im Setup - und mindestens einen Datensatz des Geräts,
seit Home Assistant läuft.</span></div>`;
const rueckstand = d.meldezeit && d.angekommen
? Math.round((Date.parse(d.angekommen) - Date.parse(d.meldezeit)) / 1000)
: null;
return `<div class="tile"><span class="label">Letzter Datensatz</span>
<dl class="rows">
<div class="row"><dt>Gerät</dt><dd>${esc(d.geraet || "unbekannt")}</dd></div>
<div class="row"><dt>Angekommen</dt><dd>${dezeitpunkt(new Date(d.angekommen))}</dd></div>
<div class="row"><dt>Gerätezeit</dt><dd>${d.meldezeit ? dezeitpunkt(new Date(d.meldezeit)) : ""}${
rueckstand !== null ? `<small>Rückstand ${de(rueckstand)} s</small>` : ""
}</dd></div>
${d.werte.map((w) => `<div class="row"><dt>${esc(w.name)}</dt><dd>${esc(String(w.wert))}${
w.einheit ? " " + esc(w.einheit) : ""
}</dd></div>`).join("")}
</dl>
<span class="label" style="margin-top:14px;color:var(--fg2);letter-spacing:0;
text-transform:none;font-size:12.5px;line-height:1.6">
${de(d.gesamt)} Werte aus derselben Meldung. Was das Gerät in diesem
Datensatz nicht geschickt hat, steht hier nicht - es behält in Home
Assistant seinen alten Wert. Nach einem Neustart ist Angekommen" der
Zeitpunkt des Neustarts, nicht der einer Übertragung der Rückstand
sagt dann, wie lange das Gerät schon schweigt.</span>
</div>`;
})()}
` : ""}`;
}
let nmOffen = false;
let einrichtenOffen = false;
let debugOffen = false;
/* Entwurf des Reglers "Fahrt beenden" (Minuten) - null heisst: nicht angefasst.
Er liegt bewusst NEBEN CONFIG.pausenzeit und nicht darin: erst der Knopf
"Speichern" uebernimmt ihn, weil derselbe Wert den Schlaf-Timeout des
@@ -4377,6 +4457,9 @@ let ZUGAENGE = [];
/* Was am Dongle steht, was noch aussteht - siehe flespi.py im Backend.
null, solange kein flespi-Token hinterlegt ist. */
let DONGLE = null;
/* Der letzte Datensatz, den das Geraet gemeldet hat - siehe geraet.py im
Backend. null, solange weder Meldezeit noch Fahrtsignal zugeordnet sind. */
let LETZTER_DATENSATZ = null;
// true, waehrend "Jetzt lesen" laeuft. Wie INTEGRATION_UPDATE_LAEUFT und aus
// demselben Grund: der Abruf geht ueber zwei Netze (wir -> flespi -> Geraet)
// und darf nicht wie ein toter Knopf aussehen.
@@ -5977,6 +6060,7 @@ function ereignisseVerdrahten() {
}
if (e.target.closest("[data-nmauf]")) { nmOffen = !nmOffen; render(); return; }
if (e.target.closest("[data-einrichten-auf]")) { einrichtenOffen = !einrichtenOffen; render(); return; }
if (e.target.closest("[data-debug-auf]")) { debugOffen = !debugOffen; render(); return; }
if (e.target.closest("[data-nmspeichern]")) {
const wert = parseInt($("#nm").value, 10);
if (!isNaN(wert) && wert > 0) { CAR.reifen.anzugsmoment = wert; profilSpeichern(); }
@@ -6701,6 +6785,9 @@ async function datenLaden(erzwingen) {
DONGLE = versionState.attributes.daten
? versionState.attributes.daten.dongle ?? null
: null;
LETZTER_DATENSATZ = versionState.attributes.daten
? versionState.attributes.daten.letzter_datensatz ?? null
: null;
}
if (entitaetenState && entitaetenState.attributes) ENTITAETEN = entitaetenState.attributes.daten;
@@ -6743,6 +6830,9 @@ async function datenLaden(erzwingen) {
nachladeVersuche = 0;
render();
// Erst nach dem ersten Zeichnen: die Fotos sollen den Aufbau der Seite
// nicht ausbremsen, sondern den naechsten Seitenwechsel beschleunigen.
bilderVorladen();
return;
}
}
@@ -1657,3 +1657,17 @@ img.carfix.klein { aspect-ratio: auto; height: auto; }
scroll-behavior: auto !important;
}
}
/* Ein Paar aus Bestaetigen und Abbrechen steht nebeneinander, beide gleich
breit: es ist eine Entscheidung zwischen zwei Wegen, keine Haupt- und
Nebensache. Welcher der gewollte ist, sagt die Fuellung, nicht die Groesse -
und der bestaetigende steht rechts, wie ueberall in iOS. */
.knopfpaar {
display: flex;
gap: 10px;
margin-top: var(--sp-4);
}
.knopfpaar .aktion {
flex: 1 1 0;
margin-top: 0;
}