` items — fake
+- Activity section: 3 hardcoded HTML cards — fake
+- **Zero API calls to real backends**
+
+### P2: Layout has no hierarchy
+Current vertical order: Summary strip → Environment grid → Risk table → 3-card row (SBOM + Reachability + Ops Signals) → Alerts → Activity → Domain nav.
+
+This is 7 sections stacked vertically. A user scrolls through 3+ screens of content with no visual priority. The most critical information (am I blocked?) competes with the least actionable (domain navigation links).
+
+### P3: No data source distinction
+Dashboard shows "5 critical findings" and "blocked" but the security posture page shows "0 findings". The user can't tell what's real. There's no "last updated" timestamp, no data source indicator, no "demo data" badge.
+
+### P4: Duplicate information
+- Environment grid AND risk table show the same environments with the same metrics
+- SBOM card recalculates stats from the same environment data shown in the grid
+- Reachability percentages are shown per-environment (B/I/R column) AND as aggregate (Reachability card)
+
+---
+
+## Proposed Layout: 3-Column Mission Board
+
+```
+┌─────────────────────────────────────────────────────────────────────┐
+│ Dashboard — Mission Board for [Demo Production] [Refresh] │
+│ Last updated: 15 Mar 2026, 23:15 UTC │
+├───────────────────────┬─────────────────────────────────────────────┤
+│ │ │
+│ SECURITY POSTURE │ ENVIRONMENTS & ACTIONS │
+│ (1/3 width) │ (2/3 width) │
+│ │ │
+│ ┌─────────────────┐ │ ┌─────────────────────────────────────────┐│
+│ │ VULNERABILITY │ │ │ PROMOTION PIPELINE ││
+│ │ SUMMARY │ │ │ ││
+│ │ │ │ │ [env cards in promotion order: ││
+│ │ Critical: 12 │ │ │ Dev → Stage → Prod per region] ││
+│ │ High: 34 │ │ │ ││
+│ │ Medium: 89 │ │ │ Blocked: prod-us-east (5 crit) ││
+│ │ Low: 156 │ │ │ Degraded: staging (stale SBOM) ││
+│ │ │ │ │ Healthy: dev, prod-eu-west ││
+│ │ [severity donut │ │ │ ││
+│ │ or bar chart] │ │ └─────────────────────────────────────────┘│
+│ │ │ │ │
+│ │ Reachable: 9 │ │ ┌─────────────────────────────────────────┐│
+│ │ Unreachable: 47 │ │ │ NEEDS YOUR ATTENTION ││
+│ │ Unknown: 23 │ │ │ ││
+│ │ │ │ │ ⚠ 3 approvals blocked (evidence stale) ││
+│ └─────────────────┘ │ │ ⚠ 2 waivers expiring in 24h ││
+│ │ │ ⚠ 1 promotion blocked by policy gate ││
+│ ┌─────────────────┐ │ │ 🔴 Feed freshness degraded ││
+│ │ SBOM HEALTH │ │ │ ││
+│ │ │ │ │ [each item links to the action page] ││
+│ │ Components: 247 │ │ └─────────────────────────────────────────┘│
+│ │ Fresh: 231 │ │ │
+│ │ Stale: 12 │ │ ┌─────────────────────────────────────────┐│
+│ │ Missing: 4 │ │ │ RECENT ACTIVITY (real events) ││
+│ │ │ │ │ ││
+│ │ B/I/R Coverage │ │ │ • admin sealed "API Gateway v2.1" ││
+│ │ B: 72% │ │ │ • Policy gate blocked prod-us-east ││
+│ │ I: 88% │ │ │ • NVD feed synced (142 new advisories) ││
+│ │ R: 61% │ │ │ • Doctor check: 1 fail, 9 warn ││
+│ └─────────────────┘ │ │ ││
+│ │ │ [live event stream, not static cards] ││
+│ ┌─────────────────┐ │ └─────────────────────────────────────────┘│
+│ │ ADVISORY FEEDS │ │ │
+│ │ │ │ │
+│ │ Sources: 55/75 │ │ │
+│ │ Healthy: 55 │ │ │
+│ │ Failed: 18 │ │ │
+│ │ Last sync: 2m │ │ │
+│ │ │ │ │
+│ │ [Configure] │ │ │
+│ └─────────────────┘ │ │
+│ │ │
+├───────────────────────┴─────────────────────────────────────────────┤
+│ PLATFORM HEALTH (full width footer bar) │
+│ │
+│ Services: 63/63 ✓ │ DB: healthy │ Events: CONNECTED │ Doctor: 7/1/1│
+│ Feed: Live │ Evidence: ON │ Offline: OK │ DLQ: 3 │
+└─────────────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## Column 1 (1/3): Security Posture At-a-Glance
+
+**Purpose**: Answer "Is my estate safe?" without leaving the dashboard.
+
+### Section 1A: Vulnerability Summary
+**Data source**: `GET /api/v1/findings/summary` (findings ledger) or `GET /api/v1/scanner/summary` (scanner service)
+
+What to show:
+- **Severity breakdown**: Critical / High / Medium / Low counts
+- **Reachability breakdown**: Reachable / Unreachable / Unknown counts
+- **Donut chart** or horizontal stacked bar colored by severity
+- **Trend arrow** (↑/↓/→) compared to 24h ago
+- **Link**: "Open Findings" → `/security/triage`
+
+Why this matters:
+- This is the #1 thing a security auditor looks at
+- Currently the dashboard shows "5 critical" per environment but it's all fake
+- Real data from the findings ledger makes this trustworthy
+
+### Section 1B: SBOM Health
+**Data source**: `GET /api/v1/scanner/sbom/summary` or computed from environment scan state
+
+What to show:
+- **Total components tracked** across all environments
+- **Freshness breakdown**: Fresh / Stale / Missing
+- **B/I/R reachability coverage bars** (the existing bar chart, but from real data)
+- **Link**: "View Supply Chain" → `/security/supply-chain-data`
+
+### Section 1C: Advisory Feed Status
+**Data source**: `GET /api/v1/advisory-sources/status` (Concelier source management)
+
+What to show:
+- **Sources active**: X of Y enabled
+- **Healthy / Failed** count
+- **Last sync timestamp**
+- **Link**: "Configure Sources" → `/setup/integrations/advisory-vex-sources`
+
+Why this matters:
+- Advisory freshness directly affects vulnerability accuracy
+- If feeds are stale, findings are stale, decisions are wrong
+- This was the 55/75 healthy we discovered in the audit
+
+---
+
+## Column 2 (2/3): Environments & Actions
+
+**Purpose**: Answer "What needs my attention?" and "What's the deployment state?"
+
+### Section 2A: Promotion Pipeline
+**Data source**: `GET /api/v1/platform/context/environments` (already loads via PlatformContextStore) + real scan/promotion state
+
+What to show:
+- Environment cards **in promotion order** (Dev → Stage → Prod, grouped by region)
+- Each card: name, region, deploy status badge, SBOM freshness, critical findings count, pending approvals
+- **Blocked environments highlighted at top** with red border
+- **Actions per card**: Detail, Findings, Promote, Approve
+- **Real metrics** from scan/promotion APIs, NOT `resolveStatusSeed()`
+
+Layout: Same card grid as current, but driven by real data.
+
+### Section 2B: Needs Your Attention
+**Data source**: Multiple — approvals API, waivers API, promotions API, notifications API
+
+What to show:
+- **Actionable items only** — things the user MUST do:
+ - Pending approvals (with count and reason)
+ - Expiring waivers (with countdown)
+ - Blocked promotions (with blocking reason: policy gate, evidence freshness, etc.)
+ - Feed degradation alerts
+- Each item is a **clickable link** to the action page
+- **NOT static HTML** — real data from APIs
+
+Why this matters:
+- The current "Alerts" section is 3 hardcoded `
` elements
+- A real operator needs to see: "You have 3 things to do today"
+
+### Section 2C: Recent Activity (Live Stream)
+**Data source**: `GET /api/v1/timeline/events` or WebSocket event stream (already have "Events: CONNECTED" in topbar)
+
+What to show:
+- **Real events**, most recent first:
+ - Release sealed/promoted/rolled back
+ - Policy gate pass/block
+ - Advisory feed sync (with advisory count)
+ - Doctor check results
+ - User actions (approval, waiver, exception)
+- **Live updates** via the event stream (already connected)
+- Maximum 10 items, with "View full activity" link → `/evidence/audit-log`
+
+Why this matters:
+- Current "Recent Activity" is 3 static cards with text descriptions — not actual activity
+- The topbar already shows "Events: CONNECTED" — the live stream is available
+
+---
+
+## Footer Bar: Platform Health
+
+**Data source**: `GET /api/v1/platform/health` or `GET /api/v1/doctor/last-run/summary`
+
+What to show (single horizontal bar, always visible):
+- **Services**: 63/63 healthy (or X unhealthy)
+- **DB**: healthy/degraded
+- **Events**: Connected/Degraded
+- **Doctor**: 7 pass / 1 warn / 1 fail (link to `/ops/operations/doctor`)
+- **Feed**: Live/Stale
+- **Evidence**: ON/OFF
+- **Offline**: OK/Sealed
+- **DLQ**: N items (link to `/ops/operations/dead-letter`)
+
+Why this matters:
+- The topbar already shows some of these (Events, Policy, Evidence, Feed, Offline)
+- But the dashboard should also show system health — an operator wants to know "is the platform itself healthy?" at a glance
+- Doctor results from the last run are critical operational context
+
+---
+
+## What to Remove
+
+1. **Risk table** — duplicate of the environment grid. Merge into a single view.
+2. **Domain navigation links** (Release Runs, Security & Risk, Platform, Evidence, Platform Setup) — the sidebar already provides this. Dashboard space is premium.
+3. **Activity section** (3 static cards) — replace with real live activity stream
+4. **All hardcoded data** — every number must come from an API or show "No data yet"
+
+---
+
+## Empty State (Fresh Install)
+
+When the dashboard has no real data (no environments, no scans, no releases):
+
+```
+┌─────────────────────────────────────────────────────────┐
+│ Welcome to Stella Ops │
+│ │
+│ Let's set up your release control plane. │
+│ │
+│ ① Connect a registry [Setup Integrations →] │
+│ ② Define environments [Topology Wizard →] │
+│ ③ Scan your first image [Start Scan →] │
+│ ④ Create a release [Create Release →] │
+│ │
+│ ───────────────────────────────────────── │
+│ Platform Health: 63/63 services ✓ │
+│ Advisory Sources: 55/75 healthy │
+│ Doctor: Run diagnostics → [Quick Check] │
+└─────────────────────────────────────────────────────────┘
+```
+
+The empty state should guide the user through setup, not show fake crisis data.
+
+---
+
+## Data Sources Required
+
+| Dashboard Section | API Endpoint | Service |
+|---|---|---|
+| Vulnerability Summary | `GET /api/v1/findings/summary` | Findings Ledger |
+| SBOM Health | `GET /api/v1/scanner/sbom/summary` | Scanner |
+| B/I/R Reachability | `GET /api/v1/reachgraph/coverage` | ReachGraph |
+| Advisory Feed Status | `GET /api/v1/advisory-sources/status` | Concelier |
+| Environment Cards | `GET /api/v1/platform/context` | Platform (exists) |
+| Environment Scan State | `GET /api/v1/scanner/environments/summary` | Scanner |
+| Pending Approvals | `GET /api/v1/approvals?status=pending` | JobEngine |
+| Expiring Waivers | `GET /api/v1/exceptions?expiresWithin=24h` | Policy |
+| Blocked Promotions | `GET /api/v1/promotions?status=blocked` | JobEngine |
+| Recent Activity | `GET /api/v1/timeline/events?limit=10` | Timeline |
+| Platform Health | `GET /api/v1/doctor/last-run/summary` | Platform/Doctor |
+| Service Count | `GET /api/v1/platform/health` | Platform |
+| DLQ Count | `GET /api/v1/jobengine/dead-letter/summary` | JobEngine |
+
+Many of these endpoints already exist (Platform context, advisory sources status, doctor, dead-letter, jobengine). Some may need summary/aggregation endpoints.
+
+---
+
+## Implementation Approach
+
+### Phase 1: Honest Empty State (S effort)
+- Replace hardcoded data with API calls that can return empty
+- When empty: show the welcome/setup guide instead of fake data
+- When real data exists: show real data
+
+### Phase 2: 3-Column Layout (M effort)
+- Restructure the template from vertical scroll to 3-column grid
+- Left column: security posture cards
+- Right column: environments + actions + activity
+- Footer: platform health bar
+
+### Phase 3: Real API Wiring (L effort)
+- Wire each section to its real API endpoint
+- Add loading skeletons per section
+- Add "last updated" timestamps
+- Handle API errors gracefully (show error state per section, not whole page)
+
+### Phase 4: Live Activity Stream (M effort)
+- Replace static activity cards with real event stream
+- Use the existing WebSocket connection (Events: CONNECTED)
+- Show 10 most recent events with live updates
+
+---
+
+## Files to Modify
+
+| File | Change |
+|------|--------|
+| `src/Web/StellaOps.Web/src/app/features/dashboard-v3/dashboard-v3.component.ts` | Complete rewrite of template + data sources |
+| `src/Web/StellaOps.Web/src/app/core/api/` | Add dashboard summary API clients |
+| `src/Platform/StellaOps.Platform.WebService/Endpoints/` | Add dashboard aggregation endpoint |
+| `src/Web/StellaOps.Web/src/app/features/dashboard-v3/dashboard-v3.component.spec.ts` | Update tests |
diff --git a/docs/qa/DEEP_ADVISORY_MIRROR_AUDIT_20260316.md b/docs/qa/DEEP_ADVISORY_MIRROR_AUDIT_20260316.md
new file mode 100644
index 000000000..1d1768fa1
--- /dev/null
+++ b/docs/qa/DEEP_ADVISORY_MIRROR_AUDIT_20260316.md
@@ -0,0 +1,185 @@
+# Deep Advisory & Mirror Audit — Stella Ops
+
+**Date**: 2026-03-16
+**Method**: Interactive Playwright walkthrough + source code analysis
+**Focus**: Advisory source catalog, mirror architecture, mode confusion, first-time operator experience
+
+---
+
+## The Core Problem: Mirror Source vs Mirror Mode Are Disconnected
+
+Stella Ops has three advisory operating modes:
+- **Direct**: Fetch advisories from upstream sources (NVD, OSV, GHSA, etc.) directly
+- **Mirror**: Consume pre-aggregated advisory bundles from an upstream Stella Ops mirror
+- **Hybrid**: Both direct and mirror sources active
+
+The mirror feature has two roles:
+- **Producer**: Create mirror domains that bundle advisories for downstream consumers
+- **Consumer**: Connect to an upstream mirror and pull bundles
+
+**The problem**: The advisory source catalog and the mirror mode are independent systems that don't coordinate:
+
+### Issue 1: StellaOps Mirror Source Enabled by Default in Direct Mode
+
+**File**: `src/Concelier/__Libraries/StellaOps.Concelier.Core/Sources/SourceDefinitions.cs:1351-1366`
+
+The `StellaMirror` source definition has:
+```csharp
+BaseEndpoint = "https://mirror.stella-ops.org/api/v1",
+DefaultPriority = 1, // HIGHEST priority
+EnabledByDefault = true // inherited from base class
+```
+
+On a fresh self-hosted install:
+- Mode badge shows **"Direct"**
+- But "StellaOps Mirror" source is **enabled** at **priority 1** (highest)
+- Its endpoint `mirror.stella-ops.org` is an external SaaS URL that doesn't exist for self-hosted
+- Health check fails: "SSL/TLS error connecting to StellaOps Mirror"
+
+**Impact**: The highest-priority advisory source is a broken external URL. If it were reachable, it would override all local direct sources. The system silently operates with a failed priority-1 source.
+
+**Fix**: Either:
+- Set `EnabledByDefault = false` for the StellaMirror source
+- Or: gate it behind mirror mode — only enable when mode is "Mirror" or "Hybrid"
+- Or: replace the hardcoded URL with a placeholder that requires explicit configuration
+
+### Issue 2: Mirror Source Appears in Create Mirror Domain Wizard
+
+When creating a new mirror domain (producer role), the source selection wizard lists ALL sources including "StellaOps Mirror" under the Mirror (1) category. An operator can select it and create a mirror domain that sources from... the broken external mirror.
+
+**Impact**: Creates a circular or broken dependency chain. A mirror domain that sources from a nonexistent upstream.
+
+**Fix**: Exclude sources of type `StellaMirror` from the Create Mirror Domain source picker. A mirror domain should only aggregate direct-fetch sources, not other mirrors.
+
+### Issue 3: No Relationship Between Catalog Toggle and Mirror Consumer Setup
+
+Two separate ways to "use a mirror" exist:
+1. **Catalog toggle**: Enable "StellaOps Mirror" source in the advisory catalog → hardcoded to `mirror.stella-ops.org`
+2. **Mirror Consumer Wizard**: "Connect to Mirror" → 4-step wizard where you enter a custom mirror URL
+
+These are independent. Enabling the catalog mirror source does NOT invoke the consumer wizard. Completing the consumer wizard does NOT update the catalog mirror source's endpoint.
+
+**Impact**: A user who completes the consumer wizard and enters `https://my-corp-mirror.internal` still has the catalog's mirror source pointing at `mirror.stella-ops.org`. Conversely, a user who toggles the catalog source thinks they've configured mirror mode, but the system mode stays "Direct".
+
+**Fix**: Unify these:
+- When the mirror consumer wizard is completed, it should: (a) update the StellaMirror source endpoint to the configured URL, (b) enable the source, (c) switch mode to "Mirror" or "Hybrid"
+- The catalog's StellaMirror source toggle should redirect to the consumer wizard if no mirror is configured
+- Or: remove the StellaMirror source from the catalog entirely and make it purely managed through the mirror dashboard
+
+### Issue 4: "Direct" Mode Label Is Misleading When Mirror Source Is Enabled
+
+The mirror context header shows "Direct" (green badge) but a mirror source is enabled at highest priority. The mode badge doesn't reflect actual source configuration.
+
+**Fix**: Derive the mode from actual source state:
+- If only direct sources are enabled → "Direct"
+- If only mirror source is enabled → "Mirror"
+- If both → "Hybrid"
+Or at minimum, show a warning: "Mirror source is enabled but unreachable"
+
+---
+
+## Advisory Source Health: Fresh Install Reality
+
+After "Check All" on a fresh install:
+- **55 healthy** (sources reachable from Docker network)
+- **18 failed** (unreachable from Docker network or nonexistent)
+
+**Failed sources by category:**
+| Source | Likely Reason |
+|--------|--------------|
+| Oracle Security | DNS/network from Docker |
+| npm/PyPI/RubyGems/Maven/Packagist/Hex.pm Advisories (6) | OSV-based, likely endpoint differences |
+| CERT-In (India) | Geo-restricted or slow |
+| FSTEC BDU (Russia) | Geo-restricted |
+| CSAF Aggregator | Endpoint format |
+| VEX Hub | Not a real endpoint yet |
+| MITRE D3FEND | Endpoint format |
+| Exploit-DB | Likely rate-limited |
+| Docker Official CVEs | Endpoint format |
+| AMD Security | DNS/network |
+| Siemens ProductCERT | Endpoint format |
+| Ruby Advisory DB | Endpoint format |
+| StellaOps Mirror | Nonexistent external URL |
+
+**Finding**: 74 out of 75 sources are enabled by default (`EnabledByDefault = true`). On a fresh install, 18 immediately fail health checks. The stats bar shows "55 enabled, 55 healthy, 19 failed" — but this is confusing because "55 enabled" should be 74 (or 75). The enabled count in the stats bar and the actual enabled count don't match.
+
+**Fix**:
+- Don't enable all 75 sources by default. Enable a curated set (Primary 4 + major vendors + major distributions = ~20-25 sources)
+- Disable ecosystem-specific sources (npm, PyPI, etc.) by default — let users enable for their stack
+- Disable geo-restricted sources (FSTEC, NKCKI) by default
+- Show a "Recommended for your platform" selection during first-time setup
+
+---
+
+## Mirror Domain Builder: Good UX, Missing Guardrails
+
+The 3-step domain builder wizard is well-designed:
+1. **Select Sources** — categorized picker with quick-select buttons, search, selection summary
+2. **Configure Domain** — auto-generated ID/name, 5 export formats (JSON/JSONL/OpenVEX/CSAF/CycloneDX), rate limits, auth, signing
+3. **Review & Create** — summary + JSON filter preview + "generate immediately" option
+
+**Good**:
+- Auto-generated domain ID from selection (`mirror-primary-4src`)
+- Multiple export formats including OpenVEX and CSAF
+- Rate limiting per domain
+- Optional signing and authentication
+- "Generate immediately" checkbox
+
+**Issues**:
+- Can select "StellaOps Mirror" as a source (circular, see Issue 2)
+- No indication of which sources are actually healthy — I might build a domain from 4 sources where 2 are failing
+- No estimate of bundle size or generation time
+
+---
+
+## Mirror Consumer Wizard: Clean but Disconnected
+
+The 4-step consumer wizard:
+1. **Connect** — Enter mirror base URL + test connection
+2. **Signature** — Verify signing key
+3. **Sync & Mode** — Schedule + mode selection
+4. **Review & Activate** — Confirm
+
+**Good**:
+- Proper signature verification step
+- "Test Connection" button
+- Mode selection in the workflow
+
+**Issues**:
+- Completing this wizard doesn't update the catalog's StellaMirror source (see Issue 3)
+- Placeholder URL `https://mirror.stella-ops.org` is the same broken SaaS URL as the catalog source
+- No way to test with the local instance's own mirror domains (self-mirroring for verification)
+
+---
+
+## Recommended Architecture Changes
+
+### Short-term (S/M effort):
+1. **Set `EnabledByDefault = false` for StellaMirror source** — prevents broken priority-1 source on fresh install
+2. **Exclude StellaMirror from Create Domain wizard** — prevents circular mirror chains
+3. **Curate default-enabled sources** — only enable Primary + top 15 vendor/distribution sources by default
+4. **Fix stats bar count** — "enabled" count should match actual enabled sources
+5. **Show health status in Create Domain source picker** — badge healthy/failed next to each source
+
+### Medium-term (L effort):
+6. **Unify catalog mirror source and consumer wizard** — catalog toggle should invoke wizard, wizard should update catalog
+7. **Derive mode from source state** — "Direct"/"Mirror"/"Hybrid" computed from actual enabled sources
+8. **Add "Recommended sources" first-run flow** — during setup, suggest sources based on detected platform (Docker → Container sources, Python → PyPI, etc.)
+
+### Long-term (XL effort):
+9. **Mirror domain ↔ consumer federation** — Instance A's domain auto-discoverable by Instance B's consumer wizard via DNS SRV or well-known URL
+10. **Self-test mirror consumer** — "Connect to localhost" option that validates the producer flow by consuming from own mirror domains
+
+---
+
+## Files to Modify
+
+| Fix | File | Change |
+|-----|------|--------|
+| #1 | `src/Concelier/__Libraries/StellaOps.Concelier.Core/Sources/SourceDefinitions.cs:1351` | Add `EnabledByDefault = false` to StellaMirror |
+| #2 | `src/Web/StellaOps.Web/src/app/features/integrations/advisory-vex-sources/mirror-domain-builder.component.ts` | Filter out sources where `category === 'Mirror'` from the picker |
+| #3 | `src/Concelier/__Libraries/StellaOps.Concelier.Core/Sources/SourceDefinitions.cs` | Set `EnabledByDefault = false` on ecosystem/geo-restricted sources |
+| #4 | `src/Web/StellaOps.Web/src/app/features/integrations/advisory-vex-sources/advisory-source-catalog.component.ts` | Fix enabled count computation |
+| #5 | `src/Web/StellaOps.Web/src/app/features/integrations/advisory-vex-sources/mirror-domain-builder.component.ts` | Show health badge next to source names |
+| #6 | Multiple files | Wire consumer wizard completion to update catalog source + mode |
+| #7 | `src/Concelier/StellaOps.Concelier.WebService/Extensions/MirrorDomainManagementEndpointExtensions.cs` | Compute mode from source state |
diff --git a/docs/qa/FIRST_TIME_USER_AUDIT_20260316.md b/docs/qa/FIRST_TIME_USER_AUDIT_20260316.md
new file mode 100644
index 000000000..b86a87eee
--- /dev/null
+++ b/docs/qa/FIRST_TIME_USER_AUDIT_20260316.md
@@ -0,0 +1,211 @@
+# First-Time User Audit — Stella Ops
+
+**Date**: 2026-03-16
+**Method**: Documentation review + Playwright live walkthrough (41 pages) + source analysis
+**Stack**: 63 containers, 111/111 canonical routes passing, demo-prod tenant
+
+---
+
+## Executive Summary
+
+Stella Ops is **technically impressive** — 41 pages load without crashes, all 111 routes pass, and the product surface is vast (releases, security, evidence, policy, operations, integrations, administration). However, a first-time user faces **significant onboarding friction**: the product assumes prior knowledge, empty states don't guide next steps, and the navigation depth is overwhelming without a guided tour.
+
+**41 pages audited, 27 findings identified (4 critical, 8 high, 10 medium, 5 low).**
+
+---
+
+## Findings
+
+### CRITICAL
+
+#### C1. No 404 Page — Unknown Routes Show Dashboard
+**Route**: `/nonexistent-route-12345` → renders Dashboard (title: "Dashboard")
+**Impact**: Users who mistype URLs or follow stale links see the dashboard instead of "Page Not Found". They won't realize they're in the wrong place.
+**Solution**: Add a wildcard catch-all route that renders a dedicated 404 component with: "This page doesn't exist" message, search bar, and links to common pages.
+**Effort**: S
+**Files**: `src/Web/StellaOps.Web/src/app/app.routes.ts` — add `{ path: '**', component: NotFoundComponent }`
+
+#### C2. System Health / Signals / Dead-Letter Show Error Text
+**Routes**: `/ops/operations/system-health`, `/ops/operations/signals`, `/ops/operations/dead-letter`
+**Impact**: Three operational pages show visible error text to the user. On a fresh setup, this makes the platform look broken.
+**Solution**: Replace raw error text with the `ErrorStateComponent` pattern (alert icon + "Something went wrong" + retry button). For fresh installs, show "No data yet" instead of errors.
+**Effort**: M
+**Files**: System health, signals, dead-letter component `.ts` files
+
+#### C3. Documentation Mentions Internal Codenames Without Explanation
+**Impact**: Docs reference "Concelier", "Excititor", "VexHub", "VexLens" without explaining what they are. The Feature Matrix says "Concelier + CLI" for advisory ingest. A new user won't know these are service names.
+**Solution**: Add a glossary section to `docs/README.md` or a standalone `docs/GLOSSARY.md`. In each doc that mentions a codename, add a brief parenthetical: "Concelier (the advisory feed aggregator)".
+**Effort**: M
+**Files**: `docs/README.md`, `docs/GLOSSARY.md` (new), `docs/CONCELIER_CLI_QUICKSTART.md`
+
+#### C4. Feature Matrix Shows Release Orchestration as "Planned" (⏳) but UI Is Built
+**Impact**: The Feature Matrix (rev 5.1, Jan 2026) lists Environment CRUD, Release Bundles, Promotion Workflows, etc. all as ⏳ (planned). But the live UI has working releases, approvals, promotions, environments. A buyer reading this would think the product can't do releases yet.
+**Solution**: Update `docs/FEATURE_MATRIX.md` to reflect current implementation status. Mark shipped features with ✅.
+**Effort**: M
+**Files**: `docs/FEATURE_MATRIX.md`
+
+---
+
+### HIGH
+
+#### H1. No Onboarding Wizard or First-Run Detection
+**Impact**: After login, the user lands on the Mission Control dashboard with mock/seed data. There's no "Welcome to Stella Ops — let's set up your first environment" flow. The Platform Setup page exists but isn't discoverable from the dashboard.
+**Solution**: Add a first-run banner on the Dashboard that detects if the tenant has 0 real environments/targets configured. Show: "Welcome! Start by setting up your deployment topology" with a CTA to `/ops/platform-setup/topology-wizard`.
+**Effort**: M
+**Files**: `dashboard-v3.component.ts`, new `first-run-banner.component.ts`
+
+#### H2. Sidebar Has 40+ Items — No Progressive Disclosure
+**Impact**: The sidebar shows all navigation items at once: Releases (6 children), Operations (11 children), Security (6 children), Evidence (6 children), Setup (8+ children). A first-time user doesn't know where to start.
+**Solution**: Consider collapsing all groups by default for first-time users (or until the user has interacted). Add a "Getting Started" pinned item at the top of the sidebar that links to a setup checklist.
+**Effort**: M
+**Files**: `app-sidebar.component.ts`
+
+#### H3. No "What Is This?" Help on Complex Pages
+**Impact**: Pages like "Policy Decisioning Studio", "Decision Capsules", "Shadow Mode", "Promotion Gate", "Risk Budget" use domain-specific language without inline help. A new user can't understand what these do.
+**Solution**: Add `(?)` tooltip icons next to page titles that show a 2-sentence explanation. Use the existing glossary/tooltip component pattern.
+**Effort**: L
+**Files**: Multiple page components, new `help-tooltip.component.ts`
+
+#### H4. Tenant Switching Not Obvious
+**Impact**: The tenant selector is in the secondary topbar row, which may not be visible or recognizable. Demo has 3 tenants but users might not know they can switch.
+**Solution**: If multiple tenants are available, show a more prominent tenant indicator. Consider adding tenant context to the breadcrumb trail.
+**Effort**: S
+**Files**: `app-topbar.component.ts`
+
+#### H5. Quickstart Doesn't Mention Default Credentials
+**Impact**: The quickstart guide says "Open https://stella-ops.local" but doesn't tell the user what credentials to use. The login page shows an OIDC form. User has to guess or search the seed SQL.
+**Solution**: Add to `docs/quickstart.md` after step 5: "Log in with: **admin** / **Admin@Stella2026!** (demo-prod tenant)".
+**Effort**: S
+**Files**: `docs/quickstart.md`
+
+#### H6. Empty Integrations Hub on Fresh Install
+**Routes**: `/setup/integrations`, `/setup/integrations/registries`, `/setup/integrations/scm`
+**Impact**: All integration categories show "No connector plugins installed" with disabled "+ Add" buttons. There's no explanation of what plugins are or how to get them. User hits a dead end.
+**Solution**: Add a "Getting started with integrations" card that explains: plugins are installed via the CLI or Docker overlays. Link to `docs/PLUGIN_SDK_GUIDE.md`. For the advisory/VEX section, highlight that it works out of the box (75 sources).
+**Effort**: M
+**Files**: `integrations-hub.component.ts`
+
+#### H7. Topology Setup Shows "Loading" Without Progress
+**Route**: `/setup/topology`
+**Impact**: The topology overview shows a "Loading" state on page load without indicating what it's loading or how long it'll take. On slow networks this could look stuck.
+**Solution**: Add a skeleton loader pattern and "Loading topology data..." text. If the load takes >5s, show a retry option.
+**Effort**: S
+**Files**: Topology overview component
+
+#### H8. UI Guide References Workspace Names That Don't Match Sidebar
+**Impact**: `docs/UI_GUIDE.md` lists workspaces as "Scans/SBOM", "Findings/Triage", "Advisories & VEX", "Runs/Scheduler", "Downloads/Offline". But the sidebar uses "Security Posture", "Supply-Chain Data", "Disposition", "Scheduled Jobs", "Offline Kit". Names don't match.
+**Solution**: Update `docs/UI_GUIDE.md` workspace names to match the current sidebar labels exactly.
+**Effort**: S
+**Files**: `docs/UI_GUIDE.md`
+
+---
+
+### MEDIUM
+
+#### M1. Dashboard Shows Seed Data That Looks Real
+**Impact**: The Mission Board shows "Active Promotions: 2", "Blocked: 1", environments with risk scores, SBOM metrics. These are seed/mock values but look like real production data. A new user can't tell what's real vs. demo.
+**Solution**: Add a subtle "Demo data" indicator on cards populated from seed data. Or detect when the tenant is `demo-prod` and show a dismissible banner: "You're viewing demo data. Create your own environment to see real metrics."
+**Effort**: M
+
+#### M2. Security Posture "Configure sources" Links to Ops Route
+**Route**: `/security/posture`
+**Impact**: The "Configure sources" action links to `/ops/integrations/advisory-vex-sources` (ops path) instead of `/setup/integrations/advisory-vex-sources` (setup path). This inconsistency may confuse users who navigated through Setup.
+**Effort**: S
+
+#### M3. Evidence "Decision Capsules" Name Is Unclear
+**Route**: `/evidence/capsules`
+**Impact**: "Decision Capsules" is a unique Stella Ops term. Without context, a first-time user won't know what this page does. The page renders content but the concept isn't self-documenting.
+**Solution**: Add a subtitle under the heading: "Sealed evidence bundles that package inputs, verdicts, and approvals for each release decision."
+**Effort**: S
+
+#### M4. Policy Studio Needs a "What to Do First" Panel
+**Route**: `/ops/policy/overview`
+**Impact**: The Policy Decisioning Studio has many tabs (Baselines, Gates, Simulation, Waivers, Risk Budget, etc.) but no guidance on what order to use them. New users don't know if they should start with baselines, gates, or simulation.
+**Solution**: Add a collapsible "Getting Started with Policies" panel that suggests: 1. Review baselines → 2. Define gates → 3. Test with simulation → 4. Enable shadow mode.
+**Effort**: M
+
+#### M5. "Ctrl+K" Command Palette Not Shown as Hint
+**Impact**: The command palette is powerful (search + quick actions) but there's no UI hint that it exists. Users who don't know the shortcut will miss it entirely.
+**Solution**: Add a "Search (Ctrl+K)" pill in the topbar search area. The current search icon doesn't communicate the keyboard shortcut.
+**Effort**: S
+
+#### M6. Operations Hub Has 11+ Sub-pages Without Grouping
+**Route**: `/ops/operations`
+**Impact**: Jobs, Signals, Offline Kit, Dead Letter, AOC, Doctor, Packs, AI Runs, Notifications, Status — these are listed flat without category headers. New users can't prioritize.
+**Solution**: Group operations into: "Health & Monitoring" (System Health, SLO, Doctor, Signals), "Job Management" (JobEngine, Scheduler, Dead Letter, Packs), "Other" (Offline Kit, AOC, AI Runs).
+**Effort**: M
+
+#### M7. Admin Pages Duplicated Across /setup and /administration Routes
+**Impact**: `/setup/identity-access` and `/administration/identity-access` exist. `/setup/trust-signing` and `/administration/trust-signing` redirect to the same components. This route duplication is confusing — users might bookmark one path while docs reference another.
+**Solution**: Canonicalize to one path family (`/setup/` for all admin). Add permanent redirects from `/administration/*` to `/setup/*`.
+**Effort**: M
+
+#### M8. Notifications Setup Has No Preview/Test
+**Route**: `/setup/notifications`
+**Impact**: Users can configure notification channels and rules but there's no "Send test notification" button to verify the channel works before going live.
+**Solution**: Add a "Test" button next to each channel that sends a sample notification.
+**Effort**: M
+
+#### M9. Usage & Limits Shows Configuration But No Actual Usage
+**Route**: `/setup/usage`
+**Impact**: The page shows quota configuration (limits, thresholds) but doesn't show current consumption. Users can't see "how much of my quota am I using?"
+**Solution**: Add current usage metrics next to quota limits: "Storage: 24 GB / 100 GB (24%)", "API calls today: 312 / 600".
+**Effort**: M (backend may already have this data via QuotaCompatibilityEndpoints)
+
+#### M10. Trust & Signing Page Has No Clear First Step
+**Route**: `/setup/trust-signing`
+**Impact**: The trust management page has many tabs (Keys, Issuers, Certificates, Watchlist, Audit, Air-Gap, Incidents, Analytics) but no "start here" guidance for a fresh install.
+**Solution**: Add a setup checklist: 1. Generate signing key → 2. Register issuer → 3. Import CA certificates.
+**Effort**: S
+
+---
+
+### LOW
+
+#### L1. Quickstart Database Connection String Is Hardcoded
+**Impact**: The quickstart shows `Host=127.1.1.1;Port=5432;Database=stellaops_platform;Username=stellaops;Password=stellaops` as a command-line argument. This works for local dev but exposes credentials in shell history.
+**Solution**: Note in docs that the connection string should use environment variables in production. Add `--connection $STELLAOPS_DB` pattern.
+**Effort**: S
+
+#### L2. Screenshot/Visual Guide Missing from Docs
+**Impact**: All docs are text-only. No screenshots of the UI, no annotated diagrams showing where to click. Visual learners have no reference.
+**Solution**: Add a "Visual Tour" section to `docs/UI_GUIDE.md` with annotated screenshots of: Dashboard, Sidebar, Releases, Security Posture, Policy Studio.
+**Effort**: L
+
+#### L3. Config Sample File Has Internal Service URLs
+**Impact**: `src/Web/StellaOps.Web/src/config/config.sample.json` contains URLs like `http://router.stella-ops.local` which only work in the Docker network. An external deployer wouldn't know what to change.
+**Solution**: Add comments/documentation in config.sample.json explaining each URL and what to replace for external deployment.
+**Effort**: S
+
+#### L4. Air-Gap Mode Not Discoverable from Main Navigation
+**Impact**: Offline/air-gap is a key differentiator but the feature is buried under Operations → Offline Kit. A user evaluating air-gap support might not find it.
+**Solution**: Add "Air-Gap Mode" as a visible chip/badge in the topbar when offline mode is configured. Add a link from the Platform Setup home page.
+**Effort**: S
+
+#### L5. CLI `stella topology` Commands Not Documented in User Docs
+**Impact**: The CLI has topology setup, validate, status, rename, delete commands but they're not mentioned in the quickstart or UI guide. Users might not know the CLI exists.
+**Solution**: Add a CLI reference section to `docs/quickstart.md` or a new `docs/CLI_REFERENCE.md`.
+**Effort**: M
+
+---
+
+## Priority Matrix
+
+| Effort\Severity | Critical | High | Medium | Low |
+|:---:|:---:|:---:|:---:|:---:|
+| **S** | — | H4, H5, H7, H8 | M2, M3, M5, M10 | L1, L3, L4 |
+| **M** | C3, C4 | H1, H2, H6 | M1, M4, M6, M7, M8, M9 | L5 |
+| **L** | — | H3 | — | L2 |
+| **S (code)** | C1 | — | — | — |
+| **M (code)** | C2 | — | — | — |
+
+**Recommended attack order**: C1 → H5 → C4 → C3 → H1 → C2 → H8 → H4 → M5 → M3
+
+---
+
+## Verification
+
+- Playwright audit script: `src/Web/StellaOps.Web/scripts/live-first-time-user-audit.mjs`
+- Audit results: `src/Web/StellaOps.Web/output/playwright/first-time-user-audit.json`
+- Screenshots: `src/Web/StellaOps.Web/output/playwright/first-time-user-audit/`
+- 41 pages audited, 0 page crashes, 0 API errors, 3 visible error text instances
diff --git a/docs/qa/FIRST_TIME_USER_HANDS_ON_AUDIT_20260316.md b/docs/qa/FIRST_TIME_USER_HANDS_ON_AUDIT_20260316.md
new file mode 100644
index 000000000..0f5764be9
--- /dev/null
+++ b/docs/qa/FIRST_TIME_USER_HANDS_ON_AUDIT_20260316.md
@@ -0,0 +1,217 @@
+# First-Time User Hands-On Audit — Stella Ops
+
+**Date**: 2026-03-16
+**Method**: Interactive Playwright browser walkthrough as a first-time DevOps/Security engineer
+**Stack**: 63 containers live at `https://stella-ops.local`, demo-prod tenant
+
+---
+
+## What I Did
+
+Walked through Stella Ops as a DevOps engineer who heard "it solves deployment and security auditing" and wants to evaluate it. I set up, logged in, explored the dashboard, drilled into security findings, tried integrations, ran diagnostics, checked policies, and created a release — end to end.
+
+---
+
+## Findings (Ordered by Discovery Sequence)
+
+### F01 — CRITICAL: Dashboard Shows Fake Data, Drilldowns Show Empty
+
+**What happened**: Dashboard shows "Production US East — blocked, 5 critical findings, SBOM missing." I clicked Findings to investigate. The Findings Explorer showed: "No baselines available", "No immediate actions required", 0 findings. The Security Posture page shows "0 findings in scope", "GUARDED" risk posture.
+
+**Root cause**: The Dashboard renders hardcoded seed/compatibility data (environment cards, risk scores, deployment timestamps). The actual security data backends (scanner, findings ledger, VEX hub) have no real scan data. The dashboard doesn't distinguish demo data from real data.
+
+**Impact**: A first-time user thinks there's a security crisis, spends time investigating, then discovers it's all fake. This destroys trust in the platform immediately.
+
+**Proposed solution**:
+1. Dashboard should query real backends for environment health, not return hardcoded mock data
+2. If no real data exists, show an honest empty state: "No scans have been run yet. Connect a registry and scan your first image."
+3. If demo mode is intentional, add a persistent "Demo Data" banner at the top that can be dismissed
+
+**Severity**: CRITICAL — this is the #1 thing that would make me distrust this product
+**Effort**: L (requires backend changes to dashboard projection)
+
+---
+
+### F02 — HIGH: Login Credentials Not Documented
+
+**What happened**: Clicked "Sign In", got an OIDC login form. Had no idea what credentials to use. The quickstart doc says "Open https://stella-ops.local" but never mentions username/password. I had to know the seed SQL to find `admin / Admin@Stella2026!`.
+
+**Proposed solution**: Add to `docs/quickstart.md` step 5: "Log in with **admin** / **Admin@Stella2026!** (demo-prod tenant)". Also show credentials on the Welcome page itself with a "Demo credentials" hint.
+
+**Severity**: HIGH — completely blocks first use
+**Effort**: S
+
+---
+
+### F03 — HIGH: AuthRef URI Required for Registry Integration — No Plain Credentials Option
+
+**What happened**: Tried "+ Add Registry". The Harbor wizard requires an "AuthRef URI" (`authref://vault/path#secret`). As a new user, I don't have a vault configured. There's no way to just enter a username/password pair.
+
+**Root cause**: Stella enforces credential indirection (vault references) for security. This is good for production but blocks evaluation/local dev.
+
+**Proposed solution**: Allow a `authref://inline/` scheme for local/dev mode, or add a "Direct credentials (dev only)" toggle in the wizard. Document the authref format clearly with examples for common vaults (HashiCorp, AWS SSM, env vars).
+
+**Severity**: HIGH — blocks the core integration workflow for new users
+**Effort**: M
+
+---
+
+### F04 — HIGH: Registry Search Returns Mock Results Despite API Errors
+
+**What happened**: In the release creation wizard, I searched for "nginx" in the component registry. The UI showed "nginx-service" at `registry.internal/nginx-service` with tag "latest" and digest `sha256:nginx1234567...`. But the browser console had 4 errors: `Failed to load resource: /api/registry/images/search?q=...`.
+
+**Root cause**: The component search has a fallback mock/seed dataset that renders when the real API fails. The user can't tell the result is fake.
+
+**Impact**: I created a release with a fake component. The sealed bundle contains `sha256:nginx12345678...` which doesn't exist in any real registry. This undermines the digest-first integrity model.
+
+**Proposed solution**:
+1. When the registry API fails, show an error toast: "Registry search unavailable — connect a registry first"
+2. Don't silently fall back to mock data in production flows
+3. If mock mode is intentional for demo, mark results with a "Demo" badge
+
+**Severity**: HIGH — silently produces releases with fake artifacts
+**Effort**: M
+
+---
+
+### F05 — MEDIUM: Bundle Version Shows Raw UUID Instead of Release Name
+
+**What happened**: After sealing my release "API Gateway v2.1", the bundle version page shows the heading: `a4451892-b6b2-41f6-8539-7b9c6d33d140 v1`. My release name is nowhere on this page.
+
+**Proposed solution**: Show the human-readable release name as the primary heading. Show the UUID/digest as secondary metadata below it.
+
+**Severity**: MEDIUM
+**Effort**: S
+
+---
+
+### F06 — MEDIUM: "Created by" Shows User ID Hash, Not Username
+
+**What happened**: Bundle version shows "Created by: b08639745d6549348d843aa311c98958". This is meaningless to an operator.
+
+**Proposed solution**: Resolve user IDs to display names in read-model projections. Show "admin" or "Admin (admin@demo.stella-ops.local)" instead of the hash.
+
+**Severity**: MEDIUM
+**Effort**: S
+
+---
+
+### F07 — MEDIUM: Topbar Context Shows "No regions defined" Initially, Then Self-Heals
+
+**What happened**: On first dashboard load, the topbar showed "Region: No regions defined" and "Env: No env defined yet" with disabled dropdowns. But the dashboard below showed 5 environment cards with regions. After a few seconds the topbar populated to "4 regions" / "All environments".
+
+**Root cause**: Race condition — the context controls render before the tenant inventory API returns. The dashboard component has its own seed data that renders immediately.
+
+**Proposed solution**: Show a loading skeleton for the context controls instead of "No regions defined". Or defer rendering until inventory loads.
+
+**Severity**: MEDIUM — confusing but self-resolves
+**Effort**: S
+
+---
+
+### F08 — MEDIUM: Doctor "Quick Check" Skips Database Checks
+
+**What happened**: Ran Doctor Quick Check. The `check.db.connection` and `check.db.latency` checks were "Skipped — Check not applicable in current context". But PostgreSQL is running and connected (the platform uses it for everything).
+
+**Root cause**: Quick Check likely runs only a subset of packs. DB checks may require "Normal" or "Full" check level.
+
+**Proposed solution**: At minimum, `check.db.connection` should always run — it's the most critical health check. If it's intentionally excluded from Quick Check, explain why in the UI: "Database checks run in Normal and Full modes."
+
+**Severity**: MEDIUM
+**Effort**: S
+
+---
+
+### F09 — MEDIUM: Doctor Stream Endpoint Returns Error
+
+**What happened**: After clicking Quick Check, a console error appeared: `Failed to load resource: /api/v1/doctor/run/dr_.../stream` (status 0). The check results still rendered.
+
+**Root cause**: The Doctor run uses a streaming endpoint (SSE/WebSocket) that failed. The UI has a fallback that loads results after the stream fails.
+
+**Proposed solution**: Either fix the streaming endpoint or suppress the console error when the fallback is used.
+
+**Severity**: MEDIUM — cosmetic but logs errors
+**Effort**: S
+
+---
+
+### F10 — MEDIUM: Events Status Flickers Between "DEGRADED" and "CONNECTED"
+
+**What happened**: On first login, topbar showed "Events: DEGRADED" (yellow). After the page re-settled with tenant data, it changed to "Events: CONNECTED" (green).
+
+**Proposed solution**: Don't show DEGRADED during initial connection establishment. Use a "Connecting..." state during the first 5 seconds after login.
+
+**Severity**: MEDIUM
+**Effort**: S
+
+---
+
+### F11 — LOW: Advisories Show "stale" on Fresh Install
+
+**What happened**: Security Posture page shows "GitHub Advisory Database — stale", "NVD — stale", "OSV — stale" under Advisories & VEX Health.
+
+**Root cause**: Expected — no feeds have been synced on a fresh install. But "stale" implies they were once fresh and degraded.
+
+**Proposed solution**: For sources that have never been synced, show "Not synced" instead of "stale". Reserve "stale" for sources whose last sync is older than the threshold.
+
+**Severity**: LOW
+**Effort**: S
+
+---
+
+### F12 — LOW: "Target path intent" Dropdown Shows "?" Instead of "→"
+
+**What happened**: In the release creation wizard, the target path dropdown shows "Dev ? Stage ? Prod" instead of "Dev → Stage → Prod". The arrow character isn't rendering.
+
+**Proposed solution**: Use HTML entity `→` or Unicode `\u2192` instead of whatever character is being used.
+
+**Severity**: LOW — cosmetic
+**Effort**: S
+
+---
+
+### F13 — OBSERVATION: What Works Well
+
+Things that impressed me as a first-time user:
+
+- **Welcome page**: Clean, professional, one "Sign In" button — no confusion
+- **Integrations Hub**: "Suggested Setup Order" section is exactly what a new user needs — tells you what to connect first and why
+- **Doctor Diagnostics**: Excellent — failed checks expand with "Likely Causes", remediation steps, copy buttons, and a `stella doctor` verification command
+- **Release Creation Wizard**: 4-step flow with digest-first component selection is elegant. The "Seal Draft" semantics with confirmation checkbox is clear and intentional
+- **Policy Decisioning Studio**: 7-tab layout with cards is well-organized. Overview page explains the canonical route family
+- **Topbar status chips**: "Events: CONNECTED", "Policy: Core Policy Pack latest", "Evidence: ON", "Feed: Live", "Offline: OK" — gives immediate platform health context
+- **Sidebar organization**: 3 logical groups (Release Control, Security & Audit, Platform & Setup) makes sense
+- **Context-aware topbar actions**: "Create Release" on releases pages, "Add Integration" on ops pages — smart
+- **Breadcrumbs**: Work correctly, provide navigation context
+- **Global search**: Ctrl+K hint visible, search placeholder "Search everything..." is inviting
+
+---
+
+## Priority Matrix
+
+| # | Severity | Issue | Effort | Fix First? |
+|---|----------|-------|--------|-----------|
+| F01 | CRITICAL | Dashboard fake data vs empty backends | L | Yes — blocks trust |
+| F02 | HIGH | Login credentials not in docs | S | Yes — 1 line fix |
+| F03 | HIGH | AuthRef required, no plain credential option | M | Yes — blocks integration |
+| F04 | HIGH | Registry search returns mock data silently | M | Yes — integrity risk |
+| F05 | MEDIUM | Bundle shows UUID not release name | S | Quick win |
+| F06 | MEDIUM | Created-by shows hash not username | S | Quick win |
+| F07 | MEDIUM | Context controls race condition | S | Quick win |
+| F08 | MEDIUM | Doctor Quick Check skips DB checks | S | Quick win |
+| F09 | MEDIUM | Doctor stream endpoint error | S | Cosmetic |
+| F10 | MEDIUM | Events status flickers on login | S | Cosmetic |
+| F11 | LOW | "stale" vs "not synced" for fresh sources | S | Polish |
+| F12 | LOW | Arrow character not rendering | S | Polish |
+
+**Recommended attack order**: F02 → F01 → F04 → F03 → F05 → F06 → F07
+
+---
+
+## Summary as a First-Time User
+
+**Would I adopt this product?** The feature depth is remarkable — release orchestration with digest-first identity, policy-as-code, VEX decisioning, evidence capsules, diagnostics engine, 75 advisory sources. The architecture is clearly well-thought-out.
+
+**What would stop me?** The fake dashboard data is a dealbreaker for evaluation. I can't trust the security posture numbers if they're hardcoded. The integration setup requires vault infrastructure I don't have yet. And the registry search silently producing mock results means I can't trust that my releases contain real artifacts.
+
+**What would I tell my team?** "The vision is excellent, the security model is serious (digest-first, sealed releases, evidence capsules, replay parity). But it needs a 'getting started for real' mode that doesn't mix demo data with real workflows. Once you get past the initial setup friction, the operator experience in Policy, Doctor, and Releases is genuinely good."
diff --git a/docs/qa/FIRST_TIME_USER_SERIES_20260316.md b/docs/qa/FIRST_TIME_USER_SERIES_20260316.md
new file mode 100644
index 000000000..98add80ed
--- /dev/null
+++ b/docs/qa/FIRST_TIME_USER_SERIES_20260316.md
@@ -0,0 +1,232 @@
+# First-Time User Audit Series — Stella Ops
+
+**Date**: 2026-03-16
+**Method**: Interactive Playwright walkthrough + source code analysis + product deep-dive
+**Stack**: 63 containers live at `https://stella-ops.local`, demo-prod tenant
+
+---
+
+## Part 1: The Product Mental Model
+
+Stella Ops is a **release control plane** for non-Kubernetes container estates. Every operator interaction answers one of three questions:
+
+1. **Is it safe?** — Vulnerability findings, SBOM health, reachability evidence, VEX dispositions, advisory feed freshness
+2. **Is it approved?** — Policy gates passed, human approvals given, evidence sealed
+3. **Is it working?** — Services healthy, feeds syncing, jobs running, no dead letters
+
+The UI, documentation, and workflows must be organized around these three pillars.
+
+---
+
+## Part 2: Findings (Prioritized)
+
+### CRITICAL
+
+#### C1. Dashboard is 100% Hardcoded — Zero Real API Data
+**Source file**: `dashboard-v3.component.ts:966-1200`
+
+Every number on the dashboard is fake:
+- `summary` signal: `{ activePromotions: 3, blockedPromotions: 1 }` — literal
+- `resolveStatusSeed()`: generates metrics by env type (dev=healthy, staging=degraded, prod+us-east=blocked)
+- `reachabilityStats`: `{ bCoverage: 72, iCoverage: 88, rCoverage: 61 }` — literal
+- `nightlyOpsSignals`: 4 hardcoded items
+- Alerts: 3 hardcoded `
` HTML elements
+- Activity: 3 hardcoded cards
+- **Not a single API call to any backend service**
+
+The dashboard tells you "5 critical findings, blocked" but security posture shows 0 findings. This destroys trust immediately.
+
+**Solution**: [See Part 3 — Dashboard Redesign]
+
+#### C2. StellaOps Mirror Source Enabled at Priority 1 on Every Fresh Install
+**Source file**: `SourceDefinitions.cs:1351-1366`
+
+The `StellaMirror` source definition:
+```csharp
+BaseEndpoint = "https://mirror.stella-ops.org/api/v1" // nonexistent external URL
+DefaultPriority = 1 // HIGHEST priority — overrides all other sources
+EnabledByDefault = true // inherited, always on
+```
+
+On every fresh self-hosted install, the highest-priority advisory source points to a URL that doesn't exist. Health check returns "SSL/TLS error." The system is in "Direct" mode but its top-priority source is a mirror.
+
+**Solution**: Set `EnabledByDefault = false` on StellaMirror. Only enable when mirror consumer wizard is completed.
+
+#### C3. Mirror Source, Mirror Mode, and Mirror Consumer Wizard Are Disconnected
+Three independent systems that should be one:
+1. **Catalog toggle**: Enable "StellaOps Mirror" → hardcoded to `mirror.stella-ops.org`
+2. **Mode badge**: Shows "Direct" regardless of which sources are enabled
+3. **Consumer wizard**: "Connect to Mirror" → 4-step wizard with custom URL
+
+Enabling the catalog source doesn't invoke the wizard. Completing the wizard doesn't update the catalog source. The mode badge doesn't reflect reality.
+
+**Solution**: Unify — catalog mirror toggle should redirect to consumer wizard. Wizard completion should update source endpoint + enable source + set mode.
+
+#### C4. 74 of 75 Advisory Sources Enabled by Default — 18 Fail Immediately
+`EnabledByDefault = true` in the base `SourceDefinition` class. Every source — including geo-restricted (FSTEC Russia, NKCKI Russia), ecosystem-specific (npm, PyPI, Maven, etc.), and the broken mirror — is enabled on fresh install.
+
+After "Check All": 55 healthy, 18 failed. Stats bar shows confusing "55 enabled" (doesn't match 74 actually enabled).
+
+**Solution**: Curate defaults. Enable ~25 core sources (Primary + major vendors + major distributions). Disable ecosystem, geo-restricted, hardware, and mirror by default.
+
+### HIGH
+
+#### H1. Login Credentials Not Documented Anywhere
+Quickstart says "Open https://stella-ops.local" — never mentions username/password. User must find `S001_demo_seed.sql` to discover `admin / Admin@Stella2026!`.
+
+**Solution**: Add credentials to quickstart.md step 5. Add hint to the Welcome page.
+
+#### H2. Registry Search Returns Mock Results Despite API Failures
+In release creation wizard, searching "nginx" returns "nginx-service" at `registry.internal/nginx-service` with fake digest `sha256:nginx1234567...`. Console shows 4 errors: `Failed to load resource: /api/registry/images/search`. The mock fallback silently produces results from a nonexistent registry.
+
+**Solution**: When registry API fails, show error — not mock results. Don't allow sealing releases with mock artifacts.
+
+#### H3. Create Mirror Domain Wizard Allows Selecting Mirror Source
+The source picker in Create Domain includes "StellaOps Mirror" under Mirror (1) category. This creates a mirror domain that sources from a nonexistent upstream mirror — circular/broken dependency.
+
+**Solution**: Filter out `category === 'Mirror'` from the Create Domain source picker.
+
+#### H4. AuthRef URI Required — No Plain Credentials for Evaluation
+Harbor registry wizard requires `authref://vault/path#secret`. No vault exists on a fresh install. No way to enter username:password for evaluation.
+
+**Solution**: Allow `authref://env/VARIABLE_NAME` or `authref://inline/base64` for dev/eval. Document authref format with examples.
+
+### MEDIUM
+
+#### M1. Bundle Version Shows UUID Instead of Release Name
+After sealing "API Gateway v2.1", the page shows `a4451892-b6b2-41f6-8539-7b9c6d33d140 v1` as the heading.
+
+**Solution**: Show human-readable name as primary heading, UUID as metadata.
+
+#### M2. "Created by" Shows User ID Hash, Not Username
+Bundle version shows `b08639745d6549348d843aa311c98958` instead of "admin".
+
+**Solution**: Resolve user IDs to display names in read-model projections.
+
+#### M3. Context Controls Race Condition on First Load
+Topbar shows "No regions defined" for 2-3 seconds, then self-heals to "4 regions". Dashboard below shows environment cards immediately (from seed data).
+
+**Solution**: Show loading skeleton for context controls until inventory loads.
+
+#### M4. Doctor Quick Check Skips Database Checks
+`check.db.connection` and `check.db.latency` show "Skipped — not applicable" despite Postgres being the primary database.
+
+**Solution**: Always include DB connection check in Quick Check.
+
+#### M5. Doctor Stream Endpoint Returns Console Error
+After Quick Check, console shows `Failed to load resource: /api/v1/doctor/run/.../stream`. Results still render via fallback.
+
+**Solution**: Fix streaming endpoint or suppress console error when fallback succeeds.
+
+#### M6. Events Status Flickers "DEGRADED" → "CONNECTED" on Login
+First 2-3 seconds show "Events: DEGRADED" (yellow), then settles to "CONNECTED" (green).
+
+**Solution**: Show "Connecting..." during initial establishment, not "DEGRADED".
+
+#### M7. Advisory Sources Show "stale" on Fresh Install (Never Synced)
+Security Posture: "GitHub Advisory Database — stale", "NVD — stale". These have never been synced.
+
+**Solution**: Show "Not synced" for sources that have never fetched. Reserve "stale" for sources whose last sync exceeds freshness threshold.
+
+#### M8. Feature Matrix Shows Release Orchestration as "Planned" (⏳) — But It's Built
+`FEATURE_MATRIX.md` lists Environment CRUD, Release Bundles, Promotion Workflows as ⏳. The live UI has working releases, approvals, promotions. A buyer would think releases don't work.
+
+**Solution**: Update FEATURE_MATRIX.md to reflect current implementation status.
+
+#### M9. Stats Bar Count Mismatch
+After Check All, stats show "55 enabled" but 74 sources are actually enabled (toggles ON). The "enabled" count in the stats bar doesn't match the source state.
+
+**Solution**: Fix stats computation to count actual enabled sources, not only those with successful health checks.
+
+### LOW
+
+#### L1. Arrow Character Not Rendering in Target Path
+Release wizard dropdown shows "Dev ? Stage ? Prod" instead of "Dev → Stage → Prod".
+
+**Solution**: Use `\u2192` or HTML entity `→`.
+
+#### L2. No 404 Page — Unknown Routes Show Dashboard
+`/nonexistent-route-12345` renders the Dashboard instead of "Page Not Found".
+
+**Solution**: Add wildcard catch-all route with a proper 404 component.
+
+---
+
+## Part 3: Dashboard Redesign
+
+### Layout: 3-Column Mission Board
+
+```
+┌──────────────────────────────────────────────────────────────┐
+│ Mission Board — [Demo Production] [↻ Refresh] │
+│ Last updated: 15 Mar 2026, 23:15 UTC │
+├──────────────────┬───────────────────────────────────────────┤
+│ │ │
+│ SECURITY POSTURE │ ENVIRONMENTS & ACTIONS (2/3) │
+│ (1/3) │ │
+│ │ ┌───────────────────────────────────────┐ │
+│ ┌──────────────┐ │ │ PROMOTION PIPELINE │ │
+│ │ Vuln Summary │ │ │ [env cards: Dev→Stage→Prod by region] │ │
+│ │ C:12 H:34 │ │ │ blocked envs highlighted at top │ │
+│ │ M:89 L:156 │ │ └───────────────────────────────────────┘ │
+│ │ [donut chart] │ │ │
+│ │ Reachable: 9 │ │ ┌───────────────────────────────────────┐ │
+│ │ Unknown: 23 │ │ │ NEEDS ATTENTION │ │
+│ └──────────────┘ │ │ ⚠ 3 approvals blocked │ │
+│ │ │ ⚠ 2 waivers expiring │ │
+│ ┌──────────────┐ │ │ 🔴 Feed freshness degraded │ │
+│ │ SBOM Health │ │ └───────────────────────────────────────┘ │
+│ │ 247 comps │ │ │
+│ │ 231 fresh │ │ ┌───────────────────────────────────────┐ │
+│ │ B:72% I:88% │ │ │ LIVE ACTIVITY │ │
+│ │ R:61% │ │ │ • admin sealed "API Gateway v2.1" │ │
+│ └──────────────┘ │ │ • NVD synced (142 new advisories) │ │
+│ │ │ • Doctor: 7 pass, 1 warn, 1 fail │ │
+│ ┌──────────────┐ │ └───────────────────────────────────────┘ │
+│ │ Feed Status │ │ │
+│ │ 55/75 active │ │ │
+│ │ 18 failed │ │ │
+│ └──────────────┘ │ │
+├──────────────────┴───────────────────────────────────────────┤
+│ PLATFORM: 63 svcs ✓ │ DB:OK │ Events:ON │ Doctor:7/1/1 │
+│ Feed:Live │ Evidence:ON │ Offline:OK │ DLQ:3 pending │
+└──────────────────────────────────────────────────────────────┘
+```
+
+### Empty State (Fresh Install)
+
+When no real data exists, show setup guidance — not fake numbers:
+
+```
+Welcome to Stella Ops — Release Control Plane
+
+① Connect a registry [Setup Integrations →]
+② Define your topology [Topology Wizard →]
+③ Scan your first image [Start Scan →]
+④ Create a release [Create Release →]
+
+Platform Health: 63/63 services ✓
+Advisory Sources: 55/75 healthy [Configure →]
+```
+
+---
+
+## Part 4: Solutions Implementation Map
+
+| # | Fix | Effort | Files |
+|---|-----|--------|-------|
+| C2 | Mirror source `EnabledByDefault = false` | S | `SourceDefinitions.cs` |
+| C4 | Curate default sources (~25 instead of 74) | S | `SourceDefinitions.cs` |
+| H1 | Add credentials to quickstart | S | `docs/quickstart.md` |
+| H3 | Filter mirror sources from domain builder | S | `mirror-domain-builder.component.ts` |
+| M8 | Update Feature Matrix status markers | S | `docs/FEATURE_MATRIX.md` |
+| L1 | Fix arrow character encoding | S | Release version form component |
+| L2 | Add 404 catch-all route | S | `app.routes.ts` |
+| M3 | Loading skeleton for context controls | S | `app-topbar.component.ts` |
+| M6 | "Connecting..." instead of "DEGRADED" | S | topbar events status |
+| M7 | "Not synced" vs "stale" for fresh sources | S | `advisory-source-catalog.component.ts` |
+| M9 | Fix stats bar enabled count | S | `advisory-source-catalog.component.ts` |
+| C1/Phase1 | Dashboard honest empty state | M | `dashboard-v3.component.ts` |
+| C3 | Unify mirror toggle → consumer wizard | M | catalog + wizard components |
+| H2 | Registry search error instead of mock | M | release version wizard |
+| C1/Phase2 | Dashboard 3-column layout with real APIs | L | `dashboard-v3.component.ts` + API clients |
diff --git a/docs/quickstart.md b/docs/quickstart.md
index 5103c3708..b857e59fb 100644
--- a/docs/quickstart.md
+++ b/docs/quickstart.md
@@ -96,6 +96,12 @@ To skip builds and only start infrastructure:
--connection "Host=127.1.1.1;Port=5432;Database=stellaops_platform;Username=stellaops;Password=stellaops"
```
5. Open **https://stella-ops.local**.
+6. Log in with the demo admin account:
+ - **Username**: `admin`
+ - **Password**: `Admin@Stella2026!`
+ - **Tenant**: demo-prod (selected automatically)
+
+ Additional demo accounts: `operator`, `viewer`, `auditor`, `developer` (same password pattern).
## What's running
diff --git a/src/Concelier/__Libraries/StellaOps.Concelier.Core/Sources/SourceDefinitions.cs b/src/Concelier/__Libraries/StellaOps.Concelier.Core/Sources/SourceDefinitions.cs
index 479babdce..ed45e42f8 100644
--- a/src/Concelier/__Libraries/StellaOps.Concelier.Core/Sources/SourceDefinitions.cs
+++ b/src/Concelier/__Libraries/StellaOps.Concelier.Core/Sources/SourceDefinitions.cs
@@ -611,7 +611,8 @@ public static class SourceDefinitions
HttpClientName = "NpmClient",
RequiresAuthentication = false,
DefaultPriority = 50,
- Tags = ImmutableArray.Create("npm", "ecosystem", "javascript", "node")
+ Tags = ImmutableArray.Create("npm", "ecosystem", "javascript", "node"),
+ EnabledByDefault = false
};
public static readonly SourceDefinition PyPi = new()
@@ -626,7 +627,8 @@ public static class SourceDefinitions
HttpClientName = "PyPiClient",
RequiresAuthentication = false,
DefaultPriority = 52,
- Tags = ImmutableArray.Create("pypi", "ecosystem", "python")
+ Tags = ImmutableArray.Create("pypi", "ecosystem", "python"),
+ EnabledByDefault = false
};
public static readonly SourceDefinition Go = new()
@@ -641,7 +643,8 @@ public static class SourceDefinitions
HttpClientName = "GoClient",
RequiresAuthentication = false,
DefaultPriority = 54,
- Tags = ImmutableArray.Create("go", "ecosystem", "golang")
+ Tags = ImmutableArray.Create("go", "ecosystem", "golang"),
+ EnabledByDefault = false
};
public static readonly SourceDefinition RubyGems = new()
@@ -656,7 +659,8 @@ public static class SourceDefinitions
HttpClientName = "RubyGemsClient",
RequiresAuthentication = false,
DefaultPriority = 56,
- Tags = ImmutableArray.Create("rubygems", "ecosystem", "ruby")
+ Tags = ImmutableArray.Create("rubygems", "ecosystem", "ruby"),
+ EnabledByDefault = false
};
public static readonly SourceDefinition Nuget = new()
@@ -672,7 +676,8 @@ public static class SourceDefinitions
RequiresAuthentication = true,
CredentialEnvVar = "GITHUB_PAT",
DefaultPriority = 58,
- Tags = ImmutableArray.Create("nuget", "ecosystem", "dotnet", "csharp")
+ Tags = ImmutableArray.Create("nuget", "ecosystem", "dotnet", "csharp"),
+ EnabledByDefault = false
};
public static readonly SourceDefinition Maven = new()
@@ -687,7 +692,8 @@ public static class SourceDefinitions
HttpClientName = "MavenClient",
RequiresAuthentication = false,
DefaultPriority = 60,
- Tags = ImmutableArray.Create("maven", "ecosystem", "java")
+ Tags = ImmutableArray.Create("maven", "ecosystem", "java"),
+ EnabledByDefault = false
};
public static readonly SourceDefinition Crates = new()
@@ -702,7 +708,8 @@ public static class SourceDefinitions
HttpClientName = "CratesClient",
RequiresAuthentication = false,
DefaultPriority = 62,
- Tags = ImmutableArray.Create("crates", "ecosystem", "rust")
+ Tags = ImmutableArray.Create("crates", "ecosystem", "rust"),
+ EnabledByDefault = false
};
public static readonly SourceDefinition Packagist = new()
@@ -717,7 +724,8 @@ public static class SourceDefinitions
HttpClientName = "PackagistClient",
RequiresAuthentication = false,
DefaultPriority = 64,
- Tags = ImmutableArray.Create("packagist", "ecosystem", "php")
+ Tags = ImmutableArray.Create("packagist", "ecosystem", "php"),
+ EnabledByDefault = false
};
public static readonly SourceDefinition Hex = new()
@@ -732,7 +740,8 @@ public static class SourceDefinitions
HttpClientName = "HexClient",
RequiresAuthentication = false,
DefaultPriority = 66,
- Tags = ImmutableArray.Create("hex", "ecosystem", "elixir", "erlang")
+ Tags = ImmutableArray.Create("hex", "ecosystem", "elixir", "erlang"),
+ EnabledByDefault = false
};
// ===== CSAF/VEX Sources =====
@@ -927,7 +936,8 @@ public static class SourceDefinitions
RequiresAuthentication = false,
DocumentationUrl = "https://www.exploit-db.com/",
DefaultPriority = 110,
- Tags = ImmutableArray.Create("exploit", "poc", "offensive")
+ Tags = ImmutableArray.Create("exploit", "poc", "offensive"),
+ EnabledByDefault = false
};
public static readonly SourceDefinition PocGithub = new()
@@ -943,7 +953,8 @@ public static class SourceDefinitions
RequiresAuthentication = true,
CredentialEnvVar = "GITHUB_PAT",
DefaultPriority = 112,
- Tags = ImmutableArray.Create("exploit", "poc", "github")
+ Tags = ImmutableArray.Create("exploit", "poc", "github"),
+ EnabledByDefault = false
};
public static readonly SourceDefinition Metasploit = new()
@@ -958,7 +969,8 @@ public static class SourceDefinitions
HttpClientName = "MetasploitClient",
RequiresAuthentication = false,
DefaultPriority = 114,
- Tags = ImmutableArray.Create("exploit", "metasploit", "rapid7")
+ Tags = ImmutableArray.Create("exploit", "metasploit", "rapid7"),
+ EnabledByDefault = false
};
// ===== Cloud Provider Advisories =====
@@ -1054,7 +1066,8 @@ public static class SourceDefinitions
HttpClientName = "IntelClient",
RequiresAuthentication = false,
DefaultPriority = 130,
- Tags = ImmutableArray.Create("intel", "hardware", "firmware", "cpu")
+ Tags = ImmutableArray.Create("intel", "hardware", "firmware", "cpu"),
+ EnabledByDefault = false
};
public static readonly SourceDefinition Amd = new()
@@ -1069,7 +1082,8 @@ public static class SourceDefinitions
HttpClientName = "AmdClient",
RequiresAuthentication = false,
DefaultPriority = 132,
- Tags = ImmutableArray.Create("amd", "hardware", "firmware", "cpu")
+ Tags = ImmutableArray.Create("amd", "hardware", "firmware", "cpu"),
+ EnabledByDefault = false
};
public static readonly SourceDefinition Arm = new()
@@ -1084,7 +1098,8 @@ public static class SourceDefinitions
HttpClientName = "ArmClient",
RequiresAuthentication = false,
DefaultPriority = 134,
- Tags = ImmutableArray.Create("arm", "hardware", "firmware", "cpu")
+ Tags = ImmutableArray.Create("arm", "hardware", "firmware", "cpu"),
+ EnabledByDefault = false
};
public static readonly SourceDefinition Siemens = new()
@@ -1099,7 +1114,8 @@ public static class SourceDefinitions
HttpClientName = "SiemensClient",
RequiresAuthentication = false,
DefaultPriority = 136,
- Tags = ImmutableArray.Create("siemens", "ics", "scada", "hardware")
+ Tags = ImmutableArray.Create("siemens", "ics", "scada", "hardware"),
+ EnabledByDefault = false
};
// ===== Package Manager Native Advisories =====
@@ -1116,7 +1132,8 @@ public static class SourceDefinitions
HttpClientName = "RustSecClient",
RequiresAuthentication = false,
DefaultPriority = 63,
- Tags = ImmutableArray.Create("rustsec", "package-manager", "rust", "cargo")
+ Tags = ImmutableArray.Create("rustsec", "package-manager", "rust", "cargo"),
+ EnabledByDefault = false
};
public static readonly SourceDefinition PyPa = new()
@@ -1131,7 +1148,8 @@ public static class SourceDefinitions
HttpClientName = "PyPaClient",
RequiresAuthentication = false,
DefaultPriority = 53,
- Tags = ImmutableArray.Create("pypa", "package-manager", "python", "pip")
+ Tags = ImmutableArray.Create("pypa", "package-manager", "python", "pip"),
+ EnabledByDefault = false
};
public static readonly SourceDefinition GoVuln = new()
@@ -1146,7 +1164,8 @@ public static class SourceDefinitions
HttpClientName = "GoVulnClient",
RequiresAuthentication = false,
DefaultPriority = 55,
- Tags = ImmutableArray.Create("govuln", "package-manager", "go", "golang")
+ Tags = ImmutableArray.Create("govuln", "package-manager", "go", "golang"),
+ EnabledByDefault = false
};
public static readonly SourceDefinition BundlerAudit = new()
@@ -1161,7 +1180,8 @@ public static class SourceDefinitions
HttpClientName = "BundlerAuditClient",
RequiresAuthentication = false,
DefaultPriority = 57,
- Tags = ImmutableArray.Create("bundler", "package-manager", "ruby", "rubysec")
+ Tags = ImmutableArray.Create("bundler", "package-manager", "ruby", "rubysec"),
+ EnabledByDefault = false
};
// ===== Additional CERTs =====
@@ -1179,7 +1199,8 @@ public static class SourceDefinitions
RequiresAuthentication = false,
Regions = ImmutableArray.Create("UA"),
DefaultPriority = 95,
- Tags = ImmutableArray.Create("cert", "ukraine")
+ Tags = ImmutableArray.Create("cert", "ukraine"),
+ EnabledByDefault = false
};
public static readonly SourceDefinition CertPl = new()
@@ -1195,7 +1216,8 @@ public static class SourceDefinitions
RequiresAuthentication = false,
Regions = ImmutableArray.Create("PL", "EU"),
DefaultPriority = 96,
- Tags = ImmutableArray.Create("cert", "poland", "eu")
+ Tags = ImmutableArray.Create("cert", "poland", "eu"),
+ EnabledByDefault = false
};
public static readonly SourceDefinition AusCert = new()
@@ -1211,7 +1233,8 @@ public static class SourceDefinitions
RequiresAuthentication = false,
Regions = ImmutableArray.Create("AU", "APAC"),
DefaultPriority = 97,
- Tags = ImmutableArray.Create("cert", "australia", "apac")
+ Tags = ImmutableArray.Create("cert", "australia", "apac"),
+ EnabledByDefault = false
};
public static readonly SourceDefinition KrCert = new()
@@ -1227,7 +1250,8 @@ public static class SourceDefinitions
RequiresAuthentication = false,
Regions = ImmutableArray.Create("KR", "APAC"),
DefaultPriority = 98,
- Tags = ImmutableArray.Create("cert", "korea", "apac")
+ Tags = ImmutableArray.Create("cert", "korea", "apac"),
+ EnabledByDefault = false
};
public static readonly SourceDefinition CertIn = new()
@@ -1243,7 +1267,8 @@ public static class SourceDefinitions
RequiresAuthentication = false,
Regions = ImmutableArray.Create("IN", "APAC"),
DefaultPriority = 99,
- Tags = ImmutableArray.Create("cert", "india", "apac")
+ Tags = ImmutableArray.Create("cert", "india", "apac"),
+ EnabledByDefault = false
};
// ===== Russian/CIS Sources =====
@@ -1261,7 +1286,8 @@ public static class SourceDefinitions
RequiresAuthentication = false,
Regions = ImmutableArray.Create("RU", "CIS"),
DefaultPriority = 100,
- Tags = ImmutableArray.Create("fstec", "bdu", "russia", "cis")
+ Tags = ImmutableArray.Create("fstec", "bdu", "russia", "cis"),
+ EnabledByDefault = false
};
public static readonly SourceDefinition Nkcki = new()
@@ -1277,7 +1303,8 @@ public static class SourceDefinitions
RequiresAuthentication = false,
Regions = ImmutableArray.Create("RU", "CIS"),
DefaultPriority = 101,
- Tags = ImmutableArray.Create("nkcki", "russia", "cis", "cert")
+ Tags = ImmutableArray.Create("nkcki", "russia", "cis", "cert"),
+ EnabledByDefault = false
};
public static readonly SourceDefinition KasperskyIcs = new()
@@ -1293,7 +1320,8 @@ public static class SourceDefinitions
RequiresAuthentication = false,
Regions = ImmutableArray.Create("RU", "CIS", "GLOBAL"),
DefaultPriority = 102,
- Tags = ImmutableArray.Create("kaspersky", "ics", "russia", "cis", "scada")
+ Tags = ImmutableArray.Create("kaspersky", "ics", "russia", "cis", "scada"),
+ EnabledByDefault = false
};
public static readonly SourceDefinition AstraLinux = new()
@@ -1309,7 +1337,8 @@ public static class SourceDefinitions
RequiresAuthentication = false,
Regions = ImmutableArray.Create("RU", "CIS"),
DefaultPriority = 48,
- Tags = ImmutableArray.Create("astra", "distro", "linux", "fstec", "russia")
+ Tags = ImmutableArray.Create("astra", "distro", "linux", "fstec", "russia"),
+ EnabledByDefault = false
};
// ===== Threat Intelligence =====
@@ -1327,7 +1356,8 @@ public static class SourceDefinitions
RequiresAuthentication = false,
DocumentationUrl = "https://attack.mitre.org/",
DefaultPriority = 140,
- Tags = ImmutableArray.Create("mitre", "attack", "threat-intel", "tactics")
+ Tags = ImmutableArray.Create("mitre", "attack", "threat-intel", "tactics"),
+ EnabledByDefault = false
};
public static readonly SourceDefinition MitreD3fend = new()
@@ -1343,7 +1373,8 @@ public static class SourceDefinitions
RequiresAuthentication = false,
DocumentationUrl = "https://d3fend.mitre.org/",
DefaultPriority = 142,
- Tags = ImmutableArray.Create("mitre", "d3fend", "threat-intel", "defensive")
+ Tags = ImmutableArray.Create("mitre", "d3fend", "threat-intel", "defensive"),
+ EnabledByDefault = false
};
// ===== StellaOps Mirror =====
@@ -1362,7 +1393,8 @@ public static class SourceDefinitions
StatusPageUrl = "https://status.stella-ops.org/",
DocumentationUrl = "https://docs.stella-ops.org/mirror/",
DefaultPriority = 1, // Highest priority when using mirror mode
- Tags = ImmutableArray.Create("stella", "mirror", "aggregated")
+ Tags = ImmutableArray.Create("stella", "mirror", "aggregated"),
+ EnabledByDefault = false
};
// ===== All Sources Collection =====
diff --git a/src/Platform/StellaOps.Platform.WebService/Endpoints/QuotaCompatibilityEndpoints.cs b/src/Platform/StellaOps.Platform.WebService/Endpoints/QuotaCompatibilityEndpoints.cs
new file mode 100644
index 000000000..0a94148d7
--- /dev/null
+++ b/src/Platform/StellaOps.Platform.WebService/Endpoints/QuotaCompatibilityEndpoints.cs
@@ -0,0 +1,709 @@
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using StellaOps.Auth.Abstractions;
+using StellaOps.Auth.ServerIntegration;
+using StellaOps.Auth.ServerIntegration.Tenancy;
+using System.Globalization;
+
+namespace StellaOps.Platform.WebService.Endpoints;
+
+public static class QuotaCompatibilityEndpoints
+{
+ public static IEndpointRouteBuilder MapQuotaCompatibilityEndpoints(this IEndpointRouteBuilder app)
+ {
+ // --- Authority quota endpoints (routed from /api/v1/authority/quotas) ---
+ var quotaGroup = app.MapGroup("/api/v1/authority/quotas")
+ .WithTags("Quota Compatibility")
+ .RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.OpsHealth))
+ .RequireTenant();
+
+ quotaGroup.MapGet("/dashboard", (TimeProvider timeProvider) =>
+ {
+ var now = timeProvider.GetUtcNow();
+ return Results.Ok(new
+ {
+ entitlement = BuildEntitlement(now),
+ consumption = BuildConsumptionMetrics(now),
+ tenantCount = 3,
+ activeAlerts = 1,
+ recentViolations = 4
+ });
+ })
+ .WithName("QuotaCompatibility.GetDashboard");
+
+ quotaGroup.MapGet("/", (TimeProvider timeProvider) =>
+ {
+ var now = timeProvider.GetUtcNow();
+ return Results.Ok(BuildEntitlement(now));
+ })
+ .WithName("QuotaCompatibility.GetEntitlement");
+
+ quotaGroup.MapGet("/consumption", (TimeProvider timeProvider) =>
+ {
+ var now = timeProvider.GetUtcNow();
+ return Results.Ok(BuildConsumptionMetrics(now));
+ })
+ .WithName("QuotaCompatibility.GetConsumption");
+
+ quotaGroup.MapGet("/history", (
+ [FromQuery] string? startDate,
+ [FromQuery] string? endDate,
+ [FromQuery] string? categories,
+ [FromQuery] string aggregation,
+ TimeProvider timeProvider) =>
+ {
+ var now = timeProvider.GetUtcNow();
+ var effectiveAggregation = string.IsNullOrWhiteSpace(aggregation) ? "daily" : aggregation;
+ var periodStart = ParseDateOrDefault(startDate, now.AddDays(-7));
+ var periodEnd = ParseDateOrDefault(endDate, now);
+
+ var requestedCategories = string.IsNullOrWhiteSpace(categories)
+ ? new[] { "license", "jobs", "api", "storage", "scans" }
+ : categories.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+
+ return Results.Ok(new
+ {
+ period = new
+ {
+ start = periodStart.ToString("O", CultureInfo.InvariantCulture),
+ end = periodEnd.ToString("O", CultureInfo.InvariantCulture)
+ },
+ points = BuildHistoryPoints(periodStart, periodEnd, effectiveAggregation, requestedCategories),
+ aggregation = effectiveAggregation
+ });
+ })
+ .WithName("QuotaCompatibility.GetHistory");
+
+ quotaGroup.MapGet("/tenants", (
+ [FromQuery] string? search,
+ [FromQuery] string? sortBy,
+ [FromQuery] string sortDir,
+ [FromQuery] int limit,
+ [FromQuery] int offset,
+ TimeProvider timeProvider) =>
+ {
+ var now = timeProvider.GetUtcNow();
+ var effectiveLimit = limit > 0 ? limit : 50;
+ var all = BuildTenantQuotaUsages(now);
+
+ if (!string.IsNullOrWhiteSpace(search))
+ {
+ all = all.Where(t =>
+ t.TenantName.Contains(search, StringComparison.OrdinalIgnoreCase) ||
+ t.TenantId.Contains(search, StringComparison.OrdinalIgnoreCase)).ToArray();
+ }
+
+ var items = all.Skip(offset > 0 ? offset : 0).Take(effectiveLimit).ToArray();
+ return Results.Ok(new
+ {
+ items,
+ total = all.Length
+ });
+ })
+ .WithName("QuotaCompatibility.GetTenantQuotas");
+
+ quotaGroup.MapGet("/tenants/{tenantId}", (
+ string tenantId,
+ TimeProvider timeProvider) =>
+ {
+ var now = timeProvider.GetUtcNow();
+ return Results.Ok(BuildTenantBreakdown(tenantId, now));
+ })
+ .WithName("QuotaCompatibility.GetTenantBreakdown");
+
+ quotaGroup.MapGet("/forecast", (
+ [FromQuery] string? category,
+ [FromQuery] string? tenantId,
+ TimeProvider timeProvider) =>
+ {
+ return Results.Ok(BuildForecasts(category));
+ })
+ .WithName("QuotaCompatibility.GetForecast");
+
+ quotaGroup.MapGet("/alerts", () =>
+ {
+ return Results.Ok(BuildAlertConfig());
+ })
+ .WithName("QuotaCompatibility.GetAlertConfig");
+
+ quotaGroup.MapPost("/alerts", (QuotaAlertConfigRequest config) =>
+ {
+ // Accept and echo back the config (mock persistence)
+ return Results.Ok(config);
+ })
+ .WithName("QuotaCompatibility.SaveAlertConfig");
+
+ quotaGroup.MapPost("/reports", (
+ QuotaReportRequestBody request,
+ TimeProvider timeProvider) =>
+ {
+ var now = timeProvider.GetUtcNow();
+ return Results.Ok(new
+ {
+ reportId = $"quota-report-{now:yyyyMMddHHmmss}",
+ status = "completed",
+ downloadUrl = (string?)null,
+ createdAt = now.ToString("O", CultureInfo.InvariantCulture),
+ completedAt = now.AddSeconds(2).ToString("O", CultureInfo.InvariantCulture),
+ expiresAt = now.AddHours(24).ToString("O", CultureInfo.InvariantCulture)
+ });
+ })
+ .WithName("QuotaCompatibility.RequestReport");
+
+ quotaGroup.MapGet("/reports/{reportId}", (
+ string reportId,
+ TimeProvider timeProvider) =>
+ {
+ var now = timeProvider.GetUtcNow();
+ return Results.Ok(new
+ {
+ reportId,
+ status = "completed",
+ downloadUrl = (string?)null,
+ createdAt = now.AddMinutes(-5).ToString("O", CultureInfo.InvariantCulture),
+ completedAt = now.AddMinutes(-4).ToString("O", CultureInfo.InvariantCulture),
+ expiresAt = now.AddHours(24).ToString("O", CultureInfo.InvariantCulture)
+ });
+ })
+ .WithName("QuotaCompatibility.GetReportStatus");
+
+ // --- Gateway rate-limit endpoints (routed from /api/v1/gateway/rate-limits) ---
+ var rateLimitGroup = app.MapGroup("/api/v1/gateway/rate-limits")
+ .WithTags("Quota Compatibility")
+ .RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.OpsHealth))
+ .RequireTenant();
+
+ rateLimitGroup.MapGet("/", (TimeProvider timeProvider) =>
+ {
+ var now = timeProvider.GetUtcNow();
+ return Results.Ok(BuildRateLimitStatuses(now));
+ })
+ .WithName("QuotaCompatibility.GetRateLimitStatus");
+
+ rateLimitGroup.MapGet("/violations", (
+ [FromQuery] string? startDate,
+ [FromQuery] string? endDate,
+ [FromQuery] string? tenantId,
+ [FromQuery] int limit,
+ TimeProvider timeProvider) =>
+ {
+ var now = timeProvider.GetUtcNow();
+ var effectiveLimit = limit > 0 ? limit : 50;
+ var periodStart = ParseDateOrDefault(startDate, now.AddDays(-7));
+ var periodEnd = ParseDateOrDefault(endDate, now);
+
+ return Results.Ok(new
+ {
+ items = BuildRateLimitViolations(now).Take(effectiveLimit).ToArray(),
+ total = 3,
+ period = new
+ {
+ start = periodStart.ToString("O", CultureInfo.InvariantCulture),
+ end = periodEnd.ToString("O", CultureInfo.InvariantCulture)
+ }
+ });
+ })
+ .WithName("QuotaCompatibility.GetRateLimitViolations");
+
+ // --- JobEngine quota endpoints (routed from /api/v1/jobengine/quotas) ---
+ var jobQuotaGroup = app.MapGroup("/api/v1/jobengine/quotas")
+ .WithTags("Quota Compatibility")
+ .RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.OpsHealth))
+ .RequireTenant();
+
+ jobQuotaGroup.MapGet("/", () =>
+ {
+ return Results.Ok(new
+ {
+ concurrentJobs = new { current = 3, limit = 10 },
+ dailyJobLimit = new { current = 47, limit = 500 },
+ queueDepth = 2,
+ averageJobDuration = 34.5
+ });
+ })
+ .WithName("QuotaCompatibility.GetJobQuotaStatus");
+
+ jobQuotaGroup.MapGet("/summary", (TimeProvider timeProvider) =>
+ {
+ var now = timeProvider.GetUtcNow();
+ return Results.Ok(new
+ {
+ concurrentJobs = new { current = 3, limit = 10, percentage = 30.0 },
+ dailyJobLimit = new { current = 47, limit = 500, percentage = 9.4 },
+ queueDepth = 2,
+ averageJobDuration = 34.5,
+ activeWorkers = 4,
+ totalWorkers = 6,
+ lastUpdated = now.ToString("O", CultureInfo.InvariantCulture)
+ });
+ })
+ .WithName("QuotaCompatibility.GetJobQuotaSummary");
+
+ return app;
+ }
+
+ // ---- Data builders ----
+
+ private static object BuildEntitlement(DateTimeOffset now) => new
+ {
+ planId = "enterprise-v2",
+ planName = "Enterprise",
+ features = new[]
+ {
+ "unlimited_tenants", "advanced_scanning", "policy_engine",
+ "air_gap_mode", "federation", "custom_crypto"
+ },
+ limits = new
+ {
+ artifacts = 50000,
+ users = 500,
+ scansPerDay = 1000,
+ storageMb = 102400,
+ concurrentJobs = 10,
+ apiRequestsPerMinute = 600
+ },
+ validFrom = now.AddYears(-1).ToString("O", CultureInfo.InvariantCulture),
+ validTo = now.AddYears(1).ToString("O", CultureInfo.InvariantCulture),
+ renewalDate = now.AddMonths(10).ToString("O", CultureInfo.InvariantCulture)
+ };
+
+ private static object[] BuildConsumptionMetrics(DateTimeOffset now)
+ {
+ var ts = now.ToString("O", CultureInfo.InvariantCulture);
+ return
+ [
+ new
+ {
+ category = "license",
+ current = 142,
+ limit = 500,
+ percentage = 28.4,
+ status = "healthy",
+ trend = "up",
+ trendPercentage = 3.2,
+ lastUpdated = ts
+ },
+ new
+ {
+ category = "jobs",
+ current = 47,
+ limit = 500,
+ percentage = 9.4,
+ status = "healthy",
+ trend = "stable",
+ trendPercentage = 0.8,
+ lastUpdated = ts
+ },
+ new
+ {
+ category = "api",
+ current = 312,
+ limit = 600,
+ percentage = 52.0,
+ status = "healthy",
+ trend = "up",
+ trendPercentage = 5.1,
+ lastUpdated = ts
+ },
+ new
+ {
+ category = "storage",
+ current = 24576,
+ limit = 102400,
+ percentage = 24.0,
+ status = "healthy",
+ trend = "up",
+ trendPercentage = 1.8,
+ lastUpdated = ts
+ },
+ new
+ {
+ category = "scans",
+ current = 187,
+ limit = 1000,
+ percentage = 18.7,
+ status = "healthy",
+ trend = "stable",
+ trendPercentage = 0.4,
+ lastUpdated = ts
+ }
+ ];
+ }
+
+ private static object[] BuildHistoryPoints(
+ DateTimeOffset start,
+ DateTimeOffset end,
+ string aggregation,
+ string[] categories)
+ {
+ var interval = aggregation switch
+ {
+ "hourly" => TimeSpan.FromHours(1),
+ "weekly" => TimeSpan.FromDays(7),
+ _ => TimeSpan.FromDays(1)
+ };
+
+ var points = new List