every.channel/evolution/proposals/ECP-0123-instant-station-guide-and-player-warmup.md
Conrad Kramer 0c41193867
Some checks failed
ci-gates / checks (push) Failing after 1m41s
deploy-cloudflare / checks (push) Successful in 1m58s
deploy-cloudflare / deploy (push) Failing after 6s
Make ECP lint runner-compatible
2026-06-10 03:41:11 -07:00

3.8 KiB

ECP-0123: Instant Station Guide and Player Warmup

Status: Draft

Problem statement

The hosted watch page can feel broken when /api/public-streams is slow, empty, or temporarily unreachable: the channel rail waits on the network before showing stations. Even after stations appear, the first channel tap pays the @moq/watch module import cost before the player element can mount. That increases time to first playing frame and makes channel selection feel unreliable.

Constraints

  • Keep the first screen a usable TV/player interface, not a marketing page.
  • Preserve manual tuning, share links, DVR mode, public station refresh, and existing relay fields.
  • Keep rollback simple: the live API remains authoritative after it answers.
  • Avoid Chrome-only transport assumptions and keep live playback failure messages actionable.
  • Keep the change static-site compatible so Cloudflare Worker asset deployment stays simple.

Decision

Embed the current starter LA station guide in index.html and render it immediately on first load. Also keep a short-lived local guide cache, merge cached entries with the HTML seed, and refresh /api/public-streams in the background with a bounded timeout. A slow or empty refresh no longer clears already visible channels.

Change the web client, publisher CLI/module defaults, runbook examples, and remote watch E2E default relay to https://relay.every.channel/anon. Default wt-publish and nbc-wt-publish to fMP4 passthrough so hotpatch-launched publishers emit the timescale and trackId CMAF catalog metadata expected by the hosted @moq/watch element. Preload/preconnect the primary player dependencies and warm the @moq/watch custom element after first paint. Let @moq/watch use its available live transport fallback instead of forcing WebTransport-only playback. Add client performance marks for guide first render, guide fetch, watch request, player module readiness, player mount, catalog/live status, and first observable canvas frame when the browser exposes it.

Alternatives considered

  • Wait only for /api/public-streams. Rejected because the UI becomes blank when the directory is slow and because children should be able to tap visible channels immediately.
  • Make the API response cacheable at the CDN. Rejected as the only fix because active streams are short TTL and stale-but-visible fallback belongs in the client.
  • Bundle @moq/watch into the static site. Deferred because CDN import fallback is already in use; warming and modulepreload reduce tap latency without changing the build graph.

Rollout / teardown plan

Ship the static web change with the existing Worker asset deploy and roll the publisher hotpatch binary to the LA nodes so their catalogs match the current watcher schema. Validate with clean-cache desktop/mobile browser loads and check the app's window.__ecPerf marks plus a live tune through the public relay. The Forgejo workflows use the locally registered namespace-profile-linux-medium runner label and ecp-forge runs a persistent forgejo-runner-agent service with a normal shell tool PATH so the Cloudflare deploy can actually leave the queue on the self-hosted forge. Teardown is removing the HTML seed/cache/warmup path, returning to live-API-only station rendering, and explicitly passing --passthrough=false only if an older watcher path is restored.

Forgejo CI and deploy jobs run inside the repository Nix dev shell instead of downloading generic Linux Rust, Trunk, age, or Node binaries. This keeps self-hosted NixOS runners reproducible and prevents dynamic-linker failures from blocking the Cloudflare asset rollout.

The workflow ECP gate starts at ECP-0120 because older proposals predate the current lint shape. The lint script uses ripgrep when available and falls back to GNU grep on the Forgejo runner.