Add read-only web dashboard and phone push notifications

Both zero-dependency (stdlib http.server + urllib), consistent with the
project's no-dependency-tree stance for a security daemon.

Web dashboard (enodia_sentinel/web.py + static/dashboard.html):
- read-only JSON API over the log dir: /api/status, /api/alerts,
  /api/alerts/<id>, /api/events
- bearer-token auth (constant-time; header or ?token=), required on non-
  loopback binds, auto-generated + persisted (0600) when unset
- binds the host's Tailscale IP by default (auto-detected), reachable from the
  tailnet but not the LAN/internet
- self-contained dark SPA: severity cards, live alert list, full snapshot
  viewer; 10s auto-refresh
- path-traversal-safe alert lookup; `enodia-sentinel web` subcommand; daemon
  now writes a pidfile so the dashboard can show live status
- hardened enodia-sentinel-web.service (read-only, no caps)

Phone push (enodia_sentinel/notify/):
- pluggable backends — ntfy, Pushover, generic webhook — each separating a pure
  build() (unit-tested, no network) from send()
- a backend turns on when its config keys are set; pushes gated by
  notify_min_severity; severity → per-service priority/tags
- fired from snapshot.capture on worker threads, errors swallowed
- desktop notify-send retained

Tests: +16 (9 web incl. a real-server 401/200 auth test, 7 notify request-build
cases). 55/55 pass. Live end-to-end verified: daemon → alert → API → page.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Luna 2026-05-31 16:18:00 -07:00
parent 0eb5077551
commit c00fff224c
17 changed files with 960 additions and 5 deletions

View file

@ -67,9 +67,24 @@ class Config:
max_snapshots: int = 300
max_snapshot_age_days: int = 60
# notifications
# notifications — desktop
notify_users: tuple[str, ...] = ()
notify_urgency: str = "critical"
# notifications — phone push (a backend is enabled when its required keys
# are set). Only alerts at/above notify_min_severity are pushed.
notify_min_severity: str = "HIGH"
notify_ntfy_url: str = "" # e.g. "https://ntfy.sh" or self-hosted base
notify_ntfy_topic: str = "" # topic name (required to enable ntfy)
notify_ntfy_token: str = "" # optional Bearer token for protected topics
notify_pushover_token: str = "" # Pushover application token
notify_pushover_user: str = "" # Pushover user/group key
notify_webhook_url: str = "" # generic JSON POST target
dashboard_url: str = "" # optional base URL embedded in pushes
# web dashboard (read-only)
web_bind: str = "" # "" = auto-detect Tailscale IP, else explicit
web_port: int = 8787
web_token: str = "" # bearer token; auto-generated if empty + non-local
# paths
log_dir: Path = Path("/var/log/enodia-sentinel")