diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index ddd2637..f09f50e 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -19,14 +19,12 @@ jobs: uses: actions/setup-node@v4 with: node-version: "20" - cache: "npm" - cache-dependency-path: frontend/package-lock.json - name: Install Frontend Dependencies run: cd frontend && npm ci - name: Lint Frontend - run: cd frontend && npm run lint || echo "Linting not configured yet" + run: cd frontend && npm run lint - name: Build Frontend run: cd frontend && npm run build @@ -35,14 +33,15 @@ jobs: uses: actions/setup-go@v5 with: go-version: "1.25" - cache: true - cache-dependency-path: backend/chat/go.sum env: GOTOOLCHAIN: local - name: Download Go modules run: cd backend/chat && GOTOOLCHAIN=local go mod download + - name: Validate Go + run: cd backend/chat && GOTOOLCHAIN=local go vet ./... + - name: Run Tests run: echo "Tests will be added in future CI steps (per TODO.md)" diff --git a/.github/MEMORY.md b/.github/MEMORY.md new file mode 100644 index 0000000..655565b --- /dev/null +++ b/.github/MEMORY.md @@ -0,0 +1,9 @@ +# MEMORY.md + +## 2026-08-05: Chat Message Admin + CI/CD + Release 1.0.5 + +**Completed**: Chat Message Admin, CI/CD, Release 1.0.5 committed + tagged + pushed. + +**Bug — nested admin page**: Fixed. Root cause: page HTML files were in `frontend/admin/` (Vite source root). Vite multi-page mode injected scripts/styles into every `.html` entry. `fetch("/admin/dashboard.html")` returned a full self-contained page, injected into `#tab-content-container`, nesting duplicate admin shell. Fix: moved all 9 page files to `frontend/public/admin/` — Vite serves `public/` contents verbatim with no processing. Fetch now returns raw HTML fragment. + +**Key lesson**: Page fragments loaded via `fetch()` must live under `public/` in Vite projects. Any `.html` in the project source root gets processed as a multi-page entry. diff --git a/.github/agents/caveman.agent.md b/.github/agents/caveman.agent.md new file mode 100644 index 0000000..b0931bc --- /dev/null +++ b/.github/agents/caveman.agent.md @@ -0,0 +1,79 @@ +--- +description: "Caveman-style agent for linearvoid project. Terse output, plan-first workflow, confirmation gating. Use for: implementing TODO.md tasks, editing project files, docs updates, release prep." +name: "Caveman" +tools: + [ + vscode/memory, + vscode/runCommand, + execute, + read, + agent, + edit, + search, + web, + playwright/browser_click, + playwright/browser_evaluate, + playwright/browser_fill_form, + playwright/browser_handle_dialog, + playwright/browser_install, + playwright/browser_navigate, + playwright/browser_press_key, + playwright/browser_type, + browser/openBrowserPage, + browser/navigatePage, + browser/runPlaywrightCode, + vscodeBrowser/openBrowserPage, + vscodeBrowser/navigatePage, + vscodeBrowser/runPlaywrightCode, + todo, + ] +handoffs: + - name: "Default" + description: "When task complete or user exits caveman mode. Also handoff for security warnings or irreversible actions." + scope: "return to default agent for normal interaction" +--- + +# Caveman Agent — Linear Void + +Caveman mode always active. Terse, no fluff, no filler, no tool-call narration. Technical substance preserved exactly. + +## Scope + +This agent handles implementation tasks within linearvoid project boundaries: + +- Backend (`backend/chat/src/`, `backend/playout/src/`) +- Frontend (`frontend/src/`, `frontend/admin/`, `frontend/public/`) +- Infrastructure (`infrastructure/`, `.gitea/workflows/`) +- Documentation (`docs/`, `*.md` at project root) +- Config (`docker-compose*.yml`, `.env.example`, `package.json`, `go.mod`) + +Outside scope: secrets management (use Gitea UI), production deployment approval, external service configuration. + +## Output Rules + +Drop articles (a/an/the), filler words, pleasantries, hedging. Fragments OK. Short synonyms. Standard acronyms OK (API, DB, CI). Never invent abbreviations. Code blocks, error strings, technical terms verbatim. Never drop negations (not/never/no/only/except). Numbers and units exact. + +## Project Workflow + +**Documentation first. Always.** + +1. **Read docs before code** — Check `docs/architecture/README.md` and relevant ADRs (`docs/architecture/adr/`) for design decisions before touching files. +2. **Propose ADR if architecture changes** — Any new endpoint pattern, data model, service dependency, or cross-cutting change needs an ADR proposal before implementation. Write to `docs/architecture/adr/ADR-XXX.md` following existing template. +3. **Plan** — Write tasks to `.github/instructions/TODO.md` with checkboxes. +4. **Confirm before acting** — Ask user. Accept `go`, `ok`, `yes`, `next`, `continue`. Abort on `no`, `stop`, `cancel`. +5. **Do the work** — Edit files, run commands. +6. **Track completion** — Mark `[x]` in TODO.md. When section done, move to `.github/instructions/DONE.md`. +7. **Log changes** — Write entry to `CHANGELOG.md` for significant work. +8. **Sync memory** — Update `.github/MEMORY.md` between major steps and at end of session. Record: current state, decisions made, lessons learned, file locations changed. This is the agent's persistent working memory across sessions. + +## Architecture + +Source of truth: `docs/architecture/README.md` and ADRs (`docs/architecture/adr/`). Follow project structure in root `README.md`. + +## Security + +Never commit raw secrets. Never print or log secrets. Use `CI_` prefix for Gitea secrets (never `GITEA_`). `.env` always gitignored. + +## Task Tracking + +Sync with `manage_todo_list` tool. Mirror state to `.github/instructions/TODO.md` checkboxes. diff --git a/.github/instructions/BACKLOG.md b/.github/instructions/BACKLOG.md new file mode 100644 index 0000000..591ab89 --- /dev/null +++ b/.github/instructions/BACKLOG.md @@ -0,0 +1,121 @@ +# BACKLOG + +## Playout & Delivery + +- [x] Implement FFmpeg + MediaMTX/SRS continuous HLS playout from playlist +- [x] Implement immutable HLS segment naming for cache-friendly delivery +- [x] Upload HLS segments to Cloudflare R2 via S3-compatible API +- [x] Implement emergency slate fallback on playout failure +- [x] Auto-restart loop for FFmpeg/MediaMTX on failure + +## Video Normalization + +- [x] Implement video normalization pipeline (H.264/AAC, 30 fps, 2 s keyframes) +- [x] Implement ffprobe verification gate rejecting non-conforming files +- [x] Enforce normalization check before upload acceptance in admin UI + +## Chat Service + +- [x] Implement WebSocket chat backend (Go) with room fan-out +- [x] Implement private DM channel routing +- [x] Implement Redis presence tracking for chat +- [ ] Implement Redis pub/sub for multi-instance chat +- [x] Implement Redis rate limiting (INCR + EXPIRE windowing, >10/10s throttle) +- [x] Implement chat message persistence (SQLite v1, PostgreSQL migration path) +- [x] Implement moderation commands (/mod, /mute, /ban) in backend +- [x] Implement moderation audit log persistence +- [x] Implement graceful degradation when Redis unavailable (core chat remains up) + +## Frontend — Viewer SPA + +- [x] Build static SPA with hls.js player integration +- [x] Build chat sidebar UI (public rooms + private DMs) +- [x] Implement schedule metadata display synced with playlist timeline +- [x] Implement responsive/mobile-friendly viewer layout + +## Frontend — Admin Interfaces + +- [x] Build admin login page with session authentication +- [x] Build admin dashboard overview page +- [x] Build video upload interface with chunked upload support +- [x] Build playlist management interface (drag-to-reorder, validation warnings) +- [x] Build schedule override/management interface (CRUD + conflict detection) +- [x] Build user management interface (mute/unmute/ban/unban actions) +- [x] Build moderation dashboard (chat log, inline mod actions, CSV export) +- [x] Build analytics dashboard (time range selector) +- [x] Build system configuration/status page (config editor, restart buttons) + +## Infrastructure — nginx + +- [x] Configure nginx reverse proxy with auth_request for admin paths +- [x] Configure nginx HLS path with CORS headers + OPTIONS preflight +- [x] Configure nginx WebSocket upgrade forwarding for /ws +- [x] Configure nginx health endpoints (/health JSON, stub_status) + +## Infrastructure — Docker + +- [x] Create Dockerfiles for playout, chat, nginx services +- [x] Create docker-compose.yml for all services +- [x] Create docker-compose.prod.yml override for production + +## Infrastructure — CI/CD (Gitea Actions) + +- [x] Implement CI workflow (lint, build, test, push images) +- [x] Implement CD workflow (pull, deploy, health checks — IP-agnostic) +- [x] Generate .env from Gitea secrets in CD workflow +- [x] Seed Prometheus config into named volume during deploy +- [x] Implement health check validation with retry in CD pipeline + +## Cloudflare Configuration + +- [ ] Configure DNS for linearvoid.tv apex and www redirect (manual) +- [ ] Configure media.linearvoid.tv for split-domain HLS delivery (manual) +- [ ] Create R2 bucket (manual) +- [ ] Configure Cache Rules: .ts/.m4s + .m3u8 (manual) +- [ ] Configure Cloudflare Pages (manual) +- [ ] Enable Tiered Cache, WAF, rate limiting (manual) +- [x] Implement Worker for immutable cache headers on segment paths +- [ ] Enforce HTTPS/WSS, redirect HTTP → HTTPS (manual) + +## Monitoring & Observability + +- [x] Set up Prometheus with chat scrape target +- [ ] Add nginx/redis/postgres/node exporters +- [x] Import Grafana app + operational dashboards +- [ ] Define Prometheus alerting rules +- [ ] Set up UptimeRobot external monitoring + +## Security + +- [x] Implement admin cookie-session auth (HttpOnly, SameSite=Strict, Secure when TLS) +- [x] Implement server-side session expiration +- [ ] Enforce HTTPS/WSS at edge and origin +- [x] Implement rate limiting for chat messages with Redis windowing + +## Data & Storage + +- [x] Set up PostgreSQL/SQLite for chat history and moderation data +- [x] Set up Redis with append-only persistence + maxmemory policy +- [ ] Implement daily/weekly/monthly Postgres backups +- [ ] Implement backup integrity verification +- [ ] Document and rehearse recovery runbook + +## Scaling & Performance + +- [ ] Optimize segment immutability and manifest TTL for cache efficiency +- [ ] Measure playback TTFB from multiple US regions +- [ ] Load-test WebSocket connections +- [ ] Benchmark admin API p95 latency +- [ ] Track cache hit ratio and R2 ops in production +- [ ] Capacity trigger monitoring +- [ ] Plan Phase 2 service separation +- [ ] Plan Phase 3 multi-origin resilience + +## Documentation + +- [x] Document API contracts in API.md +- [ ] Document deployment runbook in DEPLOYMENT.md +- [ ] Document monitoring setup in MONITORING.md +- [ ] Document security model in SECURITY.md +- [ ] Create incident runbook +- [ ] Write Level 2/3 building block views diff --git a/backend/chat/src/main.go b/backend/chat/src/main.go index 365045a..99fa9ea 100644 --- a/backend/chat/src/main.go +++ b/backend/chat/src/main.go @@ -767,6 +767,8 @@ func handleAdminScheduleOverrides(w http.ResponseWriter, r *http.Request) { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) } } + +func handleAdminSchedule(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return @@ -1062,14 +1064,9 @@ func handleAdminRestart(w http.ResponseWriter, r *http.Request) { "message": fmt.Sprintf("Restart %s manually: docker compose -f docker-compose.prod.yml restart %s", service, service), }) } - if r.Method != http.MethodGet { - http.Error(w, "method not allowed", http.StatusMethodNotAllowed) - return - } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(getAdminSystemSummary()) -} +func handleAdminSystemSummary(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { func handleAdminUsers(w http.ResponseWriter, r *http.Request) { switch r.Method { @@ -1445,6 +1442,7 @@ func validateMediaNormalization(mediaPath string) MediaNormalizationResult { return result } +func probeMediaDurationSeconds(mediaPath string) (int, error) { ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel()