Run a full Apple HIG audit; fix silent save failures and popup polish
Read Tab Bars, Sidebars, Sheets, Alerts, Action Sheets, Toolbars, Buttons, Pickers, Loading, Feedback, Privacy, and Undo/Redo off developer.apple.com and cross-checked each against the actual code. profilSpeichern() (the shared save path for ~25 fields) had no error handling at any call site, so a failed backend write was an unhandled promise rejection with zero user feedback - fixed centrally, plus the same for serviceRufen(). Also added a spinner to the "Ladt ..." bootstrap screen and gave the Anzugsmoment/km-correction popups the same primary-button styling the Setup popup already used. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -23,7 +23,13 @@ intended 96x96 square, moved the "Montiert" pill to match, added `color-scheme`
|
||||
popups stop rendering in the browser's default light palette against the dark app, root-caused and
|
||||
fixed a JS crash in the "Mein Audi" image-cycle click handler, merged the separate "Fahrzeugbilder"
|
||||
upload grid into "Bild der Übersicht" (pick a view, tap its photo to upload), and turned the back
|
||||
arrow from red to the neutral headline color — see section C). This file is the entry point for every new agent
|
||||
arrow from red to the neutral headline color; a tenth round the same day ran a real full Apple HIG
|
||||
audit (Navigation/Presentation/Selection & Input/Patterns categories, not just the sixth round's
|
||||
5-page pass) and fixed the three findings that survived: closed a second, larger silent-failure gap
|
||||
in `profilSpeichern()` itself (the shared save path for ~25 fields, previously an unhandled promise
|
||||
rejection with zero user feedback), added a spinner to the "Lädt …" bootstrap screen, and gave the
|
||||
Anzugsmoment/km-correction popups the same primary-button styling the Setup popup already used —
|
||||
see section C). This file is the entry point for every new agent
|
||||
session: what this repo is, what is finished, what is missing, and how to work here. Detail lives in
|
||||
the linked documents — this file points, it does not duplicate.
|
||||
|
||||
@@ -1082,6 +1088,44 @@ wraps the web app for iPhone; a PWA home-screen install is the accepted intermed
|
||||
confirmed the pyscript service ran via the container's HA log, then did a full page reload
|
||||
(forces a real backend re-fetch, not just optimistic local UI state) and the corrected value
|
||||
was still there.
|
||||
- [x] Tenth round the same day (2026-08-16) — a real full Apple HIG audit, not just the sixth
|
||||
round's 5-page pass: read Tab Bars, Sidebars, Sheets, Alerts, Action Sheets, Toolbars/
|
||||
Navigation Bars, Buttons, Pickers, Loading, Feedback, Privacy, and Undo/Redo directly off
|
||||
developer.apple.com and cross-checked each against the actual code (not just CSS) — most of
|
||||
the app already held up (5-tab bar, destructive-button placement, 44pt tap targets,
|
||||
`prefers-reduced-motion` support, confirm-sheet button ordering all already correct). Three
|
||||
real findings surfaced, all fixed and user-approved via `AskUserQuestion` before implementing:
|
||||
(1) **A second, larger silent-failure gap than the one already fixed for receipt upload.**
|
||||
`profilSpeichern()` — the shared save path for roughly 25 editable fields across the app
|
||||
(Anzugsmoment, service book, SmartDeal, tax, insurance, oil change, pause time, backup
|
||||
interval, start image, ...) — was `async` with no `try`/`catch` at any of its ~25 call sites,
|
||||
so a failed backend write was an unhandled promise rejection with zero user feedback; the
|
||||
popup had already closed and the UI already updated optimistically, so the change looked
|
||||
saved when it silently wasn't. Wrapped the one call inside `profilSpeichern()` itself in
|
||||
`try`/`catch`, reusing the existing `hinweis()` error-sheet pattern — one central fix instead
|
||||
of ~25 call-site patches. `serviceRufen()` (the separate wrapper used by km-correction, manual
|
||||
trip/fuel entry, tire-set switching, deletions, restart, ...) got the identical treatment.
|
||||
Verified live by monkey-patching `hass.callService` to reject inside the running app (via
|
||||
`host.hass.callService = () => Promise.reject(...)`), then triggering a real Anzugsmoment save
|
||||
through the actual UI: before the fix, the popup silently closed with a stale "Uncaught (in
|
||||
promise)" console entry and no user-visible sign anything went wrong; after the fix, a
|
||||
"Speichern fehlgeschlagen" sheet appears with the real error text. A follow-up full reload
|
||||
confirmed the failed test write never reached the backend (value reverted to the real stored
|
||||
120 Nm) — the fix only adds feedback, it doesn't change persistence behavior.
|
||||
(2) **"Lädt …" bootstrap screen got a progress indicator** (new `.lade-spinner` CSS, a plain
|
||||
rotating ring) instead of bare static text, per HIG Loading guidance to show something moving
|
||||
while content loads — notable here because this exact screen has a documented history of
|
||||
genuinely hanging, so a spinner also helps a user tell "still loading" apart from "stuck".
|
||||
Respects the existing global `prefers-reduced-motion` override (`*, *::before, *::after`
|
||||
block already forces `animation-duration:.01ms` app-wide) with no extra code.
|
||||
(3) **Anzugsmoment and km-correction popups now use `.aktion.primaer` on Speichern**, matching
|
||||
the Setup and SmartDeal popups, which already distinguished their primary action — these two
|
||||
were the only popups in the app stacking two visually-identical plain buttons.
|
||||
All three verified live in `audi_ha_test` (version `1786916894`): spinner CSS confirmed via
|
||||
`getComputedStyle` (16×16px, `border-radius:50%`, `animation-name:lade-spin`) since a genuine
|
||||
race-condition-timed screenshot proved too fast to catch reliably; Speichern/Abbrechen
|
||||
hierarchy confirmed via screenshot on both popups; error-sheet fix confirmed via the
|
||||
monkey-patch test above. Synced to `installationspaket/`.
|
||||
- [ ] Fix remaining documentation drift (statistics claim, README gaps, obsolete TODO comment) —
|
||||
text-only changes; INSTALL.md's WLAN/TommiG1 drift and stale variable names were fixed
|
||||
2026-08-12 (see section B); `DESIGN_REVIEW_2026-08-13.md` and `REVIEW_main_2026-08-13.md`
|
||||
|
||||
@@ -189,7 +189,17 @@ let PROFIL_ROH = null; // letzter vom Backend gelesener Rohstand, für configCar
|
||||
async function profilSpeichern() {
|
||||
const neuesProfil = configCarZuProfil(PROFIL_ROH);
|
||||
PROFIL_ROH = neuesProfil;
|
||||
await HASS.callService("pyscript", "audi_dashboard_profil_schreiben", { profil_json: JSON.stringify(neuesProfil) });
|
||||
// HIG "Feedback": profilSpeichern() ist der Speicherpfad fuer praktisch
|
||||
// jedes editierbare Feld im Panel (~25 Aufrufstellen) und wurde bisher an
|
||||
// keiner davon awaited oder abgefangen - ein Fehlschlag war eine unhandled
|
||||
// promise rejection ohne jede Rueckmeldung. Zentral hier gefangen, aus
|
||||
// demselben Grund wie serviceRufen() weiter unten.
|
||||
try {
|
||||
await HASS.callService("pyscript", "audi_dashboard_profil_schreiben", { profil_json: JSON.stringify(neuesProfil) });
|
||||
} catch (err) {
|
||||
console.error("audi_dashboard: profilSpeichern", err);
|
||||
hinweis("Speichern fehlgeschlagen", err && err.message ? err.message : "Die Änderung konnte nicht gespeichert werden.");
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------- Bilder
|
||||
@@ -2376,7 +2386,7 @@ function vReifen() {
|
||||
text-transform:none;font-size:12.5px;line-height:1.6">
|
||||
Korrigiert nur den bisher gefahrenen Stand - weitere Fahrten mit diesem
|
||||
Satz zählen ab hier weiter dazu.</span>
|
||||
<button class="aktion" data-kmspeichern="${k}">Speichern</button>
|
||||
<button class="aktion primaer" data-kmspeichern="${k}">Speichern</button>
|
||||
<button class="aktion" data-kmauf="${k}">Abbrechen</button>
|
||||
</div>` : ""}
|
||||
<input type="file" accept="image/*" hidden data-bildupload="${radDatei}">
|
||||
@@ -2415,7 +2425,7 @@ function vReifen() {
|
||||
<div class="feld" style="border-bottom:0"><label for="nm">Neuer Wert</label>
|
||||
<span class="mitEinheit"><input id="nm" type="number" step="5" min="0"
|
||||
value="${r.anzugsmoment}"><span>Nm</span></span></div>
|
||||
<button class="aktion" data-nmspeichern>Speichern</button>
|
||||
<button class="aktion primaer" data-nmspeichern>Speichern</button>
|
||||
<button class="aktion" data-nmauf>Abbrechen</button>
|
||||
</div>` : ""}
|
||||
</div>
|
||||
@@ -3244,7 +3254,16 @@ function randwischenVerdrahten() {
|
||||
Persistenz über hass.callService im Hintergrund - dieselbe Reihenfolge
|
||||
wie im Prototyp, nur dass jetzt zusätzlich gespeichert wird. */
|
||||
function serviceRufen(dienst, daten) {
|
||||
HASS.callService("pyscript", dienst, daten).catch((err) => console.error("audi_dashboard:", dienst, err));
|
||||
// HIG "Feedback": ein fehlgeschlagener Aufruf darf nicht nur in der Konsole
|
||||
// landen - die aufrufende Stelle hat ihr Popup meist schon geschlossen und
|
||||
// die UI optimistisch aktualisiert, sonst würde die Änderung lautlos
|
||||
// verloren gehen (derselbe Fehler, der für den Beleg-Upload und das Setup-
|
||||
// Speichern schon einmal einzeln gefixt wurde - hier zentral für alle
|
||||
// ~20 Aufrufstellen).
|
||||
HASS.callService("pyscript", dienst, daten).catch((err) => {
|
||||
console.error("audi_dashboard:", dienst, err);
|
||||
hinweis("Aktion fehlgeschlagen", err && err.message ? err.message : "Der Dienst konnte nicht ausgeführt werden.");
|
||||
});
|
||||
}
|
||||
|
||||
function ereignisseVerdrahten() {
|
||||
@@ -4254,7 +4273,7 @@ class AudiDashboardPanel extends HTMLElement {
|
||||
<button class="profilbtn" id="ringsbtn" aria-label="Einstellungen"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.6 1.6 0 0 0 .33 1.76l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.6 1.6 0 0 0-1.76-.33 1.6 1.6 0 0 0-1 1.47V21a2 2 0 1 1-4 0v-.1a1.6 1.6 0 0 0-1-1.47 1.6 1.6 0 0 0-1.77.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.6 1.6 0 0 0 4.6 15a1.6 1.6 0 0 0-1.47-1H3a2 2 0 1 1 0-4h.1a1.6 1.6 0 0 0 1.47-1 1.6 1.6 0 0 0-.33-1.77l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.6 1.6 0 0 0 9 4.6 1.6 1.6 0 0 0 10 3.13V3a2 2 0 1 1 4 0v.1a1.6 1.6 0 0 0 1 1.47 1.6 1.6 0 0 0 1.77-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.6 1.6 0 0 0 19.4 9v.04a1.6 1.6 0 0 0 1.47 1H21a2 2 0 1 1 0 4h-.1a1.6 1.6 0 0 0-1.5 1Z"/></svg></button>
|
||||
</div>
|
||||
<div class="ptr" id="ptr"><svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M20.06 8.36c-1.21-3.33-4.3-6.46-8.5-6.46-3.69 0-6.97 2-8.55 5.28a9.47 9.47 0 0 0 4.23 12.77A9.56 9.56 0 0 0 20 15.72l-.89-.44a8.56 8.56 0 0 1-11.43 3.77A8.47 8.47 0 0 1 3.9 7.62a8.38 8.38 0 0 1 7.65-4.72c3.8 0 6.64 2.94 7.66 6.1h-5.66v1h7.5V2.5h-1v5.86Z"/></svg><span>Ziehen zum Aktualisieren</span></div>
|
||||
<main id="view"><div class="tile" style="margin-top:20px"><span class="label">Lädt …</span></div></main>
|
||||
<main id="view"><div class="tile" style="margin-top:20px;display:flex;align-items:center;gap:10px"><span class="lade-spinner" aria-hidden="true"></span><span class="label" style="margin:0">Lädt …</span></div></main>
|
||||
<div id="overlay"></div>
|
||||
<nav class="tabbar" id="tabbar"></nav>
|
||||
</div>`;
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version": 1786769000}
|
||||
{"version": 1786916894}
|
||||
|
||||
@@ -329,6 +329,17 @@ button.tile, .tilebtn { transition: background .15s, transform .1s; }
|
||||
display: block;
|
||||
line-height: 1.3;
|
||||
}
|
||||
/* HIG "Loading": statt reinem Text ein sichtbar laufender Indikator, solange
|
||||
DATEN_GELADEN false ist - hilft auch, "lädt noch" optisch von "hängt fest"
|
||||
zu unterscheiden (dieser Bildschirm hatte schon ein echtes Hänger-Problem). */
|
||||
.lade-spinner {
|
||||
width: 16px; height: 16px; flex: none;
|
||||
border: 2px solid var(--line-strong);
|
||||
border-top-color: var(--fg2);
|
||||
border-radius: 50%;
|
||||
animation: lade-spin .8s linear infinite;
|
||||
}
|
||||
@keyframes lade-spin { to { transform: rotate(360deg); } }
|
||||
/* Echte Überschrift innerhalb einer Kachel (z.B. "Sommerräder"/"Winterräder")
|
||||
statt des sonst üblichen kleinen, gedämpften .label-Abschnittstitels -
|
||||
selbe Typografie-Stufe wie .title (Seitentitel), nur etwas kleiner, weil
|
||||
|
||||
@@ -189,7 +189,17 @@ let PROFIL_ROH = null; // letzter vom Backend gelesener Rohstand, für configCar
|
||||
async function profilSpeichern() {
|
||||
const neuesProfil = configCarZuProfil(PROFIL_ROH);
|
||||
PROFIL_ROH = neuesProfil;
|
||||
await HASS.callService("pyscript", "audi_dashboard_profil_schreiben", { profil_json: JSON.stringify(neuesProfil) });
|
||||
// HIG "Feedback": profilSpeichern() ist der Speicherpfad fuer praktisch
|
||||
// jedes editierbare Feld im Panel (~25 Aufrufstellen) und wurde bisher an
|
||||
// keiner davon awaited oder abgefangen - ein Fehlschlag war eine unhandled
|
||||
// promise rejection ohne jede Rueckmeldung. Zentral hier gefangen, aus
|
||||
// demselben Grund wie serviceRufen() weiter unten.
|
||||
try {
|
||||
await HASS.callService("pyscript", "audi_dashboard_profil_schreiben", { profil_json: JSON.stringify(neuesProfil) });
|
||||
} catch (err) {
|
||||
console.error("audi_dashboard: profilSpeichern", err);
|
||||
hinweis("Speichern fehlgeschlagen", err && err.message ? err.message : "Die Änderung konnte nicht gespeichert werden.");
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------- Bilder
|
||||
@@ -2376,7 +2386,7 @@ function vReifen() {
|
||||
text-transform:none;font-size:12.5px;line-height:1.6">
|
||||
Korrigiert nur den bisher gefahrenen Stand - weitere Fahrten mit diesem
|
||||
Satz zählen ab hier weiter dazu.</span>
|
||||
<button class="aktion" data-kmspeichern="${k}">Speichern</button>
|
||||
<button class="aktion primaer" data-kmspeichern="${k}">Speichern</button>
|
||||
<button class="aktion" data-kmauf="${k}">Abbrechen</button>
|
||||
</div>` : ""}
|
||||
<input type="file" accept="image/*" hidden data-bildupload="${radDatei}">
|
||||
@@ -2415,7 +2425,7 @@ function vReifen() {
|
||||
<div class="feld" style="border-bottom:0"><label for="nm">Neuer Wert</label>
|
||||
<span class="mitEinheit"><input id="nm" type="number" step="5" min="0"
|
||||
value="${r.anzugsmoment}"><span>Nm</span></span></div>
|
||||
<button class="aktion" data-nmspeichern>Speichern</button>
|
||||
<button class="aktion primaer" data-nmspeichern>Speichern</button>
|
||||
<button class="aktion" data-nmauf>Abbrechen</button>
|
||||
</div>` : ""}
|
||||
</div>
|
||||
@@ -3244,7 +3254,16 @@ function randwischenVerdrahten() {
|
||||
Persistenz über hass.callService im Hintergrund - dieselbe Reihenfolge
|
||||
wie im Prototyp, nur dass jetzt zusätzlich gespeichert wird. */
|
||||
function serviceRufen(dienst, daten) {
|
||||
HASS.callService("pyscript", dienst, daten).catch((err) => console.error("audi_dashboard:", dienst, err));
|
||||
// HIG "Feedback": ein fehlgeschlagener Aufruf darf nicht nur in der Konsole
|
||||
// landen - die aufrufende Stelle hat ihr Popup meist schon geschlossen und
|
||||
// die UI optimistisch aktualisiert, sonst würde die Änderung lautlos
|
||||
// verloren gehen (derselbe Fehler, der für den Beleg-Upload und das Setup-
|
||||
// Speichern schon einmal einzeln gefixt wurde - hier zentral für alle
|
||||
// ~20 Aufrufstellen).
|
||||
HASS.callService("pyscript", dienst, daten).catch((err) => {
|
||||
console.error("audi_dashboard:", dienst, err);
|
||||
hinweis("Aktion fehlgeschlagen", err && err.message ? err.message : "Der Dienst konnte nicht ausgeführt werden.");
|
||||
});
|
||||
}
|
||||
|
||||
function ereignisseVerdrahten() {
|
||||
@@ -4254,7 +4273,7 @@ class AudiDashboardPanel extends HTMLElement {
|
||||
<button class="profilbtn" id="ringsbtn" aria-label="Einstellungen"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.6 1.6 0 0 0 .33 1.76l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.6 1.6 0 0 0-1.76-.33 1.6 1.6 0 0 0-1 1.47V21a2 2 0 1 1-4 0v-.1a1.6 1.6 0 0 0-1-1.47 1.6 1.6 0 0 0-1.77.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.6 1.6 0 0 0 4.6 15a1.6 1.6 0 0 0-1.47-1H3a2 2 0 1 1 0-4h.1a1.6 1.6 0 0 0 1.47-1 1.6 1.6 0 0 0-.33-1.77l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.6 1.6 0 0 0 9 4.6 1.6 1.6 0 0 0 10 3.13V3a2 2 0 1 1 4 0v.1a1.6 1.6 0 0 0 1 1.47 1.6 1.6 0 0 0 1.77-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.6 1.6 0 0 0 19.4 9v.04a1.6 1.6 0 0 0 1.47 1H21a2 2 0 1 1 0 4h-.1a1.6 1.6 0 0 0-1.5 1Z"/></svg></button>
|
||||
</div>
|
||||
<div class="ptr" id="ptr"><svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M20.06 8.36c-1.21-3.33-4.3-6.46-8.5-6.46-3.69 0-6.97 2-8.55 5.28a9.47 9.47 0 0 0 4.23 12.77A9.56 9.56 0 0 0 20 15.72l-.89-.44a8.56 8.56 0 0 1-11.43 3.77A8.47 8.47 0 0 1 3.9 7.62a8.38 8.38 0 0 1 7.65-4.72c3.8 0 6.64 2.94 7.66 6.1h-5.66v1h7.5V2.5h-1v5.86Z"/></svg><span>Ziehen zum Aktualisieren</span></div>
|
||||
<main id="view"><div class="tile" style="margin-top:20px"><span class="label">Lädt …</span></div></main>
|
||||
<main id="view"><div class="tile" style="margin-top:20px;display:flex;align-items:center;gap:10px"><span class="lade-spinner" aria-hidden="true"></span><span class="label" style="margin:0">Lädt …</span></div></main>
|
||||
<div id="overlay"></div>
|
||||
<nav class="tabbar" id="tabbar"></nav>
|
||||
</div>`;
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version": 1786769000}
|
||||
{"version": 1786916894}
|
||||
|
||||
@@ -329,6 +329,17 @@ button.tile, .tilebtn { transition: background .15s, transform .1s; }
|
||||
display: block;
|
||||
line-height: 1.3;
|
||||
}
|
||||
/* HIG "Loading": statt reinem Text ein sichtbar laufender Indikator, solange
|
||||
DATEN_GELADEN false ist - hilft auch, "lädt noch" optisch von "hängt fest"
|
||||
zu unterscheiden (dieser Bildschirm hatte schon ein echtes Hänger-Problem). */
|
||||
.lade-spinner {
|
||||
width: 16px; height: 16px; flex: none;
|
||||
border: 2px solid var(--line-strong);
|
||||
border-top-color: var(--fg2);
|
||||
border-radius: 50%;
|
||||
animation: lade-spin .8s linear infinite;
|
||||
}
|
||||
@keyframes lade-spin { to { transform: rotate(360deg); } }
|
||||
/* Echte Überschrift innerhalb einer Kachel (z.B. "Sommerräder"/"Winterräder")
|
||||
statt des sonst üblichen kleinen, gedämpften .label-Abschnittstitels -
|
||||
selbe Typografie-Stufe wie .title (Seitentitel), nur etwas kleiner, weil
|
||||
|
||||
Reference in New Issue
Block a user