Skip to content

Security hardening, CI, and show-ops features - #5

Merged
SALhik merged 10 commits into
mainfrom
feature/show-ops-hardening
Jul 5, 2026
Merged

SALhik merged 10 commits into
mainfrom
feature/show-ops-hardening

Conversation

@SALhik

@SALhik SALhik commented Jul 4, 2026 •

Copy link
Copy Markdown
Owner

Summary

Security hardening, CI, and the five show-ops features (attention signal, show clock, cue alerts, show report, one-command launcher) — now reconciled with the parallel implementation that was merged via #4.

How this relates to #4

#4 was an independent implementation of the same five features by a scheduled agent (different terminology: raise_problem, report_csv/report_html). This branch resolves the collision as a hybrid:

  • This branch's implementation wins for the features (merge commit fb50315, -s ours): attention system with inline caller banner readable/clearable while locked, show clock with server-offset drift correction and configurable segments, WebAudio cue alerts, on-demand show report, launcher. This side was interactively refined and visually verified end-to-end.
  • Grafted from Add operator alerts, problem signals, show report, show timer, and easy setup #4: server/timeutil.py shared timestamp helper, CSV + HTML show-report formats (?format=txt|csv|html) with formula-injection sanitization, run.bat failure guards, null-message coercion in raise_attention, and a suspended-AudioContext guard on cue beeps.

Also in this PR (additive, not in #4)

  • POST /api/_test_reset gated behind CUELIGHT_TEST_MODE=1 (404 otherwise)
  • Join-password auth on mutating HTTP endpoints + showlog/showreport (X-CueLight-Password / ?password=)
  • Reconnect label-uniqueness fix in register_position()
  • GitHub Actions CI (ruff + tests on Python 3.10 and 3.12)

Verification

  • ruff check . clean; 125 tests pass (3 new: report csv/html formats + CSV-injection sanitization)
  • Visual pass over caller/position/observer: attention raise → banner with message → CLEAR while SYSTEM LOCKED → cleared, lock intact; show clock start/tick; report rendered in all three formats with correct numbers

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added operator attention reporting (raise/clear/withdraw) across caller and operator views, replacing prior “problem” signaling.
    • Introduced a show clock experience with start/clear controls and on-screen configuration.
    • Added password-gated saving, resume, and show log/report viewing and downloading (plus test-mode reset gating).
  • Bug Fixes
    • Improved show timing persistence and alignment using unified numeric timestamps.
    • Reworked show report generation with safer CSV output.
  • Documentation
    • Refreshed Quick Start and workflow docs to match the updated attention/clock/report flows and python -m server usage.
  • Tests / Chores
    • Added CI running linting and tests on Python 3.10 and 3.12; expanded hardening/UX coverage.

Security hardening:
- Gate POST /api/_test_reset behind CUELIGHT_TEST_MODE (never live in prod)
- Require the join password on mutating endpoints + show log/report when set
- Enforce label uniqueness on position reconnect, not just first join

CI: GitHub Actions running ruff + pytest on Python 3.10 and 3.12.

Show-ops features:
- Operator attention signal (raise a flag + short message to the caller,
  works while locked; caller banner sits above the lock overlay)
- Opt-in cue alert sounds/vibration on the position console
- Show clock/timer (START SHOW, elapsed + since-last-GO, configurable)
- On-demand show report computed from the log (nothing stored)
- One-command launcher: run.sh / run.bat / python -m server
@coderabbitai

coderabbitai Bot commented Jul 4, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0a7473aa-8ee2-45b6-94d7-ebd14f2bd5f2

📥 Commits

Reviewing files that changed from the base of the PR and between 2311e27 and e9bf428.

📒 Files selected for processing (2)
  • CLAUDE.md
  • static/js/caller.js
✅ Files skipped from review due to trivial changes (1)
  • CLAUDE.md

📝 Walkthrough

Walkthrough

This PR replaces problem signaling with attention signaling, renames show timing to a show clock using epoch fields, adds password-gated HTTP endpoints, rewrites show report generation, updates launcher startup, and refreshes frontend UI, tests, CI, and documentation.

Changes

CueLight attention/clock/password/report rework

Layer / File(s) Summary
Data model and persisted state
server/models.py, server/persistence.py
Position and AppState switch from problem/string timestamp fields to attention and epoch-based clock fields, with persistence load and serialization updated to match.
State manager and WebSocket protocol
server/state.py, server/ws.py
State methods change from problem and timer operations to attention and show clock operations, label uniqueness checks are tightened, and WebSocket handlers now route the new message types and payload fields.
HTTP API and report rendering
server/main.py, server/showreport.py
Password helpers gate the mutating and log/report HTTP endpoints, _test_reset is limited to test mode, and report building/rendering is replaced with the new text, CSV, and HTML pipeline.
Frontend UI and launcher updates
static/caller.html, static/position.html, static/observer.html, static/css/*, static/js/*, run.sh, run.bat, server/__main__.py
The caller, position, and observer UIs add attention and show clock elements; client scripts add password-aware requests, attention handling, and cue alerts; and the launchers now invoke the venv Python directly with optional port selection.
Tests, CI, and documentation
tests/test_cuelight.py, .github/workflows/ci.yml, CLAUDE.md, FEATURES.md, README.md
Tests cover the new auth, attention, clock, and report behavior; CI runs ruff and pytest on Python 3.10/3.12; and the docs are rewritten for the new workflow and endpoints.

Estimated code review effort: 4 (Complex) | ~75 minutes

Possibly related PRs

  • SALhik/CueLight#3: Also changes server/models.py and related state serialization for the same Position schema.
  • SALhik/CueLight#4: Directly overlaps with the attention/show clock/report rewrite across server/models.py, server/state.py, server/ws.py, and server/showreport.py.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title concisely reflects the main themes of the changes: security hardening, CI, and show-ops features.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/show-ops-hardening

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

SALhik added 6 commits July 4, 2026 18:56
Extracts server/timeutil.py verbatim from origin/main (exposes now_iso(),
the canonical local-timezone ISO timestamp with millisecond precision).
Replaces the inline datetime.now().astimezone().isoformat() call in
ShowLog.record() with now_iso() — the only site that duplicated this job.
report_to_csv(): renders this branch's report dict as a 4-column CSV
(section/label/metric/value) with formula-injection sanitisation via
_DANGEROUS/_safe (ported from PR #4's approach, applied to all cells).

report_to_html(): self-contained dark-theme HTML with inline CSS, no
external dependencies, rendered from the same build_report() dict.

GET /api/showreport now accepts ?format=txt|csv|html (existing default
and password gating unchanged); csv is served as text/csv with
Content-Disposition: attachment.

Tests added to TestShowReport: csv format returns text/csv with
attachment header and sane content; html format returns text/html with
DOCTYPE; CSV injection — position label "=SUM(A1)" is sanitised to
"'=SUM(A1)" in the CSV output.
…audio race

run.bat: add 'if errorlevel 1' failure guards after venv creation and
pip install so Windows users see a clear error message instead of a
silent launch failure.

server/ws.py raise_attention: change str(msg.get("message", "")) to
str(msg.get("message") or "") so a JSON null value cannot become the
string "None" in the attention message.

static/js/position.js cueAlert: guard beep() calls with
audioCtx.state === "running" check so audio does not attempt to
schedule into a suspended context before audioCtx.resume() resolves.
CLAUDE.md Show report section: document report_to_csv/report_to_html,
_DANGEROUS/_safe sanitisation, and the ?format=txt|csv|html endpoint
behaviour. Update HTTP API line to reflect the new format options.

FEATURES.md Show report entry: describe the three download formats.
API table row updated to show txt|csv|html.
@SALhik

SALhik commented Jul 4, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 4, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (4)
server/showreport.py (1)

1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add the future annotations import.

This server Python file now starts directly with imports; add from __future__ import annotations before other imports. As per coding guidelines, server/**/*.py: “Use from __future__ import annotations in every Python file.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/showreport.py` at line 1, The Python module is missing the required
future annotations import at the top of the file. Add `from __future__ import
annotations` before any other imports in this module, keeping the rest of the
import order intact, so it matches the `server/**/*.py` convention and the
existing pattern used in files like `showreport.py`.

Source: Coding guidelines

static/js/caller.js (1)

750-774: 🔒 Security & Privacy | 🔵 Trivial | 🏗️ Heavy lift

Password exposed via URL query string for log/report links.

pwQuery() puts the plaintext join password into the URL for window.open navigation on download/view/report buttons. Query-string credentials are commonly logged in browser history, server access logs, and proxy logs, and can leak via Referer headers.

This matches the server's documented support for ?password= (needed since link navigation can't set headers), but it's worth weighing a fetch+blob-download approach for these three buttons to avoid putting the password in the URL at all.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@static/js/caller.js` around lines 750 - 774, The download/view/report button
handlers in caller.js are leaking the join password through pwQuery() by
appending it to the URL passed to window.open. Replace these link navigations
with a flow that keeps the password out of the URL, such as using fetch with
pwAuth() to send the X-CueLight-Password header and then handling the response
as a blob/text for the existing downloadLogBtn, viewLogBtn, and showReportBtn
actions.
run.sh (1)

13-19: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add friendly failure messages to match run.bat's guards.

run.bat explicitly reports "Failed to create the virtual environment"/"Failed to install dependencies" on error; run.sh relies solely on set -e to exit silently with the raw tool output, which is less helpful for non-technical users running this launcher.

🛠️ Proposed fix
 if [ ! -d .venv ]; then
   echo "First run: setting up (this takes a minute)…"
-  python3 -m venv .venv
+  python3 -m venv .venv || { echo "Failed to create the virtual environment."; exit 1; }
 fi

-./.venv/bin/pip install --quiet -r requirements.txt
+./.venv/bin/pip install --quiet -r requirements.txt || { echo "Failed to install dependencies. Check your internet connection and try again."; exit 1; }
 exec ./.venv/bin/python -m server "$@"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@run.sh` around lines 13 - 19, Add explicit failure handling in run.sh to
mirror the user-friendly guards in run.bat: after the virtualenv creation step
and after the pip install step, check for failure and print clear messages like
“Failed to create the virtual environment” and “Failed to install dependencies”
before exiting. Keep the existing launcher flow in run.sh (the venv setup, pip
install, and exec call), but wrap the failing commands so non-technical users
get a helpful message instead of only raw tool output.
static/position.html (1)

26-26: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Icon-only button lacks an accessible label.

#attentionBtn only renders "⚠" with no aria-label/title, unlike lockAttentionBtn which has a text label.

-      <button class="btn-attention" id="attentionBtn">⚠</button>
+      <button class="btn-attention" id="attentionBtn" aria-label="Report a problem">⚠</button>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@static/position.html` at line 26, The icon-only attention button in the
position page lacks an accessible name, unlike the labeled lock attention
control. Update the attentionBtn element in the relevant markup to include an
accessible label such as aria-label and, if appropriate, a title so assistive
technologies can identify the button’s purpose.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 17-21: The CI job’s actions/checkout step is leaving persisted git
credentials enabled even though the workflow is read-only. Update the checkout
configuration in the workflow to set persist-credentials to false on
actions/checkout v4 so the token is not stored in git config; keep the existing
actions/setup-python and matrix.python-version setup unchanged.

In `@server/persistence.py`:
- Around line 65-66: The snapshot restore path in persistence handling does not
preserve legacy show-clock data, so older snapshots with show_start_time end up
resetting state.show_started_at to 0.0. Update the restore logic in the
persistence code to detect the legacy field and migrate it into the current
epoch-based show_started_at value, or explicitly version and invalidate outdated
snapshots; keep the fix localized around the snapshot loading/restoration flow
that sets state.show_started_at.

In `@server/showreport.py`:
- Around line 160-168: The report formatting in showreport’s latency rendering
is appending the seconds unit even when standby/go ack latency values are
missing, producing “—s” in both text and HTML output. Update the formatting
logic in the report builder that handles s['standby_ack_avg_s'],
s['standby_ack_max_s'], s['go_ack_avg_s'], and s['go_ack_max_s'] so the
placeholder stays unitless when the value is None, while still showing “s” for
real numeric latencies.
- Around line 9-18: The CSV sanitization helper _safe currently misses values
starting with LF, so a user-controlled string can bypass the formula-prefix
guard. Update the _DANGEROUS tuple in server/showreport.py to include newline as
an additional trigger, and keep the existing _safe logic in sync so any value
starting with a spreadsheet formula prefix, including LF, is prefixed with a
single quote before export.

In `@server/state.py`:
- Around line 378-388: `raise_attention` currently stringifies the incoming
message directly, which can persist the literal "None" when callers pass a null
value. Update `raise_attention` in `state.py` so the `attention_message`
assignment coerces `None` to an empty string before applying the max-length
trim, and keep the existing `attention_raised` logging behavior unchanged.

In `@static/js/caller.js`:
- Around line 374-385: Replace the inline visibility toggles in the clock modal
click handler by using UI state classes instead of setting `style.display` on
`startShowBtn` and `clearClockBtn`. Update the
`clockStrip.addEventListener("click", ...)` logic in `caller.js` to add/remove
or toggle a shared hidden class based on `state.show_started_at`, and ensure the
matching CSS utility class (such as `hidden`) exists or is reused so the modal
button visibility follows the project’s `classList` convention.

In `@static/js/position.js`:
- Around line 219-224: The UI state toggles in applyDim() and
openAttentionPanel() should use classList instead of rebuilding className or
setting style.display directly. Refactor the dim overlay and attention panel
visibility logic to add/remove/toggle state classes (for example, hidden plus
dim/red variants) on the existing elements, and keep the text updates and
localStorage persistence unchanged.
- Around line 251-289: The first alert cue can be skipped because cueAlert()
checks audioCtx.state immediately after ensureAudio() calls resume()
asynchronously. Update ensureAudio() to return/await the resume promise, and
have cueAlert() wait for audioCtx to reach running before calling beep() for
standby/GO cues. Use the ensureAudio() and cueAlert() helpers in
static/js/position.js to keep the first sound from being dropped after alerts
are enabled.

---

Nitpick comments:
In `@run.sh`:
- Around line 13-19: Add explicit failure handling in run.sh to mirror the
user-friendly guards in run.bat: after the virtualenv creation step and after
the pip install step, check for failure and print clear messages like “Failed to
create the virtual environment” and “Failed to install dependencies” before
exiting. Keep the existing launcher flow in run.sh (the venv setup, pip install,
and exec call), but wrap the failing commands so non-technical users get a
helpful message instead of only raw tool output.

In `@server/showreport.py`:
- Line 1: The Python module is missing the required future annotations import at
the top of the file. Add `from __future__ import annotations` before any other
imports in this module, keeping the rest of the import order intact, so it
matches the `server/**/*.py` convention and the existing pattern used in files
like `showreport.py`.

In `@static/js/caller.js`:
- Around line 750-774: The download/view/report button handlers in caller.js are
leaking the join password through pwQuery() by appending it to the URL passed to
window.open. Replace these link navigations with a flow that keeps the password
out of the URL, such as using fetch with pwAuth() to send the
X-CueLight-Password header and then handling the response as a blob/text for the
existing downloadLogBtn, viewLogBtn, and showReportBtn actions.

In `@static/position.html`:
- Line 26: The icon-only attention button in the position page lacks an
accessible name, unlike the labeled lock attention control. Update the
attentionBtn element in the relevant markup to include an accessible label such
as aria-label and, if appropriate, a title so assistive technologies can
identify the button’s purpose.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 26be9af8-d3dc-493d-9368-b8912ca9cf10

📥 Commits

Reviewing files that changed from the base of the PR and between 2bd3f99 and d934866.

📒 Files selected for processing (24)
  • .github/workflows/ci.yml
  • CLAUDE.md
  • FEATURES.md
  • README.md
  • run.bat
  • run.sh
  • server/__main__.py
  • server/main.py
  • server/models.py
  • server/persistence.py
  • server/showreport.py
  • server/state.py
  • server/ws.py
  • static/caller.html
  • static/css/caller.css
  • static/css/common.css
  • static/css/position.css
  • static/js/caller.js
  • static/js/editor.js
  • static/js/observer.js
  • static/js/position.js
  • static/observer.html
  • static/position.html
  • tests/test_cuelight.py

Comment thread .github/workflows/ci.yml
Comment thread server/persistence.py
Comment thread server/showreport.py
Comment thread server/showreport.py Outdated
Comment thread server/state.py
Comment thread static/js/caller.js
Comment thread static/js/position.js
Comment thread static/js/position.js
SALhik added 3 commits July 5, 2026 17:41
…t refactors

- ci.yml: set persist-credentials: false on checkout (read-only workflow)
- persistence.py: migrate legacy show_start_time ISO snapshots to epoch
  show_started_at so a mid-show upgrade keeps the show clock
- showreport.py: add LF to formula-injection prefixes; render missing ack
  latencies as bare em-dash instead of "—s" (text + HTML)
- state.py: coerce None attention messages to "" in raise_attention
- caller.js/position.js: replace style.display and className rebuilds with
  classList toggles (.hidden utility added to common.css)
- position.js: ensureAudio() returns the resume() promise; cueAlert() beeps
  after it settles so the first alert isn't dropped while suspended
- run.sh: friendly venv/pip failure messages to match run.bat guards
- position.html: aria-label/title on the icon-only attention button
- position.js: the pointerdown listener that resumes the AudioContext was
  { once: true }, so after the first tap nothing could re-resume a context
  suspended by an iOS audio interruption (call, Siri) — cueAlert's resume()
  runs outside a user gesture and can be rejected, silently killing beeps
  for the rest of the show. Restore main's behavior: any tap re-arms audio
  while alerts are on.
- showreport.py: remove unused _PAIRS dict; build_report() does the
  called/acked pairing inline and the constant's comment implied otherwise.
Replace pwQuery() query-param navigation on the three showlog/showreport
buttons with header-authenticated fetches: view buttons open a tab
synchronously (Safari popup blocker) and point it at a blob URL; the CSV
export downloads via a temporary anchor. Keeps the join password out of
uvicorn access logs and browser history. Server-side ?password= support
is unchanged for curl/direct-URL use.
@SALhik
SALhik merged commit 670e035 into main Jul 5, 2026
3 checks passed
@SALhik
SALhik deleted the feature/show-ops-hardening branch July 5, 2026 14:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant