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) |
Flower link¶
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):
PUBLIC_FLOWER_URLif set- Otherwise by
APP_ENV:develop/test→http://localhost:5555;staging→https://pr-flower-staging.jvsassoc.com - No default for
mainyet — setPUBLIC_FLOWER_URLwhen Flower is deployed
System Documentation link¶
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):
PUBLIC_DOCS_URLif set- Otherwise by
APP_ENV:develop/test→http://localhost:9001;staging→https://pr-docs-staging.jvsassoc.com - No default for
mainyet — setPUBLIC_DOCS_URLwhen 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
DBSessionDepfor 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-catalogmust 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¶
- Create
src/api/admin/routes/<feature>.py. - Include its router in
src/api/admin/router.py. - Add templates under
src/api/admin/templates/<feature>/. - Add feature assets under
src/api/admin/static/css/orstatic/js/, and reference them as{{ admin_static('css/…') }}/{{ admin_static('js/…') }}. - Link the feature from
templates/index.jinja2when operators should see it. - Add HTML smoke and mutation tests under
src/api/tests/admin/. - Update this Admin documentation section and
mkdocs.yml.
Verification¶
Run the complete admin suite:
For JavaScript changes, also check the affected files with Node: