diff --git a/homeassistant/www/audi-dashboard-app.js b/homeassistant/www/audi-dashboard-app.js
index 5c02f2b..8b0b98e 100644
--- a/homeassistant/www/audi-dashboard-app.js
+++ b/homeassistant/www/audi-dashboard-app.js
@@ -108,6 +108,7 @@ function profilZuCar(p, status) {
odo: status.km ?? 0,
odoBekannt: status.km != null,
tankPct: status.tankprozent ?? 0,
+ tankBekannt: status.tankprozent != null,
reichweite: status.reichweite_km,
batteriespannung: status.batteriespannung,
gesichert: status.gesichert,
@@ -308,8 +309,17 @@ function bildWeiter() {
das tatsächliche Tagesdatum ersetzt werden muss. */
function heute() { return new Date(); }
const dTag = 86400000;
-const mmjj = (d) => String(d.getMonth() + 1).padStart(2, "0") + "/" + String(d.getFullYear()).slice(2);
-const dedat = (d) => d.toLocaleDateString("de-DE", { day: "2-digit", month: "2-digit", year: "numeric" });
+/* Ein unlesbares Datum ergibt einen Strich, keine Rechenreste. dat() erwartet
+ TT.MM.JJJJ; steht im Profil etwas anderes (es wird laut INSTALL.md von Hand
+ gepflegt, z. B. versehentlich im ISO-Format), lieferte mmjj() sonst
+ "NaN/N" - ein Wert, der wie eine Angabe aussieht und keine ist. */
+const gueltigesDatum = (d) => d instanceof Date && !Number.isNaN(d.getTime());
+const mmjj = (d) => (gueltigesDatum(d)
+ ? String(d.getMonth() + 1).padStart(2, "0") + "/" + String(d.getFullYear()).slice(2)
+ : "–");
+const dedat = (d) => (gueltigesDatum(d)
+ ? d.toLocaleDateString("de-DE", { day: "2-digit", month: "2-digit", year: "numeric" })
+ : "–");
function dat(s) { const [d, m, j] = s.split(".").map(Number); return new Date(j, m - 1, d); }
const MISCHDAUER = 180;
@@ -866,7 +876,12 @@ function badge() {
if (!praefix) return "";
const datei = BADGE_DATEIEN[praefix];
const modellZusatz = (CONFIG.fahrzeugtitel || "").slice(praefix.length).trim();
- const zusatz = [modellZusatz, CONFIG.ausfuehrung].filter(Boolean).join(" ");
+ /* Die Ausfuehrung steht oft schon im Modellnamen ("RS 4 Avant competition"
+ plus Ausfuehrung "competition") - dann nicht ein zweites Mal anhaengen,
+ sonst steht dort "Avant competition competition". */
+ const ausfuehrung = (CONFIG.ausfuehrung || "").trim();
+ const doppelt = ausfuehrung && modellZusatz.toLowerCase().includes(ausfuehrung.toLowerCase());
+ const zusatz = [modellZusatz, doppelt ? "" : ausfuehrung].filter(Boolean).join(" ");
const alt = esc(CONFIG.fahrzeugtitel) + " Badge";
return `
@@ -1017,10 +1032,10 @@ function vHome() {
${CAR.reichweite != null ? de(CAR.reichweite) : "–"}km
-
${pct} %
+
${CAR.tankBekannt ? pct + " %" : "–"}
-
Reichweite${liter != null ? ` · noch etwa ${de(liter)} l im Tank` : ""}
+
Reichweite${CAR.tankBekannt && liter != null ? ` · noch etwa ${de(liter)} l im Tank` : ""}
@@ -3027,6 +3042,31 @@ function serviceRufen(dienst, daten) {
}
function ereignisseVerdrahten() {
+ /* Tastaturbedienung fuer die beiden Ueberlagerungen. Bisher gab es in der
+ ganzen Datei keinen keydown-Handler: Das Setup-Fenster deklariert
+ role="dialog" aria-modal="true", liess sich aber weder mit Escape
+ schliessen noch verhinderte es, dass der Tabulator in den verdeckten
+ Hintergrund wandert. Beides gehoert zum Vertrag dieser Rolle.
+ Der Handler haengt am Dokument, weil Fokus im Shadow DOM auch ausserhalb
+ von ROOT liegen kann. */
+ document.addEventListener("keydown", (e) => {
+ if (e.key === "Escape") {
+ if (sheet) { sheetSchliessen(); e.preventDefault(); return; }
+ if (setupSucheOffen) { setupSucheOffen = null; setupSuchtext = ""; render(); e.preventDefault(); return; }
+ if (setupOffen) { setupSchliessen(); e.preventDefault(); return; }
+ return;
+ }
+ if (e.key !== "Tab" || !setupOffen) return;
+ /* Fokusfalle: innerhalb des Fensters im Kreis wandern statt dahinter. */
+ const fokussierbar = ROOT.querySelectorAll(
+ '.setup-popup button:not([disabled]), .setup-popup input:not([disabled]), .setup-popup [tabindex]:not([tabindex="-1"])');
+ if (!fokussierbar.length) return;
+ const erstes = fokussierbar[0];
+ const letztes = fokussierbar[fokussierbar.length - 1];
+ const aktiv = ROOT.activeElement;
+ if (e.shiftKey && (aktiv === erstes || !aktiv)) { letztes.focus(); e.preventDefault(); }
+ else if (!e.shiftKey && aktiv === letztes) { erstes.focus(); e.preventDefault(); }
+ });
ROOT.getElementById("tabbar").innerHTML = TABS.map((t) =>
`
${ICONS[t.id]} ${t.label}
@@ -3949,7 +3989,7 @@ class AudiDashboardPanel extends HTMLElement {
wrapper.innerHTML = `
-
+
diff --git a/homeassistant/www/audi-dashboard-ios.css b/homeassistant/www/audi-dashboard-ios.css
index f975bd7..ebf5c12 100644
--- a/homeassistant/www/audi-dashboard-ios.css
+++ b/homeassistant/www/audi-dashboard-ios.css
@@ -75,7 +75,7 @@ html,body{height:100%}
/* Telefonbreite: die Ringe stehen oben rechts und oeffnen die Einstellungen
(Stand nach dem Design-Audit). Das Zahnrad entfaellt dort, damit oben rechts
nur ein Element sitzt. */
-.rings{order:9;margin-left:8px;margin-right:-10px;pointer-events:auto;cursor:pointer;
+.rings{order:9;margin-left:8px;margin-right:-10px;cursor:pointer;
min-width:44px;min-height:44px;height:44px;justify-content:flex-end;padding:0 10px}
.rings svg{height:17px}
.profilbtn{display:none}
@@ -85,7 +85,7 @@ html,body{height:100%}
.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:11.5px}
-main{padding:0 16px 30px;scroll-behavior:smooth}
+main#view{--seitenrand:16px;padding:0 var(--seitenrand) 30px;scroll-behavior:smooth}
/* ---------------- Karten und Listen ---------------- */
.tile{padding:16px 18px;margin-bottom:14px;border-radius:var(--r-tile)}
@@ -205,7 +205,7 @@ main{padding:0 16px 30px;scroll-behavior:smooth}
.phone:has(.back.on) .title{font-size:26px;text-align:left}
.title{font-size:34px}
.ptr{grid-column:2;grid-row:2}
- main{grid-column:2;grid-row:3;padding:8px 44px 48px;max-width:860px}
+ main#view{--seitenrand:44px;grid-column:2;grid-row:3;padding:8px var(--seitenrand) 48px;max-width:860px}
.tabbar{grid-column:1;grid-row:1 / 4;display:flex;flex-direction:column;gap:4px;
align-content:start;width:264px;padding:38px 14px 20px;
diff --git a/homeassistant/www/audi-dashboard.css b/homeassistant/www/audi-dashboard.css
index 118178e..b3eadb5 100644
--- a/homeassistant/www/audi-dashboard.css
+++ b/homeassistant/www/audi-dashboard.css
@@ -165,10 +165,23 @@
.themebtn:hover { background: var(--tile-2); color: var(--fg); }
.themebtn:active { background: var(--tile-2); color: var(--fg); transform: scale(.92); }
.themebtn svg { fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; }
-/* D4: die Ringe sind Marke, kein Knopf - sie sitzen links neben dem
- Fahrzeugnamen und nehmen keine Tipps an. */
-.rings { width: 42px; height: 24px; color: var(--fg); pointer-events: none; }
+/* D4 war urspruenglich: die Ringe sind Marke, kein Knopf. Seit die
+ iOS-Auflage in Telefonbreite das Zahnrad ausblendet, sind sie dort aber der
+ einzige Zugang zu den Einstellungen - und als reines div mit
+ pointer-events:none war dieser Zugang fuer Tastatur und Sprachausgabe gar
+ nicht erreichbar. Sie sind deshalb jetzt ein echter Knopf, in jeder Breite
+ bedienbar. Auf grossen Bildschirmen gibt es damit zwei Wege in die
+ Einstellungen (Ringe und Zahnrad) - unschaedlich, und der Zugang haengt
+ nicht mehr davon ab, ob audi-dashboard-ios.css geladen wurde. */
+.rings { width: 42px; height: 24px; color: var(--fg); }
.rings svg { width: 42px; height: auto; }
+
+/* Der Markenklon in der Seitenleiste wird vom Skript unbedingt in die
+ Menueleiste gehaengt; sichtbar macht ihn erst die iOS-Auflage ab 860px.
+ Das Verstecken gehoert deshalb hierher: laedt die Auflage nicht (404,
+ Cache), waere er sonst ein sechstes Element im fuenfspaltigen Raster und
+ die Menueleiste zerfiele. */
+.navmarke { display: none; }
.profilbtn {
display: inline-flex; align-items: center; justify-content: center;
width: 44px; height: 44px; border-radius: 50%;
@@ -195,11 +208,16 @@
@keyframes drehen { to { transform: rotate(360deg); } }
main#view {
+ /* Der seitliche Rand steht als Variable, weil randlose Elemente ihn per
+ negativem Rand ausgleichen muessen (siehe .standort-vollbild). Wer das
+ Padding aendert, ohne die Variable zu benutzen, verschiebt die randlosen
+ Elemente unbemerkt. */
+ --seitenrand: 20px;
flex: 1 1 0;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
- padding: 10px 20px 34px;
+ padding: 10px var(--seitenrand) 34px;
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
@@ -691,7 +709,7 @@ button.tile, .tilebtn { transition: background .15s, transform .1s; }
}
@keyframes user-pulse { 0% { transform: scale(.4); opacity: .9; } 100% { transform: scale(1); opacity: 0; } }
-.standort-vollbild { position: relative; height: min(640px, calc(100dvh - 190px)); border-radius: var(--r-tile); overflow: hidden; margin: 0 -20px; }
+.standort-vollbild { position: relative; height: min(640px, calc(100dvh - 190px)); border-radius: var(--r-tile); overflow: hidden; margin: 0 calc(-1 * var(--seitenrand)); }
.standort-karte { width: 100%; height: 100%; }
.kartensteuerung {