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:
Dieter Schlüter 2026-06-27 16:50:03 +02:00
commit 37b4c175d2
6 changed files with 126 additions and 18 deletions

View file

@ -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,