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:
@@ -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"}
|
||||
Binary file not shown.
@@ -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 & 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 & 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user