# contexts.live — developer documentation

**cx.js:** v1.0.1
**cx-connect.js:** v2.2.3
**cx-render.js:** v3.0.2
**cx-keys.js:** v2.2.0
**cx-widgets.js:** v1.1.1
**cx-server.js:** v1.0.8
**editor.js:** v3.25
**cx-base.css:** v2.5
**Port:** 8008

---

## 1. Quick Start

```html
<!-- Minimal display page -->
<!DOCTYPE html>
<html>
<head>
    <link rel="stylesheet" href="/cx/css/cx-base.css">
    <link rel="stylesheet" href="/cx/css/cx-theme-dark.css">
    <script type="module" src="/cx/lib/cx.js"></script>
</head>
<body>
    <div data-cx="st" role="alert" aria-live="assertive"></div>
</body>
</html>
```

`cx.js` is the single entry point — it loads `cx-connect.js`, `cx-render.js`, and conditionally `cx-keys.js` and `cx-widgets.js` based on a DOM scan. Do not load these separately on display or control pages.

---

## 2. File Architecture

### JS files

| file             | purpose                                           | load |
|------------------|---------------------------------------------------|------|
| `cx.js`          | entry point — loader, dev/prod switch, DOM scan   | all display/control pages |
| `cx-connect.js`  | org connection, config, CXParam, pack cache, send | loaded by cx.js |
| `cx-render.js`   | parseCx, renderers, words, dispatch, init         | loaded by cx.js |
| `cx-keys.js`     | keyboard/keypad shortcuts                         | loaded by cx.js if `controlling` in DOM |
| `cx-widgets.js`  | timer widgets (countdown, progress, hourglass)    | loaded by cx.js if timer modifier in DOM |

`cx.js` static-imports `cx-connect.js` and `cx-render.js`, then conditionally imports `cx-keys.js` and `cx-widgets.js` based on a DOM scan at `DOMContentLoaded`. `cx-connect.js` imports socket.io from the CDN ESM build directly.

### Load order

**All display and control pages — single tag:**
```html
<script type="module" src="/cx/lib/cx.js"></script>
```

`cx-keys.js` is loaded automatically when `[data-cx*="controlling"]` is found in the DOM. There is no need to load it explicitly.

**Tool pages (STT etc.) — no rendering needed:**
```html
<script type="module" src="/cx/lib/cx-connect.js"></script>
<script src="your-tool.js"></script>
```

### Dev/prod switch

`cx.js` has a `DEV_MODE` constant at the top:

```js
const DEV_MODE = true;   // loads raw modules
const DEV_MODE = false;  // loads cx.bundle.js
```

Set to `false` before deploying, then run `cx/tools/cx-build.sh` to produce the obfuscated bundle. Set back to `true` for development.

### CSS files

| file                | purpose                              |
|---------------------|--------------------------------------|
| `cx-base.css`       | reset, variables, layout primitives  |
| `cx-theme-dark.css` | dark theme — pane colours, elements  |
| `cx-theme-light.css`| light theme equivalent               |

### Paths

| what              | path                         |
|-------------------|------------------------------|
| JS libraries      | `/cx/lib/`                   |
| CSS               | `/cx/css/`                   |
| Packs             | `/org/{slug}/packs/`         |
| Media             | `/org/{slug}/media/`         |
| Audience pages    | `/org/{slug}/user/`          |
| Operator pages    | `/org/{slug}/client/`        |
| Tools             | `/org/{slug}/tools/`         |

---

## 3. The data-cx Attribute

```
data-cx  ::= triple [ SP modifier ]*
triple   ::= source [ "." render [ "." language ] ]
modifier ::= key=value | flag
```

`data-cx` comes first in the element, before `class`.

```html
<div data-cx="st" class="cx-prev"></div>
<img  data-cx="score.image">
<div data-cx="st offset=-1 lang=mi pack=show2"></div>
```

`cx-render.js` auto-adds `cx-text` class to any text render element at init.

---

## 3a. Pack Scoping

Each element resolves its pack in this order:

1. Element-level `pack=` modifier
2. Nearest `data-cx="pane ... pack=X"` ancestor
3. URL `?pack=` parameter
4. Nothing — element renders nothing until a matching cue arrives

Packs load lazily — no preloading needed. When a pack is first needed and not in `packCache`, it is fetched and all relevant elements re-render automatically.

Incoming cues carry `packName`. Each element only responds to cues whose `packName` matches its resolved pack. This allows multiple independent packs on one page:

```html
<body data-cx="row">
    <div data-cx="pane width=50 pack=Fig">
        <header class="cx-header">
            <span data-cx="meta title"></span>
            <span data-cx="meta composer"></span>
        </header>
        <div data-cx="st.text scroll controlling gold index=ordinal"></div>
    </div>
    <div data-cx="pane width=50">
        <div data-cx="score.image scroll controlling pack=figaro-score"></div>
    </div>
</body>
```

Note: `pack=` on a plain HTML element with no `data-cx` (e.g. `<header pack=Fig>`) is ignored. Only `data-cx="pane ..."` elements participate in pack inheritance.

---

## 4. Sources, Renders, Language

### Sources

Sources are field names in the line object. Any field name works.

| token    | meaning                | notes                              |
|----------|------------------------|------------------------------------|
| `st`     | subtitles / main text  |                                    |
| `sd`     | stage description      |                                    |
| `ad`     | audio description      |                                    |
| `sl`     | sign language          |                                    |
| `sp`     | speaker / character    |                                    |
| `score`  | music score images     |                                    |
| `splash` | first-click gate       | unlocks audio autoplay             |
| `meta`   | metadata               | pack fields + live state (see below)|
| *custom* | any field name         |                                    |

### meta source — pack fields vs live state

`data-cx="meta title"` resolves from `json.meta.title` (pack data).

`cx-render.js` also maintains a `state.liveMeta` object for live runtime
values. Live fields take precedence over pack fields:

| field    | value                                      |
|----------|--------------------------------------------|
| `status` | socket connection state                    |
| `org`    | org slug                                   |
| `pack`   | currently loaded pack name                 |
| `line`   | current line number                        |
| `server` | server base URL                            |

```html
<span data-cx="meta title"></span>      <!-- from pack JSON -->
<span data-cx="meta status"></span>     <!-- live: "● connected" -->
<span data-cx="meta pack"></span>       <!-- live: current pack name -->
<span data-cx="meta line"></span>       <!-- live: current line number -->
```

Live fields update in place without a cue — as soon as connection state
or pack changes.

### Renders

| token     | default? | meaning                   |
|-----------|----------|---------------------------|
| `text`    | yes      | set as innerHTML          |
| `audio`   |          | Audio object, play        |
| `video`   |          | play video in element     |
| `image`   |          | set img src               |
| `timer`   |          | countdown display         |
| `glossary`|          | term definitions panel    |

`scroll` and `speak` are flag modifiers on text renders, not render types.

---

## 5. Modifiers

### Key=value

| key      | meaning                              | example       |
|----------|--------------------------------------|---------------|
| `pack`   | resolve from a specific pack         | `pack=show2`  |
| `lang`   | language for this element only       | `lang=mi`     |
| `offset` | line offset from current cue         | `offset=-1`   |

### Flags

| flag          | meaning                                                          |
|---------------|------------------------------------------------------------------|
| `controlling` | clicks send cues to server; on `meta.video` elements: video drives cues via rAF engine |
| `toolbar`     | show Blank/Prev/Next buttons (`words controlling` only)          |
| `auto`        | enable client-side autoplay from `cueIn` timecodes (`words controlling` only) |
| `nudge`       | attach nudge controls to this `words` component                  |
| `persist`     | hold content on blank — don't clear on cueOut                    |
| `loop`        | loop audio/video                                                 |
| `mute`        | mute audio                                                       |
| `controls`    | show native browser media controls on audio/video element        |
| `fade`        | fade images                                                      |
| `append`      | add to end, scroll to bottom                                     |
| `prepend`     | add to start                                                     |
| `inpend`      | sorted insert, deduplicates                                      |
| `now`         | mark current line visually                                       |
| `status`      | on `meta.video` elements: render status bar and cue log          |

### Auto modifier

`auto` on a `words controlling` element enables client-side autoplay:

```html
<div data-cx="st.text words controlling auto"></div>
```

When a line is clicked, `startAuto(n)` schedules all subsequent lines
using their `cueIn` timecodes plus the current nudge offset. Any manual
click or `stopAuto()` call cancels pending timers.

Autoplay state is available via the component API — see section 12.

---

## 6. Per-element Pack Scoping

```html
<div data-cx="st.words controlling pack=D827"></div>
<div data-cx="st pack=D827 lang=en"></div>
```

Named pack pre-loaded at startup. Element only updates for cues from that pack.

---

## 7. Page Parameters — CXParam

Set by `cx-connect.js` from URL parameters and JS variables.

| URL          | JS variable     | data-cx modifier | page-level only? |
|--------------|-----------------|------------------|------------------|
| `?pack=`     | `packParam`     | `pack=`          |                  |
| `?lang=`     | `langParam`     | `lang=`          |                  |
| `?offset=`   | —               | `offset=`        |                  |
| `?socket=`   | `socketParam`   | —                | yes              |
| `?speed=`    | `speedParam`    | —                | yes              |
| `?startCue=` | `startCueParam` | —                | yes              |
| `?braille=`  | `brailleParam`  | —                | yes              |
| `?fade=`     | `fadeParam`     | —                | yes              |

Pages set JS variables before the module import:

```html
<script>
    let packParam = 'show2';
    let langParam = 'mi';
</script>
<script type="module" src="/cx/lib/cx-render.js"></script>
```

`window.CXParam` exposes resolved values for debugging.

---

## 8. Pack JSON Format

```json
{
    "meta": {
        "cx":       "2.0",
        "id":       "show2",
        "title":    { "en": "Bluebeard's Castle" },
        "composer": { "en": "Béla Bartók" },
        "language": "en"
    },
    "lines": [
        {
            "type":    "text",
            "ordinal": "1",
            "cueIn":   12.0,
            "cueOut":  16.5,
            "sp":      { "en": "JUDITH" },
            "st":      { "text": { "en": "Bluebeard, this is your castle?" } },
            "ad":      { "text": { "en": "Judith looks up." }, "audio": { "en": "0003.mp3" } },
            "score":   { "image": "page-000001.svg" }
        }
    ]
}
```

Score images live in the main pack alongside subtitles.
Packs in `org/slug/packs/`. Media in `org/slug/media/`.

`cueIn`/`cueOut` replace legacy `timecodeIn`/`timecodeOut` — both accepted
by `cx-render.js` for backwards compatibility.

---

## 9. Socket Architecture

### Namespaces

```
/{slug}/client      — display clients
/{slug}/controller  — controlling operators
/{slug}/cueing      — reserved for future external cueing tool
```

### Events

| event     | direction           | meaning                        |
|-----------|---------------------|--------------------------------|
| `cue`     | controller → server | pack cue or inline text        |
| `cue`     | server → client     | line to display                |
| `cue`     | server → controller | sync highlight                 |
| `monitor` | client → server     | telemetry                      |
| `note`    | controller → server | production annotation          |

Note: `stop` and `nudge` socket events are no longer used for autoplay.
Autoplay runs client-side — see section 12.

### Cue message formats

```js
{ packName: 'show2', lineNumber: '4' }     // pack cue
{ packName: 'show2', lineNumber: '-1' }    // blank
{ text: 'Interval begins' }                // inline text (no pack)
{ text: 'Kia ora', lang: 'mi' }           // inline text with lang
```

`typeof lineNumber === 'string'` is the canonical test for a real pack cue.

---

## 10. Media Path Resolution

```
if absolute → use as-is
otherwise try:
    1. org/slug/media/{pack}/{triple}/{filename}
    2. org/slug/media/{triple}/{filename}
    3. org/slug/media/{pack}/{filename}
    4. org/slug/media/{filename}
```

Recommended structure:
```
org/slug/media/show2/
    ad.audio.en/0001.mp3
    score.image/page-000001.svg
```

---

## 11. cx-connect.js API

Imported by `cx-render.js`. Also loaded directly by tool pages.

```js
CX.slug          // org slug from URL path
CX.server        // server hostname
CX.port          // port (from config.json, default 8008)
CX.baseURL       // https://server:port
CX.fileBase      // /org/slug/
CX.connect(role) // → socket.io socket for /{slug}/{role}

sendMsg(text, lang)           // inline text cue
sendCue(packName, lineNumber) // pack line cue
sendNote(text)                // production annotation
requestWakeLock()             // request screen wake lock
loadOrgCSS()                  // inject /org/{slug}/css/*.css into <head>

window.CXParam                // resolved page parameters
window._cxControllerSocket    // controller socket
```

`CXConnect.init()` — called on DOMContentLoaded when `cx-render.js` is not
loaded. Connects to `/controller` and exposes `sendMsg` globally.

`window._cxRenderLoaded` — set by `cx-render.js` at file top to suppress
`CXConnect.init()` on render pages.

---

## 12. cx-render.js API

ES module. Imports `cx-connect.js`. Loaded by display and controller pages.

```js
window.CX2              // state object for debugging
state.json              // current pack JSON
state.lineNumber        // current line index (-1 = blank)
state.language          // current display language
state.packName          // current pack name
state.audioObjects      // source → Audio instance pool
state.sockets           // { client, cueing, controller }
state.wordsComponents   // array of active words component objects
state.liveMeta          // { status, org, pack, line, server }
```

### Words component API

`state.wordsComponents[0]` (or whichever index) exposes:

```js
comp.stopAuto()              // cancel all pending auto timers
comp.startAuto(fromLine)     // schedule cues from fromLine using cueIn + nudge
comp.nudge(delta)            // adjust nudge offset (clamped ±2s)
comp.sendCue(n)              // send cue for line n
comp.isPlaying               // getter — true if auto timers are running
comp.onPlayStateChange(fn)   // register callback — called when isPlaying changes
comp.updateNudgeDisplay()    // refresh nudge value display element if present
```

`startAuto(n)` schedules all lines after `n` using `cueIn` timecodes
relative to line `n`'s `cueIn`. Any manual line click calls `stopAuto()`
first, which fires `onPlayStateChange(false)`.

### Live meta

```js
setLiveMeta(field, value)
```

Updates `state.liveMeta[field]` and re-renders all `data-cx="meta {field}"`
elements immediately. Called internally by socket connect/disconnect handlers
and by `handleCue`.

### Language linker hook

When `append` modifier is active, `cx-render.js` calls
`languageLinker(text, window.languageLink)` if both are defined, passing
the whole text for phrase-aware processing.

---

## 13. cx-keys.js

Optional — load before `cx-render.js` on controller pages.

| action       | key(s)                    |
|--------------|---------------------------|
| `next`       | Space, ArrowRight, Enter  |
| `prev`       | ArrowLeft                 |
| `blank`      | . (period)                |
| `flag`       | numpad 7                  |
| `too-late`   | numpad 8                  |
| `add-cue`    | numpad 9                  |
| `check`      | numpad -                  |
| `show-title` | numpad 4                  |
| `hide-title` | numpad 1                  |
| `mask`       | numpad 0                  |
| `zero`       | numpad 5                  |

`makeWordsComponent` wires `CXKeys` automatically when `controlling` is present.

```js
CXKeys.onAction(({ action, value }) => { ... });
```

---

## 13b. Video controller

`cx-video-controller.html` uses a video element as the cue clock. The video plays and fires cues automatically as `currentTime` advances through `cueIn` timecodes in the pack. Display pages receive cues as normal — they don't know the source is a video.

### Elements

```html
<div data-cx="meta.video.en controlling controls"></div>
<div data-cx="meta.video.en status"></div>
```

`meta.video.en` resolves the video URL from `pack.meta.video.en`. The `controlling` modifier starts the rAF engine, which reads `video.currentTime` every animation frame and emits cues as `cueIn` values are passed. The `status` modifier renders a status bar and cue log below the video.

### Seeking on incoming cues

When a cue arrives from another controller, the video seeks to the `cueIn` of that line so all controllers stay in sync. Cues emitted by the video's own engine are ignored (echo guard).

### Source filtering

The source part of the triple determines which lines fire cues. `meta.video.en` fires on every line. `st.video.en` fires only on lines with `st` content. `ad.video.en` fires only on lines with `ad` content.

### Page structure

```html
<body data-cx="scrollable">
    <header class="cx-header">
        <span data-cx="meta title"></span>
        <span data-cx="meta composer"></span>
    </header>
    <div class="cx-content">
        <div data-cx="meta.video.en controlling controls"></div>
        <div data-cx="meta.video.en status"></div>
    </div>
</body>
```

HTTPS on port 8008, PM2 name `cx-server`. Config via `services/cx-server/.env`.

### System dependencies

| dependency | install | purpose |
|------------|---------|---------|
| `poppler`  | `brew install poppler` | `pdftoppm` binary used for PDF → PNG conversion |

### npm dependencies (cx-server)

No third-party npm packages required. All imports are Node.js built-ins (`https`, `fs`, `path`, `child_process`, `util`) plus `socket.io`.

### Socket namespace

One namespace per org: `/{slug}`. All pages connect to this namespace regardless of role. Cues emitted by any connected socket are broadcast to all other connected sockets. Non-controlling pages emit a `monitor` event on receipt of each cue so the server can log display-side state.

`CX.connect()` in cx-connect.js connects to `/{slug}` automatically — no role argument needed.

### HTTP endpoints

| endpoint              | method | description                     |
|-----------------------|--------|---------------------------------|
| `/{slug}/list`        | GET    | list all packs                  |
| `/{slug}/load?pack=x` | GET    | load pack JSON                  |
| `/{slug}/save`        | POST   | save pack JSON (versioned)      |
| `/{slug}/presence`    | POST   | presence tracking               |
| `/{slug}/score?pack=x`| POST   | upload PDF; split to PNGs via pdftoppm; return `{ok, pages}` |

Versioned saves: `filename.-1.json`, `.-2.json` etc. (max 10).

### Autoplay

Autoplay is **client-side** — `cx-render.js` schedules cues using
`setTimeout` based on `cueIn` timecodes. `cx-server.js` does not drive
autoplay timing.

---

## 15. CSS Architecture

### Loading order

```html
<link rel="stylesheet" href="/cx/css/cx-base.css">
<link rel="stylesheet" href="/cx/css/cx-theme-dark.css">
```

### cx-base.css

Reset, font variables, `cx-row`/`cx-col` layout primitives,
`[class*="cx-pane-"]` shared structure. Theme-neutral.

`cx-row` / `cx-col` children get `flex:1` by default.
`cx-header`, `cx-status`, `cx-footer` get `flex:none`.
Size minority panels with `style="width:X%"` or `style="height:X%"`.

`body[data-cx~="scrollable"]` overrides the default `height:100vh; overflow:hidden` for pages that need to scroll (e.g. the video controller).

### cx-theme-dark.css / cx-theme-light.css

Colour variables + pane variants + element classes.

**Pane variants** (self-contained — background, colour, padding, gap):
`cx-pane-dark`, `cx-pane-dim`, `cx-pane-light`, `cx-pane-score`, `cx-pane-red`

**Element classes** (shape only — colour inherited from pane):
`cx-speaker`, `cx-prev`, `cx-text`, `cx-next`, `cx-title`, `cx-composer`

**Controller classes:**
`cx-controlling-panel`, `cx-words-toolbar`, `cx-words-list`, `cx-words-now`
`cx-words-gold` (gold highlight variant), `cx-words-large` (bigger text)

### Per-org CSS

`cx-connect.js` calls `loadOrgCSS()` on init, which fetches
`/org/{slug}/css/` and injects any `.css` files found into `<head>`.
This allows per-org style overrides without changing HTML pages.

---

## 16. Page Catalogue

Display pages live in `/org/{slug}/user/`. Control and monitor pages in `/org/{slug}/client/`.

### Display pages

| file                       | description                                      |
|----------------------------|--------------------------------------------------|
| `cx-display-full.html`     | Subtitle, speaker, prev/next, AD, lang menu      |
| `cx-display-simple.html`   | Full-screen centred subtitle                     |
| `cx-display-scroll.html`   | Scrolling transcript, newest at bottom           |
| `cx-display-stage.html`    | Positioned subtitle for projection screens       |
| `cx-display-score.html`    | Score full screen, subtitle overlay              |
| `cx-display-score-split.html` | Score left 60%, subtitles right 40%           |
| `cx-display-ad.html`       | Subtitles + AD audio, splash gate                |
| `cx-display-braille.html`  | Large Lexend, aria-live, braille focus mgmt      |

### Controller pages

| file                          | description                                          |
|-------------------------------|------------------------------------------------------|
| `cx-control-auto.html`        | Auto controller — play/pause, nudge, words list      |
| `cx-control-list.html`        | Manual controller — click to cue, keyboard shortcuts |
| `cx-control-full.html`        | Full controller — toolbar, keyboard, auto            |
| `cx-video-controller.html`    | Video controller — video is the cue clock            |

### Monitor pages

| file                    | description                           |
|-------------------------|---------------------------------------|
| `cx-monitor-operator.html` | Now/next monitor — ST, AD, SD      |
| `cx-monitor-score.html` | Line list left, score image right     |

### SmaTTer pages

| file                  | description                                            |
|-----------------------|--------------------------------------------------------|
| `listen.html`         | SmaTTer STT operator — record, upload, transcribe      |
| `smatter-display.html`| Scrolling bilingual transcript with Māori highlighting |

### All display/controller pages load

```html
<script type="module" src="/cx/lib/cx.js"></script>
```

---

## 17. SmaTTer Integration

### Data flow

```
listen.html (STT operator)
    → cx-connect.js → socket.emit('cue', { text: finalText, lang: 'mi' })
    → server → broadcast to /{slug}
    → cx-render.js → renderText (append mode)
    → languageLinker(text, window.languageLink) — phrase-aware Māori processing
    → smatter-display.html
```

### Key files

| file                  | role                                           |
|-----------------------|------------------------------------------------|
| `listen.html`         | UI — mic, upload, chunk config                 |
| `stt.js`              | Audio capture, whisper API, deduplication      |
| `cx-connect.js`       | Socket connection, `sendMsg(text)` for stt.js  |
| `smatter-display.html`| Audience display, Māori highlighting, splash   |
| `maoriDictionary.js`  | Word/phrase dictionary                         |
| `languageLinker.js`   | Phrase-aware bilingual text processing         |

---

## 18. Migration from v1

| old                          | new                              |
|------------------------------|----------------------------------|
| `client.js` / `client2.js`   | `cx-connect.js` + `cx-render.js` |
| `harness-for-STT.js`         | `cx-connect.js`                  |
| `/files/contexts.live/js/`   | `/cx/lib/`                       |
| `/files/contexts.live/css/`  | `/cx/css/`                       |
| `clid-text`                  | `data-cx="st"`                   |
| `clid-text-offset-+1`        | `data-cx="st offset=1"`          |
| `clid-words`                 | `data-cx="st.words"`             |
| `clid-speaker`               | `data-cx="sp"`                   |
| `clid-score`                 | `data-cx="score.image"`          |
| `clid-splash`                | `data-cx="splash"`               |
| `clid-line-audio`            | `data-cx="ad.audio.en"`          |
| `clid-controlling`           | `… controlling`                  |
| `clid-title`                 | `data-cx="meta title"`           |
| `clid-nudge`                 | nudge lives on `cx-control-auto.html` |
| `#cx-conn-status`            | `data-cx="meta status"`          |
| `org/slug/units/`            | `org/slug/packs/`                |
| `unit=` URL param            | `pack=` URL param                |
| `cx-panel-*`                 | `cx-pane-*`                      |
| server-side autoplay         | client-side `auto` modifier      |
