Files
git.stella-ops.org/src/Web/StellaOps.Web/AGENTS.md
master c7cd5234bc Document zero-restart UI dev workflow in AGENTS.md
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>
2026-03-29 00:55:17 +02:00

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-test builder (Jasmine compatibility shim in src/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/branding and 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, line
  • CompressedPath - Compact path representation
  • DriftedSink - Sink with reachability change and cause
  • DriftCause - 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.md
  • docs/technical/architecture/console-admin-rbac.md
  • docs/technical/architecture/console-branding.md

Working Agreement

    1. Update task status to DOING/DONE in both correspoding sprint file /docs/implplan/SPRINT_*.md when you start or finish work.
    1. Review this charter and the Required Reading documents before coding; confirm prerequisites are met.
    1. Keep changes deterministic (stable ordering, timestamps, hashes) and align with offline/air-gap expectations.
    1. Coordinate doc updates, tests, and cross-guild communication whenever contracts or workflows change.
    1. Revert to TODO if you pause the task without shipping changes; leave notes in commit/PR descriptions for context.

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 for ng serve without 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> with variant="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 into stella-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 use text-overflow: ellipsis

Promote Button Convention (MANDATORY)

The Promote button on release detail pages must follow a three-state model:

  1. Hidden — no further promotion path exists (single-environment release, or already at the final environment in the promotion graph)
  2. 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
  3. 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.

All pages that include navigational quick links must follow these rules:

  1. Use the <stella-quick-links> component — never raw <nav> with dot separators
  2. Pass layout="aside" for page-level quick links (use inline only for inline/contextual links)
  3. Include a description for every link explaining what the user will find there
  4. Place quick links in a right-aligned aside panel with a border and elevated background
  5. 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 card
  • shared/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 route is 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:

  1. Wrapper (*-lane-wrapper) — relative-positioned container with ::before/::after gradient pseudo-elements
  2. Scroll container (*-lane) — flex row with overflow-x: auto, hidden scrollbar, scroll-behavior: smooth
  3. Cards — fixed-width (280px) flex items with flex-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> with variant="warning" and ViewChild ref, 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, .apc cards)
  • 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 (icon field — SVG path d attribute, multi-path via |||)
  • Labels should be short (1-2 words)
  • Use status for warn/error indicators, badge for 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 navigation
  • src/styles/_tables.scss — global .stella-table CSS 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--bordered for tables that are the main page content
  • Use stella-table--striped for tables with more than 5 rows
  • Loading state must show skeleton rows, not a spinner

Filter Convention (MANDATORY)

Three filter component types:

  1. stella-filter-chip — Single-select dropdown (Region, Env, Stage, Type, Gate, Risk)
  2. stella-filter-multi — Multi-select with checkboxes + All/None (Severity, Status)
  3. stella-view-mode-switcher — Binary toggle (Operator/Auditor, view modes)

Components:

  • shared/components/stella-filter-chip/stella-filter-chip.component.ts — single-select
  • shared/components/stella-filter-multi/stella-filter-multi.component.ts — multi-select
  • shared/components/view-mode-switcher/view-mode-switcher.component.ts — binary toggle
  • shared/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-chip or stella-filter-multi inline above the table
  • Use app-filter-bar when 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:

  1. stella-filter-chip — Single-select dropdown (Region, Env, Stage, Type, Gate, Risk)
  2. stella-filter-multi — Multi-select with checkboxes + All/None (Severity, Status)
  3. 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.