adr: 0024 title: "Per-event admin config — enable / per-event cap / category" status: superseded superseded-by: 0026 implementation-status: in-progress date: 2026-06-07 implemented-date: null implemented-in: - ftl-backend feat/event-admin-config - ftl-backend bug/admin-config-cross-replica deciders: [@amalkrsihna] affects-specs: [event-ingestion] affects-code: - ftl-backend/internal/sportmonks/scoring.go - ftl-backend/internal/sportmonks/ticker.go - ftl-backend/internal/adminsettings/service.go - ftl-backend/internal/handler/admin.go - ftl-backend/cmd/api-server/main.go - ftl-backend/migrations/000044_seed_event_config.up.sql supersedes: null superseded-by: null
ADR-0024: Per-event admin config — enable / per-event cap / category¶
Context¶
Admins could already tune event weights at runtime (ADR-0019/0022 era:
admin_settings.score_event_points.event_bumps, hot-reloaded), but two gaps
remained:
- No per-event kill-switch or per-event cap. The only way to neutralise an
event was to set its weight to 0, and the micro_bump cap was a single global
MicroBumpCapPercentOfBase(10%) for every event. There was no per-event classification either. - Changes only reached one replica.
onUpdatefired only on the api-server replica that served the admin PUT; the other replica kept stale in-memory scoring/cooldown until restart (fixed inbug/admin-config-cross-replica— recorded here as the enabling dependency).
Decision¶
Add a backward-compatible per-event config layer under a new
admin_settings key event_config, additive on top of the existing weights:
- enabled (default
true) — admin kill-switch for an event's PRICE bump. The event still appears in the live feed; only its price effect is suppressed. This is distinct from weight 0: it reads as intent and survives weight edits. - maxMovePct (default
0= the globalMicroBumpCapPercentOfBase) — per-event override of the symmetric micro_bump cap, as a percent of the price anchor. - category — classification metadata (e.g. "attacking"/"defensive"/ "discipline") for grouping/display; no pricing effect.
An event with no event_config entry behaves exactly as before — fully
backward-compatible. Changes propagate to all replicas via the admin-settings
Redis pub/sub broadcaster.
Propagation (ADR companion): adminsettings.Service publishes the changed key
on admin_settings:changed; every replica subscribes and re-applies. This makes
all admin settings (weights, multipliers, cooldown, and now event_config)
cluster-consistent within one pub/sub round-trip.
Alternatives considered¶
- Reshape
event_bumpsinto per-event objects ({goal: {weight, enabled, …}}). Rejected for now: it would change the established weights payload + the admin UI's flat number inputs. A separate additiveevent_configkey keeps weights untouched and the migration trivial; the two can be unified later if desired. - Per-event cooldown/decay in this ADR. Deferred: cooldown needs per-(player, event) TTL tracking; per-event decay needs the accumulator to track per-event contributions (today decay is global per-poll). The config shape leaves room for them without another schema change.
Consequences¶
- Positive: admins get a true per-event kill-switch + per-event cap + category without a deploy, applied across all replicas; default behavior is unchanged until an override is set; the change is additive (no weights/UI break).
- Cost: two extra map lookups per priced event on the hot path (RLock-guarded,
negligible). One seeded
admin_settingsrow. - Known follow-ups: per-event cooldown + decay wiring; admin rollback/history
(
admin_settings_history+ revert); frontendScoringPanelinputs (until then, configurable viaPUT /api/admin/settings/event_config).
Migration / rollout¶
000044_seed_event_config inserts an empty {} row (additive,
ON CONFLICT DO NOTHING; adminsettings.Set only updates existing rows). With
the row empty, every event keeps its prior behavior — zero-risk default. Ships
disabled-by-omission; admins opt in per event.