Persist show data (looks/scenes, routing, timelines in project JSON, settings mirrors) on the playout host disk. Does not require Caspar for core save/load/list/get.
Implementation: src/api/routes-data.js · store: src/engine/project-store.js
Advanced sync/bundle: src/api/routes-project.js
| Route | Caspar |
|---|---|
GET /api/project, /list, POST save/load/autosave |
Not required |
GET /api/project/bundle |
Not required |
POST /api/project/reconcile, /sync, /diff, /apply-bundle |
Required |
| Method | Path | Description |
|---|---|---|
| GET | /api/project |
Active merged project (or {} if none) |
| GET | /api/project/list |
Saved slugs + activeSlug |
| POST | /api/project/save |
Save project to disk |
| POST | /api/project/load |
Load project by slug |
| POST | /api/project/autosave |
Autosave with stricter validation |
| GET | /api/project/bundle |
Export bundle (config + state + Caspar XML + media manifest) |
| POST | /api/project/reconcile |
Check live looks vs media/template index |
| POST | /api/project/sync |
Force scene take for all live channels |
| POST | /api/project/diff |
Manifest diff (body) |
| POST | /api/project/apply-bundle |
Apply remote bundle, restart |
Legacy /api/data/* → 410 Gone — use project routes instead.
| Location | Role |
|---|---|
~/highascg/projects/<slug>.json |
Working copy on the playout host |
~/highascg/projects/_autosave/<slug>.json |
Autosave for active slug |
USB stick HIGHASCGEXF → projects/*.json + projects/_autosave/*.json |
Field catalog — listed when stick is mounted; boot pulls stick → working dir |
Bridge disk HIGHASCGDAT → projects/*.json + _autosave/ |
Production sync — bidirectional mtime sync with working dir |
Save behaviour: each save/autosave writes the working copy and _autosave/, then pushes only that slug (main + autosave files) to USB and/or bridge when those volumes are mounted.
Boot behaviour: exfat-sync --boot pulls projects/ including _autosave/ from the stick (and bridge when mounted). On load, readAutosaveFile refreshes from the newest volume copy so reboot restores where the operator left off.
List behaviour: when USB is mounted, GET /api/project/list reads the catalog from the stick (source: "usb"). Without USB, local + bridge entries are merged (newest savedAt wins).
Sync map pairs: usb-projects (direction: to_project, boot pull only) and bridge-projects (direction: both).
When projectScopedMedia.enabled is true (Settings → Media), each active project gets a dedicated folder under the Caspar media root:
| Path | Role |
|---|---|
<media_root>/projects/<slug>/ |
Default upload/import target while project is active |
<media_root>/stock/, other folders |
Shared library — still browsable and usable in looks |
Portable show kit: copy ~/highascg/projects/<slug>.json plus media/projects/<slug>/ to another machine, load the project — clips referenced with project-relative ids resolve under the same folder layout.
GET /api/project/list returns activeProjectMedia: { relId, absPath } and each project row may include mediaFolder: "projects/<slug>".
The project object is the same envelope the operator UI saves:
| Area | Typical keys |
|---|---|
| Metadata | name, slug, savedAt, version |
| Scenes | scenes.scenes[] — looks with layers, transitions |
hardwareConfig |
Injected on save/autosave from live server config (v2) |
| Timelines | May be client-localStorage; sync via /api/timelines separately |
hardwareConfig (server-injected, v2)Written by the server on POST /api/project/save and POST /api/project/autosave; restored on POST /api/project/load into highascg.config.json.
Hardware hold (WO-821). POST /api/project/load with a slug and without applyHardware: true puts that
project on a hold (persisted in .highascg-state.json → project_hardware_hold, survives restarts). While a
project is held, save and autosave write its stored hardwareConfig back unchanged instead of this box’s
live config. The hold ends on an explicit apply: POST /api/project/load with applyHardware: true,
POST /api/project/apply-hardware (active project), or POST /api/project/save with captureHardware: true
(take this box’s rig into the show on purpose). A load without slug (bootstrap) and a plain save keep it.
Import (WO-823). POST /api/project/save with keepPayloadHardware: true stores the payload’s own
hardwareConfig as given (no live stamp, no hold substitution). It is for importing a project file (Godot
“Load from file”): save the file with this flag, then POST /api/project/load with applyHardware: true, so the
file’s screens and destinations are what the box runs. Do not send it on a normal save.
| Key | Contents |
|---|---|
version |
2 |
deviceGraph |
Device View graph |
screenDestinations |
PGM/PRV/multiview bindings |
osDisplay |
GPU xrandr layout (screen_N_os_*, multiview_os_*, screen_count, …) |
gpuPhysicalTopology |
Physical GPU port map (when present) |
casparServer |
Caspar generator settings dict |
audioRouting, streamingChannel, dmx, *Outputs |
Routing extras |
multiviewLayout |
Multiview editor layout |
fingerprint |
{ hostname } — informational for client mismatch UI |
Load does not run apply-os, regenerate casparcg.config, or restart Caspar — client should call those after applying hardware when heads changed.
Implementation: src/engine/project-hardware-config.js
GET /api/projectStartup probe — always 200:
{} (avoids client boot errors)curl -s http://127.0.0.1:4200/api/project | jq '.name, .scenes'
GET /api/project/listReturns the project catalog merged from mounted volumes. Each entry includes a source field indicating which volume supplied the winning copy.
| Field | Type | Description |
|---|---|---|
activeSlug |
string | null | Last loaded slug (persistence) |
projects[] |
array | Catalog entries (newest savedAt per slug) |
projects[].source |
"usb" | "bridge" | "local" |
Origin of the listed entry |
volumes.usb |
object | { mount, mounted } for HIGHASCGEXF |
volumes.bridge |
object | { mount, mounted } for HIGHASCGDAT |
Catalog rules:
projects/ as the primary catalog; merge bridge entries when bridge is also mounted.~/highascg/projects/ and merge bridge when mounted.savedAt → USB wins when the stick is mounted (field catalog).curl -s http://127.0.0.1:4200/api/project/list | jq .
{
"activeSlug": "show-a",
"volumes": {
"usb": { "mount": "/home/casparcg/exfat", "mounted": true },
"bridge": { "mount": "/home/casparcg/bridge", "mounted": false }
},
"projects": [
{
"slug": "show-a",
"name": "Show A",
"savedAt": "2026-06-07T12:00:00.000Z",
"path": "/home/casparcg/exfat/projects/show-a.json",
"source": "usb"
}
]
}
Load side effect: POST /api/project/load (and any read by slug) calls pullProjectSlugFromUsbIfNewer — if the stick copy has a newer mtime, it refreshes the working file under ~/highascg/projects/ before returning JSON.
POST /api/project/save| Field | Type | Description |
|---|---|---|
project |
object | Required full project |
force / allowReplace |
boolean | Bypass stale/replace guards |
broadcastProject |
boolean | default true — WebSocket project_sync |
curl -s -X POST http://127.0.0.1:4200/api/project/save \
-H 'Content-Type: application/json' \
-d '{"project":{"name":"Demo Show","scenes":{"scenes":[]}}}'
200:
{
"ok": true,
"slug": "demo-show",
"activeSlug": "demo-show",
"created": true
}
409 — save rejected (stale tab, unrelated scene set, older savedAt):
{
"error": "Project save rejected: …",
"reason": "stale_saved_at",
"incomingSavedAt": "…",
"storedSavedAt": "…"
}
Reasons: stale_saved_at, unrelated_scene_set, empty_over_nonempty (autosave).
Side effects: persists via persistProject, optional Art-Net reconfigure, debounced project_sync WebSocket broadcast.
POST /api/project/load| Field | Type | Description |
|---|---|---|
slug |
string | Project file slug; omit to load active slug |
applyHardware |
boolean | When true, write hardwareConfig into server config (default false — verify Device View and apply manually) |
curl -s -X POST http://127.0.0.1:4200/api/project/load \
-H 'Content-Type: application/json' \
-d '{"slug":"demo-show"}'
200 — full project object.
404 — { "error": "No project stored" }.
Sets active slug in persistence. Does not apply project hardware to the server unless applyHardware: true.
Without applyHardware: true an explicit slug load holds the project’s stored hardwareConfig (see Hardware hold above).
Autosave merge:
| Request | Merges _autosave/<slug>.json? |
|---|---|
Omit slug (active project / reboot bootstrap) |
Yes — same as GET /api/project (pickNewerFullProject) |
slug set (explicit load of another show) |
No — main projects/<slug>.json only |
POST /api/project/autosaveSame body as save (project required). Stricter validation — rejects empty projects that would wipe stored looks.
409 messages tuned for stale browser tabs. Expected stale_saved_at when scene_deck_sync already wrote a newer copy — client should refresh from GET /api/project or project_sync WS (see from_client/project-persistence-client-agent.md).
Triggers USB/bridge push on success. scene_deck_sync (WebSocket) debounces local disk writes (default 750ms, HIGHASCG_SCENE_DECK_SYNC_DEBOUNCE_MS) and does not push to USB.
GET /api/project/bundleOffline/export snapshot:
{
"config": { },
"state": { },
"casparcgConfig": "<configuration>…</configuration>",
"mediaManifest": [
{ "path": "clip.mov", "size": 12345, "mtime": 1717512345678 }
]
}
POST /api/project/reconcileCompares live scene layers to server media/template catalogs.
200:
{
"ok": true,
"reconciliation": {
"usedMedia": [],
"usedTemplates": [],
"missingMedia": [],
"missingTemplates": [],
"isClean": true
}
}
POST /api/project/syncRuns hard-cut scene take (forceCut: true) for every channel in live scene state (draft → air). Returns per-channel results array.
POST /api/project/apply-bundleApplies bundle from pre-show tooling; may rewrite config, exit process, restart Caspar. Body: bundle payload (see internal sync tools).
200: { "ok": true, "message": "Bundle applied. HighAsCG and CasparCG restarting." }
| Event | When |
|---|---|
project_sync |
After successful save (debounced ~150 ms, HIGHASCG_PROJECT_SYNC_DEBOUNCE_MS) |
| Doc | Topic |
|---|---|
| state-and-media.md | /api/state snapshot includes scene.deck |
| scene-take.md | Resolve sceneId from loaded project |
| timelines.md | Timeline CRUD separate from project file |
| system-settings-hardware.md | exFAT sync pairs (usb-projects, bridge-projects) |
| BRIDGE_DISK_AND_USB_EXFAT.md | Volume layout, boot order, seed scripts |
openapi.yaml — ProjectSaveRequest, ProjectLoadRequest, ProjectListResponse.