From 0ef4fe9cb8f9f6098f19156c22852b20fb3c6b2a Mon Sep 17 00:00:00 2001 From: dschlueter Date: Fri, 26 Jun 2026 13:48:35 +0200 Subject: [PATCH] =?UTF-8?q?feat(ui):=20SOS-Button=20prominent=20(rot=20gef?= =?UTF-8?q?=C3=BCllt,=20gro=C3=9Fes=20wei=C3=9Fes=20=E2=80=9ESOS")=20+=20B?= =?UTF-8?q?est=C3=A4tigung=20&=20Status?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- app/web/app.js | 93 ++++++++++++++++++++++++++++++++++------------ app/web/index.html | 25 ++++++++++++- 2 files changed, 92 insertions(+), 26 deletions(-) diff --git a/app/web/app.js b/app/web/app.js index 7371de8..9845881 100644 --- a/app/web/app.js +++ b/app/web/app.js @@ -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(); +}); diff --git a/app/web/index.html b/app/web/index.html index f1c782c..edceaee 100644 --- a/app/web/index.html +++ b/app/web/index.html @@ -229,7 +229,7 @@

Voice Assistant

+ 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