Skip to content

feat(public): venue/projector mode for public competitions (#77) - #141

Merged
tbcsec merged 1 commit into
mainfrom
feat/venue-mode-77
Aug 2, 2026
Merged

feat(public): venue/projector mode for public competitions (#77)#141
tbcsec merged 1 commit into
mainfrom
feat/venue-mode-77

Conversation

@tbcsec

@tbcsec tbcsec commented Aug 2, 2026

Copy link
Copy Markdown
Owner

What & why

Closes #77. A full-screen, unauthenticated venue/projector mode for putting a live competition on the big screen at an in-person event. It's an in-page toggle on the existing /public/[id] spectator view (?venue=1, bookmarkable and refresh-safe) and implied by the same public_scoreboard opt-in — no new competition column, permission, event, or WebSocket.

The stage auto-rotates three widgets — scoreboard, insights (stats + highlights), points timeline — at 15s each (?interval= override), with a first-blood takeover overlay using the app's house amber-lightning glyph. Operator controls: a fullscreen button, arrow keys to step, space to pause, Esc to exit.

Decisions (confirmed with the owner)

  • In-page toggle, not a separate route — but URL-addressable via ?venue=1 so it's still castable/bookmarkable.
  • Widgets: scoreboard, insights, timeline. The live-solve ticker was cut, but first-blood splashes stay — so the feed below still exists, used solely to drive the splash.
  • Polling, no WebSocket (per the sizing answer) → no ADR needed; reuses the established public-endpoint pattern exactly.
  • Auto-rotate + fullscreen button, keyboard as a bonus. Fixed 15s + ?interval= override.
  • First-blood mark reuses the existing lightning FirstBloodIcon, not a droplet — consistent with the rest of the app.

Backend — one read-only endpoint

  • GET /api/public/competitions/{id}/activity → recent awarded solves, each tagged is_first_blood. Freeze-aware (computed as of freeze_cutoff), so a frozen competition emits no post-cutoff solves — the splash goes silent during a freeze rather than leaking hidden movement. Same opt-in/archived 404 gating as the board; short in-process memo (public_activity_cache_seconds, 5s).
  • First-blood detection refactored into one shared _first_solvers helper so the feed and the insights leaderboard can't disagree.

Frontend

  • ?venue=1 toggle on the spectator page (Suspense-wrapped useSearchParams); usePublicActivity polls only while venue mode is on.
  • components/public/venue/venue-mode.tsx — the overlay: rotation, first-blood splash queue (primed on first load so pre-existing bloods don't storm), fullscreen + keyboard controls.
  • Extracted StatTiles/Highlights (shared with the static page, no fork) and the FirstBloodIcon (shared with the challenges page).
  • Pure helpers in lib/venue.ts (parseRotateSeconds, pickNewFirstBloods) — unit-tested.

Verification

  • Full gauntlet green: backend pytest 641, frontend tsc / eslint / vitest (197, incl. 17 new) / next build.
  • Freeze-parity test proven load-bearing (neutered the cutoff → the security test fails).
  • Two adversarial reviews (correctness + conventions) — no bugs; one letter-spacing token nit found and fixed.
  • Live-verified against a demo-seeded instance: entered venue mode, rotated through the scoreboard and timeline slides, confirmed the freeze-aware /activity feed and zero console errors.

Checklist

  • Follows the architectural rules in CONTRIBUTING.md (Pydantic schemas separate from models, one router/hook per domain, design tokens, read-only public route needs no event/permission, a11y role=tablist/aria-pressed/role=alert)
  • N/A — no schema/migration change
  • This is not a security fix (but the new endpoint is unauthenticated public surface, reviewed as such: freeze-aware + opt-in gated)

🤖 Generated with Claude Code

A full-screen, unauthenticated big-screen display for in-person events,
toggled in-page from the existing /public/[id] spectator view via ?venue=1
(bookmarkable, refresh-safe) and implied by the same public_scoreboard opt-in
— no new competition column.

It auto-rotates three widgets (scoreboard, insights, points timeline) at a
15s cadence (?interval= override), with a first-blood takeover overlay using
the app's house lightning glyph. Operator controls: fullscreen button, arrow
keys, space to pause, Esc to exit.

Backend adds one read-only public endpoint — GET /activity — returning recent
awarded solves tagged is_first_blood, freeze-aware and under the same opt-in/
archived gating as the board, so a frozen competition emits no splash. The
first-blood definition is refactored into one shared helper so the feed and
the insights leaderboard can't disagree. Polling only (no WebSocket), so no
ADR needed. StatTiles/Highlights extracted to a shared component and the
first-blood icon shared so the static and venue views don't fork.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@tbcsec
tbcsec merged commit b47f9ee into main Aug 2, 2026
4 checks passed
@tbcsec
tbcsec deleted the feat/venue-mode-77 branch August 2, 2026 18:29
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.

[Feature]: venue/projector mode

1 participant