Aucert console — product spec (running)
This is the running spec for the customer-facing Aucert console
(frontend/apps/console/). It captures what the product currently does, how
users move through it, why we made the decisions we did, and how to use it
locally. Update it alongside every product change — when this doc and the
shipped product disagree, the doc is wrong.
This doc isn't gated by CI yet. There's no pre-commit hook checking that a product change touches this file. We intend to keep it current voluntarily while the surface area is small, and formalize once the cadence stabilizes.
1. Features
The console is a Next.js 15 (App Router) sidebar-07 shell with three nav
groups. Each entry below is what's actually rendered on feat/console-*
work merged into main.
Mission control
| Surface | Route | What ships today |
|---|---|---|
| Dashboard | /dashboard, /dashboard/metrics, /dashboard/reports | Placeholder routes |
| Executions — Runs | /executions/runs | Runs list (Running / Queued / History stacked cards, status filter, pagination per card) + Start new run dialog (Source / Properties / Test suite / Devices / Summary). Row click navigates to run detail. |
| Executions — Run detail | /executions/runs/[id] | First-class feature — see §1.2 |
| Executions — Test detail | /executions/runs/[id]/tests/[testId] | First-class feature — see §1.3 |
| Executions — Schedules | /executions/schedules | Placeholder route |
| Agents — Directory | /agents/directory | First-class feature — see §1.4 |
| Agents — Task runs | /agents, /agents/task-runs | Placeholder routes |
| AgentRail (right-side workspace) | shell-wide | First-class feature — see §1.4 |
Library
| Surface | Route | What ships today |
|---|---|---|
| Tests — Repository | /tests/repository | First-class feature — see §1.5 |
| Tests — Studio | /tests/studio | Placeholder route |
| Test Groups | /tests/groups, /tests/groups/[id], /tests/groups/new | First-class feature — see §1.6 |
| Knowledge — Explorer / Connectors / Insights | /knowledge/* | Placeholder routes |
| Policies | /policies, /policies/[id] | First-class feature — see §1.1 |
Hub
| Surface | Route | What ships today |
|---|---|---|
| Settings — General / Payment methods / Subscription / Invoices / Usage | /settings/* | Placeholder routes |
1.1 Policies (first-class)
A policy is an enforcement guideline applied to components in your app (e.g. "All text inputs mask passwords", "All UI surfaces use 4.5:1 contrast"). Policies are namespaced by Source (same vocabulary as the test catalog — see §1.5):
| Source | Mutability | Purpose |
|---|---|---|
| Aucert | Immutable | Default best-practice rules |
| Apple App Store | Immutable | App Store review guidelines |
| Google Play Store | Immutable | Play Store review guidelines |
| Custom | Mutable | Workspace-authored policies |
Each policy has: id, name, description, category (UI / Security /
Performance / Compliance), source, priority (P0–P3), groups (one or
more named buckets like "Financial services" or "Healthcare"), enabled
flag, icon, and (via detail page) deterministic mock metadata —
created/updated dates, last editor, applied components, approval chain,
auto-attach rules.
See §2.1–§2.3 for the workflows that drive this feature.
1.2 Run detail (first-class)
/executions/runs/[id] is the at-a-glance view of a single test run. Two-column responsive layout (max-w-screen-2xl, sticky sidebar at lg+, sidebar stacks above main on narrow screens via explicit grid placement).
Sidebar: three grouped meta cards — Overview (id, source, started by, started at, duration), Properties (environment, retries, timeout, parallel, notify), Test coverage (suite mode, devices).
Main column widgets, conditional by run status:
- Status Report: header (title + by-type / by-team toggle) + inline stat strip (Passed / Failed / Skipped / Cancelled — adapts to Running / Queued when live) + a single thick overall progress bar + a mini-table breakdown (Passed / Failed / Skipped / Total per suite or team with a Total tfoot row). Rows are clickable; clicking a row sets a page-level filter and smooth-scrolls to Test Execution. Queued runs render a centered "queued" empty state instead.
- Build Insights (terminal runs only): AI-styled bullets framed around the build (the subject), not the test run (the instrument). Four severity tones — Healthy / Info / Watch / Critical — each with a colored dot, bold label, body, and severity chip. Different bullet set for Passed vs Failed runs.
- Fix prompts (Failed/Cancelled with ≥ 1 failed test): top issues grouped by error-message prefix; each row has a Copy prompt CTA that ships a self-contained prompt to clipboard.
- Test Execution: 5 stacked sub-cards (Running / Queued / Failed / Passed / Other) each with a paginated TestsTable. Failed card has "Retry all" + per-row retry. Mirror filter pills above the sub-cards (All / per-suite or per-team with counts) so users can change the active filter without scrolling back to Status Report. Hidden entirely for Queued runs.
The groupBy ("suite" / "team") and filter ({ kind, value } | null) are lifted to page level so Status Report and Test Execution stay in sync. Switching dimension drops a stale filter.
1.3 Test detail (first-class)
/executions/runs/[id]/tests/[testId] — the terminal node in the hierarchy: Runs list → Run detail → Test detail. Same two-column shell as Run detail. Sidebar: Run (back-link, status, started by, started at) + Test execution (test id, suite, team, device, started, duration / "Running for").
Main-column widgets, status-conditional:
- Failure context (Failed only, side-by-side
xl:grid-cols-2): an AI-style "Why this failed" card mapping the error message to a plain-English root-cause hypothesis (with raw error + collapsible stack trace) + a Fix prompt card with a copyable agent-ready prompt. - Video recording: phone-portrait player placeholder (9:16, capped at
max-w-[260px]), paired with Logs atxl:grid-cols-[minmax(280px,360px)_1fr]so Logs gets the wide column. - Reproduction steps: numbered list of the test's user actions, picked from canned templates by test-name keyword.
- Policies enforced (always): 6–9 real policies pulled from
MOCK_POLICIESin/policies/_data/policies.ts— every row is a<Link>to/policies/[id]. Status pill + category badge each sit in fixed-width right-justified slots (w-28) so columns align regardless of pill presence. Failed-first sort. When any policy failed, a header pill toggle exposes "Failed N" to collapse to just the failures. Category badge uses the sharedPolicyCategoryBadgecomponent (see §3) — same visual as the policies list. - Logs: pre-formatted output with status-aware content. Expand toggle bumps max-h from 96 to 80vh for long traces.
- Recent history: 10 tile sparkline (failed =
--danger, skipped = muted) + stability summary stats (Pass rate %, Stability label "Stable" / "Low flake" / "Flaky", Avg duration). Useful flake signal.
Queued tests collapse all artifact cards behind a "waiting in queue" placeholder.
Mock test cases live in /executions/runs/_lib/test-cases.ts (generateTestCases(run)) and produce status distributions matching the parent run's status.
1.4 AgentRail + Persona Ecosystem (first-class)
Persistent right-side workspace mounted in (shell)/layout.tsx next to AppSidebar and SidebarInset. Source: frontend/apps/console/src/components/agent-rail/. Sticky-pinned to viewport via an inner sticky top-0 h-svh container so the composer + tabs remain reachable on any-height page.
Structure:
- Collapsible to a 48-px icon strip (with chat / dispatch / agents shortcut buttons); expanded to 360 px. Open state persists to
localStorage. - Three tabs: Chat (default) / Dispatch / Agents.
Persona Ecosystem (_lib/personas.ts): six characters back the Validation Pipeline. First names share their initial with the role's first letter so customers tag/search by the role surname; first name is character. Internal L-layer names live only in the cascade-trace expand.
| Persona | Role | Internal |
|---|---|---|
| Genie | Generator | L1 Generation |
| Ember | Executor | L2 Execution |
| Ada | Analyzer | L3 Analysis |
| Dax | Decider | L4 Decision |
| Remy | Reporter | L5 Reporting |
| Rio | Rover | Rover Service |
Avatars come from /public/agents-sprite.svg (3×3 illustrated grid; cols 1+2 carry six character portraits). The Avatar component CSS-clips a single cell per persona — no external avatar service. Each persona has avatarCell: { col, row } in personas.ts.
Chat tab:
- Threads list with per-row hover delete + "+ New" button.
- Active conversation header: back arrow / title / new-chat (SquarePen) / delete-thread (Trash2).
- Multi-context chip strip above composer (runs, tests, policies, components, builds); detach × on each.
- Add-context popover with search input + grouped recents (kind-headed sticky sections; runs / tests / policies / components / builds).
- Composer: context-derived suggested-prompt pills, three-tier routing picker (Auto / Ask a specific agent / Send to coding agent — flips Send → Dispatch and adds destination chip), scope subtitle ("scoped: …").
- Message bubbles render with the
AgentBadge(avatar + "Persona Role · confidence") and an inline "How was this decided?" expand that reveals theCascadeTracetable (persona / role / internal layer / stage / confidence / model). - Two-line "what + ↳ next action" pattern on platform-authored messages. Inline tokens (filenames, run IDs, device names) render as small monospace chips via the
tokens.tsxhelper.
Dispatch tab: round-trip inbox for fix prompts sent to coding agents (Claude Code / Codex / Cursor / Replit). Sections: In flight (queued / investigating / ready) + Recent (merged / rejected / failed). Shares the same StatusPill primitive as the Agents tab — no spinners; mint ripple for active states, static muted dot for terminal-closed, destructive dot for failed.
Agents tab: live persona dashboard. Active personas render as AgentCard tiles (avatar + Name Role + status pill + single-line action + progress footer). Idle personas list as a compact roster below. Every active card and idle row is clickable; routes through the shared openChatWithPersona(persona) provider method that drills into the persona's in-flight thread (if any) or spawns a fresh thread with the persona's role pre-selected as the composer target.
/agents/directory page: a full-page workforce overview that mirrors the Agents tab. Responsive 1 / 2 / 3-column grid of AgentCards. Each card: avatar + Name Role + status pill + persona description + "Last activity" panel + stat triplet (count / rate / uptime — no inference cost overlay; uptime % takes the third slot). Whole-card click → openChatWithPersona. Hover reveals Chat + Activity buttons (absolute-positioned bottom-right so card height is constant).
Shared internals (used across all three tabs + the directory page):
Avatar— CSS-sprite avatar rendererAgentCard— slim three-row tile for the Agents tabStatusPill— single pulse-dot pill (no Loader2 spinners) withtone="active"|"done"|"idle"|"failed"tokens.tsx— inline monospace chip renderer for literal references inside agent prose
All mock data sits in _lib/mock-data.ts so the prototype works offline. Provider state (open / view / chatMode / activeThread / threads / dispatches / composerTarget) is exposed via useAgentRail() so any page can open the rail with context.
1.5 Tests Repository (first-class)
/tests/repository is the catalog of every test in the workspace. A test
is a single executable verification scenario that exists to verify one or
more Policies (§1.1) — the 1:N test ↔ policy mapping is the spine of
the product.
Each test has: id, name, description, policyIds (1+), screens
(multi-valued), component count, tags, category (UI / Security /
Performance / Compliance), teams (multi-valued — a test can be
co-owned across squads like Mobile UX and Auth & Identity), source
(Aucert / Manual / Imported), priority (P0–P3), status
(Passing / Failing / Not run), flaky flag, enabled flag, icon,
and an optional lastRunAt.
Layout: same conventions as /policies so users learn one pattern:
- View toggle (List default, Cards optional), persisted to
localStorage(aucert.tests.view). - Chip filter bar: Search · Group · Priority · Source · Status ·
Flakiness. The Group chip lets the catalog be scoped by any
defined Test Group (§1.6) — picking a group filters the visible tests
to that group's membership. The dropdown footer carries a Manage
groups link to
/tests/groups. - Default grouping in Cards view is by Priority (P0 first), with a section-header switch to flip to grouping by team / source / status.
- List columns: icon · name · ID · priority · status · source · enabled Switch — a near-identical column shape to the Policies list.
Status palette is brand-green-only with one principled exception:
Passing uses the brand green tint; Not run uses neutral muted; Failing
is the lone use of --destructive in the catalog. Industry convention
for failure state is too strong to override for incident triage.
Mock data lives in _data/tests.ts (31 tests) so the catalog renders
offline. Group definitions live alongside in _data/groups.ts.
1.6 Test Groups (first-class)
A Test Group is a saved, named collection of tests. Two flavours:
- Smart group — carries a filter predicate
(
TestGroupFilter). Membership is computed at read time: every test that satisfies the filter is a member, and any newly authored test matching the filter joins automatically. This is the "All P0 tests" / "Everything touching Checkout" pattern. Smart is the default and the one we steer users toward at creation. - Manual group — carries an explicit list of test IDs
(
members: string[]). Membership is fixed until a human edits it. Use this for hand-curated collections like "Beta release smoke" where the contents change deliberately. The editor surfaces a warning that manual groups do NOT pick up new tests automatically.
A test can be a member of zero or many groups — there is no exclusivity. The runner consumes a group to decide what to execute on a pipeline run; the catalog (§1.5) uses group filters to scope the visible test list.
Three surfaces:
| Surface | Route | Shape |
|---|---|---|
| Management list | /tests/groups | Type badge, rule summary chips, member count, edit/delete actions, + New group CTA |
| Detail / editor | /tests/groups/[id] | Identity strip (type badge + name + Delete) → Group details widget (name + description) → 2-col body: rules editor on the left, live members panel on the right |
| Create flow | /tests/groups/new | Type picker card (Smart vs Manual, Smart recommended) → Group details → editor + live members → Create / Cancel footer with inline validation (name required + at-least-one rule/member) |
Smart group editor (SmartGroupEditor): low-cardinality dimensions
(Priority, Status, Category) render as ToggleChip rows with uniform
selected-state styling (green tint + Check icon — see FRONTEND.md). High-
cardinality dimensions (Teams, Tags, Screens) use the MultiSelectChips
combobox/popover pattern so the editor scales from 5 teams to 500 without
visual collapse. Free-form query lives as a single Input.
Manual group editor (ManualGroupEditor): warning banner + search +
"show only selected" toggle + a checkbox-per-test list. Selecting a test
mutates members[].
Live members panel (LiveMembersPanel): the right column on the
detail and create pages. Subscribes to the local group state and runs
isGroupMember(group, test) over every test in the catalog, rendering
the result as a scrollable list with the running count in the header.
The vacuous-truth guard (hasAnyRule) ensures an empty smart filter
renders as zero members (not "every test in the catalog") so a half-
configured group doesn't mislead the author at creation time.
Group definitions live in
frontend/apps/console/src/app/(shell)/tests/repository/_data/groups.ts.
Reusable editor pieces extracted to
frontend/apps/console/src/app/(shell)/tests/groups/_components/group-editor.tsx
and consumed by both the detail and create pages.
2. Workflows
2.1 Toggle a policy on / off (/policies)
- Land on
/policies— defaults to List view (rows: icon · name · ID · priority · category · source · enabled Switch). Use the view-toggle in the page header to switch to Cards view (grouped by source); the choice persists tolocalStorage(aucert.policies.view). - Filter via the chip bar: Search · Source · Category · Priority ·
Group. Any active filter highlights its trigger with
--sidebar-primaryand surfaces a ghost Clear filters button. - Active-filter mode in Cards view flattens the grid into a single
section with pagination (Page size =
cols × rows.page, both screen-aware). List view paginates inline. - Toggle one policy via its inline Switch. In Cards view, toggle a whole section via the section-header Switch. Bulk Enable all / Disable all apply to whatever is currently filtered in.
- Click a row (List) or card body (Cards) — anywhere except the Switch
— to navigate to the detail page (
/policies/[id]).
2.2 Inspect a policy (/policies/[id])
- Header: icon, name, description, badges (source, category, priority, Immutable when non-Custom), Enabled Switch, and Edit (Custom) or Extend (non-Custom) CTA.
- Body uses a 2-column equal-width grid on lg+:
- Left col, stacked: Details (id, source, category, priority, groups, created, updated, last editor) + Approval chain (mock reviewer + status timeline).
- Right col: Applied to — auto-attach rules (with clickable "N components" popover listing matched components) and direct attachments.
- On narrow viewports the grid collapses to one column with Details first.
- Back link returns to
/policies.
Mock metadata is synthesized deterministically from the policy id
(getPolicyMetadata in _data/policies.ts) — swap-out point when the
backend lands.
2.3 Create a policy (/policies → Create policy dialog)
- Click Create policy (top right of
/policies). - A standardized dialog opens (max-w-4xl, h-[640px] body — same shell as the Start new run dialog).
- Fill the form: name (≤ 80 chars), description (≤ 240 chars with live counter), category select, icon picker (12 curated Lucide icons in a grid-cols-12 row).
- Submit → new
pol-NNNid assigned, source =Custom, enabled =true, appended to local state. Page auto-jumps Source filter toCustomso the new policy is immediately visible. - Attach components (optional) — open the picker, multi-select from the component registry (screens, UI, log surfaces). The form's "Attached components" row summarizes by type ("3 screens · 1 UI").
- Submit → new
pol-NNNid assigned, source =Custom, enabled =true,attachedComponentIdscarried through, appended to local state. Page auto-jumps Source filter toCustomso the new policy is immediately visible. - Form state resets on every open.
Deferred to the backlog:
- Assertion-definition field (the runnable rule body)
- Extend mode (fork an Aucert policy into a Custom copy)
- Sync from source mode (pull from GitHub / Notion / Confluence)
- Backend persistence
2.5 Attach components (/policies/[id] → Manage)
- On any policy detail page, the Applied to → Direct attachments section header surfaces a small Attach (if zero) or Manage (if there are existing attachments) button.
- Click it → opens the standardized
AttachComponentsDialog: search box, type-filter pills (All / Screens / UI components / Log surfaces), grouped multi-select list of all registry components, per-component checkbox + description. - Selecting components toggles the checkbox state. Current selection count surfaces above the list ("3 components selected").
- Click Attach N components → picker closes, the policy's
attachedComponentIdsis replaced with the full selected set, and the Direct attachments list re-renders. - Pre-existing attachments are pre-checked on open, so the dialog acts as an edit-set rather than an add-only.
The same picker is embedded in the Create policy dialog (§2.3 step 5).
2.4 Start a new run (/executions/runs → New run dialog)
- Click Start new run on the Runs page.
- Dialog opens with the standardized shell.
- Step through Source (Upload / Library / Git), Properties (env, retries, timeout, parallel, notifications), Test suite (Auto / Custom), Devices (Auto / Specific — Android only today).
- Click Start run → Summary screen for read-only review with Edit jump-backs to each section.
- Confirm & start to launch.
2.5 Inspect a run (/executions/runs/[id])
- From
/executions/runs, click any row → routes to the run detail page. Kebab actions inside the row usestopPropagationso they don't fire the row click. - Sidebar gives the run's identity and configuration at a glance;
sticky on
lg+. - On the Status Report, toggle By type ↔ By team to regroup the mini-table. Clicking any breakdown row sets the page-level filter, smooth-scrolls to Test Execution, lights the matching pill in the mirror filter, and shows a "Filtered by …" banner.
- Read Build insights to understand what the build does well or regressed (only for terminal runs). Use the Copy prompt buttons on Fix prompts to hand off failure groups to an agent.
- Drill into a specific failure by clicking its row in any Test Execution sub-card.
2.6 Inspect a test case (/executions/runs/[id]/tests/[testId])
- Click a test row anywhere in the parent run's Test Execution section. Failed tests are the typical entry point.
- Header: back link to the parent run, large title, status pill (animated for Running), Retry button (Failed) / Re-run (terminal).
- For Failed tests, read the AI explanation in "Why this failed" and copy the agent-ready prompt from "Fix prompt".
- Open the video, scrub the logs (Expand for the full trace), read the Reproduction steps to understand what the test was trying to do.
- Check the Policies enforced card to see what coverage this test
provides — failed policies sort to the top with the "Failed" status
pill in a fixed-width slot. Click any policy row to navigate to
/policies/[id](deep links work — every row references a real policy inMOCK_POLICIES). - Check Recent history for flake signal (pass-rate %, Stability label, average duration over last 10 runs of this test).
2.7 Browse the test catalog (/tests/repository)
- Land on
/tests/repository— defaults to List view grouped by Priority (P0 first). View choice persists tolocalStorage(aucert.tests.view). - Filter via the chip bar: Search · Group · Priority · Source · Status
· Flakiness. The Group chip is the scoping shortcut: pick "All
P0 tests" or "Checkout flow" to instantly narrow the catalog to that
group's members. Group definitions come from
_data/groups.ts; the chip's dropdown footer carries a Manage groups link. - Click a row → navigates to
/tests/repository/[id](test detail surface — backlog; route is wired but page is placeholder). - The enabled Switch flips a test's
enabledflag in local state. Disabled tests are excluded from runs but stay visible in the catalog (with a faded row).
2.8 Create a Test Group (/tests/groups → New group)
- Click + New group on
/tests/groups(top right) — routes to/tests/groups/new. - Pick a type: Smart (recommended, auto-maintaining) or Manual (hand-picked, fixed). Switching type wipes the opposite flavour's payload so the submitted shape always matches the type discriminator.
- Fill Group details: name (required, ≤ 80 chars) and an optional description. Inline validation arms on first submit and stays live while the user edits.
- Smart: configure rules in the editor — Priority / Status /
Category as
ToggleChiprows, Teams / Tags / Screens asMultiSelectChipscomboboxes, free-formqueryas an Input. At least one rule is required to submit; the live members panel on the right shows the matching tests in real time (vacuous-truth guard means zero rules = zero members, not "every test"). - Manual: pick tests in the searchable list with checkboxes. At least one member is required to submit.
- Click Create group → assigns the next
grp-NNNid, pushes the new group intoMOCK_GROUPS, and routes back to/tests/groupswhere the new entry appears at the bottom of the list. (Mock-only persistence today; backend swap-out replaces theMOCK_GROUPS.push.) - Cancel returns to
/tests/groupswithout saving.
2.9 Edit or delete a Test Group (/tests/groups/[id])
- From
/tests/groups, click a row → opens the detail editor. - The identity strip at the top carries the type badge, group name, and a Delete button (with a confirm dialog — deletion is irreversible).
- Below: the same Group details widget and rules editor used by the create flow, plus the live members panel on the right. Edits commit to local state immediately (no save button on the edit surface) — mirrors what an eager-save backend would do.
- The back link returns to
/tests/groups.
3. Design principles & decisions
Theme tokens only, never hex
Every color comes from a luma theme token (--primary,
--sidebar-primary, --foreground, etc.) or an Aucert extension token
(--surface-raised, --success, --warning, --info). No literal
Tailwind palette utilities (no bg-emerald-600, no #0AA864). The brand
".ai" accent renders as --sidebar-primary (the brighter green).
sidebar-07 is the only layout
Every Aucert app — console, internal Astra, future portals — uses the
sidebar-07 block: SidebarProvider → AppSidebar (brand header + nav
groups + user footer) → SidebarInset (header + page content). No
top-nav-only layouts. No custom shell scaffolds.
Brand identity is reusable
The brand chip + wordmark + ultra-wide grid live in @aucert/ui
(BrandHeader, BrandLogo, .grid-ultrawide). Default props render
Aucert; pass overrides for any other brand. Per-app you only drop in
logo SVGs, favicon, and a Google Font link — see
frontend/.context/BRAND_SETUP.md for the 10-step bootstrap checklist.
Dialog dimensions are standardized
Every "open a dialog to do a thing" surface uses identical dimensions:
| Slot | Class |
|---|---|
DialogContent | max-w-4xl sm:max-w-4xl gap-0 overflow-hidden p-0 |
| Body | flex h-[640px] flex-col gap-6 overflow-y-auto px-8 py-6 |
| Header | space-y-1 border-b border-border px-8 py-5, title text-xl |
| Footer | flex items-center justify-between border-t border-border px-8 py-4 |
| Footer buttons | Ghost Cancel on the left, primary action on the right |
Reference implementations:
frontend/apps/console/src/app/(shell)/executions/runs/NewRunDialog.tsxfrontend/apps/console/src/app/(shell)/policies/_components/create-policy-dialog.tsx
Verified pixel-equal in DOM rects (both 851 × 768± at 1440-wide viewport).
Inset dividers inside cards (invoice-sample pattern)
Cards keep their default CardContent px-6. Internal rows declare their
own py-2.5 + border-t (skipping the first row's top border via
first:border-t-0). Dividers naturally stop 24px short of each card
edge — never edge-to-edge — matching the invoice-card reference.
Applies to: Details + Applied to cards on /policies/[id].
Card padding is symmetric
Card top padding (Card py-6 24px to CardHeader text) equals card
bottom padding (last content to Card py-6 24px). Verified per pixel
via DOM rects. No trailing help-text paragraphs with their own
pb-6 — those would double the bottom padding and break symmetry.
Equal-width cards in detail-page columns
When a detail page has a wide card (e.g. Applied to with rules + direct attachments) and shorter cards (Details, Approval chain), stack the short ones in one column and put the wide one alone in the other. Result: all three cards share the same width on lg+, and the grid collapses to a single column on narrow viewports with the most information-dense card (Details) first.
Active filter highlight
When any filter is non-default (Select ≠ "All", search has content), the trigger receives:
border-sidebar-primary/60 text-sidebar-primary ring-2 ring-sidebar-primary/30
A ghost Clear filters button with an X icon appears next to the
filters whenever anything is active.
Components are typed (screen / UI / log)
The component registry (_data/components.ts) splits every attachable
target into one of three types:
| Type | Examples |
|---|---|
screen | Login screen, Settings → Account, Home feed |
ui | Password input, Loading skeleton, Modal sheet |
log | Analytics events, Crash log payload, Audit log |
Each type ships its own Lucide icon (Monitor / MousePointer2 /
FileText) used everywhere a component is rendered — list rows in
"Direct attachments", filter pills in the picker, summary chips in the
create form. The type is the only mutable taxonomy on a component for
now; tags are deferred.
Auto-attach rules vs direct attachments (policies)
A policy can attach to components two ways:
- Rules — predicate (e.g. "All UI surfaces", "All authenticated screens"). New components matching the predicate auto-attach. Rule matches are clickable popovers listing the currently matched components. Rule management ships in a dedicated future page.
- Direct attachments — explicit component → policy binding,
managed via the
AttachComponentsDialog(reachable from bothCreatePolicyDialogand the detail-page "Direct attachments" section header). Pre-existing attachments pre-check on open so the picker acts as an edit-set, not an add-only.
Both render in the same "Applied to" card on the detail page, separated by a labeled section header (no divider line between).
Mock-first, backend-second
Console features ship against inline mocks (MOCK_POLICIES,
MOCK_BUILDS, MOCK_BRANCHES, etc.) with deterministic synthesized
metadata when richer fields are needed (getPolicyMetadata,
getRuleMatchedComponents). This lets us iron out UX without waiting
for backend. Mock helpers are the swap-out point — when the backend
lands, replace them with API calls and the rest of the UI is unchanged.
Non-custom is immutable
Aucert / Apple / Google policies cannot be edited in the UI. The detail page surfaces an Immutable badge and offers Extend (fork to a Custom copy) instead of Edit. Server-side enforcement will follow once the backend lands.
Insights are about the build, not the run
Insights on a run detail page are framed as observations about the product ("Cold-start time improved 18% on Pixel 8 Pro vs v4.1.7", "Critical bug in checkout shipping calculation"). The test run is the instrument, the build is the subject — same way an X-ray report talks about the patient, not the X-ray machine. Avoid run-mechanics framing ("smoke suite completed 23% faster") in user-facing insights.
--danger and --danger-light for status colors
--destructive (chroma 0.245) is reserved for high-stakes "Delete
this" buttons. For status indicators (failed tests, regression chips,
failure pills) use the softer --danger (oklch(0.60 0.14 25) light,
oklch(0.66 0.13 22) dark) — sits in the same saturation family as
--warning / --info / --success. --danger-light
(oklch(0.627 0.17 25)) pairs with --chart-3 for tinted progress-bar
fills and pastel status surfaces. Exposed via --color-danger /
--color-danger-light so bg-danger, text-danger, bg-danger-light
etc. utilities work as expected. See frontend/packages/ui/.context/DESIGN_LIBRARY.md.
Two-tone progress bars
ProgressBar (run detail Status Report) takes a tone prop:
vivid(default) — fullbg-primary+ softerbg-danger/85for pass/fail. Used for the hero overall progress bar.soft—bg-chart-3+bg-danger-lightfor pass/fail. Used for per-suite breakdown rows so they read as a secondary visualization next to the hero bar.
Shared domain components for cross-surface concepts
When a UI atom shows the same product concept on more than one surface,
extract it as a domain-named component in frontend/apps/console/src/components/.
Single source of truth — update the visual in one place, every callsite
picks it up.
Today: PolicyCategoryBadge (src/components/policy-category-badge.tsx)
renders a policy's category as a plain outlined Badge and is used by
both /policies (list page) and /executions/runs/[id]/tests/[testId]
(Policies enforced card). Naming carries semantic intent (Policy*,
not OutlinedTag) so refactors are safer — the name itself signals
"this is THE place".
Fixed-width slots for tag-list rows
When a row has a series of trailing badges/pills and some pills are
conditional (e.g. "Failed" pill only renders for failed entries),
wrap each badge slot in a right-justified container with a fixed
width (w-28 flex justify-end). Columns align vertically across rows
regardless of which pills are present. Applied on the Policies enforced
card on /executions/runs/[id]/tests/[testId] — the status-pill slot
and category-badge slot are both w-28 so the right edges line up
whether a row has Failed/Not-reached or not.
Page containers need w-full in flex-column shells
The (shell) layout wraps page content in flex flex-1 flex-col. In
a flex-column parent, a child block's cross-axis (horizontal) width
defaults to fit-content, NOT 100% — so mx-auto max-w-screen-2xl on
a child resolves to the widest descendant's min-content, not the
cap. Symptom: two variants of the same page render at different widths
because one variant has a wider descendant. Fix: always include
w-full together with mx-auto max-w-* on the outermost page div.
// ✅ Fills available width, capped by max-w-screen-2xl, centered.
<div className="mx-auto w-full max-w-screen-2xl p-6 space-y-6">
Applied across /executions/runs, /executions/runs/[id], and
/executions/runs/[id]/tests/[testId].
Dev auth bypass when WorkOS env vars are unset
NEXT_PUBLIC_WORKOS_CLIENT_ID empty → AuthContext returns a static
dev-mode AuthValue so the console is reachable without going through
WorkOS. Lets us iterate on UX without a live identity provider.
Public auth shell (/login, /signup, /auth/error)
All three public auth surfaces share a single AuthShell two-column
layout (frontend/apps/console/src/domains/auth/components/_components/AuthShell.tsx):
- Left column — brand chip top-left, theme toggle top-right
(
ThemeToggleworks pre-authentication), form vertically centered. - Right column (≥
lgonly) —AuthVisualCarousel: five slides (Brand intro + the four pillars Validation Graph / Tests / Policies / Execution), each with a small CSS/SVG visualization. Auto-rotates every 7s, hover-pauses, prev/next arrows + dot indicators; manual interaction resets the timer.
/login and /signup are visual ports of shadcn login-02 and
signup-02 blocks. Real auth still runs through WorkOS AuthKit — the
form fields exist for visual parity, the typed email is forwarded as
login_hint so AuthKit pre-fills it, password values are discarded
(AuthKit owns the credential exchange). "Continue with Google" routes
to WorkOS with provider=GoogleOAuth, skipping AuthKit's hosted page
and going straight to Google's consent.
Below lg the right column collapses, leaving just the form column —
keeps mobile sign-in unobstructed.
4. Usage
Run the console locally
# From the repo root
pnpm install
cd frontend/apps/console
pnpm dev # http://localhost:3000
# OR run on a custom port (e.g. when another worktree owns 3000):
pnpm exec next dev --port 3002
Build production output
cd frontend/apps/console
pnpm build && pnpm start --port 3000
Type-check + lint
cd frontend/apps/console
pnpm tsc --noEmit # ESLint is not yet wired in this app
Backlog tracking
Per-feature follow-ups live as markdown lists under .tasks/backlog/:
| File | Covers |
|---|---|
.tasks/backlog/policies-followups.md | Policies feature — items deferred from the v1 list + detail + create flows |
Add a new file when you start a feature; tick boxes as items land; keep items here when they're durable enough to be picked up cross-session.
Branch / PR convention
- One feature, one branch:
feat/console-<scope>offmain. - After merge, immediately fork the next branch from the latest
main. - Update from
mainbefore requesting review — never let a PR drift. - Human review required on every PR (CLAUDE.md hard rule #8).
- Pre-commit hooks (registered by
tools/scripts/setup-dev.sh) gate every commit; never--no-verify.
Known pre-existing issues
- Shared Dialog + Sheet primitives leak Radix Presence: after
onOpenChange(false),data-stateflips to"closed"and the exit animation runs, but the element stays mounted with the body stillpointer-events: noneand scroll-locked. AffectsNewRunDialog(on main) andCreatePolicyDialog(PR #174). Fix is at the primitive level — there's a spawned follow-up task for it.
5. How to keep this doc current
When you ship a console change, also touch this file in the same PR:
- New feature — add a row to §1 and a workflow to §2.
- New surface convention (layout pattern, dialog shape, color role) — add a subsection to §3.
- New local-dev step (env var, build flag, ports) — add to §4.
- Bump the
last_updatedfrontmatter at the top.
When in doubt: would a teammate joining tomorrow need this to understand the product? If yes, write it down here.