feat(ui): SOS-Button prominent (rot gefüllt, großes weißes „SOS") + Bestätigung & Status

Der Notruf-Button war ein kleines 36px-Outline-Icon (🆘) — leicht zu übersehen.
Jetzt ein rot gefüllter Button (bg-red-600) mit großem, fettem weißem „SOS" in
der (immer sichtbaren) Kopfzeile — der Senior erkennt ihn sofort. Sprachwahl
und Menü bleiben unverändert an ihrem Platz.

- Bestätigung: statt native confirm() ein großer eigener Dialog (rote SOS-Kopf-
  zeile, Warnzeile, große Knöpfe „JA, HILFE RUFEN" / „Abbrechen"). Ein Tipp auf
  SOS, ein Tipp auf JA — schnell, aber gegen Fehlalarm geschützt.
- Feedback am Button selbst: setSosState idle|calling|done|error —
  „Rufe …" (pulsierend) → „Gerufen ✓" (grün) → zurück zu „SOS". So sieht der
  Senior unmittelbar am Knopf, dass der Alarm ausgelöst wurde.

Nur Frontend (app/web), live ohne Neustart (No-Cache-Static, Bump auf v=49).
Verifiziert via Headless-Chrome/CDP: Idle/Dialog/Calling/Done-Zustände.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-06-26 13:48:35 +02:00
commit 0ef4fe9cb8
2 changed files with 92 additions and 26 deletions

View file

@ -2245,29 +2245,74 @@ function deviceInfo() {
return { mobile: /Mobi|Android|iPhone|iPad|iPod/i.test(ua), platform: ua.slice(0, 120) };
}
if (emergencyBtn) {
emergencyBtn.addEventListener("click", async () => {
if (!confirm("Wirklich Hilfe rufen?\nIch alarmiere sofort Helfer!")) return;
const lang = (langSel && langSel.value) || "de";
// Sichtbarer Zustand am SOS-Button selbst: idle | calling | done | error.
// So erkennt der Senior unmittelbar am Knopf, dass der Alarm ausgelöst wurde.
function setSosState(state) {
if (!emergencyBtn) return;
emergencyBtn.classList.remove(
"bg-red-600", "hover:bg-red-700", "bg-green-600", "animate-pulse"
);
if (state === "calling") {
emergencyBtn.textContent = "Rufe …";
emergencyBtn.disabled = true;
try {
const g = await captureGeo();
const body = { language: lang, device: deviceInfo() };
if (g.ok) body.geo = { lat: g.lat, lon: g.lon, accuracy: g.accuracy, ts: g.ts };
else body.geo_error = g.error;
const res = await fetch("/api/emergency", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const data = await res.json().catch(() => ({}));
const notice = (data && data.notice) ||
"ACHTUNG: Es ist noch keine Benachrichtigung eingebaut.";
addMessage("emergency", "🆘 " + notice);
} catch (e) {
addMessage("emergency", "🆘 Fehler beim Auslösen des Notrufs.");
} finally {
emergencyBtn.disabled = false;
}
});
emergencyBtn.classList.add("bg-red-600", "animate-pulse");
} else if (state === "done") {
emergencyBtn.textContent = "Gerufen ✓";
emergencyBtn.disabled = true;
emergencyBtn.classList.add("bg-green-600");
} else if (state === "error") {
emergencyBtn.textContent = "Fehler";
emergencyBtn.disabled = false;
emergencyBtn.classList.add("bg-red-600", "hover:bg-red-700");
} else { // idle
emergencyBtn.textContent = "SOS";
emergencyBtn.disabled = false;
emergencyBtn.classList.add("bg-red-600", "hover:bg-red-700");
}
}
async function runEmergency() {
const lang = (langSel && langSel.value) || "de";
setSosState("calling");
try {
const g = await captureGeo();
const body = { language: lang, device: deviceInfo() };
if (g.ok) body.geo = { lat: g.lat, lon: g.lon, accuracy: g.accuracy, ts: g.ts };
else body.geo_error = g.error;
const res = await fetch("/api/emergency", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const data = await res.json().catch(() => ({}));
const notice = (data && data.notice) ||
"ACHTUNG: Es ist noch keine Benachrichtigung eingebaut.";
addMessage("emergency", "🆘 " + notice);
setSosState("done");
setTimeout(() => setSosState("idle"), 6000); // danach zurück zu „SOS"
} catch (e) {
addMessage("emergency", "🆘 Fehler beim Auslösen des Notrufs.");
setSosState("error");
setTimeout(() => setSosState("idle"), 4000);
}
}
// Großer eigener Bestätigungsdialog statt native confirm() — seniorentauglich.
const sosConfirm = $("#sos-confirm");
function openSosConfirm() { if (sosConfirm) sosConfirm.classList.remove("hidden"); }
function closeSosConfirm() { if (sosConfirm) sosConfirm.classList.add("hidden"); }
if (emergencyBtn) {
emergencyBtn.addEventListener("click", openSosConfirm);
}
const sosYesBtn = $("#sos-confirm-yes");
const sosNoBtn = $("#sos-confirm-no");
if (sosYesBtn) sosYesBtn.addEventListener("click", () => { closeSosConfirm(); runEmergency(); });
if (sosNoBtn) sosNoBtn.addEventListener("click", closeSosConfirm);
// Klick auf den Hintergrund oder Escape bricht ab (schließt nur den Dialog).
if (sosConfirm) {
sosConfirm.addEventListener("click", (e) => { if (e.target === sosConfirm) closeSosConfirm(); });
}
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && sosConfirm && !sosConfirm.classList.contains("hidden")) closeSosConfirm();
});

View file

@ -229,7 +229,7 @@
<img src="/favicon.svg?v=1" alt="" class="w-7 h-7 shrink-0" />
<h1 class="flex-1 truncate font-semibold text-base">Voice&nbsp;Assistant</h1>
<button type="button" id="emergency-btn" title="Hilfe rufen" aria-label="Hilfe rufen"
class="shrink-0 h-9 w-9 grid place-items-center rounded-lg border border-red-300 dark:border-red-800 text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors text-base">🆘</button>
class="shrink-0 h-11 px-4 grid place-items-center rounded-lg bg-red-600 hover:bg-red-700 text-white font-extrabold tracking-wide text-lg shadow-sm whitespace-nowrap transition-colors disabled:opacity-100">SOS</button>
<select id="lang-sel" title="Sprache"
class="text-sm rounded-lg border border-slate-300 dark:border-slate-600 bg-transparent px-2 py-1.5">
<option value="de">🇩🇪 DE</option>
@ -365,6 +365,27 @@
<div id="status" class="w-full max-w-3xl mx-auto mt-1 min-h-[1rem] text-xs text-slate-500 dark:text-slate-400"></div>
</footer>
<script src="/app.js?v=48"></script>
<!-- ═══════════════ SOS-Bestätigung (Notruf) ═══════════════ -->
<div id="sos-confirm" role="dialog" aria-modal="true" aria-labelledby="sos-confirm-title"
class="hidden fixed inset-0 z-[60] flex items-center justify-center bg-black/50 p-4">
<div class="w-full max-w-sm rounded-2xl bg-white dark:bg-slate-800 shadow-xl overflow-hidden">
<div class="bg-red-600 text-white px-5 py-4 flex items-center gap-3">
<span class="text-3xl font-extrabold tracking-wide">SOS</span>
<span id="sos-confirm-title" class="text-lg font-semibold">Hilfe rufen?</span>
</div>
<div class="px-5 py-4">
<p class="text-base text-slate-700 dark:text-slate-200">Möchten Sie wirklich Hilfe rufen?</p>
<p class="mt-1 text-sm font-semibold text-red-600 dark:text-red-400">Es werden sofort Helfer alarmiert.</p>
</div>
<div class="px-5 pb-5 grid grid-cols-1 gap-3">
<button type="button" id="sos-confirm-yes"
class="h-14 rounded-xl bg-red-600 hover:bg-red-700 text-white text-xl font-extrabold tracking-wide transition-colors">JA, HILFE RUFEN</button>
<button type="button" id="sos-confirm-no"
class="h-12 rounded-xl border border-slate-300 dark:border-slate-600 text-slate-700 dark:text-slate-200 text-lg font-semibold hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors">Abbrechen</button>
</div>
</div>
</div>
<script src="/app.js?v=49"></script>
</body>
</html>