Skip to main content

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.

Not yet enforced

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

SurfaceRouteWhat ships today
Dashboard/dashboard, /dashboard/metrics, /dashboard/reportsPlaceholder routes
Executions — Runs/executions/runsRuns 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/schedulesPlaceholder route
Agents — Directory/agents/directoryFirst-class feature — see §1.4
Agents — Task runs/agents, /agents/task-runsPlaceholder routes
AgentRail (right-side workspace)shell-wideFirst-class feature — see §1.4

Library

SurfaceRouteWhat ships today
Tests — Repository/tests/repositoryFirst-class feature — see §1.5
Tests — Studio/tests/studioPlaceholder route
Test Groups/tests/groups, /tests/groups/[id], /tests/groups/newFirst-class feature — see §1.6
Knowledge — Explorer / Connectors / Insights/knowledge/*Placeholder routes
Policies/policies, /policies/[id]First-class feature — see §1.1

Hub

SurfaceRouteWhat 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):

SourceMutabilityPurpose
AucertImmutableDefault best-practice rules
Apple App StoreImmutableApp Store review guidelines
Google Play StoreImmutablePlay Store review guidelines
CustomMutableWorkspace-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 at xl: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_POLICIES in /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 shared PolicyCategoryBadge component (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.

PersonaRoleInternal
GenieGeneratorL1 Generation
EmberExecutorL2 Execution
AdaAnalyzerL3 Analysis
DaxDeciderL4 Decision
RemyReporterL5 Reporting
RioRoverRover 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 the CascadeTrace table (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.tsx helper.

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 renderer
  • AgentCard — slim three-row tile for the Agents tab
  • StatusPill — single pulse-dot pill (no Loader2 spinners) with tone="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 (P0P3), 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:

SurfaceRouteShape
Management list/tests/groupsType 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/newType 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)

  1. 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 to localStorage (aucert.policies.view).
  2. Filter via the chip bar: Search · Source · Category · Priority · Group. Any active filter highlights its trigger with --sidebar-primary and surfaces a ghost Clear filters button.
  3. 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.
  4. 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.
  5. 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])

  1. Header: icon, name, description, badges (source, category, priority, Immutable when non-Custom), Enabled Switch, and Edit (Custom) or Extend (non-Custom) CTA.
  2. 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.
  3. On narrow viewports the grid collapses to one column with Details first.
  4. 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)

  1. Click Create policy (top right of /policies).
  2. A standardized dialog opens (max-w-4xl, h-[640px] body — same shell as the Start new run dialog).
  3. 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).
  4. Submit → new pol-NNN id assigned, source = Custom, enabled = true, appended to local state. Page auto-jumps Source filter to Custom so the new policy is immediately visible.
  5. 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").
  6. Submit → new pol-NNN id assigned, source = Custom, enabled = true, attachedComponentIds carried through, appended to local state. Page auto-jumps Source filter to Custom so the new policy is immediately visible.
  7. 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)

  1. 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.
  2. 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.
  3. Selecting components toggles the checkbox state. Current selection count surfaces above the list ("3 components selected").
  4. Click Attach N components → picker closes, the policy's attachedComponentIds is replaced with the full selected set, and the Direct attachments list re-renders.
  5. 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)

  1. Click Start new run on the Runs page.
  2. Dialog opens with the standardized shell.
  3. Step through Source (Upload / Library / Git), Properties (env, retries, timeout, parallel, notifications), Test suite (Auto / Custom), Devices (Auto / Specific — Android only today).
  4. Click Start run → Summary screen for read-only review with Edit jump-backs to each section.
  5. Confirm & start to launch.

2.5 Inspect a run (/executions/runs/[id])

  1. From /executions/runs, click any row → routes to the run detail page. Kebab actions inside the row use stopPropagation so they don't fire the row click.
  2. Sidebar gives the run's identity and configuration at a glance; sticky on lg+.
  3. 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.
  4. 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.
  5. 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])

  1. Click a test row anywhere in the parent run's Test Execution section. Failed tests are the typical entry point.
  2. Header: back link to the parent run, large title, status pill (animated for Running), Retry button (Failed) / Re-run (terminal).
  3. For Failed tests, read the AI explanation in "Why this failed" and copy the agent-ready prompt from "Fix prompt".
  4. Open the video, scrub the logs (Expand for the full trace), read the Reproduction steps to understand what the test was trying to do.
  5. 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 in MOCK_POLICIES).
  6. 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)

  1. Land on /tests/repository — defaults to List view grouped by Priority (P0 first). View choice persists to localStorage (aucert.tests.view).
  2. 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.
  3. Click a row → navigates to /tests/repository/[id] (test detail surface — backlog; route is wired but page is placeholder).
  4. The enabled Switch flips a test's enabled flag 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)

  1. Click + New group on /tests/groups (top right) — routes to /tests/groups/new.
  2. 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.
  3. Fill Group details: name (required, ≤ 80 chars) and an optional description. Inline validation arms on first submit and stays live while the user edits.
  4. Smart: configure rules in the editor — Priority / Status / Category as ToggleChip rows, Teams / Tags / Screens as MultiSelectChips comboboxes, free-form query as 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").
  5. Manual: pick tests in the searchable list with checkboxes. At least one member is required to submit.
  6. Click Create group → assigns the next grp-NNN id, pushes the new group into MOCK_GROUPS, and routes back to /tests/groups where the new entry appears at the bottom of the list. (Mock-only persistence today; backend swap-out replaces the MOCK_GROUPS.push.)
  7. Cancel returns to /tests/groups without saving.

2.9 Edit or delete a Test Group (/tests/groups/[id])

  1. From /tests/groups, click a row → opens the detail editor.
  2. The identity strip at the top carries the type badge, group name, and a Delete button (with a confirm dialog — deletion is irreversible).
  3. 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.
  4. 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).

Every Aucert app — console, internal Astra, future portals — uses the sidebar-07 block: SidebarProviderAppSidebar (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:

SlotClass
DialogContentmax-w-4xl sm:max-w-4xl gap-0 overflow-hidden p-0
Bodyflex h-[640px] flex-col gap-6 overflow-y-auto px-8 py-6
Headerspace-y-1 border-b border-border px-8 py-5, title text-xl
Footerflex items-center justify-between border-t border-border px-8 py-4
Footer buttonsGhost Cancel on the left, primary action on the right

Reference implementations:

  • frontend/apps/console/src/app/(shell)/executions/runs/NewRunDialog.tsx
  • frontend/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:

TypeExamples
screenLogin screen, Settings → Account, Home feed
uiPassword input, Loading skeleton, Modal sheet
logAnalytics 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:

  1. 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.
  2. Direct attachments — explicit component → policy binding, managed via the AttachComponentsDialog (reachable from both CreatePolicyDialog and 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) — full bg-primary + softer bg-danger/85 for pass/fail. Used for the hero overall progress bar.
  • softbg-chart-3 + bg-danger-light for 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 (ThemeToggle works pre-authentication), form vertically centered.
  • Right column (≥lg only) — 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/:

FileCovers
.tasks/backlog/policies-followups.mdPolicies 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> off main.
  • After merge, immediately fork the next branch from the latest main.
  • Update from main before 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-state flips to "closed" and the exit animation runs, but the element stays mounted with the body still pointer-events: none and scroll-locked. Affects NewRunDialog (on main) and CreatePolicyDialog (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:

  1. New feature — add a row to §1 and a workflow to §2.
  2. New surface convention (layout pattern, dialog shape, color role) — add a subsection to §3.
  3. New local-dev step (env var, build flag, ports) — add to §4.
  4. Bump the last_updated frontmatter at the top.

When in doubt: would a teammate joining tomorrow need this to understand the product? If yes, write it down here.