# 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.
Cloudflare deployment decrypts the API token through the repo's agenix workflow and builds web
assets through `scripts/build-web.sh` so local and runner build paths stay aligned.
