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
89
tests/test_emergency_cooldown.py
Normal file
89
tests/test_emergency_cooldown.py
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
"""Wiederauslöse-Sperre (Cooldown) für den manuellen Notruf.
|
||||
|
||||
Nach einem Alarm ist ein erneuter Alarm für emergency_cooldown_minutes blockiert
|
||||
(Schutz vor Mehrfach-Alarmen durch verwirrte Senioren).
|
||||
"""
|
||||
import asyncio
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
import app.safety.emergency as em
|
||||
from app.config import settings
|
||||
from app.main import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
class FakeUser:
|
||||
id = "u-cooldown"
|
||||
display_name = "Test"
|
||||
prefs: dict = {}
|
||||
|
||||
|
||||
class FakeStore:
|
||||
def __init__(self, last=None):
|
||||
self._last = last
|
||||
self.logged = 0
|
||||
|
||||
def log_emergency(self, *a, **k):
|
||||
self.logged += 1
|
||||
self._last = datetime.now(timezone.utc).isoformat()
|
||||
|
||||
def last_manual_emergency_at(self, user_id):
|
||||
return self._last
|
||||
|
||||
|
||||
def _iso_ago(seconds: int) -> str:
|
||||
return (datetime.now(timezone.utc) - timedelta(seconds=seconds)).isoformat()
|
||||
|
||||
|
||||
def test_status_active_within_window(monkeypatch):
|
||||
monkeypatch.setattr(em.settings, "emergency_cooldown_minutes", 5)
|
||||
s = em.emergency_cooldown_status(FakeUser(), FakeStore(last=_iso_ago(60)))
|
||||
assert s["active"] is True
|
||||
assert s["minutes_ago"] == 1
|
||||
assert 0 < s["remaining_seconds"] <= 300
|
||||
|
||||
|
||||
def test_status_expired(monkeypatch):
|
||||
monkeypatch.setattr(em.settings, "emergency_cooldown_minutes", 5)
|
||||
s = em.emergency_cooldown_status(FakeUser(), FakeStore(last=_iso_ago(600)))
|
||||
assert s["active"] is False
|
||||
assert s["remaining_seconds"] == 0
|
||||
|
||||
|
||||
def test_status_disabled_when_zero(monkeypatch):
|
||||
monkeypatch.setattr(em.settings, "emergency_cooldown_minutes", 0)
|
||||
s = em.emergency_cooldown_status(FakeUser(), FakeStore(last=_iso_ago(1)))
|
||||
assert s["active"] is False
|
||||
|
||||
|
||||
def test_second_alarm_suppressed(monkeypatch):
|
||||
monkeypatch.setattr(em.settings, "emergency_cooldown_minutes", 5)
|
||||
monkeypatch.setattr(em, "_send_email_sync", lambda *a, **k: True) # falls doch versucht
|
||||
store = FakeStore(last=_iso_ago(30))
|
||||
res = asyncio.run(em.record_manual_emergency(FakeUser(), store, language="de"))
|
||||
assert res["status"] == "cooldown"
|
||||
assert res["minutes_ago"] == 0
|
||||
assert store.logged == 0 # KEIN neuer Eintrag, KEIN zweiter Alarm
|
||||
|
||||
|
||||
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, "emergency_webhook_url", "")
|
||||
store = FakeStore(last=None)
|
||||
res = asyncio.run(em.record_manual_emergency(FakeUser(), store, language="de"))
|
||||
assert res["status"] == "alerted"
|
||||
assert res["cooldown_seconds"] == 300
|
||||
assert store.logged == 1
|
||||
|
||||
|
||||
def test_status_endpoint(monkeypatch):
|
||||
# Endpunkt-Verdrahtung: nach einem Alarm meldet /status active=True.
|
||||
monkeypatch.setattr(settings, "emergency_cooldown_minutes", 5)
|
||||
client.post("/api/emergency", json={"language": "de"})
|
||||
st = client.get("/api/emergency/status").json()
|
||||
assert st["active"] is True
|
||||
assert st["cooldown_seconds"] == 300
|
||||
|
|
@ -99,6 +99,9 @@ def test_record_manual_emergency_uses_profile(monkeypatch):
|
|||
def log_emergency(self, *a, **k):
|
||||
pass
|
||||
|
||||
def last_manual_emergency_at(self, user_id):
|
||||
return None # kein vorheriger Notruf -> keine Sperre aktiv
|
||||
|
||||
res = asyncio.run(
|
||||
em.record_manual_emergency(FakeUser(FULL_PREFS), DummyStore(), language="de", geo=GEO)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -51,7 +51,9 @@ def test_manual_emergency_logs_and_returns_localized_notice():
|
|||
assert any(e["category"] == "manual" for e in events)
|
||||
|
||||
|
||||
def test_manual_emergency_notice_language_fallback():
|
||||
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
|
||||
en = client.post("/api/emergency", json={"language": "en"}).json()["notice"]
|
||||
assert "ATTENTION" in en
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue