Build compliance tabs: Rules, Guard, Settings
# C2 — Compliance shell with Rules, Guard and Settings tabs
Part of epic "Simplify and improve Compliance UI clarity". Turns `/compliance` into a tabbed **sectioned page** and builds the Rules, Guard activity and Settings tabs, the URL contract and the redirects. Depends on C1 (`getPackRules`, `modeLabel`).
## Scope (Architecture §3.1, §3.3, §3.4, §3.5, §2.3)
**Shell (§3.1):** `CompliancePage.tsx` becomes the shell: loads `setup` (as today), renders `components/Tabs` (`idPrefix="compliance"`, labels `Repos`, `Rules`, `Guard activity` (only with `admin.guard_enabled`; `useUrlTab` falls back to Repos so a hidden tab is unreachable by URL), `Settings`) and four `TabPanel`s, tab in `?tab=` via `hooks/useUrlTab`; Repos is the default with no key. **No page title** (sidebar names the page). Each tab draws its own one-line toolbar (filters left, ≤1 visible action plus ⋯ right). The Repos panel renders `ReposTab` (default export of `pages/compliance/ReposTab.tsx`, no props). If C3 has not created `ReposTab.tsx` yet, create it by moving today's matrix body into it **verbatim**, header actions included; otherwise use C3's. (File overlap with C3, not a dependency — rebase.) The Settings tab label carries a `cue` (`TabDef.cue`) when pack updates are waiting. The operator-only "daily scan job is off" banner moves to Settings. If C3 kept an interim *Show observed* item in the Repos ⋯, remove it now that the Rules tab's Mode filter exists.
**Rules tab (§3.3):** new `RulesTab.tsx`. Toolbar: a pack picker (select over `setup.packs`, default the active pack = `settings.active_pack_id ?? setup.active_pack_id`; active one marked `Active`), filters Pillar · Level · Mode · On/Off · Kind (Scan / Change), and **one** action: org pack + `compliance:edit` → **Add rule** (`/compliance/packs/<id>/rules/new`); ready-made pack + `compliance:edit` → **Customize** with one line beside the list at the top: "Ready-made packs are read-only. Customize makes your own editable copy and makes it active." Click → `forkPack(id)`, then `navigate('/compliance?tab=rules')` with a success banner; `compliance:view` only → no action (a view-only reader of a ready-made pack sees the read-only line only). List: `ui/feed/FeedList`, one `FeedSection` per pillar (`pack.pillars` order) then "Change rules". Row title = rule name (`titleLines: 'all'`) linking to `/compliance/rules/<id>` (opens in the pane); slots `L1` (scan) or the change-kind label (change) · mode via `modeLabel` · `Off` only when off. Body heading: `24 rules · <pack name> · v<version>` with `Ready-made` or `Yours` (`24` = `rules.length` with filters cleared) — the only place pack/version appear on the tab. `?customize=1` makes Customize `variant="contained"` and scrolls it into view. Old per-pack "Change rules" lists in Settings go away.
**Guard activity tab (§3.4):** `GuardActivityPage`'s body renders inside the tab, without its own breadcrumb/title; filters use the §2.3 keys (single `rule=<pack uuid>:<key>`, split at the first `:`). Its `<Navigate to="/compliance">` for `!guard` is replaced by the shell hiding the tab. Decision values render through their labels, never raw tokens.
**Settings tab (§3.5):** `SectionCard`s with one `LoadingState` for the tab: **Daily scan** (one line: switch `Daily scan` + next-run text); **Architectural Guard** (guard only: the stuck limit, unchanged behaviour); **Pack updates** only when proposals exist; the operator-only "job off" banner. No breadcrumb, no title. View-only reader: values as plain text (`Daily scan: On · Next run Oct 26, 06:00`, `Blocks before asking: 5`), no switch/input/Save/Apply/Dismiss, one line at top "Changing settings needs compliance edit." The Packs section leaves Settings (packs live on the Rules tab).
**URL contract (§2.3), owned here:**
| Address | Meaning |
|---|---|
| `/compliance` | Repos tab (default, no key) |
| `/compliance?tab=rules[&pack=<packId>][&pillar=<key>][&level=<n>][&mode=<mode>][&on=on\|off][&kind=scan\|change]` | Rules tab; absent `pack` = org's active pack |
| `/compliance?tab=rules&pack=<id>&customize=1` | Rules tab with Customize highlighted |
| `/compliance?tab=guard[&project=<id>][&rule=<packId>:<key>][&decision=<d>]` | Guard activity |
| `/compliance?tab=settings` | Settings |
| `/compliance?setup=1` | Repos tab with the chooser open (unchanged) |
| `/compliance/settings[?fork=<id>]` | **Redirect** (replace) → `?tab=settings`, or with `fork` → `?tab=rules&pack=<id>&customize=1` |
| `/compliance/guard?…` | **Redirect** (replace) → `/compliance?tab=guard&…` carrying `project`, `decision`; legacy `pack=<p>&rule=<k>` → `rule=<p>:<k>` |
| `/compliance/rules/:ruleId`, `/compliance/packs/:packId/rules/new` | add to `LIST_DETAIL_ROUTES` (`apps/web/src/lib/listDetail.ts`) with `list: '/compliance?tab=rules'` |
Tab-owned keys: Repos owns `setup`; Rules owns `pack`, `pillar`, `level`, `mode`, `on`, `kind`, `customize`; Guard owns `project`, `rule`, `decision`. On a tab switch the shell **drops keys the target tab does not own**, in one `setSearchParams` call in its own `onSelect` wrapper around `useUrlTab`'s setter. `guardActivityPath({projectId, packId, ruleKey})` in `pages/compliance/guard.tsx` keeps its signature and returns `/compliance?tab=guard&project=…&rule=<packId>:<ruleKey>`. Redirect routes in `apps/web/src/App.tsx` (~343-361).
**Docs & links:** reader docs `apps/docs/content/using-jaah/compliance/*.md` and `apps/docs/content/administration/security-and-audit/repository-compliance.md` updated (routes/frontmatter, "where you set up rules": the Rules tab). Grep `apps/api/app` and `apps/web/src` for in-product links to `/compliance/settings` or `/compliance/guard` and repoint them to the `?tab=` form (planner grep found none in `apps/api/app`; the redirects cover any left).
**Tests:** `CompliancePage.test.tsx`, `SettingsPage.test.tsx` → new `RulesTab.test.tsx` (+ Settings tab test), `GuardActivityPage.test.tsx`; pin key-dropping on tab switch and the single-key Guard rule filter; e2e `apps/web/e2e/compliance.spec.ts` cases for settings, the managed Customize and the tabs.
Dropped/moved (Architecture §6, this child's rows): Observed filter → Rules tab Mode filter (`mode=observe`); Guard stuck limit → Settings → Architectural Guard; Pack updates → Settings, only when proposals exist, plus a cue on the tab label; Settings breadcrumb + "Compliance settings" title, the Packs section, the "No pack updates." card and per-section skeletons removed.
## Original request (verbatim, epic Task 10032)
> Go over Compliance process with oam-demo project in prod as an example. The Compliance UI seems to be too busy, very confusing, not clear where user actually sets up rules, etc. Clean up and make more straight and intuitive. Remove repetitions and stating obvious facts
## Settled requirements (PRD, verbatim)
- **R1: One place to set up rules (A3, A4, A5; D1, D2, D6).** `/compliance` is a tabbed page: Repos · Rules · Guard activity (only with `guard_enabled`) · Settings. The tab is in the URL. Rules lists every rule of the selected pack (default: the active pack), both scan and change rules. It has filters for at least pillar, level, mode and on/off. Each row opens that rule's editor (`/compliance/rules/:id` or its successor). **Add rule** sits on the Rules tab for an org pack. A ready-made pack shows its rules read-only, with the Fork/Customize action and one line of why beside the list.
- **R2: Less on screen (A2).** Each page has at most **3 visible header actions** at ≥1280px. The rest go in one overflow menu or move to the tab that owns them (e.g. Settings).
- **R3: No repetition, no obvious facts (A6, A7).** Every item F2–F10 in §4 is resolved: the repeated text is removed or said once, and the obvious label is dropped. The spec names how each one is resolved. Specifically, no rendered Compliance surface:
- shows a status badge that only restates the section it sits in (e.g. "Fail" under "Gaps");
- shows a field whose value is the default ("every repo", zero counts, "None") unless it is editable there;
- repeats the same sentence on more than one row of a list (a shared state is said once for the group);
- shows the same fact twice in one header (badge + sentence, or pack + version in the header *and* the subtitle).
- **R4: One name, one destination (A3).** A rule's name opens the same kind of page wherever it appears: the rule page. Evidence for a repo × rule is a separate, explicitly labelled action or link.
- **R5: Consistent words (A3).** Mode values use their labels (Observe/Advise/Nudge/Gate/Lock) everywhere. Every label follows `docs/standards/ui-copy-standards.md`.
- **R6: Dropped features are explicit (D5).** The architecture spec lists every removed feature with a one-line reason. Anything not listed stays reachable. No feature disappears silently.
- **R7: Available on dev (charter).** The redesigned UI is the only Compliance UI: no flag or toggle gates it. It is live on the dev control plane (`dev.jaah.ai`) for an operator whose org has Portfolio Compliance on.
Operator decisions: D1 Compliance tabs (Repos · Rules · Guard activity · Settings); D2 keep explicit Fork (renamed **Customize**) beside the Rules list with one line of why; D3 full redesign to the UI style guide; D6 Rules opens on the active pack, switchable; Architect Q1/Q2: **no capability is dropped** — rare actions move into a ⋯ menu or appear only in context.
**Binding design principle:** a page shows at most one always-visible primary action; any other action goes into the page's ⋯ (`ui/OverflowMenu`), a row's ⋯, or appears only while its condition holds. Breadcrumb ends at the parent; the page title names the page; a breadcrumb never repeats the title. Every rendered mode goes through `modeLabel`.
Full design docs: the epic's PRD and Architecture artifacts (Tasks 10032 / 10033); §-numbers refer to the Architecture document; its §6 "Dropped / moved" table is binding.
States covered: an org with **no scan yet**, **a scan running**, **results present**, **a project not scannable**, a **ready-made** active pack and an **org (forked)** pack. A viewer is either **compliance:view only** or **compliance:edit**, and `guard_enabled` is **on** or **off**. Unless a criterion names a state, it holds in all of them. Desktop = 1440×900; phone = 390×844 (`docs/standards/ui-style-guide.md` § Checking a page).
Credentials & reach (Architect §7, verbatim):
- AC1–AC17 (dev console UI), guard **on** states — `me/jaah/debug/main/operator-login` (owner/admin in several dev orgs). Environment: dev control plane via its public ALB **`https://app.dev.jaah.ai`** (`docs/architecture.md:185`). Target tenant: **Org B** — **verified**: `credential_get` succeeded. A sign-in to `app.dev.jaah.ai` returned 200. Switching to Org B returned 200, its session reads `compliance_enabled` and `guard_enabled` true, and `GET /api/compliance/setup` returned 200 (10 projects, 3 packs). The internal `dev.jaah.ai` ALB timed out from this node (tailnet-only), so oracles use `app.dev.jaah.ai`.
- AC1–AC17, guard **off** states — Same credential. Target tenant: **TimeSaver2** — **verified**: the switch returned 200, `compliance_enabled` true, `guard_enabled` false, and `GET /api/compliance/setup` returned 200 (2 projects, 2 packs)
- AC6 (compliance:view-only viewer) — Same credential: the login is `owner` in Org B (admin covers non-admin). The coder adds a view-only member through the console's Users/Groups UI at verify time and signs in as it — **verified** (owner identity probed above; creating the member is a console step, not a missing credential)
- AC8 / AC16 data (≥2 not-scannable projects, fresh results) — Same credential, Org B set up through the console (projects with no node, a rescan) — **verified** (Org B reachable with 10 projects; data is arranged at verify)
- AC18 (`./dc check quick`, unit + e2e suites) — Nothing beyond the worktree — **verified** (worktree only)
Out of scope:
- Do NOT touch `/compliance/verify` (`apps/web/src/pages/ComplianceVerifyPage.tsx`), the task manifest (`apps/web/src/components/tasks/ComplianceManifest.tsx`), org compliance signing (`apps/web/src/pages/settings/complianceSigning/`), `apps/web/src/pages/orgs/OrgSettingsTab.tsx`, `apps/web/src/pages/customers/ComplianceDeleteDialog.tsx` or the plan feature (`apps/web/src/pages/products/plan/PlanFields.tsx`).
- Do NOT change scan/score semantics: `apps/api/app/routes/compliance.py` matrix/repo/cell computation, levels, the auditor runner, the daily-scan cron behaviour, Guard enforcement in `apps/jaahd/guards/`.
- Do NOT change permissions: `compliance:view` / `compliance:edit` and the `RequireCompliance` / plan gating stay as they are.
- Do NOT add a second sidebar entry for Compliance, and do NOT add a feature flag for the new UI.
- Do NOT remove any capability silently; a removal is allowed only if listed in Architecture §6 (which drops none).
- No data migration: stored packs, rules, waivers, facts and scans are untouched.
- Do NOT redesign the Repos tab body, the repo/evidence pages or the rule page/Add rule (C3, C4, C5) beyond the verbatim move into `ReposTab.tsx`.
Done means:
- AC1: The Rules tab lists every rule of the active pack (scan + change), and its count matches the pack's rule count — On dev, open `/compliance`, click **Rules**. Count the rows with filters cleared. Compare with the rule count of that pack's rule list from `GET /api/compliance/packs/<id>/rules`. They are equal, and the heading's count equals both.
- AC2: Rules is reachable in ≤1 click from `/compliance` and the URL names it — Open `/compliance`, click **Rules**. The URL carries the tab (e.g. `?tab=rules`), and reloading reopens Rules.
- AC3: The pack picker switches packs — On Rules, pick another pack (e.g. "Jaah house conventions"). The list shows that pack's rules and the header names it. Pick the active pack again and the list returns.
- AC4: A ready-made pack is read-only with an explained Fork — As a compliance:edit viewer with a ready-made active pack, Rules shows no edit controls on rows. A Fork/Customize button and one sentence of why are visible beside the list without scrolling at 1440×900. Click it: an org pack is created and becomes active, and the same tab now shows **Add rule** and the editor opens from a row.
- AC5 (Add rule button half): With an org pack active, Rules shows **Add rule**. It opens the new-rule page (in the pane beside the Rules list). Saving returns to the new rule, and that rule appears in the Rules list (query under `COMPLIANCE_KEY`).
- AC6 (Rules, Settings): View-only viewers see no edit controls — As a compliance:view viewer, Rules and Settings show no Fork, Add rule, toggle or mode select. Each that has a reason shows it once in plain words.
- AC7 (Rules, Guard, Settings): ≤3 visible header actions — At 1440×900, on Rules, Guard activity and Settings, count the visible buttons/links in the toolbar (excluding the overflow ⋯ and the tabs). There are ≤3 (design target: ≤1).
- AC9 (Settings): On Settings: the title doesn't repeat the breadcrumb's last item word for word (the tab has neither); no "No pack updates." card when there are none.
- AC11 (Rules tab): Click a rule name on the Rules tab: it lands on that rule's page.
- AC12 (Rules, Guard, Settings): Mode labels — Search the Rules, Guard activity and Settings tabs' rendered text: no lower-case raw mode token (`observe`, `advise`, `nudge`, `gate`, `lock`) stands as a value; the label is used; Guard decisions render as labels.
- AC13 (settings, guard, rules/:id, packs/new): Old links resolve — Open each of `/compliance/settings`, `/compliance/settings?fork=<id>`, `/compliance/guard?pack=<p>&rule=<k>`, `/compliance/rules/<id>`, `/compliance/packs/<id>/rules/new` on dev. Each shows its content or redirects to the new home, never a 404 or a blank page.
- Key isolation: set a Guard filter, switch to Rules → the Rules pack is unchanged; set a Rules filter, switch to Repos → no Rules key remains in the URL (unit test pins both).
- AC16 (Rules tab): Readable, not just smaller — At 1440×900 and 390×844 on Rules: no rule name truncated to an ellipsis (names wrap); no horizontal page scroll; columns don't wrap single words mid-word. **Tightened:** attach 1440 and 390 screenshots of each tab (Rules with an org pack and a ready-made pack, Guard, Settings as editor and view-only) to the report; a tab that passes the counts but reads cramped or leaves the Customize line detached from the list fails.
- AC17: Live on dev, no flag — Open `https://dev.jaah.ai/compliance` (reach: `https://app.dev.jaah.ai/compliance`) as the operator in an org with Portfolio Compliance on. The tabbed UI shows without toggling anything. `grep -rn "compliance" apps/web/src` for a feature flag guarding the new UI returns none.
- Reader docs say the Rules tab is where rules are set up, and their routes resolve (`python3 tools/check_context.py` green).
- AC18: Tests green — `./dc check quick` passes. The changed suites under `apps/web/src/pages/compliance/*.test.tsx` and e2e (`apps/web/e2e/`) for compliance pass, updated to the new structure.
Outcome
Artifacts
Activity
- Draft
- Ready
- Runner started