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

@ -73,10 +73,15 @@ def test_second_alarm_suppressed(monkeypatch):
def test_first_alarm_goes_through(monkeypatch):
monkeypatch.setattr(em.settings, "emergency_cooldown_minutes", 5)
monkeypatch.setattr(em, "_send_email_sync", lambda *a, **k: False)
monkeypatch.setattr(em.settings, "smtp_host", "")
monkeypatch.setattr(em.settings, "emergency_webhook_url", "")
store = FakeStore(last=None)
res = asyncio.run(em.record_manual_emergency(FakeUser(), store, language="de"))
assert res["status"] == "alerted"
# Erster Alarm wird protokolliert + Sperre gesetzt; ohne nutzbaren Kanal aber
# ehrlich als "no_recipients" gemeldet (niemand wurde erreicht), nicht "alerted".
assert res["status"] == "no_recipients"
assert res["notified"] is False
assert res["notify_ready"]["any"] is False
assert res["cooldown_seconds"] == 300
assert store.logged == 1

View file

@ -41,12 +41,16 @@ def test_quota_unlimited_by_default(monkeypatch):
# --- Manueller Notruf (vom Nutzer ausgelöst) -------------------------------
def test_manual_emergency_logs_and_returns_localized_notice():
# Ohne konfigurierten Kanal (Default: kein SMTP/Webhook) -> ehrlicher Status
# no_recipients mit handlungsleitendem Hinweis (Selbsthilfe 112).
resp = client.post("/api/emergency", json={"language": "de"})
assert resp.status_code == 200
data = resp.json()
assert data["category"] == "manual"
assert "noch keine Benachrichtigung" in data["notice"]
# Notruf wurde protokolliert
assert data["status"] == "no_recipients"
assert data["notified"] is False
assert "112" in data["notice"]
# Notruf wurde trotzdem protokolliert
events = deps.get_store().list_emergency_events(10)
assert any(e["category"] == "manual" for e in events)
@ -54,11 +58,11 @@ def test_manual_emergency_logs_and_returns_localized_notice():
def test_manual_emergency_notice_language_fallback(monkeypatch):
# Sperre aus, damit beide Auslösungen unabhängig durchgehen (nicht unterdrückt).
monkeypatch.setattr(settings, "emergency_cooldown_minutes", 0)
# Unbekannte Sprache -> deutscher Fallback-Hinweis
# Englisch -> englischer Hinweis; unbekannte Sprache -> deutscher Fallback.
en = client.post("/api/emergency", json={"language": "en"}).json()["notice"]
assert "ATTENTION" in en
assert "notified" in en.lower()
xx = client.post("/api/emergency", json={"language": "xx"}).json()["notice"]
assert "ACHTUNG" in xx
assert "niemand benachrichtigt" in xx
def test_manual_emergency_does_not_count_against_quota(monkeypatch):