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:
Dieter Schlüter 2026-06-26 15:58:17 +02:00
commit 8c152ee77c
10 changed files with 290 additions and 22 deletions

View file

@ -148,6 +148,9 @@ class Store(ABC):
@abstractmethod
def list_emergency_events(self, limit: int = 50) -> list[dict]: ...
@abstractmethod
def last_manual_emergency_at(self, user_id: str) -> str | None: ...
@abstractmethod
def get_usage_for_user(self, user_id: str) -> list[dict]: ...
@ -561,6 +564,17 @@ class SQLiteStore(Store):
).fetchall()
return [dict(r) for r in rows]
def last_manual_emergency_at(self, user_id: str) -> str | None:
"""ISO-Zeitstempel des letzten vom Nutzer ausgelösten Notrufs (oder None)."""
with self._connect() as conn:
row = conn.execute(
"SELECT created_at FROM emergency_events"
" WHERE user_id = ? AND category = 'manual'"
" ORDER BY id DESC LIMIT 1",
(user_id,),
).fetchone()
return row["created_at"] if row else None
def get_usage_for_user(self, user_id: str) -> list[dict]:
with self._connect() as conn:
rows = conn.execute(