Enforced Store/Global Config vs. Per-Operator Preference
- Status: proposed
- Deciders: Platform Team
- Date: 2026-07-13
- Parent: 0005 — Adopt per-operator preferences from rwe-core
Technical Story: Splitting cosmetic per-operator preference from ops-enforced monitoring config, and defining who can set the latter
Context and Problem Statement
Section titled “Context and Problem Statement”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:
- 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.
- Enforced monitoring config — which widgets/pages a store must
see at all, set by
storeOps(for their assigned stores) orglobalOps/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.
Decision Drivers
Section titled “Decision Drivers”- 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 (
storeOpsfor their stores,globalOps/adminfor 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.
Considered Options
Section titled “Considered Options”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.
Pros and Cons
Section titled “Pros and Cons”Option A — One bucket
Section titled “Option A — One bucket”- 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.
Option B — Two-layer, preference retained but shadowed (recommended)
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.”
Decision Outcome
Section titled “Decision Outcome”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
What becomes enforced config (new layer)
Section titled “What becomes enforced config (new layer)”- 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 |
Precedence rule
Section titled “Precedence rule”- Global enforced config is a floor. A
storeList-scoped enforcement fromstoreOpsmay add required widgets or pins on top of global config, but cannot remove or loosen a global requirement set byglobalOps/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 gapPositive Consequences
Section titled “Positive Consequences”- 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/scopebands from 0008 — no new role concepts needed, just a new capability (widgetConfig:enforce) gated by existing scope.
Negative Consequences
Section titled “Negative Consequences”- 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
storeOpsconfig screen could accidentally appear to allow loosening a global requirement when it actually can’t.
Recommendations
Section titled “Recommendations”- Extend the
operator_id-keyed preference store from 0005 with a separate, ops-ownedEnforcedWidgetConfigstore — do not merge them into one keyspace. - Gate writes to
EnforcedWidgetConfigby capability (widgetConfig:enforce) resolved frompersona+scope, per 0008 —storeOpslimited to theirstoreList,globalOps/adminatglobal. - Implement resolution as global-floor → store-add-on → operator
preference, and unit-test that a
storeOpswrite cannot clear a global requirement. - 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.
- Explicitly scope this to widget visibility + optional display-mode pinning for v1; leave theme/language/layout untouched by enforcement unless a future need arises.
