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/scansRuns 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/scans/[id]First-class feature — see §1.2
Executions — Test detail/executions/scans/[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
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:

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.4 for the workflows that drive this feature.

1.2 Run detail (first-class)​

/executions/scans/[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/scans/[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/scans/_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. Names and roles are matched 1:1 to the marketing site's agent roster (frontend/apps/website/src/content/site.ts intro.agents) so the same character means the same thing on aucert.ai and in the console; internal L-layer names live only in the cascade-trace expand and are not part of that parity.

PersonaRoleInternal
GenieDesignL1 Generation
EmberProductL2 Execution
CleoComplianceL3 Analysis
DaxSecurityL4 Decision
IrisAccessibilityL5 Reporting
RioEngineeringRover 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 Retired: the Tests surface​

/tests/repository, /tests/groups, and /tests/studio were deleted (commit c9b872d2). They predated SPEC-078's expectation-validation model and were entirely mock/fixture-driven — zero API calls, no backend behind them. Expectations replace them: a Policy (§1.1) is the rule, an expectation is the checkable claim derived from it, and a validation run produces verdicts (§1.3). There is no separate authored "test" object in the model any more.

One idea from that surface is carried forward: the Smart group — a group defined by a filter, whose membership is recomputed at read time so newly authored items join automatically. Its successor is the saved expectation set (.context/GLOSSARY.md → "Expectation validation (SPEC-078)"): a named, saved filter over the expectation catalog, never a saved list of ids. The Manual group flavour (hand-picked, fixed membership) was deliberately not carried forward — a saved expectation set is always filter-backed.

The scan composer's old "Test cases" settings group went with the routes; its successor is Validation scope — see §1.6.

1.6 Validation scope + saved expectation sets (first-class)​

The scan composer's SettingsMenu Validation scope group answers "what should this scan check?". Source: frontend/apps/console/src/components/scan-composer/. Like every other composer group, it defaults to Auto — Aucert selects, so the badge counts it as non-default only once you override it.

OptionMeaning
Auto — Aucert selects (default)Aucert picks the breadth
EverythingEvery expectation in the catalog
P0 onlyScheduling breadth — the release-blocking tier
Saved setsOne saved filter, listed by name + current match count
Manage setsQuiet link out to /expectations

Everything and P0 only are string sentinels (VALIDATION_SCOPE_EVERYTHING / VALIDATION_SCOPE_P0), not saved sets — they're breadth choices, so they deliberately sit outside the fixture and can never collide with a real eset-* id.

A saved expectation set is a named, saved filter over the expectation catalog — never a saved list of expectation ids. The type carries the full predicate (SavedExpectationSetFilter: anchorKinds, policyHandles, priorities, tags — all independently optional, any-of within each field) even though the composer's preview only renders name and matchCount. That's deliberate: the shape alone makes it unambiguous that picking a set scopes validation to whatever currently matches this rule, not to N frozen expectations. Membership is computed at read time, so a newly authored or newly materialized expectation that matches joins automatically — nobody maintains membership by hand.

This is the one idea carried forward from the retired Test Groups (§1.5): the Smart group was filter-defined and auto-maintaining, and that is what a saved expectation set is. The Manual group flavour (hand-picked, fixed membership) was deliberately dropped — a saved expectation set is always filter-backed. Vocabulary matches .context/GLOSSARY.md → "Expectation validation (SPEC-078)".

Mock-only today: MOCK_SAVED_EXPECTATION_SETS in data.ts carries real policy handles from policies/_data/policies.ts so the filters read as plausible rules, but matchCount is a static number — nothing in the composer computes membership live (there's no expectation catalog wired into it). The full manage/save-a-filter UI lands on /expectations.

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.4 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.5 Start a new run (/executions/scans → 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.6 Inspect a run (/executions/scans/[id])​

  1. From /executions/scans, 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.7 Inspect a test case (/executions/scans/[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).

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

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/scans/NewScanDialog.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.
  • soft — bg-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/scans/[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/scans/[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/scans, /executions/scans/[id], and /executions/scans/[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 NewScanDialog (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.