diff --git a/docs/admin-guide.md b/docs/admin-guide.md index dbc124e..e984ed1 100644 --- a/docs/admin-guide.md +++ b/docs/admin-guide.md @@ -115,15 +115,26 @@ curl -X PATCH https://api.occupymedia.cc/connections/$CONN/publish-governance \ While held, the publish/boost route answers `202 awaiting_publish_approval` with the poll id; the poll closes after a day and rejects unanswered. Reconnecting an account keeps the setting. Changing the setting does not cancel an already-open poll. The Mastodon **boost** route runs behind the same gate. -## 6.2 Files SSO (Hub as the OIDC issuer for oCIS) +## 6.2 Control Room & media workers +The Live Control Room drives real encoders — gate it carefully. Permissions: `media.view`/`director.view` (watch), `director.take` (TAKE/CUT/PST), `director.lock_program`, `director.override_auto`, `director.change_layout` (overlays), `media.manage` (desired spec). + +Operational notes: + +- A channel must be **assigned to a media worker** before any switch succeeds ("channel is not assigned to a media worker"). Configure ingest sources and scenes first; select the preview bus. +- Capacity admission is part of every command transaction: an over-quota start is queued (`PENDING_CAPACITY`) and a stop offers its freed capacity to that queue. +- Preview URLs are signed capabilities on the API origin — set `PUBLIC_ORIGIN` correctly or same-origin checks reject the browser's preview requests. +- The worker reports the **realized version**; the UI's tally says "Tally unknown" until the worker confirms. If panels show thumbnails but never go live, check native HLS support and that preview signatures aren't being stripped by a proxy. +- Every switch is idempotent (`Idempotency-Key`) and lands in the audit chain as `director.` — incident review replays commands from the audit log. + +## 6.3 Files SSO (Hub as the OIDC issuer for oCIS) `files.occupymedia.cc` signs in through the Hub: the issuer is `https://api.occupymedia.cc/oidc`, client id `web`, PKCE only, auto-consent for that first-party client. Phase 2 (migration 034) makes sessions revocable and adds rotating refresh tokens bound to the Hub session — Hub logout reaches Drive within one access-token TTL (≤1 h). Operational notes: - `OIDC_KEY_DIR` must be a persistent volume (the API generates and seals its ES256 keyset there on first boot; losing it logs every Drive session out). - `FILES_OCID_USER` and `OIDC_RESERVED_USERNAMES` are never claimable as files accounts (`admin` is reserved by default). - Sign-in requires an **active membership** and a username usable as an account name; otherwise the consent endpoint fails closed. -## 6.3 Verification harnesses +## 6.4 Verification harnesses ```bash RADAR_EMAIL=… RADAR_PASSWORD=… node scripts/e2e/governance-sim.mjs # S1–S9 governance scenarios, live diff --git a/docs/engineering-guide.md b/docs/engineering-guide.md index e40e962..3dc0d1d 100644 --- a/docs/engineering-guide.md +++ b/docs/engineering-guide.md @@ -127,7 +127,7 @@ Route: `PATCH /connections/:id/publish-governance` (requires `chatgov.manage`). ## 6. Frontend (apps/web) -Tailwind v4 `@theme` tokens (dark-first OKLCH, `@theme inline` brand hook for tenant theming), hand-rolled primitives, cmdk palette, TanStack Virtual streams (500+ items/column, scroll-into-view), Tiptap compose with per-platform char counters/previews. **Streams flagship:** capability-gated amplify bars, keyboard `j/k` navigate, `r` repost, `q` quote. All data from `src/lib/` mock-registry (fixtures are seeded-deterministic). QA: `apps/web/scripts/visual-qa.mjs` — 14 states, 0 console errors; byte-diff confirms every interaction renders. +Tailwind v4 `@theme` tokens (dark-first OKLCH, `@theme inline` brand hook for tenant theming), hand-rolled primitives, cmdk palette, TanStack Virtual streams (500+ items/column, scroll-into-view), Tiptap compose with per-platform char counters/previews. **Streams flagship:** capability-gated amplify bars, keyboard `j/k` navigate, `r` repost, `q` quote. The Control Room (`app/(main)/control-room/`) runs on the live API — signed preview taps, realtime tally over the websocket client, fixture fallback when offline. QA: `apps/web/scripts/visual-qa.mjs`. ## 7. Testing strategy @@ -150,9 +150,17 @@ pnpm --filter @tactical-media-broadcast-hub/api test `/healthz` (liveness), `/readyz` (TCP probes: postgres/redis/temporal with per-component status). OTel OTLP tracing skeleton (disabled-safe without collector). Structured pino logs with credential-key redaction. Grafana (3001) provisioned against Prometheus (9090). Full metrics rollout is WP14. -## 9. Level 2 systems *(planned — designs in proposal.md §4.4)* +## 9. Level 2 systems — status -Live Source Monitor (adaptive polling, anti-flapping, per-host budgets); media engine (persistent encoder, graph-rewire switching, GStreamer decision gate); Auto Director (policies/rules/override/program-lock); control room (Preview/Program, TAKE/CUT); archive/grabber/crosspost (S3-mandated, encrypted, content-deduped). +Shipped and wired end-to-end: + +- **Media control / director** (`apps/api/src/media-control/`, `apps/api/src/production/`): the `PUT desired` / `take` / `cut` / `pst-select` / `lock` / `override` surface. Each command is a single PostgreSQL transaction — channel row `FOR UPDATE`, idempotency receipt lookup, director-state fold (locked/override), bus moves, capacity admission (§4.4 quota; over-quota rolls back everything, a stop drains the pending queue), desired-version floor across DB+Redis, and an `audit_append` receipt (`director.`, the transactional outbox). The worker observes desired snapshots over signed channels and reports the **realized version**; tally in the UI is worker-observed state, never optimistic. +- **Production spec** (`production.ts`, mirrored in `apps/media-worker`): scenes (`single/pip/2x1/2x2/3x3/4x4/custom`, per-slot `fit`, per-slot audio `off/on/primary` + gain, ≤1 primary slot), sources (HLS URL or video-asset playlist), overlays (`lower_third/chyron/logo/clock`, layers 0–7, rendered revisions as content-addressed PNGs, chyron speed/strip params, server clock), transitions (`cut` or `fade` 300–2000 ms), program master gain. Strict parsers reject injectable strings (`;`, `[`, `,`, `'`, `movie=`) so nothing rides along into FFmpeg graphs. +- **Control Room UI** (`apps/web/src/app/(main)/control-room/`): multiview grid (2x1–4x4/custom, up to 4 live HLS tiles, tap-to-focus, visibility-gated 1 s/2 s preview polling), switcher bar (PST/TAKE-at-frame-boundary/CUT-immediate, lock, override modes `until_released/five_minutes/until_source_offline/until_next_event`), PST preview, audio monitor, overlay editor (`director.change_layout`), fixture mode when offline. Permission gates: `media.view`, `director.view`, `director.take`, `director.lock_program`, `director.override_auto`, `director.change_layout`, `media.manage`. +- **Signed previews** (`production/preview.ts` + controller): capability URLs (expires+HMAC) minted by the authenticated endpoint; the public read route is same-origin only, serves plaintext renders, never redirects encrypted S3 objects, ETag/304-aware. +- **Worker fence** (`apps/api/src/director/`): internal `state/fence/apply` endpoints the worker uses to apply desired snapshots under a fencing token; web ingest, PeerTube and the transcode pool per their own docs. + +Still planned: media-engine graph rewiring beyond the current FFmpeg-normalize path, LLM video AI (design only), content search rollout. ## 10. Key file map @@ -165,6 +173,8 @@ Live Source Monitor (adaptive polling, anti-flapping, per-host budgets); media e | RBAC guard | `apps/api/src/rbac/rbac.guard.ts` | | Audit chain | `migrations/001_tenants.sql` (SQL) + `apps/api/src/audit/` | | Publishing + publish governance + amplify | `apps/api/src/publishing/`, `apps/scheduler/src/activities/dispatch-gate.ts` | +| Media control / director / production spec | `apps/api/src/media-control/`, `apps/api/src/production/`, `apps/media-worker/src/production.ts` | +| Control Room UI | `apps/web/src/app/(main)/control-room/`, `apps/web/src/components/control-room/` | | Matrix SSO broker | `apps/api/src/matrix-sso/` | | Connector SDK | `packages/connector-sdk/src/` | | Frontend mock-registry | `apps/web/src/lib/` | diff --git a/docs/user-manual-v2.md b/docs/user-manual-v2.md index 91bd3b9..e24382b 100644 --- a/docs/user-manual-v2.md +++ b/docs/user-manual-v2.md @@ -205,6 +205,39 @@ The math is real: it uses the connector SDK's `capacitySnapshot` rate policy. In --- +## 7.1 Live Control Room — multiview switching on a large screen + +**Where.** **Control Room** in the sidebar (`/control-room`). Built for a big browser window: on wide viewports you get the full switcher layout — multiview grid and switcher bar on one side, PST preview, audio monitor and overlay editor on the other. On a phone it collapses to a single focused tile. + +**Prerequisites.** A **virtual channel** assigned to a media worker, with ingest sources and production scenes configured, and the preview bus selected. Without that the page says exactly that and shows no panels. Every panel needs `media.view`/`director.view`; switching needs `director.take`; locking needs `director.lock_program`; overriding auto-director needs `director.override_auto`; changing overlays needs `director.change_layout`. + +**The multiview grid.** Pick a layout — **2x1, 2x2, 3x3, 4x4, or custom** (1–4 columns). Tile 1 is always **PGM** (the encoder's actual output). Every other tile is an ingest source. Behavior per tile: + +- **Tap to focus.** A focused tile is promoted into the limited live slots (up to 4 on desktop, 1 on mobile). +- **Upgrade live / Remove live** (3x3/4x4/custom tiers): promote a tile from thumbnail refresh to a live HLS video. +- **Tally borders.** Red border = on program, green = on preview. Tally comes from the **worker's realized version** — until the worker confirms it applied your desired state, the header says *"Tally unknown · awaiting matching observed version"* and borders stay neutral. +- **Bandwidth care.** Selected tiles poll previews every 1 s, the rest every 2 s; polling pauses when the tab is hidden or a tile scrolls off-screen. Live video uses native HLS; without it you get continuously refreshed thumbnails. +- The desired/observed version strip at the top (`Desired v12 · Observed v11`) shows whether the worker has caught up. + +**Switching.** The switcher bar works in broadcast semantics: + +1. **PST scene** — pick the scene to prepare (layouts: `single`, `PiP`, `2x1`, `2x2`, `3x3`, `4x4`, custom). +2. **TAKE** — PST→PGM at the *next ready frame boundary* (clean cut). +3. **CUT** — request an *immediate* switch. +4. Both wait for worker confirmation; nothing claims success from the click alone. +5. **Lock program** — freezes switching (the bar shows `LOCKED`). +6. **Operator override** — pull the channel out of automation: `Until released`, `Five minutes` (auto-expires), `Until source offline`, `Until next event`, or back to `auto`. The status word reads `AUTO`, `MANUAL`, or `LOCKED`. + +Every switch is an idempotent, audited command (`Idempotency-Key` safe to retry); the API refuses when the preview bus is empty or the worker is not assigned. + +**Audio.** Scenes carry per-slot audio: `off`, `on`, or `primary` (at most one primary slot per scene) with a per-slot gain in dB, plus a program **master gain**. The **Audio Monitor** panel meters the monitored taps; muted-and-polled like the video tiles. + +**Overlays.** The **Overlay Editor** manages per-scene graphics: **lower third**, **chyron** (scrolling strip — speed and strip width are set per overlay), **logo**, and **server clock** (`HH:MM` / `HH:MM:SS`). Overlays stack in layers 0–7 at a set position; rendered overlays go through a render revision (the worker rasterizes them to PNG) and only *rendered* revisions can go live. Editing requires `director.change_layout`. + +**Offline / fixture mode.** With the API unreachable the page still renders with fixture data, clearly marked — buttons record the request but *"Tally awaits a simulated observed frame."* + +--- + ## 8. Team **Status: live (members, invite, role change, remove) · sample data (asset library, always).**