Compare commits
9
Commits
d991d6b6c7
...
1.0.9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7fc921d6a | ||
|
|
04bb783a12 | ||
|
|
68bd241f2c | ||
|
|
4c02ab9d55 | ||
|
|
74bf7e19db | ||
|
|
92e074b6d0 | ||
|
|
a70bad1528 | ||
|
|
b47f2621e1 | ||
|
|
7244c1d658 |
No files matched your search
+33
-34
@@ -1,23 +1,18 @@
|
||||
# CD workflow — TEST LAB DEPLOYMENT
|
||||
# Deploys to test lab host 192.168.88.40:2375.
|
||||
# Production Hetzner target TBD — switch DOCKER_HOST when provisioned.
|
||||
# See TODO.md Production Deployment section for cutover plan.
|
||||
# See DEPLOYMENT.md Production Deployment section for cutover plan.
|
||||
|
||||
name: CD
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["CI"]
|
||||
types:
|
||||
- completed
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: [ubuntu-latest]
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }}
|
||||
environment: production
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
@@ -27,12 +22,11 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Gitea Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: git.allucanget.biz
|
||||
username: ${{ secrets.CI_REGISTRY_USER }}
|
||||
password: ${{ secrets.CI_REGISTRY_TOKEN }}
|
||||
- name: Login to registry on remote host
|
||||
env:
|
||||
DOCKER_HOST: tcp://192.168.88.40:2375
|
||||
run: |
|
||||
echo "${{ secrets.CI_REGISTRY_TOKEN }}" | docker login git.allucanget.biz -u "${{ secrets.CI_REGISTRY_USER }}" --password-stdin
|
||||
|
||||
- name: Prepare environment
|
||||
run: |
|
||||
@@ -49,10 +43,14 @@ jobs:
|
||||
NGINX_PORT=80
|
||||
EOF
|
||||
|
||||
- name: Seed Prometheus config
|
||||
- name: Seed Prometheus config on remote host
|
||||
env:
|
||||
DOCKER_HOST: tcp://192.168.88.40:2375
|
||||
run: |
|
||||
mkdir -p monitoring/prometheus
|
||||
cat > monitoring/prometheus/prometheus.yml << 'EOF'
|
||||
docker run --rm \
|
||||
-v prometheus-config:/etc/prometheus \
|
||||
alpine:latest sh -c '
|
||||
cat > /etc/prometheus/prometheus.yml << "PROMEOF"
|
||||
global:
|
||||
scrape_interval: 15s
|
||||
evaluation_interval: 15s
|
||||
@@ -61,29 +59,29 @@ jobs:
|
||||
- "/etc/prometheus/alerts.yml"
|
||||
|
||||
scrape_configs:
|
||||
- job_name: 'chat'
|
||||
- job_name: "chat"
|
||||
static_configs:
|
||||
- targets: ['chat:3000']
|
||||
- job_name: 'playout'
|
||||
- targets: ["chat:3000"]
|
||||
- job_name: "playout"
|
||||
static_configs:
|
||||
- targets: ['playout:8081']
|
||||
- job_name: 'nginx'
|
||||
- targets: ["playout:8081"]
|
||||
- job_name: "nginx"
|
||||
static_configs:
|
||||
- targets: ['nginx-exporter:9113']
|
||||
- job_name: 'redis'
|
||||
- targets: ["nginx-exporter:9113"]
|
||||
- job_name: "redis"
|
||||
static_configs:
|
||||
- targets: ['redis-exporter:9121']
|
||||
- job_name: 'postgres'
|
||||
- targets: ["redis-exporter:9121"]
|
||||
- job_name: "postgres"
|
||||
static_configs:
|
||||
- targets: ['postgres-exporter:9187']
|
||||
- job_name: 'node'
|
||||
- targets: ["postgres-exporter:9187"]
|
||||
- job_name: "node"
|
||||
static_configs:
|
||||
- targets: ['node-exporter:9100']
|
||||
- job_name: 'prometheus'
|
||||
- targets: ["node-exporter:9100"]
|
||||
- job_name: "prometheus"
|
||||
static_configs:
|
||||
- targets: ['localhost:9090']
|
||||
EOF
|
||||
cat > monitoring/prometheus/alerts.yml << 'EOF'
|
||||
- targets: ["localhost:9090"]
|
||||
PROMEOF
|
||||
cat > /etc/prometheus/alerts.yml << "ALERTSEOF"
|
||||
groups:
|
||||
- name: linearvoid
|
||||
rules:
|
||||
@@ -129,14 +127,15 @@ jobs:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "Low disk space"
|
||||
EOF
|
||||
ALERTSEOF
|
||||
'
|
||||
|
||||
- name: Deploy with docker-compose
|
||||
env:
|
||||
DOCKER_HOST: tcp://192.168.88.40:2375
|
||||
run: |
|
||||
docker compose -f docker-compose.prod.yml pull
|
||||
docker compose -f docker-compose.prod.yml up -d --remove-orphans
|
||||
docker compose -f docker-compose.prod.yml up -d --remove-orphans --force-recreate
|
||||
|
||||
- name: Health check
|
||||
env:
|
||||
|
||||
+1
-18
@@ -43,7 +43,7 @@ jobs:
|
||||
run: cd backend/chat && GOTOOLCHAIN=local go vet ./...
|
||||
|
||||
- name: Run Tests
|
||||
run: echo "Tests will be added in future CI steps (per TODO.md)"
|
||||
run: echo "Tests will be added in future CI steps"
|
||||
|
||||
docker:
|
||||
needs: test
|
||||
@@ -132,20 +132,3 @@ jobs:
|
||||
labels: ${{ steps.meta-nginx.outputs.labels }}
|
||||
cache-from: type=registry,ref=git.allucanget.biz/zwitschi/linearvoid/buildcache:nginx
|
||||
cache-to: type=registry,ref=git.allucanget.biz/zwitschi/linearvoid/buildcache:nginx,mode=max
|
||||
|
||||
release:
|
||||
needs: docker
|
||||
runs-on: [ubuntu-latest]
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
generate_release_notes: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.CI_REGISTRY_TOKEN }}
|
||||
@@ -1 +0,0 @@
|
||||
# MEMORY.md
|
||||
@@ -1,79 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,121 +0,0 @@
|
||||
# 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
|
||||
@@ -81,6 +81,7 @@ Thumbs.db
|
||||
# Agent / Tool files
|
||||
.github/instructions/TODO.md
|
||||
.github/instructions/DONE.md
|
||||
.github/instructions/BACKLOG.md
|
||||
# (keep AGENTS.md, MEMORY.md, and other instruction files)
|
||||
|
||||
# Temporary architecture artifacts
|
||||
|
||||
+24
-1
@@ -2,6 +2,29 @@
|
||||
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
## [1.0.9] - 2026-08-06
|
||||
|
||||
### Added
|
||||
|
||||
- **Prometheus alerts mount**: `alerts.yml` mounted into prometheus container in both dev and prod compose files.
|
||||
- **Grafana provisioning**: Prometheus datasource + dashboard provider YAML configs in `monitoring/grafana/`.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Prod prometheus**: Switched from empty named volume to bind mounts for config files (consistent with dev).
|
||||
- **CD pipeline**: Trigger on `push` (parallel with CI), removed `environment: production` (was causing silent abort).
|
||||
- **CI pipeline**: Removed redundant `release` job (handled by `release.yml`).
|
||||
- **Tag format**: Standardized on `*.*.*` (e.g. `1.0.9`), removed `v` prefix from existing tags.
|
||||
- **DEPLOYMENT.md**: Documented parallel CI/CD pipeline flow with workflow table.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Prometheus config mount**: Switched prod from bind mounts to named volume `prometheus-config`, populated via `docker run` on remote host. Fixes `not a directory` error when bind source path doesn't exist on deployment host.
|
||||
|
||||
### Removed
|
||||
|
||||
- `.github/MEMORY.md`, `.github/agents/`, `.github/instructions/` — tracking files migrated out of repo.
|
||||
|
||||
## [1.0.8] - 2026-08-06
|
||||
|
||||
### Changed
|
||||
@@ -139,7 +162,7 @@ Full backend and frontend implementation complete.
|
||||
- **Frontend** (`frontend/`): Vite + TypeScript + Tailwind + Alpine.js, HLS player with hls.js, chat sidebar with room switching and private messaging, "Now Playing" overlay, and modern UI (ADR-002, ADR-003).
|
||||
- Complete `docker-compose.yml` for local development with all services, volumes, and healthchecks.
|
||||
- All ADRs (001-007) created and linked in documentation.
|
||||
- `TODO.md`, `DONE.md`, and `CHANGELOG.md` updated with full traceability.
|
||||
- `CHANGELOG.md` updated with full traceability.
|
||||
- Git repository initialized with comprehensive initial commit.
|
||||
|
||||
The project is a fully functional, self-hosted 24/7 linear video channel with synchronized playback, custom chat, R2 delivery, moderation, and admin-ready structure. It matches the architecture defined in the arc42 template and all ADRs.
|
||||
|
||||
+14
-16
@@ -7,9 +7,8 @@ Thank you for considering contributing to Linear Void!
|
||||
1. **Read the Architecture First**: All design decisions are documented in `docs/architecture/README.md` (arc42 template) and the ADRs in `docs/architecture/adr/` (ADR-001 to ADR-007).
|
||||
2. **Follow the Structure**: Use the folder layout defined in ADR-002 (`frontend/`, `backend/playout/`, `backend/chat/`, `infrastructure/`, etc.).
|
||||
3. **Update Documentation Before Code**: Create or update an ADR before implementing significant changes.
|
||||
4. **Update TODO.md**: Plan tasks in `.github/instructions/TODO.md`, mark as completed, and move finished sections to `DONE.md`.
|
||||
5. **Commit Messages**: Reference the relevant ADR (e.g., "feat(playout): add R2 sync per ADR-001").
|
||||
6. **Test with Docker**: Run `docker compose up --build` to test changes.
|
||||
4. **Commit Messages**: Reference the relevant ADR (e.g., "feat(playout): add R2 sync per ADR-001").
|
||||
5. **Test with Docker**: Run `docker compose up --build` to test changes.
|
||||
|
||||
## Repository
|
||||
|
||||
@@ -40,23 +39,22 @@ Linear Void follows **Semantic Versioning** ([SemVer](https://semver.org)): `MAJ
|
||||
- **MINOR** — new features (endpoints, admin tabs, UI capabilities) that are backward-compatible.
|
||||
- **PATCH** — bug fixes, optimizations, documentation polish.
|
||||
|
||||
Git tags use a `v` prefix: `1.0.2`, `1.1.0`, `v2.0.0`.
|
||||
Git tags use no prefix: `1.0.2`, `1.1.0`, `2.0.0`.
|
||||
|
||||
### Where Versions Are Declared
|
||||
|
||||
| File | Token | Example |
|
||||
| ------------------------------ | -------------- | ------------------------- |
|
||||
| `frontend/package.json` | `"version"` | `"1.0.2"` |
|
||||
| `CHANGELOG.md` | `## [X.Y.Z]` | `## [1.0.2] - 2026-08-05` |
|
||||
| `README.md` | Version badge | `version-1.0.2-blue` |
|
||||
| `.github/instructions/TODO.md` | Section header | `## Version 1.0.2` |
|
||||
| File | Token | Example |
|
||||
| ----------------------- | ------------- | ------------------------- |
|
||||
| `frontend/package.json` | `"version"` | `"1.0.2"` |
|
||||
| `CHANGELOG.md` | `## [X.Y.Z]` | `## [1.0.2] - 2026-08-05` |
|
||||
| `README.md` | Version badge | `version-1.0.2-blue` |
|
||||
|
||||
### Tag Triggers
|
||||
|
||||
| Tag Pattern | Workflow | What Happens |
|
||||
| ------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| `v*` (e.g. `1.0.2`) | `release.yml` | Builds all 3 images, pushes to registry with semver tags (`1.0.2`, `1.0`, `latest`), creates Gitea Release via API |
|
||||
| Push to `main` | `ci.yml` → `cd.yml` | CI builds + pushes images with `main`/`latest` tags, CD deploys to production |
|
||||
| Tag Pattern | Workflow | What Happens |
|
||||
| ---------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| `*.*.*` (e.g. `1.0.2`) | `release.yml` | Builds all 3 images, pushes to registry with semver tags (`1.0.2`, `1.0`, `latest`), creates Gitea Release via API |
|
||||
| Push to `main` | `ci.yml` → `cd.yml` | CI builds + pushes images with `main`/`latest` tags, CD deploys to production |
|
||||
|
||||
### Full Release Procedure
|
||||
|
||||
@@ -87,7 +85,7 @@ Add entries under `## [X.Y.Z]` in `CHANGELOG.md` following [Keep a Changelog](ht
|
||||
|
||||
```bash
|
||||
# Stage all version bumps + changelog
|
||||
git add frontend/package.json CHANGELOG.md README.md .github/instructions/TODO.md
|
||||
git add frontend/package.json CHANGELOG.md README.md
|
||||
|
||||
# Commit
|
||||
git commit -m "chore: bump version to X.Y.Z"
|
||||
@@ -145,6 +143,6 @@ git.allucanget.biz/zwitschi/linearvoid/playout:latest
|
||||
|
||||
### Moving Completed Tasks
|
||||
|
||||
After release, move completed sections from `TODO.md` to `DONE.md` and update `CHANGELOG.md` if needed. The `DONE.md` file serves as the historical record linking tasks to releases.
|
||||
After release, update `CHANGELOG.md` if needed.
|
||||
|
||||
See the ADRs for architectural decisions and `docs/architecture/README.md` for the big picture. Thank you for contributing!
|
||||
+43
-5
@@ -1,5 +1,33 @@
|
||||
# Deployment Guide
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Deployment Guide](#deployment-guide)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [Environments](#environments)
|
||||
- [Production Target (Hetzner)](#production-target-hetzner)
|
||||
- [Planned Infrastructure](#planned-infrastructure)
|
||||
- [Cutover Plan](#cutover-plan)
|
||||
- [Firewall / Security Group](#firewall--security-group)
|
||||
- [Pipeline Readiness](#pipeline-readiness)
|
||||
- [Target Topology](#target-topology)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Environment File](#environment-file)
|
||||
- [Pull-and-Run Deployment](#pull-and-run-deployment)
|
||||
- [First-Time or Recovery Provisioning](#first-time-or-recovery-provisioning)
|
||||
- [Health Verification](#health-verification)
|
||||
- [Zero/Low-Downtime Update Pattern](#zerolow-downtime-update-pattern)
|
||||
- [Rollback](#rollback)
|
||||
- [Operational Tasks](#operational-tasks)
|
||||
- [View logs](#view-logs)
|
||||
- [Restart one service](#restart-one-service)
|
||||
- [Stop stack](#stop-stack)
|
||||
- [Keep volumes (recommended) vs remove volumes (destructive)](#keep-volumes-recommended-vs-remove-volumes-destructive)
|
||||
- [Nginx and Proxy Considerations](#nginx-and-proxy-considerations)
|
||||
- [Security Baseline](#security-baseline)
|
||||
- [Common Deployment Pitfalls](#common-deployment-pitfalls)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
## Environments
|
||||
|
||||
Linear Void runs in two separate environments:
|
||||
@@ -9,7 +37,7 @@ Linear Void runs in two separate environments:
|
||||
| **Test Lab** | `192.168.88.40` (internal) | `tcp://192.168.88.40:2375` | Integration testing, staging, CI/CD validation |
|
||||
| **Production** | Hetzner VPS (TBD) | `tcp://<hetzner-ip>:2375` | Public linearvoid.tv serving real viewers |
|
||||
|
||||
**Current state (2026-08-06):** Test lab is active and receives automated CD deploys. Production Hetzner VPS is **not yet provisioned**. The CD workflow (`cd.yml`) and `docker-compose.prod.yml` currently target the test lab. Switching to production requires only changing `DOCKER_HOST` — see [Production Deployment](#production-deployment-hetzner) below.
|
||||
**Current state (2026-08-06):** Test lab is active and receives automated CD deploys. Production Hetzner VPS is **not yet provisioned**. The CD workflow (`cd.yml`) and `docker-compose.prod.yml` currently target the test lab. Switching to production requires only changing `DOCKER_HOST` — see [Production Deployment](#production-target-hetzner) below.
|
||||
|
||||
## Production Target (Hetzner)
|
||||
|
||||
@@ -28,7 +56,7 @@ Linear Void runs in two separate environments:
|
||||
|
||||
### Cutover Plan
|
||||
|
||||
1. Provision Hetzner VPS (see `TODO.md` #92)
|
||||
1. Provision Hetzner VPS (see [Production Deployment](#production-target-hetzner) section)
|
||||
2. Add production secrets to Gitea (`CI_PROD_*` prefix)
|
||||
3. Update `DOCKER_HOST` in CD workflow to Hetzner public IP
|
||||
4. Trigger deploy → validate health checks
|
||||
@@ -49,7 +77,17 @@ Linear Void runs in two separate environments:
|
||||
|
||||
### Pipeline Readiness
|
||||
|
||||
The Gitea Actions CD workflow (`cd.yml`) already supports `DOCKER_HOST` as a configurable variable. To switch targets:
|
||||
Three Gitea Actions workflows drive the full pipeline:
|
||||
|
||||
| Workflow | Trigger | Purpose |
|
||||
| ------------- | ---------------- | ---------------------------------------------------------------------------------------- |
|
||||
| `ci.yml` | Push to `main` | Lint, build frontend, vet Go, build & push Docker images (`:latest`, `:main`) |
|
||||
| `cd.yml` | Push to `main` | Deploy to test lab (`192.168.88.40:2375`) — runs **in parallel** with CI |
|
||||
| `release.yml` | Tag push `*.*.*` | Build & push versioned Docker images (`:X.Y.Z`, `:X.Y`, `:latest`), create Gitea Release |
|
||||
|
||||
CI and CD run simultaneously on every push to `main`. The deploy pulls whatever `:latest` images are available in the registry at that moment.
|
||||
|
||||
The CD workflow (`cd.yml`) uses `DOCKER_HOST` to target the deployment host. To switch targets:
|
||||
|
||||
```bash
|
||||
# Test lab (current)
|
||||
@@ -59,7 +97,7 @@ DOCKER_HOST=tcp://192.168.88.40:2375
|
||||
DOCKER_HOST=tcp://<hetzner-public-ip>:2375
|
||||
```
|
||||
|
||||
No code changes needed in the pipeline. See `TODO.md` #91 and #92–#99 for full production deployment checklist.
|
||||
No code changes needed in the pipeline. See `DEPLOYMENT.md` Production Deployment section for full checklist.
|
||||
|
||||
## Target Topology
|
||||
|
||||
@@ -99,7 +137,7 @@ See .env.example and SETUP.md for full variable reference.
|
||||
|
||||
Images are built and pushed automatically by the CI and release workflows (see [CONTRIBUTING.md](CONTRIBUTING.md#versioning-and-releases) for the release procedure).
|
||||
|
||||
Every push to `main` produces `:main` and `:latest` tags. Semantic version tags (`v*`) produce `:X.Y.Z`, `:X.Y`, and `:latest` tags.
|
||||
Every push to `main` triggers CI (builds `:main` and `:latest` tags) and CD (deploys `:latest` to test lab) in parallel. Semantic version tags (`*.*.*`, e.g. `1.0.9`) trigger the release workflow which builds `:X.Y.Z`, `:X.Y`, and `:latest` tags and creates a Gitea Release.
|
||||
|
||||
From repository root:
|
||||
|
||||
|
||||
+6
-6
@@ -4,12 +4,12 @@ Prometheus + Grafana monitoring for Linear Void.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌──────────────┐ ┌───────────┐
|
||||
│ Exporters │───▶│ Prometheus │───▶│ Grafana │
|
||||
│ (7 targets) │ │ :9090 │ │ :3001 │
|
||||
└─────────────┘ └──────┬───────┘ └───────────┘
|
||||
│
|
||||
```text
|
||||
┌──────────────┐ ┌──────────────┐ ┌───────────┐
|
||||
│ Exporters │───> │ Prometheus │───>│ Grafana │
|
||||
│ (7 targets) │ │ :9090 │ │ :3001 │
|
||||
└──────────────┘ └───────┬──────┘ └───────────┘
|
||||
│
|
||||
alerts.yml (8 rules)
|
||||
```
|
||||
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
[](https://git.allucanget.biz/zwitschi/linearvoid/actions)
|
||||
[](LICENSE)
|
||||
[](https://git.allucanget.biz)
|
||||
[](https://git.allucanget.biz/zwitschi/linearvoid/releases)
|
||||
[](https://git.allucanget.biz/zwitschi/linearvoid/releases)
|
||||
[](CHANGELOG.md)
|
||||
|
||||
## Architecture
|
||||
|
||||
The architecture (detailed in [`docs/architecture/README.md`](docs/architecture/README.md)) uses:
|
||||
|
||||
- Hetzner Ashburn VPS for playout (FFmpeg + MediaMTX/SRS) and chat *(planned production; currently test lab at 192.168.88.40)*.
|
||||
- Hetzner Ashburn VPS for playout (FFmpeg + MediaMTX/SRS) and chat _(planned production; currently test lab at 192.168.88.40)_.
|
||||
- Cloudflare R2 + CDN with aggressive immutable caching of HLS `.ts` segments ($0 egress).
|
||||
- Lightweight static frontend and WebSocket chat backend.
|
||||
- Gitea self-hosted CI/CD (Actions) and Container Registry.
|
||||
@@ -112,7 +112,7 @@ This structure directly reflects the building blocks, runtime flows, deployment
|
||||
|
||||
## Getting Started
|
||||
|
||||
> **Note:** The `docker compose up` below targets local development. The test lab (192.168.88.40) receives automated CD deploys. Production Hetzner VPS is not yet provisioned — see `TODO.md`.
|
||||
> **Note:** The `docker compose up` below targets local development. The test lab (192.168.88.40) receives automated CD deploys. Production Hetzner VPS is not yet provisioned — see `DEPLOYMENT.md`.
|
||||
|
||||
```bash
|
||||
git clone https://git.allucanget.biz/zwitschi/linearvoid.git
|
||||
@@ -223,11 +223,11 @@ Contributions, issues, and further evolution (LL-HLS, ABR, multi-channel, E2EE)
|
||||
Contributions are welcome! See [`CONTRIBUTING.md`](CONTRIBUTING.md) for guidelines. Quick overview:
|
||||
|
||||
1. Read the [architecture docs](docs/architecture/README.md) and ADRs first.
|
||||
2. Plan tasks in `.github/instructions/TODO.md`.
|
||||
2. Code against the defined module structure (frontend, backend/playout, backend/chat, infrastructure, scripts, monitoring).
|
||||
3. Open PRs on [Gitea](https://git.allucanget.biz/zwitschi/linearvoid).
|
||||
4. CI validates builds and tests automatically.
|
||||
|
||||
Since this a linear project, we also rely on a linear development and release model: Only main branch is used for releases, and all features are merged into main after review and CI validation. Releases are tagged with semantic versioning (1.0.3, 1.0.4, etc.) and automatically built/pushed to the container registry.
|
||||
Since this a linear project, we also rely on a linear development and release model: Only main branch is used for releases, and all features are merged into main after review and CI validation. Releases are tagged with semantic versioning (1.0.3, 1.0.4, etc.) and automatically built/pushed to the container registry. Version numbers are declared in `frontend/package.json`, `CHANGELOG.md`, and the version badge in this README. For patches, we use even numbers for stable versions (1.0.2, 1.0.4) and odd numbers for pre-release/unstable versions (1.0.3, 1.0.5).
|
||||
|
||||
[MIT License](LICENSE)
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ flowchart LR
|
||||
|
||||
**Test Lab (active):** Internal network `192.168.88.0/24`, host `192.168.88.40`. Receives automated CD deploys from Gitea Actions. Used for integration testing and staging validation.
|
||||
|
||||
**Production (pending):** Hetzner Cloud VPS, Ashburn VA. Not yet provisioned. CD workflow and compose files are ready — only `DOCKER_HOST` needs changing. See `TODO.md` Production Deployment section.
|
||||
**Production (pending):** Hetzner Cloud VPS, Ashburn VA. Not yet provisioned. CD workflow and compose files are ready — only `DOCKER_HOST` needs changing. See Production Deployment section in [DEPLOYMENT.md](./DEPLOYMENT.md) for cutover plan.
|
||||
|
||||
## Development Prerequisites
|
||||
|
||||
|
||||
@@ -41,8 +41,17 @@ var (
|
||||
// Rate limit config (env-overridable)
|
||||
rateLimitMax = 10
|
||||
rateLimitWindow = 10 * time.Second
|
||||
|
||||
// Instance ID for multi-instance pub/sub deduplication
|
||||
instanceID = fmt.Sprintf("chat-%s", generateShortID())
|
||||
)
|
||||
|
||||
func generateShortID() string {
|
||||
b := make([]byte, 6)
|
||||
rand.Read(b)
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
type clientInfo struct {
|
||||
Username string
|
||||
Room string
|
||||
@@ -2161,7 +2170,7 @@ func handleMetrics(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
fmt.Fprintln(w, "# HELP linearvoid_chat_build_info Static build info for chat service.")
|
||||
fmt.Fprintln(w, "# TYPE linearvoid_chat_build_info gauge")
|
||||
fmt.Fprintln(w, "linearvoid_chat_build_info{service=\"chat\",database_driver=\""+databaseDriverName+"\"} 1")
|
||||
fmt.Fprintln(w, "linearvoid_chat_build_info{service=\"chat\",database_driver=\""+databaseDriverName+"\",instance_id=\""+instanceID+"\"} 1")
|
||||
}
|
||||
|
||||
// broadcast sends a message to all connected clients in a room and saves to DB
|
||||
@@ -2172,12 +2181,23 @@ func broadcast(msg Message) {
|
||||
// Persist to configured SQL backend.
|
||||
db.Create(&msg)
|
||||
|
||||
// Local fan-out
|
||||
for client := range clients {
|
||||
if err := client.WriteJSON(msg); err != nil {
|
||||
client.Close()
|
||||
delete(clients, client)
|
||||
delete(clientMeta, client)
|
||||
}
|
||||
}
|
||||
|
||||
// Redis pub/sub for multi-instance fan-out
|
||||
if redisClient != nil && msg.Room != "" {
|
||||
payload, _ := json.Marshal(map[string]any{
|
||||
"instance": instanceID,
|
||||
"msg": msg,
|
||||
})
|
||||
redisClient.Publish(context.Background(), fmt.Sprintf("room:%s:messages", msg.Room), payload)
|
||||
}
|
||||
}
|
||||
|
||||
// handleConnection handles WebSocket connections for chat (public rooms, private DMs, moderation)
|
||||
@@ -2333,6 +2353,47 @@ func pushChatHistory(conn *websocket.Conn, room string, limit int) {
|
||||
// dmsSent tracks total private messages for Prometheus metrics (set from handleMetrics).
|
||||
var dmsSent int64
|
||||
|
||||
// subscribeRoomMessages listens for Redis pub/sub messages and fans out locally.
|
||||
func subscribeRoomMessages() {
|
||||
if redisClient == nil {
|
||||
return
|
||||
}
|
||||
pubsub := redisClient.PSubscribe(context.Background(), "room:*:messages")
|
||||
defer pubsub.Close()
|
||||
|
||||
log.Printf("Redis pub/sub subscriber started (instance %s)", instanceID)
|
||||
|
||||
for {
|
||||
msg, err := pubsub.ReceiveMessage(context.Background())
|
||||
if err != nil {
|
||||
log.Printf("Redis pub/sub error: %v — retrying in 5s", err)
|
||||
time.Sleep(5 * time.Second)
|
||||
pubsub = redisClient.PSubscribe(context.Background(), "room:*:messages")
|
||||
continue
|
||||
}
|
||||
|
||||
var envelope struct {
|
||||
Instance string `json:"instance"`
|
||||
Msg Message `json:"msg"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(msg.Payload), &envelope); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip messages originating from this instance
|
||||
if envelope.Instance == instanceID {
|
||||
continue
|
||||
}
|
||||
|
||||
// Fan out to local clients
|
||||
mutex.Lock()
|
||||
for client := range clients {
|
||||
client.WriteJSON(envelope.Msg)
|
||||
}
|
||||
mutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// handlePrivateDM routes private messages to the specific user pair
|
||||
func handlePrivateDM(msg Message) {
|
||||
msg.Type = "private"
|
||||
@@ -2640,6 +2701,11 @@ func main() {
|
||||
initRedis()
|
||||
initDB()
|
||||
|
||||
// Subscribe to Redis pub/sub for multi-instance message fan-out
|
||||
go subscribeRoomMessages()
|
||||
|
||||
log.Printf("Instance ID: %s", instanceID)
|
||||
|
||||
http.HandleFunc("/ws", handleConnection)
|
||||
http.HandleFunc("/api/admin/login", handleAdminLogin)
|
||||
http.HandleFunc("/api/admin/logout", handleAdminLogout)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# docker-compose.prod.yml — TEST LAB DEPLOYMENT
|
||||
# Target: test lab host 192.168.88.40 (NOT Hetzner production).
|
||||
# Production Hetzner VPS not yet provisioned — see TODO.md Production Deployment section.
|
||||
# Production Hetzner VPS not yet provisioned — see DEPLOYMENT.md Production Deployment section.
|
||||
# Uses pre-built images from Gitea Container Registry (no local build).
|
||||
# Override DOCKER_HOST in CD workflow to switch target.
|
||||
|
||||
@@ -198,6 +198,6 @@ networks:
|
||||
volumes:
|
||||
redis-data:
|
||||
postgres-data:
|
||||
prometheus-data:
|
||||
prometheus-config:
|
||||
prometheus-data:
|
||||
grafana-data:
|
||||
@@ -153,6 +153,7 @@ services:
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
- ./monitoring/prometheus/alerts.yml:/etc/prometheus/alerts.yml
|
||||
- prometheus-data:/prometheus
|
||||
ports:
|
||||
- "9090:9090"
|
||||
|
||||
@@ -20,7 +20,14 @@
|
||||
- [Building Block View](#building-block-view)
|
||||
- [Whitebox Overall System](#whitebox-overall-system)
|
||||
- [Level 2](#level-2)
|
||||
- [Playout Service (`backend/playout/src/playlist.go`)](#playout-service-backendplayoutsrcplaylistgo)
|
||||
- [Chat Service (`backend/chat/src/main.go`)](#chat-service-backendchatsrcmaingo)
|
||||
- [Frontend SPA (`frontend/`)](#frontend-spa-frontend)
|
||||
- [Reverse Proxy (`infrastructure/nginx/nginx.conf`)](#reverse-proxy-infrastructurenginxnginxconf)
|
||||
- [Level 3](#level-3)
|
||||
- [Chat Service Modules](#chat-service-modules)
|
||||
- [Playout Service Modules](#playout-service-modules)
|
||||
- [Frontend Modules](#frontend-modules)
|
||||
- [Runtime View](#runtime-view)
|
||||
- [Playout \& Delivery Scenario](#playout--delivery-scenario)
|
||||
- [Deployment View](#deployment-view)
|
||||
@@ -453,7 +460,7 @@ flowchart LR
|
||||
|
||||
### Infrastructure Level 2
|
||||
|
||||
**Hetzner VPS (planned):** Will run playout (FFmpeg/MediaMTX), chat backend, Nginx. Monitoring via dashboards + UptimeRobot. Backups to R2. Currently test lab (`192.168.88.40`) serves this role. Production Hetzner instance not yet provisioned — see `TODO.md` Production Deployment section.
|
||||
**Hetzner VPS (planned):** Will run playout (FFmpeg/MediaMTX), chat backend, Nginx. Monitoring via dashboards + UptimeRobot. Backups to R2. Currently test lab (`192.168.88.40`) serves this role. Production Hetzner instance not yet provisioned — see Production Deployment section in [`DEPLOYMENT.md`](../../DEPLOYMENT.md).
|
||||
|
||||
**Cloudflare Layer**:
|
||||
|
||||
|
||||
@@ -60,8 +60,6 @@ Create a dedicated **admin web interface** in a new `frontend/admin/` directory
|
||||
- Integration with existing playout service and R2 keeps the architecture consistent.
|
||||
- Admin actions will be fully auditable, paving the way for ADR-005 (moderation and user management).
|
||||
|
||||
This decision was planned in `TODO.md` before implementation, per AGENTS.md.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive**:
|
||||
|
||||
@@ -59,8 +59,6 @@ Extend the admin interface started in ADR-004 with additional protected sections
|
||||
- Leveraging Prometheus/Grafana for analytics avoids duplicating metrics collection.
|
||||
- Immutable audit logging supports future compliance and moderation review (key requirement for a public chat system).
|
||||
|
||||
This ADR was planned in `TODO.md` before any implementation, per AGENTS.md.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive**:
|
||||
@@ -92,8 +90,6 @@ This ADR was planned in `TODO.md` before any implementation, per AGENTS.md.
|
||||
7. Update authentication middleware and tests.
|
||||
8. Document API contracts and update `docker-compose.yml` to expose the admin interface securely.
|
||||
|
||||
**Next Steps**: After both admin ADRs are complete, move to implementation of the Playout Service (highest priority in the core modules section of TODO.md).
|
||||
|
||||
## References
|
||||
|
||||
- ADR-001: Chat features, moderation requirements, monitoring needs.
|
||||
|
||||
@@ -95,8 +95,6 @@ We must explicitly document:
|
||||
6. Add monitoring (Prometheus exporter) and admin audit logging for configuration changes.
|
||||
7. Write tests for rate limiting, presence, and admin configuration flows.
|
||||
|
||||
**Next Steps**: After ADR-006 and ADR-007 are reviewed, mark the "Create ADRs for Data Storage, Caching, and Admin Capabilities" section as completed in TODO.md and move it to DONE.md. Then proceed with implementation of the Playout Service.
|
||||
|
||||
## References
|
||||
|
||||
- ADR-001: Chat features requiring presence, rate limiting, and pub/sub.
|
||||
|
||||
+17
-23
@@ -4,15 +4,11 @@ Triage order for Linear Void incidents. Work top-to-bottom — each tier depends
|
||||
|
||||
## Triage Order
|
||||
|
||||
```
|
||||
1. Edge Reachability (Cloudflare)
|
||||
2. Origin Health (VPS/Docker)
|
||||
3. Playout Progression (HLS)
|
||||
4. Chat Connectivity (WebSocket)
|
||||
5. Data-Plane (Redis/DB)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. Edge Reachability (Cloudflare)
|
||||
|
||||
@@ -28,17 +24,17 @@ curl -I https://linearvoid.tv/health
|
||||
```
|
||||
|
||||
**Common causes:**
|
||||
|
||||
- DNS misconfiguration (A record pointing to wrong IP)
|
||||
- Cloudflare proxy disabled (grey cloud → origin exposed directly)
|
||||
- SSL certificate expired (check Cloudflare dashboard → SSL/TLS)
|
||||
- Cloudflare outage → https://www.cloudflarestatus.com
|
||||
- Cloudflare outage → check [https://www.cloudflarestatus.com](https://www.cloudflarestatus.com)
|
||||
|
||||
**Mitigation:**
|
||||
|
||||
- Verify A record: `linearvoid.tv` → origin IP (proxied, orange cloud)
|
||||
- Bypass Cloudflare temporarily: `curl -H "Host: linearvoid.tv" http://<origin-ip>/health`
|
||||
|
||||
---
|
||||
|
||||
## 2. Origin Health (VPS/Docker)
|
||||
|
||||
**Symptoms:** Cloudflare returns 502/504, containers down.
|
||||
@@ -53,18 +49,18 @@ curl http://192.168.88.40/health
|
||||
```
|
||||
|
||||
**Common causes:**
|
||||
|
||||
- Host out of disk space: `df -h`
|
||||
- Docker daemon stopped: `systemctl status docker`
|
||||
- Container restart loop: `docker ps -a --filter "status=exited"`
|
||||
- Resource exhaustion (OOM): `dmesg | grep -i oom`
|
||||
|
||||
**Mitigation:**
|
||||
|
||||
- Restart stack: `docker compose -f docker-compose.prod.yml up -d`
|
||||
- Free disk: `docker system prune -f`
|
||||
- Check logs per service: `docker logs linearvoid-<service> --tail 100`
|
||||
|
||||
---
|
||||
|
||||
## 3. Playout Progression (HLS)
|
||||
|
||||
**Symptoms:** Viewer sees slate or buffering, playlist not advancing.
|
||||
@@ -83,19 +79,19 @@ docker logs linearvoid-playout --tail 100
|
||||
```
|
||||
|
||||
**Common causes:**
|
||||
|
||||
- FFmpeg crash → slate fallback active (health shows `degraded`)
|
||||
- Playlist file empty/missing: `cat data/playlist/playlist.txt`
|
||||
- Media file missing from playlist: `ls data/media/`
|
||||
- Disk full → HLS segments can't write
|
||||
|
||||
**Mitigation:**
|
||||
|
||||
- Verify playlist has valid entries
|
||||
- Check media files exist at paths in playlist
|
||||
- Restart playout: `docker compose restart playout`
|
||||
- Generate demo playlist if empty: see `backend/playout/start-playout.sh`
|
||||
|
||||
---
|
||||
|
||||
## 4. Chat Connectivity (WebSocket)
|
||||
|
||||
**Symptoms:** Chat not loading, messages not sending, presence broken.
|
||||
@@ -112,18 +108,18 @@ docker logs linearvoid-chat --tail 100
|
||||
```
|
||||
|
||||
**Common causes:**
|
||||
|
||||
- Redis down → presence/rate-limiting degraded (chat still works)
|
||||
- Database down → messages not persisting
|
||||
- Port not exposed: `docker port linearvoid-chat`
|
||||
|
||||
**Mitigation:**
|
||||
|
||||
- Restart chat: `docker compose restart chat`
|
||||
- Check Redis: `docker exec linearvoid-redis redis-cli ping`
|
||||
- Check DB: `docker exec linearvoid-db pg_isready -U linearvoid`
|
||||
- Chat works without Redis (graceful degradation) — check logs for "Redis connection warning"
|
||||
|
||||
---
|
||||
|
||||
## 5. Data-Plane (Redis/DB)
|
||||
|
||||
**Symptoms:** Chat works but no persistence/presence, admin stats empty.
|
||||
@@ -139,32 +135,30 @@ docker exec linearvoid-db psql -U linearvoid -c "SELECT count(*) FROM messages;"
|
||||
```
|
||||
|
||||
**Common causes:**
|
||||
|
||||
- Redis OOM (check `maxmemory` setting)
|
||||
- PostgreSQL connection limit reached
|
||||
- Disk full on volume mount
|
||||
|
||||
**Mitigation:**
|
||||
|
||||
- Redis: `docker compose restart redis` (AOF persist enabled)
|
||||
- PostgreSQL: `docker compose restart db`
|
||||
- Restore from backup: see `scripts/verify-backups.sh` for procedure
|
||||
|
||||
---
|
||||
|
||||
## Escalation
|
||||
|
||||
| Severity | When | Action |
|
||||
|---|---|---|
|
||||
| Critical | Slate active >30min, complete outage | Trigger DR plan, provision fallback host |
|
||||
| Warning | Redis down, high CPU >30min | Investigate during business hours, restart service |
|
||||
| Info | Single service blip | Monitor, no action unless recurring |
|
||||
| Severity | When | Action |
|
||||
| -------- | ------------------------------------ | -------------------------------------------------- |
|
||||
| Critical | Slate active >30min, complete outage | Trigger DR plan, provision fallback host |
|
||||
| Warning | Redis down, high CPU >30min | Investigate during business hours, restart service |
|
||||
| Info | Single service blip | Monitor, no action unless recurring |
|
||||
|
||||
## Post-Incident
|
||||
|
||||
1. Document timeline in incident channel
|
||||
2. Identify root cause
|
||||
3. Add prevention to TODO.md if actionable
|
||||
3. Add prevention to docs if actionable
|
||||
4. Update this runbook if new failure mode discovered
|
||||
|
||||
---
|
||||
|
||||
**Recovery targets:** RPO ≤24h, RTO ≤60min. Quarterly DR drill: simulate host loss, restore from backup.
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "1.0.7",
|
||||
"version": "1.0.9",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
apiVersion: 1
|
||||
|
||||
providers:
|
||||
- name: "default"
|
||||
orgId: 1
|
||||
folder: ""
|
||||
type: file
|
||||
disableDeletion: false
|
||||
updateIntervalSeconds: 10
|
||||
allowUiUpdates: true
|
||||
options:
|
||||
path: /etc/grafana/provisioning/dashboards
|
||||
@@ -0,0 +1,9 @@
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Prometheus
|
||||
type: prometheus
|
||||
access: proxy
|
||||
url: http://prometheus:9090
|
||||
isDefault: true
|
||||
editable: true
|
||||
Reference in new issue
Block a user