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,5 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
|
||||
import { bilderVorladen } from "./screens/bilder"
|
||||
|
||||
import { App as CapacitorApp } from "@capacitor/app"
|
||||
|
||||
import { DataMetricApi, zugangLesen } from "./api"
|
||||
@@ -79,6 +81,13 @@ function AngemeldeteApp({ beiAbmeldung }: { beiAbmeldung: () => void }) {
|
||||
void startklarMelden()
|
||||
}, [])
|
||||
|
||||
/* Fahrzeugfotos einmal vorab holen und dekodieren, sobald Daten da sind -
|
||||
siehe bilderVorladen(). Damit erscheint beim Umschalten der Seiten sofort
|
||||
das Bild statt kurz des Platzhalters. */
|
||||
useEffect(() => {
|
||||
if (daten.verbindung === "verbunden") bilderVorladen()
|
||||
}, [daten.verbindung])
|
||||
|
||||
/* Belege aus dem Teilen-Blatt.
|
||||
|
||||
Die Erweiterung legt das PDF ab und oeffnet die App - beim ersten Start
|
||||
|
||||
@@ -271,6 +271,24 @@ export interface DongleAngabe {
|
||||
stand: DongleStand | null;
|
||||
}
|
||||
|
||||
/** Ein einzelner Wert aus der letzten Meldung des Dongles (geraet.py). */
|
||||
export interface DatensatzWert {
|
||||
name: string;
|
||||
wert: string;
|
||||
einheit: string | null;
|
||||
}
|
||||
|
||||
/** Die letzte Meldung des Dongles, so wie sie in Home Assistant ankam.
|
||||
"angekommen" ist die Ankunftszeit, "meldezeit" die Zeit des Geraets -
|
||||
der Abstand zwischen beiden ist der Puffer-Rueckstand. */
|
||||
export interface LetzterDatensatz {
|
||||
geraet: string;
|
||||
angekommen: string;
|
||||
meldezeit: string | null;
|
||||
werte: DatensatzWert[];
|
||||
gesamt: number;
|
||||
}
|
||||
|
||||
/** Nutzlast von sensor.audi_dashboard_app_version. */
|
||||
export interface AppVersionAngabe {
|
||||
app: string | null;
|
||||
@@ -278,6 +296,7 @@ export interface AppVersionAngabe {
|
||||
integration_update: IntegrationUpdateAngabe | null;
|
||||
zugaenge?: ZugangAngabe[];
|
||||
dongle?: DongleAngabe;
|
||||
letzter_datensatz?: LetzterDatensatz | null;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------- Entitäts-Verzeichnis */
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
type Fahrzeugstatus,
|
||||
type IntegrationUpdateAngabe,
|
||||
type DongleAngabe,
|
||||
type LetzterDatensatz,
|
||||
type ZugangAngabe,
|
||||
type Profil,
|
||||
type Tankvorgang,
|
||||
@@ -75,6 +76,7 @@ export interface DatenWert {
|
||||
/** Konfiguration des Dongles und eine ggf. vorgemerkte Aenderung
|
||||
(flespi.py). null, solange kein flespi-Token hinterlegt ist. */
|
||||
dongle: DongleAngabe | null
|
||||
letzterDatensatz: LetzterDatensatz | null
|
||||
|
||||
einstellungen: Einstellungen | null
|
||||
fahrzeug: Fahrzeug | null
|
||||
@@ -123,6 +125,7 @@ export function DatenAnbieter({
|
||||
const [otaBuendel, setzeOtaBuendel] = useState<Buendelangabe | null>(null)
|
||||
const [zugaenge, setzeZugaenge] = useState<ZugangAngabe[]>([])
|
||||
const [dongle, setzeDongle] = useState<DongleAngabe | null>(null)
|
||||
const [letzterDatensatz, setzeLetzterDatensatz] = useState<LetzterDatensatz | null>(null)
|
||||
const [integrationUpdate, setzeIntegrationUpdate] = useState<IntegrationUpdateAngabe | null>(
|
||||
null,
|
||||
)
|
||||
@@ -168,6 +171,7 @@ export function DatenAnbieter({
|
||||
setzeIntegrationUpdate(versionsangabe?.integration_update ?? null)
|
||||
setzeZugaenge(versionsangabe?.zugaenge ?? [])
|
||||
setzeDongle(versionsangabe?.dongle ?? null)
|
||||
setzeLetzterDatensatz(versionsangabe?.letzter_datensatz ?? null)
|
||||
return true
|
||||
} catch (fehler) {
|
||||
// Ein voruebergehender Ausfall ist kein Ladefehler, solange schon
|
||||
@@ -301,6 +305,7 @@ export function DatenAnbieter({
|
||||
integrationUpdate,
|
||||
zugaenge,
|
||||
dongle,
|
||||
letzterDatensatz,
|
||||
einstellungen,
|
||||
fahrzeug,
|
||||
fahrten,
|
||||
|
||||
@@ -9,13 +9,13 @@ import { useEffect, useRef, useState } from "react"
|
||||
import { Accordion, ActionButton, Feld, Seg, Sheet, Slider, Switch, Tile } from "@audi-dash/ui"
|
||||
|
||||
import { ApiFehler, DIENST_DOMAIN, zugangLesen, zugangVerwerfen } from "../api"
|
||||
import type { Profil } from "../api"
|
||||
import type { LetzterDatensatz, Profil } from "../api"
|
||||
import { eigeneVersion } from "../daten/appVersion"
|
||||
import { Datumsfeld } from "./Datumsfeld"
|
||||
import { useDaten } from "../daten/DatenKontext"
|
||||
import { OtaFehler, buendelAnwenden, buendelPasst, otaMoeglich } from "../daten/ota"
|
||||
import type { Einstellungen as EinstellungenWerte } from "../daten/profilAdapter"
|
||||
import { datum, datumZeit, isoTag, standAlter } from "../format"
|
||||
import { datum, datumZeit, de, isoTag, standAlter } from "../format"
|
||||
import { useTheme } from "../theme"
|
||||
import { BildMitMenue, Wertzeile, Werteliste, bestaetigen } from "./bausteine"
|
||||
import { BILDPLAETZE, SEITE_WINTER, markenlogoUrl } from "./bilder"
|
||||
@@ -44,6 +44,7 @@ export function Einstellungen({
|
||||
serverVersion,
|
||||
zugaenge,
|
||||
dongle,
|
||||
letzterDatensatz,
|
||||
verbindung,
|
||||
api,
|
||||
profilSpeichern,
|
||||
@@ -250,6 +251,7 @@ export function Einstellungen({
|
||||
// Geraet) und darf nicht wie ein toter Knopf aussehen. Das Ergebnis selbst
|
||||
// kommt ueber die veroeffentlichte Entitaet zurueck (flespi.py), hier steht
|
||||
// nur, was den Aufruf gar nicht erst erreicht hat.
|
||||
const [debugOffen, setzeDebugOffen] = useState(false)
|
||||
const [dongleLaeuft, setzeDongleLaeuft] = useState(false)
|
||||
const [dongleFehler, setzeDongleFehler] = useState<string | null>(null)
|
||||
|
||||
@@ -409,66 +411,6 @@ export function Einstellungen({
|
||||
</ActionButton>
|
||||
</Tile>
|
||||
|
||||
{zugaenge.length > 0 && (
|
||||
<Tile>
|
||||
<span className="ads-eyebrow">Zugänge</span>
|
||||
{/* Aufklappbar, weil die Token im Alltag nichts zu suchen haben —
|
||||
man fasst sie an, wenn einer abgelaufen ist, und sonst nie. */}
|
||||
{zugaenge.map((z) => (
|
||||
<Accordion
|
||||
key={z.dienst}
|
||||
title={z.titel}
|
||||
summary={
|
||||
<span className={z.gesetzt ? "dm-token-gesetzt" : "dm-token-offen"}>
|
||||
{z.gesetzt ? (z.rest ? `gesetzt · …${z.rest}` : "gesetzt") : "nicht gesetzt"}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<p className="dm-erklaerung">{z.zweck}</p>
|
||||
<Feld label="Neuer Token" last>
|
||||
<input
|
||||
className="dm-eingabe"
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
placeholder={z.gesetzt ? "ersetzt den hinterlegten" : "noch keiner hinterlegt"}
|
||||
value={tokenEntwurf[z.dienst] ?? ""}
|
||||
onChange={(e) =>
|
||||
setzeTokenEntwurf((alt) => ({ ...alt, [z.dienst]: e.target.value }))
|
||||
}
|
||||
/>
|
||||
</Feld>
|
||||
<div className="dm-knopfreihe">
|
||||
<ActionButton
|
||||
variant="primary"
|
||||
disabled={tokenLaeuft !== null || !(tokenEntwurf[z.dienst] ?? "").trim()}
|
||||
onClick={() => void tokenSchreiben(z.dienst, tokenEntwurf[z.dienst] ?? "")}
|
||||
>
|
||||
{tokenLaeuft === z.dienst ? "Speichert …" : "Speichern"}
|
||||
</ActionButton>
|
||||
{z.gesetzt && (
|
||||
<ActionButton
|
||||
disabled={tokenLaeuft !== null}
|
||||
onClick={() => {
|
||||
if (bestaetigen("Token löschen?")) {
|
||||
void tokenSchreiben(z.dienst, "")
|
||||
}
|
||||
}}
|
||||
>
|
||||
Löschen
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
</Accordion>
|
||||
))}
|
||||
{tokenFehler && <p className="dm-fehler">{tokenFehler}</p>}
|
||||
<span className="ads-eyebrow" style={{ marginTop: "14px" }}>
|
||||
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>
|
||||
</Tile>
|
||||
)}
|
||||
|
||||
<Tile>
|
||||
<span className="ads-eyebrow">Fahrt beenden</span>
|
||||
<Slider
|
||||
@@ -482,24 +424,24 @@ export function Einstellungen({
|
||||
/>
|
||||
{/* Erscheint erst, wenn der Regler wirklich woanders steht - ein
|
||||
dauerhaft sichtbarer, meist wirkungsloser Knopf waere Beiwerk. Die
|
||||
Anordnung folgt der Speicherleiste am Seitenende: bestaetigende
|
||||
Aktion als voller Knopf, der Ausweg daneben als schlichter Text. */}
|
||||
Anordnung folgt Apples eigener Aufteilung fuer ein Wertepaar aus
|
||||
Bestaetigen und Abbrechen: ZWEI gleich grosse Knoepfe, der
|
||||
bestaetigende gefuellt und rechts (die fuehrende Hand erreicht ihn
|
||||
zuerst), der Ausweg daneben als zweite Rolle. Vorher war "Verwerfen"
|
||||
blosser Text und dadurch kaum als Bedienelement zu erkennen -
|
||||
gemeldet vom Eigentuemer am 04.09.2026. */}
|
||||
{pausenzeitGeaendert && (
|
||||
<div className="dm-speicherleiste dm-speicherleiste--kachel">
|
||||
<ActionButton onClick={() => setzePausenzeitEntwurf(null)} disabled={laeuft}>
|
||||
Verwerfen
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
variant="primary"
|
||||
onClick={() => void speichern({ pausenzeitMin: pausenzeitEntwurf as number })}
|
||||
disabled={laeuft}
|
||||
>
|
||||
{laeuft ? "Speichere …" : "Speichern"}
|
||||
</ActionButton>
|
||||
<button
|
||||
type="button"
|
||||
className="dm-textknopf"
|
||||
onClick={() => setzePausenzeitEntwurf(null)}
|
||||
disabled={laeuft}
|
||||
>
|
||||
Verwerfen
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{/* Fusszeile statt Info-Knopf: Apples eigenes Muster fuer erklaerenden
|
||||
@@ -513,44 +455,6 @@ export function Einstellungen({
|
||||
</span>
|
||||
</Tile>
|
||||
|
||||
{dongle?.aktiv && (
|
||||
<Tile>
|
||||
<span className="ads-eyebrow">Dongle</span>
|
||||
{dongle.stand?.fehler && <p className="dm-fehler">{dongle.stand.fehler}</p>}
|
||||
{(dongle.stand?.werte ?? []).length > 0 && (
|
||||
<Werteliste
|
||||
kinder={(dongle.stand?.werte ?? []).map((w) => (
|
||||
<Wertzeile
|
||||
key={w.schluessel}
|
||||
label={`${w.titel} ${w.nummer}`}
|
||||
wert={`${w.geraet ?? "–"}${w.einheit ? " " + w.einheit : ""}`}
|
||||
warnung={Boolean(w.abweichung)}
|
||||
zusatz={
|
||||
w.offen != null
|
||||
? `→ ${w.offen}, unterwegs`
|
||||
: w.abweichung
|
||||
? `(wir: ${w.unser})`
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
/>
|
||||
)}
|
||||
<ActionButton disabled={dongleLaeuft} onClick={() => void dongleAbrufen()}>
|
||||
{dongleLaeuft ? "Liest …" : "Jetzt lesen"}
|
||||
</ActionButton>
|
||||
{dongleFehler && <p className="dm-fehler">{dongleFehler}</p>}
|
||||
<span className="ads-eyebrow" style={{ marginTop: "14px" }}>
|
||||
{dongle.stand?.gelesen_am
|
||||
// Der Punkt der Abkürzung ist zugleich der Satzpunkt - sonst
|
||||
// stünde dort "vor 1 Std..".
|
||||
? `Gelesen ${standAlter(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>
|
||||
</Tile>
|
||||
)}
|
||||
|
||||
<Tile>
|
||||
<span className="ads-eyebrow">Darstellung</span>
|
||||
@@ -841,6 +745,10 @@ export function Einstellungen({
|
||||
)}
|
||||
|
||||
|
||||
{/* 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). */}
|
||||
<Tile>
|
||||
<span className="ads-eyebrow">Version</span>
|
||||
<Werteliste
|
||||
@@ -852,10 +760,6 @@ export function Einstellungen({
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Tile>
|
||||
|
||||
<Tile>
|
||||
<span className="ads-eyebrow">Integration-Update</span>
|
||||
{/* HIG "Loading": Netzwerk zu Gitea plus Entpacken dauert unbestimmt
|
||||
lang, ohne verlässliche Fortschrittsangabe - ein laufender
|
||||
Indikator statt eines nur deaktivierten Knopfes verhindert, dass
|
||||
@@ -966,17 +870,192 @@ export function Einstellungen({
|
||||
</ActionButton>
|
||||
</Tile>
|
||||
|
||||
{/* 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). */}
|
||||
<Tile>
|
||||
<span className="ads-eyebrow">Debug-Mode</span>
|
||||
<p className="dm-erklaerung">
|
||||
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.
|
||||
</p>
|
||||
<ActionButton
|
||||
variant={debugOffen ? "primary" : "default"}
|
||||
onClick={() => setzeDebugOffen(!debugOffen)}
|
||||
>
|
||||
{debugOffen ? "Fertig" : "Öffnen"}
|
||||
</ActionButton>
|
||||
</Tile>
|
||||
|
||||
{debugOffen && (
|
||||
<>
|
||||
{zugaenge.length > 0 && (
|
||||
<Tile>
|
||||
<span className="ads-eyebrow">Zugänge</span>
|
||||
{/* Aufklappbar, weil die Token im Alltag nichts zu suchen haben —
|
||||
man fasst sie an, wenn einer abgelaufen ist, und sonst nie. */}
|
||||
{zugaenge.map((z) => (
|
||||
<Accordion
|
||||
key={z.dienst}
|
||||
title={z.titel}
|
||||
summary={
|
||||
<span className={z.gesetzt ? "dm-token-gesetzt" : "dm-token-offen"}>
|
||||
{z.gesetzt ? (z.rest ? `gesetzt · …${z.rest}` : "gesetzt") : "nicht gesetzt"}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<p className="dm-erklaerung">{z.zweck}</p>
|
||||
<Feld label="Neuer Token" last>
|
||||
<input
|
||||
className="dm-eingabe"
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
placeholder={z.gesetzt ? "ersetzt den hinterlegten" : "noch keiner hinterlegt"}
|
||||
value={tokenEntwurf[z.dienst] ?? ""}
|
||||
onChange={(e) =>
|
||||
setzeTokenEntwurf((alt) => ({ ...alt, [z.dienst]: e.target.value }))
|
||||
}
|
||||
/>
|
||||
</Feld>
|
||||
<div className="dm-knopfreihe">
|
||||
<ActionButton
|
||||
variant="primary"
|
||||
disabled={tokenLaeuft !== null || !(tokenEntwurf[z.dienst] ?? "").trim()}
|
||||
onClick={() => void tokenSchreiben(z.dienst, tokenEntwurf[z.dienst] ?? "")}
|
||||
>
|
||||
{tokenLaeuft === z.dienst ? "Speichert …" : "Speichern"}
|
||||
</ActionButton>
|
||||
{z.gesetzt && (
|
||||
<ActionButton
|
||||
disabled={tokenLaeuft !== null}
|
||||
onClick={() => {
|
||||
if (bestaetigen("Token löschen?")) {
|
||||
void tokenSchreiben(z.dienst, "")
|
||||
}
|
||||
}}
|
||||
>
|
||||
Löschen
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
</Accordion>
|
||||
))}
|
||||
{tokenFehler && <p className="dm-fehler">{tokenFehler}</p>}
|
||||
<span className="ads-eyebrow" style={{ marginTop: "14px" }}>
|
||||
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>
|
||||
</Tile>
|
||||
)}
|
||||
{dongle?.aktiv && (
|
||||
<Tile>
|
||||
<span className="ads-eyebrow">Dongle</span>
|
||||
{dongle.stand?.fehler && <p className="dm-fehler">{dongle.stand.fehler}</p>}
|
||||
{(dongle.stand?.werte ?? []).length > 0 && (
|
||||
<Werteliste
|
||||
kinder={(dongle.stand?.werte ?? []).map((w) => (
|
||||
<Wertzeile
|
||||
key={w.schluessel}
|
||||
label={`${w.titel} ${w.nummer}`}
|
||||
wert={`${w.geraet ?? "–"}${w.einheit ? " " + w.einheit : ""}`}
|
||||
warnung={Boolean(w.abweichung)}
|
||||
zusatz={
|
||||
w.offen != null
|
||||
? `→ ${w.offen}, unterwegs`
|
||||
: w.abweichung
|
||||
? `(wir: ${w.unser})`
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
/>
|
||||
)}
|
||||
<ActionButton disabled={dongleLaeuft} onClick={() => void dongleAbrufen()}>
|
||||
{dongleLaeuft ? "Liest …" : "Jetzt lesen"}
|
||||
</ActionButton>
|
||||
{dongleFehler && <p className="dm-fehler">{dongleFehler}</p>}
|
||||
<span className="ads-eyebrow" style={{ marginTop: "14px" }}>
|
||||
{dongle.stand?.gelesen_am
|
||||
// Der Punkt der Abkürzung ist zugleich der Satzpunkt - sonst
|
||||
// stünde dort "vor 1 Std..".
|
||||
? `Gelesen ${standAlter(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>
|
||||
</Tile>
|
||||
)}
|
||||
<LetzterDatensatzKachel datensatz={letzterDatensatz} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{gespeichert && <p className="dm-erfolg">Gespeichert.</p>}
|
||||
{entwurf && (
|
||||
<div className="dm-speicherleiste">
|
||||
<ActionButton onClick={() => void speichern()} disabled={laeuft}>
|
||||
{laeuft ? "Speichere …" : "Änderungen speichern"}
|
||||
</ActionButton>
|
||||
<button type="button" className="dm-textknopf" onClick={() => setzeEntwurf(null)}>
|
||||
<ActionButton onClick={() => setzeEntwurf(null)} disabled={laeuft}>
|
||||
Verwerfen
|
||||
</button>
|
||||
</ActionButton>
|
||||
<ActionButton variant="primary" onClick={() => void speichern()} disabled={laeuft}>
|
||||
{laeuft ? "Speichere …" : "Speichern"}
|
||||
</ActionButton>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/* Der letzte Datensatz des Dongles. Bewusst eine eigene Komponente: die Liste
|
||||
ist lang, hat mit den uebrigen Einstellungen nichts zu tun, und so bleibt
|
||||
die Hauptkomponente lesbar. */
|
||||
function LetzterDatensatzKachel({ datensatz }: { datensatz: LetzterDatensatz | null }) {
|
||||
if (!datensatz) {
|
||||
return (
|
||||
<Tile>
|
||||
<span className="ads-eyebrow">Letzter Datensatz</span>
|
||||
<p className="dm-erklaerung">
|
||||
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.
|
||||
</p>
|
||||
</Tile>
|
||||
)
|
||||
}
|
||||
const rueckstand =
|
||||
datensatz.meldezeit && datensatz.angekommen
|
||||
? Math.round(
|
||||
(Date.parse(datensatz.angekommen) - Date.parse(datensatz.meldezeit)) / 1000,
|
||||
)
|
||||
: null
|
||||
return (
|
||||
<Tile>
|
||||
<span className="ads-eyebrow">Letzter Datensatz</span>
|
||||
<Werteliste
|
||||
kinder={
|
||||
<>
|
||||
<Wertzeile label="Gerät" wert={datensatz.geraet} />
|
||||
<Wertzeile label="Angekommen" wert={datumZeit(datensatz.angekommen)} />
|
||||
<Wertzeile
|
||||
label="Gerätezeit"
|
||||
wert={datensatz.meldezeit ? datumZeit(datensatz.meldezeit) : "–"}
|
||||
zusatz={rueckstand !== null ? `Rückstand ${de(rueckstand)} s` : undefined}
|
||||
/>
|
||||
{datensatz.werte.map((w) => (
|
||||
<Wertzeile
|
||||
key={w.name}
|
||||
label={w.name}
|
||||
wert={w.einheit ? `${w.wert} ${w.einheit}` : w.wert}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<p className="dm-erklaerung">
|
||||
{de(datensatz.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.
|
||||
</p>
|
||||
</Tile>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -229,3 +229,46 @@ export async function bildVerkleinern(datei: File): Promise<Blob> {
|
||||
}
|
||||
return datei
|
||||
}
|
||||
|
||||
/**
|
||||
* Alle Fahrzeugfotos einmal im Hintergrund holen und dekodieren.
|
||||
*
|
||||
* WARUM
|
||||
* -----
|
||||
* Gemeldet als "Bilder laden beim Umschalten der Menues einige ms verzoegert".
|
||||
* Innerhalb einer Sitzung liegt das nicht am Netz - die Adresse ist stabil und
|
||||
* Home Assistant liefert `/local/` mit `max-age` von 31 Tagen, der Abruf kommt
|
||||
* also aus dem Zwischenspeicher. Verzoegert wird das BILD selbst: ein frisch
|
||||
* erzeugtes <img> wird standardmaessig asynchron dekodiert und erst danach
|
||||
* gezeichnet, und beim Seitenwechsel entsteht jedes Mal ein neues.
|
||||
*
|
||||
* `decode()` erledigt genau diesen Schritt vorab. Danach zeichnet dasselbe
|
||||
* Bild beim naechsten Einhaengen sofort.
|
||||
*
|
||||
* WANN
|
||||
* ----
|
||||
* In der Leerlaufzeit nach dem ersten Datenladen, nicht beim Start - sonst
|
||||
* konkurriert es mit dem Abruf von Profil, Fahrten und Tankvorgaengen, den der
|
||||
* Nutzer wirklich braucht. Das Datenvolumen ist dasselbe wie beim Durchblaettern
|
||||
* aller Seiten, nur frueher und ohne Warten.
|
||||
*/
|
||||
export function bilderVorladen(): void {
|
||||
if (typeof window === "undefined" || typeof Image === "undefined") return
|
||||
const starten = () => {
|
||||
const namen = [...BILDPLAETZE.map((b) => b.datei), SEITE_WINTER.datei]
|
||||
for (const name of namen) {
|
||||
const url = bildUrl(name)
|
||||
if (!url) continue
|
||||
const bild = new Image()
|
||||
bild.src = url
|
||||
// Ein fehlendes Foto ist der Normalfall (nicht jeder Platz ist belegt) -
|
||||
// deshalb still schlucken statt zu melden.
|
||||
void bild.decode?.().catch(() => undefined)
|
||||
}
|
||||
}
|
||||
const leerlauf = (window as unknown as {
|
||||
requestIdleCallback?: (r: () => void, o?: { timeout: number }) => number
|
||||
}).requestIdleCallback
|
||||
if (leerlauf) leerlauf(starten, { timeout: 4000 })
|
||||
else window.setTimeout(starten, 2000)
|
||||
}
|
||||
|
||||
@@ -1590,8 +1590,11 @@ button.dm-listenzeile {
|
||||
background: none;
|
||||
border-top: 0;
|
||||
}
|
||||
.dm-speicherleiste--kachel > :first-child {
|
||||
flex: 1 1 auto;
|
||||
/* Beide Knoepfe gleich breit: ein Paar aus Bestaetigen und Abbrechen ist
|
||||
eine Entscheidung zwischen zwei Wegen, keine Haupt- und Nebensache. Welcher
|
||||
davon der gewollte ist, sagt die Fuellung, nicht die Groesse. */
|
||||
.dm-speicherleiste > * {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
.dm-szene__zusatz {
|
||||
|
||||
Reference in New Issue
Block a user