feat: add automatic media normalization, R2 retention cleanup, and seamless playout loops

This commit is contained in:
zwitschi committed 2026-08-13 11:59:46 +02:00
1 parent e15d89edfe
commit 06ab5b4655
16 files changed
+1103 -408

No files matched your search

+1 -79
View File
@@ -1,34 +1,14 @@
# BACKLOG
## Remaining from 2026-08-11 (summary)
- **Test Fixtures**: media + playlist fixtures created; ffmpeg generation commands documented in test headers
- **User Frontend Tests**: video player, chat integration, responsive, accessibility, coverage
- **Admin Frontend Tests**: playlist UI, moderation UI, metrics dashboards, coverage
- **Cloudflare R2 Integration Test**: upload + cache header validation done (SyncToR2 with CacheControl + ContentType); edge cache hit/miss validation remains manual
- **Cloudflare Config**: Cloudflare Pages deploy workflow and setup docs created; manual dashboard setup (Tiered Cache, WAF, rate limiting, HTTPS/HSTS) remains pending; Pages project must be created in dashboard first.
- **External Services**: UptimeRobot monitors + alerts
- **Production Deployment**: Hetzner provisioning, CD pipeline, DNS cutover, smoke test
- **Scaling & Performance**: TTFB baseline and live benchmark runs still pending; loadtest scaffolding now includes Artillery/k6 helpers with auto summaries plus worksheet field mapping; chat p95, Redis saturation, and cache-hit threshold alerting are implemented
> All remaining work is planned in [TODO](./TODO.md) (2026-08-13). This file records completed backlog items for reference; new unplanned work is added here.
## Cloudflare Configuration
### DNS & Pages
- [x] Configure DNS for linearvoid.tv apex and www redirect (manual)
- [x] Configure media.linearvoid.tv for split-domain HLS delivery (manual)
- [x] Configure Cloudflare Pages deploy workflow (`.gitea/workflows/pages-deploy.yml`) and setup docs (`docs/operations/cloudflare-pages-setup.md`, `docs/operations/gitea-cloudflare-pages.md`)
- [x] Create Cloudflare Pages project in dashboard (unavailable, created `linearvoid` via API)
- [ ] Cloudflare Pages: link custom domain (manual)
- [ ] Create Cloudflare Pages project and cut over frontend hosting (manual)
### Storage & Cache
- [x] Create R2 bucket (manual)
- [x] Configure Cache Rules: .ts/.m4s + .m3u8 (manual)
- [x] Implement Worker for immutable cache headers on segment paths
- [ ] CORS headers on HLS responses from media.linearvoid.tv (MEDIUM) — needs Cloudflare R2 CORS config
### Security & TLS
- [ ] Enable Tiered Cache, WAF, rate limiting (manual)
@@ -40,13 +20,6 @@
## Monitoring & Observability
### Prometheus & Grafana
- [x] Set up Prometheus with chat/playout/nginx/redis/postgres/node scrape targets
- [x] Add nginx-exporter, redis-exporter, postgres-exporter, node-exporter
- [x] Import Grafana app + operational dashboards
- [x] Define Prometheus alerting rules (8 rules: ChatDown, PlayoutDown, RedisDown, etc.)
### External Monitoring
- [ ] Set up UptimeRobot external monitoring
@@ -54,44 +27,16 @@
- [ ] UptimeRobot: add keyword monitor for `https://linearvoid.tv/hls/playlist.m3u8` (keyword: `#EXTM3U`, 5min)
- [ ] Alert on 2 consecutive failures, notify via email + Discord/Slack webhook
## Security
- [x] Implement admin cookie-session auth (HttpOnly, SameSite=Strict, Secure when TLS)
- [x] Implement server-side session expiration
- [x] Implement rate limiting for chat messages (Redis windowing)
- [x] Implement WebSocket connection rate limiting (5/IP/60s)
- [x] Implement admin login rate limiting (5/IP/5min)
- [ ] Enforce HTTPS/WSS at edge and origin
## Test Fixtures
- [x] Create test media fixtures in `backend/playout/testdata/`: valid H.264/AAC, invalid codec, invalid framerate
- [x] Create test playlist fixtures: `valid_playlist.txt`, `empty_playlist.txt`, `comments_only.txt`, `mixed_valid_invalid.txt`
- [x] Document ffmpeg fixture-generation commands in test file headers
## Frontend Tests
### User Frontend
- [x] Video player behavior (play, pause, seek, buffering, error handling) — HLS player startup, native/fallback, URL resolution, error recovery with retries/backoff/reconnecting overlay
- [x] Chat functionality (send/receive messages, presence, rate limiting, moderation) — send, receive, presence join/leave, DM flow, reconnect covered
- [x] State management and data flow
- [x] Responsive design and layout across different screen sizes — chat-collapsed toggle, CSS grid, now-playing aria
- [x] Accessibility testing (keyboard navigation, screen reader support) — video controls, aria-labels, aria-expanded, input labels, reconnecting overlay aria-live/role
- [x] Integration with backend APIs (mocking API responses for testing)
- [ ] Test coverage for all critical frontend components and interactions
- [ ] Add code coverage reporting and enforce minimum coverage threshold in CI/CD pipeline
- [ ] Document test cases and expected outcomes
### Admin Frontend
- [x] Playlist management UI (add/remove videos, reorder, normalize) — save/error handling, sortable rendering
- [ ] User moderation tools (mute/ban/report users, view moderation logs)
- [x] Metrics and monitoring dashboards (Prometheus/Grafana integration) — overview + analytics rendering
- [x] State management and data flow
- [x] Responsive design and layout across different screen sizes — tab content visibility, essential tabs
- [x] Accessibility testing (keyboard navigation, screen reader support) — nav aria-label, active tab aria-current, nav button focusability
- [x] Integration with backend APIs (mocking API responses for testing)
- [ ] Test coverage for all critical admin UI components and interactions
- [ ] Add code coverage reporting and enforce minimum coverage threshold in CI/CD pipeline
- [ ] Document test cases and expected outcomes
@@ -100,29 +45,9 @@
### Cloudflare R2
- [x] Upload test HLS segments and manifests to R2 bucket (SyncToR2 with mock, verified bucket/key/body)
- [x] Verify correct cache headers are applied (immutable segments via segmentCacheControl, short-lived manifests via manifestCacheControl)
- [ ] Test retrieval of segments and manifests from R2 via Cloudflare edge
- [ ] Validate cache hit/miss behavior and performance metrics
- [ ] Validate edge cache hit/miss behavior with repeat requests — script `scripts/validate-edge-cache.sh` created
- [x] Ensure integration test can run in CI/CD pipeline with proper mocking/stubbing of R2 API (putObject mock, no network calls)
- [x] Document test cases and expected outcomes
- [x] Document required stubs or mocks (putObject variable injection pattern)
## Streaming Pipeline
- [x] **Continuous R2 sync during playout (HIGH)**: fsnotify watcher uploads segments in real-time (~7s delay)
- [x] **Globally unique segment naming (HIGH)**: `r-{ISOTIME}/seg-{seq}.m4s` via run ID + ffmpeg monotonic sequence
- [x] **Atomic manifest publication (MEDIUM)**: Upload to `playlist.m3u8.tmp`, then `CopyObject` to final key
- [x] **Manifest Cache-Control alignment (LOW)**: `public, max-age=1, s-maxage=2, must-revalidate`
- [ ] **CORS headers on HLS responses (MEDIUM)**: Needs Cloudflare R2 bucket CORS config (infrastructure, not code)
- [x] **Player error recovery (MEDIUM)**: `hls.on(Hls.Events.ERROR)` handler with bounded retries (5x), exponential backoff, reconnecting overlay
- [x] **Health endpoint detail (LOW)**: Added active item, run ID, playlist revision, last segment seq, last R2 pub, consecutive failures
- [x] Source media contract + validation (H.264/AAC, 30fps, keyframe check)
- [x] fMP4 HLS packaging (6s segments, 24 window)
- [x] Slate fallback + recovery (`shouldEscalateToSlate`)
- [x] R2 cache headers (immutable segments, short-lived manifest)
- [x] Cloudflare Worker immutable `.m4s` headers
## External Services
@@ -162,9 +87,6 @@
### Load Testing
- [x] Load-test scaffolding: `ws-chat.yml`, `admin-api.js`, `run-loadtests.ps1`, summarizers, README
- [x] Cache-hit ratio exporter (`scripts/cache-hit-ratio.js` + `.ps1`)
- [x] Saturation alerts: chat p95 latency, Redis memory usage, cache-hit ratio
- [ ] Execute WebSocket load tests with real infrastructure — ramp 10→100→500→1000 connections, measure p95 latency + errors
- [ ] Execute admin API benchmarks with real infrastructure — `wrk`/`k6` against `/api/admin/*`, target p95 <1s
+169 -80
View File
@@ -4,74 +4,74 @@ Completed tasks from `.github/instructions/TODO.md`. Historical record for CHANG
## Session 2026-08-07 — Unit Tests
- [x] Chat service refactor: `main.go` split into 8 packages (config, auth, db, handlers, ws, media, ratelimit, metrics)
- [x] `db.RedisClient` + `db.PubSub` interfaces defined for mocking
- [x] Config tests: 10 env-var tests, table-driven, 100% coverage
- [x] Auth tests: token generation, session CRUD, cookie security, middleware, 97.8% coverage
- [x] DB tests: SQLite in-memory CRUD for all 4 models, 14.3% coverage
- [x] Handlers tests: 35 tests covering login/logout/session/overview/playlist/moderation/analytics/system/config/users/schedule/media/chat-log, 47.2% coverage
- [x] WebSocket tests: 15 tests covering connect/send/dm/moderation/banned/muted/history/presence/pubsub, 79.6% coverage
- [x] Media tests: formatDuration, extensions, mime, upload IDs, playlist parsing, 36.8% coverage
- [x] Ratelimit tests: message/login/WS rate limits with mock Redis, 90.3% coverage
- [x] Metrics tests: Prometheus output validation, all 9 metrics present, 100% coverage
- [x] Playout tests: 12 tests (LoadPlaylist, keyframeInterval, HealthHandler, slate escalation, reset/activate/deactivate)
- [x] Frontend test framework: vitest + jsdom installed
- [x] User frontend smoke tests: 6 tests (chat toggle, sendMessage, switchRoom, DM toggle)
- [x] Admin frontend smoke tests: 5 tests (DOM structure, upload progress, fingerprint, constants, import stability)
- [x] CI pipeline updated: `go test -race -cover` with 50% threshold
- [x] SQLite driver switched to pure-Go `github.com/glebarez/sqlite` (CGO-free)
- [x] Total: 78 tests across Go + frontend, all passing
- Chat service refactor: `main.go` split into 8 packages (config, auth, db, handlers, ws, media, ratelimit, metrics)
- `db.RedisClient` + `db.PubSub` interfaces defined for mocking
- Config tests: 10 env-var tests, table-driven, 100% coverage
- Auth tests: token generation, session CRUD, cookie security, middleware, 97.8% coverage
- DB tests: SQLite in-memory CRUD for all 4 models, 14.3% coverage
- Handlers tests: 35 tests covering login/logout/session/overview/playlist/moderation/analytics/system/config/users/schedule/media/chat-log, 47.2% coverage
- WebSocket tests: 15 tests covering connect/send/dm/moderation/banned/muted/history/presence/pubsub, 79.6% coverage
- Media tests: formatDuration, extensions, mime, upload IDs, playlist parsing, 36.8% coverage
- Ratelimit tests: message/login/WS rate limits with mock Redis, 90.3% coverage
- Metrics tests: Prometheus output validation, all 9 metrics present, 100% coverage
- Playout tests: 12 tests (LoadPlaylist, keyframeInterval, HealthHandler, slate escalation, reset/activate/deactivate)
- Frontend test framework: vitest + jsdom installed
- User frontend smoke tests: 6 tests (chat toggle, sendMessage, switchRoom, DM toggle)
- Admin frontend smoke tests: 5 tests (DOM structure, upload progress, fingerprint, constants, import stability)
- CI pipeline updated: `go test -race -cover` with 50% threshold
- SQLite driver switched to pure-Go `github.com/glebarez/sqlite` (CGO-free)
- Total: 78 tests across Go + frontend, all passing
## Session 2026-08-07 — Runtime Validation
- [x] Check `monitoring/prometheus/prometheus.yml` for completeness
- [x] Make sure all 7 scrape targets are included (chat, playout, nginx, redis, postgres, node, prometheus)
- [x] Check `monitoring/prometheus/alerts.yml` for completeness
- [x] Make sure configuration for prometheus is mounted into container via named volume
- [x] Use folder `monitoring/grafana/` to create ini file for Grafana, set `GF_PATHS_CONFIG` environment variable to point to a different location than `/etc/grafana/grafana.ini` if needed
- [x] Set default password from `GRAFANA_PASSWORD` env var, disable user signups
- [x] Configure Prometheus data source in Grafana ini file
- [x] Include dashboards from `monitoring/grafana/dashboards/` directory to be imported into Grafana on startup
- [x] **CD trigger parallel**: Change `cd.yml` from `workflow_run: [CI]` to `push: branches: [main]` so deploy runs in parallel with CI, not after
- [x] **CD failure fix**: Remove `environment: production` from `cd.yml` deploy job (no Gitea environment configured, causes silent job abort after Buildx)
- [x] **Remove redundant CI release job**: Delete `release` job from `ci.yml` — fully handled by `release.yml`
- [x] **Tag format**: Use `*.*.*` tags (e.g. `1.0.9`) — `release.yml` pattern is `*.*.*`, not `v*.*.*`. Fix existing `v1.0.x` tags to `1.0.x`
- [x] **Update DEPLOYMENT.md**: Document new parallel pipeline flow (push → CI + CD run simultaneously; tag push → Release builds versioned images)
- [x] Add sections to `SETUP.md` under `## Cloudflare` for detailed instructions:
- [x] DNS: `linearvoid.tv` apex + `www` CNAME → Cloudflare Pages
- [x] DNS: `media.linearvoid.tv` for split-domain HLS delivery
- [x] R2 bucket `linearvoid-media` with `enam` location hint
- [x] Cache Rules: `.ts/.m4s` immutable long TTL (31536000), `.m3u8` short TTL (2s, revalidate)
- [x] Cloudflare Pages: link Gitea repo, build `cd frontend && npm run build`, output `frontend/dist`
- [x] Tiered Cache: enable Smart Tiered Cache, validate `Cf-Cache-Status: HIT` from upper tier
- [x] WAF: enable OWASP Managed Ruleset, set to Simulate → review 48h → Block, enable Bot Fight Mode
- [x] Rate limiting: `/api/admin/login` + `/api/admin/*` → 10 req/10s per IP, Challenge action
- [x] HTTPS/WSS: Always Use HTTPS, HSTS max-age=31536000 includeSubDomains preload, min TLS 1.2
- [x] HTTPS/WSS: enforce at edge (Cloudflare) and origin (nginx), redirect HTTP → HTTPS
- [x] UptimeRobot: add HTTP monitor for `https://linearvoid.tv/health` (5min interval)
- [x] UptimeRobot: add keyword monitor for `https://linearvoid.tv/hls/playlist.m3u8` (keyword: `#EXTM3U`, 5min)
- [x] Alert on 2 consecutive failures, notify via email + Discord/Slack webhook
- [x] Check all known URLs for metrics and their availability:
- [x] Prometheus: `http://<host>:9090` (targets, graph, alerts)
- [x] Grafana: `http://<host>:3001` (default: admin/admin, change via `GRAFANA_PASSWORD`)
- [x] Chat metrics: `http://chat:3000/metrics` from Prometheus container
- [x] Playout metrics: `http://playout:8081/metrics` from Prometheus container
- [x] Nginx metrics: `http://nginx-exporter:9113/metrics` from Prometheus container
- [x] Redis metrics: `http://redis-exporter:9121/metrics` from Prometheus container
- [x] Postgres metrics: `http://postgres-exporter:9187/metrics` from Prometheus container
- [x] Node metrics: `http://node-exporter:9100/metrics` from Prometheus container
- [x] Make sure Prometheus config is mounted into container via named volume `prometheus-config` (not bind mount)
- [x] Make sure Prometheus alerting rules are configured and working (8 rules: ChatDown, PlayoutDown, RedisDown, etc.)
- [x] Verify that all Prometheus targets are up and reporting metrics
- [x] Verify that Grafana dashboards are imported and displaying metrics correctly
- [x] Video normalization: test with MOV, MKV, variable framerate, different audio layouts
- Check `monitoring/prometheus/prometheus.yml` for completeness
- Make sure all 7 scrape targets are included (chat, playout, nginx, redis, postgres, node, prometheus)
- Check `monitoring/prometheus/alerts.yml` for completeness
- Make sure configuration for prometheus is mounted into container via named volume
- Use folder `monitoring/grafana/` to create ini file for Grafana, set `GF_PATHS_CONFIG` environment variable to point to a different location than `/etc/grafana/grafana.ini` if needed
- Set default password from `GRAFANA_PASSWORD` env var, disable user signups
- Configure Prometheus data source in Grafana ini file
- Include dashboards from `monitoring/grafana/dashboards/` directory to be imported into Grafana on startup
- **CD trigger parallel**: Change `cd.yml` from `workflow_run: [CI]` to `push: branches: [main]` so deploy runs in parallel with CI, not after
- **CD failure fix**: Remove `environment: production` from `cd.yml` deploy job (no Gitea environment configured, causes silent job abort after Buildx)
- **Remove redundant CI release job**: Delete `release` job from `ci.yml` — fully handled by `release.yml`
- **Tag format**: Use `*.*.*` tags (e.g. `1.0.9`) — `release.yml` pattern is `*.*.*`, not `v*.*.*`. Fix existing `v1.0.x` tags to `1.0.x`
- **Update DEPLOYMENT.md**: Document new parallel pipeline flow (push → CI + CD run simultaneously; tag push → Release builds versioned images)
- Add sections to `SETUP.md` under `## Cloudflare` for detailed instructions:
- DNS: `linearvoid.tv` apex + `www` CNAME → Cloudflare Pages
- DNS: `media.linearvoid.tv` for split-domain HLS delivery
- R2 bucket `linearvoid-media` with `enam` location hint
- Cache Rules: `.ts/.m4s` immutable long TTL (31536000), `.m3u8` short TTL (2s, revalidate)
- Cloudflare Pages: link Gitea repo, build `cd frontend && npm run build`, output `frontend/dist`
- Tiered Cache: enable Smart Tiered Cache, validate `Cf-Cache-Status: HIT` from upper tier
- WAF: enable OWASP Managed Ruleset, set to Simulate → review 48h → Block, enable Bot Fight Mode
- Rate limiting: `/api/admin/login` + `/api/admin/*` → 10 req/10s per IP, Challenge action
- HTTPS/WSS: Always Use HTTPS, HSTS max-age=31536000 includeSubDomains preload, min TLS 1.2
- HTTPS/WSS: enforce at edge (Cloudflare) and origin (nginx), redirect HTTP → HTTPS
- UptimeRobot: add HTTP monitor for `https://linearvoid.tv/health` (5min interval)
- UptimeRobot: add keyword monitor for `https://linearvoid.tv/hls/playlist.m3u8` (keyword: `#EXTM3U`, 5min)
- Alert on 2 consecutive failures, notify via email + Discord/Slack webhook
- Check all known URLs for metrics and their availability:
- Prometheus: `http://<host>:9090` (targets, graph, alerts)
- Grafana: `http://<host>:3001` (default: admin/admin, change via `GRAFANA_PASSWORD`)
- Chat metrics: `http://chat:3000/metrics` from Prometheus container
- Playout metrics: `http://playout:8081/metrics` from Prometheus container
- Nginx metrics: `http://nginx-exporter:9113/metrics` from Prometheus container
- Redis metrics: `http://redis-exporter:9121/metrics` from Prometheus container
- Postgres metrics: `http://postgres-exporter:9187/metrics` from Prometheus container
- Node metrics: `http://node-exporter:9100/metrics` from Prometheus container
- Make sure Prometheus config is mounted into container via named volume `prometheus-config` (not bind mount)
- Make sure Prometheus alerting rules are configured and working (8 rules: ChatDown, PlayoutDown, RedisDown, etc.)
- Verify that all Prometheus targets are up and reporting metrics
- Verify that Grafana dashboards are imported and displaying metrics correctly
- Video normalization: test with MOV, MKV, variable framerate, different audio layouts
- Evidence: all normalized outputs probed as `h264`, `yuv420p`, `30/1`, `aac`, `48000`, `2ch`.
- [x] Video normalization: verify output plays back-to-back in continuous concat
- Video normalization: verify output plays back-to-back in continuous concat
- Evidence: concat remux reported `CONCAT_OK`; output duration `25.344667s`.
- [x] Viewer SPA: test responsive layout on iOS Safari + Android Chrome
- Viewer SPA: test responsive layout on iOS Safari + Android Chrome
- Evidence: viewport emulation (`390x844`, `412x915`) showed video/chat/toggle present; fixed 8px horizontal overflow in `frontend/src/style.css` (`100vw` -> `100%`).
- Limitation: no physical-device Safari/Chrome test in this run.
- [x] Redis: test restart, verify sessions + presence data survive
- Redis: test restart, verify sessions + presence data survive
- Evidence: admin session HTTP `200` before/after Redis restart; seeded presence key visible before/after restart; Redis health returned to `healthy`.
### Runtime Tests (all 4)
@@ -84,15 +84,15 @@ Completed tasks from `.github/instructions/TODO.md`. Historical record for CHANG
### Documentation Polish
- [x] API.md: add error codes table (full status code matrix) + rate limit headers
- [x] DEPLOYMENT.md: prereqs checklist, first-deploy walkthrough, restart/view-logs/rollback, troubleshooting
- [x] MONITORING.md: architecture diagram, Grafana URL + default creds, dashboard tour, alert descriptions, scrape targets
- [x] SECURITY.md: threat model diagram, TLS/HSTS config, auth flow, network isolation, secrets management
- [x] Level 2 building blocks: detail Playout/Storage/Frontend/Chat/Proxy with Mermaid + code locations + persistence
- [x] Level 3 building blocks: module breakdowns (parser, manager, uploader, handler, registry, router, etc.)
- [x] Recovery runbook: document quarterly disaster recovery drill
- API.md: add error codes table (full status code matrix) + rate limit headers
- DEPLOYMENT.md: prereqs checklist, first-deploy walkthrough, restart/view-logs/rollback, troubleshooting
- MONITORING.md: architecture diagram, Grafana URL + default creds, dashboard tour, alert descriptions, scrape targets
- SECURITY.md: threat model diagram, TLS/HSTS config, auth flow, network isolation, secrets management
- Level 2 building blocks: detail Playout/Storage/Frontend/Chat/Proxy with Mermaid + code locations + persistence
- Level 3 building blocks: module breakdowns (parser, manager, uploader, handler, registry, router, etc.)
- Recovery runbook: document quarterly disaster recovery drill
- Evidence: expanded `docs/incident-runbook.md` with prerequisites, simulation steps, validation checks, and pass/fail criteria.
- [x] Recovery runbook: rehearse quarterly disaster recovery drill
- Recovery runbook: rehearse quarterly disaster recovery drill
- Manual step: execute full drill on target environment and capture RTO/RPO evidence.
## Session 2026-08-06 — Documentation Adjustments, Playout, Normalization, Chat, Viewer SPA, Admin, Docker, CI/CD
@@ -975,12 +975,12 @@ Notes:
Completed 6 of 7 streaming pipeline gaps from STREAMING.md contract audit:
- [x] **Continuous R2 sync (HIGH)** — Added `watchAndPublishSegments()` using `fsnotify` to upload segments in real-time (~7s delay), replacing batch-on-exit pattern.
- [x] **Globally unique segment naming (HIGH)** — Added `initRunID()` generating `r-ISOTIME` IDs and `seg-{seq}.m4s` monotonic naming. Replaced `strftime` collision-prone pattern.
- [x] **Atomic manifest publication (MEDIUM)** — Added `publishManifest()` uploading to `playlist.m3u8.tmp` then `CopyObject` to final key.
- [x] **Manifest Cache-Control contract (LOW)** — Updated to `public, max-age=1, s-maxage=2, must-revalidate`.
- [x] **Player error recovery (MEDIUM)** — Added `hls.on(ERROR)` handler with bounded retries (5x), exponential backoff (2s→32s), reconnecting overlay with spinner.
- [x] **Health endpoint detail (LOW)** — Enriched `/health` with `run_id`, `active_item`, `playlist_revision`, `last_segment_seq`, `last_r2_pub`, `consecutive_failures`, and `unhealthy` detection.
- **Continuous R2 sync (HIGH)** — Added `watchAndPublishSegments()` using `fsnotify` to upload segments in real-time (~7s delay), replacing batch-on-exit pattern.
- **Globally unique segment naming (HIGH)** — Added `initRunID()` generating `r-ISOTIME` IDs and `seg-{seq}.m4s` monotonic naming. Replaced `strftime` collision-prone pattern.
- **Atomic manifest publication (MEDIUM)** — Added `publishManifest()` uploading to `playlist.m3u8.tmp` then `CopyObject` to final key.
- **Manifest Cache-Control contract (LOW)** — Updated to `public, max-age=1, s-maxage=2, must-revalidate`.
- **Player error recovery (MEDIUM)** — Added `hls.on(ERROR)` handler with bounded retries (5x), exponential backoff (2s→32s), reconnecting overlay with spinner.
- **Health endpoint detail (LOW)** — Enriched `/health` with `run_id`, `active_item`, `playlist_revision`, `last_segment_seq`, `last_r2_pub`, `consecutive_failures`, and `unhealthy` detection.
- [ ] CORS on `media.linearvoid.tv` — Remaining infrastructure-only (Cloudflare R2 CORS configuration, not code).
**Files changed:** `backend/playout/src/playlist.go` (+171/-35), `playlist_test.go` (+51/-7), `go.mod` (+2), `frontend/src/main.ts` (+76/-2), `main.test.ts` (+43/-1), `style.css` (+37), `index.html` (+5).
@@ -990,11 +990,100 @@ Completed 6 of 7 streaming pipeline gaps from STREAMING.md contract audit:
Completed Phase 2 (viewer) and Phase 3 (admin) responsive/accessibility tests + coverage:
- [x] **Viewer accessibility (5 tests):** video controls/autoplay/playsinline attributes, chat panel aria-labels (live region + panel label), chat toggle aria-expanded state, message input label association, reconnecting overlay aria-live=assertive + role=alert.
- [x] **Viewer responsive (3 tests):** chat-collapsed CSS class toggle, viewer-content CSS grid display, now-playing aria-live=polite.
- [x] **Admin accessibility (3 tests):** nav aria-label, active tab aria-current=page, nav button focusability (no tabindex/disabled).
- [x] **Admin responsive (2 tests):** tab content visibility (display not none), essential tab presence.
- [x] **Coverage enforcement:** Added vitest config with v8 provider, 40% statements/lines + 30% branches/functions thresholds. Added `test:coverage` script and CI step.
- **Viewer accessibility (5 tests):** video controls/autoplay/playsinline attributes, chat panel aria-labels (live region + panel label), chat toggle aria-expanded state, message input label association, reconnecting overlay aria-live=assertive + role=alert.
- **Viewer responsive (3 tests):** chat-collapsed CSS class toggle, viewer-content CSS grid display, now-playing aria-live=polite.
- **Admin accessibility (3 tests):** nav aria-label, active tab aria-current=page, nav button focusability (no tabindex/disabled).
- **Admin responsive (2 tests):** tab content visibility (display not none), essential tab presence.
- **Coverage enforcement:** Added vitest config with v8 provider, 40% statements/lines + 30% branches/functions thresholds. Added `test:coverage` script and CI step.
**Files:** `frontend/src/main.test.ts` (+65), `frontend/src/admin.test.ts` (+55), `frontend/vitest.config.ts` (+12), `frontend/package.json` (+1), `.gitea/workflows/ci.yml` (+3).
**Total:** 44 frontend tests (31 existing + 13 new).
## Session 2026-08-13 — Plan consolidation (TODO → DONE)
Completed tasks moved out of `.github/instructions/TODO.md`. Remaining work replanned in TODO from `BACKLOG.md`.
### Phase 1 — playout test fixtures + source-media contract
- Create small compliant fixture media in `backend/playout/testdata/` for the validator and playout tests
- Create invalid fixture media for codec, framerate, and audio rejection paths
- Create playlist fixtures for empty, comment-only, mixed-validity, and valid playlists
- Document the ffmpeg fixture-generation commands in the relevant test headers
### Phase 2 — viewer frontend coverage
- HLS player startup, manifest resolution, and cross-origin/local fallback tests
- Chat send/receive, presence updates, private DM flow, and reconnect tests
- Responsive-layout and accessibility checks for the viewer shell and chat sidebar
- Coverage reporting added and minimum threshold enforced in CI
### Phase 3 — admin frontend coverage
- Playlist management, moderation flows, and dashboard rendering tests
- API integration points with mocked backend responses
- Responsive-layout and accessibility checks for admin screens
- Coverage reporting added and minimum threshold enforced in CI
### Phase 4 — R2 and edge-delivery validation
- Integration test for upload and retrieval of HLS objects through the R2 path
- Segment and manifest cache headers verified (immutable media, live manifest freshness)
- Edge cache hit/miss validation scripted (`scripts/validate-edge-cache.sh`)
- Stubs/mocks documented (putObject variable-injection pattern)
### Phase 7 (partial) — load-test scaffolding + saturation alerts
- WebSocket load-test scaffolding and summary artifacts (`ws-chat.yml`, `run-loadtests.ps1`, `summarize-artillery.ps1`)
- Admin API benchmark scaffolding and endpoint summary artifacts (`admin-api.js`, `run-loadtests.ps1`, `summarize-k6.ps1`)
- Saturation thresholds added to monitoring (chat p95 latency + Redis memory usage)
- Cache-hit threshold alert operationalization added to monitoring
### Streaming pipeline gaps (audit → implemented)
- Continuous R2 sync during playout (fsnotify watcher, ~7s delay)
- Globally unique segment naming with run ID (`r-{ISOTIME}/seg-{seq}.m4s`)
- Atomic manifest publication (upload to `.tmp`, then CopyObject)
- Manifest Cache-Control: `public, max-age=1, s-maxage=2, must-revalidate`
- Player error recovery (bounded retries, exponential backoff, reconnecting overlay)
- Health endpoint detail (run ID, active item, playlist revision, segment seq, R2 pub, consecutive failures)
- CORS headers — documented as Cloudflare R2 infrastructure config
- Phase 4 edge cache validation — `scripts/validate-edge-cache.sh` created
## Session 2026-08-13 — Playout Enhancements
Completed the three Playout enhancement workstreams from `TODO.md`. Remaining polish items kept in TODO.
### 1. Automatic normalization of source media on upload (H.264/AAC, 30fps, keyframe check)
- Implement `media.NormalizeVideo` in `backend/chat/src/media/media.go` (ffmpeg exec, Linear Void profile, bounded 2h timeout, stderr capture)
- Write normalized output to `*.normalized.mp4` and re-verify with ffprobe (`ValidateNormalization` + `ProbeDurationSeconds`)
- Extend media status states: `normalizing`, `normalization_failed` (alongside `ready` / `needs_normalization`)
- Integrate into single-upload path — `HandleAdminMedia`: persist `needs_normalization` then `go normalizeMediaRecord(...)`
- Integrate into chunked-upload completion — `HandleAdminMediaUploads` `"complete"`: same async flow after assembly + validation
- Add `POST /api/admin/media/{id}/normalize` (behind `requireAdminAuth`, returns 202 + `normalizing` record)
- Admin UI (`admin.ts`): "Normalize" action + status polling until `ready`/`normalization_failed`
- Tests: `NormalizeVideo` args + ffmpeg-error path; handler `normalizeMediaRecord` success/failure; `/normalize` endpoint 202
- Retire hardcoded `backend/playout/normalize-video.sh` (superseded by in-Go normalization) - file deleted
### 2. R2 data retention: delete old segments after 24h
- Add `cleanupOldSegments(ctx)` to `backend/playout/src/playlist.go`
- Use `s3.ListObjectsV2` on prefix `hls/` to enumerate run directories
- Delete `hls/r-*/*` older than `now - R2_SEGMENT_RETENTION_HOURS` (default 24)
- Never delete `hls/playlist.m3u8` or `hls/playlist.m3u8.tmp`
- Run cleanup as a ticker goroutine (hourly) plus once at startup
- Add `R2_SEGMENT_RETENTION_HOURS` (default `24`) to `.env.example`
- Expose `linearvoid_playout_r2_cleanup_objects_total` metric and log deleted keys
- Tests: mock `ListObjectsV2`/`DeleteObjects` via injectable vars (mirror the existing `putObject` pattern)
### 3. Continuous playout: confirm ffmpeg loops without interruption
- Resolve the dual-loop ambiguity between `main()` and `start-playout.sh`
- Add a `-validate` mode to `main()`: run `ValidatePlaylist` and exit 0/1 without the ffmpeg loop
- Update `start-playout.sh` to invoke `-validate` and `exec` the Go binary (single source of truth)
- Minimize the restart gap: skip full `resetHLSOutput` when the playlist is unchanged
- Track `playlistRevision` (SHA-256 fingerprint); reset only when the playlist changed, on crash/slate recovery, or first run
- Continue segment numbering via `-start_number` + prune stale segments (`hlsNextStartNumber`)
- Verify `init.mp4` + manifest are correctly published on clean completion (existing `publishManifest` path runs after every run)
- Add metrics `linearvoid_playout_loops_total` (clean completions) and `linearvoid_playout_run_duration_seconds`
- Tests: `TestPlaylistFingerprint`, `TestHLSNextStartNumber`
+64 -54
View File
@@ -1,80 +1,90 @@
# TODO
> Completed tasks moved to [DONE](./DONE.md). Full remaining task list in [BACKLOG](./BACKLOG.md).
> Completed tasks moved to [DONE](./DONE.md). Remaining tasks in [BACKLOG](./BACKLOG.md).
## Plan
## Enhancements
### Phase 1: unblock playout test fixtures and lock down the source-media contract
### Application configuration
- [x] Create small compliant fixture media in `backend/playout/testdata/` for the validator and playout tests
- [x] Create invalid fixture media for codec, framerate, and audio rejection paths
- [x] Create playlist fixtures for empty, comment-only, mixed-validity, and valid playlists
- [x] Document the ffmpeg fixture-generation commands in the relevant test headers
- [ ] Configuration: create simple getter/setter for configuration values (e.g. `MATOMO_URL`, `MATOMO_SITE_ID`) in `backend/admin/config.go`
- [ ] Configuration: expose configuration values to frontend via `/api/admin/config` endpoint
- [ ] Configuration: make configuration values editable:
- [ ] HLS_SEGMENT_DURATION
- [ ] HLS_WINDOW_SIZE
- [ ] MATOMO_URL
- [ ] MATOMO_SITE_ID
### Phase 2: finish frontend coverage for the live viewer experience
### Admin frontend
- [x] Add tests for HLS player startup, manifest resolution, and cross-origin/local fallback behavior
- [x] Add tests for chat send/receive, presence updates, private DM flow, and reconnect behavior
- [x] Add responsive-layout and accessibility checks for the viewer shell and chat sidebar
- [x] Add coverage reporting and enforce a minimum threshold in CI
- [ ] Playlist: show `needs_normalization` badge and block/queue playlist-add until `ready`
- [ ] Playlist: show `ready` badge and allow playlist-add when `ready`
- [ ] Extend `system.html` with a section for configuration settings on top of page (HLS segment duration, window size, Matomo URL/site ID)
- [ ] Add functions to `admin.ts` to call `/api/admin/config` for getting and updating configuration values
### Phase 3: finish frontend coverage for the admin experience
### User frontend
- [x] Add tests for playlist management, moderation flows, and dashboard rendering
- [x] Add tests for API integration points with mocked backend responses
- [x] Add responsive-layout and accessibility checks for admin screens
- [x] Add coverage reporting and enforce a minimum threshold in CI
- [ ] Player frontend: Embed JavaScript snippet in `index.html` to send pageview events to Matomo
### Phase 4: validate the R2 and edge-delivery path
### Chat user features
- [x] Add an integration test for upload and retrieval of HLS objects through the R2 path
- [x] Verify segment and manifest cache headers, including immutable media and live manifest freshness
- [x] Validate edge cache hit/miss behavior with repeat requests
- [x] Document any required stubs or mocks so the test can run in CI (putObject mock pattern documented in test)
- [ ] Chat: add user registration and login flows
- [ ] Chat: private DM history and search
- [ ] Chat UI: add login/logout button, show username in chat, show/toggle user list, show/toggle online status, show typing indicator
### Monitoring
- [ ] Prometheus TSDB retention policy: delete metrics older than 30d
## Setup phases remaining
### Phase 5: complete manual Cloudflare and external-service setup
- [ ] Apply Cloudflare Pages, Tiered Cache, WAF, rate limiting, and HTTPS/HSTS settings in the dashboard
- [ ] Create or verify UptimeRobot monitors for health and HLS manifest availability
- [ ] Cloudflare Pages: link custom domain and cut over frontend hosting
- [ ] Enable Smart Tiered Cache; validate `Cf-Cache-Status: HIT` from upper tier
- [ ] WAF: enable OWASP Managed Ruleset (Simulate → review 48h → Block), enable Bot Fight Mode
- [ ] Rate limiting: `/api/admin/login` + `/api/admin/*` → 10 req/10s per IP, Challenge action
- [ ] HTTPS/WSS: Always Use HTTPS, HSTS max-age=31536000 includeSubDomains preload, min TLS 1.2; enforce at edge and origin
- [ ] UptimeRobot: add HTTP monitor for `https://linearvoid.tv/health` (5min interval)
- [ ] UptimeRobot: add keyword monitor for `https://linearvoid.tv/hls/playlist.m3u8` (keyword `#EXTM3U`, 5min)
- [ ] UptimeRobot: alert on 2 consecutive failures via email + Discord/Slack webhook
- [ ] Capture evidence in `docs/operations/cloudflare-uptimerobot-checklist.md`
### Phase 6: rehearse production deployment end to end
- [ ] Provision the Hetzner host and prepare the Docker/runtime baseline
- [ ] Configure production secrets, remote deployment, and DNS cutover
- [ ] Run health, HLS, chat, and admin smoke tests against production-like infrastructure
- [ ] Verify backup creation, restore, and integrity checks on the production host
- [ ] Provision Hetzner CX22 (2 vCPU, 4 GB, 40 GB) in Ashburn VA, Ubuntu 24.04
- [ ] Install Docker Engine + Compose plugin; open firewall: 80/443 public, 2375 runner-only
- [ ] Configure Docker daemon for remote TLS access (`-H tcp://0.0.0.0:2376` with certs)
- [ ] Create `/srv/linearvoid/data/{playlist,media,hls,chat,slate}` directories
- [ ] Add `CI_PROD_*` secrets to Gitea (DOCKER_HOST, R2 keys, admin password, DB password, Grafana password)
- [ ] Trigger CD workflow with `DOCKER_HOST` pointing to Hetzner IP
- [ ] Verify all 11 containers healthy: `/health`, `/hls/playlist.m3u8`, WebSocket
- [ ] Update Cloudflare A record `linearvoid.tv` → Hetzner IP, SSL Full (strict); test HTTPS + WSS
- [ ] Smoke test: viewer SPA loads, HLS plays, chat connects, admin login → upload → playlist → playout
- [ ] Set up Prometheus alerting notifications + UptimeRobot on production URL
- [ ] Run `scripts/backup-db.sh` + `scripts/verify-backups.sh` on production host
- [ ] Document production IP, SSH access, rollback procedure, scaling thresholds
### Phase 7: baseline scaling and performance
- [ ] Measure manifest TTFB and cache-hit distribution from multiple US regions
- [x] Prepare WebSocket load-test scaffolding and summary artifacts (`ws-chat.yml`, `run-loadtests.ps1`, `summarize-artillery.ps1`)
- [ ] Execute WebSocket throughput and chat-latency benchmarks under growing connection counts
- [x] Prepare Admin API benchmark scaffolding and endpoint summary artifacts (`admin-api.js`, `run-loadtests.ps1`, `summarize-k6.ps1`)
- [ ] Execute Admin API benchmarks and identify slow paths
- [x] Add saturation thresholds to monitoring (chat p95 latency + Redis memory usage)
- [x] Add cache-hit threshold alert operationalization to monitoring
- [ ] Execute WebSocket load tests — ramp 10→100→500→1000 connections; measure p95 latency + errors
- [ ] Execute admin API benchmarks — `wrk`/`k6` against `/api/admin/*`; target p95 <1s
- [ ] Measure manifest TTFB and cache-hit distribution from multiple US regions (WebPageTest 3+ regions, <4s warm cache)
- [ ] Tune manifest TTL to 0.5× segment duration; validate `Cf-Cache-Status` distribution
- [ ] Track cache-hit ratio and R2 ops in production (Cloudflare GraphQL → Grafana panel, alert <90%)
- [ ] Capacity triggers: Prometheus rules for cache-miss >10%/15min, CPU >80%/10min
- [ ] Plan Phase 2 service separation (playout CPU, chat latency, DB state, monitoring)
- [ ] Plan Phase 3 multi-origin resilience (second Hetzner location, warm standby, DNS failover, DB/Redis replication)
## Status Update (2026-08-10)
### Phase 8: close out remaining test and validation gaps
- [x] Phase 1 (test fixtures) complete.
- [x] Phase 2 (viewer frontend): player startup, URL resolution, chat send/receive, reconnect, presence, DM flow, message rendering all covered (27 tests).
- [x] Phase 3 (admin frontend): dashboard rendering, API integration, playlist management all covered (31 tests total).
- [x] Phase 4 (R2 validation) partially complete: SyncToR2 cache headers + integration tests done; edge cache hit/miss remains manual.
- [ ] Phase 5 (Cloudflare): Pages deploy workflow (`.gitea/workflows/pages-deploy.yml`) created; Pages/Workers setup docs written; manual dashboard tasks remain.
- [ ] Phase 6 (production deployment) pending: Hetzner provisioning, secrets, DNS cutover, smoke tests, backup/restore.
- [ ] Phase 7: Loadtest scaffolding complete; `run-loadtests.ps1` auto-generates ws-chat-summary.txt and admin-api-endpoint-summary.txt; live benchmark execution pending.
- Version tagged 1.0.11; go vet clean.
- [ ] Admin frontend: user moderation tools tests (mute/ban/report, view moderation logs)
- [ ] Frontend: coverage for all critical user + admin components and interactions
- [ ] Frontend: document test cases and expected outcomes
- [ ] Backend R2: test retrieval of segments/manifests via Cloudflare edge
- [ ] Backend R2: validate edge cache hit/miss behavior with repeat requests (`scripts/validate-edge-cache.sh`)
## Streaming Pipeline Gaps (2026-08-10 audit → 2026-08-10 implemented)
### Playout
STREAMING.md contract vs playlist.go/main.ts implementation. See Backlog for full details.
- [x] Continuous R2 sync during playout (HIGH) — implemented: fsnotify watcher uploads segments within ~7s
- [x] Globally unique segment naming with run ID (HIGH) — implemented: `r-{ISOTIME}/seg-{seq}.m4s`
- [x] Atomic manifest publication with temp key (MEDIUM) — implemented: upload to .tmp, then CopyObject
- [x] Manifest Cache-Control: add must-revalidate + s-maxage split (LOW) — implemented: `public, max-age=1, s-maxage=2, must-revalidate`
- [ ] CORS headers on HLS responses from media.linearvoid.tv (MEDIUM) — infrastructure, needs Cloudflare R2 CORS config
- [x] Player error recovery: retries, backoff, reconnecting state (MEDIUM) — implemented: hls.on(ERROR) handler with exponential backoff
- [x] Health endpoint: add active item, revision, run ID, segment info (LOW) — implemented: full STREAMING.md contract fields
- [x] Phase 4 edge cache validation — script `scripts/validate-edge-cache.sh` created
- [ ] Configure an R2 lifecycle rule on the `hls/` prefix as defense-in-depth (manual)
- [ ] Full-loop E2E (2-file playlist, assert `Looping...` with no `CRASHED` across the boundary) — manual via `scripts/test-playout-e2e.sh`