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¶
- Open Manage → Question Groups. Deep-links:
/admin/question-groups?league=mlb,/admin/question-groups?league=mlb&id=<group_uuid>, andslug=as a fallback. - Pick a League. The table lists name, slug, min interval, after-issue summary, period summary, max/match, active, and definition count.
- 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.
- Member definitions link to
/admin/question-definitions?league=<slug>&id=<qd_id>. - 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=atbatchips still deep-link a specific group. - Export to CSV downloads every group across all leagues. Columns
are the live
question_groupstable fields plus aleagueslug; JSON columns are written as JSON strings. The same file isGET /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.