Add "Local UI Development" section with instructions for using the docker-compose.dev-ui.yml override. Agents working on UI changes should use this to avoid the slow volume-copy + restart cycle. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
20 KiB
StellaOps Web Frontend
Mission
Design and build the StellaOps web user experience that surfaces backend capabilities (Authority, Concelier, Exporters) through an offline-friendly Angular application.
Team Composition
- UX Specialist ??? defines user journeys, interaction patterns, accessibility guidelines, and visual design language.
- Angular Engineers ??? implement the SPA, integrate with backend APIs, and ensure deterministic builds suitable for air-gapped deployments.
Technology Stack
- Framework: Angular 21 (standalone components, signals, built-in control flow)
- Language: TypeScript 5.9
- UI Library: Angular Material 21 + Angular CDK 21
- State: Angular Signals
- Build:
@angular/build:application(esbuild-based) - Unit Tests: Vitest via
@angular/build:unit-testbuilder (Jasmine compatibility shim insrc/test-setup.ts) - E2E Tests: Playwright
- Storybook: Storybook 10 with
@storybook/angular - Node.js: ^20.19.0 || ^22.12.0 || ^24.0.0
Operating Principles
- Favor modular Angular architecture (feature modules, shared UI kit) with strong typing via latest TypeScript/Angular releases.
- Align UI flows with backend contracts; coordinate with Authority and Concelier teams for API changes.
- Keep assets and build outputs deterministic and cacheable for Offline Kit packaging.
- Coordinate cross-module changes via docs/implplan/SPRINT*.md files updates and PR descriptions.
- Console admin flows use Authority
/console/admin/*APIs and enforce fresh-auth for privileged actions. - Branding uses Authority
/console/brandingand applies only whitelisted CSS variables.
Key Paths
src/Web/StellaOps.Web— Angular workspace.docs/— UX specs and mockups.devops/compose/docker-compose.dev-ui.yml— Dev override for zero-restart UI development.
Local UI Development (IMPORTANT)
For UI work against the Docker stack, use the dev-ui compose override to avoid restarting the gateway container after every build:
# One-time: apply the override (bind-mounts dist/ into the gateway)
cd devops/compose
docker compose -f docker-compose.stella-ops.yml -f docker-compose.dev-ui.yml up -d router-gateway
# Build (output goes directly to gateway's wwwroot — no copy, no restart)
cd src/Web/StellaOps.Web
npx ng build --configuration=development
# Or use watch mode for continuous rebuilds:
npx ng build --configuration=development --watch
After build, just refresh the browser — the gateway serves the new files immediately.
Without the override, you must copy files into the Docker volume and restart:
docker run --rm -v compose_console-dist:/output -v "...browser:/src:ro" alpine cp -a /src/. /output/
docker restart stellaops-router-gateway
This is slow and should only be used for CI/production builds.
Reachability Drift UI (Sprint 3600)
Components
- PathViewerComponent (
app/features/reachability/components/path-viewer/) - Interactive call path visualization- Displays entrypoint ??? key nodes ??? sink paths
- Highlights changed nodes with change kind indicators
- Supports collapse/expand for long paths
- RiskDriftCardComponent (
app/features/reachability/components/risk-drift-card/) - Summary card for drift analysis- Shows newly reachable / mitigated path counts
- Displays associated CVEs
- Action buttons for drill-down
Models
PathNode- Node in a reachability path with symbol, file, lineCompressedPath- Compact path representationDriftedSink- Sink with reachability change and causeDriftCause- Explanation of why reachability changed
Services
DriftApiService(app/core/services/drift-api.service.ts) - API client for drift endpoints- Mock implementations available for offline development
Integration Points
- Scan detail page includes PathViewer for reachability visualization
- Drift results linked to DSSE attestations for evidence chain
- Path export supports JSON and SARIF formats
Witness UI (Sprint 3700) - TODO
Planned Components
- WitnessModalComponent - Modal for viewing witness details
- PathVisualizationComponent - Detailed path rendering with gates
- ConfidenceTierBadgeComponent - Tier indicators (Confirmed/Likely/Present/Unreachable)
- GateBadgeComponent - Auth gate visualization
Planned Services
witness.service.ts- API client for witness endpoints- Browser-based Ed25519 signature verification
Coordination
- Sync with DevEx for project scaffolding and build pipelines.
- Partner with Docs Guild to translate UX decisions into operator guides.
- Collaborate with Security Guild to validate authentication flows and session handling.
Required Reading
docs/modules/platform/architecture-overview.mddocs/technical/architecture/console-admin-rbac.mddocs/technical/architecture/console-branding.md
Working Agreement
-
- Update task status to
DOING/DONEin both correspoding sprint file/docs/implplan/SPRINT_*.mdwhen you start or finish work.
- Update task status to
-
- Review this charter and the Required Reading documents before coding; confirm prerequisites are met.
-
- Keep changes deterministic (stable ordering, timestamps, hashes) and align with offline/air-gap expectations.
-
- Coordinate doc updates, tests, and cross-guild communication whenever contracts or workflows change.
-
- Revert to
TODOif you pause the task without shipping changes; leave notes in commit/PR descriptions for context.
- Revert to
No Mockups Convention (MANDATORY)
All UI components must connect to real backend API endpoints. Never use window.confirm(), mock data services, or stub implementations unless explicitly requested by the product owner.
Rules:
- Every button, form submission, and action handler must call a real API endpoint
- If the backend endpoint doesn't exist yet, mark the task
BLOCKED— do not create a mock - The in-memory mock clients (e.g.,
InMemoryApprovalClient) exist ONLY forng servewithout backends, never as production implementations - Error states from API failures must be surfaced to the user (never silently swallow errors)
- If an API returns 404/500, show the error in a banner or toast — don't pretend the action succeeded
Destructive Action Convention (MANDATORY)
All destructive actions (delete, revoke, purge, reset) must use <app-confirm-dialog> — never window.confirm() or unguarded inline handlers.
Rules:
- Every destructive button must open a styled
<app-confirm-dialog>withvariant="danger"before executing - The confirm dialog message must name the resource being destroyed (e.g., "Delete script 'Pre-deploy Health Check'?")
- Include "This cannot be undone." or equivalent irreversibility warning when applicable
- Never perform destructive API calls directly from a
(click)handler without confirmation
Pattern:
<app-confirm-dialog #deleteConfirm
title="Delete Script"
[message]="'Delete script \\'' + item.name + '\\'? This cannot be undone.'"
confirmLabel="Delete" cancelLabel="Cancel" variant="danger"
(confirmed)="executeDelete()" />
Truncated Text Convention (MANDATORY)
All text that may be truncated by CSS (text-overflow: ellipsis, table cell overflow, or max-width constraints) must have a [title] attribute binding to the full untruncated text.
Rules:
- Table cells with descriptions, names, digests, or IDs that truncate must include
[title]="fullValue" - Metric card labels already have
[title]="label"(built intostella-metric-card) — do not add a second one - For dynamically computed truncation, prefer
[title]over custom tooltip directives for simplicity and offline compatibility - Ensure tooltips are also applied to
<span>and<code>elements inside table cells when they usetext-overflow: ellipsis
Promote Button Convention (MANDATORY)
The Promote button on release detail pages must follow a three-state model:
- Hidden — no further promotion path exists (single-environment release, or already at the final environment in the promotion graph)
- Disabled — promotion path exists but preconditions are not met:
- Release is not yet deployed on the current environment (status =
draft) - Blocking gates are unresolved
- Tooltip must explain why promotion is disabled
- Release is not yet deployed on the current environment (status =
- Enabled — release is deployed, gates are clear, and a next environment exists
Use showPromote (computed, boolean) for visibility and canPromote (computed, boolean) for the enabled/disabled state.
Use promoteDisabledReason (computed, string | null) for the disabled tooltip.
Quick Links Convention (MANDATORY)
All pages that include navigational quick links must follow these rules:
- Use the
<stella-quick-links>component — never raw<nav>with dot separators - Pass
layout="aside"for page-level quick links (useinlineonly for inline/contextual links) - Include a
descriptionfor every link explaining what the user will find there - Place quick links in a right-aligned aside panel with a border and elevated background
- Include a
label(e.g., "Related", "Quick Links", "Shortcuts")
Pattern:
<aside class="page-aside">
<stella-quick-links
[links]="quickLinks"
label="Quick Links"
layout="aside" />
</aside>
Each link must have label, route, and description:
{ label: 'Security Posture', route: '/security', description: 'Risk posture and advisory freshness' }
Metric / KPI Cards Convention (MANDATORY)
All metric badges, stat cards, KPI tiles, and summary indicators must use <stella-metric-card>.
Do NOT create custom .stat-card, .summary-card, .kpi-card, or .posture-card elements.
Components:
shared/components/stella-metric-card/stella-metric-card.component.ts— individual cardshared/components/stella-metric-card/stella-metric-grid.component.ts— responsive grid wrapper
Usage:
<stella-metric-grid [columns]="3">
<stella-metric-card label="Risk Posture" [value]="riskLevel()" subtitle="6 findings in scope"
icon="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" route="/triage/artifacts" />
<stella-metric-card label="VEX Coverage" value="0%" subtitle="0/0 findings covered" />
</stella-metric-grid>
Design rules:
- Cards are uncolored — no severity/status color backgrounds
- Icon is mandatory (SVG path d, multi-path via
|||) - Subtitle is 5-10 words explaining the metric
- If
routeis set: card is clickable with hover lift + arrow - If no
route: static display, no hover effect
Horizontal Scroll Card Lane Pattern (Reusable)
A reusable pattern for displaying actionable items as horizontally scrollable cards with gradient fades and scroll arrows. Used in the dashboard (environment cards, pending actions) and the approvals inbox (approval cards with inline actions).
Architecture
Three layers:
- Wrapper (
*-lane-wrapper) — relative-positioned container with::before/::aftergradient pseudo-elements - Scroll container (
*-lane) — flex row withoverflow-x: auto, hidden scrollbar,scroll-behavior: smooth - Cards — fixed-width (
280px) flex items withflex-shrink: 0
Scroll arrow signals (TypeScript)
@ViewChild('myScroll') myScrollRef?: ElementRef<HTMLDivElement>;
readonly showLeftArrow = signal(false);
readonly showRightArrow = signal(false);
onScroll(): void { this.updateArrows(); }
scrollCards(direction: 'left' | 'right'): void {
this.myScrollRef?.nativeElement?.scrollBy({ left: direction === 'left' ? -300 : 300, behavior: 'smooth' });
}
private updateArrows(): void {
const el = this.myScrollRef?.nativeElement;
if (!el) { this.showLeftArrow.set(false); this.showRightArrow.set(false); return; }
this.showLeftArrow.set(el.scrollLeft > 1);
this.showRightArrow.set(el.scrollWidth - el.scrollLeft - el.clientWidth > 1);
}
Gradient fades (CSS)
.my-wrapper.can-scroll-left::before {
content: ''; position: absolute; top: 0; left: 0; bottom: 0; width: 56px;
background: linear-gradient(to right, var(--color-surface-primary) 0%, transparent 100%);
pointer-events: none; z-index: 1;
}
.my-wrapper.can-scroll-right::after { /* mirror for right side */ }
Confirmation dialogs for card actions
- Production approve:
<app-confirm-dialog>withvariant="warning"andViewChildref, call.open()programmatically - Reject with reason: Custom inline dialog overlay with
<textarea [(ngModel)]>for optional reason - Detail popup:
<app-modal size="lg">— shows summary immediately, loads full detail via API on open
Reference implementations
- Dashboard environment cards:
features/dashboard-v3/dashboard-v3.component.ts(.env-grid-wrapper) - Approvals inbox cards:
features/approvals/approvals-inbox.component.ts(.cards-lane-wrapper,.apccards) - Stella Action Card List:
shared/components/stella-action-card/(simpler variant without arrows)
Tab Navigation Convention (MANDATORY)
All page-level tab navigation must use <stella-page-tabs>.
Do NOT create custom .tabs, .tab-navigation, .tab-button, or nav[role="tablist"] elements.
Do NOT use the app-tabs / TabsComponent for page tabs (that component is for inline content tabs only).
Component: shared/components/stella-page-tabs/stella-page-tabs.component.ts
Usage (signal-based, no router):
<stella-page-tabs
[tabs]="tabs"
[activeTab]="activeTab()"
(tabChange)="activeTab.set($any($event))"
ariaLabel="Section tabs"
>
@switch (activeTab()) {
@case ('first') { <my-first-panel /> }
@case ('second') { <my-second-panel /> }
}
</stella-page-tabs>
Usage (router-based):
<stella-page-tabs
[tabs]="pageTabs"
[activeTab]="activeTab()"
ariaLabel="Page tabs"
(tabChange)="onTabChange($event)"
>
<router-outlet></router-outlet>
</stella-page-tabs>
Tab definition:
const MY_TABS: readonly StellaPageTab[] = [
{ id: 'overview', label: 'Overview', icon: 'M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z' },
{ id: 'details', label: 'Details', icon: 'M14 2H6a2...', status: 'warn', statusHint: '3 issues' },
];
Design rules:
- Every tab MUST have an SVG icon (
iconfield — SVG pathdattribute, multi-path via|||) - Labels should be short (1-2 words)
- Use
statusfor warn/error indicators,badgefor counts - Do NOT duplicate the tab bar styling — the component owns all tab CSS
- The component provides: keyboard navigation, ARIA roles, active background, bottom border, icon opacity transitions, panel border-radius, enter animation
Data Table Convention (MANDATORY)
All data tables must use <app-data-table> for interactive tables or .stella-table CSS classes for simple static tables.
Do NOT create custom table styling, sort headers, or selection checkboxes.
Components:
shared/components/data-table/data-table.component.ts— interactive table (sorting, selection, templates)shared/components/pagination/pagination.component.ts— page navigationsrc/styles/_tables.scss— global.stella-tableCSS classes
Usage (interactive table with sorting + pagination):
<app-data-table
[columns]="columns"
[data]="pagedItems()"
[loading]="loading()"
[striped]="true"
[hoverable]="true"
[selectable]="false"
emptyTitle="No items found"
emptyDescription="Adjust filters or create a new item."
(sortChange)="onSortChange($event)"
(rowClick)="onRowClick($event)"
>
<div tablePagination>
<app-pagination
[total]="totalItems()"
[currentPage]="currentPage()"
[pageSize]="pageSize()"
[pageSizes]="[5, 10, 25, 50]"
(pageChange)="onPageChange($event)"
/>
</div>
</app-data-table>
Column definition with custom cell templates:
readonly columns: TableColumn<MyItem>[] = [
{ key: 'name', label: 'Name', sortable: true },
{ key: 'status', label: 'Status', sortable: true, template: statusTemplate },
{ key: 'actions', label: 'Actions', sortable: false, align: 'right' },
];
Usage (simple static table):
<table class="stella-table stella-table--striped stella-table--hoverable stella-table--bordered">
<thead><tr><th>Name</th><th>Value</th></tr></thead>
<tbody><tr><td>...</td><td>...</td></tr></tbody>
</table>
Design rules:
- Pagination must be right-aligned below the table
- Page size options must include 5:
[5, 10, 25, 50] - All list/catalog pages must be sortable by at least the primary column
- Use
stella-table--borderedfor tables that are the main page content - Use
stella-table--stripedfor tables with more than 5 rows - Loading state must show skeleton rows, not a spinner
Filter Convention (MANDATORY)
Three filter component types:
stella-filter-chip— Single-select dropdown (Region, Env, Stage, Type, Gate, Risk)stella-filter-multi— Multi-select with checkboxes + All/None (Severity, Status)stella-view-mode-switcher— Binary toggle (Operator/Auditor, view modes)
Components:
shared/components/stella-filter-chip/stella-filter-chip.component.ts— single-selectshared/components/stella-filter-multi/stella-filter-multi.component.ts— multi-selectshared/components/view-mode-switcher/view-mode-switcher.component.ts— binary toggleshared/ui/filter-bar/filter-bar.component.ts— combined search + dropdown filters + active chips
Usage (filter chips for page-level filters):
<stella-filter-chip
label="Status"
[value]="statusFilter()"
[options]="statusOptions"
(valueChange)="statusFilter.set($event)"
/>
<stella-filter-multi
label="Severity"
[options]="severityOptions()"
(optionsChange)="onSeverityChange($event)"
/>
Usage (filter bar with search + dropdowns):
<app-filter-bar
searchPlaceholder="Search..."
[filters]="filterOptions"
[activeFilters]="activeFilters()"
(searchChange)="searchQuery.set($event)"
(filterChange)="onFilterAdded($event)"
(filterRemove)="onFilterRemoved($event)"
(filtersCleared)="clearAllFilters()"
/>
Design rules:
- Global filters (Region, Env, Window, Stage, Operator/Auditor) live in the header bar only
- Pages must NOT duplicate global filters — read from PlatformContextStore
- Page-level filters use
stella-filter-chiporstella-filter-multiinline above the table - Use
app-filter-barwhen search + multiple dropdowns + active chips are needed - Compact inline chips: 28px height, no border default, dropdown on click
Filter container overflow (CRITICAL):
Filter containers that hold stella-filter-chip or stella-filter-multi MUST use
overflow: visible so dropdown panels are not clipped. Do NOT use overflow-x: auto
or overflow: hidden on filter row containers — this clips the absolute-positioned
dropdown panels (z-index: 200) below the fold.
/* CORRECT — dropdowns escape the container */
.filters { display: flex; flex-wrap: wrap; overflow: visible; gap: 0.5rem; }
/* WRONG — clips dropdown panels */
.filters { display: flex; flex-wrap: nowrap; overflow-x: auto; }
The topbar header row uses overflow: visible for this reason — all page-level
filter rows must follow the same pattern.
Filter Convention (MANDATORY)
Three filter component types:
stella-filter-chip— Single-select dropdown (Region, Env, Stage, Type, Gate, Risk)stella-filter-multi— Multi-select with checkboxes + All/None (Severity, Status)stella-view-mode-switcher— Binary toggle (Operator/Auditor, view modes)
Global filters (Region, Env, Window, Stage, Operator/Auditor) live in the header bar only. Pages must NOT duplicate global filters. Read from PlatformContextStore.
Design: Compact inline chips, 28px height, no border default, dropdown on click.