# TVs Implemented State ## Admin - `/admin/tvs` renders inside Ballbox. - Ballbox owns the canonical TVs entity model for clubs, venues, screens, campaigns, assignments, tv devices, and impressions. - TVs admin/API reads from Ballbox Postgres through Prisma. - TVs admin/runtime pages and route handlers now import direct backend modules (`lib/tvs-backend-runtime.ts`, `lib/tvs-backend-operations.ts`, `lib/tvs-backend-*.ts`) plus shared TV types from `lib/tvs-backend-types.ts` instead of routing that access through `lib/ballbox-store.ts`. - No Streetcast compatibility layer or in-memory TVs bridge remains in runtime code. - `/admin/tvs` links each screen directly to its runtime and manifest. - `/admin/tvs` now supports admin CRUD for screens and tv devices through Ballbox-owned admin APIs. - `/admin/tvs` now supports admin CRUD for campaigns, creatives, and screen assignments through Ballbox-owned admin APIs. ## APIs - `/api/tvs/health` - `/api/tvs/screens` - `/api/tvs/manifest/[screenId]` - `/api/tvs/heartbeat` - `/api/tvs/impression` - `/api/admin/tvs/operations` - `/api/admin/tvs/screens` - `/api/admin/tvs/screens/[screenId]` - `/api/admin/tvs/tv-devices` - `/api/admin/tvs/tv-devices/[tvDeviceId]` - `/api/admin/tvs/campaigns` - `/api/admin/tvs/campaigns/[campaignId]` - `/api/admin/tvs/campaigns/[campaignId]/creatives` - `/api/admin/tvs/creatives/[creativeId]` - `/api/admin/tvs/campaigns/[campaignId]/assignments` - `/api/admin/tvs/assignments/[assignmentId]` - TVs API contract is now defined from `Zod` schemas in `lib/contracts/tvs.ts`. - Breaking changes in TVs responses are expected to fail `pnpm test:contracts`. - Error responses use a strict shared shape: `{ ok: false, error, status }`. ## Runtime - `/admin/screens` lists available screens. - `/player/[screenId]` renders the lightweight TV runtime path served as plain HTML + JavaScript. - The runtime rotates creatives from a Ballbox DB-backed manifest derived from active screen-to-campaign assignments. - The runtime posts a heartbeat to Ballbox every 30 seconds, independent of manifest refreshes. - Heartbeats persist to the linked Ballbox `TvDevice.lastSeen`. - Manifest generation respects campaign `startAt`/`endAt` and assignment `startsAt`/`endsAt` windows. - Runtime now warms a screen-scoped browser media cache and prefers cached blob playback when available. - Runtime keeps the last good manifest during transient manifest fetch failures so cached media can continue playing offline. - Runtime preloads manifest creatives after refresh and evicts stale cached media when programming changes. - Cache/media/impression/heartbeat/manifest-refresh behavior now comes from one shared runtime core in `lib/tvs-player-runtime-core.ts`; React and lightweight HTML+JS are delivery adapters over the same logic. - Browser e2e coverage now verifies cached blob playback survives an offline transition on `/player/[screenId]`. - Runtime now exposes operator-facing diagnostics for playback source, Cache API support, cached creative count, network state, and manifest refresh status to support real-device parity checks. - Runtime diagnostics now include user-agent plus capture timestamp and a built-in `Export parity snapshot` action that downloads a JSON evidence file for real-device parity runs. - Stock demo seed scheduling now keeps `screen_001` and `screen_002` on active campaign windows after `pnpm db:seed`, so local parity/e2e runs no longer depend on manually refreshing fixture dates first. - TVs admin and `/admin/screens` link operators to the lightweight player path. - This is the main special-case surface in Ballbox: TVs stay inside the same repo/deploy/domain, but the device runtime is intentionally served outside the Next client/React runtime as plain HTML + JavaScript. - `GET /api/tvs/manifest/[screenId]` returns: - `404` when the screen does not exist - `409` when the screen exists but has no active programming ## Reporting - The runtime posts impressions to Ballbox using `/api/tvs/impression`. - Ballbox records impressions directly in Ballbox Postgres. - A successful impression response returns a Ballbox `impressionId`. - `POST /api/tvs/impression` now returns `409` when the creative exists but is not active for the target screen. - Failed runtime impression writes now enter a bounded browser retry queue and flush on a timer plus browser `online` events. ## Bootstrap - TVs demo/bootstrap data still lives in `prisma/seed-data.ts`. - Running `pnpm db:seed` upserts the TVs base dataset and prints a seed summary with entity counts. - Post-`pnpm db:push` plus `pnpm db:seed` verification confirms Ballbox Postgres keeps canonical `Screen.vendingMachineId` and `Impression.tvDeviceId`, with no legacy `deviceId` column reintroduced. ## Contract Enforcement - `TVS_CONTRACT_VERSION` currently lives in `lib/contracts/tvs.ts`. - Contract tests cover: - route success/error shapes - scheduling invariants - strict schema parsing with no extra fields - runtime helper invariants for cache naming, manifest continuity, and bounded retry behavior - Playwright e2e covers: - lightweight `/player/[screenId]` cached blob playback and runtime side effects when the target screen has active programming - admin CRUD for screens, tv devices, campaigns, creatives, and assignments - cached blob playback on a real browser engine - runtime continuity after the browser context goes offline - browser-level heartbeat and impression side effects against the real Ballbox DB - absence of page-level runtime errors during the offline transition - On `ballbox-first`, repo Playwright now uses a production-style server flow (`pnpm build:e2e && pnpm start:e2e`) instead of the unstable Next 16 dev-server path, matching the server mode that already validated reliably on this Pi.