# Build-knowledge — the Operator Deck (/dashboard)

/dashboard is a viewport-locked HUD start screen, not a settings page: the user's operator card as the centrepiece against a video backdrop, real XP/levels/ranks/badges derived from a ledger...


`/dashboard` is a viewport-locked HUD start screen, not a settings page: the user's operator card as
the centrepiece against a video backdrop, real XP/levels/ranks/badges derived from a ledger,
equippable cosmetics, onboarding that knows what you have already done, and one search bar that
answers or hands off to Leviathan. Built P0–P14 (2026-07-27 → 2026-08-03); P15 (a 3D operator) is
deliberately deferred until cosmetics show engagement.

**The contracts live in `ops/todos/dashboard-deck-roadmap.md` and are stated there ONCE (C1–C10).**
This file does not restate them — it records what a future agent needs in order to build on the deck
without re-deriving it, and the traps that cost this program the most.

## The shape

```
products/leumas-{web,studio,admin}          thin — each mounts dashboardRoutes({ variant })
        │
        ▼
@leumas/features  surfaces/dashboard/       ONE subtree, byte-identical in every product
        │  ├─ Deck.jsx          the stage: topbar · launch · operator · activity · dock
        │  ├─ DeckHome.jsx      hosts the deck through the SHARED nav-views registry
        │  ├─ OperatorPage.jsx  registers the `operator` Imperium mode (NOT `deck` — taken)
        │  └─ _hud/             HudStage · HudPane · HudBackdrop · HudPanel · HudMeter
        │
        ├── @leumas/avatar      look/slots/rarity contracts (React-free) + OperatorCard
        └── @leumas/progression /api/progression — the ledger is the only truth
```

## Where things live

- `shared/packages/avatar` — the `look` blob contract and the card. `./look`, `./slots`, `./rarity`,
  `./frames`, `./auras` import **nothing**, because the API imports them byte-identically.
- `shared/engines/progression` — XP ledger, levels, ranks, badges, quests, cosmetic ownership, the
  merged activity timeline, and `createProgressionRouter`.
- `shared/engines/adapters/domain/progression` — the pure level maths, exposed as an adapter so it is
  HTTP + MCP + a chatbot functioncall for free. **This is the deck's knowledge surface**; there is no
  `adapters/knowledge/deck` bank and there should not be, because a bank is a queryable set of
  world-reference nodes and the deck has no node set.
- `shared/packages/features/src/surfaces/_hud` — the stage primitives, deck-agnostic.
- `ops/tools/deck-assets` — the ffmpeg pipeline that produced the committed backdrops and cosmetics.
- `shared/services/datacenter/src/library-assets` — the committed media, served at `/library/*`.

## The rules that are not style preferences

**The ledger is the only truth for XP.** `awardXp()` is the sole writer and never throws. Level, rank
and the next threshold are derived on read from `SUM(user_experience_events.amount)`. Badge unlocks
and cosmetic grants ARE stored — `user_badge_unlocks` is append-only so a badge is never revoked, and
`manual`/`purchase` cosmetics require an explicit `user_cosmetics` row. `/recompute` exists because
those rows are stored, not derived.

**Never cross the two datastores.** Typed tables go through `getDb()`/drizzle; dynamic collections go
through the injected `connector`. Calling `connector.get().read('userExperienceEvents')` silently runs
`CREATE TABLE IF NOT EXISTS dyn_userExperienceEvents` and returns `[]` — no error, permanently wrong.

**`feature` is opt-out; `entitlement` is opt-in.** The deck gates every optional part on `feature`
(absent means on). An `entitlement` gate denies until somebody seeds a policy row, so a freshly
downloaded appliance would render a wall of locks out of the box. A disabled part is OMITTED, never
padlocked: an upsell for something the operator switched off is noise the end user cannot act on.

**Deck code must not reach the eager bundle.** `check-deck-budget.mjs` greps the BUILT entry chunk of
every product for `lms-odeck`, `lms-stage__pane`, `backdrop-scrim` and `lms-op__card`. Two leaks got
in during P14 and neither was visible to source-walking guards: a `import './dashboard.css'` in a
component with no classes, and a *named re-export* through a lane barrel — re-exporting six innocent
names evaluates the lane, and a CSS side-effect import cannot be tree-shaken.

**The stage's CSS has four traps** — a positioned backdrop outranking static panes, `:has()`
specificity in the mobile release, cross-sheet breakpoint literals, and `color-mix` alpha caps. All
four shipped broken and none was visible to review. They are documented at length in
`shared/packages/features/src/surfaces/_hud/README.md` and pinned by
`shared/packages/features/test/deck-stage.test.js`.

## Extending it — the three cheap paths

1. **A new cosmetic.** Add a `cosmetic_definitions` row (slot, rarity, unlock rule, `assetRef`) and,
   for art-backed slots, a `.webp` under `library-assets/cosmetics/`. `cosmeticUrl()` hardcodes the
   `.webp` extension, so nothing else works. Frames and auras are procedural code and cost zero bytes.
2. **A new onboarding quest.** One row in the dynamic `quests` collection — `signal`, `target`, `xp`,
   `href`, and optionally `feature` so a tenant that switched the relevant system off never sees it.
   No deploy. `seedQuests` never overwrites an existing row, so a tenant's edits survive.
3. **A new badge.** One row plus one `registerSignal` function. Do not extend
   `BADGE_REQUIREMENT_TYPE` — `custom` plus the signal registry means a new badge is never a migration,
   and an unknown signal simply never unlocks rather than unlocking for everyone.

## Verify

```sh
pnpm check:deck            # bundle purity, asset budget/safety/shippability, eager-chunk markers
node ops/infra/scripts/smoke-deck.mjs        # appliance role, asserts BYTES for every asset variant
node ops/infra/scripts/smoke-progression.mjs # the 15-level fixture, row by row
pnpm smoke:dashboard
```

## What will bite you

- **The assets and the generator may not be in git.** `check-deck-assets` used to exit 0 on a clean
  checkout because its first act was an `existsSync` on a tree nothing had committed — CI was green
  *because* the tree was invisible. It now fails loudly. If it does, the fix is a `git add`.
- **A guard that passes because its subject is missing is worse than no guard.** That mistake appeared
  three separate times in this program: the asset tree above, an entitlement gate that always denied
  (verifying a negative proves nothing), and a test whose assertion matched a substring of a
  neighbouring selector. The habit that catches it is to **inject the exact defect and watch the test
  fail** before believing it.
- **Measure the page, do not read it.** Every one of the deck's worst defects — an invisible quest
  dock, an unreachable mobile layout, unclickable dock links, 1.01:1 text — was found with
  `document.elementFromPoint` and canvas pixel sampling against the running app, and none was visible
  in the source.


---
Source: shared/services/knowledge/build-knowledge/operator-deck.md
Canonical: https://docs.leumas.tech/p/how-to/operator-deck
