feat: runtime config, Matomo analytics, chat accounts, DM history/search, and Prometheus retention

This commit is contained in:
zwitschi committed 2026-08-13 13:12:35 +02:00
1 parent 06ab5b4655
commit 121fdbacdc
27 files changed
+1841 -161

No files matched your search

+94 -38
View File
@@ -39,7 +39,9 @@
- [Video Normalization \& Playout](#video-normalization--playout)
- [Required Source Profile (Linear Void v1)](#required-source-profile-linear-void-v1)
- [Reference Normalization Command](#reference-normalization-command)
- [Verification Gate (ffprobe)](#verification-gate-ffprobe)
- [Automatic Normalization \& Verification (ffprobe)](#automatic-normalization--verification-ffprobe)
- [Runtime Configuration Store](#runtime-configuration-store)
- [Analytics (Matomo)](#analytics-matomo)
- [Monitoring \& Resilience](#monitoring--resilience)
- [Prometheus Baseline](#prometheus-baseline)
- [Grafana Dashboards](#grafana-dashboards)
@@ -48,6 +50,7 @@
- [Chat WebSocket API](#chat-websocket-api)
- [Playout and Delivery REST API](#playout-and-delivery-rest-api)
- [Admin REST API](#admin-rest-api)
- [Viewer REST API](#viewer-rest-api)
- [Cloudflare Configuration Reference](#cloudflare-configuration-reference)
- [DNS and Zone Routing](#dns-and-zone-routing)
- [Cache Rules Baseline](#cache-rules-baseline)
@@ -58,6 +61,7 @@
- [Transport and Edge Protection](#transport-and-edge-protection)
- [Reverse Proxy Enforcement (nginx)](#reverse-proxy-enforcement-nginx)
- [Admin Authentication and Session Security](#admin-authentication-and-session-security)
- [Viewer User Accounts](#viewer-user-accounts)
- [Rate Limiting and Abuse Controls](#rate-limiting-and-abuse-controls)
- [CORS and Cross-Origin Behavior](#cors-and-cross-origin-behavior)
- [Moderation and Privacy](#moderation-and-privacy)
@@ -305,7 +309,10 @@ graph TB
- **Input**: `data/playlist/playlist.txt` — ordered list of MP4 file paths
- **Output**: HLS segments (`.m4s` and `.ts`) + live manifest (`.m3u8`) written to `data/hls/`, mirrored to R2
- **Resilience**: Restart counter (>5/60s → activate slate), Prometheus metrics on port 8081
- **Validation**: ffprobe gate on all ingested files (codec, fps, resolution, keyframe interval)
- **Validation**: ffprobe gate on all ingested files (codec, fps, resolution, keyframe interval); `-validate` mode checks the playlist without running the loop
- **Seamless looping**: Only resets HLS output when the playlist fingerprint (SHA-256) changes, on crash/slate recovery, or first run; otherwise continues segment numbering via `-start_number`
- **R2 retention**: `cleanupOldSegments` deletes `hls/r-*/*` objects older than `R2_SEGMENT_RETENTION_HOURS` (default 24h) hourly + at startup
- **Shared config**: Reads the runtime `config.json` (`loadSharedHLSConfig`) as an env fallback for HLS segment duration/window
#### Chat Service (`backend/chat/src/main.go`)
@@ -334,43 +341,50 @@ Module-level breakdown per service.
#### Chat Service Modules
| Module | File/Lines | Responsibility |
| ---------------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| **Connection Manager** | `main.go` (hub, clients map) | Track active WebSocket connections per room, fan-out messages |
| **Message Parser** | `main.go` (readPump, handleMessage) | Parse JSON envelope `{type, room?, ...}`, validate fields, route to handler |
| **Room Registry** | `main.go` (rooms map, room broadcasts) | Map room name → client set, handle join/leave/close |
| **DM Router** | `main.go` (handleDirectMessage) | Resolve target user in clients map, deliver private message |
| **Persistence Layer** | `main.go` (GORM models: Message, DirectMessage, ModerationLog) | Store/retrieve chat messages, DMs, moderation events |
| **Redis Subsystem** | `main.go` (redisClient, rate limiting, pub/sub, presence) | INCR+EXPIRE rate limit, SETEX presence, PSUBSCRIBE room fan-out |
| **Auth Handler** | `main.go` (handleAdminLogin, adminAuthMiddleware) | Session cookie creation/validation, login rate limiting |
| **Admin API Router** | `main.go` (handleAdmin\*) | Upload, playlist CRUD, schedule overrides, moderation actions, analytics, system summary |
| **Metrics Exporter** | `main.go` (prometheus.MustRegister) | Expose connected_clients, messages_total, dms_total, moderation_actions, redis_up |
| Module | File/Lines | Responsibility |
| ---------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| **Connection Manager** | `main.go` (hub, clients map) | Track active WebSocket connections per room, fan-out messages |
| **Message Parser** | `main.go` (readPump, handleMessage) | Parse JSON envelope `{type, room?, ...}`, validate fields, route to handler |
| **Room Registry** | `main.go` (rooms map, room broadcasts) | Map room name → client set, handle join/leave/close |
| **DM Router** | `main.go` (handleDirectMessage) | Resolve target user in clients map, deliver private message |
| **Persistence Layer** | `main.go` (GORM models: Message, DirectMessage, ModerationLog) | Store/retrieve chat messages, DMs, moderation events |
| **Redis Subsystem** | `main.go` (redisClient, rate limiting, pub/sub, presence) | INCR+EXPIRE rate limit, SETEX presence, PSUBSCRIBE room fan-out |
| **Auth Handler** | `main.go` (handleAdminLogin, adminAuthMiddleware) | Session cookie creation/validation, login rate limiting |
| **User Auth Handler** | `handlers/handlers.go` (HandleUserRegister/Login/Session/Logout) | Viewer register/login/logout/session (bcrypt, user session cookie) |
| **DM History/Search** | `handlers/handlers.go` (HandleUserDMHistory, HandleUserDMSearch) | Paginated DM history + search for authenticated users |
| **Runtime Config** | `config/runtime.go` (GetRuntime, SetRuntime, typed setters) | Thread-safe JSON runtime config (HLS duration/window, Matomo) |
| **Admin API Router** | `main.go` (handleAdmin\*) | Upload, playlist CRUD, schedule overrides, moderation actions, analytics, system summary |
| **Metrics Exporter** | `main.go` (prometheus.MustRegister) | Expose connected_clients, messages_total, dms_total, moderation_actions, redis_up |
#### Playout Service Modules
| Module | File/Lines | Responsibility |
| -------------------- | ---------------------------------------------- | ------------------------------------------------------------------ |
| **Playlist Parser** | `playlist.go` (parsePlaylist) | Read `playlist.txt`, skip comments/blank lines, resolve file paths |
| **FFmpeg Manager** | `playlist.go` (runPlayout, restart logic) | Spawn/monitor FFmpeg concat process, restart on crash |
| **Slate Controller** | `playlist.go` (activateSlate, deactivateSlate) | Switch output to slate loop on excessive failures |
| **R2 Sync** | `playlist.go` (syncToR2) | Upload HLS segments + manifest to R2 bucket |
| **Health Endpoint** | `playlist.go` (healthHandler) | `/health` → JSON with mode, segment count, uptime |
| **Metrics Exporter** | `playlist.go` (prometheus) | Expose restarts_total, mode (live/slate) |
| Module | File/Lines | Responsibility |
| -------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| **Playlist Parser** | `playlist.go` (parsePlaylist) | Read `playlist.txt`, skip comments/blank lines, resolve file paths |
| **FFmpeg Manager** | `playlist.go` (runPlayout, restart logic) | Spawn/monitor FFmpeg concat process, restart on crash |
| **Loop Controller** | `playlist.go` (playlistFingerprint, hlsNextStartNumber) | Detect playlist changes (SHA-256), continue segment numbering on clean loops |
| **Slate Controller** | `playlist.go` (activateSlate, deactivateSlate) | Switch output to slate loop on excessive failures |
| **R2 Sync** | `playlist.go` (syncToR2) | Upload HLS segments + manifest to R2 bucket |
| **R2 Cleanup** | `playlist.go` (cleanupOldSegments) | Delete stale `hls/r-*/*` segments older than the retention window |
| **Health Endpoint** | `playlist.go` (healthHandler) | `/health` → JSON with mode, segment count, uptime |
| **Metrics Exporter** | `playlist.go` (prometheus) | Expose restarts_total, mode (live/slate), loops_total, run_duration_seconds, r2_cleanup_objects_total |
#### Frontend Modules
| Module | File | Responsibility |
| ----------------------- | ----------------------------------------------- | ----------------------------------------------------------------- |
| **HLS Player** | `src/main.ts` | hls.js initialization, attach to video element, error recovery |
| **WebSocket Client** | `src/main.ts` | Connect/disconnect, parse envelope, dispatch to UI handlers |
| **Chat UI** | `src/main.ts` | Message list rendering, room tabs, DM panel, presence dots |
| **Schedule Overlay** | `src/main.ts` | Poll `/api/schedule`, render current/next program |
| **Admin Router** | `src/admin.ts` | Tab-based navigation, dynamic page loading |
| **Upload Manager** | `src/admin.ts` | Multipart upload with chunk progress, ffprobe validation feedback |
| **Playlist Editor** | `src/admin.ts` | Drag-reorder, add/remove items, save to `/api/admin/playlist` |
| **Moderation Panel** | `src/admin.ts` + `public/admin/moderation.html` | Inline mute/ban, action log viewer |
| **Analytics Dashboard** | `src/admin.ts` + `public/admin/analytics.html` | Message counts, top rooms, moderation stats |
| **System Config** | `src/admin.ts` + `public/admin/system.html` | Service health, config metadata, restart buttons |
| Module | File | Responsibility |
| ----------------------- | ----------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| **HLS Player** | `src/main.ts` | hls.js initialization, attach to video element, error recovery |
| **WebSocket Client** | `src/main.ts` | Connect/disconnect, parse envelope, dispatch to UI handlers |
| **Chat UI** | `src/main.ts` | Message list rendering, room tabs, DM panel, presence dots, typing indicator, user-list toggle |
| **DM Search** | `src/main.ts` | Search DM history (`/api/dm/search`) and render results |
| **Auth UI** | `src/main.ts` + `index.html` | Viewer login/register/logout, session refresh, user label |
| **Schedule Overlay** | `src/main.ts` | Poll `/api/schedule`, render current/next program |
| **Admin Router** | `src/admin.ts` | Tab-based navigation, dynamic page loading |
| **Upload Manager** | `src/admin.ts` | Multipart upload with chunk progress, normalization status badges |
| **Playlist Editor** | `src/admin.ts` | Drag-reorder, add/remove items (gated to `ready` media), save to `/api/admin/playlist` |
| **Moderation Panel** | `src/admin.ts` + `public/admin/moderation.html` | Inline mute/ban, action log viewer |
| **Analytics Dashboard** | `src/admin.ts` + `public/admin/analytics.html` | Message counts, top rooms, moderation stats |
| **System Config** | `src/admin.ts` + `public/admin/system.html` | Service health, runtime config form (HLS duration/window, Matomo), restart buttons |
## Runtime View
@@ -480,7 +494,7 @@ Immutable segment naming + targeted Cloudflare Cache Rules + R2 ($0 egress) = ne
### Video Normalization & Playout
All source assets must be pre-normalized before they are accepted into production playlists. This prevents decode resets, timestamp drift, and audio/video discontinuities during continuous concat playout.
All source assets must conform to the Linear Void source profile before they are accepted into production playlists. The chat backend **auto-normalizes** non-conforming uploads on ingest (`media.NormalizeVideo`), so admins no longer need to pre-process files manually. This prevents decode resets, timestamp drift, and audio/video discontinuities during continuous concat playout.
#### Required Source Profile (Linear Void v1)
@@ -508,12 +522,27 @@ ffmpeg -i input.mov \
output-normalized.mp4
```
#### Verification Gate (ffprobe)
#### Automatic Normalization & Verification (ffprobe)
- Validate codec, fps, resolution, and keyframe cadence before upload acceptance.
- Reject non-conforming files from admin ingest with actionable error feedback.
- `media.NormalizeVideo` runs ffmpeg with the Linear Void profile (H.264/AAC, 30fps, 2s keyframes, yuv420p, 48kHz stereo), writes `*.normalized.mp4`, and re-verifies the output with ffprobe.
- Media records carry a status: `ready`, `needs_normalization`, `normalizing`, or `normalization_failed`; the admin UI shows a "Normalize" action with status polling.
- `POST /api/admin/media/{id}/normalize` re-triggers normalization manually for failed/re-queued media.
- Keep verification logic aligned with runtime playout assumptions to preserve `-c copy` efficiency where possible.
### Runtime Configuration Store
A thread-safe, JSON-persisted runtime config store lives in `backend/chat/src/config/runtime.go` and is shared with the playout service via the playlist volume (`/app/playlist/config.json`).
- Editable via `GET/PUT /api/admin/config` from the admin UI.
- Fields: `hlsSegmentDuration` (2–15s, default 6), `hlsWindowSize` (3–100, default 24), `matomoUrl`, `matomoSiteId`.
- Playout reads the same file (`loadSharedHLSConfig`) as an env fallback for HLS segment duration/window.
- Typed setters validate values; invalid input is rejected with `400`.
### Analytics (Matomo)
- The viewer sends pageviews to Matomo driven by the public `GET /api/matomo` endpoint, which returns `matomoUrl` + `matomoSiteId` from the runtime config store.
- Analytics is optional per deployment: leave `matomoUrl` empty to disable tracking.
### Monitoring & Resilience
Linear Void uses a two-layer monitoring model:
@@ -533,6 +562,7 @@ Linear Void uses a two-layer monitoring model:
- `postgres` -> `postgres-exporter:9187`
- `node` -> `node-exporter:9100`
- `prometheus` -> `localhost:9090`
- TSDB retention: `--storage.tsdb.retention.time=30d` (delete metrics older than 30 days) + `--storage.tsdb.retention.size=15GB` (disk cap), set on the `prometheus` service in `docker-compose.yml` and `docker-compose.prod.yml`
#### Grafana Dashboards
@@ -545,7 +575,7 @@ Use the app dashboard for service-level behavior and the Prometheus overview das
Rules are implemented in `monitoring/prometheus/alerts.yml` and loaded via `rule_files` from `prometheus.yml`.
Current rule set (8):
Current rule set (10):
- `ChatDown`
- `PlayoutDown`
@@ -554,6 +584,8 @@ Current rule set (8):
- `HighChatLatency`
- `PrometheusWALFailing`
- `HighCPU`
- `LowCacheHitRatio`
- `HighRedisMemoryUsage`
- `LowDiskSpace`
Route warning/critical severities to the operations channel and keep runbook links attached to each alert definition.
@@ -606,6 +638,8 @@ Linear Void currently exposes three operational API surfaces. The canonical deta
- `/api/admin/session`
- Operational/admin domains:
- overview, analytics, users, moderation, schedule, playlist, media
- runtime config: `GET/PUT /api/admin/config` (HLS segment duration/window, Matomo)
- media normalization: `POST /api/admin/media/{id}/normalize`
- Media upload domain:
- `/api/admin/media/uploads` (init/resume)
- `/api/admin/media/uploads/{uploadId}/chunk`
@@ -615,6 +649,19 @@ Linear Void currently exposes three operational API surfaces. The canonical deta
- media metadata + upload orchestration
- moderation/audit and system status management
#### Viewer REST API
- Auth/session:
- `POST /api/auth/register`
- `POST /api/auth/login`
- `POST /api/auth/logout`
- `GET /api/auth/session`
- Private messaging:
- `GET /api/dm/history` (paginated)
- `GET /api/dm/search`
- Analytics:
- `GET /api/matomo` (public; returns Matomo URL + site ID for the viewer pageview snippet)
### Cloudflare Configuration Reference
Cloudflare is part of the production control plane, not only a CDN edge. The following configuration set is required to maintain cost, latency, and reliability targets.
@@ -680,6 +727,12 @@ Security is split between edge controls (Cloudflare), reverse proxy controls (ng
- Session cookie controls in backend include `HttpOnly`, `SameSite=Strict`, and `Secure` when request is HTTPS (derived from TLS or `X-Forwarded-Proto`).
- Session expiration is enforced server-side.
#### Viewer User Accounts
- Register/login/logout via `POST /api/auth/register`, `POST /api/auth/login`, `POST /api/auth/logout`; current session via `GET /api/auth/session`.
- Passwords are hashed with bcrypt; sessions use a `linearvoid_user_session` cookie with a 30-day default TTL (`USER_SESSION_TTL_HOURS`).
- Viewer sign-in UI (username/password) lives in `frontend/index.html`.
#### Rate Limiting and Abuse Controls
- Chat message rate limiting uses Redis `INCR + EXPIRE` windowing.
@@ -701,6 +754,9 @@ Security is split between edge controls (Cloudflare), reverse proxy controls (ng
WebSockets + Redis (presence, pub/sub, rate limits) + DB (history, audit). Public rooms + on-demand private chats. Scales independently of video.
- Private DM history (`GET /api/dm/history`, paginated) and search (`GET /api/dm/search`) are backed by persisted DMs.
- Viewer UI includes a typing indicator, user-list toggle, and DM search.
Full details, cache rule table, bitrate/storage calculations, cost model, example configs, ToS notes, and implementation phases are in [`adr/ADR-001.md`](adr/ADR-001.md).
## Architecture Decisions