Compare commits
3
Commits
cef002862e
...
06b277e9c1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06b277e9c1 | ||
|
|
1cc44282bc | ||
|
|
b6705e6dbd |
No files matched your search
@@ -2,6 +2,60 @@
|
|||||||
|
|
||||||
> All current work is planned in [TODO](./TODO.md) (2026-08-13). This file lists backlog items; new unplanned work is added here.
|
> All current work is planned in [TODO](./TODO.md) (2026-08-13). This file lists backlog items; new unplanned work is added here.
|
||||||
|
|
||||||
|
## 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
|
||||||
|
- [ ] 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 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
|
||||||
|
|
||||||
|
- [ ] 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)
|
||||||
|
|
||||||
|
### Phase 8: close out remaining test and validation gaps
|
||||||
|
|
||||||
|
- [ ] 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`)
|
||||||
|
|
||||||
|
### Playout
|
||||||
|
|
||||||
|
- [ ] 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`
|
||||||
|
|
||||||
## Cloudflare Configuration
|
## Cloudflare Configuration
|
||||||
|
|
||||||
### DNS & Pages
|
### DNS & Pages
|
||||||
@@ -27,28 +81,6 @@
|
|||||||
- [ ] UptimeRobot: add keyword monitor for `https://linearvoid.tv/hls/playlist.m3u8` (keyword: `#EXTM3U`, 5min)
|
- [ ] 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
|
- [ ] Alert on 2 consecutive failures, notify via email + Discord/Slack webhook
|
||||||
|
|
||||||
## Frontend Tests
|
|
||||||
|
|
||||||
### User Frontend
|
|
||||||
|
|
||||||
- [ ] 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
|
|
||||||
|
|
||||||
- [ ] 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
|
|
||||||
|
|
||||||
## Backend Integration Tests
|
|
||||||
|
|
||||||
### Cloudflare R2
|
|
||||||
|
|
||||||
- [ ] 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
|
|
||||||
|
|
||||||
## External Services
|
## External Services
|
||||||
|
|
||||||
> Evidence tracker: `docs/operations/cloudflare-uptimerobot-checklist.md`
|
> Evidence tracker: `docs/operations/cloudflare-uptimerobot-checklist.md`
|
||||||
@@ -102,12 +134,3 @@
|
|||||||
- [ ] Capacity triggers: Prometheus rules for cache-miss >10%/15min, CPU >80%/10min
|
- [ ] Capacity triggers: Prometheus rules for cache-miss >10%/15min, CPU >80%/10min
|
||||||
- [ ] Plan Phase 2 service separation — document boundaries (playout CPU, chat latency, DB state, monitoring), estimate per-service resources
|
- [ ] Plan Phase 2 service separation — document boundaries (playout CPU, chat latency, DB state, monitoring), estimate per-service resources
|
||||||
- [ ] Plan Phase 3 multi-origin resilience — research second Hetzner location, design warm standby + DNS failover + DB/Redis replication
|
- [ ] Plan Phase 3 multi-origin resilience — research second Hetzner location, design warm standby + DNS failover + DB/Redis replication
|
||||||
|
|
||||||
## Feature List (Future Enhancements)
|
|
||||||
|
|
||||||
- [ ] Admin backend
|
|
||||||
- [ ] Analytics: implement a settings entry to set matomo url and page id
|
|
||||||
- [ ] User frontend
|
|
||||||
- [ ] Chat: add user registration and login flows
|
|
||||||
- [ ] Chat: private DM history and search
|
|
||||||
- [ ] Analytics: add javascript snippet to send pageview events to matomo
|
|
||||||
+23
-1168
File diff suppressed because it is too large.
Load diff
@@ -2,66 +2,28 @@
|
|||||||
|
|
||||||
> Completed tasks moved to [DONE](./DONE.md). Remaining tasks in [BACKLOG](./BACKLOG.md).
|
> Completed tasks moved to [DONE](./DONE.md). Remaining tasks in [BACKLOG](./BACKLOG.md).
|
||||||
|
|
||||||
## Chat improvements (remaining)
|
## R2 Data Retention
|
||||||
|
|
||||||
- [ ] 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
|
- [ ] Adjust internal data retention for R2: remove folders /hls/r-{ISOTIME} after next run, keep only the last 2 runs or max. 10GB of data
|
||||||
|
|
||||||
## Playout improvements (remaining)
|
## Frontend Tests
|
||||||
|
|
||||||
- [ ] Test in Firefox: live playback, rebuffer/recovery, chat sync; record quirks in `docs/STREAMING.md`
|
### User Frontend
|
||||||
- [ ] Rebuild + run playout; confirm `playlist.m3u8` lists ~12 segments at 2s each
|
|
||||||
- [ ] Manual: verify viewer latency and cache-hit behavior after the change
|
|
||||||
|
|
||||||
## Setup phases remaining
|
- [ ] 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
|
||||||
|
|
||||||
### Phase 5: complete manual Cloudflare and external-service setup
|
### Admin Frontend
|
||||||
|
|
||||||
- [ ] Apply Cloudflare Pages, Tiered Cache, WAF, rate limiting, and HTTPS/HSTS settings in the dashboard
|
- [ ] Test coverage for all critical admin UI components and interactions
|
||||||
- [ ] Cloudflare Pages: link custom domain and cut over frontend hosting
|
- [ ] Add code coverage reporting and enforce minimum coverage threshold in CI/CD pipeline
|
||||||
- [ ] Enable Smart Tiered Cache; validate `Cf-Cache-Status: HIT` from upper tier
|
- [ ] Document test cases and expected outcomes
|
||||||
- [ ] 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
|
## Backend Integration Tests
|
||||||
|
|
||||||
- [ ] Provision Hetzner CX22 (2 vCPU, 4 GB, 40 GB) in Ashburn VA, Ubuntu 24.04
|
### Cloudflare R2
|
||||||
- [ ] 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
|
- [ ] Test retrieval of segments and manifests from R2 via Cloudflare edge
|
||||||
|
- [ ] Validate cache hit/miss behavior and performance metrics
|
||||||
- [ ] Execute WebSocket load tests — ramp 10→100→500→1000 connections; measure p95 latency + errors
|
- [ ] Validate edge cache hit/miss behavior with repeat requests — script `scripts/validate-edge-cache.sh` created
|
||||||
- [ ] 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)
|
|
||||||
|
|
||||||
### Phase 8: close out remaining test and validation gaps
|
|
||||||
|
|
||||||
- [ ] 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`)
|
|
||||||
|
|
||||||
### Playout
|
|
||||||
|
|
||||||
- [ ] 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`
|
|
||||||
@@ -253,6 +253,25 @@ describe("WebSocket reconnect", () => {
|
|||||||
expect(websocketInstances).toHaveLength(2);
|
expect(websocketInstances).toHaveLength(2);
|
||||||
expect(websocketInstances[1].url).toContain("room=lobby");
|
expect(websocketInstances[1].url).toContain("room=lobby");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not reconnect from a socket that was intentionally replaced", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
|
||||||
|
window.switchRoom("lobby");
|
||||||
|
expect(websocketInstances).toHaveLength(1);
|
||||||
|
const replaced = websocketInstances[0];
|
||||||
|
|
||||||
|
// A room switch intentionally replaces the socket; the old socket must not
|
||||||
|
// schedule its own reconnect, otherwise history is re-pulled on a loop.
|
||||||
|
window.switchRoom("main");
|
||||||
|
expect(websocketInstances).toHaveLength(2);
|
||||||
|
expect(replaced.onclose).toBeNull();
|
||||||
|
|
||||||
|
replaced.onclose?.();
|
||||||
|
await vi.advanceTimersByTimeAsync(3000);
|
||||||
|
|
||||||
|
expect(websocketInstances).toHaveLength(2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("URL resolution", () => {
|
describe("URL resolution", () => {
|
||||||
|
|||||||
+17
-5
@@ -79,19 +79,28 @@ export function resolveManifestUrl() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function connectWebSocket(room: string) {
|
function connectWebSocket(room: string) {
|
||||||
if (ws) ws.close();
|
// Detach the old socket's close handler before closing it. Otherwise an
|
||||||
|
// intentional reconnect (login/logout/room switch/session refresh) would
|
||||||
|
// have the replaced socket schedule yet another reconnect, and each new
|
||||||
|
// connection re-pulls the room's history — filling the panel with old
|
||||||
|
// messages on a loop.
|
||||||
|
if (ws) {
|
||||||
|
ws.onclose = null;
|
||||||
|
ws.close();
|
||||||
|
}
|
||||||
|
|
||||||
const socketUrl = new URL(resolveWebSocketBaseUrl());
|
const socketUrl = new URL(resolveWebSocketBaseUrl());
|
||||||
socketUrl.searchParams.set("username", username);
|
socketUrl.searchParams.set("username", username);
|
||||||
socketUrl.searchParams.set("room", room);
|
socketUrl.searchParams.set("room", room);
|
||||||
ws = new WebSocket(socketUrl);
|
const socket = new WebSocket(socketUrl);
|
||||||
|
ws = socket;
|
||||||
|
|
||||||
ws.onopen = () => {
|
socket.onopen = () => {
|
||||||
console.log(`Connected to chat room: ${room}`);
|
console.log(`Connected to chat room: ${room}`);
|
||||||
addMessage("system", "Connected to " + room);
|
addMessage("system", "Connected to " + room);
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
socket.onmessage = (event) => {
|
||||||
const msg = JSON.parse(event.data);
|
const msg = JSON.parse(event.data);
|
||||||
if (msg.type === "typing") {
|
if (msg.type === "typing") {
|
||||||
showTyping(msg.username);
|
showTyping(msg.username);
|
||||||
@@ -119,7 +128,10 @@ function connectWebSocket(room: string) {
|
|||||||
addMessage(msg.type || "public", msg.text, msg.username, msg.timestamp);
|
addMessage(msg.type || "public", msg.text, msg.username, msg.timestamp);
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onclose = () => {
|
socket.onclose = () => {
|
||||||
|
// Ignore close events from sockets that were intentionally replaced.
|
||||||
|
if (ws !== socket) return;
|
||||||
|
ws = null;
|
||||||
console.log("Chat disconnected. Reconnecting in 3s...");
|
console.log("Chat disconnected. Reconnecting in 3s...");
|
||||||
setTimeout(() => connectWebSocket(room), 3000);
|
setTimeout(() => connectWebSocket(room), 3000);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in new issue
Block a user