2026-06-25 03:02:52 +02:00
|
|
|
"""Manuell ausgelöster Notruf — vom Nutzer bewusst bestätigt.
|
2026-06-17 05:29:30 +02:00
|
|
|
|
2026-06-25 03:37:18 +02:00
|
|
|
KEINE automatische Erkennung (zu unzuverlässig/intransparent). Ein Notruf
|
|
|
|
|
entsteht nur durch eine ausdrückliche Nutzeraktion (Knopf + Bestätigung).
|
2026-06-18 03:24:25 +02:00
|
|
|
|
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>
2026-06-25 10:17:49 +02:00
|
|
|
Benachrichtigung von Kontaktpersonen über zwei unabhängige Kanäle (best effort):
|
|
|
|
|
- E-Mail per SMTP (settings.smtp_*, Empfänger settings.emergency_contact_email).
|
2026-06-25 17:24:36 +02:00
|
|
|
- SMS/Anruf per provider-agnostischem Webhook (settings.emergency_webhook_url).
|
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>
2026-06-25 10:17:49 +02:00
|
|
|
|
feat: Notruf mit Kontaktdaten + Geoposition anreichern (Backend, Phase 1+3)
Notruf-Meldungen tragen jetzt das Notfall-Profil der auslösenden Person und —
falls vom Gerät mitgeschickt — die Geoposition. Kanal-Rollen:
- E-Mail: Voll-Dossier (Name, Adresse, Telefone, Geburtsdatum, med. Hinweise,
Koordinaten + Karten-Link + Genauigkeit + Zeit; sonst "nicht verfügbar")
- SMS: kompakt, Name + Adresse + Karten-Link
- Anruf (TTS): Name + Wohnadresse gesprochen, KEINE URL/Koordinaten, Verweis
auf SMS/E-Mail
- schemas: GeoFix + EmergencyRequest.geo; AdminUserPrefsUpdate um Profilfelder
erweitert (full_name, Adresse, Telefone, birth_date, medical_notes,
emergency_contacts/_phones, location_consent) — in user.prefs (keine Migration)
- api/chat: geo an record_manual_emergency durchgereicht
- api/admin: list_users gibt Profilfelder mit zurück (für die spätere Admin-UI)
- emergency.py: _profile/_geo_fields/_maps_link/_location_block/_email_body,
kanal-spezifische _emergency_texts; record_manual_emergency(..., geo=...)
- Tests: 9 neue (Profil-Injektion, SMS/Call/E-Mail-Texte, Geo→Map-Link,
ohne-Geo-Fallback, Payload). Suite 237 grün.
Frontend (Phase 2: Browser-Geolocation beim 🆘 + Admin-Profil-UI) steht noch aus.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 15:33:58 +02:00
|
|
|
Die Meldungen werden mit den Profildaten der auslösenden Person (Name, Adresse,
|
|
|
|
|
Telefon, med. Hinweise) und — falls vom Gerät mitgeschickt — der Geoposition
|
|
|
|
|
angereichert. Kanal-Rollen: die E-Mail trägt das Voll-Dossier, die SMS den
|
|
|
|
|
Karten-Link, der Anruf nennt Name + Wohnadresse gesprochen und verweist für den
|
2026-06-25 17:24:36 +02:00
|
|
|
genauen Standort auf SMS/E-Mail (Koordinaten werden NICHT vorgelesen). Schlägt die
|
|
|
|
|
Geo-Erfassung fehl, wird der Grund (geo_error) in E-Mail und Log ausgewiesen.
|
2026-06-17 05:29:30 +02:00
|
|
|
"""
|
|
|
|
|
|
2026-06-25 03:37:18 +02:00
|
|
|
import asyncio
|
|
|
|
|
import logging
|
|
|
|
|
import smtplib
|
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>
2026-06-25 10:17:49 +02:00
|
|
|
from datetime import datetime, timezone
|
2026-06-25 03:37:18 +02:00
|
|
|
from email.message import EmailMessage
|
|
|
|
|
|
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>
2026-06-25 10:17:49 +02:00
|
|
|
import httpx
|
|
|
|
|
|
2026-06-25 03:37:18 +02:00
|
|
|
from app.config import settings
|
2026-06-17 05:29:30 +02:00
|
|
|
from app.metrics import metrics
|
|
|
|
|
|
2026-06-25 03:37:18 +02:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
# Hinweis je Sprache, wenn (noch) KEINE Benachrichtigung rausging.
|
2026-06-25 03:02:52 +02:00
|
|
|
_NO_NOTIFY_NOTICE: dict[str, str] = {
|
|
|
|
|
"de": "ACHTUNG: Es ist noch keine Benachrichtigung eingebaut.",
|
|
|
|
|
"en": "ATTENTION: No notification has been set up yet.",
|
|
|
|
|
"fr": "ATTENTION : aucune notification n'est encore configurée.",
|
|
|
|
|
"es": "ATENCIÓN: todavía no se ha configurado ninguna notificación.",
|
|
|
|
|
"it": "ATTENZIONE: non è ancora stata configurata alcuna notifica.",
|
|
|
|
|
"pt": "ATENÇÃO: ainda não há nenhuma notificação configurada.",
|
|
|
|
|
"pl": "UWAGA: Nie skonfigurowano jeszcze żadnego powiadomienia.",
|
|
|
|
|
"ar": "تنبيه: لم يتم إعداد أي إشعار بعد.",
|
|
|
|
|
"ru": "ВНИМАНИЕ: Уведомление пока не настроено.",
|
|
|
|
|
"zh": "注意:尚未设置任何通知。",
|
2026-06-17 05:29:30 +02:00
|
|
|
}
|
|
|
|
|
|
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>
2026-06-25 10:17:49 +02:00
|
|
|
# Hinweis je Sprache, wenn mindestens ein Kanal die Meldung rausgeschickt hat.
|
2026-06-25 03:37:18 +02:00
|
|
|
_SENT_NOTICE: dict[str, str] = {
|
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>
2026-06-25 10:17:49 +02:00
|
|
|
"de": "Hilfe wurde verständigt. Eine Kontaktperson wurde benachrichtigt.",
|
|
|
|
|
"en": "Help has been alerted. A contact person was notified.",
|
|
|
|
|
"fr": "Les secours ont été alertés. Un proche a été prévenu.",
|
|
|
|
|
"es": "Se ha avisado a ayuda. Se notificó a un contacto.",
|
|
|
|
|
"it": "L'aiuto è stato allertato. Una persona di contatto è stata avvisata.",
|
|
|
|
|
"pt": "A ajuda foi avisada. Uma pessoa de contato foi notificada.",
|
|
|
|
|
"pl": "Pomoc została powiadomiona. Osoba kontaktowa została powiadomiona.",
|
|
|
|
|
"ar": "تم تنبيه المساعدة. تم إخطار شخص الاتصال.",
|
|
|
|
|
"ru": "Помощь оповещена. Контактное лицо уведомлено.",
|
|
|
|
|
"zh": "已通知求助。已通知联系人。",
|
2026-06-25 03:37:18 +02:00
|
|
|
}
|
|
|
|
|
|
2026-06-17 05:29:30 +02:00
|
|
|
|
2026-06-25 03:37:18 +02:00
|
|
|
def notify_notice(language: str | None, sent: bool) -> str:
|
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>
2026-06-25 10:17:49 +02:00
|
|
|
"""Hinweis in Nutzersprache (Fallback Deutsch), je nachdem ob etwas rausging."""
|
2026-06-25 03:37:18 +02:00
|
|
|
table = _SENT_NOTICE if sent else _NO_NOTIFY_NOTICE
|
|
|
|
|
return table.get((language or "de").lower(), table["de"])
|
2026-06-18 03:24:25 +02:00
|
|
|
|
|
|
|
|
|
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>
2026-06-25 10:17:49 +02:00
|
|
|
def _csv(raw: str | None) -> list[str]:
|
|
|
|
|
"""Trennt eine CSV-/Semikolon-Liste in bereinigte Einzelwerte."""
|
|
|
|
|
return [v.strip() for v in (raw or "").replace(";", ",").split(",") if v.strip()]
|
|
|
|
|
|
|
|
|
|
|
2026-06-25 17:24:36 +02:00
|
|
|
# --- Profil, Standort, Gerät ----------------------------------------------
|
feat: Notruf mit Kontaktdaten + Geoposition anreichern (Backend, Phase 1+3)
Notruf-Meldungen tragen jetzt das Notfall-Profil der auslösenden Person und —
falls vom Gerät mitgeschickt — die Geoposition. Kanal-Rollen:
- E-Mail: Voll-Dossier (Name, Adresse, Telefone, Geburtsdatum, med. Hinweise,
Koordinaten + Karten-Link + Genauigkeit + Zeit; sonst "nicht verfügbar")
- SMS: kompakt, Name + Adresse + Karten-Link
- Anruf (TTS): Name + Wohnadresse gesprochen, KEINE URL/Koordinaten, Verweis
auf SMS/E-Mail
- schemas: GeoFix + EmergencyRequest.geo; AdminUserPrefsUpdate um Profilfelder
erweitert (full_name, Adresse, Telefone, birth_date, medical_notes,
emergency_contacts/_phones, location_consent) — in user.prefs (keine Migration)
- api/chat: geo an record_manual_emergency durchgereicht
- api/admin: list_users gibt Profilfelder mit zurück (für die spätere Admin-UI)
- emergency.py: _profile/_geo_fields/_maps_link/_location_block/_email_body,
kanal-spezifische _emergency_texts; record_manual_emergency(..., geo=...)
- Tests: 9 neue (Profil-Injektion, SMS/Call/E-Mail-Texte, Geo→Map-Link,
ohne-Geo-Fallback, Payload). Suite 237 grün.
Frontend (Phase 2: Browser-Geolocation beim 🆘 + Admin-Profil-UI) steht noch aus.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 15:33:58 +02:00
|
|
|
|
|
|
|
|
def _profile(user) -> dict:
|
|
|
|
|
"""Liest die Notfall-Profildaten der Person aus user.prefs (mit Fallbacks)."""
|
|
|
|
|
prefs = getattr(user, "prefs", None)
|
|
|
|
|
prefs = prefs if isinstance(prefs, dict) else {}
|
|
|
|
|
|
|
|
|
|
def g(key: str) -> str:
|
|
|
|
|
v = prefs.get(key)
|
|
|
|
|
return v.strip() if isinstance(v, str) else ""
|
|
|
|
|
|
|
|
|
|
name = g("full_name") or getattr(user, "display_name", "") or getattr(user, "id", "?")
|
|
|
|
|
street, postal, city = g("street"), g("postal_code"), g("city")
|
|
|
|
|
city_line = " ".join(p for p in (postal, city) if p)
|
|
|
|
|
address_short = ", ".join(p for p in (street, city_line) if p)
|
|
|
|
|
return {
|
|
|
|
|
"name": name,
|
|
|
|
|
"street": street,
|
|
|
|
|
"postal_code": postal,
|
|
|
|
|
"city": city,
|
|
|
|
|
"address_short": address_short,
|
|
|
|
|
"phone_landline": g("phone_landline"),
|
|
|
|
|
"phone_mobile": g("phone_mobile"),
|
|
|
|
|
"birth_date": g("birth_date"),
|
|
|
|
|
"medical_notes": g("medical_notes"),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _geo_fields(geo) -> dict | None:
|
2026-06-25 17:24:36 +02:00
|
|
|
"""Normalisiert die Geoposition (pydantic-Model ODER dict) auf {lat,lon,accuracy,ts}."""
|
feat: Notruf mit Kontaktdaten + Geoposition anreichern (Backend, Phase 1+3)
Notruf-Meldungen tragen jetzt das Notfall-Profil der auslösenden Person und —
falls vom Gerät mitgeschickt — die Geoposition. Kanal-Rollen:
- E-Mail: Voll-Dossier (Name, Adresse, Telefone, Geburtsdatum, med. Hinweise,
Koordinaten + Karten-Link + Genauigkeit + Zeit; sonst "nicht verfügbar")
- SMS: kompakt, Name + Adresse + Karten-Link
- Anruf (TTS): Name + Wohnadresse gesprochen, KEINE URL/Koordinaten, Verweis
auf SMS/E-Mail
- schemas: GeoFix + EmergencyRequest.geo; AdminUserPrefsUpdate um Profilfelder
erweitert (full_name, Adresse, Telefone, birth_date, medical_notes,
emergency_contacts/_phones, location_consent) — in user.prefs (keine Migration)
- api/chat: geo an record_manual_emergency durchgereicht
- api/admin: list_users gibt Profilfelder mit zurück (für die spätere Admin-UI)
- emergency.py: _profile/_geo_fields/_maps_link/_location_block/_email_body,
kanal-spezifische _emergency_texts; record_manual_emergency(..., geo=...)
- Tests: 9 neue (Profil-Injektion, SMS/Call/E-Mail-Texte, Geo→Map-Link,
ohne-Geo-Fallback, Payload). Suite 237 grün.
Frontend (Phase 2: Browser-Geolocation beim 🆘 + Admin-Profil-UI) steht noch aus.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 15:33:58 +02:00
|
|
|
if geo is None:
|
|
|
|
|
return None
|
|
|
|
|
if isinstance(geo, dict):
|
|
|
|
|
d = geo
|
|
|
|
|
else:
|
|
|
|
|
d = {k: getattr(geo, k, None) for k in ("lat", "lon", "accuracy", "ts")}
|
|
|
|
|
if d.get("lat") is None or d.get("lon") is None:
|
|
|
|
|
return None
|
|
|
|
|
return {"lat": d["lat"], "lon": d["lon"], "accuracy": d.get("accuracy"), "ts": d.get("ts")}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _maps_link(lat, lon) -> str:
|
|
|
|
|
return f"https://www.google.com/maps?q={lat},{lon}"
|
|
|
|
|
|
|
|
|
|
|
2026-06-25 17:24:36 +02:00
|
|
|
def _person_phones(profile: dict) -> str:
|
|
|
|
|
"""Telefonnummer(n) der Person für die Meldung (Mobil / Festnetz)."""
|
|
|
|
|
return " / ".join(t for t in (profile["phone_mobile"], profile["phone_landline"]) if t)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _device_line(device) -> str | None:
|
|
|
|
|
"""Beschriftet das auslösende Gerät (Mobilgerät/Festgerät + Plattform), falls bekannt."""
|
|
|
|
|
if not device:
|
|
|
|
|
return None
|
|
|
|
|
if isinstance(device, dict):
|
|
|
|
|
mobile, platform = device.get("mobile"), device.get("platform")
|
|
|
|
|
else:
|
|
|
|
|
mobile, platform = getattr(device, "mobile", None), getattr(device, "platform", None)
|
|
|
|
|
kind = "Mobilgerät" if mobile else ("Festgerät" if mobile is False else "Gerät")
|
|
|
|
|
return f"Auslösegerät: {kind}" + (f" ({platform})" if platform else "")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _location_block(geo, geo_error: str | None = None) -> str:
|
|
|
|
|
"""Standort-Block für die E-Mail (Koordinaten + Karte, oder Grund der Nicht-Verfügbarkeit)."""
|
feat: Notruf mit Kontaktdaten + Geoposition anreichern (Backend, Phase 1+3)
Notruf-Meldungen tragen jetzt das Notfall-Profil der auslösenden Person und —
falls vom Gerät mitgeschickt — die Geoposition. Kanal-Rollen:
- E-Mail: Voll-Dossier (Name, Adresse, Telefone, Geburtsdatum, med. Hinweise,
Koordinaten + Karten-Link + Genauigkeit + Zeit; sonst "nicht verfügbar")
- SMS: kompakt, Name + Adresse + Karten-Link
- Anruf (TTS): Name + Wohnadresse gesprochen, KEINE URL/Koordinaten, Verweis
auf SMS/E-Mail
- schemas: GeoFix + EmergencyRequest.geo; AdminUserPrefsUpdate um Profilfelder
erweitert (full_name, Adresse, Telefone, birth_date, medical_notes,
emergency_contacts/_phones, location_consent) — in user.prefs (keine Migration)
- api/chat: geo an record_manual_emergency durchgereicht
- api/admin: list_users gibt Profilfelder mit zurück (für die spätere Admin-UI)
- emergency.py: _profile/_geo_fields/_maps_link/_location_block/_email_body,
kanal-spezifische _emergency_texts; record_manual_emergency(..., geo=...)
- Tests: 9 neue (Profil-Injektion, SMS/Call/E-Mail-Texte, Geo→Map-Link,
ohne-Geo-Fallback, Payload). Suite 237 grün.
Frontend (Phase 2: Browser-Geolocation beim 🆘 + Admin-Profil-UI) steht noch aus.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 15:33:58 +02:00
|
|
|
g = _geo_fields(geo)
|
|
|
|
|
if not g:
|
2026-06-25 17:24:36 +02:00
|
|
|
reason = geo_error or "kein GPS / keine Freigabe"
|
|
|
|
|
return f"Standort (Gerät): nicht verfügbar ({reason})."
|
feat: Notruf mit Kontaktdaten + Geoposition anreichern (Backend, Phase 1+3)
Notruf-Meldungen tragen jetzt das Notfall-Profil der auslösenden Person und —
falls vom Gerät mitgeschickt — die Geoposition. Kanal-Rollen:
- E-Mail: Voll-Dossier (Name, Adresse, Telefone, Geburtsdatum, med. Hinweise,
Koordinaten + Karten-Link + Genauigkeit + Zeit; sonst "nicht verfügbar")
- SMS: kompakt, Name + Adresse + Karten-Link
- Anruf (TTS): Name + Wohnadresse gesprochen, KEINE URL/Koordinaten, Verweis
auf SMS/E-Mail
- schemas: GeoFix + EmergencyRequest.geo; AdminUserPrefsUpdate um Profilfelder
erweitert (full_name, Adresse, Telefone, birth_date, medical_notes,
emergency_contacts/_phones, location_consent) — in user.prefs (keine Migration)
- api/chat: geo an record_manual_emergency durchgereicht
- api/admin: list_users gibt Profilfelder mit zurück (für die spätere Admin-UI)
- emergency.py: _profile/_geo_fields/_maps_link/_location_block/_email_body,
kanal-spezifische _emergency_texts; record_manual_emergency(..., geo=...)
- Tests: 9 neue (Profil-Injektion, SMS/Call/E-Mail-Texte, Geo→Map-Link,
ohne-Geo-Fallback, Payload). Suite 237 grün.
Frontend (Phase 2: Browser-Geolocation beim 🆘 + Admin-Profil-UI) steht noch aus.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 15:33:58 +02:00
|
|
|
lines = [
|
|
|
|
|
f"Standort (Gerät): {g['lat']:.6f}, {g['lon']:.6f}",
|
|
|
|
|
f"Karte: {_maps_link(g['lat'], g['lon'])}",
|
|
|
|
|
]
|
|
|
|
|
if g.get("accuracy") is not None:
|
|
|
|
|
lines.append(f"Genauigkeit: ca. {round(g['accuracy'])} m")
|
|
|
|
|
if g.get("ts"):
|
|
|
|
|
lines.append(f"Zeitpunkt des Fixes: {g['ts']}")
|
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _emergency_texts(profile: dict, geo) -> dict[str, str]:
|
|
|
|
|
"""Fertige Texte für SMS und Sprachanruf (vom Webhook-Empfänger genutzt)."""
|
|
|
|
|
name = profile["name"]
|
|
|
|
|
addr = profile["address_short"]
|
2026-06-25 17:24:36 +02:00
|
|
|
tel = _person_phones(profile)
|
feat: Notruf mit Kontaktdaten + Geoposition anreichern (Backend, Phase 1+3)
Notruf-Meldungen tragen jetzt das Notfall-Profil der auslösenden Person und —
falls vom Gerät mitgeschickt — die Geoposition. Kanal-Rollen:
- E-Mail: Voll-Dossier (Name, Adresse, Telefone, Geburtsdatum, med. Hinweise,
Koordinaten + Karten-Link + Genauigkeit + Zeit; sonst "nicht verfügbar")
- SMS: kompakt, Name + Adresse + Karten-Link
- Anruf (TTS): Name + Wohnadresse gesprochen, KEINE URL/Koordinaten, Verweis
auf SMS/E-Mail
- schemas: GeoFix + EmergencyRequest.geo; AdminUserPrefsUpdate um Profilfelder
erweitert (full_name, Adresse, Telefone, birth_date, medical_notes,
emergency_contacts/_phones, location_consent) — in user.prefs (keine Migration)
- api/chat: geo an record_manual_emergency durchgereicht
- api/admin: list_users gibt Profilfelder mit zurück (für die spätere Admin-UI)
- emergency.py: _profile/_geo_fields/_maps_link/_location_block/_email_body,
kanal-spezifische _emergency_texts; record_manual_emergency(..., geo=...)
- Tests: 9 neue (Profil-Injektion, SMS/Call/E-Mail-Texte, Geo→Map-Link,
ohne-Geo-Fallback, Payload). Suite 237 grün.
Frontend (Phase 2: Browser-Geolocation beim 🆘 + Admin-Profil-UI) steht noch aus.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 15:33:58 +02:00
|
|
|
g = _geo_fields(geo)
|
|
|
|
|
|
2026-06-25 17:24:36 +02:00
|
|
|
# SMS: kompakt — Name, Adresse, Telefon, Karten-Link (trägt den genauen Standort)
|
feat: Notruf mit Kontaktdaten + Geoposition anreichern (Backend, Phase 1+3)
Notruf-Meldungen tragen jetzt das Notfall-Profil der auslösenden Person und —
falls vom Gerät mitgeschickt — die Geoposition. Kanal-Rollen:
- E-Mail: Voll-Dossier (Name, Adresse, Telefone, Geburtsdatum, med. Hinweise,
Koordinaten + Karten-Link + Genauigkeit + Zeit; sonst "nicht verfügbar")
- SMS: kompakt, Name + Adresse + Karten-Link
- Anruf (TTS): Name + Wohnadresse gesprochen, KEINE URL/Koordinaten, Verweis
auf SMS/E-Mail
- schemas: GeoFix + EmergencyRequest.geo; AdminUserPrefsUpdate um Profilfelder
erweitert (full_name, Adresse, Telefone, birth_date, medical_notes,
emergency_contacts/_phones, location_consent) — in user.prefs (keine Migration)
- api/chat: geo an record_manual_emergency durchgereicht
- api/admin: list_users gibt Profilfelder mit zurück (für die spätere Admin-UI)
- emergency.py: _profile/_geo_fields/_maps_link/_location_block/_email_body,
kanal-spezifische _emergency_texts; record_manual_emergency(..., geo=...)
- Tests: 9 neue (Profil-Injektion, SMS/Call/E-Mail-Texte, Geo→Map-Link,
ohne-Geo-Fallback, Payload). Suite 237 grün.
Frontend (Phase 2: Browser-Geolocation beim 🆘 + Admin-Profil-UI) steht noch aus.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 15:33:58 +02:00
|
|
|
sms = f"NOTRUF: {name} braucht Hilfe."
|
|
|
|
|
if addr:
|
|
|
|
|
sms += f" Adresse: {addr}."
|
2026-06-25 17:24:36 +02:00
|
|
|
if tel:
|
|
|
|
|
sms += f" Tel: {tel}."
|
feat: Notruf mit Kontaktdaten + Geoposition anreichern (Backend, Phase 1+3)
Notruf-Meldungen tragen jetzt das Notfall-Profil der auslösenden Person und —
falls vom Gerät mitgeschickt — die Geoposition. Kanal-Rollen:
- E-Mail: Voll-Dossier (Name, Adresse, Telefone, Geburtsdatum, med. Hinweise,
Koordinaten + Karten-Link + Genauigkeit + Zeit; sonst "nicht verfügbar")
- SMS: kompakt, Name + Adresse + Karten-Link
- Anruf (TTS): Name + Wohnadresse gesprochen, KEINE URL/Koordinaten, Verweis
auf SMS/E-Mail
- schemas: GeoFix + EmergencyRequest.geo; AdminUserPrefsUpdate um Profilfelder
erweitert (full_name, Adresse, Telefone, birth_date, medical_notes,
emergency_contacts/_phones, location_consent) — in user.prefs (keine Migration)
- api/chat: geo an record_manual_emergency durchgereicht
- api/admin: list_users gibt Profilfelder mit zurück (für die spätere Admin-UI)
- emergency.py: _profile/_geo_fields/_maps_link/_location_block/_email_body,
kanal-spezifische _emergency_texts; record_manual_emergency(..., geo=...)
- Tests: 9 neue (Profil-Injektion, SMS/Call/E-Mail-Texte, Geo→Map-Link,
ohne-Geo-Fallback, Payload). Suite 237 grün.
Frontend (Phase 2: Browser-Geolocation beim 🆘 + Admin-Profil-UI) steht noch aus.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 15:33:58 +02:00
|
|
|
if g:
|
|
|
|
|
sms += f" Standort: {_maps_link(g['lat'], g['lon'])}"
|
|
|
|
|
|
|
|
|
|
# Anruf: gesprochen — Name + Wohnadresse, KEINE URL/Koordinaten, Verweis auf SMS/E-Mail
|
|
|
|
|
call = f"Achtung. {name} hat einen Notruf ausgelöst und braucht dringend Hilfe."
|
|
|
|
|
if addr:
|
|
|
|
|
call += f" Wohnadresse: {addr}."
|
|
|
|
|
call += " Genaue Standortdaten und weitere Details stehen in der SMS und der E-Mail."
|
|
|
|
|
|
|
|
|
|
return {"sms": sms, "call": call}
|
|
|
|
|
|
|
|
|
|
|
2026-06-25 17:24:36 +02:00
|
|
|
def _email_body(profile: dict, geo, geo_error: str | None = None, device=None) -> str:
|
|
|
|
|
"""Voll-Dossier für die E-Mail: Gerät + Angaben zur Person + Standort."""
|
feat: Notruf mit Kontaktdaten + Geoposition anreichern (Backend, Phase 1+3)
Notruf-Meldungen tragen jetzt das Notfall-Profil der auslösenden Person und —
falls vom Gerät mitgeschickt — die Geoposition. Kanal-Rollen:
- E-Mail: Voll-Dossier (Name, Adresse, Telefone, Geburtsdatum, med. Hinweise,
Koordinaten + Karten-Link + Genauigkeit + Zeit; sonst "nicht verfügbar")
- SMS: kompakt, Name + Adresse + Karten-Link
- Anruf (TTS): Name + Wohnadresse gesprochen, KEINE URL/Koordinaten, Verweis
auf SMS/E-Mail
- schemas: GeoFix + EmergencyRequest.geo; AdminUserPrefsUpdate um Profilfelder
erweitert (full_name, Adresse, Telefone, birth_date, medical_notes,
emergency_contacts/_phones, location_consent) — in user.prefs (keine Migration)
- api/chat: geo an record_manual_emergency durchgereicht
- api/admin: list_users gibt Profilfelder mit zurück (für die spätere Admin-UI)
- emergency.py: _profile/_geo_fields/_maps_link/_location_block/_email_body,
kanal-spezifische _emergency_texts; record_manual_emergency(..., geo=...)
- Tests: 9 neue (Profil-Injektion, SMS/Call/E-Mail-Texte, Geo→Map-Link,
ohne-Geo-Fallback, Payload). Suite 237 grün.
Frontend (Phase 2: Browser-Geolocation beim 🆘 + Admin-Profil-UI) steht noch aus.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 15:33:58 +02:00
|
|
|
lines = [
|
|
|
|
|
f"{profile['name']} hat in der Voice-Assistant-App einen Notruf ausgelöst.",
|
|
|
|
|
"",
|
|
|
|
|
"Bitte umgehend Kontakt aufnehmen.",
|
|
|
|
|
"",
|
|
|
|
|
]
|
2026-06-25 17:24:36 +02:00
|
|
|
dev = _device_line(device)
|
|
|
|
|
if dev:
|
|
|
|
|
lines += [dev, ""]
|
|
|
|
|
lines.append("— Angaben zur Person —")
|
feat: Notruf mit Kontaktdaten + Geoposition anreichern (Backend, Phase 1+3)
Notruf-Meldungen tragen jetzt das Notfall-Profil der auslösenden Person und —
falls vom Gerät mitgeschickt — die Geoposition. Kanal-Rollen:
- E-Mail: Voll-Dossier (Name, Adresse, Telefone, Geburtsdatum, med. Hinweise,
Koordinaten + Karten-Link + Genauigkeit + Zeit; sonst "nicht verfügbar")
- SMS: kompakt, Name + Adresse + Karten-Link
- Anruf (TTS): Name + Wohnadresse gesprochen, KEINE URL/Koordinaten, Verweis
auf SMS/E-Mail
- schemas: GeoFix + EmergencyRequest.geo; AdminUserPrefsUpdate um Profilfelder
erweitert (full_name, Adresse, Telefone, birth_date, medical_notes,
emergency_contacts/_phones, location_consent) — in user.prefs (keine Migration)
- api/chat: geo an record_manual_emergency durchgereicht
- api/admin: list_users gibt Profilfelder mit zurück (für die spätere Admin-UI)
- emergency.py: _profile/_geo_fields/_maps_link/_location_block/_email_body,
kanal-spezifische _emergency_texts; record_manual_emergency(..., geo=...)
- Tests: 9 neue (Profil-Injektion, SMS/Call/E-Mail-Texte, Geo→Map-Link,
ohne-Geo-Fallback, Payload). Suite 237 grün.
Frontend (Phase 2: Browser-Geolocation beim 🆘 + Admin-Profil-UI) steht noch aus.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 15:33:58 +02:00
|
|
|
fields = [
|
|
|
|
|
("Adresse", profile["address_short"]),
|
|
|
|
|
("Mobil", profile["phone_mobile"]),
|
|
|
|
|
("Festnetz", profile["phone_landline"]),
|
|
|
|
|
("Geburtsdatum", profile["birth_date"]),
|
|
|
|
|
("Medizinische Hinweise", profile["medical_notes"]),
|
|
|
|
|
]
|
|
|
|
|
info = [f"{label}: {value}" for label, value in fields if value]
|
|
|
|
|
lines.extend(info or ["(keine Profilangaben hinterlegt)"])
|
2026-06-25 17:24:36 +02:00
|
|
|
lines += ["", "— Standort —", _location_block(geo, geo_error), "",
|
feat: Notruf mit Kontaktdaten + Geoposition anreichern (Backend, Phase 1+3)
Notruf-Meldungen tragen jetzt das Notfall-Profil der auslösenden Person und —
falls vom Gerät mitgeschickt — die Geoposition. Kanal-Rollen:
- E-Mail: Voll-Dossier (Name, Adresse, Telefone, Geburtsdatum, med. Hinweise,
Koordinaten + Karten-Link + Genauigkeit + Zeit; sonst "nicht verfügbar")
- SMS: kompakt, Name + Adresse + Karten-Link
- Anruf (TTS): Name + Wohnadresse gesprochen, KEINE URL/Koordinaten, Verweis
auf SMS/E-Mail
- schemas: GeoFix + EmergencyRequest.geo; AdminUserPrefsUpdate um Profilfelder
erweitert (full_name, Adresse, Telefone, birth_date, medical_notes,
emergency_contacts/_phones, location_consent) — in user.prefs (keine Migration)
- api/chat: geo an record_manual_emergency durchgereicht
- api/admin: list_users gibt Profilfelder mit zurück (für die spätere Admin-UI)
- emergency.py: _profile/_geo_fields/_maps_link/_location_block/_email_body,
kanal-spezifische _emergency_texts; record_manual_emergency(..., geo=...)
- Tests: 9 neue (Profil-Injektion, SMS/Call/E-Mail-Texte, Geo→Map-Link,
ohne-Geo-Fallback, Payload). Suite 237 grün.
Frontend (Phase 2: Browser-Geolocation beim 🆘 + Admin-Profil-UI) steht noch aus.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 15:33:58 +02:00
|
|
|
"(Diese Meldung wurde automatisch von der Voice-Assistant-App gesendet.)"]
|
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --- Versandkanäle ---------------------------------------------------------
|
|
|
|
|
|
2026-06-25 03:37:18 +02:00
|
|
|
def _contacts(user, cfg) -> list[str]:
|
|
|
|
|
"""Kontakt-E-Mail(s): pro-Nutzer-Pref (CSV) falls vorhanden, sonst Default aus der Config."""
|
|
|
|
|
prefs = getattr(user, "prefs", None) or {}
|
|
|
|
|
raw = (prefs.get("emergency_contacts") or "").strip() if isinstance(prefs, dict) else ""
|
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>
2026-06-25 10:17:49 +02:00
|
|
|
return _csv(raw) or _csv(cfg.emergency_contact_email)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _phones(user, cfg) -> list[str]:
|
|
|
|
|
"""Kontakt-Telefonnummer(n): pro-Nutzer-Pref (CSV) falls vorhanden, sonst Default aus der Config."""
|
|
|
|
|
prefs = getattr(user, "prefs", None) or {}
|
|
|
|
|
raw = (prefs.get("emergency_phones") or "").strip() if isinstance(prefs, dict) else ""
|
|
|
|
|
return _csv(raw) or _csv(cfg.emergency_contact_phone)
|
2026-06-18 03:24:25 +02:00
|
|
|
|
2026-06-25 03:37:18 +02:00
|
|
|
|
|
|
|
|
def _send_email_sync(to: list[str], subject: str, body: str, cfg) -> bool:
|
|
|
|
|
"""Versendet eine E-Mail per SMTP. Liefert True bei Erfolg, sonst False (best effort)."""
|
|
|
|
|
if not cfg.smtp_host or not to:
|
|
|
|
|
return False
|
|
|
|
|
msg = EmailMessage()
|
|
|
|
|
msg["From"] = cfg.smtp_from or cfg.smtp_user or "voice-assistant@localhost"
|
|
|
|
|
msg["To"] = ", ".join(to)
|
|
|
|
|
msg["Subject"] = subject
|
|
|
|
|
msg.set_content(body)
|
|
|
|
|
try:
|
|
|
|
|
with smtplib.SMTP(cfg.smtp_host, int(cfg.smtp_port), timeout=10) as s:
|
|
|
|
|
if cfg.smtp_starttls:
|
|
|
|
|
s.starttls()
|
|
|
|
|
if cfg.smtp_user:
|
|
|
|
|
s.login(cfg.smtp_user, cfg.smtp_password)
|
|
|
|
|
s.send_message(msg)
|
|
|
|
|
return True
|
|
|
|
|
except Exception:
|
|
|
|
|
logger.exception("Notruf-E-Mail konnte nicht versendet werden")
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
2026-06-25 17:24:36 +02:00
|
|
|
def _webhook_payload(user, language, phones, channels, profile, geo, geo_error=None, device=None) -> dict:
|
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>
2026-06-25 10:17:49 +02:00
|
|
|
"""Provider-agnostisches Notruf-Payload für den Eskalations-Webhook."""
|
|
|
|
|
return {
|
|
|
|
|
"event": "emergency",
|
|
|
|
|
"category": "manual",
|
|
|
|
|
"source": "voice-assistant",
|
|
|
|
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
|
|
|
"language": (language or "de"),
|
feat: Notruf mit Kontaktdaten + Geoposition anreichern (Backend, Phase 1+3)
Notruf-Meldungen tragen jetzt das Notfall-Profil der auslösenden Person und —
falls vom Gerät mitgeschickt — die Geoposition. Kanal-Rollen:
- E-Mail: Voll-Dossier (Name, Adresse, Telefone, Geburtsdatum, med. Hinweise,
Koordinaten + Karten-Link + Genauigkeit + Zeit; sonst "nicht verfügbar")
- SMS: kompakt, Name + Adresse + Karten-Link
- Anruf (TTS): Name + Wohnadresse gesprochen, KEINE URL/Koordinaten, Verweis
auf SMS/E-Mail
- schemas: GeoFix + EmergencyRequest.geo; AdminUserPrefsUpdate um Profilfelder
erweitert (full_name, Adresse, Telefone, birth_date, medical_notes,
emergency_contacts/_phones, location_consent) — in user.prefs (keine Migration)
- api/chat: geo an record_manual_emergency durchgereicht
- api/admin: list_users gibt Profilfelder mit zurück (für die spätere Admin-UI)
- emergency.py: _profile/_geo_fields/_maps_link/_location_block/_email_body,
kanal-spezifische _emergency_texts; record_manual_emergency(..., geo=...)
- Tests: 9 neue (Profil-Injektion, SMS/Call/E-Mail-Texte, Geo→Map-Link,
ohne-Geo-Fallback, Payload). Suite 237 grün.
Frontend (Phase 2: Browser-Geolocation beim 🆘 + Admin-Profil-UI) steht noch aus.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 15:33:58 +02:00
|
|
|
"user": {"id": getattr(user, "id", None), "name": profile["name"]},
|
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>
2026-06-25 10:17:49 +02:00
|
|
|
"channels": channels,
|
|
|
|
|
"phones": phones,
|
feat: Notruf mit Kontaktdaten + Geoposition anreichern (Backend, Phase 1+3)
Notruf-Meldungen tragen jetzt das Notfall-Profil der auslösenden Person und —
falls vom Gerät mitgeschickt — die Geoposition. Kanal-Rollen:
- E-Mail: Voll-Dossier (Name, Adresse, Telefone, Geburtsdatum, med. Hinweise,
Koordinaten + Karten-Link + Genauigkeit + Zeit; sonst "nicht verfügbar")
- SMS: kompakt, Name + Adresse + Karten-Link
- Anruf (TTS): Name + Wohnadresse gesprochen, KEINE URL/Koordinaten, Verweis
auf SMS/E-Mail
- schemas: GeoFix + EmergencyRequest.geo; AdminUserPrefsUpdate um Profilfelder
erweitert (full_name, Adresse, Telefone, birth_date, medical_notes,
emergency_contacts/_phones, location_consent) — in user.prefs (keine Migration)
- api/chat: geo an record_manual_emergency durchgereicht
- api/admin: list_users gibt Profilfelder mit zurück (für die spätere Admin-UI)
- emergency.py: _profile/_geo_fields/_maps_link/_location_block/_email_body,
kanal-spezifische _emergency_texts; record_manual_emergency(..., geo=...)
- Tests: 9 neue (Profil-Injektion, SMS/Call/E-Mail-Texte, Geo→Map-Link,
ohne-Geo-Fallback, Payload). Suite 237 grün.
Frontend (Phase 2: Browser-Geolocation beim 🆘 + Admin-Profil-UI) steht noch aus.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 15:33:58 +02:00
|
|
|
"geo": _geo_fields(geo),
|
2026-06-25 17:24:36 +02:00
|
|
|
"geo_error": geo_error,
|
|
|
|
|
"device": device if isinstance(device, dict) else None,
|
feat: Notruf mit Kontaktdaten + Geoposition anreichern (Backend, Phase 1+3)
Notruf-Meldungen tragen jetzt das Notfall-Profil der auslösenden Person und —
falls vom Gerät mitgeschickt — die Geoposition. Kanal-Rollen:
- E-Mail: Voll-Dossier (Name, Adresse, Telefone, Geburtsdatum, med. Hinweise,
Koordinaten + Karten-Link + Genauigkeit + Zeit; sonst "nicht verfügbar")
- SMS: kompakt, Name + Adresse + Karten-Link
- Anruf (TTS): Name + Wohnadresse gesprochen, KEINE URL/Koordinaten, Verweis
auf SMS/E-Mail
- schemas: GeoFix + EmergencyRequest.geo; AdminUserPrefsUpdate um Profilfelder
erweitert (full_name, Adresse, Telefone, birth_date, medical_notes,
emergency_contacts/_phones, location_consent) — in user.prefs (keine Migration)
- api/chat: geo an record_manual_emergency durchgereicht
- api/admin: list_users gibt Profilfelder mit zurück (für die spätere Admin-UI)
- emergency.py: _profile/_geo_fields/_maps_link/_location_block/_email_body,
kanal-spezifische _emergency_texts; record_manual_emergency(..., geo=...)
- Tests: 9 neue (Profil-Injektion, SMS/Call/E-Mail-Texte, Geo→Map-Link,
ohne-Geo-Fallback, Payload). Suite 237 grün.
Frontend (Phase 2: Browser-Geolocation beim 🆘 + Admin-Profil-UI) steht noch aus.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 15:33:58 +02:00
|
|
|
"message": _emergency_texts(profile, geo),
|
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>
2026-06-25 10:17:49 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _post_webhook(url: str, payload: dict, cfg) -> bool:
|
|
|
|
|
"""Sendet das Notruf-Payload per HTTP-POST an den Eskalations-Webhook (best effort)."""
|
|
|
|
|
if not url:
|
|
|
|
|
return False
|
|
|
|
|
headers = {"Content-Type": "application/json"}
|
|
|
|
|
if cfg.emergency_webhook_token:
|
|
|
|
|
headers["Authorization"] = f"Bearer {cfg.emergency_webhook_token}"
|
|
|
|
|
try:
|
|
|
|
|
async with httpx.AsyncClient(timeout=10) as c:
|
|
|
|
|
resp = await c.post(url, json=payload, headers=headers)
|
|
|
|
|
return resp.is_success
|
|
|
|
|
except Exception:
|
|
|
|
|
logger.exception("Notruf-Webhook (SMS/Anruf) konnte nicht ausgelöst werden")
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
2026-06-26 17:10:12 +02:00
|
|
|
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
|
2026-06-26 15:58:17 +02:00
|
|
|
try:
|
2026-06-26 17:10:12 +02:00
|
|
|
return max(0, int(getattr(cfg, "emergency_cooldown_minutes", 5)))
|
2026-06-26 15:58:17 +02:00
|
|
|
except (TypeError, ValueError):
|
2026-06-26 17:10:12 +02:00
|
|
|
return 5
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _cooldown_seconds(user, cfg) -> int:
|
|
|
|
|
return _resolve_cooldown_minutes(user, cfg) * 60
|
2026-06-26 15:58:17 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
2026-06-26 17:10:12 +02:00
|
|
|
Schützt verwirrte/verängstigte Senioren vor Mehrfach-Alarmen. Die Pause ist
|
|
|
|
|
pro Nutzer einstellbar (sonst greift die globale Einstellung).
|
2026-06-26 15:58:17 +02:00
|
|
|
"""
|
2026-06-26 17:10:12 +02:00
|
|
|
total = _cooldown_seconds(user, cfg)
|
2026-06-26 15:58:17 +02:00
|
|
|
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:
|
|
|
|
|
return base
|
|
|
|
|
try:
|
|
|
|
|
last_dt = datetime.fromisoformat(last)
|
|
|
|
|
except (ValueError, TypeError):
|
|
|
|
|
return base
|
|
|
|
|
elapsed = (datetime.now(timezone.utc) - last_dt).total_seconds()
|
|
|
|
|
remaining = int(total - elapsed)
|
|
|
|
|
return {
|
|
|
|
|
"active": remaining > 0,
|
|
|
|
|
"cooldown_seconds": total,
|
|
|
|
|
"remaining_seconds": max(0, remaining),
|
|
|
|
|
"minutes_ago": max(0, int(elapsed // 60)),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-06-25 17:24:36 +02:00
|
|
|
async def record_manual_emergency(user, store, language: str | None = None,
|
|
|
|
|
geo=None, geo_error: str | None = None, device=None,
|
|
|
|
|
cfg=settings) -> dict:
|
2026-06-26 15:58:17 +02:00
|
|
|
"""Protokolliert einen bestätigten Notruf, benachrichtigt Kontakte und liefert den Hinweis.
|
|
|
|
|
|
|
|
|
|
Innerhalb der Wiederauslöse-Sperre (emergency_cooldown_minutes) wird KEIN
|
|
|
|
|
neuer Alarm gesendet und kein Ereignis protokolliert — stattdessen liefert
|
|
|
|
|
die Antwort status='cooldown' mit minutes_ago/remaining_seconds, damit das
|
|
|
|
|
Frontend den beruhigenden Hinweis zeigen kann.
|
|
|
|
|
"""
|
|
|
|
|
cd = emergency_cooldown_status(user, store, cfg)
|
|
|
|
|
if cd["active"]:
|
|
|
|
|
metrics.inc("emergency_total", {"category": "manual", "source": "user_suppressed"})
|
|
|
|
|
return {
|
|
|
|
|
"category": "manual",
|
|
|
|
|
"status": "cooldown",
|
|
|
|
|
"cooldown_seconds": cd["cooldown_seconds"],
|
|
|
|
|
"remaining_seconds": cd["remaining_seconds"],
|
|
|
|
|
"minutes_ago": cd["minutes_ago"],
|
|
|
|
|
"notice": notify_notice(language, True),
|
|
|
|
|
}
|
|
|
|
|
|
feat: Notruf mit Kontaktdaten + Geoposition anreichern (Backend, Phase 1+3)
Notruf-Meldungen tragen jetzt das Notfall-Profil der auslösenden Person und —
falls vom Gerät mitgeschickt — die Geoposition. Kanal-Rollen:
- E-Mail: Voll-Dossier (Name, Adresse, Telefone, Geburtsdatum, med. Hinweise,
Koordinaten + Karten-Link + Genauigkeit + Zeit; sonst "nicht verfügbar")
- SMS: kompakt, Name + Adresse + Karten-Link
- Anruf (TTS): Name + Wohnadresse gesprochen, KEINE URL/Koordinaten, Verweis
auf SMS/E-Mail
- schemas: GeoFix + EmergencyRequest.geo; AdminUserPrefsUpdate um Profilfelder
erweitert (full_name, Adresse, Telefone, birth_date, medical_notes,
emergency_contacts/_phones, location_consent) — in user.prefs (keine Migration)
- api/chat: geo an record_manual_emergency durchgereicht
- api/admin: list_users gibt Profilfelder mit zurück (für die spätere Admin-UI)
- emergency.py: _profile/_geo_fields/_maps_link/_location_block/_email_body,
kanal-spezifische _emergency_texts; record_manual_emergency(..., geo=...)
- Tests: 9 neue (Profil-Injektion, SMS/Call/E-Mail-Texte, Geo→Map-Link,
ohne-Geo-Fallback, Payload). Suite 237 grün.
Frontend (Phase 2: Browser-Geolocation beim 🆘 + Admin-Profil-UI) steht noch aus.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 15:33:58 +02:00
|
|
|
# Bewusst minimaler Log-Eintrag — KEINE Adresse/Koordinaten (Admin-Log bleibt unkritisch).
|
2026-06-25 03:02:52 +02:00
|
|
|
store.log_emergency(user.id, "manual", "Vom Nutzer ausgelöster Notruf")
|
|
|
|
|
metrics.inc("emergency_total", {"category": "manual", "source": "user"})
|
2026-06-25 03:37:18 +02:00
|
|
|
|
feat: Notruf mit Kontaktdaten + Geoposition anreichern (Backend, Phase 1+3)
Notruf-Meldungen tragen jetzt das Notfall-Profil der auslösenden Person und —
falls vom Gerät mitgeschickt — die Geoposition. Kanal-Rollen:
- E-Mail: Voll-Dossier (Name, Adresse, Telefone, Geburtsdatum, med. Hinweise,
Koordinaten + Karten-Link + Genauigkeit + Zeit; sonst "nicht verfügbar")
- SMS: kompakt, Name + Adresse + Karten-Link
- Anruf (TTS): Name + Wohnadresse gesprochen, KEINE URL/Koordinaten, Verweis
auf SMS/E-Mail
- schemas: GeoFix + EmergencyRequest.geo; AdminUserPrefsUpdate um Profilfelder
erweitert (full_name, Adresse, Telefone, birth_date, medical_notes,
emergency_contacts/_phones, location_consent) — in user.prefs (keine Migration)
- api/chat: geo an record_manual_emergency durchgereicht
- api/admin: list_users gibt Profilfelder mit zurück (für die spätere Admin-UI)
- emergency.py: _profile/_geo_fields/_maps_link/_location_block/_email_body,
kanal-spezifische _emergency_texts; record_manual_emergency(..., geo=...)
- Tests: 9 neue (Profil-Injektion, SMS/Call/E-Mail-Texte, Geo→Map-Link,
ohne-Geo-Fallback, Payload). Suite 237 grün.
Frontend (Phase 2: Browser-Geolocation beim 🆘 + Admin-Profil-UI) steht noch aus.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 15:33:58 +02:00
|
|
|
profile = _profile(user)
|
2026-06-25 17:24:36 +02:00
|
|
|
# Diagnose-Zeile im Server-Log: erklärt fehlenden Standort (geo_error) + Geräteart.
|
|
|
|
|
logger.info("Notruf von %s: geo=%s geo_error=%r device=%r",
|
|
|
|
|
profile["name"], bool(_geo_fields(geo)), geo_error, device)
|
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>
2026-06-25 10:17:49 +02:00
|
|
|
|
feat: Notruf mit Kontaktdaten + Geoposition anreichern (Backend, Phase 1+3)
Notruf-Meldungen tragen jetzt das Notfall-Profil der auslösenden Person und —
falls vom Gerät mitgeschickt — die Geoposition. Kanal-Rollen:
- E-Mail: Voll-Dossier (Name, Adresse, Telefone, Geburtsdatum, med. Hinweise,
Koordinaten + Karten-Link + Genauigkeit + Zeit; sonst "nicht verfügbar")
- SMS: kompakt, Name + Adresse + Karten-Link
- Anruf (TTS): Name + Wohnadresse gesprochen, KEINE URL/Koordinaten, Verweis
auf SMS/E-Mail
- schemas: GeoFix + EmergencyRequest.geo; AdminUserPrefsUpdate um Profilfelder
erweitert (full_name, Adresse, Telefone, birth_date, medical_notes,
emergency_contacts/_phones, location_consent) — in user.prefs (keine Migration)
- api/chat: geo an record_manual_emergency durchgereicht
- api/admin: list_users gibt Profilfelder mit zurück (für die spätere Admin-UI)
- emergency.py: _profile/_geo_fields/_maps_link/_location_block/_email_body,
kanal-spezifische _emergency_texts; record_manual_emergency(..., geo=...)
- Tests: 9 neue (Profil-Injektion, SMS/Call/E-Mail-Texte, Geo→Map-Link,
ohne-Geo-Fallback, Payload). Suite 237 grün.
Frontend (Phase 2: Browser-Geolocation beim 🆘 + Admin-Profil-UI) steht noch aus.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 15:33:58 +02:00
|
|
|
# Kanal 1: E-Mail (SMTP) — Voll-Dossier
|
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>
2026-06-25 10:17:49 +02:00
|
|
|
to = _contacts(user, cfg)
|
feat: Notruf mit Kontaktdaten + Geoposition anreichern (Backend, Phase 1+3)
Notruf-Meldungen tragen jetzt das Notfall-Profil der auslösenden Person und —
falls vom Gerät mitgeschickt — die Geoposition. Kanal-Rollen:
- E-Mail: Voll-Dossier (Name, Adresse, Telefone, Geburtsdatum, med. Hinweise,
Koordinaten + Karten-Link + Genauigkeit + Zeit; sonst "nicht verfügbar")
- SMS: kompakt, Name + Adresse + Karten-Link
- Anruf (TTS): Name + Wohnadresse gesprochen, KEINE URL/Koordinaten, Verweis
auf SMS/E-Mail
- schemas: GeoFix + EmergencyRequest.geo; AdminUserPrefsUpdate um Profilfelder
erweitert (full_name, Adresse, Telefone, birth_date, medical_notes,
emergency_contacts/_phones, location_consent) — in user.prefs (keine Migration)
- api/chat: geo an record_manual_emergency durchgereicht
- api/admin: list_users gibt Profilfelder mit zurück (für die spätere Admin-UI)
- emergency.py: _profile/_geo_fields/_maps_link/_location_block/_email_body,
kanal-spezifische _emergency_texts; record_manual_emergency(..., geo=...)
- Tests: 9 neue (Profil-Injektion, SMS/Call/E-Mail-Texte, Geo→Map-Link,
ohne-Geo-Fallback, Payload). Suite 237 grün.
Frontend (Phase 2: Browser-Geolocation beim 🆘 + Admin-Profil-UI) steht noch aus.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 15:33:58 +02:00
|
|
|
subject = f"🆘 Notruf von {profile['name']}"
|
2026-06-25 17:24:36 +02:00
|
|
|
body = _email_body(profile, geo, geo_error, device)
|
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>
2026-06-25 10:17:49 +02:00
|
|
|
email_sent = await asyncio.to_thread(_send_email_sync, to, subject, body, cfg)
|
|
|
|
|
if email_sent:
|
2026-06-25 03:37:18 +02:00
|
|
|
metrics.inc("emergency_notify_total", {"channel": "email"})
|
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>
2026-06-25 10:17:49 +02:00
|
|
|
|
|
|
|
|
# Kanal 2: SMS/Anruf (provider-agnostischer Webhook)
|
|
|
|
|
phones = _phones(user, cfg)
|
|
|
|
|
channels = _csv(cfg.emergency_webhook_channels)
|
2026-06-25 17:24:36 +02:00
|
|
|
payload = _webhook_payload(user, language, phones, channels, profile, geo, geo_error, device)
|
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>
2026-06-25 10:17:49 +02:00
|
|
|
webhook_sent = await _post_webhook(cfg.emergency_webhook_url, payload, cfg)
|
|
|
|
|
if webhook_sent:
|
|
|
|
|
metrics.inc("emergency_notify_total", {"channel": "webhook"})
|
|
|
|
|
|
|
|
|
|
notified = email_sent or webhook_sent
|
|
|
|
|
return {
|
|
|
|
|
"category": "manual",
|
2026-06-26 15:58:17 +02:00
|
|
|
"status": "alerted",
|
|
|
|
|
"cooldown_seconds": cd["cooldown_seconds"],
|
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>
2026-06-25 10:17:49 +02:00
|
|
|
"notice": notify_notice(language, notified),
|
|
|
|
|
"email_sent": email_sent,
|
|
|
|
|
"webhook_sent": webhook_sent,
|
|
|
|
|
"channels": channels,
|
|
|
|
|
}
|