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();
});