Metabase¶
Staging analytics at data.jvsassoc.com. Metabase
connects to parlay_staging as Postgres role metabase (SELECT only).
Agents that create or edit cards should follow the project metabase
skill (.cursor/skills/metabase/) and treat this page as the catalog of
dashboards we maintain. Live SQL and filter wiring can drift — confirm with
MCP read_resource before editing.
Instance¶
| Item | Value |
|---|---|
| Site | https://data.jvsassoc.com |
| Database | parlay_staging — use Metabase database id 3 (not id 2) |
| Postgres role | metabase |
| Grants | Forward Alembic GRANT SELECT when the role exists (no-op locally) |
There are two identically named parlay_staging database connections.
Always use id 3 (newer sync).
Match timeline¶
One event-level timeline (time on X, kind lanes on Y, thin period
verticals) plus a match picker, lane key, and issuance table. Stream
events, correctives, and issue/resolve markers share the same axis.
Clock times are America/Toronto (timestamptz AT TIME ZONE
'America/Toronto'). Metabase has no per-dashboard browser timezone.
Issuance Explorer does not embed this dashboard. Event timeline
(Show timeline) loads match_timeline_points plus blocked attempts
via GET /api/matches/{id}/event-timeline and charts them in the
browser (ECharts; named Y lanes, legend, vertical period lines, browser
TZ). Period lines fall back to issuance / blocked period_label when
stream ingest has not run.
| Item | Value |
|---|---|
| Collection | Match timelines (id 5) |
| Dashboard | Match timeline (id 2) — /dashboard/2 |
| Flag | MATCH_EVENT_METRICS_ENABLED (default off; staging on) |
| Facts table | match_event_metrics |
| Chart view | match_timeline_points |
Recordings and S3 stay the payload store. match_event_metrics holds type,
time, period/phase, extracted situation and clock strings, and flags only —
no event JSON.
Ingest¶
When the flag is on, the listener best-effort flushes slim rows (skips
heartbeats and is_test matches). Catch-up is Celery
ingest_match_event_metrics / sweep_pending_match_event_metrics; prune is
14 days. See Celery,
environment, and
listener system.
Flag off: writes no-op; Metabase UI still loads. Cards error if the table/view is missing. Issuance overlays can still show if the view exists (issuance tables do not depend on the flag).
Staging yesterday backfill (shift the UTC day if needed):
make enqueue TASK=sweep_pending_match_event_metrics \
KW='scheduled_from="2026-08-31T00:00:00Z" scheduled_to="2026-09-01T00:00:00Z"' \
ENV=staging
SELECT for role metabase is revision b8e4c1a90d27 (no-op where that
role does not exist).
Schema¶
Table fields: src/lib/db/models/match_event_metric.py.
View DDL: src/lib/match_event_metrics/timeline_view.py (tests
create_all this view). Applied migrate:
alembic/versions/2a1cb81545ff_match_event_metrics.py.
match_event_metrics: match_id (internal match id), occurred_at,
event_ts, event_type, period_label (from
extract_period_from_event, not an srsim anchor), situation /
clock (from extract_situation_from_event /
extract_clock_from_event), event_eid, is_synthetic,
is_corrective.
match_timeline_points kinds / series:
| kind | series |
|---|---|
stream_event |
event_type |
stream_corrective |
corrective:{event_type} |
in_game_issued / in_game_resolved |
same as kind |
match_question_issued / match_question_resolved |
same as kind |
Also: occurred_at, event_type, period_label, is_corrective,
source_id.
Layout¶
| Order | Id | Name | Role |
|---|---|---|---|
| 1 | 46 | Match picker | Table of ingested non-test matches (matches.id as match_id, scheduled, league, home/away, event count). No {{match_id}} filter. scheduled is America/Toronto. |
| 2 | 47 | Match timeline | Line chart of individual match_timeline_points rows (markers only) plus period verticals. Optional {{match_id}} (internal match id); empty uses the latest ingested non-test match. occurred_at is America/Toronto. |
| 3 | 48 | Lane key | Table of lane number → name (6 at top). Do not use lane_label as the chart Y metric. |
| 4 | 45 | Issuance table | In-game + Match questions (prompt, issued, resolved, reason, issued_in_period). Same optional {{match_id}}. Issued/resolved are America/Toronto. Native JSON ? is written ?? (JDBC). |
Click match_id on the picker opens question 47 with that id (card
click_behavior → question 47 / {{match_id}}).
{{match_id}} is wrapped in [[ ]] so Metabase does not treat it as
required (avoids missing required parameters: #{"match_id"} on the
dashboard). When the tag is empty, SQL COALESCEs to the latest ingested
non-test match.
MCP update_dashboard cannot declare dashboard parameters. To keep the
graph on the dashboard instead of navigating away: Edit dashboard → add
a Text/ID filter Match ID (string/=, slug match_id, sectionId: id)
→ map it to cards 47 and 45 only (not the picker). Then optionally change
the picker click to update that filter (crossfilter).
Timeline lanes (question 47)¶
Metabase cartesian charts require a numeric Y. lane is 1–6;
lane_label is the text name (tooltip). The Y-axis title is empty —
read Lane key (question 48) instead of the rotated title string.
Do not set lane_label as the Y metric — that prompts
“which fields do you want to use for the X and Y axis”.
| lane | lane_label | kind |
|---|---|---|
| 1 | stream event | stream_event |
| 2 | corrective | stream_corrective |
| 3 | in-game issued | in_game_issued |
| 4 | in-game resolved | in_game_resolved |
| 5 | Match question issued | match_question_issued |
| 6 | Match question resolved | match_question_resolved |
period_change is not a view kind. Display is line (scatter can
only draw dots). At the first non-corrective period_label, the card
draws six points at lane 1–6, each 1ms later, and connects them — a
near-vertical stroke with real pixel height. Event series use markers
plus NULL neighbors so they stay dots. X is occurred_at; Y is lane;
breakout is series. If the field picker appears, set those three.
Native Metabase SQL that uses Postgres JSON ? must write ?? (JDBC).
The view DDL keeps a single ?.