Skip to content

Enforced Store/Global Config vs. Per-Operator Preference

Technical Story: Splitting cosmetic per-operator preference from ops-enforced monitoring config, and defining who can set the latter

0005 decided that dashboard layout, widget dismiss state, and similar preferences are keyed by operator_id so they follow the athlete across shared store devices. That ADR treated “preference” as one bucket.

In practice there are two different kinds of “preference,” and conflating them is a problem:

  1. Cosmetic / inconsequential preference — dark mode vs. light mode, language, or how the same data is rendered in a widget (a number tile vs. a pie chart for the same underlying metric). These change presentation only; the underlying data and the set of widgets visible are unaffected. This is squarely what 0005 was designed for.
  2. Enforced monitoring configwhich widgets/pages a store must see at all, set by storeOps (for their assigned stores) or globalOps/admin (for all stores), to guarantee that every store in scope is looking at what ops has deemed required to view or monitor. This is a governance decision, not a personal one — an individual athlete or coach should not be able to dismiss a required widget, and 0005’s per-operator dismiss model would currently let them, because it doesn’t distinguish “I don’t want to see this” from “ops requires every store to see this.”

This ADR defines the boundary between the two, and how enforced config composes with (and can override) per-operator preference, using the persona / scope model from 0008 to decide who is allowed to set enforced config at what scope.


  • Cross-store consistency — ops needs confidence that every store in a region (or globally) is actually looking at the metrics/widgets deemed required, not that each athlete quietly dismissed them.
  • Don’t break 0005 — cosmetic preference (theme, language, per-widget display format) must remain freely overridable per operator; this ADR should narrow, not replace, that decision.
  • Clear authority — enforced config should only be settable by roles with the right scope (storeOps for their stores, globalOps/admin for global), per the band model in 0008, and should be auditable (who required what, when).
  • Predictable precedence — when personal preference and enforced config disagree, the resolution order must be unambiguous and the same everywhere it’s checked (host dashboard, module-rendered widgets).
  • Avoid over-enforcement — most preferences (theme, language, chart type) should never need an enforced override; the mechanism should be opt-in per widget/setting, not a blanket lockdown.

Option A — Keep one preference bucket (status quo from 0005)

Section titled “Option A — Keep one preference bucket (status quo from 0005)”

All preferences, including widget visibility/dismiss, stay purely operator-scoped. Ops has no way to require a widget be seen.

Option B — Two-layer model: enforced config + operator preference

Section titled “Option B — Two-layer model: enforced config + operator preference”

Introduce a separate, ops-owned enforced config layer (per widget/ setting: required-visible, and optionally a pinned display mode) that is resolved before per-operator preference. Preference only applies where enforcement hasn’t pinned a value.

Option C — Enforced config replaces preference entirely for pinned items

Section titled “Option C — Enforced config replaces preference entirely for pinned items”

Same as B, but once a setting is enforced, the per-operator preference for that item is deleted/ignored rather than retained-but-shadowed.


  • Good: no new mechanism; matches 0005 as shipped.
  • Bad: no way to guarantee cross-store monitoring consistency; a required widget can be silently dismissed by any athlete.
Section titled “Option B — Two-layer, preference retained but shadowed (recommended)”
  • Good: clean precedence; personal preference is preserved underneath (so if ops later un-enforces a widget, the athlete’s prior preference reappears instead of being lost); cosmetic preferences (theme, language, chart type) are untouched unless a specific widget’s display is explicitly pinned by ops.
  • Bad: requires storing both layers and a resolution step at read time; slightly more state to reason about than a single bucket.

Option C — Enforced overwrites/deletes preference

Section titled “Option C — Enforced overwrites/deletes preference”
  • Good: simpler storage (one value per setting, last-writer-wins).
  • Bad: destructive — an athlete’s chart-type preference is lost the moment ops pins display mode for unrelated reasons, and can’t be restored if the pin is later removed; harder to reason about “why did my preference change.”

Chosen option: Option B — two-layer model, enforced config shadows (does not delete) operator preference.

What stays purely per-operator (0005’s original scope, unchanged)

Section titled “What stays purely per-operator (0005’s original scope, unchanged)”
  • Theme (dark/light)
  • Language
  • Per-widget display format for a given metric — e.g. numeric tile vs. pie chart for the same underlying data — as long as no enforced pin exists for that widget
  • Dashboard layout (position/ordering), where not constrained by a required-widget placement rule
  • Widget/page required-visible flag — if set, the widget cannot be dismissed by per-operator dismiss state at any store in scope. This is the direct fix for “storeOps/globalOps-deemed-required monitoring must not be silently hidden.”
  • Optional pinned display mode — ops may, for specific cross-store-comparability cases, pin how a required widget renders (e.g. force the same chart type everywhere so a regional review looks the same store to store). This is opt-in per widget, not a default — most widgets should have no pin, leaving display mode to operator preference.

Who can set enforced config, and at what scope

Section titled “Who can set enforced config, and at what scope”

Reuses the 0008 model directly:

Setter Scope of enforced config
storeOps Their assigned storeList only
globalOps / admin global — applies to all stores
coach, athlete Cannot set enforced config; can only set personal preference
  • Global enforced config is a floor. A storeList-scoped enforcement from storeOps may add required widgets or pins on top of global config, but cannot remove or loosen a global requirement set by globalOps/admin.
  • For any setting not enforced, per-operator preference (0005) applies unchanged.
  • When a setting is enforced, the operator’s stored preference for that setting is retained in storage but not applied while the enforcement is active — if the enforcement is later removed, the operator’s prior preference reappears rather than defaulting.
interface EnforcedWidgetConfig {
widgetId: string;
requiredVisible: boolean;
pinnedDisplayMode?: string;
setBy: { persona: 'storeOps' | 'globalOps' | 'admin'; scope: Scope };
}
// Resolution at render time, per widget:
// 1. Global enforced config (floor)
// 2. Store-scoped enforced config (can only add, not loosen)
// 3. Per-operator preference (0005) — applies only where step 1/2 left a gap

  • Ops gets a real guarantee that required monitoring widgets are visible fleet-wide, closing the gap 0005 left open.
  • Cosmetic preference (theme, language, chart type) keeps working exactly as 0005 designed it, for the large majority of settings that will never be enforced.
  • Athlete/coach preference isn’t destroyed by enforcement — it’s shadowed and can resurface, which avoids surprising “where did my setting go” reports.
  • Enforcement authority maps directly onto the persona/scope bands from 0008 — no new role concepts needed, just a new capability (widgetConfig:enforce) gated by existing scope.
  • Adds a second storage layer and a resolution step wherever preferences are read (host dashboard, any module rendering widgets) — not just a single operator_id-keyed lookup anymore.
  • Needs an audit trail (who enforced what, when, at what scope) to avoid becoming as opaque as the ad hoc role variants rejected in 0008 — not yet designed here.
  • Global-floor / store-can-only-add precedence needs to be enforced consistently at every write path (host UI, any admin/ops tooling), or a storeOps config screen could accidentally appear to allow loosening a global requirement when it actually can’t.

  1. Extend the operator_id-keyed preference store from 0005 with a separate, ops-owned EnforcedWidgetConfig store — do not merge them into one keyspace.
  2. Gate writes to EnforcedWidgetConfig by capability (widgetConfig:enforce) resolved from persona + scope, per 0008 — storeOps limited to their storeList, globalOps/admin at global.
  3. Implement resolution as global-floor → store-add-on → operator preference, and unit-test that a storeOps write cannot clear a global requirement.
  4. Design the audit trail for enforced-config changes (who/when/scope) as a fast-follow — needed before this is safe to expose in any self-service ops UI.
  5. Explicitly scope this to widget visibility + optional display-mode pinning for v1; leave theme/language/layout untouched by enforcement unless a future need arises.