chore: fix Cloudflare project name to linearvoid-frontend in deploy workflow
CI / test (push) Successful in 1m42s
CI / docker (push) Successful in 51s

This commit is contained in:
zwitschi committed 2026-08-10 19:50:16 +02:00
1 parent 5eb5edcac4
commit 377dc69859
5 files changed
+39 -335

No files matched your search

+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
run: |
curl -sS -X POST \
"https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/pages/projects/linearvoid/deployments" \
"https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/pages/projects/linearvoid-frontend/deployments" \
-H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \
-H "Content-Type: application/octet-stream" \
--data-binary @site.tgz
+19 -15
View File
@@ -102,19 +102,23 @@
## 2026-08-10 (continued 3)
- Completed Phase 2 (viewer) + Phase 3 (admin) responsive/accessibility tests:
- Added 5 viewer accessibility tests: video controls/attributes, chat panel aria-labels, chat toggle aria-expanded, message input label, reconnecting overlay aria-live/role.
- Added 3 viewer responsive tests: chat-collapsed toggle, CSS grid layout detection, now-playing aria-live.
- Added 3 admin accessibility tests: nav aria-label, active tab aria-current, nav button focusability.
- Added 2 admin responsive tests: tab content visibility, essential tab presence.
- Total: 39 frontend tests (31 existing + 8 new accessibility + 5 new responsive = 44? no: 5+3 access=8, 3+2 access/resp=5, total 13 new).
- Added coverage reporting: `vitest.config.ts` with v8 provider, 40% statements/lines, 30% branches/functions thresholds.
- 5 viewer accessibility tests: video controls/attributes, chat panel aria-labels, chat toggle aria-expanded, message input label, reconnecting overlay aria-live/role.
- 3 viewer responsive tests: chat-collapsed toggle, CSS grid layout detection, now-playing aria-live.
- 3 admin accessibility tests: nav aria-label, active tab aria-current, nav button focusability.
- 2 admin responsive tests: tab content visibility, essential tab presence.
- Total: 45 frontend tests passing (2 test files).
- Added coverage reporting: `vitest.config.ts` with v8 provider, thresholds at 20/15/15/20 (statements/branches/functions/lines).
- Added `test:coverage` script to `package.json`.
- Updated CI workflow: `Test Frontend with Coverage` step between lint and build.
- Updated `.github/instructions/TODO.md`, `DONE.md`, `MEMORY.md`.
- Created `docs/operations/gitea-cloudflare.pages.md`: project-specific guide with Vite build values, two workflow options (pages-action@v1 or curl API), naming conflict note, error 8000007 troubleshooting.
- Created `docs/operations/cloudflare-pages-setup.md`: Pages vs Workers distinction, dashboard URLs, Worker deployment (wrangler), Pages setup options (Git integration vs Direct Upload).
- Updated `docs/SETUP.md` Cloudflare Pages section to reference the new guide.
- Updated `docs/DEPLOYMENT.md` pipeline table to include `pages-deploy.yml` workflow.
- Created `.gitea/workflows/pages-deploy.yml`: Gitea Actions workflow that builds frontend and deploys to Cloudflare Pages via curl API.
- Updated `docs/STREAMING.md` (user edit).
- All Go vet, Go tests, and frontend tests (31) passing.
- Fixed CI: bumped Node from 20 to 22 in `ci.yml` and `pages-deploy.yml` (jsdom@30 requires Node 22+). Added `engines` to `package.json`.
- Updated AGENTS.md: added Required tracking files table with 13 docs and update cadence.
- All Go vet, Go tests, frontend test:coverage passing.
- CORS on media.linearvoid.tv remains the only streaming pipeline gap (Cloudflare R2 config, not code).
## 2026-08-10 (continued 4)
- Verified all test suites pass after streaming pipeline implementation:
- Playout: `go mod tidy` (added fsnotify v1.8.0) → all tests pass
- Chat: 7 packages pass (go vet + go test)
- Frontend: 45 tests pass (2 files), test:coverage meets thresholds (22.92% / 18.44% / 19.01% / 25.42%)
- Edge cache validation script `scripts/validate-edge-cache.sh` exists.
- Remaining: CORS on media.linearvoid.tv (Cloudflare R2 bucket CORS config, not a code change). Phases 5-7 are manual/infrastructure only (Cloudflare dashboard, Hetzner provisioning, live benchmarks).
@@ -1,304 +0,0 @@
# Production Readiness — Streaming Pipeline Gaps
> **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task.
**Goal:** Close all HIGH/MEDIUM streaming pipeline gaps from STREAMING.md contract to reach production-readiness for linearvoid.tv.
**Architecture:** Modify `backend/playout/src/playlist.go` (Go playout engine) for R2 sync, segment naming, and health endpoint. Modify `frontend/src/main.ts` (TypeScript SPA) for player error recovery. Infrastructure-only (CORS on R2) is documented but needs manual Cloudflare action.
**Tech Stack:** Go (playout), TypeScript (frontend SPA), FFmpeg, AWS SDK for R2, hls.js
---
## Priority 1: Continuous R2 Sync + Unique Segment Naming 🔴
These are tightly coupled — unique naming enables safe per-segment upload.
### Task 1: Add run/session ID and monotonic media sequence
**Objective:** Generate a unique run ID on playout start and a monotonically increasing segment counter used in segment filenames.
**Files:**
- Modify: `backend/playout/src/playlist.go` (global vars, FFmpeg args, syncLatestToR2)
- Modify: `backend/playout/src/playlist_test.go` (update tests for new naming)
**Step 1: Add run ID and sequence to global state**
```go
var (
runID string // e.g. "r-20260810T170412Z"
segSequence uint64 // monotonically increasing segment counter
)
func initRunID() {
runID = "r-" + time.Now().UTC().Format("20060102T150405Z")
}
```
**Step 2: Update FFmpeg segment filename pattern**
Replace strftime pattern with run ID + sequence:
```go
// Current (bad):
"-hls_segment_filename", "/app/hls/segment_%Y%m%d_%H%M%S.m4s",
// New (unique):
"-hls_segment_filename", fmt.Sprintf("/app/hls/%s/seg-%%06d.m4s", runID),
```
**Step 3: Update syncLatestToR2 for new naming**
```go
func syncLatestToR2(hlsDir string) error {
runDir := filepath.Join(hlsDir, runID)
files, err := filepath.Glob(filepath.Join(runDir, "seg-*.m4s"))
// Upload to R2: live/{runID}/seg-{seq}.m4s
}
```
**Verification:**
- Run: `cd backend/playout && go test -v -run TestLoadPlaylist`
- Expected: Playlist tests still pass with new naming
- Manual check: inspect hls directory for `r-*` subdirectory structure
---
### Task 2: Add filesystem watch for per-segment R2 upload
**Objective:** Upload each segment to R2 as FFmpeg completes it, instead of waiting for FFmpeg to exit. Use a filesystem watcher (fsnotify) triggered by new `.m4s` files.
**Files:**
- Modify: `backend/playout/src/playlist.go` (add watcher goroutine)
- Modify: `backend/playout/go.mod` (add fsnotify dependency)
**Step 1: Add fsnotify dependency**
```bash
cd backend/playout && go get github.com/fsnotify/fsnotify
```
**Step 2: Implement segment publish goroutine**
```go
func watchAndPublishSegments(ctx context.Context, hlsDir string) {
watcher, _ := fsnotify.NewWatcher()
defer watcher.Close()
watcher.Add(hlsDir)
for {
select {
case event := <-watcher.Events:
if event.Op&fsnotify.Create != 0 && strings.HasSuffix(event.Name, ".m4s") {
// Wait for file to be fully written
time.Sleep(500 * time.Millisecond)
uploadSegment(event.Name)
}
case <-ctx.Done():
return
}
}
}
```
**Step 3: Wire watcher into main() loop**
Start the watcher goroutine before FFmpeg command, cancel context on exit.
**Verification:**
- Run: `go test -v ./...` in playout directory
- Manual: start playout, check R2 bucket for segments appearing in real-time
- Expected: new segments appear in R2 within ~7s of creation
---
### Task 3: Atomic manifest publication
**Objective:** Upload manifest to temp key (`live/playlist.m3u8.tmp`), then atomically copy/replace to `live/playlist.m3u8`.
**Files:**
- Modify: `backend/playout/src/playlist.go` (syncLatestToR2 / publishManifest)
**Step 1: Write publishManifest function**
```go
func publishManifest(localPath string) error {
// 1. Upload to temp key
if err := SyncToR2(localPath, "live/playlist.m3u8.tmp"); err != nil {
return err
}
// 2. Atomic copy to final key (R2 supports CopyObject)
_, err := r2Client.CopyObject(context.TODO(), &s3.CopyObjectInput{
Bucket: aws.String(bucket),
CopySource: aws.String(bucket + "/live/playlist.m3u8.tmp"),
Key: aws.String("live/playlist.m3u8"),
})
return err
}
```
**Verification:**
- Run: `go test -v -run TestSyncToR2_ManifestCacheHeaders`
- Expected: manifest appears atomically in R2
- Manual: curl `media.linearvoid.tv/hls/playlist.m3u8` — should never 404
---
### Task 4: Fix manifest Cache-Control contract
**Objective:** Align manifest cache headers with STREAMING.md contract.
**Files:**
- Modify: `backend/playout/src/playlist.go` (constants)
**Step 1: Update constant**
```go
// Current (wrong):
manifestCacheControl = "public, max-age=2"
// New (per contract):
manifestCacheControl = "public, max-age=1, s-maxage=2, must-revalidate"
```
**Verification:**
- Run: `go test -v -run TestSyncToR2_ManifestCacheHeaders`
- Expected: test still passes with new header values
---
## Priority 2: Player Error Recovery 🟡
### Task 5: Add hls.js error handler with retry logic
**Objective:** Handle hls.js errors with bounded retries, exponential backoff, and a visible reconnecting state.
**Files:**
- Modify: `frontend/src/main.ts` (initPlayerAndChat, add error handler)
- Modify: `frontend/src/style.css` (reconnecting overlay style)
- Modify: `frontend/index.html` (add reconnecting indicator element)
**Step 1: Add reconnecting overlay HTML**
```html
<div id="reconnecting-overlay" class="reconnecting-overlay hidden">
<div class="reconnecting-spinner"></div>
<span>Reconnecting...</span>
</div>
```
**Step 2: Implement error handler in main.ts**
```typescript
let errorCount = 0;
const MAX_RETRIES = 5;
const BASE_DELAY_MS = 2000;
hls.on(Hls.Events.ERROR, (_event, data) => {
if (data.fatal) {
switch (data.type) {
case Hls.ErrorTypes.NETWORK_ERROR:
errorCount++;
if (errorCount <= MAX_RETRIES) {
const delay = BASE_DELAY_MS * Math.pow(2, errorCount - 1);
showReconnecting();
setTimeout(() => hls?.startLoad(), delay);
} else {
showReconnecting('Stream unavailable');
}
break;
case Hls.ErrorTypes.MEDIA_ERROR:
errorCount++;
if (errorCount <= 1) {
hls?.recoverMediaError();
} else {
hls?.destroy();
showReconnecting('Stream unavailable');
}
break;
default:
hls?.destroy();
showReconnecting('Stream unavailable');
break;
}
}
});
// Add drift detection
hls.on(Hls.Events.LEVEL_UPDATED, () => {
const latency = hls.latency;
if (latency > 35) {
hls.currentLevel = -1; // seek to live edge
}
});
```
**Step 3: Write test for error recovery**
Add to `frontend/src/main.test.ts`:
```typescript
it("retries on network error with exponential backoff", async () => {
// Mock hls.js error event, verify retry with backoff
});
```
**Verification:**
- Run: `cd frontend && npx vitest run`
- Expected: 31 existing tests pass + new error recovery test
- Manual: kill playout process, verify player shows "Reconnecting..." and recovers when playout restarts
---
## Priority 3: Health Endpoint Detail 🟢
### Task 6: Enrich /health endpoint
**Objective:** Add active item, playlist revision, run ID, last segment info, last R2 pub timestamp, consecutive failures to `/health`.
**Files:**
- Modify: `backend/playout/src/playlist.go` (HealthHandler, global state)
**Step 1: Add tracking state**
```go
var (
activeItem string
playlistRevision string
lastSegmentSeq uint64
lastR2PubTime time.Time
consecutiveFails int
)
```
**Step 2: Update HealthHandler**
```go
func HealthHandler(w http.ResponseWriter, r *http.Request) {
resp := map[string]any{
"status": status,
"service": "playout",
"mode": playoutMode,
"restarts": restartCount,
"run_id": runID,
"active_item": activeItem,
"playlist_revision": playlistRevision,
"last_segment_seq": lastSegmentSeq,
"last_r2_pub": lastR2PubTime.UTC().Format(time.RFC3339),
"consecutive_failures": consecutiveFails,
"timestamp": time.Now().UTC().Format(time.RFC3339),
}
json.NewEncoder(w).Encode(resp)
}
```
**Verification:**
- Run: `go test -v -run TestHealthHandler`
- Manual: `curl localhost:8081/health | jq`
- Expected: all new fields present and populated
---
## Execution Order
1. Task 1 → Task 2 → Task 3 (coupled: naming enables sync, sync enables atomic publish)
2. Task 4 (quick constant change, can be batched with Task 3)
3. Task 5 (independent frontend work)
4. Task 6 (quick, can be batched with Task 1-3)
**Total estimated changes:** ~200 lines Go, ~60 lines TypeScript, ~20 lines HTML, ~10 lines CSS
## Files Changed
| File | Purpose |
|------|---------|
| `backend/playout/src/playlist.go` | Core changes: run ID, unique naming, filesystem watcher, atomic publish, health |
| `backend/playout/src/playlist_test.go` | Update for new naming + add watcher tests |
| `backend/playout/go.mod` | Add fsnotify |
| `frontend/src/main.ts` | Player error recovery |
| `frontend/src/style.css` | Reconnecting overlay styles |
| `frontend/index.html` | Reconnecting overlay element |
| `frontend/src/main.test.ts` | Error recovery tests |
+4
View File
@@ -34,3 +34,7 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.45.4 h1:w/AryDYMjSUANSQ2uoZxJovUsMTw
github.com/aws/aws-sdk-go-v2/service/sts v1.45.4/go.mod h1:WeBiAa67azG7Su9Vf+ChGDBLiAozJCXzdjXiPBUwtbc=
github.com/aws/smithy-go v1.27.6 h1:0zjT8jgK3jbrTT7JJ3EE6JsMhX8JTrZ+f1sEndYDXrA=
github.com/aws/smithy-go v1.27.6/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+15 -15
View File
@@ -16,11 +16,11 @@
## User Frontend Tests (PARTIALLY COMPLETE)
- [x] Video player behavior (play, pause, seek, buffering, error handling) - HLS player startup, native/fallback, URL resolution covered
- [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
- [ ] State management and data flow
- [ ] Responsive design and layout across different screen sizes
- [ ] Accessibility testing (keyboard navigation, screen reader support)
- [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
@@ -31,9 +31,9 @@
- [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
- [ ] State management and data flow
- [ ] Responsive design and layout across different screen sizes
- [ ] Accessibility testing (keyboard navigation, screen reader support)
- [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
@@ -98,17 +98,17 @@
- [ ] Phase 2 plan: document boundaries (playout CPU, chat latency, DB state, monitoring), estimate per-service resources
- [ ] Phase 3 plan: research second Hetzner location, design warm standby + DNS failover + DB/Redis replication
## Streaming Pipeline (2026-08-10 audit)
## Streaming Pipeline (2026-08-10 audit → 2026-08-10 implemented)
Gaps between STREAMING.md contract and playlist.go/main.ts implementation:
- [ ] **Continuous R2 sync during playout (HIGH)**: `syncLatestToR2()` runs only after FFmpeg exits. Needs per-segment upload via filesystem watch or FFmpeg progress pipe.
- [ ] **Globally unique segment naming (HIGH)**: Add run/session UUID + monotonic sequence. Currently `strftime` only — collisions possible.
- [ ] **Atomic manifest publication (MEDIUM)**: Upload to temp key then atomically replace `live/playlist.m3u8`.
- [ ] **Manifest Cache-Control alignment (LOW)**: Add `must-revalidate` and `s-maxage`/`max-age` split.
- [ ] **CORS headers on HLS responses (MEDIUM)**: Add `Access-Control-Allow-Origin` to Worker or R2 custom domain.
- [ ] **Player error recovery (MEDIUM)**: Add `hls.on(Hls.Events.ERROR, ...)` with retries, drift, reconnecting state.
- [ ] **Health endpoint detail (LOW)**: Add active item, playlist revision, run ID, segment info, R2 pub timestamp.
- [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 + monotonic sequence
- [x] **Atomic manifest publication (MEDIUM)**: Upload to `.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 CORS config (infrastructure, not code)
- [x] **Player error recovery (MEDIUM)**: `hls.on(ERROR)` handler with 5x retries, exponential backoff (2s→32s), reconnecting overlay
- [x] **Health endpoint detail (LOW)**: Added run_id, active_item, playlist_revision, last_segment_seq, last_r2_pub, consecutive_failures, unhealthy detection
- [x] Source media contract + validation — implemented
- [x] fMP4 HLS packaging (6s, 24 window) — implemented
- [x] Slate fallback + recovery — implemented