feat(notruf): Wiederauslöse-Sperre + grüner Hinweis; "Neues Gespräch" in die Kopfzeile
1) Notruf-Sperre (Cooldown) gegen Mehrfach-Alarme verwirrter/verängstigter
Senioren:
- Neue Einstellung emergency_cooldown_minutes (Default 5), zur Laufzeit im
Admin-Bereich änderbar (RUNTIME_SETTABLE), 0 = aus.
- record_manual_emergency unterdrückt innerhalb der Sperre jeden weiteren
Alarm (kein zweiter Versand, kein Log-Eintrag) und liefert status=cooldown
mit minutes_ago/remaining_seconds. Neuer Endpunkt GET /api/emergency/status
für die Anzeige nach einem Seiten-Neuladen. Store: last_manual_emergency_at.
- UI: Nach dem Alarm wird der Button grün, zeigt „✓" und pulsiert für die
Dauer der Sperre. Erneuter Druck öffnet einen grünen Hinweis-Dialog:
„Ich habe Deine Helfer vor <n> Minuten alarmiert. Wahrscheinlich ist Deine
Hilfe schon unterwegs." Der grüne Zustand übersteht ein Neuladen
(Status-Abruf beim Start).
2) „Neues Gespräch" als Symbol (Stift) zurück in die Kopfzeile — zwischen
Sprachauswahl und Menü; löst dieselbe Aktion aus wie der Menüeintrag.
Tests: neue test_emergency_cooldown.py (Status/Suppression/Endpoint); zwei
bestehende Notruf-Tests an die neue Interface-Methode bzw. die Sperre
angepasst. 250 Offline-Tests grün. Frontend via Headless-Chrome verifiziert
(Alarm->grün/pulsierend, Hinweis-Dialog, Reload-Persistenz, Header-Button).
Bump app.js v=50 -> v=51.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
8a6f428c1b
commit
8c152ee77c
10 changed files with 290 additions and 22 deletions
108
app/web/app.js
108
app/web/app.js
|
|
@ -422,6 +422,7 @@ function loadMe() {
|
|||
applyAllowedLanguages(prefs);
|
||||
applyPlaybackChoice(prefs.tts_provider);
|
||||
applyGenderChoice(prefs.voice_gender || 'f');
|
||||
initSosCooldown(); // grünen Sperr-Zustand nach Reload wiederherstellen
|
||||
return me;
|
||||
} catch (e) {
|
||||
$("#identity").textContent = "Verbindung fehlgeschlagen";
|
||||
|
|
@ -840,6 +841,9 @@ function newConversation() {
|
|||
}, 2500);
|
||||
}
|
||||
$("#new-chat").addEventListener("click", () => { closeMenu(); newConversation(); });
|
||||
// Gleiche Aktion als Symbol oben in der Kopfzeile (zwischen Sprachwahl und Menü).
|
||||
const newChatTop = $("#new-chat-top");
|
||||
if (newChatTop) newChatTop.addEventListener("click", newConversation);
|
||||
|
||||
// ---------- Menü (⋮) verdrahten ----------
|
||||
$("#menu-toggle").addEventListener("click", () =>
|
||||
|
|
@ -2245,24 +2249,23 @@ function deviceInfo() {
|
|||
return { mobile: /Mobi|Android|iPhone|iPad|iPod/i.test(ua), platform: ua.slice(0, 120) };
|
||||
}
|
||||
|
||||
// Sichtbarer Zustand am SOS-Button selbst: idle | calling | done | error.
|
||||
// So erkennt der Senior unmittelbar am Knopf, dass der Alarm ausgelöst wurde.
|
||||
// Button bleibt ein kleines Quadrat (so groß wie die Sprachauswahl) -> Feedback
|
||||
// über Farbe/Symbol statt langer Wörter, damit es nicht aus der Form läuft.
|
||||
// Sichtbarer Zustand am SOS-Button (Quadrat in Höhe der Sprachauswahl):
|
||||
// idle (rot „SOS") | calling (rot, pulsierend) | done (grün ✓, pulsierend —
|
||||
// während der Sperre) | error. Feedback über Farbe/Symbol, nicht über Wörter.
|
||||
function setSosState(state) {
|
||||
if (!emergencyBtn) return;
|
||||
emergencyBtn.classList.remove(
|
||||
"bg-red-600", "hover:bg-red-700", "bg-green-600", "animate-pulse",
|
||||
"text-sm", "text-lg"
|
||||
"bg-red-600", "hover:bg-red-700", "bg-green-600", "hover:bg-green-700",
|
||||
"animate-pulse", "text-sm", "text-lg"
|
||||
);
|
||||
if (state === "calling") {
|
||||
emergencyBtn.textContent = "SOS"; // pulsiert = „wird gerufen“
|
||||
emergencyBtn.disabled = true;
|
||||
emergencyBtn.classList.add("bg-red-600", "animate-pulse", "text-sm");
|
||||
} else if (state === "done") {
|
||||
emergencyBtn.textContent = "✓"; // grün + Haken = Alarm raus
|
||||
emergencyBtn.disabled = true;
|
||||
emergencyBtn.classList.add("bg-green-600", "text-lg");
|
||||
emergencyBtn.textContent = "✓"; // grün + Haken + pulsiert = Alarm raus
|
||||
emergencyBtn.disabled = false; // klickbar -> beruhigender Hinweis
|
||||
emergencyBtn.classList.add("bg-green-600", "hover:bg-green-700", "animate-pulse", "text-lg");
|
||||
} else if (state === "error") {
|
||||
emergencyBtn.textContent = "!";
|
||||
emergencyBtn.disabled = false;
|
||||
|
|
@ -2274,6 +2277,34 @@ function setSosState(state) {
|
|||
}
|
||||
}
|
||||
|
||||
// ----- Wiederauslöse-Sperre (Cooldown) -----
|
||||
// Nach einem Alarm bleibt der Button für die (im Admin einstellbare) Dauer grün
|
||||
// und pulsierend; erneutes Drücken zeigt nur einen beruhigenden Hinweis, statt
|
||||
// einen zweiten Alarm auszulösen — Schutz vor Mehrfach-Alarmen.
|
||||
let sosCooldownActive = false;
|
||||
let sosAlertedAtMs = 0;
|
||||
let sosCooldownTimer = null;
|
||||
|
||||
function enterCooldown(remainingSec, minutesAgo) {
|
||||
sosCooldownActive = true;
|
||||
// Auslösezeitpunkt rekonstruieren, damit „vor X Minuten" stimmt (auch nach Reload).
|
||||
sosAlertedAtMs = Date.now() - Math.max(0, minutesAgo || 0) * 60000;
|
||||
setSosState("done");
|
||||
if (sosCooldownTimer) clearTimeout(sosCooldownTimer);
|
||||
if (remainingSec > 0) sosCooldownTimer = setTimeout(exitCooldown, remainingSec * 1000);
|
||||
else exitCooldown();
|
||||
}
|
||||
|
||||
function exitCooldown() {
|
||||
sosCooldownActive = false;
|
||||
sosCooldownTimer = null;
|
||||
setSosState("idle");
|
||||
}
|
||||
|
||||
function sosMinutesAgo() {
|
||||
return Math.max(0, Math.floor((Date.now() - sosAlertedAtMs) / 60000));
|
||||
}
|
||||
|
||||
async function runEmergency() {
|
||||
const lang = (langSel && langSel.value) || "de";
|
||||
setSosState("calling");
|
||||
|
|
@ -2288,15 +2319,26 @@ async function runEmergency() {
|
|||
body: JSON.stringify(body),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (data && data.status === "cooldown") {
|
||||
// Server hat den (Doppel-)Alarm unterdrückt -> nur beruhigen, kein neuer Alarm.
|
||||
enterCooldown(data.remaining_seconds || 0, data.minutes_ago || 0);
|
||||
openSosCooldown();
|
||||
return;
|
||||
}
|
||||
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"
|
||||
const cd = (data && data.cooldown_seconds) || 0;
|
||||
if (cd > 0) {
|
||||
enterCooldown(cd, 0); // grün/pulsierend für die Sperre
|
||||
} else {
|
||||
setSosState("done"); // Sperre aus -> nur kurzes Erfolgs-Feedback
|
||||
setTimeout(() => { if (!sosCooldownActive) setSosState("idle"); }, 6000);
|
||||
}
|
||||
} catch (e) {
|
||||
addMessage("emergency", "🆘 Fehler beim Auslösen des Notrufs.");
|
||||
setSosState("error");
|
||||
setTimeout(() => setSosState("idle"), 4000);
|
||||
setTimeout(() => { if (!sosCooldownActive) setSosState("idle"); }, 4000);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2305,17 +2347,47 @@ const sosConfirm = $("#sos-confirm");
|
|||
function openSosConfirm() { if (sosConfirm) sosConfirm.classList.remove("hidden"); }
|
||||
function closeSosConfirm() { if (sosConfirm) sosConfirm.classList.add("hidden"); }
|
||||
|
||||
// Beruhigender grüner Hinweis, wenn während der Sperre erneut gedrückt wird.
|
||||
const sosCooldownDlg = $("#sos-cooldown");
|
||||
function openSosCooldown() {
|
||||
const m = sosMinutesAgo();
|
||||
const when = m < 1 ? "gerade eben" : ("vor " + m + (m === 1 ? " Minute" : " Minuten"));
|
||||
const txtEl = $("#sos-cooldown-text");
|
||||
if (txtEl) txtEl.textContent =
|
||||
"Ich habe Deine Helfer " + when + " alarmiert. " +
|
||||
"Wahrscheinlich ist Deine Hilfe schon unterwegs.";
|
||||
if (sosCooldownDlg) sosCooldownDlg.classList.remove("hidden");
|
||||
}
|
||||
function closeSosCooldown() { if (sosCooldownDlg) sosCooldownDlg.classList.add("hidden"); }
|
||||
|
||||
// Beim Laden den Sperr-Status vom Server holen, damit der grüne Zustand auch
|
||||
// ein Seiten-Neuladen übersteht (verwirrte Senioren laden gern neu).
|
||||
async function initSosCooldown() {
|
||||
try {
|
||||
const res = await fetch("/api/emergency/status", { headers: { Accept: "application/json" } });
|
||||
if (!res.ok) return;
|
||||
const s = await res.json();
|
||||
if (s && s.active) enterCooldown(s.remaining_seconds || 0, s.minutes_ago || 0);
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
if (emergencyBtn) {
|
||||
emergencyBtn.addEventListener("click", openSosConfirm);
|
||||
emergencyBtn.addEventListener("click", () => {
|
||||
if (sosCooldownActive) openSosCooldown(); // schon alarmiert -> nur beruhigen
|
||||
else openSosConfirm(); // sonst Notruf bestätigen
|
||||
});
|
||||
}
|
||||
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(); });
|
||||
}
|
||||
const sosCooldownOkBtn = $("#sos-cooldown-ok");
|
||||
if (sosCooldownOkBtn) sosCooldownOkBtn.addEventListener("click", closeSosCooldown);
|
||||
// Klick auf den Hintergrund oder Escape schließt die Dialoge (kein Alarm).
|
||||
if (sosConfirm) sosConfirm.addEventListener("click", (e) => { if (e.target === sosConfirm) closeSosConfirm(); });
|
||||
if (sosCooldownDlg) sosCooldownDlg.addEventListener("click", (e) => { if (e.target === sosCooldownDlg) closeSosCooldown(); });
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape" && sosConfirm && !sosConfirm.classList.contains("hidden")) closeSosConfirm();
|
||||
if (e.key !== "Escape") return;
|
||||
if (sosConfirm && !sosConfirm.classList.contains("hidden")) closeSosConfirm();
|
||||
if (sosCooldownDlg && !sosCooldownDlg.classList.contains("hidden")) closeSosCooldown();
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue