feat: Notruf-Eskalation per SMS/Anruf (provider-agnostischer Webhook)

Zweiter Benachrichtigungskanal neben der E-Mail: bei einem Notruf geht
zusätzlich ein JSON-POST an EMERGENCY_WEBHOOK_URL mit Kanälen (sms,call),
Kontakt-Telefonnummern und fertigen SMS-/Anruf-Texten. Den konkreten
Gateway (Twilio, seven.io, sipgate, eigener Dienst) verdrahtet der
Empfänger dahinter — kein Provider-Lock-in im Code.

- config: EMERGENCY_WEBHOOK_URL aktiviert + _TOKEN (Bearer), _CONTACT_PHONE
  (CSV/E.164), _WEBHOOK_CHANNELS (default sms,call)
- emergency.py: async httpx-POST (best effort), Telefon-Auflösung analog
  zur E-Mail (Pref emergency_phones > Default), notified = email OR webhook;
  Hinweise kanal-neutral formuliert; Response um webhook_sent + channels ergänzt
- Tests: Webhook-Versand inkl. Payload/Bearer-Header + "ohne URL kein POST"
- Doku: §10 + Env-Var-Tabelle aktualisiert (zwei Kanäle, Payload-Format)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-06-25 10:17:49 +02:00
commit 1816a26734
4 changed files with 209 additions and 38 deletions

View file

@ -188,9 +188,7 @@ class Settings(BaseSettings):
llm_fallback: str = ""
tts_fallback: str = ""
daily_request_limit: int = 0 # 0 = unbegrenzt; Anfragen pro Nutzer pro Tag
emergency_webhook_url: str = "" # optionaler Eskalations-Webhook (später)
# Notruf-Benachrichtigung per E-Mail (Testphase, bis SMS/Anruf gebaut sind).
# Notruf-Benachrichtigung per E-Mail.
emergency_contact_email: str = "dieter.schlueter@linix.de" # Default-Kontaktperson
smtp_host: str = "" # ohne Host wird KEINE E-Mail versendet
smtp_port: int = 587
@ -198,6 +196,15 @@ class Settings(BaseSettings):
smtp_password: str = ""
smtp_from: str = "" # Absenderadresse; leer -> smtp_user
smtp_starttls: bool = True
# Notruf-Eskalation per SMS/Anruf: provider-agnostischer Webhook. Bei einem Notruf
# geht ein JSON-POST an emergency_webhook_url (Kanäle, Telefonnummern, fertige
# SMS-/Anruf-Texte). Den konkreten Gateway (Twilio, seven.io, …) verdrahtet der
# Empfänger dahinter. Ohne URL wird nichts gesendet.
emergency_webhook_url: str = ""
emergency_webhook_token: str = "" # optional -> Authorization: Bearer <token>
emergency_contact_phone: str = "" # Default-Telefonnummer(n), CSV, E.164 (+49…)
emergency_webhook_channels: str = "sms,call" # angefragte Kanäle (CSV)
model_config = SettingsConfigDict(
env_file=ENV_FILE, case_sensitive=False, extra="ignore"
)