diff --git a/BEDIENUNGSANLEITUNG.md b/BEDIENUNGSANLEITUNG.md index a74b4da..8a66864 100644 --- a/BEDIENUNGSANLEITUNG.md +++ b/BEDIENUNGSANLEITUNG.md @@ -1798,6 +1798,13 @@ Nutzer anlegen (Name eingeben → „Anlegen" → Token erscheint **einmalig** umbenennen, Token zurücksetzen und löschen. Erinnerungen je Nutzer auf- und zuklappen, neue Erinnerungen hinzufügen oder vorhandene löschen. +Im **Notfall-Profil** jedes Nutzers (aufklappbar) stehen Kontaktdaten, med. Hinweise, +„Standort im Notfall mitsenden" und **„Pause bis zum erneuten Alarm"** (Minuten): nach +einem ausgelösten Notruf ist ein erneuter Alarm so lange blockiert (Schutz vor +Mehrfach-Alarmen). **Leer** = der globale Standard greift (Tab *Konfiguration › +Einstellungen* → „Notruf-Sperre (Minuten)", Default 5), **0** = aus. Präzedenz: +Nutzer-Wert > globale Einstellung > 5. + #### Nutzer › Gespräche Nutzerliste links → Session auswählen → Gesprächs-Transkript als Chat-Bubbles ansehen. diff --git a/app/api/admin.py b/app/api/admin.py index 72fec12..c501b25 100644 --- a/app/api/admin.py +++ b/app/api/admin.py @@ -205,7 +205,7 @@ async def list_users(): **{k: (u.prefs or {}).get(k, "") for k in ( "full_name", "street", "postal_code", "city", "phone_landline", "phone_mobile", "birth_date", "medical_notes", - "emergency_contacts", "emergency_phones", + "emergency_contacts", "emergency_phones", "emergency_cooldown_minutes", )}, "location_consent": bool((u.prefs or {}).get("location_consent", False)), "is_admin": is_admin_user(u), diff --git a/app/safety/emergency.py b/app/safety/emergency.py index ef57459..1a0d7fb 100644 --- a/app/safety/emergency.py +++ b/app/safety/emergency.py @@ -270,20 +270,34 @@ async def _post_webhook(url: str, payload: dict, cfg) -> bool: return False -def _cooldown_seconds(cfg) -> int: +def _resolve_cooldown_minutes(user, cfg) -> int: + """Alarmpause in Minuten. Präzedenz: Nutzer-Pref > globale Einstellung > 5. + Leerer/fehlender Pref-Wert erbt die globale Einstellung; "0" bedeutet aus. + """ + raw = (getattr(user, "prefs", None) or {}).get("emergency_cooldown_minutes") + if raw is not None and str(raw).strip() != "": + try: + return max(0, int(raw)) + except (TypeError, ValueError): + pass try: - return max(0, int(getattr(cfg, "emergency_cooldown_minutes", 5)) * 60) + return max(0, int(getattr(cfg, "emergency_cooldown_minutes", 5))) except (TypeError, ValueError): - return 300 + return 5 + + +def _cooldown_seconds(user, cfg) -> int: + return _resolve_cooldown_minutes(user, cfg) * 60 def emergency_cooldown_status(user, store, cfg=settings) -> dict: """Aktueller Sperr-Status: ist nach dem letzten manuellen Notruf noch eine Wiederauslöse-Sperre aktiv, wie lange ist er her, wie viel bleibt. - Schützt verwirrte/verängstigte Senioren vor Mehrfach-Alarmen. + Schützt verwirrte/verängstigte Senioren vor Mehrfach-Alarmen. Die Pause ist + pro Nutzer einstellbar (sonst greift die globale Einstellung). """ - total = _cooldown_seconds(cfg) + total = _cooldown_seconds(user, cfg) last = store.last_manual_emergency_at(user.id) if total > 0 else None base = {"active": False, "cooldown_seconds": total, "remaining_seconds": 0, "minutes_ago": 0} if not last: diff --git a/app/schemas.py b/app/schemas.py index ac7f96c..ec6ab5f 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -174,6 +174,9 @@ class AdminUserPrefsUpdate(BaseModel): emergency_contacts: str | None = None # Kontakt-E-Mail(s), CSV emergency_phones: str | None = None # Kontakt-Telefon(e), CSV location_consent: bool | None = None # Standort im Notfall mitsenden? + # Alarmpause pro Nutzer (ganze Minuten, als String). Leer = globaler Standard, + # "0" = aus. Präzedenz: dieser Wert > globale Einstellung > 5. + emergency_cooldown_minutes: str | None = None @field_validator("postal_code") @classmethod @@ -210,6 +213,18 @@ class AdminUserPrefsUpdate(BaseModel): raise ValueError("Kontakt-Telefon(e): ungültige Nummer") return v + @field_validator("emergency_cooldown_minutes") + @classmethod + def _check_cooldown(cls, v): + if v is None or str(v).strip() == "": + return v # leer = globaler Standard (erben) + s = str(v).strip() + if not s.isdigit(): + raise ValueError("Pause: nur ganze Minuten (0–60)") + if int(s) > 60: + raise ValueError("Pause: höchstens 60 Minuten") + return s + class UserAdminUpdate(BaseModel): """Admin schaltet Admin-Rechte für einen Nutzer ein/aus.""" diff --git a/app/web/app.js b/app/web/app.js index 9f0e6dc..93783f9 100644 --- a/app/web/app.js +++ b/app/web/app.js @@ -368,6 +368,7 @@ const PROF_VALIDATORS = { birth_date: { ok: _isGermanDate, msg: "Geburtsdatum: TT.MM.JJJJ" }, emergency_contacts: { ok: (v) => _csvAll(v, _isEmail), msg: "Kontakt-E-Mail(s): gültige Adressen (Komma-getrennt)" }, emergency_phones: { ok: (v) => _csvAll(v, _isPhone), msg: "Kontakt-Telefon(e): gültige Nummern (Komma-getrennt)" }, + emergency_cooldown_minutes: { ok: (v) => /^\d+$/.test(v) && +v <= 60, msg: "Pause: 0–60 ganze Minuten" }, }; // Wendet die vom Admin erlaubten Sprachen auf das Sprachmenü an. Leer = alle erlaubt. @@ -1191,6 +1192,13 @@ function buildUserCard(u) { + +
leer = globaler Standard · 0 = aus
@@ -1241,7 +1249,8 @@ function buildUserCard(u) { // Notfall-Profil — Admin pflegt Kontaktdaten + med. Hinweise + Standort-Freigabe. const PROF_KEYS = ["full_name", "street", "postal_code", "city", "phone_mobile", "phone_landline", "birth_date", "medical_notes", - "emergency_contacts", "emergency_phones"]; + "emergency_contacts", "emergency_phones", + "emergency_cooldown_minutes"]; PROF_KEYS.forEach((k) => { const el = card.querySelector(`.prof-f[data-k="${k}"]`); if (el) el.value = u[k] || ""; diff --git a/app/web/index.html b/app/web/index.html index 0f4e266..27510af 100644 --- a/app/web/index.html +++ b/app/web/index.html @@ -411,6 +411,6 @@ - +