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

@ -2011,31 +2011,56 @@ unzuverlässig (Fehlalarme **wie** verpasste Notfälle) und intransparent. Ein N
- Der Notruf wird protokolliert (Admin → System → **Notrufe**); der Nutzer erhält einen
klaren Hinweis in seiner Sprache.
**Benachrichtigung (Testphase):** Ist SMTP konfiguriert, geht beim Notruf eine **E-Mail**
an die Kontaktperson(en) (`EMERGENCY_CONTACT_EMAIL`). Ohne SMTP wird nichts versendet und
der Nutzer sieht „ACHTUNG: Es ist noch keine Benachrichtigung eingebaut.". SMS/Anruf folgen
später.
**Benachrichtigung — zwei unabhängige Kanäle (best effort):**
1. **E-Mail (SMTP):** Ist `SMTP_HOST` gesetzt, geht eine E-Mail an die Kontaktperson(en)
(`EMERGENCY_CONTACT_EMAIL`; pro Nutzer übersteuerbar via Pref `emergency_contacts`).
2. **SMS/Anruf (provider-agnostischer Webhook):** Ist `EMERGENCY_WEBHOOK_URL` gesetzt, geht
zusätzlich ein JSON-POST dorthin — mit den angefragten Kanälen, den Telefonnummern
(`EMERGENCY_CONTACT_PHONE`; pro Nutzer via Pref `emergency_phones`) und **fertigen
SMS-/Anruf-Texten**. Den konkreten Gateway (Twilio, seven.io, sipgate, eigener Dienst …)
verdrahtet man hinter dem Webhook; `EMERGENCY_WEBHOOK_TOKEN` schützt ihn optional per
`Authorization: Bearer <token>`.
Geht über **mindestens einen** Kanal etwas raus, sieht der Nutzer „Hilfe wurde verständigt …",
sonst „ACHTUNG: Es ist noch keine Benachrichtigung eingebaut.".
```bash
EMERGENCY_CONTACT_EMAIL=dieter.schlueter@linix.de # Default-Kontaktperson
SMTP_HOST= # ohne Host kein Versand — KEINE Inline-Kommentare hinter Werten!
EMERGENCY_CONTACT_EMAIL=dieter.schlueter@linix.de # Default-Kontaktperson (E-Mail)
SMTP_HOST= # ohne Host kein E-Mail-Versand — KEINE Inline-Kommentare hinter Werten!
SMTP_PORT=587
SMTP_USER=
SMTP_PASSWORD=
SMTP_FROM= # muss dem SMTP_USER gehören (sonst 553 Sender rejected)
SMTP_STARTTLS=true
EMERGENCY_WEBHOOK_URL= # optional, für spätere Eskalation
EMERGENCY_WEBHOOK_URL= # SMS/Anruf-Eskalation: JSON-POST hierhin (leer = aus)
EMERGENCY_WEBHOOK_TOKEN= # optional -> Authorization: Bearer <token>
EMERGENCY_CONTACT_PHONE= # Default-Telefonnummer(n), CSV, E.164 (+49…)
EMERGENCY_WEBHOOK_CHANNELS=sms,call # angefragte Kanäle im Payload
```
Das POST-Payload, das der Gateway-Empfänger bekommt:
```json
{
"event": "emergency", "category": "manual", "source": "voice-assistant",
"timestamp": "2026-06-25T…Z", "language": "de",
"user": {"id": "…", "name": "…"},
"channels": ["sms", "call"],
"phones": ["+4915112345678"],
"message": {"sms": "NOTRUF: …", "call": "Achtung. …"}
}
```
Auslösen per API (z. B. zum Testen):
```bash
curl -s -X POST $URL/api/emergency -H 'Content-Type: application/json' \
-d '{"language":"de"}' | jq
# → {"category":"manual","notice":"…","email_sent":true|false}
# → {"category":"manual","notice":"…","email_sent":…,"webhook_sent":…,"channels":[…]}
```
> ⚠️ **Wichtig:** Der Notruf ist **kein Ersatz** für einen echten Rettungsdienst. In der
> Testphase ist die Zustellung nur E-Mail (best effort, abhängig vom Mailserver).
> ⚠️ **Wichtig:** Der Notruf ist **kein Ersatz** für einen echten Rettungsdienst. Die
> Zustellung ist best effort und hängt vom Mailserver bzw. dem hinter dem Webhook
> verdrahteten SMS/Anruf-Gateway ab.
---
@ -2295,7 +2320,10 @@ Logs: Terminal von `make run`. Mehr Details: `LOG_LEVEL=debug` in `.env`.
| `SMTP_USER` / `SMTP_PASSWORD` | (leer) | SMTP-Zugangsdaten |
| `SMTP_FROM` | (leer → `SMTP_USER`) | Absender (muss dem SMTP_USER gehören) |
| `SMTP_STARTTLS` | `true` | STARTTLS verwenden |
| `EMERGENCY_WEBHOOK_URL` | (leer) | optionaler Webhook (spätere Eskalation) |
| `EMERGENCY_WEBHOOK_URL` | (leer) | SMS/Anruf-Eskalation: JSON-POST hierhin (leer = aus) |
| `EMERGENCY_WEBHOOK_TOKEN` | (leer) | optional → `Authorization: Bearer <token>` für den Webhook |
| `EMERGENCY_CONTACT_PHONE` | (leer) | Default-Telefonnummer(n) für SMS/Anruf, CSV, E.164 |
| `EMERGENCY_WEBHOOK_CHANNELS` | `sms,call` | im Payload angefragte Kanäle (CSV) |
---