# DONE > Completed tasks from [TODO](./TODO.md). Product, feature, and release history lives in [CHANGELOG](./CHANGELOG.md); this file keeps only engineering and ops notes not captured there. ## Manifest run-scoped rewrite — candidate (a) (2026-09-03) - Playout now publishes the live R2 manifest with run-scoped media URIs. `rewriteManifestToRun` in `backend/playout/src/playlist.go` prefixes each bare relative URI (`seg-*.m4s` / `init.mp4`, plus `#EXT-X-MAP:URI`) with `r-{runID}/` before `publishManifest`'s atomic `.tmp` → `CopyObject` commit; skipped in slate mode. Added a stubbable `copyObject` hook for tests (consistent with `putObject`/`listObjects`/`deleteObjects`). - Tests: `playlist_rewrite_test.go` — `TestRewriteManifestToRun`, `TestRewriteManifestToRun_AlreadyPrefixedUnchanged`, `TestRewriteManifestToRun_HandlesTSAndComments`, `TestRewriteManifestToRun_EmptyRunID`, `TestIsMediaURI`, `TestPublishManifest_RewritesToRunScopedURIs`, `TestPublishManifest_SlateNotRewritten`. - Docs updated: `STREAMING.md`, `operations/cloudflare-workers-setup.md`, `architecture/README.md` (incl. Level-2 diagram showing the R2-bound `linearvoid-media` Worker and run-scoped layout). - **Still pending (in TODO): live Validation** — deploy the updated playout to the test lab and confirm manifest/segment URIs resolve via `media.linearvoid.tv`. ## Test infrastructure & refactors (2026-08-07) - Chat service `main.go` split into 8 packages (config, auth, db, handlers, ws, media, ratelimit, metrics); `db.RedisClient` + `db.PubSub` interfaces added for mocking. - 78 tests total (Go + frontend), all passing. Coverage: auth 97.8%, config 100%, metrics 100%, ratelimit 90.3%, ws 79.6%, handlers 47.2%, media 36.8%, db 14.3%. - Playout: 12 tests. Frontend: vitest + jsdom, 6 user + 5 admin smoke tests. - CI: `go test -race -cover` with a 50% coverage threshold; SQLite switched to pure-Go `github.com/glebarez/sqlite` (CGO-free, works on Windows). - Frontend coverage enforcement: vitest v8 provider, 40% statements/lines + 30% branches/functions thresholds, `test:coverage` script + CI step. ## Backups - `scripts/backup-db.sh` (daily pg_dump → R2, 14-day retention), `scripts/backup-weekly.sh` (weekly full dump + integrity check, 8-week retention), `scripts/verify-backups.sh` (download → temp Postgres → restore + row counts). ## Gitea self-hosting migration - Migrated repo + CI/CD from GitHub to Gitea Actions (`git.allucanget.biz/zwitschi/linearvoid`): self-hosted runner, Container Registry convention `git.allucanget.biz/zwitschi/linearvoid/:`, secrets renamed `GITEA_*` → `CI_*`. ## Streaming pipeline gaps (2026-08-10) - Continuous R2 sync via `fsnotify` (`watchAndPublishSegments`, ~7s delay); run-ID + monotonic segment naming (`r-{ISOTIME}/seg-{seq}.m4s`); atomic manifest publication (upload `.tmp` → `CopyObject`); enriched `/health` (run_id, active_item, playlist_revision, last_segment_seq, last_r2_pub, consecutive_failures); player error recovery (5 retries, 2s→32s exponential backoff, reconnecting overlay). - CORS on `media.linearvoid.tv` remains infrastructure-only (Cloudflare R2 config, not code). ## Bug fixes not in CHANGELOG - **2026-08-13 viewer crash**: `index.html` was missing the `#messages` container, so `addMessage` threw `Cannot read properties of null (reading 'appendChild')` on WebSocket open/message. Restored the chat panel structure and made `addMessage` defensive. - **2026-08-15 chat reconnect loop**: after a registered user logged in, the chat panel repeatedly filled with old messages. `connectWebSocket` left the replaced socket's `onclose` handler attached, so each intentional reconnect scheduled another reconnect and every connection re-pulled the room's 50-message history on a 3s loop. Fixed by detaching the old socket's `onclose` before closing and guarding the handler against stale sockets. Regression test added; `npm --prefix frontend test` → 58 tests passing. ## Chat improvements - Manual smoke: viewer join/leave shows a 3s flash (not a chat row); reconnect <5s shows no duplicate; admin chat-log table has no system rows ## Playout improvements - Test in Firefox: live playback, rebuffer/recovery, chat sync; record quirks in `docs/STREAMING.md` - Rebuild + run playout; confirm `playlist.m3u8` lists ~12 segments at 2s each - Manual: verify viewer latency and cache-hit behavior after the change ## Chat sidebar (2026-08-17) - [x] remove search bar from chat sidebar (not needed for single-room) - [x] remove "user" toggle button from chat sidebar (hide for now, add back when user interface is stable) - [x] make login/registration form a modal and only toggle it when user is not logged in and clicks "Login" or "Register" - [x] when user is logged in, do not show login/registration form - [x] make sure each message is only rendered once in the chat log (no duplicates on reconnect or initial load) ### Plan Files: `frontend/index.html`, `frontend/src/main.ts`, `frontend/src/style.css`. 1. **Remove search bar** (single-room) - `index.html`: delete the `
` block. - `main.ts`: remove `searchDM()` and the `dm-search-btn` listener; drop the `#dm-search-row` show/hide logic in `switchRoom()` (the `searchRow.hidden = !isDM` lines). Keep `currentDMTarget` only if the "New DM" flow stays. 2. **Remove "Users" toggle button** - `index.html`: delete the `