Skip to content

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 ?.