Skip to content

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:

  1. 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.
  2. Changes only reached one replica. onUpdate fired only on the api-server replica that served the admin PUT; the other replica kept stale in-memory scoring/cooldown until restart (fixed in bug/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:

event_config = { "<eventName>": { "enabled": bool, "maxMovePct": number, "category": string } }
  • 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 global MicroBumpCapPercentOfBase) — 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_bumps into 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 additive event_config key 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_settings row.
  • Known follow-ups: per-event cooldown + decay wiring; admin rollback/history (admin_settings_history + revert); frontend ScoringPanel inputs (until then, configurable via PUT /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.