Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# 3.10 is the documented minimum, 3.12 the development default
python-version: ["3.10", "3.12"]
steps:
- uses: actions/checkout@v4
with:
# read-only workflow: don't leave the token in .git/config
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- name: Install dependencies
run: python -m pip install -r requirements.txt ruff pytest
- name: Lint
run: ruff check .
- name: Test
run: python -m pytest tests/test_cuelight.py -v
48 changes: 23 additions & 25 deletions CLAUDE.md

Large diffs are not rendered by default.

196 changes: 83 additions & 113 deletions FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,16 @@ This document walks through every feature and how to use it.

### Starting the server

**The easy way (no terminal knowledge needed):** double-click or run the
bootstrap script in the project folder — `run.sh` on macOS/Linux
(`./run.sh` from a terminal), `run.bat` on Windows. On first run it creates a
private Python environment and installs everything; after that it just starts
the server and prints the addresses to open on each device. Keep the window
open for the whole show.

**The manual way:**

```bash
pip install -r requirements.txt
python -m server
uvicorn server.main:app --host 0.0.0.0 --port 8000
```

`python -m server` starts the server on port 8000 and prints the join URLs
(LAN IP and `cuelight.local`). The equivalent raw command is
`uvicorn server.main:app --host 0.0.0.0 --port 8000`.
No command-line experience needed: `./run.sh` (macOS/Linux) or double-clicking
`run.bat` (Windows) does all of the above — the first run creates a private
virtualenv and installs dependencies, then every run starts the server and
prints the URLs to open. `python -m server [port]` is the equivalent once
dependencies are installed.

The server listens on all interfaces on port 8000. Find the host machine's LAN
IP (Settings shows it, or `/api/info` returns it) and share
Expand Down Expand Up @@ -159,41 +152,28 @@ watches the confirmations tick in live (see *Settings → Health & Latency*).
An incoming standby or GO dismisses the overlay automatically, so a real cue
can never be blocked by it.

### Sound & vibration alerts (opt-in)

An operator watching the stage can miss a silent light change. The **ALERT**
button in the bottom bezel turns on audible/tactile alerts for incoming cues:

- **Standby** — two short high beeps (and a double vibration pulse on
Android).
- **GO** — one long lower beep (and a long vibration).

The patterns are distinct so you can tell them apart without looking. Off by
default, per device, and the choice survives page reloads. Turning it on plays
a small confirmation blip — that first tap also unlocks the phone's audio, so
alerts work from then on. (iPhones can't vibrate from the browser; the beep is
the alert there.)
### Cue alert sounds (optional)

### Raising a PROBLEM
Operators in a dark wing aren't always staring at the phone. The **ALERT**
button in the bottom bezel (off by default, per device, survives reloads)
adds a short double beep on STANDBY and a single lower beep on GO, plus
vibration on devices that support it (Android). The first tap after enabling
arms the audio (browsers require a touch before a page may make sound).

Operators can signal the caller that something is wrong — without leaving
their post or typing in the dark:
### Reporting a problem (attention)

1. Tap **PROBLEM** in the bottom bezel. A panel opens *below* the cue buttons
(they stay visible and tappable — an incoming standby or GO also closes the
panel automatically, so a cue can never be blocked by it).
2. Tap one of the big preset messages — **NOT READY**, **PROP ISSUE**,
**NEED SM** — or type a short message (60 characters max) and tap
**RAISE**. A preset is one tap; typing is optional.
3. The PROBLEM button turns orange while the flag is up. Re-raising replaces
the message; **CLEAR PROBLEM** (in the panel) withdraws it.
The **⚠** button in the bottom bezel lets an operator flag the caller without
leaving their post: tap it and either pick a quick preset (**NOT READY**,
**TECH PROBLEM**, **NEED HELP**) or type a short message and tap **RAISE ⚠**.
The button pulses orange while the report is up. On the Caller, the position's
column shows a pulsing ⚠ ATTENTION badge and the message appears in an orange
banner above the grid.

On the Caller grid the position's column gets a **solid orange ⚠ PROBLEM
badge and outline** — deliberately steady, not flashing, so it never disturbs
the calling rhythm mid-sequence. The caller taps the badge to read the message
and can clear it from there. The flag survives reconnects and even a server
restart, and every raise/clear is recorded in the show log with its message.
Observers see the flag (and message) too.
- Tap ⚠ again to **withdraw** the report yourself.
- When the caller clears it, the operator sees "✓ Seen by caller".
- **Attention works even while the system is LOCKED** — the lock overlay has
its own "⚠ REPORT PROBLEM" button, because a safety report must never be
frozen out by a hold.

### Screen dimming (running mode)

Expand Down Expand Up @@ -298,35 +278,6 @@ and show log. Operators just reload their phones (or wait for auto-reconnect)
and land back in their seats. Starting a *new* show after an EXIT overwrites the
backup at the next EXIT.

### START SHOW and the show timer

At the top of show, press **START SHOW** in the bottom bar (a confirmation
guards against mis-taps). This records the wall-clock start time, notes it in
the show log, and shows every connected operator a brief "SHOW STARTED"
banner that dismisses itself after a few seconds — it floats over the header
strip, never the cue buttons. Pressing START SHOW again simply re-records the
time (with another confirmation and a log entry), for a false start or a
per-act reset. The start time survives a mid-show server restart; EXIT clears
it.

Next to the button, a **compact timer** shows — in quiet grey text that never
competes with cue state — any combination of:

- time since show start (`SHOW 1:23:45`),
- time since the last GO (`GO +02:10`),
- the current clock time.

Pick which in **Settings → Show Timer** (first two on by default; the choice
is per device and persists).

### Problem markers

When an operator raises a **PROBLEM** (see the Position console section), the
column shows a steady orange **⚠ PROBLEM** badge and outline. Tap the badge to
read the operator's message and clear the flag. The marker never flashes, so
it won't be confused with a called standby. If the position also disconnects,
the DISCONNECTED badge takes over but the orange outline remains.

### Settings

The **SETTINGS** button opens a modal with these sections:
Expand All @@ -346,12 +297,10 @@ The **SETTINGS** button opens a modal with these sections:
the editor's Patches tab. See *OSC outbound*.
4. **Network** — the server host/address to share with operators, including the
`cuelight.local` mDNS name when it's active.
5. **Show Timer** — checkboxes choosing what the bottom-bar timer displays:
time since show start, time since last GO, clock time (see *START SHOW and
the show timer*). Per-device, persisted.
6. **Show Log** — download the show's event log as CSV, view the raw JSON, or
generate the **post-show report** (HTML page or CSV — see *Show log &
post-show report*).
5. **Show Log** — download the show's event log as CSV, view the raw JSON, or
open the **show report** (see *Show log*).
6. **Show Clock** — choose what the bottom-bar clock strip displays: time of
day, elapsed since show start, time since last GO (see *Show clock*).
7. **Previous Show** — appears only when an EXIT backup exists; resumes it.
8. **Security** — the password toggle and field (see *Password protection*).

Expand All @@ -373,6 +322,31 @@ When a showfile is loaded and the current cue targets a position that isn't
connected, a warning banner appears on the Caller (e.g.
"Cue 3: Fly not connected"), so you know before firing the cue.

### Operator attention reports

When an operator raises **⚠** on their console (see *Reporting a problem*),
the Caller sees it two ways at once: the position's column header shows a
pulsing **⚠ ATTENTION** badge (and an orange top edge), and an orange banner
above the grid lists each raised report with its message inline — readable at
a glance, no tapping required. Each row has a **CLEAR** button; clearing tells
that operator their report was seen. The banner sits **above the LOCK
overlay**, so the caller can read and clear reports during a hold without
unlocking. Observers see the same badge and banner (read-only). Reports and
their clears are recorded in the show log.

### Show clock

The centre of the bottom bar is a live clock strip. Tap it to open the
**Show Clock** dialog and hit **START SHOW** — the start time is recorded in
the show log, every position briefly sees "⏱ Show clock started", and the
strip begins counting. The clock survives a server restart mid-show; EXIT
clears it. (Tapping the strip again offers *Clear clock…* for a mis-tap.)

What the strip shows is configurable in **Settings → Show Clock** (per
device): time of day, elapsed time since show start, and time since the last
GO — any combination. Times are computed against the server's clock, so an
iPad with a drifting clock still reads true.

### Position colors

Each position is auto-assigned a color from an 8-color palette when it joins
Expand Down Expand Up @@ -747,13 +721,12 @@ is designed for an iPad in landscape (a rotate prompt appears in portrait).

---

## Show log & post-show report
## Show log

The server keeps a timestamped log of everything that happens in the show:
standbys and acks (with the operator and the cue they happened on), GOs, master
actions, cue advances and jumps, OSC fire results, lock/unlock, pauses,
positions joining/leaving, showfile/patch loads, flash-check confirmations,
problems raised/cleared (with their messages), and START SHOW presses.
positions joining/leaving, showfile/patch loads, and flash-check confirmations.
Auto-called standbys are marked `auto` so a report distinguishes them from
manual calls.

Expand All @@ -769,23 +742,17 @@ manual calls.
It doubles as a debugging trail: if a cue was missed, the log shows exactly
when the standby went out and when (or whether) it was acknowledged.

### Post-show report

After (or during) a show, **Settings → Show Log → Show report** turns the log
into a readable report — computed on demand from the log, with nothing extra
stored on the server:
### Show report

- **Standby response times** per position (min / average / max) and per cue —
how quickly each operator acknowledged their standbys.
- **Standbys never acknowledged** — standbys that were still un-acked when the
GO fired (or when the show ended).
- **Time between GOs** — the pace of the show, cue by cue.
- **Auto vs manual standby counts**, **joins/disconnects per position**, and
every **problem raised** with its message and how it was cleared.

The report is a self-contained HTML page (`GET /api/showreport`) you can save
or print, and **Report CSV** (`?format=csv`) downloads the same numbers as a
flat spreadsheet-friendly CSV.
**Settings → Show Log → Show report** opens a post-show summary computed **on
demand from the log — nothing extra is stored**. It includes the show's start
/ end / duration, totals (standbys, GOs, master GOs, cue advances, attention
reports), per-position acknowledge statistics (how many standbys/GOs each
operator received and their average / worst time-to-ack), OSC fire counts,
and the time between consecutive master GOs. Available in three formats: plain text (`?format=txt`) you can save or print,
CSV (`?format=csv`) for spreadsheet import (cells are sanitised against
formula-injection), and HTML (`?format=html`) for a self-contained dark-themed
report. `GET /api/showreport` (no format) returns the data as JSON for tooling.

---

Expand All @@ -800,6 +767,11 @@ Restrict who can join a show.
- **QR convenience:** when join info is shown with a password set, the QR code
embeds the password (`/join?pw=…`) so scanning auto-fills it.
- **Disable:** toggle the password off in Settings; the field is cleared.
- **What it protects:** joining as a Position or Observer, plus the write APIs
once a password is set — saving showfiles/patches from the editor (which
prompts for the password on first save and remembers it for the session),
resuming a previous show, and viewing/downloading the show log. The Caller
sends the password automatically.

Verification happens via `POST /api/check_password`.

Expand Down Expand Up @@ -859,21 +831,24 @@ useful for integration or debugging.
| `POST /api/check_label` | Check a label is free before joining |
| `GET /api/showfiles` | List available showfile names |
| `GET /api/showfile/{filename}` | Fetch a showfile's JSON |
| `POST /api/showfile/{filename}` | Validate and save a showfile |
| `POST /api/showfile/{filename}` | Validate and save a showfile 🔒 |
| `POST /api/csv/import` | Convert CSV text to showfile cues (or row errors) |
| `POST /api/csv/export` | Convert showfile JSON to downloadable CSV |
| `GET /api/patches` | List available OSC patch names |
| `GET /api/patch/{filename}` | Fetch a patch's JSON |
| `POST /api/patch/{filename}` | Validate and save a patch (or probe-test with `_probe_test`) |
| `GET /api/showlog` | Show event log as JSON (`?format=csv` for CSV download) |
| `GET /api/showreport` | Post-show report as a self-contained HTML page (`?format=csv` for CSV) |
| `POST /api/patch/{filename}` | Validate and save a patch (or probe-test with `_probe_test`) 🔒 |
| `GET /api/showlog` | Show event log as JSON (`?format=csv` for CSV download) 🔒 |
| `GET /api/showreport` | Post-show report from the log (`?format=txt\|csv\|html`; default JSON) 🔒 |
| `GET /api/backup_info` | Whether an EXIT backup exists (plus showfile/position count) |
| `POST /api/resume_show` | Restore the EXIT backup into the running server |
| `POST /api/resume_show` | Restore the EXIT backup into the running server 🔒 |
| `GET /api/qr?password=…` | PNG QR code for the join URL |
| `WS /ws/caller` | Caller real-time channel |
| `WS /ws/position` | Position real-time channel |
| `WS /ws/observer` | Observer real-time channel (read-only mirror) |

🔒 = when a join password is set, these require it: `X-CueLight-Password`
header or `?password=` query parameter (wrong or missing → HTTP 401).

---

## Quick reference: typical show flow
Expand All @@ -887,20 +862,15 @@ useful for integration or debugging.
and connect. A director or backup caller can pick **Watch as Observer** on
the join page instead.
6. Before the house opens, run a **flash check** (Settings → Health & Latency →
Flash all positions) and watch every operator confirm. Operators who want
audible cues turn on **ALERT** in their bezel.
7. At the top of show, press **START SHOW** — the clock starts and every
operator sees a brief notice.
8. Run the show:
Flash all positions) and watch every operator confirm.
7. Run the show:
- **Manual:** use each column's STANDBY / GO, or arm positions with PRESET and
drive them together with the MASTER buttons.
- **Showfile:** PRESET arming is automatic (including OSC positions); tap
**GO ARMED** to fire human and OSC positions together and advance each cue;
use PREV / JUMP / PAUSE as needed.
- **OSC:** STANDBY on an OSC column probes the device; GO fires the OSC
message. Results (SENT / NO REPLY) show on the button for 2 seconds.
9. **LOCK** during holds; **RESET ALL** between acts. Operators flag trouble
with **PROBLEM**; the caller reads and clears the orange badges.
10. **EXIT** to end the show. Afterwards, download the show log or generate
the **post-show report** from Settings, or **RESUME SHOW** if the EXIT
was a mistake.
8. **LOCK** during holds; **RESET ALL** between acts.
9. **EXIT** to end the show. Afterwards, download the show log from Settings,
or **RESUME SHOW** if the EXIT was a mistake.
24 changes: 10 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,24 @@ For a full walkthrough of every feature and how to use it, see [FEATURES.md](FEA

Python 3.10 or newer. Python 3.12 is recommended — the project is developed, tested, and built on 3.12.

## Quick start (no terminal knowledge needed)
## Quick start (no setup knowledge needed)

Download the project, then:
Download/clone this folder onto the machine that will run the server, then:

- **macOS / Linux:** run `./run.sh` in the project folder
- **macOS / Linux:** open Terminal in the folder and run `./run.sh`
- **Windows:** double-click `run.bat`

On first run the script sets up a private Python environment and installs
everything; after that it just starts the server and prints the addresses to
open on each device. Keep the window open during the show.
The first run sets everything up (creates a private virtualenv, installs
dependencies — takes a minute); after that it starts instantly. The launcher
prints the two URLs to open: one for the Caller, one for operators to join.
Pass a port to use something other than 8000 (`./run.sh 9000`).

## Install & run manually
## Manual install / run

```bash
pip install -r requirements.txt
python -m server
```

`python -m server` starts the server on port 8000 and prints the join URLs.
The equivalent raw command is:

```bash
python -m server # prints the join URLs, listens on port 8000
# or the explicit equivalent:
uvicorn server.main:app --host 0.0.0.0 --port 8000
```

Expand Down
14 changes: 6 additions & 8 deletions run.bat
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
@echo off
rem One-click CueLight launcher for Windows.
rem Creates a virtualenv on first run, installs dependencies, starts the server.
rem One-command CueLight launcher (Windows). Double-click or run: run.bat [port]
cd /d "%~dp0"

where python >nul 2>nul
if errorlevel 1 (
echo Python 3 is not installed. Get it from https://www.python.org/downloads/
echo Make sure to tick "Add Python to PATH" in the installer.
echo Python 3 is required. Install it from https://www.python.org/downloads/
echo ^(tick "Add python.exe to PATH" during install^)
pause
exit /b 1
)

if not exist ".venv" (
if not exist .venv (
echo First run: setting up, this takes a minute...
python -m venv .venv
if errorlevel 1 (echo Failed to create the virtual environment. & pause & exit /b 1)
)

call .venv\Scripts\activate.bat
pip install -q -r requirements.txt
.venv\Scripts\pip install --quiet -r requirements.txt
if errorlevel 1 (echo Failed to install dependencies. Check your internet connection and try again. & pause & exit /b 1)
python -m server
.venv\Scripts\python -m server %*
pause
Loading
Loading