fix(notruf): ehrlicher Status, wenn niemand verständigt werden kann
Bisher meldete ein Notruf ohne nutzbaren Kanal trotzdem Erfolg (status="alerted", grüner Haken), obwohl tatsächlich niemand benachrichtigt wurde — gefährlich für Senioren. - emergency.py: drei klare Endzustände alerted / no_recipients / delivery_failed; notify_readiness() prüft, ob ein Kanal wirklich senden würde (SMTP+Empfaenger bzw. Webhook). Mehrsprachige Hinweise fuer die Fehlfaelle (nennen Selbsthilfe 112). Antwort traegt jetzt notified + notify_ready; Warn-Log, wenn nichts zugestellt wurde. - /api/emergency/status liefert notify_ready fuer die Vorab-Warnung. - Frontend: gelbe Vorab-Warnung im SOS-Dialog, wenn kein Empfaenger eingerichtet ist; bei no_recipients/delivery_failed kein gruenes Erfolgssignal mehr, sondern roter Fehlerzustand (app.js v54). - Tests an die neue Semantik angepasst. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
9eb5c31eb6
commit
37b4c175d2
6 changed files with 126 additions and 18 deletions
|
|
@ -18,7 +18,11 @@ from app.dependencies import (
|
|||
from app.core.memory_extractor import maybe_schedule_extraction
|
||||
from app.quota import enforce_quota, record_usage, QuotaExceededError
|
||||
from app.runtime_config import runtime_settings
|
||||
from app.safety.emergency import record_manual_emergency, emergency_cooldown_status
|
||||
from app.safety.emergency import (
|
||||
record_manual_emergency,
|
||||
emergency_cooldown_status,
|
||||
notify_readiness,
|
||||
)
|
||||
from app.schemas import ChatRequest, EmergencyRequest
|
||||
|
||||
router = APIRouter()
|
||||
|
|
@ -161,5 +165,8 @@ async def trigger_emergency(payload: EmergencyRequest, user: User = Depends(requ
|
|||
@router.get("/emergency/status")
|
||||
async def emergency_status(user: User = Depends(require_user)):
|
||||
"""Aktueller Notruf-Sperr-Status des Nutzers (für die grüne Button-Anzeige,
|
||||
auch nach einem Seiten-Neuladen)."""
|
||||
return emergency_cooldown_status(user, get_store(), runtime_settings)
|
||||
auch nach einem Seiten-Neuladen) plus notify_ready für die Vorab-Warnung,
|
||||
falls (noch) kein Benachrichtigungskanal eingerichtet ist."""
|
||||
st = emergency_cooldown_status(user, get_store(), runtime_settings)
|
||||
st["notify_ready"] = notify_readiness(user, runtime_settings)
|
||||
return st
|
||||
|
|
|
|||
|
|
@ -56,6 +56,49 @@ _SENT_NOTICE: dict[str, str] = {
|
|||
"zh": "已通知求助。已通知联系人。",
|
||||
}
|
||||
|
||||
# Hinweis je Sprache, wenn GAR KEIN Kanal nutzbar ist (keine Kontakte/kein Webhook
|
||||
# konfiguriert) — niemand wurde erreicht. Handlungsleitend: selbst den Notruf rufen.
|
||||
_NO_RECIPIENTS_NOTICE: dict[str, str] = {
|
||||
"de": "Es konnte niemand benachrichtigt werden – es sind keine Notfall-Kontakte eingerichtet. Bitte rufen Sie selbst den Notruf 112 an.",
|
||||
"en": "No one could be notified – no emergency contacts are set up. Please call your emergency number (112) yourself.",
|
||||
"fr": "Personne n'a pu être prévenu – aucun contact d'urgence n'est configuré. Veuillez appeler vous-même le 112.",
|
||||
"es": "No se pudo avisar a nadie: no hay contactos de emergencia configurados. Por favor, llame usted mismo al 112.",
|
||||
"it": "Non è stato possibile avvisare nessuno: nessun contatto di emergenza è configurato. Per favore chiami lei stesso il 112.",
|
||||
"pt": "Não foi possível avisar ninguém – não há contactos de emergência configurados. Por favor, ligue você mesmo para o 112.",
|
||||
"pl": "Nie udało się nikogo powiadomić – nie skonfigurowano kontaktów alarmowych. Proszę samodzielnie zadzwonić pod numer 112.",
|
||||
"ar": "تعذّر إخطار أي شخص – لا توجد جهات اتصال للطوارئ. يرجى الاتصال بنفسك برقم الطوارئ 112.",
|
||||
"ru": "Никого не удалось уведомить — экстренные контакты не настроены. Пожалуйста, позвоните в службу спасения 112 сами.",
|
||||
"zh": "无法通知任何人——尚未设置紧急联系人。请您自行拨打急救电话 112。",
|
||||
}
|
||||
|
||||
# Hinweis je Sprache, wenn Kanäle KONFIGURIERT waren, der Versand aber scheiterte
|
||||
# (SMTP-/Webhook-Fehler). Unterscheidet den echten Zustellfehler vom Fehlen von Kontakten.
|
||||
_DELIVERY_FAILED_NOTICE: dict[str, str] = {
|
||||
"de": "Die Benachrichtigung konnte nicht zugestellt werden. Bitte versuchen Sie es erneut oder rufen Sie selbst den Notruf 112 an.",
|
||||
"en": "The notification could not be delivered. Please try again or call your emergency number (112) yourself.",
|
||||
"fr": "La notification n'a pas pu être transmise. Veuillez réessayer ou appeler vous-même le 112.",
|
||||
"es": "No se pudo entregar la notificación. Por favor, inténtelo de nuevo o llame usted mismo al 112.",
|
||||
"it": "Non è stato possibile recapitare la notifica. Per favore riprovi o chiami lei stesso il 112.",
|
||||
"pt": "Não foi possível entregar a notificação. Por favor, tente novamente ou ligue você mesmo para o 112.",
|
||||
"pl": "Nie udało się dostarczyć powiadomienia. Proszę spróbować ponownie lub samodzielnie zadzwonić pod numer 112.",
|
||||
"ar": "تعذّر تسليم الإشعار. يرجى المحاولة مرة أخرى أو الاتصال بنفسك برقم الطوارئ 112.",
|
||||
"ru": "Не удалось доставить уведомление. Пожалуйста, повторите попытку или позвоните в службу спасения 112 сами.",
|
||||
"zh": "通知发送失败。请重试或您自行拨打急救电话 112。",
|
||||
}
|
||||
|
||||
# Status -> Hinweis-Tabelle. "cooldown" nutzt bewusst den "gesendet"-Text (beruhigen).
|
||||
_NOTICE_TABLES: dict[str, dict[str, str]] = {
|
||||
"alerted": _SENT_NOTICE,
|
||||
"no_recipients": _NO_RECIPIENTS_NOTICE,
|
||||
"delivery_failed": _DELIVERY_FAILED_NOTICE,
|
||||
}
|
||||
|
||||
|
||||
def emergency_notice(language: str | None, status: str) -> str:
|
||||
"""Hinweis in Nutzersprache (Fallback Deutsch) passend zum Notruf-Status."""
|
||||
table = _NOTICE_TABLES.get(status, _NO_NOTIFY_NOTICE)
|
||||
return table.get((language or "de").lower(), table["de"])
|
||||
|
||||
|
||||
def notify_notice(language: str | None, sent: bool) -> str:
|
||||
"""Hinweis in Nutzersprache (Fallback Deutsch), je nachdem ob etwas rausging."""
|
||||
|
|
@ -214,6 +257,19 @@ def _phones(user, cfg) -> list[str]:
|
|||
return _csv(raw) or _csv(cfg.emergency_contact_phone)
|
||||
|
||||
|
||||
def notify_readiness(user, cfg=settings) -> dict:
|
||||
"""Ob für diesen Nutzer ein Benachrichtigungskanal *funktionsfähig* wäre.
|
||||
|
||||
Wichtig: nicht „Kontakt hinterlegt", sondern „Kanal würde tatsächlich senden".
|
||||
E-Mail braucht einen SMTP-Host UND mindestens einen Empfänger; SMS/Anruf
|
||||
braucht eine Webhook-URL. Dient dem Frontend für eine Vorab-Warnung und der
|
||||
sauberen Status-Unterscheidung no_recipients vs. delivery_failed.
|
||||
"""
|
||||
email = bool(cfg.smtp_host) and bool(_contacts(user, cfg))
|
||||
webhook = bool(cfg.emergency_webhook_url)
|
||||
return {"email": email, "webhook": webhook, "any": email or webhook}
|
||||
|
||||
|
||||
def _send_email_sync(to: list[str], subject: str, body: str, cfg) -> bool:
|
||||
"""Versendet eine E-Mail per SMTP. Liefert True bei Erfolg, sonst False (best effort)."""
|
||||
if not cfg.smtp_host or not to:
|
||||
|
|
@ -364,11 +420,25 @@ async def record_manual_emergency(user, store, language: str | None = None,
|
|||
metrics.inc("emergency_notify_total", {"channel": "webhook"})
|
||||
|
||||
notified = email_sent or webhook_sent
|
||||
ready = notify_readiness(user, cfg)
|
||||
# Drei ehrliche Endzustände: erreicht / kein Kanal eingerichtet / Zustellung gescheitert.
|
||||
if notified:
|
||||
status = "alerted"
|
||||
elif not ready["any"]:
|
||||
status = "no_recipients"
|
||||
else:
|
||||
status = "delivery_failed"
|
||||
if status != "alerted":
|
||||
logger.warning("Notruf von %s: status=%s, keine Benachrichtigung zugestellt "
|
||||
"(email_ready=%s webhook_ready=%s)",
|
||||
profile["name"], status, ready["email"], ready["webhook"])
|
||||
return {
|
||||
"category": "manual",
|
||||
"status": "alerted",
|
||||
"status": status,
|
||||
"cooldown_seconds": cd["cooldown_seconds"],
|
||||
"notice": notify_notice(language, notified),
|
||||
"notice": emergency_notice(language, status),
|
||||
"notified": notified,
|
||||
"notify_ready": ready,
|
||||
"email_sent": email_sent,
|
||||
"webhook_sent": webhook_sent,
|
||||
"channels": channels,
|
||||
|
|
|
|||
|
|
@ -2380,6 +2380,7 @@ async function runEmergency() {
|
|||
body: JSON.stringify(body),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (data && data.notify_ready) sosNotifyReady = !!data.notify_ready.any;
|
||||
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);
|
||||
|
|
@ -2389,12 +2390,19 @@ async function runEmergency() {
|
|||
const notice = (data && data.notice) ||
|
||||
"ACHTUNG: Es ist noch keine Benachrichtigung eingebaut.";
|
||||
addMessage("emergency", "🆘 " + notice);
|
||||
const cd = (data && data.cooldown_seconds) || 0;
|
||||
if (cd > 0) {
|
||||
enterCooldown(cd, 0); // grün/pulsierend für die Sperre
|
||||
if (data && data.status === "alerted") {
|
||||
const cd = 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);
|
||||
}
|
||||
} else {
|
||||
setSosState("done"); // Sperre aus -> nur kurzes Erfolgs-Feedback
|
||||
setTimeout(() => { if (!sosCooldownActive) setSosState("idle"); }, 6000);
|
||||
// no_recipients / delivery_failed: es ging NICHTS raus -> KEIN grünes Erfolgssignal,
|
||||
// sondern roter Fehlerzustand; der Hinweis nennt die Selbsthilfe (112).
|
||||
setSosState("error");
|
||||
setTimeout(() => { if (!sosCooldownActive) setSosState("idle"); }, 8000);
|
||||
}
|
||||
} catch (e) {
|
||||
addMessage("emergency", "🆘 Fehler beim Auslösen des Notrufs.");
|
||||
|
|
@ -2403,9 +2411,17 @@ async function runEmergency() {
|
|||
}
|
||||
}
|
||||
|
||||
// Ist (serverseitig) überhaupt ein Benachrichtigungskanal nutzbar? null = noch
|
||||
// unbekannt; erst bei sicherem `false` zeigen wir die Vorab-Warnung im Dialog.
|
||||
let sosNotifyReady = null;
|
||||
|
||||
// Großer eigener Bestätigungsdialog statt native confirm() — seniorentauglich.
|
||||
const sosConfirm = $("#sos-confirm");
|
||||
function openSosConfirm() { if (sosConfirm) sosConfirm.classList.remove("hidden"); }
|
||||
function openSosConfirm() {
|
||||
const warn = $("#sos-confirm-warn");
|
||||
if (warn) warn.classList.toggle("hidden", sosNotifyReady !== false);
|
||||
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.
|
||||
|
|
@ -2428,6 +2444,7 @@ async function initSosCooldown() {
|
|||
const res = await fetch("/api/emergency/status", { headers: { Accept: "application/json" } });
|
||||
if (!res.ok) return;
|
||||
const s = await res.json();
|
||||
if (s && s.notify_ready) sosNotifyReady = !!s.notify_ready.any;
|
||||
if (s && s.active) enterCooldown(s.remaining_seconds || 0, s.minutes_ago || 0);
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -386,6 +386,11 @@
|
|||
<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>
|
||||
<p id="sos-confirm-warn"
|
||||
class="hidden mt-3 rounded-lg bg-amber-100 dark:bg-amber-900/40 px-3 py-2 text-sm font-semibold text-amber-800 dark:text-amber-200">
|
||||
⚠️ Achtung: Es sind noch keine Notfall-Kontakte eingerichtet. Es kann niemand
|
||||
automatisch benachrichtigt werden – im Notfall bitte selbst 112 anrufen.
|
||||
</p>
|
||||
</div>
|
||||
<div class="px-5 pb-5 grid grid-cols-1 gap-3">
|
||||
<button type="button" id="sos-confirm-yes"
|
||||
|
|
@ -414,6 +419,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/app.js?v=53"></script>
|
||||
<script src="/app.js?v=54"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue