# contexts.live — architecture decisions

## Status

This document records settled decisions and open questions about
file layout, access control, and infrastructure. Updated as decisions
are made.

---

## 1. File layout

### Settled — top-level structure

```
contexts.live/              ← nginx root
    cx/
        lib/                ← JS modules (cx-render.js, cx-connect.js, cx-keys.js etc.)
        css/                ← CSS files (cx-base.css, cx-theme-dark.css etc.)
        user/               ← shared audience-facing display pages
        client/             ← shared operator/control pages (auth-protected)
        tools/              ← editor, cx-export.js, cx-merge.html etc. (auth-protected)
            editor/         ← Subtitle Editor (index.html, editor.js, editor.css)
        fonts/              ← font files
        img/                ← shared images (icons etc.)
        docs/               ← documentation
    org/
        {slug}/
            config.json
            packs/          ← {packId}.json
            logs/           ← per-org daily monitor JSONL ({slug}-YYYY-MM-DD.jsonl)
            media/
                {packId}/
                    {triple}/   ← e.g. ad.audio.en/, score.image/
            user/           ← symlink → cx/user/
            client/         ← symlink → cx/client/
            tools/          ← symlink → cx/tools/
            index.html      ← symlink → cx/tools/cx-org-index.html
    services/
        cx-auth/            ← cx-auth.js oracle, cx-auth-users.json, .env
        cx-server/          ← cx-server.js socket + API server, .env
```

### Rationale

- **`cx/user/` + `cx/client/`** — separates audience and operator pages, enabling different nginx auth rules per folder. Shared via symlinks per org — all orgs get the same pages, customised by pack/config.
- **`cx/tools/`** — browser-based tools (editor, merge, migrator) live alongside shared cx assets. Auth-protected (admin role).
- **`org/{slug}/local/`** — per-org custom pages (org-specific HTML not in the shared cx/ folders). Remains open access. Also used for per-org glossary dictionary files (`cx-dict-*.js`) which are IP-sensitive and not shared across orgs.
- **`cx/lib/`** not `cx/js/` — lib is language-agnostic, the standard convention for library code.

### Access model

| path | access |
|---|---|
| `cx/lib/`, `cx/css/`, `cx/fonts/` | public |
| `cx/user/` (via org symlink) | public or token-protected |
| `cx/client/` (via org symlink) | client or admin role |
| `cx/tools/` | admin role |
| `org/{slug}/local/` | public |
| `org/{slug}/packs/`, `media/` | public |
| `org/{slug}/logs/` | client or admin role (nginx auth) |

---

## 2. Remote delivery

### Settled — CDN target

```
https://cdn.contexts.live/cx/lib/cx-render.js
https://cdn.contexts.live/cx/css/cx-theme-dark.css
```

Cloudflare is the target CDN — globally cached, low latency for ANZ and other regions without local servers.

### Three hosting models

**1. contexts.live hosts everything**
```html
<script src="https://cdn.contexts.live/cx/lib/cx-connect.js"></script>
<script src="https://cdn.contexts.live/cx/lib/cx-render.js"></script>
<!-- packs and media served from org/{slug}/ on contexts.live -->
```

**2. Client hosts media, contexts.live hosts lib/css**
```html
<script src="https://cdn.contexts.live/cx/lib/cx-connect.js"></script>
<script src="https://cdn.contexts.live/cx/lib/cx-render.js"></script>
<!-- mediaBase to be defined — open question -->
```

**3. Client hosts everything (white-label)**
```html
<script src="/cx/lib/cx-connect.js"></script>
<script src="/cx/lib/cx-render.js"></script>
<!-- everything self-hosted, client mirrors cx/ structure -->
```

### Open questions

- **`mediaBase` variable** — how does cx-connect.js know where media lives when the client hosts it?
- **Single bundle** — `cx.js` combining connect + render + keys for simplest possible client integration?
- **Versioning** — `?v=1.2.0` URL param or version in filename for better CDN caching?

---

## 3. Media path

### Settled — canonical path

```
org/{slug}/media/{packId}/{triple}/{filename}
```

Examples:
```
org/new/media/show2/ad.audio.en/0001.mp3
org/new/media/show2/ad.audio/0001.mp3        ← language-neutral
org/new/media/show2/score.image/page-001.svg
```

### Settled — resolution candidates

```
1. org/{slug}/media/{packId}/{source}.{render}.{lang}/{filename}
2. org/{slug}/media/{packId}/{source}.{render}/{filename}
3. /cx/media/{source}.{render}/{filename}    ← shared fallback
```

### Open questions

- **Pack-independent org media** — org logo, splash images not tied to a pack. Suggest `org/{slug}/media/shared/`?
- **`cx/media/` content** — what goes in shared/common media?

---

## 4. Server infrastructure

### Settled

- **Mac Studio M2** — primary server, Southampton UK (`contexts.live`)
- nginx serves `contexts.live` from one root
- Node.js services live one level above nginx root (`Sites/services/`) — intentionally outside web root
- PM2 manages Node processes
- **cx-server.js** — socket + HTTP API, port 8008, PM2 `cx-server`
- **auth.js** — HTTPS oracle + auth server, port 8010, PM2 `cx-auth`
- **ANZ** (`anzcontexts.online`) — pure socket relay only, no file serving

### Monitor logging

Every display page emits a `monitor` socket event on each cue received. cx-server writes these to `org/{slug}/logs/{slug}-YYYY-MM-DD.jsonl` (UTC daily files, appended). The ANZ server writes its own logs independently — logs are not merged across servers. The engagement viewer (`cx-engagement.html`) reads these files directly via the web server.

### Status endpoint

`GET https://contexts.live:8008/status` returns live socket counts and last cue for all orgs. `GET /{slug}/status` for a single org. Used by org dashboard and admin dashboard for live status indicators.

### Open questions

- **ANZ file serving** — when does ANZ get `cx/lib/` etc.? Or does Cloudflare CDN make this unnecessary?
- **Cloudflare** — adopt for `cx/lib/`, `cx/css/`, `cx/media/` now or later?
- **Log compression** — nightly gzip of previous day's logs; nginx `gzip_static` or try-`.gz`-fallback in viewer (already implemented in viewer).

---

## 5. Access control

### Settled — mechanism

nginx `auth_request` oracle pattern. nginx asks auth.js on every protected request; auth.js checks the signed session cookie and returns 200 or 401. No htpasswd files for new paths.

See `cx-auth.md` for full details.

### Settled — four roles

| Role | Access |
|---|---|
| public | Open assets, user pages, org config/packs/media |
| user | Display pages — open by default; URL-token auth deferred |
| client | Client pages, tools (per-org) |
| admin | Everything |

### Open questions

- **ANZ auth** — mirror UK nginx auth config when ANZ gets file serving

---

## 6. Naming

`cx/` prefix is the established namespace. Rename to `cl/` deferred until old `cl-*` system is fully retired — low priority, high disruption.

---

## 7. Pack scoping

### Settled — how elements resolve their pack

Every `data-cx` element resolves its pack by walking up the DOM from itself, then falling back to the URL pack:

1. Element-level `pack=` modifier (e.g. `data-cx="meta title pack=Fig"`)
2. Nearest ancestor with `data-cx="pane ... pack=Fig"` — inherited automatically by all children
3. URL `?pack=` parameter — global default for the page
4. `state.packName` — set from the URL pack on init; never overridden by incoming cues

Packs are loaded lazily on first need. No preloading is required or performed during init. When an element first needs a pack that isn't in `packCache`, it triggers `loadPack` and re-dispatches once loaded.

### Settled — multi-pack pages

Multiple packs can coexist on one page. Each element resolves its own pack independently. Incoming cues carry `packName` — elements only respond to cues whose `packName` matches their resolved pack. `state.packName` / `state.json` are only used by elements with no pack specified at element or pane level.

Canonical example — score controller with separate subtitle and score packs:

```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>
```

Both panes are independent — clicking in either sends cues for its own pack; each pane only responds to cues for its own pack.

### Settled — pack inheritance does not apply to plain HTML elements

`pack=Fig` on a `<header>` or other plain HTML element with no `data-cx` attribute is ignored. Only `data-cx="pane ..."` elements participate in pack inheritance.
