# Screens Inventory — Common Module (Shell, Auth, Cross-cutting)

> **Verified accurate:** 2026-05-02 — Stage 3 backend pre-verification still holds; doc shape, atoms vs singletons split, and the `/devices/*` not-yet-in-OpenAPI warning all accurate. **One drift:** `front/common/` is now 32 files (not 21) — the +11 are batch atoms + cross-cutting screen splits (`batch7-atoms.js`, `xcut-screens-batch7.js`, dashboard splits, polish layers); net-new surfaces, original screens still owned as documented.
> **Status:** active source-of-truth doc; the canonical Common-shell catalog.

**Audience:** integration engineers wiring the Dalseen front-end shell, authentication, RBAC, dashboard, devices/peripherals, and cross-cutting screens to the real backend.
**Scope:** every consumer-visible screen, modal, hook, and singleton under `front/common/`. **21 source files, ~25 top-level screens/components, 9 modals, 7 cross-cutting singletons.**
**Companion docs:** `MODULE-MAP.md`, `DESIGN-SYSTEM.md`, `API-USAGE-MAP.md §3` (auth/me/session), `INTEGRATION-NOTES.md`, sibling `SCREENS-INVENTORY-{Retail,Pay,Dine,Accounting,Owner+HR,Platform}.md` (everything in this doc is a *dependency* of those modules).

> **Backend pre-verification note** (per integration-team direction): Common is the only module where most surfaces are **chrome around backend data, not API consumers themselves**. The screens we *do* wire to endpoints — Login, Users, Roles, Audit, Shifts — map to `auth.*`, `me`, `users.*`, `roles.*`, `shifts.*` per the Stage 3 backend tag and `docs/openapi/openapi.yaml`. The `/devices/*` family (peripheral self-test, badge punch, smart-safe deposit) is **not yet in the OpenAPI spec** — see §6 for what this means at cut-over.

---

## How to read this doc

Same shape as the other `SCREENS-INVENTORY-*.md` files:

> **<Route key or component> · <Screen / surface name>** — one-line purpose
> **File(s):** source files
> **Mounts at:** route key in `DALSEEN_NAV` (or "no route — invoked from <X>" for modals/overlays)
> **Owns / Composes / States / Actions / Data / Permissions / Quirks**

Common is structurally different from a domain module: ~half the surfaces here are **shells, atoms, or singletons** (Sidebar, Topbar, Skeleton, Spinner, fmtSAR, DalseenIntegrations gate). Those don't have route keys — they're *composed* by every other screen in the app. They're catalogued in §1 (atoms) and §7 (singletons) for completeness, with full-screen surfaces in §2–§5.

---

## 1 · Module overview

### 1.1 File census (21 files)

| File                                  | Lines | Owns                                                                                                  |
|---------------------------------------|------:|-------------------------------------------------------------------------------------------------------|
| `tokens.js`                           |   ~370 | `DALSEEN_TOKENS` (light/dark) · `DALSEEN_SYSTEMS` · `DALSEEN_NAV` (~110 entries) · `DALSEEN_PROFILES` · `DALSEEN_DATA` · `DALSEEN_KIND_LABEL` · `DALSEEN_RETAIL_GROUPS` · `DALSEEN_HR_GROUPS` |
| `roles.js`                            |   ~270 | `DALSEEN_ROLES` (12) · `DALSEEN_PERMS` matrix · `DALSEEN_ROLE_POLICIES` (biometric thresholds) · `DALSEEN_RETAIL` (extended fixture: customers, suppliers) · `DALSEEN_ONBOARDING_STEPS` · `DALSEEN_HEALTH_FACTORS` · `DALSEEN_PLATFORM` (tenants) · `canSee()` · `isReadonly()` · `_onboardingFor()` · `_healthFor()` |
| `strings.js`                          |    37 | `STRINGS.{en,ar}` (shared) · `fmtSAR()` · `fmtNum()`                                                |
| `icons.compiled.js`                   |   ~600 | `Icon` component (~70 named glyphs)                                                                |
| `loading.js`                          |   183 | `Skeleton`, `SkeletonText`, `SkeletonCard`, `SkeletonTable`, `SkeletonStatGrid`, `Spinner`, `ErrorState`, `EmptyState`, `AsyncBoundary` |
| `shell.compiled.js`                   |   830 | Original `Sidebar`, `Topbar`, `RetailGroupedNav` · `useT()`, `systemColor()` (still the source of these 2 atoms) |
| `shell-polish.jsx`                    |  1591 | **Replacement** `Sidebar` + `Topbar` (overrides `shell.compiled.js`) · Command Palette (⌘K) · pinned-nav · context menus · branch/shift/cashier popovers · `MyAccountScreen` · `MySecurityScreen` |
| `dashboard-splash.compiled.js`        |   253 | `DashboardSplash` — first-login/setup-just-finished hero card with system tiles                    |
| `dashboard.compiled.js`               |   733 | `DashboardScreen` (the 5-tab Today / Live / Act / Close / Roll-up command center) · `Card`, `SectionTitle`, `Pill`, `MiniStat`, `IconBtn`, `SysCard` (all internal atoms) · `btn()` legacy export |
| `auth.compiled.js`                    |  1120 | `LoginScreen` (creds → 2FA → tenant) · `UsersScreen` · `InviteModal` · `EditUserModal` · `RolesScreen` (legacy stub) · `AccessAuditScreen` · `OnboardingAdminsStep` · `DALSEEN_USERS` (13 seeded) · `DALSEEN_PERM_ACTIONS` · `DALSEEN_HIGH_RISK` · `DALSEEN_ACCESS_AUDIT` |
| `roles-editor.compiled.js`            |   745 | `RolesHubV2` — full role editor (replaces the auth.compiled `RolesScreen`) · `JDTab`, `PermissionsTab`, `PolicyTab`, `CloneDialog`, `AssignPreview` · `seedRolePolicyStore()` · `DALSEEN_ROLE_POLICY` · `DALSEEN_getRolePolicy()` (overrides the simpler one in `roles.js`) |
| `biometric-shift.compiled.js`         |   630 | `BiometricPrompt` (WebAuthn platform authenticator) · `StartShiftModal` · `ShiftLauncher` · `hasBiometricCapability()` · `getCurrentShift()` · `requireShift()` |
| `modals.compiled.js`                  |   831 | `PayModal` (SoftPOS Tap-on-Phone payment) · `TailoringScreen` (vertical demo) · `AICopilot` (right-side dock) · `TweaksPanel` (bottom-right design panel) |
| `shared.compiled.js`                  |   420 | `DesignedModule` (generic stub for un-built screens) · `ZatcaScreen` (live clearance center) · `btn()` shared button helper |
| `shared-modules.compiled.js`          |   367 | `SharedAI` (cross-system AI assistant) · `SharedBI` (BI dashboards index) · `SharedCRM` · `SharedManufacturing` · internal atoms: `Page`, `Header`, `StatCard`, `Pill`, `Btn`, `TableShell` |
| `app-marketplace-screen.jsx`          |  1149 | `AppMarketplaceScreen` (App Store) · `Hero`, `HeroSpotlight`, `CollectionsRow`, `FeaturedRow`, `CategoryBar`, `AppGrid`, `AppCard`, `AppDrawer`, `DeveloperCTA` · `AppMarketplaceCatalog.CATALOG` (~40 apps) |
| `dalseen-integrations.js`             |   179 | `DalseenIntegrations` central gate — `shouldPostGL()`, `shouldDecrementStock()`, `shouldPostCOGS()`, `acctModeFor()`, `stockModeFor()`, `summary()` · per-branch overrides · subscriber bus |
| `dalseen-integrations-screen.jsx`     |   325 | `DalseenIntegrationsScreen` — UI to flip the above gate (Books mode · Stock mode · per-branch overrides) |
| `devices.js`                          |   175 | `DALSEEN_DEVICES` registry (9 default devices: fingerprint, printer, drawer, scanner, CFD, payterm, badge, camera, safe) · `selfTest()` · `register()` · `dalseen:device-changed` event bus · `PublicKeyCredential` capability probe |
| `api-seeds.js`                        |   ~500 | (Read-only here — seeds the API mock layer; full spec in `MODULE-MAP.md §4`) |
| `api.js`                              |   ~210 | Old façade — `CONFIG.MOCK_MODE`, `API_BASE_URL`, latency knobs (superseded by `front/api/api-foundation.js`; full spec in `MODULE-MAP.md §3.1`) |

> **The "duplicate" pattern.** Three subsystems have a v1 + v2 pair: Sidebar/Topbar (`shell.compiled.js` → overridden by `shell-polish.jsx`), Roles (`auth.compiled.js#RolesScreen` → overridden by `roles-editor.compiled.js#RolesHubV2`), and Role-policy reader (`roles.js#DALSEEN_getRolePolicy` → overridden by `roles-editor.compiled.js`). Load order matters — `shell-polish.jsx` and `roles-editor.compiled.js` *must* load **after** their v1 counterparts. The boot sequence in `index.html` enforces this, but anyone refactoring the script tags needs to know.

### 1.2 Top-level routes

The Common module contributes 8 route keys to `DALSEEN_NAV` (most of these mount under the Retail or Owner sidebar group depending on role):

| Route key                    | Sidebar label                  | Component                          | File                                    | Status |
|------------------------------|--------------------------------|------------------------------------|------------------------------------------|--------|
| `dashboard`                  | Dashboard                      | `DashboardScreen`                  | `dashboard.compiled.js`                 | live · reads `DALSEEN_DATA` (synthesised demo) |
| `retail.users`               | Users & Access                 | `UsersScreen`                      | `auth.compiled.js`                      | live (fixture) — `DALSEEN_USERS` |
| `retail.roles`               | Roles & Permissions            | `RolesHubV2`                       | `roles-editor.compiled.js`              | live (fixture) — `DALSEEN_ROLES` + `DALSEEN_ROLE_POLICY` |
| `admin.audit` (when present) | Access Audit                   | `AccessAuditScreen`                | `auth.compiled.js`                      | live (fixture) — `DALSEEN_ACCESS_AUDIT` |
| `core.account`               | My Account                     | `MyAccountScreen`                  | `shell-polish.jsx`                      | live (read-only against `DALSEEN_USERS`) |
| `core.security`              | My Security                    | `MySecurityScreen`                 | `shell-polish.jsx`                      | live (fixture) — sessions, devices, recovery codes |
| `shared.marketplace`         | App Marketplace                | `AppMarketplaceScreen`             | `app-marketplace-screen.jsx`            | live (fixture) — `AppMarketplaceCatalog.CATALOG` (40 apps) |
| `shared.integrations`        | Dalseen Integrations           | `DalseenIntegrationsScreen`        | `dalseen-integrations-screen.jsx`       | live · writes through `DalseenIntegrations` gate |

> **Login is not a route.** `LoginScreen` mounts ahead of the shell; the gate is in `app-main.compiled.js`. Once `onAuthenticated(user)` fires, the shell + sidebar + dashboard come up. There is no `/login` deep link in `DALSEEN_NAV`.

### 1.3 Data sources in play

The Common module is the **only** module that owns its own seed data outright — everywhere else reads from `window.API.*`, `DALSEEN_DATA`, or its own inline arrays. Common publishes:

| Surface                              | Purpose                                              | Consumers (not exhaustive) |
|--------------------------------------|------------------------------------------------------|------------|
| `DALSEEN_USERS`                      | 13 seeded users (Faisal, Noura, Sara, Majed, Yasser, …) — the access list every other screen filters/searches | `UsersScreen`, `MyAccountScreen`, `LoginScreen`, `EditUserModal`, every audit row, retail Users-screen's `name`/`assignedBranches`/`lastActive` aliases (derived in `auth.compiled.js` lines ~30–80) |
| `DALSEEN_ROLES`                      | 12 role records with bilingual labels + scope + color | `RolesHubV2`, `Sidebar` (role switcher), `EditUserModal`, `canSee()` |
| `DALSEEN_PERMS`                      | Role → nav-id matrix (`'*'` for owner)                | `canSee()` everywhere — gates the Sidebar render |
| `DALSEEN_ROLE_POLICIES`              | Per-role biometric / shift / threshold policy         | `BiometricPrompt`, `StartShiftModal`, high-risk action gates in Retail POS |
| `DALSEEN_ROLE_POLICY` (v2 store)     | Full editable policy graph keyed by role              | `RolesHubV2` (read/write), retail high-value action gates (read) |
| `DALSEEN_PERM_ACTIONS`               | 5 action types — view/edit/approve/export/delete      | `PermissionsTab` legend |
| `DALSEEN_HIGH_RISK`                  | 8 actions that require step-up 2FA                    | `BiometricPrompt`, retail refund/void/discount gates |
| `DALSEEN_ACCESS_AUDIT`               | Auth/permission/data-export events                    | `AccessAuditScreen` |
| `DALSEEN_NAV`                        | Source of truth for sidebar entries (~110 routes)     | `Sidebar`, `RetailGroupedNav`, `GroupedNav`, `CommandPalette` |
| `DALSEEN_DEVICES`                    | 9 peripherals with `selfTest()` adapters              | Retail Devices screen, retail POS pre-flight, badge punch (HR), smart-safe deposit (Pay) |
| `DALSEEN_DATA` (`branches`, `menu`, `tables`, `tickets`, `business`) | Cross-module fixture left over from before modules split out | Dashboard, Dine front-of-house screens, Retail headers, branch popover |
| `STRINGS.{en,ar}` + `fmtSAR` + `fmtNum` | Bilingual chrome strings + money formatting          | Every screen in the app |
| `DALSEEN_TOKENS.{light,dark}`        | Color/spacing/typography tokens                       | Every screen — passed in as `T` prop |

> **Cut-over implication.** Of these, the only ones that survive into production unchanged are `STRINGS`, `fmtSAR`/`fmtNum`, `DALSEEN_TOKENS`, and `DALSEEN_NAV` (the nav tree itself is FE-owned routing config, not a backend resource). Everything else is a fixture: `DALSEEN_USERS` becomes `GET /users`, `DALSEEN_ROLES` becomes `GET /roles`, `DALSEEN_DEVICES` becomes the device-registry endpoint family (currently absent — see §6.3). The `DALSEEN_PERMS` matrix is a special case: the *permission set per role* is a backend resource, but the *route-id → permission-id* mapping (which sidebar item is gated by which permission) is FE config and stays here.

### 1.4 The four distinct surfaces

The Common module's surfaces cluster into four operationally-distinct groups:

**Pre-auth / auth (no shell):**
- `LoginScreen` (creds → 2FA → tenant select)

**Shell chrome (composed by every other screen):**
- `Sidebar` + `Topbar` (`shell-polish.jsx` versions, with v1 fallback in `shell.compiled.js`)
- `CommandPalette` (⌘K)
- `BranchPopover`, `ShiftPopover`, `CashierPopover`, `OwnerProfilePopover`
- `DashboardSplash` (first-time hero)

**Stand-alone screens (mount under a route):**
- `DashboardScreen` (5 tabs)
- `UsersScreen`, `RolesHubV2`, `AccessAuditScreen`
- `MyAccountScreen`, `MySecurityScreen`
- `AppMarketplaceScreen`, `DalseenIntegrationsScreen`
- `SharedAI`, `SharedBI`, `SharedCRM`, `SharedManufacturing`, `ZatcaScreen` (cross-system shared modules)

**Modals & overlays (invoked from anywhere):**
- `BiometricPrompt`, `StartShiftModal`, `ShiftLauncher`
- `PayModal` (SoftPOS)
- `InviteModal`, `EditUserModal`, `CloneDialog`, `AssignPreview`
- `AICopilot`, `TweaksPanel`

---

## 2 · Pre-auth screens

### 2.1 `LoginScreen` · Sign-in

> Two-pane layout — left brand panel (DALSEEN logo, Arabic geometric pattern, compliance row: ZATCA · SAMA · PDPL · ISO 27001), right form panel (email → password → 2FA → optional tenant pick). Bilingual via top-right `EN/AR` segmented pill.

- **File:** `auth.compiled.js` lines 126–360 (`LoginScreen`)
- **Mounts at:** **no route** — gate ahead of the shell, mounted directly by `app-main.compiled.js` when no session
- **Owns:** stage state machine (`credentials | 2fa | tenant`), 6-digit OTP input array, attempt counter (locks at 5)
- **Composes:** raw HTML + inline styles (no shared atoms — pre-auth has no `T` token plumbing yet, falls back to a 7-key inline default if `DALSEEN_TOKENS` isn't ready)
- **States:** `credentials` (default) · `2fa` (TOTP or SMS, picked from `user.twofaMethod`) · `tenant` (only if user has multi-tenant access — currently single-tenant in fixtures so this branch never fires) · `locked` (after 5 fails) · `error` (inline strip below the input)
- **Actions:** `submitCreds()` → 500ms simulated → `setStage('2fa')` if `u.twofa`, else `onAuthenticated(u)`. `submitOtp()` → 400ms simulated → accepts any 6-digit code in demo (no real TOTP verify FE-side — that's a backend concern). Alt sign-in button: "Sign in with OTP to phone" (no handler today — placeholder)
- **Data today:** `DALSEEN_USERS` (matched by `email`)
- **Data at cut-over:** `POST /auth/login` { email, password } → `{ token, user, requires_2fa, twofa_method }`. Then `POST /auth/2fa/verify` { code } → `{ token, session }`. Then `GET /me` → tenant + branch + permissions. (Per `routes-api.php` and OpenAPI spec — see `API-USAGE-MAP.md §3.1`.)
- **Permissions:** none — pre-auth
- **Quirks:**
  1. Demo accepts **any 6-digit code** for 2FA. This is hard-coded — production must verify against the actual TOTP secret / SMS challenge.
  2. **Lockout state lives in component memory** (`useState`). Refreshing the page clears it. Production must persist failed-attempt count server-side and 401 with a Retry-After header.
  3. `setStage('tenant')` branch is **dead code** in current fixtures — every user has exactly one tenant. Wire when multi-tenant lands.
  4. Email autofill defaults to `faisal@dalseen.sa` (the owner) — a demo aid, **must be removed** before pilot.
  5. The compliance row ("ZATCA Ph.II · SAMA · PDPL · ISO 27001") is **purely decorative** today. Production may want these to link to attestation PDFs.

---

## 3 · Shell chrome

The shell is the persistent UI scaffold that wraps every authenticated screen. Three layers, in load order:

1. `shell.compiled.js` defines v1 `Sidebar` + `Topbar` + the helpers `useT()` / `systemColor()`.
2. `shell-polish.jsx` **replaces** `window.Sidebar` and `window.Topbar` with v2 components (1591 lines vs ~830).
3. `app-main.compiled.js` (in the parent dir) mounts `<Sidebar>` and `<Topbar>` around the active screen.

### 3.1 `Sidebar` (v2 — `shell-polish.jsx`)

> System switcher at the top (Core · Retail · Pay · Dine · HR · Platform), grouped nav per system, user footer at the bottom. Pinned items live above the system list. Right-click any nav row → context menu with Pin / Open in new tab / Copy link.

- **File:** `shell-polish.jsx` (`Sidebar` lines ~233–392, falls through to `Section`, `GroupedNav`, `NavRow`, `UnifiedFooter`, `FooterPopover`)
- **Mounts at:** rendered by `app-main.compiled.js` for every authenticated route
- **Owns:** active system + active route highlight; pinned-route persistence (`localStorage:dalseen:pinned-nav`); group open/close state per system (`localStorage:dalseen:nav-groups-{system}`); right-click context menu position
- **Composes:** `DALSEEN_NAV` (filtered by `canSee(role, id)`), `DALSEEN_SYSTEMS`, `DALSEEN_KIND_LABEL`, `DALSEEN_RETAIL_GROUPS`, `DALSEEN_HR_GROUPS`, `Icon`, `useT`, `systemColor`
- **States:** persistent (no loading) — sidebar is hydrated from local config at mount
- **Actions:** `onNav(id)` → switch active route · `onSystem(sys)` → switch active system (auto-picks first nav row in the new system) · `onTogglePin(id)` · `onSignOut()` (delegated up)
- **Data today:** `DALSEEN_NAV` (FE config) + `DALSEEN_PERMS` matrix
- **Data at cut-over:** `DALSEEN_NAV` *stays FE-owned* (it's routing config, not data) — but the gate `canSee(role, id)` reads from `me.permissions[]` returned by `GET /me`. The mapping from `nav.id` → `permission.code` is currently *implicit* (`canSee` just does `perms.ids.includes(id)`); production may want an explicit map table.
- **Permissions:** the sidebar itself is unconditional, but each row is gated by `canSee(role, id)`
- **Quirks:**
  1. `canSee` returns `true` for `release-notes` for **everyone** (hard-coded escape) — keep this when wiring real perms.
  2. `isReadonly(role)` short-circuits: a readonly role still sees rows, but the screens beyond should respect `readonly: true` and disable their CTAs. This is enforced **per screen**, not centrally — easy to forget when adding a new screen.
  3. The `superadmin` role's nav lives behind `system === 'platform'` filter. `Sidebar` shows the Platform system pill only when `role === 'superadmin'`.
  4. `DALSEEN_PERMS.owner` uses `{ all: true, hidePlatform: true }` — meaning owner sees everything *except* the Platform system. Add this short-circuit at the role-policy editor (`RolesHubV2`) when shipping; today it's hard-coded in `canSee`.

### 3.2 `Topbar` (v2 — `shell-polish.jsx`)

> Adapts per active system. Universal: search pill (opens command palette), branch picker, AI Copilot trigger, language toggle, theme toggle, profile menu. Retail/POS adds: shift status pill (open/closed/no-shift), cashier name pill, register pill. Platform adds: tenant selector pill.

- **File:** `shell-polish.jsx` (`Topbar` lines ~1134–1306, plus popovers `BranchPopover`, `ShiftPopover`, `CashierPopover`, `OwnerProfilePopover`, `Popover`, `ContextPill`)
- **Mounts at:** rendered by `app-main.compiled.js` for every authenticated route
- **Owns:** popover open/close state, hover state, theme/lang/role override delegation
- **Composes:** `DALSEEN_TOKENS`, `Icon`, `getCurrentShift()`, `DALSEEN_DATA.branches`, `DALSEEN_USERS` (avatar)
- **States:** static; popovers slide in/out; AI dock toggles a sibling
- **Actions:** `onOpenTweaks()` · `onAskAI()` · `onSignOut()` · `onRole(roleId)` · `onLang(lang)` · branch pill click → `<BranchPopover>` lists branches + lets user switch
- **Data today:** `getCurrentShift()` reads `sessionStorage:dalseen:currentShift` · branches from `DALSEEN_DATA.branches`
- **Data at cut-over:**
  - Branch list → `GET /branches` (cached per-session)
  - Active shift → `GET /shifts/current` (returns the open shift for the active branch + cashier)
  - Profile → `GET /me`
  - Branch switch → `POST /me/active-branch { id }` then re-load with new `X-Branch-Id` header
- **Permissions:** branch pill respects `me.assigned_branches`; shift pill only renders if `role` ∈ retail roles
- **Quirks:**
  1. `getCurrentShift()` reads from `sessionStorage` — meaning shift state is **per-tab**, not per-user. Two tabs → two different "current shift" reads. Production must make this server-truth.
  2. `BranchPopover` shows the first 7 branches only — not paginated. Tenants with > 7 branches see truncation. Production needs a search input.
  3. The "register pill" in retail mode is currently a static label — not click-actionable. Plan: opens a register-picker popover (close current shift, open a new one on a different register).

### 3.3 `CommandPalette` (⌘K)

> Spotlight-style overlay. Matches against `DALSEEN_NAV` items by label (en + ar) and id; arrow keys to nav, Enter to go.

- **File:** `shell-polish.jsx` (`CommandPalette` lines ~543–717)
- **Mounts at:** **no route** — toggled by `⌘K` / `Ctrl+K` from anywhere; rendered as a fixed-position overlay
- **Owns:** query string, selected index, ranked list memo
- **Composes:** `DALSEEN_NAV`, filtered by `canSee(role, id)` and `activeSystem`
- **States:** closed (default) · open · empty result
- **Actions:** keystroke filtering · `Enter` → `onNav(id)` and close · `Esc` → close
- **Data:** FE only — no backend call
- **Permissions:** matches sidebar gating (`canSee`)
- **Quirks:** No fuzzy match (substring only). No history of recent commands. No shortcuts list page.

### 3.4 `MyAccountScreen` (`core.account`)

> User's own profile — name, email, phone, role, last login, password / 2FA management, language preference. Read against `DALSEEN_USERS` matched by id.

- **File:** `shell-polish.jsx` (`MyAccountScreen` lines ~1328–1470)
- **Mounts at:** `core.account`
- **Owns:** local edit state; pending-save indicator
- **Composes:** `DALSEEN_USERS` lookup, `DALSEEN_TOKENS`, `Icon`
- **States:** view · edit · saving (200ms simulated) · saved (toast)
- **Actions:** `Edit profile`, `Change password`, `Manage 2FA`, `Switch language`, `Sign out everywhere`
- **Data today:** local mutation of `DALSEEN_USERS` entry
- **Data at cut-over:** `GET /me`, `PATCH /me`, `POST /me/password`, `POST /me/2fa/{enable,disable}`, `POST /me/sessions/revoke-all`
- **Permissions:** user can edit own profile; admin/owner can edit anyone's via the Users screen instead
- **Quirks:** "Sign out everywhere" is a no-op today — production must invalidate every active session token.

### 3.5 `MySecurityScreen` (`core.security`)

> Active sessions list, trusted devices, recovery codes, 2FA setup status.

- **File:** `shell-polish.jsx` (`MySecurityScreen` lines ~1471–1585)
- **Mounts at:** `core.security`
- **Owns:** sessions table state, recovery-codes reveal toggle
- **Composes:** `DALSEEN_USERS` (last login fields), inline session fixture (3–5 demo rows)
- **States:** view · loading (when revoking) · empty (no other sessions)
- **Actions:** `Revoke session(s)`, `Revoke all`, `Regenerate recovery codes`, `Export codes`
- **Data today:** inline fixture
- **Data at cut-over:** `GET /me/sessions`, `POST /me/sessions/{id}/revoke`, `POST /me/recovery-codes/regenerate`. The `recovery-codes` endpoint is **currently missing** from the OpenAPI spec — see §6.3.
- **Permissions:** self-only
- **Quirks:** sessions fixture is hard-coded inside the component; not extracted to a window global. Trivial to wire to `useApi(API.me.sessions.list)` once the live wrapper exists.

### 3.6 `DashboardSplash`

> First-render hero card. Shows once after onboarding completes (`sessionStorage` flag prevents replay). Tile per system (Retail / Pay / Dine / HR) with "Go to <system>" CTA.

- **File:** `dashboard-splash.compiled.js`
- **Mounts at:** **no route** — overlays the `dashboard` route on first render after onboarding
- **Owns:** `sessionStorage:dalseen:splash-seen` flag · system tile hover state
- **Composes:** `DALSEEN_SYSTEMS`, `DALSEEN_TOKENS`, `Icon`
- **States:** visible (first time) · dismissed (forever, until session reset)
- **Actions:** Click any tile → `onSystem(id)`; click "Skip" → mark seen, mount real dashboard
- **Data:** none (chrome only)
- **Permissions:** none — runs for any newly-onboarded role
- **Quirks:** The dismiss flag is **session-scoped**. Closing all tabs replays it — fine for a demo, may surprise users in production. Move to `localStorage` (or backend `me.preferences.splash_dismissed`) before pilot.

---

## 4 · Stand-alone screens

### 4.1 `dashboard` · DashboardScreen

> Cross-module command center. 5 tabs:
> - **Today** — system tiles (revenue, txns, avg ticket per system) + quick actions
> - **Live** — real-time tick (synthesised every 5s) — current shift POS feed, kitchen tickets, payment flow
> - **Act** — outstanding tasks: low stock, pending JE approvals, payroll cut-off, stuck onboarding
> - **Close** — end-of-day checklist; persists per-day in `localStorage`
> - **Roll-up** — 7d / 30d / 90d KPIs with sparklines

- **File:** `dashboard.compiled.js`
- **Mounts at:** `dashboard`
- **Owns:** active tab; live tick interval (5s); close-checklist state per ISO date; range selector
- **Composes:** `DALSEEN_DATA` (synthesised), `DALSEEN_SYSTEMS`, `Icon`, internal atoms `Card`, `SectionTitle`, `Pill`, `MiniStat`, `IconBtn`, `SysCard`
- **States:** loading (200ms simulated on tab switch), live-paused (when tab is hidden via `document.visibilityState`)
- **Actions:** `goSys(sys)` → switch active system; click any KPI → drill into the source screen
- **Data today:** `DALSEEN_DATA` (random walk based on `Date.now()` seed)
- **Data at cut-over:** Five separate aggregator endpoints (one per tab):
  - `GET /dashboard/today?branchId=…` — system summary cards
  - `GET /dashboard/live?branchId=…&since=…` — long-poll or SSE for tick
  - `GET /dashboard/act?role=…` — filtered by what the active role can act on
  - `GET /dashboard/close?date=…` — checklist items + completion state
  - `GET /dashboard/rollup?range={7d|30d|90d}` — KPI series

  These aggregators are **all currently missing** from the OpenAPI spec — see §6.3. Today the FE composes them client-side from per-resource endpoints; at cut-over the backend should provide unified aggregates so dashboards stay performant when tenants get to 50+ branches.
- **Permissions:** every role sees the dashboard, but each tab's content is filtered by `canSee` of the underlying nav id. Cashier role sees only their own shift's data.
- **Quirks:**
  1. The "live tick" tabs `setInterval` even when the tab is hidden — wastes CPU. Wrap in `document.visibilitychange`.
  2. Close-checklist state is **per-browser**, not per-shift. Two cashiers on two devices each see their own checklist. Production should anchor to the active shift id.
  3. The `Roll-up` tab synthesises sparklines deterministically from `Date.now()` seed — meaning every refresh gives the same shape. Replace with real series at cut-over.

### 4.2 `retail.users` · UsersScreen

> Users & access list. Filterable by role/branch/status. Inline avatar, role chip, branch chips, last-active relative time, 2FA badge. Header CTA: Invite user.

- **File:** `auth.compiled.js` (`UsersScreen` lines ~366–688)
- **Mounts at:** `retail.users`
- **Owns:** filter state (role, branch, status), search query, sort column, modal stack (`InviteModal`, `EditUserModal`, suspend confirm, delete confirm)
- **Composes:** `DALSEEN_USERS`, `DALSEEN_ROLES`, `DALSEEN_DATA.branches`, `Icon`, `EmptyState`
- **States:** populated (default; 13 seeded) · filtered-empty · loading (after invite/edit, 300ms simulated) · error (none surfaced today)
- **Actions:** `Invite user`, `Edit user`, `Suspend`, `Resume`, `Resend invite`, `Revoke invite`, `Reset password`, `Toggle 2FA`, `Change role`, `Re-assign branches`
- **Data today:** `DALSEEN_USERS` (mutated in-place; alias fields auto-rebuilt by the IIFE at top of `auth.compiled.js`)
- **Data at cut-over:** `GET /users` (with filter params), `POST /users/invite`, `PATCH /users/{id}`, `POST /users/{id}/suspend`, `POST /users/{id}/resume`, `POST /users/{id}/resend-invite`, `POST /users/{id}/reset-password`. Per `routes-api.php` lines covering `/users` group.
- **Permissions:** `users.view`, `users.invite`, `users.edit`, `users.suspend`, `users.delete`. Owner unrestricted; manager scoped to own branch; cashier blind.
- **Quirks:**
  1. The IIFE that builds `assignedBranches`, `assignedWarehouses`, `lastActive`, `invited` aliases runs **once at boot**. If you mutate `DALSEEN_USERS` later (e.g. add a user), the new user is missing those fields — the screen handles it but the retail-side users screen does not. Centralise into `_normalize(u)` and call on every add.
  2. Branch IDs in `DALSEEN_USERS.branches` (`b1`–`b7`) and in retail's `assignedBranches` (`BR-OLAYA`, `BR-HQ`, …) are **two different ID systems** with a hard-coded translation map (`BRANCH_TO_RETAIL` at top of `auth.compiled.js`). Production must reconcile to one canonical id.
  3. "Suspend" sets `status: 'suspended'` and adds `suspendedAt` + `suspendReason` — but the `LoginScreen` only checks `status === 'suspended'`. If a separate `disabled` status is needed, add the gate there too.
  4. "Resend invite" silently re-mutates `invitedAt` / `invitedExp`. No email actually sends in demo. Wire the real notification at cut-over.

### 4.3 `retail.roles` · RolesHubV2

> Two-pane role editor. Left: role list (12 roles, draft/published chips, user count). Right: tabbed editor:
> - **JD** — bilingual job description, responsibilities, reporting line
> - **Permissions** — tree by system → screen → action (view/edit/approve/export/delete)
> - **Policies** — biometric required, biometric reason, shift biometric, high-value threshold (SAR), session timeout, IP allow-list

- **File:** `roles-editor.compiled.js`
- **Mounts at:** `retail.roles`
- **Owns:** selected role id, active tab (`jd | permissions | policies`), `DALSEEN_ROLE_POLICY` mutations, draft/published version state, clone/assign dialogs
- **Composes:** `DALSEEN_ROLES`, `DALSEEN_NAV` (groups by system for permissions tree), `DALSEEN_PERM_ACTIONS`, `DALSEEN_USERS` (for user-count chip)
- **States:** view (read-only published role) · edit-draft · saving · published (locked) · cloning (modal)
- **Actions:** `Edit policy`, `Save draft`, `Publish`, `Clone role`, `Assign users`, `View change history`
- **Data today:** `DALSEEN_ROLE_POLICY` (seeded once via `seedRolePolicyStore()` from `DALSEEN_ROLE_POLICIES`)
- **Data at cut-over:** `GET /roles`, `GET /roles/{id}`, `PATCH /roles/{id}` (policy + JD + version bump), `POST /roles/{id}/publish`, `POST /roles/{id}/clone`, `GET /roles/{id}/users`, `GET /roles/{id}/history`. The `roles.*` namespace is documented in `routes-api.php`; permission tree resource (`/permissions`) returns the canonical permission catalog.
- **Permissions:** `roles.view`, `roles.edit`, `roles.publish`, `roles.assign`. Only `owner` and `superadmin` by default.
- **Quirks:**
  1. The seed factory copies `DALSEEN_ROLE_POLICIES` (the simpler, flat one in `roles.js`) into a richer tree. The simpler one is **still read** by `BiometricPrompt` via `DALSEEN_getRolePolicy()` — *which is overridden* by `roles-editor.compiled.js` to read from the editor's tree. So changes in the editor *do* flow into the biometric gate, but only after `roles-editor.compiled.js` has loaded. Boot-order race: if the editor file is missing/late, the biometric gate falls back to the v1 reader and silently uses stale thresholds.
  2. Permission tree builds from `DALSEEN_NAV` — meaning a new screen is auto-listed. But the permission *codes* it generates (`<system>.<id>.<action>`) must also exist in the backend's permission catalog — kept in sync manually today.
  3. Policy version + draft state is FE-only. No backend audit trail. Wire to `roles.history` at cut-over so you can see who changed what when.
  4. "Assign users" preview mode shows a delta but doesn't write — wire to `POST /roles/{id}/users` with the diff.

### 4.4 `admin.audit` · AccessAuditScreen

> Auth + permission events table. Filter by actor, event type, severity, date range. Severity badges: ok / warn / err.

- **File:** `auth.compiled.js` (`AccessAuditScreen` lines ~994–1067)
- **Mounts at:** `admin.audit` (when present in nav — gated to owner / auditor / superadmin)
- **Owns:** filter state, sort
- **Composes:** `DALSEEN_ACCESS_AUDIT` (15 seeded events), `DALSEEN_USERS` (actor name lookup)
- **States:** populated · filtered-empty · loading (none — pure client-side)
- **Actions:** `Export CSV` (no-op today), `View event detail` (modal — also stub)
- **Data today:** `DALSEEN_ACCESS_AUDIT`
- **Data at cut-over:** `GET /audit?type=access&since=…&until=…&actor=…&severity=…`. Audit log endpoint family covered in `routes-api.php`. Production should paginate (default 100/page) and stream-export rather than fetch all.
- **Permissions:** `audit.view` (owner, auditor, superadmin only)
- **Quirks:**
  1. The 15 seed events include 2 hard-coded "21:12–21:14 attack-from-185.220" rows. Demo color — strip before pilot.
  2. Times are stored as `HH:MM` + `DD MMM YYYY` strings, not ISO. Sorts lexicographically, not chronologically. Move to ISO at cut-over.
  3. "Export CSV" button is a placeholder.

### 4.5 `shared.marketplace` · AppMarketplaceScreen

> App Store. Hero with search + featured app spotlight, curated collections (3 cards), featured row (3 large cards), category bar (~10 categories), app grid (~40 apps), developer CTA, slide-in detail drawer.

- **File:** `app-marketplace-screen.jsx`
- **Mounts at:** `shared.marketplace`
- **Owns:** search query, active category, install set (`localStorage:dalseen:installed-apps`), drawer-open app id, hover state per card
- **Composes:** `AppMarketplaceCatalog.CATALOG`, `AppMarketplaceCatalog.CATEGORIES`, `Icon`, `DALSEEN_TOKENS`. Internal atoms: `Hero`, `HeroSpotlight`, `CollectionsRow`, `CollectionCard`, `FeaturedRow`, `FeaturedCard`, `CategoryBar`, `AppGrid`, `AppCard`, `AppDrawer`, `LogoTile`, `VerifiedBadge`, `InstallButton`, `DeveloperCTA`
- **States:** browse (default) · drawer-open · empty-search · empty-category
- **Actions:** `Install`, `Uninstall`, `Open` (jumps to the route the app contributes), `Filter by category`, `Search`, `Open detail drawer`
- **Data today:** `CATALOG` is an inline 40-app fixture inside `app-marketplace-screen.jsx`. Install state in `localStorage`.
- **Data at cut-over:** `GET /apps?cat=&q=`, `GET /apps/{id}`, `POST /apps/{id}/install`, `POST /apps/{id}/uninstall`. App marketplace endpoints are **currently missing** from the OpenAPI spec — see §6.3.
- **Permissions:** browse — anyone. Install/uninstall — `apps.install` (owner, manager).
- **Quirks:**
  1. Apps that are "installed" don't actually contribute new nav rows today — install is a state flag only. Production must also re-fetch `DALSEEN_NAV` (or backend equivalent) after install so the new screens appear.
  2. The `bulletsFor()`, `permissionsFor()`, `infoRowsFor()` helpers generate copy from the app's category — **not from real catalog data**. At cut-over the backend should ship these as fields per app.
  3. Editorial headlines for the hero are hard-coded for 5 named apps (`woocommerce`, `shopify`, etc.) and fall through to a generic for the rest. Externalise.
  4. The "Verified" badge is purely visual — no signed manifest backing it. If the marketplace is opened to third parties, this needs a real verification flow.

### 4.6 `shared.integrations` · DalseenIntegrationsScreen

> The control surface for the integrations gate. Three sections:
> - **Books mode** — Built-in / External / Off (with provider picker when External: QuickBooks, Xero, Zoho)
> - **Stock mode** — Deep / Off, then sub-mode: Zero-friction / Periodic / Both
> - **Per-branch overrides** — table of branches with optional per-branch `acct` / `stock` / `mode` overrides

- **File:** `dalseen-integrations-screen.jsx`
- **Mounts at:** `shared.integrations`
- **Owns:** local form state, save / dirty flag
- **Composes:** `DalseenIntegrations` (the singleton), `DALSEEN_DATA.branches`, `Icon`
- **States:** view (default) · editing · saving · saved (toast)
- **Actions:** `setGlobal({ acct, stock, stockMode, externalProvider })`, `setBranch(branchId, patch)`, `clearBranch(branchId)`
- **Data today:** writes through `DalseenIntegrations.setGlobal()` / `.setBranch()` / `.clearBranch()` → mirrored onto `AcctStore.state.settings.dalseenIntegrations` so it persists with the rest of accounting settings (and saved via `AcctStore.save()`)
- **Data at cut-over:** `GET /tenants/me/integrations`, `PATCH /tenants/me/integrations`, `PATCH /tenants/me/integrations/branches/{branchId}`, `DELETE /tenants/me/integrations/branches/{branchId}`. Integrations endpoint family is **currently missing** from the OpenAPI spec — see §6.3.
- **Permissions:** `tenant.settings.edit` — owner only.
- **Quirks:**
  1. The `DalseenIntegrations` singleton's subscriber bus broadcasts changes — every screen that reads `shouldPostGL()` re-renders on flip. Be aware that flipping books mode mid-day affects every in-flight transaction; production should require an end-of-day boundary.
  2. `externalProvider` accepts any of `quickbooks | xero | zoho` but the actual outbound integration is **not implemented** today. Wiring those is a separate work-stream.
  3. Per-branch overrides are stored as a flat map; if a branch is deleted, the override row is **not** auto-cleaned. `clearBranch(branchId)` must be called explicitly.

### 4.7 Cross-system shared modules

Four screens that exist *between* systems — accessed from the Core sidebar group:

#### 4.7.1 `shared.ai` · SharedAI

> Cross-system AI assistant. Chat thread (left) + suggested actions (right). Each suggested action is a navigation jump.

- **File:** `shared-modules.compiled.js` (`SharedAI` lines ~46–119)
- **Mounts at:** `shared.ai`
- **Owns:** thread state, input draft, suggested-actions list (filtered by active context)
- **Composes:** `Page`, `Header`, `Pill`, `Btn`, `TableShell` (internal atoms)
- **States:** empty thread (default) · streaming (typing dots) · error
- **Actions:** `send(prompt)`, `apply(action)` → `onNav(action.target)`
- **Data today:** Chat is **client-only** — uses `window.claude.complete()` if available, otherwise a deterministic mock response. Suggested actions are hard-coded based on active screen.
- **Data at cut-over:** `POST /ai/chat { messages, context }` (streaming), `POST /ai/suggested-actions { context }`. AI endpoints are **product-side TBD** — see `INTEGRATION-NOTES.md §AI`.
- **Permissions:** `ai.use` (everyone except `superadmin` since they bypass tenant context)
- **Quirks:** suggested actions can navigate to screens the user can't see (`canSee` is not consulted). Filter at render.

#### 4.7.2 `shared.bi` · SharedBI

> BI dashboards index. List of preset dashboards (Exec, Finance, Operations, Retail, HR), each opens a separate iframe / embed.

- **File:** `shared-modules.compiled.js` (`SharedBI` lines ~124–204)
- **Mounts at:** `shared.bi`
- **Owns:** active dashboard id
- **Composes:** internal atoms
- **States:** index (default) · embed-loading · embed-loaded · embed-error
- **Actions:** `openDashboard(id)`
- **Data today:** static list of 5 dashboards with stub embed URLs
- **Data at cut-over:** `GET /bi/dashboards` (list), embed URL signed per-tenant per-user. BI endpoints **TBD with the analytics team**.
- **Permissions:** `bi.view`
- **Quirks:** No drill-through today — dashboards are read-only embeds.

#### 4.7.3 `shared.crm` · SharedCRM

> Cross-system CRM index. Customer segments (All / Active / At-risk / VIP), customer table.

- **File:** `shared-modules.compiled.js` (`SharedCRM` lines ~220–280)
- **Mounts at:** `shared.crm`
- **Owns:** active segment, search
- **Composes:** internal atoms; reads inline `CUSTOMERS_SHARED` array
- **States:** populated · filtered-empty
- **Actions:** `select(customer)` → opens detail
- **Data today:** inline array (5 demo customers)
- **Data at cut-over:** `GET /crm/customers` (the same as `crm.customers.list` from the Retail/Pay namespaces — this screen is a *cross-system view*, not a different resource)
- **Permissions:** `crm.view`
- **Quirks:** duplicates the Retail customers screen. Plan: deprecate this once Retail's customer screen is wired and add a "view across all systems" toggle there instead.

#### 4.7.4 `shared.manufacturing` · SharedManufacturing

> Manufacturing module — production runs, BOMs, work orders.

- **File:** `shared-modules.compiled.js` (`SharedManufacturing` lines ~294–364)
- **Mounts at:** `shared.manufacturing`
- **Owns:** active tab (Runs / BOMs / Work orders)
- **Composes:** internal atoms; inline `MANUFACTURING_*` arrays
- **States:** populated only — no empty state today
- **Actions:** `start run`, `complete run` (no-ops)
- **Data today:** inline arrays
- **Data at cut-over:** `GET /manufacturing/runs`, `POST /manufacturing/runs`, `GET /manufacturing/boms`. **Currently missing** from the OpenAPI spec — see §6.3.
- **Permissions:** `manufacturing.view`
- **Quirks:** This screen is a **demonstrative scaffold** — "manufacturing" is on the roadmap as a separate module. Treat as a stub.

### 4.8 `shared.zatca` · ZatcaScreen

> ZATCA Phase II live clearance center. Real-time invoice → e-invoice JSON → ZATCA endpoint → cleared / rejected status feed.

- **File:** `shared.compiled.js` (`ZatcaScreen` lines ~210–419)
- **Mounts at:** `shared.zatca`
- **Owns:** filter (date range, status), selected row id
- **Composes:** internal atoms · `fmtSAR` · simulated event stream
- **States:** populated (default) · streaming (new rows tick in) · empty
- **Actions:** `Retry submission`, `View XML`, `Export batch`
- **Data today:** synthesised from `DALSEEN_DATA` invoices with random-walk clearance times
- **Data at cut-over:** `GET /zatca/submissions`, `POST /zatca/submissions/{id}/retry`, `GET /zatca/submissions/{id}/xml`. The ZATCA submission ledger is in the API mock (`api-namespace.js`'s in-process `__vatSubmissions`); the real backend has the same surface per `BACKEND-MAPPING.md §ZATCA`.
- **Permissions:** `zatca.view` (anyone), `zatca.retry` (accountant, owner)
- **Quirks:**
  1. The simulated stream produces a new row every ~3s. Production should switch to SSE / WebSocket.
  2. The `clr` (clearance time) field is shown as `Xs` — meaning seconds. ZATCA itself returns ISO timestamps; convert.

### 4.9 `DesignedModule` (fallback stub)

> Generic designed module — a fully-styled "this screen is coming" stub used when a `DALSEEN_NAV` entry doesn't yet have a real component. Has real structure (header, pattern preview, "wire to your API endpoints" CTA).

- **File:** `shared.compiled.js` (`DesignedModule` lines ~36–207)
- **Mounts at:** **fallback** — used by `app-main.compiled.js` when the active route's component isn't found in `window.<Component>`
- **Owns:** nothing — pure presentation
- **Composes:** internal atoms
- **States:** static
- **Actions:** none (CTA is decorative)
- **Data:** none
- **Permissions:** inherited from the route
- **Quirks:** This is **the ship-on-empty escape hatch** — better than a blank screen for missing routes. Don't remove until every nav id has a component.

---

## 5 · Modals & overlays

### 5.1 `BiometricPrompt`

> WebAuthn platform-authenticator gate. Shown before any high-risk action (refund, payroll, role change, …).

- **File:** `biometric-shift.compiled.js` (`BiometricPrompt` lines ~31–228)
- **Mounts at:** **invoked from anywhere** — consumers call `<BiometricPrompt purpose="..." onSuccess={…} onCancel={…} onFallback={…} />`
- **Owns:** WebAuthn challenge state, fallback PIN entry state, attempt counter
- **Composes:** `useT`, `Icon`, `hasBiometricCapability()` probe
- **States:** probing-capability · ready · prompting · success · cancelled · failed · fallback-pin
- **Actions:** trigger `navigator.credentials.get({ publicKey, userVerification:'required' })`. On `NotAllowedError` or unsupported → fallback to 6-digit PIN.
- **Data today:** none — calls real `PublicKeyCredential` API where supported (most modern browsers); fallback PIN accepts anything
- **Data at cut-over:** `POST /auth/webauthn/challenge` → server returns challenge; `POST /auth/webauthn/verify` → server signs off and grants step-up token. PIN fallback: `POST /auth/pin/verify`. WebAuthn endpoints **must be added** to the OpenAPI spec — currently absent.
- **Permissions:** invoked by the gated action's permission check
- **Quirks:**
  1. `onSuccess` only fires after the credential resolves — production must also include the **step-up token** the prompt grants, so the gated action knows it has fresh auth. Today there is no token plumbing.
  2. The PIN fallback accepts any 6 digits in demo. Production must verify against `me.pin_hash`.
  3. iOS Safari needs a user gesture to invoke `navigator.credentials.get` — the modal handles this with a "Authenticate" button rather than auto-trigger. Keep this UX at cut-over.

### 5.2 `StartShiftModal`

> Cashier opens a register. Required fields: terminal pick, opening float (cash count by denomination), biometric check.

- **File:** `biometric-shift.compiled.js` (`StartShiftModal` lines ~229–516)
- **Mounts at:** **invoked from** `ShiftLauncher` component, retail POS pre-flight, or programmatically via `requireShift()`
- **Owns:** terminal selection, denomination breakdown ({500: 0, 200: 0, 100: 0, 50: 0, 20: 0, 10: 0, 5: 0, 1: 0}), computed total, biometric step state
- **Composes:** `BiometricPrompt`, `DALSEEN_DATA.branches` (terminal list), `fmtSAR`
- **States:** `pick-terminal` → `count-float` → `biometric` → `submitting` → `done` (closes + posts `dalseen:shift-opened` event) · `error` (any step)
- **Actions:** open shift via `POST /shifts/open { branchId, terminalId, openingFloat, biometricToken }`
- **Data today:** writes to `sessionStorage:dalseen:currentShift` so `getCurrentShift()` returns it everywhere
- **Data at cut-over:** `POST /shifts/open` per `routes-api.php` shifts group — returns `{ shiftId, openedAt, expectedFloat }`
- **Permissions:** retail roles (cashier, manager, owner) only
- **Quirks:**
  1. **Per-tab session storage** is the wrong place for active shift. Two tabs = two shifts opened. Production must enforce single-active-shift per (user, branch, register) server-side and reject the second open.
  2. The denomination breakdown rounds to integers. Production must allow halalas (decimals) to match real cash drawers.
  3. `biometricToken` plumbing TBD — see Quirk 1 of `BiometricPrompt`.

### 5.3 `ShiftLauncher`

> Floating action that appears when a retail user has no active shift. Opens `StartShiftModal`.

- **File:** `biometric-shift.compiled.js` (`ShiftLauncher` lines ~517–611)
- **Mounts at:** rendered globally; visible only when `getCurrentShift() == null` AND `role` ∈ retail roles
- **Owns:** modal-open state
- **Composes:** `StartShiftModal`
- **States:** hidden (default) · pulsing (when first nav into a retail screen) · modal-open
- **Actions:** `Open shift` → mounts `<StartShiftModal>`
- **Data:** none
- **Permissions:** retail roles only
- **Quirks:** Listens for `window 'dalseen:open-shift'` event so `requireShift()` from anywhere can pop the launcher.

### 5.4 `PayModal`

> SoftPOS (Tap-on-Phone) payment flow. Used when collecting payment from any screen — Retail POS settle, Dine ticket close, Pay request.

- **File:** `modals.compiled.js` (`PayModal` lines ~8–290)
- **Mounts at:** **invoked from anywhere** — `<PayModal amount={…} onSuccess={…} onCancel={…} />`
- **Owns:** payment-method state (Tap / Cash / mada Hayyak / STCPay / Transfer), tap-animation step, processor result
- **Composes:** `Icon`, `fmtSAR`
- **States:** method-pick · waiting-tap · tap-detected · processing → success / declined / cancelled
- **Actions:** `pay()` → simulated 1.2s → 95% success
- **Data today:** all client-side
- **Data at cut-over:** `POST /payments/charge { amount, method, reference, idempotencyKey }`. Per `BACKEND-MAPPING.md §Pay`. mada / STCPay / SoftPOS each have their own provider integration (Geidea, etc.).
- **Permissions:** any role with an open shift
- **Quirks:**
  1. The "Tap on Phone" animation is decorative — NFC reader is not actually opened. SoftPOS in production needs the device's payment terminal SDK.
  2. Footer reads "SAMA certified · PCI DSS 4.0" — informational only.
  3. Idempotency-Key is **not generated** today. Production must — to prevent double-charge on retry.

### 5.5 `InviteModal` & `EditUserModal`

> The two write surfaces for `UsersScreen`.

- **File:** `auth.compiled.js` (`InviteModal` ~689, `EditUserModal` ~801)
- **Mounts at:** invoked from `UsersScreen`
- **Owns:** form state, validation errors, submit lock
- **Composes:** `DALSEEN_ROLES`, `DALSEEN_DATA.branches`, `Icon`
- **States:** editing · validating · submitting · success (closes) · error (inline)
- **Actions:** `Invite` → push to `DALSEEN_USERS` with `status:'invited'`. `Save` → mutate `DALSEEN_USERS` entry.
- **Data at cut-over:** `POST /users/invite`, `PATCH /users/{id}` (see §4.2)
- **Permissions:** `users.invite`, `users.edit`
- **Quirks:** `EditUserModal` writes back the v1 fields (`branches`, `email`) and re-derives the v2 aliases (`assignedBranches`, `lastActive`) on next render. The IIFE that does this only runs at boot — see Quirk 1 of `UsersScreen`.

### 5.6 `CloneDialog` & `AssignPreview`

> Two small dialogs in the Roles editor.

- **File:** `roles-editor.compiled.js`
- **Mounts at:** invoked from `RolesHubV2`
- **Owns:** name field (clone) or diff view (assign preview)
- **Data at cut-over:** `POST /roles/{id}/clone`, `POST /roles/{id}/users` (see §4.3)
- **Permissions:** `roles.edit` / `roles.assign`
- **Quirks:** "Assign users preview" doesn't actually write today — see Quirk 4 of §4.3.

### 5.7 `AICopilot`

> Right-side dock — AI assistant with current-screen context. Different from `SharedAI` (which is a full-screen surface); this one slides in from the right.

- **File:** `modals.compiled.js` (`AICopilot` lines ~528–706)
- **Mounts at:** **invoked from** `Topbar` "Ask AI" pill, or programmatically via `window.openAI()` (TBD)
- **Owns:** dock open/closed, thread state, context capture (`location.hash` + active screen)
- **Composes:** `Icon`, `useT`, `window.claude.complete()` if available
- **States:** closed · open · streaming · error
- **Actions:** `send`, `apply suggested action`, `pin to dashboard` (no-op today)
- **Data:** see §4.7.1 — same backend at cut-over (`POST /ai/chat`)
- **Permissions:** `ai.use`
- **Quirks:** Two separate AI surfaces (`AICopilot` dock vs `SharedAI` screen) is intentional today — the dock follows you, the screen is for deep work. Keep both at cut-over but consolidate the API client.

### 5.8 `TweaksPanel`

> Bottom-right floating panel — design-system tweak controls (font swap, density, theme override). For demo purposes only.

- **File:** `modals.compiled.js` (`TweaksPanel` lines ~707–828)
- **Mounts at:** invoked from Topbar "Tweaks" toggle
- **Owns:** active tweak values, persistence to `localStorage:dalseen:tweaks`
- **Data:** none
- **Permissions:** any
- **Quirks:** **Pre-pilot only.** Strip from production builds — design tweaks shouldn't be customer-visible.

### 5.9 `OnboardingAdminsStep`

> Tiny card slot inside the onboarding wizard's "Admins invited" step.

- **File:** `auth.compiled.js` (`OnboardingAdminsStep` lines ~1071–1119)
- **Mounts at:** invoked from `front/owner/owner-onboarding.*` (see `SCREENS-INVENTORY-Owner+HR.md §3`)
- **Owns:** invitee list, add-row state
- **Composes:** `InviteModal`
- **Data at cut-over:** `POST /users/invite` (same as §4.2) but with `setup_token` so backend knows which tenant onboarding it ties to
- **Permissions:** `tenant.setup` (the just-created owner during onboarding)
- **Quirks:** Bridges the onboarding wizard and the user-management screen — same backend resource, two different surfaces.

---

## 6 · Cross-cutting concerns

### 6.1 `DalseenIntegrations` central gate

> The single source of truth for "should I post a JE? should I decrement stock? what's the books mode for this branch?"

Documented fully in `INTEGRATION-NOTES.md §Integrations gate`. Quick reference:

```js
window.DalseenIntegrations.shouldPostGL(branchId)         // → boolean
window.DalseenIntegrations.shouldDecrementStock(branchId) // → boolean
window.DalseenIntegrations.shouldPostCOGS(branchId)       // = shouldPostGL && shouldDecrementStock
window.DalseenIntegrations.acctModeFor(branchId)          // → 'built-in'|'external'|'off'
window.DalseenIntegrations.stockModeFor(branchId)         // → 'zf'|'pi'|'both'|'off'
window.DalseenIntegrations.summary(branchId, lang)        // → { acct, stock, acctValue, stockValue, modeValue }
window.DalseenIntegrations.onChange(fn)                   // subscribe; returns unsubscribe
```

State persists onto `AcctStore.state.settings.dalseenIntegrations` so it round-trips with tenant settings. UI is `DalseenIntegrationsScreen` (§4.6).

**Cut-over implication:** every retail/dine/pay flow that posts a JE or decrements stock gates on this. When wiring real APIs:
- `shouldPostGL=true` + `acct=built-in` → call `POST /accounting/journal-entries`
- `shouldPostGL=true` + `acct=external` → push to outbound queue (QuickBooks/Xero/Zoho adapter)
- `shouldPostGL=false` → skip GL entirely; still mirror to event log
- `shouldDecrementStock=false` → skip `POST /inventory/adjustments`
- `shouldPostCOGS=false` → skip the COGS leg of the JE

The gate's read API is **already in production-final shape** — it does not need cut-over wiring. Only the screen that flips it does (see §4.6).

### 6.2 `DALSEEN_DEVICES` peripheral registry

> Source of truth for what hardware is plugged in at each register.

```js
window.DALSEEN_DEVICES = {
  list: [ /* 9 default devices */ ],
  get(id),
  selfTest(id?),  // returns { id, ok }[]; broadcasts dalseen:device-changed
  register(adapter),
}
```

The 9 defaults: fingerprint reader (SecuGen Hamster Pro 20), receipt printer (Star Micronics TSP143), cash drawer (APG Vasario 1616), barcode scanner (Honeywell Voyager 1450g), customer display, mada terminal (Geidea A920 SoftPOS), RFID/NFC badge reader (ACR1252U), face-verify camera (Logitech C920), smart safe (Tidel Series 4e).

The fingerprint device's `test()` actually probes `PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()` — meaning self-test on a real machine reflects real platform-authenticator availability. Other devices return synthesised pass/fail.

Consumers:
- Retail Devices screen (per-register pre-flight)
- Retail POS — checks `printer-01.status` before opening shift
- HR badge punch — `badge-01` adapter
- Pay smart-safe deposit — `safe-01` adapter
- Pay terminal pre-flight — `terminal-01`

**Cut-over implication:** these adapters are **client-side device drivers**. The backend doesn't manage them — it just receives the events they emit (e.g. `POST /pos/badge-punch { rfid }`, `POST /pos/safe-deposit { amount, count }`). What *is* missing today is a backend **device-registry endpoint** that records which devices are *paired with* which register — without it, a register going offline is invisible to ops. Add `GET /branches/{id}/registers/{rid}/devices` and `POST /devices/heartbeat` at cut-over.

### 6.3 Backend gaps the Common module surfaces

This is the single most important section for cut-over planning. Common reveals **6 distinct surfaces with no backend match yet**, ordered by severity:

| Severity | Surface | Endpoint(s) needed | Workaround today |
|----------|---------|--------------------|------------------|
| 🔴 high | **WebAuthn step-up** (`BiometricPrompt`) | `POST /auth/webauthn/challenge`, `POST /auth/webauthn/verify`, `POST /auth/pin/verify` | Demo accepts any 6 digits — production cannot ship without these |
| 🔴 high | **Dashboard aggregators** (5 endpoints) | `GET /dashboard/{today,live,act,close,rollup}` | FE composes from per-resource queries — won't scale past 50 branches |
| 🟠 medium | **App marketplace** | `GET /apps`, `POST /apps/{id}/{install,uninstall}` | Inline 40-app fixture; install flag in localStorage |
| 🟠 medium | **Integrations gate persistence** | `GET /tenants/me/integrations`, `PATCH /tenants/me/integrations`, `PATCH /tenants/me/integrations/branches/{id}` | Mirrored onto `AcctStore.state.settings`, persists with tenant — *acceptable for pilot* if AcctStore is wired |
| 🟠 medium | **Recovery codes** (`MySecurityScreen`) | `GET /me/recovery-codes`, `POST /me/recovery-codes/regenerate` | Not generatable today |
| 🟡 low | **Manufacturing module** | `GET /manufacturing/{runs,boms,workorders}` | Stub screen — explicit "coming soon" UX is acceptable |
| 🟡 low | **Device registry** (per §6.2) | `GET /branches/{id}/registers/{rid}/devices`, `POST /devices/heartbeat` | Devices are local-only; ops can't see offline registers |

> **Recommendation.** Block pilot on the two 🔴 items. The 🟠 items can ship behind a feature flag (App Marketplace and Manufacturing are optional surfaces; integrations gate persists via AcctStore as a stop-gap). The 🟡 device-registry can be deferred to v1.1 if pilot tenants are < 5 branches.

### 6.4 Boot order (Common's contribution)

Common files load in this exact order (from `index.html`):

```
1. tokens.js
2. roles.js
3. strings.js
4. icons.compiled.js
5. loading.js                   (Skeleton, Spinner, EmptyState, ErrorState, AsyncBoundary)
6. devices.js                   (DALSEEN_DEVICES, capability probe)
7. dalseen-integrations.js      (gate singleton)
8. shell.compiled.js            (v1 Sidebar, Topbar, useT, systemColor)
9. shell-polish.jsx             (v2 — overrides Sidebar + Topbar)
10. icons.compiled.js (already loaded)
11. dashboard-splash.compiled.js
12. dashboard.compiled.js
13. auth.compiled.js            (LoginScreen, UsersScreen, RolesScreen v1, AccessAuditScreen)
14. roles-editor.compiled.js    (RolesHubV2 — overrides RolesScreen and DALSEEN_getRolePolicy)
15. biometric-shift.compiled.js
16. modals.compiled.js          (PayModal, AICopilot, TweaksPanel, TailoringScreen)
17. shared.compiled.js          (DesignedModule, ZatcaScreen)
18. shared-modules.compiled.js  (SharedAI/BI/CRM/Manufacturing)
19. app-marketplace-screen.jsx
20. dalseen-integrations-screen.jsx
21. api.js (legacy façade)
22. front/api/* (foundation, fetch, session, namespace, hooks, states)
23. api-seeds.js
24. app-main.compiled.js        (root mount)
```

> **Don't reorder casually.** `shell-polish` MUST come after `shell`, and `roles-editor` MUST come after `auth`. Both are "v2 overrides" of v1 globals.

### 6.5 Permission gate plumbing

Three things gate UI:

1. **Sidebar visibility** — `canSee(role, navId)` in `roles.js`. Returns `true` if `DALSEEN_PERMS[role].all` or `id` in `DALSEEN_PERMS[role].ids`. Public escapes for `release-notes`.
2. **Read-only mode** — `isReadonly(role)`. Wraps any role marked readonly in `DALSEEN_PERMS`. Screens are expected to **honour this themselves** by disabling CTAs. There's no central enforcement — any new screen must remember to check.
3. **High-risk action gate** — for refunds, role changes, payroll approvals: invoke `BiometricPrompt` before the action fires. Threshold from `DALSEEN_ROLE_POLICIES[role].highValueThreshold` (or v2 store).

At cut-over, all three become `me.permissions[]` checks (and a server-side enforcement for things 1 and 3 — server must return 403 even if FE forgets to gate).

---

## 7 · Cross-module consumer notes

What other modules pull from Common:

| Other module | Pulls from Common |
|------|------|
| **Retail** | `Sidebar`, `Topbar`, `PayModal`, `BiometricPrompt`, `StartShiftModal`, `DALSEEN_USERS`, `DALSEEN_DEVICES`, `fmtSAR`, `Icon`, `Skeleton*`, `EmptyState`, `DalseenIntegrations.shouldPostGL/shouldDecrementStock/shouldPostCOGS` |
| **Pay** | `Sidebar`, `Topbar`, `PayModal`, `BiometricPrompt`, `DALSEEN_DEVICES.safe-01/terminal-01`, `fmtSAR` |
| **Dine** | `Sidebar`, `Topbar`, `PayModal` (when settling tickets), `Icon`, `fmtSAR` |
| **Accounting** | `Sidebar`, `Topbar`, `Skeleton*`, `EmptyState`, `AsyncBoundary`, `DalseenIntegrations` (the most-coupled consumer — books mode determines GL posting), `ZatcaScreen` (cross-link) |
| **Owner + HR** | `Sidebar`, `Topbar`, `BiometricPrompt` (payroll approval), `DALSEEN_DEVICES.badge-01`, `OnboardingAdminsStep`, `Icon` |
| **Platform** (super-admin) | `Sidebar` (Platform system pill), `Topbar` (tenant-selector pill), `DALSEEN_PLATFORM` (tenants), `DALSEEN_HEALTH_FACTORS` |

> **The single most over-coupled atom** is `Icon`. Every screen pulls from it — meaning adding a new glyph means every screen across all modules can use it without import. **The single most under-coupled** is `DesignedModule` — only `app-main.compiled.js` falls through to it, but it should *always* be the last-resort wrapper for an unknown route, not removed.

---

## 8 · References

- **Routes file summary:** `docs/handoff/routes-api.php` — auth (`/auth/*`), me (`/me`, `/me/*`), users (`/users`, `/users/{id}/*`), roles (`/roles`, `/roles/{id}/*`), shifts (`/shifts/*`), audit (`/audit`).
- **Live-wrapper template:** `front/owner/owner-live.jsx` and `front/platform/platform-live.jsx` — these are the only two `*-live.jsx` files today; Common screens wire to APIs **directly** rather than through a live wrapper because most surfaces here are chrome.
- **Permission catalog (canonical):** backend's `/permissions` resource. FE's `DALSEEN_PERMS` matrix is the *demo-time* version; production reads `me.permissions[]` and uses the same keys.
- **Sibling consumer surfaces:**
  - `SCREENS-INVENTORY-Retail.md` — every screen that pulls `Sidebar`, `PayModal`, `StartShiftModal` from Common
  - `SCREENS-INVENTORY-Owner+HR.md` — onboarding wizard chains into `OnboardingAdminsStep` (this doc §5.9)
  - `SCREENS-INVENTORY-Accounting.md` — Books mode flows gate on `DalseenIntegrations.shouldPostGL` (this doc §6.1)
  - `SCREENS-INVENTORY-Platform.md` — super-admin uses Common's role-policy editor against any tenant
- **Flow definitions** that pass *through* Common surfaces:
  - `FLOW-INVENTORY.md §1.1 Open shift` → uses `StartShiftModal` (this doc §5.2)
  - `FLOW-INVENTORY.md §1.5 High-value refund` → gates on `BiometricPrompt` (this doc §5.1)
  - `FLOW-INVENTORY.md §6.2 Invite admin during onboarding` → uses `OnboardingAdminsStep` (this doc §5.9)

---

**End of SCREENS-INVENTORY (Common).** This is the final per-module inventory; with this in place, the per-module catalogue is complete (Retail, Pay, Dine, Accounting, Owner+HR, Platform, Common). The next document — `USER-FLOWS.md` — synthesises across these to trace cross-screen user journeys.
