# cx-server.js

Socket and HTTP API server for contexts.live.
**v1.1.1** · HTTPS port 8008 · PM2 name `cx-server`

---

## Overview

cx-server handles two things:

- **Socket.io namespaces** — real-time cue relay between controller and display pages
- **HTTP API** — pack file management (list, load, save) and editor presence tracking

It is a separate Node.js process from auth.js, managed by PM2.

---

## Configuration

Config via `services/cx-server/.env`:

```
CX_PORT=8008
CX_BASE=/Volumes/Data/www/Sites/contexts.live
CERTS_PATH=/etc/letsencrypt/live/contexts.live
LOG_LEVEL=normal    # verbose | normal | quiet
```

Log at `services/cx-server/cx-server.log`.

---

## Socket namespaces

One namespace per org, created automatically on startup by scanning `org/`:

```
/{slug}    — all pages; cues broadcast to all connected sockets
```

Cues sent from any page are broadcast to all sockets in the namespace.

### Cue event

```js
// Any page → server → all sockets in namespace
socket.emit('cue', {
    packName:   'show2',
    lineNumber: '4',        // string; '-1' = blank
})

// Inline text cue (no pack)
socket.emit('cue', { text: 'Interval begins' })
```

### Monitor event

Display pages emit a `monitor` event on every cue received:

```js
socket.emit('monitor', {
    lang: 'en',           // display language
    pack: 'show2',        // current pack
    line: 4,              // current line number
    url:  'https://...',  // full page URL
})
```

cx-server writes monitor events to a per-org daily JSONL file:

```
org/{slug}/logs/{slug}-YYYY-MM-DD.jsonl
```

Each line is one JSON object:

```json
{"ts":1748973073000,"slug":"new","lang":"en","pack":"grain","line":82,"url":"https://contexts.live/org/new/user/cx-display-subs.html","ip":"145.40.188.35"}
```

Fields: `ts` (Unix ms), `slug`, `lang`, `pack`, `line`, `url`, `ip`.

The logs directory is created automatically. Files are appended on each monitor event regardless of `LOG_LEVEL`.

---

## HTTP endpoints

All endpoints are prefixed with the org slug.

### `GET /{slug}/list`

Returns all current pack JSON files with metadata.

```json
{
    "ok": true,
    "packs": [
        {
            "file": "show2.json",
            "pack": "show2",
            "title": "Bluebeard's Castle",
            "mtime": 1234567890,
            "editors": [
                { "identity": "192.168.1.1", "since": 1234567800, "lastSeen": 1234567890 }
            ]
        }
    ]
}
```

Versioned files (`.-1.json`, `.-2.json` etc.) are excluded from the listing.
`editors` contains active presence entries — pruned if `lastSeen` is more than 50 seconds ago.

---

### `GET /{slug}/load?pack=name`

Returns the raw JSON content of `org/{slug}/packs/{name}.json`.

Also accepts `?file=name.json` for explicit filename.

---

### `POST /{slug}/save`

Saves pack JSON to `org/{slug}/packs/{name}.json` with versioning.

**Request body** — JSON pack object with optional control fields:

```json
{
    "meta": { ... },
    "lines": [ ... ],
    "_mtime": 1234567890,   // optional — current file mtime at load time
    "_force": false         // optional — skip conflict check
}
```

`_mtime` and `_force` are stripped before writing.

**Conflict detection** — if `_mtime` is supplied and the file on disk is newer, returns:

```json
{ "ok": false, "conflict": true }
```

The editor prompts the user and resends with `_force: true` if confirmed.

**Versioning** — before saving, `shiftVersions()` renames existing files:

```
name.json    → name.-1.json
name.-1.json → name.-2.json
...up to name.-9.json (max 10 versions)
```

---

### `POST /{slug}/presence`

Editor heartbeat — records that a user has a file open.

**Request body:**

```json
{ "file": "show2.json" }
```

Identity is the client IP (`X-Forwarded-For` or `socket.remoteAddress`).

First call logs `PRESENCE OPEN`; subsequent calls within 50 seconds log `PRESENCE PULSE` (verbose only).

Presence data is in-memory only — resets on server restart.

---

## Org discovery

On startup, cx-server scans `org/` for directories containing `config.json`. For each org found, it creates the socket namespaces and logs:

```
/{slug}/client  /{slug}/controller
```

New orgs added after startup are not picked up until cx-server restarts.

---

## Status endpoints

### `GET /status`

Returns live socket counts and last cue for all orgs.

```json
{
    "ok": true,
    "status": {
        "new": {
            "slug": "new",
            "connected": 3,
            "lastCue": { "ts": 1748973073000, "pack": "grain", "line": 82 }
        },
        "demo": {
            "slug": "demo",
            "connected": 0,
            "lastCue": null
        }
    }
}
```

`lastCue` is null if no cue has been received since server startup.

### `GET /{slug}/status`

Returns status for a single org.

```json
{
    "ok": true,
    "slug": "new",
    "connected": 3,
    "lastCue": { "ts": 1748973073000, "pack": "grain", "line": 82 }
}
```

Both endpoints are unauthenticated — they return no sensitive data.

---

## PM2

```bash
pm2 start services/cx-server/cx-server.js --name cx-server
pm2 restart cx-server
pm2 logs cx-server --lines 50
```
