# contexts.live — design decisions & rationale

---

## cx-connect.js + cx-render.js split

`client2.js` was split into two files:

- **`cx-connect.js`** — org connection, config, CXParam, pack cache, send functions, wake lock. Loaded by all pages including tools.
- **`cx-render.js`** — parseCx, renderers, dispatchToElements, words component, language menu, init. Loaded by display/controller pages only.

This means SmaTTer (`listen.html` + `stt.js`) loads only `cx-connect.js` — it gets `sendMsg()` without loading any rendering code. Any future tool does the same.

`cx-render.js` sets `window._cxRenderLoaded = true` so `cx-connect.js` knows not to auto-init a controller socket (render pages manage their own sockets).

---

## cx-connect.js auto-init

When `cx-render.js` is not loaded, `cx-connect.js` auto-inits a controller socket on DOMContentLoaded via `CXConnect.init()`. This means tool pages (SmaTTer etc.) don't need any socket setup code — `sendMsg()` just works.

---

## harness-for-STT.js retired

The old `harness-for-STT.js` was a 40-line file that loaded config, connected a socket, and exposed `sendMsg()`. `cx-connect.js` does all of this and more. `listen.html` now just loads `cx-connect.js` before `stt.js`. No other changes to `stt.js`.

---

## stt.js unchanged

`stt.js` calls `sendMsg(finalText)` at line 453. Since `cx-connect.js` provides `sendMsg` as a global function, `stt.js` required zero changes for the migration.

---

## Inline text cues

Cue messages can carry a `text` field directly: `{ text: 'Interval begins' }`. This bypasses pack content and is used directly by `append` elements. Introduced for SmaTTer STT output — text that doesn't live in a pack.

The `text` field is passed through `handleCue` → `makeCtx` → `renderText`. Only offset=0 elements use inline text; offset elements still resolve from the pack.

---

## languageLinker hook in cx-render.js

When `append` modifier is active, `cx-render.js` calls:

```js
languageLinker(text, window.languageLink)
```

`languageLinker` is the phrase-aware bilingual processor (from `languageLinker.js`). `window.languageLink` is the per-match callback defined by the page (e.g. `maoriMeaning`). If neither is defined, plain text is used.

Key decision: pass the **whole text** to `languageLinker`, not word-by-word. `languageLinker` needs the full sentence for phrase matching and neighbour-context ambiguous word resolution.

`window.languageLink` not `const languageLink` — `const` is block-scoped to the `<script>` tag and not visible to `cx-render.js`.

---

## cx-pane-* naming

Renamed from `cx-panel-*` to `cx-pane-*`. `panel` implies something bolted on. `pane` is a real UI term (VS Code panes), shorter, and unambiguous.

---

## Pane colour system

Each `cx-pane-*` class is self-contained: background, text colour, padding, gap, flex column. Element classes (`cx-text`, `cx-prev` etc.) define shape only — colour inherited from pane. Switching a page from dark to light is a CSS class change on the body or pane, not individual element changes.

HTML contains only structural sizing (`style="width:40%"`). All colours and spacing are in CSS.

---

## cx-row / cx-col children default to flex:1

Children of `cx-row` / `cx-col` get `flex:1` by default. Only the minority panels need explicit sizing. This means for a 60/40 split, only the 60% panel needs `style="width:60%"` — the other fills automatically.

`cx-header`, `cx-status`, `cx-footer` get `flex:none` to prevent growing.

---

## Auto cx-text class

`cx-render.js` automatically adds `cx-text` to any text render element at init. Eliminates a source of forgetting to add the class in HTML.

---

## persist modifier

`persist` prevents blanking on `cueOut`. Implemented in `dispatchToElements` — when `lineNumber === -1` and the element has `persist`, simply return without clearing. Per-element, so non-persist elements on the same page still blank normally.

Used on operator/monitor pages where blank gaps between cues are disorienting.

---

## Audio not stopped on blank

When a blank cue (`lineNumber === -1`) arrives, `dispatchToElements` clears text elements but does NOT stop audio. Audio plays to natural completion. Before this fix, AD audio was being cut off by `cueOut` blanking mid-play.

---

## Autoplay double-fire fix

When `autoplay: true`, server2 starts the timer and returns immediately without broadcasting the cue. If it did both, the client would receive two cue messages for the same line. The timer fires the first cue itself.

Both outer and inner `setTimeout` IDs are tracked in `org.timers`. `stopTimer` cancels both. Before this fix, clicking a line during autoplay left an inner timeout firing after the stop.

---

## Score in main pack

Score images live in the main pack alongside subtitles, not in a separate `_score` pack. Score is just another source field — same pattern as AD audio. Independent timecodes are supported by putting score on its own line.

---

## Page metadata

Every page has `cx-description` and `cx-purpose` meta tags. `org-index.html` reads these to build the local page card grid with descriptions and tooltips. One mechanism for both human readability and machine use.

---

## cx-description card grid in org-index

Local pages are shown as a card grid (not a dropdown optgroup). Each card shows the filename in red, the `cx-description` below it, and the `cx-purpose` as a small tag. Clicking opens the page in a new tab with the current pack appended as `?pack=`.

---

## smatter-display.html generalisation

`smatter-display.html` is designed to be generalised for any language pair by swapping `maoriDictionary.js` and updating `window.languageLink`. Comments at the top of the file explain the five steps. For plain scrolling without language processing, use `scrolling.html` instead.

---

## cueing namespace reserved

The `/cueing` namespace exists in server2 as a simple relay but `cx-render.js` never connects to it. It is reserved for an external timing/cueing tool that will record operator clicks against timecodes. The cueing tool will be a separate JS file (`cx-cue.js`) that loads `cx-connect.js` and `cx-keys.js`.

---

## What is not carried forward from v1

- `clid-source_n` — untested, never in production
- jQuery — not used
- `parameters['audioST']` / `parameters['audioAD']`
- `_score` pack name suffix
- `tsvToJson` / `txtToJson` — build tools only
- `ack` socket events
- `cue-ad` as a separate socket event
- `hash` and `cueTime` in cue messages
- `cx-panel-*` naming — replaced by `cx-pane-*`
- `harness-for-STT.js` — replaced by `cx-connect.js`
- `client.js` / `client2.js` — replaced by `cx-connect.js` + `cx-render.js`

---

## optCharStandalone removed

The "emit character rows as standalone lines" option was a v1 export workaround.
In v2 JSON, speaker names live in the `sp` field on the content row itself —
character rows fold into the following row at export. The option had no meaningful
effect in v2 and was removed in editor v2.3 to avoid confusion.

---

## Editor validation — grid vs panel

Fatal errors that will break runtime behaviour (cueOut ≤ cueIn,
media filename missing extension) show a red left border on the grid row.
Everything else is panel-only. The principle: don't interrupt authoring with
warnings for things that might be intentional. Only mark things that are
unambiguously broken.

---

## Glossary/terms system architecture

Term highlighting and definitions panels are fully decoupled:

- `data-cx="st.text append terms=mi"` — scrolling text with highlighting. `renderText` delegates to `renderTextWithTerms` when `params.terms` is set.
- `data-cx="glossary terms=mi append"` — definitions panel. Receives cues independently, resolves `st.text` content using `state.language`, runs its own lookup.

Key decisions:
- `terms=` is a dictionary name, not a language code. Enables `terms=medical`, `terms=legal` etc.
- Both elements get the cue stream directly — no event passing between them.
- `glossary` source is intercepted early in `dispatchToElements` before `elJson` is declared, using `state.json` directly.
- `glossary` source skipped in auto-class init — it's not a text element.
- Dictionary files: `cx-dict-{name}.js` with named exports `phrases`, `words`, `ambiguous`, `dictionary`.

---

## Non-printable lines and offset elements

Offset elements (`offset=-1`, `offset=1` etc.) walk in the offset direction
skipping lines where `resolveContent` returns empty for the requested source.
This handles `ch`-only lines, act/scene markers, section titles, and any other
structural line that has no printable content for the relevant source.

The walk happens in `dispatchToElements` when computing `targetLine` for offset
elements. `resolveContent(line, source, render, state.language)` is the check —
if it returns nothing, the line is skipped and the walk continues.

This keeps offset display elements consistent with the `words controlling` list,
which also skips non-printable lines.

---

## isBlank flag in dispatchToElements

`handleCue(-1)` (blank cue) was previously detected by checking
`state.lineNumber === -1` inside `dispatchToElements`. But `state.lineNumber`
is never set to -1 — it holds the last real line number so offset elements
continue to resolve correctly after a blank.

Fix: `handleCue` passes `isBlank = (lineNum === -1)` explicitly to
`dispatchToElements`. The blank handler checks `isBlank` not `state.lineNumber`.

`append` and `persist` elements return early when `isBlank` is true — transcripts
and accumulating panels are never cleared by blank cues. This is semantically
correct, not a workaround.

---

## scheduleBlank only on real cues

`scheduleBlank` was called inside `handleCue` for all cues including blank ones
(`lineNum === -1`). When a blank fired `handleCue(-1)`, that called
`scheduleBlank` again, which scheduled another `handleCue(-1)`, creating an
infinite loop.

Fix: `scheduleBlank()` only called when `lineNum !== -1`.

---

## language-menu render type

`data-cx="language-menu"` replaces `class="cx-language-menu"`. `buildLanguageMenu()`
(which scanned for the old class) is removed entirely.

In `dispatchToElements`, `source === 'language-menu'` is intercepted before the
main loop logic and calls `renderLanguageMenu` directly. The `language-menu`
source is also skipped in auto-class init.

The language menu is rebuilt on every `dispatchToElements` call. This is
intentional — languages can theoretically vary across lines. The rebuild
cost is acceptable.

---

## SmaTTer migration

- `listen.html` → `cx-control-stt.html`
- `stt.js` → `cx-stt.js` (ES module, `DOMContentLoaded` wrapper)
- `stt-responsive.css` → `cx-stt.css` (standalone light theme, pending cx variable rewrite)
- `smatter-display.html` retired — superseded by `cx-display-glossary-scroll.html?splash=1`

`cx-stt.js` calls `CXConnect.init()` directly (not via a `controlling` element)
since it's a tool page that doesn't load `cx-render.js`. `CXConnect.init()`
creates the controller socket and sets `window._cxControllerSocket` so
`sendMsg()` works.

The SmaTTer splash animation is available as `?splash=1` on the glossary
scroll page — hidden by default, shown when the param is present. A
`MutationObserver` watches the scroll element and dismisses the splash
when first text arrives.

---

## cx-purpose values

Standardised to four values:
- `display` — audience-facing display pages
- `control` — operator/controller pages  
- `monitor` — monitor/preview pages
- `tool` — utility tools (STT, editor, etc.)

Old value `audience display` is retired.

---

## firstClickDone guard

The `firstClickDone` flag (set by splash tap) was guarding the client socket
cue handler — all cues were dropped until the user tapped. This broke pages
without a splash element (no tap ever happened).

Fix: guard moved to `renderAudio`, `renderVideo`, `renderSpeak` only — the
renderers that actually need user interaction to unblock browser autoplay
restrictions. Text rendering is never gated.


---

## cx-admin.css

A shared stylesheet for all admin, tool and dashboard pages. Extracted from inline `<style>` blocks in `cx-admin-dashboard.html`, `cx-tools-index.html`, `cx-user-admin.html`, `cx-org-index.html`, `cx-engagement.html`, `cx-cues-from-log.html`, `cx-merge.html`, `cx-pack-migrate.html`.

Covers: body scroll override, site header/brand/tagline, `.main` layout, section headings, `.cx-grid`/`.cx-card` card grid, buttons, toolbar, form primitives, empty/loading states, page-index styles (formerly injected by cx-index.js).

All admin/tool pages load: cx-base.css → cx-theme-dark.css → cx-admin.css.

Key decision: `.main` has no max-width — `padding: 0 3rem` for breathing room without a hard cap.

---

## CSS moved out of cx-index.js

`buildPageIndex` previously injected a `<style>` tag with hardcoded hex colours and `cxpi-*` class names. This is now removed — all styles live in cx-admin.css under `cx-*` class names. `buildPageIndex` emits `cx-grid`, `cx-card`, `cx-card-title`, `cx-card-desc`, `cx-pills`, `cx-pill`, `cx-tags`, `cx-tag`, `cx-qr-*` class names throughout.

Rule: JS that generates DOM should never inject CSS.

---

## Unified grid/card class names

All admin/tool pages previously used bespoke grid/card class names (`card-grid`, `org-grid`, `tool-grid`, `media-grid`). Unified to `cx-grid` / `cx-card` / `cx-card-title` / `cx-card-desc` throughout. Single definition in cx-admin.css.

---

## Glossary panel — data-cx selector replacing class

`.cx-gloss-panel` class removed from cx-base.css deprecated aliases. The glossary panel element uses `data-cx="glossary terms=..."` and cx-base.css targets it via `[data-cx~="glossary"]`. No class needed on the element.

The `[data-cx~="glossary"]` rule gives `flex: 1; min-height: 0; overflow-y: auto`. Theme border and background are in cx-theme-dark/light.css.

---

## Pane layout — fill and fixed

Two pane patterns formalised in cx-base.css:

- `data-cx="pane"` — fills remaining space (`flex: 1 !important`)
- `data-cx="pane fixed"` — fixed height, does not grow (`flex: none !important`)

The `fixed` modifier name is available; height is set inline on the element since it's page-specific.

Language menu inside a pane (`[data-cx^="pane"] [data-cx~="language-menu"]`) overrides the default `position: absolute` to `position: static`, making it flow inline within the pane column.

---

## Per-pane language scoping

`renderLanguageMenu` now supports scoped glossary language selection:

- If the menu's containing pane has a sibling `[data-cx~="glossary"][data-cx*="terms="]` element, the menu builds its language list from the dictionary's available languages (not pack subtitle languages).
- On change, sets `state.language` globally AND sets `pane._cxLang` on the pane element.
- `resolvePaneParams` reads `_cxLang` from the pane and folds it into params as `lang`.
- `renderGlossary` uses `ctx.language` (which resolves `params.lang ?? state.language`) for definition lookup.

Result: a glossary pane with its own language menu switches definition language independently of the subtitle language menu. Pages with a single menu work exactly as before.

---

## Dictionary language objects

Dictionary values in `cx-dict-*.js` can be either plain strings or language objects:

```js
// Plain string (Māori dictionary style)
'rīwai': 'potato'

// Language object (Figaro dictionary style)
'Figaro': {
    en: 'Valet to Count Almaviva...',
    mi: '...',
    de: '...',
    fr: '...'
}
```

`renderGlossary` handles both. For objects, it resolves `defObj[ctx.language] || defObj.en || Object.values(defObj)[0]`.

Dictionary key lookup tries three variants in order: normalised lowercase → plain lowercase → original case. This makes capitalised keys (character names etc.) work without requiring authors to lowercase them.

---

## Dictionary files in org/local/

`cx-dict-{name}.js` files live in `org/{slug}/local/`, not `cx/lib/`. Rationale: dictionaries are org IP, may differ per organisation, and should be protected by org access controls. `cx-render.js` loads them via `import(CX.fileBase + 'local/cx-dict-${name}.js')`.

---

## cx-auth renamed to cx-auth/

`services/auth/` renamed to `services/cx-auth/`. Files renamed accordingly: `auth.js` → `cx-auth.js`, `auth-users.json` → `cx-auth-users.json`, `auth.log` → `cx-auth.log`. Consistent with `cx-server/` naming convention.

---

## URL token access for user pages

Org display pages (`/org/{slug}/user/`) can be token-protected by setting `access: "token"` and `token: "..."` in `org/{slug}/config.json`. cx-auth.js reads the config on each auth-check request via `SITE_ROOT` env var. Admin/client sessions always bypass the token check. Open by default — no config change needed for public orgs.

`org/{slug}/local/` was a flat directory of both audience and operator pages. Replaced with two shared folders via symlinks:

- **`cx/user/`** — audience-facing display pages. Open access. Symlinked as `org/{slug}/user/`.
- **`cx/client/`** — operator/control pages. Auth-protected (client or admin role). Symlinked as `org/{slug}/client/`.
- **`cx/tools/`** — unchanged. Admin-only utilities.
- **`org/{slug}/local/`** — retained for per-org custom pages not in the shared cx/ folders.

Key decisions:
- Shared pages live in `cx/` — all orgs get the same pages, no duplication.
- Folder name `client` chosen over `admin` or `operator` — standard industry language that clients will understand.
- `monitor` purpose folded into `control` in the org dashboard — both appear under the Control heading. `monitor` remains a valid `cx-purpose` value.
- `cx-org-index.html` scans `local/`, `user/`, and `client/` in that order. Empty sections are silently omitted.
- QR sheet lives in `cx/client/` — operator tool, not audience-facing.
