Skip to content

Question groups

The page at /admin/question-groups manages league-scoped in-game issuance lanes. Each question definition belongs to one group. Groups own the open-question mutex, a live min-interval floor after resolution, waits after other groups issue, optional period caps, and an optional per-match ceiling. Subject uniqueness (one non-pivotal in-game question per player per period) is not a group rule — it is match-wide. See Question definitions.

Match questions do not use question groups.

To see how a group’s interval and period map stack with league / match / definition layers, use Question Inheritance. To reconstruct why a published in-game question issued, use Issuance Explorer.

Data source

The admin page is a thin HTML shell. Alpine.js loads groups via GET /api/question-groups?league= and saves with POST /api/question-groups?league= or PATCH /api/question-groups/{id}. Deletes use DELETE /api/question-groups/{id} (409 while definitions still reference the group).

This is database configuration.

Fields

Field Notes
Name Operator label (editable). Shown in the definition dropdown.
Slug Immutable per-league key (atbat, half_inning, default). Copied onto definitions and issuance snapshots.
Description Optional operator notes.
Issuance min interval Live lane floor (seconds) after this group resolves. Combined via max(match floor, this value, last definition snapshot). Default 0.
Wait after other groups issue Per other group, seconds to wait after that group issues before this group may issue. Form rows (name + seconds), not JSON. Blank / 0 = no wait. Retracted issuances do not start the clock. Same-group pacing stays on Issuance min interval.
Period distribution JSON ints only (not [normal, pivotal]). {} = no group period caps; [] / {dist: even} = 1 per registered league key when Max per match is blank, else even-split that integer (leftovers pinned in registered order; live eval, not shuffled).
Max per match Optional group-wide ceiling. Retracted issuances are excluded. Even period maps also split this integer across registered keys.
Active Inactive groups cannot receive new issuances.

Each field label has a ? help modal.

Workflow

  1. Open Manage → Question Groups. Deep-links: /admin/question-groups?league=mlb, /admin/question-groups?league=mlb&id=<group_uuid>, and slug= as a fallback.
  2. Pick a League. The table lists name, slug, min interval, after-issue summary, period summary, max/match, active, and definition count.
  3. New group or Edit opens a modal. Wait after other groups issue is one number input per other group in the league. Period distribution is a JSON textarea (same chrome as league defaults). Slug is locked after create.
  4. Member definitions link to /admin/question-definitions?league=<slug>&id=<qd_id>.
  5. From the question definition editor, pick a group from the dropdown (no free-text slug). Manage groups opens this page for the current league. Sidebar g=atbat chips still deep-link a specific group.
  6. Export to CSV downloads every group across all leagues. Columns are the live question_groups table fields plus a league slug; JSON columns are written as JSON strings. The same file is GET /api/question-groups/export.

Do not invent a new slug on a definition. Create the group here first.

Period distribution

Values are ints (issuances of this group in that period):

Stored shape Meaning
{} No group period caps (match-wide map still applies).
[] or {"dist": "even"} Max per match blank: 1 per registered league key. Max set: even-split that integer (leftovers pinned in registered order). Evaluated live. Table summary is even (1/key) vs even (split N).
{"dist": "even", "9:B": 2} Even baseline with int overrides.
{"1:T": 1, "5:T": 2} Explicit. Omitted keys are uncapped (unlisted: allow).
{"unlisted": "block", "1:T": 2} Only that key; omitted periods blocked.
{"1": 0} Never issue this group while the event is in that period.

AND with the match-wide period map and each definition’s issuance_period_distribution. Pivotal skip of the match map does not skip this group map.

Suggested MLB seed after backfill: set atbat.period_distribution to [] or {"dist": "even"} and leave Max per match blank for one at-bat / multi-choice per regulation half. A max of 30 with even is not 1/key — it splits 30 across those halves.