Skip to content

Admin web application

The FastAPI service includes a server-rendered operations UI under /admin. It provides database configuration tools and development diagnostics without a separate frontend deployment.

Pages

Page URL Purpose
Admin home /admin/ Navigation to admin tools
Matches /admin/matches Browse matches by local day/range; edit question overrides and listener dispatch (auto_dispatch_listener)
Live /play Public Live page (outside admin login); answer live questions as a player; must follow matches to see questions; last 10 answers. Admin nav opens it in a new tab.
History /admin/history Search a player; audit answers/points grouped by match
Clone Past Match /admin/test-match-listeners Clone a CLOSED match and start a live srsim listener (non-main)
Users /admin/users Create/manage player and admin accounts
User Feedback /admin/user-feedback Read-only inbox of player reports on resolved answers; filter by submitted day, user, or match
Brands /admin/brands Manage brand name, hex colors, and S3 logo
Brand Sponsorships /admin/brand-sponsorships Assign brand question sponsorships / match sponsor across matches
Question Definitions /admin/question-definitions Create and maintain in-game question definitions
Question Groups /admin/question-groups Configure in-game issuance lanes (interval, period caps, max per match)
Match Questions /admin/match-question-definitions Create and maintain pre-game Match question definitions
Question Inheritance /admin/question-inheritance Read-only: pick a definition and see how live league / group / definition settings combine
Issuance Explorer /admin/issuance-explorer Read-only: pick a match, optional on-demand timeline, filter published / blocked / period, inspect selection, At play, snapshot, and resolution
League Defaults /admin/league-defaults Edit per-league question defaults (caps, interval, points, answer window)
Push Notifications /admin/push-notifications Push notification gates, constraints, message templates, per-user delivery lookup (Advanced)
Match Listeners /admin/match-listeners Browse lifecycle rows; Start/Stop listeners; logs (under Advanced) — lifecycle cases
Flower env-specific (see below) Celery monitoring dashboard (under Advanced)
System Documentation env-specific (see below) Narrative guides site for this environment (under Advanced)
API Documentation /docs OpenAPI / Swagger UI for /api (under Advanced); narrative guides in MkDocs API
Plumbing test /admin/test/plumbing/view Exercise Celery and PostgreSQL from an HTML page (under Advanced)
Plumbing JSON /admin/test/plumbing Machine-readable plumbing result
Metrics /metrics Prometheus metrics scrape endpoint (under Advanced)

Admin nav (under Advanced) and the home page include a Flower link when a URL is known for the current environment. Resolution (in api/admin/flower.py):

  1. PUBLIC_FLOWER_URL if set
  2. Otherwise by APP_ENV: develop/testhttp://localhost:5555; staginghttps://pr-flower-staging.jvsassoc.com
  3. No default for main yet — set PUBLIC_FLOWER_URL when Flower is deployed

Admin nav (under Advanced) and the home page include a System Documentation link when a URL is known for the current environment. Resolution (in api/admin/mkdocs.py):

  1. PUBLIC_DOCS_URL if set
  2. Otherwise by APP_ENV: develop/testhttp://localhost:9001; staginghttps://pr-docs-staging.jvsassoc.com
  3. No default for main yet — set PUBLIC_DOCS_URL when MkDocs is deployed

See Question definition editor, Question groups, Match question definition editor, Users, Brands, Brand Sponsorships, Question Inheritance, Issuance Explorer, User Feedback, League Defaults, Matches, Live, History, Match Listeners, and Clone Past Match for operator workflows.

Security boundary

HTML routes under /admin require an admin login session (POST /admin/login against an active User with is_admin; cookie parlay_admin_session, 7-day max-age). Seed with scripts/db/add_admin_user.py. The public Live page at /play is separate and omits the admin nav; players log in in-page. Static files at /admin/static are not gated. JSON under /api/... uses Bearer API keys; mutating routes require admin unless player-allowed.

Frontend architecture

Admin code is under src/api/admin/:

Concern Location
Router aggregation src/api/admin/router.py
Feature routes src/api/admin/routes/
Shared Jinja environment src/api/admin/jinja.py
Templates src/api/admin/templates/
CSS and JavaScript src/api/admin/static/
Route tests src/api/tests/admin/

src/api/app.py mounts static files at /admin/static and includes the admin router with the /admin prefix.

UI stack

  • Jinja2 for page shells and server-rendered content
  • Tailwind CSS loaded by base.jinja2
  • Flowbite for optional component chrome
  • Alpine.js for reactive state on interactive workspaces
  • CodeMirror for raw JSON editing
  • Plain JavaScript modules for domain-specific state and serialization

There is no Node build, React, Vue, jQuery, or shadcn dependency.

Local CSS/JS under src/api/admin/static/ must be loaded with the Jinja helper admin_static('css/…') / admin_static('js/…') (defined in jinja.py). That appends a short content-hash ?v= query so browsers pick up deploys and local edits. Do not hand-edit ?v= query strings.

Feature templates extend base.jinja2. Alpine and Flowbite are loaded once by the base template; feature pages must not load duplicate copies.

Shared UI helpers

Cross-feature pieces live in static/js/base.js and templates/_macros.jinja2. Prefer these over copying helpers into a feature Alpine app:

Helper Role
AdminToast / AdminClipboard Toasts and clipboard
AdminHttp Bearer bootstrap from POST /admin/api-session (sessionStorage cache; 401 clears + refreshes); parseError
AdminDate Browser TZ calendar day / UTC conversion
AdminDayRange Day-range presets (Today / Yesterday / Last 5 / Custom)
AdminMatchStatus Match.status Title Case labels + color badges
AdminMatches Match list fetch (list / loadClosed; omit league for all leagues), option labels, bindPicker
AdminPlayerSession Live player login + localStorage; admin fetchPointsForUserId for History
AdminPlayerAnswers Shared answer-history list helpers (listForUser / listForUserId, points/time labels, field-map helpers, shortUuid / copyId, bind)
admin_page_header Title + blurb
admin_league_options / admin_league_field League options / labeled select
admin_day / admin_day_range Single day or preset day range (browser local)
admin_match_search Free-text filter over loaded matches / listener groups
admin_match_select / admin_match_picker Match <select> / league+day+select pick-one
player_session.css Shared Live / History gate chrome + answer-history cards
admin_answer_history_list Shared answer row markup (Live recent + History)

Match selection predictability: league controls and browser-local day semantics are shared; pick-one tools use admin_match_picker + AdminMatches.bindPicker against GET /api/matches; browse tools may add day range / match-id text but reuse the same field chrome (admin_day_range supports per-page presets, e.g. Matches includes Tomorrow). Option labels always use AdminMatches.optionLabel (display_label · scheduled · TEST · shortcode; TEST only on is_test rows). History (answer-derived filter) stays outside the pick-one picker.

Context processor (api.admin.jinja) supplies default_league, flower_url, mkdocs_url, and test_match_listeners_enabled.

Database and request patterns

  • Use DBSessionDep for route database access.
  • SQLModel ORM queries use db.exec(select(...)).
  • Raw SQL uses db.execute(text(...)).
  • Mutations use Pydantic request models and explicit response models where practical.
  • Static routes such as /meta/sample-catalog must be registered before dynamic /{definition_id} routes.

Question-definition changes and today's-match overrides write directly to PostgreSQL. Pages label this as Database configuration.

Adding an admin feature

  1. Create src/api/admin/routes/<feature>.py.
  2. Include its router in src/api/admin/router.py.
  3. Add templates under src/api/admin/templates/<feature>/.
  4. Add feature assets under src/api/admin/static/css/ or static/js/, and reference them as {{ admin_static('css/…') }} / {{ admin_static('js/…') }}.
  5. Link the feature from templates/index.jinja2 when operators should see it.
  6. Add HTML smoke and mutation tests under src/api/tests/admin/.
  7. Update this Admin documentation section and mkdocs.yml.

Verification

Run the complete admin suite:

uv run pytest src/api/tests/admin/
uv run ruff check src/api/admin src/api/tests/admin

For JavaScript changes, also check the affected files with Node:

node --check src/api/admin/static/js/<file>.js