Files
PTah 563b836acc feat: Seaca mobile API, enrollment, FCM push and admin UI (0.9.0)
Adds mobile device registration by admin codes, refresh tokens, push channel
in notification policy, and Settings section for managing Seaca clients.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 13:19:14 +10:00

141 lines
4.2 KiB
Python

"""Global notification policy: severity ≥ min → selected channels."""
from __future__ import annotations
from dataclasses import dataclass
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.config import get_settings
from app.models.notification_channel import NotificationChannel
from app.models.notification_policy import POLICY_ROW_ID, NotificationPolicy
from app.services.notification_settings import (
CHANNEL_EMAIL,
CHANNEL_TELEGRAM,
CHANNEL_WEBHOOK,
VALID_SEVERITIES,
)
CHANNEL_MOBILE = "mobile"
DEFAULT_CHANNELS = frozenset({CHANNEL_TELEGRAM, CHANNEL_WEBHOOK, CHANNEL_EMAIL, CHANNEL_MOBILE})
@dataclass(frozen=True)
class NotificationPolicyConfig:
min_severity: str
use_telegram: bool
use_webhook: bool
use_email: bool
use_mobile: bool
source: str # env | db
def allows_channel(self, channel: str) -> bool:
if channel == CHANNEL_TELEGRAM:
return self.use_telegram
if channel == CHANNEL_WEBHOOK:
return self.use_webhook
if channel == CHANNEL_EMAIL:
return self.use_email
if channel == CHANNEL_MOBILE:
return self.use_mobile
return False
def _parse_channels_csv(value: str) -> tuple[bool, bool, bool, bool]:
parts = {p.strip().lower() for p in value.split(",") if p.strip()}
return (
CHANNEL_TELEGRAM in parts or "tg" in parts,
CHANNEL_WEBHOOK in parts,
CHANNEL_EMAIL in parts or "mail" in parts or "smtp" in parts,
CHANNEL_MOBILE in parts or "push" in parts or "fcm" in parts,
)
def _policy_from_env() -> NotificationPolicyConfig:
settings = get_settings()
use_tg, use_wh, use_em, use_mob = _parse_channels_csv(settings.notify_channels)
min_sev = settings.notify_min_severity.strip() or "warning"
if min_sev not in VALID_SEVERITIES:
min_sev = "warning"
return NotificationPolicyConfig(
min_severity=min_sev,
use_telegram=use_tg,
use_webhook=use_wh,
use_email=use_em,
use_mobile=use_mob,
source="env",
)
def get_effective_notification_policy(db: Session | None = None) -> NotificationPolicyConfig:
if db is None:
from app.database import SessionLocal
session = SessionLocal()
try:
return get_effective_notification_policy(session)
finally:
session.close()
row = db.get(NotificationPolicy, POLICY_ROW_ID)
env_cfg = _policy_from_env()
if row is None:
return env_cfg
min_sev = (row.min_severity or "").strip() or env_cfg.min_severity
if min_sev not in VALID_SEVERITIES:
min_sev = env_cfg.min_severity
return NotificationPolicyConfig(
min_severity=min_sev,
use_telegram=bool(row.use_telegram),
use_webhook=bool(row.use_webhook),
use_email=bool(row.use_email),
use_mobile=bool(getattr(row, "use_mobile", False)),
source="db",
)
def _sync_channel_min_severity(db: Session, min_severity: str) -> None:
for channel in (CHANNEL_TELEGRAM, CHANNEL_WEBHOOK, CHANNEL_EMAIL):
row = db.get(NotificationChannel, channel)
if row is not None:
row.min_severity = min_severity
def upsert_notification_policy(
db: Session,
*,
min_severity: str,
use_telegram: bool,
use_webhook: bool,
use_email: bool,
use_mobile: bool = False,
) -> NotificationPolicyConfig:
if min_severity not in VALID_SEVERITIES:
raise ValueError(f"invalid min_severity: {min_severity}")
row = db.get(NotificationPolicy, POLICY_ROW_ID)
if row is None:
row = NotificationPolicy(
id=POLICY_ROW_ID,
min_severity=min_severity,
use_telegram=use_telegram,
use_webhook=use_webhook,
use_email=use_email,
use_mobile=use_mobile,
)
db.add(row)
else:
row.min_severity = min_severity
row.use_telegram = use_telegram
row.use_webhook = use_webhook
row.use_email = use_email
row.use_mobile = use_mobile
_sync_channel_min_severity(db, min_severity)
db.commit()
db.refresh(row)
return get_effective_notification_policy(db)