Skip to content

Live (Play)

Public Live page for answering open questions. Open questions are delivered over WebSocket; answers stay on REST.

URL /play (also linked from admin nav; opens in a new tab)
Stream WS /api/play/stream?api_key=
REST POST /api/auth/login · GET/PATCH /api/users/me · POST/DELETE /api/matches/{id}/follow · PUT /api/question-answers · GET /api/question-answers · GET /api/matches (today’s slate for Following, optional followed=true) · GET /api/users/me/points · GET /api/question-answers/open (catch-up only)

Mobile / client authors: Live play client.

Workflow

  1. Log in with username or email + password (accounts via admin Users or POST /api/auth/register). The API key is stored in localStorage. Profile load reads match_daily_follow_count and syncs browser IANA match_follow_timezone when it differs.
  2. The page opens WS /api/play/stream, applies snapshot / question.opened / question.closed, and submits answers with Bearer PUT /api/question-answers. Open questions are the primary UI: countdown and prompt first, then a situation / clock / period line when the issuing event had that context, then large choice buttons; a Pivotal badge when the definition was marked is_pivotal_moment; a Match question badge for pre-game Match questions; sponsors and issuance IDs sit below. With multiple open questions, unanswered questions stay on top; answering one sinks that question below the others (still changeable until the window closes, or until first pitch for Match questions). When a Match question locks (answer_locked=true), /play removes it from the open column and refreshes Recent answers. Locked pending Match answers stay at the top of Recent answers and do not consume the usual 10-row recent limit (Pending until the match is scored). Live plays a short browser tone on question.opened and a different tone on question.closed with reason=resolved (not on reconnect snapshot, lock republish, retracts, or stale closes). Sounds are off by default; Turn sound on/off in the header toggles this and persists in localStorage.
  3. Following: the bar lists each followed match as a label with (questions/pivotal) issuance totals from the match list (e.g. NYY @ CHC (7/3) = 7 non-pivotal questions issued, 3 pivotal moments — mutually exclusive; plus remaining daily slots when restricted; hover shows the match shortcode after a short delay). Counts refresh with the followable slate poll and bump on live question.opened. The Choose-matches modal lists today’s matches only (calendar day in the user’s match_follow_timezone; all statuses), grouped by league, with the same (questions/pivotal) counts and shortcode in each row. Follows for other days stay on the server but are hidden from the bar/modal until that day’s slate.
  4. Restricted (non-null daily limit): must choose matches via POST/DELETE …/follow; empty selection shows no questions. Unfollow before start refunds today's slot; unfollow after start (non-test) does not. Test matches never count and always refund.
  5. Unrestricted (null limit): must still follow matches to see live questions (empty selection shows none); no daily quota. Each league header has All / None for bulk select. On login the local list is unioned with server follows (so clone auto-follow appears), then pruned to today’s slate. Toggle / clear / league bulk also sync POST/DELETE …/follow. Clone-and-listen also appends the new id to localStorage immediately.
  6. Closed matches cannot be newly followed (checkbox disabled; All skips them). Matches already followed before close stay selected and may be unfollowed (no quota refund).
  7. Following a non-test in progress match asks for confirmation: unfollowing later will not refund the daily follow slot (same as server rules). Unfollow after start asks again before dropping the follow.
  8. Points and recent answers refresh on a slower REST cadence (and after closes). If the socket drops, the page reconnects and falls back to a slow GET /open poll until the stream is healthy again.

Layout: mobile-first — compact header/Following, questions in a single column, Recent answers collapsed by default under ~900px (expandable). From ~900px up, questions take a wider column beside a sticky Recent answers panel.

Recent answers cards (shared admin_answer_history_list) show match label, shortcode, a short issuance id (first 8 of question_issuance_id) with a copy button for the full UUID, and answered-at. Expand for the full issuance id (also copyable), definition id, sponsors, and event/condition details.

Report this: on a resolved recent answer, a logged-in player can file user feedback. Locked pending Match answers stay unsubmittable. The modal asks for a Topic (from GET /api/user-feedback/form for that row’s kind) and an optional description (max 250 characters). One submission per answer. After a successful POST (or a 409 already submitted), the control becomes a quiet Feedback sent. History offers the same control for operators reviewing a player. Mobile contract: Report this. Operators review submissions on User Feedback.

Notes

  • /play is outside /admin (no admin session required).
  • If the stored Bearer key returns Invalid or revoked API key, Play clears the local session (same as Change) and shows the login form again.
  • Admin operators sign in at /admin/login; Manage → Users creates accounts.
  • Ops (cache TTL, WS connection caps, Caddy/ALB path routing): Environment configuration.