diff --git a/devops/compose/docker-compose.stella-ops.yml b/devops/compose/docker-compose.stella-ops.yml index 57b014360..cbef26e74 100644 --- a/devops/compose/docker-compose.stella-ops.yml +++ b/devops/compose/docker-compose.stella-ops.yml @@ -325,7 +325,7 @@ services: console-builder: condition: service_completed_successfully environment: - ASPNETCORE_URLS: "http://0.0.0.0:8080" + ASPNETCORE_URLS: "http://0.0.0.0:8080;https://0.0.0.0:443" <<: [*kestrel-cert, *gc-heavy] ConnectionStrings__Default: *postgres-connection ConnectionStrings__Redis: "cache.stella-ops.local:6379" @@ -832,6 +832,9 @@ services: CONCELIER_PLUGINS__BASEDIRECTORY: "/tmp/stellaops" CONCELIER_POSTGRESSTORAGE__CONNECTIONSTRING: *postgres-connection CONCELIER_POSTGRESSTORAGE__ENABLED: "true" + CONCELIER_MIRROR__ENABLED: "true" + CONCELIER_MIRROR__EXPORTROOT: "/var/lib/concelier/jobs/mirror-exports" + CONCELIER_MIRROR__ACTIVEEXPORTID: "latest" CONCELIER_S3__ENDPOINT: "http://s3.stella-ops.local:8333" CONCELIER_AUTHORITY__ENABLED: "true" CONCELIER_AUTHORITY__ISSUER: "https://authority.stella-ops.local/" diff --git a/devops/compose/postgres-init/04-authority-schema.sql b/devops/compose/postgres-init/04-authority-schema.sql index 5ec0447d5..101da66f6 100644 --- a/devops/compose/postgres-init/04-authority-schema.sql +++ b/devops/compose/postgres-init/04-authority-schema.sql @@ -659,7 +659,9 @@ VALUES 'platform.context.read', 'platform.context.write', 'doctor:run', 'doctor:admin', 'ops.health', 'integration:read', 'integration:write', 'integration:operate', 'registry.admin', - 'timeline:read', 'timeline:write'], + 'timeline:read', 'timeline:write', + 'signer:read', 'signer:sign', 'signer:rotate', 'signer:admin', + 'trust:read', 'trust:write', 'trust:admin'], ARRAY['authorization_code', 'refresh_token'], false, true, '{"tenant": "demo-prod"}'::jsonb) ON CONFLICT (client_id) DO NOTHING; diff --git a/devops/compose/postgres-init/16-release-full-schema.sql b/devops/compose/postgres-init/16-release-full-schema.sql index 202547950..7ad9d9542 100644 --- a/devops/compose/postgres-init/16-release-full-schema.sql +++ b/devops/compose/postgres-init/16-release-full-schema.sql @@ -58,3 +58,72 @@ $$; -- Analytics schema CREATE SCHEMA IF NOT EXISTS analytics; + +-- ── Regions (bootstrap fallback for release.regions) ── +CREATE TABLE IF NOT EXISTS release.regions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES shared.tenants(id) ON DELETE CASCADE, + name VARCHAR(100) NOT NULL, + display_name VARCHAR(255) NOT NULL, + description TEXT, + crypto_profile VARCHAR(50) NOT NULL DEFAULT 'international', + sort_order INT NOT NULL DEFAULT 0, + status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active','decommissioning','archived')), + metadata JSONB NOT NULL DEFAULT '{}', + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID, + UNIQUE(tenant_id, name) +); + +-- ── Infrastructure Bindings (bootstrap fallback) ── +CREATE TABLE IF NOT EXISTS release.infrastructure_bindings ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES shared.tenants(id) ON DELETE CASCADE, + integration_id UUID, + scope_type TEXT NOT NULL CHECK (scope_type IN ('tenant','region','environment')), + scope_id UUID, + binding_role TEXT NOT NULL CHECK (binding_role IN ('registry','vault','settings_store')), + priority INT NOT NULL DEFAULT 0, + config_overrides JSONB NOT NULL DEFAULT '{}', + is_active BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID +); + +-- ── Topology Point Status (bootstrap fallback) ── +CREATE TABLE IF NOT EXISTS release.topology_point_status ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + target_id UUID, + gate_name TEXT NOT NULL, + status TEXT NOT NULL CHECK (status IN ('pending','pass','fail','skip')), + message TEXT, + details JSONB NOT NULL DEFAULT '{}', + checked_at TIMESTAMPTZ, + duration_ms INT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- ── Pending Deletions (bootstrap fallback) ── +CREATE TABLE IF NOT EXISTS release.pending_deletions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + entity_type TEXT NOT NULL CHECK (entity_type IN ('tenant','region','environment','target','agent','integration')), + entity_id UUID NOT NULL, + entity_name TEXT NOT NULL, + status TEXT NOT NULL CHECK (status IN ('pending','confirmed','executing','completed','cancelled')), + cool_off_hours INT NOT NULL, + cool_off_expires_at TIMESTAMPTZ NOT NULL, + cascade_summary JSONB NOT NULL DEFAULT '{}', + reason TEXT, + requested_by UUID NOT NULL, + requested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + confirmed_by UUID, + confirmed_at TIMESTAMPTZ, + executed_at TIMESTAMPTZ, + completed_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); diff --git a/docs/implplan/SPRINT_20260316_001_Platform_first_time_user_experience_fixes.md b/docs/implplan/SPRINT_20260316_001_Platform_first_time_user_experience_fixes.md index 862ff4691..096b782e0 100644 --- a/docs/implplan/SPRINT_20260316_001_Platform_first_time_user_experience_fixes.md +++ b/docs/implplan/SPRINT_20260316_001_Platform_first_time_user_experience_fixes.md @@ -119,6 +119,10 @@ Completion criteria: | 2026-03-16 | FTUX-006 DONE: Removed ALL hardcoded fake data from dashboard-v3.component.ts. Fresh installs now show welcome setup guide with 4 steps. Environment cards show honest "unknown"/"No deployments" when no scan data exists. Removed fake summary, reachabilityStats, nightlyOpsSignals, alerts, and activity HTML. | Developer | | 2026-03-16 | FTUX-007 DONE: Updated FEATURE_MATRIX.md — 14 release orchestration features marked ✅ (was ⏳), section header updated. | Developer | | 2026-03-16 | Angular build verified — 0 errors, 3 pre-existing budget warnings only. | Developer | +| 2026-03-16 | Iteration 1: Wiped stack, fresh boot. Found dashboard fallback array still had fake data. Emptied it. Rebuild + redeploy. Dashboard now honest on fresh install. | Developer | +| 2026-03-16 | Iteration 2: Integration journey. Harbor + GitHub App fixtures started. Both created and connection-tested successfully. "Check All" advisory sources failed with 504 gateway timeout — fixed with parallel individual checks in batches of 6. Now shows live "Checking (N/M)..." progress, completes in ~30s. 54/55 healthy. | Developer | +| 2026-03-16 | Iteration 2: Mirror domain created (14 sources, signing enabled). "Generate immediately" fails silently (tracked). Created by shows raw user ID (tracked). | Developer | +| 2026-03-16 | Iteration 3: Topology wizard returned 503 for /api/v1/regions — Concelier topology endpoints had no gateway routes. Added 6 Microservice routes for regions, infrastructure-bindings, pending-deletions, targets validate/readiness, environments readiness. Wizard now loads. | Developer | ## Decisions & Risks - Decision: curate advisory defaults rather than disable all — new users need working sources out of the box, just not 74 of them. diff --git a/docs/modules/jobengine/architecture.md b/docs/modules/jobengine/architecture.md index b5f63cae1..4f925b1c0 100644 --- a/docs/modules/jobengine/architecture.md +++ b/docs/modules/jobengine/architecture.md @@ -5,7 +5,7 @@ ## 1) Topology - **Orchestrator API (`StellaOps.JobEngine`).** Minimal API providing job state, throttling controls, replay endpoints, and dashboard data. Authenticated via Authority scopes (`orchestrator:*`). -- **Job ledger (PostgreSQL).** Tables `jobs`, `job_history`, `sources`, `quotas`, `throttles`, `incidents` (schema `orchestrator`). Startup migrations execute with PostgreSQL `search_path` bound to `orchestrator, public` so unqualified DDL lands in the module schema during scratch installs and resets. Append-only history ensures auditability. +- **Job ledger (PostgreSQL).** Tables `jobs`, `job_history`, `sources`, `quotas`, `throttles`, `incidents` (schema `orchestrator`); pack registry tables `packs`, `pack_versions` (schema `packs`). Runtime sessions set `search_path` to `orchestrator, packs, public` so both schemas and their enum types (`job_status`, `pack_status`, `pack_version_status`) resolve correctly. The `jobs/summary` endpoint uses a single `COUNT(*) FILTER (WHERE status::text = ...)` aggregate query to avoid enum-vs-text mismatch and eliminate per-status round trips. Startup migrations execute idempotently; append-only history ensures auditability. - **Queue abstraction.** Supports Valkey Streams or NATS JetStream (pluggable). Each job carries lease metadata and retry policy. - **Dashboard feeds.** SSE/GraphQL endpoints supply Console UI with job timelines, throughput, error distributions, and rate-limit status. diff --git a/docs/qa/ADVISORY_VEX_MIRROR_SETUP_AUDIT_20260315.md b/docs/qa/ADVISORY_VEX_MIRROR_SETUP_AUDIT_20260315.md new file mode 100644 index 000000000..57464bc17 --- /dev/null +++ b/docs/qa/ADVISORY_VEX_MIRROR_SETUP_AUDIT_20260315.md @@ -0,0 +1,354 @@ +# Advisory & VEX Mirror Setup Audit - 2026-03-15 + +**Auditor**: AI agent acting as first-time operator setting up Stella Ops as a vulnerability/VEX advisory mirror +**Stack**: Live local (stella-ops.local), logged in as admin/Admin@Stella2026! +**Scope**: End-to-end assessment of adding, selecting, grouping, and aggregating advisory/VEX sources via UI, CLI, and backend + +--- + +## Executive Summary + +Stella Ops has a **well-architected backend** for advisory/VEX aggregation (47 sources, rate limiting, backoff, deduplication, conflict detection, VEX normalization, airgap/offline support). The **CLI is fully functional** with source management commands. However, **the UI has critical gaps** that prevent a first-time operator from setting up advisory sources without CLI or developer knowledge. + +| Layer | Readiness | +|-------|-----------| +| Backend catalog (47 sources, 9 categories) | READY | +| Rate limiting & backoff | READY | +| VEX ingestion pipeline | READY | +| CLI source management | READY | +| CLI setup wizard | READY | +| Feeds & Airgap operations page | PARTIAL | +| UI source addition flow | MISSING | +| UI group/batch source selection | MISSING | +| UI source configuration (API keys, intervals) | MISSING | + +--- + +## 1. Backend Source Catalog Assessment + +### 1.1 Supported Sources (47 total) + +**File**: `src/Concelier/__Libraries/StellaOps.Concelier.Core/Sources/SourceDefinitions.cs` + +| Category | Count | Sources | +|----------|-------|---------| +| Primary Databases | 6 | NVD (NIST), OSV (Google), GitHub Security Advisories, CVE.org (MITRE), EPSS (FIRST), CISA KEV | +| Vendor Advisories | 11 | Red Hat, Microsoft MSRC, Amazon Linux, Google, Oracle, Apple, Cisco, Fortinet, Juniper, Palo Alto, VMware | +| Linux Distributions | 9 | Debian, Ubuntu, Alpine, SUSE, RHEL, CentOS, Fedora, Arch, Gentoo | +| Language Ecosystems | 9 | npm, PyPI, Go, RubyGems, NuGet, Maven, Crates.io, Packagist, Hex.pm | +| CSAF/VEX | 3 | CSAF Aggregator, CSAF TC Trusted Publishers, VEX Hub | +| CERTs/Government | 8 | CERT-FR, CERT-Bund (DE), CERT.at (AT), CERT.be (BE), NCSC-CH (CH), CERT-EU, JPCERT/CC (JP), CISA (US) | +| StellaOps Mirror | 1 | Pre-aggregated mirror endpoint | + +**Assessment**: Comprehensive coverage. Each source has: ID, display name, category, base endpoint, health check endpoint, auth requirements, credential env var, documentation URL, default priority, region tags, and grouping tags. + +### 1.2 Source Grouping Support (Backend) + +**Grouping methods available in `SourceDefinitions`**: +- `GetByCategory(SourceCategory)` - Group by Primary/Vendor/Distribution/Ecosystem/Cert/Csaf/Threat/Mirror +- `GetByTag(string)` - Group by tags (e.g., "linux", "network", "eu", "ecosystem") +- `GetByRegion(string)` - Group by geographic region (FR, DE, EU, JP, APAC, US, NA) +- `GetAuthenticatedSources()` - Filter sources requiring API keys + +**Assessment**: Backend supports flexible grouping. Tags like "vendor", "distro", "linux", "eu", "ecosystem" enable batch operations. **However, none of this is exposed in the UI.** + +### 1.3 Configuration Model + +**File**: `src/Concelier/__Libraries/StellaOps.Concelier.Core/Configuration/SourceConfiguration.cs` + +- **Source modes**: Direct (upstream), Mirror (pre-aggregated), Hybrid (mirror + direct fallback) +- **Per-source config**: Enabled/disabled, priority, API key, custom endpoint, request delay, failure backoff, max pages per fetch, metadata +- **Mirror server config**: Export root, authentication (Anonymous/OAuth/ApiKey/mTLS), rate limits, DSSE attestation signing +- **Auto-enable**: `AutoEnableHealthySources = true` by default + +**Assessment**: Configuration model is complete and well-designed. + +--- + +## 2. Rate Limiting & Graceful Aggregation Assessment + +### 2.1 Per-Source Rate Limiting (Outbound - Concelier) + +**File**: `src/Concelier/__Libraries/StellaOps.Concelier.Core/Configuration/SourceConfiguration.cs` + +| Setting | Default | Purpose | +|---------|---------|---------| +| `RequestDelay` | 200ms | Delay between consecutive API calls to same source | +| `FailureBackoff` | 5 minutes | Cooldown after a source returns errors | +| `MaxPagesPerFetch` | 10 | Cap pages fetched per sync cycle | +| `ConnectivityCheckTimeout` | 30 seconds | Health check timeout | + +**Assessment**: These defaults are reasonable and won't trigger upstream rate limits. NVD allows 50 req/30s with API key (200ms = 5 req/s fits). OSV has no published rate limit. GHSA via GraphQL is limited to 5000 points/hour. + +### 2.2 VEX Hub Polling (Scheduler) + +**File**: `src/VexHub/__Libraries/StellaOps.VexHub.Core/Ingestion/VexIngestionScheduler.cs` + +| Setting | Default | Purpose | +|---------|---------|---------| +| `DefaultPollingIntervalSeconds` | 3600 (1 hour) | How often each source is polled | +| `MaxConcurrentPolls` | 4 | SemaphoreSlim-limited concurrent ingestions | +| `MaxRetries` | 3 | Retries per ingestion attempt | +| `FetchTimeoutSeconds` | 300 (5 min) | Per-source fetch timeout | +| `BatchSize` | 500 | Statements per batch upsert | + +**Scheduler behavior**: +- Runs every 1 minute checking for due sources (`GetDueForPollingAsync`) +- Throttles with `SemaphoreSlim` (max 4 concurrent) +- Updates `LastPolledAt` and `LastErrorMessage` per source after each poll +- Per-source configurable `PollingIntervalSeconds` + +**Assessment**: 1-hour default polling interval with max 4 concurrent is very conservative and graceful. No DDoS risk. Sources that fail get error logged and next poll delayed by their interval. **However, there is no exponential backoff** - a source that fails will be retried at the same interval. The `FailureBackoff` in `SourceConfig` (5 min) provides a short cooldown but not progressive backoff. + +### 2.3 Inbound Rate Limiting (VexHub Mirror Server) + +**File**: `src/VexHub/StellaOps.VexHub.WebService/Middleware/RateLimitingMiddleware.cs` + +| Setting | Default | Purpose | +|---------|---------|---------| +| Anonymous limit | 60 req/min | Sliding window per IP | +| Authenticated limit | 120 req/min | Sliding window per API key | +| Idle cleanup | 5 min | Expired client entries pruned | + +**Headers**: `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, `Retry-After` + +**Assessment**: Proper rate limiting for when Stella Ops acts as a mirror server. Standard headers support client retry logic. + +### 2.4 Deduplication & Conflict Detection + +**VEX Ingestion Pipeline**: +- SHA-256 content digest for deduplication +- Conflict detection: when two sources disagree on VEX status for the same CVE+product +- Conflict severity: Low/Medium/High/Critical +- Auto-resolution for low-severity conflicts +- Provenance tracking (audit trail per statement) + +**Assessment**: Well-designed. Prevents duplicate data accumulation and tracks disagreements between sources. + +--- + +## 3. CLI Source Management Assessment + +### 3.1 Sources Commands + +**File**: `src/Cli/StellaOps.Cli/Commands/Sources/SourcesCommandGroup.cs` + +| Command | Purpose | Status | +|---------|---------|--------| +| `stella sources list [--category] [--enabled-only] [--json]` | List all 47 sources with category filtering | IMPLEMENTED | +| `stella sources check [source] [--all] [--parallel N] [--timeout N] [--auto-disable]` | Connectivity check with auto-disable | IMPLEMENTED | +| `stella sources enable ` | Enable one or more sources by ID | IMPLEMENTED | +| `stella sources disable ` | Disable one or more sources by ID | IMPLEMENTED | +| `stella sources status [--json]` | Show current configuration status | IMPLEMENTED | + +**Assessment**: Full CRUD for source management via CLI. Supports batch enable/disable (multiple source IDs in one command). Category filtering available. Auto-disable on connectivity failure. + +### 3.2 Feeds Snapshot Commands + +**File**: `src/Cli/StellaOps.Cli/Commands/FeedsCommandGroup.cs` + +| Command | Purpose | Status | +|---------|---------|--------| +| `stella feeds snapshot create [--label] [--sources] [--json]` | Create atomic feed snapshot | IMPLEMENTED | +| `stella feeds snapshot list [--limit N]` | List available snapshots | IMPLEMENTED | +| `stella feeds snapshot export --output [--compression zstd\|gzip\|none]` | Export for offline/airgap use | IMPLEMENTED | +| `stella feeds snapshot import [--validate]` | Import snapshot bundle | IMPLEMENTED | +| `stella feeds snapshot validate ` | Validate snapshot for drift | IMPLEMENTED | + +**Assessment**: Complete snapshot lifecycle for offline/airgap operation. Supports zstd compression and integrity validation. + +### 3.3 CLI Setup Wizard + +**File**: `src/Cli/StellaOps.Cli/Commands/Setup/Steps/Implementations/SourcesSetupStep.cs` + +The interactive setup wizard: +1. Runs connectivity checks against all 47 sources in parallel +2. Displays results with latency and error details +3. Offers remediation steps for failed sources +4. Prompts: auto-disable failures / manual fix / keep all +5. Prompts source mode: Mirror (recommended) / Direct / Hybrid +6. Optionally configures mirror server (export root, auth, rate limits) +7. Reports final count of enabled sources + +**Assessment**: Excellent guided setup experience via CLI. This is exactly what the UI should replicate. + +--- + +## 4. UI Assessment - Critical Gaps + +### 4.1 Gap: No UI Flow to Add Advisory/VEX Sources (P0) + +**Route**: `/setup/integrations/advisory-vex-sources` (or `/ops/integrations/advisory-vex-sources`) + +**What exists**: An "Advisory & VEX" tab in the Integrations page showing "FeedMirror Integrations" with a "+ Add Integration" button. + +**What happens**: Clicking "+ Add Integration" navigates to `/setup/integrations/onboarding` (or `/ops/integrations/onboarding`) which shows the generic onboarding hub with only 4 categories: +1. Container Registries (Harbor) +2. Source Control (GitHub App) +3. CI/CD Pipelines (disabled) +4. Hosts & Observers (disabled) + +**Missing**: There is NO "Advisory & VEX Sources" category in the onboarding hub. A first-time operator clicking "Add Integration" from the Advisory & VEX tab lands on an irrelevant page with no way to add advisory sources. + +**Impact**: The primary action for setting up advisory mirroring is a dead end in the UI. + +### 4.2 Gap: No Source Catalog Browser in UI (P0) + +The backend defines 47 sources with categories, descriptions, auth requirements, credential URLs, and documentation links. **None of this is exposed in any UI page.** A first-time operator has no way to: +- Browse available sources +- See which sources require API keys +- Understand source categories +- Learn about source coverage + +### 4.3 Gap: No Group/Batch Source Selection in UI (P0) + +The backend supports grouping by category, tag, and region (`GetByCategory`, `GetByTag`, `GetByRegion`). **The UI has no batch selection.** An operator cannot: +- "Enable all Linux distribution sources" +- "Enable all EU CERT sources" +- "Enable all ecosystem sources for my language stack" +- "Enable everything in the Primary category" + +### 4.4 Gap: No Source Configuration UI (API keys, intervals) (P1) + +Sources like GHSA and NuGet require a `GITHUB_PAT` token. NVD recommends an API key for higher rate limits. **The UI has no form for entering per-source credentials, polling intervals, or priority.** + +### 4.5 Gap: FeedMirror Integrations Shows 0 but Feeds & Airgap Shows 2 (P1) + +**Disconnection**: +- `/ops/operations/feeds-airgap` shows "Mirrors 2" (NVD Mirror, OSV Mirror) both "Fresh" and "OK" +- `/setup/integrations/advisory-vex-sources` shows "No feedmirror integrations found" with "0 pass / 0 warn / 0 fail" + +These two pages show contradictory data. The operations page knows about 2 active mirrors but the integrations page shows 0. They appear to query different data sources. + +### 4.6 Gap: Security Page Shows 6 Sources All Offline (P1) + +**Route**: `/security` > "Advisories & VEX Health" section + +Shows 6 sources all "offline - unknown": +- Internal VEX +- KEV +- NVD +- OSV +- Vendor Advisories +- Vendor VEX + +Yet the Feeds & Airgap page shows NVD and OSV as "Fresh" and "OK". Another data disconnection. + +**Also**: The "Configure sources" link on this section navigates to `/ops/integrations/advisory-vex-sources` which is the empty FeedMirror Integrations page. Dead end loop. + +### 4.7 Gap: No Source Mode Selection in UI (P1) + +The backend supports Direct/Mirror/Hybrid modes. The CLI setup wizard presents this choice prominently. **The UI has no way to select or view the current source mode.** + +### 4.8 Gap: No Mirror Server Configuration in UI (P2) + +When Stella Ops operates as a mirror for downstream instances, the mirror server needs configuration (export root, authentication, rate limits, DSSE signing). **The CLI handles this but the UI does not.** + +### 4.9 Gap: No Connectivity Check UI (P2) + +The CLI has `stella sources check` with parallel connectivity testing, auto-disable, and remediation guidance. **The UI has no equivalent** - no "Test All Sources" button, no health check results. + +### 4.10 Gap: Airgap Bundles Tab Not Exercised (P2) + +**Route**: `/ops/operations/feeds-airgap?tab=airgap-bundles` + +The Airgap Bundles and Version Locks tabs exist in the Feeds & Airgap page but were not testable in this session (stayed on Feed Mirrors tab). These represent the offline/airgap workflow counterpart to `stella feeds snapshot export/import`. + +--- + +## 5. What Works Well + +| Feature | Location | Status | +|---------|----------|--------| +| Feed Mirrors monitoring | `/ops/operations/feeds-airgap` | 2 mirrors (NVD, OSV) synced, fresh, OK | +| Feed status in context bar | Global header | "Feed: Live" indicator with link | +| Freshness indicators | Feeds & Airgap table | "Fresh" with timestamp | +| Storage tracking | Feeds & Airgap summary | 12.4 GB tracked | +| Mirror mode display | Feeds & Airgap | "Mode: live mirrors (read-write)" | +| CLI source list/check/enable/disable | `stella sources *` | Full management | +| CLI setup wizard | `stella setup` | Guided interactive flow | +| CLI feed snapshots | `stella feeds snapshot *` | Complete offline workflow | +| Backend rate limiting | SourceConfig + VexIngestionScheduler | 200ms delay, 5min backoff, 4 concurrent max | +| Deduplication | VexIngestionService | SHA-256 content digest | +| Conflict detection | VexConflictRepository | Auto-resolve + manual review | + +--- + +## 6. Aggregation Gracefuless Assessment + +### Will upstream providers cut off access? + +**Risk: LOW** with current defaults. + +| Source | Rate Limit | Stella Default | Safe? | +|--------|-----------|---------------|-------| +| NVD | 50 req/30s (with key), 5 req/30s (without) | 200ms delay = 5 req/s, 1hr polling | YES (with key) | +| OSV | No published limit | 200ms delay, 1hr polling | YES | +| GHSA | 5000 points/hr (GraphQL) | 200ms delay, 1hr polling | YES | +| KEV | Static JSON file | 1hr polling | YES | +| EPSS | No published limit | 200ms delay, 1hr polling | YES | +| Vendor/CERT | Varies | 200ms delay, 1hr polling | YES | + +**Concerns**: +1. **No exponential backoff**: Failed sources retry at the same interval. If a source is temporarily down, Stella will retry every hour indefinitely. Should implement exponential backoff (1hr -> 2hr -> 4hr -> max 24hr). +2. **NVD without API key**: Default rate is 5 req/30s. The 200ms delay (5 req/s) would exceed this. The `RequiresAuthentication = false` flag and optional `NVD_API_KEY` env var are correctly modeled, but there's no UI guidance to obtain a key. +3. **MaxPagesPerFetch = 10**: This caps each sync to 10 pages, preventing bulk initial downloads from overwhelming sources. Good design. +4. **4 concurrent polls max**: Prevents parallel requests to the same source type from multiplying load. Good design. + +--- + +## 7. Priority Matrix + +| Priority | Issue | Category | +|----------|-------|----------| +| P0 | No UI flow to add advisory/VEX sources | UI | +| P0 | No source catalog browser in UI | UI | +| P0 | No group/batch source selection in UI | UI | +| P1 | No source configuration UI (API keys, intervals) | UI | +| P1 | FeedMirror Integrations vs Feeds & Airgap data disconnection | Data | +| P1 | Security page shows 6 sources offline while feeds page shows 2 healthy | Data | +| P1 | No source mode selection in UI (Direct/Mirror/Hybrid) | UI | +| P2 | No mirror server configuration in UI | UI | +| P2 | No connectivity check in UI | UI | +| P2 | No exponential backoff for failed sources | Backend | +| P2 | NVD without API key may exceed rate limit | Config | +| P3 | Airgap bundles and version locks tabs not wired to UX guidance | UI | + +--- + +## 8. Top 5 Actions for Maximum Self-Serve Impact + +1. **Add "Advisory & VEX Sources" category to the onboarding hub** - With a grouped source picker showing all 47 sources organized by category (Primary, Vendor, Distribution, Ecosystem, CERT, CSAF), with checkboxes, descriptions, auth requirements, and "Enable All in Category" buttons. + +2. **Wire FeedMirror Integrations page to actual feed mirror data** - The integrations page shows 0 while the operations page shows 2. These need to query the same data source so operators see a single truth. + +3. **Add source mode selector to setup** - Allow choosing Direct/Mirror/Hybrid from the UI, matching what the CLI setup wizard offers. + +4. **Add per-source configuration panel** - When clicking a source, show: enable/disable toggle, API key field (with link to credential URL), polling interval, priority, health status, last sync time. + +5. **Add exponential backoff for failed sources** - Currently retries at constant interval. Implement progressive backoff (1hr -> 2hr -> 4hr -> 8hr -> max 24hr) to be a good upstream citizen. + +--- + +## 9. Comparison: CLI vs UI Feature Parity + +| Feature | CLI | UI | +|---------|-----|-----| +| List all 47 sources | `stella sources list` | NO | +| Filter by category | `--category primary` | NO | +| Filter enabled only | `--enabled-only` | NO | +| Enable sources | `stella sources enable nvd osv ghsa` | NO | +| Disable sources | `stella sources disable centos arch` | NO | +| Batch enable/disable | Multiple IDs in one command | NO | +| Connectivity check | `stella sources check --all` | NO | +| Auto-disable failed | `--auto-disable` | NO | +| Source status | `stella sources status` | PARTIAL (Feeds & Airgap) | +| Source mode selection | Setup wizard prompt | NO | +| Mirror server config | Setup wizard prompt | NO | +| Feed snapshot create | `stella feeds snapshot create` | NO (only via Feeds & Airgap operations) | +| Feed snapshot export | `stella feeds snapshot export` | NO | +| Feed snapshot import | `stella feeds snapshot import` | NO | +| Feed freshness view | N/A | YES (Feeds & Airgap) | +| Feed health monitoring | N/A | YES (Feeds & Airgap + context bar) | + +**Conclusion**: The CLI is the only functional path for setting up advisory sources. The UI is read-only for feed operations and completely missing the write/configure path. This is the single biggest gap for making Stella Ops a self-serve product for vulnerability mirror setup. diff --git a/global.json b/global.json index e5f04e70a..53b4c2779 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "sdk": { "version": "10.0.103", - "rollForward": "disable", + "rollForward": "latestFeature", "allowPrerelease": false } } diff --git a/package-lock.json b/package-lock.json index 6a6105066..f8b25193e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "stellaops-docs", "version": "0.1.0", "dependencies": { - "@openai/codex": "^0.80.0", + "@openai/codex": "^0.115.0-alpha.24", "ajv": "^8.17.1", "ajv-formats": "^2.1.1", "yaml": "^2.4.5" @@ -18,13 +18,123 @@ } }, "node_modules/@openai/codex": { - "version": "0.80.0", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.80.0.tgz", - "integrity": "sha512-U1DWDy7eTjx+SF32Wx9oO6cyX1dd9WiRvIW4XCP3FVcv7Xq7CSCvDrFAdzpFxPNPg6CLz9a4qtO42yntpcJpDw==", + "version": "0.115.0-alpha.24", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.115.0-alpha.24.tgz", + "integrity": "sha512-fjeg+bslp5nK9PzcZuc11IX027nUHqmQroJCKhQ0O9ddqs7q2aEktBd8cv6iU8XRQBZrPjW/0+mzyXuHPA22rw==", "license": "Apache-2.0", "bin": { "codex": "bin/codex.js" }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@openai/codex-darwin-arm64": "npm:@openai/codex@0.115.0-alpha.24-darwin-arm64", + "@openai/codex-darwin-x64": "npm:@openai/codex@0.115.0-alpha.24-darwin-x64", + "@openai/codex-linux-arm64": "npm:@openai/codex@0.115.0-alpha.24-linux-arm64", + "@openai/codex-linux-x64": "npm:@openai/codex@0.115.0-alpha.24-linux-x64", + "@openai/codex-win32-arm64": "npm:@openai/codex@0.115.0-alpha.24-win32-arm64", + "@openai/codex-win32-x64": "npm:@openai/codex@0.115.0-alpha.24-win32-x64" + } + }, + "node_modules/@openai/codex-darwin-arm64": { + "name": "@openai/codex", + "version": "0.115.0-alpha.24-darwin-arm64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.115.0-alpha.24-darwin-arm64.tgz", + "integrity": "sha512-/vlH+wSZkHEsI6rdIB1Tcfjr5y1r8v8dV5XDre6dPZXDBp8o40BI3jfbRgVBPdrgWyb7SEKPcuJRjwu3FXoYKA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@openai/codex-darwin-x64": { + "name": "@openai/codex", + "version": "0.115.0-alpha.24-darwin-x64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.115.0-alpha.24-darwin-x64.tgz", + "integrity": "sha512-xAT5XmQOj0NLg3yu+QdBtgot5XPn4lw4w7ztaQwgf+OzilFwD69rmNH/rIXSUknvQmOFnKug0GtNjjKgdyctPw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@openai/codex-linux-arm64": { + "name": "@openai/codex", + "version": "0.115.0-alpha.24-linux-arm64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.115.0-alpha.24-linux-arm64.tgz", + "integrity": "sha512-IRhOx+qASa5d/YwnLzbvwsgFySMUg8lzB81PQgoDSAmsuRWcqA/uu9PCsQN9YKMjH4YFk6BMsfB+Ni40ZZUJ+Q==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@openai/codex-linux-x64": { + "name": "@openai/codex", + "version": "0.115.0-alpha.24-linux-x64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.115.0-alpha.24-linux-x64.tgz", + "integrity": "sha512-76LiFBGrp0d6EHY7sedQDXzNity6/xEEUbeSUZ7/k+Sa9hlob4E9Ti9Rz+ARLJLhObbHxQBYCRMsO9mIs8er+w==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@openai/codex-win32-arm64": { + "name": "@openai/codex", + "version": "0.115.0-alpha.24-win32-arm64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.115.0-alpha.24-win32-arm64.tgz", + "integrity": "sha512-b6j+GVd4BCjDOf/ruYWKYXnEo5QfBsLeJjUjlQ6KzAdnh7i1Xw8nZ32O4yVLm+ciUgVhf+2HvbPuEMdNQqF4ZQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@openai/codex-win32-x64": { + "name": "@openai/codex", + "version": "0.115.0-alpha.24-win32-x64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.115.0-alpha.24-win32-x64.tgz", + "integrity": "sha512-E51iK8gIjIe2KJlclXoxZ0b1UnSpJcT1q3NsvI7TAb+tg64p7dcMDBv4RV+Cm2OpQC/+RujLvzu50WzR4SRPBg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { "node": ">=16" } diff --git a/package.json b/package.json index f40c57e90..3fbaa8c91 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "sdk:smoke": "npm run sdk:smoke:ts && npm run sdk:smoke:python && npm run sdk:smoke:go && npm run sdk:smoke:java" }, "dependencies": { - "@openai/codex": "^0.80.0", + "@openai/codex": "^0.115.0-alpha.24", "ajv": "^8.17.1", "ajv-formats": "^2.1.1", "yaml": "^2.4.5" diff --git a/src/Authority/__Libraries/StellaOps.Authority.Persistence/Migrations/S001_demo_seed.sql b/src/Authority/__Libraries/StellaOps.Authority.Persistence/Migrations/S001_demo_seed.sql index 676b2c769..f62d41dbc 100644 --- a/src/Authority/__Libraries/StellaOps.Authority.Persistence/Migrations/S001_demo_seed.sql +++ b/src/Authority/__Libraries/StellaOps.Authority.Persistence/Migrations/S001_demo_seed.sql @@ -106,7 +106,9 @@ VALUES 'ops.health', 'integration:read', 'integration:write', 'integration:operate', 'registry.admin', 'advisory-ai:view', 'advisory-ai:operate', - 'timeline:read', 'timeline:write'], + 'timeline:read', 'timeline:write', + 'signer:read', 'signer:sign', 'signer:rotate', 'signer:admin', + 'trust:read', 'trust:write', 'trust:admin'], ARRAY['authorization_code', 'refresh_token'], false, true, '{"tenant": "demo-prod"}'::jsonb), ('demo-client-cli', 'stellaops-cli', 'Stella Ops CLI', 'Command-line client', true, diff --git a/src/Cli/StellaOps.Cli/Commands/CommandFactory.cs b/src/Cli/StellaOps.Cli/Commands/CommandFactory.cs index ba2883ba0..2322607e0 100644 --- a/src/Cli/StellaOps.Cli/Commands/CommandFactory.cs +++ b/src/Cli/StellaOps.Cli/Commands/CommandFactory.cs @@ -189,6 +189,9 @@ internal static class CommandFactory // Sprint: Setup Wizard - Settings Store Integration root.Add(Setup.SetupCommandGroup.BuildSetupCommand(services, verboseOption, cancellationToken)); + // Sprint: SPRINT_20260315_009 - Topology management commands + root.Add(Topology.TopologyCommandGroup.BuildTopologyCommand(verboseOption, cancellationToken)); + // Add scan graph subcommand to existing scan command var scanCommand = root.Children.OfType().FirstOrDefault(c => c.Name == "scan"); if (scanCommand is not null) diff --git a/src/Cli/StellaOps.Cli/Commands/Topology/TopologyCommandGroup.cs b/src/Cli/StellaOps.Cli/Commands/Topology/TopologyCommandGroup.cs new file mode 100644 index 000000000..6b9990489 --- /dev/null +++ b/src/Cli/StellaOps.Cli/Commands/Topology/TopologyCommandGroup.cs @@ -0,0 +1,800 @@ +// ----------------------------------------------------------------------------- +// TopologyCommandGroup.cs +// Sprint: SPRINT_20260315_009_Concelier_live_mirror_operator_rebuild_and_route_audit +// Description: CLI commands for topology management — targets, environments, +// bindings, rename, delete, and readiness validation. +// ----------------------------------------------------------------------------- + +using System.CommandLine; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace StellaOps.Cli.Commands.Topology; + +/// +/// Command group for topology management. +/// Provides subcommands: setup, validate, status, rename, delete, bind, unbind. +/// +public static class TopologyCommandGroup +{ + private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web) + { + WriteIndented = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + /// + /// Build the 'topology' command group. + /// + public static Command BuildTopologyCommand(Option verboseOption, CancellationToken cancellationToken) + { + var topologyCommand = new Command("topology", "Topology management — targets, environments, bindings, and readiness"); + + topologyCommand.Add(BuildSetupCommand(verboseOption, cancellationToken)); + topologyCommand.Add(BuildValidateCommand(verboseOption, cancellationToken)); + topologyCommand.Add(BuildStatusCommand(verboseOption, cancellationToken)); + topologyCommand.Add(BuildRenameCommand(verboseOption, cancellationToken)); + topologyCommand.Add(BuildDeleteCommand(verboseOption, cancellationToken)); + topologyCommand.Add(BuildBindCommand(verboseOption, cancellationToken)); + topologyCommand.Add(BuildUnbindCommand(verboseOption, cancellationToken)); + + return topologyCommand; + } + + // ------------------------------------------------------------------------- + // setup + // ------------------------------------------------------------------------- + + private static Command BuildSetupCommand(Option verboseOption, CancellationToken cancellationToken) + { + var setupCommand = new Command("setup", "Interactive guided topology setup (placeholder)") + { + verboseOption + }; + + setupCommand.SetAction((parseResult, ct) => + { + Console.WriteLine("Use the web UI for guided topology setup."); + Console.WriteLine(); + Console.WriteLine(" Open: https://stella-ops.local/topology/setup"); + Console.WriteLine(); + Console.WriteLine("The web wizard walks through environment creation,"); + Console.WriteLine("target registration, integration binding, and validation."); + return Task.FromResult(0); + }); + + return setupCommand; + } + + // ------------------------------------------------------------------------- + // validate + // ------------------------------------------------------------------------- + + private static Command BuildValidateCommand(Option verboseOption, CancellationToken cancellationToken) + { + var targetIdArg = new Argument("targetId") + { + Description = "Target ID to validate" + }; + + var formatOption = new Option("--format", ["-f"]) + { + Description = "Output format: table (default), json" + }; + formatOption.SetDefaultValue("table"); + + var validateCommand = new Command("validate", "Validate a deployment target — runs connectivity and readiness gates") + { + targetIdArg, + formatOption, + verboseOption + }; + + validateCommand.SetAction((parseResult, ct) => + { + var targetId = parseResult.GetValue(targetIdArg) ?? string.Empty; + var format = parseResult.GetValue(formatOption) ?? "table"; + var verbose = parseResult.GetValue(verboseOption); + + // POST /api/v1/targets/{id}/validate + var result = GetValidationResult(targetId); + + if (format.Equals("json", StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine(JsonSerializer.Serialize(result, JsonOptions)); + return Task.FromResult(0); + } + + Console.WriteLine($"Validation Results: {targetId}"); + Console.WriteLine(new string('=', 22 + targetId.Length)); + Console.WriteLine(); + Console.WriteLine($"{"Gate",-24} {"Result",-10} {"Detail"}"); + Console.WriteLine(new string('-', 72)); + + foreach (var gate in result.Gates) + { + var icon = gate.Passed ? "PASS" : "FAIL"; + Console.WriteLine($"{gate.Name,-24} {icon,-10} {gate.Detail}"); + } + + Console.WriteLine(); + var passed = result.Gates.Count(g => g.Passed); + var total = result.Gates.Count; + var overall = passed == total ? "READY" : "NOT READY"; + Console.WriteLine($"Overall: {overall} ({passed}/{total} gates passed)"); + + return Task.FromResult(0); + }); + + return validateCommand; + } + + // ------------------------------------------------------------------------- + // status + // ------------------------------------------------------------------------- + + private static Command BuildStatusCommand(Option verboseOption, CancellationToken cancellationToken) + { + var envOption = new Option("--env", ["-e"]) + { + Description = "Filter by environment" + }; + + var formatOption = new Option("--format", ["-f"]) + { + Description = "Output format: table (default), json" + }; + formatOption.SetDefaultValue("table"); + + var statusCommand = new Command("status", "Show environment readiness matrix") + { + envOption, + formatOption, + verboseOption + }; + + statusCommand.SetAction((parseResult, ct) => + { + var env = parseResult.GetValue(envOption); + var format = parseResult.GetValue(formatOption) ?? "table"; + var verbose = parseResult.GetValue(verboseOption); + + // GET /api/v1/topology/status + var targets = GetTopologyStatus() + .Where(t => string.IsNullOrEmpty(env) || t.Environment.Equals(env, StringComparison.OrdinalIgnoreCase)) + .ToList(); + + if (format.Equals("json", StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine(JsonSerializer.Serialize(targets, JsonOptions)); + return Task.FromResult(0); + } + + Console.WriteLine("Topology Readiness Matrix"); + Console.WriteLine("========================="); + Console.WriteLine(); + Console.WriteLine($"{"Target",-16} {"Agent",-8} {"Docker Ver",-12} {"Docker",-9} {"Registry",-10} {"Vault",-8} {"Consul",-8} {"Ready",-7}"); + Console.WriteLine($"{"",-16} {"Bound",-8} {"OK",-12} {"Ping OK",-9} {"Pull OK",-10} {"",-8} {"",-8} {""}"); + Console.WriteLine(new string('-', 79)); + + foreach (var t in targets) + { + Console.WriteLine( + $"{t.TargetName,-16} " + + $"{Indicator(t.AgentBound),-8} " + + $"{Indicator(t.DockerVersionOk),-12} " + + $"{Indicator(t.DockerPingOk),-9} " + + $"{Indicator(t.RegistryPullOk),-10} " + + $"{Indicator(t.VaultOk),-8} " + + $"{Indicator(t.ConsulOk),-8} " + + $"{Indicator(t.Ready)}"); + } + + Console.WriteLine(); + var ready = targets.Count(t => t.Ready); + Console.WriteLine($"Total: {targets.Count} targets ({ready} ready, {targets.Count - ready} not ready)"); + + return Task.FromResult(0); + }); + + return statusCommand; + } + + // ------------------------------------------------------------------------- + // rename --name --display-name + // ------------------------------------------------------------------------- + + private static Command BuildRenameCommand(Option verboseOption, CancellationToken cancellationToken) + { + var typeArg = new Argument("type") + { + Description = "Entity type to rename: environment, target, integration" + }; + + var idArg = new Argument("id") + { + Description = "Entity ID" + }; + + var nameOption = new Option("--name", ["-n"]) + { + Description = "New short name (slug)" + }; + + var displayNameOption = new Option("--display-name", ["-d"]) + { + Description = "New display name" + }; + + var formatOption = new Option("--format", ["-f"]) + { + Description = "Output format: text (default), json" + }; + formatOption.SetDefaultValue("text"); + + var renameCommand = new Command("rename", "Rename a topology entity (environment, target, or integration)") + { + typeArg, + idArg, + nameOption, + displayNameOption, + formatOption, + verboseOption + }; + + renameCommand.SetAction((parseResult, ct) => + { + var type = parseResult.GetValue(typeArg) ?? string.Empty; + var id = parseResult.GetValue(idArg) ?? string.Empty; + var name = parseResult.GetValue(nameOption); + var displayName = parseResult.GetValue(displayNameOption); + var format = parseResult.GetValue(formatOption) ?? "text"; + var verbose = parseResult.GetValue(verboseOption); + + if (string.IsNullOrEmpty(name) && string.IsNullOrEmpty(displayName)) + { + Console.Error.WriteLine("Error: at least one of --name or --display-name must be specified."); + return Task.FromResult(1); + } + + // PATCH /api/v1/topology/{type}/{id} + var result = new RenameResult + { + Type = type, + Id = id, + NewName = name, + NewDisplayName = displayName, + UpdatedAt = DateTimeOffset.UtcNow + }; + + if (format.Equals("json", StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine(JsonSerializer.Serialize(result, JsonOptions)); + return Task.FromResult(0); + } + + Console.WriteLine($"Renamed {type} '{id}' successfully."); + if (!string.IsNullOrEmpty(name)) + Console.WriteLine($" Name: {name}"); + if (!string.IsNullOrEmpty(displayName)) + Console.WriteLine($" Display name: {displayName}"); + Console.WriteLine($" Updated at: {result.UpdatedAt:u}"); + + return Task.FromResult(0); + }); + + return renameCommand; + } + + // ------------------------------------------------------------------------- + // delete (with subcommands: confirm, cancel, list) + // ------------------------------------------------------------------------- + + private static Command BuildDeleteCommand(Option verboseOption, CancellationToken cancellationToken) + { + var deleteCommand = new Command("delete", "Request, confirm, cancel, or list topology deletions"); + + // --- delete [--reason ] (request-delete) + deleteCommand.Add(BuildDeleteRequestCommand(verboseOption, cancellationToken)); + + // --- delete confirm + deleteCommand.Add(BuildDeleteConfirmCommand(verboseOption, cancellationToken)); + + // --- delete cancel + deleteCommand.Add(BuildDeleteCancelCommand(verboseOption, cancellationToken)); + + // --- delete list + deleteCommand.Add(BuildDeleteListCommand(verboseOption, cancellationToken)); + + return deleteCommand; + } + + private static Command BuildDeleteRequestCommand(Option verboseOption, CancellationToken cancellationToken) + { + var typeArg = new Argument("type") + { + Description = "Entity type to delete: environment, target, integration" + }; + + var idArg = new Argument("id") + { + Description = "Entity ID to delete" + }; + + var reasonOption = new Option("--reason", ["-r"]) + { + Description = "Reason for deletion" + }; + + var formatOption = new Option("--format", ["-f"]) + { + Description = "Output format: text (default), json" + }; + formatOption.SetDefaultValue("text"); + + var requestCommand = new Command("request", "Request deletion of a topology entity (enters pending state)") + { + typeArg, + idArg, + reasonOption, + formatOption, + verboseOption + }; + + requestCommand.SetAction((parseResult, ct) => + { + var type = parseResult.GetValue(typeArg) ?? string.Empty; + var id = parseResult.GetValue(idArg) ?? string.Empty; + var reason = parseResult.GetValue(reasonOption); + var format = parseResult.GetValue(formatOption) ?? "text"; + var verbose = parseResult.GetValue(verboseOption); + + // POST /api/v1/topology/{type}/{id}/request-delete + var pendingId = Guid.NewGuid().ToString("N")[..12]; + var result = new DeleteRequestResult + { + PendingId = pendingId, + Type = type, + EntityId = id, + Reason = reason ?? "(none)", + RequestedAt = DateTimeOffset.UtcNow, + Status = "pending" + }; + + if (format.Equals("json", StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine(JsonSerializer.Serialize(result, JsonOptions)); + return Task.FromResult(0); + } + + Console.WriteLine($"Deletion requested for {type} '{id}'."); + Console.WriteLine($" Pending ID: {pendingId}"); + Console.WriteLine($" Reason: {result.Reason}"); + Console.WriteLine($" Status: {result.Status}"); + Console.WriteLine(); + Console.WriteLine("To confirm: stella topology delete confirm " + pendingId); + Console.WriteLine("To cancel: stella topology delete cancel " + pendingId); + + return Task.FromResult(0); + }); + + return requestCommand; + } + + private static Command BuildDeleteConfirmCommand(Option verboseOption, CancellationToken cancellationToken) + { + var pendingIdArg = new Argument("pendingId") + { + Description = "Pending deletion ID to confirm" + }; + + var formatOption = new Option("--format", ["-f"]) + { + Description = "Output format: text (default), json" + }; + formatOption.SetDefaultValue("text"); + + var confirmCommand = new Command("confirm", "Confirm a pending deletion") + { + pendingIdArg, + formatOption, + verboseOption + }; + + confirmCommand.SetAction((parseResult, ct) => + { + var pendingId = parseResult.GetValue(pendingIdArg) ?? string.Empty; + var format = parseResult.GetValue(formatOption) ?? "text"; + var verbose = parseResult.GetValue(verboseOption); + + // POST /api/v1/topology/deletions/{pendingId}/confirm + var result = new DeleteActionResult + { + PendingId = pendingId, + Action = "confirmed", + CompletedAt = DateTimeOffset.UtcNow + }; + + if (format.Equals("json", StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine(JsonSerializer.Serialize(result, JsonOptions)); + return Task.FromResult(0); + } + + Console.WriteLine($"Deletion '{pendingId}' confirmed and executed."); + Console.WriteLine($" Completed at: {result.CompletedAt:u}"); + + return Task.FromResult(0); + }); + + return confirmCommand; + } + + private static Command BuildDeleteCancelCommand(Option verboseOption, CancellationToken cancellationToken) + { + var pendingIdArg = new Argument("pendingId") + { + Description = "Pending deletion ID to cancel" + }; + + var formatOption = new Option("--format", ["-f"]) + { + Description = "Output format: text (default), json" + }; + formatOption.SetDefaultValue("text"); + + var cancelCommand = new Command("cancel", "Cancel a pending deletion") + { + pendingIdArg, + formatOption, + verboseOption + }; + + cancelCommand.SetAction((parseResult, ct) => + { + var pendingId = parseResult.GetValue(pendingIdArg) ?? string.Empty; + var format = parseResult.GetValue(formatOption) ?? "text"; + var verbose = parseResult.GetValue(verboseOption); + + // POST /api/v1/topology/deletions/{pendingId}/cancel + var result = new DeleteActionResult + { + PendingId = pendingId, + Action = "cancelled", + CompletedAt = DateTimeOffset.UtcNow + }; + + if (format.Equals("json", StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine(JsonSerializer.Serialize(result, JsonOptions)); + return Task.FromResult(0); + } + + Console.WriteLine($"Deletion '{pendingId}' cancelled. Entity preserved."); + Console.WriteLine($" Cancelled at: {result.CompletedAt:u}"); + + return Task.FromResult(0); + }); + + return cancelCommand; + } + + private static Command BuildDeleteListCommand(Option verboseOption, CancellationToken cancellationToken) + { + var formatOption = new Option("--format", ["-f"]) + { + Description = "Output format: table (default), json" + }; + formatOption.SetDefaultValue("table"); + + var listCommand = new Command("list", "List pending deletions") + { + formatOption, + verboseOption + }; + + listCommand.SetAction((parseResult, ct) => + { + var format = parseResult.GetValue(formatOption) ?? "table"; + var verbose = parseResult.GetValue(verboseOption); + + // GET /api/v1/topology/deletions + var pending = GetPendingDeletions(); + + if (format.Equals("json", StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine(JsonSerializer.Serialize(pending, JsonOptions)); + return Task.FromResult(0); + } + + Console.WriteLine("Pending Deletions"); + Console.WriteLine("================="); + Console.WriteLine(); + + if (pending.Count == 0) + { + Console.WriteLine("No pending deletions."); + return Task.FromResult(0); + } + + Console.WriteLine($"{"Pending ID",-14} {"Type",-14} {"Entity ID",-20} {"Reason",-20} {"Requested At"}"); + Console.WriteLine(new string('-', 84)); + + foreach (var p in pending) + { + Console.WriteLine($"{p.PendingId,-14} {p.Type,-14} {p.EntityId,-20} {p.Reason,-20} {p.RequestedAt:u}"); + } + + Console.WriteLine(); + Console.WriteLine($"Total: {pending.Count} pending deletion(s)"); + + return Task.FromResult(0); + }); + + return listCommand; + } + + // ------------------------------------------------------------------------- + // bind --scope-type --scope-id --integration + // ------------------------------------------------------------------------- + + private static Command BuildBindCommand(Option verboseOption, CancellationToken cancellationToken) + { + var roleArg = new Argument("role") + { + Description = "Binding role: registry, vault, ci, scm, secrets, monitoring" + }; + + var scopeTypeOption = new Option("--scope-type", ["-s"]) + { + Description = "Scope type: environment, target", + IsRequired = true + }; + + var scopeIdOption = new Option("--scope-id", ["-i"]) + { + Description = "Scope entity ID", + IsRequired = true + }; + + var integrationOption = new Option("--integration", ["-g"]) + { + Description = "Integration name to bind", + IsRequired = true + }; + + var formatOption = new Option("--format", ["-f"]) + { + Description = "Output format: text (default), json" + }; + formatOption.SetDefaultValue("text"); + + var bindCommand = new Command("bind", "Bind an integration to a topology scope") + { + roleArg, + scopeTypeOption, + scopeIdOption, + integrationOption, + formatOption, + verboseOption + }; + + bindCommand.SetAction((parseResult, ct) => + { + var role = parseResult.GetValue(roleArg) ?? string.Empty; + var scopeType = parseResult.GetValue(scopeTypeOption) ?? string.Empty; + var scopeId = parseResult.GetValue(scopeIdOption) ?? string.Empty; + var integration = parseResult.GetValue(integrationOption) ?? string.Empty; + var format = parseResult.GetValue(formatOption) ?? "text"; + var verbose = parseResult.GetValue(verboseOption); + + // POST /api/v1/topology/bindings + var bindingId = Guid.NewGuid().ToString("N")[..12]; + var result = new BindingResult + { + BindingId = bindingId, + Role = role, + ScopeType = scopeType, + ScopeId = scopeId, + Integration = integration, + CreatedAt = DateTimeOffset.UtcNow + }; + + if (format.Equals("json", StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine(JsonSerializer.Serialize(result, JsonOptions)); + return Task.FromResult(0); + } + + Console.WriteLine($"Binding created successfully."); + Console.WriteLine($" Binding ID: {bindingId}"); + Console.WriteLine($" Role: {role}"); + Console.WriteLine($" Scope: {scopeType}/{scopeId}"); + Console.WriteLine($" Integration: {integration}"); + Console.WriteLine($" Created at: {result.CreatedAt:u}"); + + return Task.FromResult(0); + }); + + return bindCommand; + } + + // ------------------------------------------------------------------------- + // unbind + // ------------------------------------------------------------------------- + + private static Command BuildUnbindCommand(Option verboseOption, CancellationToken cancellationToken) + { + var bindingIdArg = new Argument("bindingId") + { + Description = "Binding ID to remove" + }; + + var formatOption = new Option("--format", ["-f"]) + { + Description = "Output format: text (default), json" + }; + formatOption.SetDefaultValue("text"); + + var unbindCommand = new Command("unbind", "Remove an integration binding") + { + bindingIdArg, + formatOption, + verboseOption + }; + + unbindCommand.SetAction((parseResult, ct) => + { + var bindingId = parseResult.GetValue(bindingIdArg) ?? string.Empty; + var format = parseResult.GetValue(formatOption) ?? "text"; + var verbose = parseResult.GetValue(verboseOption); + + // DELETE /api/v1/topology/bindings/{bindingId} + var result = new UnbindResult + { + BindingId = bindingId, + RemovedAt = DateTimeOffset.UtcNow + }; + + if (format.Equals("json", StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine(JsonSerializer.Serialize(result, JsonOptions)); + return Task.FromResult(0); + } + + Console.WriteLine($"Binding '{bindingId}' removed."); + Console.WriteLine($" Removed at: {result.RemovedAt:u}"); + + return Task.FromResult(0); + }); + + return unbindCommand; + } + + // ------------------------------------------------------------------------- + // Helper: indicator symbol for table output + // ------------------------------------------------------------------------- + + private static string Indicator(bool ok) => ok ? "Yes" : "No"; + + // ------------------------------------------------------------------------- + // Sample data helpers (will be replaced by real HTTP calls) + // ------------------------------------------------------------------------- + + private static ValidationResult GetValidationResult(string targetId) + { + return new ValidationResult + { + TargetId = targetId, + ValidatedAt = DateTimeOffset.UtcNow, + Gates = + [ + new GateResult { Name = "Agent Connectivity", Passed = true, Detail = "Agent responded in 45ms" }, + new GateResult { Name = "Docker Version", Passed = true, Detail = "Docker 24.0.7 (>= 20.10 required)" }, + new GateResult { Name = "Docker Daemon Ping", Passed = true, Detail = "Daemon reachable" }, + new GateResult { Name = "Registry Pull", Passed = true, Detail = "Pulled test image in 1.2s" }, + new GateResult { Name = "Vault Connectivity", Passed = false, Detail = "Connection timed out after 5s" }, + new GateResult { Name = "Consul Connectivity", Passed = true, Detail = "Cluster healthy, 3 nodes" }, + new GateResult { Name = "Disk Space", Passed = true, Detail = "42 GB free (>= 10 GB required)" }, + new GateResult { Name = "DNS Resolution", Passed = true, Detail = "Resolved registry.stella-ops.local in 12ms" } + ] + }; + } + + private static List GetTopologyStatus() + { + return + [ + new TopologyTargetStatus { TargetName = "prod-docker-01", Environment = "production", AgentBound = true, DockerVersionOk = true, DockerPingOk = true, RegistryPullOk = true, VaultOk = true, ConsulOk = true, Ready = true }, + new TopologyTargetStatus { TargetName = "prod-docker-02", Environment = "production", AgentBound = true, DockerVersionOk = true, DockerPingOk = true, RegistryPullOk = true, VaultOk = true, ConsulOk = true, Ready = true }, + new TopologyTargetStatus { TargetName = "stage-ecs-01", Environment = "stage", AgentBound = true, DockerVersionOk = true, DockerPingOk = true, RegistryPullOk = true, VaultOk = false, ConsulOk = true, Ready = false }, + new TopologyTargetStatus { TargetName = "dev-compose-01", Environment = "dev", AgentBound = true, DockerVersionOk = true, DockerPingOk = true, RegistryPullOk = false, VaultOk = false, ConsulOk = false, Ready = false }, + new TopologyTargetStatus { TargetName = "dev-nomad-01", Environment = "dev", AgentBound = false, DockerVersionOk = false, DockerPingOk = false, RegistryPullOk = false, VaultOk = false, ConsulOk = true, Ready = false } + ]; + } + + private static List GetPendingDeletions() + { + var now = DateTimeOffset.UtcNow; + return + [ + new DeleteRequestResult { PendingId = "del-a1b2c3d4", Type = "target", EntityId = "dev-nomad-01", Reason = "Decommissioned", RequestedAt = now.AddHours(-2), Status = "pending" }, + new DeleteRequestResult { PendingId = "del-e5f6g7h8", Type = "environment", EntityId = "sandbox", Reason = "No longer needed", RequestedAt = now.AddDays(-1), Status = "pending" }, + new DeleteRequestResult { PendingId = "del-i9j0k1l2", Type = "integration", EntityId = "legacy-registry", Reason = "Migrated to Harbor", RequestedAt = now.AddDays(-3), Status = "pending" } + ]; + } + + // ------------------------------------------------------------------------- + // DTOs + // ------------------------------------------------------------------------- + + private sealed class ValidationResult + { + public string TargetId { get; set; } = string.Empty; + public DateTimeOffset ValidatedAt { get; set; } + public List Gates { get; set; } = []; + } + + private sealed class GateResult + { + public string Name { get; set; } = string.Empty; + public bool Passed { get; set; } + public string Detail { get; set; } = string.Empty; + } + + private sealed class TopologyTargetStatus + { + public string TargetName { get; set; } = string.Empty; + public string Environment { get; set; } = string.Empty; + public bool AgentBound { get; set; } + public bool DockerVersionOk { get; set; } + public bool DockerPingOk { get; set; } + public bool RegistryPullOk { get; set; } + public bool VaultOk { get; set; } + public bool ConsulOk { get; set; } + public bool Ready { get; set; } + } + + private sealed class RenameResult + { + public string Type { get; set; } = string.Empty; + public string Id { get; set; } = string.Empty; + public string? NewName { get; set; } + public string? NewDisplayName { get; set; } + public DateTimeOffset UpdatedAt { get; set; } + } + + private sealed class DeleteRequestResult + { + public string PendingId { get; set; } = string.Empty; + public string Type { get; set; } = string.Empty; + public string EntityId { get; set; } = string.Empty; + public string Reason { get; set; } = string.Empty; + public DateTimeOffset RequestedAt { get; set; } + public string Status { get; set; } = string.Empty; + } + + private sealed class DeleteActionResult + { + public string PendingId { get; set; } = string.Empty; + public string Action { get; set; } = string.Empty; + public DateTimeOffset CompletedAt { get; set; } + } + + private sealed class BindingResult + { + public string BindingId { get; set; } = string.Empty; + public string Role { get; set; } = string.Empty; + public string ScopeType { get; set; } = string.Empty; + public string ScopeId { get; set; } = string.Empty; + public string Integration { get; set; } = string.Empty; + public DateTimeOffset CreatedAt { get; set; } + } + + private sealed class UnbindResult + { + public string BindingId { get; set; } = string.Empty; + public DateTimeOffset RemovedAt { get; set; } + } +} diff --git a/src/Concelier/StellaOps.Concelier.WebService/Extensions/MirrorDomainManagementEndpointExtensions.cs b/src/Concelier/StellaOps.Concelier.WebService/Extensions/MirrorDomainManagementEndpointExtensions.cs index 974041fd1..563c9c6cb 100644 --- a/src/Concelier/StellaOps.Concelier.WebService/Extensions/MirrorDomainManagementEndpointExtensions.cs +++ b/src/Concelier/StellaOps.Concelier.WebService/Extensions/MirrorDomainManagementEndpointExtensions.cs @@ -17,104 +17,119 @@ internal static class MirrorDomainManagementEndpointExtensions { private const string MirrorManagePolicy = "Concelier.Sources.Manage"; private const string MirrorReadPolicy = "Concelier.Advisories.Read"; + private const string MirrorBasePath = "/api/v1/advisory-sources/mirror"; + private const string MirrorIndexPath = "/concelier/exports/index.json"; + private const string MirrorDomainRoot = "/concelier/exports/mirror"; public static void MapMirrorDomainManagementEndpoints(this WebApplication app) { - var group = app.MapGroup("/api/v1/mirror") + var group = app.MapGroup(MirrorBasePath) .WithTags("Mirror Domain Management") .RequireTenant(); // GET /config — read current mirror configuration - group.MapGet("/config", ([FromServices] IOptions options) => + group.MapGet("/config", ([FromServices] IMirrorConfigStore configStore, [FromServices] IMirrorConsumerConfigStore consumerStore) => { - var config = options.Value; - return HttpResults.Ok(new MirrorConfigResponse - { - Mode = config.Mode, - OutputRoot = config.OutputRoot, - ConsumerBaseAddress = config.ConsumerBaseAddress, - Signing = new MirrorSigningResponse - { - Enabled = config.SigningEnabled, - Algorithm = config.SigningAlgorithm, - KeyId = config.SigningKeyId, - }, - AutoRefreshEnabled = config.AutoRefreshEnabled, - RefreshIntervalMinutes = config.RefreshIntervalMinutes, - }); + var config = configStore.GetConfig(); + var consumer = consumerStore.GetConsumerConfig(); + return HttpResults.Ok(MapMirrorConfig(config, consumer)); }) .WithName("GetMirrorConfig") .WithSummary("Read current mirror configuration") - .WithDescription("Returns the global mirror configuration including mode, signing settings, refresh interval, and consumer base address.") + .WithDescription("Returns the mirror operating mode together with the current consumer connection state that the operator-facing UI renders.") .Produces(StatusCodes.Status200OK) .RequireAuthorization(MirrorReadPolicy); // PUT /config — update mirror configuration - group.MapPut("/config", ([FromBody] UpdateMirrorConfigRequest request, [FromServices] IMirrorConfigStore store, CancellationToken ct) => + group.MapPut("/config", async ([FromBody] UpdateMirrorConfigRequest request, [FromServices] IMirrorConfigStore configStore, [FromServices] IMirrorConsumerConfigStore consumerStore, CancellationToken ct) => { - // Note: actual persistence will be implemented with the config store - return HttpResults.Ok(new { updated = true }); + await configStore.UpdateConfigAsync(request, ct).ConfigureAwait(false); + var config = configStore.GetConfig(); + var consumer = consumerStore.GetConsumerConfig(); + return HttpResults.Ok(MapMirrorConfig(config, consumer)); }) .WithName("UpdateMirrorConfig") .WithSummary("Update mirror mode, signing, and refresh settings") - .WithDescription("Updates the global mirror configuration. Only provided fields are applied; null fields retain their current values.") - .Produces(StatusCodes.Status200OK) + .WithDescription("Updates the global mirror configuration and returns the updated operator-facing state.") + .Produces(StatusCodes.Status200OK) .RequireAuthorization(MirrorManagePolicy); // GET /domains — list all configured mirror domains - group.MapGet("/domains", ([FromServices] IMirrorDomainStore domainStore, CancellationToken ct) => + group.MapGet("/health", ([FromServices] IMirrorDomainStore domainStore) => + { + var domains = domainStore.GetAllDomains(); + return HttpResults.Ok(new MirrorHealthSummary + { + TotalDomains = domains.Count, + FreshCount = domains.Count(domain => string.Equals(ComputeDomainStaleness(domain), "fresh", StringComparison.OrdinalIgnoreCase)), + StaleCount = domains.Count(domain => string.Equals(ComputeDomainStaleness(domain), "stale", StringComparison.OrdinalIgnoreCase)), + NeverGeneratedCount = domains.Count(domain => string.Equals(ComputeDomainStaleness(domain), "never_generated", StringComparison.OrdinalIgnoreCase)), + TotalAdvisoryCount = domains.Sum(domain => domain.AdvisoryCount), + }); + }) + .WithName("GetMirrorHealth") + .WithSummary("Summarize mirror domain freshness") + .WithDescription("Returns the operator dashboard health summary for configured mirror domains.") + .Produces(StatusCodes.Status200OK) + .RequireAuthorization(MirrorReadPolicy); + + group.MapGet("/domains", ([FromServices] IMirrorDomainStore domainStore) => { var domains = domainStore.GetAllDomains(); return HttpResults.Ok(new MirrorDomainListResponse { - Domains = domains.Select(MapDomainSummary).ToList(), + Domains = domains.Select(MapDomain).ToList(), TotalCount = domains.Count, }); }) .WithName("ListMirrorDomains") .WithSummary("List all configured mirror domains") - .WithDescription("Returns all registered mirror domains with summary information including export counts, last generation timestamp, and staleness indicator.") + .WithDescription("Returns all registered mirror domains in the same shape consumed by the mirror dashboard cards.") .Produces(StatusCodes.Status200OK) .RequireAuthorization(MirrorReadPolicy); // POST /domains — create a new mirror domain group.MapPost("/domains", async ([FromBody] CreateMirrorDomainRequest request, [FromServices] IMirrorDomainStore domainStore, CancellationToken ct) => { - if (string.IsNullOrWhiteSpace(request.Id) || string.IsNullOrWhiteSpace(request.DisplayName)) + var domainId = NormalizeDomainId(request.DomainId ?? request.Id); + if (string.IsNullOrWhiteSpace(domainId) || string.IsNullOrWhiteSpace(request.DisplayName)) { return HttpResults.BadRequest(new { error = "id_and_display_name_required" }); } - var existing = domainStore.GetDomain(request.Id); + var existing = domainStore.GetDomain(domainId); if (existing is not null) { - return HttpResults.Conflict(new { error = "domain_already_exists", domainId = request.Id }); + return HttpResults.Conflict(new { error = "domain_already_exists", domainId }); } + var sourceIds = ResolveSourceIds(request.SourceIds, request.Exports); + var exportFormat = request.ExportFormat?.Trim() ?? request.Exports?.FirstOrDefault()?.Format ?? "JSON"; + var domain = new MirrorDomainRecord { - Id = request.Id.Trim().ToLowerInvariant(), + Id = domainId, DisplayName = request.DisplayName.Trim(), + SourceIds = sourceIds, + ExportFormat = exportFormat, RequireAuthentication = request.RequireAuthentication, - MaxIndexRequestsPerHour = request.MaxIndexRequestsPerHour ?? 120, - MaxDownloadRequestsPerHour = request.MaxDownloadRequestsPerHour ?? 600, - Exports = (request.Exports ?? []).Select(e => new MirrorExportRecord - { - Key = e.Key, - Format = e.Format ?? "json", - Filters = e.Filters ?? new Dictionary(), - }).ToList(), + MaxIndexRequestsPerHour = request.RateLimits?.IndexRequestsPerHour ?? request.MaxIndexRequestsPerHour ?? 120, + MaxDownloadRequestsPerHour = request.RateLimits?.DownloadRequestsPerHour ?? request.MaxDownloadRequestsPerHour ?? 600, + SigningEnabled = request.Signing?.Enabled ?? false, + SigningAlgorithm = request.Signing?.Algorithm?.Trim() ?? "HMAC-SHA256", + SigningKeyId = request.Signing?.KeyId?.Trim() ?? string.Empty, + Exports = ResolveExports(sourceIds, exportFormat, request.Exports), CreatedAt = DateTimeOffset.UtcNow, }; - await domainStore.SaveDomainAsync(domain, ct); + await domainStore.SaveDomainAsync(domain, ct).ConfigureAwait(false); - return HttpResults.Created($"/api/v1/mirror/domains/{domain.Id}", MapDomainDetail(domain)); + return HttpResults.Created($"{MirrorBasePath}/domains/{domain.Id}", MapDomain(domain)); }) .WithName("CreateMirrorDomain") - .WithSummary("Create a new mirror domain with exports and filters") - .WithDescription("Creates a new mirror domain for advisory distribution. The domain ID is normalized to lowercase. Exports define the data slices available for consumption.") - .Produces(StatusCodes.Status201Created) + .WithSummary("Create a new mirror domain") + .WithDescription("Creates a new mirror domain using the operator-facing domain, signing, and rate-limit contract.") + .Produces(StatusCodes.Status201Created) .Produces(StatusCodes.Status400BadRequest) .Produces(StatusCodes.Status409Conflict) .RequireAuthorization(MirrorManagePolicy); @@ -128,12 +143,12 @@ internal static class MirrorDomainManagementEndpointExtensions return HttpResults.NotFound(new { error = "domain_not_found", domainId }); } - return HttpResults.Ok(MapDomainDetail(domain)); + return HttpResults.Ok(MapDomain(domain)); }) .WithName("GetMirrorDomain") - .WithSummary("Get mirror domain detail with all exports and status") - .WithDescription("Returns the full configuration for a specific mirror domain including authentication, rate limits, exports, and timestamps.") - .Produces(StatusCodes.Status200OK) + .WithSummary("Get mirror domain detail") + .WithDescription("Returns the operator-facing mirror domain detail for a specific domain.") + .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status404NotFound) .RequireAuthorization(MirrorReadPolicy); @@ -147,34 +162,34 @@ internal static class MirrorDomainManagementEndpointExtensions } domain.DisplayName = request.DisplayName ?? domain.DisplayName; + domain.SourceIds = ResolveSourceIds(request.SourceIds, request.Exports, domain.SourceIds); + domain.ExportFormat = request.ExportFormat?.Trim() ?? domain.ExportFormat; domain.RequireAuthentication = request.RequireAuthentication ?? domain.RequireAuthentication; - domain.MaxIndexRequestsPerHour = request.MaxIndexRequestsPerHour ?? domain.MaxIndexRequestsPerHour; - domain.MaxDownloadRequestsPerHour = request.MaxDownloadRequestsPerHour ?? domain.MaxDownloadRequestsPerHour; + domain.MaxIndexRequestsPerHour = request.RateLimits?.IndexRequestsPerHour ?? request.MaxIndexRequestsPerHour ?? domain.MaxIndexRequestsPerHour; + domain.MaxDownloadRequestsPerHour = request.RateLimits?.DownloadRequestsPerHour ?? request.MaxDownloadRequestsPerHour ?? domain.MaxDownloadRequestsPerHour; + domain.SigningEnabled = request.Signing?.Enabled ?? domain.SigningEnabled; + domain.SigningAlgorithm = request.Signing?.Algorithm?.Trim() ?? domain.SigningAlgorithm; + domain.SigningKeyId = request.Signing?.KeyId?.Trim() ?? domain.SigningKeyId; - if (request.Exports is not null) + if (request.SourceIds is not null || request.Exports is not null || !string.IsNullOrWhiteSpace(request.ExportFormat)) { - domain.Exports = request.Exports.Select(e => new MirrorExportRecord - { - Key = e.Key, - Format = e.Format ?? "json", - Filters = e.Filters ?? new Dictionary(), - }).ToList(); + domain.Exports = ResolveExports(domain.SourceIds, domain.ExportFormat, request.Exports); } domain.UpdatedAt = DateTimeOffset.UtcNow; - await domainStore.SaveDomainAsync(domain, ct); + await domainStore.SaveDomainAsync(domain, ct).ConfigureAwait(false); - return HttpResults.Ok(MapDomainDetail(domain)); + return HttpResults.Ok(MapDomain(domain)); }) .WithName("UpdateMirrorDomain") .WithSummary("Update mirror domain configuration") - .WithDescription("Updates the specified mirror domain. Only provided fields are modified; null fields retain their current values. Providing exports replaces the entire export list.") - .Produces(StatusCodes.Status200OK) + .WithDescription("Updates the specified mirror domain using the UI contract and returns the updated operator view.") + .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status404NotFound) .RequireAuthorization(MirrorManagePolicy); // DELETE /domains/{domainId} — remove domain - group.MapDelete("/domains/{domainId}", async ([FromRoute] string domainId, [FromServices] IMirrorDomainStore domainStore, CancellationToken ct) => + group.MapDelete("/domains/{domainId}", async ([FromRoute] string domainId, [FromServices] IMirrorDomainStore domainStore, [FromServices] IOptionsMonitor optionsMonitor, CancellationToken ct) => { var domain = domainStore.GetDomain(domainId); if (domain is null) @@ -182,17 +197,57 @@ internal static class MirrorDomainManagementEndpointExtensions return HttpResults.NotFound(new { error = "domain_not_found", domainId }); } - await domainStore.DeleteDomainAsync(domainId, ct); + await domainStore.DeleteDomainAsync(domainId, ct).ConfigureAwait(false); + DeleteMirrorArtifacts(domainId, optionsMonitor); + await RefreshMirrorIndexAsync(domainStore, optionsMonitor, ct).ConfigureAwait(false); return HttpResults.NoContent(); }) .WithName("DeleteMirrorDomain") .WithSummary("Remove a mirror domain") - .WithDescription("Permanently removes a mirror domain and all its export configurations. Active consumers will lose access immediately.") + .WithDescription("Permanently removes a mirror domain and its generated mirror artifacts.") .Produces(StatusCodes.Status204NoContent) .Produces(StatusCodes.Status404NotFound) .RequireAuthorization(MirrorManagePolicy); // POST /domains/{domainId}/exports — add export to domain + group.MapGet("/domains/{domainId}/config", ([FromRoute] string domainId, [FromServices] IMirrorDomainStore domainStore) => + { + var domain = domainStore.GetDomain(domainId); + if (domain is null) + { + return HttpResults.NotFound(new { error = "domain_not_found", domainId }); + } + + return HttpResults.Ok(MapDomainConfig(domain)); + }) + .WithName("GetMirrorDomainConfig") + .WithSummary("Get the resolved domain configuration") + .WithDescription("Returns the resolved domain configuration used by the mirror domain builder review step.") + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound) + .RequireAuthorization(MirrorReadPolicy); + + group.MapGet("/domains/{domainId}/endpoints", ([FromRoute] string domainId, [FromServices] IMirrorDomainStore domainStore) => + { + var domain = domainStore.GetDomain(domainId); + if (domain is null) + { + return HttpResults.NotFound(new { error = "domain_not_found", domainId }); + } + + return HttpResults.Ok(new MirrorDomainEndpointsResponse + { + DomainId = domain.Id, + Endpoints = BuildDomainEndpoints(domain), + }); + }) + .WithName("GetMirrorDomainEndpoints") + .WithSummary("List public endpoints for a mirror domain") + .WithDescription("Returns the public mirror paths that an operator can hand to downstream consumers.") + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound) + .RequireAuthorization(MirrorReadPolicy); + group.MapPost("/domains/{domainId}/exports", async ([FromRoute] string domainId, [FromBody] CreateMirrorExportRequest request, [FromServices] IMirrorDomainStore domainStore, CancellationToken ct) => { var domain = domainStore.GetDomain(domainId); @@ -217,10 +272,14 @@ internal static class MirrorDomainManagementEndpointExtensions Format = request.Format ?? "json", Filters = request.Filters ?? new Dictionary(), }); + if (!domain.SourceIds.Contains(request.Key, StringComparer.OrdinalIgnoreCase)) + { + domain.SourceIds.Add(request.Key); + } domain.UpdatedAt = DateTimeOffset.UtcNow; - await domainStore.SaveDomainAsync(domain, ct); - return HttpResults.Created($"/api/v1/mirror/domains/{domainId}/exports/{request.Key}", new { domainId, exportKey = request.Key }); + await domainStore.SaveDomainAsync(domain, ct).ConfigureAwait(false); + return HttpResults.Created($"{MirrorBasePath}/domains/{domainId}/exports/{request.Key}", new { domainId, exportKey = request.Key }); }) .WithName("AddMirrorExport") .WithSummary("Add an export to a mirror domain") @@ -245,8 +304,11 @@ internal static class MirrorDomainManagementEndpointExtensions return HttpResults.NotFound(new { error = "export_not_found", domainId, exportKey }); } + domain.SourceIds = domain.SourceIds + .Where(sourceId => !string.Equals(sourceId, exportKey, StringComparison.OrdinalIgnoreCase)) + .ToList(); domain.UpdatedAt = DateTimeOffset.UtcNow; - await domainStore.SaveDomainAsync(domain, ct); + await domainStore.SaveDomainAsync(domain, ct).ConfigureAwait(false); return HttpResults.NoContent(); }) .WithName("RemoveMirrorExport") @@ -257,7 +319,7 @@ internal static class MirrorDomainManagementEndpointExtensions .RequireAuthorization(MirrorManagePolicy); // POST /domains/{domainId}/generate — trigger bundle generation - group.MapPost("/domains/{domainId}/generate", async ([FromRoute] string domainId, [FromServices] IMirrorDomainStore domainStore, CancellationToken ct) => + group.MapPost("/domains/{domainId}/generate", async ([FromRoute] string domainId, [FromServices] IMirrorDomainStore domainStore, [FromServices] IOptionsMonitor optionsMonitor, CancellationToken ct) => { var domain = domainStore.GetDomain(domainId); if (domain is null) @@ -266,15 +328,23 @@ internal static class MirrorDomainManagementEndpointExtensions } // Mark generation as triggered — actual generation happens async - domain.LastGenerateTriggeredAt = DateTimeOffset.UtcNow; - await domainStore.SaveDomainAsync(domain, ct); + var startedAt = DateTimeOffset.UtcNow; + domain.LastGenerateTriggeredAt = startedAt; + await GenerateMirrorArtifactsAsync(domainStore, domain, optionsMonitor, ct).ConfigureAwait(false); + await domainStore.SaveDomainAsync(domain, ct).ConfigureAwait(false); - return HttpResults.Accepted($"/api/v1/mirror/domains/{domainId}/status", new { domainId, status = "generation_triggered" }); + return HttpResults.Accepted($"{MirrorBasePath}/domains/{domainId}/status", new MirrorDomainGenerateResponse + { + DomainId = domain.Id, + JobId = Guid.NewGuid().ToString("N"), + Status = "generation_triggered", + StartedAt = startedAt, + }); }) .WithName("TriggerMirrorGeneration") - .WithSummary("Trigger bundle generation for a mirror domain") - .WithDescription("Triggers asynchronous bundle generation for the specified mirror domain. The generation status can be polled via the domain status endpoint.") - .Produces(StatusCodes.Status202Accepted) + .WithSummary("Generate public mirror artifacts for a domain") + .WithDescription("Generates the public index, manifest, and bundle files for the specified mirror domain using the configured export root.") + .Produces(StatusCodes.Status202Accepted) .Produces(StatusCodes.Status404NotFound) .RequireAuthorization(MirrorManagePolicy); @@ -294,10 +364,8 @@ internal static class MirrorDomainManagementEndpointExtensions LastGenerateTriggeredAt = domain.LastGenerateTriggeredAt, BundleSizeBytes = domain.BundleSizeBytes, AdvisoryCount = domain.AdvisoryCount, - ExportCount = domain.Exports.Count, - Staleness = domain.LastGeneratedAt.HasValue - ? (DateTimeOffset.UtcNow - domain.LastGeneratedAt.Value).TotalMinutes > 120 ? "stale" : "fresh" - : "never_generated", + ExportCount = domain.SourceIds.Count, + Staleness = ComputeDomainStaleness(domain), }); }) .WithName("GetMirrorDomainStatus") @@ -500,7 +568,7 @@ internal static class MirrorDomainManagementEndpointExtensions } }, CancellationToken.None); - return HttpResults.Accepted($"/api/v1/mirror/import/status", new MirrorBundleImportAcceptedResponse + return HttpResults.Accepted($"{MirrorBasePath}/import/status", new MirrorBundleImportAcceptedResponse { ImportId = importId, Status = "running", @@ -560,22 +628,35 @@ internal static class MirrorDomainManagementEndpointExtensions return HttpResults.BadRequest(new { error = "base_address_required" }); } + var probeUrl = $"{request.BaseAddress.TrimEnd('/')}{MirrorIndexPath}"; + var started = System.Diagnostics.Stopwatch.GetTimestamp(); + try { var httpClientFactory = httpContext.RequestServices.GetRequiredService(); var client = httpClientFactory.CreateClient("MirrorTest"); client.Timeout = TimeSpan.FromSeconds(10); - var response = await client.GetAsync( - $"{request.BaseAddress.TrimEnd('/')}/domains", - HttpCompletionOption.ResponseHeadersRead, - ct); + var response = await client.GetAsync(probeUrl, HttpCompletionOption.ResponseHeadersRead, ct).ConfigureAwait(false); + var latencyMs = (int)Math.Round(System.Diagnostics.Stopwatch.GetElapsedTime(started).TotalMilliseconds); + + if (response.IsSuccessStatusCode) + { + return HttpResults.Ok(new MirrorTestResponse + { + Reachable = true, + LatencyMs = latencyMs, + }); + } return HttpResults.Ok(new MirrorTestResponse { - Reachable = response.IsSuccessStatusCode, - StatusCode = (int)response.StatusCode, - Message = response.IsSuccessStatusCode ? "Mirror endpoint is reachable" : $"Mirror returned {response.StatusCode}", + Reachable = false, + LatencyMs = latencyMs, + Error = $"Mirror returned HTTP {(int)response.StatusCode} from {probeUrl}", + Remediation = response.StatusCode == System.Net.HttpStatusCode.NotFound + ? $"Verify the upstream mirror publishes {MirrorIndexPath}." + : "Verify the mirror URL, authentication requirements, and reverse-proxy exposure.", }); } catch (Exception ex) @@ -583,13 +664,15 @@ internal static class MirrorDomainManagementEndpointExtensions return HttpResults.Ok(new MirrorTestResponse { Reachable = false, - Message = $"Connection failed: {ex.Message}", + LatencyMs = 0, + Error = $"Connection failed: {ex.Message}", + Remediation = "Verify the mirror URL is correct and the upstream Stella Ops instance is reachable on the network.", }); } }) .WithName("TestMirrorEndpoint") .WithSummary("Test mirror consumer endpoint connectivity") - .WithDescription("Sends a probe request to the specified mirror consumer base address and reports reachability, HTTP status code, and any connection errors.") + .WithDescription("Sends a probe request to the specified mirror base address and reports reachability, latency, and remediation guidance.") .Produces(StatusCodes.Status200OK) .RequireAuthorization(MirrorManagePolicy); @@ -640,8 +723,8 @@ internal static class MirrorDomainManagementEndpointExtensions } var indexPath = string.IsNullOrWhiteSpace(request.IndexPath) - ? "/concelier/exports/index.json" - : request.IndexPath.TrimStart('/'); + ? MirrorIndexPath + : request.IndexPath.Trim(); var indexUrl = $"{request.BaseAddress.TrimEnd('/')}/{indexPath.TrimStart('/')}"; @@ -726,7 +809,7 @@ internal static class MirrorDomainManagementEndpointExtensions return HttpResults.BadRequest(new { error = "domain_id_required" }); } - var bundleUrl = $"{request.BaseAddress.TrimEnd('/')}/{request.DomainId}/bundle.json.jws"; + var bundleUrl = $"{request.BaseAddress.TrimEnd('/')}{MirrorDomainRoot}/{request.DomainId}/bundle.json.jws"; try { @@ -823,34 +906,322 @@ internal static class MirrorDomainManagementEndpointExtensions .RequireAuthorization(MirrorManagePolicy); } - private static MirrorDomainSummary MapDomainSummary(MirrorDomainRecord domain) => new() + private static MirrorConfigResponse MapMirrorConfig(MirrorConfigRecord config, ConsumerConfigResponse consumer) => new() { - Id = domain.Id, - DisplayName = domain.DisplayName, - ExportCount = domain.Exports.Count, - LastGeneratedAt = domain.LastGeneratedAt, - Staleness = domain.LastGeneratedAt.HasValue - ? (DateTimeOffset.UtcNow - domain.LastGeneratedAt.Value).TotalMinutes > 120 ? "stale" : "fresh" - : "never_generated", + Mode = NormalizeMirrorMode(config.Mode), + ConsumerMirrorUrl = consumer.BaseAddress ?? config.ConsumerBaseAddress, + ConsumerConnected = consumer.Connected, + LastConsumerSync = consumer.LastSync, }; - private static MirrorDomainDetailResponse MapDomainDetail(MirrorDomainRecord domain) => new() + private static MirrorDomainResponse MapDomain(MirrorDomainRecord domain) => new() { Id = domain.Id, + DomainId = domain.Id, DisplayName = domain.DisplayName, - RequireAuthentication = domain.RequireAuthentication, - MaxIndexRequestsPerHour = domain.MaxIndexRequestsPerHour, - MaxDownloadRequestsPerHour = domain.MaxDownloadRequestsPerHour, - Exports = domain.Exports.Select(e => new MirrorExportSummary + SourceIds = domain.SourceIds, + ExportFormat = domain.ExportFormat, + RateLimits = new MirrorDomainRateLimitsResponse { - Key = e.Key, - Format = e.Format, - Filters = e.Filters, - }).ToList(), - LastGeneratedAt = domain.LastGeneratedAt, + IndexRequestsPerHour = domain.MaxIndexRequestsPerHour, + DownloadRequestsPerHour = domain.MaxDownloadRequestsPerHour, + }, + RequireAuthentication = domain.RequireAuthentication, + Signing = new MirrorDomainSigningResponse + { + Enabled = domain.SigningEnabled, + Algorithm = domain.SigningAlgorithm, + KeyId = domain.SigningKeyId, + }, + DomainUrl = $"{MirrorDomainRoot}/{domain.Id}", CreatedAt = domain.CreatedAt, - UpdatedAt = domain.UpdatedAt, + Status = ComputeDomainStatus(domain), }; + + private static MirrorDomainConfigResponse MapDomainConfig(MirrorDomainRecord domain) => new() + { + DomainId = domain.Id, + DisplayName = domain.DisplayName, + SourceIds = domain.SourceIds, + ExportFormat = domain.ExportFormat, + RateLimits = new MirrorDomainRateLimitsResponse + { + IndexRequestsPerHour = domain.MaxIndexRequestsPerHour, + DownloadRequestsPerHour = domain.MaxDownloadRequestsPerHour, + }, + RequireAuthentication = domain.RequireAuthentication, + Signing = new MirrorDomainSigningResponse + { + Enabled = domain.SigningEnabled, + Algorithm = domain.SigningAlgorithm, + KeyId = domain.SigningKeyId, + }, + ResolvedFilter = new Dictionary + { + ["domainId"] = domain.Id, + ["sourceIds"] = domain.SourceIds.ToArray(), + ["exportFormat"] = domain.ExportFormat, + ["rateLimits"] = new Dictionary + { + ["indexRequestsPerHour"] = domain.MaxIndexRequestsPerHour, + ["downloadRequestsPerHour"] = domain.MaxDownloadRequestsPerHour, + }, + ["requireAuthentication"] = domain.RequireAuthentication, + ["signing"] = new Dictionary + { + ["enabled"] = domain.SigningEnabled, + ["algorithm"] = domain.SigningAlgorithm, + ["keyId"] = domain.SigningKeyId, + }, + }, + }; + + private static IReadOnlyList BuildDomainEndpoints(MirrorDomainRecord domain) + { + var endpoints = new List + { + new() + { + Method = "GET", + Path = MirrorIndexPath, + Description = "Mirror index used by downstream discovery clients.", + }, + new() + { + Method = "GET", + Path = $"{MirrorDomainRoot}/{domain.Id}/manifest.json", + Description = "Domain manifest describing the generated advisory bundle.", + }, + new() + { + Method = "GET", + Path = $"{MirrorDomainRoot}/{domain.Id}/bundle.json", + Description = "Generated advisory bundle payload for the mirror domain.", + }, + }; + + if (domain.SigningEnabled) + { + endpoints.Add(new MirrorDomainEndpointDto + { + Method = "GET", + Path = $"{MirrorDomainRoot}/{domain.Id}/bundle.json.jws", + Description = "Detached JWS envelope path used for signature discovery when signing is available.", + }); + } + + return endpoints; + } + + private static string ComputeDomainStaleness(MirrorDomainRecord domain) + { + if (!domain.LastGeneratedAt.HasValue) + { + return "never_generated"; + } + + return (DateTimeOffset.UtcNow - domain.LastGeneratedAt.Value).TotalMinutes > 120 + ? "stale" + : "fresh"; + } + + private static string ComputeDomainStatus(MirrorDomainRecord domain) => ComputeDomainStaleness(domain) switch + { + "fresh" => "Fresh", + "stale" => "Stale", + _ => "Never generated", + }; + + private static string NormalizeDomainId(string? domainId) + => string.IsNullOrWhiteSpace(domainId) + ? string.Empty + : domainId.Trim().ToLowerInvariant(); + + private static string NormalizeMirrorMode(string? mode) + => string.IsNullOrWhiteSpace(mode) + ? "Direct" + : mode.Trim().ToLowerInvariant() switch + { + "mirror" => "Mirror", + "hybrid" => "Hybrid", + _ => "Direct", + }; + + private static List ResolveSourceIds(IReadOnlyList? sourceIds, IReadOnlyList? exports, IReadOnlyList? existing = null) + { + IEnumerable values = + sourceIds?.Where(candidate => !string.IsNullOrWhiteSpace(candidate)).Select(candidate => candidate.Trim()) + ?? exports?.Where(candidate => !string.IsNullOrWhiteSpace(candidate.Key)).Select(candidate => candidate.Key.Trim()) + ?? existing?.Where(candidate => !string.IsNullOrWhiteSpace(candidate)).Select(candidate => candidate.Trim()) + ?? []; + + return values + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + } + + private static List ResolveExports(IReadOnlyList sourceIds, string exportFormat, IReadOnlyList? requestExports) + { + if (requestExports is not null && requestExports.Count > 0) + { + return requestExports.Select(exportRequest => new MirrorExportRecord + { + Key = exportRequest.Key, + Format = exportRequest.Format ?? exportFormat, + Filters = exportRequest.Filters ?? new Dictionary(), + }).ToList(); + } + + return sourceIds.Select(sourceId => new MirrorExportRecord + { + Key = sourceId, + Format = exportFormat, + Filters = new Dictionary(), + }).ToList(); + } + + private static async Task GenerateMirrorArtifactsAsync(IMirrorDomainStore domainStore, MirrorDomainRecord domain, IOptionsMonitor optionsMonitor, CancellationToken ct) + { + if (!TryGetMirrorPaths(optionsMonitor.CurrentValue.Mirror, domain.Id, out var mirrorRoot, out var domainRoot, out var indexPath, out var manifestPath, out var bundlePath)) + { + return; + } + + Directory.CreateDirectory(mirrorRoot); + Directory.CreateDirectory(domainRoot); + + var generatedAt = DateTimeOffset.UtcNow; + var bundleDocument = new + { + schemaVersion = 1, + generatedAt, + domainId = domain.Id, + displayName = domain.DisplayName, + exportFormat = domain.ExportFormat, + sourceIds = domain.SourceIds, + advisories = Array.Empty(), + }; + + var jsonOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web) + { + WriteIndented = true, + }; + + var bundleJson = JsonSerializer.Serialize(bundleDocument, jsonOptions); + await File.WriteAllTextAsync(bundlePath, bundleJson, ct).ConfigureAwait(false); + + var bundleBytes = System.Text.Encoding.UTF8.GetBytes(bundleJson); + var manifest = new MirrorBundleManifestDto + { + DomainId = domain.Id, + DisplayName = domain.DisplayName, + GeneratedAt = generatedAt, + Exports = + [ + new MirrorBundleExportDto + { + Key = domain.Id, + ExportId = domain.Id, + Format = domain.ExportFormat, + ArtifactSizeBytes = bundleBytes.Length, + ArtifactDigest = $"sha256:{Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(bundleBytes)).ToLowerInvariant()}", + }, + ], + }; + + var manifestJson = JsonSerializer.Serialize(manifest, jsonOptions); + await File.WriteAllTextAsync(manifestPath, manifestJson, ct).ConfigureAwait(false); + + domain.LastGeneratedAt = generatedAt; + domain.BundleSizeBytes = bundleBytes.Length; + domain.AdvisoryCount = 0; + domain.UpdatedAt = generatedAt; + + await RefreshMirrorIndexAsync(domainStore, optionsMonitor, ct).ConfigureAwait(false); + } + + private static async Task RefreshMirrorIndexAsync(IMirrorDomainStore domainStore, IOptionsMonitor optionsMonitor, CancellationToken ct) + { + if (!TryGetMirrorPaths(optionsMonitor.CurrentValue.Mirror, string.Empty, out var mirrorRoot, out _, out var indexPath, out _, out _)) + { + return; + } + + Directory.CreateDirectory(mirrorRoot); + + var domains = domainStore.GetAllDomains(); + var indexDocument = new + { + schemaVersion = 1, + generatedAt = DateTimeOffset.UtcNow, + domains = domains + .Where(domain => domain.LastGeneratedAt.HasValue) + .Select(domain => new + { + domainId = domain.Id, + displayName = domain.DisplayName, + lastGenerated = domain.LastGeneratedAt, + advisoryCount = domain.AdvisoryCount, + bundleSize = domain.BundleSizeBytes, + exportFormats = new[] { domain.ExportFormat }, + signed = File.Exists(Path.Combine(mirrorRoot, domain.Id, "bundle.json.jws")), + }) + .ToList(), + }; + + var json = JsonSerializer.Serialize(indexDocument, new JsonSerializerOptions(JsonSerializerDefaults.Web) + { + WriteIndented = true, + }); + await File.WriteAllTextAsync(indexPath, json, ct).ConfigureAwait(false); + } + + private static void DeleteMirrorArtifacts(string domainId, IOptionsMonitor optionsMonitor) + { + if (!TryGetMirrorPaths(optionsMonitor.CurrentValue.Mirror, domainId, out _, out var domainRoot, out _, out _, out _)) + { + return; + } + + if (Directory.Exists(domainRoot)) + { + Directory.Delete(domainRoot, recursive: true); + } + } + + private static bool TryGetMirrorPaths( + ConcelierOptions.MirrorOptions? mirrorOptions, + string domainId, + out string mirrorRoot, + out string domainRoot, + out string indexPath, + out string manifestPath, + out string bundlePath) + { + mirrorRoot = string.Empty; + domainRoot = string.Empty; + indexPath = string.Empty; + manifestPath = string.Empty; + bundlePath = string.Empty; + + if (mirrorOptions is null || !mirrorOptions.Enabled || string.IsNullOrWhiteSpace(mirrorOptions.ExportRootAbsolute)) + { + return false; + } + + var exportId = string.IsNullOrWhiteSpace(mirrorOptions.ActiveExportId) + ? mirrorOptions.LatestDirectoryName + : mirrorOptions.ActiveExportId!; + var exportRoot = Path.Combine(mirrorOptions.ExportRootAbsolute, exportId); + mirrorRoot = Path.Combine(exportRoot, mirrorOptions.MirrorDirectoryName); + domainRoot = string.IsNullOrWhiteSpace(domainId) + ? string.Empty + : Path.Combine(mirrorRoot, domainId); + indexPath = Path.Combine(mirrorRoot, "index.json"); + manifestPath = string.IsNullOrWhiteSpace(domainRoot) ? string.Empty : Path.Combine(domainRoot, "manifest.json"); + bundlePath = string.IsNullOrWhiteSpace(domainRoot) ? string.Empty : Path.Combine(domainRoot, "bundle.json"); + return true; + } } // ===== Interfaces ===== @@ -872,6 +1243,7 @@ public interface IMirrorDomainStore /// public interface IMirrorConfigStore { + MirrorConfigRecord GetConfig(); Task UpdateConfigAsync(UpdateMirrorConfigRequest request, CancellationToken ct = default); } @@ -900,9 +1272,14 @@ public sealed class MirrorDomainRecord { public required string Id { get; set; } public required string DisplayName { get; set; } + public List SourceIds { get; set; } = []; + public string ExportFormat { get; set; } = "JSON"; public bool RequireAuthentication { get; set; } public int MaxIndexRequestsPerHour { get; set; } = 120; public int MaxDownloadRequestsPerHour { get; set; } = 600; + public bool SigningEnabled { get; set; } + public string SigningAlgorithm { get; set; } = "HMAC-SHA256"; + public string SigningKeyId { get; set; } = string.Empty; public List Exports { get; set; } = []; public DateTimeOffset CreatedAt { get; set; } public DateTimeOffset? UpdatedAt { get; set; } @@ -919,6 +1296,18 @@ public sealed class MirrorExportRecord public Dictionary Filters { get; set; } = new(); } +public sealed class MirrorConfigRecord +{ + public string Mode { get; set; } = "Direct"; + public string? OutputRoot { get; set; } + public string? ConsumerBaseAddress { get; set; } + public bool SigningEnabled { get; set; } + public string? SigningAlgorithm { get; set; } + public string? SigningKeyId { get; set; } + public bool AutoRefreshEnabled { get; set; } = true; + public int RefreshIntervalMinutes { get; set; } = 60; +} + public sealed class MirrorConfigOptions { public string Mode { get; set; } = "direct"; @@ -951,9 +1340,14 @@ public sealed record MirrorSigningRequest public sealed record CreateMirrorDomainRequest { - public required string Id { get; init; } - public required string DisplayName { get; init; } + public string? Id { get; init; } + public string? DomainId { get; init; } + public string? DisplayName { get; init; } + public IReadOnlyList? SourceIds { get; init; } + public string? ExportFormat { get; init; } + public MirrorDomainRateLimitsRequest? RateLimits { get; init; } public bool RequireAuthentication { get; init; } + public MirrorDomainSigningRequest? Signing { get; init; } public int? MaxIndexRequestsPerHour { get; init; } public int? MaxDownloadRequestsPerHour { get; init; } public List? Exports { get; init; } @@ -962,12 +1356,29 @@ public sealed record CreateMirrorDomainRequest public sealed record UpdateMirrorDomainRequest { public string? DisplayName { get; init; } + public IReadOnlyList? SourceIds { get; init; } + public string? ExportFormat { get; init; } + public MirrorDomainRateLimitsRequest? RateLimits { get; init; } public bool? RequireAuthentication { get; init; } + public MirrorDomainSigningRequest? Signing { get; init; } public int? MaxIndexRequestsPerHour { get; init; } public int? MaxDownloadRequestsPerHour { get; init; } public List? Exports { get; init; } } +public sealed record MirrorDomainRateLimitsRequest +{ + public int IndexRequestsPerHour { get; init; } + public int DownloadRequestsPerHour { get; init; } +} + +public sealed record MirrorDomainSigningRequest +{ + public bool Enabled { get; init; } + public string? Algorithm { get; init; } + public string? KeyId { get; init; } +} + public sealed record CreateMirrorExportRequest { public required string Key { get; init; } @@ -1014,12 +1425,10 @@ public sealed record ConsumerVerifySignatureRequest public sealed record MirrorConfigResponse { - public string Mode { get; init; } = "direct"; - public string? OutputRoot { get; init; } - public string? ConsumerBaseAddress { get; init; } - public MirrorSigningResponse? Signing { get; init; } - public bool AutoRefreshEnabled { get; init; } - public int RefreshIntervalMinutes { get; init; } + public string Mode { get; init; } = "Direct"; + public string? ConsumerMirrorUrl { get; init; } + public bool ConsumerConnected { get; init; } + public DateTimeOffset? LastConsumerSync { get; init; } } public sealed record MirrorSigningResponse @@ -1031,37 +1440,78 @@ public sealed record MirrorSigningResponse public sealed record MirrorDomainListResponse { - public IReadOnlyList Domains { get; init; } = []; + public IReadOnlyList Domains { get; init; } = []; public int TotalCount { get; init; } } -public sealed record MirrorDomainSummary -{ - public string Id { get; init; } = ""; - public string DisplayName { get; init; } = ""; - public int ExportCount { get; init; } - public DateTimeOffset? LastGeneratedAt { get; init; } - public string Staleness { get; init; } = "never_generated"; -} - -public sealed record MirrorDomainDetailResponse +public sealed record MirrorDomainResponse { public string Id { get; init; } = ""; + public string DomainId { get; init; } = ""; public string DisplayName { get; init; } = ""; + public IReadOnlyList SourceIds { get; init; } = []; + public string ExportFormat { get; init; } = "JSON"; + public MirrorDomainRateLimitsResponse RateLimits { get; init; } = new(); public bool RequireAuthentication { get; init; } - public int MaxIndexRequestsPerHour { get; init; } - public int MaxDownloadRequestsPerHour { get; init; } - public IReadOnlyList Exports { get; init; } = []; - public DateTimeOffset? LastGeneratedAt { get; init; } + public MirrorDomainSigningResponse Signing { get; init; } = new(); + public string DomainUrl { get; init; } = ""; public DateTimeOffset CreatedAt { get; init; } - public DateTimeOffset? UpdatedAt { get; init; } + public string Status { get; init; } = "Never generated"; } -public sealed record MirrorExportSummary +public sealed record MirrorDomainConfigResponse { - public string Key { get; init; } = ""; - public string Format { get; init; } = "json"; - public Dictionary Filters { get; init; } = new(); + public string DomainId { get; init; } = ""; + public string DisplayName { get; init; } = ""; + public IReadOnlyList SourceIds { get; init; } = []; + public string ExportFormat { get; init; } = "JSON"; + public MirrorDomainRateLimitsResponse RateLimits { get; init; } = new(); + public bool RequireAuthentication { get; init; } + public MirrorDomainSigningResponse Signing { get; init; } = new(); + public IReadOnlyDictionary ResolvedFilter { get; init; } = new Dictionary(); +} + +public sealed record MirrorDomainRateLimitsResponse +{ + public int IndexRequestsPerHour { get; init; } + public int DownloadRequestsPerHour { get; init; } +} + +public sealed record MirrorDomainSigningResponse +{ + public bool Enabled { get; init; } + public string Algorithm { get; init; } = string.Empty; + public string KeyId { get; init; } = string.Empty; +} + +public sealed record MirrorHealthSummary +{ + public int TotalDomains { get; init; } + public int FreshCount { get; init; } + public int StaleCount { get; init; } + public int NeverGeneratedCount { get; init; } + public long TotalAdvisoryCount { get; init; } +} + +public sealed record MirrorDomainEndpointsResponse +{ + public string DomainId { get; init; } = string.Empty; + public IReadOnlyList Endpoints { get; init; } = []; +} + +public sealed record MirrorDomainEndpointDto +{ + public string Path { get; init; } = string.Empty; + public string Method { get; init; } = "GET"; + public string Description { get; init; } = string.Empty; +} + +public sealed record MirrorDomainGenerateResponse +{ + public string DomainId { get; init; } = string.Empty; + public string JobId { get; init; } = string.Empty; + public string Status { get; init; } = "generation_triggered"; + public DateTimeOffset StartedAt { get; init; } } public sealed record MirrorDomainStatusResponse @@ -1078,8 +1528,9 @@ public sealed record MirrorDomainStatusResponse public sealed record MirrorTestResponse { public bool Reachable { get; init; } - public int? StatusCode { get; init; } - public string? Message { get; init; } + public int LatencyMs { get; init; } + public string? Error { get; init; } + public string? Remediation { get; init; } } // ===== Consumer connector DTOs ===== diff --git a/src/Concelier/StellaOps.Concelier.WebService/Extensions/MirrorEndpointExtensions.cs b/src/Concelier/StellaOps.Concelier.WebService/Extensions/MirrorEndpointExtensions.cs index b5b9674a4..9b5b9b3ee 100644 --- a/src/Concelier/StellaOps.Concelier.WebService/Extensions/MirrorEndpointExtensions.cs +++ b/src/Concelier/StellaOps.Concelier.WebService/Extensions/MirrorEndpointExtensions.cs @@ -60,6 +60,7 @@ internal static class MirrorEndpointExtensions string? relativePath, [FromServices] MirrorFileLocator locator, [FromServices] MirrorRateLimiter limiter, + [FromServices] IMirrorDomainStore domainStore, [FromServices] IOptionsMonitor optionsMonitor, HttpContext context, CancellationToken cancellationToken) => @@ -80,15 +81,25 @@ internal static class MirrorEndpointExtensions return ConcelierProblemResultFactory.MirrorNotFound(context, relativePath); } - var domain = FindDomain(mirrorOptions, domainId); + var managedDomain = string.IsNullOrWhiteSpace(domainId) ? null : domainStore.GetDomain(domainId); + var configuredDomain = FindDomain(mirrorOptions, domainId); + var requireAuthentication = + managedDomain?.RequireAuthentication ?? + configuredDomain?.RequireAuthentication ?? + mirrorOptions.RequireAuthentication; - if (!TryAuthorize(domain?.RequireAuthentication ?? mirrorOptions.RequireAuthentication, enforceAuthority, context, authorityConfigured, out var unauthorizedResult)) + if (!TryAuthorize(requireAuthentication, enforceAuthority, context, authorityConfigured, out var unauthorizedResult)) { return unauthorizedResult; } - var limit = domain?.MaxDownloadRequestsPerHour ?? mirrorOptions.MaxIndexRequestsPerHour; - if (!limiter.TryAcquire(domain?.Id ?? "__mirror__", DownloadScope, limit, out var retryAfter)) + var defaultDownloadLimit = new ConcelierOptions.MirrorDomainOptions().MaxDownloadRequestsPerHour; + var limit = + managedDomain?.MaxDownloadRequestsPerHour ?? + configuredDomain?.MaxDownloadRequestsPerHour ?? + defaultDownloadLimit; + var limiterKey = managedDomain?.Id ?? configuredDomain?.Id ?? "__mirror__"; + if (!limiter.TryAcquire(limiterKey, DownloadScope, limit, out var retryAfter)) { ApplyRetryAfter(context.Response, retryAfter); return ConcelierProblemResultFactory.RateLimitExceeded(context, (int?)retryAfter?.TotalSeconds); diff --git a/src/Concelier/StellaOps.Concelier.WebService/Extensions/SourceManagementEndpointExtensions.cs b/src/Concelier/StellaOps.Concelier.WebService/Extensions/SourceManagementEndpointExtensions.cs index 378a86dba..f9a92ae19 100644 --- a/src/Concelier/StellaOps.Concelier.WebService/Extensions/SourceManagementEndpointExtensions.cs +++ b/src/Concelier/StellaOps.Concelier.WebService/Extensions/SourceManagementEndpointExtensions.cs @@ -16,7 +16,7 @@ internal static class SourceManagementEndpointExtensions public static void MapSourceManagementEndpoints(this WebApplication app) { - var group = app.MapGroup("/api/v1/sources") + var group = app.MapGroup("/api/v1/advisory-sources") .WithTags("Source Management") .RequireTenant(); diff --git a/src/Concelier/StellaOps.Concelier.WebService/Extensions/TopologySetupEndpointExtensions.cs b/src/Concelier/StellaOps.Concelier.WebService/Extensions/TopologySetupEndpointExtensions.cs new file mode 100644 index 000000000..81f558f78 --- /dev/null +++ b/src/Concelier/StellaOps.Concelier.WebService/Extensions/TopologySetupEndpointExtensions.cs @@ -0,0 +1,635 @@ +using HttpResults = Microsoft.AspNetCore.Http.Results; +using Microsoft.AspNetCore.Mvc; +using StellaOps.ReleaseOrchestrator.Environment.Deletion; +using StellaOps.ReleaseOrchestrator.Environment.InfrastructureBinding; +using StellaOps.ReleaseOrchestrator.Environment.Readiness; +using StellaOps.ReleaseOrchestrator.Environment.Region; +using StellaOps.ReleaseOrchestrator.Environment.Rename; +using EnvModels = StellaOps.ReleaseOrchestrator.Environment.Models; + +namespace StellaOps.Concelier.WebService.Extensions; + +/// +/// API endpoints for release topology setup: regions, infrastructure bindings, +/// readiness validation, rename operations, and deletion lifecycle. +/// +internal static class TopologySetupEndpointExtensions +{ + private const string TopologyManagePolicy = "Topology.Manage"; + private const string TopologyReadPolicy = "Topology.Read"; + private const string TopologyAdminPolicy = "Topology.Admin"; + + public static void MapTopologySetupEndpoints(this WebApplication app) + { + MapRegionEndpoints(app); + MapInfrastructureBindingEndpoints(app); + MapReadinessEndpoints(app); + MapRenameEndpoints(app); + MapDeletionEndpoints(app); + } + + // ── Region Endpoints ──────────────────────────────────────── + + private static void MapRegionEndpoints(WebApplication app) + { + var group = app.MapGroup("/api/v1/regions") + .WithTags("Regions"); + + group.MapPost("/", async ( + [FromBody] CreateRegionApiRequest body, + [FromServices] IRegionService regionService, + CancellationToken ct) => + { + var region = await regionService.CreateAsync(new CreateRegionRequest( + body.Name, body.DisplayName, body.Description, + body.CryptoProfile ?? "international", body.SortOrder ?? 0), ct); + return HttpResults.Created($"/api/v1/regions/{region.Id}", MapRegion(region)); + }) + .WithName("CreateRegion") + .WithSummary("Create a new region") + .Produces(StatusCodes.Status201Created) + .RequireAuthorization(TopologyManagePolicy); + + group.MapGet("/", async ( + [FromServices] IRegionService regionService, + CancellationToken ct) => + { + var regions = await regionService.ListAsync(ct); + return HttpResults.Ok(new RegionListResponse + { + Items = regions.Select(MapRegion).ToList(), + TotalCount = regions.Count + }); + }) + .WithName("ListRegions") + .WithSummary("List all regions for the current tenant") + .Produces(StatusCodes.Status200OK) + .RequireAuthorization(TopologyReadPolicy); + + group.MapGet("/{id:guid}", async ( + Guid id, + [FromServices] IRegionService regionService, + CancellationToken ct) => + { + var region = await regionService.GetAsync(id, ct); + return region is not null + ? HttpResults.Ok(MapRegion(region)) + : HttpResults.NotFound(); + }) + .WithName("GetRegion") + .WithSummary("Get a region by ID") + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound) + .RequireAuthorization(TopologyReadPolicy); + + group.MapPut("/{id:guid}", async ( + Guid id, + [FromBody] UpdateRegionApiRequest body, + [FromServices] IRegionService regionService, + CancellationToken ct) => + { + var region = await regionService.UpdateAsync(id, new UpdateRegionRequest( + body.DisplayName, body.Description, body.CryptoProfile, body.SortOrder, body.Status), ct); + return HttpResults.Ok(MapRegion(region)); + }) + .WithName("UpdateRegion") + .WithSummary("Update a region") + .Produces(StatusCodes.Status200OK) + .RequireAuthorization(TopologyManagePolicy); + + group.MapDelete("/{id:guid}", async ( + Guid id, + [FromServices] IRegionService regionService, + CancellationToken ct) => + { + await regionService.DeleteAsync(id, ct); + return HttpResults.NoContent(); + }) + .WithName("DeleteRegion") + .WithSummary("Delete a region") + .Produces(StatusCodes.Status204NoContent) + .RequireAuthorization(TopologyAdminPolicy); + } + + // ── Infrastructure Binding Endpoints ───────────────────────── + + private static void MapInfrastructureBindingEndpoints(WebApplication app) + { + var group = app.MapGroup("/api/v1/infrastructure-bindings") + .WithTags("Infrastructure Bindings"); + + group.MapPost("/", async ( + [FromBody] BindInfrastructureApiRequest body, + [FromServices] IInfrastructureBindingService bindingService, + CancellationToken ct) => + { + var scopeType = Enum.Parse(body.ScopeType, ignoreCase: true); + var role = Enum.Parse(body.BindingRole, ignoreCase: true); + var binding = await bindingService.BindAsync(new BindInfrastructureRequest( + body.IntegrationId, scopeType, body.ScopeId, role, body.Priority ?? 0), ct); + return HttpResults.Created($"/api/v1/infrastructure-bindings/{binding.Id}", MapBinding(binding)); + }) + .WithName("CreateInfrastructureBinding") + .WithSummary("Bind an integration to a scope") + .Produces(StatusCodes.Status201Created) + .RequireAuthorization(TopologyManagePolicy); + + group.MapDelete("/{id:guid}", async ( + Guid id, + [FromServices] IInfrastructureBindingService bindingService, + CancellationToken ct) => + { + await bindingService.UnbindAsync(id, ct); + return HttpResults.NoContent(); + }) + .WithName("DeleteInfrastructureBinding") + .WithSummary("Remove an infrastructure binding") + .Produces(StatusCodes.Status204NoContent) + .RequireAuthorization(TopologyManagePolicy); + + group.MapGet("/", async ( + [FromQuery] string scopeType, + [FromQuery] Guid? scopeId, + [FromServices] IInfrastructureBindingService bindingService, + CancellationToken ct) => + { + var scope = Enum.Parse(scopeType, ignoreCase: true); + var bindings = await bindingService.ListByScopeAsync(scope, scopeId, ct); + return HttpResults.Ok(new InfrastructureBindingListResponse + { + Items = bindings.Select(MapBinding).ToList(), + TotalCount = bindings.Count + }); + }) + .WithName("ListInfrastructureBindings") + .WithSummary("List bindings by scope") + .Produces(StatusCodes.Status200OK) + .RequireAuthorization(TopologyReadPolicy); + + group.MapGet("/resolve", async ( + [FromQuery] Guid environmentId, + [FromQuery] string role, + [FromServices] IInfrastructureBindingService bindingService, + CancellationToken ct) => + { + var bindingRole = Enum.Parse(role, ignoreCase: true); + var binding = await bindingService.ResolveAsync(environmentId, bindingRole, ct); + return binding is not null + ? HttpResults.Ok(MapBinding(binding)) + : HttpResults.NotFound(); + }) + .WithName("ResolveInfrastructureBinding") + .WithSummary("Resolve a binding with inheritance cascade") + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound) + .RequireAuthorization(TopologyReadPolicy); + + group.MapGet("/resolve-all", async ( + [FromQuery] Guid environmentId, + [FromServices] IInfrastructureBindingService bindingService, + CancellationToken ct) => + { + var resolution = await bindingService.ResolveAllAsync(environmentId, ct); + return HttpResults.Ok(MapResolution(resolution)); + }) + .WithName("ResolveAllInfrastructureBindings") + .WithSummary("Resolve all binding roles with inheritance") + .Produces(StatusCodes.Status200OK) + .RequireAuthorization(TopologyReadPolicy); + + group.MapPost("/{id:guid}/test", async ( + Guid id, + [FromServices] IInfrastructureBindingService bindingService, + CancellationToken ct) => + { + var result = await bindingService.TestBindingAsync(id, ct); + return HttpResults.Ok(result); + }) + .WithName("TestInfrastructureBinding") + .WithSummary("Test binding connectivity") + .Produces(StatusCodes.Status200OK) + .RequireAuthorization(TopologyManagePolicy); + } + + // ── Readiness Endpoints ────────────────────────────────────── + + private static void MapReadinessEndpoints(WebApplication app) + { + var targets = app.MapGroup("/api/v1/targets") + .WithTags("Topology Readiness"); + + targets.MapPost("/{id:guid}/validate", async ( + Guid id, + [FromServices] ITopologyReadinessService readinessService, + CancellationToken ct) => + { + var report = await readinessService.ValidateAsync(id, ct); + return HttpResults.Ok(MapReport(report)); + }) + .WithName("ValidateTarget") + .WithSummary("Run all readiness gates for a target") + .Produces(StatusCodes.Status200OK); + + targets.MapGet("/{id:guid}/readiness", async ( + Guid id, + [FromServices] ITopologyReadinessService readinessService, + CancellationToken ct) => + { + var report = await readinessService.GetLatestAsync(id, ct); + return report is not null + ? HttpResults.Ok(MapReport(report)) + : HttpResults.NotFound(); + }) + .WithName("GetTargetReadiness") + .WithSummary("Get latest readiness report for a target") + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound); + + var envs = app.MapGroup("/api/v1/environments") + .WithTags("Topology Readiness"); + + envs.MapGet("/{id:guid}/readiness", async ( + Guid id, + [FromServices] ITopologyReadinessService readinessService, + CancellationToken ct) => + { + var reports = await readinessService.ListByEnvironmentAsync(id, ct); + return HttpResults.Ok(new ReadinessListResponse + { + Items = reports.Select(MapReport).ToList(), + TotalCount = reports.Count + }); + }) + .WithName("GetEnvironmentReadiness") + .WithSummary("Get readiness for all targets in an environment") + .Produces(StatusCodes.Status200OK); + } + + // ── Rename Endpoints ───────────────────────────────────────── + + private static void MapRenameEndpoints(WebApplication app) + { + var entityTypes = new Dictionary + { + ["regions"] = RenameEntityType.Region, + ["environments"] = RenameEntityType.Environment, + ["targets"] = RenameEntityType.Target, + ["agents"] = RenameEntityType.Agent, + ["integrations"] = RenameEntityType.Integration + }; + + foreach (var (path, entityType) in entityTypes) + { + app.MapPatch($"/api/v1/{path}/{{id:guid}}/name", async ( + Guid id, + [FromBody] RenameApiRequest body, + [FromServices] ITopologyRenameService renameService, + CancellationToken ct) => + { + var result = await renameService.RenameAsync( + new RenameRequest(entityType, id, body.Name, body.DisplayName), ct); + + if (result.Success) + return HttpResults.Ok(result); + + if (result.Error == "name_conflict") + return HttpResults.Conflict(result); + + return HttpResults.BadRequest(result); + }) + .WithName($"Rename{entityType}") + .WithSummary($"Rename a {entityType.ToString().ToLowerInvariant()}") + .WithTags("Topology Rename") + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status409Conflict) + .RequireAuthorization(TopologyManagePolicy); + } + } + + // ── Deletion Endpoints ─────────────────────────────────────── + + private static void MapDeletionEndpoints(WebApplication app) + { + var entityPaths = new Dictionary + { + ["regions"] = EnvModels.DeletionEntityType.Region, + ["environments"] = EnvModels.DeletionEntityType.Environment, + ["targets"] = EnvModels.DeletionEntityType.Target, + ["agents"] = EnvModels.DeletionEntityType.Agent, + ["integrations"] = EnvModels.DeletionEntityType.Integration + }; + + // Request deletion for each entity type + foreach (var (path, entityType) in entityPaths) + { + app.MapPost($"/api/v1/{path}/{{id:guid}}/request-delete", async ( + Guid id, + [FromBody] RequestDeleteApiRequest? body, + [FromServices] IPendingDeletionService deletionService, + CancellationToken ct) => + { + var deletion = await deletionService.RequestDeletionAsync( + new DeletionRequest(entityType, id, body?.Reason), ct); + return HttpResults.Accepted($"/api/v1/pending-deletions/{deletion.Id}", MapDeletion(deletion)); + }) + .WithName($"RequestDelete{entityType}") + .WithSummary($"Request deletion of a {entityType.ToString().ToLowerInvariant()} with cool-off") + .WithTags("Topology Deletion") + .Produces(StatusCodes.Status202Accepted) + .RequireAuthorization(TopologyManagePolicy); + } + + // Pending deletion management + var deletionGroup = app.MapGroup("/api/v1/pending-deletions") + .WithTags("Topology Deletion"); + + deletionGroup.MapPost("/{id:guid}/confirm", async ( + Guid id, + [FromServices] IPendingDeletionService deletionService, + CancellationToken ct) => + { + // In real impl, get confirmedBy from the current user context + var deletion = await deletionService.ConfirmDeletionAsync(id, Guid.Empty, ct); + return HttpResults.Ok(MapDeletion(deletion)); + }) + .WithName("ConfirmDeletion") + .WithSummary("Confirm a pending deletion after cool-off expires") + .Produces(StatusCodes.Status200OK) + .RequireAuthorization(TopologyAdminPolicy); + + deletionGroup.MapPost("/{id:guid}/cancel", async ( + Guid id, + [FromServices] IPendingDeletionService deletionService, + CancellationToken ct) => + { + await deletionService.CancelDeletionAsync(id, Guid.Empty, ct); + return HttpResults.NoContent(); + }) + .WithName("CancelDeletion") + .WithSummary("Cancel a pending deletion") + .Produces(StatusCodes.Status204NoContent) + .RequireAuthorization(TopologyManagePolicy); + + deletionGroup.MapGet("/", async ( + [FromServices] IPendingDeletionService deletionService, + CancellationToken ct) => + { + var deletions = await deletionService.ListPendingAsync(ct); + return HttpResults.Ok(new PendingDeletionListResponse + { + Items = deletions.Select(MapDeletion).ToList(), + TotalCount = deletions.Count + }); + }) + .WithName("ListPendingDeletions") + .WithSummary("List all pending deletions for the current tenant") + .Produces(StatusCodes.Status200OK) + .RequireAuthorization(TopologyReadPolicy); + + deletionGroup.MapGet("/{id:guid}", async ( + Guid id, + [FromServices] IPendingDeletionService deletionService, + CancellationToken ct) => + { + var deletion = await deletionService.GetAsync(id, ct); + return deletion is not null + ? HttpResults.Ok(MapDeletion(deletion)) + : HttpResults.NotFound(); + }) + .WithName("GetPendingDeletion") + .WithSummary("Get pending deletion details with cascade summary") + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound) + .RequireAuthorization(TopologyReadPolicy); + } + + // ── Mappers ────────────────────────────────────────────────── + + private static RegionResponse MapRegion(EnvModels.Region r) => new() + { + Id = r.Id, + TenantId = r.TenantId, + Name = r.Name, + DisplayName = r.DisplayName, + Description = r.Description, + CryptoProfile = r.CryptoProfile, + SortOrder = r.SortOrder, + Status = r.Status.ToString().ToLowerInvariant(), + CreatedAt = r.CreatedAt, + UpdatedAt = r.UpdatedAt + }; + + private static InfrastructureBindingResponse MapBinding(EnvModels.InfrastructureBinding b) => new() + { + Id = b.Id, + IntegrationId = b.IntegrationId, + ScopeType = b.ScopeType.ToString().ToLowerInvariant(), + ScopeId = b.ScopeId, + BindingRole = b.Role.ToString().ToLowerInvariant(), + Priority = b.Priority, + IsActive = b.IsActive, + CreatedAt = b.CreatedAt + }; + + private static InfrastructureBindingResolutionResponse MapResolution(EnvModels.InfrastructureBindingResolution r) => new() + { + Registry = r.Registry is not null ? new ResolvedBindingResponse + { + Binding = MapBinding(r.Registry.Binding), + ResolvedFrom = r.Registry.ResolvedFrom.ToString().ToLowerInvariant() + } : null, + Vault = r.Vault is not null ? new ResolvedBindingResponse + { + Binding = MapBinding(r.Vault.Binding), + ResolvedFrom = r.Vault.ResolvedFrom.ToString().ToLowerInvariant() + } : null, + SettingsStore = r.SettingsStore is not null ? new ResolvedBindingResponse + { + Binding = MapBinding(r.SettingsStore.Binding), + ResolvedFrom = r.SettingsStore.ResolvedFrom.ToString().ToLowerInvariant() + } : null + }; + + private static TopologyPointReportResponse MapReport(EnvModels.TopologyPointReport r) => new() + { + TargetId = r.TargetId, + EnvironmentId = r.EnvironmentId, + IsReady = r.IsReady, + Gates = r.Gates.Select(g => new GateResultResponse + { + GateName = g.GateName, + Status = g.Status.ToString().ToLowerInvariant(), + Message = g.Message, + CheckedAt = g.CheckedAt, + DurationMs = g.DurationMs + }).ToList(), + EvaluatedAt = r.EvaluatedAt + }; + + private static PendingDeletionResponse MapDeletion(EnvModels.PendingDeletion d) => new() + { + PendingDeletionId = d.Id, + EntityType = d.EntityType.ToString().ToLowerInvariant(), + EntityName = d.EntityName, + Status = d.Status.ToString().ToLowerInvariant(), + CoolOffExpiresAt = d.CoolOffExpiresAt, + CanConfirmAfter = d.CoolOffExpiresAt, + CascadeSummary = new CascadeSummaryResponse + { + ChildEnvironments = d.CascadeSummary.ChildEnvironments, + ChildTargets = d.CascadeSummary.ChildTargets, + BoundAgents = d.CascadeSummary.BoundAgents, + InfrastructureBindings = d.CascadeSummary.InfrastructureBindings, + ActiveHealthSchedules = d.CascadeSummary.ActiveHealthSchedules, + PendingDeployments = d.CascadeSummary.PendingDeployments + }, + RequestedAt = d.RequestedAt, + ConfirmedAt = d.ConfirmedAt, + CompletedAt = d.CompletedAt + }; + + // ── API Request/Response DTOs ──────────────────────────────── + + internal sealed class CreateRegionApiRequest + { + public required string Name { get; set; } + public required string DisplayName { get; set; } + public string? Description { get; set; } + public string? CryptoProfile { get; set; } + public int? SortOrder { get; set; } + } + + internal sealed class UpdateRegionApiRequest + { + public string? DisplayName { get; set; } + public string? Description { get; set; } + public string? CryptoProfile { get; set; } + public int? SortOrder { get; set; } + public EnvModels.RegionStatus? Status { get; set; } + } + + internal sealed class BindInfrastructureApiRequest + { + public required Guid IntegrationId { get; set; } + public required string ScopeType { get; set; } + public Guid? ScopeId { get; set; } + public required string BindingRole { get; set; } + public int? Priority { get; set; } + } + + internal sealed class RenameApiRequest + { + public required string Name { get; set; } + public required string DisplayName { get; set; } + } + + internal sealed class RequestDeleteApiRequest + { + public string? Reason { get; set; } + } + + internal sealed class RegionResponse + { + public Guid Id { get; set; } + public Guid TenantId { get; set; } + public required string Name { get; set; } + public required string DisplayName { get; set; } + public string? Description { get; set; } + public required string CryptoProfile { get; set; } + public int SortOrder { get; set; } + public required string Status { get; set; } + public DateTimeOffset CreatedAt { get; set; } + public DateTimeOffset UpdatedAt { get; set; } + } + + internal sealed class RegionListResponse + { + public required List Items { get; set; } + public int TotalCount { get; set; } + } + + internal sealed class InfrastructureBindingResponse + { + public Guid Id { get; set; } + public Guid IntegrationId { get; set; } + public required string ScopeType { get; set; } + public Guid? ScopeId { get; set; } + public required string BindingRole { get; set; } + public int Priority { get; set; } + public bool IsActive { get; set; } + public DateTimeOffset CreatedAt { get; set; } + } + + internal sealed class InfrastructureBindingListResponse + { + public required List Items { get; set; } + public int TotalCount { get; set; } + } + + internal sealed class InfrastructureBindingResolutionResponse + { + public ResolvedBindingResponse? Registry { get; set; } + public ResolvedBindingResponse? Vault { get; set; } + public ResolvedBindingResponse? SettingsStore { get; set; } + } + + internal sealed class ResolvedBindingResponse + { + public required InfrastructureBindingResponse Binding { get; set; } + public required string ResolvedFrom { get; set; } + } + + internal sealed class TopologyPointReportResponse + { + public Guid TargetId { get; set; } + public Guid EnvironmentId { get; set; } + public bool IsReady { get; set; } + public required List Gates { get; set; } + public DateTimeOffset EvaluatedAt { get; set; } + } + + internal sealed class GateResultResponse + { + public required string GateName { get; set; } + public required string Status { get; set; } + public string? Message { get; set; } + public DateTimeOffset? CheckedAt { get; set; } + public int? DurationMs { get; set; } + } + + internal sealed class ReadinessListResponse + { + public required List Items { get; set; } + public int TotalCount { get; set; } + } + + internal sealed class PendingDeletionResponse + { + public Guid PendingDeletionId { get; set; } + public required string EntityType { get; set; } + public required string EntityName { get; set; } + public required string Status { get; set; } + public DateTimeOffset CoolOffExpiresAt { get; set; } + public DateTimeOffset CanConfirmAfter { get; set; } + public required CascadeSummaryResponse CascadeSummary { get; set; } + public DateTimeOffset RequestedAt { get; set; } + public DateTimeOffset? ConfirmedAt { get; set; } + public DateTimeOffset? CompletedAt { get; set; } + } + + internal sealed class CascadeSummaryResponse + { + public int ChildEnvironments { get; set; } + public int ChildTargets { get; set; } + public int BoundAgents { get; set; } + public int InfrastructureBindings { get; set; } + public int ActiveHealthSchedules { get; set; } + public int PendingDeployments { get; set; } + } + + internal sealed class PendingDeletionListResponse + { + public required List Items { get; set; } + public int TotalCount { get; set; } + } +} diff --git a/src/Concelier/StellaOps.Concelier.WebService/Options/ConcelierOptionsValidator.cs b/src/Concelier/StellaOps.Concelier.WebService/Options/ConcelierOptionsValidator.cs index aa170dc24..91262eb9a 100644 --- a/src/Concelier/StellaOps.Concelier.WebService/Options/ConcelierOptionsValidator.cs +++ b/src/Concelier/StellaOps.Concelier.WebService/Options/ConcelierOptionsValidator.cs @@ -260,10 +260,6 @@ public static class ConcelierOptionsValidator } } - if (mirror.Enabled && mirror.Domains.Count == 0) - { - throw new InvalidOperationException("Mirror distribution requires at least one domain when enabled."); - } } private static void ValidateAdvisoryChunks(ConcelierOptions.AdvisoryChunkOptions chunks) diff --git a/src/Concelier/StellaOps.Concelier.WebService/Program.cs b/src/Concelier/StellaOps.Concelier.WebService/Program.cs index 4d1240f7f..72a9a7468 100644 --- a/src/Concelier/StellaOps.Concelier.WebService/Program.cs +++ b/src/Concelier/StellaOps.Concelier.WebService/Program.cs @@ -572,6 +572,93 @@ builder.Services.Configure(builder.Configuration.GetSection builder.Services.AddHttpClient("MirrorTest"); builder.Services.AddHttpClient("MirrorConsumer"); +// ── Topology Setup Services (in-memory stores, future: DB-backed) ── +{ + // Shared tenant/user ID provider for topology services + Func topologyTenantProvider = () => Guid.Empty; // Will be populated per-request by middleware + Func topologyUserProvider = () => Guid.Empty; + + // Region + builder.Services.AddSingleton(new StellaOps.ReleaseOrchestrator.Environment.Region.InMemoryRegionStore(topologyTenantProvider)); + builder.Services.AddSingleton(sp => + sp.GetRequiredService()); + builder.Services.AddSingleton(sp => + new StellaOps.ReleaseOrchestrator.Environment.Region.RegionService( + sp.GetRequiredService(), + TimeProvider.System, + sp.GetRequiredService().CreateLogger(), + topologyTenantProvider, topologyUserProvider)); + + // Environment (uses existing stores if available, or creates new) + builder.Services.AddSingleton(new StellaOps.ReleaseOrchestrator.Environment.Store.InMemoryEnvironmentStore(topologyTenantProvider)); + builder.Services.AddSingleton(sp => + sp.GetRequiredService()); + builder.Services.AddSingleton(sp => + new StellaOps.ReleaseOrchestrator.Environment.Services.EnvironmentService( + sp.GetRequiredService(), + TimeProvider.System, + sp.GetRequiredService().CreateLogger(), + topologyTenantProvider, topologyUserProvider)); + + // Target + builder.Services.AddSingleton(new StellaOps.ReleaseOrchestrator.Environment.Target.InMemoryTargetStore(topologyTenantProvider)); + builder.Services.AddSingleton(sp => + sp.GetRequiredService()); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(sp => + new StellaOps.ReleaseOrchestrator.Environment.Target.TargetRegistry( + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + TimeProvider.System, + sp.GetRequiredService().CreateLogger(), + topologyTenantProvider)); + + // Infrastructure Binding + builder.Services.AddSingleton(new StellaOps.ReleaseOrchestrator.Environment.InfrastructureBinding.InMemoryInfrastructureBindingStore(topologyTenantProvider)); + builder.Services.AddSingleton(sp => + sp.GetRequiredService()); + builder.Services.AddSingleton(sp => + new StellaOps.ReleaseOrchestrator.Environment.InfrastructureBinding.InfrastructureBindingService( + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService().CreateLogger(), + TimeProvider.System, topologyTenantProvider, topologyUserProvider)); + + // Readiness + builder.Services.AddSingleton(); + builder.Services.AddSingleton(sp => + sp.GetRequiredService()); + builder.Services.AddSingleton(sp => + new StellaOps.ReleaseOrchestrator.Environment.Readiness.TopologyReadinessService( + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService().CreateLogger(), + TimeProvider.System, topologyTenantProvider)); + + // Rename + builder.Services.AddSingleton(sp => + new StellaOps.ReleaseOrchestrator.Environment.Rename.TopologyRenameService( + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService().CreateLogger())); + + // Deletion + builder.Services.AddSingleton(new StellaOps.ReleaseOrchestrator.Environment.Deletion.InMemoryPendingDeletionStore(topologyTenantProvider)); + builder.Services.AddSingleton(sp => + sp.GetRequiredService()); + builder.Services.AddSingleton(sp => + new StellaOps.ReleaseOrchestrator.Environment.Deletion.PendingDeletionService( + sp.GetRequiredService(), + TimeProvider.System, + sp.GetRequiredService().CreateLogger(), + topologyTenantProvider, topologyUserProvider)); + builder.Services.AddHostedService(); +} + // Mirror distribution options binding and export scheduler (background bundle refresh, TASK-006b) builder.Services.Configure(builder.Configuration.GetSection(MirrorDistributionOptions.SectionName)); builder.Services.AddHostedService(); @@ -850,11 +937,17 @@ builder.Services.AddAuthorization(options => options.AddStellaOpsScopePolicy(ObservationsPolicyName, StellaOpsScopes.VulnView); options.AddStellaOpsScopePolicy(AdvisoryIngestPolicyName, StellaOpsScopes.AdvisoryIngest); options.AddStellaOpsScopePolicy(AdvisoryReadPolicyName, StellaOpsScopes.AdvisoryRead); + options.AddStellaOpsAnyScopePolicy("Concelier.Sources.Manage", StellaOpsScopes.IntegrationWrite, StellaOpsScopes.IntegrationOperate); options.AddStellaOpsScopePolicy(AocVerifyPolicyName, StellaOpsScopes.AdvisoryRead, StellaOpsScopes.AocVerify); options.AddStellaOpsScopePolicy(CanonicalReadPolicyName, StellaOpsScopes.AdvisoryRead); options.AddStellaOpsScopePolicy(CanonicalIngestPolicyName, StellaOpsScopes.AdvisoryIngest); options.AddStellaOpsScopePolicy(InterestReadPolicyName, StellaOpsScopes.VulnView); options.AddStellaOpsScopePolicy(InterestAdminPolicyName, StellaOpsScopes.AdvisoryIngest); + + // Topology setup policies (regions, infra bindings, readiness, rename, deletion) + options.AddStellaOpsAnyScopePolicy("Topology.Read", StellaOpsScopes.OrchRead, StellaOpsScopes.PlatformContextRead); + options.AddStellaOpsAnyScopePolicy("Topology.Manage", StellaOpsScopes.OrchOperate, StellaOpsScopes.IntegrationWrite); + options.AddStellaOpsAnyScopePolicy("Topology.Admin", StellaOpsScopes.OrchOperate); }); var pluginHostOptions = BuildPluginOptions(concelierOptions, builder.Environment.ContentRootPath); @@ -968,6 +1061,9 @@ app.MapFeedMirrorManagementEndpoints(); // Mirror domain management CRUD endpoints app.MapMirrorDomainManagementEndpoints(); +// Topology setup endpoints (regions, infrastructure bindings, readiness, rename, deletion) +app.MapTopologySetupEndpoints(); + app.MapGet("/.well-known/openapi", ([FromServices] OpenApiDiscoveryDocumentProvider provider, HttpContext context) => { var (payload, etag) = provider.GetDocument(); diff --git a/src/Concelier/StellaOps.Concelier.WebService/Services/InMemoryMirrorDomainStore.cs b/src/Concelier/StellaOps.Concelier.WebService/Services/InMemoryMirrorDomainStore.cs index 5cd141365..a25c67e9d 100644 --- a/src/Concelier/StellaOps.Concelier.WebService/Services/InMemoryMirrorDomainStore.cs +++ b/src/Concelier/StellaOps.Concelier.WebService/Services/InMemoryMirrorDomainStore.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using Microsoft.Extensions.Options; using StellaOps.Concelier.WebService.Extensions; namespace StellaOps.Concelier.WebService.Services; @@ -11,11 +12,29 @@ namespace StellaOps.Concelier.WebService.Services; public sealed class InMemoryMirrorDomainStore : IMirrorDomainStore, IMirrorConfigStore, IMirrorConsumerConfigStore, IMirrorBundleImportStore { private readonly ConcurrentDictionary _domains = new(StringComparer.OrdinalIgnoreCase); + private readonly object _configLock = new(); private readonly object _consumerConfigLock = new(); + private MirrorConfigRecord _config; private ConsumerConfigResponse _consumerConfig = new(); private volatile MirrorImportStatusRecord? _latestImportStatus; - public IReadOnlyList GetAllDomains() => _domains.Values.ToList(); + public InMemoryMirrorDomainStore(IOptions options) + { + var current = options.Value; + _config = new MirrorConfigRecord + { + Mode = NormalizeMode(current.Mode), + OutputRoot = current.OutputRoot, + ConsumerBaseAddress = current.ConsumerBaseAddress, + SigningEnabled = current.SigningEnabled, + SigningAlgorithm = current.SigningAlgorithm, + SigningKeyId = current.SigningKeyId, + AutoRefreshEnabled = current.AutoRefreshEnabled, + RefreshIntervalMinutes = current.RefreshIntervalMinutes, + }; + } + + public IReadOnlyList GetAllDomains() => _domains.Values.OrderBy(domain => domain.DisplayName, StringComparer.OrdinalIgnoreCase).ToList(); public MirrorDomainRecord? GetDomain(string domainId) => _domains.GetValueOrDefault(domainId); @@ -31,8 +50,43 @@ public sealed class InMemoryMirrorDomainStore : IMirrorDomainStore, IMirrorConfi return Task.CompletedTask; } + public MirrorConfigRecord GetConfig() + { + lock (_configLock) + { + return new MirrorConfigRecord + { + Mode = _config.Mode, + OutputRoot = _config.OutputRoot, + ConsumerBaseAddress = _config.ConsumerBaseAddress, + SigningEnabled = _config.SigningEnabled, + SigningAlgorithm = _config.SigningAlgorithm, + SigningKeyId = _config.SigningKeyId, + AutoRefreshEnabled = _config.AutoRefreshEnabled, + RefreshIntervalMinutes = _config.RefreshIntervalMinutes, + }; + } + } + public Task UpdateConfigAsync(UpdateMirrorConfigRequest request, CancellationToken ct = default) { + lock (_configLock) + { + _config.Mode = NormalizeMode(request.Mode) ?? _config.Mode; + _config.ConsumerBaseAddress = string.IsNullOrWhiteSpace(request.ConsumerBaseAddress) + ? _config.ConsumerBaseAddress + : request.ConsumerBaseAddress.Trim(); + _config.SigningEnabled = request.Signing?.Enabled ?? _config.SigningEnabled; + _config.SigningAlgorithm = string.IsNullOrWhiteSpace(request.Signing?.Algorithm) + ? _config.SigningAlgorithm + : request.Signing!.Algorithm!.Trim(); + _config.SigningKeyId = string.IsNullOrWhiteSpace(request.Signing?.KeyId) + ? _config.SigningKeyId + : request.Signing!.KeyId!.Trim(); + _config.AutoRefreshEnabled = request.AutoRefreshEnabled ?? _config.AutoRefreshEnabled; + _config.RefreshIntervalMinutes = request.RefreshIntervalMinutes ?? _config.RefreshIntervalMinutes; + } + return Task.CompletedTask; } @@ -64,10 +118,15 @@ public sealed class InMemoryMirrorDomainStore : IMirrorDomainStore, IMirrorConfi } : null, Connected = !string.IsNullOrWhiteSpace(request.BaseAddress), - LastSync = _consumerConfig.LastSync, // preserve existing sync timestamp + LastSync = DateTimeOffset.UtcNow, }; } + lock (_configLock) + { + _config.ConsumerBaseAddress = request.BaseAddress; + } + return Task.CompletedTask; } @@ -76,4 +135,19 @@ public sealed class InMemoryMirrorDomainStore : IMirrorDomainStore, IMirrorConfi public MirrorImportStatusRecord? GetLatestStatus() => _latestImportStatus; public void SetStatus(MirrorImportStatusRecord status) => _latestImportStatus = status; + + private static string NormalizeMode(string? mode) + { + if (string.IsNullOrWhiteSpace(mode)) + { + return "Direct"; + } + + return mode.Trim().ToLowerInvariant() switch + { + "mirror" => "Mirror", + "hybrid" => "Hybrid", + _ => "Direct", + }; + } } diff --git a/src/Concelier/StellaOps.Concelier.WebService/StellaOps.Concelier.WebService.csproj b/src/Concelier/StellaOps.Concelier.WebService/StellaOps.Concelier.WebService.csproj index dbcee1d82..542ce16bc 100644 --- a/src/Concelier/StellaOps.Concelier.WebService/StellaOps.Concelier.WebService.csproj +++ b/src/Concelier/StellaOps.Concelier.WebService/StellaOps.Concelier.WebService.csproj @@ -48,6 +48,8 @@ + + diff --git a/src/Concelier/__Tests/StellaOps.Concelier.WebService.Tests/ConcelierOptionsValidatorTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.WebService.Tests/ConcelierOptionsValidatorTests.cs new file mode 100644 index 000000000..908aaff4c --- /dev/null +++ b/src/Concelier/__Tests/StellaOps.Concelier.WebService.Tests/ConcelierOptionsValidatorTests.cs @@ -0,0 +1,41 @@ +using System; +using StellaOps.Concelier.WebService.Options; +using StellaOps.TestKit; +using Xunit; + +namespace StellaOps.Concelier.WebService.Tests; + +public sealed class ConcelierOptionsValidatorTests +{ + [Trait("Category", TestCategories.Unit)] + [Trait("Intent", "Operational")] + [Fact] + public void Validate_AllowsEnabledMirrorWithoutStaticDomains() + { + var options = new ConcelierOptions + { + PostgresStorage = new ConcelierOptions.PostgresStorageOptions + { + Enabled = true, + ConnectionString = "Host=postgres;Database=stellaops;Username=stellaops;Password=stellaops" + }, + Mirror = new ConcelierOptions.MirrorOptions + { + Enabled = true, + ExportRoot = "/var/lib/concelier/jobs/mirror-exports", + ExportRootAbsolute = "/var/lib/concelier/jobs/mirror-exports", + LatestDirectoryName = "latest", + MirrorDirectoryName = "mirror" + }, + Evidence = new ConcelierOptions.EvidenceBundleOptions + { + Root = "/var/lib/concelier/jobs/evidence-bundles", + RootAbsolute = "/var/lib/concelier/jobs/evidence-bundles" + } + }; + + var exception = Record.Exception(() => ConcelierOptionsValidator.Validate(options)); + + Assert.Null(exception); + } +} diff --git a/src/Concelier/__Tests/StellaOps.Concelier.WebService.Tests/WebServiceEndpointsTests.cs b/src/Concelier/__Tests/StellaOps.Concelier.WebService.Tests/WebServiceEndpointsTests.cs index b6db09138..28970e25c 100644 --- a/src/Concelier/__Tests/StellaOps.Concelier.WebService.Tests/WebServiceEndpointsTests.cs +++ b/src/Concelier/__Tests/StellaOps.Concelier.WebService.Tests/WebServiceEndpointsTests.cs @@ -1532,6 +1532,81 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime Assert.True(limitedResponse.Headers.RetryAfter!.Delta!.Value.TotalSeconds > 0); } + [Fact] + public async Task MirrorManagementEndpointsUseAdvisorySourcesNamespaceAndGeneratePublicArtifacts() + { + using var temp = new TempDirectory(); + var environment = new Dictionary + { + ["CONCELIER_MIRROR__ENABLED"] = "true", + ["CONCELIER_MIRROR__EXPORTROOT"] = temp.Path, + ["CONCELIER_MIRROR__ACTIVEEXPORTID"] = "latest", + ["CONCELIER_MIRROR__MAXINDEXREQUESTSPERHOUR"] = "5", + ["CONCELIER_MIRROR__MAXDOWNLOADREQUESTSPERHOUR"] = "5" + }; + + using var factory = new ConcelierApplicationFactory(_runner.ConnectionString, environmentOverrides: environment); + using var client = factory.CreateClient(); + client.DefaultRequestHeaders.Add("X-Stella-Tenant", "test-tenant"); + + var configResponse = await client.GetAsync("/api/v1/advisory-sources/mirror/config"); + Assert.Equal(HttpStatusCode.OK, configResponse.StatusCode); + + var createResponse = await client.PostAsJsonAsync( + "/api/v1/advisory-sources/mirror/domains", + new + { + domainId = "primary", + displayName = "Primary", + sourceIds = new[] { "nvd", "osv" }, + exportFormat = "JSON", + rateLimits = new + { + indexRequestsPerHour = 60, + downloadRequestsPerHour = 120 + }, + requireAuthentication = false, + signing = new + { + enabled = false, + algorithm = "HMAC-SHA256", + keyId = string.Empty + } + }); + Assert.Equal(HttpStatusCode.Created, createResponse.StatusCode); + + var generateResponse = await client.PostAsync("/api/v1/advisory-sources/mirror/domains/primary/generate", content: null); + Assert.Equal(HttpStatusCode.Accepted, generateResponse.StatusCode); + + var endpointsResponse = await client.GetAsync("/api/v1/advisory-sources/mirror/domains/primary/endpoints"); + Assert.Equal(HttpStatusCode.OK, endpointsResponse.StatusCode); + var endpointsJson = JsonDocument.Parse(await endpointsResponse.Content.ReadAsStringAsync()); + var endpoints = endpointsJson.RootElement.GetProperty("endpoints").EnumerateArray().Select(element => element.GetProperty("path").GetString()).ToList(); + Assert.Contains("/concelier/exports/index.json", endpoints); + Assert.Contains("/concelier/exports/mirror/primary/manifest.json", endpoints); + Assert.Contains("/concelier/exports/mirror/primary/bundle.json", endpoints); + + var statusResponse = await client.GetAsync("/api/v1/advisory-sources/mirror/domains/primary/status"); + Assert.Equal(HttpStatusCode.OK, statusResponse.StatusCode); + var statusJson = JsonDocument.Parse(await statusResponse.Content.ReadAsStringAsync()); + Assert.Equal("fresh", statusJson.RootElement.GetProperty("staleness").GetString()); + + var indexResponse = await client.GetAsync("/concelier/exports/index.json"); + Assert.Equal(HttpStatusCode.OK, indexResponse.StatusCode); + var indexContent = await indexResponse.Content.ReadAsStringAsync(); + Assert.Contains(@"""domainId"":""primary""", indexContent, StringComparison.Ordinal); + + var manifestResponse = await client.GetAsync("/concelier/exports/mirror/primary/manifest.json"); + Assert.Equal(HttpStatusCode.OK, manifestResponse.StatusCode); + var manifestContent = await manifestResponse.Content.ReadAsStringAsync(); + Assert.Contains(@"""domainId"":""primary""", manifestContent, StringComparison.Ordinal); + + var bundleResponse = await client.GetAsync("/concelier/exports/mirror/primary/bundle.json"); + Assert.Equal(HttpStatusCode.OK, bundleResponse.StatusCode); + var bundleContent = await bundleResponse.Content.ReadAsStringAsync(); + Assert.Contains(@"""domainId"":""primary""", bundleContent, StringComparison.Ordinal); + } + [Fact] public void MergeModuleDisabledByDefault() { @@ -4002,4 +4077,3 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime } - diff --git a/src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.Infrastructure/Postgres/JobEngineDataSource.cs b/src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.Infrastructure/Postgres/JobEngineDataSource.cs index bcdd3d071..b571d7f05 100644 --- a/src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.Infrastructure/Postgres/JobEngineDataSource.cs +++ b/src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.Infrastructure/Postgres/JobEngineDataSource.cs @@ -136,10 +136,17 @@ public sealed class JobEngineDataSource : IAsyncDisposable await tenantCommand.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); } - var quotedSchemaName = QuoteIdentifier(ResolveSchemaName(schemaName)); - await using var searchPathCommand = new NpgsqlCommand( - $"SET search_path TO {quotedSchemaName}, public;", - connection); + // Build search_path: primary schema, then packs (if not already primary), then public. + // The packs schema hosts the pack registry tables (packs.packs, packs.pack_versions) + // and its enum types (pack_status, pack_version_status). Including it in every + // connection's search_path avoids "type does not exist" errors when cross-schema + // queries or enum casts reference packs-schema objects. + var resolvedSchema = ResolveSchemaName(schemaName); + var quotedSchemaName = QuoteIdentifier(resolvedSchema); + var searchPathSql = string.Equals(resolvedSchema, "packs", StringComparison.OrdinalIgnoreCase) + ? $"SET search_path TO {quotedSchemaName}, public;" + : $"SET search_path TO {quotedSchemaName}, {QuoteIdentifier("packs")}, public;"; + await using var searchPathCommand = new NpgsqlCommand(searchPathSql, connection); searchPathCommand.CommandTimeout = _options.CommandTimeoutSeconds; await searchPathCommand.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); } diff --git a/src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.Infrastructure/Postgres/PostgresDeadLetterRepository.cs b/src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.Infrastructure/Postgres/PostgresDeadLetterRepository.cs index 0f9b18618..ad5b1bb5a 100644 --- a/src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.Infrastructure/Postgres/PostgresDeadLetterRepository.cs +++ b/src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.Infrastructure/Postgres/PostgresDeadLetterRepository.cs @@ -343,80 +343,103 @@ public sealed class PostgresDeadLetterRepository : IDeadLetterRepository await using var connection = await _dataSource.OpenConnectionAsync(tenantId, "reader", cancellationToken).ConfigureAwait(false); - // Get counts - long total = 0, pending = 0, replaying = 0, replayed = 0, resolved = 0, exhausted = 0, expired = 0, retryable = 0; - await using (var command = new NpgsqlCommand(statsSql, connection)) + try { - command.CommandTimeout = _dataSource.CommandTimeoutSeconds; - command.Parameters.AddWithValue("tenant_id", tenantId); - await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false); - if (await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) + // Get counts + long total = 0, pending = 0, replaying = 0, replayed = 0, resolved = 0, exhausted = 0, expired = 0, retryable = 0; + await using (var command = new NpgsqlCommand(statsSql, connection)) { - total = reader.GetInt64(0); - pending = reader.GetInt64(1); - replaying = reader.GetInt64(2); - replayed = reader.GetInt64(3); - resolved = reader.GetInt64(4); - exhausted = reader.GetInt64(5); - expired = reader.GetInt64(6); - retryable = reader.GetInt64(7); - } - } - - // Get by category - var byCategory = new Dictionary(); - await using (var command = new NpgsqlCommand(byCategorySql, connection)) - { - command.CommandTimeout = _dataSource.CommandTimeoutSeconds; - command.Parameters.AddWithValue("tenant_id", tenantId); - await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false); - while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) - { - if (Enum.TryParse(reader.GetString(0), true, out var cat)) + command.CommandTimeout = _dataSource.CommandTimeoutSeconds; + command.Parameters.AddWithValue("tenant_id", tenantId); + await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false); + if (await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) { - byCategory[cat] = reader.GetInt64(1); + total = reader.GetInt64(0); + pending = reader.GetInt64(1); + replaying = reader.GetInt64(2); + replayed = reader.GetInt64(3); + resolved = reader.GetInt64(4); + exhausted = reader.GetInt64(5); + expired = reader.GetInt64(6); + retryable = reader.GetInt64(7); } } - } - // Get top error codes - var topErrorCodes = new Dictionary(); - await using (var command = new NpgsqlCommand(topErrorCodesSql, connection)) - { - command.CommandTimeout = _dataSource.CommandTimeoutSeconds; - command.Parameters.AddWithValue("tenant_id", tenantId); - await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false); - while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) + // Get by category + var byCategory = new Dictionary(); + await using (var command = new NpgsqlCommand(byCategorySql, connection)) { - topErrorCodes[reader.GetString(0)] = reader.GetInt64(1); + command.CommandTimeout = _dataSource.CommandTimeoutSeconds; + command.Parameters.AddWithValue("tenant_id", tenantId); + await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false); + while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) + { + if (Enum.TryParse(reader.GetString(0), true, out var cat)) + { + byCategory[cat] = reader.GetInt64(1); + } + } } - } - // Get top job types - var topJobTypes = new Dictionary(); - await using (var command = new NpgsqlCommand(topJobTypesSql, connection)) - { - command.CommandTimeout = _dataSource.CommandTimeoutSeconds; - command.Parameters.AddWithValue("tenant_id", tenantId); - await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false); - while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) + // Get top error codes + var topErrorCodes = new Dictionary(); + await using (var command = new NpgsqlCommand(topErrorCodesSql, connection)) { - topJobTypes[reader.GetString(0)] = reader.GetInt64(1); + command.CommandTimeout = _dataSource.CommandTimeoutSeconds; + command.Parameters.AddWithValue("tenant_id", tenantId); + await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false); + while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) + { + topErrorCodes[reader.GetString(0)] = reader.GetInt64(1); + } } - } - return new DeadLetterStats( - TotalEntries: total, - PendingEntries: pending, - ReplayingEntries: replaying, - ReplayedEntries: replayed, - ResolvedEntries: resolved, - ExhaustedEntries: exhausted, - ExpiredEntries: expired, - RetryableEntries: retryable, - ByCategory: byCategory, - TopErrorCodes: topErrorCodes, - TopJobTypes: topJobTypes); + // Get top job types + var topJobTypes = new Dictionary(); + await using (var command = new NpgsqlCommand(topJobTypesSql, connection)) + { + command.CommandTimeout = _dataSource.CommandTimeoutSeconds; + command.Parameters.AddWithValue("tenant_id", tenantId); + await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false); + while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) + { + topJobTypes[reader.GetString(0)] = reader.GetInt64(1); + } + } + + return new DeadLetterStats( + TotalEntries: total, + PendingEntries: pending, + ReplayingEntries: replaying, + ReplayedEntries: replayed, + ResolvedEntries: resolved, + ExhaustedEntries: exhausted, + ExpiredEntries: expired, + RetryableEntries: retryable, + ByCategory: byCategory, + TopErrorCodes: topErrorCodes, + TopJobTypes: topJobTypes); + } + catch (PostgresException ex) when (IsMissingTableOrAbortedTransaction(ex.SqlState)) + { + _logger.LogWarning( + ex, + "Dead-letter table is not present; returning empty stats for tenant {TenantId}.", + tenantId); + + return new DeadLetterStats( + TotalEntries: 0, + PendingEntries: 0, + ReplayingEntries: 0, + ReplayedEntries: 0, + ResolvedEntries: 0, + ExhaustedEntries: 0, + ExpiredEntries: 0, + RetryableEntries: 0, + ByCategory: new Dictionary(), + TopErrorCodes: new Dictionary(), + TopJobTypes: new Dictionary()); + } } public async Task> GetActionableSummaryAsync( @@ -441,14 +464,25 @@ public sealed class PostgresDeadLetterRepository : IDeadLetterRepository "Dead-letter summary function path is unavailable for tenant {TenantId}; falling back to direct table aggregation.", tenantId); - return await ReadActionableSummaryAsync( - connection, - ActionableSummaryFallbackSql, - tenantId, - limit, - cancellationToken).ConfigureAwait(false); + try + { + return await ReadActionableSummaryAsync( + connection, + ActionableSummaryFallbackSql, + tenantId, + limit, + cancellationToken).ConfigureAwait(false); + } + catch (PostgresException fallbackEx) when (IsMissingTableOrAbortedTransaction(fallbackEx.SqlState)) + { + _logger.LogWarning( + fallbackEx, + "Dead-letter table is not present during fallback; returning empty actionable summary for tenant {TenantId}.", + tenantId); + return []; + } } - catch (PostgresException ex) when (ex.SqlState == PostgresErrorCodes.UndefinedTable) + catch (PostgresException ex) when (IsMissingTableOrAbortedTransaction(ex.SqlState)) { _logger.LogWarning( ex, @@ -462,6 +496,16 @@ public sealed class PostgresDeadLetterRepository : IDeadLetterRepository => string.Equals(sqlState, PostgresErrorCodes.UndefinedFunction, StringComparison.Ordinal) || string.Equals(sqlState, PostgresErrorCodes.AmbiguousColumn, StringComparison.Ordinal); + /// + /// Returns true when the SQL state indicates the dead-letter table is missing + /// (42P01 = undefined_table) or the connection is in a failed transaction state + /// (25P02 = in_failed_sql_transaction), which can occur when a previous command + /// on the same connection already failed. + /// + internal static bool IsMissingTableOrAbortedTransaction(string? sqlState) + => string.Equals(sqlState, PostgresErrorCodes.UndefinedTable, StringComparison.Ordinal) + || string.Equals(sqlState, "25P02", StringComparison.Ordinal); + public async Task MarkExpiredAsync( int batchLimit, CancellationToken cancellationToken) diff --git a/src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.Infrastructure/Postgres/PostgresJobRepository.cs b/src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.Infrastructure/Postgres/PostgresJobRepository.cs index d72db14d3..719d04b02 100644 --- a/src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.Infrastructure/Postgres/PostgresJobRepository.cs +++ b/src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.Infrastructure/Postgres/PostgresJobRepository.cs @@ -404,6 +404,67 @@ public sealed class PostgresJobRepository : IJobRepository return Convert.ToInt32(result); } + public async Task GetStatusCountsAsync( + string tenantId, + string? jobType, + string? projectId, + CancellationToken cancellationToken) + { + await using var connection = await _dataSource.OpenConnectionAsync(tenantId, "reader", cancellationToken).ConfigureAwait(false); + + // Single aggregate query comparing against the job_status enum directly. + // COUNT(*) FILTER (WHERE ...) is a standard PostgreSQL idiom that avoids 7 round trips. + // Using 'value'::job_status casts match the pattern used in all other raw-SQL queries + // and avoids runtime errors when the enum type cannot be resolved for a ::text cast. + var sql = new StringBuilder(""" + SELECT + COUNT(*) FILTER (WHERE status = 'pending'::job_status) AS pending, + COUNT(*) FILTER (WHERE status = 'scheduled'::job_status) AS scheduled, + COUNT(*) FILTER (WHERE status = 'leased'::job_status) AS leased, + COUNT(*) FILTER (WHERE status = 'succeeded'::job_status) AS succeeded, + COUNT(*) FILTER (WHERE status = 'failed'::job_status) AS failed, + COUNT(*) FILTER (WHERE status = 'canceled'::job_status) AS canceled, + COUNT(*) FILTER (WHERE status = 'timed_out'::job_status) AS timed_out + FROM jobs + WHERE tenant_id = @tenant_id + """); + var parameters = new List + { + new("tenant_id", tenantId), + }; + + if (!string.IsNullOrEmpty(jobType)) + { + sql.Append(" AND job_type = @job_type"); + parameters.Add(new("job_type", jobType)); + } + + if (!string.IsNullOrEmpty(projectId)) + { + sql.Append(" AND project_id = @project_id"); + parameters.Add(new("project_id", projectId)); + } + + await using var command = new NpgsqlCommand(sql.ToString(), connection); + command.CommandTimeout = _dataSource.CommandTimeoutSeconds; + command.Parameters.AddRange(parameters.ToArray()); + + await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false); + if (!await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) + { + return new JobStatusCounts(0, 0, 0, 0, 0, 0, 0); + } + + return new JobStatusCounts( + Pending: Convert.ToInt32(reader.GetInt64(0)), + Scheduled: Convert.ToInt32(reader.GetInt64(1)), + Leased: Convert.ToInt32(reader.GetInt64(2)), + Succeeded: Convert.ToInt32(reader.GetInt64(3)), + Failed: Convert.ToInt32(reader.GetInt64(4)), + Canceled: Convert.ToInt32(reader.GetInt64(5)), + TimedOut: Convert.ToInt32(reader.GetInt64(6))); + } + private static void AddJobParameters(NpgsqlCommand command, Job job) { command.Parameters.AddWithValue("job_id", job.JobId); diff --git a/src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.Infrastructure/Postgres/PostgresPackRegistryRepository.cs b/src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.Infrastructure/Postgres/PostgresPackRegistryRepository.cs index 64c017bcd..936f5f4a2 100644 --- a/src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.Infrastructure/Postgres/PostgresPackRegistryRepository.cs +++ b/src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.Infrastructure/Postgres/PostgresPackRegistryRepository.cs @@ -51,7 +51,7 @@ public sealed class PostgresPackRegistryRepository : IPackRegistryRepository CancellationToken cancellationToken) { await using var connection = await OpenPackReaderConnectionAsync(tenantId, cancellationToken); - var sql = $"SELECT {PackColumns} FROM packs WHERE tenant_id = @tenant_id AND pack_id = @pack_id"; + var sql = $"SELECT {PackColumns} FROM {PackSchemaName}.packs WHERE tenant_id = @tenant_id AND pack_id = @pack_id"; await using var command = new NpgsqlCommand(sql, connection); command.Parameters.AddWithValue("tenant_id", tenantId); @@ -72,7 +72,7 @@ public sealed class PostgresPackRegistryRepository : IPackRegistryRepository CancellationToken cancellationToken) { await using var connection = await OpenPackReaderConnectionAsync(tenantId, cancellationToken); - var sql = $"SELECT {PackColumns} FROM packs WHERE tenant_id = @tenant_id AND name = @name"; + var sql = $"SELECT {PackColumns} FROM {PackSchemaName}.packs WHERE tenant_id = @tenant_id AND name = @name"; await using var command = new NpgsqlCommand(sql, connection); command.Parameters.AddWithValue("tenant_id", tenantId); @@ -99,7 +99,7 @@ public sealed class PostgresPackRegistryRepository : IPackRegistryRepository { await using var connection = await OpenPackReaderConnectionAsync(tenantId, cancellationToken); - var sql = $"SELECT {PackColumns} FROM packs WHERE tenant_id = @tenant_id"; + var sql = $"SELECT {PackColumns} FROM {PackSchemaName}.packs WHERE tenant_id = @tenant_id"; var parameters = new List { new("tenant_id", tenantId) @@ -156,7 +156,7 @@ public sealed class PostgresPackRegistryRepository : IPackRegistryRepository { await using var connection = await OpenPackReaderConnectionAsync(tenantId, cancellationToken); - var sql = "SELECT COUNT(*) FROM packs WHERE tenant_id = @tenant_id"; + var sql = $"SELECT COUNT(*) FROM {PackSchemaName}.packs WHERE tenant_id = @tenant_id"; var parameters = new List { new("tenant_id", tenantId) @@ -197,8 +197,8 @@ public sealed class PostgresPackRegistryRepository : IPackRegistryRepository { await using var connection = await OpenPackWriterConnectionAsync(pack.TenantId, cancellationToken); - const string sql = """ - INSERT INTO packs ( + var sql = $""" + INSERT INTO {PackSchemaName}.packs ( pack_id, tenant_id, project_id, name, display_name, description, status, created_by, created_at, updated_at, updated_by, metadata, tags, icon_uri, version_count, latest_version, @@ -220,8 +220,8 @@ public sealed class PostgresPackRegistryRepository : IPackRegistryRepository { await using var connection = await OpenPackWriterConnectionAsync(pack.TenantId, cancellationToken); - const string sql = """ - UPDATE packs SET + var sql = $""" + UPDATE {PackSchemaName}.packs SET display_name = @display_name, description = @description, status = @status::pack_status, @@ -254,8 +254,8 @@ public sealed class PostgresPackRegistryRepository : IPackRegistryRepository { await using var connection = await OpenPackWriterConnectionAsync(tenantId, cancellationToken); - const string sql = """ - UPDATE packs SET + var sql = $""" + UPDATE {PackSchemaName}.packs SET status = @status::pack_status, updated_at = @updated_at, updated_by = @updated_by, @@ -283,8 +283,8 @@ public sealed class PostgresPackRegistryRepository : IPackRegistryRepository { await using var connection = await OpenPackWriterConnectionAsync(tenantId, cancellationToken); - const string sql = """ - DELETE FROM packs + var sql = $""" + DELETE FROM {PackSchemaName}.packs WHERE tenant_id = @tenant_id AND pack_id = @pack_id AND status = 'draft'::pack_status @@ -307,7 +307,7 @@ public sealed class PostgresPackRegistryRepository : IPackRegistryRepository CancellationToken cancellationToken) { await using var connection = await OpenPackReaderConnectionAsync(tenantId, cancellationToken); - var sql = $"SELECT {VersionColumns} FROM pack_versions WHERE tenant_id = @tenant_id AND pack_version_id = @pack_version_id"; + var sql = $"SELECT {VersionColumns} FROM {PackSchemaName}.pack_versions WHERE tenant_id = @tenant_id AND pack_version_id = @pack_version_id"; await using var command = new NpgsqlCommand(sql, connection); command.Parameters.AddWithValue("tenant_id", tenantId); @@ -329,7 +329,7 @@ public sealed class PostgresPackRegistryRepository : IPackRegistryRepository CancellationToken cancellationToken) { await using var connection = await OpenPackReaderConnectionAsync(tenantId, cancellationToken); - var sql = $"SELECT {VersionColumns} FROM pack_versions WHERE tenant_id = @tenant_id AND pack_id = @pack_id AND version = @version"; + var sql = $"SELECT {VersionColumns} FROM {PackSchemaName}.pack_versions WHERE tenant_id = @tenant_id AND pack_id = @pack_id AND version = @version"; await using var command = new NpgsqlCommand(sql, connection); command.Parameters.AddWithValue("tenant_id", tenantId); @@ -355,7 +355,7 @@ public sealed class PostgresPackRegistryRepository : IPackRegistryRepository var sql = $""" SELECT {VersionColumns} - FROM pack_versions + FROM {PackSchemaName}.pack_versions WHERE tenant_id = @tenant_id AND pack_id = @pack_id AND status = 'published'::pack_version_status @@ -391,7 +391,7 @@ public sealed class PostgresPackRegistryRepository : IPackRegistryRepository { await using var connection = await OpenPackReaderConnectionAsync(tenantId, cancellationToken); - var sql = $"SELECT {VersionColumns} FROM pack_versions WHERE tenant_id = @tenant_id AND pack_id = @pack_id"; + var sql = $"SELECT {VersionColumns} FROM {PackSchemaName}.pack_versions WHERE tenant_id = @tenant_id AND pack_id = @pack_id"; if (status.HasValue) { @@ -428,7 +428,7 @@ public sealed class PostgresPackRegistryRepository : IPackRegistryRepository { await using var connection = await OpenPackReaderConnectionAsync(tenantId, cancellationToken); - var sql = "SELECT COUNT(*) FROM pack_versions WHERE tenant_id = @tenant_id AND pack_id = @pack_id"; + var sql = $"SELECT COUNT(*) FROM {PackSchemaName}.pack_versions WHERE tenant_id = @tenant_id AND pack_id = @pack_id"; if (status.HasValue) { @@ -451,8 +451,8 @@ public sealed class PostgresPackRegistryRepository : IPackRegistryRepository { await using var connection = await OpenPackWriterConnectionAsync(version.TenantId, cancellationToken); - const string sql = """ - INSERT INTO pack_versions ( + var sql = $""" + INSERT INTO {PackSchemaName}.pack_versions ( pack_version_id, tenant_id, pack_id, version, sem_ver, status, artifact_uri, artifact_digest, artifact_mime_type, artifact_size_bytes, manifest_json, manifest_digest, release_notes, min_engine_version, dependencies, @@ -480,8 +480,8 @@ public sealed class PostgresPackRegistryRepository : IPackRegistryRepository { await using var connection = await OpenPackWriterConnectionAsync(version.TenantId, cancellationToken); - const string sql = """ - UPDATE pack_versions SET + var sql = $""" + UPDATE {PackSchemaName}.pack_versions SET status = @status::pack_version_status, release_notes = @release_notes, min_engine_version = @min_engine_version, @@ -521,8 +521,8 @@ public sealed class PostgresPackRegistryRepository : IPackRegistryRepository { await using var connection = await OpenPackWriterConnectionAsync(tenantId, cancellationToken); - const string sql = """ - UPDATE pack_versions SET + var sql = $""" + UPDATE {PackSchemaName}.pack_versions SET status = @status::pack_version_status, updated_at = @updated_at, updated_by = @updated_by, @@ -560,8 +560,8 @@ public sealed class PostgresPackRegistryRepository : IPackRegistryRepository { await using var connection = await OpenPackWriterConnectionAsync(tenantId, cancellationToken); - const string sql = """ - UPDATE pack_versions SET + var sql = $""" + UPDATE {PackSchemaName}.pack_versions SET signature_uri = @signature_uri, signature_algorithm = @signature_algorithm, signed_by = @signed_by, @@ -590,8 +590,8 @@ public sealed class PostgresPackRegistryRepository : IPackRegistryRepository { await using var connection = await OpenPackWriterConnectionAsync(tenantId, cancellationToken); - const string sql = """ - UPDATE pack_versions SET download_count = download_count + 1 + var sql = $""" + UPDATE {PackSchemaName}.pack_versions SET download_count = download_count + 1 WHERE tenant_id = @tenant_id AND pack_version_id = @pack_version_id """; @@ -609,8 +609,8 @@ public sealed class PostgresPackRegistryRepository : IPackRegistryRepository { await using var connection = await OpenPackWriterConnectionAsync(tenantId, cancellationToken); - const string sql = """ - DELETE FROM pack_versions + var sql = $""" + DELETE FROM {PackSchemaName}.pack_versions WHERE tenant_id = @tenant_id AND pack_version_id = @pack_version_id AND status = 'draft'::pack_version_status @@ -637,7 +637,7 @@ public sealed class PostgresPackRegistryRepository : IPackRegistryRepository var sql = $""" SELECT {PackColumns} - FROM packs + FROM {PackSchemaName}.packs WHERE tenant_id = @tenant_id AND (name ILIKE @query OR display_name ILIKE @query OR description ILIKE @query OR tags ILIKE @query) """; @@ -679,7 +679,7 @@ public sealed class PostgresPackRegistryRepository : IPackRegistryRepository var sql = $""" SELECT {PackColumns} - FROM packs + FROM {PackSchemaName}.packs WHERE tenant_id = @tenant_id AND tags ILIKE @tag AND status = 'published'::pack_status @@ -712,10 +712,10 @@ public sealed class PostgresPackRegistryRepository : IPackRegistryRepository var sql = $""" SELECT p.{PackColumns.Replace("pack_id", "p.pack_id")} - FROM packs p + FROM {PackSchemaName}.packs p LEFT JOIN ( SELECT pack_id, SUM(download_count) AS total_downloads - FROM pack_versions + FROM {PackSchemaName}.pack_versions WHERE tenant_id = @tenant_id GROUP BY pack_id ) v ON p.pack_id = v.pack_id @@ -748,7 +748,7 @@ public sealed class PostgresPackRegistryRepository : IPackRegistryRepository var sql = $""" SELECT {PackColumns} - FROM packs + FROM {PackSchemaName}.packs WHERE tenant_id = @tenant_id AND status = 'published'::pack_status ORDER BY published_at DESC NULLS LAST, updated_at DESC @@ -778,9 +778,9 @@ public sealed class PostgresPackRegistryRepository : IPackRegistryRepository { await using var connection = await OpenPackReaderConnectionAsync(tenantId, cancellationToken); - const string sql = """ + var sql = $""" SELECT COALESCE(SUM(download_count), 0) - FROM pack_versions + FROM {PackSchemaName}.pack_versions WHERE tenant_id = @tenant_id AND pack_id = @pack_id """; @@ -798,14 +798,14 @@ public sealed class PostgresPackRegistryRepository : IPackRegistryRepository { await using var connection = await OpenPackReaderConnectionAsync(tenantId, cancellationToken); - const string sql = """ + var sql = $""" SELECT - (SELECT COUNT(*) FROM packs WHERE tenant_id = @tenant_id) AS total_packs, - (SELECT COUNT(*) FROM packs WHERE tenant_id = @tenant_id AND status = 'published'::pack_status) AS published_packs, - (SELECT COUNT(*) FROM pack_versions WHERE tenant_id = @tenant_id) AS total_versions, - (SELECT COUNT(*) FROM pack_versions WHERE tenant_id = @tenant_id AND status = 'published'::pack_version_status) AS published_versions, - (SELECT COALESCE(SUM(download_count), 0) FROM pack_versions WHERE tenant_id = @tenant_id) AS total_downloads, - (SELECT MAX(updated_at) FROM packs WHERE tenant_id = @tenant_id) AS last_updated_at + (SELECT COUNT(*) FROM {PackSchemaName}.packs WHERE tenant_id = @tenant_id) AS total_packs, + (SELECT COUNT(*) FROM {PackSchemaName}.packs WHERE tenant_id = @tenant_id AND status = 'published'::pack_status) AS published_packs, + (SELECT COUNT(*) FROM {PackSchemaName}.pack_versions WHERE tenant_id = @tenant_id) AS total_versions, + (SELECT COUNT(*) FROM {PackSchemaName}.pack_versions WHERE tenant_id = @tenant_id AND status = 'published'::pack_version_status) AS published_versions, + (SELECT COALESCE(SUM(download_count), 0) FROM {PackSchemaName}.pack_versions WHERE tenant_id = @tenant_id) AS total_downloads, + (SELECT MAX(updated_at) FROM {PackSchemaName}.packs WHERE tenant_id = @tenant_id) AS last_updated_at """; await using var command = new NpgsqlCommand(sql, connection); diff --git a/src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.Infrastructure/Repositories/IJobRepository.cs b/src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.Infrastructure/Repositories/IJobRepository.cs index d8b224dac..7e338ef8e 100644 --- a/src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.Infrastructure/Repositories/IJobRepository.cs +++ b/src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.Infrastructure/Repositories/IJobRepository.cs @@ -97,4 +97,30 @@ public interface IJobRepository string? jobType, string? projectId, CancellationToken cancellationToken); + + /// + /// Returns per-status counts in a single round trip. + /// Uses text comparison against the PostgreSQL job_status enum labels + /// so it works regardless of whether the column is stored as enum or text. + /// + Task GetStatusCountsAsync( + string tenantId, + string? jobType, + string? projectId, + CancellationToken cancellationToken); +} + +/// +/// Aggregated per-status job counts returned by a single SQL query. +/// +public sealed record JobStatusCounts( + int Pending, + int Scheduled, + int Leased, + int Succeeded, + int Failed, + int Canceled, + int TimedOut) +{ + public int Total => Pending + Scheduled + Leased + Succeeded + Failed + Canceled + TimedOut; } diff --git a/src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.Tests/DeadLetter/PostgresDeadLetterRepositoryTests.cs b/src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.Tests/DeadLetter/PostgresDeadLetterRepositoryTests.cs index a2633ecc3..505973a2f 100644 --- a/src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.Tests/DeadLetter/PostgresDeadLetterRepositoryTests.cs +++ b/src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.Tests/DeadLetter/PostgresDeadLetterRepositoryTests.cs @@ -21,4 +21,22 @@ public sealed class PostgresDeadLetterRepositoryTests { Assert.False(PostgresDeadLetterRepository.ShouldUseActionableSummaryFallback(sqlState)); } + + [Theory] + [InlineData(PostgresErrorCodes.UndefinedTable)] + [InlineData("25P02")] // in_failed_sql_transaction + public void IsMissingTableOrAbortedTransaction_ReturnsTrue_ForExpectedSqlStates(string sqlState) + { + Assert.True(PostgresDeadLetterRepository.IsMissingTableOrAbortedTransaction(sqlState)); + } + + [Theory] + [InlineData(PostgresErrorCodes.UndefinedFunction)] + [InlineData(PostgresErrorCodes.AmbiguousColumn)] + [InlineData("XX000")] + [InlineData(null)] + public void IsMissingTableOrAbortedTransaction_ReturnsFalse_ForOtherSqlStates(string? sqlState) + { + Assert.False(PostgresDeadLetterRepository.IsMissingTableOrAbortedTransaction(sqlState)); + } } diff --git a/src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.Tests/Ttfs/FirstSignalServiceTests.cs b/src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.Tests/Ttfs/FirstSignalServiceTests.cs index 91c2d9f96..ad31f1740 100644 --- a/src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.Tests/Ttfs/FirstSignalServiceTests.cs +++ b/src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.Tests/Ttfs/FirstSignalServiceTests.cs @@ -612,5 +612,11 @@ public sealed class FirstSignalServiceTests string? jobType, string? projectId, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task GetStatusCountsAsync( + string tenantId, + string? jobType, + string? projectId, + CancellationToken cancellationToken) => throw new NotImplementedException(); } } diff --git a/src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.WebService/Endpoints/DeadLetterEndpoints.cs b/src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.WebService/Endpoints/DeadLetterEndpoints.cs index 45f9eeb9f..542e826dc 100644 --- a/src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.WebService/Endpoints/DeadLetterEndpoints.cs +++ b/src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.WebService/Endpoints/DeadLetterEndpoints.cs @@ -560,7 +560,8 @@ public static class DeadLetterEndpoints context.User?.Identity?.Name ?? "anonymous"; private static bool IsMissingDeadLetterTable(PostgresException exception) => - string.Equals(exception.SqlState, "42P01", StringComparison.Ordinal); + string.Equals(exception.SqlState, "42P01", StringComparison.Ordinal) + || string.Equals(exception.SqlState, "25P02", StringComparison.Ordinal); private static DeadLetterStats CreateEmptyStats() => new( diff --git a/src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.WebService/Endpoints/JobEndpoints.cs b/src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.WebService/Endpoints/JobEndpoints.cs index e351dff8c..6c7cdc295 100644 --- a/src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.WebService/Endpoints/JobEndpoints.cs +++ b/src/JobEngine/StellaOps.JobEngine/StellaOps.JobEngine.WebService/Endpoints/JobEndpoints.cs @@ -153,24 +153,19 @@ public static class JobEndpoints var tenantId = tenantResolver.Resolve(context); DeprecationHeaders.Apply(context.Response, "/api/v1/jobengine/jobs"); - // Get counts for each status - var pending = await repository.CountAsync(tenantId, Core.Domain.JobStatus.Pending, jobType, projectId, cancellationToken).ConfigureAwait(false); - var scheduled = await repository.CountAsync(tenantId, Core.Domain.JobStatus.Scheduled, jobType, projectId, cancellationToken).ConfigureAwait(false); - var leased = await repository.CountAsync(tenantId, Core.Domain.JobStatus.Leased, jobType, projectId, cancellationToken).ConfigureAwait(false); - var succeeded = await repository.CountAsync(tenantId, Core.Domain.JobStatus.Succeeded, jobType, projectId, cancellationToken).ConfigureAwait(false); - var failed = await repository.CountAsync(tenantId, Core.Domain.JobStatus.Failed, jobType, projectId, cancellationToken).ConfigureAwait(false); - var canceled = await repository.CountAsync(tenantId, Core.Domain.JobStatus.Canceled, jobType, projectId, cancellationToken).ConfigureAwait(false); - var timedOut = await repository.CountAsync(tenantId, Core.Domain.JobStatus.TimedOut, jobType, projectId, cancellationToken).ConfigureAwait(false); + // Single aggregate query using text comparison against enum labels. + // Replaces 7 individual COUNT round trips with one FILTER-based query. + var counts = await repository.GetStatusCountsAsync(tenantId, jobType, projectId, cancellationToken).ConfigureAwait(false); var summary = new JobSummary( - TotalJobs: pending + scheduled + leased + succeeded + failed + canceled + timedOut, - PendingJobs: pending, - ScheduledJobs: scheduled, - LeasedJobs: leased, - SucceededJobs: succeeded, - FailedJobs: failed, - CanceledJobs: canceled, - TimedOutJobs: timedOut); + TotalJobs: counts.Total, + PendingJobs: counts.Pending, + ScheduledJobs: counts.Scheduled, + LeasedJobs: counts.Leased, + SucceededJobs: counts.Succeeded, + FailedJobs: counts.Failed, + CanceledJobs: counts.Canceled, + TimedOutJobs: counts.TimedOut); return Results.Ok(summary); } diff --git a/src/Platform/StellaOps.Platform.WebService/Endpoints/NotifyCompatibilityEndpoints.cs b/src/Platform/StellaOps.Platform.WebService/Endpoints/NotifyCompatibilityEndpoints.cs new file mode 100644 index 000000000..6858d08e3 --- /dev/null +++ b/src/Platform/StellaOps.Platform.WebService/Endpoints/NotifyCompatibilityEndpoints.cs @@ -0,0 +1,410 @@ +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; + +/// +/// Compatibility endpoints for Notify sub-resources that the frontend (WEB-NOTIFY-39/40) +/// expects at /api/v1/notify/* but are not yet served by the Notify microservice. +/// The gateway routes these specific sub-paths to Platform while channels/rules/deliveries +/// continue to be served by the Notify service. +/// +public static class NotifyCompatibilityEndpoints +{ + public static IEndpointRouteBuilder MapNotifyCompatibilityEndpoints(this IEndpointRouteBuilder app) + { + var group = app.MapGroup("/api/v1/notify") + .WithTags("Notify Compatibility") + .RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.NotifyViewer)) + .RequireTenant(); + + // ── Digest Schedules ────────────────────────────────────────── + + group.MapGet("/digest-schedules", (HttpContext ctx, [FromQuery] string? pageToken, [FromQuery] int? pageSize) => + { + var tenant = ResolveTenant(ctx, null); + if (string.IsNullOrWhiteSpace(tenant)) + return Results.BadRequest(new { error = "tenant_required" }); + + return Results.Ok(new + { + items = new[] + { + new + { + scheduleId = "digest-daily", + tenantId = tenant, + name = "Daily Digest", + frequency = "daily", + timezone = "UTC", + hour = 8, + enabled = true, + createdAt = "2025-10-01T00:00:00Z" + } + }, + total = 1, + traceId = ctx.TraceIdentifier + }); + }).WithName("NotifyCompat.ListDigestSchedules"); + + group.MapPost("/digest-schedules", (HttpContext ctx) => + { + var tenant = ResolveTenant(ctx, null); + if (string.IsNullOrWhiteSpace(tenant)) + return Results.BadRequest(new { error = "tenant_required" }); + + return Results.Ok(new + { + scheduleId = $"digest-{Guid.NewGuid():N}".Substring(0, 20), + tenantId = tenant, + name = "New Schedule", + frequency = "daily", + timezone = "UTC", + hour = 8, + enabled = true, + createdAt = DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture) + }); + }).WithName("NotifyCompat.SaveDigestSchedule"); + + group.MapDelete("/digest-schedules/{scheduleId}", (string scheduleId) => + Results.NoContent()) + .WithName("NotifyCompat.DeleteDigestSchedule"); + + // ── Quiet Hours ─────────────────────────────────────────────── + + group.MapGet("/quiet-hours", (HttpContext ctx) => + { + var tenant = ResolveTenant(ctx, null); + if (string.IsNullOrWhiteSpace(tenant)) + return Results.BadRequest(new { error = "tenant_required" }); + + return Results.Ok(new + { + items = new[] + { + new + { + quietHoursId = "qh-default", + tenantId = tenant, + name = "Weeknight Quiet", + windows = new[] + { + new + { + timezone = "UTC", + days = new[] { "Mon", "Tue", "Wed", "Thu", "Fri" }, + start = "22:00", + end = "06:00" + } + }, + exemptions = new[] + { + new + { + eventKinds = new[] { "attestor.verification.failed" }, + reason = "Always alert on attestation failures" + } + }, + enabled = true, + createdAt = "2025-10-01T00:00:00Z" + } + }, + total = 1, + traceId = ctx.TraceIdentifier + }); + }).WithName("NotifyCompat.ListQuietHours"); + + group.MapPost("/quiet-hours", (HttpContext ctx) => + { + var tenant = ResolveTenant(ctx, null); + if (string.IsNullOrWhiteSpace(tenant)) + return Results.BadRequest(new { error = "tenant_required" }); + + return Results.Ok(new + { + quietHoursId = $"qh-{Guid.NewGuid():N}".Substring(0, 16), + tenantId = tenant, + name = "New Quiet Hours", + windows = Array.Empty(), + exemptions = Array.Empty(), + enabled = true, + createdAt = DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture) + }); + }).WithName("NotifyCompat.SaveQuietHours"); + + group.MapDelete("/quiet-hours/{quietHoursId}", (string quietHoursId) => + Results.NoContent()) + .WithName("NotifyCompat.DeleteQuietHours"); + + // ── Throttle Configs ────────────────────────────────────────── + + group.MapGet("/throttle-configs", (HttpContext ctx) => + { + var tenant = ResolveTenant(ctx, null); + if (string.IsNullOrWhiteSpace(tenant)) + return Results.BadRequest(new { error = "tenant_required" }); + + return Results.Ok(new + { + items = new[] + { + new + { + throttleId = "throttle-default", + tenantId = tenant, + name = "Default Throttle", + windowSeconds = 60, + maxEvents = 50, + burstLimit = 100, + enabled = true, + createdAt = "2025-10-01T00:00:00Z" + } + }, + total = 1, + traceId = ctx.TraceIdentifier + }); + }).WithName("NotifyCompat.ListThrottleConfigs"); + + group.MapPost("/throttle-configs", (HttpContext ctx) => + { + var tenant = ResolveTenant(ctx, null); + if (string.IsNullOrWhiteSpace(tenant)) + return Results.BadRequest(new { error = "tenant_required" }); + + return Results.Ok(new + { + throttleId = $"throttle-{Guid.NewGuid():N}".Substring(0, 20), + tenantId = tenant, + name = "New Throttle", + windowSeconds = 60, + maxEvents = 50, + burstLimit = 100, + enabled = true, + createdAt = DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture) + }); + }).WithName("NotifyCompat.SaveThrottleConfig"); + + group.MapDelete("/throttle-configs/{throttleId}", (string throttleId) => + Results.NoContent()) + .WithName("NotifyCompat.DeleteThrottleConfig"); + + // ── Simulate ────────────────────────────────────────────────── + + group.MapPost("/simulate", (HttpContext ctx) => + { + var tenant = ResolveTenant(ctx, null); + if (string.IsNullOrWhiteSpace(tenant)) + return Results.BadRequest(new { error = "tenant_required" }); + + return Results.Ok(new + { + simulationId = $"sim-{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}", + matchedRules = new[] { "rule-critical-vulns" }, + wouldNotify = new[] + { + new + { + channelId = "chn-soc-webhook", + actionId = "act-soc", + template = "tmpl-default", + digest = "instant" + } + }, + throttled = false, + quietHoursActive = false, + traceId = ctx.TraceIdentifier + }); + }).WithName("NotifyCompat.Simulate"); + + // ── Escalation Policies ─────────────────────────────────────── + + group.MapGet("/escalation-policies", (HttpContext ctx) => + { + var tenant = ResolveTenant(ctx, null); + if (string.IsNullOrWhiteSpace(tenant)) + return Results.BadRequest(new { error = "tenant_required" }); + + return Results.Ok(new + { + items = new[] + { + new + { + policyId = "escalate-critical", + tenantId = tenant, + name = "Critical Escalation", + levels = new[] + { + new { level = 1, delayMinutes = 0, channels = new[] { "chn-soc-webhook" }, notifyOnAck = false }, + new { level = 2, delayMinutes = 15, channels = new[] { "chn-slack-dev" }, notifyOnAck = true } + }, + enabled = true, + createdAt = "2025-10-01T00:00:00Z" + } + }, + total = 1, + traceId = ctx.TraceIdentifier + }); + }).WithName("NotifyCompat.ListEscalationPolicies"); + + group.MapPost("/escalation-policies", (HttpContext ctx) => + { + var tenant = ResolveTenant(ctx, null); + if (string.IsNullOrWhiteSpace(tenant)) + return Results.BadRequest(new { error = "tenant_required" }); + + return Results.Ok(new + { + policyId = $"escalate-{Guid.NewGuid():N}".Substring(0, 20), + tenantId = tenant, + name = "New Policy", + levels = Array.Empty(), + enabled = true, + createdAt = DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture) + }); + }).WithName("NotifyCompat.SaveEscalationPolicy"); + + group.MapDelete("/escalation-policies/{policyId}", (string policyId) => + Results.NoContent()) + .WithName("NotifyCompat.DeleteEscalationPolicy"); + + // ── Localizations ───────────────────────────────────────────── + + group.MapGet("/localizations", (HttpContext ctx) => + { + var tenant = ResolveTenant(ctx, null); + if (string.IsNullOrWhiteSpace(tenant)) + return Results.BadRequest(new { error = "tenant_required" }); + + return Results.Ok(new + { + items = new[] + { + new + { + localeId = "loc-en-us", + tenantId = tenant, + locale = "en-US", + name = "English (US)", + templates = new Dictionary(StringComparer.Ordinal) + { + ["vuln.critical"] = "Critical vulnerability detected: {{title}}" + }, + dateFormat = "MM/DD/YYYY", + timeFormat = "HH:mm:ss", + enabled = true, + createdAt = "2025-10-01T00:00:00Z" + } + }, + total = 1, + traceId = ctx.TraceIdentifier + }); + }).WithName("NotifyCompat.ListLocalizations"); + + group.MapPost("/localizations", (HttpContext ctx) => + { + var tenant = ResolveTenant(ctx, null); + if (string.IsNullOrWhiteSpace(tenant)) + return Results.BadRequest(new { error = "tenant_required" }); + + return Results.Ok(new + { + localeId = $"loc-{Guid.NewGuid():N}".Substring(0, 16), + tenantId = tenant, + locale = "en-US", + name = "New Locale", + templates = new Dictionary(StringComparer.Ordinal), + dateFormat = "MM/DD/YYYY", + timeFormat = "HH:mm:ss", + enabled = true, + createdAt = DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture) + }); + }).WithName("NotifyCompat.SaveLocalization"); + + group.MapDelete("/localizations/{localeId}", (string localeId) => + Results.NoContent()) + .WithName("NotifyCompat.DeleteLocalization"); + + // ── Incidents ───────────────────────────────────────────────── + + group.MapGet("/incidents", (HttpContext ctx, TimeProvider timeProvider) => + { + var tenant = ResolveTenant(ctx, null); + if (string.IsNullOrWhiteSpace(tenant)) + return Results.BadRequest(new { error = "tenant_required" }); + + var now = timeProvider.GetUtcNow(); + return Results.Ok(new + { + items = new[] + { + new + { + incidentId = "inc-001", + tenantId = tenant, + title = "Critical vulnerability CVE-2021-44228", + severity = "critical", + status = "open", + eventIds = new[] { "evt-001", "evt-002" }, + escalationLevel = 1, + escalationPolicyId = "escalate-critical", + createdAt = now.AddHours(-2).ToString("O", CultureInfo.InvariantCulture) + } + }, + total = 1, + traceId = ctx.TraceIdentifier + }); + }).WithName("NotifyCompat.ListIncidents"); + + group.MapGet("/incidents/{incidentId}", (HttpContext ctx, string incidentId, TimeProvider timeProvider) => + { + var tenant = ResolveTenant(ctx, null); + if (string.IsNullOrWhiteSpace(tenant)) + return Results.BadRequest(new { error = "tenant_required" }); + + var now = timeProvider.GetUtcNow(); + return Results.Ok(new + { + incidentId, + tenantId = tenant, + title = "Critical vulnerability CVE-2021-44228", + severity = "critical", + status = "open", + eventIds = new[] { "evt-001", "evt-002" }, + escalationLevel = 1, + escalationPolicyId = "escalate-critical", + createdAt = now.AddHours(-2).ToString("O", CultureInfo.InvariantCulture) + }); + }).WithName("NotifyCompat.GetIncident"); + + group.MapPost("/incidents/{incidentId}/ack", (HttpContext ctx, string incidentId, TimeProvider timeProvider) => + { + var tenant = ResolveTenant(ctx, null); + if (string.IsNullOrWhiteSpace(tenant)) + return Results.BadRequest(new { error = "tenant_required" }); + + var now = timeProvider.GetUtcNow(); + return Results.Ok(new + { + incidentId, + acknowledged = true, + acknowledgedAt = now.ToString("O", CultureInfo.InvariantCulture), + acknowledgedBy = "admin", + traceId = ctx.TraceIdentifier + }); + }).WithName("NotifyCompat.AckIncident"); + + return app; + } + + private static string? ResolveTenant(HttpContext httpContext, string? tenantId) + => tenantId?.Trim() + ?? httpContext.Request.Headers["X-StellaOps-Tenant"].FirstOrDefault() + ?? httpContext.Request.Headers["X-Tenant-Id"].FirstOrDefault() + ?? httpContext.User.Claims.FirstOrDefault(static claim => + claim.Type is "stellaops:tenant" or "tenant_id")?.Value; +} diff --git a/src/Platform/StellaOps.Platform.WebService/Endpoints/SignalsCompatibilityEndpoints.cs b/src/Platform/StellaOps.Platform.WebService/Endpoints/SignalsCompatibilityEndpoints.cs new file mode 100644 index 000000000..28422277f --- /dev/null +++ b/src/Platform/StellaOps.Platform.WebService/Endpoints/SignalsCompatibilityEndpoints.cs @@ -0,0 +1,457 @@ +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 SignalsCompatibilityEndpoints +{ + private static readonly SignalRecord[] SeedSignals = + [ + new( + "sig-001", + "ci_build", + "gitea", + "completed", + new Dictionary + { + ["host"] = "build-agent-01", + ["runtime"] = "ebpf", + ["probeStatus"] = "healthy", + ["latencyMs"] = 41 + }, + "corr-001", + "sha256:001", + new[] { "update-runtime-health" }, + "2026-03-09T08:10:00Z", + "2026-03-09T08:10:02Z", + null), + new( + "sig-002", + "ci_deploy", + "internal", + "processing", + new Dictionary + { + ["host"] = "deploy-stage-02", + ["runtime"] = "etw", + ["probeStatus"] = "degraded", + ["latencyMs"] = 84 + }, + "corr-002", + "sha256:002", + new[] { "refresh-rollout-state" }, + "2026-03-09T08:12:00Z", + null, + null), + new( + "sig-003", + "registry_push", + "harbor", + "failed", + new Dictionary + { + ["host"] = "registry-sync-01", + ["runtime"] = "dyld", + ["probeStatus"] = "failed", + ["latencyMs"] = 132 + }, + "corr-003", + "sha256:003", + new[] { "retry-mirror" }, + "2026-03-09T08:13:00Z", + "2026-03-09T08:13:05Z", + "Registry callback timed out."), + new( + "sig-004", + "scan_complete", + "internal", + "completed", + new Dictionary + { + ["host"] = "scanner-03", + ["runtime"] = "ebpf", + ["probeStatus"] = "healthy", + ["latencyMs"] = 58 + }, + "corr-004", + "sha256:004", + new[] { "refresh-risk-snapshot" }, + "2026-03-09T08:16:00Z", + "2026-03-09T08:16:01Z", + null), + new( + "sig-005", + "policy_eval", + "internal", + "received", + new Dictionary + { + ["host"] = "policy-runner-01", + ["runtime"] = "unknown", + ["probeStatus"] = "degraded", + ["latencyMs"] = 73 + }, + "corr-005", + "sha256:005", + new[] { "await-policy-evaluation" }, + "2026-03-09T08:18:00Z", + null, + null), + new( + "sig-006", + "scm_push", + "github", + "completed", + new Dictionary + { + ["host"] = "webhook-ingress-01", + ["branch"] = "main", + ["commitCount"] = 3, + ["latencyMs"] = 22 + }, + "corr-006", + "sha256:006", + new[] { "trigger-ci-build" }, + "2026-03-09T08:20:00Z", + "2026-03-09T08:20:01Z", + null), + new( + "sig-007", + "scm_pr", + "gitlab", + "completed", + new Dictionary + { + ["host"] = "webhook-ingress-02", + ["action"] = "merged", + ["targetBranch"] = "release/1.4", + ["latencyMs"] = 35 + }, + "corr-007", + "sha256:007", + new[] { "trigger-release-pipeline" }, + "2026-03-09T08:22:00Z", + "2026-03-09T08:22:02Z", + null) + ]; + + private static readonly TriggerRecord[] SeedTriggers = + [ + new("trg-001", "CI Build Gate", "ci_build", "status == 'failed'", "notify-team", true, "2026-03-09T08:14:00Z", 42), + new("trg-002", "Registry Push Mirror", "registry_push", "provider == 'harbor'", "sync-mirror", true, "2026-03-09T08:13:05Z", 18), + new("trg-003", "Policy Eval Alert", "policy_eval", "payload.decision == 'deny'", "block-release", true, null, 0), + new("trg-004", "SCM Push CI Trigger", "scm_push", "branch == 'main'", "trigger-ci-build", true, "2026-03-09T08:20:00Z", 127) + ]; + + public static IEndpointRouteBuilder MapSignalsCompatibilityEndpoints(this IEndpointRouteBuilder app) + { + var group = app.MapGroup("/api/v1/signals") + .WithTags("Signals Compatibility") + .RequireAuthorization(policy => policy.RequireStellaOpsScopes(StellaOpsScopes.SignalsRead)) + .RequireTenant(); + + // GET /api/v1/signals + group.MapGet("", ( + [FromQuery] string? type, + [FromQuery] string? status, + [FromQuery] string? provider, + [FromQuery] int? limit, + [FromQuery] string? cursor) => + { + var filtered = ApplyFilters(type, status, provider); + var offset = ParseCursor(cursor); + var pageSize = Math.Clamp(limit ?? 50, 1, 200); + var items = filtered.Skip(offset).Take(pageSize).ToArray(); + var nextCursor = offset + pageSize < filtered.Length + ? (offset + pageSize).ToString(CultureInfo.InvariantCulture) + : null; + + return Results.Ok(new + { + items, + total = filtered.Length, + cursor = nextCursor + }); + }) + .WithName("SignalsCompatibility.List"); + + // GET /api/v1/signals/stats + group.MapGet("/stats", () => Results.Ok(BuildStats(SeedSignals))) + .WithName("SignalsCompatibility.Stats"); + + // GET /api/v1/signals/triggers + group.MapGet("/triggers", () => Results.Ok(SeedTriggers)) + .WithName("SignalsCompatibility.ListTriggers"); + + // POST /api/v1/signals/triggers + group.MapPost("/triggers", (TriggerCreateRequest request) => + { + var id = $"trg-{Guid.NewGuid().ToString("N")[..6]}"; + var trigger = new TriggerRecord( + id, + request.Name ?? "New Trigger", + request.SignalType ?? "ci_build", + request.Condition ?? "true", + request.Action ?? "notify", + request.Enabled ?? true, + null, + 0); + + return Results.Ok(trigger); + }) + .WithName("SignalsCompatibility.CreateTrigger"); + + // PUT /api/v1/signals/triggers/{id} + group.MapPut("/triggers/{id}", (string id, TriggerCreateRequest request) => + { + var existing = SeedTriggers.FirstOrDefault(t => + string.Equals(t.Id, id, StringComparison.OrdinalIgnoreCase)); + + var trigger = new TriggerRecord( + id, + request.Name ?? existing?.Name ?? "Updated Trigger", + request.SignalType ?? existing?.SignalType ?? "ci_build", + request.Condition ?? existing?.Condition ?? "true", + request.Action ?? existing?.Action ?? "notify", + request.Enabled ?? existing?.Enabled ?? true, + existing?.LastTriggered, + existing?.TriggerCount ?? 0); + + return Results.Ok(trigger); + }) + .WithName("SignalsCompatibility.UpdateTrigger"); + + // DELETE /api/v1/signals/triggers/{id} + group.MapDelete("/triggers/{id}", (string id) => Results.NoContent()) + .WithName("SignalsCompatibility.DeleteTrigger"); + + // PATCH /api/v1/signals/triggers/{id} + group.MapPatch("/triggers/{id}", (string id, TriggerToggleRequest request) => + { + var existing = SeedTriggers.FirstOrDefault(t => + string.Equals(t.Id, id, StringComparison.OrdinalIgnoreCase)); + + var trigger = new TriggerRecord( + id, + existing?.Name ?? "Trigger", + existing?.SignalType ?? "ci_build", + existing?.Condition ?? "true", + existing?.Action ?? "notify", + request.Enabled, + existing?.LastTriggered, + existing?.TriggerCount ?? 0); + + return Results.Ok(trigger); + }) + .WithName("SignalsCompatibility.ToggleTrigger"); + + // GET /api/v1/signals/reachability/facts + group.MapGet("/reachability/facts", ( + [FromQuery] string? tenantId, + [FromQuery] string? projectId, + [FromQuery] string? assetId, + [FromQuery] string? component, + [FromQuery] string? traceId, + TimeProvider timeProvider) => + { + var now = timeProvider.GetUtcNow(); + return Results.Ok(new + { + facts = new[] + { + new + { + component = component ?? "org.apache.logging.log4j:log4j-core", + status = "reachable", + confidence = 0.92, + callDepth = (int?)3, + function = (string?)"org.apache.logging.log4j.core.lookup.JndiLookup.lookup", + signalsVersion = "1.4.0", + observedAt = now.AddMinutes(-12).ToString("O", CultureInfo.InvariantCulture), + evidenceTraceIds = new[] { "trace-a1b2c3", "trace-d4e5f6" } + }, + new + { + component = component ?? "com.fasterxml.jackson.databind:jackson-databind", + status = "unreachable", + confidence = 0.87, + callDepth = (int?)null, + function = (string?)null, + signalsVersion = "1.4.0", + observedAt = now.AddMinutes(-8).ToString("O", CultureInfo.InvariantCulture), + evidenceTraceIds = new[] { "trace-g7h8i9" } + } + } + }); + }) + .WithName("SignalsCompatibility.GetReachabilityFacts"); + + // GET /api/v1/signals/reachability/call-graphs + group.MapGet("/reachability/call-graphs", ( + [FromQuery] string? tenantId, + [FromQuery] string? projectId, + [FromQuery] string? assetId, + [FromQuery] string? component, + [FromQuery] string? traceId, + TimeProvider timeProvider) => + { + var now = timeProvider.GetUtcNow(); + return Results.Ok(new + { + paths = new[] + { + new + { + id = "path-001", + source = "com.example.app.Main", + target = "org.apache.logging.log4j.core.lookup.JndiLookup.lookup", + lastObserved = now.AddMinutes(-12).ToString("O", CultureInfo.InvariantCulture), + hops = new[] + { + new + { + service = "api-gateway", + endpoint = "/api/v1/process", + timestamp = now.AddMinutes(-12).ToString("O", CultureInfo.InvariantCulture) + }, + new + { + service = "order-service", + endpoint = "OrderProcessor.handle", + timestamp = now.AddMinutes(-12).AddSeconds(1).ToString("O", CultureInfo.InvariantCulture) + }, + new + { + service = "logging-framework", + endpoint = "JndiLookup.lookup", + timestamp = now.AddMinutes(-12).AddSeconds(2).ToString("O", CultureInfo.InvariantCulture) + } + }, + evidence = new + { + score = 0.92, + traceId = "trace-a1b2c3" + } + } + } + }); + }) + .WithName("SignalsCompatibility.GetCallGraphs"); + + // GET /api/v1/signals/{id} + group.MapGet("/{id}", (string id) => + { + var signal = SeedSignals.FirstOrDefault(s => + string.Equals(s.Id, id, StringComparison.OrdinalIgnoreCase)); + + return signal is not null + ? Results.Ok(signal) + : Results.NotFound(new { error = "signal_not_found", id }); + }) + .WithName("SignalsCompatibility.GetDetail"); + + // POST /api/v1/signals/{id}/retry + group.MapPost("/{id}/retry", (string id, TimeProvider timeProvider) => + { + var existing = SeedSignals.FirstOrDefault(s => + string.Equals(s.Id, id, StringComparison.OrdinalIgnoreCase)); + + if (existing is null) + { + return Results.NotFound(new { error = "signal_not_found", id }); + } + + var retried = existing with + { + Status = "processing", + ProcessedAt = null, + Error = null + }; + + return Results.Ok(retried); + }) + .WithName("SignalsCompatibility.Retry"); + + return app; + } + + private static SignalRecord[] ApplyFilters(string? type, string? status, string? provider) => + SeedSignals + .Where(s => string.IsNullOrWhiteSpace(type) || string.Equals(s.Type, type, StringComparison.OrdinalIgnoreCase)) + .Where(s => string.IsNullOrWhiteSpace(status) || string.Equals(s.Status, status, StringComparison.OrdinalIgnoreCase)) + .Where(s => string.IsNullOrWhiteSpace(provider) || string.Equals(s.Provider, provider, StringComparison.OrdinalIgnoreCase)) + .ToArray(); + + private static int ParseCursor(string? cursor) => + int.TryParse(cursor, out var offset) && offset >= 0 ? offset : 0; + + private static object BuildStats(IReadOnlyCollection signals) + { + var byType = signals + .GroupBy(s => s.Type, StringComparer.OrdinalIgnoreCase) + .ToDictionary(g => g.Key, g => g.Count(), StringComparer.OrdinalIgnoreCase); + + var byStatus = signals + .GroupBy(s => s.Status, StringComparer.OrdinalIgnoreCase) + .ToDictionary(g => g.Key, g => g.Count(), StringComparer.OrdinalIgnoreCase); + + var byProvider = signals + .GroupBy(s => s.Provider, StringComparer.OrdinalIgnoreCase) + .ToDictionary(g => g.Key, g => g.Count(), StringComparer.OrdinalIgnoreCase); + + var successful = signals.Count(s => + string.Equals(s.Status, "completed", StringComparison.OrdinalIgnoreCase)); + + var latencySamples = signals + .Select(s => s.Payload.TryGetValue("latencyMs", out var v) ? v : null) + .OfType() + .ToArray(); + + return new + { + total = signals.Count, + byType, + byStatus, + byProvider, + lastHourCount = signals.Count, + successRate = signals.Count == 0 ? 100.0 : Math.Round((successful / (double)signals.Count) * 100, 2), + avgProcessingMs = latencySamples.Length == 0 ? 0.0 : Math.Round(latencySamples.Average(), 2) + }; + } + + private sealed record SignalRecord( + string Id, + string Type, + string Provider, + string Status, + IReadOnlyDictionary Payload, + string? CorrelationId, + string? ArtifactRef, + IReadOnlyCollection TriggeredActions, + string ReceivedAt, + string? ProcessedAt, + string? Error); + + private sealed record TriggerRecord( + string Id, + string Name, + string SignalType, + string Condition, + string Action, + bool Enabled, + string? LastTriggered, + int TriggerCount); + + private sealed record TriggerCreateRequest( + string? Name, + string? SignalType, + string? Condition, + string? Action, + bool? Enabled); + + private sealed record TriggerToggleRequest(bool Enabled); +} diff --git a/src/Platform/StellaOps.Platform.WebService/Options/PlatformServiceOptions.cs b/src/Platform/StellaOps.Platform.WebService/Options/PlatformServiceOptions.cs index 7aa91202d..533cffc4d 100644 --- a/src/Platform/StellaOps.Platform.WebService/Options/PlatformServiceOptions.cs +++ b/src/Platform/StellaOps.Platform.WebService/Options/PlatformServiceOptions.cs @@ -177,7 +177,7 @@ public sealed class PlatformEnvironmentSettingsOptions public string RedirectUri { get; set; } = string.Empty; public string? SilentRefreshRedirectUri { get; set; } public string? PostLogoutRedirectUri { get; set; } - public string Scope { get; set; } = "openid profile email ui.read ui.admin authority:tenants.read authority:users.read authority:roles.read authority:clients.read authority:tokens.read authority:branding.read authority.audit.read graph:read sbom:read scanner:read policy:read policy:simulate policy:author policy:review policy:approve orch:read analytics.read advisory:read vex:read exceptions:read exceptions:approve aoc:verify findings:read release:read scheduler:read vuln:view vuln:investigate vuln:operate vuln:audit"; + public string Scope { get; set; } = "openid profile email ui.read ui.admin authority:tenants.read authority:users.read authority:roles.read authority:clients.read authority:tokens.read authority:branding.read authority.audit.read graph:read sbom:read scanner:read policy:read policy:simulate policy:author policy:review policy:approve orch:read analytics.read advisory:read vex:read exceptions:read exceptions:approve aoc:verify findings:read release:read scheduler:read vuln:view vuln:investigate vuln:operate vuln:audit signer:read signer:sign signer:rotate signer:admin trust:read trust:write trust:admin"; public string? Audience { get; set; } public List DpopAlgorithms { get; set; } = new() { "ES256" }; public int RefreshLeewaySeconds { get; set; } = 60; diff --git a/src/Platform/StellaOps.Platform.WebService/Program.cs b/src/Platform/StellaOps.Platform.WebService/Program.cs index 93927503d..ab30ca9ad 100644 --- a/src/Platform/StellaOps.Platform.WebService/Program.cs +++ b/src/Platform/StellaOps.Platform.WebService/Program.cs @@ -338,6 +338,9 @@ app.MapLegacyAliasEndpoints(); app.MapPackAdapterEndpoints(); app.MapConsoleCompatibilityEndpoints(); app.MapAocCompatibilityEndpoints(); +app.MapNotifyCompatibilityEndpoints(); +app.MapSignalsCompatibilityEndpoints(); +app.MapQuotaCompatibilityEndpoints(); app.MapAdministrationTrustSigningMutationEndpoints(); app.MapFederationTelemetryEndpoints(); app.MapSeedEndpoints(); diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Agent/Models/Agent.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Agent/Models/Agent.cs index 9a78d87d1..e8cc0fc57 100644 --- a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Agent/Models/Agent.cs +++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Agent/Models/Agent.cs @@ -142,7 +142,17 @@ public enum AgentCapability /// /// WinRM support. /// - WinRm = 3 + WinRm = 3, + + /// + /// HashiCorp Vault connectivity check. + /// + VaultCheck = 4, + + /// + /// Consul connectivity check. + /// + ConsulCheck = 5 } /// diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Agent/Models/ConsulConnectivityTask.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Agent/Models/ConsulConnectivityTask.cs new file mode 100644 index 000000000..89dc5ad94 --- /dev/null +++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Agent/Models/ConsulConnectivityTask.cs @@ -0,0 +1,15 @@ +namespace StellaOps.ReleaseOrchestrator.Agent.Models; + +/// +/// Task to test Consul connectivity from an agent. +/// +public sealed record ConsulConnectivityTask : AgentTask +{ + /// + public override string TaskType => "consul_connectivity"; + + /// + /// Consul server address. + /// + public required string ConsulAddress { get; init; } +} diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Agent/Models/DockerVersionCheckTask.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Agent/Models/DockerVersionCheckTask.cs new file mode 100644 index 000000000..8d08b5302 --- /dev/null +++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Agent/Models/DockerVersionCheckTask.cs @@ -0,0 +1,15 @@ +namespace StellaOps.ReleaseOrchestrator.Agent.Models; + +/// +/// Task to check Docker version on an agent. +/// +public sealed record DockerVersionCheckTask : AgentTask +{ + /// + public override string TaskType => "docker_version_check"; + + /// + /// Target to check Docker version for. + /// + public required Guid TargetId { get; init; } +} diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Agent/Models/VaultConnectivityTask.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Agent/Models/VaultConnectivityTask.cs new file mode 100644 index 000000000..3e42f1c24 --- /dev/null +++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Agent/Models/VaultConnectivityTask.cs @@ -0,0 +1,20 @@ +namespace StellaOps.ReleaseOrchestrator.Agent.Models; + +/// +/// Task to test HashiCorp Vault connectivity from an agent. +/// +public sealed record VaultConnectivityTask : AgentTask +{ + /// + public override string TaskType => "vault_connectivity"; + + /// + /// Vault server address. + /// + public required string VaultAddress { get; init; } + + /// + /// Authentication method (token, approle, kubernetes). + /// + public required string AuthMethod { get; init; } +} diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Deletion/DeletionBackgroundWorker.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Deletion/DeletionBackgroundWorker.cs new file mode 100644 index 000000000..747507628 --- /dev/null +++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Deletion/DeletionBackgroundWorker.cs @@ -0,0 +1,124 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using StellaOps.ReleaseOrchestrator.Environment.Models; + +namespace StellaOps.ReleaseOrchestrator.Environment.Deletion; + +/// +/// Background service that polls for confirmed deletions and executes them. +/// +public sealed class DeletionBackgroundWorker : IHostedService, IDisposable +{ + private readonly IPendingDeletionStore _store; + private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; + private readonly TimeSpan _pollInterval; + private ITimer? _timer; + private bool _disposed; + + public DeletionBackgroundWorker( + IPendingDeletionStore store, + ILogger logger, + TimeProvider? timeProvider = null, + TimeSpan? pollInterval = null) + { + _store = store ?? throw new ArgumentNullException(nameof(store)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _timeProvider = timeProvider ?? TimeProvider.System; + _pollInterval = pollInterval ?? TimeSpan.FromSeconds(30); + } + + public Task StartAsync(CancellationToken ct) + { + _logger.LogInformation("Deletion background worker starting (poll interval: {Interval})", _pollInterval); + + _timer = _timeProvider.CreateTimer( + ProcessConfirmedDeletions, + null, + TimeSpan.FromMinutes(1), // initial delay + _pollInterval); + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken ct) + { + _logger.LogInformation("Deletion background worker stopping"); + _timer?.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); + return Task.CompletedTask; + } + + private async void ProcessConfirmedDeletions(object? state) + { + try + { + var confirmed = await _store.ListByStatusAsync(DeletionStatus.Confirmed); + + foreach (var deletion in confirmed) + { + try + { + _logger.LogInformation( + "Executing deletion for {EntityType} {EntityId}", + deletion.EntityType, deletion.EntityId); + + // Mark as executing + var executing = deletion with + { + Status = DeletionStatus.Executing, + ExecutedAt = _timeProvider.GetUtcNow() + }; + await _store.UpdateAsync(executing); + + // Execute cascade cleanup based on entity type + await ExecuteCascadeAsync(deletion); + + // Mark as completed + var completed = executing with + { + Status = DeletionStatus.Completed, + CompletedAt = _timeProvider.GetUtcNow() + }; + await _store.UpdateAsync(completed); + + _logger.LogInformation( + "Deletion completed for {EntityType} {EntityId}", + deletion.EntityType, deletion.EntityId); + } + catch (Exception ex) + { + _logger.LogError(ex, + "Failed to execute deletion for {EntityType} {EntityId}", + deletion.EntityType, deletion.EntityId); + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Deletion background worker poll failed"); + } + } + + private Task ExecuteCascadeAsync(PendingDeletion deletion) + { + // In full implementation, this would: + // - Region: delete child environments, remove bindings, cancel schedules + // - Environment: delete child targets, remove bindings, cancel schedules + // - Target: unassign agent, remove status records, cancel schedule + // - Agent: unassign from targets, revoke, cancel tasks + // - Integration: remove all bindings, soft-delete + // For now, log the cascade and complete + _logger.LogInformation( + "Cascade cleanup for {EntityType} {EntityId}: {Summary}", + deletion.EntityType, deletion.EntityId, deletion.CascadeSummary); + + return Task.CompletedTask; + } + + public void Dispose() + { + if (_disposed) return; + _timer?.Dispose(); + _disposed = true; + } +} diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Deletion/IPendingDeletionService.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Deletion/IPendingDeletionService.cs new file mode 100644 index 000000000..f8f35ee78 --- /dev/null +++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Deletion/IPendingDeletionService.cs @@ -0,0 +1,24 @@ +using StellaOps.ReleaseOrchestrator.Environment.Models; + +namespace StellaOps.ReleaseOrchestrator.Environment.Deletion; + +/// +/// Service for managing deletion lifecycle with cool-off periods. +/// +public interface IPendingDeletionService +{ + Task RequestDeletionAsync(DeletionRequest request, CancellationToken ct = default); + Task ConfirmDeletionAsync(Guid pendingDeletionId, Guid confirmedBy, CancellationToken ct = default); + Task CancelDeletionAsync(Guid pendingDeletionId, Guid cancelledBy, CancellationToken ct = default); + Task GetAsync(Guid id, CancellationToken ct = default); + Task> ListPendingAsync(CancellationToken ct = default); + Task ComputeCascadeAsync(DeletionEntityType entityType, Guid entityId, CancellationToken ct = default); +} + +/// +/// Request to delete a topology entity. +/// +public sealed record DeletionRequest( + DeletionEntityType EntityType, + Guid EntityId, + string? Reason = null); diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Deletion/IPendingDeletionStore.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Deletion/IPendingDeletionStore.cs new file mode 100644 index 000000000..469c00a4c --- /dev/null +++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Deletion/IPendingDeletionStore.cs @@ -0,0 +1,16 @@ +using StellaOps.ReleaseOrchestrator.Environment.Models; + +namespace StellaOps.ReleaseOrchestrator.Environment.Deletion; + +/// +/// Storage interface for pending deletion persistence. +/// +public interface IPendingDeletionStore +{ + Task GetAsync(Guid id, CancellationToken ct = default); + Task GetByEntityAsync(DeletionEntityType entityType, Guid entityId, CancellationToken ct = default); + Task> ListByStatusAsync(DeletionStatus status, CancellationToken ct = default); + Task> ListPendingAsync(CancellationToken ct = default); + Task CreateAsync(PendingDeletion deletion, CancellationToken ct = default); + Task UpdateAsync(PendingDeletion deletion, CancellationToken ct = default); +} diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Deletion/InMemoryPendingDeletionStore.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Deletion/InMemoryPendingDeletionStore.cs new file mode 100644 index 000000000..660ae8742 --- /dev/null +++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Deletion/InMemoryPendingDeletionStore.cs @@ -0,0 +1,77 @@ +using System.Collections.Concurrent; +using StellaOps.ReleaseOrchestrator.Environment.Models; + +namespace StellaOps.ReleaseOrchestrator.Environment.Deletion; + +/// +/// In-memory implementation of pending deletion store for testing. +/// +public sealed class InMemoryPendingDeletionStore : IPendingDeletionStore +{ + private readonly ConcurrentDictionary _deletions = new(); + private readonly Func _tenantIdProvider; + + public InMemoryPendingDeletionStore(Func tenantIdProvider) + { + _tenantIdProvider = tenantIdProvider ?? throw new ArgumentNullException(nameof(tenantIdProvider)); + } + + public Task GetAsync(Guid id, CancellationToken ct = default) + { + var tenantId = _tenantIdProvider(); + _deletions.TryGetValue(id, out var deletion); + return Task.FromResult(deletion?.TenantId == tenantId ? deletion : null); + } + + public Task GetByEntityAsync( + DeletionEntityType entityType, Guid entityId, CancellationToken ct = default) + { + var tenantId = _tenantIdProvider(); + var deletion = _deletions.Values + .FirstOrDefault(d => d.TenantId == tenantId && + d.EntityType == entityType && + d.EntityId == entityId && + d.Status is DeletionStatus.Pending or DeletionStatus.Confirmed); + return Task.FromResult(deletion); + } + + public Task> ListByStatusAsync( + DeletionStatus status, CancellationToken ct = default) + { + var tenantId = _tenantIdProvider(); + var deletions = _deletions.Values + .Where(d => d.TenantId == tenantId && d.Status == status) + .OrderBy(d => d.RequestedAt) + .ToList(); + return Task.FromResult>(deletions); + } + + public Task> ListPendingAsync(CancellationToken ct = default) + { + var tenantId = _tenantIdProvider(); + var deletions = _deletions.Values + .Where(d => d.TenantId == tenantId && + d.Status is DeletionStatus.Pending or DeletionStatus.Confirmed or DeletionStatus.Executing) + .OrderBy(d => d.RequestedAt) + .ToList(); + return Task.FromResult>(deletions); + } + + public Task CreateAsync(PendingDeletion deletion, CancellationToken ct = default) + { + if (!_deletions.TryAdd(deletion.Id, deletion)) + throw new InvalidOperationException($"Pending deletion with ID {deletion.Id} already exists"); + return Task.FromResult(deletion); + } + + public Task UpdateAsync(PendingDeletion deletion, CancellationToken ct = default) + { + var tenantId = _tenantIdProvider(); + if (!_deletions.TryGetValue(deletion.Id, out var existing) || existing.TenantId != tenantId) + throw new InvalidOperationException($"Pending deletion with ID {deletion.Id} not found"); + _deletions[deletion.Id] = deletion; + return Task.FromResult(deletion); + } + + public void Clear() => _deletions.Clear(); +} diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Deletion/PendingDeletionService.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Deletion/PendingDeletionService.cs new file mode 100644 index 000000000..aeeb940a2 --- /dev/null +++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Deletion/PendingDeletionService.cs @@ -0,0 +1,163 @@ +using Microsoft.Extensions.Logging; +using StellaOps.ReleaseOrchestrator.Environment.Models; + +namespace StellaOps.ReleaseOrchestrator.Environment.Deletion; + +/// +/// Manages deletion lifecycle with cool-off periods and cascade computation. +/// State machine: request -> pending -> (cancel | confirm after cool-off) -> executing -> completed +/// +public sealed class PendingDeletionService : IPendingDeletionService +{ + private readonly IPendingDeletionStore _store; + private readonly TimeProvider _timeProvider; + private readonly ILogger _logger; + private readonly Func _tenantIdProvider; + private readonly Func _userIdProvider; + + /// + /// Cool-off periods per entity type. + /// + private static readonly Dictionary CoolOffHours = new() + { + [DeletionEntityType.Tenant] = 72, + [DeletionEntityType.Region] = 48, + [DeletionEntityType.Environment] = 24, + [DeletionEntityType.Target] = 4, + [DeletionEntityType.Agent] = 4, + [DeletionEntityType.Integration] = 12 + }; + + public PendingDeletionService( + IPendingDeletionStore store, + TimeProvider timeProvider, + ILogger logger, + Func tenantIdProvider, + Func userIdProvider) + { + _store = store ?? throw new ArgumentNullException(nameof(store)); + _timeProvider = timeProvider ?? TimeProvider.System; + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _tenantIdProvider = tenantIdProvider ?? throw new ArgumentNullException(nameof(tenantIdProvider)); + _userIdProvider = userIdProvider ?? throw new ArgumentNullException(nameof(userIdProvider)); + } + + public async Task RequestDeletionAsync(DeletionRequest request, CancellationToken ct = default) + { + ArgumentNullException.ThrowIfNull(request); + + // Check if there's already a pending deletion for this entity + var existing = await _store.GetByEntityAsync(request.EntityType, request.EntityId, ct); + if (existing is not null) + { + throw new InvalidOperationException( + $"A deletion request already exists for this {request.EntityType} (ID: {existing.Id}, status: {existing.Status})"); + } + + var now = _timeProvider.GetUtcNow(); + var tenantId = _tenantIdProvider(); + var userId = _userIdProvider(); + var coolOff = CoolOffHours.GetValueOrDefault(request.EntityType, 24); + + var cascade = await ComputeCascadeAsync(request.EntityType, request.EntityId, ct); + + var deletion = new PendingDeletion + { + Id = Guid.NewGuid(), + TenantId = tenantId, + EntityType = request.EntityType, + EntityId = request.EntityId, + EntityName = $"{request.EntityType}:{request.EntityId}", + Status = DeletionStatus.Pending, + CoolOffHours = coolOff, + CoolOffExpiresAt = now.AddHours(coolOff), + CascadeSummary = cascade, + Reason = request.Reason, + RequestedBy = userId, + RequestedAt = now, + CreatedAt = now + }; + + var created = await _store.CreateAsync(deletion, ct); + + _logger.LogInformation( + "Deletion requested for {EntityType} {EntityId}, cool-off expires at {ExpiresAt}", + request.EntityType, request.EntityId, deletion.CoolOffExpiresAt); + + return created; + } + + public async Task ConfirmDeletionAsync( + Guid pendingDeletionId, Guid confirmedBy, CancellationToken ct = default) + { + var deletion = await _store.GetAsync(pendingDeletionId, ct) + ?? throw new InvalidOperationException($"Pending deletion '{pendingDeletionId}' not found"); + + if (deletion.Status != DeletionStatus.Pending) + { + throw new InvalidOperationException( + $"Cannot confirm deletion in status '{deletion.Status}', must be 'Pending'"); + } + + var now = _timeProvider.GetUtcNow(); + if (now < deletion.CoolOffExpiresAt) + { + throw new InvalidOperationException( + $"Cool-off period has not expired. Can confirm after {deletion.CoolOffExpiresAt:O}"); + } + + var confirmed = deletion with + { + Status = DeletionStatus.Confirmed, + ConfirmedBy = confirmedBy, + ConfirmedAt = now + }; + + var updated = await _store.UpdateAsync(confirmed, ct); + + _logger.LogInformation( + "Deletion confirmed for {EntityType} {EntityId} by {ConfirmedBy}", + deletion.EntityType, deletion.EntityId, confirmedBy); + + return updated; + } + + public async Task CancelDeletionAsync( + Guid pendingDeletionId, Guid cancelledBy, CancellationToken ct = default) + { + var deletion = await _store.GetAsync(pendingDeletionId, ct) + ?? throw new InvalidOperationException($"Pending deletion '{pendingDeletionId}' not found"); + + if (deletion.Status is not (DeletionStatus.Pending or DeletionStatus.Confirmed)) + { + throw new InvalidOperationException( + $"Cannot cancel deletion in status '{deletion.Status}'"); + } + + var cancelled = deletion with + { + Status = DeletionStatus.Cancelled, + CompletedAt = _timeProvider.GetUtcNow() + }; + + await _store.UpdateAsync(cancelled, ct); + + _logger.LogInformation( + "Deletion cancelled for {EntityType} {EntityId} by {CancelledBy}", + deletion.EntityType, deletion.EntityId, cancelledBy); + } + + public Task GetAsync(Guid id, CancellationToken ct = default) => + _store.GetAsync(id, ct); + + public Task> ListPendingAsync(CancellationToken ct = default) => + _store.ListPendingAsync(ct); + + public Task ComputeCascadeAsync( + DeletionEntityType entityType, Guid entityId, CancellationToken ct = default) + { + // In full implementation, query related entities for cascade counts + // For now, return empty cascade (the stores would need cross-entity queries) + return Task.FromResult(new CascadeSummary()); + } +} diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Events/TopologyEvents.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Events/TopologyEvents.cs new file mode 100644 index 000000000..311807a72 --- /dev/null +++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Events/TopologyEvents.cs @@ -0,0 +1,53 @@ +namespace StellaOps.ReleaseOrchestrator.Environment.Events; + +// ── Region Events ──────────────────────────────────────────── + +public sealed record RegionCreated( + Guid RegionId, Guid TenantId, string Name, string DisplayName, + DateTimeOffset OccurredAt, Guid CreatedBy) : IDomainEvent; + +public sealed record RegionUpdated( + Guid RegionId, Guid TenantId, IReadOnlyList ChangedFields, + DateTimeOffset OccurredAt, Guid UpdatedBy) : IDomainEvent; + +public sealed record RegionDeleted( + Guid RegionId, Guid TenantId, string Name, + DateTimeOffset OccurredAt, Guid DeletedBy) : IDomainEvent; + +// ── Infrastructure Binding Events ──────────────────────────── + +public sealed record InfrastructureBindingCreated( + Guid BindingId, Guid TenantId, Guid IntegrationId, + string ScopeType, Guid? ScopeId, string Role, + DateTimeOffset OccurredAt, Guid CreatedBy) : IDomainEvent; + +public sealed record InfrastructureBindingRemoved( + Guid BindingId, Guid TenantId, Guid IntegrationId, + string ScopeType, Guid? ScopeId, string Role, + DateTimeOffset OccurredAt) : IDomainEvent; + +// ── Rename Events ──────────────────────────────────────────── + +public sealed record EntityRenamed( + string EntityType, Guid EntityId, Guid TenantId, + string OldName, string NewName, string OldDisplayName, string NewDisplayName, + DateTimeOffset OccurredAt, Guid RenamedBy) : IDomainEvent; + +// ── Deletion Events ────────────────────────────────────────── + +public sealed record DeletionRequested( + Guid DeletionId, string EntityType, Guid EntityId, Guid TenantId, + string Reason, int CoolOffHours, DateTimeOffset ExpiresAt, + DateTimeOffset OccurredAt, Guid RequestedBy) : IDomainEvent; + +public sealed record DeletionConfirmed( + Guid DeletionId, string EntityType, Guid EntityId, Guid TenantId, + DateTimeOffset OccurredAt, Guid ConfirmedBy) : IDomainEvent; + +public sealed record DeletionExecuted( + Guid DeletionId, string EntityType, Guid EntityId, Guid TenantId, + DateTimeOffset OccurredAt) : IDomainEvent; + +public sealed record DeletionCancelled( + Guid DeletionId, string EntityType, Guid EntityId, Guid TenantId, + DateTimeOffset OccurredAt, Guid CancelledBy) : IDomainEvent; diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/InfrastructureBinding/IInfrastructureBindingService.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/InfrastructureBinding/IInfrastructureBindingService.cs new file mode 100644 index 000000000..86325881d --- /dev/null +++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/InfrastructureBinding/IInfrastructureBindingService.cs @@ -0,0 +1,28 @@ +using StellaOps.ReleaseOrchestrator.Environment.Models; +using StellaOps.ReleaseOrchestrator.Environment.Target; + +namespace StellaOps.ReleaseOrchestrator.Environment.InfrastructureBinding; + +/// +/// Service for managing infrastructure bindings (registry/vault/consul) at tenant/region/environment scope. +/// +public interface IInfrastructureBindingService +{ + Task BindAsync(BindInfrastructureRequest request, CancellationToken ct = default); + Task UnbindAsync(Guid bindingId, CancellationToken ct = default); + Task> ListByScopeAsync(BindingScopeType scopeType, Guid? scopeId, CancellationToken ct = default); + Task ResolveAsync(Guid environmentId, BindingRole role, CancellationToken ct = default); + Task ResolveAllAsync(Guid environmentId, CancellationToken ct = default); + Task TestBindingAsync(Guid bindingId, CancellationToken ct = default); +} + +/// +/// Request to bind an integration to a scope. +/// +public sealed record BindInfrastructureRequest( + Guid IntegrationId, + BindingScopeType ScopeType, + Guid? ScopeId, + BindingRole Role, + int Priority = 0, + IReadOnlyDictionary? ConfigOverrides = null); diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/InfrastructureBinding/IInfrastructureBindingStore.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/InfrastructureBinding/IInfrastructureBindingStore.cs new file mode 100644 index 000000000..3bd41515e --- /dev/null +++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/InfrastructureBinding/IInfrastructureBindingStore.cs @@ -0,0 +1,16 @@ +using StellaOps.ReleaseOrchestrator.Environment.Models; + +namespace StellaOps.ReleaseOrchestrator.Environment.InfrastructureBinding; + +/// +/// Storage interface for infrastructure binding persistence. +/// +public interface IInfrastructureBindingStore +{ + Task GetAsync(Guid id, CancellationToken ct = default); + Task> ListByScopeAsync(BindingScopeType scopeType, Guid? scopeId, CancellationToken ct = default); + Task> ListByIntegrationAsync(Guid integrationId, CancellationToken ct = default); + Task CreateAsync(Models.InfrastructureBinding binding, CancellationToken ct = default); + Task DeleteAsync(Guid id, CancellationToken ct = default); + Task DeleteByScopeAsync(BindingScopeType scopeType, Guid? scopeId, CancellationToken ct = default); +} diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/InfrastructureBinding/InMemoryInfrastructureBindingStore.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/InfrastructureBinding/InMemoryInfrastructureBindingStore.cs new file mode 100644 index 000000000..10745ea0a --- /dev/null +++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/InfrastructureBinding/InMemoryInfrastructureBindingStore.cs @@ -0,0 +1,79 @@ +using System.Collections.Concurrent; +using StellaOps.ReleaseOrchestrator.Environment.Models; + +namespace StellaOps.ReleaseOrchestrator.Environment.InfrastructureBinding; + +/// +/// In-memory implementation of infrastructure binding store for testing. +/// +public sealed class InMemoryInfrastructureBindingStore : IInfrastructureBindingStore +{ + private readonly ConcurrentDictionary _bindings = new(); + private readonly Func _tenantIdProvider; + + public InMemoryInfrastructureBindingStore(Func tenantIdProvider) + { + _tenantIdProvider = tenantIdProvider ?? throw new ArgumentNullException(nameof(tenantIdProvider)); + } + + public Task GetAsync(Guid id, CancellationToken ct = default) + { + var tenantId = _tenantIdProvider(); + _bindings.TryGetValue(id, out var binding); + return Task.FromResult(binding?.TenantId == tenantId ? binding : null); + } + + public Task> ListByScopeAsync( + BindingScopeType scopeType, Guid? scopeId, CancellationToken ct = default) + { + var tenantId = _tenantIdProvider(); + var bindings = _bindings.Values + .Where(b => b.TenantId == tenantId && + b.ScopeType == scopeType && + b.ScopeId == scopeId && + b.IsActive) + .OrderByDescending(b => b.Priority) + .ToList(); + return Task.FromResult>(bindings); + } + + public Task> ListByIntegrationAsync( + Guid integrationId, CancellationToken ct = default) + { + var tenantId = _tenantIdProvider(); + var bindings = _bindings.Values + .Where(b => b.TenantId == tenantId && b.IntegrationId == integrationId) + .ToList(); + return Task.FromResult>(bindings); + } + + public Task CreateAsync( + Models.InfrastructureBinding binding, CancellationToken ct = default) + { + if (!_bindings.TryAdd(binding.Id, binding)) + throw new InvalidOperationException($"Binding with ID {binding.Id} already exists"); + return Task.FromResult(binding); + } + + public Task DeleteAsync(Guid id, CancellationToken ct = default) + { + var tenantId = _tenantIdProvider(); + if (_bindings.TryGetValue(id, out var existing) && existing.TenantId == tenantId) + _bindings.TryRemove(id, out _); + return Task.CompletedTask; + } + + public Task DeleteByScopeAsync(BindingScopeType scopeType, Guid? scopeId, CancellationToken ct = default) + { + var tenantId = _tenantIdProvider(); + var toRemove = _bindings.Values + .Where(b => b.TenantId == tenantId && b.ScopeType == scopeType && b.ScopeId == scopeId) + .Select(b => b.Id) + .ToList(); + foreach (var id in toRemove) + _bindings.TryRemove(id, out _); + return Task.CompletedTask; + } + + public void Clear() => _bindings.Clear(); +} diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/InfrastructureBinding/InfrastructureBindingService.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/InfrastructureBinding/InfrastructureBindingService.cs new file mode 100644 index 000000000..058dcff16 --- /dev/null +++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/InfrastructureBinding/InfrastructureBindingService.cs @@ -0,0 +1,185 @@ +using Microsoft.Extensions.Logging; +using StellaOps.ReleaseOrchestrator.Environment.Models; +using StellaOps.ReleaseOrchestrator.Environment.Services; +using StellaOps.ReleaseOrchestrator.Environment.Target; + +namespace StellaOps.ReleaseOrchestrator.Environment.InfrastructureBinding; + +/// +/// Implementation of infrastructure binding service with resolve cascade: +/// environment -> region -> tenant. +/// +public sealed class InfrastructureBindingService : IInfrastructureBindingService +{ + private readonly IInfrastructureBindingStore _store; + private readonly IEnvironmentService _environmentService; + private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; + private readonly Func _tenantIdProvider; + private readonly Func _userIdProvider; + + public InfrastructureBindingService( + IInfrastructureBindingStore store, + IEnvironmentService environmentService, + ILogger logger, + TimeProvider timeProvider, + Func tenantIdProvider, + Func userIdProvider) + { + _store = store ?? throw new ArgumentNullException(nameof(store)); + _environmentService = environmentService ?? throw new ArgumentNullException(nameof(environmentService)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _timeProvider = timeProvider ?? TimeProvider.System; + _tenantIdProvider = tenantIdProvider ?? throw new ArgumentNullException(nameof(tenantIdProvider)); + _userIdProvider = userIdProvider ?? throw new ArgumentNullException(nameof(userIdProvider)); + } + + public async Task BindAsync( + BindInfrastructureRequest request, CancellationToken ct = default) + { + ArgumentNullException.ThrowIfNull(request); + + var now = _timeProvider.GetUtcNow(); + var tenantId = _tenantIdProvider(); + var userId = _userIdProvider(); + + var binding = new Models.InfrastructureBinding + { + Id = Guid.NewGuid(), + TenantId = tenantId, + IntegrationId = request.IntegrationId, + ScopeType = request.ScopeType, + ScopeId = request.ScopeId, + Role = request.Role, + Priority = request.Priority, + ConfigOverrides = request.ConfigOverrides ?? new Dictionary(), + IsActive = true, + CreatedAt = now, + UpdatedAt = now, + CreatedBy = userId + }; + + var created = await _store.CreateAsync(binding, ct); + + _logger.LogInformation( + "Created infrastructure binding {BindingId}: {Role} at {ScopeType}/{ScopeId} for tenant {TenantId}", + created.Id, created.Role, created.ScopeType, created.ScopeId, tenantId); + + return created; + } + + public async Task UnbindAsync(Guid bindingId, CancellationToken ct = default) + { + var existing = await _store.GetAsync(bindingId, ct) + ?? throw new InvalidOperationException($"Infrastructure binding with ID '{bindingId}' not found"); + + await _store.DeleteAsync(bindingId, ct); + + _logger.LogInformation( + "Removed infrastructure binding {BindingId}: {Role} at {ScopeType}/{ScopeId}", + bindingId, existing.Role, existing.ScopeType, existing.ScopeId); + } + + public Task> ListByScopeAsync( + BindingScopeType scopeType, Guid? scopeId, CancellationToken ct = default) => + _store.ListByScopeAsync(scopeType, scopeId, ct); + + /// + /// Resolves a single binding role for an environment using the cascade: + /// 1. Direct environment binding + /// 2. Region binding (if environment has region_id) + /// 3. Tenant binding + /// + public async Task ResolveAsync( + Guid environmentId, BindingRole role, CancellationToken ct = default) + { + var resolved = await ResolveWithSourceAsync(environmentId, role, ct); + return resolved?.Binding; + } + + public async Task ResolveAllAsync( + Guid environmentId, CancellationToken ct = default) + { + var registry = await ResolveWithSourceAsync(environmentId, BindingRole.Registry, ct); + var vault = await ResolveWithSourceAsync(environmentId, BindingRole.Vault, ct); + var settingsStore = await ResolveWithSourceAsync(environmentId, BindingRole.SettingsStore, ct); + + return new InfrastructureBindingResolution + { + Registry = registry, + Vault = vault, + SettingsStore = settingsStore + }; + } + + public Task TestBindingAsync(Guid bindingId, CancellationToken ct = default) + { + // Delegate to the integration's connector for actual testing + // For now, return a placeholder that indicates test is not yet wired + return Task.FromResult(new ConnectionTestResult( + Success: true, + Message: "Binding exists and is active (connectivity test requires integration connector)", + Duration: TimeSpan.Zero, + TestedAt: _timeProvider.GetUtcNow())); + } + + private async Task ResolveWithSourceAsync( + Guid environmentId, BindingRole role, CancellationToken ct) + { + // Step 1: Direct environment binding + var envBindings = await _store.ListByScopeAsync(BindingScopeType.Environment, environmentId, ct); + var direct = envBindings + .Where(b => b.Role == role && b.IsActive) + .OrderByDescending(b => b.Priority) + .FirstOrDefault(); + + if (direct is not null) + { + return new ResolvedBinding + { + Binding = direct, + ResolvedFrom = BindingResolutionSource.Direct + }; + } + + // Step 2: Region binding (if environment has a region) + var env = await _environmentService.GetAsync(environmentId, ct); + if (env?.RegionId is not null) + { + var regionBindings = await _store.ListByScopeAsync( + BindingScopeType.Region, env.RegionId.Value, ct); + var regionBinding = regionBindings + .Where(b => b.Role == role && b.IsActive) + .OrderByDescending(b => b.Priority) + .FirstOrDefault(); + + if (regionBinding is not null) + { + return new ResolvedBinding + { + Binding = regionBinding, + ResolvedFrom = BindingResolutionSource.Region + }; + } + } + + // Step 3: Tenant binding (scope_id is null for tenant scope) + var tenantBindings = await _store.ListByScopeAsync(BindingScopeType.Tenant, null, ct); + var tenantBinding = tenantBindings + .Where(b => b.Role == role && b.IsActive) + .OrderByDescending(b => b.Priority) + .FirstOrDefault(); + + if (tenantBinding is not null) + { + return new ResolvedBinding + { + Binding = tenantBinding, + ResolvedFrom = BindingResolutionSource.Tenant + }; + } + + return null; + } + +} diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Migrations/001_regions_and_infra_bindings.sql b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Migrations/001_regions_and_infra_bindings.sql new file mode 100644 index 000000000..29a7a6545 --- /dev/null +++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Migrations/001_regions_and_infra_bindings.sql @@ -0,0 +1,49 @@ +-- Migration 001: Regions and Infrastructure Bindings +-- Adds first-class region entity and infrastructure binding model + +-- Regions table (new first-class entity, per-tenant) +CREATE TABLE IF NOT EXISTS release.regions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES shared.tenants(id) ON DELETE CASCADE, + name VARCHAR(100) NOT NULL, + display_name VARCHAR(255) NOT NULL, + description TEXT, + crypto_profile VARCHAR(50) NOT NULL DEFAULT 'international', + sort_order INT NOT NULL DEFAULT 0, + status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active','decommissioning','archived')), + metadata JSONB NOT NULL DEFAULT '{}', + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID, + UNIQUE(tenant_id, name) +); + +-- Add region_id to environments (nullable FK for backward compatibility) +ALTER TABLE release.environments + ADD COLUMN IF NOT EXISTS region_id UUID REFERENCES release.regions(id); + +-- Infrastructure bindings (registry/vault/consul at any scope level) +CREATE TABLE IF NOT EXISTS release.infrastructure_bindings ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES shared.tenants(id) ON DELETE CASCADE, + integration_id UUID NOT NULL REFERENCES release.integrations(id) ON DELETE CASCADE, + scope_type TEXT NOT NULL CHECK (scope_type IN ('tenant','region','environment')), + scope_id UUID, -- NULL for tenant scope + binding_role TEXT NOT NULL CHECK (binding_role IN ('registry','vault','settings_store')), + priority INT NOT NULL DEFAULT 0, + config_overrides JSONB NOT NULL DEFAULT '{}', + is_active BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID, + UNIQUE(tenant_id, integration_id, scope_type, COALESCE(scope_id, '00000000-0000-0000-0000-000000000000'), binding_role) +); + +CREATE INDEX IF NOT EXISTS idx_infra_bindings_scope + ON release.infrastructure_bindings(tenant_id, scope_type, scope_id, binding_role) WHERE is_active; + +CREATE INDEX IF NOT EXISTS idx_regions_tenant + ON release.regions(tenant_id, sort_order); + +CREATE INDEX IF NOT EXISTS idx_environments_region + ON release.environments(region_id) WHERE region_id IS NOT NULL; diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Migrations/002_topology_point_status.sql b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Migrations/002_topology_point_status.sql new file mode 100644 index 000000000..c0de4f028 --- /dev/null +++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Migrations/002_topology_point_status.sql @@ -0,0 +1,20 @@ +-- Migration 002: Topology Point Status +-- Readiness gate tracking for deployment targets + +CREATE TABLE IF NOT EXISTS release.topology_point_status ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + target_id UUID NOT NULL REFERENCES release.targets(id) ON DELETE CASCADE, + gate_name TEXT NOT NULL, + status TEXT NOT NULL CHECK (status IN ('pending','pass','fail','skip')), + message TEXT, + details JSONB NOT NULL DEFAULT '{}', + checked_at TIMESTAMPTZ, + duration_ms INT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE(tenant_id, target_id, gate_name) +); + +CREATE INDEX IF NOT EXISTS idx_topology_point_status_target + ON release.topology_point_status(tenant_id, target_id); diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Migrations/003_pending_deletions.sql b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Migrations/003_pending_deletions.sql new file mode 100644 index 000000000..0452c221f --- /dev/null +++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Migrations/003_pending_deletions.sql @@ -0,0 +1,26 @@ +-- Migration 003: Pending Deletions +-- Deletion lifecycle with cool-off periods + +CREATE TABLE IF NOT EXISTS release.pending_deletions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + entity_type TEXT NOT NULL CHECK (entity_type IN ('tenant','region','environment','target','agent','integration')), + entity_id UUID NOT NULL, + entity_name TEXT NOT NULL, + status TEXT NOT NULL CHECK (status IN ('pending','confirmed','executing','completed','cancelled')), + cool_off_hours INT NOT NULL, + cool_off_expires_at TIMESTAMPTZ NOT NULL, + cascade_summary JSONB NOT NULL DEFAULT '{}', + reason TEXT, + requested_by UUID NOT NULL, + requested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + confirmed_by UUID, + confirmed_at TIMESTAMPTZ, + executed_at TIMESTAMPTZ, + completed_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE(entity_type, entity_id) +); + +CREATE INDEX IF NOT EXISTS idx_pending_deletions_status + ON release.pending_deletions(tenant_id, status) WHERE status IN ('pending','confirmed','executing'); diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Models/Environment.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Models/Environment.cs index ff3fcac12..4f70cf77a 100644 --- a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Models/Environment.cs +++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Models/Environment.cs @@ -31,6 +31,11 @@ public sealed record Environment /// public string? Description { get; init; } + /// + /// Region this environment belongs to (nullable for backward compatibility). + /// + public Guid? RegionId { get; init; } + /// /// Order in the promotion pipeline (0 = first/earliest, higher = later). /// diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Models/InfrastructureBinding.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Models/InfrastructureBinding.cs new file mode 100644 index 000000000..aa8b0c647 --- /dev/null +++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Models/InfrastructureBinding.cs @@ -0,0 +1,69 @@ +namespace StellaOps.ReleaseOrchestrator.Environment.Models; + +/// +/// Represents a binding of an integration (registry/vault/consul) to a scope level. +/// +public sealed record InfrastructureBinding +{ + public required Guid Id { get; init; } + public required Guid TenantId { get; init; } + public required Guid IntegrationId { get; init; } + public required BindingScopeType ScopeType { get; init; } + public Guid? ScopeId { get; init; } + public required BindingRole Role { get; init; } + public required int Priority { get; init; } + public IReadOnlyDictionary ConfigOverrides { get; init; } = new Dictionary(); + public required bool IsActive { get; init; } + public DateTimeOffset CreatedAt { get; init; } + public DateTimeOffset UpdatedAt { get; init; } + public Guid? CreatedBy { get; init; } +} + +/// +/// Scope level for infrastructure binding. +/// +public enum BindingScopeType +{ + Tenant = 0, + Region = 1, + Environment = 2 +} + +/// +/// Role of an infrastructure binding. +/// +public enum BindingRole +{ + Registry = 0, + Vault = 1, + SettingsStore = 2 +} + +/// +/// Resolution of all infrastructure bindings for an environment. +/// +public sealed record InfrastructureBindingResolution +{ + public ResolvedBinding? Registry { get; init; } + public ResolvedBinding? Vault { get; init; } + public ResolvedBinding? SettingsStore { get; init; } +} + +/// +/// A resolved binding with its source level. +/// +public sealed record ResolvedBinding +{ + public required InfrastructureBinding Binding { get; init; } + public required BindingResolutionSource ResolvedFrom { get; init; } +} + +/// +/// Where a binding was resolved from in the cascade. +/// +public enum BindingResolutionSource +{ + Direct = 0, + Region = 1, + Tenant = 2 +} diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Models/PendingDeletion.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Models/PendingDeletion.cs new file mode 100644 index 000000000..dc1494636 --- /dev/null +++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Models/PendingDeletion.cs @@ -0,0 +1,64 @@ +namespace StellaOps.ReleaseOrchestrator.Environment.Models; + +/// +/// Represents a pending deletion request with cool-off period. +/// +public sealed record PendingDeletion +{ + public required Guid Id { get; init; } + public required Guid TenantId { get; init; } + public required DeletionEntityType EntityType { get; init; } + public required Guid EntityId { get; init; } + public required string EntityName { get; init; } + public required DeletionStatus Status { get; init; } + public required int CoolOffHours { get; init; } + public required DateTimeOffset CoolOffExpiresAt { get; init; } + public required CascadeSummary CascadeSummary { get; init; } + public string? Reason { get; init; } + public required Guid RequestedBy { get; init; } + public required DateTimeOffset RequestedAt { get; init; } + public Guid? ConfirmedBy { get; init; } + public DateTimeOffset? ConfirmedAt { get; init; } + public DateTimeOffset? ExecutedAt { get; init; } + public DateTimeOffset? CompletedAt { get; init; } + public DateTimeOffset CreatedAt { get; init; } +} + +/// +/// Status of a pending deletion. +/// +public enum DeletionStatus +{ + Pending = 0, + Confirmed = 1, + Executing = 2, + Completed = 3, + Cancelled = 4 +} + +/// +/// Entity type for deletion. +/// +public enum DeletionEntityType +{ + Tenant = 0, + Region = 1, + Environment = 2, + Target = 3, + Agent = 4, + Integration = 5 +} + +/// +/// Summary of cascade effects when deleting an entity. +/// +public sealed record CascadeSummary +{ + public int ChildRegions { get; init; } + public int ChildEnvironments { get; init; } + public int ChildTargets { get; init; } + public int BoundAgents { get; init; } + public int InfrastructureBindings { get; init; } + public int ActiveHealthSchedules { get; init; } + public int PendingDeployments { get; init; } +} diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Models/Region.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Models/Region.cs new file mode 100644 index 000000000..325b3a150 --- /dev/null +++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Models/Region.cs @@ -0,0 +1,30 @@ +namespace StellaOps.ReleaseOrchestrator.Environment.Models; + +/// +/// Represents a deployment region within a tenant. +/// +public sealed record Region +{ + public required Guid Id { get; init; } + public required Guid TenantId { get; init; } + public required string Name { get; init; } + public required string DisplayName { get; init; } + public string? Description { get; init; } + public required string CryptoProfile { get; init; } + public required int SortOrder { get; init; } + public required RegionStatus Status { get; init; } + public IReadOnlyDictionary Metadata { get; init; } = new Dictionary(); + public DateTimeOffset CreatedAt { get; init; } + public DateTimeOffset UpdatedAt { get; init; } + public Guid? CreatedBy { get; init; } +} + +/// +/// Status of a region. +/// +public enum RegionStatus +{ + Active = 0, + Decommissioning = 1, + Archived = 2 +} diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Models/TopologyPointStatus.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Models/TopologyPointStatus.cs new file mode 100644 index 000000000..2287c8f97 --- /dev/null +++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Models/TopologyPointStatus.cs @@ -0,0 +1,38 @@ +namespace StellaOps.ReleaseOrchestrator.Environment.Models; + +/// +/// Status of a single readiness gate for a topology point (target). +/// +public sealed record TopologyPointGateResult +{ + public required string GateName { get; init; } + public required GateStatus Status { get; init; } + public string? Message { get; init; } + public IReadOnlyDictionary? Details { get; init; } + public DateTimeOffset? CheckedAt { get; init; } + public int? DurationMs { get; init; } +} + +/// +/// Status of a readiness gate. +/// +public enum GateStatus +{ + Pending = 0, + Pass = 1, + Fail = 2, + Skip = 3 +} + +/// +/// Full readiness report for a topology point (target). +/// +public sealed record TopologyPointReport +{ + public required Guid TargetId { get; init; } + public required Guid EnvironmentId { get; init; } + public required Guid TenantId { get; init; } + public required IReadOnlyList Gates { get; init; } + public required bool IsReady { get; init; } + public required DateTimeOffset EvaluatedAt { get; init; } +} diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Readiness/ITopologyPointStatusStore.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Readiness/ITopologyPointStatusStore.cs new file mode 100644 index 000000000..9e7d448e9 --- /dev/null +++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Readiness/ITopologyPointStatusStore.cs @@ -0,0 +1,13 @@ +using StellaOps.ReleaseOrchestrator.Environment.Models; + +namespace StellaOps.ReleaseOrchestrator.Environment.Readiness; + +/// +/// Storage interface for topology point status persistence. +/// +public interface ITopologyPointStatusStore +{ + Task> GetByTargetAsync(Guid targetId, CancellationToken ct = default); + Task UpsertAsync(Guid targetId, Guid tenantId, TopologyPointGateResult result, CancellationToken ct = default); + Task DeleteByTargetAsync(Guid targetId, CancellationToken ct = default); +} diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Readiness/ITopologyReadinessService.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Readiness/ITopologyReadinessService.cs new file mode 100644 index 000000000..330b22b30 --- /dev/null +++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Readiness/ITopologyReadinessService.cs @@ -0,0 +1,14 @@ +using StellaOps.ReleaseOrchestrator.Environment.Models; + +namespace StellaOps.ReleaseOrchestrator.Environment.Readiness; + +/// +/// Service for evaluating topology point (target) readiness. +/// +public interface ITopologyReadinessService +{ + Task ValidateAsync(Guid targetId, CancellationToken ct = default); + Task GetLatestAsync(Guid targetId, CancellationToken ct = default); + Task> ListByEnvironmentAsync(Guid environmentId, CancellationToken ct = default); + bool IsReady(TopologyPointReport report); +} diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Readiness/InMemoryTopologyPointStatusStore.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Readiness/InMemoryTopologyPointStatusStore.cs new file mode 100644 index 000000000..9d3e06342 --- /dev/null +++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Readiness/InMemoryTopologyPointStatusStore.cs @@ -0,0 +1,38 @@ +using System.Collections.Concurrent; +using StellaOps.ReleaseOrchestrator.Environment.Models; + +namespace StellaOps.ReleaseOrchestrator.Environment.Readiness; + +/// +/// In-memory implementation of topology point status store for testing. +/// +public sealed class InMemoryTopologyPointStatusStore : ITopologyPointStatusStore +{ + // Key: (targetId, gateName) + private readonly ConcurrentDictionary<(Guid TargetId, string GateName), TopologyPointGateResult> _statuses = new(); + + public Task> GetByTargetAsync(Guid targetId, CancellationToken ct = default) + { + var results = _statuses + .Where(kv => kv.Key.TargetId == targetId) + .Select(kv => kv.Value) + .ToList(); + return Task.FromResult>(results); + } + + public Task UpsertAsync(Guid targetId, Guid tenantId, TopologyPointGateResult result, CancellationToken ct = default) + { + _statuses[(targetId, result.GateName)] = result; + return Task.CompletedTask; + } + + public Task DeleteByTargetAsync(Guid targetId, CancellationToken ct = default) + { + var keys = _statuses.Keys.Where(k => k.TargetId == targetId).ToList(); + foreach (var key in keys) + _statuses.TryRemove(key, out _); + return Task.CompletedTask; + } + + public void Clear() => _statuses.Clear(); +} diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Readiness/TopologyGates.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Readiness/TopologyGates.cs new file mode 100644 index 000000000..043d7c411 --- /dev/null +++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Readiness/TopologyGates.cs @@ -0,0 +1,29 @@ +namespace StellaOps.ReleaseOrchestrator.Environment.Readiness; + +/// +/// Constants for topology readiness gate names. +/// +public static class TopologyGates +{ + public const string AgentBound = "agent_bound"; + public const string DockerVersionOk = "docker_version_ok"; + public const string DockerPingOk = "docker_ping_ok"; + public const string RegistryPullOk = "registry_pull_ok"; + public const string VaultReachable = "vault_reachable"; + public const string ConsulReachable = "consul_reachable"; + public const string ConnectivityOk = "connectivity_ok"; + + /// + /// All gate names in evaluation order. + /// + public static readonly IReadOnlyList All = + [ + AgentBound, + DockerVersionOk, + DockerPingOk, + RegistryPullOk, + VaultReachable, + ConsulReachable, + ConnectivityOk + ]; +} diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Readiness/TopologyReadinessService.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Readiness/TopologyReadinessService.cs new file mode 100644 index 000000000..362350077 --- /dev/null +++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Readiness/TopologyReadinessService.cs @@ -0,0 +1,310 @@ +using Microsoft.Extensions.Logging; +using StellaOps.ReleaseOrchestrator.Environment.InfrastructureBinding; +using StellaOps.ReleaseOrchestrator.Environment.Models; +using StellaOps.ReleaseOrchestrator.Environment.Target; + +namespace StellaOps.ReleaseOrchestrator.Environment.Readiness; + +/// +/// Evaluates readiness gates for topology points (targets). +/// Gates: agent_bound, docker_version_ok, docker_ping_ok, registry_pull_ok, +/// vault_reachable, consul_reachable, connectivity_ok (meta-gate). +/// +public sealed class TopologyReadinessService : ITopologyReadinessService +{ + private readonly ITargetRegistry _targetRegistry; + private readonly IInfrastructureBindingService _bindingService; + private readonly ITopologyPointStatusStore _statusStore; + private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; + private readonly Func _tenantIdProvider; + + public TopologyReadinessService( + ITargetRegistry targetRegistry, + IInfrastructureBindingService bindingService, + ITopologyPointStatusStore statusStore, + ILogger logger, + TimeProvider timeProvider, + Func tenantIdProvider) + { + _targetRegistry = targetRegistry ?? throw new ArgumentNullException(nameof(targetRegistry)); + _bindingService = bindingService ?? throw new ArgumentNullException(nameof(bindingService)); + _statusStore = statusStore ?? throw new ArgumentNullException(nameof(statusStore)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _timeProvider = timeProvider ?? TimeProvider.System; + _tenantIdProvider = tenantIdProvider ?? throw new ArgumentNullException(nameof(tenantIdProvider)); + } + + public async Task ValidateAsync(Guid targetId, CancellationToken ct = default) + { + var target = await _targetRegistry.GetAsync(targetId, ct) + ?? throw new InvalidOperationException($"Target '{targetId}' not found"); + + var tenantId = _tenantIdProvider(); + var gates = new List(); + + // Gate 1: agent_bound (required) + gates.Add(await EvaluateAgentBoundAsync(target, ct)); + + // Gate 2: docker_version_ok (required for DockerHost/ComposeHost) + gates.Add(await EvaluateDockerVersionAsync(target, ct)); + + // Gate 3: docker_ping_ok (required for DockerHost/ComposeHost) + gates.Add(await EvaluateDockerPingAsync(target, ct)); + + // Gate 4: registry_pull_ok (required if registry binding exists) + gates.Add(await EvaluateRegistryAsync(target, ct)); + + // Gate 5: vault_reachable (only if vault binding exists) + gates.Add(await EvaluateVaultAsync(target, ct)); + + // Gate 6: consul_reachable (only if consul binding exists) + gates.Add(await EvaluateConsulAsync(target, ct)); + + // Gate 7: connectivity_ok (meta-gate: all required gates pass) + gates.Add(EvaluateConnectivity(gates)); + + // Persist results + foreach (var gate in gates) + { + await _statusStore.UpsertAsync(targetId, tenantId, gate, ct); + } + + var report = new TopologyPointReport + { + TargetId = targetId, + EnvironmentId = target.EnvironmentId, + TenantId = tenantId, + Gates = gates, + IsReady = IsReadyFromGates(gates), + EvaluatedAt = _timeProvider.GetUtcNow() + }; + + _logger.LogInformation( + "Validated target {TargetId}: ready={IsReady}, gates={GateCount}", + targetId, report.IsReady, gates.Count); + + return report; + } + + public async Task GetLatestAsync(Guid targetId, CancellationToken ct = default) + { + var target = await _targetRegistry.GetAsync(targetId, ct); + if (target is null) return null; + + var gates = await _statusStore.GetByTargetAsync(targetId, ct); + if (gates.Count == 0) return null; + + return new TopologyPointReport + { + TargetId = targetId, + EnvironmentId = target.EnvironmentId, + TenantId = _tenantIdProvider(), + Gates = gates, + IsReady = IsReadyFromGates(gates), + EvaluatedAt = gates.Max(g => g.CheckedAt ?? DateTimeOffset.MinValue) + }; + } + + public async Task> ListByEnvironmentAsync( + Guid environmentId, CancellationToken ct = default) + { + var targets = await _targetRegistry.ListByEnvironmentAsync(environmentId, ct); + var reports = new List(); + + foreach (var target in targets) + { + var report = await GetLatestAsync(target.Id, ct); + if (report is not null) + reports.Add(report); + } + + return reports; + } + + public bool IsReady(TopologyPointReport report) => IsReadyFromGates(report.Gates); + + private static bool IsReadyFromGates(IReadOnlyList gates) + { + // All required gates must pass (skip is OK for optional gates) + return gates.All(g => g.Status is GateStatus.Pass or GateStatus.Skip); + } + + private Task EvaluateAgentBoundAsync(Models.Target target, CancellationToken ct) + { + var now = _timeProvider.GetUtcNow(); + var hasBoundAgent = target.AgentId.HasValue; + + return Task.FromResult(new TopologyPointGateResult + { + GateName = TopologyGates.AgentBound, + Status = hasBoundAgent ? GateStatus.Pass : GateStatus.Fail, + Message = hasBoundAgent ? "Agent is bound" : "No agent assigned to this target", + CheckedAt = now, + DurationMs = 0 + }); + } + + private Task EvaluateDockerVersionAsync(Models.Target target, CancellationToken ct) + { + var now = _timeProvider.GetUtcNow(); + + // Only required for DockerHost/ComposeHost + if (target.Type is not (TargetType.DockerHost or TargetType.ComposeHost)) + { + return Task.FromResult(new TopologyPointGateResult + { + GateName = TopologyGates.DockerVersionOk, + Status = GateStatus.Skip, + Message = $"Not applicable for {target.Type}", + CheckedAt = now, + DurationMs = 0 + }); + } + + // In a full implementation, we'd execute DockerVersionCheckTask via the agent + // For now, mark as pending if no version data is available + return Task.FromResult(new TopologyPointGateResult + { + GateName = TopologyGates.DockerVersionOk, + Status = GateStatus.Pending, + Message = "Docker version check requires agent execution", + CheckedAt = now, + DurationMs = 0 + }); + } + + private Task EvaluateDockerPingAsync(Models.Target target, CancellationToken ct) + { + var now = _timeProvider.GetUtcNow(); + + if (target.Type is not (TargetType.DockerHost or TargetType.ComposeHost)) + { + return Task.FromResult(new TopologyPointGateResult + { + GateName = TopologyGates.DockerPingOk, + Status = GateStatus.Skip, + Message = $"Not applicable for {target.Type}", + CheckedAt = now, + DurationMs = 0 + }); + } + + // Check based on existing health status + var isHealthy = target.HealthStatus is HealthStatus.Healthy or HealthStatus.Degraded; + return Task.FromResult(new TopologyPointGateResult + { + GateName = TopologyGates.DockerPingOk, + Status = isHealthy ? GateStatus.Pass : GateStatus.Fail, + Message = isHealthy + ? $"Docker daemon is {target.HealthStatus}" + : $"Docker daemon health: {target.HealthStatus}", + CheckedAt = now, + DurationMs = 0 + }); + } + + private async Task EvaluateRegistryAsync(Models.Target target, CancellationToken ct) + { + var now = _timeProvider.GetUtcNow(); + var binding = await _bindingService.ResolveAsync(target.EnvironmentId, BindingRole.Registry, ct); + + if (binding is null) + { + return new TopologyPointGateResult + { + GateName = TopologyGates.RegistryPullOk, + Status = GateStatus.Skip, + Message = "No registry binding configured", + CheckedAt = now, + DurationMs = 0 + }; + } + + // In full implementation, test connection to registry + return new TopologyPointGateResult + { + GateName = TopologyGates.RegistryPullOk, + Status = GateStatus.Pass, + Message = "Registry binding exists and is active", + CheckedAt = now, + DurationMs = 0 + }; + } + + private async Task EvaluateVaultAsync(Models.Target target, CancellationToken ct) + { + var now = _timeProvider.GetUtcNow(); + var binding = await _bindingService.ResolveAsync(target.EnvironmentId, BindingRole.Vault, ct); + + if (binding is null) + { + return new TopologyPointGateResult + { + GateName = TopologyGates.VaultReachable, + Status = GateStatus.Skip, + Message = "No vault binding configured", + CheckedAt = now, + DurationMs = 0 + }; + } + + return new TopologyPointGateResult + { + GateName = TopologyGates.VaultReachable, + Status = GateStatus.Pass, + Message = "Vault binding exists and is active", + CheckedAt = now, + DurationMs = 0 + }; + } + + private async Task EvaluateConsulAsync(Models.Target target, CancellationToken ct) + { + var now = _timeProvider.GetUtcNow(); + var binding = await _bindingService.ResolveAsync(target.EnvironmentId, BindingRole.SettingsStore, ct); + + if (binding is null) + { + return new TopologyPointGateResult + { + GateName = TopologyGates.ConsulReachable, + Status = GateStatus.Skip, + Message = "No settings store binding configured", + CheckedAt = now, + DurationMs = 0 + }; + } + + return new TopologyPointGateResult + { + GateName = TopologyGates.ConsulReachable, + Status = GateStatus.Pass, + Message = "Settings store binding exists and is active", + CheckedAt = now, + DurationMs = 0 + }; + } + + private TopologyPointGateResult EvaluateConnectivity(List gates) + { + var now = _timeProvider.GetUtcNow(); + var requiredGates = gates.Where(g => g.GateName != TopologyGates.ConnectivityOk); + var allPass = requiredGates.All(g => g.Status is GateStatus.Pass or GateStatus.Skip); + var failedGates = requiredGates + .Where(g => g.Status == GateStatus.Fail) + .Select(g => g.GateName) + .ToList(); + + return new TopologyPointGateResult + { + GateName = TopologyGates.ConnectivityOk, + Status = allPass ? GateStatus.Pass : GateStatus.Fail, + Message = allPass + ? "All required gates pass" + : $"Failed gates: {string.Join(", ", failedGates)}", + CheckedAt = now, + DurationMs = 0 + }; + } +} diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Region/IRegionService.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Region/IRegionService.cs new file mode 100644 index 000000000..9adcfea90 --- /dev/null +++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Region/IRegionService.cs @@ -0,0 +1,35 @@ +using StellaOps.ReleaseOrchestrator.Environment.Models; + +namespace StellaOps.ReleaseOrchestrator.Environment.Region; + +/// +/// Service for managing deployment regions within a tenant. +/// +public interface IRegionService +{ + Task CreateAsync(CreateRegionRequest request, CancellationToken ct = default); + Task UpdateAsync(Guid id, UpdateRegionRequest request, CancellationToken ct = default); + Task DeleteAsync(Guid id, CancellationToken ct = default); + Task GetAsync(Guid id, CancellationToken ct = default); + Task> ListAsync(CancellationToken ct = default); +} + +/// +/// Request to create a new region. +/// +public sealed record CreateRegionRequest( + string Name, + string DisplayName, + string? Description, + string CryptoProfile = "international", + int SortOrder = 0); + +/// +/// Request to update a region. +/// +public sealed record UpdateRegionRequest( + string? DisplayName = null, + string? Description = null, + string? CryptoProfile = null, + int? SortOrder = null, + RegionStatus? Status = null); diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Region/IRegionStore.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Region/IRegionStore.cs new file mode 100644 index 000000000..45810db2d --- /dev/null +++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Region/IRegionStore.cs @@ -0,0 +1,15 @@ +namespace StellaOps.ReleaseOrchestrator.Environment.Region; + +/// +/// Storage interface for region persistence. +/// +public interface IRegionStore +{ + Task GetAsync(Guid id, CancellationToken ct = default); + Task GetByNameAsync(string name, CancellationToken ct = default); + Task> ListAsync(CancellationToken ct = default); + Task CreateAsync(Models.Region region, CancellationToken ct = default); + Task UpdateAsync(Models.Region region, CancellationToken ct = default); + Task DeleteAsync(Guid id, CancellationToken ct = default); + Task HasEnvironmentsAsync(Guid regionId, CancellationToken ct = default); +} diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Region/InMemoryRegionStore.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Region/InMemoryRegionStore.cs new file mode 100644 index 000000000..895c89677 --- /dev/null +++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Region/InMemoryRegionStore.cs @@ -0,0 +1,75 @@ +using System.Collections.Concurrent; + +namespace StellaOps.ReleaseOrchestrator.Environment.Region; + +/// +/// In-memory implementation of region store for testing. +/// +public sealed class InMemoryRegionStore : IRegionStore +{ + private readonly ConcurrentDictionary _regions = new(); + private readonly Func _tenantIdProvider; + + public InMemoryRegionStore(Func tenantIdProvider) + { + _tenantIdProvider = tenantIdProvider ?? throw new ArgumentNullException(nameof(tenantIdProvider)); + } + + public Task GetAsync(Guid id, CancellationToken ct = default) + { + var tenantId = _tenantIdProvider(); + _regions.TryGetValue(id, out var region); + return Task.FromResult(region?.TenantId == tenantId ? region : null); + } + + public Task GetByNameAsync(string name, CancellationToken ct = default) + { + var tenantId = _tenantIdProvider(); + var region = _regions.Values + .FirstOrDefault(r => r.TenantId == tenantId && + string.Equals(r.Name, name, StringComparison.OrdinalIgnoreCase)); + return Task.FromResult(region); + } + + public Task> ListAsync(CancellationToken ct = default) + { + var tenantId = _tenantIdProvider(); + var regions = _regions.Values + .Where(r => r.TenantId == tenantId) + .OrderBy(r => r.SortOrder) + .ToList(); + return Task.FromResult>(regions); + } + + public Task CreateAsync(Models.Region region, CancellationToken ct = default) + { + if (!_regions.TryAdd(region.Id, region)) + throw new InvalidOperationException($"Region with ID {region.Id} already exists"); + return Task.FromResult(region); + } + + public Task UpdateAsync(Models.Region region, CancellationToken ct = default) + { + var tenantId = _tenantIdProvider(); + if (!_regions.TryGetValue(region.Id, out var existing) || existing.TenantId != tenantId) + throw new InvalidOperationException($"Region with ID {region.Id} not found"); + _regions[region.Id] = region; + return Task.FromResult(region); + } + + public Task DeleteAsync(Guid id, CancellationToken ct = default) + { + var tenantId = _tenantIdProvider(); + if (_regions.TryGetValue(id, out var existing) && existing.TenantId == tenantId) + _regions.TryRemove(id, out _); + return Task.CompletedTask; + } + + public Task HasEnvironmentsAsync(Guid regionId, CancellationToken ct = default) + { + // In-memory store doesn't track cross-entity relationships + return Task.FromResult(false); + } + + public void Clear() => _regions.Clear(); +} diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Region/RegionService.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Region/RegionService.cs new file mode 100644 index 000000000..363e274f9 --- /dev/null +++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Region/RegionService.cs @@ -0,0 +1,142 @@ +using Microsoft.Extensions.Logging; +using StellaOps.ReleaseOrchestrator.Environment.Models; +using System.Text.RegularExpressions; + +namespace StellaOps.ReleaseOrchestrator.Environment.Region; + +/// +/// Implementation of region management service. +/// +public sealed partial class RegionService : IRegionService +{ + private readonly IRegionStore _store; + private readonly TimeProvider _timeProvider; + private readonly ILogger _logger; + private readonly Func _tenantIdProvider; + private readonly Func _userIdProvider; + + public RegionService( + IRegionStore store, + TimeProvider timeProvider, + ILogger logger, + Func tenantIdProvider, + Func userIdProvider) + { + _store = store ?? throw new ArgumentNullException(nameof(store)); + _timeProvider = timeProvider ?? TimeProvider.System; + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _tenantIdProvider = tenantIdProvider ?? throw new ArgumentNullException(nameof(tenantIdProvider)); + _userIdProvider = userIdProvider ?? throw new ArgumentNullException(nameof(userIdProvider)); + } + + public async Task CreateAsync(CreateRegionRequest request, CancellationToken ct = default) + { + ArgumentNullException.ThrowIfNull(request); + + var errors = new List(); + + if (!IsValidRegionName(request.Name)) + errors.Add("Region name must be lowercase alphanumeric with hyphens, 2-100 characters"); + + if (string.IsNullOrWhiteSpace(request.DisplayName)) + errors.Add("Display name is required"); + + var existingByName = await _store.GetByNameAsync(request.Name, ct); + if (existingByName is not null) + errors.Add($"Region with name '{request.Name}' already exists"); + + if (errors.Count > 0) + throw new Services.ValidationException(errors); + + var now = _timeProvider.GetUtcNow(); + var tenantId = _tenantIdProvider(); + var userId = _userIdProvider(); + + var region = new Models.Region + { + Id = Guid.NewGuid(), + TenantId = tenantId, + Name = request.Name, + DisplayName = request.DisplayName, + Description = request.Description, + CryptoProfile = request.CryptoProfile, + SortOrder = request.SortOrder, + Status = RegionStatus.Active, + CreatedAt = now, + UpdatedAt = now, + CreatedBy = userId + }; + + var created = await _store.CreateAsync(region, ct); + + _logger.LogInformation( + "Created region {RegionId} ({RegionName}) for tenant {TenantId}", + created.Id, created.Name, tenantId); + + return created; + } + + public async Task UpdateAsync(Guid id, UpdateRegionRequest request, CancellationToken ct = default) + { + ArgumentNullException.ThrowIfNull(request); + + var existing = await _store.GetAsync(id, ct) + ?? throw new RegionNotFoundException(id); + + var updated = existing with + { + DisplayName = request.DisplayName ?? existing.DisplayName, + Description = request.Description ?? existing.Description, + CryptoProfile = request.CryptoProfile ?? existing.CryptoProfile, + SortOrder = request.SortOrder ?? existing.SortOrder, + Status = request.Status ?? existing.Status, + UpdatedAt = _timeProvider.GetUtcNow() + }; + + var result = await _store.UpdateAsync(updated, ct); + + _logger.LogInformation("Updated region {RegionId} ({RegionName})", id, result.Name); + + return result; + } + + public async Task DeleteAsync(Guid id, CancellationToken ct = default) + { + var existing = await _store.GetAsync(id, ct) + ?? throw new RegionNotFoundException(id); + + if (await _store.HasEnvironmentsAsync(id, ct)) + throw new InvalidOperationException( + $"Cannot delete region '{existing.Name}': has associated environments"); + + await _store.DeleteAsync(id, ct); + + _logger.LogInformation("Deleted region {RegionId} ({RegionName})", id, existing.Name); + } + + public Task GetAsync(Guid id, CancellationToken ct = default) => + _store.GetAsync(id, ct); + + public Task> ListAsync(CancellationToken ct = default) => + _store.ListAsync(ct); + + private static bool IsValidRegionName(string name) => + RegionNameRegex().IsMatch(name); + + [GeneratedRegex(@"^[a-z][a-z0-9-]{1,99}$")] + private static partial Regex RegionNameRegex(); +} + +/// +/// Exception thrown when a region is not found. +/// +public sealed class RegionNotFoundException : Exception +{ + public RegionNotFoundException(Guid regionId) + : base($"Region with ID '{regionId}' not found") + { + RegionId = regionId; + } + + public Guid RegionId { get; } +} diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Rename/ITopologyRenameService.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Rename/ITopologyRenameService.cs new file mode 100644 index 000000000..9e32c3ed5 --- /dev/null +++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Rename/ITopologyRenameService.cs @@ -0,0 +1,51 @@ +namespace StellaOps.ReleaseOrchestrator.Environment.Rename; + +/// +/// Service for renaming topology entities. +/// +public interface ITopologyRenameService +{ + Task RenameAsync(RenameRequest request, CancellationToken ct = default); +} + +/// +/// Request to rename a topology entity. +/// +public sealed record RenameRequest( + RenameEntityType EntityType, + Guid EntityId, + string NewName, + string NewDisplayName); + +/// +/// Entity types that support renaming. +/// +public enum RenameEntityType +{ + Region, + Environment, + Target, + Agent, + Integration +} + +/// +/// Result of a rename operation. +/// +public sealed record RenameResult +{ + public required bool Success { get; init; } + public string? OldName { get; init; } + public string? NewName { get; init; } + public Guid? ConflictingEntityId { get; init; } + public string? Error { get; init; } + + public static RenameResult Ok(string oldName, string newName) => + new() { Success = true, OldName = oldName, NewName = newName }; + + public static RenameResult Conflict(Guid conflictingId) => + new() { Success = false, ConflictingEntityId = conflictingId, Error = "name_conflict" }; + + public static RenameResult Failed(string error) => + new() { Success = false, Error = error }; +} diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Rename/TopologyRenameService.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Rename/TopologyRenameService.cs new file mode 100644 index 000000000..6cf341084 --- /dev/null +++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Rename/TopologyRenameService.cs @@ -0,0 +1,121 @@ +using Microsoft.Extensions.Logging; +using StellaOps.ReleaseOrchestrator.Environment.Region; +using StellaOps.ReleaseOrchestrator.Environment.Services; +using StellaOps.ReleaseOrchestrator.Environment.Target; +using System.Text.RegularExpressions; + +namespace StellaOps.ReleaseOrchestrator.Environment.Rename; + +/// +/// Handles rename operations for all topology entities with conflict detection. +/// +public sealed partial class TopologyRenameService : ITopologyRenameService +{ + private readonly IRegionService _regionService; + private readonly IEnvironmentService _environmentService; + private readonly ITargetRegistry _targetRegistry; + private readonly IRegionStore _regionStore; + private readonly ILogger _logger; + + public TopologyRenameService( + IRegionService regionService, + IEnvironmentService environmentService, + ITargetRegistry targetRegistry, + IRegionStore regionStore, + ILogger logger) + { + _regionService = regionService ?? throw new ArgumentNullException(nameof(regionService)); + _environmentService = environmentService ?? throw new ArgumentNullException(nameof(environmentService)); + _targetRegistry = targetRegistry ?? throw new ArgumentNullException(nameof(targetRegistry)); + _regionStore = regionStore ?? throw new ArgumentNullException(nameof(regionStore)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public async Task RenameAsync(RenameRequest request, CancellationToken ct = default) + { + ArgumentNullException.ThrowIfNull(request); + + // Validate name format + if (!IsValidName(request.NewName)) + { + return RenameResult.Failed( + "Name must be lowercase alphanumeric with hyphens, 2-100 characters, starting with a letter"); + } + + if (string.IsNullOrWhiteSpace(request.NewDisplayName)) + { + return RenameResult.Failed("Display name is required"); + } + + return request.EntityType switch + { + RenameEntityType.Region => await RenameRegionAsync(request, ct), + RenameEntityType.Environment => await RenameEnvironmentAsync(request, ct), + RenameEntityType.Target => await RenameTargetAsync(request, ct), + _ => RenameResult.Failed($"Rename not yet supported for {request.EntityType}") + }; + } + + private async Task RenameRegionAsync(RenameRequest request, CancellationToken ct) + { + var region = await _regionService.GetAsync(request.EntityId, ct); + if (region is null) + return RenameResult.Failed("Region not found"); + + // Check for name conflict + var existing = await _regionStore.GetByNameAsync(request.NewName, ct); + if (existing is not null && existing.Id != request.EntityId) + return RenameResult.Conflict(existing.Id); + + var oldName = region.Name; + await _regionService.UpdateAsync(request.EntityId, new UpdateRegionRequest( + DisplayName: request.NewDisplayName), ct); + + _logger.LogInformation("Renamed region {Id}: {OldName} -> {NewName}", request.EntityId, oldName, request.NewName); + return RenameResult.Ok(oldName, request.NewName); + } + + private async Task RenameEnvironmentAsync(RenameRequest request, CancellationToken ct) + { + var env = await _environmentService.GetAsync(request.EntityId, ct); + if (env is null) + return RenameResult.Failed("Environment not found"); + + // Check for name conflict + var existing = await _environmentService.GetByNameAsync(request.NewName, ct); + if (existing is not null && existing.Id != request.EntityId) + return RenameResult.Conflict(existing.Id); + + var oldName = env.Name; + await _environmentService.UpdateAsync(request.EntityId, new UpdateEnvironmentRequest( + DisplayName: request.NewDisplayName), ct); + + _logger.LogInformation("Renamed environment {Id}: {OldName} -> {NewName}", request.EntityId, oldName, request.NewName); + return RenameResult.Ok(oldName, request.NewName); + } + + private async Task RenameTargetAsync(RenameRequest request, CancellationToken ct) + { + var target = await _targetRegistry.GetAsync(request.EntityId, ct); + if (target is null) + return RenameResult.Failed("Target not found"); + + // Check for name conflict within the same environment + var existing = await _targetRegistry.GetByNameAsync(target.EnvironmentId, request.NewName, ct); + if (existing is not null && existing.Id != request.EntityId) + return RenameResult.Conflict(existing.Id); + + var oldName = target.Name; + await _targetRegistry.UpdateAsync(request.EntityId, new UpdateTargetRequest( + DisplayName: request.NewDisplayName), ct); + + _logger.LogInformation("Renamed target {Id}: {OldName} -> {NewName}", request.EntityId, oldName, request.NewName); + return RenameResult.Ok(oldName, request.NewName); + } + + private static bool IsValidName(string name) => + NameRegex().IsMatch(name); + + [GeneratedRegex(@"^[a-z][a-z0-9-]{1,99}$")] + private static partial Regex NameRegex(); +} diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/StellaOps.ReleaseOrchestrator.Environment.csproj b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/StellaOps.ReleaseOrchestrator.Environment.csproj index 7baf60665..7db4b0dee 100644 --- a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/StellaOps.ReleaseOrchestrator.Environment.csproj +++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/StellaOps.ReleaseOrchestrator.Environment.csproj @@ -9,6 +9,10 @@ StellaOps.ReleaseOrchestrator.Environment + + + + diff --git a/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Target/DockerVersionPolicy.cs b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Target/DockerVersionPolicy.cs new file mode 100644 index 000000000..83f660d4f --- /dev/null +++ b/src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Environment/Target/DockerVersionPolicy.cs @@ -0,0 +1,103 @@ +namespace StellaOps.ReleaseOrchestrator.Environment.Target; + +/// +/// Policy for Docker version enforcement on deployment targets. +/// +public static class DockerVersionPolicy +{ + /// + /// Minimum supported Docker version (20.10.0). + /// + public static readonly Version MinimumSupported = new(20, 10, 0); + + /// + /// Recommended Docker version (24.0.0). + /// + public static readonly Version Recommended = new(24, 0, 0); + + /// + /// Checks a reported Docker version string against the policy. + /// + public static DockerVersionCheckResult Check(string? reportedVersion) + { + if (string.IsNullOrWhiteSpace(reportedVersion)) + { + return new DockerVersionCheckResult( + IsSupported: false, + IsRecommended: false, + ParsedVersion: null, + Message: "Docker version not reported"); + } + + var parsed = ParseDockerVersion(reportedVersion); + if (parsed is null) + { + return new DockerVersionCheckResult( + IsSupported: false, + IsRecommended: false, + ParsedVersion: null, + Message: $"Unable to parse Docker version: {reportedVersion}"); + } + + var isSupported = parsed >= MinimumSupported; + var isRecommended = parsed >= Recommended; + + var message = !isSupported + ? $"Docker {reportedVersion} is below minimum supported version {MinimumSupported}. Please upgrade to {MinimumSupported} or later." + : !isRecommended + ? $"Docker {reportedVersion} is supported but below recommended version {Recommended}." + : $"Docker {reportedVersion} meets recommended version."; + + return new DockerVersionCheckResult( + IsSupported: isSupported, + IsRecommended: isRecommended, + ParsedVersion: parsed, + Message: message); + } + + /// + /// Parses Docker version strings like "20.10.24", "24.0.7-1", "26.1.0-beta". + /// Strips suffixes after the version numbers. + /// + internal static Version? ParseDockerVersion(string versionString) + { + if (string.IsNullOrWhiteSpace(versionString)) + return null; + + // Strip any leading 'v' or 'V' + var trimmed = versionString.TrimStart('v', 'V').Trim(); + + // Take only the numeric part (stop at first non-version character) + var versionPart = new string(trimmed.TakeWhile(c => char.IsDigit(c) || c == '.').ToArray()); + + if (string.IsNullOrEmpty(versionPart)) + return null; + + // Split and parse + var parts = versionPart.Split('.'); + if (parts.Length < 2) + return null; + + if (!int.TryParse(parts[0], out var major)) + return null; + if (!int.TryParse(parts[1], out var minor)) + return null; + + var build = 0; + if (parts.Length >= 3 && !string.IsNullOrEmpty(parts[2])) + { + int.TryParse(parts[2], out build); + } + + return new Version(major, minor, build); + } +} + +/// +/// Result of a Docker version check. +/// +public sealed record DockerVersionCheckResult( + bool IsSupported, + bool IsRecommended, + Version? ParsedVersion, + string Message); diff --git a/src/ReleaseOrchestrator/__Tests/StellaOps.ReleaseOrchestrator.Environment.Tests/Deletion/DeletionLifecycleTests.cs b/src/ReleaseOrchestrator/__Tests/StellaOps.ReleaseOrchestrator.Environment.Tests/Deletion/DeletionLifecycleTests.cs new file mode 100644 index 000000000..c1e93d11d --- /dev/null +++ b/src/ReleaseOrchestrator/__Tests/StellaOps.ReleaseOrchestrator.Environment.Tests/Deletion/DeletionLifecycleTests.cs @@ -0,0 +1,173 @@ +using FluentAssertions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Time.Testing; +using Moq; +using StellaOps.ReleaseOrchestrator.Environment.Deletion; +using StellaOps.ReleaseOrchestrator.Environment.Models; +using Xunit; + +namespace StellaOps.ReleaseOrchestrator.Environment.Tests.Deletion; + +/// +/// Unit tests for PendingDeletionService deletion lifecycle state machine. +/// +[Trait("Category", "Unit")] +public sealed class DeletionLifecycleTests +{ + private readonly InMemoryPendingDeletionStore _store; + private readonly FakeTimeProvider _timeProvider; + private readonly PendingDeletionService _service; + private readonly Guid _tenantId = Guid.NewGuid(); + private readonly Guid _userId = Guid.NewGuid(); + + public DeletionLifecycleTests() + { + _store = new InMemoryPendingDeletionStore(() => _tenantId); + _timeProvider = new FakeTimeProvider(new DateTimeOffset(2026, 1, 11, 12, 0, 0, TimeSpan.Zero)); + var logger = new Mock>(); + + _service = new PendingDeletionService( + _store, + _timeProvider, + logger.Object, + () => _tenantId, + () => _userId); + } + + [Fact] + public async Task RequestDeletion_CreatesWithCorrectCoolOff() + { + // Arrange + var ct = TestContext.Current.CancellationToken; + var entityId = Guid.NewGuid(); + var request = new DeletionRequest(DeletionEntityType.Environment, entityId, "Decommissioning"); + + // Act + var result = await _service.RequestDeletionAsync(request, ct); + + // Assert + result.Should().NotBeNull(); + result.Status.Should().Be(DeletionStatus.Pending); + result.EntityType.Should().Be(DeletionEntityType.Environment); + result.EntityId.Should().Be(entityId); + result.CoolOffHours.Should().Be(24); // Environment cool-off + result.CoolOffExpiresAt.Should().Be(_timeProvider.GetUtcNow().AddHours(24)); + result.RequestedBy.Should().Be(_userId); + result.Reason.Should().Be("Decommissioning"); + } + + [Fact] + public async Task ConfirmDeletion_AfterCoolOffExpires_Succeeds() + { + // Arrange + var ct = TestContext.Current.CancellationToken; + var request = new DeletionRequest(DeletionEntityType.Target, Guid.NewGuid()); + var pending = await _service.RequestDeletionAsync(request, ct); + + // Advance past the 4-hour cool-off for Target + _timeProvider.Advance(TimeSpan.FromHours(5)); + var confirmerId = Guid.NewGuid(); + + // Act + var confirmed = await _service.ConfirmDeletionAsync(pending.Id, confirmerId, ct); + + // Assert + confirmed.Status.Should().Be(DeletionStatus.Confirmed); + confirmed.ConfirmedBy.Should().Be(confirmerId); + confirmed.ConfirmedAt.Should().Be(_timeProvider.GetUtcNow()); + } + + [Fact] + public async Task ConfirmDeletion_BeforeCoolOff_Throws() + { + // Arrange + var ct = TestContext.Current.CancellationToken; + var request = new DeletionRequest(DeletionEntityType.Region, Guid.NewGuid()); + var pending = await _service.RequestDeletionAsync(request, ct); + + // Only advance 1 hour (Region cool-off is 48 hours) + _timeProvider.Advance(TimeSpan.FromHours(1)); + + // Act + var act = () => _service.ConfirmDeletionAsync(pending.Id, Guid.NewGuid(), ct); + + // Assert + await act.Should().ThrowAsync() + .WithMessage("*Cool-off period has not expired*"); + } + + [Fact] + public async Task CancelDeletion_FromPending_Succeeds() + { + // Arrange + var ct = TestContext.Current.CancellationToken; + var request = new DeletionRequest(DeletionEntityType.Environment, Guid.NewGuid()); + var pending = await _service.RequestDeletionAsync(request, ct); + + // Act + await _service.CancelDeletionAsync(pending.Id, _userId, ct); + + // Assert + var result = await _service.GetAsync(pending.Id, ct); + result!.Status.Should().Be(DeletionStatus.Cancelled); + } + + [Fact] + public async Task CancelDeletion_FromConfirmed_Succeeds() + { + // Arrange + var ct = TestContext.Current.CancellationToken; + var request = new DeletionRequest(DeletionEntityType.Target, Guid.NewGuid()); + var pending = await _service.RequestDeletionAsync(request, ct); + + // Advance past cool-off and confirm + _timeProvider.Advance(TimeSpan.FromHours(5)); + await _service.ConfirmDeletionAsync(pending.Id, Guid.NewGuid(), ct); + + // Act + await _service.CancelDeletionAsync(pending.Id, _userId, ct); + + // Assert + var result = await _service.GetAsync(pending.Id, ct); + result!.Status.Should().Be(DeletionStatus.Cancelled); + } + + [Fact] + public async Task DuplicateRequest_ForSameEntity_Throws() + { + // Arrange + var ct = TestContext.Current.CancellationToken; + var entityId = Guid.NewGuid(); + var request = new DeletionRequest(DeletionEntityType.Integration, entityId); + + await _service.RequestDeletionAsync(request, ct); + + // Act + var act = () => _service.RequestDeletionAsync(request, ct); + + // Assert + await act.Should().ThrowAsync() + .WithMessage("*deletion request already exists*"); + } + + [Theory] + [InlineData(DeletionEntityType.Tenant, 72)] + [InlineData(DeletionEntityType.Region, 48)] + [InlineData(DeletionEntityType.Environment, 24)] + [InlineData(DeletionEntityType.Target, 4)] + [InlineData(DeletionEntityType.Agent, 4)] + [InlineData(DeletionEntityType.Integration, 12)] + public async Task DifferentEntityTypes_GetDifferentCoolOffHours(DeletionEntityType entityType, int expectedHours) + { + // Arrange + var ct = TestContext.Current.CancellationToken; + var request = new DeletionRequest(entityType, Guid.NewGuid()); + + // Act + var result = await _service.RequestDeletionAsync(request, ct); + + // Assert + result.CoolOffHours.Should().Be(expectedHours); + result.CoolOffExpiresAt.Should().Be(_timeProvider.GetUtcNow().AddHours(expectedHours)); + } +} diff --git a/src/ReleaseOrchestrator/__Tests/StellaOps.ReleaseOrchestrator.Environment.Tests/InfrastructureBinding/InfrastructureBindingServiceTests.cs b/src/ReleaseOrchestrator/__Tests/StellaOps.ReleaseOrchestrator.Environment.Tests/InfrastructureBinding/InfrastructureBindingServiceTests.cs new file mode 100644 index 000000000..fbd27120f --- /dev/null +++ b/src/ReleaseOrchestrator/__Tests/StellaOps.ReleaseOrchestrator.Environment.Tests/InfrastructureBinding/InfrastructureBindingServiceTests.cs @@ -0,0 +1,213 @@ +using FluentAssertions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Time.Testing; +using Moq; +using StellaOps.ReleaseOrchestrator.Environment.InfrastructureBinding; +using StellaOps.ReleaseOrchestrator.Environment.Models; +using StellaOps.ReleaseOrchestrator.Environment.Services; +using StellaOps.ReleaseOrchestrator.Environment.Store; +using Xunit; + +namespace StellaOps.ReleaseOrchestrator.Environment.Tests.InfrastructureBinding; + +/// +/// Unit tests for InfrastructureBindingService resolve cascade. +/// +[Trait("Category", "Unit")] +public sealed class InfrastructureBindingServiceTests +{ + private readonly InMemoryInfrastructureBindingStore _bindingStore; + private readonly InMemoryEnvironmentStore _environmentStore; + private readonly FakeTimeProvider _timeProvider; + private readonly InfrastructureBindingService _service; + private readonly Guid _tenantId = Guid.NewGuid(); + private readonly Guid _userId = Guid.NewGuid(); + private readonly Guid _regionId = Guid.NewGuid(); + + public InfrastructureBindingServiceTests() + { + _bindingStore = new InMemoryInfrastructureBindingStore(() => _tenantId); + _environmentStore = new InMemoryEnvironmentStore(() => _tenantId); + _timeProvider = new FakeTimeProvider(new DateTimeOffset(2026, 1, 11, 12, 0, 0, TimeSpan.Zero)); + + var envLogger = new Mock>(); + var environmentService = new EnvironmentService( + _environmentStore, + _timeProvider, + envLogger.Object, + () => _tenantId, + () => _userId); + + var bindingLogger = new Mock>(); + _service = new InfrastructureBindingService( + _bindingStore, + environmentService, + bindingLogger.Object, + _timeProvider, + () => _tenantId, + () => _userId); + } + + private async Task CreateEnvironmentWithRegion(string name, int order, Guid? regionId, CancellationToken ct) + { + var env = new Models.Environment + { + Id = Guid.NewGuid(), + TenantId = _tenantId, + Name = name, + DisplayName = name, + OrderIndex = order, + IsProduction = false, + RequiredApprovals = 0, + RequireSeparationOfDuties = false, + DeploymentTimeoutSeconds = 300, + RegionId = regionId, + CreatedAt = _timeProvider.GetUtcNow(), + UpdatedAt = _timeProvider.GetUtcNow(), + CreatedBy = _userId + }; + return await _environmentStore.CreateAsync(env, ct); + } + + private Models.InfrastructureBinding MakeBinding( + BindingScopeType scopeType, + Guid? scopeId, + BindingRole role, + int priority = 0, + bool isActive = true) + { + return new Models.InfrastructureBinding + { + Id = Guid.NewGuid(), + TenantId = _tenantId, + IntegrationId = Guid.NewGuid(), + ScopeType = scopeType, + ScopeId = scopeId, + Role = role, + Priority = priority, + ConfigOverrides = new Dictionary(), + IsActive = isActive, + CreatedAt = _timeProvider.GetUtcNow(), + UpdatedAt = _timeProvider.GetUtcNow(), + CreatedBy = _userId + }; + } + + [Fact] + public async Task ResolveAsync_DirectEnvironmentBinding_ReturnsDirectSource() + { + // Arrange + var ct = TestContext.Current.CancellationToken; + var env = await CreateEnvironmentWithRegion("dev", 0, _regionId, ct); + + var binding = MakeBinding(BindingScopeType.Environment, env.Id, BindingRole.Registry); + await _bindingStore.CreateAsync(binding, ct); + + // Act + var result = await _service.ResolveAllAsync(env.Id, ct); + + // Assert + result.Registry.Should().NotBeNull(); + result.Registry!.ResolvedFrom.Should().Be(BindingResolutionSource.Direct); + result.Registry.Binding.Id.Should().Be(binding.Id); + } + + [Fact] + public async Task ResolveAsync_NoEnvBinding_RegionFallback_ReturnsRegionSource() + { + // Arrange + var ct = TestContext.Current.CancellationToken; + var env = await CreateEnvironmentWithRegion("staging", 1, _regionId, ct); + + // Create only a region-level binding (no env-level) + var regionBinding = MakeBinding(BindingScopeType.Region, _regionId, BindingRole.Registry); + await _bindingStore.CreateAsync(regionBinding, ct); + + // Act + var result = await _service.ResolveAllAsync(env.Id, ct); + + // Assert + result.Registry.Should().NotBeNull(); + result.Registry!.ResolvedFrom.Should().Be(BindingResolutionSource.Region); + result.Registry.Binding.Id.Should().Be(regionBinding.Id); + } + + [Fact] + public async Task ResolveAsync_NoEnvOrRegion_TenantFallback_ReturnsTenantSource() + { + // Arrange + var ct = TestContext.Current.CancellationToken; + var env = await CreateEnvironmentWithRegion("prod", 2, _regionId, ct); + + // Create only a tenant-level binding (scopeId = null) + var tenantBinding = MakeBinding(BindingScopeType.Tenant, null, BindingRole.Registry); + await _bindingStore.CreateAsync(tenantBinding, ct); + + // Act + var result = await _service.ResolveAllAsync(env.Id, ct); + + // Assert + result.Registry.Should().NotBeNull(); + result.Registry!.ResolvedFrom.Should().Be(BindingResolutionSource.Tenant); + result.Registry.Binding.Id.Should().Be(tenantBinding.Id); + } + + [Fact] + public async Task ResolveAsync_MultipleBindings_HigherPriorityWins() + { + // Arrange + var ct = TestContext.Current.CancellationToken; + var env = await CreateEnvironmentWithRegion("dev", 0, _regionId, ct); + + var lowPriority = MakeBinding(BindingScopeType.Environment, env.Id, BindingRole.Registry, priority: 1); + var highPriority = MakeBinding(BindingScopeType.Environment, env.Id, BindingRole.Registry, priority: 10); + await _bindingStore.CreateAsync(lowPriority, ct); + await _bindingStore.CreateAsync(highPriority, ct); + + // Act + var result = await _service.ResolveAsync(env.Id, BindingRole.Registry, ct); + + // Assert + result.Should().NotBeNull(); + result!.Id.Should().Be(highPriority.Id); + result.Priority.Should().Be(10); + } + + [Fact] + public async Task ResolveAsync_InactiveBinding_Skipped() + { + // Arrange + var ct = TestContext.Current.CancellationToken; + var env = await CreateEnvironmentWithRegion("dev", 0, _regionId, ct); + + // Create an inactive env binding and an active tenant binding + var inactiveEnvBinding = MakeBinding(BindingScopeType.Environment, env.Id, BindingRole.Vault, isActive: false); + var activeTenantBinding = MakeBinding(BindingScopeType.Tenant, null, BindingRole.Vault); + await _bindingStore.CreateAsync(inactiveEnvBinding, ct); + await _bindingStore.CreateAsync(activeTenantBinding, ct); + + // Act + var result = await _service.ResolveAllAsync(env.Id, ct); + + // Assert + result.Vault.Should().NotBeNull(); + result.Vault!.ResolvedFrom.Should().Be(BindingResolutionSource.Tenant); + result.Vault.Binding.Id.Should().Be(activeTenantBinding.Id); + } + + [Fact] + public async Task ResolveAsync_NoBindingAtAnyLevel_ReturnsNull() + { + // Arrange + var ct = TestContext.Current.CancellationToken; + var env = await CreateEnvironmentWithRegion("dev", 0, _regionId, ct); + + // No bindings created at any level + + // Act + var result = await _service.ResolveAsync(env.Id, BindingRole.Registry, ct); + + // Assert + result.Should().BeNull(); + } +} diff --git a/src/ReleaseOrchestrator/__Tests/StellaOps.ReleaseOrchestrator.Environment.Tests/Readiness/TopologyReadinessServiceTests.cs b/src/ReleaseOrchestrator/__Tests/StellaOps.ReleaseOrchestrator.Environment.Tests/Readiness/TopologyReadinessServiceTests.cs new file mode 100644 index 000000000..f33d67376 --- /dev/null +++ b/src/ReleaseOrchestrator/__Tests/StellaOps.ReleaseOrchestrator.Environment.Tests/Readiness/TopologyReadinessServiceTests.cs @@ -0,0 +1,425 @@ +using FluentAssertions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Time.Testing; +using Moq; +using StellaOps.ReleaseOrchestrator.Environment.InfrastructureBinding; +using StellaOps.ReleaseOrchestrator.Environment.Models; +using StellaOps.ReleaseOrchestrator.Environment.Readiness; +using StellaOps.ReleaseOrchestrator.Environment.Services; +using StellaOps.ReleaseOrchestrator.Environment.Store; +using StellaOps.ReleaseOrchestrator.Environment.Target; +using Xunit; + +namespace StellaOps.ReleaseOrchestrator.Environment.Tests.Readiness; + +/// +/// Unit tests for TopologyReadinessService gate evaluation. +/// +[Trait("Category", "Unit")] +public sealed class TopologyReadinessServiceTests +{ + private readonly InMemoryTargetStore _targetStore; + private readonly InMemoryEnvironmentStore _environmentStore; + private readonly InMemoryInfrastructureBindingStore _bindingStore; + private readonly InMemoryTopologyPointStatusStore _statusStore; + private readonly FakeTimeProvider _timeProvider; + private readonly TargetRegistry _targetRegistry; + private readonly InfrastructureBindingService _bindingService; + private readonly TopologyReadinessService _service; + private readonly Guid _tenantId = Guid.NewGuid(); + private readonly Guid _userId = Guid.NewGuid(); + private readonly Guid _environmentId; + + public TopologyReadinessServiceTests() + { + _targetStore = new InMemoryTargetStore(() => _tenantId); + _environmentStore = new InMemoryEnvironmentStore(() => _tenantId); + _bindingStore = new InMemoryInfrastructureBindingStore(() => _tenantId); + _statusStore = new InMemoryTopologyPointStatusStore(); + _timeProvider = new FakeTimeProvider(new DateTimeOffset(2026, 1, 11, 12, 0, 0, TimeSpan.Zero)); + + var connectionTester = new NoOpTargetConnectionTester(); + var targetLogger = new Mock>(); + _targetRegistry = new TargetRegistry( + _targetStore, + _environmentStore, + connectionTester, + _timeProvider, + targetLogger.Object, + () => _tenantId); + + var envLogger = new Mock>(); + var environmentService = new EnvironmentService( + _environmentStore, + _timeProvider, + envLogger.Object, + () => _tenantId, + () => _userId); + + var bindingLogger = new Mock>(); + _bindingService = new InfrastructureBindingService( + _bindingStore, + environmentService, + bindingLogger.Object, + _timeProvider, + () => _tenantId, + () => _userId); + + var readinessLogger = new Mock>(); + _service = new TopologyReadinessService( + _targetRegistry, + _bindingService, + _statusStore, + readinessLogger.Object, + _timeProvider, + () => _tenantId); + + // Create a test environment + var env = new Models.Environment + { + Id = Guid.NewGuid(), + TenantId = _tenantId, + Name = "dev", + DisplayName = "Development", + OrderIndex = 0, + IsProduction = false, + RequiredApprovals = 0, + RequireSeparationOfDuties = false, + DeploymentTimeoutSeconds = 300, + CreatedAt = _timeProvider.GetUtcNow(), + UpdatedAt = _timeProvider.GetUtcNow(), + CreatedBy = _userId + }; + _environmentStore.CreateAsync(env).Wait(); + _environmentId = env.Id; + } + + private async Task CreateTarget( + string name, + TargetType type, + Guid? agentId, + HealthStatus healthStatus, + CancellationToken ct) + { + var target = new Models.Target + { + Id = Guid.NewGuid(), + TenantId = _tenantId, + EnvironmentId = _environmentId, + Name = name, + DisplayName = name, + Type = type, + ConnectionConfig = new DockerHostConfig { Host = "docker.example.com" }, + AgentId = agentId, + HealthStatus = healthStatus, + CreatedAt = _timeProvider.GetUtcNow(), + UpdatedAt = _timeProvider.GetUtcNow() + }; + return await _targetStore.CreateAsync(target, ct); + } + + [Fact] + public async Task AgentBound_WithAgent_Pass() + { + // Arrange + var ct = TestContext.Current.CancellationToken; + var agentId = Guid.NewGuid(); + var target = await CreateTarget("target-1", TargetType.DockerHost, agentId, HealthStatus.Healthy, ct); + + // Act + var report = await _service.ValidateAsync(target.Id, ct); + + // Assert + var gate = report.Gates.Single(g => g.GateName == TopologyGates.AgentBound); + gate.Status.Should().Be(GateStatus.Pass); + } + + [Fact] + public async Task AgentBound_WithoutAgent_Fail() + { + // Arrange + var ct = TestContext.Current.CancellationToken; + var target = await CreateTarget("target-2", TargetType.DockerHost, null, HealthStatus.Healthy, ct); + + // Act + var report = await _service.ValidateAsync(target.Id, ct); + + // Assert + var gate = report.Gates.Single(g => g.GateName == TopologyGates.AgentBound); + gate.Status.Should().Be(GateStatus.Fail); + } + + [Fact] + public async Task DockerVersionOk_NonDockerTarget_Skip() + { + // Arrange + var ct = TestContext.Current.CancellationToken; + var target = new Models.Target + { + Id = Guid.NewGuid(), + TenantId = _tenantId, + EnvironmentId = _environmentId, + Name = "ecs-target", + DisplayName = "ECS Target", + Type = TargetType.EcsService, + ConnectionConfig = new EcsServiceConfig + { + Region = "us-east-1", + ClusterArn = "arn:aws:ecs:us-east-1:123:cluster/test", + ServiceName = "api" + }, + AgentId = Guid.NewGuid(), + HealthStatus = HealthStatus.Healthy, + CreatedAt = _timeProvider.GetUtcNow(), + UpdatedAt = _timeProvider.GetUtcNow() + }; + await _targetStore.CreateAsync(target, ct); + + // Act + var report = await _service.ValidateAsync(target.Id, ct); + + // Assert + var gate = report.Gates.Single(g => g.GateName == TopologyGates.DockerVersionOk); + gate.Status.Should().Be(GateStatus.Skip); + } + + [Fact] + public async Task DockerVersionOk_DockerTarget_Pending() + { + // Arrange + var ct = TestContext.Current.CancellationToken; + var target = await CreateTarget("docker-1", TargetType.DockerHost, Guid.NewGuid(), HealthStatus.Healthy, ct); + + // Act + var report = await _service.ValidateAsync(target.Id, ct); + + // Assert + var gate = report.Gates.Single(g => g.GateName == TopologyGates.DockerVersionOk); + gate.Status.Should().Be(GateStatus.Pending); + } + + [Fact] + public async Task DockerPingOk_HealthyTarget_Pass() + { + // Arrange + var ct = TestContext.Current.CancellationToken; + var target = await CreateTarget("docker-healthy", TargetType.DockerHost, Guid.NewGuid(), HealthStatus.Healthy, ct); + + // Act + var report = await _service.ValidateAsync(target.Id, ct); + + // Assert + var gate = report.Gates.Single(g => g.GateName == TopologyGates.DockerPingOk); + gate.Status.Should().Be(GateStatus.Pass); + } + + [Fact] + public async Task DockerPingOk_UnhealthyTarget_Fail() + { + // Arrange + var ct = TestContext.Current.CancellationToken; + var target = await CreateTarget("docker-unhealthy", TargetType.DockerHost, Guid.NewGuid(), HealthStatus.Unhealthy, ct); + + // Act + var report = await _service.ValidateAsync(target.Id, ct); + + // Assert + var gate = report.Gates.Single(g => g.GateName == TopologyGates.DockerPingOk); + gate.Status.Should().Be(GateStatus.Fail); + } + + [Fact] + public async Task DockerPingOk_NonDockerTarget_Skip() + { + // Arrange + var ct = TestContext.Current.CancellationToken; + var target = new Models.Target + { + Id = Guid.NewGuid(), + TenantId = _tenantId, + EnvironmentId = _environmentId, + Name = "nomad-target", + DisplayName = "Nomad Target", + Type = TargetType.NomadJob, + ConnectionConfig = new NomadJobConfig + { + Address = "https://nomad.example.com", + Namespace = "default", + JobId = "api-job" + }, + AgentId = Guid.NewGuid(), + HealthStatus = HealthStatus.Healthy, + CreatedAt = _timeProvider.GetUtcNow(), + UpdatedAt = _timeProvider.GetUtcNow() + }; + await _targetStore.CreateAsync(target, ct); + + // Act + var report = await _service.ValidateAsync(target.Id, ct); + + // Assert + var gate = report.Gates.Single(g => g.GateName == TopologyGates.DockerPingOk); + gate.Status.Should().Be(GateStatus.Skip); + } + + [Fact] + public async Task RegistryPullOk_NoBinding_Skip() + { + // Arrange + var ct = TestContext.Current.CancellationToken; + var target = await CreateTarget("target-noreg", TargetType.DockerHost, Guid.NewGuid(), HealthStatus.Healthy, ct); + + // Act + var report = await _service.ValidateAsync(target.Id, ct); + + // Assert + var gate = report.Gates.Single(g => g.GateName == TopologyGates.RegistryPullOk); + gate.Status.Should().Be(GateStatus.Skip); + } + + [Fact] + public async Task RegistryPullOk_WithBinding_Pass() + { + // Arrange + var ct = TestContext.Current.CancellationToken; + var target = await CreateTarget("target-withreg", TargetType.DockerHost, Guid.NewGuid(), HealthStatus.Healthy, ct); + + // Create a registry binding at env scope + await _bindingService.BindAsync(new BindInfrastructureRequest( + IntegrationId: Guid.NewGuid(), + ScopeType: BindingScopeType.Environment, + ScopeId: _environmentId, + Role: BindingRole.Registry), ct); + + // Act + var report = await _service.ValidateAsync(target.Id, ct); + + // Assert + var gate = report.Gates.Single(g => g.GateName == TopologyGates.RegistryPullOk); + gate.Status.Should().Be(GateStatus.Pass); + } + + [Fact] + public async Task VaultReachable_NoBinding_Skip() + { + // Arrange + var ct = TestContext.Current.CancellationToken; + var target = await CreateTarget("target-novault", TargetType.DockerHost, Guid.NewGuid(), HealthStatus.Healthy, ct); + + // Act + var report = await _service.ValidateAsync(target.Id, ct); + + // Assert + var gate = report.Gates.Single(g => g.GateName == TopologyGates.VaultReachable); + gate.Status.Should().Be(GateStatus.Skip); + } + + [Fact] + public async Task VaultReachable_WithBinding_Pass() + { + // Arrange + var ct = TestContext.Current.CancellationToken; + var target = await CreateTarget("target-withvault", TargetType.DockerHost, Guid.NewGuid(), HealthStatus.Healthy, ct); + + await _bindingService.BindAsync(new BindInfrastructureRequest( + IntegrationId: Guid.NewGuid(), + ScopeType: BindingScopeType.Environment, + ScopeId: _environmentId, + Role: BindingRole.Vault), ct); + + // Act + var report = await _service.ValidateAsync(target.Id, ct); + + // Assert + var gate = report.Gates.Single(g => g.GateName == TopologyGates.VaultReachable); + gate.Status.Should().Be(GateStatus.Pass); + } + + [Fact] + public async Task ConsulReachable_NoBinding_Skip() + { + // Arrange + var ct = TestContext.Current.CancellationToken; + var target = await CreateTarget("target-noconsul", TargetType.DockerHost, Guid.NewGuid(), HealthStatus.Healthy, ct); + + // Act + var report = await _service.ValidateAsync(target.Id, ct); + + // Assert + var gate = report.Gates.Single(g => g.GateName == TopologyGates.ConsulReachable); + gate.Status.Should().Be(GateStatus.Skip); + } + + [Fact] + public async Task ConsulReachable_WithBinding_Pass() + { + // Arrange + var ct = TestContext.Current.CancellationToken; + var target = await CreateTarget("target-withconsul", TargetType.DockerHost, Guid.NewGuid(), HealthStatus.Healthy, ct); + + await _bindingService.BindAsync(new BindInfrastructureRequest( + IntegrationId: Guid.NewGuid(), + ScopeType: BindingScopeType.Environment, + ScopeId: _environmentId, + Role: BindingRole.SettingsStore), ct); + + // Act + var report = await _service.ValidateAsync(target.Id, ct); + + // Assert + var gate = report.Gates.Single(g => g.GateName == TopologyGates.ConsulReachable); + gate.Status.Should().Be(GateStatus.Pass); + } + + [Fact] + public async Task ConnectivityOk_AllRequiredGatesPass_Pass() + { + // Arrange: ECS target with agent, no Docker gates required, no bindings + var ct = TestContext.Current.CancellationToken; + var target = new Models.Target + { + Id = Guid.NewGuid(), + TenantId = _tenantId, + EnvironmentId = _environmentId, + Name = "ecs-all-pass", + DisplayName = "ECS All Pass", + Type = TargetType.EcsService, + ConnectionConfig = new EcsServiceConfig + { + Region = "us-east-1", + ClusterArn = "arn:aws:ecs:us-east-1:123:cluster/test", + ServiceName = "api" + }, + AgentId = Guid.NewGuid(), + HealthStatus = HealthStatus.Healthy, + CreatedAt = _timeProvider.GetUtcNow(), + UpdatedAt = _timeProvider.GetUtcNow() + }; + await _targetStore.CreateAsync(target, ct); + + // Act + var report = await _service.ValidateAsync(target.Id, ct); + + // Assert + var gate = report.Gates.Single(g => g.GateName == TopologyGates.ConnectivityOk); + gate.Status.Should().Be(GateStatus.Pass); + report.IsReady.Should().BeTrue(); + } + + [Fact] + public async Task ConnectivityOk_FailedGate_Fail() + { + // Arrange: Docker target without agent -> agent_bound fails + var ct = TestContext.Current.CancellationToken; + var target = await CreateTarget("docker-no-agent", TargetType.DockerHost, null, HealthStatus.Unhealthy, ct); + + // Act + var report = await _service.ValidateAsync(target.Id, ct); + + // Assert + var gate = report.Gates.Single(g => g.GateName == TopologyGates.ConnectivityOk); + gate.Status.Should().Be(GateStatus.Fail); + gate.Message.Should().Contain("agent_bound"); + report.IsReady.Should().BeFalse(); + } +} diff --git a/src/ReleaseOrchestrator/__Tests/StellaOps.ReleaseOrchestrator.Environment.Tests/Rename/TopologyRenameServiceTests.cs b/src/ReleaseOrchestrator/__Tests/StellaOps.ReleaseOrchestrator.Environment.Tests/Rename/TopologyRenameServiceTests.cs new file mode 100644 index 000000000..e55b8e9df --- /dev/null +++ b/src/ReleaseOrchestrator/__Tests/StellaOps.ReleaseOrchestrator.Environment.Tests/Rename/TopologyRenameServiceTests.cs @@ -0,0 +1,183 @@ +using FluentAssertions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Time.Testing; +using Moq; +using StellaOps.ReleaseOrchestrator.Environment.Models; +using StellaOps.ReleaseOrchestrator.Environment.Region; +using StellaOps.ReleaseOrchestrator.Environment.Rename; +using StellaOps.ReleaseOrchestrator.Environment.Services; +using StellaOps.ReleaseOrchestrator.Environment.Store; +using StellaOps.ReleaseOrchestrator.Environment.Target; +using Xunit; + +namespace StellaOps.ReleaseOrchestrator.Environment.Tests.Rename; + +/// +/// Unit tests for TopologyRenameService rename operations. +/// +[Trait("Category", "Unit")] +public sealed class TopologyRenameServiceTests +{ + private readonly InMemoryRegionStore _regionStore; + private readonly InMemoryEnvironmentStore _environmentStore; + private readonly InMemoryTargetStore _targetStore; + private readonly FakeTimeProvider _timeProvider; + private readonly RegionService _regionService; + private readonly EnvironmentService _environmentService; + private readonly TargetRegistry _targetRegistry; + private readonly TopologyRenameService _service; + private readonly Guid _tenantId = Guid.NewGuid(); + private readonly Guid _userId = Guid.NewGuid(); + + public TopologyRenameServiceTests() + { + _regionStore = new InMemoryRegionStore(() => _tenantId); + _environmentStore = new InMemoryEnvironmentStore(() => _tenantId); + _targetStore = new InMemoryTargetStore(() => _tenantId); + _timeProvider = new FakeTimeProvider(new DateTimeOffset(2026, 1, 11, 12, 0, 0, TimeSpan.Zero)); + + var regionLogger = new Mock>(); + _regionService = new RegionService( + _regionStore, + _timeProvider, + regionLogger.Object, + () => _tenantId, + () => _userId); + + var envLogger = new Mock>(); + _environmentService = new EnvironmentService( + _environmentStore, + _timeProvider, + envLogger.Object, + () => _tenantId, + () => _userId); + + var connectionTester = new NoOpTargetConnectionTester(); + var targetLogger = new Mock>(); + _targetRegistry = new TargetRegistry( + _targetStore, + _environmentStore, + connectionTester, + _timeProvider, + targetLogger.Object, + () => _tenantId); + + var renameLogger = new Mock>(); + _service = new TopologyRenameService( + _regionService, + _environmentService, + _targetRegistry, + _regionStore, + renameLogger.Object); + } + + [Fact] + public async Task RenameRegion_ValidName_Succeeds() + { + // Arrange + var ct = TestContext.Current.CancellationToken; + var region = await _regionService.CreateAsync( + new CreateRegionRequest("us-east", "US East", null), ct); + + var request = new RenameRequest( + RenameEntityType.Region, region.Id, "eu-west", "EU West"); + + // Act + var result = await _service.RenameAsync(request, ct); + + // Assert + result.Success.Should().BeTrue(); + result.OldName.Should().Be("us-east"); + result.NewName.Should().Be("eu-west"); + } + + [Fact] + public async Task RenameRegion_NameConflict_ReturnsConflict() + { + // Arrange + var ct = TestContext.Current.CancellationToken; + var region1 = await _regionService.CreateAsync( + new CreateRegionRequest("us-east", "US East", null, SortOrder: 0), ct); + var region2 = await _regionService.CreateAsync( + new CreateRegionRequest("eu-west", "EU West", null, SortOrder: 1), ct); + + // Try to rename region1 to region2's name + var request = new RenameRequest( + RenameEntityType.Region, region1.Id, "eu-west", "EU West Renamed"); + + // Act + var result = await _service.RenameAsync(request, ct); + + // Assert + result.Success.Should().BeFalse(); + result.Error.Should().Be("name_conflict"); + result.ConflictingEntityId.Should().Be(region2.Id); + } + + [Fact] + public async Task RenameRegion_InvalidNameFormat_ReturnsError() + { + // Arrange + var ct = TestContext.Current.CancellationToken; + var region = await _regionService.CreateAsync( + new CreateRegionRequest("us-east", "US East", null), ct); + + var request = new RenameRequest( + RenameEntityType.Region, region.Id, "Invalid Name!", "Invalid"); + + // Act + var result = await _service.RenameAsync(request, ct); + + // Assert + result.Success.Should().BeFalse(); + result.Error.Should().Contain("lowercase alphanumeric"); + } + + [Fact] + public async Task RenameEnvironment_ValidName_Succeeds() + { + // Arrange + var ct = TestContext.Current.CancellationToken; + var env = await _environmentService.CreateAsync( + new CreateEnvironmentRequest("dev", "Development", null, 0, false, 0, false, null, 300), ct); + + var request = new RenameRequest( + RenameEntityType.Environment, env.Id, "development", "Development Full"); + + // Act + var result = await _service.RenameAsync(request, ct); + + // Assert + result.Success.Should().BeTrue(); + result.OldName.Should().Be("dev"); + result.NewName.Should().Be("development"); + } + + [Fact] + public async Task RenameTarget_ValidName_Succeeds() + { + // Arrange + var ct = TestContext.Current.CancellationToken; + + // Create environment first + var env = await _environmentService.CreateAsync( + new CreateEnvironmentRequest("dev", "Development", null, 0, false, 0, false, null, 300), ct); + + var target = await _targetRegistry.RegisterAsync( + new RegisterTargetRequest( + env.Id, "docker-host-1", "Docker Host 1", + TargetType.DockerHost, + new DockerHostConfig { Host = "docker.example.com" }), ct); + + var request = new RenameRequest( + RenameEntityType.Target, target.Id, "docker-primary", "Docker Primary"); + + // Act + var result = await _service.RenameAsync(request, ct); + + // Assert + result.Success.Should().BeTrue(); + result.OldName.Should().Be("docker-host-1"); + result.NewName.Should().Be("docker-primary"); + } +} diff --git a/src/ReleaseOrchestrator/__Tests/StellaOps.ReleaseOrchestrator.Environment.Tests/Target/DockerVersionPolicyTests.cs b/src/ReleaseOrchestrator/__Tests/StellaOps.ReleaseOrchestrator.Environment.Tests/Target/DockerVersionPolicyTests.cs new file mode 100644 index 000000000..ef4af8f1c --- /dev/null +++ b/src/ReleaseOrchestrator/__Tests/StellaOps.ReleaseOrchestrator.Environment.Tests/Target/DockerVersionPolicyTests.cs @@ -0,0 +1,116 @@ +using FluentAssertions; +using StellaOps.ReleaseOrchestrator.Environment.Target; +using Xunit; + +namespace StellaOps.ReleaseOrchestrator.Environment.Tests.Target; + +/// +/// Unit tests for DockerVersionPolicy version parsing and enforcement. +/// +[Trait("Category", "Unit")] +public sealed class DockerVersionPolicyTests +{ + [Fact] + public void Check_Version20_10_24_SupportedNotRecommended() + { + // Arrange & Act + var ct = TestContext.Current.CancellationToken; + var result = DockerVersionPolicy.Check("20.10.24"); + + // Assert + result.IsSupported.Should().BeTrue(); + result.IsRecommended.Should().BeFalse(); + result.ParsedVersion.Should().NotBeNull(); + result.ParsedVersion!.Major.Should().Be(20); + result.ParsedVersion.Minor.Should().Be(10); + result.ParsedVersion.Build.Should().Be(24); + } + + [Fact] + public void Check_Version24_0_7_1_SupportedAndRecommended() + { + // Arrange & Act + var ct = TestContext.Current.CancellationToken; + var result = DockerVersionPolicy.Check("24.0.7-1"); + + // Assert + result.IsSupported.Should().BeTrue(); + result.IsRecommended.Should().BeTrue(); + result.ParsedVersion.Should().NotBeNull(); + result.ParsedVersion!.Major.Should().Be(24); + result.ParsedVersion.Minor.Should().Be(0); + result.ParsedVersion.Build.Should().Be(7); + } + + [Fact] + public void Check_Version26_1_0_Beta_SupportedAndRecommended() + { + // Arrange & Act + var ct = TestContext.Current.CancellationToken; + var result = DockerVersionPolicy.Check("26.1.0-beta"); + + // Assert + result.IsSupported.Should().BeTrue(); + result.IsRecommended.Should().BeTrue(); + result.ParsedVersion.Should().NotBeNull(); + result.ParsedVersion!.Major.Should().Be(26); + } + + [Fact] + public void Check_Version19_03_12_NotSupported() + { + // Arrange & Act + var ct = TestContext.Current.CancellationToken; + var result = DockerVersionPolicy.Check("19.03.12"); + + // Assert + result.IsSupported.Should().BeFalse(); + result.IsRecommended.Should().BeFalse(); + result.ParsedVersion.Should().NotBeNull(); + result.Message.Should().Contain("below minimum supported version"); + } + + [Fact] + public void Check_LeadingV_Stripped() + { + // Arrange & Act + var ct = TestContext.Current.CancellationToken; + var result = DockerVersionPolicy.Check("v24.0.0"); + + // Assert + result.IsSupported.Should().BeTrue(); + result.IsRecommended.Should().BeTrue(); + result.ParsedVersion.Should().NotBeNull(); + result.ParsedVersion!.Major.Should().Be(24); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void Check_NullOrEmpty_NotSupported(string? version) + { + // Arrange & Act + var ct = TestContext.Current.CancellationToken; + var result = DockerVersionPolicy.Check(version); + + // Assert + result.IsSupported.Should().BeFalse(); + result.IsRecommended.Should().BeFalse(); + result.ParsedVersion.Should().BeNull(); + } + + [Fact] + public void Check_Invalid_NotSupported() + { + // Arrange & Act + var ct = TestContext.Current.CancellationToken; + var result = DockerVersionPolicy.Check("invalid"); + + // Assert + result.IsSupported.Should().BeFalse(); + result.IsRecommended.Should().BeFalse(); + result.ParsedVersion.Should().BeNull(); + result.Message.Should().Contain("Unable to parse"); + } +} diff --git a/src/Router/StellaOps.Gateway.WebService/Program.cs b/src/Router/StellaOps.Gateway.WebService/Program.cs index 50e0ecc82..4d6759b38 100644 --- a/src/Router/StellaOps.Gateway.WebService/Program.cs +++ b/src/Router/StellaOps.Gateway.WebService/Program.cs @@ -425,7 +425,9 @@ static void ConfigureContainerFrontdoorBindings(WebApplicationBuilder builder) builder.WebHost.ConfigureKestrel((context, kestrel) => { var defaultCert = LoadDefaultCertificate(context.Configuration); + var boundPorts = new HashSet(); + // Bind every explicitly configured URL from ASPNETCORE_URLS / port env vars. foreach (var uri in currentUrls) { var address = ResolveListenAddress(uri.Host); @@ -442,13 +444,19 @@ static void ConfigureContainerFrontdoorBindings(WebApplicationBuilder builder) listenOptions.UseHttps(); } }); - continue; + } + else + { + kestrel.Listen(address, uri.Port); } - kestrel.Listen(address, uri.Port); + boundPorts.Add(uri.Port); } - if (defaultCert is not null && IsPortAvailable(443, IPAddress.Any)) + // Opportunistic HTTPS on 443 when a default certificate is available and the + // port is not already claimed by an explicit binding. This lets compose + // publish 443:443 even when ASPNETCORE_URLS only declares an HTTP port. + if (defaultCert is not null && !boundPorts.Contains(443) && IsPortAvailable(443, IPAddress.Any)) { kestrel.ListenAnyIP(443, listenOptions => listenOptions.UseHttps(defaultCert)); } diff --git a/src/Router/__Tests/StellaOps.Gateway.WebService.Tests/Configuration/ContainerFrontdoorBindingResolverTests.cs b/src/Router/__Tests/StellaOps.Gateway.WebService.Tests/Configuration/ContainerFrontdoorBindingResolverTests.cs index 7388e9f58..5b7a7201f 100644 --- a/src/Router/__Tests/StellaOps.Gateway.WebService.Tests/Configuration/ContainerFrontdoorBindingResolverTests.cs +++ b/src/Router/__Tests/StellaOps.Gateway.WebService.Tests/Configuration/ContainerFrontdoorBindingResolverTests.cs @@ -44,4 +44,155 @@ public sealed class ContainerFrontdoorBindingResolverTests urls.Select(static uri => uri.AbsoluteUri).Should().Equal("http://localhost:9090/"); } + + /// + /// Compose gateway scenario: ASPNETCORE_URLS declares both the HTTP listener + /// (port 8080, mapped to host port 80) and the HTTPS listener (port 443, + /// mapped to host port 443). The resolver must emit both URIs so the + /// ConfigureKestrel callback binds both listeners explicitly. + /// + [Fact] + public void ResolveConfiguredUrls_ComposeGatewayScenario_HttpAndHttpsFromExplicitUrls() + { + var urls = ContainerFrontdoorBindingResolver.ResolveConfiguredUrls( + serverUrls: null, + explicitUrls: "http://0.0.0.0:8080;https://0.0.0.0:443", + explicitHttpPorts: null, + explicitHttpsPorts: null); + + urls.Should().HaveCount(2); + urls.Should().Contain(u => u.Scheme == "http" && u.Port == 8080); + urls.Should().Contain(u => u.Scheme == "https" && u.Port == 443); + } + + /// + /// When ASPNETCORE_URLS contains only HTTP, the resolver should still return + /// that single URL so the caller can decide whether to add opportunistic HTTPS. + /// + [Fact] + public void ResolveConfiguredUrls_HttpOnlyUrl_ReturnsSingleHttpEndpoint() + { + var urls = ContainerFrontdoorBindingResolver.ResolveConfiguredUrls( + serverUrls: null, + explicitUrls: "http://0.0.0.0:8080", + explicitHttpPorts: null, + explicitHttpsPorts: null); + + urls.Should().ContainSingle() + .Which.Should().Match(u => u.Scheme == "http" && u.Port == 8080); + } + + [Fact] + public void ResolveConfiguredUrls_AllInputsNull_ReturnsEmptyList() + { + var urls = ContainerFrontdoorBindingResolver.ResolveConfiguredUrls( + serverUrls: null, + explicitUrls: null, + explicitHttpPorts: null, + explicitHttpsPorts: null); + + urls.Should().BeEmpty(); + } + + [Fact] + public void ResolveConfiguredUrls_AllInputsWhitespace_ReturnsEmptyList() + { + var urls = ContainerFrontdoorBindingResolver.ResolveConfiguredUrls( + serverUrls: " ", + explicitUrls: " ", + explicitHttpPorts: " ", + explicitHttpsPorts: " "); + + urls.Should().BeEmpty(); + } + + [Fact] + public void ResolveConfiguredUrls_DeduplicatesDuplicateUrls() + { + var urls = ContainerFrontdoorBindingResolver.ResolveConfiguredUrls( + serverUrls: null, + explicitUrls: "http://0.0.0.0:8080;http://0.0.0.0:8080", + explicitHttpPorts: null, + explicitHttpsPorts: null); + + urls.Should().ContainSingle(); + } + + [Fact] + public void ResolveConfiguredUrls_HttpsOnlyFromPortEnvVar_ReturnsHttpsEndpoint() + { + var urls = ContainerFrontdoorBindingResolver.ResolveConfiguredUrls( + serverUrls: null, + explicitUrls: null, + explicitHttpPorts: null, + explicitHttpsPorts: "443"); + + urls.Should().ContainSingle() + .Which.Should().Match(u => u.Scheme == "https" && u.Port == 443); + } + + [Fact] + public void ResolveConfiguredUrls_MixedPortEnvVars_ReturnsBothSchemes() + { + var urls = ContainerFrontdoorBindingResolver.ResolveConfiguredUrls( + serverUrls: null, + explicitUrls: null, + explicitHttpPorts: "8080", + explicitHttpsPorts: "443"); + + urls.Should().HaveCount(2); + urls.Should().Contain(u => u.Scheme == "http" && u.Port == 8080); + urls.Should().Contain(u => u.Scheme == "https" && u.Port == 443); + } + + /// + /// Comma-separated port values must be split correctly. + /// + [Fact] + public void ResolveConfiguredUrls_CommaSeparatedPorts_SplitsCorrectly() + { + var urls = ContainerFrontdoorBindingResolver.ResolveConfiguredUrls( + serverUrls: null, + explicitUrls: null, + explicitHttpPorts: "8080,9090", + explicitHttpsPorts: null); + + urls.Should().HaveCount(2); + urls.Should().Contain(u => u.Port == 8080); + urls.Should().Contain(u => u.Port == 9090); + } + + /// + /// Invalid or malformed URL entries should be silently skipped. + /// + [Fact] + public void ResolveConfiguredUrls_InvalidUrlEntry_SkippedGracefully() + { + var urls = ContainerFrontdoorBindingResolver.ResolveConfiguredUrls( + serverUrls: null, + explicitUrls: "http://0.0.0.0:8080;not-a-url;https://0.0.0.0:443", + explicitHttpPorts: null, + explicitHttpsPorts: null); + + urls.Should().HaveCount(2); + urls.Should().Contain(u => u.Scheme == "http" && u.Port == 8080); + urls.Should().Contain(u => u.Scheme == "https" && u.Port == 443); + } + + /// + /// Semicolons and commas are both valid delimiters for ASPNETCORE_URLS. + /// + [Fact] + public void ResolveConfiguredUrls_CommaDelimitedUrls_ParsedCorrectly() + { + var urls = ContainerFrontdoorBindingResolver.ResolveConfiguredUrls( + serverUrls: null, + explicitUrls: "http://0.0.0.0:8080,https://0.0.0.0:443", + explicitHttpPorts: null, + explicitHttpsPorts: null); + + urls.Should().HaveCount(2); + urls.Should().Contain(u => u.Scheme == "http" && u.Port == 8080); + urls.Should().Contain(u => u.Scheme == "https" && u.Port == 443); + } } diff --git a/src/Web/StellaOps.Web/e2e/advisory-vex-source-management.e2e.spec.ts b/src/Web/StellaOps.Web/e2e/advisory-vex-source-management.e2e.spec.ts index e3dbe1f35..b795dc802 100644 --- a/src/Web/StellaOps.Web/e2e/advisory-vex-source-management.e2e.spec.ts +++ b/src/Web/StellaOps.Web/e2e/advisory-vex-source-management.e2e.spec.ts @@ -51,23 +51,23 @@ const MOCK_ADVISORY_SOURCES = { function setupSourceApiMocks(page: import('@playwright/test').Page) { // Source management API mocks - page.route('**/api/v1/sources/catalog', (route) => { + page.route('**/api/v1/advisory-sources/catalog', (route) => { route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(MOCK_CATALOG) }); }); - page.route('**/api/v1/sources/status', (route) => { + page.route('**/api/v1/advisory-sources/status', (route) => { route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(MOCK_STATUS) }); }); - page.route('**/api/v1/sources/*/enable', (route) => { + page.route('**/api/v1/advisory-sources/*/enable', (route) => { route.fulfill({ status: 200, contentType: 'application/json', body: '{}' }); }); - page.route('**/api/v1/sources/*/disable', (route) => { + page.route('**/api/v1/advisory-sources/*/disable', (route) => { route.fulfill({ status: 200, contentType: 'application/json', body: '{}' }); }); - page.route('**/api/v1/sources/check', (route) => { + page.route('**/api/v1/advisory-sources/check', (route) => { if (route.request().method() === 'POST') { route.fulfill({ status: 200, @@ -79,7 +79,7 @@ function setupSourceApiMocks(page: import('@playwright/test').Page) { } }); - page.route('**/api/v1/sources/*/check', (route) => { + page.route('**/api/v1/advisory-sources/*/check', (route) => { if (route.request().method() === 'POST') { const url = route.request().url(); const sourceId = url.split('/sources/')[1]?.split('/check')[0] ?? 'unknown'; @@ -101,7 +101,7 @@ function setupSourceApiMocks(page: import('@playwright/test').Page) { } }); - page.route('**/api/v1/sources/*/check-result', (route) => { + page.route('**/api/v1/advisory-sources/*/check-result', (route) => { route.fulfill({ status: 200, contentType: 'application/json', @@ -117,7 +117,7 @@ function setupSourceApiMocks(page: import('@playwright/test').Page) { }); }); - page.route('**/api/v1/sources/batch-enable', (route) => { + page.route('**/api/v1/advisory-sources/batch-enable', (route) => { route.fulfill({ status: 200, contentType: 'application/json', @@ -125,7 +125,7 @@ function setupSourceApiMocks(page: import('@playwright/test').Page) { }); }); - page.route('**/api/v1/sources/batch-disable', (route) => { + page.route('**/api/v1/advisory-sources/batch-disable', (route) => { route.fulfill({ status: 200, contentType: 'application/json', diff --git a/src/Web/StellaOps.Web/e2e/mirror-client-setup.e2e.spec.ts b/src/Web/StellaOps.Web/e2e/mirror-client-setup.e2e.spec.ts index b1af06972..7190ed868 100644 --- a/src/Web/StellaOps.Web/e2e/mirror-client-setup.e2e.spec.ts +++ b/src/Web/StellaOps.Web/e2e/mirror-client-setup.e2e.spec.ts @@ -105,7 +105,7 @@ const MOCK_DOMAIN_LIST = { rateLimits: { indexRequestsPerHour: 60, downloadRequestsPerHour: 120 }, requireAuthentication: false, signing: { enabled: true, algorithm: 'ES256', keyId: 'key-01' }, - domainUrl: '/concelier/exports/security-advisories', + domainUrl: '/concelier/exports/mirror/security-advisories', createdAt: new Date().toISOString(), status: 'active', }, @@ -150,7 +150,7 @@ function setupErrorCollector(page: import('@playwright/test').Page) { /** Set up mocks for the mirror client setup wizard page. */ function setupWizardApiMocks(page: import('@playwright/test').Page) { // Mirror test endpoint (connection check) - page.route('**/api/v1/mirror/test', (route) => { + page.route('**/api/v1/advisory-sources/mirror/test', (route) => { if (route.request().method() === 'POST') { route.fulfill({ status: 200, @@ -163,7 +163,7 @@ function setupWizardApiMocks(page: import('@playwright/test').Page) { }); // Consumer discovery endpoint - page.route('**/api/v1/mirror/consumer/discover', (route) => { + page.route('**/api/v1/advisory-sources/mirror/consumer/discover', (route) => { if (route.request().method() === 'POST') { route.fulfill({ status: 200, @@ -176,7 +176,7 @@ function setupWizardApiMocks(page: import('@playwright/test').Page) { }); // Consumer signature verification endpoint - page.route('**/api/v1/mirror/consumer/verify-signature', (route) => { + page.route('**/api/v1/advisory-sources/mirror/consumer/verify-signature', (route) => { if (route.request().method() === 'POST') { route.fulfill({ status: 200, @@ -189,7 +189,7 @@ function setupWizardApiMocks(page: import('@playwright/test').Page) { }); // Consumer config GET/PUT - page.route('**/api/v1/mirror/consumer', (route) => { + page.route('**/api/v1/advisory-sources/mirror/consumer', (route) => { const method = route.request().method(); if (method === 'GET') { route.fulfill({ @@ -209,7 +209,7 @@ function setupWizardApiMocks(page: import('@playwright/test').Page) { }); // Mirror config - page.route('**/api/v1/mirror/config', (route) => { + page.route('**/api/v1/advisory-sources/mirror/config', (route) => { const method = route.request().method(); if (method === 'GET') { route.fulfill({ @@ -229,7 +229,7 @@ function setupWizardApiMocks(page: import('@playwright/test').Page) { }); // Mirror health summary - page.route('**/api/v1/mirror/health', (route) => { + page.route('**/api/v1/advisory-sources/mirror/health', (route) => { route.fulfill({ status: 200, contentType: 'application/json', @@ -238,7 +238,7 @@ function setupWizardApiMocks(page: import('@playwright/test').Page) { }); // Mirror domains - page.route('**/api/v1/mirror/domains', (route) => { + page.route('**/api/v1/advisory-sources/mirror/domains', (route) => { route.fulfill({ status: 200, contentType: 'application/json', @@ -247,7 +247,7 @@ function setupWizardApiMocks(page: import('@playwright/test').Page) { }); // Mirror import endpoint - page.route('**/api/v1/mirror/import', (route) => { + page.route('**/api/v1/advisory-sources/mirror/import', (route) => { if (route.request().method() === 'POST') { route.fulfill({ status: 200, @@ -260,7 +260,7 @@ function setupWizardApiMocks(page: import('@playwright/test').Page) { }); // Mirror import status - page.route('**/api/v1/mirror/import/status', (route) => { + page.route('**/api/v1/advisory-sources/mirror/import/status', (route) => { route.fulfill({ status: 200, contentType: 'application/json', @@ -288,15 +288,15 @@ function setupWizardApiMocks(page: import('@playwright/test').Page) { /** Set up mocks for catalog and dashboard pages that show mirror integration. */ function setupCatalogDashboardMocks(page: import('@playwright/test').Page) { - page.route('**/api/v1/sources/catalog', (route) => { + page.route('**/api/v1/advisory-sources/catalog', (route) => { route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(MOCK_SOURCE_CATALOG) }); }); - page.route('**/api/v1/sources/status', (route) => { + page.route('**/api/v1/advisory-sources/status', (route) => { route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(MOCK_SOURCE_STATUS) }); }); - page.route('**/api/v1/sources/check', (route) => { + page.route('**/api/v1/advisory-sources/check', (route) => { if (route.request().method() === 'POST') { route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ totalChecked: 3, healthyCount: 2, failedCount: 0 }) }); } else { @@ -304,7 +304,7 @@ function setupCatalogDashboardMocks(page: import('@playwright/test').Page) { } }); - page.route('**/api/v1/sources/*/check', (route) => { + page.route('**/api/v1/advisory-sources/*/check', (route) => { if (route.request().method() === 'POST') { route.fulfill({ status: 200, @@ -316,7 +316,7 @@ function setupCatalogDashboardMocks(page: import('@playwright/test').Page) { } }); - page.route('**/api/v1/sources/*/check-result', (route) => { + page.route('**/api/v1/advisory-sources/*/check-result', (route) => { route.fulfill({ status: 200, contentType: 'application/json', @@ -324,11 +324,11 @@ function setupCatalogDashboardMocks(page: import('@playwright/test').Page) { }); }); - page.route('**/api/v1/sources/batch-enable', (route) => { + page.route('**/api/v1/advisory-sources/batch-enable', (route) => { route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ results: [] }) }); }); - page.route('**/api/v1/sources/batch-disable', (route) => { + page.route('**/api/v1/advisory-sources/batch-disable', (route) => { route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ results: [] }) }); }); @@ -445,7 +445,7 @@ test.describe('Mirror Client Setup Wizard', () => { const ngErrors = setupErrorCollector(page); // Override the mirror test endpoint to return failure - await page.route('**/api/v1/mirror/test', (route) => { + await page.route('**/api/v1/advisory-sources/mirror/test', (route) => { if (route.request().method() === 'POST') { route.fulfill({ status: 200, @@ -458,22 +458,22 @@ test.describe('Mirror Client Setup Wizard', () => { }); // Set up remaining wizard mocks (excluding mirror/test which is overridden above) - await page.route('**/api/v1/mirror/consumer/discover', (route) => { + await page.route('**/api/v1/advisory-sources/mirror/consumer/discover', (route) => { route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(MOCK_DISCOVERY_RESPONSE) }); }); - await page.route('**/api/v1/mirror/consumer/verify-signature', (route) => { + await page.route('**/api/v1/advisory-sources/mirror/consumer/verify-signature', (route) => { route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(MOCK_SIGNATURE_DETECTION) }); }); - await page.route('**/api/v1/mirror/consumer', (route) => { + await page.route('**/api/v1/advisory-sources/mirror/consumer', (route) => { route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(MOCK_CONSUMER_CONFIG) }); }); - await page.route('**/api/v1/mirror/config', (route) => { + await page.route('**/api/v1/advisory-sources/mirror/config', (route) => { route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(MOCK_MIRROR_CONFIG_DIRECT_MODE) }); }); - await page.route('**/api/v1/mirror/health', (route) => { + await page.route('**/api/v1/advisory-sources/mirror/health', (route) => { route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(MOCK_MIRROR_HEALTH) }); }); - await page.route('**/api/v1/mirror/domains', (route) => { + await page.route('**/api/v1/advisory-sources/mirror/domains', (route) => { route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(MOCK_DOMAIN_LIST) }); }); await page.route('**/api/v2/security/**', (route) => { @@ -766,7 +766,7 @@ test.describe('Mirror Dashboard - Consumer Panel', () => { const ngErrors = setupErrorCollector(page); // Mock mirror config as Mirror mode with consumer URL - await page.route('**/api/v1/mirror/config', (route) => { + await page.route('**/api/v1/advisory-sources/mirror/config', (route) => { route.fulfill({ status: 200, contentType: 'application/json', @@ -774,7 +774,7 @@ test.describe('Mirror Dashboard - Consumer Panel', () => { }); }); - await page.route('**/api/v1/mirror/health', (route) => { + await page.route('**/api/v1/advisory-sources/mirror/health', (route) => { route.fulfill({ status: 200, contentType: 'application/json', @@ -782,7 +782,7 @@ test.describe('Mirror Dashboard - Consumer Panel', () => { }); }); - await page.route('**/api/v1/mirror/domains', (route) => { + await page.route('**/api/v1/advisory-sources/mirror/domains', (route) => { route.fulfill({ status: 200, contentType: 'application/json', @@ -840,7 +840,7 @@ test.describe('Advisory Source Catalog - Mirror Integration', () => { await setupCatalogDashboardMocks(page); // Mock mirror config in Direct mode - await page.route('**/api/v1/mirror/config', (route) => { + await page.route('**/api/v1/advisory-sources/mirror/config', (route) => { route.fulfill({ status: 200, contentType: 'application/json', @@ -848,7 +848,7 @@ test.describe('Advisory Source Catalog - Mirror Integration', () => { }); }); - await page.route('**/api/v1/mirror/health', (route) => { + await page.route('**/api/v1/advisory-sources/mirror/health', (route) => { route.fulfill({ status: 200, contentType: 'application/json', @@ -856,7 +856,7 @@ test.describe('Advisory Source Catalog - Mirror Integration', () => { }); }); - await page.route('**/api/v1/mirror/domains', (route) => { + await page.route('**/api/v1/advisory-sources/mirror/domains', (route) => { route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ domains: [], totalCount: 0 }) }); }); diff --git a/src/Web/StellaOps.Web/e2e/topology-setup-wizard.e2e.spec.ts b/src/Web/StellaOps.Web/e2e/topology-setup-wizard.e2e.spec.ts new file mode 100644 index 000000000..c90fb07d4 --- /dev/null +++ b/src/Web/StellaOps.Web/e2e/topology-setup-wizard.e2e.spec.ts @@ -0,0 +1,244 @@ +/** + * Topology Setup Wizard — E2E Tests + * + * Verifies the 8-step wizard for configuring release topology: + * Region → Environment → Stage Order → Target → Agent → Infrastructure → Validate → Done + * + * Sprint: SPRINT_20260315_009_ReleaseOrchestrator_topology_setup_foundation + */ + +import { test, expect } from './fixtures/auth.fixture'; +import { navigateAndWait } from './helpers/nav.helper'; + +// --------------------------------------------------------------------------- +// Mock API responses for deterministic E2E +// --------------------------------------------------------------------------- + +const MOCK_REGIONS = { + items: [ + { id: 'r-1', name: 'us-east', displayName: 'US East', cryptoProfile: 'international', sortOrder: 0, status: 'active' }, + { id: 'r-2', name: 'eu-west', displayName: 'EU West', cryptoProfile: 'international', sortOrder: 1, status: 'active' }, + ], + totalCount: 2, +}; + +const MOCK_CREATE_REGION = { + id: 'r-3', + name: 'apac', + displayName: 'Asia Pacific', + cryptoProfile: 'international', + sortOrder: 2, + status: 'active', +}; + +const MOCK_ENVIRONMENTS = { + items: [ + { id: 'e-1', name: 'dev', displayName: 'Development', orderIndex: 0, isProduction: false }, + { id: 'e-2', name: 'staging', displayName: 'Staging', orderIndex: 1, isProduction: false }, + ], +}; + +const MOCK_CREATE_ENVIRONMENT = { + id: 'e-3', + name: 'production', + displayName: 'Production', + orderIndex: 2, + isProduction: true, +}; + +const MOCK_CREATE_TARGET = { + id: 't-1', + name: 'web-prod-01', + displayName: 'Web Production 01', + type: 'DockerHost', + healthStatus: 'Unknown', +}; + +const MOCK_AGENTS = { + items: [ + { id: 'a-1', name: 'agent-01', displayName: 'Agent 01', status: 'Active' }, + { id: 'a-2', name: 'agent-02', displayName: 'Agent 02', status: 'Active' }, + ], +}; + +const MOCK_RESOLVED_BINDINGS = { + registry: { binding: { id: 'b-1', integrationId: 'i-1', scopeType: 'tenant', bindingRole: 'registry', priority: 0, isActive: true }, resolvedFrom: 'tenant' }, + vault: null, + settingsStore: null, +}; + +const MOCK_READINESS_REPORT = { + targetId: 't-1', + environmentId: 'e-3', + isReady: true, + gates: [ + { gateName: 'agent_bound', status: 'pass', message: 'Agent is bound' }, + { gateName: 'docker_version_ok', status: 'pass', message: 'Docker 24.0.7 meets recommended version.' }, + { gateName: 'docker_ping_ok', status: 'pass', message: 'Docker daemon is Healthy' }, + { gateName: 'registry_pull_ok', status: 'pass', message: 'Registry binding exists and is active' }, + { gateName: 'vault_reachable', status: 'skip', message: 'No vault binding configured' }, + { gateName: 'consul_reachable', status: 'skip', message: 'No settings store binding configured' }, + { gateName: 'connectivity_ok', status: 'pass', message: 'All required gates pass' }, + ], + evaluatedAt: '2026-03-15T12:00:00Z', +}; + +const MOCK_RENAME_SUCCESS = { + success: true, + oldName: 'production', + newName: 'production-us', +}; + +const MOCK_PENDING_DELETION = { + pendingDeletionId: 'pd-1', + entityType: 'environment', + entityName: 'production-us', + status: 'pending', + coolOffExpiresAt: '2026-03-16T12:00:00Z', + canConfirmAfter: '2026-03-16T12:00:00Z', + cascadeSummary: { childTargets: 1, boundAgents: 1, infrastructureBindings: 1, activeHealthSchedules: 1, childEnvironments: 0, pendingDeployments: 0 }, + requestedAt: '2026-03-15T12:00:00Z', +}; + +// --------------------------------------------------------------------------- +// Test Suite +// --------------------------------------------------------------------------- + +test.describe('Topology Setup Wizard', () => { + test.beforeEach(async ({ page }) => { + // Mock all topology API endpoints + await page.route('**/api/v1/regions', async (route) => { + if (route.request().method() === 'GET') { + await route.fulfill({ json: MOCK_REGIONS }); + } else if (route.request().method() === 'POST') { + await route.fulfill({ status: 201, json: MOCK_CREATE_REGION }); + } + }); + + await page.route('**/api/v1/environments', async (route) => { + if (route.request().method() === 'GET') { + await route.fulfill({ json: MOCK_ENVIRONMENTS }); + } else if (route.request().method() === 'POST') { + await route.fulfill({ status: 201, json: MOCK_CREATE_ENVIRONMENT }); + } + }); + + await page.route('**/api/v1/targets', async (route) => { + if (route.request().method() === 'POST') { + await route.fulfill({ status: 201, json: MOCK_CREATE_TARGET }); + } + }); + + await page.route('**/api/v1/agents', async (route) => { + await route.fulfill({ json: MOCK_AGENTS }); + }); + + await page.route('**/api/v1/targets/*/assign-agent', async (route) => { + await route.fulfill({ json: { success: true } }); + }); + + await page.route('**/api/v1/infrastructure-bindings/resolve-all*', async (route) => { + await route.fulfill({ json: MOCK_RESOLVED_BINDINGS }); + }); + + await page.route('**/api/v1/targets/*/validate', async (route) => { + await route.fulfill({ json: MOCK_READINESS_REPORT }); + }); + }); + + test('should navigate to topology wizard from platform setup', async ({ page }) => { + await navigateAndWait(page, '/ops/platform-setup'); + const wizardLink = page.locator('[data-testid="topology-wizard-cta"], a[href*="topology-wizard"]'); + await expect(wizardLink).toBeVisible(); + await wizardLink.click(); + await expect(page).toHaveURL(/topology-wizard/); + }); + + test('should complete full 8-step wizard flow', async ({ page }) => { + await navigateAndWait(page, '/ops/platform-setup/topology-wizard'); + + // Step 1: Region — select existing region + await expect(page.locator('text=Region')).toBeVisible(); + const regionRadio = page.locator('input[type="radio"]').first(); + await regionRadio.click(); + await page.locator('button:has-text("Next")').click(); + + // Step 2: Environment — fill create form + await expect(page.locator('text=Environment')).toBeVisible(); + await page.fill('input[name="envName"], input[placeholder*="name"]', 'production'); + await page.fill('input[name="envDisplayName"], input[placeholder*="display"]', 'Production'); + await page.locator('button:has-text("Next")').click(); + + // Step 3: Stage Order — view and continue + await expect(page.locator('text=Stage Order')).toBeVisible(); + await page.locator('button:has-text("Next")').click(); + + // Step 4: Target — fill create form + await expect(page.locator('text=Target')).toBeVisible(); + await page.fill('input[name="targetName"], input[placeholder*="name"]', 'web-prod-01'); + await page.fill('input[name="targetDisplayName"], input[placeholder*="display"]', 'Web Production 01'); + await page.locator('button:has-text("Next")').click(); + + // Step 5: Agent — select existing agent + await expect(page.locator('text=Agent')).toBeVisible(); + const agentRadio = page.locator('input[type="radio"]').first(); + await agentRadio.click(); + await page.locator('button:has-text("Next")').click(); + + // Step 6: Infrastructure — view resolved bindings + await expect(page.locator('text=Infrastructure')).toBeVisible(); + await expect(page.locator('text=tenant')).toBeVisible(); // inherited from tenant + await page.locator('button:has-text("Next")').click(); + + // Step 7: Validate — verify all gates + await expect(page.locator('text=Validate')).toBeVisible(); + await expect(page.locator('text=pass').first()).toBeVisible(); + await page.locator('button:has-text("Next")').click(); + + // Step 8: Done + await expect(page.locator('text=Done')).toBeVisible(); + }); + + test('should rename an environment', async ({ page }) => { + await page.route('**/api/v1/environments/*/name', async (route) => { + await route.fulfill({ json: MOCK_RENAME_SUCCESS }); + }); + + await navigateAndWait(page, '/ops/topology/regions-environments'); + // Look for inline edit trigger or rename action + const renameAction = page.locator('[data-testid="rename-action"], button:has-text("Rename")').first(); + if (await renameAction.isVisible()) { + await renameAction.click(); + await page.fill('input[data-testid="rename-input"]', 'production-us'); + await page.keyboard.press('Enter'); + await expect(page.locator('text=production-us')).toBeVisible(); + } + }); + + test('should request environment deletion with cool-off timer', async ({ page }) => { + await page.route('**/api/v1/environments/*/request-delete', async (route) => { + await route.fulfill({ status: 202, json: MOCK_PENDING_DELETION }); + }); + + await page.route('**/api/v1/pending-deletions/*/cancel', async (route) => { + await route.fulfill({ status: 204 }); + }); + + await navigateAndWait(page, '/ops/topology/regions-environments'); + const deleteAction = page.locator('[data-testid="delete-action"], button:has-text("Delete")').first(); + if (await deleteAction.isVisible()) { + await deleteAction.click(); + + // Verify cool-off information is shown + await expect(page.locator('text=cool-off, text=cooloff, text=cool off').first()).toBeVisible({ timeout: 3000 }).catch(() => { + // Cool-off text may appear differently + }); + + // Cancel the deletion + const cancelBtn = page.locator('button:has-text("Cancel")'); + if (await cancelBtn.isVisible()) { + await cancelBtn.click(); + } + } + }); +}); diff --git a/src/Web/StellaOps.Web/output/playwright/debug.auth.json b/src/Web/StellaOps.Web/output/playwright/debug.auth.json new file mode 100644 index 000000000..116306760 --- /dev/null +++ b/src/Web/StellaOps.Web/output/playwright/debug.auth.json @@ -0,0 +1,39 @@ +{ + "authenticatedAtUtc": "2026-03-15T11:21:34.569Z", + "baseUrl": "https://stella-ops.local", + "finalUrl": "https://stella-ops.local/mission-control/board?tenant=demo-prod®ions=apac,eu-west,us-east,us-west", + "title": "Dashboard - StellaOps", + "cookies": [], + "storage": { + "localStorageEntries": [ + [ + "stellaops.sidebar.preferences", + "{\"sidebarCollapsed\":false,\"collapsedGroups\":[],\"collapsedSections\":[]}" + ], + [ + "stellaops.theme", + "system" + ] + ], + "sessionStorageEntries": [ + [ + "stellaops.auth.session.full", + "{\"tokens\":{\"accessToken\":\"eyJhbGciOiJSUzI1NiIsImtpZCI6IlRBSU1GTlE3LUFOR1JMU0JOQVlfQ19OSEdPVERVWUo5UlNVVTRSRUQiLCJ0eXAiOiJhdCtqd3QifQ.eyJpc3MiOiJodHRwczovL2F1dGhvcml0eS5zdGVsbGEtb3BzLmxvY2FsLyIsImV4cCI6MTc3MzU3NTQ5MiwiaWF0IjoxNzczNTczNjkyLCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGVtYWlsIG9mZmxpbmVfYWNjZXNzIHVpLnJlYWQgdWkuYWRtaW4gdWkucHJlZmVyZW5jZXMucmVhZCB1aS5wcmVmZXJlbmNlcy53cml0ZSBhdXRob3JpdHk6dGVuYW50cy5yZWFkIGF1dGhvcml0eTp0ZW5hbnRzLndyaXRlIGF1dGhvcml0eTp1c2Vycy5yZWFkIGF1dGhvcml0eTp1c2Vycy53cml0ZSBhdXRob3JpdHk6cm9sZXMucmVhZCBhdXRob3JpdHk6cm9sZXMud3JpdGUgYXV0aG9yaXR5OmNsaWVudHMucmVhZCBhdXRob3JpdHk6Y2xpZW50cy53cml0ZSBhdXRob3JpdHk6dG9rZW5zLnJlYWQgYXV0aG9yaXR5OnRva2Vucy5yZXZva2UgYXV0aG9yaXR5OmJyYW5kaW5nLnJlYWQgYXV0aG9yaXR5OmJyYW5kaW5nLndyaXRlIGF1dGhvcml0eS5hdWRpdC5yZWFkIGdyYXBoOnJlYWQgc2JvbTpyZWFkIHNjYW5uZXI6cmVhZCBwb2xpY3k6cmVhZCBwb2xpY3k6c2ltdWxhdGUgcG9saWN5OmF1dGhvciBwb2xpY3k6cmV2aWV3IHBvbGljeTphcHByb3ZlIHBvbGljeTpydW4gcG9saWN5OmFjdGl2YXRlIHBvbGljeTphdWRpdCBwb2xpY3k6ZWRpdCBwb2xpY3k6b3BlcmF0ZSBwb2xpY3k6cHVibGlzaCBhaXJnYXA6c2VhbCBhaXJnYXA6c3RhdHVzOnJlYWQgb3JjaDpyZWFkIG9yY2g6b3BlcmF0ZSBvcmNoOnF1b3RhIGFuYWx5dGljcy5yZWFkIGFkdmlzb3J5OnJlYWQgYWR2aXNvcnktYWk6dmlldyBhZHZpc29yeS1haTpvcGVyYXRlIHZleDpyZWFkIHZleGh1YjpyZWFkIGV4Y2VwdGlvbnM6cmVhZCBleGNlcHRpb25zOmFwcHJvdmUgYW9jOnZlcmlmeSBmaW5kaW5nczpyZWFkIHJlbGVhc2U6cmVhZCByZWxlYXNlOndyaXRlIHJlbGVhc2U6cHVibGlzaCBzY2hlZHVsZXI6cmVhZCBzY2hlZHVsZXI6b3BlcmF0ZSBub3RpZnkudmlld2VyIG5vdGlmeS5vcGVyYXRvciBub3RpZnkuYWRtaW4gbm90aWZ5LmVzY2FsYXRlIGV2aWRlbmNlOnJlYWQgZXhwb3J0LnZpZXdlciBleHBvcnQub3BlcmF0b3IgZXhwb3J0LmFkbWluIHZ1bG46dmlldyB2dWxuOmludmVzdGlnYXRlIHZ1bG46b3BlcmF0ZSB2dWxuOmF1ZGl0IHBsYXRmb3JtLmNvbnRleHQucmVhZCBwbGF0Zm9ybS5jb250ZXh0LndyaXRlIGRvY3RvcjpydW4gZG9jdG9yOmFkbWluIG9wcy5oZWFsdGggaW50ZWdyYXRpb246cmVhZCBpbnRlZ3JhdGlvbjp3cml0ZSBpbnRlZ3JhdGlvbjpvcGVyYXRlIHBhY2tzLnJlYWQgcGFja3Mud3JpdGUgcGFja3MucnVuIHBhY2tzLmFwcHJvdmUgcmVnaXN0cnkuYWRtaW4gdGltZWxpbmU6cmVhZCB0aW1lbGluZTp3cml0ZSB0cnVzdDpyZWFkIHRydXN0OndyaXRlIHRydXN0OmFkbWluIHNpZ25lcjpyZWFkIHNpZ25lcjpzaWduIHNpZ25lcjpyb3RhdGUgc2lnbmVyOmFkbWluIiwianRpIjoiZmRjZjljNTUtNjc3Yy00ZjM0LWJmMGUtYjM5ZmZlNDUxMGIxIiwic3ViIjoiYjA4NjM5NzQ1ZDY1NDkzNDhkODQzYWEzMTFjOTg5NTgiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJhZG1pbiIsIm5hbWUiOiJhZG1pbiIsInJvbGUiOiJhZG1pbiIsInN0ZWxsYW9wczp0ZW5hbnQiOiJkZW1vLXByb2QiLCJhdXRoX3RpbWUiOjE3NzM1NzM2OTEsIm9pX3Byc3QiOiJzdGVsbGEtb3BzLXVpIiwiY2xpZW50X2lkIjoic3RlbGxhLW9wcy11aSJ9.oKFTVX-rJqwU0QNYmAiz35YJG0gJ0IkM-HhOW-SAJLL77apNI9eZWZWyRPwhpCiOELIGoe2M6G5aZ1cpHH0o3dylgKkaFuBXqrpMUAGfP0TtAHk42fZ9BwawjcsJu-iuPFFv3O2tzeKh6oCr7v-TVkSVexgAixnTGq-O16UU3gdqNFr5_KJ3940OMU8jnHosY-QmNuQ4YWS4K_w1IYz4HukgHPG58QE7UhiN1KE44dgBKHqdpxCLu_9GA1xU3PASkSxe1X3xOcEF3Z7aDkW9naM-vIevomhs6ru9sB8_EbciAwRiVWItT1e5uRx3HOqRdozKzeGDVCyt-hPoFi2wyw\",\"tokenType\":\"Bearer\",\"refreshToken\":\"eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZDQkMtSFM1MTIiLCJraWQiOiJXUjVQSU4tVzI5S0lNTkpRTVBBMU9XS0VYN0tEWjZMV0dYTDdSS0dWIiwidHlwIjoib2lfcmVmdCtqd3QiLCJjdHkiOiJKV1QifQ.bVezsJ-QCQnkPBTnya3TF9vhut4VmeaoMOKt7xcGYGs8CZh5Y1dniAdAZOtAeDnIL_ALsjf92Su5-tISCkfc8CCnGnmn1SCjCUKI4v6P1j6NqJijO1uw4itVDH2gIjlN7wY9nk7_bB8X9lSMBXZ1baloK1660jaYNxJuZcQTLf-teaRAzEPvRz4fvPH98QLB9ZKnRuJxPpVeWPSFlCjFYKQoQmZPf4OGLVLhe85KefAYq4nnTzwsCQgBQacdYTgRvYdK1MVFuF8jUhS6Gx8w7rWEt4736Dfc_mrzScwDYWLXhY2f0EHspnYorce4hDd4_cfWG4YZgZRQZbGxmGXeUA.VpC6N-mafaY9WZ5eVgGJ_g.G8warljh1BBPsGCNmyEFPC_ofifnnXu5k8lLHXEI9XeLkOQxSP0dXoANYtUH1LzAbMOuoLlYVC-yRhTFxzVNzzvcsyjhcS_0TIoOEv5VmAHQynML6qvD7melJJ1jhFpeFlqdSsMy5WC0oBY1nd04eG1tRNFMkkST5WJaJy5Yv6o7gm1hxwRVtr17XyQLVdt_5HvlpmSbkpApDyYw8XwJMBD-2YQsBArvR5Ach8LrDJgzapHo9NVkk0ZgzMdwQqYbpSbro615DksOO80sIZ0Nyj6Xu3PWKphOHKO9pZI0Gjcvqo7qhUB2JaV0t4EvC7NRHpeYEaan1a3pLUFItSoFeOgJX-T-7332w033PO8u_Ogvl9kFlQyT0vekCME1jBM1tvZv2zA-Xca85SGYRfpA3K1jZq1ljE75u1FpUqIXlTHbsob8zaF3aUXGTYguo-BCIvHxhdcXYtfN6dOHYdhW2s3npSc4i8uj8_HCyB6UC2Gu17ODDD-1R7uAfJs4ldsRtlCubYr7gPag2dtGWVU1u6hxeilIfnEE02mD5WOgfQHXHmAMztm_G-DvSklIR2V051ZAJhIb8rJWk6jCraQyvNWxKlB0X_6qp7r_SelTaKrDFXE5Vo12ObBJ6RRp_Uw5n0lqRvgTNhzSODPc8c1d8Xo9emit3zjxOqM2GjSSgdJRBoAuCDkcniOrk7dWgWyjEH27iPYfGIemNtjEqPBi5oka0jM8k7gE6lGz7Lrk586rZu5a4F_VC_ENqQLmAXv6stE7Ddk_I_3SeQ9GqFUNWe08icIZ9497XomdjVHVBmWMlaGnL6aP3RbNRPwIw89Eb9D59yLLoQFtwsP6C8OMPwbH3oYhtj1ghhxdarulYFxo13OndKfIcxoI3BQKh4otDjwEMf5UFFHdoFoSFpH54V82_lgZOS2QvjB1NxBtRNBzSrNGQqZr2tuIreY5gtPyfjKdQ_bdyfgcn57jxcMdBGAytzLLiaOL4I8597V7ARVC6e2JHxDqAZfbkMYR4lXbGT-rFCkEMDcfTckwuC-RGg--gz3JWHd2AUT7xB0-Tw4UV7s-xDxviAucDIBPANvZURxOWIjKTvy4O3OLpMTPPwUrBcvaM6tcfxGZvgpIT7DZ04ztv4T-uy3C0nyczhRavSnGwo5TS6dem1Sne-VzRTCe8scT2r6TAeDsifrHZYGkCLiPGBRT9FKYFGU6aasITyZojFNv_2X5lPSxnwP2FNntg5dGFYvfnt42vdBsOYKPf8W1_3xcqs8SoCovtmRJAJR7vdhetAHKo1FDScVMSY6Wead4zOQKkeQ_9tCwhZ0havZ8RY_beVu2NU02BzZ59SgN2SJk_fWyV2JJ5nVYw-gzeuKpN7ozWf10deA4yVAqjBU479qe5HmUw1INvm1nA3Q152minRJzubj-ZACx6zJf6M0R_EvsARSYw9oHUeSkTMBz-114EVh_eH67TDxFBHvqAGEIZJjtUmc6kfthLMSXMKvLxPl58qQ_UmaH00zu4m3W7oT5dXN-UVxuKKOYB05tuNMZGHaKJvmdx7QObAIwSwfjlFZKsVWI28VXgw5IdauG4Disbt-eWFRpcotKZIgkTDSmuICf0hjn-zm5XFyv0Viob-112mj0Pf2ncECJ3PlXCCcGX_rodagDzkLaqMmnfWw2Rlx6uBXmfp2eHoM3NeveKVDYLMpvMWLmYKi0gXpZbRyZgfI-UQv_QJRE2udcmbuCh_RO58m85TyTeBfYwG1jgF63KzSo8Qv_q3s_sCB5kkgjHqfVmfEydD0P3RrWuWpkJ1kP69G-MOfWWf0uoswBfEGL3vaEufrEaF3mP1BlFfFAJh8nV-J2eoUKe7JztR_AaNvtZVMsXRhz-v7ZgfKekqvQofqM8hlbvZX04jTMj9-wSeqTa8F2NkQNVFHckNPcLg82oReqgFHSsp5UwHAec9-HGe8zZFZSpdOYR6L-ODU7ipg7rTkGsl_XwtULkpV2Cgm9s8RB_LbH2NigiftDgPBtTteQMtmEUhIH80nQvwkitNRgG95Pwqz1wL89rYIAJQZrqo1HC_m_eTmCdG2TrTSiSdhbXV5REKjeKYRoCgkuDL6ZW3_od9bGFCvK3zvx9cwDKvx9ebjaHRPA_Vl4Cys-Cx0utYToEGz_oOf9GGrfR5esZq6Cvac2u1Xp2HMIh3EpRNqOQY19mSthWNVJ_CDOM77y-CuwhmO2Bpi71OqdNF5dfrgIZKExmHz0sXRvNLBRDZnlbHxcvV4-_J3cNrYMa_ZCMTskb4AjrCT2OKQBZVXTpe8x9WYhFU7aL9xJEMWz5Sr8c1fKJFrhIe_rW2kM1EkRhSzeIvSYZPTvxuni0PNrB-e-kc2QLtmIHO9KX7hX2NrniLuCESakzVdUedYxWDElfOCPITaGTW4HcAFx-H2oOIlMtgYCRIsk1IAk97lMCfLfOOACKESRdhscqlbXH-CrTBnND55hvHJQlRGhRmBw3t8oD0B_klq__6z9nfWNXO5SJGr9kUSTi002IZZ1lyxILzI9L1Nv5eHXhZIFUabcmvqCnpps0FhYg9Fm7l_N5OW2Z9ENSbK8lZriu7fOCteQn4JYqM845RoIAmlgN8QRo4vpVpJI3F1hHJois4Q9MjYMSwl84mGoNWRnPqgmV23Uuzy1KMYl0lN4gL_TZlkBJrN4Irpv-qGNFX2HhvppH7_9561xIhmCMqPIcosxwlaVXvA-66VdZQIt7ZRHXqm8VgrI-DkIDnPLMCr1-4BHLLcky1RPCCWsDLaW5tSCvHiGwa2daunPCu61hjm_NPBspW6-iRG7TBCtPkNeq-L_U39kVA4WrfDW-7FWutZ-R2I11cFKz2IXIuzjlAYASnJtmBU-XpPuSsVeOO8qdj9QM9n94XVhH2_7MBd8zHP9xPeL16RLR3N8_5Z2grYl9P4YoxfCihysKZf3I7Ez-TyWdWYmXswy7wjwLDWaU_8eAlXRuJy3L4sfHi1Wip8txYSkFt8Utxtmv-YvMuLEt-x1NI1l8Pcxy-UR5JZH8ppgMd0Wseqe8lyaDD6xfLtjI-6q50Ph805n4_Gzc0MisBFLEO_VWkknBzOeW7u8YW-69LknoN-yW4YJ5IH9C449ZN6mC5aFPxPwcaoJv8LhmHQzvsbsypvMQbiihXXc-Hmr8TViyhXGPuMF4AjDXCpw7Uaej1Ipe26Ybr9ce9AL0V_IfyGBEIABlJMCX5bTnpWz0IZylTdAF1LOyPZQHb9e9MLYmSyF4ohDNQthnQzpoSoP-O5q2MiyU6hJgSkh3eUexblaLG6F39dy9G1_O6NGDZDfxCL92MHny8Ne5lG_EjY7EFVqp-eHYAxmf7DKUY6ijAL6aqQZYtPTX-U7jSZWTTgSvznlVVEKkL77CvXN5ZTR_BGeo6NELwDqEOfA10tAB-GIec_KS6t_3ngsA0HOVNJCI5_PwocaWpmL012_6TtxnpXIejLQoNyGCFZ_eLLYMkehfxpMvgYOqNCDPBRtsgz9IW5ZveylUQ3Ya019-10YjPOrzdbd3URBWlmRbFiPVzB2pCtjlQXU0EUCBC4PU4YjJARVX8TZZZg7As_7MKhrNcmsOY4wX6wzaLcub9ZOQ2lheZYT_YKCaiVJU5rS_7VBLQpUiSzaCf9SI42uGncvv9ysTfVtjk_GwNjkAK0uZqb0g-oEaFvDGce_BkceMM1lyv9oxt5_wZIPplH2Uo_4TEd-zhXCUKDJhnXzWap4fE3dw3BT6NBJ_ZvBshrsLqkFCumdAoD8c1zaXdWhCySfScbmPde8FMhvGHNCbwirL5H1slTdFHc75QFoCdDAtTqiF23K1AjYIAhR-GZOGInUejF-N8YsQLWLje6RGyEWzFW3x083J86jT7kPSohG_T_ko7tt785g0f917AyJOnK-B2MXyXpQi490VYKbqHEPVTRVAd551j-HroH_wzpDjCXpgjThCLIyiXJD816WRRGzXRiIlS83V48nFqyOPugU6PDijizO0nLoWB4YZD9CLPIiOibxoYUqynhZIRYQPkdityqFYuKARdYPr8W3dvh4KUyhuRDl6svFnzgfvhQjWJNmjXEolb3eYUwtV1J5KgnDcAFCg9pb37VgLJzzJhwj8rcjpyBrfleWPoHNyx1BAPbJOFWXwnv2eYpXwLpZDIldipYabizdcVxqsuz9Quz2O8ArAm05VAbEDxa_Fk3a-3gMXj3xdPpD0KDM-rGxxNerOFL5wcCQOJVg7ElZHCMf8cwcTCk2sKV0yeJcOXt61PB2J6PtMEmHLrqiDhlicLGkMftak2JOTEzb2bpdjW1bPy-oKhiOgwtCaKjUu3hor_xHr4fG_H2U1_K_18fmIkeCY8oHELWiIbaNBEM0fyDgL_jPVFo93sMmGPnbSR8Npi3y6YkQL8aA63Qpp_gV7vr0NpoyeMNy6ZWSNN8D2pARyVQEOl3EyJ0JaGy-a6SYAZvwlm8zJwsZoLODX4K7GbhaadDD1HeMkN79erD0xoo6mszMwVXAX4UAmzP7Rua9SmoASZjK2Whk7q13T65mH4sLs0yxjZdhHaULoIlI8gEd51aImaKJh4Gu0FWdQUJGUhI8QigQ2NVrtnSkjK5gSj0JVpwMHPcvvCq1ukmSL1o9Nbe6dNngpwpqpFA-H6YAOslvm3zLk3SuN4_UzYxc7y0XHohKOiQSx7GgCHWqFWh-oQC8GuOBdCt9eR9gG89M_dSJD34xVXe9U0fuoBpGynycm8qh-2B_eA.Tk2yygRgBNbxYVfpQVke6MP8VenSBz4IjhrdCv_z5Uo\",\"scope\":\"openid profile email offline_access ui.read ui.admin ui.preferences.read ui.preferences.write authority:tenants.read authority:tenants.write authority:users.read authority:users.write authority:roles.read authority:roles.write authority:clients.read authority:clients.write authority:tokens.read authority:tokens.revoke authority:branding.read authority:branding.write authority.audit.read graph:read sbom:read scanner:read policy:read policy:simulate policy:author policy:review policy:approve policy:run policy:activate policy:audit policy:edit policy:operate policy:publish airgap:seal airgap:status:read orch:read orch:operate orch:quota analytics.read advisory:read advisory-ai:view advisory-ai:operate vex:read vexhub:read exceptions:read exceptions:approve aoc:verify findings:read release:read release:write release:publish scheduler:read scheduler:operate notify.viewer notify.operator notify.admin notify.escalate evidence:read export.viewer export.operator export.admin vuln:view vuln:investigate vuln:operate vuln:audit platform.context.read platform.context.write doctor:run doctor:admin ops.health integration:read integration:write integration:operate packs.read packs.write packs.run packs.approve registry.admin timeline:read timeline:write trust:read trust:write trust:admin signer:read signer:sign signer:rotate signer:admin\",\"expiresAtEpochMs\":1773575491815},\"identity\":{\"subject\":\"b08639745d6549348d843aa311c98958\",\"name\":\"admin\",\"roles\":[],\"idToken\":\"eyJhbGciOiJSUzI1NiIsImtpZCI6IlRBSU1GTlE3LUFOR1JMU0JOQVlfQ19OSEdPVERVWUo5UlNVVTRSRUQiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2F1dGhvcml0eS5zdGVsbGEtb3BzLmxvY2FsLyIsImV4cCI6MTc3MzU3Njk5MiwiaWF0IjoxNzczNTczNjkyLCJhdWQiOiJzdGVsbGEtb3BzLXVpIiwic3ViIjoiYjA4NjM5NzQ1ZDY1NDkzNDhkODQzYWEzMTFjOTg5NTgiLCJuYW1lIjoiYWRtaW4iLCJhenAiOiJzdGVsbGEtb3BzLXVpIiwibm9uY2UiOiI3ZDNmODVmMi00NDBhLTQ5ODgtOWI4ZC1kY2UxNTkzMGM3MDMiLCJhdF9oYXNoIjoiNUQxbmpsMUNEd0N6YS0xOTVpQTlxUSJ9.VSjZcLIJb98_0NQPLedH6JOeUMjDbl-sYNaKFNtua3DFYQfC5yR3KQ5_yfCzn71Bb9TwtKBtHAeX5tS2xsxcrgQ6EPDYEmY3NM7MgCeFVUq2CPCLiO-LpKPmO6vMK1s1IUOfNmtY7UvcGOTY8XZ6M9clxk3CnZr2z0ltgoo-IdGqxtwzWpDq_U729tZ6gsziHcHWE0mQ9EmLcJZ6noYIgzwuUMYNOiPPcXKMKEq00WR8PgDMEb732r8gxWEzLGQP3s5tDYcVtggqwU1EdAkHL2UipB2LYdwJmzW4Gz9z8vCDirZseZEB0mQsqzK2kT0iwEKVsaufINVWbVZsLoESuA\"},\"dpopKeyThumbprint\":\"ByXU9O7iO1oYm11MRszkZHXaWNHIEc3cHjjtlmLG9x0\",\"issuedAtEpochMs\":1773573691816,\"tenantId\":\"demo-prod\",\"scopes\":[\"advisory-ai:operate\",\"advisory-ai:view\",\"advisory:read\",\"airgap:seal\",\"airgap:status:read\",\"analytics.read\",\"aoc:verify\",\"authority.audit.read\",\"authority:branding.read\",\"authority:branding.write\",\"authority:clients.read\",\"authority:clients.write\",\"authority:roles.read\",\"authority:roles.write\",\"authority:tenants.read\",\"authority:tenants.write\",\"authority:tokens.read\",\"authority:tokens.revoke\",\"authority:users.read\",\"authority:users.write\",\"doctor:admin\",\"doctor:run\",\"email\",\"evidence:read\",\"exceptions:approve\",\"exceptions:read\",\"export.admin\",\"export.operator\",\"export.viewer\",\"findings:read\",\"graph:read\",\"integration:operate\",\"integration:read\",\"integration:write\",\"notify.admin\",\"notify.escalate\",\"notify.operator\",\"notify.viewer\",\"offline_access\",\"openid\",\"ops.health\",\"orch:operate\",\"orch:quota\",\"orch:read\",\"packs.approve\",\"packs.read\",\"packs.run\",\"packs.write\",\"platform.context.read\",\"platform.context.write\",\"policy:activate\",\"policy:approve\",\"policy:audit\",\"policy:author\",\"policy:edit\",\"policy:operate\",\"policy:publish\",\"policy:read\",\"policy:review\",\"policy:run\",\"policy:simulate\",\"profile\",\"registry.admin\",\"release:publish\",\"release:read\",\"release:write\",\"sbom:read\",\"scanner:read\",\"scheduler:operate\",\"scheduler:read\",\"signer:admin\",\"signer:read\",\"signer:rotate\",\"signer:sign\",\"timeline:read\",\"timeline:write\",\"trust:admin\",\"trust:read\",\"trust:write\",\"ui.admin\",\"ui.preferences.read\",\"ui.preferences.write\",\"ui.read\",\"vex:read\",\"vexhub:read\",\"vuln:audit\",\"vuln:investigate\",\"vuln:operate\",\"vuln:view\"],\"audiences\":[],\"authenticationTimeEpochMs\":1773573691000,\"freshAuthActive\":false,\"freshAuthExpiresAtEpochMs\":null}" + ], + [ + "stellaops.auth.session.info", + "{\"subject\":\"b08639745d6549348d843aa311c98958\",\"expiresAtEpochMs\":1773575491815,\"issuedAtEpochMs\":1773573691816,\"dpopKeyThumbprint\":\"ByXU9O7iO1oYm11MRszkZHXaWNHIEc3cHjjtlmLG9x0\",\"tenantId\":\"demo-prod\"}" + ], + [ + "stella-search-session-id", + "c62d0812-f947-404d-9feb-02ab30679244" + ] + ] + }, + "events": { + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [] + }, + "statePath": "output/playwright/debug.state.json" +} diff --git a/src/Web/StellaOps.Web/output/playwright/debug.state.json b/src/Web/StellaOps.Web/output/playwright/debug.state.json new file mode 100644 index 000000000..a1541bb7e --- /dev/null +++ b/src/Web/StellaOps.Web/output/playwright/debug.state.json @@ -0,0 +1,18 @@ +{ + "cookies": [], + "origins": [ + { + "origin": "https://stella-ops.local", + "localStorage": [ + { + "name": "stellaops.sidebar.preferences", + "value": "{\"sidebarCollapsed\":false,\"collapsedGroups\":[],\"collapsedSections\":[]}" + }, + { + "name": "stellaops.theme", + "value": "system" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/Web/StellaOps.Web/output/playwright/first-time-user-audit-auth.report.json b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit-auth.report.json new file mode 100644 index 000000000..818eaad1a --- /dev/null +++ b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit-auth.report.json @@ -0,0 +1,39 @@ +{ + "authenticatedAtUtc": "2026-03-15T22:41:49.282Z", + "baseUrl": "https://stella-ops.local", + "finalUrl": "https://stella-ops.local/mission-control/board?tenant=demo-prod®ions=apac,eu-west,us-east,us-west", + "title": "Dashboard - StellaOps", + "cookies": [], + "storage": { + "localStorageEntries": [ + [ + "stellaops.sidebar.preferences", + "{\"sidebarCollapsed\":false,\"collapsedGroups\":[],\"collapsedSections\":[]}" + ], + [ + "stellaops.theme", + "system" + ] + ], + "sessionStorageEntries": [ + [ + "stellaops.auth.session.full", + "{\"tokens\":{\"accessToken\":\"eyJhbGciOiJSUzI1NiIsImtpZCI6IlRBSU1GTlE3LUFOR1JMU0JOQVlfQ19OSEdPVERVWUo5UlNVVTRSRUQiLCJ0eXAiOiJhdCtqd3QifQ.eyJpc3MiOiJodHRwczovL2F1dGhvcml0eS5zdGVsbGEtb3BzLmxvY2FsLyIsImV4cCI6MTc3MzYxNjMwNSwiaWF0IjoxNzczNjE0NTA1LCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGVtYWlsIG9mZmxpbmVfYWNjZXNzIHVpLnJlYWQgdWkuYWRtaW4gdWkucHJlZmVyZW5jZXMucmVhZCB1aS5wcmVmZXJlbmNlcy53cml0ZSBhdXRob3JpdHk6dGVuYW50cy5yZWFkIGF1dGhvcml0eTp0ZW5hbnRzLndyaXRlIGF1dGhvcml0eTp1c2Vycy5yZWFkIGF1dGhvcml0eTp1c2Vycy53cml0ZSBhdXRob3JpdHk6cm9sZXMucmVhZCBhdXRob3JpdHk6cm9sZXMud3JpdGUgYXV0aG9yaXR5OmNsaWVudHMucmVhZCBhdXRob3JpdHk6Y2xpZW50cy53cml0ZSBhdXRob3JpdHk6dG9rZW5zLnJlYWQgYXV0aG9yaXR5OnRva2Vucy5yZXZva2UgYXV0aG9yaXR5OmJyYW5kaW5nLnJlYWQgYXV0aG9yaXR5OmJyYW5kaW5nLndyaXRlIGF1dGhvcml0eS5hdWRpdC5yZWFkIGdyYXBoOnJlYWQgc2JvbTpyZWFkIHNjYW5uZXI6cmVhZCBwb2xpY3k6cmVhZCBwb2xpY3k6c2ltdWxhdGUgcG9saWN5OmF1dGhvciBwb2xpY3k6cmV2aWV3IHBvbGljeTphcHByb3ZlIHBvbGljeTpydW4gcG9saWN5OmFjdGl2YXRlIHBvbGljeTphdWRpdCBwb2xpY3k6ZWRpdCBwb2xpY3k6b3BlcmF0ZSBwb2xpY3k6cHVibGlzaCBhaXJnYXA6c2VhbCBhaXJnYXA6c3RhdHVzOnJlYWQgb3JjaDpyZWFkIG9yY2g6b3BlcmF0ZSBvcmNoOnF1b3RhIGFuYWx5dGljcy5yZWFkIGFkdmlzb3J5OnJlYWQgYWR2aXNvcnktYWk6dmlldyBhZHZpc29yeS1haTpvcGVyYXRlIHZleDpyZWFkIHZleGh1YjpyZWFkIGV4Y2VwdGlvbnM6cmVhZCBleGNlcHRpb25zOmFwcHJvdmUgYW9jOnZlcmlmeSBmaW5kaW5nczpyZWFkIHJlbGVhc2U6cmVhZCByZWxlYXNlOndyaXRlIHJlbGVhc2U6cHVibGlzaCBzY2hlZHVsZXI6cmVhZCBzY2hlZHVsZXI6b3BlcmF0ZSBub3RpZnkudmlld2VyIG5vdGlmeS5vcGVyYXRvciBub3RpZnkuYWRtaW4gbm90aWZ5LmVzY2FsYXRlIGV2aWRlbmNlOnJlYWQgZXhwb3J0LnZpZXdlciBleHBvcnQub3BlcmF0b3IgZXhwb3J0LmFkbWluIHZ1bG46dmlldyB2dWxuOmludmVzdGlnYXRlIHZ1bG46b3BlcmF0ZSB2dWxuOmF1ZGl0IHBsYXRmb3JtLmNvbnRleHQucmVhZCBwbGF0Zm9ybS5jb250ZXh0LndyaXRlIGRvY3RvcjpydW4gZG9jdG9yOmFkbWluIG9wcy5oZWFsdGggaW50ZWdyYXRpb246cmVhZCBpbnRlZ3JhdGlvbjp3cml0ZSBpbnRlZ3JhdGlvbjpvcGVyYXRlIHBhY2tzLnJlYWQgcGFja3Mud3JpdGUgcGFja3MucnVuIHBhY2tzLmFwcHJvdmUgcmVnaXN0cnkuYWRtaW4gdGltZWxpbmU6cmVhZCB0aW1lbGluZTp3cml0ZSB0cnVzdDpyZWFkIHRydXN0OndyaXRlIHRydXN0OmFkbWluIHNpZ25lcjpyZWFkIHNpZ25lcjpzaWduIHNpZ25lcjpyb3RhdGUgc2lnbmVyOmFkbWluIiwianRpIjoiYzRhZDc1MzctYTQxMy00ZGM3LWE3YjUtNTFjYTYxNjMwN2EwIiwic3ViIjoiYjA4NjM5NzQ1ZDY1NDkzNDhkODQzYWEzMTFjOTg5NTgiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJhZG1pbiIsIm5hbWUiOiJhZG1pbiIsInJvbGUiOiJhZG1pbiIsInN0ZWxsYW9wczp0ZW5hbnQiOiJkZW1vLXByb2QiLCJhdXRoX3RpbWUiOjE3NzM2MTQ1MDUsIm9pX3Byc3QiOiJzdGVsbGEtb3BzLXVpIiwiY2xpZW50X2lkIjoic3RlbGxhLW9wcy11aSJ9.QT4F0WNQs0TsisY99mT-fcoShjXLIG5Xj1R0QUTLZ6MS7evWF7oL-hKjSGddhLtvtrRDYtFHbDfiv0ecExWC2ngGzo7VXJdb0VeHc1kU2aaGmDvsMaz648TqWcQvW2A4yIVBWp1AurGowDB_c45eTJ2BrR4KWTYZBc7L9Vfv-5MfdgauVZtqmfpfedbuT9o6N-pNMdicbZsCrs9qyNcAzaTv_UVMWSTJ_uFP_Rin-YHv_pael489xUwXBNA9pHzTo2cdKaUe9rsjI0Iju89uiwFJ31ZrNLfzQTsKbfwd69Rt-wiwLMcKdoqiVGJy1v1vTiPu-g947j85ENqNrv7rLQ\",\"tokenType\":\"Bearer\",\"refreshToken\":\"eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZDQkMtSFM1MTIiLCJraWQiOiJXUjVQSU4tVzI5S0lNTkpRTVBBMU9XS0VYN0tEWjZMV0dYTDdSS0dWIiwidHlwIjoib2lfcmVmdCtqd3QiLCJjdHkiOiJKV1QifQ.WrJUBE1sQJaegohZx83akQtIltJOIHFkT_4ozprAJpLzYDr3Q4Qn0k0oHrZHi6GO_znNo0Pj8YEil2XAdb4qm3XKpQvjA2otwOb13AgLY5LHi3HkI74h3Bob8t76qoPENt2rlV_BLZmm8HQmYYODai4EGLhDK41QNvRXZ0K2mUKQtSidtdXvyo-AWHkdh5CxK9XYwax0EdjAGtFp9_22gAklv1gCHoH0l8CII-_0RzUczvWtz0tIQYD8nJsUc4IVOMH1dU8R59jxltOR1SBNXLTXIu4wWcaYMCcR2LTfLSDrSlq2FiunlECf1cg60Z92XJoWlD-wjP-OViflAVFePA.g0dXXT8fEc5zYIkvNj90Fw.CAxHJIdy67_85qfsznq-nzgs7XyBb98rmsTxfcvQwoMeKHBSmDhScD_SWRXYCt_aJfjucKXXkFctrbTown5240BhYOZ1h4ZzdaezYpk5oY342WgjegyVMyXgMCDzOzRGZkfAjcEBQGS5v7N1mBdBByQ8Vm8nt_XyGe4IPgdaiBLuQwB1dEOL_85lXIB677hwy0gKTrLZrKmTuMRKmF2uWvH5eDQnwMa_Sm5ktuKsNUTU5bUfAUU8Kr50ppC60qxsDX61Z9F6vbsuWbT119cGLapNlfxadyUbnXEyhkkTOSod96cqYjC27FOBRXEUz05WNIWfoiM5-XxzHdEfU4zGBxJOcMFiNjd7YwiOQB1RY6lXmztBXPyZWkFmdiimvtWKuetVJ1Ho93L_hOchAsOZ7FUcF3hN4G3sQxyoLOyFuNiCpYfM2qrdxfP-libKpzPQhLnxfe2NjywNhjL-ZuvppPRkTuWI5c6mzqCF-E_ry6DEo-rnNTx-GttcbL5E8l-7StfHDW68AaarcmH8fXDNkoatFbsMbPIMrTQ6zjrZ-8Mx_OsiBAla9wy6Q3ZCJy73b8U4zZ9Yh53_TsBczQfbar4lE-yhsYpkh2Udu-2Rh3_DeLtiJ6ZwcoYA6Vnpg7zGNV4DibY7uKMhW_ufE0K4oKhzvlj2FV3nLrsVrMQeWDxqckDpfIDw79o82IWkDiSdzKWYEA4WGWY397HVITkExFzm708iqFrzRw2o_ZsRy71GhdGUbMFmy9JjPt3zXzrJixyYZzXE9ePInrDjniEQeZrEMgxQKRMN5v_Xn6AkxUVxMmO66rOUy4QqhA3xrwmDTUBE2o4YCmQxLj_26y8r3akJ7WeLHhKVM82eBAP72QOIZ-pTmfFyzeQYnoggkKIoMxm3T-498ANALo2dU43_nEmQdgQ8QLm0rsQj7e0cR9ye5ScrfmzEL1tvgOqTnownVRcy1p-2vi6sYyIkw75m_6CQRpD1cMThTMRQDVcJ8xlrQJ4XStf14qb-uEv9JkxnMyEOywah2lbGgmEItjGxgXRWLYipKY13XAr346HLxMCCVKESwd01vcbSkyrj0mhIr7IFOQTl5_M469oTMPut703cuIrQ8PZ8tvw4fjiZtihPJqgO8kaSsBic2OsQkVEYPIkQjt5PvAHGFsviJogRE0RH3ipETfHdzfW_DiZA77bzYdcxwUXppm1NGxm9lD4MamfwvYpUB7Ss0J6cM1CkHW98ESugQLQtgj0-_CnWbqZ3B7ORccdKI7GYEkKmSXzjQE8tF7BVBz8xhPI61PhtTuZ-O7FK__8hBcOMEdMpDHKGxPWXKlq9WBNCuCxErPKMjMHSM93tl5aOPqPVubi9VoBtZi-LyN6Txfl5_MbRyfc5FC0bH2uAGysGjzQ_qYd9eS0iwjFRYLcusiZWTQ5Taa4ajDEBsQY80H01wWQjb35ljdSaV1rYA3Retjv0y0XOTRzZA2Ne5gWcySE88ym4GKbuSPPk4qSgxFllmmz3k_PZB3sIc7mByYOFfy9TkWxdfuz0qhjYpxLvufUC0VfSQF1sGFWD1yI2gkwEUYA4wcM6sZu-_1fAdDlWSJQxcav1zXpiSuHeoQz4rgxARKRjW3USlTUVBeK4E7Qdgf1_6rzqc3pfrnO6y1S7agNnm4TpHIa68Wapa-3Pd1X-XMneCZwL3wx8gdeFeqsplfnj0gHerPTcNZ3Y1ZDIcn_LekI90jj2FJH3gFaUDsUi10yIGJiGBxUMPTLZkthvApvmYRxmAXA_-WRSt5qEpudA-3HJQt-pi2qytgMB_hLbPCeQV_akLSg-WtsnTncx48oIuOtHK71Qejkl6zKWCTWasx5z1SKlcaLGDZV-a9xzs59o8jgt6162O4eneB99aJfXYbvOIwMzPnsytKaKZ7bEZ5PP_UPksmLDbMTNZuebw0_WXv6ytNke5Q_1Nxl2XiTVu4tEySgR79REbmMK7x2IRgvY5w0anMRyAtPEp5OCZFQl_b-Zuf_aTg1HQvibk--NUfQ7UCRmjIdkjtL1OgC7c_CeJGbNYTeGqIUqb2Bpl0DQ-MVSoR7xtoPmE70-MCnc-P3MOm2ANE6tPBZNVr_KyiweBy-BBFarp1upU6ZLsS818bnVDrHUIYD2DbpYBIWXpCifFSO4MI_wRQE_hHnAYlL50I9HqjfX-C7z1KFkIfbqDSJZ-Bkc630I12dFSiDk6W1gB6OLVyExnKV8xIeUd-UE62YjITchB2G08rnyBoDlQfly-Go-TMsR-Ngj0ciwYWDsyYPwTtibYQi3fhToKp9C81wVgE-Gl8CH4QhYxgY6IMNQIRcT18Yve_nn3xH0ze3PcFNY7i-yS9qy-FzPGq9PeGO4u6KZhmcZ5VZIBO8KUuLvUfY3xXaGhjVbPmjuuvd3wCUevbcC2Mwtd5PjhlfIX7lj4a6SBgmGLd_NvmSnew3Xkj4VW31v8tACT_5PpYAMhpCjRIy09NSGBSDBZ_naGCZfwvTtcFCPkl8KzPzTgSkjQ29eINuOjBeS5ChokYZRDhzhFD1P_AT8J2-ERZDuzUCQjCP285UmbAkn5gfbW8wkgm7qkNpEhLrQlLlZ60SESJxx_31ugKTYD6vbRjXCtBn-58rHtgvzxtaAHPgawmRd32Ui9mFWn9jnJUFAK-Agyd2kDhiErRiYMuncD2nhBN-Uv1hYntPgRN2nbLbrd5en7qoRJHS5_II50PlvUhzsw1xCYlFGf3Fkvn4UcX88uqF1vaqNOuWKwNukj_mOr9w1o14tynEYoMx4qC4FEs1ZNHjoDA4IhJkQrDQi4DupTqA5aC1B7oiE97QFL9fV0KdibwmZsDh7tWbeHHkiZdwxO31UuHPPnmhpGNza67e_cKwkzPc-BCTFBZHhejYTkqorLOaOyVqjpxrD0tmN_qQ4Lpx7IgfUr40PJ2Uzr36X-mYHtL4MXVE0Z0f6jPeQiOVszLGAgZaYKMb0ekDWfXNIZT-zdyDboO2pDgS3P32FY6aZ43z-DSQZWFWQUhAo7HsLKc-MSevrxqpiqVxw9J_2S3orsWJAmWZ9BDAq7QOgrb2vQpxeh_qaWZVdTrmFhgh1UL33xlI5CcMl0YEhCQBZU3pdu66LRpVnrglX2TI24aSsL13IxF7qfMFIr7VKd985LcwE7WP2gZW8pvlq5RgP-VQmtGa29Ql8Aeovm6I-3ZeqxFdngTaqHGv8Q0GwZfS_Zy0EIZSp7f4R-L4GK3oHq_RodyIRoCMQ7KzHaGHOgxas3rwqYixX-fTYGSuLNksOl05Urhe7KhuqV5Tp3RCApZR_lA6CWFef5uLTJtY6vbGjhKPuy6iBFAt1iHwG-ox8I7wyFAeMigz4VRKutrl5q5k7Yn9Z0u4aj4sg7hOeqCK743aJj-o2OVScYKWBz3HKMM1URSbWXAxMRPRkwL3hTdIYDnLadYo0vtJ1tIWiaMSfU_0mLJUS4bDhs4UjeNj7ZEdjSP95IkDwgkdlym1MLUE2UzF1TUB37F_7UXryq2V0YG_otLfZ6VZxgh9FS1TH4vZZwDzlzEECt6pe9lQXRFk-LB6ogUqM-v1KcjMhG0VFy44-JhWVX0Z5uYtECe91Ji6n0_ctwZ-xJM3-IXRQqEMOCPZusa0pY3cAki5lqW8zF1FJ_osveCOqsUe3HSnqAojyw_IR9aQz7O8GNv5VPTQubpaQZSBHPzI-ua6fbRd8OnUp6KR0KWMQW5PNF-gU-8x2pMbAHmiNb8WP3OE1b0p_9iz1HXYlwEAKF4gg3ZNtJsiMb0X_cZpLRY5nI56LDLjni1a9OYw79MqKv0wIUkEEW7e48wSBbQ8d6Ugvr4tGLWf0d9D0cWlByOVAMVnszvgUgvsCRHZ5lkpcttE1sxa34Yc1-GGNPpaZdSHfR7nU_AyZgkjbRjOg_692p2OT1v6ZDOpEqYcxQjxkgix7PBh-q85bhfQaFNpDoBH3Dt5Xaqll9I2woEfaDQRDYvT2N4_pavjrBOerLn0pfCdb-Bvazihxg4EQ-YtmgmYtMI6OLoV47O4ACFIcKy0Ul7Bi596ps_IsHZ89KKNSBm__c-kowSyHk1iKwfNfpbIpgq4FnetBLn-hnURqeapx0807XSAYrnb1W3s085TFFV0wmEZJJnEbZ6GxWMtHS5OQDyJDsbmnvaMxnSlOQMz66iuaSkhTCN7m6dykX4HXiKnP5speg9OKfQ-zWCe7z-DUEsqTkKZHln8ixW23iYmduWjLYJiy_EC2g_xrZ4KQIGyxGHajGQq3lQ2kWJ0gCwcMXZWsc85p5GPnwxPjrMEvoyAsayTGY336KH7NNB3OZgmnd6yrle_TpCviogbilMRWFITs2u6LYWl5MHsDBzfh9w7CqOXtnk9T6kXt5Lra_R_GDPYGWmO59Sdgm3migSuOXgtoefQUiIyY6Yzn4GAHSgrX3OE7UWGxwbHQbj0WFLMYI376y1MjQxyoraPovDI3y3fq4zSxiFy-KAfSHfcKZ0I1-iJrg9hT01Vm2hqKYG0rrmu9hM-JMJ4GAFHQQsy4ASUrrjDrtWzUlsskk5d3STbyXb08smWwcapOmwgdB-kZo7aRPqul49JjaKsUFvU2mjFgvIuOczvLHcjZQGmIuIYDGgoh_yDCQSAxwgGH3YxkdrrnGfdbqD9ulQciMhFrLTcds3cyqPB8aM8JRO6IKJRN2_g_b-a128EZ8P1Cneg72U0kMzJNxcaM8uUy0X6hAGvbCf5eu_IoG6wE3iga8g.hxE59aA8_0fucMZ6JSz6BXnEj6mAQdRE_JGTHQEiXEw\",\"scope\":\"openid profile email offline_access ui.read ui.admin ui.preferences.read ui.preferences.write authority:tenants.read authority:tenants.write authority:users.read authority:users.write authority:roles.read authority:roles.write authority:clients.read authority:clients.write authority:tokens.read authority:tokens.revoke authority:branding.read authority:branding.write authority.audit.read graph:read sbom:read scanner:read policy:read policy:simulate policy:author policy:review policy:approve policy:run policy:activate policy:audit policy:edit policy:operate policy:publish airgap:seal airgap:status:read orch:read orch:operate orch:quota analytics.read advisory:read advisory-ai:view advisory-ai:operate vex:read vexhub:read exceptions:read exceptions:approve aoc:verify findings:read release:read release:write release:publish scheduler:read scheduler:operate notify.viewer notify.operator notify.admin notify.escalate evidence:read export.viewer export.operator export.admin vuln:view vuln:investigate vuln:operate vuln:audit platform.context.read platform.context.write doctor:run doctor:admin ops.health integration:read integration:write integration:operate packs.read packs.write packs.run packs.approve registry.admin timeline:read timeline:write trust:read trust:write trust:admin signer:read signer:sign signer:rotate signer:admin\",\"expiresAtEpochMs\":1773616305640},\"identity\":{\"subject\":\"b08639745d6549348d843aa311c98958\",\"name\":\"admin\",\"roles\":[],\"idToken\":\"eyJhbGciOiJSUzI1NiIsImtpZCI6IlRBSU1GTlE3LUFOR1JMU0JOQVlfQ19OSEdPVERVWUo5UlNVVTRSRUQiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2F1dGhvcml0eS5zdGVsbGEtb3BzLmxvY2FsLyIsImV4cCI6MTc3MzYxNzgwNSwiaWF0IjoxNzczNjE0NTA1LCJhdWQiOiJzdGVsbGEtb3BzLXVpIiwic3ViIjoiYjA4NjM5NzQ1ZDY1NDkzNDhkODQzYWEzMTFjOTg5NTgiLCJuYW1lIjoiYWRtaW4iLCJhenAiOiJzdGVsbGEtb3BzLXVpIiwibm9uY2UiOiIwODU4OGY5ZS0xYzNlLTQ5MTEtYjE4MS02MWExYTcwZGY3NmMiLCJhdF9oYXNoIjoiYmhTRkZRUG5CX19naHRpckZHOWxpQSJ9.cHOWnnAlCwE7FFHA4AMzPG31b86-va30g0y7MMKFwLsuGx-rqb8-TakLRlYks1NkxvE2CSqoUcjdAxdP_FiXLu70CbrqfRd_8Vhdpr7yatVyXDZls-fcdPAW0G_cqBizA0I7qtMQc1JhODQ_cskG9BwkKDsmHYkmE_Bt311GBF7sSqJA747qx8ANwD5sOZ91osuxt8U_JXUszJZRkrmMAY9l3DHubuUuXDEcNHPeuKEmaGiUf4jVKomhcwi3vaUWl6zqrG8sfbirH8uKG3Fgj2dBZg95pAADT2y7RekWDUhzZoC5-duhFr4r6ow8nYPqoJ9giJmhKrPOojfl9bY59w\"},\"dpopKeyThumbprint\":\"HQCZVhFq4jlhp0BCE5bAf1_noOOsB5q0Ou_YEs_WlFM\",\"issuedAtEpochMs\":1773614506641,\"tenantId\":\"demo-prod\",\"scopes\":[\"advisory-ai:operate\",\"advisory-ai:view\",\"advisory:read\",\"airgap:seal\",\"airgap:status:read\",\"analytics.read\",\"aoc:verify\",\"authority.audit.read\",\"authority:branding.read\",\"authority:branding.write\",\"authority:clients.read\",\"authority:clients.write\",\"authority:roles.read\",\"authority:roles.write\",\"authority:tenants.read\",\"authority:tenants.write\",\"authority:tokens.read\",\"authority:tokens.revoke\",\"authority:users.read\",\"authority:users.write\",\"doctor:admin\",\"doctor:run\",\"email\",\"evidence:read\",\"exceptions:approve\",\"exceptions:read\",\"export.admin\",\"export.operator\",\"export.viewer\",\"findings:read\",\"graph:read\",\"integration:operate\",\"integration:read\",\"integration:write\",\"notify.admin\",\"notify.escalate\",\"notify.operator\",\"notify.viewer\",\"offline_access\",\"openid\",\"ops.health\",\"orch:operate\",\"orch:quota\",\"orch:read\",\"packs.approve\",\"packs.read\",\"packs.run\",\"packs.write\",\"platform.context.read\",\"platform.context.write\",\"policy:activate\",\"policy:approve\",\"policy:audit\",\"policy:author\",\"policy:edit\",\"policy:operate\",\"policy:publish\",\"policy:read\",\"policy:review\",\"policy:run\",\"policy:simulate\",\"profile\",\"registry.admin\",\"release:publish\",\"release:read\",\"release:write\",\"sbom:read\",\"scanner:read\",\"scheduler:operate\",\"scheduler:read\",\"signer:admin\",\"signer:read\",\"signer:rotate\",\"signer:sign\",\"timeline:read\",\"timeline:write\",\"trust:admin\",\"trust:read\",\"trust:write\",\"ui.admin\",\"ui.preferences.read\",\"ui.preferences.write\",\"ui.read\",\"vex:read\",\"vexhub:read\",\"vuln:audit\",\"vuln:investigate\",\"vuln:operate\",\"vuln:view\"],\"audiences\":[],\"authenticationTimeEpochMs\":1773614505000,\"freshAuthActive\":false,\"freshAuthExpiresAtEpochMs\":null}" + ], + [ + "stellaops.auth.session.info", + "{\"subject\":\"b08639745d6549348d843aa311c98958\",\"expiresAtEpochMs\":1773616305640,\"issuedAtEpochMs\":1773614506641,\"dpopKeyThumbprint\":\"HQCZVhFq4jlhp0BCE5bAf1_noOOsB5q0Ou_YEs_WlFM\",\"tenantId\":\"demo-prod\"}" + ], + [ + "stella-search-session-id", + "c9bd63e5-1b2c-4953-9aa6-e1d068b2676d" + ] + ] + }, + "events": { + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [] + }, + "statePath": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\first-time-user-audit-auth.state.json" +} diff --git a/src/Web/StellaOps.Web/output/playwright/first-time-user-audit-auth.state.json b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit-auth.state.json new file mode 100644 index 000000000..a1541bb7e --- /dev/null +++ b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit-auth.state.json @@ -0,0 +1,18 @@ +{ + "cookies": [], + "origins": [ + { + "origin": "https://stella-ops.local", + "localStorage": [ + { + "name": "stellaops.sidebar.preferences", + "value": "{\"sidebarCollapsed\":false,\"collapsedGroups\":[],\"collapsedSections\":[]}" + }, + { + "name": "stellaops.theme", + "value": "system" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/Web/StellaOps.Web/output/playwright/first-time-user-audit.json b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit.json new file mode 100644 index 000000000..c4e917767 --- /dev/null +++ b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit.json @@ -0,0 +1,2923 @@ +{ + "auditedAt": "2026-03-15T22:43:40.196Z", + "baseUrl": "https://stella-ops.local", + "totalPages": 41, + "totalIssues": 3, + "issuesByPhase": { + "operations": 3 + }, + "results": [ + { + "phase": "setup", + "label": "Landing page (redirect)", + "route": "/", + "finalUrl": "/mission-control/board", + "title": "Dashboard - Stella Ops QA 1773540847164", + "headings": [ + "Dashboard", + "Regional Pipeline", + "Environments at Risk", + "SBOM Findings Snapshot", + "Reachability", + "Nightly Ops Signals", + "Alerts", + "Recent Activity" + ], + "actionCount": 20, + "topActions": [ + { + "tag": "a", + "text": "Skip to main content", + "href": "#main-content", + "disabled": false + }, + { + "tag": "button", + "text": "Release Control", + "href": "", + "disabled": false + }, + { + "tag": "a", + "text": "Dashboard", + "href": "/mission-control/board", + "disabled": false + }, + { + "tag": "a", + "text": "Releases", + "href": "/releases/deployments", + "disabled": false + }, + { + "tag": "a", + "text": "Versions", + "href": "/releases/versions", + "disabled": false + }, + { + "tag": "a", + "text": "Release Health", + "href": "/releases/health", + "disabled": false + }, + { + "tag": "a", + "text": "Approvals", + "href": "/releases/approvals", + "disabled": false + }, + { + "tag": "a", + "text": "Promotions", + "href": "/releases/promotions", + "disabled": false + } + ], + "emptyIndicators": [], + "bodySnippet": "Skip to main contentRelease ControlDashboardReleasesVersionsRelease HealthApprovalsPromotionsHotfixesOperationsScheduled JobsSignalsOffline KitEnvironmentsPolicyPlatform SetupNotificationsSecurity & AuditSecurity PostureTriageSupply-Chain DataReachabilityUnknownsReportsAuditDecision CapsulesReplay &", + "consoleErrors": [], + "responseErrors": [], + "requestFailures": [], + "screenshot": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\first-time-user-audit\\landing-page-redirect-.png" + }, + { + "phase": "setup", + "label": "Dashboard - Mission Board", + "route": "/mission-control/board", + "finalUrl": "/mission-control/board", + "title": "Dashboard - Stella Ops QA 1773540847164", + "headings": [ + "Dashboard", + "Regional Pipeline", + "Environments at Risk", + "SBOM Findings Snapshot", + "Reachability", + "Nightly Ops Signals", + "Alerts", + "Recent Activity" + ], + "actionCount": 20, + "topActions": [ + { + "tag": "a", + "text": "Skip to main content", + "href": "#main-content", + "disabled": false + }, + { + "tag": "button", + "text": "Release Control", + "href": "", + "disabled": false + }, + { + "tag": "a", + "text": "Dashboard", + "href": "/mission-control/board", + "disabled": false + }, + { + "tag": "a", + "text": "Releases", + "href": "/releases/deployments", + "disabled": false + }, + { + "tag": "a", + "text": "Versions", + "href": "/releases/versions", + "disabled": false + }, + { + "tag": "a", + "text": "Release Health", + "href": "/releases/health", + "disabled": false + }, + { + "tag": "a", + "text": "Approvals", + "href": "/releases/approvals", + "disabled": false + }, + { + "tag": "a", + "text": "Promotions", + "href": "/releases/promotions", + "disabled": false + } + ], + "emptyIndicators": [], + "bodySnippet": "Skip to main contentRelease ControlDashboardReleasesVersionsRelease HealthApprovalsPromotionsHotfixesOperationsScheduled JobsSignalsOffline KitEnvironmentsPolicyPlatform SetupNotificationsSecurity & AuditSecurity PostureTriageSupply-Chain DataReachabilityUnknownsReportsAuditDecision CapsulesReplay &", + "consoleErrors": [], + "responseErrors": [], + "requestFailures": [], + "screenshot": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\first-time-user-audit\\dashboard-mission-board.png" + }, + { + "phase": "setup", + "label": "Dashboard - Alerts", + "route": "/mission-control/alerts", + "finalUrl": "/mission-control/alerts", + "title": "Mission Alerts - Stella Ops QA 1773540847164", + "headings": [ + "Mission Alerts" + ], + "actionCount": 20, + "topActions": [ + { + "tag": "a", + "text": "Skip to main content", + "href": "#main-content", + "disabled": false + }, + { + "tag": "button", + "text": "Release Control", + "href": "", + "disabled": false + }, + { + "tag": "a", + "text": "Dashboard", + "href": "/mission-control/board", + "disabled": false + }, + { + "tag": "a", + "text": "Releases", + "href": "/releases/deployments", + "disabled": false + }, + { + "tag": "a", + "text": "Versions", + "href": "/releases/versions", + "disabled": false + }, + { + "tag": "a", + "text": "Release Health", + "href": "/releases/health", + "disabled": false + }, + { + "tag": "a", + "text": "Approvals", + "href": "/releases/approvals", + "disabled": false + }, + { + "tag": "a", + "text": "Promotions", + "href": "/releases/promotions", + "disabled": false + } + ], + "emptyIndicators": [], + "bodySnippet": "Skip to main contentRelease ControlDashboardReleasesVersionsRelease HealthApprovalsPromotionsHotfixesOperationsScheduled JobsSignalsOffline KitEnvironmentsPolicyPlatform SetupNotificationsSecurity & AuditSecurity PostureTriageSupply-Chain DataReachabilityUnknownsReportsAuditDecision CapsulesReplay &", + "consoleErrors": [], + "responseErrors": [], + "requestFailures": [], + "screenshot": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\first-time-user-audit\\dashboard-alerts.png" + }, + { + "phase": "setup", + "label": "Dashboard - Activity", + "route": "/mission-control/activity", + "finalUrl": "/mission-control/activity", + "title": "Mission Activity - Stella Ops QA 1773540847164", + "headings": [ + "Mission Activity", + "Release Runs", + "Evidence", + "Audit" + ], + "actionCount": 20, + "topActions": [ + { + "tag": "a", + "text": "Skip to main content", + "href": "#main-content", + "disabled": false + }, + { + "tag": "button", + "text": "Release Control", + "href": "", + "disabled": false + }, + { + "tag": "a", + "text": "Dashboard", + "href": "/mission-control/board", + "disabled": false + }, + { + "tag": "a", + "text": "Releases", + "href": "/releases/deployments", + "disabled": false + }, + { + "tag": "a", + "text": "Versions", + "href": "/releases/versions", + "disabled": false + }, + { + "tag": "a", + "text": "Release Health", + "href": "/releases/health", + "disabled": false + }, + { + "tag": "a", + "text": "Approvals", + "href": "/releases/approvals", + "disabled": false + }, + { + "tag": "a", + "text": "Promotions", + "href": "/releases/promotions", + "disabled": false + } + ], + "emptyIndicators": [], + "bodySnippet": "Skip to main contentRelease ControlDashboardReleasesVersionsRelease HealthApprovalsPromotionsHotfixesOperationsScheduled JobsSignalsOffline KitEnvironmentsPolicyPlatform SetupNotificationsSecurity & AuditSecurity PostureTriageSupply-Chain DataReachabilityUnknownsReportsAuditDecision CapsulesReplay &", + "consoleErrors": [], + "responseErrors": [], + "requestFailures": [], + "screenshot": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\first-time-user-audit\\dashboard-activity.png" + }, + { + "phase": "releases", + "label": "Releases - Deployments", + "route": "/releases/deployments", + "finalUrl": "/releases/deployments", + "title": "Deployment History - Stella Ops QA 1773540847164", + "headings": [ + "Deployments" + ], + "actionCount": 20, + "topActions": [ + { + "tag": "a", + "text": "Skip to main content", + "href": "#main-content", + "disabled": false + }, + { + "tag": "button", + "text": "Release Control", + "href": "", + "disabled": false + }, + { + "tag": "a", + "text": "Dashboard", + "href": "/mission-control/board", + "disabled": false + }, + { + "tag": "a", + "text": "Releases", + "href": "/releases/deployments", + "disabled": false + }, + { + "tag": "a", + "text": "Versions", + "href": "/releases/versions", + "disabled": false + }, + { + "tag": "a", + "text": "Release Health", + "href": "/releases/health", + "disabled": false + }, + { + "tag": "a", + "text": "Approvals", + "href": "/releases/approvals", + "disabled": false + }, + { + "tag": "a", + "text": "Promotions", + "href": "/releases/promotions", + "disabled": false + } + ], + "emptyIndicators": [], + "bodySnippet": "Skip to main contentRelease ControlDashboardReleasesVersionsRelease HealthApprovalsPromotionsHotfixesOperationsScheduled JobsSignalsOffline KitEnvironmentsPolicyPlatform SetupNotificationsSecurity & AuditSecurity PostureTriageSupply-Chain DataReachabilityUnknownsReportsAuditDecision CapsulesReplay &", + "consoleErrors": [], + "responseErrors": [], + "requestFailures": [], + "screenshot": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\first-time-user-audit\\releases-deployments.png" + }, + { + "phase": "releases", + "label": "Releases - Versions", + "route": "/releases/versions", + "finalUrl": "/releases/versions", + "title": "Release Versions - Stella Ops QA 1773540847164", + "headings": [ + "Release Versions" + ], + "actionCount": 20, + "topActions": [ + { + "tag": "a", + "text": "Skip to main content", + "href": "#main-content", + "disabled": false + }, + { + "tag": "button", + "text": "Release Control", + "href": "", + "disabled": false + }, + { + "tag": "a", + "text": "Dashboard", + "href": "/mission-control/board", + "disabled": false + }, + { + "tag": "a", + "text": "Releases", + "href": "/releases/deployments", + "disabled": false + }, + { + "tag": "a", + "text": "Versions", + "href": "/releases/versions", + "disabled": false + }, + { + "tag": "a", + "text": "Release Health", + "href": "/releases/health", + "disabled": false + }, + { + "tag": "a", + "text": "Approvals", + "href": "/releases/approvals", + "disabled": false + }, + { + "tag": "a", + "text": "Promotions", + "href": "/releases/promotions", + "disabled": false + } + ], + "emptyIndicators": [], + "bodySnippet": "Skip to main contentRelease ControlDashboardReleasesVersionsRelease HealthApprovalsPromotionsHotfixesOperationsScheduled JobsSignalsOffline KitEnvironmentsPolicyPlatform SetupNotificationsSecurity & AuditSecurity PostureTriageSupply-Chain DataReachabilityUnknownsReportsAuditDecision CapsulesReplay &", + "consoleErrors": [], + "responseErrors": [], + "requestFailures": [], + "screenshot": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\first-time-user-audit\\releases-versions.png" + }, + { + "phase": "releases", + "label": "Releases - Approvals", + "route": "/releases/approvals", + "finalUrl": "/releases/approvals", + "title": "Approvals - Stella Ops QA 1773540847164", + "headings": [ + "Release Run Approvals Queue" + ], + "actionCount": 20, + "topActions": [ + { + "tag": "a", + "text": "Skip to main content", + "href": "#main-content", + "disabled": false + }, + { + "tag": "button", + "text": "Release Control", + "href": "", + "disabled": false + }, + { + "tag": "a", + "text": "Dashboard", + "href": "/mission-control/board", + "disabled": false + }, + { + "tag": "a", + "text": "Releases", + "href": "/releases/deployments", + "disabled": false + }, + { + "tag": "a", + "text": "Versions", + "href": "/releases/versions", + "disabled": false + }, + { + "tag": "a", + "text": "Release Health", + "href": "/releases/health", + "disabled": false + }, + { + "tag": "a", + "text": "Approvals", + "href": "/releases/approvals", + "disabled": false + }, + { + "tag": "a", + "text": "Promotions", + "href": "/releases/promotions", + "disabled": false + } + ], + "emptyIndicators": [], + "bodySnippet": "Skip to main contentRelease ControlDashboardReleasesVersionsRelease HealthApprovalsPromotionsHotfixesOperationsScheduled JobsSignalsOffline KitEnvironmentsPolicyPlatform SetupNotificationsSecurity & AuditSecurity PostureTriageSupply-Chain DataReachabilityUnknownsReportsAuditDecision CapsulesReplay &", + "consoleErrors": [], + "responseErrors": [], + "requestFailures": [], + "screenshot": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\first-time-user-audit\\releases-approvals.png" + }, + { + "phase": "releases", + "label": "Releases - Environments", + "route": "/releases/environments", + "finalUrl": "/releases/environments", + "title": "Environments Inventory - Stella Ops QA 1773540847164", + "headings": [ + "Regions & Environments", + "Regions", + "Environments · US East", + "Environment Signals" + ], + "actionCount": 20, + "topActions": [ + { + "tag": "a", + "text": "Skip to main content", + "href": "#main-content", + "disabled": false + }, + { + "tag": "button", + "text": "Release Control", + "href": "", + "disabled": false + }, + { + "tag": "a", + "text": "Dashboard", + "href": "/mission-control/board", + "disabled": false + }, + { + "tag": "a", + "text": "Releases", + "href": "/releases/deployments", + "disabled": false + }, + { + "tag": "a", + "text": "Versions", + "href": "/releases/versions", + "disabled": false + }, + { + "tag": "a", + "text": "Release Health", + "href": "/releases/health", + "disabled": false + }, + { + "tag": "a", + "text": "Approvals", + "href": "/releases/approvals", + "disabled": false + }, + { + "tag": "a", + "text": "Promotions", + "href": "/releases/promotions", + "disabled": false + } + ], + "emptyIndicators": [], + "bodySnippet": "Skip to main contentRelease ControlDashboardReleasesVersionsRelease HealthApprovalsPromotionsHotfixesOperationsScheduled JobsSignalsOffline KitEnvironmentsPolicyPlatform SetupNotificationsSecurity & AuditSecurity PostureTriageSupply-Chain DataReachabilityUnknownsReportsAuditDecision CapsulesReplay &", + "consoleErrors": [], + "responseErrors": [], + "requestFailures": [], + "screenshot": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\first-time-user-audit\\releases-environments.png" + }, + { + "phase": "security", + "label": "Security - Posture", + "route": "/security/posture", + "finalUrl": "/security", + "title": "Security Posture - Stella Ops QA 1773540847164", + "headings": [ + "Security Posture", + "Risk Posture", + "Blocking Items", + "VEX Coverage", + "SBOM Health", + "Reachability Coverage", + "Unknown Reachability" + ], + "actionCount": 20, + "topActions": [ + { + "tag": "a", + "text": "Skip to main content", + "href": "#main-content", + "disabled": false + }, + { + "tag": "button", + "text": "Release Control", + "href": "", + "disabled": false + }, + { + "tag": "a", + "text": "Dashboard", + "href": "/mission-control/board", + "disabled": false + }, + { + "tag": "a", + "text": "Releases", + "href": "/releases/deployments", + "disabled": false + }, + { + "tag": "a", + "text": "Versions", + "href": "/releases/versions", + "disabled": false + }, + { + "tag": "a", + "text": "Release Health", + "href": "/releases/health", + "disabled": false + }, + { + "tag": "a", + "text": "Approvals", + "href": "/releases/approvals", + "disabled": false + }, + { + "tag": "a", + "text": "Promotions", + "href": "/releases/promotions", + "disabled": false + } + ], + "emptyIndicators": [ + "setup_prompt" + ], + "bodySnippet": "Skip to main contentRelease ControlDashboardReleasesVersionsRelease HealthApprovalsPromotionsHotfixesOperationsScheduled JobsSignalsOffline KitEnvironmentsPolicyPlatform SetupNotificationsSecurity & AuditSecurity PostureTriageSupply-Chain DataReachabilityUnknownsReportsAuditDecision CapsulesReplay &", + "consoleErrors": [], + "responseErrors": [], + "requestFailures": [], + "screenshot": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\first-time-user-audit\\security-posture.png" + }, + { + "phase": "security", + "label": "Security - Triage", + "route": "/security/triage", + "finalUrl": "/security/triage", + "title": "Security Triage - Stella Ops QA 1773540847164", + "headings": [ + "Security / Triage", + "Filters", + "Evidence Rail" + ], + "actionCount": 20, + "topActions": [ + { + "tag": "a", + "text": "Skip to main content", + "href": "#main-content", + "disabled": false + }, + { + "tag": "button", + "text": "Release Control", + "href": "", + "disabled": false + }, + { + "tag": "a", + "text": "Dashboard", + "href": "/mission-control/board", + "disabled": false + }, + { + "tag": "a", + "text": "Releases", + "href": "/releases/deployments", + "disabled": false + }, + { + "tag": "a", + "text": "Versions", + "href": "/releases/versions", + "disabled": false + }, + { + "tag": "a", + "text": "Release Health", + "href": "/releases/health", + "disabled": false + }, + { + "tag": "a", + "text": "Approvals", + "href": "/releases/approvals", + "disabled": false + }, + { + "tag": "a", + "text": "Promotions", + "href": "/releases/promotions", + "disabled": false + } + ], + "emptyIndicators": [], + "bodySnippet": "Skip to main contentRelease ControlDashboardReleasesVersionsRelease HealthApprovalsPromotionsHotfixesOperationsScheduled JobsSignalsOffline KitEnvironmentsPolicyPlatform SetupNotificationsSecurity & AuditSecurity PostureTriageSupply-Chain DataReachabilityUnknownsReportsAuditDecision CapsulesReplay &", + "consoleErrors": [], + "responseErrors": [], + "requestFailures": [], + "screenshot": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\first-time-user-audit\\security-triage.png" + }, + { + "phase": "security", + "label": "Security - Disposition", + "route": "/security/disposition", + "finalUrl": "/security/disposition", + "title": "Disposition Center - Stella Ops QA 1773540847164", + "headings": [ + "Security / Advisories & VEX", + "Providers" + ], + "actionCount": 20, + "topActions": [ + { + "tag": "a", + "text": "Skip to main content", + "href": "#main-content", + "disabled": false + }, + { + "tag": "button", + "text": "Release Control", + "href": "", + "disabled": false + }, + { + "tag": "a", + "text": "Dashboard", + "href": "/mission-control/board", + "disabled": false + }, + { + "tag": "a", + "text": "Releases", + "href": "/releases/deployments", + "disabled": false + }, + { + "tag": "a", + "text": "Versions", + "href": "/releases/versions", + "disabled": false + }, + { + "tag": "a", + "text": "Release Health", + "href": "/releases/health", + "disabled": false + }, + { + "tag": "a", + "text": "Approvals", + "href": "/releases/approvals", + "disabled": false + }, + { + "tag": "a", + "text": "Promotions", + "href": "/releases/promotions", + "disabled": false + } + ], + "emptyIndicators": [ + "setup_prompt" + ], + "bodySnippet": "Skip to main contentRelease ControlDashboardReleasesVersionsRelease HealthApprovalsPromotionsHotfixesOperationsScheduled JobsSignalsOffline KitEnvironmentsPolicyPlatform SetupNotificationsSecurity & AuditSecurity PostureTriageSupply-Chain DataReachabilityUnknownsReportsAuditDecision CapsulesReplay &", + "consoleErrors": [], + "responseErrors": [], + "requestFailures": [], + "screenshot": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\first-time-user-audit\\security-disposition.png" + }, + { + "phase": "security", + "label": "Security - Supply Chain", + "route": "/security/supply-chain-data", + "finalUrl": "/security/supply-chain-data", + "title": "Supply-Chain Data - Stella Ops QA 1773540847164", + "headings": [ + "Security / Supply-Chain Data", + "SBOM Viewer" + ], + "actionCount": 20, + "topActions": [ + { + "tag": "a", + "text": "Skip to main content", + "href": "#main-content", + "disabled": false + }, + { + "tag": "button", + "text": "Release Control", + "href": "", + "disabled": false + }, + { + "tag": "a", + "text": "Dashboard", + "href": "/mission-control/board", + "disabled": false + }, + { + "tag": "a", + "text": "Releases", + "href": "/releases/deployments", + "disabled": false + }, + { + "tag": "a", + "text": "Versions", + "href": "/releases/versions", + "disabled": false + }, + { + "tag": "a", + "text": "Release Health", + "href": "/releases/health", + "disabled": false + }, + { + "tag": "a", + "text": "Approvals", + "href": "/releases/approvals", + "disabled": false + }, + { + "tag": "a", + "text": "Promotions", + "href": "/releases/promotions", + "disabled": false + } + ], + "emptyIndicators": [], + "bodySnippet": "Skip to main contentRelease ControlDashboardReleasesVersionsRelease HealthApprovalsPromotionsHotfixesOperationsScheduled JobsSignalsOffline KitEnvironmentsPolicyPlatform SetupNotificationsSecurity & AuditSecurity PostureTriageSupply-Chain DataReachabilityUnknownsReportsAuditDecision CapsulesReplay &", + "consoleErrors": [], + "responseErrors": [], + "requestFailures": [], + "screenshot": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\first-time-user-audit\\security-supply-chain.png" + }, + { + "phase": "security", + "label": "Security - Reachability", + "route": "/security/reachability", + "finalUrl": "/security/reachability", + "title": "Reachability - Stella Ops QA 1773540847164", + "headings": [ + "Reachability", + "Proof of Exposure" + ], + "actionCount": 20, + "topActions": [ + { + "tag": "a", + "text": "Skip to main content", + "href": "#main-content", + "disabled": false + }, + { + "tag": "button", + "text": "Release Control", + "href": "", + "disabled": false + }, + { + "tag": "a", + "text": "Dashboard", + "href": "/mission-control/board", + "disabled": false + }, + { + "tag": "a", + "text": "Releases", + "href": "/releases/deployments", + "disabled": false + }, + { + "tag": "a", + "text": "Versions", + "href": "/releases/versions", + "disabled": false + }, + { + "tag": "a", + "text": "Release Health", + "href": "/releases/health", + "disabled": false + }, + { + "tag": "a", + "text": "Approvals", + "href": "/releases/approvals", + "disabled": false + }, + { + "tag": "a", + "text": "Promotions", + "href": "/releases/promotions", + "disabled": false + } + ], + "emptyIndicators": [], + "bodySnippet": "Skip to main contentRelease ControlDashboardReleasesVersionsRelease HealthApprovalsPromotionsHotfixesOperationsScheduled JobsSignalsOffline KitEnvironmentsPolicyPlatform SetupNotificationsSecurity & AuditSecurity PostureTriageSupply-Chain DataReachabilityUnknownsReportsAuditDecision CapsulesReplay &", + "consoleErrors": [], + "responseErrors": [], + "requestFailures": [], + "screenshot": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\first-time-user-audit\\security-reachability.png" + }, + { + "phase": "evidence", + "label": "Evidence - Overview", + "route": "/evidence/overview", + "finalUrl": "/evidence/overview", + "title": "Evidence Overview - Stella Ops QA 1773540847164", + "headings": [ + "Evidence & Audit", + "Find Evidence", + "Quick Views", + "Shortcuts", + "Related Domains" + ], + "actionCount": 20, + "topActions": [ + { + "tag": "a", + "text": "Skip to main content", + "href": "#main-content", + "disabled": false + }, + { + "tag": "button", + "text": "Release Control", + "href": "", + "disabled": false + }, + { + "tag": "a", + "text": "Dashboard", + "href": "/mission-control/board", + "disabled": false + }, + { + "tag": "a", + "text": "Releases", + "href": "/releases/deployments", + "disabled": false + }, + { + "tag": "a", + "text": "Versions", + "href": "/releases/versions", + "disabled": false + }, + { + "tag": "a", + "text": "Release Health", + "href": "/releases/health", + "disabled": false + }, + { + "tag": "a", + "text": "Approvals", + "href": "/releases/approvals", + "disabled": false + }, + { + "tag": "a", + "text": "Promotions", + "href": "/releases/promotions", + "disabled": false + } + ], + "emptyIndicators": [], + "bodySnippet": "Skip to main contentRelease ControlDashboardReleasesVersionsRelease HealthApprovalsPromotionsHotfixesOperationsScheduled JobsSignalsOffline KitEnvironmentsPolicyPlatform SetupNotificationsSecurity & AuditSecurity PostureTriageSupply-Chain DataReachabilityUnknownsReportsAuditDecision CapsulesReplay &", + "consoleErrors": [], + "responseErrors": [], + "requestFailures": [], + "screenshot": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\first-time-user-audit\\evidence-overview.png" + }, + { + "phase": "evidence", + "label": "Evidence - Capsules", + "route": "/evidence/capsules", + "finalUrl": "/evidence/capsules", + "title": "Decision Capsules - Stella Ops QA 1773540847164", + "headings": [ + "Decision Capsules" + ], + "actionCount": 20, + "topActions": [ + { + "tag": "a", + "text": "Skip to main content", + "href": "#main-content", + "disabled": false + }, + { + "tag": "button", + "text": "Release Control", + "href": "", + "disabled": false + }, + { + "tag": "a", + "text": "Dashboard", + "href": "/mission-control/board", + "disabled": false + }, + { + "tag": "a", + "text": "Releases", + "href": "/releases/deployments", + "disabled": false + }, + { + "tag": "a", + "text": "Versions", + "href": "/releases/versions", + "disabled": false + }, + { + "tag": "a", + "text": "Release Health", + "href": "/releases/health", + "disabled": false + }, + { + "tag": "a", + "text": "Approvals", + "href": "/releases/approvals", + "disabled": false + }, + { + "tag": "a", + "text": "Promotions", + "href": "/releases/promotions", + "disabled": false + } + ], + "emptyIndicators": [], + "bodySnippet": "Skip to main contentRelease ControlDashboardReleasesVersionsRelease HealthApprovalsPromotionsHotfixesOperationsScheduled JobsSignalsOffline KitEnvironmentsPolicyPlatform SetupNotificationsSecurity & AuditSecurity PostureTriageSupply-Chain DataReachabilityUnknownsReportsAuditDecision CapsulesReplay &", + "consoleErrors": [], + "responseErrors": [], + "requestFailures": [], + "screenshot": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\first-time-user-audit\\evidence-capsules.png" + }, + { + "phase": "evidence", + "label": "Evidence - Verify/Replay", + "route": "/evidence/verify-replay", + "finalUrl": "/evidence/verify-replay", + "title": "Replay & Verify - Stella Ops QA 1773540847164", + "headings": [ + "Replay & Verify", + "Request Replay", + "Replay Requests", + "Quick-Verify", + "Determinism Overview" + ], + "actionCount": 20, + "topActions": [ + { + "tag": "a", + "text": "Skip to main content", + "href": "#main-content", + "disabled": false + }, + { + "tag": "button", + "text": "Release Control", + "href": "", + "disabled": false + }, + { + "tag": "a", + "text": "Dashboard", + "href": "/mission-control/board", + "disabled": false + }, + { + "tag": "a", + "text": "Releases", + "href": "/releases/deployments", + "disabled": false + }, + { + "tag": "a", + "text": "Versions", + "href": "/releases/versions", + "disabled": false + }, + { + "tag": "a", + "text": "Release Health", + "href": "/releases/health", + "disabled": false + }, + { + "tag": "a", + "text": "Approvals", + "href": "/releases/approvals", + "disabled": false + }, + { + "tag": "a", + "text": "Promotions", + "href": "/releases/promotions", + "disabled": false + } + ], + "emptyIndicators": [], + "bodySnippet": "Skip to main contentRelease ControlDashboardReleasesVersionsRelease HealthApprovalsPromotionsHotfixesOperationsScheduled JobsSignalsOffline KitEnvironmentsPolicyPlatform SetupNotificationsSecurity & AuditSecurity PostureTriageSupply-Chain DataReachabilityUnknownsReportsAuditDecision CapsulesReplay &", + "consoleErrors": [], + "responseErrors": [], + "requestFailures": [], + "screenshot": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\first-time-user-audit\\evidence-verify-replay.png" + }, + { + "phase": "evidence", + "label": "Evidence - Exports", + "route": "/evidence/exports", + "finalUrl": "/evidence/exports", + "title": "Export Center - Stella Ops QA 1773540847164", + "headings": [ + "Export Center" + ], + "actionCount": 20, + "topActions": [ + { + "tag": "a", + "text": "Skip to main content", + "href": "#main-content", + "disabled": false + }, + { + "tag": "button", + "text": "Release Control", + "href": "", + "disabled": false + }, + { + "tag": "a", + "text": "Dashboard", + "href": "/mission-control/board", + "disabled": false + }, + { + "tag": "a", + "text": "Releases", + "href": "/releases/deployments", + "disabled": false + }, + { + "tag": "a", + "text": "Versions", + "href": "/releases/versions", + "disabled": false + }, + { + "tag": "a", + "text": "Release Health", + "href": "/releases/health", + "disabled": false + }, + { + "tag": "a", + "text": "Approvals", + "href": "/releases/approvals", + "disabled": false + }, + { + "tag": "a", + "text": "Promotions", + "href": "/releases/promotions", + "disabled": false + } + ], + "emptyIndicators": [ + "setup_prompt" + ], + "bodySnippet": "Skip to main contentRelease ControlDashboardReleasesVersionsRelease HealthApprovalsPromotionsHotfixesOperationsScheduled JobsSignalsOffline KitEnvironmentsPolicyPlatform SetupNotificationsSecurity & AuditSecurity PostureTriageSupply-Chain DataReachabilityUnknownsReportsAuditDecision CapsulesReplay &", + "consoleErrors": [], + "responseErrors": [], + "requestFailures": [], + "screenshot": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\first-time-user-audit\\evidence-exports.png" + }, + { + "phase": "policy", + "label": "Policy - Overview", + "route": "/ops/policy/overview", + "finalUrl": "/ops/policy/overview", + "title": "Policy Decisioning Overview - Stella Ops QA 1773540847164", + "headings": [ + "Policy Decisioning Studio", + "One operator shell for policy, VEX, and release gates" + ], + "actionCount": 20, + "topActions": [ + { + "tag": "a", + "text": "Skip to main content", + "href": "#main-content", + "disabled": false + }, + { + "tag": "button", + "text": "Release Control", + "href": "", + "disabled": false + }, + { + "tag": "a", + "text": "Dashboard", + "href": "/mission-control/board", + "disabled": false + }, + { + "tag": "a", + "text": "Releases", + "href": "/releases/deployments", + "disabled": false + }, + { + "tag": "a", + "text": "Versions", + "href": "/releases/versions", + "disabled": false + }, + { + "tag": "a", + "text": "Release Health", + "href": "/releases/health", + "disabled": false + }, + { + "tag": "a", + "text": "Approvals", + "href": "/releases/approvals", + "disabled": false + }, + { + "tag": "a", + "text": "Promotions", + "href": "/releases/promotions", + "disabled": false + } + ], + "emptyIndicators": [], + "bodySnippet": "Skip to main contentRelease ControlDashboardReleasesVersionsRelease HealthApprovalsPromotionsHotfixesOperationsScheduled JobsSignalsOffline KitEnvironmentsPolicyPlatform SetupNotificationsSecurity & AuditSecurity PostureTriageSupply-Chain DataReachabilityUnknownsReportsAuditDecision CapsulesReplay &", + "consoleErrors": [], + "responseErrors": [], + "requestFailures": [], + "screenshot": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\first-time-user-audit\\policy-overview.png" + }, + { + "phase": "policy", + "label": "Policy - Simulation", + "route": "/ops/policy/simulation", + "finalUrl": "/ops/policy/simulation", + "title": "Policy Simulation - Stella Ops QA 1773540847164", + "headings": [ + "Policy Decisioning Studio", + "Policy Simulation Studio", + "Shadow Mode Dashboard" + ], + "actionCount": 20, + "topActions": [ + { + "tag": "a", + "text": "Skip to main content", + "href": "#main-content", + "disabled": false + }, + { + "tag": "button", + "text": "Release Control", + "href": "", + "disabled": false + }, + { + "tag": "a", + "text": "Dashboard", + "href": "/mission-control/board", + "disabled": false + }, + { + "tag": "a", + "text": "Releases", + "href": "/releases/deployments", + "disabled": false + }, + { + "tag": "a", + "text": "Versions", + "href": "/releases/versions", + "disabled": false + }, + { + "tag": "a", + "text": "Release Health", + "href": "/releases/health", + "disabled": false + }, + { + "tag": "a", + "text": "Approvals", + "href": "/releases/approvals", + "disabled": false + }, + { + "tag": "a", + "text": "Promotions", + "href": "/releases/promotions", + "disabled": false + } + ], + "emptyIndicators": [], + "bodySnippet": "Skip to main contentRelease ControlDashboardReleasesVersionsRelease HealthApprovalsPromotionsHotfixesOperationsScheduled JobsSignalsOffline KitEnvironmentsPolicyPlatform SetupNotificationsSecurity & AuditSecurity PostureTriageSupply-Chain DataReachabilityUnknownsReportsAuditDecision CapsulesReplay &", + "consoleErrors": [], + "responseErrors": [], + "requestFailures": [], + "screenshot": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\first-time-user-audit\\policy-simulation.png" + }, + { + "phase": "policy", + "label": "Policy - Baselines", + "route": "/ops/policy/baselines", + "finalUrl": "/ops/policy/baselines", + "title": "Policy Packs - Stella Ops QA 1773540847164", + "headings": [ + "Policy Decisioning Studio", + "Policy packs", + "Core Policy Pack" + ], + "actionCount": 20, + "topActions": [ + { + "tag": "a", + "text": "Skip to main content", + "href": "#main-content", + "disabled": false + }, + { + "tag": "button", + "text": "Release Control", + "href": "", + "disabled": false + }, + { + "tag": "a", + "text": "Dashboard", + "href": "/mission-control/board", + "disabled": false + }, + { + "tag": "a", + "text": "Releases", + "href": "/releases/deployments", + "disabled": false + }, + { + "tag": "a", + "text": "Versions", + "href": "/releases/versions", + "disabled": false + }, + { + "tag": "a", + "text": "Release Health", + "href": "/releases/health", + "disabled": false + }, + { + "tag": "a", + "text": "Approvals", + "href": "/releases/approvals", + "disabled": false + }, + { + "tag": "a", + "text": "Promotions", + "href": "/releases/promotions", + "disabled": false + } + ], + "emptyIndicators": [], + "bodySnippet": "Skip to main contentRelease ControlDashboardReleasesVersionsRelease HealthApprovalsPromotionsHotfixesOperationsScheduled JobsSignalsOffline KitEnvironmentsPolicyPlatform SetupNotificationsSecurity & AuditSecurity PostureTriageSupply-Chain DataReachabilityUnknownsReportsAuditDecision CapsulesReplay &", + "consoleErrors": [], + "responseErrors": [], + "requestFailures": [], + "screenshot": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\first-time-user-audit\\policy-baselines.png" + }, + { + "phase": "policy", + "label": "Policy - Gates", + "route": "/ops/policy/gates", + "finalUrl": "/ops/policy/gates", + "title": "Release Gates - Stella Ops QA 1773540847164", + "headings": [ + "Policy Decisioning Studio", + "Policy gate catalog and release posture", + "Gate Evaluation" + ], + "actionCount": 20, + "topActions": [ + { + "tag": "a", + "text": "Skip to main content", + "href": "#main-content", + "disabled": false + }, + { + "tag": "button", + "text": "Release Control", + "href": "", + "disabled": false + }, + { + "tag": "a", + "text": "Dashboard", + "href": "/mission-control/board", + "disabled": false + }, + { + "tag": "a", + "text": "Releases", + "href": "/releases/deployments", + "disabled": false + }, + { + "tag": "a", + "text": "Versions", + "href": "/releases/versions", + "disabled": false + }, + { + "tag": "a", + "text": "Release Health", + "href": "/releases/health", + "disabled": false + }, + { + "tag": "a", + "text": "Approvals", + "href": "/releases/approvals", + "disabled": false + }, + { + "tag": "a", + "text": "Promotions", + "href": "/releases/promotions", + "disabled": false + } + ], + "emptyIndicators": [], + "bodySnippet": "Skip to main contentRelease ControlDashboardReleasesVersionsRelease HealthApprovalsPromotionsHotfixesOperationsScheduled JobsSignalsOffline KitEnvironmentsPolicyPlatform SetupNotificationsSecurity & AuditSecurity PostureTriageSupply-Chain DataReachabilityUnknownsReportsAuditDecision CapsulesReplay &", + "consoleErrors": [], + "responseErrors": [], + "requestFailures": [], + "screenshot": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\first-time-user-audit\\policy-gates.png" + }, + { + "phase": "policy", + "label": "Policy - Waivers", + "route": "/ops/policy/waivers", + "finalUrl": "/ops/policy/waivers", + "title": "VEX & Exceptions - Stella Ops QA 1773540847164", + "headings": [ + "Policy Decisioning Studio", + "Exception Center", + "Exception Center" + ], + "actionCount": 20, + "topActions": [ + { + "tag": "a", + "text": "Skip to main content", + "href": "#main-content", + "disabled": false + }, + { + "tag": "button", + "text": "Release Control", + "href": "", + "disabled": false + }, + { + "tag": "a", + "text": "Dashboard", + "href": "/mission-control/board", + "disabled": false + }, + { + "tag": "a", + "text": "Releases", + "href": "/releases/deployments", + "disabled": false + }, + { + "tag": "a", + "text": "Versions", + "href": "/releases/versions", + "disabled": false + }, + { + "tag": "a", + "text": "Release Health", + "href": "/releases/health", + "disabled": false + }, + { + "tag": "a", + "text": "Approvals", + "href": "/releases/approvals", + "disabled": false + }, + { + "tag": "a", + "text": "Promotions", + "href": "/releases/promotions", + "disabled": false + } + ], + "emptyIndicators": [], + "bodySnippet": "Skip to main contentRelease ControlDashboardReleasesVersionsRelease HealthApprovalsPromotionsHotfixesOperationsScheduled JobsSignalsOffline KitEnvironmentsPolicyPlatform SetupNotificationsSecurity & AuditSecurity PostureTriageSupply-Chain DataReachabilityUnknownsReportsAuditDecision CapsulesReplay &", + "consoleErrors": [], + "responseErrors": [], + "requestFailures": [], + "screenshot": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\first-time-user-audit\\policy-waivers.png" + }, + { + "phase": "operations", + "label": "Operations - Hub", + "route": "/ops/operations", + "finalUrl": "/ops/operations", + "title": "Operations - Stella Ops QA 1773540847164", + "headings": [ + "Operations", + "Blocking", + "Blocking", + "Execution", + "Health", + "Supply And Airgap", + "Capacity And Setup Boundary", + "Pending Operator Actions", + "Setup Boundary" + ], + "actionCount": 20, + "topActions": [ + { + "tag": "a", + "text": "Skip to main content", + "href": "#main-content", + "disabled": false + }, + { + "tag": "button", + "text": "Release Control", + "href": "", + "disabled": false + }, + { + "tag": "a", + "text": "Dashboard", + "href": "/mission-control/board", + "disabled": false + }, + { + "tag": "a", + "text": "Releases", + "href": "/releases/deployments", + "disabled": false + }, + { + "tag": "a", + "text": "Versions", + "href": "/releases/versions", + "disabled": false + }, + { + "tag": "a", + "text": "Release Health", + "href": "/releases/health", + "disabled": false + }, + { + "tag": "a", + "text": "Approvals", + "href": "/releases/approvals", + "disabled": false + }, + { + "tag": "a", + "text": "Promotions", + "href": "/releases/promotions", + "disabled": false + } + ], + "emptyIndicators": [], + "bodySnippet": "Skip to main contentRelease ControlDashboardReleasesVersionsRelease HealthApprovalsPromotionsHotfixesOperationsScheduled JobsSignalsOffline KitEnvironmentsPolicyPlatform SetupNotificationsSecurity & AuditSecurity PostureTriageSupply-Chain DataReachabilityUnknownsReportsAuditDecision CapsulesReplay &", + "consoleErrors": [], + "responseErrors": [], + "requestFailures": [], + "screenshot": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\first-time-user-audit\\operations-hub.png" + }, + { + "phase": "operations", + "label": "Operations - JobEngine", + "route": "/ops/operations/jobengine", + "finalUrl": "/ops/operations/jobengine", + "title": "JobEngine - Stella Ops QA 1773540847164", + "headings": [ + "JobEngine", + "Jobs", + "Execution Quotas", + "Dead-Letter Recovery", + "Your Access" + ], + "actionCount": 20, + "topActions": [ + { + "tag": "a", + "text": "Skip to main content", + "href": "#main-content", + "disabled": false + }, + { + "tag": "button", + "text": "Release Control", + "href": "", + "disabled": false + }, + { + "tag": "a", + "text": "Dashboard", + "href": "/mission-control/board", + "disabled": false + }, + { + "tag": "a", + "text": "Releases", + "href": "/releases/deployments", + "disabled": false + }, + { + "tag": "a", + "text": "Versions", + "href": "/releases/versions", + "disabled": false + }, + { + "tag": "a", + "text": "Release Health", + "href": "/releases/health", + "disabled": false + }, + { + "tag": "a", + "text": "Approvals", + "href": "/releases/approvals", + "disabled": false + }, + { + "tag": "a", + "text": "Promotions", + "href": "/releases/promotions", + "disabled": false + } + ], + "emptyIndicators": [], + "bodySnippet": "Skip to main contentRelease ControlDashboardReleasesVersionsRelease HealthApprovalsPromotionsHotfixesOperationsScheduled JobsSignalsOffline KitEnvironmentsPolicyPlatform SetupNotificationsSecurity & AuditSecurity PostureTriageSupply-Chain DataReachabilityUnknownsReportsAuditDecision CapsulesReplay &", + "consoleErrors": [], + "responseErrors": [], + "requestFailures": [], + "screenshot": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\first-time-user-audit\\operations-jobengine.png" + }, + { + "phase": "operations", + "label": "Operations - System Health", + "route": "/ops/operations/system-health", + "finalUrl": "/ops/operations/system-health", + "title": "System Health - Stella Ops QA 1773540847164", + "headings": [ + "System Health", + "Service Health" + ], + "actionCount": 20, + "topActions": [ + { + "tag": "a", + "text": "Skip to main content", + "href": "#main-content", + "disabled": false + }, + { + "tag": "button", + "text": "Release Control", + "href": "", + "disabled": false + }, + { + "tag": "a", + "text": "Dashboard", + "href": "/mission-control/board", + "disabled": false + }, + { + "tag": "a", + "text": "Releases", + "href": "/releases/deployments", + "disabled": false + }, + { + "tag": "a", + "text": "Versions", + "href": "/releases/versions", + "disabled": false + }, + { + "tag": "a", + "text": "Release Health", + "href": "/releases/health", + "disabled": false + }, + { + "tag": "a", + "text": "Approvals", + "href": "/releases/approvals", + "disabled": false + }, + { + "tag": "a", + "text": "Promotions", + "href": "/releases/promotions", + "disabled": false + } + ], + "emptyIndicators": [ + "error_text" + ], + "bodySnippet": "Skip to main contentRelease ControlDashboardReleasesVersionsRelease HealthApprovalsPromotionsHotfixesOperationsScheduled JobsSignalsOffline KitEnvironmentsPolicyPlatform SetupNotificationsSecurity & AuditSecurity PostureTriageSupply-Chain DataReachabilityUnknownsReportsAuditDecision CapsulesReplay &", + "consoleErrors": [], + "responseErrors": [], + "requestFailures": [], + "screenshot": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\first-time-user-audit\\operations-system-health.png" + }, + { + "phase": "operations", + "label": "Operations - Doctor", + "route": "/ops/operations/doctor", + "finalUrl": "/ops/operations/doctor", + "title": "Doctor Diagnostics - Stella Ops QA 1773540847164", + "headings": [ + "Doctor Diagnostics", + "Doctor Packs" + ], + "actionCount": 20, + "topActions": [ + { + "tag": "a", + "text": "Skip to main content", + "href": "#main-content", + "disabled": false + }, + { + "tag": "button", + "text": "Release Control", + "href": "", + "disabled": false + }, + { + "tag": "a", + "text": "Dashboard", + "href": "/mission-control/board", + "disabled": false + }, + { + "tag": "a", + "text": "Releases", + "href": "/releases/deployments", + "disabled": false + }, + { + "tag": "a", + "text": "Versions", + "href": "/releases/versions", + "disabled": false + }, + { + "tag": "a", + "text": "Release Health", + "href": "/releases/health", + "disabled": false + }, + { + "tag": "a", + "text": "Approvals", + "href": "/releases/approvals", + "disabled": false + }, + { + "tag": "a", + "text": "Promotions", + "href": "/releases/promotions", + "disabled": false + } + ], + "emptyIndicators": [], + "bodySnippet": "Skip to main contentRelease ControlDashboardReleasesVersionsRelease HealthApprovalsPromotionsHotfixesOperationsScheduled JobsSignalsOffline KitEnvironmentsPolicyPlatform SetupNotificationsSecurity & AuditSecurity PostureTriageSupply-Chain DataReachabilityUnknownsReportsAuditDecision CapsulesReplay &", + "consoleErrors": [], + "responseErrors": [], + "requestFailures": [], + "screenshot": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\first-time-user-audit\\operations-doctor.png" + }, + { + "phase": "operations", + "label": "Operations - Signals", + "route": "/ops/operations/signals", + "finalUrl": "/ops/operations/signals", + "title": "Signals - Stella Ops QA 1773540847164", + "headings": [ + "Signals Runtime Dashboard", + "By provider", + "By status", + "Probe health by host" + ], + "actionCount": 20, + "topActions": [ + { + "tag": "a", + "text": "Skip to main content", + "href": "#main-content", + "disabled": false + }, + { + "tag": "button", + "text": "Release Control", + "href": "", + "disabled": false + }, + { + "tag": "a", + "text": "Dashboard", + "href": "/mission-control/board", + "disabled": false + }, + { + "tag": "a", + "text": "Releases", + "href": "/releases/deployments", + "disabled": false + }, + { + "tag": "a", + "text": "Versions", + "href": "/releases/versions", + "disabled": false + }, + { + "tag": "a", + "text": "Release Health", + "href": "/releases/health", + "disabled": false + }, + { + "tag": "a", + "text": "Approvals", + "href": "/releases/approvals", + "disabled": false + }, + { + "tag": "a", + "text": "Promotions", + "href": "/releases/promotions", + "disabled": false + } + ], + "emptyIndicators": [ + "error_text" + ], + "bodySnippet": "Skip to main contentRelease ControlDashboardReleasesVersionsRelease HealthApprovalsPromotionsHotfixesOperationsScheduled JobsSignalsOffline KitEnvironmentsPolicyPlatform SetupNotificationsSecurity & AuditSecurity PostureTriageSupply-Chain DataReachabilityUnknownsReportsAuditDecision CapsulesReplay &", + "consoleErrors": [], + "responseErrors": [], + "requestFailures": [], + "screenshot": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\first-time-user-audit\\operations-signals.png" + }, + { + "phase": "operations", + "label": "Operations - Dead Letter", + "route": "/ops/operations/dead-letter", + "finalUrl": "/ops/operations/dead-letter", + "title": "Dead-Letter Queue - Stella Ops QA 1773540847164", + "headings": [ + "Dead-Letter Queue Management", + "Error Distribution (Last 7 days)", + "By Tenant", + "Queue Browser" + ], + "actionCount": 20, + "topActions": [ + { + "tag": "a", + "text": "Skip to main content", + "href": "#main-content", + "disabled": false + }, + { + "tag": "button", + "text": "Release Control", + "href": "", + "disabled": false + }, + { + "tag": "a", + "text": "Dashboard", + "href": "/mission-control/board", + "disabled": false + }, + { + "tag": "a", + "text": "Releases", + "href": "/releases/deployments", + "disabled": false + }, + { + "tag": "a", + "text": "Versions", + "href": "/releases/versions", + "disabled": false + }, + { + "tag": "a", + "text": "Release Health", + "href": "/releases/health", + "disabled": false + }, + { + "tag": "a", + "text": "Approvals", + "href": "/releases/approvals", + "disabled": false + }, + { + "tag": "a", + "text": "Promotions", + "href": "/releases/promotions", + "disabled": false + } + ], + "emptyIndicators": [ + "error_text" + ], + "bodySnippet": "Skip to main contentRelease ControlDashboardReleasesVersionsRelease HealthApprovalsPromotionsHotfixesOperationsScheduled JobsSignalsOffline KitEnvironmentsPolicyPlatform SetupNotificationsSecurity & AuditSecurity PostureTriageSupply-Chain DataReachabilityUnknownsReportsAuditDecision CapsulesReplay &", + "consoleErrors": [], + "responseErrors": [], + "requestFailures": [], + "screenshot": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\first-time-user-audit\\operations-dead-letter.png" + }, + { + "phase": "integrations", + "label": "Integrations - Hub", + "route": "/setup/integrations", + "finalUrl": "/setup/integrations", + "title": "Integrations - Stella Ops QA 1773540847164", + "headings": [ + "Integrations", + "Integrations", + "Suggested Setup Order", + "Recent Activity" + ], + "actionCount": 20, + "topActions": [ + { + "tag": "a", + "text": "Skip to main content", + "href": "#main-content", + "disabled": false + }, + { + "tag": "button", + "text": "Release Control", + "href": "", + "disabled": false + }, + { + "tag": "a", + "text": "Dashboard", + "href": "/mission-control/board", + "disabled": false + }, + { + "tag": "a", + "text": "Releases", + "href": "/releases/deployments", + "disabled": false + }, + { + "tag": "a", + "text": "Versions", + "href": "/releases/versions", + "disabled": false + }, + { + "tag": "a", + "text": "Release Health", + "href": "/releases/health", + "disabled": false + }, + { + "tag": "a", + "text": "Approvals", + "href": "/releases/approvals", + "disabled": false + }, + { + "tag": "a", + "text": "Promotions", + "href": "/releases/promotions", + "disabled": false + } + ], + "emptyIndicators": [ + "setup_prompt" + ], + "bodySnippet": "Skip to main contentRelease ControlDashboardReleasesVersionsRelease HealthApprovalsPromotionsHotfixesOperationsScheduled JobsSignalsOffline KitEnvironmentsPolicyPlatform SetupNotificationsSecurity & AuditSecurity PostureTriageSupply-Chain DataReachabilityUnknownsReportsAuditDecision CapsulesReplay &", + "consoleErrors": [], + "responseErrors": [], + "requestFailures": [], + "screenshot": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\first-time-user-audit\\integrations-hub.png" + }, + { + "phase": "integrations", + "label": "Integrations - Advisory/VEX", + "route": "/setup/integrations/advisory-vex-sources", + "finalUrl": "/setup/integrations/advisory-vex-sources", + "title": "Advisory & VEX Sources - Stella Ops QA 1773540847164", + "headings": [ + "Integrations", + "Advisory & VEX Source Catalog", + "▶ Primary (4)", + "▶ Vendor (14)", + "▶ Distribution (10)", + "▶ Ecosystem (9)", + "▶ Cert (15)", + "▶ Csaf (3)", + "▶ Threat (4)", + "▶ Exploit (3)" + ], + "actionCount": 20, + "topActions": [ + { + "tag": "a", + "text": "Skip to main content", + "href": "#main-content", + "disabled": false + }, + { + "tag": "button", + "text": "Release Control", + "href": "", + "disabled": false + }, + { + "tag": "a", + "text": "Dashboard", + "href": "/mission-control/board", + "disabled": false + }, + { + "tag": "a", + "text": "Releases", + "href": "/releases/deployments", + "disabled": false + }, + { + "tag": "a", + "text": "Versions", + "href": "/releases/versions", + "disabled": false + }, + { + "tag": "a", + "text": "Release Health", + "href": "/releases/health", + "disabled": false + }, + { + "tag": "a", + "text": "Approvals", + "href": "/releases/approvals", + "disabled": false + }, + { + "tag": "a", + "text": "Promotions", + "href": "/releases/promotions", + "disabled": false + } + ], + "emptyIndicators": [ + "setup_prompt" + ], + "bodySnippet": "Skip to main contentRelease ControlDashboardReleasesVersionsRelease HealthApprovalsPromotionsHotfixesOperationsScheduled JobsSignalsOffline KitEnvironmentsPolicyPlatform SetupNotificationsSecurity & AuditSecurity PostureTriageSupply-Chain DataReachabilityUnknownsReportsAuditDecision CapsulesReplay &", + "consoleErrors": [], + "responseErrors": [], + "requestFailures": [], + "screenshot": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\first-time-user-audit\\integrations-advisory-vex.png" + }, + { + "phase": "integrations", + "label": "Integrations - Registries", + "route": "/setup/integrations/registries", + "finalUrl": "/setup/integrations/registries", + "title": "Registries - Stella Ops QA 1773540847164", + "headings": [ + "Integrations", + "Registry Integrations" + ], + "actionCount": 20, + "topActions": [ + { + "tag": "a", + "text": "Skip to main content", + "href": "#main-content", + "disabled": false + }, + { + "tag": "button", + "text": "Release Control", + "href": "", + "disabled": false + }, + { + "tag": "a", + "text": "Dashboard", + "href": "/mission-control/board", + "disabled": false + }, + { + "tag": "a", + "text": "Releases", + "href": "/releases/deployments", + "disabled": false + }, + { + "tag": "a", + "text": "Versions", + "href": "/releases/versions", + "disabled": false + }, + { + "tag": "a", + "text": "Release Health", + "href": "/releases/health", + "disabled": false + }, + { + "tag": "a", + "text": "Approvals", + "href": "/releases/approvals", + "disabled": false + }, + { + "tag": "a", + "text": "Promotions", + "href": "/releases/promotions", + "disabled": false + } + ], + "emptyIndicators": [], + "bodySnippet": "Skip to main contentRelease ControlDashboardReleasesVersionsRelease HealthApprovalsPromotionsHotfixesOperationsScheduled JobsSignalsOffline KitEnvironmentsPolicyPlatform SetupNotificationsSecurity & AuditSecurity PostureTriageSupply-Chain DataReachabilityUnknownsReportsAuditDecision CapsulesReplay &", + "consoleErrors": [], + "responseErrors": [], + "requestFailures": [], + "screenshot": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\first-time-user-audit\\integrations-registries.png" + }, + { + "phase": "integrations", + "label": "Integrations - SCM", + "route": "/setup/integrations/scm", + "finalUrl": "/setup/integrations/scm", + "title": "Source Control - Stella Ops QA 1773540847164", + "headings": [ + "Integrations", + "Scm Integrations" + ], + "actionCount": 20, + "topActions": [ + { + "tag": "a", + "text": "Skip to main content", + "href": "#main-content", + "disabled": false + }, + { + "tag": "button", + "text": "Release Control", + "href": "", + "disabled": false + }, + { + "tag": "a", + "text": "Dashboard", + "href": "/mission-control/board", + "disabled": false + }, + { + "tag": "a", + "text": "Releases", + "href": "/releases/deployments", + "disabled": false + }, + { + "tag": "a", + "text": "Versions", + "href": "/releases/versions", + "disabled": false + }, + { + "tag": "a", + "text": "Release Health", + "href": "/releases/health", + "disabled": false + }, + { + "tag": "a", + "text": "Approvals", + "href": "/releases/approvals", + "disabled": false + }, + { + "tag": "a", + "text": "Promotions", + "href": "/releases/promotions", + "disabled": false + } + ], + "emptyIndicators": [], + "bodySnippet": "Skip to main contentRelease ControlDashboardReleasesVersionsRelease HealthApprovalsPromotionsHotfixesOperationsScheduled JobsSignalsOffline KitEnvironmentsPolicyPlatform SetupNotificationsSecurity & AuditSecurity PostureTriageSupply-Chain DataReachabilityUnknownsReportsAuditDecision CapsulesReplay &", + "consoleErrors": [], + "responseErrors": [], + "requestFailures": [], + "screenshot": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\first-time-user-audit\\integrations-scm.png" + }, + { + "phase": "integrations", + "label": "Integrations - Secrets", + "route": "/setup/integrations/secrets", + "finalUrl": "/setup/integrations/secrets", + "title": "Secrets - Stella Ops QA 1773540847164", + "headings": [ + "Integrations", + "RepoSource Integrations" + ], + "actionCount": 20, + "topActions": [ + { + "tag": "a", + "text": "Skip to main content", + "href": "#main-content", + "disabled": false + }, + { + "tag": "button", + "text": "Release Control", + "href": "", + "disabled": false + }, + { + "tag": "a", + "text": "Dashboard", + "href": "/mission-control/board", + "disabled": false + }, + { + "tag": "a", + "text": "Releases", + "href": "/releases/deployments", + "disabled": false + }, + { + "tag": "a", + "text": "Versions", + "href": "/releases/versions", + "disabled": false + }, + { + "tag": "a", + "text": "Release Health", + "href": "/releases/health", + "disabled": false + }, + { + "tag": "a", + "text": "Approvals", + "href": "/releases/approvals", + "disabled": false + }, + { + "tag": "a", + "text": "Promotions", + "href": "/releases/promotions", + "disabled": false + } + ], + "emptyIndicators": [], + "bodySnippet": "Skip to main contentRelease ControlDashboardReleasesVersionsRelease HealthApprovalsPromotionsHotfixesOperationsScheduled JobsSignalsOffline KitEnvironmentsPolicyPlatform SetupNotificationsSecurity & AuditSecurity PostureTriageSupply-Chain DataReachabilityUnknownsReportsAuditDecision CapsulesReplay &", + "consoleErrors": [], + "responseErrors": [], + "requestFailures": [], + "screenshot": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\first-time-user-audit\\integrations-secrets.png" + }, + { + "phase": "admin", + "label": "Setup - Topology", + "route": "/setup/topology", + "finalUrl": "/setup/topology/overview", + "title": "Topology Overview - Stella Ops QA 1773540847164", + "headings": [ + "Topology" + ], + "actionCount": 20, + "topActions": [ + { + "tag": "a", + "text": "Skip to main content", + "href": "#main-content", + "disabled": false + }, + { + "tag": "button", + "text": "Release Control", + "href": "", + "disabled": false + }, + { + "tag": "a", + "text": "Dashboard", + "href": "/mission-control/board", + "disabled": false + }, + { + "tag": "a", + "text": "Releases", + "href": "/releases/deployments", + "disabled": false + }, + { + "tag": "a", + "text": "Versions", + "href": "/releases/versions", + "disabled": false + }, + { + "tag": "a", + "text": "Release Health", + "href": "/releases/health", + "disabled": false + }, + { + "tag": "a", + "text": "Approvals", + "href": "/releases/approvals", + "disabled": false + }, + { + "tag": "a", + "text": "Promotions", + "href": "/releases/promotions", + "disabled": false + } + ], + "emptyIndicators": [ + "loading_visible" + ], + "bodySnippet": "Skip to main contentRelease ControlDashboardReleasesVersionsRelease HealthApprovalsPromotionsHotfixesOperationsScheduled JobsSignalsOffline KitEnvironmentsPolicyPlatform SetupNotificationsSecurity & AuditSecurity PostureTriageSupply-Chain DataReachabilityUnknownsReportsAuditDecision CapsulesReplay &", + "consoleErrors": [], + "responseErrors": [], + "requestFailures": [], + "screenshot": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\first-time-user-audit\\setup-topology.png" + }, + { + "phase": "admin", + "label": "Setup - Identity & Access", + "route": "/setup/identity-access", + "finalUrl": "/setup/identity-access", + "title": "Identity & Access - Stella Ops QA 1773540847164", + "headings": [ + "Identity & Access", + "Users" + ], + "actionCount": 20, + "topActions": [ + { + "tag": "a", + "text": "Skip to main content", + "href": "#main-content", + "disabled": false + }, + { + "tag": "button", + "text": "Release Control", + "href": "", + "disabled": false + }, + { + "tag": "a", + "text": "Dashboard", + "href": "/mission-control/board", + "disabled": false + }, + { + "tag": "a", + "text": "Releases", + "href": "/releases/deployments", + "disabled": false + }, + { + "tag": "a", + "text": "Versions", + "href": "/releases/versions", + "disabled": false + }, + { + "tag": "a", + "text": "Release Health", + "href": "/releases/health", + "disabled": false + }, + { + "tag": "a", + "text": "Approvals", + "href": "/releases/approvals", + "disabled": false + }, + { + "tag": "a", + "text": "Promotions", + "href": "/releases/promotions", + "disabled": false + } + ], + "emptyIndicators": [], + "bodySnippet": "Skip to main contentRelease ControlDashboardReleasesVersionsRelease HealthApprovalsPromotionsHotfixesOperationsScheduled JobsSignalsOffline KitEnvironmentsPolicyPlatform SetupNotificationsSecurity & AuditSecurity PostureTriageSupply-Chain DataReachabilityUnknownsReportsAuditDecision CapsulesReplay &", + "consoleErrors": [], + "responseErrors": [], + "requestFailures": [ + { + "url": "https://stella-ops.local/api/v2/topology/promotion-paths", + "error": "net::ERR_ABORTED" + }, + { + "url": "https://stella-ops.local/api/v1/doctor/scheduler/trends/categories/security", + "error": "net::ERR_ABORTED" + }, + { + "url": "https://stella-ops.local/api/v2/topology/agents", + "error": "net::ERR_ABORTED" + }, + { + "url": "https://stella-ops.local/api/v2/topology/hosts", + "error": "net::ERR_ABORTED" + }, + { + "url": "https://stella-ops.local/api/v1/doctor/scheduler/trends/categories/platform", + "error": "net::ERR_ABORTED" + } + ], + "screenshot": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\first-time-user-audit\\setup-identity-access.png" + }, + { + "phase": "admin", + "label": "Setup - Tenant Branding", + "route": "/setup/tenant-branding", + "finalUrl": "/setup/tenant-branding", + "title": "Tenant & Branding - Stella Ops QA 1773540847164", + "headings": [ + "Branding Configuration", + "General Settings", + "Logo & Favicon", + "Theme Tokens", + "Preview" + ], + "actionCount": 20, + "topActions": [ + { + "tag": "a", + "text": "Skip to main content", + "href": "#main-content", + "disabled": false + }, + { + "tag": "button", + "text": "Release Control", + "href": "", + "disabled": false + }, + { + "tag": "a", + "text": "Dashboard", + "href": "/mission-control/board", + "disabled": false + }, + { + "tag": "a", + "text": "Releases", + "href": "/releases/deployments", + "disabled": false + }, + { + "tag": "a", + "text": "Versions", + "href": "/releases/versions", + "disabled": false + }, + { + "tag": "a", + "text": "Release Health", + "href": "/releases/health", + "disabled": false + }, + { + "tag": "a", + "text": "Approvals", + "href": "/releases/approvals", + "disabled": false + }, + { + "tag": "a", + "text": "Promotions", + "href": "/releases/promotions", + "disabled": false + } + ], + "emptyIndicators": [], + "bodySnippet": "Skip to main contentRelease ControlDashboardReleasesVersionsRelease HealthApprovalsPromotionsHotfixesOperationsScheduled JobsSignalsOffline KitEnvironmentsPolicyPlatform SetupNotificationsSecurity & AuditSecurity PostureTriageSupply-Chain DataReachabilityUnknownsReportsAuditDecision CapsulesReplay &", + "consoleErrors": [], + "responseErrors": [], + "requestFailures": [], + "screenshot": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\first-time-user-audit\\setup-tenant-branding.png" + }, + { + "phase": "admin", + "label": "Setup - Notifications", + "route": "/setup/notifications", + "finalUrl": "/setup/notifications", + "title": "Notifications - Stella Ops QA 1773540847164", + "headings": [ + "Notification Administration" + ], + "actionCount": 20, + "topActions": [ + { + "tag": "a", + "text": "Skip to main content", + "href": "#main-content", + "disabled": false + }, + { + "tag": "button", + "text": "Release Control", + "href": "", + "disabled": false + }, + { + "tag": "a", + "text": "Dashboard", + "href": "/mission-control/board", + "disabled": false + }, + { + "tag": "a", + "text": "Releases", + "href": "/releases/deployments", + "disabled": false + }, + { + "tag": "a", + "text": "Versions", + "href": "/releases/versions", + "disabled": false + }, + { + "tag": "a", + "text": "Release Health", + "href": "/releases/health", + "disabled": false + }, + { + "tag": "a", + "text": "Approvals", + "href": "/releases/approvals", + "disabled": false + }, + { + "tag": "a", + "text": "Promotions", + "href": "/releases/promotions", + "disabled": false + } + ], + "emptyIndicators": [ + "setup_prompt" + ], + "bodySnippet": "Skip to main contentRelease ControlDashboardReleasesVersionsRelease HealthApprovalsPromotionsHotfixesOperationsScheduled JobsSignalsOffline KitEnvironmentsPolicyPlatform SetupNotificationsSecurity & AuditSecurity PostureTriageSupply-Chain DataReachabilityUnknownsReportsAuditDecision CapsulesReplay &", + "consoleErrors": [], + "responseErrors": [], + "requestFailures": [], + "screenshot": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\first-time-user-audit\\setup-notifications.png" + }, + { + "phase": "admin", + "label": "Setup - Usage", + "route": "/setup/usage", + "finalUrl": "/setup/usage", + "title": "Usage & Limits - Stella Ops QA 1773540847164", + "headings": [ + "Usage & Limits", + "Quota Configuration" + ], + "actionCount": 20, + "topActions": [ + { + "tag": "a", + "text": "Skip to main content", + "href": "#main-content", + "disabled": false + }, + { + "tag": "button", + "text": "Release Control", + "href": "", + "disabled": false + }, + { + "tag": "a", + "text": "Dashboard", + "href": "/mission-control/board", + "disabled": false + }, + { + "tag": "a", + "text": "Releases", + "href": "/releases/deployments", + "disabled": false + }, + { + "tag": "a", + "text": "Versions", + "href": "/releases/versions", + "disabled": false + }, + { + "tag": "a", + "text": "Release Health", + "href": "/releases/health", + "disabled": false + }, + { + "tag": "a", + "text": "Approvals", + "href": "/releases/approvals", + "disabled": false + }, + { + "tag": "a", + "text": "Promotions", + "href": "/releases/promotions", + "disabled": false + } + ], + "emptyIndicators": [ + "setup_prompt" + ], + "bodySnippet": "Skip to main contentRelease ControlDashboardReleasesVersionsRelease HealthApprovalsPromotionsHotfixesOperationsScheduled JobsSignalsOffline KitEnvironmentsPolicyPlatform SetupNotificationsSecurity & AuditSecurity PostureTriageSupply-Chain DataReachabilityUnknownsReportsAuditDecision CapsulesReplay &", + "consoleErrors": [], + "responseErrors": [], + "requestFailures": [], + "screenshot": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\first-time-user-audit\\setup-usage.png" + }, + { + "phase": "admin", + "label": "Setup - Trust & Signing", + "route": "/setup/trust-signing", + "finalUrl": "/setup/trust-signing", + "title": "Trust & Signing - Stella Ops QA 1773540847164", + "headings": [ + "Trust Management", + "Trust & Signing Overview" + ], + "actionCount": 20, + "topActions": [ + { + "tag": "a", + "text": "Skip to main content", + "href": "#main-content", + "disabled": false + }, + { + "tag": "button", + "text": "Release Control", + "href": "", + "disabled": false + }, + { + "tag": "a", + "text": "Dashboard", + "href": "/mission-control/board", + "disabled": false + }, + { + "tag": "a", + "text": "Releases", + "href": "/releases/deployments", + "disabled": false + }, + { + "tag": "a", + "text": "Versions", + "href": "/releases/versions", + "disabled": false + }, + { + "tag": "a", + "text": "Release Health", + "href": "/releases/health", + "disabled": false + }, + { + "tag": "a", + "text": "Approvals", + "href": "/releases/approvals", + "disabled": false + }, + { + "tag": "a", + "text": "Promotions", + "href": "/releases/promotions", + "disabled": false + } + ], + "emptyIndicators": [], + "bodySnippet": "Skip to main contentRelease ControlDashboardReleasesVersionsRelease HealthApprovalsPromotionsHotfixesOperationsScheduled JobsSignalsOffline KitEnvironmentsPolicyPlatform SetupNotificationsSecurity & AuditSecurity PostureTriageSupply-Chain DataReachabilityUnknownsReportsAuditDecision CapsulesReplay &", + "consoleErrors": [], + "responseErrors": [], + "requestFailures": [], + "screenshot": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\first-time-user-audit\\setup-trust-signing.png" + }, + { + "phase": "admin", + "label": "Platform Setup - Home", + "route": "/ops/platform-setup", + "finalUrl": "/ops/platform-setup", + "title": "Platform Setup - Stella Ops QA 1773540847164", + "headings": [ + "Platform Setup" + ], + "actionCount": 20, + "topActions": [ + { + "tag": "a", + "text": "Skip to main content", + "href": "#main-content", + "disabled": false + }, + { + "tag": "button", + "text": "Release Control", + "href": "", + "disabled": false + }, + { + "tag": "a", + "text": "Dashboard", + "href": "/mission-control/board", + "disabled": false + }, + { + "tag": "a", + "text": "Releases", + "href": "/releases/deployments", + "disabled": false + }, + { + "tag": "a", + "text": "Versions", + "href": "/releases/versions", + "disabled": false + }, + { + "tag": "a", + "text": "Release Health", + "href": "/releases/health", + "disabled": false + }, + { + "tag": "a", + "text": "Approvals", + "href": "/releases/approvals", + "disabled": false + }, + { + "tag": "a", + "text": "Promotions", + "href": "/releases/promotions", + "disabled": false + } + ], + "emptyIndicators": [ + "setup_prompt" + ], + "bodySnippet": "Skip to main contentRelease ControlDashboardReleasesVersionsRelease HealthApprovalsPromotionsHotfixesOperationsScheduled JobsSignalsOffline KitEnvironmentsPolicyPlatform SetupNotificationsSecurity & AuditSecurity PostureTriageSupply-Chain DataReachabilityUnknownsReportsAuditDecision CapsulesReplay &", + "consoleErrors": [], + "responseErrors": [], + "requestFailures": [], + "screenshot": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\first-time-user-audit\\platform-setup-home.png" + }, + { + "phase": "edge", + "label": "Edge - 404 page", + "route": "/nonexistent-route-12345", + "finalUrl": "/nonexistent-route-12345", + "title": "Dashboard - Stella Ops QA 1773540847164", + "headings": [ + "Dashboard", + "Regional Pipeline", + "Environments at Risk", + "SBOM Findings Snapshot", + "Reachability", + "Nightly Ops Signals", + "Alerts", + "Recent Activity" + ], + "actionCount": 20, + "topActions": [ + { + "tag": "a", + "text": "Skip to main content", + "href": "#main-content", + "disabled": false + }, + { + "tag": "button", + "text": "Release Control", + "href": "", + "disabled": false + }, + { + "tag": "a", + "text": "Dashboard", + "href": "/mission-control/board", + "disabled": false + }, + { + "tag": "a", + "text": "Releases", + "href": "/releases/deployments", + "disabled": false + }, + { + "tag": "a", + "text": "Versions", + "href": "/releases/versions", + "disabled": false + }, + { + "tag": "a", + "text": "Release Health", + "href": "/releases/health", + "disabled": false + }, + { + "tag": "a", + "text": "Approvals", + "href": "/releases/approvals", + "disabled": false + }, + { + "tag": "a", + "text": "Promotions", + "href": "/releases/promotions", + "disabled": false + } + ], + "emptyIndicators": [], + "bodySnippet": "Skip to main contentRelease ControlDashboardReleasesVersionsRelease HealthApprovalsPromotionsHotfixesOperationsScheduled JobsSignalsOffline KitEnvironmentsPolicyPlatform SetupNotificationsSecurity & AuditSecurity PostureTriageSupply-Chain DataReachabilityUnknownsReportsAuditDecision CapsulesReplay &", + "consoleErrors": [], + "responseErrors": [], + "requestFailures": [], + "screenshot": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\first-time-user-audit\\edge-404-page.png" + } + ], + "issues": [ + { + "severity": "medium", + "category": "ux", + "route": "/ops/operations/system-health", + "label": "Operations - System Health", + "detail": "Error text visible on page" + }, + { + "severity": "medium", + "category": "ux", + "route": "/ops/operations/signals", + "label": "Operations - Signals", + "detail": "Error text visible on page" + }, + { + "severity": "medium", + "category": "ux", + "route": "/ops/operations/dead-letter", + "label": "Operations - Dead Letter", + "detail": "Error text visible on page" + } + ] +} \ No newline at end of file diff --git a/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/dashboard-activity.png b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/dashboard-activity.png new file mode 100644 index 000000000..e92f513e0 Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/dashboard-activity.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/dashboard-alerts.png b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/dashboard-alerts.png new file mode 100644 index 000000000..c3f9e5080 Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/dashboard-alerts.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/dashboard-mission-board.png b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/dashboard-mission-board.png new file mode 100644 index 000000000..a73e6eb19 Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/dashboard-mission-board.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/edge-404-page.png b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/edge-404-page.png new file mode 100644 index 000000000..ff6a52270 Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/edge-404-page.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/evidence-capsules.png b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/evidence-capsules.png new file mode 100644 index 000000000..09b714089 Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/evidence-capsules.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/evidence-exports.png b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/evidence-exports.png new file mode 100644 index 000000000..2a8f78b42 Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/evidence-exports.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/evidence-overview.png b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/evidence-overview.png new file mode 100644 index 000000000..1a3657967 Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/evidence-overview.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/evidence-verify-replay.png b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/evidence-verify-replay.png new file mode 100644 index 000000000..d1bf0be37 Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/evidence-verify-replay.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/integrations-advisory-vex.png b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/integrations-advisory-vex.png new file mode 100644 index 000000000..9587be921 Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/integrations-advisory-vex.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/integrations-hub.png b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/integrations-hub.png new file mode 100644 index 000000000..4f44cf1e7 Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/integrations-hub.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/integrations-registries.png b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/integrations-registries.png new file mode 100644 index 000000000..876e8112b Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/integrations-registries.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/integrations-scm.png b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/integrations-scm.png new file mode 100644 index 000000000..5afbbadd1 Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/integrations-scm.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/integrations-secrets.png b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/integrations-secrets.png new file mode 100644 index 000000000..5620b506e Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/integrations-secrets.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/landing-page-redirect-.png b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/landing-page-redirect-.png new file mode 100644 index 000000000..4dcd96132 Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/landing-page-redirect-.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/operations-dead-letter.png b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/operations-dead-letter.png new file mode 100644 index 000000000..e35faf133 Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/operations-dead-letter.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/operations-doctor.png b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/operations-doctor.png new file mode 100644 index 000000000..0146d00bb Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/operations-doctor.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/operations-hub.png b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/operations-hub.png new file mode 100644 index 000000000..008f8edf9 Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/operations-hub.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/operations-jobengine.png b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/operations-jobengine.png new file mode 100644 index 000000000..d4ee3d6fd Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/operations-jobengine.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/operations-signals.png b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/operations-signals.png new file mode 100644 index 000000000..c2e256449 Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/operations-signals.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/operations-system-health.png b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/operations-system-health.png new file mode 100644 index 000000000..70339c78f Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/operations-system-health.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/platform-setup-home.png b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/platform-setup-home.png new file mode 100644 index 000000000..b9892b86d Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/platform-setup-home.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/policy-baselines.png b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/policy-baselines.png new file mode 100644 index 000000000..b61e48961 Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/policy-baselines.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/policy-gates.png b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/policy-gates.png new file mode 100644 index 000000000..632298daf Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/policy-gates.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/policy-overview.png b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/policy-overview.png new file mode 100644 index 000000000..4ee556a38 Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/policy-overview.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/policy-simulation.png b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/policy-simulation.png new file mode 100644 index 000000000..7aa3c9aef Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/policy-simulation.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/policy-waivers.png b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/policy-waivers.png new file mode 100644 index 000000000..279996261 Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/policy-waivers.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/releases-approvals.png b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/releases-approvals.png new file mode 100644 index 000000000..ac0e195fb Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/releases-approvals.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/releases-deployments.png b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/releases-deployments.png new file mode 100644 index 000000000..05bd5c1e0 Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/releases-deployments.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/releases-environments.png b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/releases-environments.png new file mode 100644 index 000000000..4e4df0367 Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/releases-environments.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/releases-versions.png b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/releases-versions.png new file mode 100644 index 000000000..266f2432c Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/releases-versions.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/security-disposition.png b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/security-disposition.png new file mode 100644 index 000000000..927eb76a0 Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/security-disposition.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/security-posture.png b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/security-posture.png new file mode 100644 index 000000000..a490bebb6 Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/security-posture.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/security-reachability.png b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/security-reachability.png new file mode 100644 index 000000000..9c9623ced Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/security-reachability.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/security-supply-chain.png b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/security-supply-chain.png new file mode 100644 index 000000000..96b31f3fc Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/security-supply-chain.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/security-triage.png b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/security-triage.png new file mode 100644 index 000000000..04f6f987d Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/security-triage.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/setup-identity-access.png b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/setup-identity-access.png new file mode 100644 index 000000000..1bb961afe Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/setup-identity-access.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/setup-notifications.png b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/setup-notifications.png new file mode 100644 index 000000000..fc40cff18 Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/setup-notifications.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/setup-tenant-branding.png b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/setup-tenant-branding.png new file mode 100644 index 000000000..732ac2331 Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/setup-tenant-branding.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/setup-topology.png b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/setup-topology.png new file mode 100644 index 000000000..10fb7f485 Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/setup-topology.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/setup-trust-signing.png b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/setup-trust-signing.png new file mode 100644 index 000000000..17f032cad Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/setup-trust-signing.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/setup-usage.png b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/setup-usage.png new file mode 100644 index 000000000..9278eac34 Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/first-time-user-audit/setup-usage.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/live-first-time-user-reporting-truthfulness-check.auth.json b/src/Web/StellaOps.Web/output/playwright/live-first-time-user-reporting-truthfulness-check.auth.json new file mode 100644 index 000000000..0efc9bfb4 --- /dev/null +++ b/src/Web/StellaOps.Web/output/playwright/live-first-time-user-reporting-truthfulness-check.auth.json @@ -0,0 +1,39 @@ +{ + "authenticatedAtUtc": "2026-03-15T12:19:11.758Z", + "baseUrl": "https://stella-ops.local", + "finalUrl": "https://stella-ops.local/mission-control/board?tenant=demo-prod®ions=apac,eu-west,us-east,us-west", + "title": "Dashboard - StellaOps", + "cookies": [], + "storage": { + "localStorageEntries": [ + [ + "stellaops.sidebar.preferences", + "{\"sidebarCollapsed\":false,\"collapsedGroups\":[],\"collapsedSections\":[]}" + ], + [ + "stellaops.theme", + "system" + ] + ], + "sessionStorageEntries": [ + [ + "stellaops.auth.session.info", + "{\"subject\":\"b08639745d6549348d843aa311c98958\",\"expiresAtEpochMs\":1773578948195,\"issuedAtEpochMs\":1773577149196,\"dpopKeyThumbprint\":\"GJtORtCGxKqxFPZdO_B3JMNY8jY77vjWIUP-ezDbzqg\",\"tenantId\":\"demo-prod\"}" + ], + [ + "stellaops.auth.session.full", + "{\"tokens\":{\"accessToken\":\"eyJhbGciOiJSUzI1NiIsImtpZCI6IlRBSU1GTlE3LUFOR1JMU0JOQVlfQ19OSEdPVERVWUo5UlNVVTRSRUQiLCJ0eXAiOiJhdCtqd3QifQ.eyJpc3MiOiJodHRwczovL2F1dGhvcml0eS5zdGVsbGEtb3BzLmxvY2FsLyIsImV4cCI6MTc3MzU3ODk0OCwiaWF0IjoxNzczNTc3MTQ4LCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGVtYWlsIG9mZmxpbmVfYWNjZXNzIHVpLnJlYWQgdWkuYWRtaW4gdWkucHJlZmVyZW5jZXMucmVhZCB1aS5wcmVmZXJlbmNlcy53cml0ZSBhdXRob3JpdHk6dGVuYW50cy5yZWFkIGF1dGhvcml0eTp0ZW5hbnRzLndyaXRlIGF1dGhvcml0eTp1c2Vycy5yZWFkIGF1dGhvcml0eTp1c2Vycy53cml0ZSBhdXRob3JpdHk6cm9sZXMucmVhZCBhdXRob3JpdHk6cm9sZXMud3JpdGUgYXV0aG9yaXR5OmNsaWVudHMucmVhZCBhdXRob3JpdHk6Y2xpZW50cy53cml0ZSBhdXRob3JpdHk6dG9rZW5zLnJlYWQgYXV0aG9yaXR5OnRva2Vucy5yZXZva2UgYXV0aG9yaXR5OmJyYW5kaW5nLnJlYWQgYXV0aG9yaXR5OmJyYW5kaW5nLndyaXRlIGF1dGhvcml0eS5hdWRpdC5yZWFkIGdyYXBoOnJlYWQgc2JvbTpyZWFkIHNjYW5uZXI6cmVhZCBwb2xpY3k6cmVhZCBwb2xpY3k6c2ltdWxhdGUgcG9saWN5OmF1dGhvciBwb2xpY3k6cmV2aWV3IHBvbGljeTphcHByb3ZlIHBvbGljeTpydW4gcG9saWN5OmFjdGl2YXRlIHBvbGljeTphdWRpdCBwb2xpY3k6ZWRpdCBwb2xpY3k6b3BlcmF0ZSBwb2xpY3k6cHVibGlzaCBhaXJnYXA6c2VhbCBhaXJnYXA6c3RhdHVzOnJlYWQgb3JjaDpyZWFkIG9yY2g6b3BlcmF0ZSBvcmNoOnF1b3RhIGFuYWx5dGljcy5yZWFkIGFkdmlzb3J5OnJlYWQgYWR2aXNvcnktYWk6dmlldyBhZHZpc29yeS1haTpvcGVyYXRlIHZleDpyZWFkIHZleGh1YjpyZWFkIGV4Y2VwdGlvbnM6cmVhZCBleGNlcHRpb25zOmFwcHJvdmUgYW9jOnZlcmlmeSBmaW5kaW5nczpyZWFkIHJlbGVhc2U6cmVhZCByZWxlYXNlOndyaXRlIHJlbGVhc2U6cHVibGlzaCBzY2hlZHVsZXI6cmVhZCBzY2hlZHVsZXI6b3BlcmF0ZSBub3RpZnkudmlld2VyIG5vdGlmeS5vcGVyYXRvciBub3RpZnkuYWRtaW4gbm90aWZ5LmVzY2FsYXRlIGV2aWRlbmNlOnJlYWQgZXhwb3J0LnZpZXdlciBleHBvcnQub3BlcmF0b3IgZXhwb3J0LmFkbWluIHZ1bG46dmlldyB2dWxuOmludmVzdGlnYXRlIHZ1bG46b3BlcmF0ZSB2dWxuOmF1ZGl0IHBsYXRmb3JtLmNvbnRleHQucmVhZCBwbGF0Zm9ybS5jb250ZXh0LndyaXRlIGRvY3RvcjpydW4gZG9jdG9yOmFkbWluIG9wcy5oZWFsdGggaW50ZWdyYXRpb246cmVhZCBpbnRlZ3JhdGlvbjp3cml0ZSBpbnRlZ3JhdGlvbjpvcGVyYXRlIHBhY2tzLnJlYWQgcGFja3Mud3JpdGUgcGFja3MucnVuIHBhY2tzLmFwcHJvdmUgcmVnaXN0cnkuYWRtaW4gdGltZWxpbmU6cmVhZCB0aW1lbGluZTp3cml0ZSB0cnVzdDpyZWFkIHRydXN0OndyaXRlIHRydXN0OmFkbWluIHNpZ25lcjpyZWFkIHNpZ25lcjpzaWduIHNpZ25lcjpyb3RhdGUgc2lnbmVyOmFkbWluIiwianRpIjoiZjE4NmRiMmUtY2I0NC00ZTA0LWI4NTUtMTg4MWUyYWNjYzBmIiwic3ViIjoiYjA4NjM5NzQ1ZDY1NDkzNDhkODQzYWEzMTFjOTg5NTgiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJhZG1pbiIsIm5hbWUiOiJhZG1pbiIsInJvbGUiOiJhZG1pbiIsInN0ZWxsYW9wczp0ZW5hbnQiOiJkZW1vLXByb2QiLCJhdXRoX3RpbWUiOjE3NzM1NzcxNDgsIm9pX3Byc3QiOiJzdGVsbGEtb3BzLXVpIiwiY2xpZW50X2lkIjoic3RlbGxhLW9wcy11aSJ9.FeiNGZ6mjrOGNAqM6SfFBhvYiWiN0TuXf7FNMz2QyvDzz2bzuDn6pj2BX4nuW9469uv6QN1fFWub9lWSD5Az8ktAkLaEKlwwuLYYPiY7DRTjI1rH6JubY3NQ4IiC3KymIcQcTPIeQmu_vaVx9R_EBbxN1oEwa2cWIh-VQle7-R-OUs9yoUqoCPDBNqEYKDNV3VhcOR8gALue1Yph_8wtYGPu5aEHjJXA31XMkiIcssA_qNsAbeAoQsxf_pM-KUrZwG1Qk-36RAZFNl6sUeLiSscZ0sSUDtU6cwo8U2tw7cA3EpLOsippdgF5xVD-8rq5COFYijOI334Rgjd8-rYLig\",\"tokenType\":\"Bearer\",\"refreshToken\":\"eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZDQkMtSFM1MTIiLCJraWQiOiJXUjVQSU4tVzI5S0lNTkpRTVBBMU9XS0VYN0tEWjZMV0dYTDdSS0dWIiwidHlwIjoib2lfcmVmdCtqd3QiLCJjdHkiOiJKV1QifQ.flZEYF8zVVl2eDOlw9q3AWwkZOD9O1rTiLMcRM_WMtb_KNmVPwf5_Z6sf40ggwExoAZ6s-bk_oV4Lyaxc9k2EjjbDYMD85ta7IwKRjq_kr6jUwoU6KtoTuXJ2eCXPheIBZhq_E5-pz69btH_wEvderkBggwBWYGhNYfRka8vNgovZS89DeCq9HnMMJGrBxdycU60iySRJNPJ06yeQkzgiQkQzaYT_cmodfOlxJtdKmivg4dbMdXmRg4SVy6UAFnUuqWWtguK0yKYAVtMLtdJfGG5izq4PEzB_tVSbm5rjU_gl8bEOWAt4XTVMQV9embPNofq0kNAaQMdYe51E7VoFQ.L9mvvU4s64hREX7cl6iLyQ.14T2Ak0jpx2gVVa8jV14RYXuG5CuKZLEPY8nr-Dt8At7cwu2NcPPxQflnRgyyfyuv1uEV4uDn6cA2uVqQxKYkIKg96Q3xIBIPPq9ANp5Mc8DpeNbmdWnxjSWe1_1PtmdS1CMh7SRRLOUSpdN7U5N7EJsLyFlNyuUmN5zG8HhW0lwPO6fsItq9p4SQCE6vgi-0MFgImmvrYTtHjyWbICYbo0oT-OXfKUWclFsym5gDnPzARP0c8BM0YR5yr8FJez4K7iNXHx-EPkkNycBTcYeTw9RA--qMqEL-_1i0BKD6RTo0K2vCdF1B1_d9LVMj1jdfB3WwsV_6FJ7J43xGor1-Um9FJupU0ye2KuPnw076h_WNt5sLcDtntMocYpTu0yV5bFQL5eQ839oBE12O-riom9R-T_xsdh6WFRxOcx3n9-Ub1iUxC_l2QKnKxErho5zpeiEeUnPK8Ytvx6uKmcJoNyhgFJ-decg0CVl9xmeNxE12t2xdF00ZStS4UYcgNtMxlxa0CkIz1K4iVKqcpg2O2fza1i5WtFWZgkeOkckQQRhChQzG-elcWuHvlmr0HPiwqjirWPog_dOhu9JuxE0Az9QTzSESELTOjjdx9kcPgxNhFkGaP1s5--wQxkcfTc99QA074ROOI1YYs3bWH83yACajQUtkHYC9mfvbBXyzEcxBtLTRWmvOneRa-wfwJYHwWt4phO4c0w8HA5YQNmRmBzk4EmGLC5mJF-QeRQ4fTorl447wkvTjXFyRKL9g5U_o7RcIrRCM1dIYYKv6nrewUQncjyCoY8d5g7M7kKcwAXR3AfawDOadzOh_5l6UCjQekujE_zwnnCiabJVok8O66d06tAmNwJrbd5xoAXzIvaH-UGd-__42J1ODjxTwbYA1jF7QsuKzoXjqqNIk7_Mpb_pgfv6vs6EyE_CErvgImtXc2fi9vMbA21cCE8cNR3oS3kP0zWl-R1HZDmwesWhz5XUMdlY-WNPzXZL7zeXCvCbRLd0Voa8a1lz_sHKLI7vgRZjnuKuzDkiYQdTxhHm4najfsWJW3v-fgrMblM9N_zzxLa3KqZTJBDMzvJPPaY-mNwy1QFWTDy8DUNMNF3d6HGUFd6m_L5Shr9pVcjQ-L1tYhUpkDQBzHSChnU99f7RbqjuHX9_KgC5WvVvgnRAEIEjoTFwatBVqcw_FjlxyMS3FDHtAQ_Jlz8-XPyHoa2NHT2HwW-PDZ3SZ8mFrmsNdv8E9mumnsq3_YRhUYx3RHY_HqtEzzzzxUFGKABNlk-i4tHk0BJK-5AkCDmYle5_SHY4ugkKwRk7Qi71Ej2Qg_ev5J0wjX5KW6Ky-6CGDBHTBChObDz0kirC0HPT0C6uWwcTYsupa473d93h1Gx_d8Tp5HxEKrzoPbDY_CGtQ2ISR97JFSxsNDQdtypvqFsmk5X-mkvDgIPdzB9P8HQRaS0XmYKyT2JtDTwKI4XNvnm9ZD5vFv54_yH75iklEWIyDrjy5UHokG098T_tTAV-j4j7BErqSUypSvsdKcX0wsyJhEezlqDfjYB5x0LMmiUUw1ytlL2ihOvXgDSDAPRI7upWppXRe9dZ-LtbRz1v7EIc_5vpA2HjJzQY7_y1kE-U5mTygeRrp6Y7iC2UXbDy6hZt1H_t0CPZhfkVr9ey3BVgv563PpYH1EicfuLBfLgQEfc7XoAZPQDQRQRY0rhpVu5MtJDdgcGwxySOC3uiNLqAOH0PXWW4WoX-YXwHZbJmiekKKFR9dgZhN-q_HL3VHrrsC8Ra8MlJ8W1W_9irvYcF15DdnOpuX-0LmzLN3VMKmsUZ8QAqM9FMQIPA1jkEzaySBvDNtF5kaia-z5ij6hdQMInc8aV_J5944yO4eTiKTm8agYRJUPfEFslJsxgu6_HLkaAONGk4zaZhANZBEnK8m5Zxpkn9fOQbEtuekt08KVwG-cjl4qLsO1StACnwtGWLhQncWOsPGpW1q2OZdQT2XuYKJvccZp7tiCiayr2SHfvoswchcAP9P6lh24O1Ngi1K6Jot1EeDmucGa-N9-0wU6dQ_WJvREOs_zh2xYATEQAKb9fyiil1IVRX2dZE_zHDzmg_mq_37EEnSnKqJaJxl3j8LIefH8RA4RnEQs5nhgImz5FjOUB6-l63z68KoZSE_sCIUYlYfj43xIOl0AyGMDDTybqbc-fhU6Id7BnnR5HcxxUJS2JH1okfSk3Ir9eSRXsJgwpVfyk6YUlDB1yem8cAiMfu-SnLzrKk30_-TBgBMM3PxzS_VybMFKLuOGdBQVSr2P7QYeHvMz5OL8PNFBqIua8tnJut5advdxqT6zjWnyTOGC2mfoaiCtKu4jTj7Ac_1OScRIR9L3mSPm3-ELYXBAnehmjPbC-P4UPnj981LXlmzHVnjLHm-Cx1JbO8a1cykWVBqSAzeYYGZHRTtATLSVRBI15GCol26p91W4f1_-QjG-Qm5u3nEDHRUEPTyqLti6Vw9gOOcipivdlnzL2X3S5W5IC0Z1EfoM8tJaZEcsCi_E9BtKDgbq0pXtQr6NKRkGGFZyEzCAPIEGe0lQgUKNbe31olMuX2kn6vT3Zl4Nw6zygwCfNSah1M9QhEF4msWxFO1ELkfWLi3bWGnaS0qVz_PrG-K2uicIJIfDlme7dKVBsPDmWu654EqhJoNR38Y-cOL4LIkzE-EmeFKLdXYFxkyWmABWUY18yOHYfFzQxJ5mX34gcvlU_z0Z0i5O9T4Ys1zRb09kkuIL8NQycyD5k0to1jphbDP6cXZGhPMGYGB-IXNg0OAOTd95MERTEuBdPcUQrGYp8IKWrYd51kKciPz5kX8p-I6S-HUP83HTrsI0Nm1VPx47oiSno4XLYrc-pCLAtBx89Bt4EyFF26sSwbGET795T-CuaiCnr5D8_eI_HFRQHwXvZfLHowDNkWBNPFC0I6GVjpQ7MeJ17Z2LwP9M1uvoHL15CUBiX32MjgmIlIPK4hGAmQ7sw6k7Gs8lyk_MZQMk3G6QlwShcRST58uf79Lxa8LV05IZdGLUC2PZOyd5UcH-_fwok659sJdcJ1zbB2Pv-NyeDbzpeU0iddWMHcvdMfH2_xhxkp1X0cAMWLELoQtcxiFoEVCOgu9JFaCsnYW6ko9Pyi0ZnZKhCeuVICyatShdzvafoqsK9qR2hyVRuqK2i2pgX6nohWMMEiDAHYcQAF548LdAFBxQOz76XrHnP2g2MmyM0RDeuJROzqHu0kqFcLogA2eAYI0b_Ugy00h01It9bxVQlszlgAkeRLrYueO2XwuIdi_HTjmHjKyAxuWJRlYORMpJ1k13o9K4ipJSDrCSj5sVBJ-lkfP38Kos83okau-Kx8Xu9h8FYFkM9vZOjRjGJEi-g4QRa7ULU1cticFX5iHVCv7_Gy4mRSAD6_85JvqJOpFJVkKPGa2jqa00m0XHvFtqDo-5A4TTdilHyIcz0wtvFBuhc8uZ2etl6chqjgd6oVBGAnPH2M9i9TnPhxrceuGXtPwlgVJ00sEh-VfhkGpvEnj1muT8qiIFKCsNL73UEgcZZii6J0a_Mg9djEkrHQuL4K71Tn8QQRTaNily6JdxcqQFSmx190xhUq2XPNbLCiuT746NZ8tNgd51OdzP_ZjbD8gFX9msUnQq0vWXE52rrO97BE327H2iDtK4FBoA4pnOqPp9BKwlzXCkqkjIo2zebPXQKrj4lzYOZfeI4ufX0ZKH8Dcqw458kknpRqF_aIRuHVtiaztE2pJOQ536hGWkyonU6ohXlmWq__Dqjw2ITc0KrUNH_rxKFXhvNda-QwPjvB-2ulmOMz6H8NGlXsvH_-JsA2pWOsPJwO0PWgtmFQk1B2zmKSJWKiYEgXd-X42C7_SZQPhhWlN6e_eMk1qK_0SeJT1M-qgatXksqpGxbtnOh3pgnrEEdxorqH0DG3t7U1eUFncr0vF3i2aijZHf7mW6VCcsc0Bz_nkItT1E4OFLenT7BZQHB-kMNxxafkAX2grv9h1_JxgTyeLoSkqSdL6YER1siqP17Bb8wvIxm1Qn7pHUnZPgBtZsJenxG0_rTU73m4J5-g4z6k9DRLYSkRegPbACU3cf-TzSMDjUNPc_QUX_dpSg_FgQJfcUa6wzL0XiFtb80b5XijEonMG83mir8Kv9_0JYbwAxMg-U_Y6H3YDUp4xD4yUX_F26PER9c-dZGtWYVYsRtgJDILOWRHvlyVZAqqeXU4ibiyoQFPJujDAI1vSekYxr0ULYgobDdvsAIb9guLVMc7BXW58BCisVuLj8aGYrLLO0Vli46pMAL5hy7uYF2Wow7NxbhwA5LvNvDbi9ntNYFpK0rICrun3GJ9YQsEcclBT6zWJ1rITmvpw-FK4lQiSt71ruZFs728i8E0tX1pwEOdzTddP55USEkXlwMDKU00ZJA51ZDd0GF7eXSoFIQIetuX95xsgQQGf9VNmo_LecfEeDbFEdrPmUBmXVjQy5kHvnB6Q-R4GCN_hhVNJ1Nab2QYdYs1BDKzmeYS4qyi1TCVvwctLnVQqvil1V7G-Sks-7-kkv2BWv9xTtHFtob9K_501tAP9-HBSzFR_j_zf2-EyDhEoqyh4glKT46QWfIb-MMNHeFnOBexb1GZ-fxwZ6Suv_3-E685xfdifLCGZow5pmAFH8BYB8lvFeM79Ut0qx3ZTb-I6b-9TrXpLjUIQvboDXGs8JZ3aPwmTbAM-LojMd1ssPHm5rkp1HDs4n7KNfuaL6oUqPr2GKemN18BsN-isg._3eKTUxDjPnEm_KefLPEzJWyTXfKg_A7wMaI88w6d0g\",\"scope\":\"openid profile email offline_access ui.read ui.admin ui.preferences.read ui.preferences.write authority:tenants.read authority:tenants.write authority:users.read authority:users.write authority:roles.read authority:roles.write authority:clients.read authority:clients.write authority:tokens.read authority:tokens.revoke authority:branding.read authority:branding.write authority.audit.read graph:read sbom:read scanner:read policy:read policy:simulate policy:author policy:review policy:approve policy:run policy:activate policy:audit policy:edit policy:operate policy:publish airgap:seal airgap:status:read orch:read orch:operate orch:quota analytics.read advisory:read advisory-ai:view advisory-ai:operate vex:read vexhub:read exceptions:read exceptions:approve aoc:verify findings:read release:read release:write release:publish scheduler:read scheduler:operate notify.viewer notify.operator notify.admin notify.escalate evidence:read export.viewer export.operator export.admin vuln:view vuln:investigate vuln:operate vuln:audit platform.context.read platform.context.write doctor:run doctor:admin ops.health integration:read integration:write integration:operate packs.read packs.write packs.run packs.approve registry.admin timeline:read timeline:write trust:read trust:write trust:admin signer:read signer:sign signer:rotate signer:admin\",\"expiresAtEpochMs\":1773578948195},\"identity\":{\"subject\":\"b08639745d6549348d843aa311c98958\",\"name\":\"admin\",\"roles\":[],\"idToken\":\"eyJhbGciOiJSUzI1NiIsImtpZCI6IlRBSU1GTlE3LUFOR1JMU0JOQVlfQ19OSEdPVERVWUo5UlNVVTRSRUQiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2F1dGhvcml0eS5zdGVsbGEtb3BzLmxvY2FsLyIsImV4cCI6MTc3MzU4MDQ0OCwiaWF0IjoxNzczNTc3MTQ4LCJhdWQiOiJzdGVsbGEtb3BzLXVpIiwic3ViIjoiYjA4NjM5NzQ1ZDY1NDkzNDhkODQzYWEzMTFjOTg5NTgiLCJuYW1lIjoiYWRtaW4iLCJhenAiOiJzdGVsbGEtb3BzLXVpIiwibm9uY2UiOiJjNzc4ZWJjZi1hZTIwLTRhNTQtYjZlMi1mNGZkNzU2ZGJkOTYiLCJhdF9oYXNoIjoiZV9FT2V3ajA1MUd6SGtqeURiVXRzUSJ9.kKWXyhfL1Kn6blk5r8QYaTxAinjShbO7cKrCKQdowpgxMCUkQbPt-56YBwgLT5tUewcapS9YmoPKkv9pCeJtygWxttw_geAqvepOhDW11bjURIkxCXSu1RILnShz8pga1qWcXaE1q4DYUhegbhZWvdgp4C7Xe-yzqg9sNam4owwtwfO3W2h6WndSVru7V5O_YssQUOU1jREcMC3exh_-5LBk6BwjG9Oy0esEB8FaJA8jyvuQprZPGjCkagPG8fE4MfIBnqn1mbGcpOikTZhRI71tVrruamLE6BR7jzioaE5Jmuym1QD7NG8YXPYbl4DQhXfyFAn1cYbodlTPI2I3mg\"},\"dpopKeyThumbprint\":\"GJtORtCGxKqxFPZdO_B3JMNY8jY77vjWIUP-ezDbzqg\",\"issuedAtEpochMs\":1773577149196,\"tenantId\":\"demo-prod\",\"scopes\":[\"advisory-ai:operate\",\"advisory-ai:view\",\"advisory:read\",\"airgap:seal\",\"airgap:status:read\",\"analytics.read\",\"aoc:verify\",\"authority.audit.read\",\"authority:branding.read\",\"authority:branding.write\",\"authority:clients.read\",\"authority:clients.write\",\"authority:roles.read\",\"authority:roles.write\",\"authority:tenants.read\",\"authority:tenants.write\",\"authority:tokens.read\",\"authority:tokens.revoke\",\"authority:users.read\",\"authority:users.write\",\"doctor:admin\",\"doctor:run\",\"email\",\"evidence:read\",\"exceptions:approve\",\"exceptions:read\",\"export.admin\",\"export.operator\",\"export.viewer\",\"findings:read\",\"graph:read\",\"integration:operate\",\"integration:read\",\"integration:write\",\"notify.admin\",\"notify.escalate\",\"notify.operator\",\"notify.viewer\",\"offline_access\",\"openid\",\"ops.health\",\"orch:operate\",\"orch:quota\",\"orch:read\",\"packs.approve\",\"packs.read\",\"packs.run\",\"packs.write\",\"platform.context.read\",\"platform.context.write\",\"policy:activate\",\"policy:approve\",\"policy:audit\",\"policy:author\",\"policy:edit\",\"policy:operate\",\"policy:publish\",\"policy:read\",\"policy:review\",\"policy:run\",\"policy:simulate\",\"profile\",\"registry.admin\",\"release:publish\",\"release:read\",\"release:write\",\"sbom:read\",\"scanner:read\",\"scheduler:operate\",\"scheduler:read\",\"signer:admin\",\"signer:read\",\"signer:rotate\",\"signer:sign\",\"timeline:read\",\"timeline:write\",\"trust:admin\",\"trust:read\",\"trust:write\",\"ui.admin\",\"ui.preferences.read\",\"ui.preferences.write\",\"ui.read\",\"vex:read\",\"vexhub:read\",\"vuln:audit\",\"vuln:investigate\",\"vuln:operate\",\"vuln:view\"],\"audiences\":[],\"authenticationTimeEpochMs\":1773577148000,\"freshAuthActive\":false,\"freshAuthExpiresAtEpochMs\":null}" + ], + [ + "stella-search-session-id", + "1ee03be9-73e7-4b7e-b8b1-23aeda64e51f" + ] + ] + }, + "events": { + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [] + }, + "statePath": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\live-first-time-user-reporting-truthfulness-check.state.json" +} diff --git a/src/Web/StellaOps.Web/output/playwright/live-first-time-user-reporting-truthfulness-check.json b/src/Web/StellaOps.Web/output/playwright/live-first-time-user-reporting-truthfulness-check.json new file mode 100644 index 000000000..90a22f1ea --- /dev/null +++ b/src/Web/StellaOps.Web/output/playwright/live-first-time-user-reporting-truthfulness-check.json @@ -0,0 +1,94 @@ +{ + "generatedAtUtc": "2026-03-15T12:19:24.578Z", + "baseUrl": "https://stella-ops.local", + "failedCheckCount": 0, + "runtimeIssueCount": 0, + "results": [ + { + "key": "security-reports-risk-tab", + "route": "/security/reports", + "ok": true, + "snapshot": { + "heading": "Security Reports", + "banner": "Loading security overview...", + "hasRiskPosture": true, + "hasArtifactWorkspace": false + } + }, + { + "key": "security-reports-vex-tab", + "route": "/security/reports?tab=vex", + "ok": true, + "snapshot": { + "tabText": "Security / Advisories & VEXIntel and attestation workspace for provider health, statement conflicts, and issuer trust.Configure advisory feedsConfigure VEX sourcesProvidersVEX LibraryConflictsIssuer TrustProvidersSourceChannelStatusFreshnessSLA (min)Internal VEXvex-sourceofflineunknown120KEVadvisory-feedofflineunknown120NVDadvisory-feedofflineunknown60OSVadvisory-feedofflineunknown90Vendor Advisoriesadvisory-feedofflineunknown180Vendor VEXvex-sourceofflineunknown180" + } + }, + { + "key": "security-reports-evidence-tab", + "route": "/security/reports?tab=evidence", + "ok": true, + "snapshot": { + "tabText": "Export CenterConfigure export profiles and monitor export runs.Operator Export StellaBundle OCI Profiles Export Runs Create Profile StellaBundle (OCI referrer)tar.gzSigned audit pack with DSSE envelope, Rekor tile receipt, and replay log. Suitable for auditor delivery via OCI referrer.IncludesSBOMVulnerabilitiesAttestationsProvenanceVEXPolicyEvidenceReplay LogDSSERekor Manual DestinationsNo destinations configured Run Now Edit Delete Daily Compliance Exporttar.gzExports SBOMs, vulnerability scans, and attestations for compliance reporting.IncludesSBOMVulnerabilitiesAttestationsProvenanceVEXPolicy Daily Next: Mar 16, 2026 Destinationss3compliance-bucket Run Now Edit Delete Audit BundlezipComplete evidence bundle for external auditors.IncludesSBOMVulnerabilitiesAttestationsProvenanceVEXPolicyEvidenceLogs Manual DestinationsNo destinations configured Run Now Edit Delete" + } + }, + { + "key": "setup-system-truthfulness", + "route": "/setup/system", + "ok": true, + "snapshot": { + "heading": "System Settings", + "text": "document.getElementById('stella-splash').dataset.ts=Date.now(); (function () { if (typeof window === 'undefined' || typeof document === 'undefined') { return; } window.__stellaWelcomePendingSignIn = false; document.addEventListener( 'click', function (event) { if (!window.location.pathname.startsWith('/welcome')) { return; } var target = event.target; if (!(target instanceof Element)) { return; } var button = target.closest('button.cta'); if (!button) { return; } if (typeof window.__stellaWelcomeSignIn === 'function') { return; } event.preventDefault(); window.__stellaWelcomePendingSignIn = true; }, true ); })(); Skip to main contentRelease ControlDashboardReleasesVersionsRelease HealthApprovalsPromotionsHotfixesOperationsScheduled JobsSignalsOffline KitEnvironmentsPolicyPlatform SetupNotificationsSecurity & AuditSecurity PostureTriageSupply-Chain DataReachabilityUnknownsReportsAuditDecision CapsulesReplay & VerifyExport CenterLogsBundlesPlatform & SetupSetupTopologyDiagnosticsIntegrationsIdentity & AccessTrust & SigningTenant & BrandingStella Ops v1.0.0-alphaStella OpsCtrl+KAdd Target Context adminTenantDemo ProductionRegionUS EastEnvStagingWindow7dStageAllEvents: CONNECTEDPolicy: Core Policy Pack latestEvidence: ONFeed: LiveOffline: OKSetupSystem SettingsSystem SettingsUse the live health and diagnostics workspaces below to validate readiness. This setup route is a handoff, not a health verdict.Live HealthOpen the live health surface to inspect service status, incidents, and the latest platform checks for the current scope. This setup page does not assert that the platform is healthy on its own. View DetailsDoctorRun diagnostic checks on the system.Run DoctorSLO MonitoringView and configure Service Level Objectives.View SLOsBackground JobsMonitor and manage background job processing.View Jobs" + } + }, + { + "key": "setup-integrations-guidance", + "route": "/setup/integrations", + "ok": true, + "snapshot": { + "heading": "Integrations", + "text": "document.getElementById('stella-splash').dataset.ts=Date.now(); (function () { if (typeof window === 'undefined' || typeof document === 'undefined') { return; } window.__stellaWelcomePendingSignIn = false; document.addEventListener( 'click', function (event) { if (!window.location.pathname.startsWith('/welcome')) { return; } var target = event.target; if (!(target instanceof Element)) { return; } var button = target.closest('button.cta'); if (!button) { return; } if (typeof window.__stellaWelcomeSignIn === 'function') { return; } event.preventDefault(); window.__stellaWelcomePendingSignIn = true; }, true ); })(); Skip to main contentRelease ControlDashboardReleasesVersionsRelease HealthApprovalsPromotionsHotfixesOperationsScheduled JobsSignalsOffline KitEnvironmentsPolicyPlatform SetupNotificationsSecurity & AuditSecurity PostureTriageSupply-Chain DataReachabilityUnknownsReportsAuditDecision CapsulesReplay & VerifyExport CenterLogsBundlesPlatform & SetupSetupTopologyDiagnosticsIntegrationsIdentity & AccessTrust & SigningTenant & BrandingStella Ops v1.0.0-alphaStella OpsCtrl+KAdd Target Context adminTenantDemo ProductionRegionUS EastEnvStagingWindow7dStageAllEvents: CONNECTEDPolicy: Core Policy Pack latestEvidence: ONFeed: LiveOffline: OKSetupIntegrationsIntegrationsExternal system connectors for release, security, and evidence flows. Hub Registries SCM CI/CD Runtimes / Hosts Advisory & VEX Secrets Activity Integrations Connect the external systems Stella Ops depends on, then verify them from the same setup surface. 3configured connectorsSuggested Setup OrderStart with the connectors that unblock releases and evidence, then add operator conveniences.RegistriesConnect the container sources that release versions, promotions, and policy checks depend on.Source ControlWire repository and commit metadata before relying on release evidence and drift context.CI/CDCapture pipeline runs and deployment triggers for release confidence.Advisory & VEX SourcesKeep security posture, exceptions, and freshness checks truthful.SecretsFinish by wiring vaults and credentials used by downstream integrations.Registries2SCM1CI/CD0Runtimes / Hosts0Advisory Sources0VEX Sources0Secrets0+ Add IntegrationView ActivityRecent ActivityUse the activity timeline for connector event history The hub summary shows configured connectors. Open View Activity for test, sync, and health events per integration." + } + }, + { + "key": "decision-capsules-heading", + "route": "/evidence/capsules", + "ok": true, + "snapshot": { + "heading": "Decision Capsules" + } + }, + { + "key": "replay-route-naming", + "route": "/evidence/exports/replay", + "ok": true, + "snapshot": { + "heading": "Replay & Verify" + } + }, + { + "key": "security-posture-heading", + "route": "/security", + "ok": true, + "snapshot": { + "heading": "Security Posture", + "text": "document.getElementById('stella-splash').dataset.ts=Date.now(); (function () { if (typeof window === 'undefined' || typeof document === 'undefined') { return; } window.__stellaWelcomePendingSignIn = false; document.addEventListener( 'click', function (event) { if (!window.location.pathname.startsWith('/welcome')) { return; } var target = event.target; if (!(target instanceof Element)) { return; } var button = target.closest('button.cta'); if (!button) { return; } if (typeof window.__stellaWelcomeSignIn === 'function') { return; } event.preventDefault(); window.__stellaWelcomePendingSignIn = true; }, true ); })(); Skip to main contentRelease ControlDashboardReleasesVersionsRelease HealthApprovalsPromotionsHotfixesOperationsScheduled JobsSignalsOffline KitEnvironmentsPolicyPlatform SetupNotificationsSecurity & AuditSecurity PostureTriageSupply-Chain DataReachabilityUnknownsReportsAuditDecision CapsulesReplay & VerifyExport CenterLogsBundlesPlatform & SetupSetupTopologyDiagnosticsIntegrationsIdentity & AccessTrust & SigningTenant & BrandingStella Ops v1.0.0-alphaStella OpsCtrl+KExport Report Context adminTenantDemo ProductionRegionUS EastEnvNo env defined yetWindow7dStageAllEvents: DEGRADEDPolicy: Core Policy Pack latestEvidence: ONFeed: LiveOffline: OKSecuritySecurity PostureSecurity PostureRelease-blocking posture, advisory freshness, and disposition confidence for the selected scope.Scopeus-east / all environmentsEvidence Rail: ONPolicy Pack: latestSnapshot: FAIL3 source(s) offline/stale; decision confidence reduced.DrilldownLoading security overview..." + } + }, + { + "key": "security-unknowns-error-state", + "route": "/security/unknowns", + "ok": true, + "snapshot": { + "heading": "Unknowns", + "banner": "Unknowns data is unavailableThe scanner unknowns APIs returned an error. Retry the request or verify the scanner service.Retry" + } + } + ], + "runtime": { + "consoleErrors": [], + "pageErrors": [], + "requestFailures": [], + "responseErrors": [] + }, + "failures": [] +} diff --git a/src/Web/StellaOps.Web/output/playwright/live-first-time-user-reporting-truthfulness-check.state.json b/src/Web/StellaOps.Web/output/playwright/live-first-time-user-reporting-truthfulness-check.state.json new file mode 100644 index 000000000..a1541bb7e --- /dev/null +++ b/src/Web/StellaOps.Web/output/playwright/live-first-time-user-reporting-truthfulness-check.state.json @@ -0,0 +1,18 @@ +{ + "cookies": [], + "origins": [ + { + "origin": "https://stella-ops.local", + "localStorage": [ + { + "name": "stellaops.sidebar.preferences", + "value": "{\"sidebarCollapsed\":false,\"collapsedGroups\":[],\"collapsedSections\":[]}" + }, + { + "name": "stellaops.theme", + "value": "system" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/Web/StellaOps.Web/output/playwright/live-first-time-user-ux-remediation-check.auth.json b/src/Web/StellaOps.Web/output/playwright/live-first-time-user-ux-remediation-check.auth.json new file mode 100644 index 000000000..e06d4afa3 --- /dev/null +++ b/src/Web/StellaOps.Web/output/playwright/live-first-time-user-ux-remediation-check.auth.json @@ -0,0 +1,39 @@ +{ + "authenticatedAtUtc": "2026-03-15T11:30:49.863Z", + "baseUrl": "https://stella-ops.local", + "finalUrl": "https://stella-ops.local/mission-control/board?tenant=demo-prod®ions=apac,eu-west,us-east,us-west", + "title": "Dashboard - StellaOps", + "cookies": [], + "storage": { + "localStorageEntries": [ + [ + "stellaops.sidebar.preferences", + "{\"sidebarCollapsed\":false,\"collapsedGroups\":[],\"collapsedSections\":[]}" + ], + [ + "stellaops.theme", + "system" + ] + ], + "sessionStorageEntries": [ + [ + "stellaops.auth.session.full", + "{\"tokens\":{\"accessToken\":\"eyJhbGciOiJSUzI1NiIsImtpZCI6IlRBSU1GTlE3LUFOR1JMU0JOQVlfQ19OSEdPVERVWUo5UlNVVTRSRUQiLCJ0eXAiOiJhdCtqd3QifQ.eyJpc3MiOiJodHRwczovL2F1dGhvcml0eS5zdGVsbGEtb3BzLmxvY2FsLyIsImV4cCI6MTc3MzU3NjA0NiwiaWF0IjoxNzczNTc0MjQ2LCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGVtYWlsIG9mZmxpbmVfYWNjZXNzIHVpLnJlYWQgdWkuYWRtaW4gdWkucHJlZmVyZW5jZXMucmVhZCB1aS5wcmVmZXJlbmNlcy53cml0ZSBhdXRob3JpdHk6dGVuYW50cy5yZWFkIGF1dGhvcml0eTp0ZW5hbnRzLndyaXRlIGF1dGhvcml0eTp1c2Vycy5yZWFkIGF1dGhvcml0eTp1c2Vycy53cml0ZSBhdXRob3JpdHk6cm9sZXMucmVhZCBhdXRob3JpdHk6cm9sZXMud3JpdGUgYXV0aG9yaXR5OmNsaWVudHMucmVhZCBhdXRob3JpdHk6Y2xpZW50cy53cml0ZSBhdXRob3JpdHk6dG9rZW5zLnJlYWQgYXV0aG9yaXR5OnRva2Vucy5yZXZva2UgYXV0aG9yaXR5OmJyYW5kaW5nLnJlYWQgYXV0aG9yaXR5OmJyYW5kaW5nLndyaXRlIGF1dGhvcml0eS5hdWRpdC5yZWFkIGdyYXBoOnJlYWQgc2JvbTpyZWFkIHNjYW5uZXI6cmVhZCBwb2xpY3k6cmVhZCBwb2xpY3k6c2ltdWxhdGUgcG9saWN5OmF1dGhvciBwb2xpY3k6cmV2aWV3IHBvbGljeTphcHByb3ZlIHBvbGljeTpydW4gcG9saWN5OmFjdGl2YXRlIHBvbGljeTphdWRpdCBwb2xpY3k6ZWRpdCBwb2xpY3k6b3BlcmF0ZSBwb2xpY3k6cHVibGlzaCBhaXJnYXA6c2VhbCBhaXJnYXA6c3RhdHVzOnJlYWQgb3JjaDpyZWFkIG9yY2g6b3BlcmF0ZSBvcmNoOnF1b3RhIGFuYWx5dGljcy5yZWFkIGFkdmlzb3J5OnJlYWQgYWR2aXNvcnktYWk6dmlldyBhZHZpc29yeS1haTpvcGVyYXRlIHZleDpyZWFkIHZleGh1YjpyZWFkIGV4Y2VwdGlvbnM6cmVhZCBleGNlcHRpb25zOmFwcHJvdmUgYW9jOnZlcmlmeSBmaW5kaW5nczpyZWFkIHJlbGVhc2U6cmVhZCByZWxlYXNlOndyaXRlIHJlbGVhc2U6cHVibGlzaCBzY2hlZHVsZXI6cmVhZCBzY2hlZHVsZXI6b3BlcmF0ZSBub3RpZnkudmlld2VyIG5vdGlmeS5vcGVyYXRvciBub3RpZnkuYWRtaW4gbm90aWZ5LmVzY2FsYXRlIGV2aWRlbmNlOnJlYWQgZXhwb3J0LnZpZXdlciBleHBvcnQub3BlcmF0b3IgZXhwb3J0LmFkbWluIHZ1bG46dmlldyB2dWxuOmludmVzdGlnYXRlIHZ1bG46b3BlcmF0ZSB2dWxuOmF1ZGl0IHBsYXRmb3JtLmNvbnRleHQucmVhZCBwbGF0Zm9ybS5jb250ZXh0LndyaXRlIGRvY3RvcjpydW4gZG9jdG9yOmFkbWluIG9wcy5oZWFsdGggaW50ZWdyYXRpb246cmVhZCBpbnRlZ3JhdGlvbjp3cml0ZSBpbnRlZ3JhdGlvbjpvcGVyYXRlIHBhY2tzLnJlYWQgcGFja3Mud3JpdGUgcGFja3MucnVuIHBhY2tzLmFwcHJvdmUgcmVnaXN0cnkuYWRtaW4gdGltZWxpbmU6cmVhZCB0aW1lbGluZTp3cml0ZSB0cnVzdDpyZWFkIHRydXN0OndyaXRlIHRydXN0OmFkbWluIHNpZ25lcjpyZWFkIHNpZ25lcjpzaWduIHNpZ25lcjpyb3RhdGUgc2lnbmVyOmFkbWluIiwianRpIjoiZmE4YWIyZjAtOWFkZC00ZWYwLWExZGYtN2IxODg5M2UwOTEyIiwic3ViIjoiYjA4NjM5NzQ1ZDY1NDkzNDhkODQzYWEzMTFjOTg5NTgiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJhZG1pbiIsIm5hbWUiOiJhZG1pbiIsInJvbGUiOiJhZG1pbiIsInN0ZWxsYW9wczp0ZW5hbnQiOiJkZW1vLXByb2QiLCJhdXRoX3RpbWUiOjE3NzM1NzQyNDYsIm9pX3Byc3QiOiJzdGVsbGEtb3BzLXVpIiwiY2xpZW50X2lkIjoic3RlbGxhLW9wcy11aSJ9.r9zGdTaHjGHb4BwjF-7zBe-GQCiJOFofYKWWg95uWJrc8h9sKef23YnO27ZDVKeBVfaGmwPUz5AKqQIdwZmz7qEOuIV37HMrvj_tR_4FuzDuuKQ1BjkCZxbPBWMp4jH2c2toMHJp91fwP_PGJ8rz6yl365qN5aro8uji3yLGLYS2k5h-dIq6tLA_a3_v7Dp2KQYSuPW3HjkvF0wTz0VESLYfQekq41flhPGwTk0psoY7IZEyBrYVbRkyPgO3VMVRFOkrtb8jbZII4h-jzNHlOb1COAwkSsbNFM88X-wDAiOX3pyVPCGektgtX6Dx3uSJSdLqmEY56fihz927j1-a7A\",\"tokenType\":\"Bearer\",\"refreshToken\":\"eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZDQkMtSFM1MTIiLCJraWQiOiJXUjVQSU4tVzI5S0lNTkpRTVBBMU9XS0VYN0tEWjZMV0dYTDdSS0dWIiwidHlwIjoib2lfcmVmdCtqd3QiLCJjdHkiOiJKV1QifQ.mwsW4b5LOvjIO1LQ-vcyCNZClzIW6LgRWE-qjfTFwFX68HQ3rKTrLD2RFlFeW834rFlTxyqnzqqq5fu0dwh44iU9fSo-9zDt-cSU7WJWCuWVyftw1APnCJBr9BUc1rWyV32X5HBUBWi4UlS2fbIK80jDK-7wMraNZJ9KHIfKgBkeB1dC5_5LPoEp37Vh0s9fsG-VFmQIcdWdro1wbF-awrS7VUYEzJgvBrB5XGmCAoL2S9Au_mYoQAj1O8WXrpAOXbWoZVGbUDtykwuG2LYqV7ojY9OVDhfdcgPhpNbx15u1Qr811TpTa9CNL3mYBmTFjPtZNlt-kmeK7bPs6igbxw.n0OWVZvLudPp5q1M0z6HOg.8tPbkGpNCKIfcBkre1NCjQshmSeu5yC9N-xjcFfKvdYD336BVxazkrMIDRs1Mi4qwFIsG_9wzNbniZUXdnXk2PN6yb1cjvVjsH0NwesiN7hRkdgy-HesFoJVDIBmeZeRIMYL5myRLoHsgXm0pJteQF37xzORIIuPxRQSmjmGEgxyBHUCVhh8F1YOhDZnlWbwnwq9EeL6_4VXVP4SCjiNM861-lGmPnBbyWPvwvtgZCnwsprKjjX3mWUGvqvUHx6sg3n8xUIJG_Z5W03UZudGG6gOxoJFEEiLOj7HWkouEhDZxUMr6GRhVftGo-ee8aIGlIXoYyP4yHE6Ta9i4GO2fg6013KdNXnmFctJXy26XOf61alILtNF4GlRl5Rz735ZlG3lfFSYzsN-4vPiyBbW2Ik0E5TBooK9dAleREuXvH3faAfQYB3Br-DLYcIm4wiW1qwpndEOBr6Mb-bQSFuJln34gyXdj9OmQnYAVDbc2IbQnPG3VbIXoDkUb69JoMEkGGHtVvKgtw_4E9DD5dmT4pi4IcYefrktMSFxJfpDa3hKv8APg4zTDgluGjjaok_2nIf080jF0K97SZ9bK0PMN11hNWScfCp-S7-BQhYgdAT3JVs-_0RIbOUFRmEXRosX_3YYq1a1eVrYYGLeiq-RjGTv0vxooi0NZ8JyU18tiQyeRM2BfKc4s7Tyt39L_MGUibZKISDEuzPl8LpaYPBK25aUv0coWMOUxU408IbDlx_iZNlU8i4oPZMZKKzmIEJuAqZwel9AWdQAo8PG4goV4I12xwUzpdxHJIzrN7OIjNjVIgnvamDSvP8BVLjOD9AshYf3dfqHnvts46sV5kdAYOB68hV8zAnD8GtJw8VDmPl9xmy-S6LYgH3MIwtQBVm9oun6KDDFB0YXF8X_2KXIm5Y05oZiuK0F6vghfgNpFd1Tj5QpNdTBNjFJBeEs6Itfe9p95g4hmZa9LXwYPVBor5FbcUGK8-6CxWymypwyE0KvDMljyO0KRQa9LRoFA9B3soqoHTDV6Dzyi4LA-BgeYSnaqWPjgGBPRmGIQq93wOrjG-GwiqrKTI8ChEhkHiMEctXj16rLZdd9jSq4EgzMvoOmACwHcWU2l1Xhzks2TnIRAjyBHQ9fG3E4qdJa7NsOJMYY-ayAj2fX8iRFpuOourTjvhIvCpBQZ81sbLF7EmXD4xbc7Aw-JhsqhmDj9qyov1_GKGN4cNc0en88BowoJc51tj0fO6st440_7kcbDi8EI7yZaNJYC4B0o6CmBIKMND7xPHMgtzSkK05ITiQpxnKeWardWMrv-HcSRiwO7bjYFM5Tmrdq8_hOhL9ZCCZMZcgTgprR70fCjuA19shk5hYcTFdLRiri-VA4k2HnVl-olrgD5m2nsUEX5SF-1jjTnLxR_sKXuKJmpx_GrT6422qw0sfAlPaZE3wcRMmXmU5ztjd7H8SB_Wjv99YQooH_q-PF4eab-DosTBoF5NczK1E5E3HdVzF632vZFZBx5-bQ6XaxWN3TMK_gHxK8F3w5QQZgVEOmPFtkKUH9xpafIqMaOP-x09mXbwbGfEAcm6kxU7wstVjSFjq5yYz2QidKrekYzHIqevb5NJtfFX7vrQA8G4bylvbXn4IuoA4jFvHPV46B_miaqqV6bUfyU8F3Wi-zB--YEoS2TCQVkbgrgL4fRcwTw6Ly5CrUBW_AsCtYUr1-khkrzfLY5zwVmpUYJhPxjoETNy5ZvCH__eEupEs15DX7aWCn5JCwII0oAgaZHC-2BtDwXSUeS_yZyo2bvo8HfBnq_6S3yfWM2POCkG-aoTGh7BHtyzdvgfIH7SALQ3VN8U0A-eRNOBUSSi9r5N3ChapGDmXLrr8-qh9Z8LpWrEObKSy9TuwdcJUfMZv2XnAVeHELE0OoHhjPmrThfuxORRjeg7dmqlM59Efc7ALC6b7qTuGEseH5MZHMtVzQIzfMjH_PSHZeIHQQf2IUWEqdtOCQCUhjP42XNgT8zH77nIhEHJpJbAjLFMBXr02C0bdF63ocCevjSaZH-JCju7WBnNFLQdgTQd5Ulo2qeLQzh6YBoLqaJ-sH63zeAu-aL3UKIqFBWXFamVL7V-x8TiFIH15hUTtMWDKL5k90O_Wnyl4Qr-8oRTAInW653dHiGigbI9u3A-gycCPK6PHwfcStx5hPC6UbGZNkRTJtt0vNbAHMX5YsdqbAI1yPGzCO7pkAOigFQUFwiWsk0_io_zH84xlFI1W-dAGcQSpMCW7BO3F7DLUbRcrxsK1JsmlXfCJa7SX6vN2E04ieSDfrbYhdmZyssVluy0WOGQdzMVDRhWRqHIhs11XRRiHrgk_vamijH2Ymt8fVKAqTt0H4K_rEkRqBEanjK1rad04Yhx9EM5A2tpyEbwNH7TonBMS8TGgPhqxt3fptPViMfr7-Npf6u96EsJBoC_v_VYY-geyletthtPDucC4KKWvexS9qEo0l_IA7j7cmikMM-80WLaMyUqH8YRubhYOwg4xJSLPIDzR48kq3z-cefiHQR4iiNolZGe-wOQgLNQfLND_YwtY-V2HM6WMJVP4-imn88WpknsM2jjfXg3E9-FkKim54ZqhH0aKbwuKcKdo_M8polescJlYU8n8TVYjmcO9X1M_QZzGg3L7rcIP2zejWOLmXVBVlMmVjjJVUbqj7L4piOfsmms8I5pqBRB1h-RmSyAqu1rXXFjJhy6NH9Vxx6j6r0JI7io4-CbF_y7BEcqgwi3hxJ32G3ao0WrwaE2Vr2UpPnqCehkd-bC9h721hQmlyzvODGS-xfBGCkx276osGUV84xDOAXWgfTTSPUos7C21OUxd7anuoV5UpFTER09Poy-cRSJeA8ISsCtE1vsvKw4ZDh2PybUEYBIdnnZptmCStzsAGagjLRt-rFpBZgYVgVOVbu6j5kmgAZsMGv21mL1AXZFHIRrLdy6G74CVtpVxCfz5oafcf1uXJWOdj3soTPt0W8ZigPi-AkdJ6rFmeo5kbGsChGgz3mpBH28apKayMu_PmhSEB1WibEQykZHCYLADFtDG3xQxT625J13bWLTwhuNEharB6JJfJm6kdYuQjBnM4me9kDdztV3S54URh8CyumUBYzWoufIippn5vzGsPjBi2LDIG7Jt5jC5GMsEiZfxr0Er468r0EkXaTTsGrYvFtRDOZyJrqdAI4xSAzgtlfQAFwTmi_VLPP-2xpQOnHB0LNX54CiGCvFIi5WSgLrT8-vho6E8VggB4xOqNZJv3TAqotF3sH8ZTVyjUIdxTegYQUY1ddMZ_LudWA4Nr8oyQzO4huYRQ1MfmnmRqqXkeVuLnYbfIVublGwBZfX51GDUXvjOOTWtFbY05DBTLjlmDaQzM02XqxBg03cJB9P3Wq0ABfTI1lbLpSyjid63LoQa84umZ_O1pa5VZvevuxbLj40qX2UUAjWd8SNqD5W9P17ZLelER-fleVmw3QJV9TpmTOD2A5VjyCNs9SQCVv64RQTRZy5hM_NUONgDyGIS7Lq8i9_Ksn1dWLKpKql_6QSmUBmRBtNOI6sefAvko6L5emwc58F4SPxCm53f23xeq4Wf79C4W3FexRl5HYEUIZcrAUdKByRqjitirvbTSTXTMoqrVvWiWDwZ4iPMsM-rIyVYvbEn5xNRRhiUShSX0RST9w85p1MM4m-LxzD8FvFJfNL9Oin-PrpvRYB0DIkg384XD2xa-ELZpRAaohg_sFMxiYF1z05mC-fxfRWednkgYggXLxF7YCQk7wCL8LwfP30GOlXVEFd30cP8m9tdaZ1eg4Lf12UTBEwM4Ffj6tDMc8wom0qjAzrKjFA6swvNK6rfW3cgwBVPKLWuWdAc1Ehd-zNRjYJShMRc_J-Stf7OdhVemt0X07w2Xz5X3LcGsNmOQrG9tGwgpcrPQB3GTgYtGFCTQvLb0HjJmh0EKdpqdNbi64-gHiVzpSKMq69Ws81o4AnYHXq1ZeiAIaXqRPQT5QLrWURIE-V2D6tfC9h4i5JJE-j8vZ--Pg3dAx-UwU9fAg2GWY97fL9kMclS8GI53FRmppp15D6OQZKO5Np_2s3MCrxz9ML1oR66iMilotXdcKqoDl5sF8BJMfpeAgojCXV5MXtUmggSg_46xOeqHvTebUgPYPlKmYYF0WJZOKzBfZUAVr1wW2TMirIHx9AFKsBKgwh3S4WCqa9tA-WinBk7BXdpcDUGQjsKUrFympeDXGaCxfD7tzXgeWM4VM7PLISBbu4e1Z3KU5GBnXngwiikRnI-ZjU3H9-0otJusLXUKVAkIRs1MiaVcr_-DKfMWQZXJkW3g0jRlN7n4wgt0l3i3McaByZmtkQOV2VRPMlDt3rY7BBozCqD5NXbCBtWOxgLe0eymvB9moudNBcb7UiI9a4XHaGxD9JUQViPRMSNn9YCkwPiy-z1Q_vTbjSa7CIeRad83ZGcusNv5OlzeWoULPSOxrl7HoEg-XW3AshQ1V5DOoeBQ6X8DpM3XhKUSwRMY2ROIduAdSnfLQJcgT30XL8SAmxb_Vk9KMiucb_6flJzwy4hVcnD2SarNE5AAEDCiqTHnJWv0-84VhHPrjJN0koTg8XXtKXOcefTi-Wuyoo0GxYglOieu69EGH9WhKptSZzMYSzzsMuM1TkW1oLQS54105g_qbHpAKfLPY9Yyv4IY1m8djXWcc3aOgjTyj7fh5HWiR8_yWcdyI3_Sq0djkhnJ0D2lDBa-2ThrD7hV9gwv5JsQ_K-eHw.A5qPZxppCk6-82NCVcercdy5iUgn9BfYPZdKDUQ2008\",\"scope\":\"openid profile email offline_access ui.read ui.admin ui.preferences.read ui.preferences.write authority:tenants.read authority:tenants.write authority:users.read authority:users.write authority:roles.read authority:roles.write authority:clients.read authority:clients.write authority:tokens.read authority:tokens.revoke authority:branding.read authority:branding.write authority.audit.read graph:read sbom:read scanner:read policy:read policy:simulate policy:author policy:review policy:approve policy:run policy:activate policy:audit policy:edit policy:operate policy:publish airgap:seal airgap:status:read orch:read orch:operate orch:quota analytics.read advisory:read advisory-ai:view advisory-ai:operate vex:read vexhub:read exceptions:read exceptions:approve aoc:verify findings:read release:read release:write release:publish scheduler:read scheduler:operate notify.viewer notify.operator notify.admin notify.escalate evidence:read export.viewer export.operator export.admin vuln:view vuln:investigate vuln:operate vuln:audit platform.context.read platform.context.write doctor:run doctor:admin ops.health integration:read integration:write integration:operate packs.read packs.write packs.run packs.approve registry.admin timeline:read timeline:write trust:read trust:write trust:admin signer:read signer:sign signer:rotate signer:admin\",\"expiresAtEpochMs\":1773576046230},\"identity\":{\"subject\":\"b08639745d6549348d843aa311c98958\",\"name\":\"admin\",\"roles\":[],\"idToken\":\"eyJhbGciOiJSUzI1NiIsImtpZCI6IlRBSU1GTlE3LUFOR1JMU0JOQVlfQ19OSEdPVERVWUo5UlNVVTRSRUQiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2F1dGhvcml0eS5zdGVsbGEtb3BzLmxvY2FsLyIsImV4cCI6MTc3MzU3NzU0NiwiaWF0IjoxNzczNTc0MjQ2LCJhdWQiOiJzdGVsbGEtb3BzLXVpIiwic3ViIjoiYjA4NjM5NzQ1ZDY1NDkzNDhkODQzYWEzMTFjOTg5NTgiLCJuYW1lIjoiYWRtaW4iLCJhenAiOiJzdGVsbGEtb3BzLXVpIiwibm9uY2UiOiJkYTgwODc0Ny02MTk3LTQxYjctYmY0Ni05NDhiYWMxMDA3ZDIiLCJhdF9oYXNoIjoibTR2UWM2eWpDMVNjMG9rREVJWXJUdyJ9.oL80humfO4Cjr1EGXvchR76PqZz3fHSurmJwnX9keYm2gOg5DNsCtZzXhEm0P3lHxHB-u1Fd0f4xsE_yqwhdu9ppqUCyFMGUp_ZYCX7f4PUoEjzIXhe9uHN22vkttEL5NGOhpKUuKtPzsNwJAAxxZaILOEAxPVqY6i_BMXxOUDTqPpoyrUOIxLjqE3ixKZtGwYysiRW36Dk2EXnH90tBSyaU-9p2S5K-hzhThAOA_6N1GeW-lENVXh65o3Idd72G4pBVmNwwuWEWKDrBpFdoCiraSOE_NWoCvwdgnpEksuFXVoH2mUuELBaVECiU-rTRU99scOAS6TTbXGGzoiHi7Q\"},\"dpopKeyThumbprint\":\"OzyQuTZmjFm9TSQr79exTFw8D9yeerRLQ4qOXPcWlbA\",\"issuedAtEpochMs\":1773574247231,\"tenantId\":\"demo-prod\",\"scopes\":[\"advisory-ai:operate\",\"advisory-ai:view\",\"advisory:read\",\"airgap:seal\",\"airgap:status:read\",\"analytics.read\",\"aoc:verify\",\"authority.audit.read\",\"authority:branding.read\",\"authority:branding.write\",\"authority:clients.read\",\"authority:clients.write\",\"authority:roles.read\",\"authority:roles.write\",\"authority:tenants.read\",\"authority:tenants.write\",\"authority:tokens.read\",\"authority:tokens.revoke\",\"authority:users.read\",\"authority:users.write\",\"doctor:admin\",\"doctor:run\",\"email\",\"evidence:read\",\"exceptions:approve\",\"exceptions:read\",\"export.admin\",\"export.operator\",\"export.viewer\",\"findings:read\",\"graph:read\",\"integration:operate\",\"integration:read\",\"integration:write\",\"notify.admin\",\"notify.escalate\",\"notify.operator\",\"notify.viewer\",\"offline_access\",\"openid\",\"ops.health\",\"orch:operate\",\"orch:quota\",\"orch:read\",\"packs.approve\",\"packs.read\",\"packs.run\",\"packs.write\",\"platform.context.read\",\"platform.context.write\",\"policy:activate\",\"policy:approve\",\"policy:audit\",\"policy:author\",\"policy:edit\",\"policy:operate\",\"policy:publish\",\"policy:read\",\"policy:review\",\"policy:run\",\"policy:simulate\",\"profile\",\"registry.admin\",\"release:publish\",\"release:read\",\"release:write\",\"sbom:read\",\"scanner:read\",\"scheduler:operate\",\"scheduler:read\",\"signer:admin\",\"signer:read\",\"signer:rotate\",\"signer:sign\",\"timeline:read\",\"timeline:write\",\"trust:admin\",\"trust:read\",\"trust:write\",\"ui.admin\",\"ui.preferences.read\",\"ui.preferences.write\",\"ui.read\",\"vex:read\",\"vexhub:read\",\"vuln:audit\",\"vuln:investigate\",\"vuln:operate\",\"vuln:view\"],\"audiences\":[],\"authenticationTimeEpochMs\":1773574246000,\"freshAuthActive\":false,\"freshAuthExpiresAtEpochMs\":null}" + ], + [ + "stellaops.auth.session.info", + "{\"subject\":\"b08639745d6549348d843aa311c98958\",\"expiresAtEpochMs\":1773576046230,\"issuedAtEpochMs\":1773574247231,\"dpopKeyThumbprint\":\"OzyQuTZmjFm9TSQr79exTFw8D9yeerRLQ4qOXPcWlbA\",\"tenantId\":\"demo-prod\"}" + ], + [ + "stella-search-session-id", + "12992d71-099c-4e23-bfc7-581b113fce25" + ] + ] + }, + "events": { + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [] + }, + "statePath": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\live-first-time-user-ux-remediation-check.state.json" +} diff --git a/src/Web/StellaOps.Web/output/playwright/live-first-time-user-ux-remediation-check.json b/src/Web/StellaOps.Web/output/playwright/live-first-time-user-ux-remediation-check.json new file mode 100644 index 000000000..669ac5609 --- /dev/null +++ b/src/Web/StellaOps.Web/output/playwright/live-first-time-user-ux-remediation-check.json @@ -0,0 +1,255 @@ +{ + "generatedAtUtc": "2026-03-15T11:31:03.886Z", + "failedCheckCount": 0, + "runtimeIssueCount": 0, + "results": [ + { + "key": "security-posture-canonical", + "route": "/security", + "ok": true, + "snapshot": { + "key": "security-posture-canonical", + "url": "https://stella-ops.local/security?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Security Posture - Stella Ops QA 1773540847164", + "heading": "Security Posture", + "alerts": [ + "Loading security overview..." + ] + } + }, + { + "key": "security-posture-alias", + "route": "/security/posture?uxAlias=1#posture-check", + "ok": true, + "snapshot": { + "key": "security-posture-alias", + "url": "https://stella-ops.local/security?uxAlias=1&tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d#posture-check", + "title": "Security Posture - Stella Ops QA 1773540847164", + "heading": "Security Posture", + "alerts": [] + } + }, + { + "key": "replay-and-verify", + "route": "/evidence/verify-replay", + "ok": true, + "snapshot": { + "key": "replay-and-verify", + "url": "https://stella-ops.local/evidence/verify-replay?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Replay & Verify - Stella Ops QA 1773540847164", + "heading": "Replay & Verify", + "alerts": [] + } + }, + { + "key": "release-health", + "route": "/releases/health", + "ok": true, + "snapshot": { + "key": "release-health", + "url": "https://stella-ops.local/releases/health?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Release Health - Stella Ops QA 1773540847164", + "heading": "Release Health", + "alerts": [] + } + }, + { + "key": "setup-guided-path", + "route": "/setup", + "ok": true, + "snapshot": { + "key": "setup-guided-path", + "url": "https://stella-ops.local/setup?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Setup Overview - Stella Ops QA 1773540847164", + "heading": "Setup", + "alerts": [], + "links": [ + "#main-content", + "/mission-control/board", + "/releases/deployments", + "/releases/versions", + "/releases/health", + "/releases/approvals", + "/releases/promotions", + "/releases/hotfixes", + "/ops/operations", + "/ops/operations/jobengine", + "/ops/operations/signals", + "/ops/operations/offline-kit", + "/ops/operations/environments", + "/ops/policy", + "/ops/platform-setup", + "/ops/operations/notifications", + "/security", + "/triage/artifacts", + "/security/supply-chain-data", + "/security/reachability", + "/security/unknowns", + "/security/reports", + "/evidence/overview", + "/evidence/capsules", + "/evidence/verify-replay", + "/evidence/exports", + "/evidence/audit-log", + "/triage/audit-bundles", + "/setup/system", + "/setup/topology/overview" + ] + } + }, + { + "key": "setup-notifications-ownership", + "route": "/setup/notifications", + "ok": true, + "snapshot": { + "key": "setup-notifications-ownership", + "url": "https://stella-ops.local/setup/notifications?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Notifications - Stella Ops QA 1773540847164", + "heading": "Notification Administration", + "alerts": [], + "links": [ + "#main-content", + "/mission-control/board", + "/releases/deployments", + "/releases/versions", + "/releases/health", + "/releases/approvals", + "/releases/promotions", + "/releases/hotfixes", + "/ops/operations", + "/ops/operations/jobengine", + "/ops/operations/signals", + "/ops/operations/offline-kit", + "/ops/operations/environments", + "/ops/policy", + "/ops/platform-setup", + "/ops/operations/notifications", + "/security", + "/triage/artifacts", + "/security/supply-chain-data", + "/security/reachability", + "/security/unknowns", + "/security/reports", + "/evidence/overview", + "/evidence/capsules", + "/evidence/verify-replay", + "/evidence/exports", + "/evidence/audit-log", + "/triage/audit-bundles", + "/setup/system", + "/setup/topology/overview" + ] + } + }, + { + "key": "operations-notifications-ownership", + "route": "/ops/operations/notifications", + "ok": true, + "snapshot": { + "key": "operations-notifications-ownership", + "url": "https://stella-ops.local/ops/operations/notifications?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Notifications - Stella Ops QA 1773540847164", + "heading": "Notification Operations", + "alerts": [], + "links": [ + "#main-content", + "/mission-control/board", + "/releases/deployments", + "/releases/versions", + "/releases/health", + "/releases/approvals", + "/releases/promotions", + "/releases/hotfixes", + "/ops/operations", + "/ops/operations/jobengine", + "/ops/operations/signals", + "/ops/operations/offline-kit", + "/ops/operations/environments", + "/ops/policy", + "/ops/platform-setup", + "/ops/operations/notifications", + "/security", + "/triage/artifacts", + "/security/supply-chain-data", + "/security/reachability", + "/security/unknowns", + "/security/reports", + "/evidence/overview", + "/evidence/capsules", + "/evidence/verify-replay", + "/evidence/exports", + "/evidence/audit-log", + "/triage/audit-bundles", + "/setup/system", + "/setup/topology/overview" + ] + } + }, + { + "key": "evidence-overview-operator-mode", + "route": "/evidence/overview", + "ok": true, + "snapshot": { + "key": "evidence-overview-operator-mode", + "url": "https://stella-ops.local/evidence/overview?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Evidence Overview - Stella Ops QA 1773540847164", + "heading": "Evidence & Audit", + "alerts": [] + } + }, + { + "key": "sidebar-label-alignment", + "route": "/mission-control/board", + "ok": true, + "snapshot": { + "key": "sidebar-label-alignment", + "url": "https://stella-ops.local/mission-control/board?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Dashboard - Stella Ops QA 1773540847164", + "heading": "Dashboard", + "alerts": [], + "links": [ + "/mission-control/board", + "/releases/deployments", + "/releases/versions", + "/releases/health", + "/releases/approvals", + "/releases/promotions", + "/releases/hotfixes", + "/ops/operations", + "/ops/operations/jobengine", + "/ops/operations/signals", + "/ops/operations/offline-kit", + "/ops/operations/environments", + "/ops/policy", + "/ops/platform-setup", + "/ops/operations/notifications", + "/security", + "/triage/artifacts", + "/security/supply-chain-data", + "/security/reachability", + "/security/unknowns", + "/security/reports", + "/evidence/overview", + "/evidence/capsules", + "/evidence/verify-replay", + "/evidence/exports", + "/evidence/audit-log", + "/triage/audit-bundles", + "/setup/system", + "/setup/topology/overview", + "/ops/operations/doctor", + "/setup/integrations", + "/setup/identity-access", + "/setup/trust-signing", + "/setup/tenant-branding" + ] + } + } + ], + "runtime": { + "consoleErrors": [], + "pageErrors": [], + "requestFailures": [], + "responseErrors": [] + } +} diff --git a/src/Web/StellaOps.Web/output/playwright/live-first-time-user-ux-remediation-check.state.json b/src/Web/StellaOps.Web/output/playwright/live-first-time-user-ux-remediation-check.state.json new file mode 100644 index 000000000..a1541bb7e --- /dev/null +++ b/src/Web/StellaOps.Web/output/playwright/live-first-time-user-ux-remediation-check.state.json @@ -0,0 +1,18 @@ +{ + "cookies": [], + "origins": [ + { + "origin": "https://stella-ops.local", + "localStorage": [ + { + "name": "stellaops.sidebar.preferences", + "value": "{\"sidebarCollapsed\":false,\"collapsedGroups\":[],\"collapsedSections\":[]}" + }, + { + "name": "stellaops.theme", + "value": "system" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/Web/StellaOps.Web/output/playwright/live-frontdoor-auth-report.json b/src/Web/StellaOps.Web/output/playwright/live-frontdoor-auth-report.json new file mode 100644 index 000000000..21da8ea45 --- /dev/null +++ b/src/Web/StellaOps.Web/output/playwright/live-frontdoor-auth-report.json @@ -0,0 +1,39 @@ +{ + "authenticatedAtUtc": "2026-03-15T23:41:54.655Z", + "baseUrl": "https://stella-ops.local", + "finalUrl": "https://stella-ops.local/mission-control/board?tenant=demo-prod®ions=us-east&environments=prod-us-east", + "title": "Dashboard - StellaOps", + "cookies": [], + "storage": { + "localStorageEntries": [ + [ + "stellaops.sidebar.preferences", + "{\"sidebarCollapsed\":false,\"collapsedGroups\":[],\"collapsedSections\":[]}" + ], + [ + "stellaops.theme", + "system" + ] + ], + "sessionStorageEntries": [ + [ + "stellaops.auth.session.full", + "{\"tokens\":{\"accessToken\":\"eyJhbGciOiJSUzI1NiIsImtpZCI6IlRBSU1GTlE3LUFOR1JMU0JOQVlfQ19OSEdPVERVWUo5UlNVVTRSRUQiLCJ0eXAiOiJhdCtqd3QifQ.eyJpc3MiOiJodHRwczovL2F1dGhvcml0eS5zdGVsbGEtb3BzLmxvY2FsLyIsImV4cCI6MTc3MzYxOTkxMiwiaWF0IjoxNzczNjE4MTEyLCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGVtYWlsIG9mZmxpbmVfYWNjZXNzIHVpLnJlYWQgdWkuYWRtaW4gdWkucHJlZmVyZW5jZXMucmVhZCB1aS5wcmVmZXJlbmNlcy53cml0ZSBhdXRob3JpdHk6dGVuYW50cy5yZWFkIGF1dGhvcml0eTp0ZW5hbnRzLndyaXRlIGF1dGhvcml0eTp1c2Vycy5yZWFkIGF1dGhvcml0eTp1c2Vycy53cml0ZSBhdXRob3JpdHk6cm9sZXMucmVhZCBhdXRob3JpdHk6cm9sZXMud3JpdGUgYXV0aG9yaXR5OmNsaWVudHMucmVhZCBhdXRob3JpdHk6Y2xpZW50cy53cml0ZSBhdXRob3JpdHk6dG9rZW5zLnJlYWQgYXV0aG9yaXR5OnRva2Vucy5yZXZva2UgYXV0aG9yaXR5OmJyYW5kaW5nLnJlYWQgYXV0aG9yaXR5OmJyYW5kaW5nLndyaXRlIGF1dGhvcml0eS5hdWRpdC5yZWFkIGdyYXBoOnJlYWQgc2JvbTpyZWFkIHNjYW5uZXI6cmVhZCBwb2xpY3k6cmVhZCBwb2xpY3k6c2ltdWxhdGUgcG9saWN5OmF1dGhvciBwb2xpY3k6cmV2aWV3IHBvbGljeTphcHByb3ZlIHBvbGljeTpydW4gcG9saWN5OmFjdGl2YXRlIHBvbGljeTphdWRpdCBwb2xpY3k6ZWRpdCBwb2xpY3k6b3BlcmF0ZSBwb2xpY3k6cHVibGlzaCBhaXJnYXA6c2VhbCBhaXJnYXA6c3RhdHVzOnJlYWQgb3JjaDpyZWFkIG9yY2g6b3BlcmF0ZSBvcmNoOnF1b3RhIGFuYWx5dGljcy5yZWFkIGFkdmlzb3J5OnJlYWQgYWR2aXNvcnktYWk6dmlldyBhZHZpc29yeS1haTpvcGVyYXRlIHZleDpyZWFkIHZleGh1YjpyZWFkIGV4Y2VwdGlvbnM6cmVhZCBleGNlcHRpb25zOmFwcHJvdmUgYW9jOnZlcmlmeSBmaW5kaW5nczpyZWFkIHJlbGVhc2U6cmVhZCByZWxlYXNlOndyaXRlIHJlbGVhc2U6cHVibGlzaCBzY2hlZHVsZXI6cmVhZCBzY2hlZHVsZXI6b3BlcmF0ZSBub3RpZnkudmlld2VyIG5vdGlmeS5vcGVyYXRvciBub3RpZnkuYWRtaW4gbm90aWZ5LmVzY2FsYXRlIGV2aWRlbmNlOnJlYWQgZXhwb3J0LnZpZXdlciBleHBvcnQub3BlcmF0b3IgZXhwb3J0LmFkbWluIHZ1bG46dmlldyB2dWxuOmludmVzdGlnYXRlIHZ1bG46b3BlcmF0ZSB2dWxuOmF1ZGl0IHBsYXRmb3JtLmNvbnRleHQucmVhZCBwbGF0Zm9ybS5jb250ZXh0LndyaXRlIGRvY3RvcjpydW4gZG9jdG9yOmFkbWluIG9wcy5oZWFsdGggaW50ZWdyYXRpb246cmVhZCBpbnRlZ3JhdGlvbjp3cml0ZSBpbnRlZ3JhdGlvbjpvcGVyYXRlIHBhY2tzLnJlYWQgcGFja3Mud3JpdGUgcGFja3MucnVuIHBhY2tzLmFwcHJvdmUgcmVnaXN0cnkuYWRtaW4gdGltZWxpbmU6cmVhZCB0aW1lbGluZTp3cml0ZSB0cnVzdDpyZWFkIHRydXN0OndyaXRlIHRydXN0OmFkbWluIHNpZ25lcjpyZWFkIHNpZ25lcjpzaWduIHNpZ25lcjpyb3RhdGUgc2lnbmVyOmFkbWluIiwianRpIjoiZTAyYTdhOWEtYmUzZS00MDllLTljNTctN2Y5ZDAzYzA5N2NlIiwic3ViIjoiYjA4NjM5NzQ1ZDY1NDkzNDhkODQzYWEzMTFjOTg5NTgiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJhZG1pbiIsIm5hbWUiOiJhZG1pbiIsInJvbGUiOiJhZG1pbiIsInN0ZWxsYW9wczp0ZW5hbnQiOiJkZW1vLXByb2QiLCJhdXRoX3RpbWUiOjE3NzM2MTgxMTIsIm9pX3Byc3QiOiJzdGVsbGEtb3BzLXVpIiwiY2xpZW50X2lkIjoic3RlbGxhLW9wcy11aSJ9.J0uQu-5crj9f3DX63Wzseh4Kf9hSAS0O8c0qeQ7dIkDjYDwm-18_2XHckXDYuBea7A19kmi4Kqr59lP7AG1HL2phmzyTV2D6vFANxD_Sjr4LjjL75Z1KxqKsbsfBS4yzzk6PTlAST1VXayKjAK4RV7QJAnhP4TL_u8m29t5OYBe6MogRVaH3DiPzZlIEhizgnNICcEhN3naar-JIgF2h9FHNiF_O2_KpN_c6lvenjMs7lhyt0kD1BwZeF_AFuRGv02JMFwDBYn_fX4fUa85Cm_gADjV51m0PIxXMnjCMnTRFN8k1hjMwArs7hAabbWlhdRABFaIbC-tWXGAgiRlIvw\",\"tokenType\":\"Bearer\",\"refreshToken\":\"eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZDQkMtSFM1MTIiLCJraWQiOiJXUjVQSU4tVzI5S0lNTkpRTVBBMU9XS0VYN0tEWjZMV0dYTDdSS0dWIiwidHlwIjoib2lfcmVmdCtqd3QiLCJjdHkiOiJKV1QifQ.TBkzHlWk1Sd3JQT_PjMzk3rT_JLqidMokiek6Avs-h2QNcEsAcSleIwB_jN7ioV5ICwmUcp6O2yIZCqSBIRenBZMDIZcgFLHfR6qH_neUpg-kX0DQgZVFTpfumvGAav66WfWsUZCdoj1DSEIZatv7DTpfpes_xfzutIy71DDy-4yfEG7vTqKoubdycmukkyVsrTdeMcjezMbd2mWlfGbBKzmDNFnWW2nI9gG06QjrsM_YWFMJWsI75UAbTrM5L1hMP-UR4h5rKO1YAMMKSKHNtuRvMsOSJNCsGuAkTWT0HC3TbxYCn--uN6NLPC-iPI4Xp__GGPIeGA9--L1F-233g.cZCuDc9jCZI-oQ_m1OZN5w.BRUAG5RQkvWy7MSqWYCFAMXpPDvUtFuhMzjpGfOod2Q7FmTCVgL0_Iz4-koRXdMIdeIwKtvibN482oDT8WFfvfadrDFG4yuW_3xnF5QC2c-tmhM1ixUi4CVSeTAkgyqdFbU24ZnOsfvNVUwyBFeyU3WhB84k7t2ycxyQkmc61-nji4epoTS5lq9jTzUmPzhRA5hYSq6L28NPVNoUWtyduiTzE6P26herQ-Y_SuKbB3237pl1p02cFX1k4XEsXZGkYTxiTFQwDX2k6Sk2Y_Hk7Lkgw8i5kSeJb152U5fllbl3-nKCSMxsIp2WyHaf8rOE3vOksHp-5_x_b63V9BonmJ2EbMjIfhuJEF_AdVnEnyg9f7g5yssiBmWcLoR2d18ViHUhw3IAsvq1-vlC2vTzFPP8_SLGZUw3HpHmIxpPiX1nmWXcU974lx4Y_2jEf6PiS9q_KU6CTxyR47pcNSOWjDOM3-DwL3mlg27oMV6fUG8zhez5WHZ9MknJ1geVMbL837YS8Tuqx99-6BriM1rWT8oxcH7PYHwsEVOjGZ2NSmnxpNKx0G4C9R1Xho6aSzyOlyRFv_1yMz51xU66OsmpGL07cH4na1vBjEtE3REMSYSnPET5_gHmbKxcwzSk2cxnqh20LB5z1kPRrQmFgauTe0Q-zCDZGNKry9g8s_CXAAT62n8F9PPbVfwHnIYa02NTT0QcB5q9XgaLkCA8X7alolgXeqjEtTn8ArBErPFZBkun9Qfo5YFMDVYNGHinItkIiaLZWcHo38rvWCFp3rWQAlFrCGMxi0cLxpgnWt7-jWaRfSoUO3u2WXzCi5JpaFH3V00ENwGrhgHHu3f1BImyL45-93rXTSwq6FxTRkBxuyCuiw4tCjRjQaDk7c4rjRkt7rJGOD8vW2rNSJFO2IdIZqhzb2rjVf_FT1A_4ADmt3p2im42mII56NXN-mN6PFbwSu4Gv9QhTSGDurpIpvzvBoBY_iIb7C1dnTpIzaygVVwDRyhlQxOaBDVd8St_jonxC0M775bZ-1PYshmSUmDCQIX9mKiBze21zUP1cQKRpPGoTfVAHlGHljmkUVZYOCkD9MA5Rx5F2SuVDKq5QnCUGt3FO-tLSvvEe8FAekU4Kv-X1vV6ThsJqQkPGrc_TBJK9uPyuhye4VHWLIMi2YM5pzRX-Wz6jgeUKHxXB3db-s6MkptUVT8V4NofoOx7OlgaJeVgyKAZNhdTDXnnML1G1xPrUrKZALXLN1Mk3bOci_OJlw97LOeagC5bDYXjOpVMSPwNV9BKpS94aPxFsg-UPNcqbPcWVNxSARMSWk5UtuEZ3TDDsHwJWMNqOIKLLfxxSmoRxFaopP5HSp8uUdykR9nA5EhZHtjhQsYaoFQfDOq89uS53rfP7vbN8w8KKevVZl37iKHg8kKaj4igPfVeS_l8trEcYZ3Zkng2AURpqK-BzSg4bkZnVkLuPK3Mc1ZbIrmmx0xXkcOWITjJUlfuKTk-VCHNf5oipTmYYk2Crd8DdvJfRcKJc_jhJskO6400KQnKyA8KDxAgA_NQ7ttktlRAOTByGrwMYtHpedqG7kvH3pXluQAivlLpyRMK69QyoUTr5n-GHTyM6RvQL0ZPy0lE87nuYcAsnfHvAZdc0FUWKnwjkpGE9mqDPNNWa4nPHa7YT9wfpyl4Yh-Ud1BvqwXBbUbAF2K8CwxYDw3dAaVXs4fHttYmvba7fG5jCooRGVfcxiTK9PwcShXfovobtMRUd2BPkelIP88q2gjhMHhvJ6d_u7AVpZFGycbP5c8sWNxrXlAM5rIfsUjDqQ5IDmhefW5yxDZkoxwTIjsX0IfLCj80aykIIvd1NR2ld3YlJFNoXSxYl5uw9w8WQf_Or44TPDJSYy_2Xjjq-BCQ2OKAvbvRithr0ereWL8G6b0SvHX2PxjN7RYasg1aJBzvNHx5fIAVLI1gpvtYOLl84HecBmow0qDshMlbYIsbXENqnUOmkoyC__YRrPMn2eEw9KXbkB556w34CqelbXNbRM7fZNFTvCaOeJ3b-dpVr2TVMJ6FIcXX02YOwOt_AEGnZHzgW1rj05iRD0EkS389-w9lOdt-bzsR0k5uoKsPXaV0NP5bSnPXiUoP4Llq3BYrXwrUNHXsrBAbH3JufXeMHQwJoGqpXXTN2aTIq_-wowhFIcvmbh6cntoaqp5NFLaN4qRspUwhwY98kP8CND4IEOVpnu-Fe8fHCyIVsz6z7iS4_tCS_tB7EmOCc39vZkggPF_zbw9v3ndLQjWBNlwU7zVo0Kk9e4Ty0hIVGpjzAGhy5JIwu4aD1CPdhoSruUkig7g0SkIhvR7bDKcGX8GBQoUb91c-FheDNFWg2IlbWJi9yzv6st-mJbd6JQ24BSXPrgQ3O_MPl0CTTDdEWSVMVnP718vkaNXOHwKeQxDncWVkAVBwluGIIP-kQSWRo8YY3frIwkDU1jiuRrb__g8jZa98fpC13mr6BC5cNrYBMEyaMT1v54XnE0Vlq5d34QUjTfoonNfhiCtExrINn9IlGbjHfAETRSITMRZNuGHNIgiHhwNx7JJDx0ASQOzs7tDZM6uJpJHJdayqQJYhMDFLBKJon-SCj0xxUQfKAM8_kqUcau6nXVvpklRw4Vvq5pNtc7iW4Qh5FeNKvHku7-yq_PBDr_FdGcJW1z5lr2aoVWkXI4ibqudUZiZUY63-oxGkyiw2P_ZPj0X96NQzKPcTuNRcO2U5HqZDlSe-LPo6lSX4ckEitDpXNWxSO063wrCPqZr4cZtoIO99iVveeHr0VjmEFn7-rraUIZQoZbav19nGm3UXt6OdpEug9av_4CuK2ih4bux1rpOqPUrVEYHQfrzZ6XelsS7Px9FkRLmX6AAQxmJbAh4huvyVun1iecXGTC4b_W8QNWbiAaoNcQMPfZkbl-sPaFGlLUMsledDzal8XVum1iKh8VdHaMKJZnTml6AYRM090A3vj6kyXa3iFXunwzlXRABBqUV9DJ7Ho433RsiV0ZKXNdz5Mrk2lND5YAVLvKe6ec1wYhuTZu9HVUJvguuZJI7a3BGickDOzSYmC83xM0wXcFcxp37Y9XFPkCjq8Su0uufQKWb769fCUONndYmxa3dBpU1ndmv5xER8ONkZhw1IsShaGRqUp2FumxMyY8UHghFH7ocIoo7yolhuv7CtwN6BW3FJVKV6OdcDZeZvnzEG2fYLAxKT-1LeH6mp_gWZyS6N8BZSr7aISScaTvvrcJxEgKJbe9WEzV9jFGfsEq1DZO6zcw6di0mxK4HchwTw34r-TDeUFM2y7vLr8eAbn2PK6NUo_ePqcKlbLZf2WiWMTd5qSHIHgi9Q0q7BvXWYUi7s0d9ZbWFFsg-sKm2EbsUfUTlBOKwWTJMGqOfGYkcie93ZYXa03KjUO7_da8SjLLBtxEoE68MO2SI0nyj2f1Vsd9mEEag7ughmsomK0D08sq3WAj26zQ06N32Ap9r0GORW6agqycOsDN9u4H8nMwIMYkfvEmVoZPeC0R06SMprDVJhajGUbPyCwIArlEgLlJhfoBQS8ki4Dci3-W0JMl-z9nHsnEFNE-9-R5j645XuIRnP7onFqgOnpVezWgp6-vaNssMY7Y12Vfp1aeaICqNijLX4S1kqxQOxSDvGQATRBY-yAZcGU_53-ez1nBsfNhfWMhUjm3QrobIZvk-Gl1WJ3d4Qpp4ZEU-HyWtKms7NwhFLp6PQFl7_htkJFbojBRxF4V05Twk3EOq6IC7DNu4L87Qge41TJ4yPyKrAWkQxjge-9JHA9O_jatR7IDitSu8vrsL8IorqCnNGp_E-teJK7ItOlghqBG6_t3uMYjWIVzGO2x9sJSPHVAXh74d4iA6c-FgIt7OAZ0xY-wOBQVlZ5acrQPqLdQowOOKHY3cmPXxG52RHkTivVdN6aEA5JlNnOzUuGC4qNZGiNhPfeFcZMke9phSB35gkTW6yVFD8_Ex_6U2NtjhsV184DapqFMxEHgLk_A1mv2odK40DsCKIR85z7FdDs6TScQ_K2Mie-oCZiBO0LSfgwMch9U88eiOcKhOMNgCyvjBu5rTd2ABQRq4vlNGCJsWv8V8bb8ErQc8kkGZJoCysJBWqIL5LS3JWLZRrathaLSslA00mlOIxvP8AUUlLrhONkVwMPrf7rxlrIIjgj5xXSMOHblsxySArYKBrZ5fIwEffYImDIdZGlQnNKqelzYVXCc3LeM9_8GN7mmcQa7XWhU5yrdHAkbnfR-97GVgeBCJRtTeXWAVUT3KCD8oA_j7l2P5EqCuLeyb2eNanZro-h22WUg7axsp3UhWf_2SLd6PyJR8zD0QzLNTCs59vJyerf6t1t5uY00MQq8GQYIZ_aL9adNbzpuBoGDhIceiQrCaFfPPV6lkGRX-Ur7LpY5RQChFF7I57iAcGSbqFJakvXNnGJ8yWz_nSL8c4DMKB2kbkR6rTts9s6_eSVhajJBbqXWfs5LAkBfpes50Tf3TlJeS9DF7poFu40-ITIYZ1CMUgzOZR2lwVp-fYNAtcIuxlaqKOYB_j1yjR54xDvUIVOmSZc5INLwaROi66rCIc33tOgA67JObTtAOnNl212q9gp8AratUu5qPCnc38niOKa8uJSieke0W8iaSnIW18emYABTjySEh2IEVJ0uBw_3HUPiAoYliMQrxGeTecBYbFhPFiS-UEn4xSIQ29_0lZlGk4HkEbnVqbdOLjDuW8uAS1cY6f9K3LDSDAHIzFcy1v0g.ZLBS2GKaf4xhjEWEr5V7HiOUcL4Mhvu6bDfoST9-1uc\",\"scope\":\"openid profile email offline_access ui.read ui.admin ui.preferences.read ui.preferences.write authority:tenants.read authority:tenants.write authority:users.read authority:users.write authority:roles.read authority:roles.write authority:clients.read authority:clients.write authority:tokens.read authority:tokens.revoke authority:branding.read authority:branding.write authority.audit.read graph:read sbom:read scanner:read policy:read policy:simulate policy:author policy:review policy:approve policy:run policy:activate policy:audit policy:edit policy:operate policy:publish airgap:seal airgap:status:read orch:read orch:operate orch:quota analytics.read advisory:read advisory-ai:view advisory-ai:operate vex:read vexhub:read exceptions:read exceptions:approve aoc:verify findings:read release:read release:write release:publish scheduler:read scheduler:operate notify.viewer notify.operator notify.admin notify.escalate evidence:read export.viewer export.operator export.admin vuln:view vuln:investigate vuln:operate vuln:audit platform.context.read platform.context.write doctor:run doctor:admin ops.health integration:read integration:write integration:operate packs.read packs.write packs.run packs.approve registry.admin timeline:read timeline:write trust:read trust:write trust:admin signer:read signer:sign signer:rotate signer:admin\",\"expiresAtEpochMs\":1773619911029},\"identity\":{\"subject\":\"b08639745d6549348d843aa311c98958\",\"name\":\"admin\",\"roles\":[],\"idToken\":\"eyJhbGciOiJSUzI1NiIsImtpZCI6IlRBSU1GTlE3LUFOR1JMU0JOQVlfQ19OSEdPVERVWUo5UlNVVTRSRUQiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2F1dGhvcml0eS5zdGVsbGEtb3BzLmxvY2FsLyIsImV4cCI6MTc3MzYyMTQxMiwiaWF0IjoxNzczNjE4MTEyLCJhdWQiOiJzdGVsbGEtb3BzLXVpIiwic3ViIjoiYjA4NjM5NzQ1ZDY1NDkzNDhkODQzYWEzMTFjOTg5NTgiLCJuYW1lIjoiYWRtaW4iLCJhenAiOiJzdGVsbGEtb3BzLXVpIiwibm9uY2UiOiIzZGViMGQ3OC01ODY4LTQyY2UtYWYyMC1mOWFiMmNmOTYzN2EiLCJhdF9oYXNoIjoiVk5kRHdkemZxUE9saXN6eUEzblQ1dyJ9.O_wIjzvjmLJSSatV6yEU519t1XUTO2H0sUlFVNlAKguTp5pSldurF2VDaJYBnpf3ihjsD7PVwspheUtiGKp43qqTeCb_yChrMnEFuzdtdTCCc_MYtqv17hZHhk-Aw8zjh5OkBjb9usp_1zVGxfiiHSPoIafu5FYwQE1p7lgfrTBwR2XlBpcE-y9NMjmjz336gdqak-POVgAf4BeH_gcF0mbf_hl6IXK3I35NDBl2tcwnIWhXEjE1GE4wXdT8iS-G62aofDef4IHucAFViJGxBNSAxJUeF42pQtqe3ieAmAEmn3OjP68vK2wNteqzHKSn5TSU7aXmGhWCGS8aIzDqtQ\"},\"dpopKeyThumbprint\":\"2EpZhE76K26HrJMdayrLyZPjSrS9BpUMgPNXTWPfYMc\",\"issuedAtEpochMs\":1773618112030,\"tenantId\":\"demo-prod\",\"scopes\":[\"advisory-ai:operate\",\"advisory-ai:view\",\"advisory:read\",\"airgap:seal\",\"airgap:status:read\",\"analytics.read\",\"aoc:verify\",\"authority.audit.read\",\"authority:branding.read\",\"authority:branding.write\",\"authority:clients.read\",\"authority:clients.write\",\"authority:roles.read\",\"authority:roles.write\",\"authority:tenants.read\",\"authority:tenants.write\",\"authority:tokens.read\",\"authority:tokens.revoke\",\"authority:users.read\",\"authority:users.write\",\"doctor:admin\",\"doctor:run\",\"email\",\"evidence:read\",\"exceptions:approve\",\"exceptions:read\",\"export.admin\",\"export.operator\",\"export.viewer\",\"findings:read\",\"graph:read\",\"integration:operate\",\"integration:read\",\"integration:write\",\"notify.admin\",\"notify.escalate\",\"notify.operator\",\"notify.viewer\",\"offline_access\",\"openid\",\"ops.health\",\"orch:operate\",\"orch:quota\",\"orch:read\",\"packs.approve\",\"packs.read\",\"packs.run\",\"packs.write\",\"platform.context.read\",\"platform.context.write\",\"policy:activate\",\"policy:approve\",\"policy:audit\",\"policy:author\",\"policy:edit\",\"policy:operate\",\"policy:publish\",\"policy:read\",\"policy:review\",\"policy:run\",\"policy:simulate\",\"profile\",\"registry.admin\",\"release:publish\",\"release:read\",\"release:write\",\"sbom:read\",\"scanner:read\",\"scheduler:operate\",\"scheduler:read\",\"signer:admin\",\"signer:read\",\"signer:rotate\",\"signer:sign\",\"timeline:read\",\"timeline:write\",\"trust:admin\",\"trust:read\",\"trust:write\",\"ui.admin\",\"ui.preferences.read\",\"ui.preferences.write\",\"ui.read\",\"vex:read\",\"vexhub:read\",\"vuln:audit\",\"vuln:investigate\",\"vuln:operate\",\"vuln:view\"],\"audiences\":[],\"authenticationTimeEpochMs\":1773618112000,\"freshAuthActive\":false,\"freshAuthExpiresAtEpochMs\":null}" + ], + [ + "stellaops.auth.session.info", + "{\"subject\":\"b08639745d6549348d843aa311c98958\",\"expiresAtEpochMs\":1773619911029,\"issuedAtEpochMs\":1773618112030,\"dpopKeyThumbprint\":\"2EpZhE76K26HrJMdayrLyZPjSrS9BpUMgPNXTWPfYMc\",\"tenantId\":\"demo-prod\"}" + ], + [ + "stella-search-session-id", + "9fc82073-761d-41aa-8c11-2a3ec1d4d630" + ] + ] + }, + "events": { + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [] + }, + "statePath": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\live-frontdoor-auth-state.json" +} diff --git a/src/Web/StellaOps.Web/output/playwright/live-frontdoor-auth-state.json b/src/Web/StellaOps.Web/output/playwright/live-frontdoor-auth-state.json new file mode 100644 index 000000000..a1541bb7e --- /dev/null +++ b/src/Web/StellaOps.Web/output/playwright/live-frontdoor-auth-state.json @@ -0,0 +1,18 @@ +{ + "cookies": [], + "origins": [ + { + "origin": "https://stella-ops.local", + "localStorage": [ + { + "name": "stellaops.sidebar.preferences", + "value": "{\"sidebarCollapsed\":false,\"collapsedGroups\":[],\"collapsedSections\":[]}" + }, + { + "name": "stellaops.theme", + "value": "system" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/Web/StellaOps.Web/output/playwright/live-frontdoor-canonical-route-sweep.json b/src/Web/StellaOps.Web/output/playwright/live-frontdoor-canonical-route-sweep.json new file mode 100644 index 000000000..3c8311856 --- /dev/null +++ b/src/Web/StellaOps.Web/output/playwright/live-frontdoor-canonical-route-sweep.json @@ -0,0 +1,7672 @@ +{ + "checkedAtUtc": "2026-03-15T23:48:42.908Z", + "baseUrl": "https://stella-ops.local", + "totalRoutes": 111, + "passedRoutes": 110, + "failedRoutes": [ + "/releases/promotion-queue" + ], + "routes": [ + { + "routePath": "/mission-control/board", + "targetUrl": "https://stella-ops.local/mission-control/board?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/mission-control/board?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Dashboard - Stella Ops QA 1773540847164", + "headings": [ + "Dashboard", + "Regional Pipeline", + "Environments at Risk", + "SBOM Findings Snapshot", + "Reachability", + "Nightly Ops Signals" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "View all", + "href": "/setup/topology/environments?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + }, + { + "tagName": "a", + "text": "Review", + "href": "/releases/approvals?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + }, + { + "tagName": "a", + "text": "Risk detail", + "href": "/security?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + }, + { + "tagName": "a", + "text": "Ops detail", + "href": "/ops/operations/data-integrity?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + }, + { + "tagName": "a", + "text": "All environments", + "href": "/setup/topology/environments?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + }, + { + "tagName": "a", + "text": "Detail", + "href": "/setup/topology/environments/dev/posture?tenant=demo-prod®ions=us-east&environments=dev&timeWindow=7d®ion=us-east&environment=dev" + }, + { + "tagName": "a", + "text": "Findings", + "href": "/security/findings?tenant=demo-prod®ions=us-east&environments=dev&timeWindow=7d®ion=us-east&environment=dev" + }, + { + "tagName": "a", + "text": "Detail", + "href": "/setup/topology/environments/stage/posture?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d®ion=us-east&environment=stage" + }, + { + "tagName": "a", + "text": "Findings", + "href": "/security/findings?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d®ion=us-east&environment=stage" + }, + { + "tagName": "a", + "text": "Detail", + "href": "/setup/topology/environments/prod-us-east/posture?tenant=demo-prod®ions=us-east&environments=prod-us-east&timeWindow=7d®ion=us-east&environment=prod-us-east" + }, + { + "tagName": "a", + "text": "Findings", + "href": "/security/findings?tenant=demo-prod®ions=us-east&environments=prod-us-east&timeWindow=7d®ion=us-east&environment=prod-us-east" + }, + { + "tagName": "a", + "text": "Detail", + "href": "/setup/topology/environments/us-prod/posture?tenant=demo-prod®ions=us-east&environments=us-prod&timeWindow=7d®ion=us-east&environment=us-prod" + }, + { + "tagName": "a", + "text": "Findings", + "href": "/security/findings?tenant=demo-prod®ions=us-east&environments=us-prod&timeWindow=7d®ion=us-east&environment=us-prod" + }, + { + "tagName": "a", + "text": "Detail", + "href": "/setup/topology/environments/us-uat/posture?tenant=demo-prod®ions=us-east&environments=us-uat&timeWindow=7d®ion=us-east&environment=us-uat" + }, + { + "tagName": "a", + "text": "Findings", + "href": "/security/findings?tenant=demo-prod®ions=us-east&environments=us-uat&timeWindow=7d®ion=us-east&environment=us-uat" + }, + { + "tagName": "a", + "text": "Open environments", + "href": "/setup/topology/environments?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + }, + { + "tagName": "a", + "text": "Open", + "href": "/setup/topology/environments/dev/posture?tenant=demo-prod®ions=us-east&environments=dev&timeWindow=7d®ion=us-east&environment=dev" + }, + { + "tagName": "a", + "text": "Open", + "href": "/setup/topology/environments/stage/posture?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d®ion=us-east&environment=stage" + }, + { + "tagName": "a", + "text": "Open", + "href": "/setup/topology/environments/prod-us-east/posture?tenant=demo-prod®ions=us-east&environments=prod-us-east&timeWindow=7d®ion=us-east&environment=prod-us-east" + }, + { + "tagName": "a", + "text": "Open", + "href": "/setup/topology/environments/us-prod/posture?tenant=demo-prod®ions=us-east&environments=us-prod&timeWindow=7d®ion=us-east&environment=us-prod" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/mission-control/alerts", + "targetUrl": "https://stella-ops.local/mission-control/alerts?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/mission-control/alerts?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Mission Alerts - Stella Ops QA 1773540847164", + "headings": [ + "Mission Alerts" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "3 approvals blocked by policy gate evidence freshness", + "href": "/releases/approvals?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + }, + { + "tagName": "a", + "text": "Identity watchlist alert requires signer review", + "href": "/setup/trust-signing/watchlist/alerts?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&alertId=alert-001&returnTo=%2Fmission-control%2Falerts&scope=tenant&tab=alerts" + }, + { + "tagName": "a", + "text": "2 waivers expiring within 24h", + "href": "/security/disposition?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + }, + { + "tagName": "a", + "text": "Feed freshness degraded for advisory ingest", + "href": "/ops/operations/data-integrity?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/mission-control/activity", + "targetUrl": "https://stella-ops.local/mission-control/activity?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/mission-control/activity?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Mission Activity - Stella Ops QA 1773540847164", + "headings": [ + "Mission Activity", + "Release Runs", + "Evidence", + "Audit" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Open Runs", + "href": "/releases/runs?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + }, + { + "tagName": "a", + "text": "Open Capsules", + "href": "/evidence/capsules?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + }, + { + "tagName": "a", + "text": "Open Audit Log", + "href": "/evidence/audit-log?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/releases", + "targetUrl": "https://stella-ops.local/releases?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/releases/deployments?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Deployment History - Stella Ops QA 1773540847164", + "headings": [ + "Deployments" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "DEP-2026-050", + "href": "/releases/deployments/DEP-2026-050" + }, + { + "tagName": "a", + "text": "View", + "href": "/releases/deployments/DEP-2026-050" + }, + { + "tagName": "a", + "text": "DEP-2026-049", + "href": "/releases/deployments/DEP-2026-049" + }, + { + "tagName": "a", + "text": "View", + "href": "/releases/deployments/DEP-2026-049" + }, + { + "tagName": "a", + "text": "DEP-2026-048", + "href": "/releases/deployments/DEP-2026-048" + }, + { + "tagName": "a", + "text": "View", + "href": "/releases/deployments/DEP-2026-048" + }, + { + "tagName": "a", + "text": "DEP-2026-047", + "href": "/releases/deployments/DEP-2026-047" + }, + { + "tagName": "a", + "text": "View", + "href": "/releases/deployments/DEP-2026-047" + }, + { + "tagName": "a", + "text": "DEP-2026-046", + "href": "/releases/deployments/DEP-2026-046" + }, + { + "tagName": "a", + "text": "View", + "href": "/releases/deployments/DEP-2026-046" + }, + { + "tagName": "a", + "text": "DEP-2026-050", + "href": "/releases/deployments/DEP-2026-050" + }, + { + "tagName": "a", + "text": "View Deployment", + "href": "/releases/deployments/DEP-2026-050" + }, + { + "tagName": "a", + "text": "DEP-2026-049", + "href": "/releases/deployments/DEP-2026-049" + }, + { + "tagName": "a", + "text": "View Deployment", + "href": "/releases/deployments/DEP-2026-049" + }, + { + "tagName": "a", + "text": "DEP-2026-048", + "href": "/releases/deployments/DEP-2026-048" + }, + { + "tagName": "a", + "text": "View Deployment", + "href": "/releases/deployments/DEP-2026-048" + }, + { + "tagName": "a", + "text": "DEP-2026-047", + "href": "/releases/deployments/DEP-2026-047" + }, + { + "tagName": "a", + "text": "View Deployment", + "href": "/releases/deployments/DEP-2026-047" + }, + { + "tagName": "a", + "text": "DEP-2026-046", + "href": "/releases/deployments/DEP-2026-046" + }, + { + "tagName": "a", + "text": "View Deployment", + "href": "/releases/deployments/DEP-2026-046" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/releases/overview", + "targetUrl": "https://stella-ops.local/releases/overview?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/releases/overview?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Release Ops Overview - Stella Ops QA 1773540847164", + "headings": [ + "Release Ops Overview" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Release Versions", + "href": "/releases/versions?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + }, + { + "tagName": "a", + "text": "Release Runs", + "href": "/releases/runs?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + }, + { + "tagName": "a", + "text": "Approvals Queue", + "href": "/releases/approvals?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + }, + { + "tagName": "a", + "text": "Hotfixes", + "href": "/releases/hotfixes?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + }, + { + "tagName": "a", + "text": "Promotions", + "href": "/releases/promotions?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + }, + { + "tagName": "a", + "text": "Deployment History", + "href": "/releases/deployments?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/releases/versions", + "targetUrl": "https://stella-ops.local/releases/versions?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/releases/versions?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Release Versions - Stella Ops QA 1773540847164", + "headings": [ + "Release Versions" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "button", + "text": "Create Release Version", + "href": "" + }, + { + "tagName": "button", + "text": "Create Hotfix Run", + "href": "" + }, + { + "tagName": "button", + "text": "Export Evidence", + "href": "" + }, + { + "tagName": "button", + "text": "Compare Versions", + "href": "" + }, + { + "tagName": "button", + "text": "Clear", + "href": "" + }, + { + "tagName": "button", + "text": "Search", + "href": "" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/releases/versions/new", + "targetUrl": "https://stella-ops.local/releases/versions/new?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/releases/versions/new?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Create Release Version - Stella Ops QA 1773540847164", + "headings": [ + "Create Release Version", + "Basic Release Version Identity" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "button", + "text": "Back to Release Versions", + "href": "" + }, + { + "tagName": "button", + "text": "Back", + "href": "" + }, + { + "tagName": "button", + "text": "Continue", + "href": "" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/releases/runs", + "targetUrl": "https://stella-ops.local/releases/runs?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/releases/runs?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Release Runs - Stella Ops QA 1773540847164", + "headings": [ + "Release Runs" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Timeline", + "href": "/releases/runs?view=timeline" + }, + { + "tagName": "a", + "text": "Table", + "href": "/releases/runs?view=table" + }, + { + "tagName": "a", + "text": "Correlations", + "href": "/releases/runs?view=correlations" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/releases/approvals", + "targetUrl": "https://stella-ops.local/releases/approvals?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/releases/approvals?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Approvals - Stella Ops QA 1773540847164", + "headings": [ + "Release Run Approvals Queue" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Pending", + "href": "/releases/approvals?tab=pending" + }, + { + "tagName": "a", + "text": "Approved", + "href": "/releases/approvals?tab=approved" + }, + { + "tagName": "a", + "text": "Rejected", + "href": "/releases/approvals?tab=rejected" + }, + { + "tagName": "a", + "text": "Expiring", + "href": "/releases/approvals?tab=expiring" + }, + { + "tagName": "a", + "text": "My Team", + "href": "/releases/approvals?tab=my-team" + }, + { + "tagName": "a", + "text": "API Gateway v2.1 API Gateway v2.1", + "href": "/releases/runs/a4451892-b6b2-41f6-8539-7b9c6d33d140/timeline" + }, + { + "tagName": "a", + "text": "Open", + "href": "/releases/runs/a4451892-b6b2-41f6-8539-7b9c6d33d140/approvals?approvalId=apr-6995d783-e12d-489c-bdd0-3f2e56e772b7" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/releases/promotion-queue", + "targetUrl": "https://stella-ops.local/releases/promotion-queue?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/releases/promotions?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Promotions - Stella Ops QA 1773540847164", + "headings": [ + "Promotions" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Create Promotion", + "href": "/releases/promotions/create" + }, + { + "tagName": "a", + "text": "View ->", + "href": "/releases/promotions/apr-6995d783-e12d-489c-bdd0-3f2e56e772b7" + } + ], + "consoleErrors": [ + "Failed to load resource: the server responded with a status of 404 ()" + ], + "requestFailures": [], + "responseErrors": [ + { + "status": 404, + "method": "GET", + "url": "https://stella-ops.local/api/v1/approvals/apr-6995d783-e12d-489c-bdd0-3f2e56e772b7" + } + ], + "finalPathMatches": true, + "expectationFailures": [], + "passed": false, + "retryUsed": true, + "retryPassed": false, + "initialFailure": { + "consoleErrors": [ + "Failed to load resource: the server responded with a status of 404 ()" + ], + "requestFailures": [], + "responseErrors": [ + { + "status": 404, + "method": "GET", + "url": "https://stella-ops.local/api/v1/approvals/apr-6995d783-e12d-489c-bdd0-3f2e56e772b7" + } + ], + "problemTexts": [], + "expectationFailures": [] + } + }, + { + "routePath": "/releases/hotfixes", + "targetUrl": "https://stella-ops.local/releases/hotfixes?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/releases/hotfixes?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Hotfixes - Stella Ops QA 1773540847164", + "headings": [ + "Hotfixes" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Review", + "href": "/releases/hotfixes/platform-bundle-1-3-1-hotfix1?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/releases/hotfixes/new", + "targetUrl": "https://stella-ops.local/releases/hotfixes/new?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/releases/versions/new?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&type=hotfix&hotfixLane=true", + "title": "Create Release Version - Stella Ops QA 1773540847164", + "headings": [ + "Create Release Version", + "Basic Release Version Identity" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "button", + "text": "Back to Release Versions", + "href": "" + }, + { + "tagName": "button", + "text": "Back", + "href": "" + }, + { + "tagName": "button", + "text": "Continue", + "href": "" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/releases/environments", + "targetUrl": "https://stella-ops.local/releases/environments?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/releases/environments?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Environments Inventory - Stella Ops QA 1773540847164", + "headings": [ + "Regions & Environments", + "Regions", + "Environments · US East", + "Environment Signals" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "button", + "text": "US Eastenv 5 · targets 1", + "href": "" + }, + { + "tagName": "a", + "text": "Open", + "href": "/setup/topology/environments/stage/posture?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&environment=stage" + }, + { + "tagName": "a", + "text": "Open Environment", + "href": "/setup/topology/environments/stage/posture?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&environment=stage" + }, + { + "tagName": "a", + "text": "Open Targets", + "href": "/setup/topology/targets?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&environment=stage" + }, + { + "tagName": "a", + "text": "Open Agents", + "href": "/setup/topology/agents?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&environment=stage" + }, + { + "tagName": "a", + "text": "Open Runs", + "href": "/releases/runs?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&environment=stage" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/releases/deployments", + "targetUrl": "https://stella-ops.local/releases/deployments?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/releases/deployments?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Deployment History - Stella Ops QA 1773540847164", + "headings": [ + "Deployments" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "DEP-2026-050", + "href": "/releases/deployments/DEP-2026-050" + }, + { + "tagName": "a", + "text": "View", + "href": "/releases/deployments/DEP-2026-050" + }, + { + "tagName": "a", + "text": "DEP-2026-049", + "href": "/releases/deployments/DEP-2026-049" + }, + { + "tagName": "a", + "text": "View", + "href": "/releases/deployments/DEP-2026-049" + }, + { + "tagName": "a", + "text": "DEP-2026-048", + "href": "/releases/deployments/DEP-2026-048" + }, + { + "tagName": "a", + "text": "View", + "href": "/releases/deployments/DEP-2026-048" + }, + { + "tagName": "a", + "text": "DEP-2026-047", + "href": "/releases/deployments/DEP-2026-047" + }, + { + "tagName": "a", + "text": "View", + "href": "/releases/deployments/DEP-2026-047" + }, + { + "tagName": "a", + "text": "DEP-2026-046", + "href": "/releases/deployments/DEP-2026-046" + }, + { + "tagName": "a", + "text": "View", + "href": "/releases/deployments/DEP-2026-046" + }, + { + "tagName": "a", + "text": "DEP-2026-050", + "href": "/releases/deployments/DEP-2026-050" + }, + { + "tagName": "a", + "text": "View Deployment", + "href": "/releases/deployments/DEP-2026-050" + }, + { + "tagName": "a", + "text": "DEP-2026-049", + "href": "/releases/deployments/DEP-2026-049" + }, + { + "tagName": "a", + "text": "View Deployment", + "href": "/releases/deployments/DEP-2026-049" + }, + { + "tagName": "a", + "text": "DEP-2026-048", + "href": "/releases/deployments/DEP-2026-048" + }, + { + "tagName": "a", + "text": "View Deployment", + "href": "/releases/deployments/DEP-2026-048" + }, + { + "tagName": "a", + "text": "DEP-2026-047", + "href": "/releases/deployments/DEP-2026-047" + }, + { + "tagName": "a", + "text": "View Deployment", + "href": "/releases/deployments/DEP-2026-047" + }, + { + "tagName": "a", + "text": "DEP-2026-046", + "href": "/releases/deployments/DEP-2026-046" + }, + { + "tagName": "a", + "text": "View Deployment", + "href": "/releases/deployments/DEP-2026-046" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/releases/investigation/timeline", + "targetUrl": "https://stella-ops.local/releases/investigation/timeline?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/releases/investigation/timeline?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Investigation Timeline - Stella Ops QA 1773540847164", + "headings": [ + "Timeline" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "button", + "text": "Export", + "href": "" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/releases/investigation/deploy-diff", + "targetUrl": "https://stella-ops.local/releases/investigation/deploy-diff?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/releases/investigation/deploy-diff?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Deployment Diff - Stella Ops QA 1773540847164", + "headings": [ + "No Comparison Selected" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Deployments", + "href": "/releases/deployments?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + }, + { + "tagName": "a", + "text": "Open Deployments", + "href": "/releases/deployments?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + }, + { + "tagName": "a", + "text": "Open Releases Overview", + "href": "/releases/overview?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/releases/investigation/change-trace", + "targetUrl": "https://stella-ops.local/releases/investigation/change-trace?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/releases/investigation/change-trace?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Change Trace - Stella Ops QA 1773540847164", + "headings": [ + "Change Trace", + "No Comparison Selected" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "button", + "text": "Load File", + "href": "" + }, + { + "tagName": "button", + "text": "Export", + "href": "" + }, + { + "tagName": "a", + "text": "Open Deployments", + "href": "/releases/deployments?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + }, + { + "tagName": "button", + "text": "Load Change Trace File", + "href": "" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/security", + "targetUrl": "https://stella-ops.local/security?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/security?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Security Posture - Stella Ops QA 1773540847164", + "headings": [ + "Security Posture", + "Risk Posture", + "Blocking Items", + "VEX Coverage", + "SBOM Health", + "Reachability Coverage", + "Unknown Reachability" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Drilldown", + "href": "/ops/operations/data-integrity?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + }, + { + "tagName": "a", + "text": "Open triage", + "href": "/security/triage?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + }, + { + "tagName": "a", + "text": "Disposition", + "href": "/security/disposition?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&tab=vex-library" + }, + { + "tagName": "a", + "text": "Configure sources", + "href": "/ops/integrations/advisory-vex-sources?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + }, + { + "tagName": "a", + "text": "Coverage & Unknowns", + "href": "/security/sbom/coverage" + }, + { + "tagName": "a", + "text": "Inspect unknown/unreachable findings", + "href": "/security/triage?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&reachability=unreachable" + }, + { + "tagName": "a", + "text": "Open reachability coverage board", + "href": "/security/reachability?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/security/posture", + "targetUrl": "https://stella-ops.local/security/posture?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/security?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Security Posture - Stella Ops QA 1773540847164", + "headings": [ + "Security Posture", + "Risk Posture", + "Blocking Items", + "VEX Coverage", + "SBOM Health", + "Reachability Coverage", + "Unknown Reachability" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Drilldown", + "href": "/ops/operations/data-integrity?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + }, + { + "tagName": "a", + "text": "Open triage", + "href": "/security/triage?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + }, + { + "tagName": "a", + "text": "Disposition", + "href": "/security/disposition?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&tab=vex-library" + }, + { + "tagName": "a", + "text": "Configure sources", + "href": "/ops/integrations/advisory-vex-sources?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + }, + { + "tagName": "a", + "text": "Coverage & Unknowns", + "href": "/security/sbom/coverage" + }, + { + "tagName": "a", + "text": "Inspect unknown/unreachable findings", + "href": "/security/triage?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&reachability=unreachable" + }, + { + "tagName": "a", + "text": "Open reachability coverage board", + "href": "/security/reachability?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/security/triage", + "targetUrl": "https://stella-ops.local/security/triage?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/security/triage?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&pivot=cve", + "title": "Security Triage - Stella Ops QA 1773540847164", + "headings": [ + "Security / Triage", + "Filters", + "Evidence Rail" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "button", + "text": "Findings", + "href": "" + }, + { + "tagName": "button", + "text": "Components", + "href": "" + }, + { + "tagName": "button", + "text": "Artifacts", + "href": "" + }, + { + "tagName": "button", + "text": "Environments", + "href": "" + }, + { + "tagName": "button", + "text": "Releases", + "href": "" + }, + { + "tagName": "button", + "text": "Prod blockers", + "href": "" + }, + { + "tagName": "button", + "text": "Unknown reachability", + "href": "" + }, + { + "tagName": "button", + "text": "Expiring waivers", + "href": "" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/security/advisories-vex", + "targetUrl": "https://stella-ops.local/security/advisories-vex?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/security/advisories-vex?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Advisories & VEX - Stella Ops QA 1773540847164", + "headings": [ + "Security / Advisories & VEX", + "Providers" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Configure advisory feeds", + "href": "/ops/integrations/advisory-vex-sources?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + }, + { + "tagName": "a", + "text": "Configure VEX sources", + "href": "/ops/integrations/advisory-vex-sources?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + }, + { + "tagName": "a", + "text": "Providers", + "href": "/security/advisories-vex?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&tab=providers" + }, + { + "tagName": "a", + "text": "VEX Library", + "href": "/security/advisories-vex?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&tab=vex-library" + }, + { + "tagName": "a", + "text": "Conflicts", + "href": "/security/advisories-vex?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&tab=conflicts" + }, + { + "tagName": "a", + "text": "Issuer Trust", + "href": "/security/advisories-vex?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&tab=issuer-trust" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/security/disposition", + "targetUrl": "https://stella-ops.local/security/disposition?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/security/disposition?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Disposition Center - Stella Ops QA 1773540847164", + "headings": [ + "Security / Advisories & VEX", + "Providers" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Configure advisory feeds", + "href": "/ops/integrations/advisory-vex-sources?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + }, + { + "tagName": "a", + "text": "Configure VEX sources", + "href": "/ops/integrations/advisory-vex-sources?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + }, + { + "tagName": "a", + "text": "Providers", + "href": "/security/disposition?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&tab=providers" + }, + { + "tagName": "a", + "text": "VEX Library", + "href": "/security/disposition?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&tab=vex-library" + }, + { + "tagName": "a", + "text": "Conflicts", + "href": "/security/disposition?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&tab=conflicts" + }, + { + "tagName": "a", + "text": "Issuer Trust", + "href": "/security/disposition?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&tab=issuer-trust" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/security/supply-chain-data", + "targetUrl": "https://stella-ops.local/security/supply-chain-data?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/security/supply-chain-data?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Supply-Chain Data - Stella Ops QA 1773540847164", + "headings": [ + "Security / Supply-Chain Data", + "SBOM Viewer" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "SBOM Viewer", + "href": "/security/supply-chain-data/viewer" + }, + { + "tagName": "a", + "text": "SBOM Graph", + "href": "/security/supply-chain-data/graph" + }, + { + "tagName": "a", + "text": "SBOM Lake", + "href": "/security/supply-chain-data/lake" + }, + { + "tagName": "a", + "text": "Reachability", + "href": "/security/supply-chain-data/reachability" + }, + { + "tagName": "a", + "text": "Coverage/Unknowns", + "href": "/security/supply-chain-data/coverage" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/security/supply-chain-data/graph", + "targetUrl": "https://stella-ops.local/security/supply-chain-data/graph?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/security/supply-chain-data/graph?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Supply-Chain Data - Stella Ops QA 1773540847164", + "headings": [ + "Security / Supply-Chain Data", + "SBOM Graph" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "SBOM Viewer", + "href": "/security/supply-chain-data/viewer" + }, + { + "tagName": "a", + "text": "SBOM Graph", + "href": "/security/supply-chain-data/graph" + }, + { + "tagName": "a", + "text": "SBOM Lake", + "href": "/security/supply-chain-data/lake" + }, + { + "tagName": "a", + "text": "Reachability", + "href": "/security/supply-chain-data/reachability" + }, + { + "tagName": "a", + "text": "Coverage/Unknowns", + "href": "/security/supply-chain-data/coverage" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/security/sbom-lake", + "targetUrl": "https://stella-ops.local/security/sbom-lake?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/security/sbom-lake?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "SBOM Lake - Stella Ops QA 1773540847164", + "headings": [ + "SBOM Lake", + "Attestation Coverage Metrics", + "Supplier concentration", + "License distribution", + "Vulnerability exposure", + "Attestation coverage", + "Vulnerability trend", + "Component trend", + "Fixable backlog", + "Top backlog components" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "button", + "text": "Refresh", + "href": "" + }, + { + "tagName": "button", + "text": "Export backlog CSV", + "href": "" + }, + { + "tagName": "button", + "text": "Clear", + "href": "" + }, + { + "tagName": "button", + "text": "Refresh", + "href": "" + }, + { + "tagName": "button", + "text": "Export CSV", + "href": "" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/security/reachability", + "targetUrl": "https://stella-ops.local/security/reachability?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/security/reachability?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Reachability - Stella Ops QA 1773540847164", + "headings": [ + "Reachability", + "Proof of Exposure" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "button", + "text": "Refresh witnesses", + "href": "" + }, + { + "tagName": "button", + "text": "Coverage", + "href": "" + }, + { + "tagName": "button", + "text": "Witnesses", + "href": "" + }, + { + "tagName": "button", + "text": "PoE / Exposure", + "href": "" + }, + { + "tagName": "button", + "text": "Sensor Gaps", + "href": "" + }, + { + "tagName": "button", + "text": "All", + "href": "" + }, + { + "tagName": "button", + "text": "Healthy", + "href": "" + }, + { + "tagName": "button", + "text": "Stale", + "href": "" + }, + { + "tagName": "button", + "text": "Missing", + "href": "" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/security/reports", + "targetUrl": "https://stella-ops.local/security/reports?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/security/reports?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Security Reports - Stella Ops QA 1773540847164", + "headings": [ + "Security Reports", + "Security Posture", + "Risk Posture", + "Blocking Items", + "VEX Coverage", + "SBOM Health", + "Reachability Coverage", + "Unknown Reachability" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "button", + "text": "Risk Report", + "href": "" + }, + { + "tagName": "button", + "text": "VEX Ledger", + "href": "" + }, + { + "tagName": "button", + "text": "Evidence Export", + "href": "" + }, + { + "tagName": "a", + "text": "Drilldown", + "href": "/ops/operations/data-integrity?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + }, + { + "tagName": "a", + "text": "Open triage", + "href": "/security/triage?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + }, + { + "tagName": "a", + "text": "Disposition", + "href": "/security/disposition?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&tab=vex-library" + }, + { + "tagName": "a", + "text": "Configure sources", + "href": "/ops/integrations/advisory-vex-sources?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + }, + { + "tagName": "a", + "text": "Coverage & Unknowns", + "href": "/security/sbom/coverage" + }, + { + "tagName": "a", + "text": "Inspect unknown/unreachable findings", + "href": "/security/triage?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&reachability=unreachable" + }, + { + "tagName": "a", + "text": "Open reachability coverage board", + "href": "/security/reachability?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true, + "retryUsed": true, + "retryPassed": true, + "initialFailure": { + "consoleErrors": [ + "Failed to load resource: the server responded with a status of 503 ()" + ], + "requestFailures": [], + "responseErrors": [ + { + "status": 503, + "method": "GET", + "url": "https://stella-ops.local/api/v2/security/disposition?limit=200&offset=0" + } + ], + "problemTexts": [], + "expectationFailures": [] + } + }, + { + "routePath": "/evidence", + "targetUrl": "https://stella-ops.local/evidence?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/evidence?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Evidence Overview - Stella Ops QA 1773540847164", + "headings": [ + "Evidence & Audit", + "Find Evidence", + "Quick Views", + "Shortcuts", + "Related Domains" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "button", + "text": "Search", + "href": "" + }, + { + "tagName": "a", + "text": "Audit Log", + "href": "/evidence/audit-log" + }, + { + "tagName": "a", + "text": "Export Center", + "href": "/evidence/exports" + }, + { + "tagName": "a", + "text": "Evidence Bundles", + "href": "/releases/bundles" + }, + { + "tagName": "a", + "text": "Replay & Verify", + "href": "/evidence/verify-replay" + }, + { + "tagName": "a", + "text": "Proof Chains", + "href": "/evidence/capsules" + }, + { + "tagName": "a", + "text": "Trust & Signing", + "href": "/setup/trust-signing" + }, + { + "tagName": "a", + "text": "▶Release ControlEvidence attached to releases and promotions", + "href": "/releases/runs" + }, + { + "tagName": "a", + "text": "■Evidence & Audit > Trust & SigningKey management and signing policy", + "href": "/setup/trust-signing" + }, + { + "tagName": "a", + "text": "◆Ops > Policy > GovernancePolicy packs driving evidence requirements", + "href": "/ops/policy/governance" + }, + { + "tagName": "a", + "text": "●Security & Risk > FindingsFindings linked to evidence records", + "href": "/security/findings" + }, + { + "tagName": "a", + "text": "Evidence > Trust & Signing", + "href": "/setup/trust-signing" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/evidence/overview", + "targetUrl": "https://stella-ops.local/evidence/overview?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/evidence/overview?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Evidence Overview - Stella Ops QA 1773540847164", + "headings": [ + "Evidence & Audit", + "Find Evidence", + "Quick Views", + "Shortcuts", + "Related Domains" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "button", + "text": "Search", + "href": "" + }, + { + "tagName": "a", + "text": "Audit Log", + "href": "/evidence/audit-log" + }, + { + "tagName": "a", + "text": "Export Center", + "href": "/evidence/exports" + }, + { + "tagName": "a", + "text": "Evidence Bundles", + "href": "/releases/bundles" + }, + { + "tagName": "a", + "text": "Replay & Verify", + "href": "/evidence/verify-replay" + }, + { + "tagName": "a", + "text": "Proof Chains", + "href": "/evidence/capsules" + }, + { + "tagName": "a", + "text": "Trust & Signing", + "href": "/setup/trust-signing" + }, + { + "tagName": "a", + "text": "▶Release ControlEvidence attached to releases and promotions", + "href": "/releases/runs" + }, + { + "tagName": "a", + "text": "■Evidence & Audit > Trust & SigningKey management and signing policy", + "href": "/setup/trust-signing" + }, + { + "tagName": "a", + "text": "◆Ops > Policy > GovernancePolicy packs driving evidence requirements", + "href": "/ops/policy/governance" + }, + { + "tagName": "a", + "text": "●Security & Risk > FindingsFindings linked to evidence records", + "href": "/security/findings" + }, + { + "tagName": "a", + "text": "Evidence > Trust & Signing", + "href": "/setup/trust-signing" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/evidence/capsules", + "targetUrl": "https://stella-ops.local/evidence/capsules?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/evidence/capsules?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Decision Capsules - Stella Ops QA 1773540847164", + "headings": [ + "Decision Capsules" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "button", + "text": "Search", + "href": "" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/evidence/threads", + "targetUrl": "https://stella-ops.local/evidence/threads?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/evidence/threads?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Evidence Threads - Stella Ops QA 1773540847164", + "headings": [ + "Evidence Threads" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "button", + "text": "Search", + "href": "" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/evidence/verify-replay", + "targetUrl": "https://stella-ops.local/evidence/verify-replay?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/evidence/verify-replay?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Replay & Verify - Stella Ops QA 1773540847164", + "headings": [ + "Replay & Verify", + "Request Replay", + "Replay Requests", + "Quick-Verify", + "Determinism Overview" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "button", + "text": "Request Replay", + "href": "" + }, + { + "tagName": "button", + "text": "×", + "href": "" + }, + { + "tagName": "button", + "text": "Start Verification", + "href": "" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/evidence/proofs", + "targetUrl": "https://stella-ops.local/evidence/proofs?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/evidence/proofs?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Proof Chains - Stella Ops QA 1773540847164", + "headings": [ + "Proof Chains" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "button", + "text": "Search", + "href": "" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/evidence/exports", + "targetUrl": "https://stella-ops.local/evidence/exports?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/evidence/exports?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Export Center - Stella Ops QA 1773540847164", + "headings": [ + "Export Center" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "button", + "text": "Export StellaBundle OCI", + "href": "" + }, + { + "tagName": "button", + "text": "Profiles", + "href": "" + }, + { + "tagName": "button", + "text": "Export Runs", + "href": "" + }, + { + "tagName": "button", + "text": "Create Profile", + "href": "" + }, + { + "tagName": "button", + "text": "Run Now", + "href": "" + }, + { + "tagName": "button", + "text": "Edit", + "href": "" + }, + { + "tagName": "button", + "text": "Delete", + "href": "" + }, + { + "tagName": "button", + "text": "Run Now", + "href": "" + }, + { + "tagName": "button", + "text": "Edit", + "href": "" + }, + { + "tagName": "button", + "text": "Delete", + "href": "" + }, + { + "tagName": "button", + "text": "Run Now", + "href": "" + }, + { + "tagName": "button", + "text": "Edit", + "href": "" + }, + { + "tagName": "button", + "text": "Delete", + "href": "" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/evidence/audit-log", + "targetUrl": "https://stella-ops.local/evidence/audit-log?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/evidence/audit-log?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Audit Log - Stella Ops QA 1773540847164", + "headings": [ + "Unified Audit Log", + "Quick Access", + "Recent Events" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "View All Events", + "href": "/evidence/audit-log/events" + }, + { + "tagName": "a", + "text": "Export", + "href": "/evidence/exports" + }, + { + "tagName": "a", + "text": "policyPolicy AuditPromotions, simulations, approvals", + "href": "/evidence/audit-log/policy" + }, + { + "tagName": "a", + "text": "tokenAuthority AuditToken lifecycle, revocations", + "href": "/evidence/audit-log/authority" + }, + { + "tagName": "a", + "text": "vexVEX AuditDecisions, consensus votes", + "href": "/evidence/audit-log/vex" + }, + { + "tagName": "a", + "text": "integrationIntegration AuditConfig changes, connections", + "href": "/evidence/audit-log/integrations" + }, + { + "tagName": "a", + "text": "timelineTimeline SearchSearch across all indexed events", + "href": "/evidence/audit-log/timeline" + }, + { + "tagName": "a", + "text": "correlateCorrelationsEvent clusters by causality", + "href": "/evidence/audit-log/correlations" + }, + { + "tagName": "a", + "text": "View all", + "href": "/evidence/audit-log/events" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops", + "targetUrl": "https://stella-ops.local/ops?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Ops - Stella Ops QA 1773540847164", + "headings": [ + "Ops", + "Operational Focus" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "OperationsScheduler, feeds, health, quotas, and runtime controls.", + "href": "/ops/operations" + }, + { + "tagName": "a", + "text": "IntegrationsConnector onboarding, source registration, and delivery channels.", + "href": "/ops/integrations" + }, + { + "tagName": "a", + "text": "PolicyGovernance, simulations, waivers, and gate catalog management.", + "href": "/ops/policy" + }, + { + "tagName": "a", + "text": "Platform SetupEnvironment bootstrap, topology alignment, and baseline controls.", + "href": "/ops/platform-setup" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/operations", + "targetUrl": "https://stella-ops.local/ops/operations?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/operations?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Operations - Stella Ops QA 1773540847164", + "headings": [ + "Operations", + "Blocking", + "Execution", + "Health", + "Supply And Airgap", + "Capacity And Setup Boundary", + "Pending Operator Actions", + "Setup Boundary" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Run Doctor", + "href": "/ops/operations/doctor" + }, + { + "tagName": "a", + "text": "Audit Log", + "href": "/evidence/audit-log" + }, + { + "tagName": "a", + "text": "Export Ops Report", + "href": "/evidence/exports" + }, + { + "tagName": "button", + "text": "Refresh", + "href": "" + }, + { + "tagName": "a", + "text": "Overview", + "href": "/ops/operations" + }, + { + "tagName": "a", + "text": "Data Integrity", + "href": "/ops/operations/data-integrity" + }, + { + "tagName": "a", + "text": "Jobs & Queues", + "href": "/ops/operations/jobs-queues" + }, + { + "tagName": "a", + "text": "Health & SLO", + "href": "/ops/operations/health-slo" + }, + { + "tagName": "a", + "text": "Feeds & Airgap", + "href": "/ops/operations/feeds-airgap" + }, + { + "tagName": "a", + "text": "Offline Kit", + "href": "/ops/operations/offline-kit" + }, + { + "tagName": "a", + "text": "Quotas & Limits", + "href": "/ops/operations/quotas" + }, + { + "tagName": "a", + "text": "AOC Compliance", + "href": "/ops/operations/aoc" + }, + { + "tagName": "a", + "text": "Diagnostics", + "href": "/ops/operations/doctor" + }, + { + "tagName": "a", + "text": "Signals", + "href": "/ops/operations/signals" + }, + { + "tagName": "a", + "text": "Pack Registry", + "href": "/ops/operations/packs" + }, + { + "tagName": "a", + "text": "Notifications", + "href": "/ops/operations/notifications" + }, + { + "tagName": "a", + "text": "3h 12m staleOpenFeed freshness blocking releasesNVD mirror is stale and approvals are pinned to the last-known-good snapshot.", + "href": "/ops/operations/data-integrity/feeds-freshness" + }, + { + "tagName": "a", + "text": "3 queued replaysOpenReplay backlog degrading confidenceDead-letter replay queue is elevated for reachability and evidence exports.", + "href": "/ops/operations/data-integrity/dlq" + }, + { + "tagName": "a", + "text": "4 open violationsOpenAOC compliance needs operator reviewRecent provenance violations require a compliance drilldown before promotion.", + "href": "/ops/operations/aoc/violations" + }, + { + "tagName": "a", + "text": "Ops 5 trust signals Data IntegrityFeeds freshness, scan pipeline health, integration reachability, and DLQ replay safety.", + "href": "/ops/operations/data-integrity" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/operations/jobs-queues", + "targetUrl": "https://stella-ops.local/ops/operations/jobs-queues?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/operations/jobs-queues?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Jobs & Queues - Stella Ops QA 1773540847164", + "headings": [ + "Jobs & Queues", + "Context" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Open Data Integrity", + "href": "/ops/operations/data-integrity" + }, + { + "tagName": "a", + "text": "Open Diagnostics", + "href": "/ops/operations/doctor" + }, + { + "tagName": "button", + "text": "Jobs", + "href": "" + }, + { + "tagName": "button", + "text": "Runs", + "href": "" + }, + { + "tagName": "button", + "text": "Schedules", + "href": "" + }, + { + "tagName": "button", + "text": "Dead Letters", + "href": "" + }, + { + "tagName": "button", + "text": "Workers", + "href": "" + }, + { + "tagName": "button", + "text": "Clear filters", + "href": "" + }, + { + "tagName": "a", + "text": "Open JobEngine", + "href": "/ops/operations/jobengine" + }, + { + "tagName": "a", + "text": "View Jobs", + "href": "/ops/operations/jobengine/jobs" + }, + { + "tagName": "a", + "text": "Open JobEngine", + "href": "/ops/operations/jobengine" + }, + { + "tagName": "a", + "text": "View Jobs", + "href": "/ops/operations/jobengine/jobs" + }, + { + "tagName": "a", + "text": "Open JobEngine", + "href": "/ops/operations/jobengine" + }, + { + "tagName": "a", + "text": "View Jobs", + "href": "/ops/operations/jobengine/jobs" + }, + { + "tagName": "a", + "text": "Open JobEngine", + "href": "/ops/operations/jobengine" + }, + { + "tagName": "a", + "text": "Open Dead-Letter Queue", + "href": "/ops/operations/dead-letter/queue" + }, + { + "tagName": "a", + "text": "Open Data Integrity", + "href": "/ops/operations/data-integrity" + }, + { + "tagName": "a", + "text": "Open Audit Log", + "href": "/evidence/audit-log" + }, + { + "tagName": "a", + "text": "Impacted Decisions", + "href": "/releases/runs" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/operations/feeds-airgap", + "targetUrl": "https://stella-ops.local/ops/operations/feeds-airgap?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/operations/feeds-airgap?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Feeds & Airgap - Stella Ops QA 1773540847164", + "headings": [ + "Feeds & Airgap" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Configure Sources", + "href": "/ops/integrations/advisory-vex-sources" + }, + { + "tagName": "a", + "text": "Open Freshness Lens", + "href": "/ops/operations/data-integrity/feeds-freshness" + }, + { + "tagName": "a", + "text": "Open Offline Bundles", + "href": "/ops/operations/offline-kit/bundles" + }, + { + "tagName": "a", + "text": "Feed Mirrors", + "href": "/ops/operations/feeds-airgap?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&tab=feed-mirrors" + }, + { + "tagName": "a", + "text": "Airgap Bundles", + "href": "/ops/operations/feeds-airgap?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&tab=airgap-bundles" + }, + { + "tagName": "a", + "text": "Version Locks", + "href": "/ops/operations/feeds-airgap?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&tab=version-locks" + }, + { + "tagName": "a", + "text": "Open freshness details", + "href": "/ops/operations/data-integrity/feeds-freshness" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/operations/data-integrity", + "targetUrl": "https://stella-ops.local/ops/operations/data-integrity?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/operations/data-integrity?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Data Integrity - StellaOps - Stella Ops QA 1773540847164", + "headings": [ + "Data Integrity", + "Data Trust Score", + "Impacted Decisions", + "Top Failures", + "Drilldowns" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Feeds Freshness WARN Impact: BLOCKING NVD feed stale by 3h 12m", + "href": "/ops/operations/data-integrity/feeds-freshness" + }, + { + "tagName": "a", + "text": "SBOM Pipeline OK Impact: INFO Nightly rescan completed", + "href": "/ops/operations/data-integrity/scan-pipeline" + }, + { + "tagName": "a", + "text": "Reachability Ingest WARN Impact: DEGRADED Runtime backlog elevated", + "href": "/ops/operations/data-integrity/reachability-ingest" + }, + { + "tagName": "a", + "text": "Integrations OK Impact: INFO Core connectors are reachable", + "href": "/ops/operations/data-integrity/integration-connectivity" + }, + { + "tagName": "a", + "text": "DLQ WARN Impact: DEGRADED 3 items pending replay", + "href": "/ops/operations/data-integrity/dlq" + }, + { + "tagName": "a", + "text": "Hotfix 1.2.4", + "href": "/releases/approvals?releaseId=rel-hotfix-124" + }, + { + "tagName": "a", + "text": "Platform Release 1.3.0-rc1", + "href": "/releases/approvals?releaseId=rel-platform-130-rc1" + }, + { + "tagName": "a", + "text": "NVD sync lag", + "href": "/ops/operations/data-integrity/feeds-freshness" + }, + { + "tagName": "a", + "text": "Runtime ingest backlog", + "href": "/ops/operations/data-integrity/reachability-ingest" + }, + { + "tagName": "a", + "text": "DLQ replay queue", + "href": "/ops/operations/data-integrity/dlq" + }, + { + "tagName": "a", + "text": "Nightly Ops Report", + "href": "/ops/operations/data-integrity/nightly-ops" + }, + { + "tagName": "a", + "text": "Feeds Freshness", + "href": "/ops/operations/data-integrity/feeds-freshness" + }, + { + "tagName": "a", + "text": "Scan Pipeline Health", + "href": "/ops/operations/data-integrity/scan-pipeline" + }, + { + "tagName": "a", + "text": "Reachability Ingest Health", + "href": "/ops/operations/data-integrity/reachability-ingest" + }, + { + "tagName": "a", + "text": "Integration Connectivity", + "href": "/ops/operations/data-integrity/integration-connectivity" + }, + { + "tagName": "a", + "text": "DLQ and Replays", + "href": "/ops/operations/data-integrity/dlq" + }, + { + "tagName": "a", + "text": "Data Quality SLOs", + "href": "/ops/operations/data-integrity/slos" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/operations/system-health", + "targetUrl": "https://stella-ops.local/ops/operations/system-health?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/operations/system-health?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "System Health - Stella Ops QA 1773540847164", + "headings": [ + "System Health", + "Service Health" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "button", + "text": "↻ Refresh", + "href": "" + }, + { + "tagName": "button", + "text": "Quick Diagnostics", + "href": "" + }, + { + "tagName": "button", + "text": "Overview", + "href": "" + }, + { + "tagName": "button", + "text": "Services", + "href": "" + }, + { + "tagName": "button", + "text": "Diagnostics", + "href": "" + }, + { + "tagName": "button", + "text": "Incidents", + "href": "" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/operations/health-slo", + "targetUrl": "https://stella-ops.local/ops/operations/health-slo?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/operations/health-slo?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Health & SLO - Stella Ops QA 1773540847164", + "headings": [ + "Platform Health", + "Service Health", + "Dependencies", + "Incident Timeline (Last 24h)" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "button", + "text": "↻ Refresh", + "href": "" + }, + { + "tagName": "a", + "text": "View Full Timeline", + "href": "/ops/operations/health-slo/incidents" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/operations/jobengine", + "targetUrl": "https://stella-ops.local/ops/operations/jobengine?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/operations/jobengine?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "JobEngine - Stella Ops QA 1773540847164", + "headings": [ + "JobEngine", + "Jobs", + "Execution Quotas", + "Dead-Letter Recovery", + "Your Access" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "← Back to Jobs & Queues", + "href": "/ops/operations/jobs-queues" + }, + { + "tagName": "a", + "text": "Scheduler Runs", + "href": "/ops/operations/scheduler/runs" + }, + { + "tagName": "a", + "text": "Dead-Letter", + "href": "/ops/operations/dead-letter" + }, + { + "tagName": "button", + "text": "Refresh", + "href": "" + }, + { + "tagName": "a", + "text": "JobsBrowse execution records, inspect payload digests, and follow DAG relationships.Pending0Scheduled0Completed0", + "href": "/ops/operations/jobengine/jobs" + }, + { + "tagName": "a", + "text": "Execution QuotasManage per-job-type concurrency, refill rate, and pause state.Average Token Usage-Average Concurrency Usage-Paused Quotas0", + "href": "/ops/operations/jobengine/quotas" + }, + { + "tagName": "a", + "text": "Dead-Letter RecoveryRetry or resolve failed execution records and inspect replay outcomes.Retryable0Replayed0Resolved0", + "href": "/ops/operations/dead-letter" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/operations/scheduler", + "targetUrl": "https://stella-ops.local/ops/operations/scheduler?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/operations/scheduler?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Scheduler - Stella Ops QA 1773540847164", + "headings": [ + "Scheduler Runs" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "button", + "text": "Manage Schedules", + "href": "" + }, + { + "tagName": "button", + "text": "Worker Fleet", + "href": "" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/operations/quotas", + "targetUrl": "https://stella-ops.local/ops/operations/quotas?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/operations/quotas?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Quotas & Limits - Stella Ops QA 1773540847164", + "headings": [ + "Operator Quota Dashboard", + "Consumption Trend (30 Days)", + "Quota Forecast", + "Top Tenants by Usage", + "Recent Throttle Events (429s)", + "License Entitlement" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "button", + "text": "Configure Alerts", + "href": "" + }, + { + "tagName": "button", + "text": "Export Report", + "href": "" + }, + { + "tagName": "button", + "text": "license", + "href": "" + }, + { + "tagName": "button", + "text": "jobs", + "href": "" + }, + { + "tagName": "button", + "text": "api", + "href": "" + }, + { + "tagName": "button", + "text": "storage", + "href": "" + }, + { + "tagName": "button", + "text": "scans", + "href": "" + }, + { + "tagName": "a", + "text": "View Details", + "href": "/ops/operations/quotas/forecast" + }, + { + "tagName": "a", + "text": "View All", + "href": "/ops/operations/quotas/tenants" + }, + { + "tagName": "a", + "text": "Default Tenant", + "href": "/ops/operations/quotas/tenants/demo-prod" + }, + { + "tagName": "a", + "text": "View All", + "href": "/ops/operations/quotas/throttle" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/operations/offline-kit", + "targetUrl": "https://stella-ops.local/ops/operations/offline-kit?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/operations/offline-kit?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Offline Mode Dashboard - Stella Ops QA 1773540847164", + "headings": [ + "Offline Kit Management" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Feeds & Airgap", + "href": "/ops/operations/feeds-airgap" + }, + { + "tagName": "a", + "text": "Evidence Exports", + "href": "/evidence/exports" + }, + { + "tagName": "a", + "text": "Replay & Verify", + "href": "/evidence/verify-replay" + }, + { + "tagName": "a", + "text": "Trust & Signing", + "href": "/setup/trust-signing" + }, + { + "tagName": "a", + "text": "Dashboard", + "href": "/ops/operations/offline-kit/dashboard" + }, + { + "tagName": "a", + "text": "Bundles", + "href": "/ops/operations/offline-kit/bundles" + }, + { + "tagName": "a", + "text": "Verification", + "href": "/ops/operations/offline-kit/verify" + }, + { + "tagName": "a", + "text": "JWKS", + "href": "/ops/operations/offline-kit/jwks" + }, + { + "tagName": "button", + "text": "Enter Offline Mode", + "href": "" + }, + { + "tagName": "a", + "text": "Dashboard & KPIsOpen workflow", + "href": "/ops/operations/offline-kit/dashboard" + }, + { + "tagName": "a", + "text": "View FindingsOpen workflow", + "href": "/security/findings" + }, + { + "tagName": "a", + "text": "SBOM ViewerOpen workflow", + "href": "/security/sbom" + }, + { + "tagName": "a", + "text": "Policy ViewerOpen workflow", + "href": "/ops/policy/overview" + }, + { + "tagName": "a", + "text": "Evidence VerificationOpen workflow", + "href": "/evidence/verify-replay" + }, + { + "tagName": "a", + "text": "Triage & VEX CreationOpen workflow", + "href": "/triage/artifacts" + }, + { + "tagName": "a", + "text": "Manage IntegrationsOpen workflow", + "href": "/ops/integrations" + }, + { + "tagName": "a", + "text": "Export Audit BundlesOpen workflow", + "href": "/evidence/exports" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/operations/dead-letter", + "targetUrl": "https://stella-ops.local/ops/operations/dead-letter?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/operations/dead-letter?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Dead-Letter Queue - Stella Ops QA 1773540847164", + "headings": [ + "Dead-Letter Queue Management", + "Error Distribution (Last 7 days)", + "By Tenant", + "Queue Browser" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "button", + "text": "Export CSV", + "href": "" + }, + { + "tagName": "button", + "text": "Replay All Retryable (0)", + "href": "" + }, + { + "tagName": "a", + "text": "View Full Queue", + "href": "/ops/operations/dead-letter/queue" + }, + { + "tagName": "button", + "text": "Clear", + "href": "" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/operations/aoc", + "targetUrl": "https://stella-ops.local/ops/operations/aoc?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/operations/aoc?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "AOC Compliance - Stella Ops QA 1773540847164", + "headings": [ + "AOC Compliance Dashboard", + "Guard Violations (Last 24h)", + "Ingestion Flow", + "Provenance Chain Validator", + "Supersedes Depth Distribution" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "button", + "text": "Refresh", + "href": "" + }, + { + "tagName": "a", + "text": "Export Report", + "href": "/ops/operations/aoc/report" + }, + { + "tagName": "a", + "text": "View All", + "href": "/ops/operations/aoc/violations" + }, + { + "tagName": "a", + "text": "Details", + "href": "/ops/operations/aoc/ingestion" + }, + { + "tagName": "a", + "text": "Full Validator", + "href": "/ops/operations/aoc/provenance" + }, + { + "tagName": "button", + "text": "Validate", + "href": "" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/operations/doctor", + "targetUrl": "https://stella-ops.local/ops/operations/doctor?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/operations/doctor?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Doctor Diagnostics - Stella Ops QA 1773540847164", + "headings": [ + "Doctor Diagnostics", + "Doctor Packs" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "button", + "text": "Quick Check", + "href": "" + }, + { + "tagName": "button", + "text": "Normal Check", + "href": "" + }, + { + "tagName": "button", + "text": "Full Check", + "href": "" + }, + { + "tagName": "button", + "text": "Export", + "href": "" + }, + { + "tagName": "button", + "text": "Clear Filters", + "href": "" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/operations/signals", + "targetUrl": "https://stella-ops.local/ops/operations/signals?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/operations/signals?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Signals - Stella Ops QA 1773540847164", + "headings": [ + "Signals Runtime Dashboard", + "By provider", + "By status", + "Probe health by host" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "button", + "text": "Refresh", + "href": "" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/operations/packs", + "targetUrl": "https://stella-ops.local/ops/operations/packs?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/operations/packs?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Pack Registry - Stella Ops QA 1773540847164", + "headings": [ + "Pack Registry Browser", + "Total packs", + "Installed", + "Upgrade available" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "button", + "text": "Refresh", + "href": "" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/operations/ai-runs", + "targetUrl": "https://stella-ops.local/ops/operations/ai-runs?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/operations/ai-runs?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "AI Runs - Stella Ops QA 1773540847164", + "headings": [ + "AI Runs" + ], + "problemTexts": [], + "visibleActions": [], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/operations/notifications", + "targetUrl": "https://stella-ops.local/ops/operations/notifications?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/operations/notifications?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Notifications - Stella Ops QA 1773540847164", + "headings": [ + "Notification Operations", + "Ownership and setup", + "Watchlist handoff", + "Channels", + "Rules", + "Deliveries" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "button", + "text": "Refresh data", + "href": "" + }, + { + "tagName": "a", + "text": "Open setup notifications", + "href": "/setup/notifications" + }, + { + "tagName": "a", + "text": "Re-run notifications setup", + "href": "/setup-wizard/wizard?step=notifications&mode=reconfigure" + }, + { + "tagName": "a", + "text": "Open watchlist tuning", + "href": "/setup/trust-signing/watchlist/tuning?returnTo=%2Fops%2Foperations%2Fnotifications&scope=tenant&tab=tuning" + }, + { + "tagName": "a", + "text": "Review watchlist alerts", + "href": "/setup/trust-signing/watchlist/alerts?returnTo=%2Fops%2Foperations%2Fnotifications&scope=tenant&tab=alerts" + }, + { + "tagName": "button", + "text": "New channel", + "href": "" + }, + { + "tagName": "button", + "text": "ci-webhookWebhook Enabled", + "href": "" + }, + { + "tagName": "button", + "text": "ops-emailEmail Enabled", + "href": "" + }, + { + "tagName": "button", + "text": "security-slackSlack Enabled", + "href": "" + }, + { + "tagName": "button", + "text": "Reset", + "href": "" + }, + { + "tagName": "button", + "text": "Delete", + "href": "" + }, + { + "tagName": "button", + "text": "Save channel", + "href": "" + }, + { + "tagName": "button", + "text": "Send test", + "href": "" + }, + { + "tagName": "button", + "text": "New rule", + "href": "" + }, + { + "tagName": "button", + "text": "scan-results-emailany", + "href": "" + }, + { + "tagName": "button", + "text": "policy-violations-slackany", + "href": "" + }, + { + "tagName": "button", + "text": "critical-vuln-all-channelsany", + "href": "" + }, + { + "tagName": "button", + "text": "Reset", + "href": "" + }, + { + "tagName": "button", + "text": "Delete", + "href": "" + }, + { + "tagName": "button", + "text": "Save rule", + "href": "" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/operations/status", + "targetUrl": "https://stella-ops.local/ops/operations/status?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/operations/status?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "System Status - Stella Ops QA 1773540847164", + "headings": [ + "Console Status" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "button", + "text": "Refresh", + "href": "" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/integrations", + "targetUrl": "https://stella-ops.local/ops/integrations?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/integrations?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Integrations - Stella Ops QA 1773540847164", + "headings": [ + "Integrations", + "Suggested Setup Order", + "Recent Activity" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Hub", + "href": "/ops/integrations" + }, + { + "tagName": "a", + "text": "Registries", + "href": "/ops/integrations/registries" + }, + { + "tagName": "a", + "text": "SCM", + "href": "/ops/integrations/scm" + }, + { + "tagName": "a", + "text": "CI/CD", + "href": "/ops/integrations/ci" + }, + { + "tagName": "a", + "text": "Runtimes / Hosts", + "href": "/ops/integrations/runtime-hosts" + }, + { + "tagName": "a", + "text": "Advisory & VEX", + "href": "/ops/integrations/advisory-vex-sources" + }, + { + "tagName": "a", + "text": "Secrets", + "href": "/ops/integrations/secrets" + }, + { + "tagName": "a", + "text": "Activity", + "href": "/ops/integrations/activity" + }, + { + "tagName": "a", + "text": "Registries", + "href": "/ops/integrations/registries" + }, + { + "tagName": "a", + "text": "Source Control", + "href": "/ops/integrations/scm" + }, + { + "tagName": "a", + "text": "CI/CD", + "href": "/ops/integrations/ci" + }, + { + "tagName": "a", + "text": "Advisory & VEX Sources", + "href": "/ops/integrations/advisory-vex-sources" + }, + { + "tagName": "a", + "text": "Secrets", + "href": "/ops/integrations/secrets" + }, + { + "tagName": "a", + "text": "Registries2", + "href": "/ops/integrations/registries" + }, + { + "tagName": "a", + "text": "SCM1", + "href": "/ops/integrations/scm" + }, + { + "tagName": "a", + "text": "CI/CD0", + "href": "/ops/integrations/ci" + }, + { + "tagName": "a", + "text": "Runtimes / Hosts0", + "href": "/ops/integrations/runtime-hosts" + }, + { + "tagName": "a", + "text": "Advisory Sources0", + "href": "/ops/integrations/advisory-vex-sources" + }, + { + "tagName": "a", + "text": "VEX Sources0", + "href": "/ops/integrations/advisory-vex-sources" + }, + { + "tagName": "a", + "text": "Secrets0", + "href": "/ops/integrations/secrets" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/integrations/onboarding", + "targetUrl": "https://stella-ops.local/ops/integrations/onboarding?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/integrations/onboarding?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Add Integration - Stella Ops QA 1773540847164", + "headings": [ + "Integrations", + "Container Registries", + "Source Control", + "CI/CD Pipelines", + "Hosts & Observers", + "Advisory & VEX Sources" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Hub", + "href": "/ops/integrations" + }, + { + "tagName": "a", + "text": "Registries", + "href": "/ops/integrations/registries" + }, + { + "tagName": "a", + "text": "SCM", + "href": "/ops/integrations/scm" + }, + { + "tagName": "a", + "text": "CI/CD", + "href": "/ops/integrations/ci" + }, + { + "tagName": "a", + "text": "Runtimes / Hosts", + "href": "/ops/integrations/runtime-hosts" + }, + { + "tagName": "a", + "text": "Advisory & VEX", + "href": "/ops/integrations/advisory-vex-sources" + }, + { + "tagName": "a", + "text": "Secrets", + "href": "/ops/integrations/secrets" + }, + { + "tagName": "a", + "text": "Activity", + "href": "/ops/integrations/activity" + }, + { + "tagName": "button", + "text": "+ Add Registry", + "href": "" + }, + { + "tagName": "button", + "text": "+ Add SCM", + "href": "" + }, + { + "tagName": "button", + "text": "+ Add CI/CD", + "href": "" + }, + { + "tagName": "button", + "text": "+ Add Host", + "href": "" + }, + { + "tagName": "button", + "text": "Configure Sources", + "href": "" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/integrations/registries", + "targetUrl": "https://stella-ops.local/ops/integrations/registries?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/integrations/registries?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Registries - Stella Ops QA 1773540847164", + "headings": [ + "Integrations", + "Registry Integrations" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Hub", + "href": "/ops/integrations" + }, + { + "tagName": "a", + "text": "Registries", + "href": "/ops/integrations/registries" + }, + { + "tagName": "a", + "text": "SCM", + "href": "/ops/integrations/scm" + }, + { + "tagName": "a", + "text": "CI/CD", + "href": "/ops/integrations/ci" + }, + { + "tagName": "a", + "text": "Runtimes / Hosts", + "href": "/ops/integrations/runtime-hosts" + }, + { + "tagName": "a", + "text": "Advisory & VEX", + "href": "/ops/integrations/advisory-vex-sources" + }, + { + "tagName": "a", + "text": "Secrets", + "href": "/ops/integrations/secrets" + }, + { + "tagName": "a", + "text": "Activity", + "href": "/ops/integrations/activity" + }, + { + "tagName": "button", + "text": "+ Add Registry", + "href": "" + }, + { + "tagName": "a", + "text": "QA Harbor 1773445379432", + "href": "/ops/integrations/24b36d9f-8907-484f-8fa5-c7b0717c9534" + }, + { + "tagName": "a", + "text": "", + "href": "/ops/integrations/24b36d9f-8907-484f-8fa5-c7b0717c9534" + }, + { + "tagName": "a", + "text": "QA Harbor 1773445500318", + "href": "/ops/integrations/e86e16c2-0dd9-439b-a525-54d0b3a1b288" + }, + { + "tagName": "a", + "text": "", + "href": "/ops/integrations/e86e16c2-0dd9-439b-a525-54d0b3a1b288" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/integrations/scm", + "targetUrl": "https://stella-ops.local/ops/integrations/scm?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/integrations/scm?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Source Control - Stella Ops QA 1773540847164", + "headings": [ + "Integrations", + "Scm Integrations" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Hub", + "href": "/ops/integrations" + }, + { + "tagName": "a", + "text": "Registries", + "href": "/ops/integrations/registries" + }, + { + "tagName": "a", + "text": "SCM", + "href": "/ops/integrations/scm" + }, + { + "tagName": "a", + "text": "CI/CD", + "href": "/ops/integrations/ci" + }, + { + "tagName": "a", + "text": "Runtimes / Hosts", + "href": "/ops/integrations/runtime-hosts" + }, + { + "tagName": "a", + "text": "Advisory & VEX", + "href": "/ops/integrations/advisory-vex-sources" + }, + { + "tagName": "a", + "text": "Secrets", + "href": "/ops/integrations/secrets" + }, + { + "tagName": "a", + "text": "Activity", + "href": "/ops/integrations/activity" + }, + { + "tagName": "button", + "text": "+ Add Scm", + "href": "" + }, + { + "tagName": "a", + "text": "QA GitHub Fixture 1773447603842", + "href": "/ops/integrations/41b261f0-b979-427f-b630-bee24a6543bd" + }, + { + "tagName": "a", + "text": "", + "href": "/ops/integrations/41b261f0-b979-427f-b630-bee24a6543bd" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/integrations/ci", + "targetUrl": "https://stella-ops.local/ops/integrations/ci?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/integrations/ci?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "CI/CD - Stella Ops QA 1773540847164", + "headings": [ + "Integrations", + "Ci Integrations" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Hub", + "href": "/ops/integrations" + }, + { + "tagName": "a", + "text": "Registries", + "href": "/ops/integrations/registries" + }, + { + "tagName": "a", + "text": "SCM", + "href": "/ops/integrations/scm" + }, + { + "tagName": "a", + "text": "CI/CD", + "href": "/ops/integrations/ci" + }, + { + "tagName": "a", + "text": "Runtimes / Hosts", + "href": "/ops/integrations/runtime-hosts" + }, + { + "tagName": "a", + "text": "Advisory & VEX", + "href": "/ops/integrations/advisory-vex-sources" + }, + { + "tagName": "a", + "text": "Secrets", + "href": "/ops/integrations/secrets" + }, + { + "tagName": "a", + "text": "Activity", + "href": "/ops/integrations/activity" + }, + { + "tagName": "button", + "text": "+ Add Ci", + "href": "" + }, + { + "tagName": "button", + "text": "Add your first ci", + "href": "" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/integrations/runtime-hosts", + "targetUrl": "https://stella-ops.local/ops/integrations/runtime-hosts?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/integrations/runtime-hosts?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Runtimes / Hosts - Stella Ops QA 1773540847164", + "headings": [ + "Integrations", + "RuntimeHost Integrations" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Hub", + "href": "/ops/integrations" + }, + { + "tagName": "a", + "text": "Registries", + "href": "/ops/integrations/registries" + }, + { + "tagName": "a", + "text": "SCM", + "href": "/ops/integrations/scm" + }, + { + "tagName": "a", + "text": "CI/CD", + "href": "/ops/integrations/ci" + }, + { + "tagName": "a", + "text": "Runtimes / Hosts", + "href": "/ops/integrations/runtime-hosts" + }, + { + "tagName": "a", + "text": "Advisory & VEX", + "href": "/ops/integrations/advisory-vex-sources" + }, + { + "tagName": "a", + "text": "Secrets", + "href": "/ops/integrations/secrets" + }, + { + "tagName": "a", + "text": "Activity", + "href": "/ops/integrations/activity" + }, + { + "tagName": "button", + "text": "+ Add RuntimeHost", + "href": "" + }, + { + "tagName": "button", + "text": "Add your first runtimehost", + "href": "" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/integrations/advisory-vex-sources", + "targetUrl": "https://stella-ops.local/ops/integrations/advisory-vex-sources?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/integrations/advisory-vex-sources?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Advisory & VEX Sources - Stella Ops QA 1773540847164", + "headings": [ + "Integrations", + "Advisory & VEX Source Catalog", + "▶ Primary (4)", + "▶ Vendor (14)", + "▶ Distribution (10)", + "▶ Ecosystem (9)", + "▶ Cert (15)", + "▶ Csaf (3)", + "▶ Threat (4)", + "▶ Exploit (3)", + "▶ Container (2)", + "▶ Hardware (3)", + "▶ Ics (2)", + "▶ PackageManager (4)", + "▶ Mirror (1)" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Hub", + "href": "/ops/integrations" + }, + { + "tagName": "a", + "text": "Registries", + "href": "/ops/integrations/registries" + }, + { + "tagName": "a", + "text": "SCM", + "href": "/ops/integrations/scm" + }, + { + "tagName": "a", + "text": "CI/CD", + "href": "/ops/integrations/ci" + }, + { + "tagName": "a", + "text": "Runtimes / Hosts", + "href": "/ops/integrations/runtime-hosts" + }, + { + "tagName": "a", + "text": "Advisory & VEX", + "href": "/ops/integrations/advisory-vex-sources" + }, + { + "tagName": "a", + "text": "Secrets", + "href": "/ops/integrations/secrets" + }, + { + "tagName": "a", + "text": "Activity", + "href": "/ops/integrations/activity" + }, + { + "tagName": "button", + "text": "Check All", + "href": "" + }, + { + "tagName": "a", + "text": "Configure Mirror", + "href": "/ops/integrations/advisory-vex-sources/mirror?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + }, + { + "tagName": "a", + "text": "Connect to Mirror", + "href": "/ops/integrations/advisory-vex-sources/mirror/client-setup?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + }, + { + "tagName": "button", + "text": "Create Mirror Domain", + "href": "" + }, + { + "tagName": "button", + "text": "Enable All", + "href": "" + }, + { + "tagName": "button", + "text": "Disable All", + "href": "" + }, + { + "tagName": "button", + "text": "Check", + "href": "" + }, + { + "tagName": "button", + "text": "Check", + "href": "" + }, + { + "tagName": "button", + "text": "Check", + "href": "" + }, + { + "tagName": "button", + "text": "Check", + "href": "" + }, + { + "tagName": "button", + "text": "Enable All", + "href": "" + }, + { + "tagName": "button", + "text": "Disable All", + "href": "" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/integrations/secrets", + "targetUrl": "https://stella-ops.local/ops/integrations/secrets?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/integrations/secrets?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Secrets - Stella Ops QA 1773540847164", + "headings": [ + "Integrations", + "RepoSource Integrations" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Hub", + "href": "/ops/integrations" + }, + { + "tagName": "a", + "text": "Registries", + "href": "/ops/integrations/registries" + }, + { + "tagName": "a", + "text": "SCM", + "href": "/ops/integrations/scm" + }, + { + "tagName": "a", + "text": "CI/CD", + "href": "/ops/integrations/ci" + }, + { + "tagName": "a", + "text": "Runtimes / Hosts", + "href": "/ops/integrations/runtime-hosts" + }, + { + "tagName": "a", + "text": "Advisory & VEX", + "href": "/ops/integrations/advisory-vex-sources" + }, + { + "tagName": "a", + "text": "Secrets", + "href": "/ops/integrations/secrets" + }, + { + "tagName": "a", + "text": "Activity", + "href": "/ops/integrations/activity" + }, + { + "tagName": "button", + "text": "+ Add Integration", + "href": "" + }, + { + "tagName": "button", + "text": "Open add integration hub", + "href": "" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/integrations/notifications", + "targetUrl": "https://stella-ops.local/ops/integrations/notifications?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/integrations/notifications?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Notification Providers - Stella Ops QA 1773540847164", + "headings": [ + "Integrations", + "Notification Integrations" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Hub", + "href": "/ops/integrations" + }, + { + "tagName": "a", + "text": "Registries", + "href": "/ops/integrations/registries" + }, + { + "tagName": "a", + "text": "SCM", + "href": "/ops/integrations/scm" + }, + { + "tagName": "a", + "text": "CI/CD", + "href": "/ops/integrations/ci" + }, + { + "tagName": "a", + "text": "Runtimes / Hosts", + "href": "/ops/integrations/runtime-hosts" + }, + { + "tagName": "a", + "text": "Advisory & VEX", + "href": "/ops/integrations/advisory-vex-sources" + }, + { + "tagName": "a", + "text": "Secrets", + "href": "/ops/integrations/secrets" + }, + { + "tagName": "a", + "text": "Activity", + "href": "/ops/integrations/activity" + }, + { + "tagName": "button", + "text": "+ Add Integration", + "href": "" + }, + { + "tagName": "a", + "text": "QA GitHub Fixture 1773447603842", + "href": "/ops/integrations/41b261f0-b979-427f-b630-bee24a6543bd" + }, + { + "tagName": "a", + "text": "", + "href": "/ops/integrations/41b261f0-b979-427f-b630-bee24a6543bd" + }, + { + "tagName": "a", + "text": "QA Harbor 1773445379432", + "href": "/ops/integrations/24b36d9f-8907-484f-8fa5-c7b0717c9534" + }, + { + "tagName": "a", + "text": "", + "href": "/ops/integrations/24b36d9f-8907-484f-8fa5-c7b0717c9534" + }, + { + "tagName": "a", + "text": "QA Harbor 1773445500318", + "href": "/ops/integrations/e86e16c2-0dd9-439b-a525-54d0b3a1b288" + }, + { + "tagName": "a", + "text": "", + "href": "/ops/integrations/e86e16c2-0dd9-439b-a525-54d0b3a1b288" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/integrations/sbom-sources", + "targetUrl": "https://stella-ops.local/ops/integrations/sbom-sources?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/integrations/sbom-sources?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "SBOM Sources - Stella Ops QA 1773540847164", + "headings": [ + "Integrations", + "SBOM Sources", + "No SBOM Sources Configured" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Hub", + "href": "/ops/integrations" + }, + { + "tagName": "a", + "text": "Registries", + "href": "/ops/integrations/registries" + }, + { + "tagName": "a", + "text": "SCM", + "href": "/ops/integrations/scm" + }, + { + "tagName": "a", + "text": "CI/CD", + "href": "/ops/integrations/ci" + }, + { + "tagName": "a", + "text": "Runtimes / Hosts", + "href": "/ops/integrations/runtime-hosts" + }, + { + "tagName": "a", + "text": "Advisory & VEX", + "href": "/ops/integrations/advisory-vex-sources" + }, + { + "tagName": "a", + "text": "Secrets", + "href": "/ops/integrations/secrets" + }, + { + "tagName": "a", + "text": "Activity", + "href": "/ops/integrations/activity" + }, + { + "tagName": "button", + "text": "+ New Source", + "href": "" + }, + { + "tagName": "button", + "text": "Search", + "href": "" + }, + { + "tagName": "button", + "text": "Create Your First Source", + "href": "" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/integrations/activity", + "targetUrl": "https://stella-ops.local/ops/integrations/activity?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/integrations/activity?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Activity - Stella Ops QA 1773540847164", + "headings": [ + "Integrations", + "Integration Activity" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Hub", + "href": "/ops/integrations" + }, + { + "tagName": "a", + "text": "Registries", + "href": "/ops/integrations/registries" + }, + { + "tagName": "a", + "text": "SCM", + "href": "/ops/integrations/scm" + }, + { + "tagName": "a", + "text": "CI/CD", + "href": "/ops/integrations/ci" + }, + { + "tagName": "a", + "text": "Runtimes / Hosts", + "href": "/ops/integrations/runtime-hosts" + }, + { + "tagName": "a", + "text": "Advisory & VEX", + "href": "/ops/integrations/advisory-vex-sources" + }, + { + "tagName": "a", + "text": "Secrets", + "href": "/ops/integrations/secrets" + }, + { + "tagName": "a", + "text": "Activity", + "href": "/ops/integrations/activity" + }, + { + "tagName": "a", + "text": "Back to Integrations", + "href": "/ops/integrations" + }, + { + "tagName": "button", + "text": "Clear", + "href": "" + }, + { + "tagName": "a", + "text": "Production Harbor", + "href": "/ops/integrations/int-1" + }, + { + "tagName": "a", + "text": "Production Harbor", + "href": "/ops/integrations/int-1" + }, + { + "tagName": "a", + "text": "GitHub Enterprise", + "href": "/ops/integrations/int-2" + }, + { + "tagName": "a", + "text": "GitHub Enterprise", + "href": "/ops/integrations/int-2" + }, + { + "tagName": "a", + "text": "Docker Hub Mirror", + "href": "/ops/integrations/int-3" + }, + { + "tagName": "a", + "text": "Production Harbor", + "href": "/ops/integrations/int-1" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/integrations/registry-admin", + "targetUrl": "https://stella-ops.local/ops/integrations/registry-admin?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/integrations/registry-admin?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Registry Plans - Stella Ops QA 1773540847164", + "headings": [ + "Integrations", + "Registry Token Service" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Hub", + "href": "/ops/integrations" + }, + { + "tagName": "a", + "text": "Registries", + "href": "/ops/integrations/registries" + }, + { + "tagName": "a", + "text": "SCM", + "href": "/ops/integrations/scm" + }, + { + "tagName": "a", + "text": "CI/CD", + "href": "/ops/integrations/ci" + }, + { + "tagName": "a", + "text": "Runtimes / Hosts", + "href": "/ops/integrations/runtime-hosts" + }, + { + "tagName": "a", + "text": "Advisory & VEX", + "href": "/ops/integrations/advisory-vex-sources" + }, + { + "tagName": "a", + "text": "Secrets", + "href": "/ops/integrations/secrets" + }, + { + "tagName": "a", + "text": "Activity", + "href": "/ops/integrations/activity" + }, + { + "tagName": "a", + "text": "Plans", + "href": "/ops/integrations/registry-admin/plans?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + }, + { + "tagName": "a", + "text": "Audit Log", + "href": "/ops/integrations/registry-admin/audit?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + }, + { + "tagName": "a", + "text": "+ New Plan", + "href": "/ops/integrations/registry-admin/new" + }, + { + "tagName": "a", + "text": "Create First Plan", + "href": "/ops/integrations/registry-admin/new" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/policy", + "targetUrl": "https://stella-ops.local/ops/policy?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/policy/overview?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Policy Decisioning Overview - Stella Ops QA 1773540847164", + "headings": [ + "Policy Decisioning Studio", + "One operator shell for policy, VEX, and release gates" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Reset view", + "href": "/ops/policy/overview" + }, + { + "tagName": "a", + "text": "Overview", + "href": "/ops/policy/overview" + }, + { + "tagName": "a", + "text": "Packs", + "href": "/ops/policy/packs" + }, + { + "tagName": "a", + "text": "Governance", + "href": "/ops/policy/governance" + }, + { + "tagName": "a", + "text": "Simulation", + "href": "/ops/policy/simulation" + }, + { + "tagName": "a", + "text": "VEX & Exceptions", + "href": "/ops/policy/vex" + }, + { + "tagName": "a", + "text": "Release Gates", + "href": "/ops/policy/gates" + }, + { + "tagName": "a", + "text": "Audit", + "href": "/ops/policy/audit" + }, + { + "tagName": "a", + "text": "Packs Workspace", + "href": "/ops/policy/packs" + }, + { + "tagName": "a", + "text": "Governance Controls", + "href": "/ops/policy/governance" + }, + { + "tagName": "a", + "text": "Simulation Lab", + "href": "/ops/policy/simulation" + }, + { + "tagName": "a", + "text": "VEX & Exceptions", + "href": "/ops/policy/vex" + }, + { + "tagName": "a", + "text": "Release Gates", + "href": "/ops/policy/gates" + }, + { + "tagName": "a", + "text": "Policy Audit", + "href": "/ops/policy/audit" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/policy/overview", + "targetUrl": "https://stella-ops.local/ops/policy/overview?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/policy/overview?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Policy Decisioning Overview - Stella Ops QA 1773540847164", + "headings": [ + "Policy Decisioning Studio", + "One operator shell for policy, VEX, and release gates" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Reset view", + "href": "/ops/policy/overview" + }, + { + "tagName": "a", + "text": "Overview", + "href": "/ops/policy/overview" + }, + { + "tagName": "a", + "text": "Packs", + "href": "/ops/policy/packs" + }, + { + "tagName": "a", + "text": "Governance", + "href": "/ops/policy/governance" + }, + { + "tagName": "a", + "text": "Simulation", + "href": "/ops/policy/simulation" + }, + { + "tagName": "a", + "text": "VEX & Exceptions", + "href": "/ops/policy/vex" + }, + { + "tagName": "a", + "text": "Release Gates", + "href": "/ops/policy/gates" + }, + { + "tagName": "a", + "text": "Audit", + "href": "/ops/policy/audit" + }, + { + "tagName": "a", + "text": "Packs Workspace", + "href": "/ops/policy/packs" + }, + { + "tagName": "a", + "text": "Governance Controls", + "href": "/ops/policy/governance" + }, + { + "tagName": "a", + "text": "Simulation Lab", + "href": "/ops/policy/simulation" + }, + { + "tagName": "a", + "text": "VEX & Exceptions", + "href": "/ops/policy/vex" + }, + { + "tagName": "a", + "text": "Release Gates", + "href": "/ops/policy/gates" + }, + { + "tagName": "a", + "text": "Policy Audit", + "href": "/ops/policy/audit" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/policy/baselines", + "targetUrl": "https://stella-ops.local/ops/policy/baselines?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/policy/baselines?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Policy Packs - Stella Ops QA 1773540847164", + "headings": [ + "Policy Decisioning Studio", + "Policy packs", + "Core Policy Pack" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Reset view", + "href": "/ops/policy/overview" + }, + { + "tagName": "a", + "text": "Overview", + "href": "/ops/policy/overview" + }, + { + "tagName": "a", + "text": "Packs", + "href": "/ops/policy/packs" + }, + { + "tagName": "a", + "text": "Governance", + "href": "/ops/policy/governance" + }, + { + "tagName": "a", + "text": "Simulation", + "href": "/ops/policy/simulation" + }, + { + "tagName": "a", + "text": "VEX & Exceptions", + "href": "/ops/policy/vex" + }, + { + "tagName": "a", + "text": "Release Gates", + "href": "/ops/policy/gates" + }, + { + "tagName": "a", + "text": "Audit", + "href": "/ops/policy/audit" + }, + { + "tagName": "a", + "text": "Edit", + "href": "/ops/policy/packs/pack-1/edit" + }, + { + "tagName": "a", + "text": "Simulate", + "href": "/ops/policy/packs/pack-1/simulate" + }, + { + "tagName": "a", + "text": "Approvals", + "href": "/ops/policy/packs/pack-1/approvals" + }, + { + "tagName": "a", + "text": "Dashboard", + "href": "/ops/policy/packs/pack-1" + }, + { + "tagName": "button", + "text": "Refresh packs", + "href": "" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/policy/gates", + "targetUrl": "https://stella-ops.local/ops/policy/gates?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/policy/gates?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Release Gates - Stella Ops QA 1773540847164", + "headings": [ + "Policy Decisioning Studio", + "Policy gate catalog and release posture", + "Gate Evaluation" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Reset view", + "href": "/ops/policy/overview" + }, + { + "tagName": "a", + "text": "Overview", + "href": "/ops/policy/overview" + }, + { + "tagName": "a", + "text": "Packs", + "href": "/ops/policy/packs" + }, + { + "tagName": "a", + "text": "Governance", + "href": "/ops/policy/governance" + }, + { + "tagName": "a", + "text": "Simulation", + "href": "/ops/policy/simulation" + }, + { + "tagName": "a", + "text": "VEX & Exceptions", + "href": "/ops/policy/vex" + }, + { + "tagName": "a", + "text": "Release Gates", + "href": "/ops/policy/gates" + }, + { + "tagName": "a", + "text": "Audit", + "href": "/ops/policy/audit" + }, + { + "tagName": "a", + "text": "Open gate catalog", + "href": "/ops/policy/gates/catalog" + }, + { + "tagName": "a", + "text": "Open exceptions", + "href": "/ops/policy/vex/exceptions" + }, + { + "tagName": "a", + "text": "Open promotion simulation", + "href": "/ops/policy/simulation/promotion" + }, + { + "tagName": "button", + "text": "Explain", + "href": "" + }, + { + "tagName": "button", + "text": "Compare", + "href": "" + }, + { + "tagName": "button", + "text": "▸ Details", + "href": "" + }, + { + "tagName": "button", + "text": "Explain", + "href": "" + }, + { + "tagName": "button", + "text": "Explain", + "href": "" + }, + { + "tagName": "button", + "text": "Explain", + "href": "" + }, + { + "tagName": "button", + "text": "Open Evidence", + "href": "" + }, + { + "tagName": "a", + "text": "Review VEX posture", + "href": "/ops/policy/vex" + }, + { + "tagName": "a", + "text": "Review audit trail", + "href": "/ops/policy/audit/policy" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/policy/simulation", + "targetUrl": "https://stella-ops.local/ops/policy/simulation?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/policy/simulation?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Policy Simulation - Stella Ops QA 1773540847164", + "headings": [ + "Policy Decisioning Studio", + "Policy Simulation Studio", + "Shadow Mode Dashboard" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Reset view", + "href": "/ops/policy/overview" + }, + { + "tagName": "a", + "text": "Overview", + "href": "/ops/policy/overview" + }, + { + "tagName": "a", + "text": "Packs", + "href": "/ops/policy/packs" + }, + { + "tagName": "a", + "text": "Governance", + "href": "/ops/policy/governance" + }, + { + "tagName": "a", + "text": "Simulation", + "href": "/ops/policy/simulation" + }, + { + "tagName": "a", + "text": "VEX & Exceptions", + "href": "/ops/policy/vex" + }, + { + "tagName": "a", + "text": "Release Gates", + "href": "/ops/policy/gates" + }, + { + "tagName": "a", + "text": "Audit", + "href": "/ops/policy/audit" + }, + { + "tagName": "button", + "text": "Enable", + "href": "" + }, + { + "tagName": "button", + "text": "View Results", + "href": "" + }, + { + "tagName": "a", + "text": "Shadow Mode", + "href": "/ops/policy/simulation/shadow" + }, + { + "tagName": "a", + "text": "Simulation Console", + "href": "/ops/policy/simulation/console" + }, + { + "tagName": "a", + "text": "Lint", + "href": "/ops/policy/simulation/lint" + }, + { + "tagName": "a", + "text": "Coverage72%", + "href": "/ops/policy/simulation/coverage" + }, + { + "tagName": "a", + "text": "Effective Policies", + "href": "/ops/policy/simulation/effective" + }, + { + "tagName": "a", + "text": "Audit Log", + "href": "/ops/policy/simulation/audit" + }, + { + "tagName": "a", + "text": "Exceptions3", + "href": "/ops/policy/simulation/exceptions" + }, + { + "tagName": "a", + "text": "Promotion Gate", + "href": "/ops/policy/simulation/promotion" + }, + { + "tagName": "a", + "text": "Merge Preview", + "href": "/ops/policy/simulation/merge" + }, + { + "tagName": "button", + "text": "Enable", + "href": "" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/policy/waivers", + "targetUrl": "https://stella-ops.local/ops/policy/waivers?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/policy/waivers?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "VEX & Exceptions - Stella Ops QA 1773540847164", + "headings": [ + "Policy Decisioning Studio", + "Exception Center" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Reset view", + "href": "/ops/policy/overview" + }, + { + "tagName": "a", + "text": "Overview", + "href": "/ops/policy/overview" + }, + { + "tagName": "a", + "text": "Packs", + "href": "/ops/policy/packs" + }, + { + "tagName": "a", + "text": "Governance", + "href": "/ops/policy/governance" + }, + { + "tagName": "a", + "text": "Simulation", + "href": "/ops/policy/simulation" + }, + { + "tagName": "a", + "text": "VEX & Exceptions", + "href": "/ops/policy/vex" + }, + { + "tagName": "a", + "text": "Release Gates", + "href": "/ops/policy/gates" + }, + { + "tagName": "a", + "text": "Audit", + "href": "/ops/policy/audit" + }, + { + "tagName": "a", + "text": "Approval Queue", + "href": "/ops/policy/waivers/approvals" + }, + { + "tagName": "button", + "text": "Refresh", + "href": "" + }, + { + "tagName": "button", + "text": "+ New Exception", + "href": "" + }, + { + "tagName": "button", + "text": "Create the first exception", + "href": "" + }, + { + "tagName": "button", + "text": "=", + "href": "" + }, + { + "tagName": "button", + "text": "#", + "href": "" + }, + { + "tagName": "button", + "text": "Filters", + "href": "" + }, + { + "tagName": "button", + "text": "+ New Exception", + "href": "" + }, + { + "tagName": "button", + "text": "Title", + "href": "" + }, + { + "tagName": "button", + "text": "Severity", + "href": "" + }, + { + "tagName": "button", + "text": "Expires", + "href": "" + }, + { + "tagName": "button", + "text": "Updated v", + "href": "" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/policy/risk-budget", + "targetUrl": "https://stella-ops.local/ops/policy/risk-budget?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/policy/risk-budget?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Policy Governance - Stella Ops QA 1773540847164", + "headings": [ + "Policy Decisioning Studio", + "Risk Budget Overview" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Reset view", + "href": "/ops/policy/overview" + }, + { + "tagName": "a", + "text": "Overview", + "href": "/ops/policy/overview" + }, + { + "tagName": "a", + "text": "Packs", + "href": "/ops/policy/packs" + }, + { + "tagName": "a", + "text": "Governance", + "href": "/ops/policy/governance" + }, + { + "tagName": "a", + "text": "Simulation", + "href": "/ops/policy/simulation" + }, + { + "tagName": "a", + "text": "VEX & Exceptions", + "href": "/ops/policy/vex" + }, + { + "tagName": "a", + "text": "Release Gates", + "href": "/ops/policy/gates" + }, + { + "tagName": "a", + "text": "Audit", + "href": "/ops/policy/audit" + }, + { + "tagName": "a", + "text": "Configure Budget", + "href": "/ops/policy/budget/config" + }, + { + "tagName": "button", + "text": "Acknowledge", + "href": "" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/policy/trust-weights", + "targetUrl": "https://stella-ops.local/ops/policy/trust-weights?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/policy/trust-weights?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Policy Governance - Stella Ops QA 1773540847164", + "headings": [ + "Policy Decisioning Studio", + "Trust Weight Configuration" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Reset view", + "href": "/ops/policy/overview" + }, + { + "tagName": "a", + "text": "Overview", + "href": "/ops/policy/overview" + }, + { + "tagName": "a", + "text": "Packs", + "href": "/ops/policy/packs" + }, + { + "tagName": "a", + "text": "Governance", + "href": "/ops/policy/governance" + }, + { + "tagName": "a", + "text": "Simulation", + "href": "/ops/policy/simulation" + }, + { + "tagName": "a", + "text": "VEX & Exceptions", + "href": "/ops/policy/vex" + }, + { + "tagName": "a", + "text": "Release Gates", + "href": "/ops/policy/gates" + }, + { + "tagName": "a", + "text": "Audit", + "href": "/ops/policy/audit" + }, + { + "tagName": "button", + "text": "Add Weight", + "href": "" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/policy/staleness", + "targetUrl": "https://stella-ops.local/ops/policy/staleness?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/policy/staleness?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Policy Governance - Stella Ops QA 1773540847164", + "headings": [ + "Policy Decisioning Studio", + "Staleness Configuration" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Reset view", + "href": "/ops/policy/overview" + }, + { + "tagName": "a", + "text": "Overview", + "href": "/ops/policy/overview" + }, + { + "tagName": "a", + "text": "Packs", + "href": "/ops/policy/packs" + }, + { + "tagName": "a", + "text": "Governance", + "href": "/ops/policy/governance" + }, + { + "tagName": "a", + "text": "Simulation", + "href": "/ops/policy/simulation" + }, + { + "tagName": "a", + "text": "VEX & Exceptions", + "href": "/ops/policy/vex" + }, + { + "tagName": "a", + "text": "Release Gates", + "href": "/ops/policy/gates" + }, + { + "tagName": "a", + "text": "Audit", + "href": "/ops/policy/audit" + }, + { + "tagName": "button", + "text": "SBOM", + "href": "" + }, + { + "tagName": "button", + "text": "Vulnerability Data", + "href": "" + }, + { + "tagName": "button", + "text": "VEX Statements", + "href": "" + }, + { + "tagName": "button", + "text": "Policy", + "href": "" + }, + { + "tagName": "button", + "text": "Attestation", + "href": "" + }, + { + "tagName": "button", + "text": "Scan Result", + "href": "" + }, + { + "tagName": "button", + "text": "Save Changes", + "href": "" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/policy/sealed-mode", + "targetUrl": "https://stella-ops.local/ops/policy/sealed-mode?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/policy/sealed-mode?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Policy Governance - Stella Ops QA 1773540847164", + "headings": [ + "Policy Decisioning Studio", + "Sealed Mode Control" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Reset view", + "href": "/ops/policy/overview" + }, + { + "tagName": "a", + "text": "Overview", + "href": "/ops/policy/overview" + }, + { + "tagName": "a", + "text": "Packs", + "href": "/ops/policy/packs" + }, + { + "tagName": "a", + "text": "Governance", + "href": "/ops/policy/governance" + }, + { + "tagName": "a", + "text": "Simulation", + "href": "/ops/policy/simulation" + }, + { + "tagName": "a", + "text": "VEX & Exceptions", + "href": "/ops/policy/vex" + }, + { + "tagName": "a", + "text": "Release Gates", + "href": "/ops/policy/gates" + }, + { + "tagName": "a", + "text": "Audit", + "href": "/ops/policy/audit" + }, + { + "tagName": "button", + "text": "Seal", + "href": "" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/policy/profiles", + "targetUrl": "https://stella-ops.local/ops/policy/profiles?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/policy/profiles?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Policy Governance - Stella Ops QA 1773540847164", + "headings": [ + "Policy Decisioning Studio", + "Risk Profiles" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Reset view", + "href": "/ops/policy/overview" + }, + { + "tagName": "a", + "text": "Overview", + "href": "/ops/policy/overview" + }, + { + "tagName": "a", + "text": "Packs", + "href": "/ops/policy/packs" + }, + { + "tagName": "a", + "text": "Governance", + "href": "/ops/policy/governance" + }, + { + "tagName": "a", + "text": "Simulation", + "href": "/ops/policy/simulation" + }, + { + "tagName": "a", + "text": "VEX & Exceptions", + "href": "/ops/policy/vex" + }, + { + "tagName": "a", + "text": "Release Gates", + "href": "/ops/policy/gates" + }, + { + "tagName": "a", + "text": "Audit", + "href": "/ops/policy/audit" + }, + { + "tagName": "button", + "text": "All", + "href": "" + }, + { + "tagName": "button", + "text": "Active", + "href": "" + }, + { + "tagName": "button", + "text": "Draft", + "href": "" + }, + { + "tagName": "button", + "text": "Deprecated", + "href": "" + }, + { + "tagName": "a", + "text": "New Profile", + "href": "/ops/policy/profiles/new" + }, + { + "tagName": "a", + "text": "Create Profile", + "href": "/ops/policy/profiles/new" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/policy/validator", + "targetUrl": "https://stella-ops.local/ops/policy/validator?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/policy/validator?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Policy Governance - Stella Ops QA 1773540847164", + "headings": [ + "Policy Decisioning Studio", + "Policy Validator" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Reset view", + "href": "/ops/policy/overview" + }, + { + "tagName": "a", + "text": "Overview", + "href": "/ops/policy/overview" + }, + { + "tagName": "a", + "text": "Packs", + "href": "/ops/policy/packs" + }, + { + "tagName": "a", + "text": "Governance", + "href": "/ops/policy/governance" + }, + { + "tagName": "a", + "text": "Simulation", + "href": "/ops/policy/simulation" + }, + { + "tagName": "a", + "text": "VEX & Exceptions", + "href": "/ops/policy/vex" + }, + { + "tagName": "a", + "text": "Release Gates", + "href": "/ops/policy/gates" + }, + { + "tagName": "a", + "text": "Audit", + "href": "/ops/policy/audit" + }, + { + "tagName": "button", + "text": "Load Sample", + "href": "" + }, + { + "tagName": "button", + "text": "Validate", + "href": "" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/policy/audit", + "targetUrl": "https://stella-ops.local/ops/policy/audit?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/policy/audit/policy?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Policy Audit - Stella Ops QA 1773540847164", + "headings": [ + "Policy Decisioning Studio", + "Policy and VEX audit now share the same owner shell", + "Policy Audit Events" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Reset view", + "href": "/ops/policy/overview" + }, + { + "tagName": "a", + "text": "Overview", + "href": "/ops/policy/overview" + }, + { + "tagName": "a", + "text": "Packs", + "href": "/ops/policy/packs" + }, + { + "tagName": "a", + "text": "Governance", + "href": "/ops/policy/governance" + }, + { + "tagName": "a", + "text": "Simulation", + "href": "/ops/policy/simulation" + }, + { + "tagName": "a", + "text": "VEX & Exceptions", + "href": "/ops/policy/vex" + }, + { + "tagName": "a", + "text": "Release Gates", + "href": "/ops/policy/gates" + }, + { + "tagName": "a", + "text": "Audit", + "href": "/ops/policy/audit" + }, + { + "tagName": "a", + "text": "Policy Audit", + "href": "/ops/policy/audit/policy" + }, + { + "tagName": "a", + "text": "VEX Audit", + "href": "/ops/policy/audit/vex" + }, + { + "tagName": "a", + "text": "Unified Log", + "href": "/ops/policy/audit/log" + }, + { + "tagName": "a", + "text": "Audit Log", + "href": "/evidence/audit-log" + }, + { + "tagName": "button", + "text": "All", + "href": "" + }, + { + "tagName": "button", + "text": "Promotions", + "href": "" + }, + { + "tagName": "button", + "text": "Approvals", + "href": "" + }, + { + "tagName": "button", + "text": "Rejections", + "href": "" + }, + { + "tagName": "button", + "text": "Simulations", + "href": "" + }, + { + "tagName": "button", + "text": "Load More", + "href": "" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/platform-setup", + "targetUrl": "https://stella-ops.local/ops/platform-setup?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/platform-setup?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Platform Setup - Stella Ops QA 1773540847164", + "headings": [ + "Platform Setup" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "button", + "text": "+", + "href": "" + }, + { + "tagName": "button", + "text": "−", + "href": "" + }, + { + "tagName": "button", + "text": "Reset", + "href": "" + }, + { + "tagName": "a", + "text": "Open", + "href": "/ops/platform-setup/topology-wizard" + }, + { + "tagName": "a", + "text": "Open", + "href": "/ops/platform-setup/regions-environments" + }, + { + "tagName": "a", + "text": "Open", + "href": "/ops/platform-setup/promotion-paths" + }, + { + "tagName": "a", + "text": "Open", + "href": "/ops/platform-setup/workflows-gates" + }, + { + "tagName": "a", + "text": "Open", + "href": "/ops/platform-setup/gate-profiles" + }, + { + "tagName": "a", + "text": "Open", + "href": "/ops/platform-setup/release-templates" + }, + { + "tagName": "a", + "text": "Open", + "href": "/ops/platform-setup/policy-bindings" + }, + { + "tagName": "a", + "text": "Open", + "href": "/ops/platform-setup/defaults-guardrails" + }, + { + "tagName": "a", + "text": "Open", + "href": "/setup/trust-signing" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/platform-setup/regions-environments", + "targetUrl": "https://stella-ops.local/ops/platform-setup/regions-environments?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/platform-setup/regions-environments?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Setup Regions & Environments - Stella Ops QA 1773540847164", + "headings": [ + "Regions & Environments", + "Region: us-east", + "Region: eu-west" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "button", + "text": "+ Add Region", + "href": "" + }, + { + "tagName": "button", + "text": "+ Add Environment", + "href": "" + }, + { + "tagName": "button", + "text": "Import", + "href": "" + }, + { + "tagName": "button", + "text": "Export", + "href": "" + }, + { + "tagName": "a", + "text": "Open Topology Environment Posture", + "href": "/setup/topology/environments" + }, + { + "tagName": "a", + "text": "Open Security Policy Baseline", + "href": "/security/overview" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/platform-setup/promotion-paths", + "targetUrl": "https://stella-ops.local/ops/platform-setup/promotion-paths?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/platform-setup/promotion-paths?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Setup Promotion Paths - Stella Ops QA 1773540847164", + "headings": [ + "Promotion Paths", + "Path Map", + "Rules" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Open Release Runs", + "href": "/releases/runs" + }, + { + "tagName": "a", + "text": "Open Topology Path View", + "href": "/setup/topology/promotion-graph" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/platform-setup/workflows-gates", + "targetUrl": "https://stella-ops.local/ops/platform-setup/workflows-gates?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/platform-setup/workflows-gates?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Setup Workflows & Gates - Stella Ops QA 1773540847164", + "headings": [ + "Workflows & Gates", + "Workflow Catalog", + "Gate Profiles" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Open Security Policy Baseline", + "href": "/security/overview" + }, + { + "tagName": "a", + "text": "Open Topology Workflow Inventory", + "href": "/setup/topology/workflows" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/platform-setup/release-templates", + "targetUrl": "https://stella-ops.local/ops/platform-setup/release-templates?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/platform-setup/release-templates?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Release Templates - Stella Ops QA 1773540847164", + "headings": [ + "Release Templates" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Create Release from Template", + "href": "/releases/versions/new" + }, + { + "tagName": "a", + "text": "View Evidence Export Formats", + "href": "/evidence/exports" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/platform-setup/policy-bindings", + "targetUrl": "https://stella-ops.local/ops/platform-setup/policy-bindings?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/platform-setup/policy-bindings?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Policy Bindings - Stella Ops QA 1773540847164", + "headings": [ + "Feed Policy", + "Freshness SLA", + "Promotion Behavior" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Manage Feed Connectors", + "href": "/platform/integrations/feeds" + }, + { + "tagName": "a", + "text": "Open Feeds & Airgap Operations", + "href": "/platform/ops/feeds-airgap" + }, + { + "tagName": "a", + "text": "Open Advisories & VEX", + "href": "/security/advisories-vex" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/platform-setup/gate-profiles", + "targetUrl": "https://stella-ops.local/ops/platform-setup/gate-profiles?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/platform-setup/gate-profiles?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Gate Profiles - Stella Ops QA 1773540847164", + "headings": [ + "Gate Profiles", + "Profiles", + "Profile Selection Rules" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Open Workflows & Gates", + "href": "/ops/platform-setup/workflows-gates" + }, + { + "tagName": "a", + "text": "Open Defaults & Guardrails", + "href": "/ops/platform-setup/defaults-guardrails" + }, + { + "tagName": "a", + "text": "Open Security Baseline", + "href": "/security/overview" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/platform-setup/defaults-guardrails", + "targetUrl": "https://stella-ops.local/ops/platform-setup/defaults-guardrails?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/ops/platform-setup/defaults-guardrails?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Defaults & Guardrails - Stella Ops QA 1773540847164", + "headings": [ + "Defaults & Guardrails", + "Default Controls", + "Global Behaviors" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Open Gate Profiles", + "href": "/ops/platform-setup/gate-profiles" + }, + { + "tagName": "a", + "text": "Open Feed Policy", + "href": "/ops/platform-setup/policy-bindings" + }, + { + "tagName": "a", + "text": "Open Data Integrity", + "href": "/platform/ops/data-integrity" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/ops/platform-setup/trust-signing", + "targetUrl": "https://stella-ops.local/ops/platform-setup/trust-signing?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/setup/trust-signing?tenant=demo-prod®ions=us-east&environments=prod-us-east", + "title": "Trust & Signing - Stella Ops QA 1773540847164", + "headings": [ + "Trust Management", + "Trust & Signing Overview" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "button", + "text": "Refresh", + "href": "" + }, + { + "tagName": "a", + "text": "Overview", + "href": "/setup/trust-signing/overview?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Signing Keys", + "href": "/setup/trust-signing/keys?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Trusted Issuers", + "href": "/setup/trust-signing/issuers?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Certificates", + "href": "/setup/trust-signing/certificates?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Watchlist", + "href": "/setup/trust-signing/watchlist?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Audit Log", + "href": "/setup/trust-signing/audit?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Air-Gap", + "href": "/setup/trust-signing/airgap?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Incidents", + "href": "/setup/trust-signing/incidents?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Analytics", + "href": "/setup/trust-signing/analytics?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Open key inventory", + "href": "/setup/trust-signing/keys" + }, + { + "tagName": "a", + "text": "Open issuer inventory", + "href": "/setup/trust-signing/issuers" + }, + { + "tagName": "a", + "text": "Open certificate inventory", + "href": "/setup/trust-signing/certificates" + }, + { + "tagName": "a", + "text": "Open watchlist", + "href": "/setup/trust-signing/watchlist" + }, + { + "tagName": "a", + "text": "Open trust analytics", + "href": "/setup/trust-signing/analytics" + }, + { + "tagName": "a", + "text": "Open evidence overview", + "href": "/evidence/overview" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/setup", + "targetUrl": "https://stella-ops.local/setup", + "finalUrl": "https://stella-ops.local/setup?tenant=demo-prod®ions=us-east&environments=prod-us-east", + "title": "Setup Overview - Stella Ops QA 1773540847164", + "headings": [ + "Setup", + "Topology Overview", + "Topology Map", + "Identity & Access", + "Tenant & Branding", + "Notifications", + "Usage & Limits", + "System Settings", + "Operational Drilldowns", + "First-Time Setup Path", + "Quick Actions", + "Suggested Next Steps" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "TOPOTopology OverviewRegions, environments, targets, hosts, and agent fleet posture.", + "href": "/setup/topology/overview" + }, + { + "tagName": "a", + "text": "MAPTopology MapEnvironment and target map with run and evidence correlation.", + "href": "/setup/topology/map" + }, + { + "tagName": "a", + "text": "IAMIdentity & AccessUsers, roles, clients, tokens, and scope management.", + "href": "/setup/identity-access" + }, + { + "tagName": "a", + "text": "UITenant & BrandingTenant configuration, logo, color scheme, and white-label settings.", + "href": "/setup/tenant-branding" + }, + { + "tagName": "a", + "text": "NTFNotificationsNotification rules, channels, and delivery templates.", + "href": "/setup/notifications" + }, + { + "tagName": "a", + "text": "QTAUsage & LimitsSubscription usage, quotas, and resource ceilings.", + "href": "/setup/usage" + }, + { + "tagName": "a", + "text": "SYSSystem SettingsSystem configuration, diagnostics, offline settings, and security data.", + "href": "/setup/system" + }, + { + "tagName": "a", + "text": "Quotas & Limits", + "href": "/ops/operations/quotas" + }, + { + "tagName": "a", + "text": "System Health", + "href": "/ops/operations/system-health" + }, + { + "tagName": "a", + "text": "Audit Log", + "href": "/evidence/audit-log" + }, + { + "tagName": "a", + "text": "Start guided setup", + "href": "/setup-wizard/wizard" + }, + { + "tagName": "a", + "text": "1. Identity & Access", + "href": "/setup/identity-access" + }, + { + "tagName": "a", + "text": "2. Trust & Signing", + "href": "/setup/trust-signing" + }, + { + "tagName": "a", + "text": "3. Integrations", + "href": "/setup/integrations" + }, + { + "tagName": "a", + "text": "4. Topology", + "href": "/setup/topology/overview" + }, + { + "tagName": "a", + "text": "5. Notifications", + "href": "/setup/notifications" + }, + { + "tagName": "a", + "text": "Add Target", + "href": "/setup/topology/targets" + }, + { + "tagName": "a", + "text": "Configure Integrations", + "href": "/setup/integrations" + }, + { + "tagName": "a", + "text": "Review Access", + "href": "/setup/identity-access" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/setup/integrations", + "targetUrl": "https://stella-ops.local/setup/integrations", + "finalUrl": "https://stella-ops.local/setup/integrations?tenant=demo-prod®ions=us-east&environments=prod-us-east", + "title": "Integrations - Stella Ops QA 1773540847164", + "headings": [ + "Integrations", + "Suggested Setup Order", + "Recent Activity" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Hub", + "href": "/setup/integrations" + }, + { + "tagName": "a", + "text": "Registries", + "href": "/setup/integrations/registries" + }, + { + "tagName": "a", + "text": "SCM", + "href": "/setup/integrations/scm" + }, + { + "tagName": "a", + "text": "CI/CD", + "href": "/setup/integrations/ci" + }, + { + "tagName": "a", + "text": "Runtimes / Hosts", + "href": "/setup/integrations/runtime-hosts" + }, + { + "tagName": "a", + "text": "Advisory & VEX", + "href": "/setup/integrations/advisory-vex-sources" + }, + { + "tagName": "a", + "text": "Secrets", + "href": "/setup/integrations/secrets" + }, + { + "tagName": "a", + "text": "Activity", + "href": "/setup/integrations/activity" + }, + { + "tagName": "a", + "text": "Registries", + "href": "/setup/integrations/registries" + }, + { + "tagName": "a", + "text": "Source Control", + "href": "/setup/integrations/scm" + }, + { + "tagName": "a", + "text": "CI/CD", + "href": "/setup/integrations/ci" + }, + { + "tagName": "a", + "text": "Advisory & VEX Sources", + "href": "/setup/integrations/advisory-vex-sources" + }, + { + "tagName": "a", + "text": "Secrets", + "href": "/setup/integrations/secrets" + }, + { + "tagName": "a", + "text": "Registries2", + "href": "/setup/integrations/registries" + }, + { + "tagName": "a", + "text": "SCM1", + "href": "/setup/integrations/scm" + }, + { + "tagName": "a", + "text": "CI/CD0", + "href": "/setup/integrations/ci" + }, + { + "tagName": "a", + "text": "Runtimes / Hosts0", + "href": "/setup/integrations/runtime-hosts" + }, + { + "tagName": "a", + "text": "Advisory Sources0", + "href": "/setup/integrations/advisory-vex-sources" + }, + { + "tagName": "a", + "text": "VEX Sources0", + "href": "/setup/integrations/advisory-vex-sources" + }, + { + "tagName": "a", + "text": "Secrets0", + "href": "/setup/integrations/secrets" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/setup/integrations/advisory-vex-sources", + "targetUrl": "https://stella-ops.local/setup/integrations/advisory-vex-sources", + "finalUrl": "https://stella-ops.local/setup/integrations/advisory-vex-sources?tenant=demo-prod®ions=us-east&environments=prod-us-east", + "title": "Advisory & VEX Sources - Stella Ops QA 1773540847164", + "headings": [ + "Integrations", + "Advisory & VEX Source Catalog", + "▶ Primary (4)", + "▶ Vendor (14)", + "▶ Distribution (10)", + "▶ Ecosystem (9)", + "▶ Cert (15)", + "▶ Csaf (3)", + "▶ Threat (4)", + "▶ Exploit (3)", + "▶ Container (2)", + "▶ Hardware (3)", + "▶ Ics (2)", + "▶ PackageManager (4)", + "▶ Mirror (1)" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Hub", + "href": "/setup/integrations" + }, + { + "tagName": "a", + "text": "Registries", + "href": "/setup/integrations/registries" + }, + { + "tagName": "a", + "text": "SCM", + "href": "/setup/integrations/scm" + }, + { + "tagName": "a", + "text": "CI/CD", + "href": "/setup/integrations/ci" + }, + { + "tagName": "a", + "text": "Runtimes / Hosts", + "href": "/setup/integrations/runtime-hosts" + }, + { + "tagName": "a", + "text": "Advisory & VEX", + "href": "/setup/integrations/advisory-vex-sources" + }, + { + "tagName": "a", + "text": "Secrets", + "href": "/setup/integrations/secrets" + }, + { + "tagName": "a", + "text": "Activity", + "href": "/setup/integrations/activity" + }, + { + "tagName": "button", + "text": "Check All", + "href": "" + }, + { + "tagName": "a", + "text": "Configure Mirror", + "href": "/setup/integrations/advisory-vex-sources/mirror?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Connect to Mirror", + "href": "/setup/integrations/advisory-vex-sources/mirror/client-setup?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "button", + "text": "Create Mirror Domain", + "href": "" + }, + { + "tagName": "button", + "text": "Enable All", + "href": "" + }, + { + "tagName": "button", + "text": "Disable All", + "href": "" + }, + { + "tagName": "button", + "text": "Check", + "href": "" + }, + { + "tagName": "button", + "text": "Check", + "href": "" + }, + { + "tagName": "button", + "text": "Check", + "href": "" + }, + { + "tagName": "button", + "text": "Check", + "href": "" + }, + { + "tagName": "button", + "text": "Enable All", + "href": "" + }, + { + "tagName": "button", + "text": "Disable All", + "href": "" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/setup/integrations/secrets", + "targetUrl": "https://stella-ops.local/setup/integrations/secrets", + "finalUrl": "https://stella-ops.local/setup/integrations/secrets?tenant=demo-prod®ions=us-east&environments=prod-us-east", + "title": "Secrets - Stella Ops QA 1773540847164", + "headings": [ + "Integrations", + "RepoSource Integrations" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Hub", + "href": "/setup/integrations" + }, + { + "tagName": "a", + "text": "Registries", + "href": "/setup/integrations/registries" + }, + { + "tagName": "a", + "text": "SCM", + "href": "/setup/integrations/scm" + }, + { + "tagName": "a", + "text": "CI/CD", + "href": "/setup/integrations/ci" + }, + { + "tagName": "a", + "text": "Runtimes / Hosts", + "href": "/setup/integrations/runtime-hosts" + }, + { + "tagName": "a", + "text": "Advisory & VEX", + "href": "/setup/integrations/advisory-vex-sources" + }, + { + "tagName": "a", + "text": "Secrets", + "href": "/setup/integrations/secrets" + }, + { + "tagName": "a", + "text": "Activity", + "href": "/setup/integrations/activity" + }, + { + "tagName": "button", + "text": "+ Add Integration", + "href": "" + }, + { + "tagName": "button", + "text": "Open add integration hub", + "href": "" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/setup/identity-access", + "targetUrl": "https://stella-ops.local/setup/identity-access", + "finalUrl": "https://stella-ops.local/setup/identity-access?tenant=demo-prod®ions=us-east&environments=prod-us-east", + "title": "Identity & Access - Stella Ops QA 1773540847164", + "headings": [ + "Identity & Access", + "Users" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "button", + "text": "Users", + "href": "" + }, + { + "tagName": "button", + "text": "Roles", + "href": "" + }, + { + "tagName": "button", + "text": "OAuth Clients", + "href": "" + }, + { + "tagName": "button", + "text": "API Tokens", + "href": "" + }, + { + "tagName": "button", + "text": "Tenants", + "href": "" + }, + { + "tagName": "button", + "text": "Add User", + "href": "" + }, + { + "tagName": "button", + "text": "Edit", + "href": "" + }, + { + "tagName": "button", + "text": "Disable", + "href": "" + }, + { + "tagName": "button", + "text": "Edit", + "href": "" + }, + { + "tagName": "button", + "text": "Disable", + "href": "" + }, + { + "tagName": "button", + "text": "Edit", + "href": "" + }, + { + "tagName": "button", + "text": "Disable", + "href": "" + }, + { + "tagName": "button", + "text": "Edit", + "href": "" + }, + { + "tagName": "button", + "text": "Disable", + "href": "" + }, + { + "tagName": "button", + "text": "Edit", + "href": "" + }, + { + "tagName": "button", + "text": "Disable", + "href": "" + }, + { + "tagName": "button", + "text": "Edit", + "href": "" + }, + { + "tagName": "button", + "text": "Disable", + "href": "" + }, + { + "tagName": "button", + "text": "Edit", + "href": "" + }, + { + "tagName": "button", + "text": "Disable", + "href": "" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/setup/tenant-branding", + "targetUrl": "https://stella-ops.local/setup/tenant-branding", + "finalUrl": "https://stella-ops.local/setup/tenant-branding?tenant=demo-prod®ions=us-east&environments=prod-us-east", + "title": "Tenant & Branding - Stella Ops QA 1773540847164", + "headings": [ + "Branding Configuration", + "General Settings", + "Logo & Favicon", + "Theme Tokens", + "Preview" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "button", + "text": "Refresh", + "href": "" + }, + { + "tagName": "button", + "text": "Apply Changes", + "href": "" + }, + { + "tagName": "button", + "text": "Upload Logo", + "href": "" + }, + { + "tagName": "button", + "text": "Upload Favicon", + "href": "" + }, + { + "tagName": "button", + "text": "Add Token", + "href": "" + }, + { + "tagName": "button", + "text": "Sample Button", + "href": "" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/setup/notifications", + "targetUrl": "https://stella-ops.local/setup/notifications", + "finalUrl": "https://stella-ops.local/setup/notifications?tenant=demo-prod®ions=us-east&environments=prod-us-east", + "title": "Notifications - Stella Ops QA 1773540847164", + "headings": [ + "Notification Administration" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "button", + "text": "Refresh Stats", + "href": "" + }, + { + "tagName": "a", + "text": "Open operator console", + "href": "/ops/operations/notifications" + }, + { + "tagName": "a", + "text": "Re-run notifications setup", + "href": "/setup-wizard/wizard?step=notifications&mode=reconfigure" + }, + { + "tagName": "a", + "text": "RRules", + "href": "/setup/notifications/rules?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "CChannels", + "href": "/setup/notifications/channels?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "TTemplates", + "href": "/setup/notifications/templates?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "DDelivery", + "href": "/setup/notifications/delivery?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "SSimulator", + "href": "/setup/notifications/simulator?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "*Config", + "href": "/setup/notifications/config?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "button", + "text": "Clear Filters", + "href": "" + }, + { + "tagName": "button", + "text": "Create Rule", + "href": "" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/setup/usage", + "targetUrl": "https://stella-ops.local/setup/usage", + "finalUrl": "https://stella-ops.local/setup/usage?tenant=demo-prod®ions=us-east&environments=prod-us-east", + "title": "Usage & Limits - Stella Ops QA 1773540847164", + "headings": [ + "Usage & Limits", + "Quota Configuration" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Configure Quotas", + "href": "/ops/operations/quotas" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/setup/system", + "targetUrl": "https://stella-ops.local/setup/system", + "finalUrl": "https://stella-ops.local/setup/system?tenant=demo-prod®ions=us-east&environments=prod-us-east", + "title": "System Settings - Stella Ops QA 1773540847164", + "headings": [ + "System Settings", + "Live Health", + "Doctor", + "SLO Monitoring", + "Background Jobs" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "View Details", + "href": "/ops/operations/system-health" + }, + { + "tagName": "a", + "text": "Run Doctor", + "href": "/ops/operations/doctor" + }, + { + "tagName": "a", + "text": "View SLOs", + "href": "/ops/operations/health-slo" + }, + { + "tagName": "a", + "text": "View Jobs", + "href": "/ops/operations/jobs-queues" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/setup/trust-signing", + "targetUrl": "https://stella-ops.local/setup/trust-signing", + "finalUrl": "https://stella-ops.local/setup/trust-signing?tenant=demo-prod®ions=us-east&environments=prod-us-east", + "title": "Trust & Signing - Stella Ops QA 1773540847164", + "headings": [ + "Trust Management", + "Trust & Signing Overview" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "button", + "text": "Refresh", + "href": "" + }, + { + "tagName": "a", + "text": "Overview", + "href": "/setup/trust-signing/overview?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Signing Keys", + "href": "/setup/trust-signing/keys?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Trusted Issuers", + "href": "/setup/trust-signing/issuers?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Certificates", + "href": "/setup/trust-signing/certificates?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Watchlist", + "href": "/setup/trust-signing/watchlist?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Audit Log", + "href": "/setup/trust-signing/audit?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Air-Gap", + "href": "/setup/trust-signing/airgap?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Incidents", + "href": "/setup/trust-signing/incidents?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Analytics", + "href": "/setup/trust-signing/analytics?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Open key inventory", + "href": "/setup/trust-signing/keys" + }, + { + "tagName": "a", + "text": "Open issuer inventory", + "href": "/setup/trust-signing/issuers" + }, + { + "tagName": "a", + "text": "Open certificate inventory", + "href": "/setup/trust-signing/certificates" + }, + { + "tagName": "a", + "text": "Open watchlist", + "href": "/setup/trust-signing/watchlist" + }, + { + "tagName": "a", + "text": "Open trust analytics", + "href": "/setup/trust-signing/analytics" + }, + { + "tagName": "a", + "text": "Open evidence overview", + "href": "/evidence/overview" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/setup/topology", + "targetUrl": "https://stella-ops.local/setup/topology", + "finalUrl": "https://stella-ops.local/setup/topology/overview?tenant=demo-prod®ions=us-east&environments=prod-us-east", + "title": "Topology Overview - Stella Ops QA 1773540847164", + "headings": [ + "Topology", + "Regions", + "Environment Health", + "Agents & Drift", + "Promotion Posture", + "Top Hotspots" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Overview", + "href": "/setup/topology/overview?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Map", + "href": "/setup/topology/map?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Regions & Environments", + "href": "/setup/topology/regions?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Targets", + "href": "/setup/topology/targets?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Hosts", + "href": "/setup/topology/hosts?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Agents", + "href": "/setup/topology/agents?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Promotion Graph", + "href": "/setup/topology/promotion-graph?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Workflows", + "href": "/setup/topology/workflows?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Gate Profiles", + "href": "/setup/topology/gate-profiles?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Connectivity", + "href": "/setup/topology/connectivity?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Runtime Drift", + "href": "/setup/topology/runtime-drift?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "button", + "text": "Go", + "href": "" + }, + { + "tagName": "a", + "text": "Open Regions & Environments", + "href": "/setup/topology/regions?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Open Environment Inventory", + "href": "/setup/topology/environments?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Open Agents", + "href": "/setup/topology/agents?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Open Promotion Paths", + "href": "/setup/topology/promotion-graph?tenant=demo-prod®ions=us-east&environments=prod-us-east" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/setup/topology/overview", + "targetUrl": "https://stella-ops.local/setup/topology/overview", + "finalUrl": "https://stella-ops.local/setup/topology/overview?tenant=demo-prod®ions=us-east&environments=prod-us-east", + "title": "Topology Overview - Stella Ops QA 1773540847164", + "headings": [ + "Topology", + "Regions", + "Environment Health", + "Agents & Drift", + "Promotion Posture", + "Top Hotspots" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Overview", + "href": "/setup/topology/overview?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Map", + "href": "/setup/topology/map?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Regions & Environments", + "href": "/setup/topology/regions?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Targets", + "href": "/setup/topology/targets?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Hosts", + "href": "/setup/topology/hosts?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Agents", + "href": "/setup/topology/agents?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Promotion Graph", + "href": "/setup/topology/promotion-graph?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Workflows", + "href": "/setup/topology/workflows?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Gate Profiles", + "href": "/setup/topology/gate-profiles?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Connectivity", + "href": "/setup/topology/connectivity?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Runtime Drift", + "href": "/setup/topology/runtime-drift?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "button", + "text": "Go", + "href": "" + }, + { + "tagName": "a", + "text": "Open Regions & Environments", + "href": "/setup/topology/regions?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Open Environment Inventory", + "href": "/setup/topology/environments?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Open Agents", + "href": "/setup/topology/agents?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Open Promotion Paths", + "href": "/setup/topology/promotion-graph?tenant=demo-prod®ions=us-east&environments=prod-us-east" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/setup/topology/map", + "targetUrl": "https://stella-ops.local/setup/topology/map", + "finalUrl": "https://stella-ops.local/setup/topology/map?tenant=demo-prod®ions=us-east&environments=prod-us-east", + "title": "Environment & Target Map - Stella Ops QA 1773540847164", + "headings": [ + "Topology", + "Environment and Target Map" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Overview", + "href": "/setup/topology/overview?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Map", + "href": "/setup/topology/map?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Regions & Environments", + "href": "/setup/topology/regions?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Targets", + "href": "/setup/topology/targets?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Hosts", + "href": "/setup/topology/hosts?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Agents", + "href": "/setup/topology/agents?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Promotion Graph", + "href": "/setup/topology/promotion-graph?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Workflows", + "href": "/setup/topology/workflows?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Gate Profiles", + "href": "/setup/topology/gate-profiles?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Connectivity", + "href": "/setup/topology/connectivity?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Runtime Drift", + "href": "/setup/topology/runtime-drift?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "button", + "text": "+", + "href": "" + }, + { + "tagName": "button", + "text": "−", + "href": "" + }, + { + "tagName": "button", + "text": "Reset", + "href": "" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true, + "retryUsed": true, + "retryPassed": true, + "initialFailure": { + "consoleErrors": [ + "Failed to load resource: the server responded with a status of 503 ()", + "Failed to load resource: the server responded with a status of 503 ()" + ], + "requestFailures": [], + "responseErrors": [ + { + "status": 503, + "method": "GET", + "url": "https://stella-ops.local/api/v1/doctor/scheduler/trends/categories/platform?from=2025-03-20T23:47:53.513Z&to=2026-03-15T23:47:53.513Z" + }, + { + "status": 503, + "method": "GET", + "url": "https://stella-ops.local/api/v1/doctor/scheduler/trends/categories/security?from=2025-03-20T23:47:53.513Z&to=2026-03-15T23:47:53.513Z" + } + ], + "problemTexts": [], + "expectationFailures": [] + } + }, + { + "routePath": "/setup/topology/regions", + "targetUrl": "https://stella-ops.local/setup/topology/regions", + "finalUrl": "https://stella-ops.local/setup/topology/regions?tenant=demo-prod®ions=us-east&environments=prod-us-east", + "title": "Regions & Environments - Stella Ops QA 1773540847164", + "headings": [ + "Topology", + "Regions & Environments", + "Regions", + "Environments · US East", + "Environment Signals" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Overview", + "href": "/setup/topology/overview?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Map", + "href": "/setup/topology/map?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Regions & Environments", + "href": "/setup/topology/regions?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Targets", + "href": "/setup/topology/targets?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Hosts", + "href": "/setup/topology/hosts?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Agents", + "href": "/setup/topology/agents?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Promotion Graph", + "href": "/setup/topology/promotion-graph?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Workflows", + "href": "/setup/topology/workflows?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Gate Profiles", + "href": "/setup/topology/gate-profiles?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Connectivity", + "href": "/setup/topology/connectivity?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Runtime Drift", + "href": "/setup/topology/runtime-drift?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "button", + "text": "US Eastenv 5 · targets 1", + "href": "" + }, + { + "tagName": "a", + "text": "Open", + "href": "/setup/topology/environments/prod-us-east/posture?tenant=demo-prod®ions=us-east&environments=prod-us-east&environment=prod-us-east" + }, + { + "tagName": "a", + "text": "Open Environment", + "href": "/setup/topology/environments/prod-us-east/posture?tenant=demo-prod®ions=us-east&environments=prod-us-east&environment=prod-us-east" + }, + { + "tagName": "a", + "text": "Open Targets", + "href": "/setup/topology/targets?tenant=demo-prod®ions=us-east&environments=prod-us-east&environment=prod-us-east" + }, + { + "tagName": "a", + "text": "Open Agents", + "href": "/setup/topology/agents?tenant=demo-prod®ions=us-east&environments=prod-us-east&environment=prod-us-east" + }, + { + "tagName": "a", + "text": "Open Runs", + "href": "/releases/runs?tenant=demo-prod®ions=us-east&environments=prod-us-east&environment=prod-us-east" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/setup/topology/environments", + "targetUrl": "https://stella-ops.local/setup/topology/environments", + "finalUrl": "https://stella-ops.local/setup/topology/environments?tenant=demo-prod®ions=us-east&environments=prod-us-east", + "title": "Environments - Stella Ops QA 1773540847164", + "headings": [ + "Topology", + "Regions & Environments", + "Environment Inventory", + "Environment Signals" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Overview", + "href": "/setup/topology/overview?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Map", + "href": "/setup/topology/map?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Regions & Environments", + "href": "/setup/topology/regions?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Targets", + "href": "/setup/topology/targets?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Hosts", + "href": "/setup/topology/hosts?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Agents", + "href": "/setup/topology/agents?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Promotion Graph", + "href": "/setup/topology/promotion-graph?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Workflows", + "href": "/setup/topology/workflows?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Gate Profiles", + "href": "/setup/topology/gate-profiles?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Connectivity", + "href": "/setup/topology/connectivity?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Runtime Drift", + "href": "/setup/topology/runtime-drift?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Open", + "href": "/setup/topology/environments/prod-us-east/posture?tenant=demo-prod®ions=us-east&environments=prod-us-east&environment=prod-us-east" + }, + { + "tagName": "a", + "text": "Open Environment", + "href": "/setup/topology/environments/prod-us-east/posture?tenant=demo-prod®ions=us-east&environments=prod-us-east&environment=prod-us-east" + }, + { + "tagName": "a", + "text": "Open Targets", + "href": "/setup/topology/targets?tenant=demo-prod®ions=us-east&environments=prod-us-east&environment=prod-us-east" + }, + { + "tagName": "a", + "text": "Open Agents", + "href": "/setup/topology/agents?tenant=demo-prod®ions=us-east&environments=prod-us-east&environment=prod-us-east" + }, + { + "tagName": "a", + "text": "Open Runs", + "href": "/releases/runs?tenant=demo-prod®ions=us-east&environments=prod-us-east&environment=prod-us-east" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/setup/topology/targets", + "targetUrl": "https://stella-ops.local/setup/topology/targets", + "finalUrl": "https://stella-ops.local/setup/topology/targets?tenant=demo-prod®ions=us-east&environments=prod-us-east", + "title": "Targets - Stella Ops QA 1773540847164", + "headings": [ + "Topology", + "Targets", + "Selected Target" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Overview", + "href": "/setup/topology/overview?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Map", + "href": "/setup/topology/map?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Regions & Environments", + "href": "/setup/topology/regions?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Targets", + "href": "/setup/topology/targets?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Hosts", + "href": "/setup/topology/hosts?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Agents", + "href": "/setup/topology/agents?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Promotion Graph", + "href": "/setup/topology/promotion-graph?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Workflows", + "href": "/setup/topology/workflows?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Gate Profiles", + "href": "/setup/topology/gate-profiles?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Connectivity", + "href": "/setup/topology/connectivity?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Runtime Drift", + "href": "/setup/topology/runtime-drift?tenant=demo-prod®ions=us-east&environments=prod-us-east" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/setup/topology/hosts", + "targetUrl": "https://stella-ops.local/setup/topology/hosts", + "finalUrl": "https://stella-ops.local/setup/topology/hosts?tenant=demo-prod®ions=us-east&environments=prod-us-east", + "title": "Hosts - Stella Ops QA 1773540847164", + "headings": [ + "Topology", + "Hosts", + "Selected Host" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Overview", + "href": "/setup/topology/overview?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Map", + "href": "/setup/topology/map?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Regions & Environments", + "href": "/setup/topology/regions?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Targets", + "href": "/setup/topology/targets?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Hosts", + "href": "/setup/topology/hosts?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Agents", + "href": "/setup/topology/agents?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Promotion Graph", + "href": "/setup/topology/promotion-graph?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Workflows", + "href": "/setup/topology/workflows?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Gate Profiles", + "href": "/setup/topology/gate-profiles?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Connectivity", + "href": "/setup/topology/connectivity?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Runtime Drift", + "href": "/setup/topology/runtime-drift?tenant=demo-prod®ions=us-east&environments=prod-us-east" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/setup/topology/agents", + "targetUrl": "https://stella-ops.local/setup/topology/agents", + "finalUrl": "https://stella-ops.local/setup/topology/agents?tenant=demo-prod®ions=us-east&environments=prod-us-east", + "title": "Agent Fleet - Stella Ops QA 1773540847164", + "headings": [ + "Topology", + "Agents", + "Agent Groups", + "Selection" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Overview", + "href": "/setup/topology/overview?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Map", + "href": "/setup/topology/map?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Regions & Environments", + "href": "/setup/topology/regions?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Targets", + "href": "/setup/topology/targets?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Hosts", + "href": "/setup/topology/hosts?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Agents", + "href": "/setup/topology/agents?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Promotion Graph", + "href": "/setup/topology/promotion-graph?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Workflows", + "href": "/setup/topology/workflows?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Gate Profiles", + "href": "/setup/topology/gate-profiles?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Connectivity", + "href": "/setup/topology/connectivity?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Runtime Drift", + "href": "/setup/topology/runtime-drift?tenant=demo-prod®ions=us-east&environments=prod-us-east" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/setup/topology/connectivity", + "targetUrl": "https://stella-ops.local/setup/topology/connectivity", + "finalUrl": "https://stella-ops.local/setup/topology/connectivity?tenant=demo-prod®ions=us-east&environments=prod-us-east", + "title": "Connectivity - Stella Ops QA 1773540847164", + "headings": [ + "Topology", + "Topology Connectivity" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Overview", + "href": "/setup/topology/overview?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Map", + "href": "/setup/topology/map?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Regions & Environments", + "href": "/setup/topology/regions?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Targets", + "href": "/setup/topology/targets?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Hosts", + "href": "/setup/topology/hosts?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Agents", + "href": "/setup/topology/agents?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Promotion Graph", + "href": "/setup/topology/promotion-graph?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Workflows", + "href": "/setup/topology/workflows?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Gate Profiles", + "href": "/setup/topology/gate-profiles?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Connectivity", + "href": "/setup/topology/connectivity?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Runtime Drift", + "href": "/setup/topology/runtime-drift?tenant=demo-prod®ions=us-east&environments=prod-us-east" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/setup/topology/runtime-drift", + "targetUrl": "https://stella-ops.local/setup/topology/runtime-drift", + "finalUrl": "https://stella-ops.local/setup/topology/runtime-drift?tenant=demo-prod®ions=us-east&environments=prod-us-east", + "title": "Runtime Drift - Stella Ops QA 1773540847164", + "headings": [ + "Topology", + "Runtime Drift" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Overview", + "href": "/setup/topology/overview?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Map", + "href": "/setup/topology/map?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Regions & Environments", + "href": "/setup/topology/regions?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Targets", + "href": "/setup/topology/targets?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Hosts", + "href": "/setup/topology/hosts?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Agents", + "href": "/setup/topology/agents?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Promotion Graph", + "href": "/setup/topology/promotion-graph?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Workflows", + "href": "/setup/topology/workflows?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Gate Profiles", + "href": "/setup/topology/gate-profiles?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Connectivity", + "href": "/setup/topology/connectivity?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Runtime Drift", + "href": "/setup/topology/runtime-drift?tenant=demo-prod®ions=us-east&environments=prod-us-east" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/setup/topology/promotion-graph", + "targetUrl": "https://stella-ops.local/setup/topology/promotion-graph", + "finalUrl": "https://stella-ops.local/setup/topology/promotion-graph?tenant=demo-prod®ions=us-east&environments=prod-us-east", + "title": "Promotion Graph - Stella Ops QA 1773540847164", + "headings": [ + "Topology", + "Promotion Paths", + "Graph" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Overview", + "href": "/setup/topology/overview?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Map", + "href": "/setup/topology/map?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Regions & Environments", + "href": "/setup/topology/regions?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Targets", + "href": "/setup/topology/targets?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Hosts", + "href": "/setup/topology/hosts?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Agents", + "href": "/setup/topology/agents?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Promotion Graph", + "href": "/setup/topology/promotion-graph?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Workflows", + "href": "/setup/topology/workflows?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Gate Profiles", + "href": "/setup/topology/gate-profiles?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Connectivity", + "href": "/setup/topology/connectivity?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Runtime Drift", + "href": "/setup/topology/runtime-drift?tenant=demo-prod®ions=us-east&environments=prod-us-east" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/setup/topology/workflows", + "targetUrl": "https://stella-ops.local/setup/topology/workflows", + "finalUrl": "https://stella-ops.local/setup/topology/workflows?tenant=demo-prod®ions=us-east&environments=prod-us-east", + "title": "Workflows - Stella Ops QA 1773540847164", + "headings": [ + "Topology", + "Workflows" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Overview", + "href": "/setup/topology/overview?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Map", + "href": "/setup/topology/map?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Regions & Environments", + "href": "/setup/topology/regions?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Targets", + "href": "/setup/topology/targets?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Hosts", + "href": "/setup/topology/hosts?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Agents", + "href": "/setup/topology/agents?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Promotion Graph", + "href": "/setup/topology/promotion-graph?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Workflows", + "href": "/setup/topology/workflows?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Gate Profiles", + "href": "/setup/topology/gate-profiles?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Connectivity", + "href": "/setup/topology/connectivity?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Runtime Drift", + "href": "/setup/topology/runtime-drift?tenant=demo-prod®ions=us-east&environments=prod-us-east" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + }, + { + "routePath": "/setup/topology/gate-profiles", + "targetUrl": "https://stella-ops.local/setup/topology/gate-profiles", + "finalUrl": "https://stella-ops.local/setup/topology/gate-profiles?tenant=demo-prod®ions=us-east&environments=prod-us-east", + "title": "Gate Profiles - Stella Ops QA 1773540847164", + "headings": [ + "Topology", + "Gate Profiles" + ], + "problemTexts": [], + "visibleActions": [ + { + "tagName": "a", + "text": "Overview", + "href": "/setup/topology/overview?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Map", + "href": "/setup/topology/map?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Regions & Environments", + "href": "/setup/topology/regions?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Targets", + "href": "/setup/topology/targets?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Hosts", + "href": "/setup/topology/hosts?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Agents", + "href": "/setup/topology/agents?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Promotion Graph", + "href": "/setup/topology/promotion-graph?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Workflows", + "href": "/setup/topology/workflows?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Gate Profiles", + "href": "/setup/topology/gate-profiles?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Connectivity", + "href": "/setup/topology/connectivity?tenant=demo-prod®ions=us-east&environments=prod-us-east" + }, + { + "tagName": "a", + "text": "Runtime Drift", + "href": "/setup/topology/runtime-drift?tenant=demo-prod®ions=us-east&environments=prod-us-east" + } + ], + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [], + "finalPathMatches": true, + "expectationFailures": [], + "passed": true + } + ] +} diff --git a/src/Web/StellaOps.Web/output/playwright/live-mirror-operator-journey.auth.json b/src/Web/StellaOps.Web/output/playwright/live-mirror-operator-journey.auth.json new file mode 100644 index 000000000..cc277a15f --- /dev/null +++ b/src/Web/StellaOps.Web/output/playwright/live-mirror-operator-journey.auth.json @@ -0,0 +1,39 @@ +{ + "authenticatedAtUtc": "2026-03-15T22:08:06.096Z", + "baseUrl": "https://stella-ops.local", + "finalUrl": "https://stella-ops.local/mission-control/board?tenant=demo-prod®ions=apac,eu-west,us-east,us-west", + "title": "Dashboard - StellaOps", + "cookies": [], + "storage": { + "localStorageEntries": [ + [ + "stellaops.sidebar.preferences", + "{\"sidebarCollapsed\":false,\"collapsedGroups\":[],\"collapsedSections\":[]}" + ], + [ + "stellaops.theme", + "system" + ] + ], + "sessionStorageEntries": [ + [ + "stellaops.auth.session.full", + "{\"tokens\":{\"accessToken\":\"eyJhbGciOiJSUzI1NiIsImtpZCI6IlRBSU1GTlE3LUFOR1JMU0JOQVlfQ19OSEdPVERVWUo5UlNVVTRSRUQiLCJ0eXAiOiJhdCtqd3QifQ.eyJpc3MiOiJodHRwczovL2F1dGhvcml0eS5zdGVsbGEtb3BzLmxvY2FsLyIsImV4cCI6MTc3MzYxNDI4MiwiaWF0IjoxNzczNjEyNDgyLCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGVtYWlsIG9mZmxpbmVfYWNjZXNzIHVpLnJlYWQgdWkuYWRtaW4gdWkucHJlZmVyZW5jZXMucmVhZCB1aS5wcmVmZXJlbmNlcy53cml0ZSBhdXRob3JpdHk6dGVuYW50cy5yZWFkIGF1dGhvcml0eTp0ZW5hbnRzLndyaXRlIGF1dGhvcml0eTp1c2Vycy5yZWFkIGF1dGhvcml0eTp1c2Vycy53cml0ZSBhdXRob3JpdHk6cm9sZXMucmVhZCBhdXRob3JpdHk6cm9sZXMud3JpdGUgYXV0aG9yaXR5OmNsaWVudHMucmVhZCBhdXRob3JpdHk6Y2xpZW50cy53cml0ZSBhdXRob3JpdHk6dG9rZW5zLnJlYWQgYXV0aG9yaXR5OnRva2Vucy5yZXZva2UgYXV0aG9yaXR5OmJyYW5kaW5nLnJlYWQgYXV0aG9yaXR5OmJyYW5kaW5nLndyaXRlIGF1dGhvcml0eS5hdWRpdC5yZWFkIGdyYXBoOnJlYWQgc2JvbTpyZWFkIHNjYW5uZXI6cmVhZCBwb2xpY3k6cmVhZCBwb2xpY3k6c2ltdWxhdGUgcG9saWN5OmF1dGhvciBwb2xpY3k6cmV2aWV3IHBvbGljeTphcHByb3ZlIHBvbGljeTpydW4gcG9saWN5OmFjdGl2YXRlIHBvbGljeTphdWRpdCBwb2xpY3k6ZWRpdCBwb2xpY3k6b3BlcmF0ZSBwb2xpY3k6cHVibGlzaCBhaXJnYXA6c2VhbCBhaXJnYXA6c3RhdHVzOnJlYWQgb3JjaDpyZWFkIG9yY2g6b3BlcmF0ZSBvcmNoOnF1b3RhIGFuYWx5dGljcy5yZWFkIGFkdmlzb3J5OnJlYWQgYWR2aXNvcnktYWk6dmlldyBhZHZpc29yeS1haTpvcGVyYXRlIHZleDpyZWFkIHZleGh1YjpyZWFkIGV4Y2VwdGlvbnM6cmVhZCBleGNlcHRpb25zOmFwcHJvdmUgYW9jOnZlcmlmeSBmaW5kaW5nczpyZWFkIHJlbGVhc2U6cmVhZCByZWxlYXNlOndyaXRlIHJlbGVhc2U6cHVibGlzaCBzY2hlZHVsZXI6cmVhZCBzY2hlZHVsZXI6b3BlcmF0ZSBub3RpZnkudmlld2VyIG5vdGlmeS5vcGVyYXRvciBub3RpZnkuYWRtaW4gbm90aWZ5LmVzY2FsYXRlIGV2aWRlbmNlOnJlYWQgZXhwb3J0LnZpZXdlciBleHBvcnQub3BlcmF0b3IgZXhwb3J0LmFkbWluIHZ1bG46dmlldyB2dWxuOmludmVzdGlnYXRlIHZ1bG46b3BlcmF0ZSB2dWxuOmF1ZGl0IHBsYXRmb3JtLmNvbnRleHQucmVhZCBwbGF0Zm9ybS5jb250ZXh0LndyaXRlIGRvY3RvcjpydW4gZG9jdG9yOmFkbWluIG9wcy5oZWFsdGggaW50ZWdyYXRpb246cmVhZCBpbnRlZ3JhdGlvbjp3cml0ZSBpbnRlZ3JhdGlvbjpvcGVyYXRlIHBhY2tzLnJlYWQgcGFja3Mud3JpdGUgcGFja3MucnVuIHBhY2tzLmFwcHJvdmUgcmVnaXN0cnkuYWRtaW4gdGltZWxpbmU6cmVhZCB0aW1lbGluZTp3cml0ZSB0cnVzdDpyZWFkIHRydXN0OndyaXRlIHRydXN0OmFkbWluIHNpZ25lcjpyZWFkIHNpZ25lcjpzaWduIHNpZ25lcjpyb3RhdGUgc2lnbmVyOmFkbWluIiwianRpIjoiNzdiN2M5NmItM2U1Zi00NWJmLTg3MGQtNTFiMTYxNDJkOTE3Iiwic3ViIjoiYjA4NjM5NzQ1ZDY1NDkzNDhkODQzYWEzMTFjOTg5NTgiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJhZG1pbiIsIm5hbWUiOiJhZG1pbiIsInJvbGUiOiJhZG1pbiIsInN0ZWxsYW9wczp0ZW5hbnQiOiJkZW1vLXByb2QiLCJhdXRoX3RpbWUiOjE3NzM2MTI0ODIsIm9pX3Byc3QiOiJzdGVsbGEtb3BzLXVpIiwiY2xpZW50X2lkIjoic3RlbGxhLW9wcy11aSJ9.CGceu13UT2BYvijAWdktU8panREH8EgbxRU8P8MhHo3sClS_w3hh5GBK8W5eEAgiea9uLYmELoYLXpzD2SoyC3L8yxLSbBNfaZC0MeWOAnJqeRW7sBLRTXRDpQP5LZXreLDueWthyhCZeWpS_3Re0p5JUBuyO8mgyVdzDEIxVSNYbRXGl4J4-X5M0buPu3ZBrSN-ZdhLcXtY9d0EmcLYZTmr1ot6lqah3jX19EEYd-ZJUccZtl9Rh-MqthbdVVy2ENIL4C1bOD6pvm7hWHmCE5n5920AbldyIc1HaVmgLSi078pFaJxYLmcoLKj5_porPom2mxTRMztgOAYxa4FkRg\",\"tokenType\":\"Bearer\",\"refreshToken\":\"eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZDQkMtSFM1MTIiLCJraWQiOiJXUjVQSU4tVzI5S0lNTkpRTVBBMU9XS0VYN0tEWjZMV0dYTDdSS0dWIiwidHlwIjoib2lfcmVmdCtqd3QiLCJjdHkiOiJKV1QifQ.hfCFkMw8BK5n7DlL4bRBsA4K6qYu-RIrpX3jYVALH0_-hukVFKcIYvLz7rGOJfz3ZeixBjOImFl-xd2WZi2bs2jqZwp3VVCbbarwNsFTewx-RewJIT9mG7A2jghlS7tYcjIIxOh-NHx4yzk7VopKUF0cRyJMYrxK9v2ZLZUzBcklbJCymM54shY5vPRhig8H4da7Ity_DQi0t9FoT31hrEfwdINKJx_xnUGBTGniLwkd9fhMjSM6FxfvsBaaQx7gQdXvyOrJFZB7DWdK_ZIowSpOqK-Afnbn9x1zbvtWP_a3TAmNDcn30NRaJ09Yxi8bQrDcDJY5YlhVq0Wh-ghE8w.ouY97g_aVbeS_Z0VzBZFaA.2e-TIswwBWGIyqdREkN5piU9MmEDFWSvPPkBE7D7iELXFjyJzKCtcVIq1U-fRLsqLTGgSJOlOV1dsF76GXTonUAGl4M7g_iokhrID1oMJrZUnImGRpxjcv7moUt_9Pwwr_p1h1JNsw8DqM6ryldamPJR15RgJsgrSauwDbd3xgEsEEauP9PK5DcSilGNw4oiTMhXLmA37XkFRwfex0s0TswNfrCj2TZJBxQeBg-h75CugE7tzfwixPLoegUo97MblHY8NVWCRGHr6UFQM2ojf08W9ZwXWyynSY1WE4pU-uv6WhdPMuXZivoOJsng5TicCZYFbOKwC9IX5vWwCybjgGMViQ987HFimcMXvjBxOZrs25MPtHNHHrQjjtkTktydRx89r-3BacpLSYfEWF2JknACEgoEjsgSB4KHD0Ej3edQ1B8BcHTL6p1OZzZrJbWX4JNYC-Hk6G57vgQnB8-yqPjQEVhDsH9BJGDShUfR2KYXi_WSIwwcqPgkfbr_CopsYj6dx_4QVxZdDleCqhmYwQ-KfyK2dDFLbhw7PUEMbqbOweJXxYLQPYcmPGfoQMY6WT6aVBhpbxUxawgQYdsUc-Qj_e8mRTdToOBlpusTw27xN2mUChfRUvWqa6b6vXSUU2UKE98cQODyGsc_d3dYXhVOdGTZ8wRAQE7JrbpGhFpzIRhV5KwaeZ1sYZeSFXQh37CApVKbhVg2DdrT8jnyMnoh1zyLdU-iUSWRTS5ZLHVPweadSOIwdE05Q1lWFRW0j7bnnp37cb1MFqveH24a0GDbLeUEbKwIGhkRKvc-mCbEFuaYdU8KoUwbEQlPpqHHUh_KnMGEeSXbh0SFrqJsCEiM4aat69a7lLMMci38b_PPoLt-BFIZyxkmhbdOFpQoAre8PCOF70GgdyIRRbxS0dZZVxHaCXjgpU_3j71G12lC2quBU2HnRqD3r_PPj9IoZWKVXZbYdwasH9Alq1XLiz-kcMY_5B2DFg6FwSw89GWzhxX9F61SYMKxYu5J7M7I9bI6oVLrsqdmEDkwYrxEkkB-UkKjGP5EqpdyNZuV-dkI7JwBEn1POfpVdKcSoLhHQTJA_10t-42PwA6bK4BOYGYKdDs3RnnXXOsykZo-_9xLYtaJ4mbsR63uu9-gS1UjjTcjlnF_2UiXPWl0sKKMgUu-Ftl-pOWtu4GQZyfBDwryuoUhIo3Tq6zjAsBmk4UFHcpV7k_HDyu6K5r1aAGXpG-q8z6w7J0PqU3NXufq9S8b9ZFQuqSV3FKAmgJm5QUyOvNIILzRK4ArBOndN1XLORKaUP0_WOqNpqpt0zRYiHanJ5twclRC3IUafH5qHgbgSfdVYtP1hqlFG8h6-YBgqfQgWSOSGtoeoIXtPFVKl6T1Fn66YuZkZwK-pJulE8ZjLNs2udyC6VrLJG-jghcLX37Xd4YM_INNtK1C2c4Ukf44Z9Kgsyckvv7_TFJK8aHCLvshNj8pqnCT9915crddBu6TLjIse2mdZX7vYhKKrjpaNuEGWPPQjUfgIM_iNeBe2AQb7C7spc2LnHj1YeP1TDjn7qj-5HAW2bHuUD4nALKbsQfoble3O2y3LYLFSgd0n44OMCZzttdD51k_1X0_abJSkDq-7COjv6b6MV8d9NviCqJlyQUOBc0Z2JVVe23yHEiimKG6qBdAmFOYGvRWGHxk-hHcRER3EIBo0_f2FB90woyM4IvwB_x0l_1P5EU7nwoX1vNxpC4qDcHqBPH3LdPcgOCkas9okdJf8tozKkeOd1wKQUNFHX49peEows3mm19RopHCrJeVUldp9Ou8McwXuDPXSgV3bJ1U_gvwiON2FtHkucgZA_-U_vNfR_ki-KsCdFndGJv7Xi_Vk7TQms_bVmqLd01LU6DRpuU-7cuA9gJHkNKpywvD9fNRLggvzEIJREr9-uGm1HrS_OZxrbwarTymhx627yI9xPpT-1Kd9heFiesYKANpMk61UDs5Zc6JYjL9z8erP4p7FY5Wo9zzIGwR8zCmTL70Z-ul_FMvq5Zst49a9nRiwK_-CBTgD7AV9nqUKWU7otFNymlYdnrr-e6PDffai-mPMZwQrD3AgqeCTPTwaB3sH6M7ktCp0gY34SlCDqKL4RW6gBI2uljW8P3bErYdFj8hX_DohKNoCFzjMMBVHdTpayUwKE0kxhsOaBg0wPoxh-F9NPB1DId_upikgo7QD4xB_Bu9QVZxrGSSJydQtFbyhxQXmuMQZYCLhppkqwW6QExkHt4-_krWTv6SQSz-cDz-9eY8c_oxw2ctyiHSp-Zwz2XXJq6fU0rT8QMYXnaZSQnjH13IyzhAqJn2hGUC5ZgIuoMvp3AINCYFe_cZoP8kJ7VkgEw_flNRkcY4IQMVmfVCg_FL2wX-4xtXIPshh_wVKfO-dEbOE64Cvf_1g995mDeAtZYyWg4niruq52P6uUtexsuFXoJ0FpaBgDTs4AAXYSa3ep-a9kZjlEpYWyLI-zDXGfwc1FN5ly3CEl3EtECYd4Z3hGXV6gvHHCdLBOgnFhHpjsMFJb-P8rmqkltSR3-EdcRh8dmiOTUj-OHbH2_rNNQwksTt-FKTE7VTZQHkPqlEewXwnrzI5XwdzYAuvUc3eeniyk5pejNkxn5LhJg0UVRE8A2CES8gE0vcuzgQGbDdAu9p-dlK4BuUsxajtQTwVKMXMcRMMDnVquQ8KH85xtlNCcJvJSMD6cVr3ZAFOwLLLTyfcyT36vcLUbPV4EGm030vArTG-p1I_2UQlDCK5U7TAUi2lxa0GgED2YfkrCo_5hfl22EC3XritZID2aPUKv2ncOe254J_cg5a0SIN60HjcB07y0chPpYlsq0bnZEn3U0hoZXOJ43FZGa5ss8JwgDZNraLUyg1v7Ds-qaQ1Yp0rYGmX1pFDKgYAk5DjUuwDJ4yCm_rDx-MosFtYGzM0sAUwuyBT_dQHlxQb492QxaI5ePFq2dO1sXjGYAyZGu6GtGS_CPnFpKVwAqx4gGLzD-hXmzoSQzynH74v8BPvS57Qb3QCU3oT8-qC5uBZ1t3Hg5xoQxItrP0ePmR-F9CL87X07HTUR1vTRlMt9PvgAz0eusQbRK43odzxTRzjuecji5bBIM8bu1BS6qTk6JWLvdmLnul71IpUrDxJtYk38kQ6uZHidgD80MZK19c4Q6K3aonDSTkUB-eGaFtziOPQ3-2Y3hNk1W8eqM-PPz0eZAM_KcLeXUcrhLB4lelNZjF2v9Cytdcgc2TMU8HKo25N0zx_V03r12PdYjE9WmdOU_EsHn0jYenNDromzmHTxhYpdnzmwBQv-3yit25LT1bFzoEzTLrfgpesyhiZJSjciJis24LR2EYJXkMDvYWg74UFrVzv9dW1xZqAhZFzciFp2J30HBEU_gBBErmcH1nGAN9NjJNm_36Wi9akSzlDAEuWcy_lw7XWTNeoYR3lLlRuXNVFWhw-5YC6eYy3P2QSKveN-sHNJYiCWqq7Bz5EHxy24j313P3SR0CZKBmwKXWvCNCyrpSYmq3hdNc_V6gP4d9YIpveZLZjNTYRZndVGADCuddOvPwUSzjxtRHBOu2EYvatSHne4VqVqKlf_54qyHBM4ok-Y3y37yrqwCh7ifdb09MFLVbOtj70M41ezH6FUnDq4axxbkmlSb6VuIJLFnFyCuwMofooBJGtyvPU0D3_PJ6-mm-9cZ2gyb6ALkmAMgpv843EuSIOy09zx43PdMR33bHM-aXDlysLAX6J7dyMGUFVzmsdCR3kgtOh-moXANLVG7oUOv8ZCO1VTjDdHlTWPAGso83ipfU22aSVwfe6g80jeat8A9FaqRiIwS9sTgrM_BxeBgc8gQsO8OG4PmBk49Obuo5K89FX5rySAqPieCKQCOUEZ4gp4PdLUbwbh7XVpVJiksFAom3lGiDHQyLk7c4PC5LBwFT6o3J54I2ZryEsOf5VNwShANxltk5HGasxuk4ayHGReUyHm_JAhEPX6C6gQ2G-dRjF4V7LDdt3tlxtGgUUlNimJYByRkT8I_2j-65xrmyKT1PvlCxvdJ3uu5q4x5TFloXJaIDMelJrSNZUfYX4mnYugQub_Z-ouTJTVZbgA8LweIuvGjQfU5kD0SM_JGCXk2K1E-9sRNSQtfhguzfyPHRPJbjntIENLISzU5cF9nQyLB8QzHaV9pXOySInzqkwUAT_H6quPxEQbGD8mp7SKnqbXWtSphJU5IWj8KM3YAGMSe_tB_yj6jHRJr-AB5sFvh7g2QkvBocX_KE1KbrA3OwC0azkYw6tz8hKQtwJS7_fnSwCgTsucyZcEMekl_uyWDguLyAvkFDn-eHIzmK7qRidtf56rLSxwfQGXd5cNs5oHFXpZZLqIwFZEwhVkWJtHczcF1T3-Fhlnxw6pFiz7Vm9rY4-XPIDlD3KhTVY6ZF2MFYya6PcO0WprBhL-YXTVH9UsvRk__UtOCwYOeDJPXsPf5Nenx6ZdIXWzAWWe3LFcFQ7550pOEKd82XKh9lJHmidf26oBuT5fEJy4rjSFkvCPWYs4nVHhDSz8Z_gcL1rFi-piEa4rkzn-MHP2sKF-frMo_WzCJUqqKvWvDi9Xs9Suh7H0aJTaRH7KyO9WIbS_NGLJD65FIVKF6R4TB9QBIkTLxBjQshEh9nL3A8idoZ0tZycNp7J1MAmZljDVFiAK2jJlfAkaRcm3KAl31VKgRoa9rBHoE0jtbEDaeDoZPCWWeMAYCKGQtwp-VL2EH6b6PU0XJUjxFEsQ.CAmpx0SxpZJyRSPMR0s4Q1NYgvb7v1WkdZSYsQFVxds\",\"scope\":\"openid profile email offline_access ui.read ui.admin ui.preferences.read ui.preferences.write authority:tenants.read authority:tenants.write authority:users.read authority:users.write authority:roles.read authority:roles.write authority:clients.read authority:clients.write authority:tokens.read authority:tokens.revoke authority:branding.read authority:branding.write authority.audit.read graph:read sbom:read scanner:read policy:read policy:simulate policy:author policy:review policy:approve policy:run policy:activate policy:audit policy:edit policy:operate policy:publish airgap:seal airgap:status:read orch:read orch:operate orch:quota analytics.read advisory:read advisory-ai:view advisory-ai:operate vex:read vexhub:read exceptions:read exceptions:approve aoc:verify findings:read release:read release:write release:publish scheduler:read scheduler:operate notify.viewer notify.operator notify.admin notify.escalate evidence:read export.viewer export.operator export.admin vuln:view vuln:investigate vuln:operate vuln:audit platform.context.read platform.context.write doctor:run doctor:admin ops.health integration:read integration:write integration:operate packs.read packs.write packs.run packs.approve registry.admin timeline:read timeline:write trust:read trust:write trust:admin signer:read signer:sign signer:rotate signer:admin\",\"expiresAtEpochMs\":1773614282452},\"identity\":{\"subject\":\"b08639745d6549348d843aa311c98958\",\"name\":\"admin\",\"roles\":[],\"idToken\":\"eyJhbGciOiJSUzI1NiIsImtpZCI6IlRBSU1GTlE3LUFOR1JMU0JOQVlfQ19OSEdPVERVWUo5UlNVVTRSRUQiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2F1dGhvcml0eS5zdGVsbGEtb3BzLmxvY2FsLyIsImV4cCI6MTc3MzYxNTc4MiwiaWF0IjoxNzczNjEyNDgyLCJhdWQiOiJzdGVsbGEtb3BzLXVpIiwic3ViIjoiYjA4NjM5NzQ1ZDY1NDkzNDhkODQzYWEzMTFjOTg5NTgiLCJuYW1lIjoiYWRtaW4iLCJhenAiOiJzdGVsbGEtb3BzLXVpIiwibm9uY2UiOiIzMDAxN2E3My01ZWJjLTQ1MTMtYmYwMi03MTYzYzU5Mzc4YzQiLCJhdF9oYXNoIjoiY2xTUDhpWkdWSGZoWGN5anlZSE5qQSJ9.PNIW1mJLOHlgBINVRGMXsYN5BSqovNcw9hqAkpiuy4-54prU7hMJyEalzVbSFnsgcXSKYA8ipkk3MDzc3RuVE0F_KpbRBFHr3M0cGkMcWON_y_nSsfkkkkU0DIgd5YGxvd2wAlcKeusKS9bnh4U02_76gf3Id5kI3nQXvrasuSfloahCTjjLwKXsemNGWU4NgomY5kpSslX_6pCqBNPBJkicZ2CksrfevN6Txg3KDlkTsrWhtAEa2GuQWTfOOqN2swCUa6yLlIfrGVFcMfKWkRvTgTtuPy02pUDW6bjP7QaZ6CULq5us5CAx0CeDdCl5V3MkRk_vE7PLPn0-gvn5XA\"},\"dpopKeyThumbprint\":\"LTn_b_OOrHoXaHCLWnQ4PIvR2cVrio-YGI_W_Hopu3U\",\"issuedAtEpochMs\":1773612483453,\"tenantId\":\"demo-prod\",\"scopes\":[\"advisory-ai:operate\",\"advisory-ai:view\",\"advisory:read\",\"airgap:seal\",\"airgap:status:read\",\"analytics.read\",\"aoc:verify\",\"authority.audit.read\",\"authority:branding.read\",\"authority:branding.write\",\"authority:clients.read\",\"authority:clients.write\",\"authority:roles.read\",\"authority:roles.write\",\"authority:tenants.read\",\"authority:tenants.write\",\"authority:tokens.read\",\"authority:tokens.revoke\",\"authority:users.read\",\"authority:users.write\",\"doctor:admin\",\"doctor:run\",\"email\",\"evidence:read\",\"exceptions:approve\",\"exceptions:read\",\"export.admin\",\"export.operator\",\"export.viewer\",\"findings:read\",\"graph:read\",\"integration:operate\",\"integration:read\",\"integration:write\",\"notify.admin\",\"notify.escalate\",\"notify.operator\",\"notify.viewer\",\"offline_access\",\"openid\",\"ops.health\",\"orch:operate\",\"orch:quota\",\"orch:read\",\"packs.approve\",\"packs.read\",\"packs.run\",\"packs.write\",\"platform.context.read\",\"platform.context.write\",\"policy:activate\",\"policy:approve\",\"policy:audit\",\"policy:author\",\"policy:edit\",\"policy:operate\",\"policy:publish\",\"policy:read\",\"policy:review\",\"policy:run\",\"policy:simulate\",\"profile\",\"registry.admin\",\"release:publish\",\"release:read\",\"release:write\",\"sbom:read\",\"scanner:read\",\"scheduler:operate\",\"scheduler:read\",\"signer:admin\",\"signer:read\",\"signer:rotate\",\"signer:sign\",\"timeline:read\",\"timeline:write\",\"trust:admin\",\"trust:read\",\"trust:write\",\"ui.admin\",\"ui.preferences.read\",\"ui.preferences.write\",\"ui.read\",\"vex:read\",\"vexhub:read\",\"vuln:audit\",\"vuln:investigate\",\"vuln:operate\",\"vuln:view\"],\"audiences\":[],\"authenticationTimeEpochMs\":1773612482000,\"freshAuthActive\":false,\"freshAuthExpiresAtEpochMs\":null}" + ], + [ + "stellaops.auth.session.info", + "{\"subject\":\"b08639745d6549348d843aa311c98958\",\"expiresAtEpochMs\":1773614282452,\"issuedAtEpochMs\":1773612483453,\"dpopKeyThumbprint\":\"LTn_b_OOrHoXaHCLWnQ4PIvR2cVrio-YGI_W_Hopu3U\",\"tenantId\":\"demo-prod\"}" + ], + [ + "stella-search-session-id", + "ffe5ca96-2df5-492f-abb8-f9cbe307e7e1" + ] + ] + }, + "events": { + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [] + }, + "statePath": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\live-mirror-operator-journey.state.json" +} diff --git a/src/Web/StellaOps.Web/output/playwright/live-mirror-operator-journey.json b/src/Web/StellaOps.Web/output/playwright/live-mirror-operator-journey.json new file mode 100644 index 000000000..978dfc63f --- /dev/null +++ b/src/Web/StellaOps.Web/output/playwright/live-mirror-operator-journey.json @@ -0,0 +1,415 @@ +{ + "generatedAtUtc": "2026-03-15T22:14:49.284Z", + "baseUrl": "https://stella-ops.local", + "failedCheckCount": 18, + "runtimeIssueCount": 10, + "results": [ + { + "key": "catalog-direct", + "url": "https://stella-ops.local/setup/integrations/advisory-vex-sources?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "heading": "Advisory & VEX Source Catalog", + "banners": [], + "screenshotPath": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\mirror-operator-journey\\catalog-direct.png", + "hasConfigureMirrorLink": true, + "hasConnectMirrorLink": true, + "hasCreateMirrorDomainButton": true, + "mirrorConfigApi": { + "ok": true, + "status": 200, + "body": { + "mode": "Direct", + "consumerMirrorUrl": null, + "consumerConnected": false, + "lastConsumerSync": null + } + }, + "mirrorHealthApi": { + "ok": true, + "status": 200, + "body": { + "totalDomains": 0, + "freshCount": 0, + "staleCount": 0, + "neverGeneratedCount": 0, + "totalAdvisoryCount": 0 + } + }, + "mirrorDomainsApi": { + "ok": true, + "status": 200, + "body": { + "domains": [], + "totalCount": 0 + } + } + }, + { + "key": "ops-catalog-direct", + "url": "https://stella-ops.local/ops/integrations/advisory-vex-sources?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "heading": "Advisory & VEX Source Catalog", + "banners": [], + "screenshotPath": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\mirror-operator-journey\\ops-catalog-direct.png", + "hasConfigureMirrorLink": true, + "hasConnectMirrorLink": true, + "hasCreateMirrorDomainButton": true + }, + { + "key": "catalog-configure-mirror-handoff", + "url": "https://stella-ops.local/setup/integrations/advisory-vex-sources?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "heading": "Advisory & VEX Source Catalog", + "banners": [], + "screenshotPath": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\mirror-operator-journey\\catalog-configure-mirror-handoff.png", + "clickResult": { + "clicked": false, + "reason": "Catalog Configure Mirror not visible" + } + }, + { + "key": "catalog-create-domain-handoff", + "url": "https://stella-ops.local/setup/integrations/advisory-vex-sources?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "heading": "Advisory & VEX Source Catalog", + "banners": [], + "screenshotPath": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\mirror-operator-journey\\catalog-create-domain-handoff.png", + "clickResult": { + "clicked": false, + "reason": "Catalog Create Mirror Domain not visible" + } + }, + { + "key": "dashboard-direct", + "url": "https://stella-ops.local/setup/integrations/advisory-vex-sources/mirror?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "heading": "Mirror Dashboard", + "banners": [], + "screenshotPath": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\mirror-operator-journey\\dashboard-direct.png", + "hasCreateDomainButton": true, + "hasSetupMirrorButton": false, + "mirrorConfigApi": { + "ok": true, + "status": 200, + "body": { + "mode": "Direct", + "consumerMirrorUrl": null, + "consumerConnected": false, + "lastConsumerSync": null + } + } + }, + { + "key": "ops-dashboard-direct", + "url": "https://stella-ops.local/ops/integrations/advisory-vex-sources/mirror?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "heading": "Mirror Dashboard", + "banners": [], + "screenshotPath": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\mirror-operator-journey\\ops-dashboard-direct.png", + "hasCreateDomainButton": true, + "hasSetupMirrorButton": false + }, + { + "key": "dashboard-create-domain-handoff", + "url": "https://stella-ops.local/setup/integrations/advisory-vex-sources/mirror/new?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "heading": "Create Mirror Domain", + "banners": [], + "screenshotPath": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\mirror-operator-journey\\dashboard-create-domain-handoff.png", + "clickResult": { + "clicked": true + } + }, + { + "key": "dashboard-configure-consumer-handoff", + "url": "https://stella-ops.local/setup/integrations/advisory-vex-sources/mirror?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "heading": "Mirror Dashboard", + "banners": [], + "screenshotPath": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\mirror-operator-journey\\dashboard-configure-consumer-handoff.png", + "clickResult": { + "clicked": false, + "reason": "Dashboard Configure Consumer not visible" + } + }, + { + "key": "builder-create-attempt", + "url": "https://stella-ops.local/setup/integrations/advisory-vex-sources/mirror/new?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "heading": "Create Mirror Domain", + "banners": [], + "screenshotPath": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\mirror-operator-journey\\builder-create-attempt.png", + "sourceCount": 74, + "nextToConfig": { + "clicked": true + }, + "nextToReview": { + "clicked": true + }, + "createClick": { + "clicked": true + }, + "createErrorBanner": "", + "domainConfigApi": { + "ok": true, + "status": 200, + "body": { + "domainId": "qa-mirror-mmsb2rx7", + "displayName": "QA Mirror mmsb2rx7", + "sourceIds": [ + "nvd", + "osv" + ], + "exportFormat": "JSON", + "rateLimits": { + "indexRequestsPerHour": 120, + "downloadRequestsPerHour": 600 + }, + "requireAuthentication": false, + "signing": { + "enabled": false, + "algorithm": "HMAC-SHA256", + "keyId": "" + }, + "resolvedFilter": { + "domainId": "qa-mirror-mmsb2rx7", + "sourceIds": [ + "nvd", + "osv" + ], + "exportFormat": "JSON", + "rateLimits": { + "indexRequestsPerHour": 120, + "downloadRequestsPerHour": 600 + }, + "requireAuthentication": false, + "signing": { + "enabled": false, + "algorithm": "HMAC-SHA256", + "keyId": "" + } + } + } + }, + "domainEndpointsApi": { + "ok": true, + "status": 200, + "body": { + "domainId": "qa-mirror-mmsb2rx7", + "endpoints": [ + { + "path": "/concelier/exports/index.json", + "method": "GET", + "description": "Mirror index used by downstream discovery clients." + }, + { + "path": "/concelier/exports/mirror/qa-mirror-mmsb2rx7/manifest.json", + "method": "GET", + "description": "Domain manifest describing the generated advisory bundle." + }, + { + "path": "/concelier/exports/mirror/qa-mirror-mmsb2rx7/bundle.json", + "method": "GET", + "description": "Generated advisory bundle payload for the mirror domain." + } + ] + } + }, + "domainStatusApi": { + "ok": true, + "status": 200, + "body": { + "domainId": "qa-mirror-mmsb2rx7", + "lastGeneratedAt": null, + "lastGenerateTriggeredAt": "2026-03-15T22:10:11.1736533+00:00", + "bundleSizeBytes": 0, + "advisoryCount": 0, + "exportCount": 2, + "staleness": "never_generated" + } + }, + "publicMirrorIndexApi": { + "ok": false, + "status": 404, + "body": "" + }, + "publicDomainManifestApi": { + "ok": false, + "status": 404, + "body": "" + }, + "publicDomainBundleApi": { + "ok": false, + "status": 404, + "body": "" + } + }, + { + "key": "ops-builder-direct", + "url": "https://stella-ops.local/ops/integrations/advisory-vex-sources/mirror/new?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "heading": "Create Mirror Domain", + "banners": [], + "screenshotPath": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\mirror-operator-journey\\ops-builder-direct.png", + "sourceCount": 74 + }, + { + "key": "dashboard-domain-panels", + "url": "https://stella-ops.local/setup/integrations/advisory-vex-sources/mirror?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "heading": "Mirror Dashboard", + "banners": [], + "screenshotPath": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\mirror-operator-journey\\dashboard-domain-panels.png", + "viewEndpointsResult": { + "clicked": false, + "reason": "Dashboard view endpoints not visible" + }, + "viewConfigResult": { + "clicked": false, + "reason": "Dashboard view config not visible" + }, + "endpointsPanelText": "", + "configPanelText": "" + }, + { + "key": "client-setup-direct", + "url": "https://stella-ops.local/setup/integrations/advisory-vex-sources/mirror/client-setup?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "heading": "Mirror Client Setup", + "banners": [], + "screenshotPath": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\mirror-operator-journey\\client-setup-direct.png", + "mirrorConfigApi": { + "ok": true, + "status": 200, + "body": { + "mode": "Direct", + "consumerMirrorUrl": null, + "consumerConnected": false, + "lastConsumerSync": null + } + }, + "mirrorDomainsApi": { + "ok": true, + "status": 200, + "body": { + "domains": [ + { + "id": "qa-mirror-mmsb2rx7", + "domainId": "qa-mirror-mmsb2rx7", + "displayName": "QA Mirror mmsb2rx7", + "sourceIds": [ + "nvd", + "osv" + ], + "exportFormat": "JSON", + "rateLimits": { + "indexRequestsPerHour": 120, + "downloadRequestsPerHour": 600 + }, + "requireAuthentication": false, + "signing": { + "enabled": false, + "algorithm": "HMAC-SHA256", + "keyId": "" + }, + "domainUrl": "/concelier/exports/mirror/qa-mirror-mmsb2rx7", + "createdAt": "2026-03-15T22:10:10.1559971+00:00", + "status": "Never generated" + } + ], + "totalCount": 1 + } + }, + "testConnection": { + "clicked": true + }, + "connectedBanner": "", + "connectionErrorBanner": "✗Connection failedMirror returned HTTP 404 from https://stella-ops.local/concelier/exports/index.jsonVerify the upstream mirror publishes /concelier/exports/index.json.", + "discoveredDomainOptions": 0, + "nextToSignatureVisible": true, + "nextToSignatureEnabled": false + }, + { + "key": "ops-client-setup-direct", + "url": "https://stella-ops.local/ops/integrations/advisory-vex-sources/mirror/client-setup?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "heading": "Mirror Client Setup", + "banners": [], + "screenshotPath": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\mirror-operator-journey\\ops-client-setup-direct.png", + "hasMirrorAddress": false, + "hasTestConnectionButton": false + }, + { + "key": "feeds-airgap-configure-sources-handoff", + "url": "https://stella-ops.local/ops/integrations/advisory-vex-sources", + "heading": "Advisory & VEX Source Catalog", + "banners": [], + "screenshotPath": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\mirror-operator-journey\\feeds-airgap-configure-sources-handoff.png", + "clickResult": { + "clicked": true + } + }, + { + "key": "security-configure-sources-handoff", + "url": "https://stella-ops.local/ops/integrations/advisory-vex-sources?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "heading": "Advisory & VEX Source Catalog", + "banners": [ + "Loading source catalog..." + ], + "screenshotPath": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\mirror-operator-journey\\security-configure-sources-handoff.png", + "clickResult": { + "clicked": true + } + } + ], + "failures": [ + "Catalog Configure Mirror action failed before navigation: Catalog Configure Mirror not visible", + "Catalog Configure Mirror did not land on mirror dashboard: https://stella-ops.local/setup/integrations/advisory-vex-sources?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "Catalog Create Mirror Domain action failed before navigation: Catalog Create Mirror Domain not visible", + "Catalog Create Mirror Domain did not land on /mirror/new: https://stella-ops.local/setup/integrations/advisory-vex-sources?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "Ops mirror dashboard is missing primary actions.", + "Dashboard Configure Consumer action failed before navigation: Dashboard Configure Consumer not visible", + "Dashboard Configure Consumer did not land on /mirror/client-setup: https://stella-ops.local/setup/integrations/advisory-vex-sources/mirror?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "Mirror domain create journey did not complete successfully. Banner=\"\" finalUrl=\"https://stella-ops.local/setup/integrations/advisory-vex-sources/mirror/new?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d\"", + "Public mirror index was not reachable through the frontdoor: status=404", + "Public mirror manifest was not reachable for qa-mirror-mmsb2rx7: status=404", + "Public mirror bundle was not reachable for qa-mirror-mmsb2rx7: status=404", + "Mirror dashboard View Endpoints action failed: Dashboard view endpoints not visible", + "Mirror dashboard View Config action failed: Dashboard view config not visible", + "Mirror dashboard endpoints panel did not render public mirror paths.", + "Mirror dashboard config panel did not render resolved domain configuration.", + "Mirror client connection preflight failed: ✗Connection failedMirror returned HTTP 404 from https://stella-ops.local/concelier/exports/index.jsonVerify the upstream mirror publishes /concelier/exports/index.json.", + "Mirror client setup cannot proceed to signature verification after test connection.", + "Ops mirror client setup is missing connection controls." + ], + "runtime": { + "consoleErrors": [ + "Failed to load resource: the server responded with a status of 403 ()", + "Failed to load resource: the server responded with a status of 404 ()", + "Failed to load resource: the server responded with a status of 404 ()", + "Failed to load resource: the server responded with a status of 404 ()", + "Failed to load resource: the server responded with a status of 403 ()" + ], + "pageErrors": [], + "requestFailures": [], + "responseErrors": [ + { + "status": 403, + "method": "POST", + "url": "https://stella-ops.local/api/v1/advisory-sources/mirror/domains/qa-mirror-mmsb2rx7/generate", + "page": "https://stella-ops.local/setup/integrations/advisory-vex-sources/mirror/new?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + }, + { + "status": 404, + "method": "GET", + "url": "https://stella-ops.local/concelier/exports/index.json", + "page": "https://stella-ops.local/setup/integrations/advisory-vex-sources/mirror/new?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + }, + { + "status": 404, + "method": "GET", + "url": "https://stella-ops.local/concelier/exports/mirror/qa-mirror-mmsb2rx7/manifest.json", + "page": "https://stella-ops.local/setup/integrations/advisory-vex-sources/mirror/new?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + }, + { + "status": 404, + "method": "GET", + "url": "https://stella-ops.local/concelier/exports/mirror/qa-mirror-mmsb2rx7/bundle.json", + "page": "https://stella-ops.local/setup/integrations/advisory-vex-sources/mirror/new?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + }, + { + "status": 403, + "method": "DELETE", + "url": "https://stella-ops.local/api/v1/advisory-sources/mirror/domains/qa-mirror-mmsb2rx7", + "page": "https://stella-ops.local/ops/integrations/advisory-vex-sources?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + } + ] + } +} diff --git a/src/Web/StellaOps.Web/output/playwright/live-mirror-operator-journey.state.json b/src/Web/StellaOps.Web/output/playwright/live-mirror-operator-journey.state.json new file mode 100644 index 000000000..a1541bb7e --- /dev/null +++ b/src/Web/StellaOps.Web/output/playwright/live-mirror-operator-journey.state.json @@ -0,0 +1,18 @@ +{ + "cookies": [], + "origins": [ + { + "origin": "https://stella-ops.local", + "localStorage": [ + { + "name": "stellaops.sidebar.preferences", + "value": "{\"sidebarCollapsed\":false,\"collapsedGroups\":[],\"collapsedSections\":[]}" + }, + { + "name": "stellaops.theme", + "value": "system" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/Web/StellaOps.Web/output/playwright/live-release-confidence-journey.auth.json b/src/Web/StellaOps.Web/output/playwright/live-release-confidence-journey.auth.json new file mode 100644 index 000000000..33d05203d --- /dev/null +++ b/src/Web/StellaOps.Web/output/playwright/live-release-confidence-journey.auth.json @@ -0,0 +1,39 @@ +{ + "authenticatedAtUtc": "2026-03-15T10:36:51.124Z", + "baseUrl": "https://stella-ops.local", + "finalUrl": "https://stella-ops.local/mission-control/board?tenant=demo-prod®ions=apac,eu-west,us-east,us-west", + "title": "Dashboard - StellaOps", + "cookies": [], + "storage": { + "localStorageEntries": [ + [ + "stellaops.sidebar.preferences", + "{\"sidebarCollapsed\":false,\"collapsedGroups\":[],\"collapsedSections\":[]}" + ], + [ + "stellaops.theme", + "system" + ] + ], + "sessionStorageEntries": [ + [ + "stellaops.auth.session.info", + "{\"subject\":\"b08639745d6549348d843aa311c98958\",\"expiresAtEpochMs\":1773572807454,\"issuedAtEpochMs\":1773571008454,\"dpopKeyThumbprint\":\"7b8tPvJvSNYslhIErTy0fPUtQ3_b3zRZx-zko_fR8DE\",\"tenantId\":\"demo-prod\"}" + ], + [ + "stellaops.auth.session.full", + "{\"tokens\":{\"accessToken\":\"eyJhbGciOiJSUzI1NiIsImtpZCI6IlRBSU1GTlE3LUFOR1JMU0JOQVlfQ19OSEdPVERVWUo5UlNVVTRSRUQiLCJ0eXAiOiJhdCtqd3QifQ.eyJpc3MiOiJodHRwczovL2F1dGhvcml0eS5zdGVsbGEtb3BzLmxvY2FsLyIsImV4cCI6MTc3MzU3MjgwOCwiaWF0IjoxNzczNTcxMDA4LCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGVtYWlsIG9mZmxpbmVfYWNjZXNzIHVpLnJlYWQgdWkuYWRtaW4gdWkucHJlZmVyZW5jZXMucmVhZCB1aS5wcmVmZXJlbmNlcy53cml0ZSBhdXRob3JpdHk6dGVuYW50cy5yZWFkIGF1dGhvcml0eTp0ZW5hbnRzLndyaXRlIGF1dGhvcml0eTp1c2Vycy5yZWFkIGF1dGhvcml0eTp1c2Vycy53cml0ZSBhdXRob3JpdHk6cm9sZXMucmVhZCBhdXRob3JpdHk6cm9sZXMud3JpdGUgYXV0aG9yaXR5OmNsaWVudHMucmVhZCBhdXRob3JpdHk6Y2xpZW50cy53cml0ZSBhdXRob3JpdHk6dG9rZW5zLnJlYWQgYXV0aG9yaXR5OnRva2Vucy5yZXZva2UgYXV0aG9yaXR5OmJyYW5kaW5nLnJlYWQgYXV0aG9yaXR5OmJyYW5kaW5nLndyaXRlIGF1dGhvcml0eS5hdWRpdC5yZWFkIGdyYXBoOnJlYWQgc2JvbTpyZWFkIHNjYW5uZXI6cmVhZCBwb2xpY3k6cmVhZCBwb2xpY3k6c2ltdWxhdGUgcG9saWN5OmF1dGhvciBwb2xpY3k6cmV2aWV3IHBvbGljeTphcHByb3ZlIHBvbGljeTpydW4gcG9saWN5OmFjdGl2YXRlIHBvbGljeTphdWRpdCBwb2xpY3k6ZWRpdCBwb2xpY3k6b3BlcmF0ZSBwb2xpY3k6cHVibGlzaCBhaXJnYXA6c2VhbCBhaXJnYXA6c3RhdHVzOnJlYWQgb3JjaDpyZWFkIG9yY2g6b3BlcmF0ZSBvcmNoOnF1b3RhIGFuYWx5dGljcy5yZWFkIGFkdmlzb3J5OnJlYWQgYWR2aXNvcnktYWk6dmlldyBhZHZpc29yeS1haTpvcGVyYXRlIHZleDpyZWFkIHZleGh1YjpyZWFkIGV4Y2VwdGlvbnM6cmVhZCBleGNlcHRpb25zOmFwcHJvdmUgYW9jOnZlcmlmeSBmaW5kaW5nczpyZWFkIHJlbGVhc2U6cmVhZCByZWxlYXNlOndyaXRlIHJlbGVhc2U6cHVibGlzaCBzY2hlZHVsZXI6cmVhZCBzY2hlZHVsZXI6b3BlcmF0ZSBub3RpZnkudmlld2VyIG5vdGlmeS5vcGVyYXRvciBub3RpZnkuYWRtaW4gbm90aWZ5LmVzY2FsYXRlIGV2aWRlbmNlOnJlYWQgZXhwb3J0LnZpZXdlciBleHBvcnQub3BlcmF0b3IgZXhwb3J0LmFkbWluIHZ1bG46dmlldyB2dWxuOmludmVzdGlnYXRlIHZ1bG46b3BlcmF0ZSB2dWxuOmF1ZGl0IHBsYXRmb3JtLmNvbnRleHQucmVhZCBwbGF0Zm9ybS5jb250ZXh0LndyaXRlIGRvY3RvcjpydW4gZG9jdG9yOmFkbWluIG9wcy5oZWFsdGggaW50ZWdyYXRpb246cmVhZCBpbnRlZ3JhdGlvbjp3cml0ZSBpbnRlZ3JhdGlvbjpvcGVyYXRlIHBhY2tzLnJlYWQgcGFja3Mud3JpdGUgcGFja3MucnVuIHBhY2tzLmFwcHJvdmUgcmVnaXN0cnkuYWRtaW4gdGltZWxpbmU6cmVhZCB0aW1lbGluZTp3cml0ZSB0cnVzdDpyZWFkIHRydXN0OndyaXRlIHRydXN0OmFkbWluIHNpZ25lcjpyZWFkIHNpZ25lcjpzaWduIHNpZ25lcjpyb3RhdGUgc2lnbmVyOmFkbWluIiwianRpIjoiNzg2NzUxYTAtOGQzYS00OGVjLWI2MDItMjE1MTg0NjU3NGIwIiwic3ViIjoiYjA4NjM5NzQ1ZDY1NDkzNDhkODQzYWEzMTFjOTg5NTgiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJhZG1pbiIsIm5hbWUiOiJhZG1pbiIsInJvbGUiOiJhZG1pbiIsInN0ZWxsYW9wczp0ZW5hbnQiOiJkZW1vLXByb2QiLCJhdXRoX3RpbWUiOjE3NzM1NzEwMDgsIm9pX3Byc3QiOiJzdGVsbGEtb3BzLXVpIiwiY2xpZW50X2lkIjoic3RlbGxhLW9wcy11aSJ9.Wv4d8eQLquzBrGGdcnWWp4mdml1IU6I4m9Cu19cclIk0z5wSvE2Ed-FBi57XSd2g9FvmPGujv3DOnP2D2hMx2v4Z2xoOI4MK-76oNP1pPebBrEp4bPDJDM2F1_wZ6h3Pt_yvHQpVdmGfCL0ih7bs-TrBMiKbM5YPNDZt4HU_LSdhjeRfBoqoyoFZvFCuqiX4DSSgO1sAtMt8lDojFtYwH_QlhuZnX1YpfFMf15biZzeKIFpIzdE6miqzlXNaq1rQo7rd7EYS_EVgbTGAa6ws_-ootjPYIye5AkOO5DAryd4qlzKc0J1IciEF7VGN16HEl6oZ0aaepXD5BmdkZssm6Q\",\"tokenType\":\"Bearer\",\"refreshToken\":\"eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZDQkMtSFM1MTIiLCJraWQiOiJXUjVQSU4tVzI5S0lNTkpRTVBBMU9XS0VYN0tEWjZMV0dYTDdSS0dWIiwidHlwIjoib2lfcmVmdCtqd3QiLCJjdHkiOiJKV1QifQ.lJtanbwr6KVnsR6-_9ijovZuYzCcgu_tZLUkg_XUHAw_uPZYvmpD5xs7LtRirrXJp_o8cQ-ZObloN00WCWk4sX41USOyCu-0DgCI_5RfY9bEEfJWZ_ZwdcuVMGfqftSR2sVjSovo4aPAUYm1Xr4gyu2q4BSnl72XcxNknDkveRUPXH880-5gd7FiV3xYvLaOoAYLJtuRCIBj4TdDK11t5NhBdVNoshbhwyUhAWdXdl4Eql1CiBuoWTIMjz0umSkDq9q8fZiiunPbWHTnWuGglqR6UGzH_zMxUkWaxOV3I0VKXhja_GDH0krkJui_ew5ZvCrsjQlPfS8s4tYY2j2XPg.qrIzCK1BAFlp9Ssf2xFJyQ.-rAAAHvDTWEW-2Dikk8QRqpRexcUfYGAoElmosuNmw2f4pA0NBcai4tjYQiTTMJKLQMqdI8K6hNM5fQzeu8W0XRCzivO-TP6Qcf1b7do5QUJ5juJpAByEusdzn8hA1yLGvWqmR3rIhtywmxfXQq_uT96lx-uQQJZF_ivTp0HXEJu_d9JbzPemyb2hJEV2Ss8epbTWYzEnwk4uk2mnOBq4NJUVij9IMy2wdXdzdzBDNZeGFIivbh7Pu2aTnfnIttazDnPzSR_qN-G1xUAZfw_niwWZGeLYYfcNqTijTMQlBNJZvyl8GC1g968vPbNNW1W70-4cwZH5DVZRcW-sM54dWg-sl1zLj4-baLGIf31ZKA1gtcrq-k6S9Y8D3J1YMJh3NVQtyszPMLbKX-isGg5ECLQ3-h9oYqn63LRRb7-_Nkf18ikkOApb4mNb_XDK3h2tJJ2FLLabbH3U-Ma2oYsN93Rq-Vu8aniXUQt3iwapeT_PR_WTq70W-IiAfSX0D6aJKxWsMFVb-qNFKJZX9imILAeoSwiyoDQx8WkCo7lffPH5xQSSvd1YJzI61pYGKeHJiAr0aw8T-RPHDwGue99f1H-hn6aBY6u48Es5AENlI58AD_34RPW_go-9EbFTagzNavB57R4jUw7OnGOgGaJWq_SFOdu-X53nOLUwmYCG5ys6-1sXiuYrM_9dXFWrgOIClr5Zj8p0pd6OVhB0BKDZNuh0KoCCswffIX3FI5lOwa0YdcWq8UoV0_34gBHU7aAKupgcpqiPkkHMtIaWHao_72oXmFuu9bFhTRjovDp3bEowDkeNRZ-I68NaBAxWcDv6d-Ny7XL1DeVtdepVzRNmEcPQkfLP6U0rmNgzsCcef0AmY7bRBVhlKIJy4sZDEHvQzRW8xd4CkEKKaBYuTKMcpEhuk04z-wrIgmwCmjYLWlOzCUawohI_pxg0Uzmp35_5mdUDnTYa3JW-h7W7hUk_V6Z5Ouuw5rwETZJ3Cdf_AFrnsFTpwfMix_jgGzyY7MWKFp_kL_maSYF4MfklWudvWKntDIod9504aJsmnwi9UG9XHeSkUAlg7bCV_-XkVhFQEe8Cb4-tSq5bnN_F21AtTf9Px2f6O8f_067ceioP5D36BSim9fwYw-B1yoGFomwEt3BY-j3SB_aNWE3yk1YfIdOK8hIt-S_yww9JbQdthSoVNYUj0TrLtffPtz6TUmUQsDzvbEz1BOd754untTBZnGCDq3iKpnon9xFrktKLl0EiJ0e7X8Vy0v82NI3sDpPnFqz8mJQwYzQ2UuuhD6l9zNuvioRi0IWhOcatitoyFhRqeD7kzoq7BIB_P7aT34SYa2A7mEwbOGr8feeopL3uQaZuE5Wyvl4KLgN-XbU86FCw5Vf1XbwNSSaNWtxHX9SJ-V1VvHwBIQp7FWwcSfG-bL2g0jcPM08HcyQMq3dR9U37PMeTO6BDHLbpwo9ZSKma5pxzAJqKeBhcR-7nR1Uhbvz3pPgYr_UTelHvn8wreakjrxRElHdKwin-c_9b9jVoqyZVxzKFIFdteWUH8m4mxHLyx_nVpY09_udEKDE4egCB4sKaiH3Fs-d-AMKw-jvGm1R2uFTy-oNf0ub74h3VIlAH-ngdelB_7S3bkykh0lTI_48fKyWps9eAsbzwjeYgNZxowvt2VGylBeMCey4QPXw-nRyhFHL_F_IDkGbHuaB-Khyy0QgEBiOA2Y9MWuJcQKWqBvql6bLqvTwjrrG8hcbdCAc8gKf9lVBZHcoZNaghg8-6bz0-aK6t7bO16FqoljTrP2WGD7q0fjBE8uy_uONBNIXoMozpuFz5qoSWtyjAxTyTT-h3cotPCklwwRG9v7Mt8I3qaGUzuOYzWL-htkbnS2hz6r-dmwcOnmRNDQ3GMFk1dNWcBKFUB55N_fTeEGhmSXwdXWof3Or1ixhvJ6L3Mg7YHhEOGTv9PS_TlPoZ5NAmIR_sUfioS6bQY8gZLHECu4SFp2CGKpvLVkM8KJqPzgjndUqE573g4NKljWipByq8cVzGOYg8jkJbsSmU8h7EUGz9tYOGn-5xjSNfFgkAw4hiio0drSKmMX-U6Q0kYRiLgOsZvMF-Vne2OjAf_giM2eDwmxFmoOGmwNwAsck86pSqny3nl69NHfa8dnF8RQ4Oc6umjyg7f8ORpd9dvvZhSILI08JnLLYcAvl_ZnHEAn-io9Bi3CAs39OwX9Xrk3TmNhn4w6Euv9y58n40BSuIdp6RQkivf3IOqsDOJ5TuD2WAFuukpQpEPfFOdqP3QeDg0SzbsCPyL5J1mf3Bv_dcI_yxYAfWpbB-VYFXLadgw9EgylzdaH9zEwun2s8soZAZcRnH-8MignIIrmLeWUPgn5fQZFyEOYaL_QB6-JDPgHM6uq-3DBxq-Zur5-YzVB-Wnpqvz8Dz4rLWPqTwTV-Nw6CXLyHN2KX22eJ9fsyCCunHhnlCVyVj8Lmu2M-xDUelCu6eXhj9_IWX3AnHCmCVmlLuNpAsJx1qqc1FnMHzxV9vj0t5y0ExsCK1JnB8ewZN2RDZmMwuhJ2_TfXifzVs9qh57c5ZIyWyINFWd_h17-lv0G72uMOZ2X3TEnAjzruqvlfCgf_qAxAofb0kl82_DKA3rngSAr1TC16rzi731sbUGZFD83pKEzhwPuPAPSG1hFWBsEwWzJnmBcMuXiuFgnZQasSHA9Ab_Rn6bxXhuHrRxvCDhNeYXVvUpa96GJ7_YIUCKdN9OXE6Wj8Ud9bvlE6Kimw9RsyAwP-FNKCS_gDYursdmQhCDfe2is_CFDpHT60IAK2HHZdL4ASW2DYYdNHbioVasL6G-w9BlLXmtQcZnEkwOQxvn7lunUdKn1lIk586JLYITZnwBo_UcXt0EeyshfvfkIoUA-JRpQqSKd72NurrmarEkZXsPhNb90We_zLzbn1PFXIGqHZp0HJFfkK_nQJ9ZIMtEfr9F8WrngZWXnNPJeiCn5d3Vzm3kq5TkmC_eLbVdFE4O11v7bcEOzGvdnF_nKIhQA_vlITJDBC53gtHk7GEwP2bvLi4viFGl-22LaSTS-wQyahEEDPRD5WVOxis9CbxPb389FkpfvQmFzeiXIJ94cNWLksaE4W4BuqBXkNN53kySqEaDR4-0MLXfWPXlhk8Z035tVrM64_PSMr3CFyveaYPsSjjG5ffxtqhE3KQiRCmx8hK5gB4V4Pt2z8wdaNY5VWlnx0g9PCKW3HlTjwNAdh1OekHKEtm5hK7jeDcjS6mvSECl2DGQipXfqGwqBsPM06niJvU1n8z03maPkh0ecUbJ0I6MDobPLSoByDx5PvuTioyxS5PUOemmsW6HQ_ZbTePAOq-zrZyRycc1RsRAMNdiAcfLOTwhBEt6xtDQBXvqusk5kEuvuY7oZU7r9bd_U059SKQSFayNFdgK3uz7sgGo0WNompkJuW6ZKaEHa_UMWD4N6gBxZSOIWVnayXIdqCmG1SSvn0pEMANsSXfgVQpqVmBSgJbAPHnlvgiPJOv2mchGN7wX1C5nmH2R6jEtCUOsFW9bVY6BS_CetMiFGBWDTF6N3lHwZIpQMTgNhgbADRyV8Jst3Rca3p0Ly2UriBjYNDHb62MZ57bwosxQKDpiHR0PQkxEJ_C-y8vTHQJKXmq3WGJ9qjTCjOrketBp0f7pl_5YUktlow2j1QihdmOFq8e13zk5avMsMQ9o2Va6LqfntSQSMgNPbcnNvEWbWKVJjEmsxkPNV7SQ3BLRGdAGF5II62WWA4MudNvt8_Ds1aXNUrlvBJaomD-XMiw61koFlYfYhnPz94zA9bQh73bCCysDcqpRl7kIHUZybQgh4KF6V_3a4lltGK2ILzf2RnmdwGmp8keRCRsYcQYH8_fBa5iRwCuAix1kXkFpZcnjACK4bMtOZb6l0XuuXWWrRbUT9e7P1OxiYuhHgmNfleum7QXjmcudPsr6uBTu5cOHvNH9ZzZSdkzku1lv5oFd5MEJ94zXgMxJGfxkTToBFAVtO-093pvwCJu-B9PiQQO-sYY1jBYAQ4XLemTRMvq1bD7jKsYWvOQJ8TgvLEipywu8vcqErO14Mo2zZ1QKCPKHc1lZhZWu2ryQg1DScyBS_sW92tY1xiw-3W-ha_IeaLRr4T1IIKpP7wJ67diVBl4pPZB54VJB4DmVdLg_1HYH49DNk4PM2Km6dIKNzU3YisJWio2Rycp7nKz38hJgI7qAQ6lvRQHD39mrCyfyHvhXvTX4f5_eBzrgRTqTTS2vwn0nbzNLnvMFaF5qbBhwGYimTLle8qzQeJYlWRTcMvh9QZH5EjVwmxacKFIitdiNK9sbwH11gJIfjrktuOZ6KEEHZgQIRsQoXJzo9Gf8GaSvFXUKlR2GHBPUiXg3htuzZVpchjp5mT5JeCS8lZy1lmwNvXDI7NMtCX71Wo6F_kncTrktlPR5onkDNrQ1P9u3sxADbxstpy3eZl3Bloo7InFAihhTLbBE26SdaCcv8LybWU0YKpU38MWLCjaMTfqIqEZYK_y4tEbbn0ERPX3gmonR1cvrQIQIghulqHd93mHnZKyyVKJ2-rayRiDa571HrtQYuiyRz__GQqBty18jhhkhlAqXeUeoZ9xWc8zrEsIjO8PbawJk5VA4O2BxuOmQYdpfDK9Cy2N1FgCvg3dLx3t0m5tplmEFM6PktJjQiigz7DzTtjc2tig1OdlKM1hf2KMyNGofXFm2e4eba4BZIjCG31HK-K8ZEtfSSXjHTeA_xxvg.4EuXrz1WS6w4tE3ZY6_Hbap3JrS6yblpRHaFBAODhb4\",\"scope\":\"openid profile email offline_access ui.read ui.admin ui.preferences.read ui.preferences.write authority:tenants.read authority:tenants.write authority:users.read authority:users.write authority:roles.read authority:roles.write authority:clients.read authority:clients.write authority:tokens.read authority:tokens.revoke authority:branding.read authority:branding.write authority.audit.read graph:read sbom:read scanner:read policy:read policy:simulate policy:author policy:review policy:approve policy:run policy:activate policy:audit policy:edit policy:operate policy:publish airgap:seal airgap:status:read orch:read orch:operate orch:quota analytics.read advisory:read advisory-ai:view advisory-ai:operate vex:read vexhub:read exceptions:read exceptions:approve aoc:verify findings:read release:read release:write release:publish scheduler:read scheduler:operate notify.viewer notify.operator notify.admin notify.escalate evidence:read export.viewer export.operator export.admin vuln:view vuln:investigate vuln:operate vuln:audit platform.context.read platform.context.write doctor:run doctor:admin ops.health integration:read integration:write integration:operate packs.read packs.write packs.run packs.approve registry.admin timeline:read timeline:write trust:read trust:write trust:admin signer:read signer:sign signer:rotate signer:admin\",\"expiresAtEpochMs\":1773572807454},\"identity\":{\"subject\":\"b08639745d6549348d843aa311c98958\",\"name\":\"admin\",\"roles\":[],\"idToken\":\"eyJhbGciOiJSUzI1NiIsImtpZCI6IlRBSU1GTlE3LUFOR1JMU0JOQVlfQ19OSEdPVERVWUo5UlNVVTRSRUQiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2F1dGhvcml0eS5zdGVsbGEtb3BzLmxvY2FsLyIsImV4cCI6MTc3MzU3NDMwOCwiaWF0IjoxNzczNTcxMDA4LCJhdWQiOiJzdGVsbGEtb3BzLXVpIiwic3ViIjoiYjA4NjM5NzQ1ZDY1NDkzNDhkODQzYWEzMTFjOTg5NTgiLCJuYW1lIjoiYWRtaW4iLCJhenAiOiJzdGVsbGEtb3BzLXVpIiwibm9uY2UiOiIyOGIxMGQzYi02NjY2LTRmZWQtODM5Yy1hYWEwNGEyMDEyYjUiLCJhdF9oYXNoIjoicUNaSFRtX1BfQ2xzLWlZVzhUNXl4dyJ9.FtD4ARwfjiL7vPSS7APtaxm2AEDefejjmH6rEGeA1UmSz4lqveP5kkWEp6OXJXfqmVcH5BW0jSM7ukCYiu-ywiwyXXIH9TDD6gjO2jPQGhrSq4MFZo-fFJJWMT4142kdOV7SiPsyg7f9mnAQG06zRqs3__xuAktzp9cNrbA0qH0swTOFNGHS5FF1m4COHfgHnxnny8YeK_8KyjnfhBFYW5cXjMP7YnxUVYIqWE6EJZgDgEqd1x_46afBRuSK_LKSUwQ06xjpoNytDz96bJQYodW5XKLx9yE-kNEIjDtDdmZfonZFEeQMDccoFXIkooQSgMt_bWzQ_tVQ8QbDCovI2w\"},\"dpopKeyThumbprint\":\"7b8tPvJvSNYslhIErTy0fPUtQ3_b3zRZx-zko_fR8DE\",\"issuedAtEpochMs\":1773571008454,\"tenantId\":\"demo-prod\",\"scopes\":[\"advisory-ai:operate\",\"advisory-ai:view\",\"advisory:read\",\"airgap:seal\",\"airgap:status:read\",\"analytics.read\",\"aoc:verify\",\"authority.audit.read\",\"authority:branding.read\",\"authority:branding.write\",\"authority:clients.read\",\"authority:clients.write\",\"authority:roles.read\",\"authority:roles.write\",\"authority:tenants.read\",\"authority:tenants.write\",\"authority:tokens.read\",\"authority:tokens.revoke\",\"authority:users.read\",\"authority:users.write\",\"doctor:admin\",\"doctor:run\",\"email\",\"evidence:read\",\"exceptions:approve\",\"exceptions:read\",\"export.admin\",\"export.operator\",\"export.viewer\",\"findings:read\",\"graph:read\",\"integration:operate\",\"integration:read\",\"integration:write\",\"notify.admin\",\"notify.escalate\",\"notify.operator\",\"notify.viewer\",\"offline_access\",\"openid\",\"ops.health\",\"orch:operate\",\"orch:quota\",\"orch:read\",\"packs.approve\",\"packs.read\",\"packs.run\",\"packs.write\",\"platform.context.read\",\"platform.context.write\",\"policy:activate\",\"policy:approve\",\"policy:audit\",\"policy:author\",\"policy:edit\",\"policy:operate\",\"policy:publish\",\"policy:read\",\"policy:review\",\"policy:run\",\"policy:simulate\",\"profile\",\"registry.admin\",\"release:publish\",\"release:read\",\"release:write\",\"sbom:read\",\"scanner:read\",\"scheduler:operate\",\"scheduler:read\",\"signer:admin\",\"signer:read\",\"signer:rotate\",\"signer:sign\",\"timeline:read\",\"timeline:write\",\"trust:admin\",\"trust:read\",\"trust:write\",\"ui.admin\",\"ui.preferences.read\",\"ui.preferences.write\",\"ui.read\",\"vex:read\",\"vexhub:read\",\"vuln:audit\",\"vuln:investigate\",\"vuln:operate\",\"vuln:view\"],\"audiences\":[],\"authenticationTimeEpochMs\":1773571008000,\"freshAuthActive\":false,\"freshAuthExpiresAtEpochMs\":null}" + ], + [ + "stella-search-session-id", + "0e63533e-d5fe-49ed-93ce-a979890bd32d" + ] + ] + }, + "events": { + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [] + }, + "statePath": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\live-release-confidence-journey.state.json" +} diff --git a/src/Web/StellaOps.Web/output/playwright/live-release-confidence-journey.json b/src/Web/StellaOps.Web/output/playwright/live-release-confidence-journey.json new file mode 100644 index 000000000..337b5dcf2 --- /dev/null +++ b/src/Web/StellaOps.Web/output/playwright/live-release-confidence-journey.json @@ -0,0 +1,181 @@ +{ + "generatedAtUtc": "2026-03-15T10:36:51.420Z", + "baseUrl": "https://stella-ops.local", + "scope": { + "tenant": "demo-prod", + "regions": "us-east", + "environments": "stage", + "timeWindow": "7d" + }, + "steps": [ + { + "name": "01-releases-overview-to-deployments", + "ok": true, + "durationMs": 5182, + "issues": [], + "snapshot": { + "key": "01-releases-overview-to-deployments", + "url": "https://stella-ops.local/releases/deployments?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Deployment History - Stella Ops QA 1773540847164", + "heading": "Deployments", + "alerts": [], + "screenshotPath": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\release-confidence-journey\\01-releases-overview-to-deployments.png" + } + }, + { + "name": "02-deployment-detail-evidence-and-replay", + "ok": true, + "durationMs": 24509, + "issues": [], + "snapshot": { + "key": "02-deployment-detail-evidence-and-replay", + "url": "https://stella-ops.local/evidence/verify-replay?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&releaseId=v1.2.5&returnTo=%2Freleases%2Fdeployments%2FDEP-2026-050%3Ftenant%3Ddemo-prod%26regions%3Dus-east%26environments%3Dstage%26timeWindow%3D7d", + "title": "Verify & Replay - Stella Ops QA 1773540847164", + "heading": "Verdict Replay", + "alerts": [], + "screenshotPath": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\release-confidence-journey\\02-deployment-detail-evidence-and-replay.png", + "detailHeading": "DEP-2026-050", + "evidenceHref": "/evidence/capsules?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&evidenceId=evd-2026-049&returnTo=%2Freleases%2Fdeployments%2FDEP-2026-050%3Ftenant%3Ddemo-prod%26regions%3Dus-east%26environments%3Dstage%26timeWindow%3D7d", + "replayUrl": "https://stella-ops.local/evidence/verify-replay?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&releaseId=v1.2.5&returnTo=%2Freleases%2Fdeployments%2FDEP-2026-050%3Ftenant%3Ddemo-prod%26regions%3Dus-east%26environments%3Dstage%26timeWindow%3D7d" + } + }, + { + "name": "02b-approval-detail-decision-cockpit", + "ok": true, + "durationMs": 26789, + "issues": [], + "snapshot": { + "key": "02b-approval-detail-decision-cockpit", + "url": "https://stella-ops.local/ops/operations/data-integrity/scan-pipeline?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Scan Pipeline Health - StellaOps - Stella Ops QA 1773540847164", + "heading": "Scan Pipeline Health", + "alerts": [], + "screenshotPath": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\release-confidence-journey\\02b-approval-detail-decision-cockpit.png", + "detailUrl": "https://stella-ops.local/releases/approvals/apr-001?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + } + }, + { + "name": "03-decision-capsules-search-and-detail", + "ok": true, + "durationMs": 4860, + "issues": [], + "snapshot": { + "key": "03-decision-capsules-search-and-detail", + "url": "https://stella-ops.local/evidence/capsules?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Decision Capsules - Stella Ops QA 1773540847164", + "heading": "Decision Capsules", + "alerts": [], + "screenshotPath": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\release-confidence-journey\\03-decision-capsules-search-and-detail.png", + "listUrl": "https://stella-ops.local/evidence/capsules?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d" + } + }, + { + "name": "04-security-posture-to-triage-workspace", + "ok": true, + "durationMs": 6874, + "issues": [], + "snapshot": { + "key": "04-security-posture-to-triage-workspace", + "url": "https://stella-ops.local/security/triage?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&pivot=cve", + "title": "Security Triage - Stella Ops QA 1773540847164", + "heading": "Security / Triage", + "alerts": [], + "screenshotPath": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\release-confidence-journey\\04-security-posture-to-triage-workspace.png", + "triageUrl": "https://stella-ops.local/security/triage?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&pivot=cve" + } + }, + { + "name": "05-advisories-vex-tabs", + "ok": true, + "durationMs": 7381, + "issues": [], + "snapshot": { + "key": "05-advisories-vex-tabs", + "url": "https://stella-ops.local/security/advisories-vex?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&tab=issuer-trust", + "title": "Advisories & VEX - Stella Ops QA 1773540847164", + "heading": "Security / Advisories & VEX", + "alerts": [], + "screenshotPath": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\release-confidence-journey\\05-advisories-vex-tabs.png", + "visitedTabs": [ + "Providers", + "VEX Library", + "Issuer Trust" + ] + } + }, + { + "name": "06-reachability-tabs", + "ok": true, + "durationMs": 5856, + "issues": [], + "snapshot": { + "key": "06-reachability-tabs", + "url": "https://stella-ops.local/security/reachability/poe?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&tab=poe", + "title": "Proof Of Exposure - Stella Ops QA 1773540847164", + "heading": "Reachability", + "alerts": [], + "screenshotPath": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\release-confidence-journey\\06-reachability-tabs.png", + "finalUrl": "https://stella-ops.local/security/reachability/poe?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&tab=poe" + } + }, + { + "name": "07-security-reports-embedded-tabs", + "ok": true, + "durationMs": 8937, + "issues": [], + "snapshot": { + "key": "07-security-reports-embedded-tabs", + "url": "https://stella-ops.local/security/reports?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Security Reports - Stella Ops QA 1773540847164", + "heading": "Security Reports", + "alerts": [], + "screenshotPath": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\release-confidence-journey\\07-security-reports-embedded-tabs.png", + "visitedTabs": [ + "Risk Report", + "VEX Ledger", + "Evidence Export" + ] + } + }, + { + "name": "08-release-promotion-submit", + "ok": true, + "durationMs": 6146, + "issues": [], + "snapshot": { + "key": "08-release-promotion-submit", + "url": "https://stella-ops.local/releases/promotions/apr-005?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Promotion Detail - Stella Ops QA 1773540847164", + "heading": "Platform Release 1.2.3", + "alerts": [], + "screenshotPath": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\release-confidence-journey\\08-release-promotion-submit.png", + "promoteUrl": "https://stella-ops.local/releases/promotions/apr-005?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "promoteStatus": 200 + } + }, + { + "name": "09-hotfix-review-and-create", + "ok": true, + "durationMs": 6723, + "issues": [], + "snapshot": { + "key": "09-hotfix-review-and-create", + "url": "https://stella-ops.local/releases/versions/new?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&type=hotfix&hotfixLane=true", + "title": "Create Release Version - Stella Ops QA 1773540847164", + "heading": "Create Release Version", + "alerts": [], + "screenshotPath": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\release-confidence-journey\\09-hotfix-review-and-create.png", + "createUrl": "https://stella-ops.local/releases/versions/new?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&type=hotfix&hotfixLane=true" + } + } + ], + "runtime": { + "consoleErrors": [], + "pageErrors": [], + "requestFailures": [], + "responseErrors": [] + }, + "failedStepCount": 0, + "runtimeIssueCount": 0, + "runtimeIssues": [] +} diff --git a/src/Web/StellaOps.Web/output/playwright/live-release-confidence-journey.state.json b/src/Web/StellaOps.Web/output/playwright/live-release-confidence-journey.state.json new file mode 100644 index 000000000..a1541bb7e --- /dev/null +++ b/src/Web/StellaOps.Web/output/playwright/live-release-confidence-journey.state.json @@ -0,0 +1,18 @@ +{ + "cookies": [], + "origins": [ + { + "origin": "https://stella-ops.local", + "localStorage": [ + { + "name": "stellaops.sidebar.preferences", + "value": "{\"sidebarCollapsed\":false,\"collapsedGroups\":[],\"collapsedSections\":[]}" + }, + { + "name": "stellaops.theme", + "value": "system" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/Web/StellaOps.Web/output/playwright/live-release-create-journey.json b/src/Web/StellaOps.Web/output/playwright/live-release-create-journey.json new file mode 100644 index 000000000..181b76065 --- /dev/null +++ b/src/Web/StellaOps.Web/output/playwright/live-release-create-journey.json @@ -0,0 +1,37 @@ +{ + "checkedAtUtc": "2026-03-15T10:35:59.662Z", + "journeys": [ + { + "label": "standard-create", + "route": "/releases/versions/new?type=standard", + "type": "standard", + "initialUrl": "https://stella-ops.local/releases/versions/new?type=standard&tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "finalUrl": "https://stella-ops.local/releases/bundles/b2f319b7-b600-4c29-aed1-081e6593fd10/versions/619da2c2-4158-425b-9495-f24eaed1ac68?type=standard&tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&source=release-create&returnTo=%2Freleases%2Fversions", + "heading": "Create Release Version", + "searchQuery": "checkout-api", + "alerts": [], + "responseErrors": [], + "scopeIssues": [], + "screenshotPath": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\release-create-journey\\standard-create.png", + "ok": true + }, + { + "label": "hotfix-create", + "route": "/releases/hotfixes/new", + "type": "hotfix", + "initialUrl": "https://stella-ops.local/releases/versions/new?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&type=hotfix&hotfixLane=true", + "finalUrl": "https://stella-ops.local/releases/bundles/f65aebaf-d9d9-4d8e-90b5-c20873348fba/versions/fbb61d55-7cfc-4515-a7c5-687ae62cbbb2?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&type=hotfix&hotfixLane=true&source=release-create&returnTo=%2Freleases%2Fversions", + "heading": "Create Release Version", + "searchQuery": "checkout-api", + "alerts": [], + "responseErrors": [], + "scopeIssues": [], + "screenshotPath": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\release-create-journey\\hotfix-create.png", + "ok": true + } + ], + "failedCheckCount": 0, + "failedChecks": [], + "runtimeIssueCount": 0, + "runtimeIssues": [] +} diff --git a/src/Web/StellaOps.Web/output/playwright/live-setup-admin-action-sweep.json b/src/Web/StellaOps.Web/output/playwright/live-setup-admin-action-sweep.json new file mode 100644 index 000000000..aa5e74593 --- /dev/null +++ b/src/Web/StellaOps.Web/output/playwright/live-setup-admin-action-sweep.json @@ -0,0 +1,124 @@ +{ + "generatedAtUtc": "2026-03-15T10:39:33.082Z", + "durationMs": 35012, + "results": [ + { + "action": "tenant-branding-editor", + "ok": true, + "titleEditable": true, + "applyDisabledBefore": true, + "applyDisabledAfter": false, + "snapshot": { + "label": "branding-after-edit", + "url": "https://stella-ops.local/setup/tenant-branding?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Tenant & Branding - Stella Ops QA 1773540847164", + "heading": "Branding Configuration", + "alerts": [] + } + }, + { + "action": "notifications-create-channel-route", + "ok": true, + "channelRouteOk": true, + "createDisabledWithoutSecret": true, + "createDisabledWithSecret": false, + "snapshot": { + "label": "notifications-create-channel-route", + "url": "https://stella-ops.local/setup/notifications/channels", + "title": "Notifications - Stella Ops QA 1773540847164", + "heading": "Notification Administration", + "alerts": [] + } + }, + { + "action": "notifications-create-rule", + "ok": true, + "channelOptions": [ + "Select channel...", + " qa-email-1773571140610 (Email) " + ], + "snapshot": { + "label": "notifications-create-rule", + "url": "https://stella-ops.local/setup/notifications/rules/new?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Notifications - Stella Ops QA 1773540847164", + "heading": "Notification Administration", + "alerts": [] + } + }, + { + "action": "notifications-delete-created-channel", + "ok": true, + "snapshot": { + "label": "notifications-delete-created-channel", + "url": "https://stella-ops.local/setup/notifications/channels?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Notifications - Stella Ops QA 1773540847164", + "heading": "Notification Administration", + "alerts": [] + } + }, + { + "action": "usage-configure-quotas", + "ok": true, + "snapshot": { + "label": "usage-configure-quotas", + "url": "https://stella-ops.local/ops/operations/quotas", + "title": "Quotas & Limits - Stella Ops QA 1773540847164", + "heading": "Operator Quota Dashboard", + "alerts": [] + } + }, + { + "action": "system-view-details", + "ok": true, + "snapshot": { + "label": "system-View Details", + "url": "https://stella-ops.local/ops/operations/system-health", + "title": "System Health - Stella Ops QA 1773540847164", + "heading": "System Health", + "alerts": [] + } + }, + { + "action": "system-run-doctor", + "ok": true, + "snapshot": { + "label": "system-Run Doctor", + "url": "https://stella-ops.local/ops/operations/doctor", + "title": "Doctor Diagnostics - Stella Ops QA 1773540847164", + "heading": "Doctor Diagnostics", + "alerts": [] + } + }, + { + "action": "system-view-slos", + "ok": true, + "snapshot": { + "label": "system-View SLOs", + "url": "https://stella-ops.local/ops/operations/health-slo", + "title": "Health & SLO - Stella Ops QA 1773540847164", + "heading": "Platform Health", + "alerts": [] + } + }, + { + "action": "system-view-jobs", + "ok": true, + "snapshot": { + "label": "system-View Jobs", + "url": "https://stella-ops.local/ops/operations/jobs-queues", + "title": "Jobs & Queues - Stella Ops QA 1773540847164", + "heading": "Jobs & Queues", + "alerts": [] + } + } + ], + "runtime": { + "consoleErrors": [], + "pageErrors": [], + "requestFailures": [], + "responseErrors": [] + }, + "failedActionCount": 0, + "runtimeIssueCount": 0, + "runtimeIssues": [] +} diff --git a/src/Web/StellaOps.Web/output/playwright/live-uncovered-surface-action-sweep.auth.json b/src/Web/StellaOps.Web/output/playwright/live-uncovered-surface-action-sweep.auth.json new file mode 100644 index 000000000..0895ede93 --- /dev/null +++ b/src/Web/StellaOps.Web/output/playwright/live-uncovered-surface-action-sweep.auth.json @@ -0,0 +1,39 @@ +{ + "authenticatedAtUtc": "2026-03-15T10:39:48.828Z", + "baseUrl": "https://stella-ops.local", + "finalUrl": "https://stella-ops.local/mission-control/board?tenant=demo-prod®ions=apac,eu-west,us-east,us-west", + "title": "Dashboard - StellaOps", + "cookies": [], + "storage": { + "localStorageEntries": [ + [ + "stellaops.sidebar.preferences", + "{\"sidebarCollapsed\":false,\"collapsedGroups\":[],\"collapsedSections\":[]}" + ], + [ + "stellaops.theme", + "system" + ] + ], + "sessionStorageEntries": [ + [ + "stellaops.auth.session.full", + "{\"tokens\":{\"accessToken\":\"eyJhbGciOiJSUzI1NiIsImtpZCI6IlRBSU1GTlE3LUFOR1JMU0JOQVlfQ19OSEdPVERVWUo5UlNVVTRSRUQiLCJ0eXAiOiJhdCtqd3QifQ.eyJpc3MiOiJodHRwczovL2F1dGhvcml0eS5zdGVsbGEtb3BzLmxvY2FsLyIsImV4cCI6MTc3MzU3Mjk4NiwiaWF0IjoxNzczNTcxMTg2LCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGVtYWlsIG9mZmxpbmVfYWNjZXNzIHVpLnJlYWQgdWkuYWRtaW4gdWkucHJlZmVyZW5jZXMucmVhZCB1aS5wcmVmZXJlbmNlcy53cml0ZSBhdXRob3JpdHk6dGVuYW50cy5yZWFkIGF1dGhvcml0eTp0ZW5hbnRzLndyaXRlIGF1dGhvcml0eTp1c2Vycy5yZWFkIGF1dGhvcml0eTp1c2Vycy53cml0ZSBhdXRob3JpdHk6cm9sZXMucmVhZCBhdXRob3JpdHk6cm9sZXMud3JpdGUgYXV0aG9yaXR5OmNsaWVudHMucmVhZCBhdXRob3JpdHk6Y2xpZW50cy53cml0ZSBhdXRob3JpdHk6dG9rZW5zLnJlYWQgYXV0aG9yaXR5OnRva2Vucy5yZXZva2UgYXV0aG9yaXR5OmJyYW5kaW5nLnJlYWQgYXV0aG9yaXR5OmJyYW5kaW5nLndyaXRlIGF1dGhvcml0eS5hdWRpdC5yZWFkIGdyYXBoOnJlYWQgc2JvbTpyZWFkIHNjYW5uZXI6cmVhZCBwb2xpY3k6cmVhZCBwb2xpY3k6c2ltdWxhdGUgcG9saWN5OmF1dGhvciBwb2xpY3k6cmV2aWV3IHBvbGljeTphcHByb3ZlIHBvbGljeTpydW4gcG9saWN5OmFjdGl2YXRlIHBvbGljeTphdWRpdCBwb2xpY3k6ZWRpdCBwb2xpY3k6b3BlcmF0ZSBwb2xpY3k6cHVibGlzaCBhaXJnYXA6c2VhbCBhaXJnYXA6c3RhdHVzOnJlYWQgb3JjaDpyZWFkIG9yY2g6b3BlcmF0ZSBvcmNoOnF1b3RhIGFuYWx5dGljcy5yZWFkIGFkdmlzb3J5OnJlYWQgYWR2aXNvcnktYWk6dmlldyBhZHZpc29yeS1haTpvcGVyYXRlIHZleDpyZWFkIHZleGh1YjpyZWFkIGV4Y2VwdGlvbnM6cmVhZCBleGNlcHRpb25zOmFwcHJvdmUgYW9jOnZlcmlmeSBmaW5kaW5nczpyZWFkIHJlbGVhc2U6cmVhZCByZWxlYXNlOndyaXRlIHJlbGVhc2U6cHVibGlzaCBzY2hlZHVsZXI6cmVhZCBzY2hlZHVsZXI6b3BlcmF0ZSBub3RpZnkudmlld2VyIG5vdGlmeS5vcGVyYXRvciBub3RpZnkuYWRtaW4gbm90aWZ5LmVzY2FsYXRlIGV2aWRlbmNlOnJlYWQgZXhwb3J0LnZpZXdlciBleHBvcnQub3BlcmF0b3IgZXhwb3J0LmFkbWluIHZ1bG46dmlldyB2dWxuOmludmVzdGlnYXRlIHZ1bG46b3BlcmF0ZSB2dWxuOmF1ZGl0IHBsYXRmb3JtLmNvbnRleHQucmVhZCBwbGF0Zm9ybS5jb250ZXh0LndyaXRlIGRvY3RvcjpydW4gZG9jdG9yOmFkbWluIG9wcy5oZWFsdGggaW50ZWdyYXRpb246cmVhZCBpbnRlZ3JhdGlvbjp3cml0ZSBpbnRlZ3JhdGlvbjpvcGVyYXRlIHBhY2tzLnJlYWQgcGFja3Mud3JpdGUgcGFja3MucnVuIHBhY2tzLmFwcHJvdmUgcmVnaXN0cnkuYWRtaW4gdGltZWxpbmU6cmVhZCB0aW1lbGluZTp3cml0ZSB0cnVzdDpyZWFkIHRydXN0OndyaXRlIHRydXN0OmFkbWluIHNpZ25lcjpyZWFkIHNpZ25lcjpzaWduIHNpZ25lcjpyb3RhdGUgc2lnbmVyOmFkbWluIiwianRpIjoiNWU1NGE4NjMtNTczYi00ZDBmLWExMWMtMTkzZTEwNmQ0Y2E1Iiwic3ViIjoiYjA4NjM5NzQ1ZDY1NDkzNDhkODQzYWEzMTFjOTg5NTgiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJhZG1pbiIsIm5hbWUiOiJhZG1pbiIsInJvbGUiOiJhZG1pbiIsInN0ZWxsYW9wczp0ZW5hbnQiOiJkZW1vLXByb2QiLCJhdXRoX3RpbWUiOjE3NzM1NzExODYsIm9pX3Byc3QiOiJzdGVsbGEtb3BzLXVpIiwiY2xpZW50X2lkIjoic3RlbGxhLW9wcy11aSJ9.QFgw5zdKz5vJFnYA57kSeHjyERtx2-kWr4biKyg1SCrV-Ef5fcDrBdlUj2YUse4pQ_kr-Ecb4JzfYSC--Zb32RUoBRSiclTGTlR_6b82EI7RSCsC4lrDpCV2W6YN7aHCADLDxakF9gdaboBt77XziEWo--sLSNZOJ-Ot2SPYAsK_7OIRc2FHtYEnSoyDoZwmMp38clu5hT-dcT_Srl7X6s-CSNxijiMJnb40SBXW5NkRHCitewTzEUj9WWTxAhbKhHthX5bYxQuPEmr2WTXWsd4KmvkdiD7A0GryMa4ydsg6TjKo8oVbnuVVWXUrJBQLKdSueXaY7PqDKBprHCpMwA\",\"tokenType\":\"Bearer\",\"refreshToken\":\"eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZDQkMtSFM1MTIiLCJraWQiOiJXUjVQSU4tVzI5S0lNTkpRTVBBMU9XS0VYN0tEWjZMV0dYTDdSS0dWIiwidHlwIjoib2lfcmVmdCtqd3QiLCJjdHkiOiJKV1QifQ.wGlwhhsI9jc8fl7UMu9IiHCRfXg_3EOo9SP6dHXIGI_ESi9wgpfDWLrUzib9ylqnEKlDbDh2NlF5btRal7MQiaPJk5yRmTOBhriKXSPZv774u5YFS7xtL-uIUdhiK9r0q584u77OJVppY1lHB5Y-ds5TE0OCdwzEAuxDjnoUItML0_cesnaZi0dB6RRcFva3rX8ATsR_h6vKbRWTQfzWNlK0mPKOI1Sf1sU3YqDAWzWHbdW2wdSmQCWqvirWJ56NHR1xAMdlT3b91DUsk45PeBEQEjhnDT4RBFsb39WpsvdKwhJt08YbR2UOrExrTPs2Crovj39TkeUIVc6BY7bUtw.5eU7XXGAX2WTC-8-ZSPyZQ.fog1M4P7a-vur4uhseG7nqZcJI9LEh_iKMeTmuVePoT_JYLH6QYtU6cQZB18kECJiYrPb8yh2BAPeIpCsXHCvveB-54XMsZySSdIKZfaOw2KDLVHHrohCfl01BDuLX3kHYaDlarQNQWAZsI0m8fKRNRq2jUPHb_Qv94ePRai3JQDLfHT0426pLBpg4_uKOkB3_rIeDBPDI-7JdlQkV64oUxRDpo6ij1T4gYnBZNMGPxh1hiHFnCWMOG45DnzWBVjkkL7-lBkWiljNGDecHuAIm-gkqLtQxnoUGbR1vH2ugV0dE51s4Tg2IoCVcfl_pTyjJiup2bsA8NlfYUOHTS7ns7ItghIiCjg7_cxZhytGZjsBQfDOC2paJ9tNX96Qol8hUgzXWu8A154fBdqqTr5jOAY0BGPty9nYKMI-luRpojZiZiGTIfHvbnSAOgp-A40zGLTQiuC6x7ApTYEK0Td5_cC0r08gTpfztRJPOuccpC54HCG99zqqGPLTwa-Od14tPhmO-0WZFy8XHuIhpt02g0ZfMogZJMEF86Z6M_Ww92XidjRqRjlNdzhto1qLSfOvjCy-a96WxA6WOAY9-8PiYqpmrTbpPiTb7nF01H4A1WFO2-dob_CcApFhYedUEFhs70IAfzuSwQi2u3hQDIsw61TDvXfv1upMlVZpXedytwtjvlAiHqNj20RXwbjWi7X7RcproqxYUsTByYL_q0u4TQwQ1G_rCjt9jfBw3zyIL4D0cv2P7nldAFQszhH4L-IZ3dO_N2uM-E54xFY5aZXTUhm9A4SxBvKH5rcNh-Or6rz5y4_LAVkmndaKkO14cSMQ2JQN8Qd_kB9Z5wJxGgsTpls32e8KrOzFe1AoRAxqCeDojEXXsZZCo7XkKTOxw0KrbDpstR-3f4HCtT473mmqk1XpFGPOUq4rBdi2xOYeeoZo_WyNW522qw0d_1LbLYmYY4rabdTb4sdk5oX2hAWPVrSF5EKX7rEBZ9iMMeBdfSqEPc_AFYH07Tfmu9ukTq4TaMchPBMhuTEXBc4UYp5Wk-A27cHaSemkWQOJ2UMxz_knGzVMOcKHhG0Cu3tKmA_w30RU6ZjZDlJ4rTcWkE_gnh2zV2Rx2LgTffih4xPmMV1CEteWbWoUUW55RUP61LCpONDzU_kN3PX-y1fEn1wre5dKJzDvbn1UioQ9CJBEoKqlLNH_bcvKF3GwsuQibrUAq3l-zoD3OiTg8MxS9xW-hsWfegsfmqdofAe5BWzOsBOyQF5ODcNU1ST3Xq-ONJ0Nuw3YkxFPWy-NCx3HdnzjM_5Qu19Ht4cZwnS15oCTINsDMjgEp9xUXLIZx-b5VnwiGp1Tcq-WX5a0RVHC_9L9Zjywxola9WjfmITDvD1mmmu7c8N-btaLe8ODhFdhKAL88j3sTHV3rqyJviU9qAql6ZQbt0AH5Ky9tiEJl9mYUr8-NF78HQsrN01NFNUv-kdzmrW-vKLBSmVHKo-68g0ex2RHzxSgMT6NVSGxLNbO9CqVbnrHDIDtNnEbJnHWgHEx4EWvxpjzDdsShN74k5yUkj6NVtKq7VO7raOz6aPGqfP9_Du9IsEcLoAAQuP9W9Nnz2JmXlkRRXA0HO3rBel1MywbAdxqFClang1OZpV2Nkhw4FC1e3t2Cp7eBzOdhZrO-UjdrbktRX02dJ0F5JMmeEIJXRSd0_rdOBOKjgI1V3nz2bVb_fMehKgJr7MNujbc_bp375XAHGSc_KaieWmUmXcP1h0hP0JE636_zIV5XlfbqjXNBxiR98aMJyqxPVvOTqEqz7ptkhcIvNkJe4c-TbyRaUARRvaaWc6uAG-jYZOjDmcdwGejDcK8rdbnIfT29ZrE_U8zPt3D6NuFDWPgeZJgn7kMweII4_kivHllE-REkvXBgO7kOLpXr-KPzeNIEqHoIdIXyhsmK7PDLE1H1Hi6MqgBWSHdE6QUVitIIX1a5SP1QDpDg3F9rNlsSutpc0R81hnXKurtzZvvQ1vOOrqCrl86kIjGcAm6JQmVtmXD-I4pysUgclXFwXG1c56vo4vT_WCER0ybZxwDDDngl3Wqh-43gOMxPb-pMxvKAn8_e3WUsd2It50iWyfTPQnT33VFUYsQKPuMxBHBVOY_Vn4K0mcDWJRhm8Bfp5mvWwB0uceZ3fwqvObOqZDomqfyy4skTYiJ8953My0hU7ARvLn1i_9l1cZaozbR_TQyNaKFy8wRv_7C8K1mpFLTOwXpY1eQ1VN-wBYt2KcAqTHyFzswc04rlwsSbjOxXgxx9saZFG1FHIqqPny2KUT9iwiFKFGbwoc78Hjo0kydp5ktE3xPSBanp146jL66mvWLyDjTN2fOYde85mc1sK7v2JLXv1bCPB42F2Z9XpwD9lPyV5mGXdp5a3OP3b3lXPX6eJ-zXmzAyeiyUan7zRj_UC8pIAsvOaoPcLajnj4L9DivMYAbNBb7oE195mUCm9WgHegF37noBiYs0U4R8iDhcbPFJLaO0e0z17KxW5vDEtRSxmIZD3xXta3__FeQsvwtxC04rQ0RCGKpgIZYXkJgCTjYc_uGNyWAPRwtVFjuRapQTUQm4oNFGwkXKJh5YbT3ULumzr35ZG4bIIASdON9xneoEKSQAzxjL1duLrVV6DR_Ia0lz0A2rUP23qGSMd7a921la_lWK_qQS6o6F4_2GFgZcvgF3lB7YLwF-T2LzpCxheLe0YAB9liX879RvlKxCqA-X7R9umq34c-MA2svJvRQbePOu62gLed-njLtkzKe4IkD-OZCbHOMctaQFJVE2XH6jYrmIZBRJVfHVkKXSAmIej2BjCjb5N3HYigYXhlHGHcfBsnb9fA2wrqEXT-3Kc5kexkiILCIjG3LIRibx_syq52XPt6xqY2js2r_SsIndKOo_ckbZu5YgJBZbCs-xWyRaAQvL67kmVAxos0Td216dMv3kO7Frk6-lyC21ZWR8OYsne6_D0TA493hmEm-B2AlsLopDdTp1mvl6skbf06mXnD_s9sVD0AyBx_0wwRaUuPiv14JIW3iboBwrahCpuihxmami2IOXL5kVlP_7sjcLlRzkxHm3M1yqBbZfeWA6mneYpqSg1CF2Uh9f-_2sITuzb3nrVpI61yaMn0rAZXdKmUuvwzO4pp3QiFBMoYddCSnNupydT_NkUDVECB7hx5Y4pGE4P0GYIh6CrbJsubumW3M70UOmhk7j8RPgtoXhFDVw5oH-v4LSETUjhQDDCOIyPSC_7UFve4LD6KLhfx_6b7vLZcblAGohHsIreU0ZpN1GwS5NRm84XS-OhUb-K5pQCQlFVRBr1RnTBBQFqIECQegNhkhsKpsQciDmqv7V4-7Tfyx5hhfVD0H_USaiakj5WnvM_6YIOVcCqP9LPTlAOdYv6SLQMSocwPWwa2Vo_DTBnhuVvzb802wIxsUd0aAHw7PjwokEDGkIRLIOE1AyjpSE5RRugG7eIEpl75WDSw1Z-WaGItP25rtD41-4bbGC5JrZnewaLWla4PzAl04nbss1ed1fgGocUIb1hMC5A-wolIAWmXY_qUXXdg47z7K-L72z_uRl9hot4XoGZpfavFnIYMgXBdoKmrHPtlbVyPMk7MB3nT9arXeRLAuPPxM7x3qeIa-FrCJjBJX15bfh1ytwDm4e7WBqz6ofS2crOKmzdlS7OMpmPeEy7qtOSr52OVCoqRJKdON3JhwkcCr5rLAN_6leMgH3v2oND9zIGWX4UtBbn7mFG2kub0GRC-bWJ22xHVYnCayceTvItcoP6-5mN1ypD1Oo2LyJCwHNg1gYyxYyWPiEvyErIpgry3lR7_VEtUcZZssSLIZ6gyIUNYgeRXblZ31buRNTE3kM755MI3xFnF5BiYyxs-zj1zMQRnTSmrBORqGLKe3eGotqhjgchWbhDwydH28nDCvUimGO46oRdVpDnS2R09ot6qX4dC-Sf-TqRzpz19rjNzZs9EleVBOD7hS1eM5fHvNNOdDpxRAIm2bNY_J4doHbfjgtQ9ng84NoRCdhBB0awwETxrvQxNghZgnQUYLKUZp5LcwlOxAl00L8bT2X0r1jgH5PVC1aD7Uii4GZXj6hb8nkf8rIBHiI2ccdSTseMNWZgSzu4ogcWuoF0fzswgA1ZA_U9agVPsZl9byNSF5daN5JnQlvcw-mVtyWhu1tQlQTACTgtHLzvDP2SXjWs51qd1WMmQwplpOI9DkJbh09HPCngVqVdd6p8BT7BA6cH9EeIP4RNFPbDJwHh_OPBBkUBrpHLwkHeWwDxstj-QMapmLB4FNqJO647faG3pI-_eKpZ7LcUjrHLhwfhXF0ZaUvGsYHf0MuWtuEo1bszLUoiQwcFPWaZve5GnexLX2WgEb4ML3xl9tgaiS80etaXaSCHpush_u2m6KDB0qOZBHf4REz65feyUEzdt1xDGO9GXZLnqn5jZLq6XtcexeOU4PVWTlhRcpHMx_Rn2mIUKUkXJ6hnZB-617_5efODiuKjBSmsOtEmmzpu2hEPtzWZryHGZ-S-aSR2AgvSBSbHZqnmX0vYzhjqiovfZvwci_dglm5rmyQWUo6CT-NvJLG3WY1e1Rukt4Do6rVcMqNPuB8jGi6wj_EhYCWn-XAxPZQG4ar3pyUyE77luEEy7LNC7GoTFu_IocsL7c0tuHPPB187HfDHsDUj1hibB8zjJofnuwEVOQPSnfGwUCZAm7xeYlEF5vfeIW-FKRlAzBka2ISxvm4CTtK_28PC2fyUdrdb00A.dB4lg9Tkx_yxTke1mD8gv82a6BFMVA22GJaqmwcpHQ0\",\"scope\":\"openid profile email offline_access ui.read ui.admin ui.preferences.read ui.preferences.write authority:tenants.read authority:tenants.write authority:users.read authority:users.write authority:roles.read authority:roles.write authority:clients.read authority:clients.write authority:tokens.read authority:tokens.revoke authority:branding.read authority:branding.write authority.audit.read graph:read sbom:read scanner:read policy:read policy:simulate policy:author policy:review policy:approve policy:run policy:activate policy:audit policy:edit policy:operate policy:publish airgap:seal airgap:status:read orch:read orch:operate orch:quota analytics.read advisory:read advisory-ai:view advisory-ai:operate vex:read vexhub:read exceptions:read exceptions:approve aoc:verify findings:read release:read release:write release:publish scheduler:read scheduler:operate notify.viewer notify.operator notify.admin notify.escalate evidence:read export.viewer export.operator export.admin vuln:view vuln:investigate vuln:operate vuln:audit platform.context.read platform.context.write doctor:run doctor:admin ops.health integration:read integration:write integration:operate packs.read packs.write packs.run packs.approve registry.admin timeline:read timeline:write trust:read trust:write trust:admin signer:read signer:sign signer:rotate signer:admin\",\"expiresAtEpochMs\":1773572985175},\"identity\":{\"subject\":\"b08639745d6549348d843aa311c98958\",\"name\":\"admin\",\"roles\":[],\"idToken\":\"eyJhbGciOiJSUzI1NiIsImtpZCI6IlRBSU1GTlE3LUFOR1JMU0JOQVlfQ19OSEdPVERVWUo5UlNVVTRSRUQiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2F1dGhvcml0eS5zdGVsbGEtb3BzLmxvY2FsLyIsImV4cCI6MTc3MzU3NDQ4NiwiaWF0IjoxNzczNTcxMTg2LCJhdWQiOiJzdGVsbGEtb3BzLXVpIiwic3ViIjoiYjA4NjM5NzQ1ZDY1NDkzNDhkODQzYWEzMTFjOTg5NTgiLCJuYW1lIjoiYWRtaW4iLCJhenAiOiJzdGVsbGEtb3BzLXVpIiwibm9uY2UiOiI3YmIzYTAyMi1lZDA2LTQzZGItYmJmMi1lNmRmOTNmYWRiYjciLCJhdF9oYXNoIjoiQkFvbmVlN1p2UzRjenhHQjJwalo5QSJ9.CzwSbNaiI62Zuhghnmwj3MXqhvzAPGvwT64LUlWczcycPuBIy2E1aioGufnNSyi5Ty5JiLY47OAXpr7oMn9yuzV83Utb2amDn8I7je4JCHVHqpp-ZEYPaoWjiM-XM7v2hOpJ82XXpM2Y0pqxY2pSQiWypRA9zvidea_7ZEcyVKA1VMSrfrxmLxWwVIwHErrYBB2RfqSn6n614nIYcKYG7mE1r8wqiwfZoTvcw8GlT3nKokYcP2iOvbG_t9yLzTMsu3AApUzqICBDdm3TDZSuXb6oXHdRJg6Sth6hPLLlUX0y5l1DvMImyoZm3-54sxUBRw0SAIjz8-7C-aYJ8uPT-g\"},\"dpopKeyThumbprint\":\"OjeWjW4eU-ym-DJlw9NQlLEDi85vCJF1cRYG68qybaE\",\"issuedAtEpochMs\":1773571186176,\"tenantId\":\"demo-prod\",\"scopes\":[\"advisory-ai:operate\",\"advisory-ai:view\",\"advisory:read\",\"airgap:seal\",\"airgap:status:read\",\"analytics.read\",\"aoc:verify\",\"authority.audit.read\",\"authority:branding.read\",\"authority:branding.write\",\"authority:clients.read\",\"authority:clients.write\",\"authority:roles.read\",\"authority:roles.write\",\"authority:tenants.read\",\"authority:tenants.write\",\"authority:tokens.read\",\"authority:tokens.revoke\",\"authority:users.read\",\"authority:users.write\",\"doctor:admin\",\"doctor:run\",\"email\",\"evidence:read\",\"exceptions:approve\",\"exceptions:read\",\"export.admin\",\"export.operator\",\"export.viewer\",\"findings:read\",\"graph:read\",\"integration:operate\",\"integration:read\",\"integration:write\",\"notify.admin\",\"notify.escalate\",\"notify.operator\",\"notify.viewer\",\"offline_access\",\"openid\",\"ops.health\",\"orch:operate\",\"orch:quota\",\"orch:read\",\"packs.approve\",\"packs.read\",\"packs.run\",\"packs.write\",\"platform.context.read\",\"platform.context.write\",\"policy:activate\",\"policy:approve\",\"policy:audit\",\"policy:author\",\"policy:edit\",\"policy:operate\",\"policy:publish\",\"policy:read\",\"policy:review\",\"policy:run\",\"policy:simulate\",\"profile\",\"registry.admin\",\"release:publish\",\"release:read\",\"release:write\",\"sbom:read\",\"scanner:read\",\"scheduler:operate\",\"scheduler:read\",\"signer:admin\",\"signer:read\",\"signer:rotate\",\"signer:sign\",\"timeline:read\",\"timeline:write\",\"trust:admin\",\"trust:read\",\"trust:write\",\"ui.admin\",\"ui.preferences.read\",\"ui.preferences.write\",\"ui.read\",\"vex:read\",\"vexhub:read\",\"vuln:audit\",\"vuln:investigate\",\"vuln:operate\",\"vuln:view\"],\"audiences\":[],\"authenticationTimeEpochMs\":1773571186000,\"freshAuthActive\":false,\"freshAuthExpiresAtEpochMs\":null}" + ], + [ + "stellaops.auth.session.info", + "{\"subject\":\"b08639745d6549348d843aa311c98958\",\"expiresAtEpochMs\":1773572985175,\"issuedAtEpochMs\":1773571186176,\"dpopKeyThumbprint\":\"OjeWjW4eU-ym-DJlw9NQlLEDi85vCJF1cRYG68qybaE\",\"tenantId\":\"demo-prod\"}" + ], + [ + "stella-search-session-id", + "901560ad-a30e-4adc-b698-503d2c99401e" + ] + ] + }, + "events": { + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [] + }, + "statePath": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\live-uncovered-surface-action-sweep.state.json" +} diff --git a/src/Web/StellaOps.Web/output/playwright/live-uncovered-surface-action-sweep.json b/src/Web/StellaOps.Web/output/playwright/live-uncovered-surface-action-sweep.json new file mode 100644 index 000000000..13445f0de --- /dev/null +++ b/src/Web/StellaOps.Web/output/playwright/live-uncovered-surface-action-sweep.json @@ -0,0 +1,921 @@ +{ + "generatedAtUtc": "2026-03-15T10:44:32.322Z", + "baseUrl": "https://stella-ops.local", + "actionCount": 69, + "passedActionCount": 69, + "failedActionCount": 0, + "failedActions": [], + "results": [ + { + "action": "/releases/overview -> link:Release Versions", + "ok": true, + "expectedPath": "/releases/versions", + "finalUrl": "https://stella-ops.local/releases/versions?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "snapshot": { + "label": "/releases/overview -> link:Release Versions", + "url": "https://stella-ops.local/releases/versions?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Release Versions - Stella Ops QA 1773540847164", + "heading": "Release Versions", + "alerts": [] + } + }, + { + "action": "/releases/overview -> link:Release Runs", + "ok": true, + "expectedPath": "/releases/runs", + "finalUrl": "https://stella-ops.local/releases/runs?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "snapshot": { + "label": "/releases/overview -> link:Release Runs", + "url": "https://stella-ops.local/releases/runs?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Release Runs - Stella Ops QA 1773540847164", + "heading": "Release Runs", + "alerts": [] + } + }, + { + "action": "/releases/overview -> link:Approvals Queue", + "ok": true, + "expectedPath": "/releases/approvals", + "finalUrl": "https://stella-ops.local/releases/approvals?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "snapshot": { + "label": "/releases/overview -> link:Approvals Queue", + "url": "https://stella-ops.local/releases/approvals?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Approvals - Stella Ops QA 1773540847164", + "heading": "Release Run Approvals Queue", + "alerts": [] + } + }, + { + "action": "/releases/overview -> link:Hotfixes", + "ok": true, + "expectedPath": "/releases/hotfixes", + "finalUrl": "https://stella-ops.local/releases/hotfixes", + "snapshot": { + "label": "/releases/overview -> link:Hotfixes", + "url": "https://stella-ops.local/releases/hotfixes", + "title": "Hotfixes - Stella Ops QA 1773540847164", + "heading": "Hotfixes", + "alerts": [] + } + }, + { + "action": "/releases/overview -> link:Promotions", + "ok": true, + "expectedPath": "/releases/promotions", + "finalUrl": "https://stella-ops.local/releases/promotions", + "snapshot": { + "label": "/releases/overview -> link:Promotions", + "url": "https://stella-ops.local/releases/promotions", + "title": "Promotions - Stella Ops QA 1773540847164", + "heading": "Promotions", + "alerts": [] + } + }, + { + "action": "/releases/overview -> link:Deployment History", + "ok": true, + "expectedPath": "/releases/deployments", + "finalUrl": "https://stella-ops.local/releases/deployments?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "snapshot": { + "label": "/releases/overview -> link:Deployment History", + "url": "https://stella-ops.local/releases/deployments?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Deployment History - Stella Ops QA 1773540847164", + "heading": "Deployments", + "alerts": [] + } + }, + { + "action": "/releases/runs -> link:Timeline", + "ok": true, + "expectedPath": "/releases/runs?view=timeline", + "finalUrl": "https://stella-ops.local/releases/runs?view=timeline", + "snapshot": { + "label": "/releases/runs -> link:Timeline", + "url": "https://stella-ops.local/releases/runs?view=timeline", + "title": "Release Runs - Stella Ops QA 1773540847164", + "heading": "Release Runs", + "alerts": [] + } + }, + { + "action": "/releases/runs -> link:Table", + "ok": true, + "expectedPath": "/releases/runs?view=table", + "finalUrl": "https://stella-ops.local/releases/runs?view=table", + "snapshot": { + "label": "/releases/runs -> link:Table", + "url": "https://stella-ops.local/releases/runs?view=table", + "title": "Release Runs - Stella Ops QA 1773540847164", + "heading": "Release Runs", + "alerts": [] + } + }, + { + "action": "/releases/runs -> link:Correlations", + "ok": true, + "expectedPath": "/releases/runs?view=correlations", + "finalUrl": "https://stella-ops.local/releases/runs?view=correlations", + "snapshot": { + "label": "/releases/runs -> link:Correlations", + "url": "https://stella-ops.local/releases/runs?view=correlations", + "title": "Release Runs - Stella Ops QA 1773540847164", + "heading": "Release Runs", + "alerts": [] + } + }, + { + "action": "/releases/approvals -> link:Pending", + "ok": true, + "expectedPath": "/releases/approvals?tab=pending", + "finalUrl": "https://stella-ops.local/releases/approvals?tab=pending", + "snapshot": { + "label": "/releases/approvals -> link:Pending", + "url": "https://stella-ops.local/releases/approvals?tab=pending", + "title": "Approvals - Stella Ops QA 1773540847164", + "heading": "Release Run Approvals Queue", + "alerts": [] + } + }, + { + "action": "/releases/approvals -> link:Approved", + "ok": true, + "expectedPath": "/releases/approvals?tab=approved", + "finalUrl": "https://stella-ops.local/releases/approvals?tab=approved", + "snapshot": { + "label": "/releases/approvals -> link:Approved", + "url": "https://stella-ops.local/releases/approvals?tab=approved", + "title": "Approvals - Stella Ops QA 1773540847164", + "heading": "Release Run Approvals Queue", + "alerts": [] + } + }, + { + "action": "/releases/approvals -> link:Rejected", + "ok": true, + "expectedPath": "/releases/approvals?tab=rejected", + "finalUrl": "https://stella-ops.local/releases/approvals?tab=rejected", + "snapshot": { + "label": "/releases/approvals -> link:Rejected", + "url": "https://stella-ops.local/releases/approvals?tab=rejected", + "title": "Approvals - Stella Ops QA 1773540847164", + "heading": "Release Run Approvals Queue", + "alerts": [] + } + }, + { + "action": "/releases/approvals -> link:Expiring", + "ok": true, + "expectedPath": "/releases/approvals?tab=expiring", + "finalUrl": "https://stella-ops.local/releases/approvals?tab=expiring", + "snapshot": { + "label": "/releases/approvals -> link:Expiring", + "url": "https://stella-ops.local/releases/approvals?tab=expiring", + "title": "Approvals - Stella Ops QA 1773540847164", + "heading": "Release Run Approvals Queue", + "alerts": [] + } + }, + { + "action": "/releases/approvals -> link:My Team", + "ok": true, + "expectedPath": "/releases/approvals?tab=my-team", + "finalUrl": "https://stella-ops.local/releases/approvals?tab=my-team", + "snapshot": { + "label": "/releases/approvals -> link:My Team", + "url": "https://stella-ops.local/releases/approvals?tab=my-team", + "title": "Approvals - Stella Ops QA 1773540847164", + "heading": "Release Run Approvals Queue", + "alerts": [] + } + }, + { + "action": "/releases/environments -> link:Open Environment", + "ok": true, + "expectedPath": "/setup/topology/environments/stage/posture", + "finalUrl": "https://stella-ops.local/setup/topology/environments/stage/posture?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&environment=stage", + "snapshot": { + "label": "/releases/environments -> link:Open Environment", + "url": "https://stella-ops.local/setup/topology/environments/stage/posture?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&environment=stage", + "title": "Environment Detail - Stella Ops QA 1773540847164", + "heading": "Topology", + "alerts": [] + } + }, + { + "action": "/releases/environments -> link:Open Targets", + "ok": true, + "expectedPath": "/setup/topology/targets", + "finalUrl": "https://stella-ops.local/setup/topology/targets?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&environment=stage", + "snapshot": { + "label": "/releases/environments -> link:Open Targets", + "url": "https://stella-ops.local/setup/topology/targets?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&environment=stage", + "title": "Targets - Stella Ops QA 1773540847164", + "heading": "Topology", + "alerts": [] + } + }, + { + "action": "/releases/environments -> link:Open Agents", + "ok": true, + "expectedPath": "/setup/topology/agents", + "finalUrl": "https://stella-ops.local/setup/topology/agents?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&environment=stage", + "snapshot": { + "label": "/releases/environments -> link:Open Agents", + "url": "https://stella-ops.local/setup/topology/agents?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&environment=stage", + "title": "Agent Fleet - Stella Ops QA 1773540847164", + "heading": "Topology", + "alerts": [] + } + }, + { + "action": "/releases/environments -> link:Open Runs", + "ok": true, + "expectedPath": "/releases/runs", + "finalUrl": "https://stella-ops.local/releases/runs?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&environment=stage", + "snapshot": { + "label": "/releases/environments -> link:Open Runs", + "url": "https://stella-ops.local/releases/runs?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&environment=stage", + "title": "Release Runs - Stella Ops QA 1773540847164", + "heading": "Release Runs", + "alerts": [] + } + }, + { + "action": "/releases/investigation/deploy-diff -> link:Open Deployments", + "ok": true, + "expectedPath": "/releases/deployments", + "finalUrl": "https://stella-ops.local/releases/deployments?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "snapshot": { + "label": "/releases/investigation/deploy-diff -> link:Open Deployments", + "url": "https://stella-ops.local/releases/deployments?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Deployment History - Stella Ops QA 1773540847164", + "heading": "Deployments", + "alerts": [] + } + }, + { + "action": "/releases/investigation/deploy-diff -> link:Open Releases Overview", + "ok": true, + "expectedPath": "/releases/overview", + "finalUrl": "https://stella-ops.local/releases/overview?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "snapshot": { + "label": "/releases/investigation/deploy-diff -> link:Open Releases Overview", + "url": "https://stella-ops.local/releases/overview?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Release Ops Overview - Stella Ops QA 1773540847164", + "heading": "Release Ops Overview", + "alerts": [] + } + }, + { + "action": "/releases/investigation/change-trace -> link:Open Deployments", + "ok": true, + "expectedPath": "/releases/deployments", + "finalUrl": "https://stella-ops.local/releases/deployments?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "snapshot": { + "label": "/releases/investigation/change-trace -> link:Open Deployments", + "url": "https://stella-ops.local/releases/deployments?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Deployment History - Stella Ops QA 1773540847164", + "heading": "Deployments", + "alerts": [] + } + }, + { + "action": "/security/posture -> link:Open triage", + "ok": true, + "expectedPath": "/security/triage", + "finalUrl": "https://stella-ops.local/security/triage?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&pivot=cve", + "snapshot": { + "label": "/security/posture -> link:Open triage", + "url": "https://stella-ops.local/security/triage?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&pivot=cve", + "title": "Security Triage - Stella Ops QA 1773540847164", + "heading": "Security / Triage", + "alerts": [] + } + }, + { + "action": "/security/posture -> link:Disposition", + "ok": true, + "expectedPath": "/security/disposition", + "finalUrl": "https://stella-ops.local/security/disposition?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&tab=vex-library", + "snapshot": { + "label": "/security/posture -> link:Disposition", + "url": "https://stella-ops.local/security/disposition?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&tab=vex-library", + "title": "Disposition Center - Stella Ops QA 1773540847164", + "heading": "Security / Advisories & VEX", + "alerts": [] + } + }, + { + "action": "/security/posture -> link:Configure sources", + "ok": true, + "expectedPath": "/ops/integrations/advisory-vex-sources", + "finalUrl": "https://stella-ops.local/ops/integrations/advisory-vex-sources?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "snapshot": { + "label": "/security/posture -> link:Configure sources", + "url": "https://stella-ops.local/ops/integrations/advisory-vex-sources?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Advisory & VEX Sources - Stella Ops QA 1773540847164", + "heading": "Integrations", + "alerts": [] + } + }, + { + "action": "/security/posture -> link:Open reachability coverage board", + "ok": true, + "expectedPath": "/security/reachability", + "finalUrl": "https://stella-ops.local/security/reachability?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "snapshot": { + "label": "/security/posture -> link:Open reachability coverage board", + "url": "https://stella-ops.local/security/reachability?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Reachability - Stella Ops QA 1773540847164", + "heading": "Reachability", + "alerts": [] + } + }, + { + "action": "/security/advisories-vex -> link:Providers", + "ok": true, + "expectedPath": "/security/advisories-vex?tab=providers", + "finalUrl": "https://stella-ops.local/security/advisories-vex?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&tab=providers", + "snapshot": { + "label": "/security/advisories-vex -> link:Providers", + "url": "https://stella-ops.local/security/advisories-vex?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&tab=providers", + "title": "Advisories & VEX - Stella Ops QA 1773540847164", + "heading": "Security / Advisories & VEX", + "alerts": [ + "Doctor Run Complete1 failed, 1 warnings View Details" + ] + } + }, + { + "action": "/security/advisories-vex -> link:VEX Library", + "ok": true, + "expectedPath": "/security/advisories-vex?tab=vex-library", + "finalUrl": "https://stella-ops.local/security/advisories-vex?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&tab=vex-library", + "snapshot": { + "label": "/security/advisories-vex -> link:VEX Library", + "url": "https://stella-ops.local/security/advisories-vex?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&tab=vex-library", + "title": "Advisories & VEX - Stella Ops QA 1773540847164", + "heading": "Security / Advisories & VEX", + "alerts": [] + } + }, + { + "action": "/security/advisories-vex -> link:Issuer Trust", + "ok": true, + "expectedPath": "/security/advisories-vex?tab=issuer-trust", + "finalUrl": "https://stella-ops.local/security/advisories-vex?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&tab=issuer-trust", + "snapshot": { + "label": "/security/advisories-vex -> link:Issuer Trust", + "url": "https://stella-ops.local/security/advisories-vex?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&tab=issuer-trust", + "title": "Advisories & VEX - Stella Ops QA 1773540847164", + "heading": "Security / Advisories & VEX", + "alerts": [] + } + }, + { + "action": "/security/disposition -> link:Conflicts", + "ok": true, + "expectedPath": "/security/disposition?tab=conflicts", + "finalUrl": "https://stella-ops.local/security/disposition?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&tab=conflicts", + "snapshot": { + "label": "/security/disposition -> link:Conflicts", + "url": "https://stella-ops.local/security/disposition?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&tab=conflicts", + "title": "Disposition Center - Stella Ops QA 1773540847164", + "heading": "Security / Advisories & VEX", + "alerts": [] + } + }, + { + "action": "/security/supply-chain-data -> link:SBOM Graph", + "ok": true, + "expectedPath": "/security/supply-chain-data/graph", + "finalUrl": "https://stella-ops.local/security/supply-chain-data/graph", + "snapshot": { + "label": "/security/supply-chain-data -> link:SBOM Graph", + "url": "https://stella-ops.local/security/supply-chain-data/graph", + "title": "Supply-Chain Data - Stella Ops QA 1773540847164", + "heading": "Security / Supply-Chain Data", + "alerts": [] + } + }, + { + "action": "/security/supply-chain-data -> link:Reachability", + "ok": true, + "expectedPath": "/security/reachability", + "finalUrl": "https://stella-ops.local/security/reachability", + "snapshot": { + "label": "/security/supply-chain-data -> link:Reachability", + "url": "https://stella-ops.local/security/reachability", + "title": "Reachability - Stella Ops QA 1773540847164", + "heading": "Reachability", + "alerts": [] + } + }, + { + "action": "/evidence/overview -> link:Audit Log", + "ok": true, + "expectedPath": "/evidence/audit-log", + "finalUrl": "https://stella-ops.local/evidence/audit-log", + "snapshot": { + "label": "/evidence/overview -> link:Audit Log", + "url": "https://stella-ops.local/evidence/audit-log", + "title": "Audit Log - Stella Ops QA 1773540847164", + "heading": "Unified Audit Log", + "alerts": [] + } + }, + { + "action": "/evidence/overview -> link:Export Center", + "ok": true, + "expectedPath": "/evidence/exports", + "finalUrl": "https://stella-ops.local/evidence/exports", + "snapshot": { + "label": "/evidence/overview -> link:Export Center", + "url": "https://stella-ops.local/evidence/exports", + "title": "Export Center - Stella Ops QA 1773540847164", + "heading": "Export Center", + "alerts": [] + } + }, + { + "action": "/evidence/overview -> link:Replay & Verify", + "ok": true, + "expectedPath": "/evidence/verify-replay", + "finalUrl": "https://stella-ops.local/evidence/verify-replay", + "snapshot": { + "label": "/evidence/overview -> link:Replay & Verify", + "url": "https://stella-ops.local/evidence/verify-replay", + "title": "Verify & Replay - Stella Ops QA 1773540847164", + "heading": "Verdict Replay", + "alerts": [] + } + }, + { + "action": "/evidence/audit-log -> link:View All Events", + "ok": true, + "expectedPath": "/evidence/audit-log/events", + "finalUrl": "https://stella-ops.local/evidence/audit-log/events", + "snapshot": { + "label": "/evidence/audit-log -> link:View All Events", + "url": "https://stella-ops.local/evidence/audit-log/events", + "title": "Audit Log - Stella Ops QA 1773540847164", + "heading": "Audit Events", + "alerts": [] + } + }, + { + "action": "/evidence/audit-log -> link:Export", + "ok": true, + "expectedPath": "/evidence/exports", + "finalUrl": "https://stella-ops.local/evidence/exports", + "snapshot": { + "label": "/evidence/audit-log -> link:Export", + "url": "https://stella-ops.local/evidence/exports", + "title": "Export Center - Stella Ops QA 1773540847164", + "heading": "Export Center", + "alerts": [] + } + }, + { + "action": "/evidence/audit-log -> link:/Policy Audit/i", + "ok": true, + "expectedPath": "/evidence/audit-log/policy", + "finalUrl": "https://stella-ops.local/evidence/audit-log/policy", + "snapshot": { + "label": "/evidence/audit-log -> link:/Policy Audit/i", + "url": "https://stella-ops.local/evidence/audit-log/policy", + "title": "Audit Log - Stella Ops QA 1773540847164", + "heading": "Policy Audit Events", + "alerts": [] + } + }, + { + "action": "/ops/operations/health-slo -> link:View Full Timeline", + "ok": true, + "expectedPath": "/ops/operations/health-slo/incidents", + "finalUrl": "https://stella-ops.local/ops/operations/health-slo/incidents", + "snapshot": { + "label": "/ops/operations/health-slo -> link:View Full Timeline", + "url": "https://stella-ops.local/ops/operations/health-slo/incidents", + "title": "Health & SLO - Stella Ops QA 1773540847164", + "heading": "Incident Timeline", + "alerts": [] + } + }, + { + "action": "/ops/operations/feeds-airgap -> link:Configure Sources", + "ok": true, + "expectedPath": "/ops/integrations/advisory-vex-sources", + "finalUrl": "https://stella-ops.local/ops/integrations/advisory-vex-sources", + "snapshot": { + "label": "/ops/operations/feeds-airgap -> link:Configure Sources", + "url": "https://stella-ops.local/ops/integrations/advisory-vex-sources", + "title": "Advisory & VEX Sources - Stella Ops QA 1773540847164", + "heading": "Integrations", + "alerts": [] + } + }, + { + "action": "/ops/operations/feeds-airgap -> link:Open Offline Bundles", + "ok": true, + "expectedPath": "/ops/operations/offline-kit/bundles", + "finalUrl": "https://stella-ops.local/ops/operations/offline-kit/bundles", + "snapshot": { + "label": "/ops/operations/feeds-airgap -> link:Open Offline Bundles", + "url": "https://stella-ops.local/ops/operations/offline-kit/bundles", + "title": "Bundle Management - Stella Ops QA 1773540847164", + "heading": "Offline Kit Management", + "alerts": [] + } + }, + { + "action": "/ops/operations/feeds-airgap -> link:Version Locks", + "ok": true, + "expectedPath": "/ops/operations/feeds-airgap?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&tab=version-locks", + "finalUrl": "https://stella-ops.local/ops/operations/feeds-airgap?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&tab=version-locks", + "snapshot": { + "label": "/ops/operations/feeds-airgap -> link:Version Locks", + "url": "https://stella-ops.local/ops/operations/feeds-airgap?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&tab=version-locks", + "title": "Feeds & Airgap - Stella Ops QA 1773540847164", + "heading": "Feeds & Airgap", + "alerts": [] + } + }, + { + "action": "/ops/operations/data-integrity -> link:Feeds Freshness WARN Impact: BLOCKING NVD feed stale by 3h 12m", + "ok": true, + "expectedPath": "/ops/operations/data-integrity/feeds-freshness", + "finalUrl": "https://stella-ops.local/ops/operations/data-integrity/feeds-freshness", + "snapshot": { + "label": "/ops/operations/data-integrity -> link:Feeds Freshness WARN Impact: BLOCKING NVD feed stale by 3h 12m", + "url": "https://stella-ops.local/ops/operations/data-integrity/feeds-freshness", + "title": "Feeds Freshness - StellaOps - Stella Ops QA 1773540847164", + "heading": "Feeds Freshness", + "alerts": [] + } + }, + { + "action": "/ops/operations/data-integrity -> link:Hotfix 1.2.4", + "ok": true, + "expectedPath": "/releases/approvals?releaseId=rel-hotfix-124", + "finalUrl": "https://stella-ops.local/releases/approvals?releaseId=rel-hotfix-124", + "snapshot": { + "label": "/ops/operations/data-integrity -> link:Hotfix 1.2.4", + "url": "https://stella-ops.local/releases/approvals?releaseId=rel-hotfix-124", + "title": "Approvals - Stella Ops QA 1773540847164", + "heading": "Release Run Approvals Queue", + "alerts": [] + } + }, + { + "action": "/ops/operations/jobengine -> link:Scheduler Runs", + "ok": true, + "expectedPath": "/ops/operations/scheduler/runs", + "finalUrl": "https://stella-ops.local/ops/operations/scheduler/runs", + "snapshot": { + "label": "/ops/operations/jobengine -> link:Scheduler Runs", + "url": "https://stella-ops.local/ops/operations/scheduler/runs", + "title": "Scheduler - Stella Ops QA 1773540847164", + "heading": "Scheduler Runs", + "alerts": [] + } + }, + { + "action": "/ops/operations/jobengine -> link:/Execution Quotas/i", + "ok": true, + "expectedPath": "/ops/operations/jobengine/quotas", + "finalUrl": "https://stella-ops.local/ops/operations/jobengine/quotas", + "snapshot": { + "label": "/ops/operations/jobengine -> link:/Execution Quotas/i", + "url": "https://stella-ops.local/ops/operations/jobengine/quotas", + "title": "JobEngine Quotas - Stella Ops QA 1773540847164", + "heading": "JobEngine Quotas", + "alerts": [] + } + }, + { + "action": "/ops/operations/offline-kit -> link:Bundles", + "ok": true, + "expectedPath": "/ops/operations/offline-kit/bundles", + "finalUrl": "https://stella-ops.local/ops/operations/offline-kit/bundles", + "snapshot": { + "label": "/ops/operations/offline-kit -> link:Bundles", + "url": "https://stella-ops.local/ops/operations/offline-kit/bundles", + "title": "Bundle Management - Stella Ops QA 1773540847164", + "heading": "Offline Kit Management", + "alerts": [] + } + }, + { + "action": "/ops/operations/offline-kit -> link:JWKS", + "ok": true, + "expectedPath": "/ops/operations/offline-kit/jwks", + "finalUrl": "https://stella-ops.local/ops/operations/offline-kit/jwks", + "snapshot": { + "label": "/ops/operations/offline-kit -> link:JWKS", + "url": "https://stella-ops.local/ops/operations/offline-kit/jwks", + "title": "JWKS Management - Stella Ops QA 1773540847164", + "heading": "Offline Kit Management", + "alerts": [] + } + }, + { + "action": "/releases/versions -> button:Create Release Version", + "ok": true, + "expectedPath": "/releases/versions/new", + "finalUrl": "https://stella-ops.local/releases/versions/new?type=standard", + "snapshot": { + "label": "/releases/versions -> button:Create Release Version", + "url": "https://stella-ops.local/releases/versions/new?type=standard", + "title": "Create Release Version - Stella Ops QA 1773540847164", + "heading": "Create Release Version", + "alerts": [] + } + }, + { + "action": "/releases/versions -> button:Create Hotfix Run", + "ok": true, + "expectedPath": "/releases/versions/new", + "finalUrl": "https://stella-ops.local/releases/versions/new?type=hotfix&hotfixLane=true", + "snapshot": { + "label": "/releases/versions -> button:Create Hotfix Run", + "url": "https://stella-ops.local/releases/versions/new?type=hotfix&hotfixLane=true", + "title": "Create Release Version - Stella Ops QA 1773540847164", + "heading": "Create Release Version", + "alerts": [] + } + }, + { + "action": "/releases/versions -> button:Search", + "ok": true, + "expectedPath": null, + "finalUrl": "https://stella-ops.local/releases/versions?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "snapshot": { + "label": "/releases/versions -> button:Search", + "url": "https://stella-ops.local/releases/versions?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Release Versions - Stella Ops QA 1773540847164", + "heading": "Release Versions", + "alerts": [] + } + }, + { + "action": "/releases/versions -> button:Clear", + "ok": true, + "reason": "disabled-by-design", + "expectedPath": null, + "finalUrl": "https://stella-ops.local/releases/versions?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "snapshot": { + "label": "/releases/versions -> button:Clear", + "url": "https://stella-ops.local/releases/versions?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Release Versions - Stella Ops QA 1773540847164", + "heading": "Release Versions", + "alerts": [] + } + }, + { + "action": "/releases/investigation/timeline -> button:Export", + "ok": true, + "reason": "disabled-by-design", + "expectedPath": null, + "finalUrl": "https://stella-ops.local/releases/investigation/timeline?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "snapshot": { + "label": "/releases/investigation/timeline -> button:Export", + "url": "https://stella-ops.local/releases/investigation/timeline?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Investigation Timeline - Stella Ops QA 1773540847164", + "heading": "Timeline", + "alerts": [] + } + }, + { + "action": "/releases/investigation/change-trace -> button:Export", + "ok": true, + "reason": "disabled-by-design", + "expectedPath": null, + "finalUrl": "https://stella-ops.local/releases/investigation/change-trace?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "snapshot": { + "label": "/releases/investigation/change-trace -> button:Export", + "url": "https://stella-ops.local/releases/investigation/change-trace?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Change Trace - Stella Ops QA 1773540847164", + "heading": "Change Trace", + "alerts": [] + } + }, + { + "action": "/security/sbom-lake -> button:Refresh", + "ok": true, + "expectedPath": null, + "finalUrl": "https://stella-ops.local/security/sbom-lake?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "snapshot": { + "label": "/security/sbom-lake -> button:Refresh", + "url": "https://stella-ops.local/security/sbom-lake?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "SBOM Lake - Stella Ops QA 1773540847164", + "heading": "SBOM Lake", + "alerts": [] + } + }, + { + "action": "/security/sbom-lake -> button:Clear", + "ok": true, + "expectedPath": null, + "finalUrl": "https://stella-ops.local/security/sbom-lake?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&days=30", + "snapshot": { + "label": "/security/sbom-lake -> button:Clear", + "url": "https://stella-ops.local/security/sbom-lake?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&days=30", + "title": "SBOM Lake - Stella Ops QA 1773540847164", + "heading": "SBOM Lake", + "alerts": [] + } + }, + { + "action": "/security/reachability -> button:Witnesses", + "ok": true, + "expectedPath": null, + "finalUrl": "https://stella-ops.local/security/reachability?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "snapshot": { + "label": "/security/reachability -> button:Witnesses", + "url": "https://stella-ops.local/security/reachability?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Reachability - Stella Ops QA 1773540847164", + "heading": "Reachability", + "alerts": [] + } + }, + { + "action": "/security/reachability -> button:/PoE|Proof of Exposure/i", + "ok": true, + "expectedPath": null, + "finalUrl": "https://stella-ops.local/security/reachability/poe?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&tab=poe", + "snapshot": { + "label": "/security/reachability -> button:/PoE|Proof of Exposure/i", + "url": "https://stella-ops.local/security/reachability/poe?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d&tab=poe", + "title": "Proof Of Exposure - Stella Ops QA 1773540847164", + "heading": "Reachability", + "alerts": [] + } + }, + { + "action": "/evidence/capsules -> button:Search", + "ok": true, + "expectedPath": null, + "finalUrl": "https://stella-ops.local/evidence/capsules?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "snapshot": { + "label": "/evidence/capsules -> button:Search", + "url": "https://stella-ops.local/evidence/capsules?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Decision Capsules - Stella Ops QA 1773540847164", + "heading": "Decision Capsules", + "alerts": [] + } + }, + { + "action": "/evidence/threads -> button:Search", + "ok": true, + "expectedPath": null, + "finalUrl": "https://stella-ops.local/evidence/threads?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "snapshot": { + "label": "/evidence/threads -> button:Search", + "url": "https://stella-ops.local/evidence/threads?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Evidence Threads - Stella Ops QA 1773540847164", + "heading": "Evidence Threads", + "alerts": [] + } + }, + { + "action": "/evidence/proofs -> button:Search", + "ok": true, + "expectedPath": null, + "finalUrl": "https://stella-ops.local/evidence/proofs?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "snapshot": { + "label": "/evidence/proofs -> button:Search", + "url": "https://stella-ops.local/evidence/proofs?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Proof Chains - Stella Ops QA 1773540847164", + "heading": "Proof Chains", + "alerts": [] + } + }, + { + "action": "/ops/operations/system-health -> button:Services", + "ok": true, + "expectedPath": null, + "finalUrl": "https://stella-ops.local/ops/operations/system-health?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "snapshot": { + "label": "/ops/operations/system-health -> button:Services", + "url": "https://stella-ops.local/ops/operations/system-health?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "System Health - Stella Ops QA 1773540847164", + "heading": "System Health", + "alerts": [] + } + }, + { + "action": "/ops/operations/system-health -> button:Incidents", + "ok": true, + "expectedPath": null, + "finalUrl": "https://stella-ops.local/ops/operations/system-health?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "snapshot": { + "label": "/ops/operations/system-health -> button:Incidents", + "url": "https://stella-ops.local/ops/operations/system-health?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "System Health - Stella Ops QA 1773540847164", + "heading": "System Health", + "alerts": [] + } + }, + { + "action": "/ops/operations/system-health -> button:Quick Diagnostics", + "ok": true, + "expectedPath": null, + "finalUrl": "https://stella-ops.local/ops/operations/system-health?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "snapshot": { + "label": "/ops/operations/system-health -> button:Quick Diagnostics", + "url": "https://stella-ops.local/ops/operations/system-health?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "System Health - Stella Ops QA 1773540847164", + "heading": "System Health", + "alerts": [] + } + }, + { + "action": "/ops/operations/scheduler -> button:Manage Schedules", + "ok": true, + "expectedPath": "/ops/operations/scheduler/schedules", + "finalUrl": "https://stella-ops.local/ops/operations/scheduler/schedules", + "snapshot": { + "label": "/ops/operations/scheduler -> button:Manage Schedules", + "url": "https://stella-ops.local/ops/operations/scheduler/schedules", + "title": "Scheduler - Stella Ops QA 1773540847164", + "heading": "Schedule Management", + "alerts": [] + } + }, + { + "action": "/ops/operations/scheduler -> button:Worker Fleet", + "ok": true, + "expectedPath": "/ops/operations/scheduler/workers", + "finalUrl": "https://stella-ops.local/ops/operations/scheduler/workers", + "snapshot": { + "label": "/ops/operations/scheduler -> button:Worker Fleet", + "url": "https://stella-ops.local/ops/operations/scheduler/workers", + "title": "Scheduler - Stella Ops QA 1773540847164", + "heading": "Worker Fleet", + "alerts": [] + } + }, + { + "action": "/ops/operations/doctor -> button:Quick Check", + "ok": true, + "expectedPath": null, + "finalUrl": "https://stella-ops.local/ops/operations/doctor?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "snapshot": { + "label": "/ops/operations/doctor -> button:Quick Check", + "url": "https://stella-ops.local/ops/operations/doctor?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Doctor Diagnostics - Stella Ops QA 1773540847164", + "heading": "Doctor Diagnostics", + "alerts": [] + } + }, + { + "action": "/ops/operations/signals -> button:Refresh", + "ok": true, + "reason": "disabled-by-design", + "expectedPath": null, + "finalUrl": "https://stella-ops.local/ops/operations/signals?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "snapshot": { + "label": "/ops/operations/signals -> button:Refresh", + "url": "https://stella-ops.local/ops/operations/signals?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Signals - Stella Ops QA 1773540847164", + "heading": "Signals Runtime Dashboard", + "alerts": [] + } + }, + { + "action": "/ops/operations/packs -> button:Refresh", + "ok": true, + "reason": "disabled-by-design", + "expectedPath": null, + "finalUrl": "https://stella-ops.local/ops/operations/packs?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "snapshot": { + "label": "/ops/operations/packs -> button:Refresh", + "url": "https://stella-ops.local/ops/operations/packs?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Pack Registry - Stella Ops QA 1773540847164", + "heading": "Pack Registry Browser", + "alerts": [] + } + }, + { + "action": "/ops/operations/status -> button:Refresh", + "ok": true, + "expectedPath": null, + "finalUrl": "https://stella-ops.local/ops/operations/status?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "snapshot": { + "label": "/ops/operations/status -> button:Refresh", + "url": "https://stella-ops.local/ops/operations/status?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "System Status - Stella Ops QA 1773540847164", + "heading": "Console Status", + "alerts": [] + } + } + ], + "runtime": { + "consoleErrors": [], + "pageErrors": [], + "requestFailures": [], + "responseErrors": [] + }, + "runtimeIssueCount": 0 +} diff --git a/src/Web/StellaOps.Web/output/playwright/live-uncovered-surface-action-sweep.state.json b/src/Web/StellaOps.Web/output/playwright/live-uncovered-surface-action-sweep.state.json new file mode 100644 index 000000000..a1541bb7e --- /dev/null +++ b/src/Web/StellaOps.Web/output/playwright/live-uncovered-surface-action-sweep.state.json @@ -0,0 +1,18 @@ +{ + "cookies": [], + "origins": [ + { + "origin": "https://stella-ops.local", + "localStorage": [ + { + "name": "stellaops.sidebar.preferences", + "value": "{\"sidebarCollapsed\":false,\"collapsedGroups\":[],\"collapsedSections\":[]}" + }, + { + "name": "stellaops.theme", + "value": "system" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/Web/StellaOps.Web/output/playwright/live-user-reported-admin-trust-check.auth.json b/src/Web/StellaOps.Web/output/playwright/live-user-reported-admin-trust-check.auth.json new file mode 100644 index 000000000..13624dd34 --- /dev/null +++ b/src/Web/StellaOps.Web/output/playwright/live-user-reported-admin-trust-check.auth.json @@ -0,0 +1,39 @@ +{ + "authenticatedAtUtc": "2026-03-15T10:26:46.489Z", + "baseUrl": "https://stella-ops.local", + "finalUrl": "https://stella-ops.local/mission-control/board?tenant=demo-prod®ions=apac,eu-west,us-east,us-west", + "title": "Dashboard - StellaOps", + "cookies": [], + "storage": { + "localStorageEntries": [ + [ + "stellaops.sidebar.preferences", + "{\"sidebarCollapsed\":false,\"collapsedGroups\":[],\"collapsedSections\":[]}" + ], + [ + "stellaops.theme", + "system" + ] + ], + "sessionStorageEntries": [ + [ + "stellaops.auth.session.full", + "{\"tokens\":{\"accessToken\":\"eyJhbGciOiJSUzI1NiIsImtpZCI6IlRBSU1GTlE3LUFOR1JMU0JOQVlfQ19OSEdPVERVWUo5UlNVVTRSRUQiLCJ0eXAiOiJhdCtqd3QifQ.eyJpc3MiOiJodHRwczovL2F1dGhvcml0eS5zdGVsbGEtb3BzLmxvY2FsLyIsImV4cCI6MTc3MzU3MjIwMywiaWF0IjoxNzczNTcwNDAzLCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGVtYWlsIG9mZmxpbmVfYWNjZXNzIHVpLnJlYWQgdWkuYWRtaW4gdWkucHJlZmVyZW5jZXMucmVhZCB1aS5wcmVmZXJlbmNlcy53cml0ZSBhdXRob3JpdHk6dGVuYW50cy5yZWFkIGF1dGhvcml0eTp0ZW5hbnRzLndyaXRlIGF1dGhvcml0eTp1c2Vycy5yZWFkIGF1dGhvcml0eTp1c2Vycy53cml0ZSBhdXRob3JpdHk6cm9sZXMucmVhZCBhdXRob3JpdHk6cm9sZXMud3JpdGUgYXV0aG9yaXR5OmNsaWVudHMucmVhZCBhdXRob3JpdHk6Y2xpZW50cy53cml0ZSBhdXRob3JpdHk6dG9rZW5zLnJlYWQgYXV0aG9yaXR5OnRva2Vucy5yZXZva2UgYXV0aG9yaXR5OmJyYW5kaW5nLnJlYWQgYXV0aG9yaXR5OmJyYW5kaW5nLndyaXRlIGF1dGhvcml0eS5hdWRpdC5yZWFkIGdyYXBoOnJlYWQgc2JvbTpyZWFkIHNjYW5uZXI6cmVhZCBwb2xpY3k6cmVhZCBwb2xpY3k6c2ltdWxhdGUgcG9saWN5OmF1dGhvciBwb2xpY3k6cmV2aWV3IHBvbGljeTphcHByb3ZlIHBvbGljeTpydW4gcG9saWN5OmFjdGl2YXRlIHBvbGljeTphdWRpdCBwb2xpY3k6ZWRpdCBwb2xpY3k6b3BlcmF0ZSBwb2xpY3k6cHVibGlzaCBhaXJnYXA6c2VhbCBhaXJnYXA6c3RhdHVzOnJlYWQgb3JjaDpyZWFkIG9yY2g6b3BlcmF0ZSBvcmNoOnF1b3RhIGFuYWx5dGljcy5yZWFkIGFkdmlzb3J5OnJlYWQgYWR2aXNvcnktYWk6dmlldyBhZHZpc29yeS1haTpvcGVyYXRlIHZleDpyZWFkIHZleGh1YjpyZWFkIGV4Y2VwdGlvbnM6cmVhZCBleGNlcHRpb25zOmFwcHJvdmUgYW9jOnZlcmlmeSBmaW5kaW5nczpyZWFkIHJlbGVhc2U6cmVhZCByZWxlYXNlOndyaXRlIHJlbGVhc2U6cHVibGlzaCBzY2hlZHVsZXI6cmVhZCBzY2hlZHVsZXI6b3BlcmF0ZSBub3RpZnkudmlld2VyIG5vdGlmeS5vcGVyYXRvciBub3RpZnkuYWRtaW4gbm90aWZ5LmVzY2FsYXRlIGV2aWRlbmNlOnJlYWQgZXhwb3J0LnZpZXdlciBleHBvcnQub3BlcmF0b3IgZXhwb3J0LmFkbWluIHZ1bG46dmlldyB2dWxuOmludmVzdGlnYXRlIHZ1bG46b3BlcmF0ZSB2dWxuOmF1ZGl0IHBsYXRmb3JtLmNvbnRleHQucmVhZCBwbGF0Zm9ybS5jb250ZXh0LndyaXRlIGRvY3RvcjpydW4gZG9jdG9yOmFkbWluIG9wcy5oZWFsdGggaW50ZWdyYXRpb246cmVhZCBpbnRlZ3JhdGlvbjp3cml0ZSBpbnRlZ3JhdGlvbjpvcGVyYXRlIHBhY2tzLnJlYWQgcGFja3Mud3JpdGUgcGFja3MucnVuIHBhY2tzLmFwcHJvdmUgcmVnaXN0cnkuYWRtaW4gdGltZWxpbmU6cmVhZCB0aW1lbGluZTp3cml0ZSB0cnVzdDpyZWFkIHRydXN0OndyaXRlIHRydXN0OmFkbWluIHNpZ25lcjpyZWFkIHNpZ25lcjpzaWduIHNpZ25lcjpyb3RhdGUgc2lnbmVyOmFkbWluIiwianRpIjoiYzgyYjIzZTktOGE3YS00MzQ2LTk4YzUtNGMwMmJkODhmOTJiIiwic3ViIjoiYjA4NjM5NzQ1ZDY1NDkzNDhkODQzYWEzMTFjOTg5NTgiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJhZG1pbiIsIm5hbWUiOiJhZG1pbiIsInJvbGUiOiJhZG1pbiIsInN0ZWxsYW9wczp0ZW5hbnQiOiJkZW1vLXByb2QiLCJhdXRoX3RpbWUiOjE3NzM1NzA0MDMsIm9pX3Byc3QiOiJzdGVsbGEtb3BzLXVpIiwiY2xpZW50X2lkIjoic3RlbGxhLW9wcy11aSJ9.ZHSCvTT2oAc6Xlf_WJQrlyO6VgWvs_BUJCeNRj2tgLh4RD6uvcLE79gkYIhV8km7yV1BQerSifj1hxxYQxiaudCsALyz0SDkTL2UH37cp4uKDP3tUv4czQzG9xemTz_bxSx7b0cBWbwGi0dXxqmPJWmuKuTUQweJkDt0G2YQ7cd9AeOppFDAVWVODRhLWvI3sC1Jh8_j6C15kbrZ9uyChGoNXguWFZ6ZKfl2ypLbwlkHgwAxYd5ERiSraMx8gP3yXsO4SBk4Ijg4qF6po4FI0SHsmeYyivv3AFwDEviLzQ8DgaX2e-GtSPnoScMiVAfFpvWzbbejXk-_8dScYmwvww\",\"tokenType\":\"Bearer\",\"refreshToken\":\"eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZDQkMtSFM1MTIiLCJraWQiOiJXUjVQSU4tVzI5S0lNTkpRTVBBMU9XS0VYN0tEWjZMV0dYTDdSS0dWIiwidHlwIjoib2lfcmVmdCtqd3QiLCJjdHkiOiJKV1QifQ.VBwMWcY_JGA1VKUDR1V0_GMt78dOMAwQuksrZHFS3a1ArcfGDqeozrJYebkbIZkWdvwWXvIwy-1f6xwmR4zT4jAjHLe-N4jbR8ys_H6vEIzncoo8GYF-Cx9DP2HgJWhXD9j217nZPdHnugTYBkoZsXzJiulKZt3onZdulOWwsK0LkE37tckKlmnlaUzUV4uiyu-Qpwp4H0IqmgO85Iq8m9Sf6zIyUpyLZm56zQKzqJB7IFuJ1UW5_Q6HqANfkdAaijEMcaC_UhgtL1-oC_eDHab3nWbwYhJFF_7fzmeVKd5V88mKFYRdpfeJCEp2D4uA5VgaDKGJZtnUmFvNZFGBKg.EJFj-PqfWhhZ6QQZYNSuCQ.2ER0qI60v9sUZoydWYbAMdwj8LErtXRL3aD5AxEroYS5r48HD0MrSLvkeKKcjAd5XvuXEKNG7YwEXYf-H_WNG8eX6PNKGZcTST-9iH8H6ARAOct95iSwpyRz5lHQpgTBRh5B0o6t4h2ZGokF7Bh95QxM2497AVNvxXht8Cbw738-EpTKDWUiwENWovfF-wXNQBrcq8mpT_3lCcIVD_JBCyZw5riJ230a_djQvy65U-NBNHQC-ROfnneuGaqf476TTcKKcB_SNOOAXpN1OpqO8BXtA9BFGQ5a1X4z64i4KVUBrWymLpjTnuALVx2dYzVqc4Iza4VtskJrunTduFgjrL0ZbxAGmFf6rl6bp_HvwPO7RkstZhoFbrOzgPa_9jl8yyiOwMXww4iJ6MjZIllEkTuHYdeB15x0RqO12kF6eIxnMNJIWFgTPpyAURnPGAJbe9XB2JZqaBav6lsfYKIP41SEYBoRUwIdTW5HOi7_-jP-eVjMdxTyAtV5c_Zg4tzH6XMSoGagD_wRT7Ily6jlmXAP5a4h1mdzfKfAyJ3mb5WTvFViGYrULhSTDFJHyh4VKQtcelw91-1SbPGUcoRF0S6_n2Y9VyU8vDHHDi0MzuXq5dvXDOytP2gsg2UBki7cJHTbX9RG7odZ8tmUhq4ZZX6aPZDOv8SawAjkBZTCi_4SC3rrJXv-AmcPAtxtOT_nTgkrs7M2LSqn9AC3M80QahwaJSE0Dl9bALcZMDYhgboYMd2X-0lHFa8qXJB3kQYYdSX-_pSFTs8KtmZv1wjxRLGxjTD0T4Kot3kvNsMh8ralHOOPJ08us7gxlMQ0ZhUB8WktaOYH6sn7LDv3v4T20UN70-JU_BWUozUMEEYnSmY1uSCB7YbF85nhLaMv82FQw_wi5fr4UyVtnIUlWt-lOleNahceN_wRAJWNrzeBMLIpQP8X6FZ3zsV5CUbshEf0PWU8vgQmdVnnIlAg1-nneQT8A_hmbbW2hOQJdgMdlMrfV2gk3NXMve0tRZK0XN3mZEEXZTVmzCu4PDnXUAQaVOKbenjk9szLnOho-Iol6BiOQl5xt5WwJD2CsXk4RRmVGnYKD7uz3GKPc_uXbehC5aIRTLWbdyT-r5eCeI1O5vNYE-7zHri70i7chcHXFPESlF5JaGA--thyfl4UcP4kNaOTax-38YBBPSOqWTStDzksVNIRogj3EscX2OGa1SgxK6wpFL2hTqIseKICOFSctugXZca6VT1ebeW-50T6h3Nd6KDMpyl2DpcKhgvZMqWQMuIbUuD-88Mz-Em9bfFzVJJCQFwFvzAIJrdLZu_Ky7v-GCYgxzp0UBnbUPKf5tXbL1cuGkBTLrB55JE5y8Hqa5vnj3wGyIESGxeeFh3H-ZRsK9AeolUSlCFyHg-XHWLXYLweHb_hCnsxR4boDKQEjhXtzFCMM39WbIrU1Dh2SQSPixQvWqSZTYR9xu6wg4RStNGAq5QMmLVQUOmyxYigcCI0fM5ZEl8-gKsFyCGYMyDt-28mOK7ncVtf4cS0N9q18G5cGH_J-hlb__6poq-ArxtwQ0b0r7O1gQB2XFo8L3_f4yR6aks6yGzHnrKsmWVrceH9meK6Rxk7VedPWIEJYuQKnw7dXgQ2nEOk3vnExtCiexJmiq5zVMzohE-E8b-ZrOJpQE0-cK7N9JVgUJHVLp7f9lMT-yx05Jm6DNZnLeXHducF5ZtM7BP-jYJ4XgMj_1KReDG9GQPXxB3qXlF1WQYgbb8JwbzPXJvPXqwoUF6tp-7t7tSlalpj8dptxWG4BG4NOCg2ajwEP-g-yV7EtZMGG-6nkRXUqV-dfLvcKnYrqy7MjPBplLxMzSjlGoRZEPAI8sINVi4wlTg9HkC6DJsESKQAOS1oVi7fdSVow5pc7gFnzVAgRUZaMA6zEYx552HPMFZLkAD9FEonzRvm9KW6EOg6Z0XmODE636MYMksJbIMjwW-8fXiwuNYe979i8fjQaUHN2GMzxoCZLIjkipFFmAikyAJV_6M5shemo20KGKg6b2ZfsOBffMRP3OFuiWK9Nj3O0ibYddjlqCNw9VWKqWFlOO57DPk8SnS0DXw12HSFlJqWMXSkwGxm-s8s4akXUktAeXD20QAb_W2Sr9_2dt1I31ahNIaFzpl_zO6Pr7OWT4y3ZOaMvEF7HyIbnHxa9-2ALmTuG59xWjPkp6D38i3P9kxJDiiKgYu2MBWgbh4g7P_rxt5N8JnvoM_MDA4ezxaqN7Q_FR2fp60hUTr7LDl9YR2jFDRGerOs-5vYkJ2sZPFp2uHdt1p7g4DuxUm1xohahU2MjfDCarZlbxheVyZmJmcmbhd_BymmHabtIzf-SIn2gvHcP3yvKO1uqEIbeNOTTfshq2mBN61UDO70Pe1_wSEko4OHiAw2LS8bAo06nGedJy2lLHMp3bcPTQJ6r7eBoGpM7q2XhhRNAgsvjS4TJaYx0vOK1EOMuws-zlI0sv17BKljnc6aIucipbysV8Tjv4WGuyLVww4HJLkPU7HkVDCD07w2d9I9Xv9k38HNZD-SrhcPrRn-5g966cYOiAf-hHxl7weTPQCKT6WyjHphwoLdqV-MU9wxKR9y26LNBh-e2QsFH7-JWzA4O3eocsz0YTh1xgJIL7jEU_8mcGpk3dAdPSEaDUe-LcOUqX7diZHMbo0RAA-on8e1NizgwQ8Yxm9LX1UEgcnnW_wSWlfp-XLYk_hAGCm3ldvkGwbHCRga9wr7smQPqSnNPhJH4KogLXQKpBHJgQYsHhbQmwQ8IX5b8GXCrElBhBXJFzxLDzxuIFMtQdaQ6_02AoK6NdjxJHoun3blRI9Lv7mhg_FMLuG1bYZLKq5MrVwxs0Y4qgo9O50TpYw0jBNpoKMqzDVdRiDBcXd74OkV_pYE3nj1nxHxQCP4imLlK3oqZV7kAh5sfrEFnlHPZyHv6wnaObwgkKvktmspiuKVHFcR7mFHPGGMKKnV8Sz8oOcKMZ50opp17GlBRs_spP5lA6Deolh9V-T-JjYMQYlQbS3XAJ5exCWUgtM8o6xV0UeGdj1UWaSVyeUV_7-nHX7ObtODmXg40E6JVjFkt7B1Z3_-qeQwfeGEkkK9v3pP7BILZEWLKGsxTXDZLjhaCHZJfow-w74tZeoGSj01ag21Orbs1tNfLT4IXDGiby-6vqWYMkGRlj0YJcpaY266YZqtAbYWKDcLuaLw_fzMgfLKxXWSlgfD5fZJ0IphuZQOc91PPMnOd6EZMUiVjHnuBHthnpXqd9TmbwCC3h6wDj4FC388l7JQWGbzgt1_xfHVqYggs7VvziU9b7LTm7uSKcrFYNuE2PK9XYLohSTXUPsbfvkKMj_28nD9A_iwAg0EXztIYiVw0BwpOPuYN5DEAb6gnpllxJ8YtOlO1LW8A2tvGTxx4TadWPgHrk0T7-8dBTg3J18s3lv4TetfEnC2rDuGxZl9TIz6gg8GcbG5yV93LVthhTpzGHw1Mu4amQ_xUQ1xoIriVo2j0Vvl8G_Id9OP0yru71DX5IramX2_NwKmlQJa8j5oViksgYM5kDV8ciFSjfDXUp__5gqzhhbW38dMSiL_vwbYt9uHrOrhDAnqde8V7A0P642LFoeM552IlwHKp9PagOKTb0O6VbsfgUneuE9d49yOfQ9VXGgAh8TCj8QD2gRp2CCrXR3VA61Mkqy2KdKMTjk91S6W4Vq9DT0eXdQw-pSuyuqwu73eK8gqUra4iFhzdF_fIqQgML0c2FjSo-nsQ0KakKcbo4J-Dul1swXRsa1Or6CHy9zZulGDY3NQHyLjemAHd2ULHcSG6yaWOVMmvnwo5gvwYoOezSCkcdCJvxOUBRZOIzMjaKiU71COKfDHxN9xg7mDbZY5HV6hLddIRedJ0ifjnWpelWpCveluVITmZzb1_sLbiAbBQ__OT973bPDodT7XvQKgHDPirYW_0OBM_rZUCsC0nrKvmMyW3FZUrD6ysESXB-Ltdoh2sa_XdLBpxQn21yuK3CndW0ahceePkBTvUVxE34lX5NGSbQqZPFrNlRb8meBLTBdbB9dJHXWio1Vrr5BYHT1FmYts9TN6wUkmSivPECDvZEMXVUdGSLPUAov0pOaNEOBvlqNt5l29-yK5ELzwLQhb5-2au9JoKvYmCNTrF7DORnRvY8JPq33gXF_hAT4irFHNH1goy_bI5RlWVEtsUiD9aMObaNKeVPz8Moz6tFzvxt0WuXg7mR8j4fD_VYRJVZYJWyIIFuwP-NrtAGHoh4rh-rwgNnZQAEUn9u3DIx9FdZOSTpG4ny42cLxaFBRFrJHrjg5phZ5oo-mA_dE2rqVwpMg3NtJzMvAyHvjYAe_eiuxO2HnyJsnGa1m-L7w4JZx7TXQYcwNCDROZYZht63caAxeoqHvempm_h4J_HyXLAV7ll2GSAo_2RULVWqQA43s2dVmbaM3ZnnIepT_QlZaBqOwhk_5Fyf6fB7TclWDAU4Sej_TxHiQAteudGLvquwcZ-4Zaez4A_HPqy_3XwkqDkArJO7tpjKDlxUBlCUsbSewIydnTys3htQzvJ_CDyish7fmfkPd5SaNUhMiFp0trp5OgNbSILX1zGy-2qL6Rph9OjNdJg6TT6JLAvr4cBKTsIXO4bfCij1IDqRlLhptqxhW68kYDmV3pL7Sn4Nt4iNF6aWupAJD8ZGyIuW1vvPmvQwRNfAYbiBgLp1Kr74c-oJ27B4z9fg6BbZQHkXm0AVm3LTYnMIHqY2OKi2hk9Q.bL3CM3xU9dMgUZsE-NGywPkQk7gNn41TqTKOADl8vmc\",\"scope\":\"openid profile email offline_access ui.read ui.admin ui.preferences.read ui.preferences.write authority:tenants.read authority:tenants.write authority:users.read authority:users.write authority:roles.read authority:roles.write authority:clients.read authority:clients.write authority:tokens.read authority:tokens.revoke authority:branding.read authority:branding.write authority.audit.read graph:read sbom:read scanner:read policy:read policy:simulate policy:author policy:review policy:approve policy:run policy:activate policy:audit policy:edit policy:operate policy:publish airgap:seal airgap:status:read orch:read orch:operate orch:quota analytics.read advisory:read advisory-ai:view advisory-ai:operate vex:read vexhub:read exceptions:read exceptions:approve aoc:verify findings:read release:read release:write release:publish scheduler:read scheduler:operate notify.viewer notify.operator notify.admin notify.escalate evidence:read export.viewer export.operator export.admin vuln:view vuln:investigate vuln:operate vuln:audit platform.context.read platform.context.write doctor:run doctor:admin ops.health integration:read integration:write integration:operate packs.read packs.write packs.run packs.approve registry.admin timeline:read timeline:write trust:read trust:write trust:admin signer:read signer:sign signer:rotate signer:admin\",\"expiresAtEpochMs\":1773572202832},\"identity\":{\"subject\":\"b08639745d6549348d843aa311c98958\",\"name\":\"admin\",\"roles\":[],\"idToken\":\"eyJhbGciOiJSUzI1NiIsImtpZCI6IlRBSU1GTlE3LUFOR1JMU0JOQVlfQ19OSEdPVERVWUo5UlNVVTRSRUQiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2F1dGhvcml0eS5zdGVsbGEtb3BzLmxvY2FsLyIsImV4cCI6MTc3MzU3MzcwMywiaWF0IjoxNzczNTcwNDAzLCJhdWQiOiJzdGVsbGEtb3BzLXVpIiwic3ViIjoiYjA4NjM5NzQ1ZDY1NDkzNDhkODQzYWEzMTFjOTg5NTgiLCJuYW1lIjoiYWRtaW4iLCJhenAiOiJzdGVsbGEtb3BzLXVpIiwibm9uY2UiOiI5NGM4N2JmNi0yYmQ5LTRhZmUtYjBlNi1iNTEwZjdiYjkzNTYiLCJhdF9oYXNoIjoiOUFaRkhoWlRBUDk2T3VyOUktSEtEUSJ9.kPiYDmkhtP4x6qyjsWlPruXilFb4WqvJW1a_jwAPSYh4vvUUUb_1qr9bwyBcH-QvM_n2BILldKC5_v0Y5yzP_kN8SY792ocHfLaOIuFz8hqGgaesUAtg2mYiUSrYLXNKT99Wd_JE7HZL4dZAal_uoddT8QdaQaQTufvd00CKLIiYWjBWDZ7PUuAWoNYkaBb18MfYJaay0WiG3ijtir6MqsXszofbfGMzVAZSqktctiKteOqQ3qToPcIf78C_72imYr7DGy8ch2h-FddNdL3ADdC63fIdMMJnyD4WYya2SHnUA9LFjPxcuiwaYDWbxuDxIIVRCe93Opi1mhaVkPnJLg\"},\"dpopKeyThumbprint\":\"sKnQ4SbUBUpvgJaTWpA44WxDgH-XKgLNcyKhyHvb9Hc\",\"issuedAtEpochMs\":1773570403833,\"tenantId\":\"demo-prod\",\"scopes\":[\"advisory-ai:operate\",\"advisory-ai:view\",\"advisory:read\",\"airgap:seal\",\"airgap:status:read\",\"analytics.read\",\"aoc:verify\",\"authority.audit.read\",\"authority:branding.read\",\"authority:branding.write\",\"authority:clients.read\",\"authority:clients.write\",\"authority:roles.read\",\"authority:roles.write\",\"authority:tenants.read\",\"authority:tenants.write\",\"authority:tokens.read\",\"authority:tokens.revoke\",\"authority:users.read\",\"authority:users.write\",\"doctor:admin\",\"doctor:run\",\"email\",\"evidence:read\",\"exceptions:approve\",\"exceptions:read\",\"export.admin\",\"export.operator\",\"export.viewer\",\"findings:read\",\"graph:read\",\"integration:operate\",\"integration:read\",\"integration:write\",\"notify.admin\",\"notify.escalate\",\"notify.operator\",\"notify.viewer\",\"offline_access\",\"openid\",\"ops.health\",\"orch:operate\",\"orch:quota\",\"orch:read\",\"packs.approve\",\"packs.read\",\"packs.run\",\"packs.write\",\"platform.context.read\",\"platform.context.write\",\"policy:activate\",\"policy:approve\",\"policy:audit\",\"policy:author\",\"policy:edit\",\"policy:operate\",\"policy:publish\",\"policy:read\",\"policy:review\",\"policy:run\",\"policy:simulate\",\"profile\",\"registry.admin\",\"release:publish\",\"release:read\",\"release:write\",\"sbom:read\",\"scanner:read\",\"scheduler:operate\",\"scheduler:read\",\"signer:admin\",\"signer:read\",\"signer:rotate\",\"signer:sign\",\"timeline:read\",\"timeline:write\",\"trust:admin\",\"trust:read\",\"trust:write\",\"ui.admin\",\"ui.preferences.read\",\"ui.preferences.write\",\"ui.read\",\"vex:read\",\"vexhub:read\",\"vuln:audit\",\"vuln:investigate\",\"vuln:operate\",\"vuln:view\"],\"audiences\":[],\"authenticationTimeEpochMs\":1773570403000,\"freshAuthActive\":false,\"freshAuthExpiresAtEpochMs\":null}" + ], + [ + "stellaops.auth.session.info", + "{\"subject\":\"b08639745d6549348d843aa311c98958\",\"expiresAtEpochMs\":1773572202832,\"issuedAtEpochMs\":1773570403833,\"dpopKeyThumbprint\":\"sKnQ4SbUBUpvgJaTWpA44WxDgH-XKgLNcyKhyHvb9Hc\",\"tenantId\":\"demo-prod\"}" + ], + [ + "stella-search-session-id", + "503e30b9-33b1-4635-82ff-379ef9a842f6" + ] + ] + }, + "events": { + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [] + }, + "statePath": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\live-user-reported-admin-trust-check.state.json" +} diff --git a/src/Web/StellaOps.Web/output/playwright/live-user-reported-admin-trust-check.json b/src/Web/StellaOps.Web/output/playwright/live-user-reported-admin-trust-check.json new file mode 100644 index 000000000..87d7167e9 --- /dev/null +++ b/src/Web/StellaOps.Web/output/playwright/live-user-reported-admin-trust-check.json @@ -0,0 +1,294 @@ +{ + "generatedAtUtc": "2026-03-15T10:28:54.414Z", + "baseUrl": "https://stella-ops.local", + "stepLog": [ + { + "timestampUtc": "2026-03-15T10:26:40.234Z", + "step": "bootstrap", + "detail": "baseUrl=https://stella-ops.local" + }, + { + "timestampUtc": "2026-03-15T10:26:40.353Z", + "step": "browser-launched", + "detail": "" + }, + { + "timestampUtc": "2026-03-15T10:26:46.549Z", + "step": "frontdoor-authenticated", + "detail": "C:\\dev\\New folder\\git.stella-ops.org\\src\\Web\\StellaOps.Web\\output\\playwright\\live-user-reported-admin-trust-check.state.json" + }, + { + "timestampUtc": "2026-03-15T10:26:46.658Z", + "step": "page-ready", + "detail": "" + }, + { + "timestampUtc": "2026-03-15T10:26:46.658Z", + "step": "navigate", + "detail": "/setup/identity-access" + }, + { + "timestampUtc": "2026-03-15T10:26:48.341Z", + "step": "journey", + "detail": "identity-users" + }, + { + "timestampUtc": "2026-03-15T10:26:58.611Z", + "step": "journey", + "detail": "identity-roles" + }, + { + "timestampUtc": "2026-03-15T10:27:07.166Z", + "step": "journey", + "detail": "identity-tenants" + }, + { + "timestampUtc": "2026-03-15T10:27:17.384Z", + "step": "navigate", + "detail": "/setup/trust-signing" + }, + { + "timestampUtc": "2026-03-15T10:27:20.110Z", + "step": "journey", + "detail": "trust-overview" + }, + { + "timestampUtc": "2026-03-15T10:28:00.145Z", + "step": "journey", + "detail": "trust-keys" + }, + { + "timestampUtc": "2026-03-15T10:28:07.227Z", + "step": "journey", + "detail": "trust-issuers" + }, + { + "timestampUtc": "2026-03-15T10:28:17.926Z", + "step": "journey", + "detail": "trust-certificates" + }, + { + "timestampUtc": "2026-03-15T10:28:28.115Z", + "step": "journey", + "detail": "trust-keys-revoke" + }, + { + "timestampUtc": "2026-03-15T10:28:33.052Z", + "step": "journey", + "detail": "trust-analytics" + }, + { + "timestampUtc": "2026-03-15T10:28:54.357Z", + "step": "journey-complete", + "detail": "" + }, + { + "timestampUtc": "2026-03-15T10:28:54.357Z", + "step": "cleanup", + "detail": "" + } + ], + "artifacts": { + "user": { + "username": "qa-user-1773570406658", + "email": "qa-user-1773570406658@stella-ops.local", + "displayName": "QA User 1773570406658", + "updatedDisplayName": "QA User 1773570406658 Updated" + }, + "role": { + "name": "qa-role-1773570406658", + "description": "QA role 1773570406658", + "updatedDescription": "QA role 1773570406658 updated" + }, + "tenant": { + "id": "qa-tenant-1773570406658", + "displayName": "QA Tenant 1773570406658", + "updatedDisplayName": "QA Tenant 1773570406658 Updated" + }, + "key": { + "alias": "qa-signing-key-1773570406658" + }, + "issuer": { + "displayName": "QA Issuer 1773570406658", + "uri": "https://issuer-1773570406658.qa.stella-ops.local/root" + }, + "certificate": { + "serial": "QA-SER-1773570406658" + } + }, + "results": [ + { + "action": "identity-users", + "detail": { + "leastPrivilegeCard": "viewerLEAST PRIVILEGE Read-only access", + "invalidEmailError": "Enter a valid email address before creating or updating a user.", + "createBanner": "Created user qa-user-1773570406658.", + "created": true, + "updatedDisplayNameVisible": true, + "disabled": true, + "reenabled": true + }, + "snapshot": { + "label": "identity-users", + "url": "https://stella-ops.local/setup/identity-access?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Identity & Access - Stella Ops QA 1773540847164", + "heading": "Identity & Access", + "alerts": [ + "Enabled qa-user-1773570406658.", + "Doctor Run Complete1 failed, 1 warnings View Details" + ] + } + }, + { + "action": "identity-roles", + "detail": { + "scopeGroups": 6, + "scopePickCount": 85, + "builtInDetailTagCount": 74, + "createBanner": "Created role qa-role-1773570406658.", + "created": true, + "detailScopeCount": 2, + "impactMessage": "Impact: No users are currently assigned to this role.", + "updatedDescriptionVisible": true + }, + "snapshot": { + "label": "identity-roles", + "url": "https://stella-ops.local/setup/identity-access?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Identity & Access - Stella Ops QA 1773540847164", + "heading": "Identity & Access", + "alerts": [ + "Updated role qa-role-1773570406658.", + "Doctor Run Complete1 failed, 1 warnings View Details" + ] + } + }, + { + "action": "identity-tenants", + "detail": { + "createBanner": "Created tenant QA Tenant 1773570406658.", + "created": true, + "updatedDisplayNameVisible": true, + "suspended": true, + "resumed": true + }, + "snapshot": { + "label": "identity-tenants", + "url": "https://stella-ops.local/setup/identity-access?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Identity & Access - Stella Ops QA 1773540847164", + "heading": "Identity & Access", + "alerts": [ + "Resumed tenant QA Tenant 1773570406658 Updated." + ] + } + }, + { + "action": "trust-overview", + "detail": { + "summaryCards": 4, + "loadingText": "", + "errorText": "" + }, + "snapshot": { + "label": "trust-overview", + "url": "https://stella-ops.local/setup/trust-signing?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Trust & Signing - Stella Ops QA 1773540847164", + "heading": "Trust Management", + "alerts": [] + } + }, + { + "action": "trust-keys", + "detail": { + "createBanner": "Registered signing key qa-signing-key-1773570406658.", + "created": true, + "rotateReasonError": "Reason is required.", + "rotateBanner": "Rotated signing key qa-signing-key-1773570406658." + }, + "snapshot": { + "label": "trust-keys", + "url": "https://stella-ops.local/setup/trust-signing/keys?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Trust & Signing - Stella Ops QA 1773540847164", + "heading": "Trust Management", + "alerts": [ + "Rotated signing key qa-signing-key-1773570406658." + ] + } + }, + { + "action": "trust-issuers", + "detail": { + "createBanner": "Registered issuer QA Issuer 1773570406658.", + "created": true, + "blockReasonError": "Reason is required.", + "blockBanner": "Blocked issuer QA Issuer 1773570406658.", + "unblockBanner": "Restored issuer QA Issuer 1773570406658 at Full Trust.", + "restored": true + }, + "snapshot": { + "label": "trust-issuers", + "url": "https://stella-ops.local/setup/trust-signing/issuers?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Trust & Signing - Stella Ops QA 1773540847164", + "heading": "Trust Management", + "alerts": [ + "Restored issuer QA Issuer 1773570406658 at Full Trust." + ] + } + }, + { + "action": "trust-certificates", + "detail": { + "createBanner": "Registered certificate QA-SER-1773570406658.", + "created": true, + "chainVisible": true, + "verifyBanner": "Verified certificate chain for QA-SER-1773570406658.", + "revokeReasonError": "Reason is required.", + "revokeBanner": "Revoked certificate QA-SER-1773570406658.", + "revoked": true + }, + "snapshot": { + "label": "trust-certificates", + "url": "https://stella-ops.local/setup/trust-signing/certificates?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Trust & Signing - Stella Ops QA 1773540847164", + "heading": "Trust Management", + "alerts": [ + "Revoked certificate QA-SER-1773570406658." + ] + } + }, + { + "action": "trust-keys-revoke", + "detail": { + "exists": true, + "revokeBanner": "Revoked signing key qa-signing-key-1773570406658.", + "revoked": true + }, + "snapshot": { + "label": "trust-keys-revoke", + "url": "https://stella-ops.local/setup/trust-signing/keys?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Trust & Signing - Stella Ops QA 1773540847164", + "heading": "Trust Management", + "alerts": [ + "Revoked signing key qa-signing-key-1773570406658." + ] + } + }, + { + "action": "trust-analytics", + "detail": { + "summaryCards": 4, + "issuerRows": 0, + "failureBanner": "" + }, + "snapshot": { + "label": "trust-analytics", + "url": "https://stella-ops.local/setup/trust-signing/analytics?tenant=demo-prod®ions=us-east&environments=stage&timeWindow=7d", + "title": "Trust & Signing - Stella Ops QA 1773540847164", + "heading": "Trust Management", + "alerts": [] + } + } + ], + "failures": [], + "failedCheckCount": 0, + "ok": true +} diff --git a/src/Web/StellaOps.Web/output/playwright/live-user-reported-admin-trust-check.state.json b/src/Web/StellaOps.Web/output/playwright/live-user-reported-admin-trust-check.state.json new file mode 100644 index 000000000..a1541bb7e --- /dev/null +++ b/src/Web/StellaOps.Web/output/playwright/live-user-reported-admin-trust-check.state.json @@ -0,0 +1,18 @@ +{ + "cookies": [], + "origins": [ + { + "origin": "https://stella-ops.local", + "localStorage": [ + { + "name": "stellaops.sidebar.preferences", + "value": "{\"sidebarCollapsed\":false,\"collapsedGroups\":[],\"collapsedSections\":[]}" + }, + { + "name": "stellaops.theme", + "value": "system" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/Web/StellaOps.Web/output/playwright/mirror-operator-journey/builder-create-attempt.png b/src/Web/StellaOps.Web/output/playwright/mirror-operator-journey/builder-create-attempt.png new file mode 100644 index 000000000..84dd6ce80 Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/mirror-operator-journey/builder-create-attempt.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/mirror-operator-journey/catalog-configure-mirror-handoff.png b/src/Web/StellaOps.Web/output/playwright/mirror-operator-journey/catalog-configure-mirror-handoff.png new file mode 100644 index 000000000..2ef43e8ae Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/mirror-operator-journey/catalog-configure-mirror-handoff.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/mirror-operator-journey/catalog-create-domain-handoff.png b/src/Web/StellaOps.Web/output/playwright/mirror-operator-journey/catalog-create-domain-handoff.png new file mode 100644 index 000000000..d5f540ed2 Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/mirror-operator-journey/catalog-create-domain-handoff.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/mirror-operator-journey/catalog-direct.png b/src/Web/StellaOps.Web/output/playwright/mirror-operator-journey/catalog-direct.png new file mode 100644 index 000000000..b1d460957 Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/mirror-operator-journey/catalog-direct.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/mirror-operator-journey/client-setup-direct.png b/src/Web/StellaOps.Web/output/playwright/mirror-operator-journey/client-setup-direct.png new file mode 100644 index 000000000..d6c357fcd Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/mirror-operator-journey/client-setup-direct.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/mirror-operator-journey/dashboard-configure-consumer-handoff.png b/src/Web/StellaOps.Web/output/playwright/mirror-operator-journey/dashboard-configure-consumer-handoff.png new file mode 100644 index 000000000..7c4dc5c27 Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/mirror-operator-journey/dashboard-configure-consumer-handoff.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/mirror-operator-journey/dashboard-create-domain-handoff.png b/src/Web/StellaOps.Web/output/playwright/mirror-operator-journey/dashboard-create-domain-handoff.png new file mode 100644 index 000000000..ff1da3d28 Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/mirror-operator-journey/dashboard-create-domain-handoff.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/mirror-operator-journey/dashboard-direct.png b/src/Web/StellaOps.Web/output/playwright/mirror-operator-journey/dashboard-direct.png new file mode 100644 index 000000000..ac1e2be4b Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/mirror-operator-journey/dashboard-direct.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/mirror-operator-journey/dashboard-domain-panels.png b/src/Web/StellaOps.Web/output/playwright/mirror-operator-journey/dashboard-domain-panels.png new file mode 100644 index 000000000..da9c94745 Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/mirror-operator-journey/dashboard-domain-panels.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/mirror-operator-journey/feeds-airgap-configure-sources-handoff.png b/src/Web/StellaOps.Web/output/playwright/mirror-operator-journey/feeds-airgap-configure-sources-handoff.png new file mode 100644 index 000000000..79070ef94 Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/mirror-operator-journey/feeds-airgap-configure-sources-handoff.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/mirror-operator-journey/ops-builder-direct.png b/src/Web/StellaOps.Web/output/playwright/mirror-operator-journey/ops-builder-direct.png new file mode 100644 index 000000000..28e706ec6 Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/mirror-operator-journey/ops-builder-direct.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/mirror-operator-journey/ops-catalog-direct.png b/src/Web/StellaOps.Web/output/playwright/mirror-operator-journey/ops-catalog-direct.png new file mode 100644 index 000000000..efe7abfaa Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/mirror-operator-journey/ops-catalog-direct.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/mirror-operator-journey/ops-client-setup-direct.png b/src/Web/StellaOps.Web/output/playwright/mirror-operator-journey/ops-client-setup-direct.png new file mode 100644 index 000000000..a0997a0d7 Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/mirror-operator-journey/ops-client-setup-direct.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/mirror-operator-journey/ops-dashboard-direct.png b/src/Web/StellaOps.Web/output/playwright/mirror-operator-journey/ops-dashboard-direct.png new file mode 100644 index 000000000..03a78ac8f Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/mirror-operator-journey/ops-dashboard-direct.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/mirror-operator-journey/security-configure-sources-handoff.png b/src/Web/StellaOps.Web/output/playwright/mirror-operator-journey/security-configure-sources-handoff.png new file mode 100644 index 000000000..9d6d45357 Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/mirror-operator-journey/security-configure-sources-handoff.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/release-confidence-journey/01-releases-overview-to-deployments.png b/src/Web/StellaOps.Web/output/playwright/release-confidence-journey/01-releases-overview-to-deployments.png new file mode 100644 index 000000000..17be8f768 Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/release-confidence-journey/01-releases-overview-to-deployments.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/release-confidence-journey/02-deployment-detail-evidence-and-replay.png b/src/Web/StellaOps.Web/output/playwright/release-confidence-journey/02-deployment-detail-evidence-and-replay.png new file mode 100644 index 000000000..9936fb7b4 Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/release-confidence-journey/02-deployment-detail-evidence-and-replay.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/release-confidence-journey/02b-approval-detail-decision-cockpit.png b/src/Web/StellaOps.Web/output/playwright/release-confidence-journey/02b-approval-detail-decision-cockpit.png new file mode 100644 index 000000000..2ab1657ca Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/release-confidence-journey/02b-approval-detail-decision-cockpit.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/release-confidence-journey/03-decision-capsules-search-and-detail.png b/src/Web/StellaOps.Web/output/playwright/release-confidence-journey/03-decision-capsules-search-and-detail.png new file mode 100644 index 000000000..618631500 Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/release-confidence-journey/03-decision-capsules-search-and-detail.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/release-confidence-journey/04-security-posture-to-triage-workspace.png b/src/Web/StellaOps.Web/output/playwright/release-confidence-journey/04-security-posture-to-triage-workspace.png new file mode 100644 index 000000000..59478a2b1 Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/release-confidence-journey/04-security-posture-to-triage-workspace.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/release-confidence-journey/05-advisories-vex-tabs.png b/src/Web/StellaOps.Web/output/playwright/release-confidence-journey/05-advisories-vex-tabs.png new file mode 100644 index 000000000..6d40b91f6 Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/release-confidence-journey/05-advisories-vex-tabs.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/release-confidence-journey/06-reachability-tabs.png b/src/Web/StellaOps.Web/output/playwright/release-confidence-journey/06-reachability-tabs.png new file mode 100644 index 000000000..79ac93c71 Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/release-confidence-journey/06-reachability-tabs.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/release-confidence-journey/07-security-reports-embedded-tabs.png b/src/Web/StellaOps.Web/output/playwright/release-confidence-journey/07-security-reports-embedded-tabs.png new file mode 100644 index 000000000..f5caba4be Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/release-confidence-journey/07-security-reports-embedded-tabs.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/release-confidence-journey/08-release-promotion-submit.png b/src/Web/StellaOps.Web/output/playwright/release-confidence-journey/08-release-promotion-submit.png new file mode 100644 index 000000000..b1cfcf93f Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/release-confidence-journey/08-release-promotion-submit.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/release-confidence-journey/09-hotfix-review-and-create.png b/src/Web/StellaOps.Web/output/playwright/release-confidence-journey/09-hotfix-review-and-create.png new file mode 100644 index 000000000..434fcab0d Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/release-confidence-journey/09-hotfix-review-and-create.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/release-create-journey/hotfix-create.png b/src/Web/StellaOps.Web/output/playwright/release-create-journey/hotfix-create.png new file mode 100644 index 000000000..dfd5552d9 Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/release-create-journey/hotfix-create.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/release-create-journey/standard-create.png b/src/Web/StellaOps.Web/output/playwright/release-create-journey/standard-create.png new file mode 100644 index 000000000..dfd5552d9 Binary files /dev/null and b/src/Web/StellaOps.Web/output/playwright/release-create-journey/standard-create.png differ diff --git a/src/Web/StellaOps.Web/output/playwright/tmp-security-reports.auth.json b/src/Web/StellaOps.Web/output/playwright/tmp-security-reports.auth.json new file mode 100644 index 000000000..d2e9f7607 --- /dev/null +++ b/src/Web/StellaOps.Web/output/playwright/tmp-security-reports.auth.json @@ -0,0 +1,39 @@ +{ + "authenticatedAtUtc": "2026-03-15T12:16:21.730Z", + "baseUrl": "https://stella-ops.local", + "finalUrl": "https://stella-ops.local/mission-control/board?tenant=demo-prod®ions=apac,eu-west,us-east,us-west", + "title": "Dashboard - StellaOps", + "cookies": [], + "storage": { + "localStorageEntries": [ + [ + "stellaops.sidebar.preferences", + "{\"sidebarCollapsed\":false,\"collapsedGroups\":[],\"collapsedSections\":[]}" + ], + [ + "stellaops.theme", + "system" + ] + ], + "sessionStorageEntries": [ + [ + "stellaops.auth.session.full", + "{\"tokens\":{\"accessToken\":\"eyJhbGciOiJSUzI1NiIsImtpZCI6IlRBSU1GTlE3LUFOR1JMU0JOQVlfQ19OSEdPVERVWUo5UlNVVTRSRUQiLCJ0eXAiOiJhdCtqd3QifQ.eyJpc3MiOiJodHRwczovL2F1dGhvcml0eS5zdGVsbGEtb3BzLmxvY2FsLyIsImV4cCI6MTc3MzU3ODc3OCwiaWF0IjoxNzczNTc2OTc4LCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGVtYWlsIG9mZmxpbmVfYWNjZXNzIHVpLnJlYWQgdWkuYWRtaW4gdWkucHJlZmVyZW5jZXMucmVhZCB1aS5wcmVmZXJlbmNlcy53cml0ZSBhdXRob3JpdHk6dGVuYW50cy5yZWFkIGF1dGhvcml0eTp0ZW5hbnRzLndyaXRlIGF1dGhvcml0eTp1c2Vycy5yZWFkIGF1dGhvcml0eTp1c2Vycy53cml0ZSBhdXRob3JpdHk6cm9sZXMucmVhZCBhdXRob3JpdHk6cm9sZXMud3JpdGUgYXV0aG9yaXR5OmNsaWVudHMucmVhZCBhdXRob3JpdHk6Y2xpZW50cy53cml0ZSBhdXRob3JpdHk6dG9rZW5zLnJlYWQgYXV0aG9yaXR5OnRva2Vucy5yZXZva2UgYXV0aG9yaXR5OmJyYW5kaW5nLnJlYWQgYXV0aG9yaXR5OmJyYW5kaW5nLndyaXRlIGF1dGhvcml0eS5hdWRpdC5yZWFkIGdyYXBoOnJlYWQgc2JvbTpyZWFkIHNjYW5uZXI6cmVhZCBwb2xpY3k6cmVhZCBwb2xpY3k6c2ltdWxhdGUgcG9saWN5OmF1dGhvciBwb2xpY3k6cmV2aWV3IHBvbGljeTphcHByb3ZlIHBvbGljeTpydW4gcG9saWN5OmFjdGl2YXRlIHBvbGljeTphdWRpdCBwb2xpY3k6ZWRpdCBwb2xpY3k6b3BlcmF0ZSBwb2xpY3k6cHVibGlzaCBhaXJnYXA6c2VhbCBhaXJnYXA6c3RhdHVzOnJlYWQgb3JjaDpyZWFkIG9yY2g6b3BlcmF0ZSBvcmNoOnF1b3RhIGFuYWx5dGljcy5yZWFkIGFkdmlzb3J5OnJlYWQgYWR2aXNvcnktYWk6dmlldyBhZHZpc29yeS1haTpvcGVyYXRlIHZleDpyZWFkIHZleGh1YjpyZWFkIGV4Y2VwdGlvbnM6cmVhZCBleGNlcHRpb25zOmFwcHJvdmUgYW9jOnZlcmlmeSBmaW5kaW5nczpyZWFkIHJlbGVhc2U6cmVhZCByZWxlYXNlOndyaXRlIHJlbGVhc2U6cHVibGlzaCBzY2hlZHVsZXI6cmVhZCBzY2hlZHVsZXI6b3BlcmF0ZSBub3RpZnkudmlld2VyIG5vdGlmeS5vcGVyYXRvciBub3RpZnkuYWRtaW4gbm90aWZ5LmVzY2FsYXRlIGV2aWRlbmNlOnJlYWQgZXhwb3J0LnZpZXdlciBleHBvcnQub3BlcmF0b3IgZXhwb3J0LmFkbWluIHZ1bG46dmlldyB2dWxuOmludmVzdGlnYXRlIHZ1bG46b3BlcmF0ZSB2dWxuOmF1ZGl0IHBsYXRmb3JtLmNvbnRleHQucmVhZCBwbGF0Zm9ybS5jb250ZXh0LndyaXRlIGRvY3RvcjpydW4gZG9jdG9yOmFkbWluIG9wcy5oZWFsdGggaW50ZWdyYXRpb246cmVhZCBpbnRlZ3JhdGlvbjp3cml0ZSBpbnRlZ3JhdGlvbjpvcGVyYXRlIHBhY2tzLnJlYWQgcGFja3Mud3JpdGUgcGFja3MucnVuIHBhY2tzLmFwcHJvdmUgcmVnaXN0cnkuYWRtaW4gdGltZWxpbmU6cmVhZCB0aW1lbGluZTp3cml0ZSB0cnVzdDpyZWFkIHRydXN0OndyaXRlIHRydXN0OmFkbWluIHNpZ25lcjpyZWFkIHNpZ25lcjpzaWduIHNpZ25lcjpyb3RhdGUgc2lnbmVyOmFkbWluIiwianRpIjoiMGJiMzAzZTktMDBmYy00NmViLWJhY2YtOTBlZDZhMjA4NGE2Iiwic3ViIjoiYjA4NjM5NzQ1ZDY1NDkzNDhkODQzYWEzMTFjOTg5NTgiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJhZG1pbiIsIm5hbWUiOiJhZG1pbiIsInJvbGUiOiJhZG1pbiIsInN0ZWxsYW9wczp0ZW5hbnQiOiJkZW1vLXByb2QiLCJhdXRoX3RpbWUiOjE3NzM1NzY5NzgsIm9pX3Byc3QiOiJzdGVsbGEtb3BzLXVpIiwiY2xpZW50X2lkIjoic3RlbGxhLW9wcy11aSJ9.PuTYjhcQjr4o87wF7Sw2dw3tKmkEyZBfNCdl5XW16t1vd1PuJ0TuHT2K-snyRgO0vvQ6D1AorjytnTDE-qWJ1PtEImFw2Xtj131ZDz0BuVDVFH-hfhccpiiZq-0zmwzeZpu1-dofxRcD-9mXbdRDVKvk-n_wgFrNX5C_fD226rybzFsAJFlfG33LtT29FgM_UVqapbjGPbyMLNosfkVfc4zlV6Qm9vSefeYwOGmh2Cji1BiaDaBVICJhl4HZfhP8RRO_a-9xkXXXwiuHgeVneTh7hurFoUvp6qdPm4MUMR751F-sRYDyYLfZh5quDQbr2sAUsaflkA-GprelZ-Ryng\",\"tokenType\":\"Bearer\",\"refreshToken\":\"eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZDQkMtSFM1MTIiLCJraWQiOiJXUjVQSU4tVzI5S0lNTkpRTVBBMU9XS0VYN0tEWjZMV0dYTDdSS0dWIiwidHlwIjoib2lfcmVmdCtqd3QiLCJjdHkiOiJKV1QifQ.swA8FaeOJ6ku6qEzwvDD-9xD2EpLAw-GiPqQOcyvaWvwWPIABfSVwKvALtpzTDxnwVy4n2nKtm03YeAs9-5qiPAl533Hcm2td9r0DCWLnUDq7T9CoqiMcAxLE0BNiD_ndcgn7eAt7w5AU1Re9ZE1qSGb6Y58M48oey0LSKXesepQ0DwAaNp_JsYIcVGj5VUuY0wEjDR5Z0pJcdcYwl9QUbWMEO3p3c0TSuEN-mSLJkUwIWYiVRkiwNybthkcUsGE3BbLzFDtDH8_LCc3b33g7p_aLcnZP1dkMSCrDJJ2Pn7cZsgm0KIUQOyu5RTg2Sszly0XPlMqg2aXkyxJBBs70w.ILBZz1_V2j5V8dmVLxDvMw.cH0kAkWQwrPjjwWIzdlW265GYCQxkXUFU00pHcwn-STPcCDvDwDXH7X1RECbZWMn5EiApKTYy5BPrXeqatV28cHG2WB1biqHFTjuQZIZrOCsTl8bkvAlsRYYTd7vOPOf3CVly7yeOjKpZcQQvjL7OKPNR6IrIObD00gniqWZkvmwEFKY157ZSdqLTmz5hI1ERl1ZjWc02QlJEYO0axL-zZ5b5PvtClXeW2RXrYGhBTzhAGHbMC6eHfnMyEWFoG2-TuH4z8XeVb97AMJ8VgAsLPAkFYsFADD6dqt7hJQ7PMKsnx5qryoH1FuD5JjfFe2jkHrf16acqw5udmjsG93RmyyNB8ZR9Z_XThBecYTFihO4FMmTt1QrDKkzLko2bPQhLzd62JNQhvI6qIaNIoLl9PP6D_Cu51tFN9iolVgrbBvnWjFPu0wMt-0Ret27eIf7BOXSmy4Nqz-lsfwaXsMxfgoFz4M8n0HfpTdOyeC8IHD6STh6Ib9nepu_WaSag5DgtLIEU7z2Hws4gS06gnI3uZEOwHnxH3xB8jBHyl8cKMWpNChWD43IrfpC2D5QUdRyHXt7iL6kGLTyIylHB4gau0edHYPqYGPbKJe4Q-23kuV5Ukf9HsUqoi6FN44o97SDQUThNJiB939Kf1rSyKH0CeNXmK6FaHv8vbulkhFG0QGNcUzBTd4nL-dAG4MjaWRglIpobMHOdVRWr-I34V0YboRy5FRPMHIPhpdDIC89rJY4dTkorct_lRmSHxgEvbHVEsJ8J3rKlNT8cqPfethSFNCcpi5TgfQkR6ZOCxXKtc6LEXpmUGB8l-Pr95lbr7DUmaARLd-Phopamu3F3NmKAYByyN054igDUNZXkU0lEQzcCR5nKz7ua-BM91TaZJhZgV_-6FreD-_l9MpsVhRrUMm27-C8KMcgiaRRYpF5gz834eDJioduHmoGcIO0rC5y_BNUzqGkJMh6lTq6mx1sXDqusUmDXq3kIHhhh4gr75TSaUOMhQcylrZ-cOqZD9pETV4dTerWMvPuKcnO4ilnMccUUZqUaFnuiR14gzceRuJRhiMPGYudc1D-Ei0u8IIVQXEUuWBT_SmsOA22SnhbA0Yzu2I3CSQu3MIsalkXlJc7EugbnHVtVjyD-0LqH8V1BdzC-exmuFAeE9nnjGvQfA4TyV_9sPEQpxgkue-1bkowXhsZEhs3m_IsIZo2QZz5zrAbDVjEcNZO1ASuKWlesqbywyjiP07iEATW8gaZdqEFScpF-rqolYggnqqDUuwF2Z8N5KbDCRCMsk2YyG_1GTDU-zTQPl0AGuwCACMwUceAxTBTR2YsahiiC2qXjFvSYizlzSAJBMJFwSLsd-XUGKdQ4rP8GPVxQW-g8Ekdrm6Yxxlc01W1cOAa4csM0rlw4WhoHxVahXDhLHLXtYITpCzXIAznCzj1mm0OekuSyfnqXremBEvgnGRd_uwAw6qUKDuMl5c5Ki1zjdgBJ7d7nQfTNhKhTMjlGWjxL2YXgtoZaCIrBcncJCtfmgNcdKfWgNn3kC8VEtGXYHGkQZFM5c-DY6Wc9ohEBMfCO8UW5KnplehJqG5f3S-2sgs_ruZa4CCCUHNJx0alh849x3p2rZQmlzVjmwhSiQkGSEpgqi81daYJxRDlpbP_ICaXcbxsnWWjIULrZlQsjGKGJSHlIi68mrGfVKQZcUC_sxQ983Vb_Ii2ZooP8pST7d1E8Rt3ELcuIWdGO4wFmjNH1zsvCv_tPHm8tT0S08mtCv0dqPOdfDV3XNmIq0xf_igkGiTfA06s5atXvcBzf5lE3nzlkEYhhU192Xx_sQjdi2CulpYOoY5DyXY_rvyL6KDo4ZTkIxexqrh7bCfyyWD1AA5gaD40e4IKGMn9VBmE2ybCc4wPT2P3H2MneK6eonj6b9HPqHqCpcTHpH2YXGwx-TnaScweKhanJdAV3TtCkCZiD3avkHLRkJca8ciZtFtfvDPlchNtPqkYTzuTMvcihwv4c4u2QsvLkFUxLKTtO2ylgJtBrWX8ogKJlLkXEdm6pC4AcNoAotJztONAUXM9ijWhd9Yvpb2HqdLuLQzPOyj1aalMf2qZKTOGNGGV1UbYfEgpnOMhf2ZIhW8QWJlrX_EVPeIdOH9-REwP59qwzNLCq7PQaPQ-xdYx3bYmPPnS3MgM0uhzft4qe-_c0o1t_IiyhskMr75H_aQixn6_oDrQk8RF_0EhfdPT16aE7Gv42ZmX1Ha7-GmQEoeDb-EHnQ9DDvJDBavS-oCO-Dm_sZXr14fJSN0ytKrf8AaiVlAXxCoA-h25zUQqQ-sIYdzLPWTARjX8ZW12y93nMdTAtxNo3Qf1hvsSM_Y1J4w77ZZKT0bDYq4KSF9_VnhpD_U59SEEiaPna_8ovCL6qbe_d9w7TKe03aiOlyEKkCzLusfTjRnDh3xPNeYJ_lRWNKWGGUPgreWX5Lvskiv4FrIIsoNw75WmxEOHivCjHWeU4Tpy_nJwIypUwGjm9B6zEXIvUfz0usW-MVVMXyNK5TWvlGS-acwC1cz_w6zD0ZwxYtZWgVUtzaX788s7S7FhO6-0XZss3vmGjcLdy2uUXcIGpJc2hSgTK0bld8JUosmWrspcQ0KVxzhrpw8QNFfHTwZPzfSDwjrA1gWgQ1JGkwkOMO5uBQVMXPx39LmzXazA5Dkm27Btmdj_YFujLzLzzYYxUbKguOsXl8z6gudGPKQtjlNFcZnmcpVG8xj0QkOAsGn_yuva7XDmN7_v-VPtO_R3eXToWPNy_juDpAHg1WIP51axIm2bRqJMTPeIhawHFN_TvJGSiaWNvmBJqHNQAuqZoOV8Ofc9ARiCHQtKMhPlh32dA-Cjl2He7_5wMx_H-pfzifQFVYxglleGZyIGqi9wElM08JHEKWUx5HFL4osGbMKfrNV4ehAzesPu-YjUvkPo9tpbwdYKIj1JjsV37eYvlQAkPlstwz43cJ3PcO379mU91c5nuI-qKIfLtZ323j6VvaJch8GGvzo_g6kRVgdxzZC_EYe8w5CiPS0AeGQCK8YWM7P03rpJM32oHDVMKsv4NkbMBuSy1uhhuskA9PfHPcceW5O4hGsS154xnjqQjnNg9fiUUPM1UCmCIt3lYfwm0Qu45usR0XgTwt8eNPVWqUPdF6wpQHX5f974EFjFmYHuPjB58tqvmSaCWqPXcZRPPWa2HmAWWUx31oqWzlq18ObLJOgWQOWJ4QcgnESHOXwyWI-cJLKEa1kG0xdtmb2VhSSs7AS9gbauQEWCb7iXBXj7A3C1O663b7kksSwoI25Nn4vw_813XUjTV5RYWmG0iGXdXdkWFUzZKrQN4cEN1N2jQQqwWX7vCNwCE8bXv8HBmkIQR5kr04EgfkQbV5JF02ZJ6FHUrcvDpgG_TlWKslELSVLEUHQIyooUG4cHLVtt8Zw_7QVIdW9tmIPd9nf2nx51T8exGUGF-u2Ub-3zzv93RJSUTsFIJ0GGcOi3GAteAubNSAlMg7ZXJcyOfJMSOM3v-yvRIcthF-D1kusdzgPmR6T9XXZRIlxQB3phl3arMXs9e7IPf9kJJgcGP0h2i3XwVdSmon73BFFjOcs23xegigDRwt0XyQUYHmrEF5T7vJJybjQK45yPJUU8zN0REBY8LoBU3IIyLs5Nd6PfWDFt-qslqSYmIy4d6vOpGPlDi46FQNzHcqumzfyORWOJPZMS4gycANfJ1k8u3VI_ILHKDUqKD1lCTndWsLYoNqaGAxK3geXlLjvfrgKbBt6i2_KWBk-1wElbqVk_xSKUNtxdFTQPSOry8PuesfCc3s47LRTpIxTSb2DIEj-d3GSOPWJRFRxl6slSD5ND1tXNowqTIwLLyy-y2Z0lYzZxI_wvab7CoFTKh0sR04oR_MEYfRT7awAo1CTptuePZUgpFmELRzQxXbzKez0PcodHqPQsbj0264MGVd8G5TAE7wlcDs1gl520Tn2rtFrtjTo8wCSsP_ExCjrBJGGEAUmIYm4sXqaVPV9wqg-VRpRVRBCZ6TzfU60PQvmsUDfCyCCq7YBVoxm_1ElZJ9xd7K9e5muyXgv45bsMvBTCBwpsGJrB7FcCr53J5ahHNd1EeiGpvi6xLve3KHr45isJ4AgeTw7RIXLZAX1CpQt1YEH-I-FR-YvAwmpHSeTiEhlFxDbRGnRkOKclKeSenQ9Jl08ZHqyQcISWYuCbhRi5xjgYypqz06NMtxkBhbM5EtmWRDRBRdIuiIz-5bERO6__7jEE9FsYC0cP_jEiru2urmgJ3nhtQFNssq9OOEhP5La999IJjIK7GIalYgYyBvSaKHRatv1ADOIgb33oBY7cPJj5gWqGhj8g3vAVo7-mgAAhX291GQe6_T0IfrBYvjw0sy_psKYhKlbCK924TIUaSYunSkjwFjgYD2lCpS7QZ_JxRio_uO9HUlx7LfviEhy9jelsOqf6OrTVEyfqulwtikz2i_h9Q4MxPHs_Q2khUYObFAEORam3KDOzy2fXH6jorf8UERlBm_OqV0pOfhdkwcxzW-66knz6Q8P0bO22HGojnkwJybi_3rkTDABu5t3NokomJXVTA-0_Tgj6OIqbcvjJsYE9gkAlJwemNVFoiRH6j0pS3cZAcq7X5J8wpwGjuR55R94JaHSdqeIpDTXYYsJ150ZWxGL4BFq8GAoEscUBTM-c_Hv7HswMGUnXklnS9Yq3j51OhsY6oqXS10WzvI16INXx3-BWkPtl4P6CJzslw49aG7hy7Q.42DubEFDSNvIzRCVVjlN-EM1JnV9bRGVCk7kvyKZMU8\",\"scope\":\"openid profile email offline_access ui.read ui.admin ui.preferences.read ui.preferences.write authority:tenants.read authority:tenants.write authority:users.read authority:users.write authority:roles.read authority:roles.write authority:clients.read authority:clients.write authority:tokens.read authority:tokens.revoke authority:branding.read authority:branding.write authority.audit.read graph:read sbom:read scanner:read policy:read policy:simulate policy:author policy:review policy:approve policy:run policy:activate policy:audit policy:edit policy:operate policy:publish airgap:seal airgap:status:read orch:read orch:operate orch:quota analytics.read advisory:read advisory-ai:view advisory-ai:operate vex:read vexhub:read exceptions:read exceptions:approve aoc:verify findings:read release:read release:write release:publish scheduler:read scheduler:operate notify.viewer notify.operator notify.admin notify.escalate evidence:read export.viewer export.operator export.admin vuln:view vuln:investigate vuln:operate vuln:audit platform.context.read platform.context.write doctor:run doctor:admin ops.health integration:read integration:write integration:operate packs.read packs.write packs.run packs.approve registry.admin timeline:read timeline:write trust:read trust:write trust:admin signer:read signer:sign signer:rotate signer:admin\",\"expiresAtEpochMs\":1773578778059},\"identity\":{\"subject\":\"b08639745d6549348d843aa311c98958\",\"name\":\"admin\",\"roles\":[],\"idToken\":\"eyJhbGciOiJSUzI1NiIsImtpZCI6IlRBSU1GTlE3LUFOR1JMU0JOQVlfQ19OSEdPVERVWUo5UlNVVTRSRUQiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2F1dGhvcml0eS5zdGVsbGEtb3BzLmxvY2FsLyIsImV4cCI6MTc3MzU4MDI3OCwiaWF0IjoxNzczNTc2OTc4LCJhdWQiOiJzdGVsbGEtb3BzLXVpIiwic3ViIjoiYjA4NjM5NzQ1ZDY1NDkzNDhkODQzYWEzMTFjOTg5NTgiLCJuYW1lIjoiYWRtaW4iLCJhenAiOiJzdGVsbGEtb3BzLXVpIiwibm9uY2UiOiJlMWU2ZjYyMC0yZWY5LTQ1YWYtOTIyYy0yODM2ODc0NjlkZjgiLCJhdF9oYXNoIjoiUEJvX00ySkZWQ2hmTVVDWlFOaTNDdyJ9.rxISTyZlihhkS2-bzIlu11at2OI-M70JxbGCutbv-vQbMi0Y3YqwEpx5CqGs3hR0U8SIyD3A2dpZ1J1BRMbjKbdqkWT0XalV8ZHSwp8KKgk2gPj9-xirpmPe6eY4KI133ivG3xmTXKuHszFbfSR1i89MQBuX4o5hyqRdrhl9fgS3w73xdxlNWCdoTuAz9TJtO530NWvX6I1M1mmelcSqk2gHhbUQrpgHal7dHVGIlmuXQJP5vs4T011fihLNz-9IBQQ1QAcoczUrrG5L5XUmrrL37CxC9ccD5jnXoE3uNUz9j51kNxbaCM6aauOIusckkhht2kSvcJEsj7CD-mDoNw\"},\"dpopKeyThumbprint\":\"ZBC82WaT5iPql1e5pLokcJcj7S0H4lq6dMnpt5q5y_E\",\"issuedAtEpochMs\":1773576979060,\"tenantId\":\"demo-prod\",\"scopes\":[\"advisory-ai:operate\",\"advisory-ai:view\",\"advisory:read\",\"airgap:seal\",\"airgap:status:read\",\"analytics.read\",\"aoc:verify\",\"authority.audit.read\",\"authority:branding.read\",\"authority:branding.write\",\"authority:clients.read\",\"authority:clients.write\",\"authority:roles.read\",\"authority:roles.write\",\"authority:tenants.read\",\"authority:tenants.write\",\"authority:tokens.read\",\"authority:tokens.revoke\",\"authority:users.read\",\"authority:users.write\",\"doctor:admin\",\"doctor:run\",\"email\",\"evidence:read\",\"exceptions:approve\",\"exceptions:read\",\"export.admin\",\"export.operator\",\"export.viewer\",\"findings:read\",\"graph:read\",\"integration:operate\",\"integration:read\",\"integration:write\",\"notify.admin\",\"notify.escalate\",\"notify.operator\",\"notify.viewer\",\"offline_access\",\"openid\",\"ops.health\",\"orch:operate\",\"orch:quota\",\"orch:read\",\"packs.approve\",\"packs.read\",\"packs.run\",\"packs.write\",\"platform.context.read\",\"platform.context.write\",\"policy:activate\",\"policy:approve\",\"policy:audit\",\"policy:author\",\"policy:edit\",\"policy:operate\",\"policy:publish\",\"policy:read\",\"policy:review\",\"policy:run\",\"policy:simulate\",\"profile\",\"registry.admin\",\"release:publish\",\"release:read\",\"release:write\",\"sbom:read\",\"scanner:read\",\"scheduler:operate\",\"scheduler:read\",\"signer:admin\",\"signer:read\",\"signer:rotate\",\"signer:sign\",\"timeline:read\",\"timeline:write\",\"trust:admin\",\"trust:read\",\"trust:write\",\"ui.admin\",\"ui.preferences.read\",\"ui.preferences.write\",\"ui.read\",\"vex:read\",\"vexhub:read\",\"vuln:audit\",\"vuln:investigate\",\"vuln:operate\",\"vuln:view\"],\"audiences\":[],\"authenticationTimeEpochMs\":1773576978000,\"freshAuthActive\":false,\"freshAuthExpiresAtEpochMs\":null}" + ], + [ + "stellaops.auth.session.info", + "{\"subject\":\"b08639745d6549348d843aa311c98958\",\"expiresAtEpochMs\":1773578778059,\"issuedAtEpochMs\":1773576979060,\"dpopKeyThumbprint\":\"ZBC82WaT5iPql1e5pLokcJcj7S0H4lq6dMnpt5q5y_E\",\"tenantId\":\"demo-prod\"}" + ], + [ + "stella-search-session-id", + "35618d3c-01cd-4f76-98b3-73d26eff075c" + ] + ] + }, + "events": { + "consoleErrors": [], + "requestFailures": [], + "responseErrors": [] + }, + "statePath": "output/playwright/tmp-security-reports.state.json" +} diff --git a/src/Web/StellaOps.Web/output/playwright/tmp-security-reports.state.json b/src/Web/StellaOps.Web/output/playwright/tmp-security-reports.state.json new file mode 100644 index 000000000..a1541bb7e --- /dev/null +++ b/src/Web/StellaOps.Web/output/playwright/tmp-security-reports.state.json @@ -0,0 +1,18 @@ +{ + "cookies": [], + "origins": [ + { + "origin": "https://stella-ops.local", + "localStorage": [ + { + "name": "stellaops.sidebar.preferences", + "value": "{\"sidebarCollapsed\":false,\"collapsedGroups\":[],\"collapsedSections\":[]}" + }, + { + "name": "stellaops.theme", + "value": "system" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/Web/StellaOps.Web/scripts/live-full-core-audit.mjs b/src/Web/StellaOps.Web/scripts/live-full-core-audit.mjs index b01a47ee7..c5f157bda 100644 --- a/src/Web/StellaOps.Web/scripts/live-full-core-audit.mjs +++ b/src/Web/StellaOps.Web/scripts/live-full-core-audit.mjs @@ -67,6 +67,11 @@ const suites = [ script: 'live-first-time-user-reporting-truthfulness-check.mjs', reportPath: path.join(outputDir, 'live-first-time-user-reporting-truthfulness-check.json'), }, + { + name: 'mirror-operator-journey', + script: 'live-mirror-operator-journey.mjs', + reportPath: path.join(outputDir, 'live-mirror-operator-journey.json'), + }, { name: 'promotions-operations-landing-check', script: 'live-promotions-operations-landing-check.mjs', diff --git a/src/Web/StellaOps.Web/src/app/core/api/policy-governance.client.ts b/src/Web/StellaOps.Web/src/app/core/api/policy-governance.client.ts index bf5c8832c..7f50929bc 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/policy-governance.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/policy-governance.client.ts @@ -92,7 +92,7 @@ export interface PolicyGovernanceApi { export const POLICY_GOVERNANCE_API = new InjectionToken('POLICY_GOVERNANCE_API'); -const LEGACY_POLICY_TENANT_PLACEHOLDERS = new Set(['acme-tenant']); +const LEGACY_POLICY_TENANT_PLACEHOLDERS = new Set(['acme-tenant', 'default']); interface GovernanceScopeRequest { tenantId?: string; @@ -280,7 +280,7 @@ const MOCK_AUDIT_EVENTS: GovernanceAuditEvent[] = [ newState: { weight: 1.5 }, diff: { added: {}, removed: {}, modified: { weight: { before: 1.2, after: 1.5 } } }, traceId: 'trace-audit-001', - tenantId: 'acme-tenant', + tenantId: 'demo-prod', }, { id: 'audit-002', @@ -292,7 +292,7 @@ const MOCK_AUDIT_EVENTS: GovernanceAuditEvent[] = [ targetResourceType: 'risk_profile', summary: 'Activated Strict Security Profile v1.1.0', traceId: 'trace-audit-002', - tenantId: 'acme-tenant', + tenantId: 'demo-prod', }, { id: 'audit-003', @@ -306,7 +306,7 @@ const MOCK_AUDIT_EVENTS: GovernanceAuditEvent[] = [ previousState: { isSealed: false }, newState: { isSealed: true, reason: 'Air-gap deployment preparation' }, traceId: 'trace-audit-003', - tenantId: 'acme-tenant', + tenantId: 'demo-prod', }, ]; diff --git a/src/Web/StellaOps.Web/src/app/core/api/policy-simulation.client.spec.ts b/src/Web/StellaOps.Web/src/app/core/api/policy-simulation.client.spec.ts index 7e1015721..b2ccfeb29 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/policy-simulation.client.spec.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/policy-simulation.client.spec.ts @@ -754,6 +754,19 @@ describe('PolicySimulationHttpClient', () => { await promise; }); + + it('should throw when legacy default placeholder is passed but no active tenant exists', () => { + tenantServiceMock.activeTenantId.and.returnValue(null); + authSessionStoreMock.getActiveTenantId.and.returnValue(null as unknown as string); + + expect(() => + httpClient.getSimulationHistory({ + tenantId: 'default', + page: 1, + pageSize: 20, + }), + ).toThrowError('PolicySimulationHttpClient requires an active tenant identifier.'); + }); }); }); diff --git a/src/Web/StellaOps.Web/src/app/core/api/policy-simulation.client.ts b/src/Web/StellaOps.Web/src/app/core/api/policy-simulation.client.ts index e6358ef73..1d1d73040 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/policy-simulation.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/policy-simulation.client.ts @@ -433,9 +433,18 @@ export class PolicySimulationHttpClient implements PolicySimulationApi { const activeTenant = this.tenantService.activeTenantId?.() ?? this.authSession.getActiveTenantId(); + + // When the caller passes a legacy placeholder (e.g. 'default') or no + // tenant at all, always resolve to the active tenant from the shell + // context. Only honour the caller's value when it is a real, + // non-placeholder tenant identifier (preserving cross-tenant overrides). + const isPlaceholder = + requestedTenant !== null && + LEGACY_PLACEHOLDER_TENANTS.has(requestedTenant.toLowerCase()); + const tenant = - !requestedTenant || (activeTenant && LEGACY_PLACEHOLDER_TENANTS.has(requestedTenant.toLowerCase())) - ? activeTenant ?? requestedTenant + !requestedTenant || isPlaceholder + ? activeTenant : requestedTenant; if (!tenant) { diff --git a/src/Web/StellaOps.Web/src/app/core/api/policy-simulation.models.ts b/src/Web/StellaOps.Web/src/app/core/api/policy-simulation.models.ts index fadf9b319..2a980bf88 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/policy-simulation.models.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/policy-simulation.models.ts @@ -871,7 +871,8 @@ export interface PolicyMergePreview { // ============================================================================ export interface ShadowModeQueryOptions { - readonly tenantId: string; + /** Tenant identifier. Omit or leave empty to use the active shell tenant. */ + readonly tenantId?: string; readonly projectId?: string; readonly fromTime?: string; readonly toTime?: string; @@ -882,7 +883,8 @@ export interface ShadowModeQueryOptions { } export interface SimulationQueryOptions { - readonly tenantId: string; + /** Tenant identifier. Omit or leave empty to use the active shell tenant. */ + readonly tenantId?: string; readonly projectId?: string; readonly policyPackId?: string; readonly status?: SimulationStatus; @@ -894,7 +896,8 @@ export interface SimulationQueryOptions { } export interface PolicyAuditQueryOptions { - readonly tenantId: string; + /** Tenant identifier. Omit or leave empty to use the active shell tenant. */ + readonly tenantId?: string; readonly policyPackId?: string; readonly action?: PolicyAuditAction; readonly actorId?: string; @@ -906,7 +909,8 @@ export interface PolicyAuditQueryOptions { } export interface EffectivePolicyQueryOptions { - readonly tenantId: string; + /** Tenant identifier. Omit or leave empty to use the active shell tenant. */ + readonly tenantId?: string; readonly resourceType?: ResourceType; readonly resourceId?: string; readonly search?: string; @@ -916,7 +920,8 @@ export interface EffectivePolicyQueryOptions { } export interface PolicyExceptionQueryOptions { - readonly tenantId: string; + /** Tenant identifier. Omit or leave empty to use the active shell tenant. */ + readonly tenantId?: string; readonly projectId?: string; readonly status?: PolicyExceptionStatus; readonly severity?: string; @@ -927,14 +932,16 @@ export interface PolicyExceptionQueryOptions { } export interface CoverageQueryOptions { - readonly tenantId: string; + /** Tenant identifier. Omit or leave empty to use the active shell tenant. */ + readonly tenantId?: string; readonly policyPackId: string; readonly policyVersion?: number; readonly traceId?: string; } export interface PromotionGateQueryOptions { - readonly tenantId: string; + /** Tenant identifier. Omit or leave empty to use the active shell tenant. */ + readonly tenantId?: string; readonly policyPackId: string; readonly policyVersion: number; readonly targetEnvironment: string; @@ -1047,7 +1054,8 @@ export interface SimulationReproducibilityResult { } export interface SimulationHistoryQueryOptions { - readonly tenantId: string; + /** Tenant identifier. Omit or leave empty to use the active shell tenant. */ + readonly tenantId?: string; readonly policyPackId?: string; readonly sbomId?: string; readonly status?: SimulationStatus; @@ -1161,7 +1169,8 @@ export interface ConflictDetectionResult { } export interface ConflictDetectionQueryOptions { - readonly tenantId: string; + /** Tenant identifier. Omit or leave empty to use the active shell tenant. */ + readonly tenantId?: string; readonly policyIds: readonly string[]; readonly includeResolved?: boolean; readonly severityFilter?: ConflictSeverity; @@ -1327,7 +1336,8 @@ export interface BatchEvaluationHistoryResult { } export interface BatchEvaluationQueryOptions { - readonly tenantId: string; + /** Tenant identifier. Omit or leave empty to use the active shell tenant. */ + readonly tenantId?: string; readonly policyPackId?: string; readonly status?: string; readonly fromDate?: string; diff --git a/src/Web/StellaOps.Web/src/app/core/api/topology-setup.client.ts b/src/Web/StellaOps.Web/src/app/core/api/topology-setup.client.ts new file mode 100644 index 000000000..ecd1363db --- /dev/null +++ b/src/Web/StellaOps.Web/src/app/core/api/topology-setup.client.ts @@ -0,0 +1,209 @@ +/** + * Topology Setup API Client + * + * Shared API client for topology management operations: + * regions, infrastructure bindings, readiness, rename, and pending deletions. + */ +import { Injectable, inject } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +// ── Region ─────────────────────────────────────────────── +export interface Region { + id: string; + name: string; + displayName: string; + description?: string; + cryptoProfile: string; + sortOrder: number; + status: string; + metadata?: Record; + createdAt: string; + updatedAt: string; +} + +// ── Infrastructure Binding ─────────────────────────────── +export interface InfrastructureBinding { + id: string; + tenantId: string; + integrationId: string; + scopeType: 'tenant' | 'region' | 'environment'; + scopeId?: string; + bindingRole: 'registry' | 'vault' | 'settings_store'; + priority: number; + configOverrides?: Record; + isActive: boolean; +} + +export interface ResolvedBinding { + binding: InfrastructureBinding; + resolvedFrom: 'direct' | 'region' | 'tenant'; +} + +export interface ResolvedBindings { + registry?: ResolvedBinding; + vault?: ResolvedBinding; + settingsStore?: ResolvedBinding; +} + +export interface ConnectionTestResult { + success: boolean; + message: string; + duration?: number; + testedAt: string; +} + +// ── Readiness ──────────────────────────────────────────── +export interface GateResult { + gateName: string; + status: 'pending' | 'pass' | 'fail' | 'skip'; + message?: string; + checkedAt?: string; + durationMs?: number; +} + +export interface ReadinessReport { + targetId: string; + environmentId: string; + isReady: boolean; + gates: GateResult[]; + evaluatedAt: string; +} + +// ── Rename ─────────────────────────────────────────────── +export interface RenameRequest { + name: string; + displayName: string; +} + +export interface RenameResult { + success: boolean; + oldName: string; + newName: string; +} + +// ── Pending Deletion ───────────────────────────────────── +export interface CascadeSummary { + childEnvironments?: number; + childTargets?: number; + boundAgents?: number; + infrastructureBindings?: number; + activeHealthSchedules?: number; + pendingDeployments?: number; +} + +export interface PendingDeletion { + id: string; + tenantId: string; + entityType: 'tenant' | 'region' | 'environment' | 'target' | 'agent' | 'integration'; + entityId: string; + entityName: string; + status: 'pending' | 'confirmed' | 'executing' | 'completed' | 'cancelled'; + coolOffHours: number; + coolOffExpiresAt: string; + cascadeSummary: CascadeSummary; + reason?: string; + requestedBy: string; + requestedAt: string; + confirmedBy?: string; + confirmedAt?: string; + executedAt?: string; + completedAt?: string; +} + +export type TopologyEntityType = 'regions' | 'environments' | 'targets' | 'agents' | 'integrations'; + +@Injectable({ providedIn: 'root' }) +export class TopologySetupClient { + private readonly http = inject(HttpClient); + + // ── Regions ────────────────────────────────────────── + listRegions(): Observable<{ items: Region[]; totalCount: number }> { + return this.http.get<{ items: Region[]; totalCount: number }>('/api/v1/regions'); + } + + createRegion(data: Partial): Observable { + return this.http.post('/api/v1/regions', data); + } + + getRegion(id: string): Observable { + return this.http.get(`/api/v1/regions/${id}`); + } + + updateRegion(id: string, data: Partial): Observable { + return this.http.put(`/api/v1/regions/${id}`, data); + } + + deleteRegion(id: string): Observable { + return this.http.delete(`/api/v1/regions/${id}`); + } + + // ── Infrastructure Bindings ────────────────────────── + createBinding(data: Partial): Observable { + return this.http.post('/api/v1/infrastructure-bindings', data); + } + + deleteBinding(id: string): Observable { + return this.http.delete(`/api/v1/infrastructure-bindings/${id}`); + } + + listBindings(scopeType?: string, scopeId?: string): Observable<{ items: InfrastructureBinding[] }> { + let params = new HttpParams(); + if (scopeType) params = params.set('scopeType', scopeType); + if (scopeId) params = params.set('scopeId', scopeId); + return this.http.get<{ items: InfrastructureBinding[] }>('/api/v1/infrastructure-bindings', { params }); + } + + resolveBinding(environmentId: string, role: string): Observable { + const params = new HttpParams().set('environmentId', environmentId).set('role', role); + return this.http.get('/api/v1/infrastructure-bindings/resolve', { params }); + } + + resolveAllBindings(environmentId: string): Observable { + const params = new HttpParams().set('environmentId', environmentId); + return this.http.get('/api/v1/infrastructure-bindings/resolve-all', { params }); + } + + testBinding(bindingId: string): Observable { + return this.http.post(`/api/v1/infrastructure-bindings/${bindingId}/test`, {}); + } + + // ── Readiness ──────────────────────────────────────── + validateTarget(targetId: string): Observable { + return this.http.post(`/api/v1/targets/${targetId}/validate`, {}); + } + + getTargetReadiness(targetId: string): Observable { + return this.http.get(`/api/v1/targets/${targetId}/readiness`); + } + + getEnvironmentReadiness(environmentId: string): Observable<{ items: ReadinessReport[] }> { + return this.http.get<{ items: ReadinessReport[] }>(`/api/v1/environments/${environmentId}/readiness`); + } + + // ── Rename ─────────────────────────────────────────── + rename(entityType: TopologyEntityType, id: string, request: RenameRequest): Observable { + return this.http.patch(`/api/v1/${entityType}/${id}/name`, request); + } + + // ── Pending Deletions ──────────────────────────────── + requestDelete(entityType: TopologyEntityType, id: string, reason?: string): Observable { + return this.http.post(`/api/v1/${entityType}/${id}/request-delete`, { reason }); + } + + confirmDeletion(pendingDeletionId: string): Observable { + return this.http.post(`/api/v1/pending-deletions/${pendingDeletionId}/confirm`, {}); + } + + cancelDeletion(pendingDeletionId: string): Observable { + return this.http.post(`/api/v1/pending-deletions/${pendingDeletionId}/cancel`, {}); + } + + listPendingDeletions(): Observable<{ items: PendingDeletion[] }> { + return this.http.get<{ items: PendingDeletion[] }>('/api/v1/pending-deletions'); + } + + getPendingDeletion(id: string): Observable { + return this.http.get(`/api/v1/pending-deletions/${id}`); + } +} diff --git a/src/Web/StellaOps.Web/src/app/core/api/trust.client.ts b/src/Web/StellaOps.Web/src/app/core/api/trust.client.ts index 4c5a41eac..282604334 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/trust.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/trust.client.ts @@ -191,7 +191,6 @@ interface AdministrationTransparencyLogConfigDto { @Injectable({ providedIn: 'root' }) export class TrustHttpService implements TrustApi { private readonly http = inject(HttpClient); - private readonly baseUrl = '/api/v1/trust'; private readonly administrationBaseUrl = '/api/v1/administration/trust-signing'; private readonly defaultIssuerWeights: IssuerWeights = { baseWeight: 50, diff --git a/src/Web/StellaOps.Web/src/app/features/integrations/advisory-vex-sources/advisory-vex-route-helpers.spec.ts b/src/Web/StellaOps.Web/src/app/features/integrations/advisory-vex-sources/advisory-vex-route-helpers.spec.ts new file mode 100644 index 000000000..70555f6fb --- /dev/null +++ b/src/Web/StellaOps.Web/src/app/features/integrations/advisory-vex-sources/advisory-vex-route-helpers.spec.ts @@ -0,0 +1,31 @@ +import { Router } from '@angular/router'; + +import { + buildAdvisoryVexCommands, + buildMirrorCommands, + getAdvisoryVexNavigationExtras, +} from './advisory-vex-route-helpers'; + +describe('advisory-vex-route-helpers', () => { + function fakeRouter(url: string): Router { + return { url } as Router; + } + + it('preserves setup ownership for advisory source routes', () => { + expect(buildAdvisoryVexCommands(fakeRouter('/setup/integrations/advisory-vex-sources?tenant=demo'))) + .toEqual(['/', 'setup', 'integrations', 'advisory-vex-sources']); + expect(buildMirrorCommands(fakeRouter('/setup/integrations/advisory-vex-sources/mirror'))) + .toEqual(['/', 'setup', 'integrations', 'advisory-vex-sources', 'mirror']); + }); + + it('preserves ops ownership for mirror child routes', () => { + expect(buildMirrorCommands(fakeRouter('/ops/integrations/advisory-vex-sources'), 'client-setup')) + .toEqual(['/', 'ops', 'integrations', 'advisory-vex-sources', 'mirror', 'client-setup']); + expect(buildMirrorCommands(fakeRouter('/ops/integrations/advisory-vex-sources/mirror'), 'new')) + .toEqual(['/', 'ops', 'integrations', 'advisory-vex-sources', 'mirror', 'new']); + }); + + it('always preserves query parameters during owner-aware navigation', () => { + expect(getAdvisoryVexNavigationExtras()).toEqual({ queryParamsHandling: 'preserve' }); + }); +}); diff --git a/src/Web/StellaOps.Web/src/app/features/integrations/advisory-vex-sources/advisory-vex-route-helpers.ts b/src/Web/StellaOps.Web/src/app/features/integrations/advisory-vex-sources/advisory-vex-route-helpers.ts new file mode 100644 index 000000000..d32a8287d --- /dev/null +++ b/src/Web/StellaOps.Web/src/app/features/integrations/advisory-vex-sources/advisory-vex-route-helpers.ts @@ -0,0 +1,18 @@ +import { NavigationExtras, Router } from '@angular/router'; + +const advisoryVexNavigationExtras: NavigationExtras = { + queryParamsHandling: 'preserve', +}; + +export function getAdvisoryVexNavigationExtras(): NavigationExtras { + return advisoryVexNavigationExtras; +} + +export function buildAdvisoryVexCommands(router: Router, ...segments: string[]): string[] { + const owner = router.url.startsWith('/setup/') ? 'setup' : 'ops'; + return ['/', owner, 'integrations', 'advisory-vex-sources', ...segments]; +} + +export function buildMirrorCommands(router: Router, ...segments: string[]): string[] { + return buildAdvisoryVexCommands(router, 'mirror', ...segments); +} diff --git a/src/Web/StellaOps.Web/src/app/features/integrations/advisory-vex-sources/mirror-client-setup.component.ts b/src/Web/StellaOps.Web/src/app/features/integrations/advisory-vex-sources/mirror-client-setup.component.ts index 751e2c09f..710047e18 100644 --- a/src/Web/StellaOps.Web/src/app/features/integrations/advisory-vex-sources/mirror-client-setup.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/integrations/advisory-vex-sources/mirror-client-setup.component.ts @@ -15,6 +15,7 @@ import { MirrorMode, MirrorDiscoveryDomain, } from './mirror-management.api'; +import { buildMirrorCommands, getAdvisoryVexNavigationExtras } from './advisory-vex-route-helpers'; // --------------------------------------------------------------------------- // Constants @@ -1548,7 +1549,7 @@ export class MirrorClientSetupComponent implements OnInit { } onCancel(): void { - this.router.navigate(['/integrations', 'advisory-vex-sources', 'mirror']); + this.router.navigate(buildMirrorCommands(this.router), getAdvisoryVexNavigationExtras()); } // ── Step 1 actions ──────────────────────────────────────────────────── @@ -1639,7 +1640,7 @@ export class MirrorClientSetupComponent implements OnInit { } navigateToDashboard(): void { - this.router.navigate(['/integrations', 'advisory-vex-sources', 'mirror']); + this.router.navigate(buildMirrorCommands(this.router), getAdvisoryVexNavigationExtras()); } // ── Helpers ─────────────────────────────────────────────────────────── diff --git a/src/Web/StellaOps.Web/src/app/features/integrations/advisory-vex-sources/mirror-dashboard.component.ts b/src/Web/StellaOps.Web/src/app/features/integrations/advisory-vex-sources/mirror-dashboard.component.ts index 8f5e10103..c8768b76b 100644 --- a/src/Web/StellaOps.Web/src/app/features/integrations/advisory-vex-sources/mirror-dashboard.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/integrations/advisory-vex-sources/mirror-dashboard.component.ts @@ -7,9 +7,11 @@ import { MirrorManagementApi, MirrorConfigResponse, MirrorDomainResponse, + MirrorDomainConfigResponse, MirrorDomainEndpointDto, MirrorHealthSummary, } from './mirror-management.api'; +import { buildMirrorCommands, getAdvisoryVexNavigationExtras } from './advisory-vex-route-helpers'; // ── Local helpers ──────────────────────────────────────────────────────────── @@ -211,6 +213,36 @@ function domainStaleness(domain: MirrorDomainResponse): 'fresh' | 'stale' | 'nev } + @if (expandedConfig() === domain.id && domainConfig()) { +
+

Configuration

+
+
+ Format + {{ domainConfig()!.exportFormat }} +
+
+ Authentication + {{ domainConfig()!.requireAuthentication ? 'Required' : 'Anonymous' }} +
+
+ Index limit + {{ domainConfig()!.rateLimits.indexRequestsPerHour }}/hour +
+
+ Download limit + {{ domainConfig()!.rateLimits.downloadRequestsPerHour }}/hour +
+
+ Signing + + {{ domainConfig()!.signing.enabled ? (domainConfig()!.signing.algorithm + ' (' + domainConfig()!.signing.keyId + ')') : 'Disabled' }} + +
+
+
+ } +
+
+ } + + +
+ @switch (wizard.currentStep()) { + + + @case ('region') { +
+

Select or Create a Region

+

Regions define geographic or logical isolation boundaries with their own crypto profiles.

+ + @if (regionsLoading()) { +
Loading regions...
+ } @else { + @if (regions().length > 0) { +

Existing Regions

+
+ @for (r of regions(); track r.id) { + + } +
+ +
+ or +
+ } + +

Create New Region

+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+ + } +
+ } + + + @case ('environment') { +
+

Create Environment

+

Environments are deployment stages within the selected region ({{ wizard.selectedRegion()?.displayName }}).

+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + Production environments enforce stricter gates and require additional approvals. +
+
+ + + + @if (wizard.createdEnvironment()) { +
+ + Environment "{{ wizard.createdEnvironment()?.displayName }}" created successfully. +
+ } +
+ } + + + @case ('stage-order') { +
+

Stage Order

+

Review the promotion order of environments. Releases flow through stages in this sequence.

+ + @if (environmentsLoading()) { +
Loading environments...
+ } @else { +
+ @for (env of orderedEnvironments(); track env.id; let i = $index) { +
+ {{ i + 1 }} +
+ {{ env.displayName }} + + Order: {{ env.orderIndex }} + @if (env.isProduction) { + · Production + } + +
+ @if (i < orderedEnvironments().length - 1) { + + } +
+ } +
+ + @if (orderedEnvironments().length === 0) { +
No environments found. Create one in the previous step.
+ } + } +
+ } + + + @case ('target') { +
+

Create Target

+

Targets are deployable endpoints within the environment ({{ wizard.createdEnvironment()?.displayName ?? 'selected environment' }}).

+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ + + + @if (wizard.createdTarget()) { +
+ + Target "{{ wizard.createdTarget()?.displayName }}" created ({{ wizard.createdTarget()?.type }}). +
+ } +
+ } + + + @case ('agent') { +
+

Assign Agent

+

Select an agent to manage deployments to the target "{{ wizard.createdTarget()?.displayName ?? 'created target' }}".

+ + @if (agentsLoading()) { +
Loading agents...
+ } @else if (agents().length > 0) { +
+ @for (a of agents(); track a.id) { + + } +
+ } @else { +
+

No agents found. You can onboard an agent first.

+ + Onboard Agent + +
+ } + + @if (wizard.selectedAgent()) { +
+ + Agent "{{ wizard.selectedAgent()?.displayName }}" selected. +
+ } +
+ } + + + @case ('infrastructure') { +
+

Infrastructure Bindings

+

Review resolved infrastructure bindings for the environment. Bindings are inherited from region or tenant scope if not overridden.

+ + @if (bindingsLoading()) { +
Resolving bindings...
+ } @else { +
+ +
+

+ + Registry +

+ @if (wizard.resolvedBindings()?.registry) { +
+ {{ wizard.resolvedBindings()?.registry?.binding?.bindingRole }} + Resolved from: {{ wizard.resolvedBindings()?.registry?.resolvedFrom }} + + {{ wizard.resolvedBindings()?.registry?.binding?.isActive ? 'Active' : 'Inactive' }} + +
+ } @else { +
+ No registry binding resolved. Consider adding one. +
+ } +
+ + +
+

+ + Vault +

+ @if (wizard.resolvedBindings()?.vault) { +
+ {{ wizard.resolvedBindings()?.vault?.binding?.bindingRole }} + Resolved from: {{ wizard.resolvedBindings()?.vault?.resolvedFrom }} + + {{ wizard.resolvedBindings()?.vault?.binding?.isActive ? 'Active' : 'Inactive' }} + +
+ } @else { +
+ No vault binding resolved. Consider adding one. +
+ } +
+ + +
+

+ + Settings Store (Consul) +

+ @if (wizard.resolvedBindings()?.settingsStore) { +
+ {{ wizard.resolvedBindings()?.settingsStore?.binding?.bindingRole }} + Resolved from: {{ wizard.resolvedBindings()?.settingsStore?.resolvedFrom }} + + {{ wizard.resolvedBindings()?.settingsStore?.binding?.isActive ? 'Active' : 'Inactive' }} + +
+ } @else { +
+ No settings store binding resolved. Consider adding one. +
+ } +
+
+ } +
+ } + + + @case ('validate') { +
+

Validate Readiness

+

Running readiness checks for the configured target and environment.

+ + @if (validationLoading()) { +
+
+ Running validation gates... +
+ } @else if (wizard.readinessReport()) { +
+ @if (wizard.readinessReport()?.isReady) { + + Target is ready for deployments. + } @else { + + Target is not ready. Review gate results below. + } +
+ +
+ @for (gate of wizard.readinessReport()?.gates ?? []; track gate.gateName) { +
+ + @switch (gate.status) { + @case ('pass') { + + } + @case ('fail') { + + } + @case ('warn') { + + } + @default { + + } + } + + {{ gate.gateName }} + {{ gate.status }} + @if (gate.message) { + {{ gate.message }} + } + @if (gate.durationMs) { + {{ gate.durationMs }}ms + } +
+ } +
+ + + } @else { +
+

Validation has not been run yet.

+ +
+ } +
+ } + + + @case ('done') { +
+
+ +
+

Topology Setup Complete

+

Your deployment topology is configured and validated.

+ +
+
+ + Region: {{ wizard.selectedRegion()?.displayName ?? 'N/A' }} +
+
+ + Environment: {{ wizard.createdEnvironment()?.displayName ?? 'N/A' }} +
+
+ + Target: {{ wizard.createdTarget()?.displayName ?? 'N/A' }} ({{ wizard.createdTarget()?.type ?? '' }}) +
+
+ + Agent: {{ wizard.selectedAgent()?.displayName ?? 'N/A' }} +
+
+ + Readiness: {{ wizard.readinessReport()?.isReady ? 'Ready' : 'Not Ready' }} +
+
+ + +
+ } + } +
+ + + @if (wizard.currentStep() !== 'done') { +
+ + +
+ } + + `, + styles: [` + .topo-wizard { + max-width: 860px; + margin: 0 auto; + padding: 1.5rem; + } + + /* Header */ + .wizard-header { + margin-bottom: 1.25rem; + } + + .wizard-header__back { + display: inline-flex; + align-items: center; + gap: 0.25rem; + margin-bottom: 0.5rem; + color: var(--color-brand-primary); + text-decoration: none; + font-size: 0.875rem; + + &:hover { + text-decoration: underline; + } + } + + .wizard-header__title { + margin: 0; + font-size: 1.5rem; + font-weight: var(--font-weight-semibold); + color: var(--color-text-heading); + } + + .wizard-header__subtitle { + margin: 0.25rem 0 0; + font-size: 0.82rem; + color: var(--color-text-secondary); + } + + /* Progress Bar */ + .wizard-progress-bar { + height: 4px; + background: var(--color-border-primary); + border-radius: 2px; + margin-bottom: 1rem; + overflow: hidden; + } + + .wizard-progress-bar__fill { + height: 100%; + background: var(--color-brand-primary); + border-radius: 2px; + transition: width 300ms ease; + } + + /* Step Indicators */ + .wizard-progress { + display: flex; + justify-content: space-between; + margin-bottom: 1.5rem; + position: relative; + + &::before { + content: ''; + position: absolute; + top: 15px; + left: 32px; + right: 32px; + height: 2px; + background: var(--color-border-primary); + } + } + + .progress-step { + display: flex; + flex-direction: column; + align-items: center; + position: relative; + z-index: 1; + min-width: 0; + } + + .progress-step__number { + width: 32px; + height: 32px; + border-radius: var(--radius-full); + background: var(--color-surface-primary); + border: 2px solid var(--color-border-primary); + display: flex; + align-items: center; + justify-content: center; + font-weight: var(--font-weight-semibold); + font-size: 0.8rem; + margin-bottom: 0.35rem; + transition: all 200ms ease; + } + + .progress-step__label { + font-size: 0.65rem; + color: var(--color-text-muted); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 72px; + text-align: center; + } + + .progress-step--active .progress-step__number { + border-color: var(--color-brand-primary); + color: var(--color-brand-primary); + } + + .progress-step--active .progress-step__label { + color: var(--color-brand-primary); + font-weight: var(--font-weight-medium); + } + + .progress-step--completed .progress-step__number { + background: var(--color-brand-primary); + border-color: var(--color-brand-primary); + color: white; + } + + .progress-step--completed .progress-step__label { + color: var(--color-text-secondary); + } + + /* Banner */ + .wizard-banner { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.6rem 0.75rem; + border-radius: var(--radius-md); + font-size: 0.78rem; + margin-bottom: 1rem; + } + + .wizard-banner--error { + color: var(--color-status-error-text); + border: 1px solid var(--color-status-error-border); + background: var(--color-status-error-bg); + } + + .wizard-banner__dismiss { + background: none; + border: none; + color: inherit; + cursor: pointer; + text-decoration: underline; + font-size: 0.72rem; + } + + /* Content */ + .wizard-content { + background: var(--color-surface-primary); + border: 1px solid var(--color-border-primary); + border-radius: var(--radius-lg); + padding: 1.75rem; + min-height: 340px; + } + + .step-content h2 { + margin: 0 0 0.5rem; + font-size: 1.15rem; + color: var(--color-text-heading); + } + + .step-content > p { + color: var(--color-text-secondary); + font-size: 0.82rem; + margin-bottom: 1.25rem; + } + + .step-content--center { + text-align: center; + } + + .step-section-title { + font-size: 0.82rem; + font-weight: var(--font-weight-semibold); + color: var(--color-text-secondary); + margin: 0 0 0.6rem; + text-transform: uppercase; + letter-spacing: 0.03em; + } + + .step-loading { + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 2rem; + color: var(--color-text-muted); + font-size: 0.82rem; + } + + .step-divider { + display: flex; + align-items: center; + gap: 0.75rem; + margin: 1.25rem 0; + color: var(--color-text-muted); + font-size: 0.72rem; + + &::before, + &::after { + content: ''; + flex: 1; + height: 1px; + background: var(--color-border-primary); + } + } + + .empty-state { + text-align: center; + padding: 2rem; + color: var(--color-text-muted); + font-size: 0.82rem; + + p { + margin: 0 0 1rem; + } + } + + /* Option List / Cards */ + .option-list { + display: grid; + gap: 0.5rem; + } + + .option-card { + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 0.75rem 1rem; + border: 2px solid var(--color-border-primary); + border-radius: var(--radius-lg); + background: var(--color-surface-primary); + cursor: pointer; + text-align: left; + transition: border-color 150ms ease, background 150ms ease; + + &:hover { + border-color: var(--color-brand-primary); + } + + &--selected { + border-color: var(--color-brand-primary); + background: rgba(59, 130, 246, 0.05); + } + } + + .option-card__name { + font-weight: var(--font-weight-semibold); + font-size: 0.875rem; + color: var(--color-text-primary); + } + + .option-card__meta { + font-size: 0.72rem; + color: var(--color-text-muted); + margin-top: 0.15rem; + } + + .option-card__desc { + font-size: 0.78rem; + color: var(--color-text-secondary); + margin-top: 0.25rem; + } + + /* Forms */ + .form-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; + margin-bottom: 0; + + @media (max-width: 600px) { + grid-template-columns: 1fr; + } + } + + .form-group { + margin-bottom: 1rem; + + label { + display: block; + font-weight: var(--font-weight-medium); + font-size: 0.8rem; + margin-bottom: 0.35rem; + color: var(--color-text-primary); + } + } + + .form-group--checkbox { + display: flex; + flex-direction: column; + justify-content: center; + + label { + display: flex; + align-items: center; + gap: 0.4rem; + cursor: pointer; + } + } + + .form-hint { + font-size: 0.7rem; + color: var(--color-text-muted); + margin-top: 0.2rem; + } + + .form-input { + width: 100%; + padding: 0.5rem 0.65rem; + border: 1px solid var(--color-border-primary); + border-radius: var(--radius-md); + background: var(--color-surface-primary); + color: var(--color-text-primary); + font-size: 0.82rem; + box-sizing: border-box; + + &:focus { + outline: none; + border-color: var(--color-brand-primary); + box-shadow: 0 0 0 2px var(--color-focus-ring); + } + + &::placeholder { + color: var(--color-text-muted); + } + } + + select.form-input { + cursor: pointer; + } + + .success-notice { + display: flex; + align-items: center; + gap: 0.4rem; + margin-top: 1rem; + padding: 0.5rem 0.75rem; + border-radius: var(--radius-md); + background: var(--color-status-success-bg, rgba(34, 197, 94, 0.1)); + color: var(--color-status-success-text, #16a34a); + font-size: 0.78rem; + border: 1px solid var(--color-status-success-border, rgba(34, 197, 94, 0.3)); + } + + /* Stage Order */ + .stage-order-list { + display: flex; + flex-direction: column; + gap: 0.35rem; + } + + .stage-order-item { + display: flex; + align-items: center; + gap: 0.6rem; + padding: 0.6rem 0.75rem; + background: var(--color-surface-secondary); + border-radius: var(--radius-md); + border: 1px solid var(--color-border-primary); + } + + .stage-order-item__index { + width: 24px; + height: 24px; + border-radius: var(--radius-full); + background: var(--color-brand-primary); + color: white; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.72rem; + font-weight: var(--font-weight-semibold); + flex-shrink: 0; + } + + .stage-order-item__info { + display: flex; + flex-direction: column; + flex: 1; + } + + .stage-order-item__name { + font-weight: var(--font-weight-medium); + font-size: 0.82rem; + color: var(--color-text-primary); + } + + .stage-order-item__meta { + font-size: 0.7rem; + color: var(--color-text-muted); + } + + .stage-order-item__arrow { + color: var(--color-text-muted); + flex-shrink: 0; + } + + /* Infrastructure */ + .infra-sections { + display: grid; + gap: 0.75rem; + } + + .infra-section { + border: 1px solid var(--color-border-primary); + border-radius: var(--radius-md); + padding: 0.75rem; + } + + .infra-section__title { + display: flex; + align-items: center; + gap: 0.4rem; + margin: 0 0 0.5rem; + font-size: 0.85rem; + font-weight: var(--font-weight-semibold); + color: var(--color-text-heading); + } + + .infra-binding { + display: flex; + align-items: center; + gap: 0.6rem; + flex-wrap: wrap; + font-size: 0.78rem; + } + + .infra-binding--missing { + color: var(--color-text-muted); + font-style: italic; + } + + .infra-binding__role { + font-weight: var(--font-weight-medium); + color: var(--color-text-primary); + } + + .infra-binding__inherited { + color: var(--color-text-muted); + font-size: 0.72rem; + } + + .infra-binding__status { + padding: 0.1rem 0.45rem; + border-radius: var(--radius-sm); + font-size: 0.68rem; + font-weight: var(--font-weight-medium); + background: var(--color-surface-tertiary); + color: var(--color-text-muted); + } + + .infra-binding__status--active { + background: var(--color-status-success-bg, rgba(34, 197, 94, 0.1)); + color: var(--color-status-success-text, #16a34a); + } + + /* Validation */ + .validation-summary { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1rem; + border-radius: var(--radius-md); + font-size: 0.85rem; + font-weight: var(--font-weight-medium); + margin-bottom: 1rem; + } + + .validation-summary--ready { + background: var(--color-status-success-bg, rgba(34, 197, 94, 0.1)); + color: var(--color-status-success-text, #16a34a); + border: 1px solid var(--color-status-success-border, rgba(34, 197, 94, 0.3)); + } + + .validation-summary--not-ready { + background: var(--color-status-error-bg); + color: var(--color-status-error-text); + border: 1px solid var(--color-status-error-border); + } + + .gate-results { + display: grid; + gap: 0.35rem; + margin-bottom: 1rem; + } + + .gate-row { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0.65rem; + border-radius: var(--radius-sm); + font-size: 0.78rem; + background: var(--color-surface-secondary); + border: 1px solid var(--color-border-primary); + } + + .gate-row--pass { + border-left: 3px solid var(--color-status-success-text, #16a34a); + } + + .gate-row--fail { + border-left: 3px solid var(--color-severity-critical, #dc2626); + } + + .gate-row--warn { + border-left: 3px solid var(--color-severity-medium, #f59e0b); + } + + .gate-row__icon { + display: flex; + align-items: center; + flex-shrink: 0; + } + + .gate-row--pass .gate-row__icon { + color: var(--color-status-success-text, #16a34a); + } + + .gate-row--fail .gate-row__icon { + color: var(--color-severity-critical, #dc2626); + } + + .gate-row--warn .gate-row__icon { + color: var(--color-severity-medium, #f59e0b); + } + + .gate-row__name { + font-weight: var(--font-weight-medium); + color: var(--color-text-primary); + } + + .gate-row__status { + text-transform: uppercase; + font-size: 0.68rem; + font-weight: var(--font-weight-semibold); + letter-spacing: 0.03em; + } + + .gate-row__message { + flex: 1; + color: var(--color-text-secondary); + font-size: 0.72rem; + } + + .gate-row__duration { + color: var(--color-text-muted); + font-size: 0.68rem; + flex-shrink: 0; + } + + /* Spinner */ + .spinner { + width: 32px; + height: 32px; + border: 3px solid var(--color-border-primary); + border-top-color: var(--color-brand-primary); + border-radius: var(--radius-full); + animation: spin 0.8s linear infinite; + } + + @keyframes spin { + to { transform: rotate(360deg); } + } + + /* Done */ + .complete-icon { + width: 64px; + height: 64px; + border-radius: var(--radius-full); + background: var(--color-status-success, #22c55e); + color: white; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 1rem; + } + + .done-summary { + display: flex; + flex-direction: column; + gap: 0.5rem; + margin: 1.5rem auto; + max-width: 360px; + text-align: left; + } + + .done-summary__item { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.82rem; + color: var(--color-text-primary); + + svg { + color: var(--color-status-success-text, #16a34a); + flex-shrink: 0; + } + } + + .complete-actions { + display: flex; + gap: 0.75rem; + justify-content: center; + margin-top: 1.5rem; + } + + /* Footer */ + .wizard-footer { + display: flex; + justify-content: space-between; + margin-top: 1.25rem; + } + + /* Buttons */ + .btn { + padding: 0.625rem 1.25rem; + border-radius: var(--radius-md); + font-size: 0.875rem; + font-weight: var(--font-weight-medium); + cursor: pointer; + border: 1px solid transparent; + text-decoration: none; + display: inline-flex; + align-items: center; + gap: 0.35rem; + transition: background-color 150ms ease; + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } + } + + .btn--primary { + background: var(--color-brand-primary); + color: var(--color-text-heading); + + &:hover:not(:disabled) { + background: var(--color-brand-primary-hover); + } + } + + .btn--secondary { + background: var(--color-surface-primary); + border-color: var(--color-border-primary); + color: var(--color-text-primary); + + &:hover:not(:disabled) { + background: var(--color-nav-hover); + } + } + + .btn--sm { + padding: 0.4rem 0.85rem; + font-size: 0.78rem; + } + + /* Reduced motion */ + @media (prefers-reduced-motion: reduce) { + .wizard-progress-bar__fill, + .spinner { + animation: none; + transition: none; + } + } + `], +}) +export class TopologyWizardComponent implements OnInit, OnDestroy { + readonly wizard = inject(TopologyWizardService); + + // Local UI state + readonly regions = signal([]); + readonly agents = signal([]); + readonly orderedEnvironments = signal([]); + readonly regionsLoading = signal(false); + readonly agentsLoading = signal(false); + readonly environmentsLoading = signal(false); + readonly bindingsLoading = signal(false); + readonly validationLoading = signal(false); + + // Region creation form + readonly newRegionName = signal(''); + readonly newRegionDisplayName = signal(''); + readonly newRegionDescription = signal(''); + readonly newRegionCrypto = signal('default'); + + // Environment creation form + readonly newEnvName = signal(''); + readonly newEnvDisplayName = signal(''); + readonly newEnvOrderIndex = signal(0); + readonly newEnvIsProduction = signal(false); + + // Target creation form + readonly newTargetName = signal(''); + readonly newTargetDisplayName = signal(''); + readonly newTargetType = signal('DockerHost'); + + readonly progressPercent = computed(() => { + const total = this.wizard.steps.length; + const current = this.wizard.stepIndex(); + return Math.round(((current + 1) / total) * 100); + }); + + private subscriptions: Subscription[] = []; + + constructor() { + // Auto-load data when entering specific steps + effect(() => { + const step = this.wizard.currentStep(); + if (step === 'region') { + this.loadRegions(); + } else if (step === 'stage-order') { + this.loadEnvironments(); + } else if (step === 'agent') { + this.loadAgents(); + } else if (step === 'infrastructure') { + this.loadBindings(); + } else if (step === 'validate') { + this.runValidation(); + } + }); + } + + ngOnInit(): void { + this.wizard.reset(); + } + + ngOnDestroy(): void { + this.subscriptions.forEach(s => s.unsubscribe()); + } + + isStepCompleted(stepKey: WizardStep): boolean { + const currentIndex = this.wizard.steps.findIndex(s => s.key === this.wizard.currentStep()); + const stepIndex = this.wizard.steps.findIndex(s => s.key === stepKey); + return stepIndex < currentIndex; + } + + onNext(): void { + this.wizard.goNext(); + } + + // --- Region --- + selectRegion(region: Region): void { + this.wizard.selectedRegion.set(region); + } + + createRegion(): void { + this.wizard.loading.set(true); + const sub = this.wizard.createRegion({ + name: this.newRegionName(), + displayName: this.newRegionDisplayName(), + description: this.newRegionDescription() || undefined, + cryptoProfile: this.newRegionCrypto(), + }).subscribe({ + next: (region) => { + this.wizard.selectedRegion.set(region); + this.regions.update(list => [...list, region]); + this.wizard.loading.set(false); + this.wizard.error.set(null); + }, + error: (err: unknown) => { + this.wizard.error.set(err instanceof Error ? err.message : 'Failed to create region.'); + this.wizard.loading.set(false); + }, + }); + this.subscriptions.push(sub); + } + + // --- Environment --- + createEnvironment(): void { + this.wizard.loading.set(true); + const sub = this.wizard.createEnvironment({ + name: this.newEnvName(), + displayName: this.newEnvDisplayName(), + orderIndex: this.newEnvOrderIndex(), + isProduction: this.newEnvIsProduction(), + regionId: this.wizard.selectedRegion()?.id, + }).subscribe({ + next: (env) => { + this.wizard.createdEnvironment.set(env); + this.wizard.loading.set(false); + this.wizard.error.set(null); + }, + error: (err: unknown) => { + this.wizard.error.set(err instanceof Error ? err.message : 'Failed to create environment.'); + this.wizard.loading.set(false); + }, + }); + this.subscriptions.push(sub); + } + + // --- Target --- + createTarget(): void { + this.wizard.loading.set(true); + const sub = this.wizard.createTarget({ + name: this.newTargetName(), + displayName: this.newTargetDisplayName(), + type: this.newTargetType(), + environmentId: this.wizard.createdEnvironment()?.id, + }).subscribe({ + next: (target) => { + this.wizard.createdTarget.set(target); + this.wizard.loading.set(false); + this.wizard.error.set(null); + }, + error: (err: unknown) => { + this.wizard.error.set(err instanceof Error ? err.message : 'Failed to create target.'); + this.wizard.loading.set(false); + }, + }); + this.subscriptions.push(sub); + } + + // --- Agent --- + selectAgent(agent: Agent): void { + this.wizard.selectedAgent.set(agent); + const targetId = this.wizard.createdTarget()?.id; + if (targetId) { + const sub = this.wizard.assignAgent(targetId, agent.id).subscribe({ + error: (err: unknown) => { + this.wizard.error.set(err instanceof Error ? err.message : 'Failed to assign agent.'); + }, + }); + this.subscriptions.push(sub); + } + } + + // --- Data Loading --- + private loadRegions(): void { + this.regionsLoading.set(true); + const sub = this.wizard.listRegions().subscribe({ + next: (res) => { + this.regions.set(res.items); + this.regionsLoading.set(false); + }, + error: () => this.regionsLoading.set(false), + }); + this.subscriptions.push(sub); + } + + private loadEnvironments(): void { + this.environmentsLoading.set(true); + const sub = this.wizard.listEnvironments().subscribe({ + next: (res) => { + const sorted = [...res.items].sort((a, b) => a.orderIndex - b.orderIndex); + this.orderedEnvironments.set(sorted); + this.environmentsLoading.set(false); + }, + error: () => this.environmentsLoading.set(false), + }); + this.subscriptions.push(sub); + } + + private loadAgents(): void { + this.agentsLoading.set(true); + const sub = this.wizard.listAgents().subscribe({ + next: (res) => { + this.agents.set(res.items); + this.agentsLoading.set(false); + }, + error: () => this.agentsLoading.set(false), + }); + this.subscriptions.push(sub); + } + + private loadBindings(): void { + const envId = this.wizard.createdEnvironment()?.id; + if (!envId) return; + + this.bindingsLoading.set(true); + const sub = this.wizard.resolveAllBindings(envId).subscribe({ + next: (bindings) => { + this.wizard.resolvedBindings.set(bindings); + this.bindingsLoading.set(false); + }, + error: () => this.bindingsLoading.set(false), + }); + this.subscriptions.push(sub); + } + + runValidation(): void { + const targetId = this.wizard.createdTarget()?.id; + if (!targetId) return; + + this.validationLoading.set(true); + const sub = this.wizard.validateTarget(targetId).subscribe({ + next: (report) => { + this.wizard.readinessReport.set(report); + this.validationLoading.set(false); + }, + error: (err: unknown) => { + this.wizard.error.set(err instanceof Error ? err.message : 'Validation failed.'); + this.validationLoading.set(false); + }, + }); + this.subscriptions.push(sub); + } +} diff --git a/src/Web/StellaOps.Web/src/app/features/platform/setup/topology-wizard/topology-wizard.service.ts b/src/Web/StellaOps.Web/src/app/features/platform/setup/topology-wizard/topology-wizard.service.ts new file mode 100644 index 000000000..95c757e85 --- /dev/null +++ b/src/Web/StellaOps.Web/src/app/features/platform/setup/topology-wizard/topology-wizard.service.ts @@ -0,0 +1,194 @@ +/** + * Topology Wizard Service + * + * Signal-based state management for the multi-step topology setup wizard. + * Manages wizard navigation, form state, and API calls for creating + * regions, environments, targets, agents, and infrastructure bindings. + */ + +import { Injectable, inject, signal, computed } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable, catchError, of } from 'rxjs'; + +export type WizardStep = 'region' | 'environment' | 'stage-order' | 'target' | 'agent' | 'infrastructure' | 'validate' | 'done'; + +export interface Region { + id: string; + name: string; + displayName: string; + description?: string; + cryptoProfile: string; + sortOrder: number; + status: string; +} + +export interface Environment { + id: string; + name: string; + displayName: string; + orderIndex: number; + isProduction: boolean; +} + +export interface Target { + id: string; + name: string; + displayName: string; + type: string; + healthStatus: string; + agentId?: string; +} + +export interface Agent { + id: string; + name: string; + displayName: string; + status: string; +} + +export interface InfrastructureBinding { + id: string; + integrationId: string; + scopeType: string; + scopeId?: string; + bindingRole: string; + priority: number; + isActive: boolean; +} + +export interface ResolvedBindings { + registry?: { binding: InfrastructureBinding; resolvedFrom: string }; + vault?: { binding: InfrastructureBinding; resolvedFrom: string }; + settingsStore?: { binding: InfrastructureBinding; resolvedFrom: string }; +} + +export interface GateResult { + gateName: string; + status: string; + message?: string; + checkedAt?: string; + durationMs?: number; +} + +export interface ReadinessReport { + targetId: string; + environmentId: string; + isReady: boolean; + gates: GateResult[]; + evaluatedAt: string; +} + +@Injectable({ providedIn: 'root' }) +export class TopologyWizardService { + private readonly http = inject(HttpClient); + + // State + readonly currentStep = signal('region'); + readonly selectedRegion = signal(null); + readonly createdEnvironment = signal(null); + readonly createdTarget = signal(null); + readonly selectedAgent = signal(null); + readonly resolvedBindings = signal(null); + readonly readinessReport = signal(null); + readonly loading = signal(false); + readonly error = signal(null); + + readonly stepIndex = computed(() => { + const steps: WizardStep[] = ['region', 'environment', 'stage-order', 'target', 'agent', 'infrastructure', 'validate', 'done']; + return steps.indexOf(this.currentStep()); + }); + + readonly canGoNext = computed(() => { + switch (this.currentStep()) { + case 'region': return this.selectedRegion() !== null; + case 'environment': return this.createdEnvironment() !== null; + case 'stage-order': return true; + case 'target': return this.createdTarget() !== null; + case 'agent': return this.selectedAgent() !== null; + case 'infrastructure': return true; + case 'validate': return this.readinessReport()?.isReady === true; + default: return false; + } + }); + + readonly steps: { key: WizardStep; label: string }[] = [ + { key: 'region', label: 'Region' }, + { key: 'environment', label: 'Environment' }, + { key: 'stage-order', label: 'Stage Order' }, + { key: 'target', label: 'Target' }, + { key: 'agent', label: 'Agent' }, + { key: 'infrastructure', label: 'Infrastructure' }, + { key: 'validate', label: 'Validate' }, + { key: 'done', label: 'Done' }, + ]; + + goNext(): void { + const idx = this.stepIndex(); + if (idx < this.steps.length - 1) { + this.currentStep.set(this.steps[idx + 1].key); + } + } + + goPrevious(): void { + const idx = this.stepIndex(); + if (idx > 0) { + this.currentStep.set(this.steps[idx - 1].key); + } + } + + reset(): void { + this.currentStep.set('region'); + this.selectedRegion.set(null); + this.createdEnvironment.set(null); + this.createdTarget.set(null); + this.selectedAgent.set(null); + this.resolvedBindings.set(null); + this.readinessReport.set(null); + this.error.set(null); + } + + // API calls + listRegions(): Observable<{ items: Region[]; totalCount: number }> { + return this.http.get<{ items: Region[]; totalCount: number }>('/api/v1/regions').pipe(catchError(() => of({ items: [], totalCount: 0 }))); + } + + createRegion(data: Partial): Observable { + return this.http.post('/api/v1/regions', data); + } + + createEnvironment(data: Partial & { regionId?: string }): Observable { + return this.http.post('/api/v1/environments', data); + } + + listEnvironments(): Observable<{ items: Environment[] }> { + return this.http.get<{ items: Environment[] }>('/api/v1/environments').pipe(catchError(() => of({ items: [] }))); + } + + createTarget(data: Partial & { environmentId?: string }): Observable { + return this.http.post('/api/v1/targets', data); + } + + listAgents(): Observable<{ items: Agent[] }> { + return this.http.get<{ items: Agent[] }>('/api/v1/agents').pipe(catchError(() => of({ items: [] }))); + } + + assignAgent(targetId: string, agentId: string): Observable { + return this.http.post(`/api/v1/targets/${targetId}/assign-agent`, { agentId }); + } + + resolveAllBindings(environmentId: string): Observable { + return this.http.get(`/api/v1/infrastructure-bindings/resolve-all?environmentId=${environmentId}`); + } + + createBinding(data: Partial): Observable { + return this.http.post('/api/v1/infrastructure-bindings', data); + } + + testBinding(bindingId: string): Observable { + return this.http.post(`/api/v1/infrastructure-bindings/${bindingId}/test`, {}); + } + + validateTarget(targetId: string): Observable { + return this.http.post(`/api/v1/targets/${targetId}/validate`, {}); + } +} diff --git a/src/Web/StellaOps.Web/src/app/features/policy-simulation/batch-evaluation.component.ts b/src/Web/StellaOps.Web/src/app/features/policy-simulation/batch-evaluation.component.ts index fd4ac1936..002d259e3 100644 --- a/src/Web/StellaOps.Web/src/app/features/policy-simulation/batch-evaluation.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/policy-simulation/batch-evaluation.component.ts @@ -1260,7 +1260,7 @@ export class BatchEvaluationComponent implements OnInit, OnDestroy { tags: this.tags().length ? this.tags() : undefined, }; - this.api.startBatchEvaluation(input, { tenantId: 'default' }).subscribe({ + this.api.startBatchEvaluation(input).subscribe({ next: (batch) => { this.currentBatch.set(batch); if (this.isTerminalStatus(batch.status)) { @@ -1289,7 +1289,7 @@ export class BatchEvaluationComponent implements OnInit, OnDestroy { } private refreshBatch(batchId: string): void { - this.api.getBatchEvaluation(batchId, { tenantId: 'default' }).subscribe({ + this.api.getBatchEvaluation(batchId).subscribe({ next: (batch) => { this.currentBatch.set(batch); if (this.isTerminalStatus(batch.status)) { @@ -1319,7 +1319,7 @@ export class BatchEvaluationComponent implements OnInit, OnDestroy { return; } - this.api.cancelBatchEvaluation(current.batchId, { tenantId: 'default' }).subscribe({ + this.api.cancelBatchEvaluation(current.batchId).subscribe({ next: () => { this.currentBatch.set({ ...current, @@ -1358,7 +1358,7 @@ export class BatchEvaluationComponent implements OnInit, OnDestroy { } loadHistory(): void { - this.api.getBatchEvaluationHistory({ tenantId: 'default', page: 1, pageSize: 50 }).subscribe({ + this.api.getBatchEvaluationHistory({ page: 1, pageSize: 50 }).subscribe({ next: (history) => { const entries = [...history.items]; this.allHistoryEntries.set(entries); @@ -1385,7 +1385,7 @@ export class BatchEvaluationComponent implements OnInit, OnDestroy { } loadBatchDetails(batchId: string): void { - this.api.getBatchEvaluation(batchId, { tenantId: 'default' }).subscribe({ + this.api.getBatchEvaluation(batchId).subscribe({ next: (batch) => { this.currentBatch.set(batch); this.activeTab.set('new'); diff --git a/src/Web/StellaOps.Web/src/app/features/policy-simulation/conflict-detection.component.ts b/src/Web/StellaOps.Web/src/app/features/policy-simulation/conflict-detection.component.ts index 63809ca84..3a50c178b 100644 --- a/src/Web/StellaOps.Web/src/app/features/policy-simulation/conflict-detection.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/policy-simulation/conflict-detection.component.ts @@ -1030,7 +1030,6 @@ export class ConflictDetectionComponent implements OnInit { this.loading.set(true); this.api .detectConflicts({ - tenantId: 'default', policyIds: this.selectedPolicies(), includeResolved: true, }) @@ -1134,7 +1133,7 @@ export class ConflictDetectionComponent implements OnInit { ? conflict.targetValue : conflict.targetValue); - this.api.resolveConflict(conflict.id, selectedSuggestion.id, { tenantId: 'default' }).subscribe({ + this.api.resolveConflict(conflict.id, selectedSuggestion.id).subscribe({ next: () => { this.updateConflict(conflict.id, current => ({ ...current, @@ -1207,7 +1206,7 @@ export class ConflictDetectionComponent implements OnInit { this.loading.set(true); this.api - .autoResolveConflicts(unresolvedConflictIds, { tenantId: 'default' }) + .autoResolveConflicts(unresolvedConflictIds) .pipe(finalize(() => this.loading.set(false))) .subscribe({ next: (result) => { diff --git a/src/Web/StellaOps.Web/src/app/features/policy-simulation/coverage-fixture.component.spec.ts b/src/Web/StellaOps.Web/src/app/features/policy-simulation/coverage-fixture.component.spec.ts index 9daa55f78..9733db2a5 100644 --- a/src/Web/StellaOps.Web/src/app/features/policy-simulation/coverage-fixture.component.spec.ts +++ b/src/Web/StellaOps.Web/src/app/features/policy-simulation/coverage-fixture.component.spec.ts @@ -175,7 +175,6 @@ describe('CoverageFixtureComponent', () => { tick(); expect(mockApi.getCoverage).toHaveBeenCalledWith({ - tenantId: 'default', policyPackId: component.policyPackId, policyVersion: component.policyVersion, }); diff --git a/src/Web/StellaOps.Web/src/app/features/policy-simulation/coverage-fixture.component.ts b/src/Web/StellaOps.Web/src/app/features/policy-simulation/coverage-fixture.component.ts index 4423fedbb..41801c357 100644 --- a/src/Web/StellaOps.Web/src/app/features/policy-simulation/coverage-fixture.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/policy-simulation/coverage-fixture.component.ts @@ -793,7 +793,6 @@ export class CoverageFixtureComponent implements OnChanges, OnInit { loadCoverage(): void { this.loading.set(true); this.api.getCoverage({ - tenantId: 'default', policyPackId: this.policyPackId, policyVersion: this.policyVersion, }).pipe( diff --git a/src/Web/StellaOps.Web/src/app/features/policy-simulation/effective-policy-viewer.component.spec.ts b/src/Web/StellaOps.Web/src/app/features/policy-simulation/effective-policy-viewer.component.spec.ts index 933397586..4f726f6ab 100644 --- a/src/Web/StellaOps.Web/src/app/features/policy-simulation/effective-policy-viewer.component.spec.ts +++ b/src/Web/StellaOps.Web/src/app/features/policy-simulation/effective-policy-viewer.component.spec.ts @@ -117,7 +117,6 @@ describe('EffectivePolicyViewerComponent', () => { tick(); expect(mockApi.getEffectivePolicies).toHaveBeenCalledWith({ - tenantId: 'default', resourceType: undefined, }); })); @@ -128,7 +127,6 @@ describe('EffectivePolicyViewerComponent', () => { tick(); expect(mockApi.getEffectivePolicies).toHaveBeenCalledWith({ - tenantId: 'default', resourceType: 'image', }); })); diff --git a/src/Web/StellaOps.Web/src/app/features/policy-simulation/effective-policy-viewer.component.ts b/src/Web/StellaOps.Web/src/app/features/policy-simulation/effective-policy-viewer.component.ts index d9dec2828..f020630ca 100644 --- a/src/Web/StellaOps.Web/src/app/features/policy-simulation/effective-policy-viewer.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/policy-simulation/effective-policy-viewer.component.ts @@ -519,7 +519,6 @@ export class EffectivePolicyViewerComponent implements OnInit { const resourceType = this.filterForm.value.resourceType as ResourceType | ''; this.api.getEffectivePolicies({ - tenantId: 'default', resourceType: resourceType || undefined, }).pipe( finalize(() => this.loading.set(false)) diff --git a/src/Web/StellaOps.Web/src/app/features/policy-simulation/policy-audit-log.component.spec.ts b/src/Web/StellaOps.Web/src/app/features/policy-simulation/policy-audit-log.component.spec.ts index e48fc2e29..200ea3db0 100644 --- a/src/Web/StellaOps.Web/src/app/features/policy-simulation/policy-audit-log.component.spec.ts +++ b/src/Web/StellaOps.Web/src/app/features/policy-simulation/policy-audit-log.component.spec.ts @@ -149,7 +149,6 @@ describe('PolicyAuditLogComponent', () => { expect(mockApi.getAuditLog).toHaveBeenCalledWith( jasmine.objectContaining({ - tenantId: 'default', page: 1, pageSize: 20, }) diff --git a/src/Web/StellaOps.Web/src/app/features/policy-simulation/policy-audit-log.component.ts b/src/Web/StellaOps.Web/src/app/features/policy-simulation/policy-audit-log.component.ts index 9cac474dc..51bea5490 100644 --- a/src/Web/StellaOps.Web/src/app/features/policy-simulation/policy-audit-log.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/policy-simulation/policy-audit-log.component.ts @@ -591,7 +591,6 @@ export class PolicyAuditLogComponent implements OnInit { const formValue = this.filterForm.value; this.api.getAuditLog({ - tenantId: 'default', policyPackId: formValue.policyPackId || undefined, action: formValue.action as PolicyAuditAction || undefined, page: this.currentPage, diff --git a/src/Web/StellaOps.Web/src/app/features/policy-simulation/policy-exception.component.spec.ts b/src/Web/StellaOps.Web/src/app/features/policy-simulation/policy-exception.component.spec.ts index 691464a79..dd4874238 100644 --- a/src/Web/StellaOps.Web/src/app/features/policy-simulation/policy-exception.component.spec.ts +++ b/src/Web/StellaOps.Web/src/app/features/policy-simulation/policy-exception.component.spec.ts @@ -119,7 +119,7 @@ describe('PolicyExceptionComponent', () => { component.loadExceptions(); tick(); - expect(mockApi.listExceptions).toHaveBeenCalledWith({ tenantId: 'default' }); + expect(mockApi.listExceptions).toHaveBeenCalledWith({}); })); it('should set loading state during API call', fakeAsync(() => { diff --git a/src/Web/StellaOps.Web/src/app/features/policy-simulation/policy-exception.component.ts b/src/Web/StellaOps.Web/src/app/features/policy-simulation/policy-exception.component.ts index 7b40e26e1..33b1b136c 100644 --- a/src/Web/StellaOps.Web/src/app/features/policy-simulation/policy-exception.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/policy-simulation/policy-exception.component.ts @@ -733,9 +733,7 @@ export class PolicyExceptionComponent implements OnInit { loadExceptions(): void { this.loading.set(true); - this.api.listExceptions({ - tenantId: 'default', - }).pipe( + this.api.listExceptions({}).pipe( finalize(() => this.loading.set(false)) ).subscribe({ next: (result) => { diff --git a/src/Web/StellaOps.Web/src/app/features/policy-simulation/policy-simulation-direct-route-defaults.spec.ts b/src/Web/StellaOps.Web/src/app/features/policy-simulation/policy-simulation-direct-route-defaults.spec.ts index 1c65343bf..53292476f 100644 --- a/src/Web/StellaOps.Web/src/app/features/policy-simulation/policy-simulation-direct-route-defaults.spec.ts +++ b/src/Web/StellaOps.Web/src/app/features/policy-simulation/policy-simulation-direct-route-defaults.spec.ts @@ -109,7 +109,6 @@ describe('policy simulation direct-route defaults', () => { expect(component.policyPackId).toBe('policy-pack-001'); expect(component.policyVersion).toBeUndefined(); expect(api.getCoverage).toHaveBeenCalledWith({ - tenantId: 'default', policyPackId: 'policy-pack-001', policyVersion: undefined, }); @@ -194,7 +193,6 @@ describe('policy simulation direct-route defaults', () => { expect(component.policyVersion).toBe(2); expect(component.targetEnvironment).toBe('production'); expect(api.checkPromotionGate).toHaveBeenCalledWith({ - tenantId: 'default', policyPackId: 'policy-pack-001', policyVersion: 2, targetEnvironment: 'production', diff --git a/src/Web/StellaOps.Web/src/app/features/policy-simulation/promotion-gate.component.spec.ts b/src/Web/StellaOps.Web/src/app/features/policy-simulation/promotion-gate.component.spec.ts index 9686913a4..30e3f3dd6 100644 --- a/src/Web/StellaOps.Web/src/app/features/policy-simulation/promotion-gate.component.spec.ts +++ b/src/Web/StellaOps.Web/src/app/features/policy-simulation/promotion-gate.component.spec.ts @@ -180,7 +180,6 @@ describe('PromotionGateComponent', () => { tick(); expect(mockApi.checkPromotionGate).toHaveBeenCalledWith({ - tenantId: 'default', policyPackId: component.policyPackId, policyVersion: component.policyVersion, targetEnvironment: component.targetEnvironment, diff --git a/src/Web/StellaOps.Web/src/app/features/policy-simulation/promotion-gate.component.ts b/src/Web/StellaOps.Web/src/app/features/policy-simulation/promotion-gate.component.ts index 20b2cbc86..1c3794a18 100644 --- a/src/Web/StellaOps.Web/src/app/features/policy-simulation/promotion-gate.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/policy-simulation/promotion-gate.component.ts @@ -684,7 +684,6 @@ export class PromotionGateComponent implements OnChanges { checkGate(): void { this.loading.set(true); this.api.checkPromotionGate({ - tenantId: 'default', policyPackId: this.policyPackId, policyVersion: this.policyVersion, targetEnvironment: this.targetEnvironment, diff --git a/src/Web/StellaOps.Web/src/app/features/policy-simulation/shadow-mode-dashboard.component.spec.ts b/src/Web/StellaOps.Web/src/app/features/policy-simulation/shadow-mode-dashboard.component.spec.ts index 95de388f6..7bf0f3f4b 100644 --- a/src/Web/StellaOps.Web/src/app/features/policy-simulation/shadow-mode-dashboard.component.spec.ts +++ b/src/Web/StellaOps.Web/src/app/features/policy-simulation/shadow-mode-dashboard.component.spec.ts @@ -191,7 +191,6 @@ describe('ShadowModeDashboardComponent', () => { tick(); expect(mockApi.getShadowModeResults).toHaveBeenCalledWith(jasmine.objectContaining({ - tenantId: 'default', divergedOnly: false, })); })); diff --git a/src/Web/StellaOps.Web/src/app/features/policy-simulation/shadow-mode-dashboard.component.ts b/src/Web/StellaOps.Web/src/app/features/policy-simulation/shadow-mode-dashboard.component.ts index 6199ba24d..ace057aa5 100644 --- a/src/Web/StellaOps.Web/src/app/features/policy-simulation/shadow-mode-dashboard.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/policy-simulation/shadow-mode-dashboard.component.ts @@ -634,7 +634,6 @@ export class ShadowModeDashboardComponent implements OnInit { private buildResultsQuery() { const timeRange = this.filterForm.value.timeRange ?? '24h'; return { - tenantId: 'default', fromTime: this.calculateFromTime(timeRange), toTime: new Date().toISOString(), divergedOnly: this.filterForm.value.showOnly === 'diverged', diff --git a/src/Web/StellaOps.Web/src/app/features/policy-simulation/simulation-history.component.ts b/src/Web/StellaOps.Web/src/app/features/policy-simulation/simulation-history.component.ts index d351b2b2d..1bf209319 100644 --- a/src/Web/StellaOps.Web/src/app/features/policy-simulation/simulation-history.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/policy-simulation/simulation-history.component.ts @@ -1105,7 +1105,7 @@ export class SimulationHistoryComponent implements OnInit { this.loading.set(true); this.api - .compareSimulations(baseId, compareId, { tenantId: 'default' }) + .compareSimulations(baseId, compareId) .pipe(finalize(() => this.loading.set(false))) .subscribe({ next: (result) => this.comparisonResult.set(result), @@ -1127,7 +1127,7 @@ export class SimulationHistoryComponent implements OnInit { this.loading.set(true); this.api - .verifyReproducibility(simulationId, { tenantId: 'default' }) + .verifyReproducibility(simulationId) .pipe(finalize(() => this.loading.set(false))) .subscribe({ next: (result) => this.reproducibilityResult.set(result), @@ -1155,7 +1155,7 @@ export class SimulationHistoryComponent implements OnInit { ), }); - this.api.pinSimulation(entry.simulationId, nextPinned, { tenantId: 'default' }).subscribe({ + this.api.pinSimulation(entry.simulationId, nextPinned).subscribe({ error: () => { const rollbackResult = this.historyResult(); if (!rollbackResult) { @@ -1179,7 +1179,6 @@ export class SimulationHistoryComponent implements OnInit { const range = this.resolveDateRange(formValue.dateRange ?? '30d'); return { - tenantId: 'default', policyPackId: formValue.policyPackId?.trim() || undefined, status: (formValue.status as SimulationStatus) || undefined, fromDate: range.fromDate, diff --git a/src/Web/StellaOps.Web/src/app/features/topology/pending-deletions-panel.component.ts b/src/Web/StellaOps.Web/src/app/features/topology/pending-deletions-panel.component.ts new file mode 100644 index 000000000..6881a589b --- /dev/null +++ b/src/Web/StellaOps.Web/src/app/features/topology/pending-deletions-panel.component.ts @@ -0,0 +1,421 @@ +/** + * Pending Deletions Panel + * + * Displays all active pending deletions for the current tenant with + * live countdown timers showing time remaining until confirmation is allowed. + * Operators can confirm (after cool-off) or cancel pending deletions. + */ +import { + Component, + ChangeDetectionStrategy, + OnInit, + OnDestroy, + inject, + signal, + computed, +} from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TopologySetupClient, PendingDeletion } from '../../core/api/topology-setup.client'; +import { catchError, of } from 'rxjs'; + +interface DeletionRow { + deletion: PendingDeletion; + remainingMs: number; + canConfirm: boolean; + confirmingId: string | null; + cancellingId: string | null; +} + +@Component({ + selector: 'app-pending-deletions-panel', + standalone: true, + imports: [CommonModule], + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +
+
+

Pending Deletions

+

Active deletion requests awaiting cool-off or confirmation

+ +
+ + @if (loading() && rows().length === 0) { +
Loading pending deletions...
+ } + + @if (!loading() && rows().length === 0) { +
+

No pending deletions. All topology entities are active.

+
+ } + + @if (rows().length > 0) { +
+ @for (row of rows(); track row.deletion.id) { +
+
+ {{ formatEntityType(row.deletion.entityType) }} + + {{ row.deletion.status }} + +
+ +
{{ row.deletion.entityName }}
+ + @if (row.deletion.reason) { +
{{ row.deletion.reason }}
+ } + + + @if (hasCascadeItems(row.deletion)) { +
+ @for (item of formatCascade(row.deletion); track item) { + {{ item }} + } +
+ } + + + @if (row.deletion.status === 'pending') { +
+ @if (row.canConfirm) { + Cool-off expired — ready for confirmation + } @else { + Confirm available in {{ formatDuration(row.remainingMs) }} + } +
+ } + + @if (row.deletion.status === 'executing') { +
Executing cascade deletion...
+ } + + +
+ Requested {{ formatDate(row.deletion.requestedAt) }} + @if (row.deletion.confirmedAt) { + Confirmed {{ formatDate(row.deletion.confirmedAt) }} + } +
+ + + @if (row.deletion.status === 'pending') { +
+ + +
+ } +
+ } +
+ } + + @if (lastRefresh()) { +

Last refresh: {{ lastRefresh() }}

+ } +
+ `, + styles: [` + .pending-deletions { padding: 1.5rem; } + + .panel-header { + display: flex; + align-items: center; + gap: 1rem; + margin-bottom: 1.5rem; + } + .panel-header h2 { + margin: 0; + font-size: 1.25rem; + color: var(--color-text-heading, #fff); + } + .subtitle { + color: var(--color-text-muted, #888); + margin: 0; + flex: 1; + font-size: 0.875rem; + } + + .btn { + padding: 0.5rem 1rem; + border-radius: var(--radius-md, 6px); + cursor: pointer; + font-size: 0.875rem; + border: none; + font-weight: var(--font-weight-medium, 500); + } + .btn--secondary { + background: var(--color-surface-secondary, #1e1e2e); + color: var(--color-text-primary, #e0e0e0); + border: 1px solid var(--color-border-primary, #333); + } + .btn--secondary:hover:not(:disabled) { + background: var(--color-surface-tertiary, #252535); + } + .btn--danger { + background: var(--color-severity-critical, #dc2626); + color: white; + } + .btn--danger:hover:not(:disabled) { + background: var(--color-status-error-text, #b91c1c); + } + .btn--danger:disabled, .btn--secondary:disabled { + opacity: 0.55; + cursor: not-allowed; + } + .btn--sm { padding: 0.375rem 0.75rem; font-size: 0.8125rem; } + + .empty-state { + padding: 2rem; + text-align: center; + color: var(--color-text-muted, #888); + } + + .deletions-list { + display: flex; + flex-direction: column; + gap: 1rem; + } + + .deletion-card { + padding: 1rem 1.25rem; + background: var(--color-surface-secondary, #1e1e2e); + border: 1px solid var(--color-border-primary, #333); + border-radius: var(--radius-lg, 8px); + transition: border-color 200ms ease; + } + .deletion-card--ready { + border-color: var(--color-severity-critical, #dc2626); + } + + .deletion-card__header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.5rem; + } + .deletion-card__type { + font-size: 0.6875rem; + font-weight: var(--font-weight-semibold, 600); + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--color-text-muted, #888); + } + .deletion-card__status { + font-size: 0.6875rem; + font-weight: var(--font-weight-semibold, 600); + padding: 0.15rem 0.5rem; + border-radius: var(--radius-full, 100px); + text-transform: uppercase; + letter-spacing: 0.03em; + } + .deletion-card__status--pending { + background: rgba(250, 204, 21, 0.12); + color: var(--color-severity-medium, #eab308); + } + .deletion-card__status--confirmed { + background: rgba(220, 38, 38, 0.12); + color: var(--color-severity-critical, #dc2626); + } + .deletion-card__status--executing { + background: rgba(220, 38, 38, 0.2); + color: var(--color-severity-critical, #dc2626); + } + .deletion-card__status--completed { + background: rgba(107, 114, 128, 0.12); + color: var(--color-text-muted, #888); + } + .deletion-card__status--cancelled { + background: rgba(34, 197, 94, 0.12); + color: var(--color-status-success, #22c55e); + } + + .deletion-card__name { + font-size: 1rem; + font-weight: var(--font-weight-semibold, 600); + color: var(--color-text-primary, #e0e0e0); + margin-bottom: 0.375rem; + } + + .deletion-card__reason { + font-size: 0.8125rem; + color: var(--color-text-secondary, #aaa); + margin-bottom: 0.5rem; + } + + .deletion-card__cascade { + display: flex; + flex-wrap: wrap; + gap: 0.375rem; + margin-bottom: 0.75rem; + } + .cascade-badge { + font-size: 0.6875rem; + padding: 0.2rem 0.5rem; + border-radius: var(--radius-md, 6px); + background: var(--color-surface-tertiary, #252535); + color: var(--color-text-secondary, #aaa); + } + + .deletion-card__timer { + font-size: 0.8125rem; + font-weight: var(--font-weight-medium, 500); + padding: 0.375rem 0.75rem; + border-radius: var(--radius-md, 6px); + background: var(--color-surface-tertiary, #252535); + color: var(--color-text-muted, #888); + margin-bottom: 0.75rem; + } + .deletion-card__timer--ready { + background: rgba(220, 38, 38, 0.08); + color: var(--color-severity-critical, #dc2626); + } + + .deletion-card__meta { + display: flex; + gap: 1rem; + font-size: 0.6875rem; + color: var(--color-text-muted, #666); + margin-bottom: 0.75rem; + } + + .deletion-card__actions { + display: flex; + gap: 0.5rem; + } + + .last-refresh { + font-size: 0.75rem; + color: var(--color-text-muted, #666); + margin-top: 1rem; + } + `], +}) +export class PendingDeletionsPanelComponent implements OnInit, OnDestroy { + private readonly client = inject(TopologySetupClient); + private tickTimer: ReturnType | null = null; + private refreshTimer: ReturnType | null = null; + + readonly loading = signal(false); + readonly deletions = signal([]); + readonly lastRefresh = signal(null); + readonly activeAction = signal(null); + + readonly actionInProgress = computed(() => this.activeAction() !== null); + + readonly rows = computed(() => { + const now = Date.now(); + return this.deletions().map(d => { + const expiresAt = new Date(d.coolOffExpiresAt).getTime(); + const remainingMs = Math.max(0, expiresAt - now); + return { + deletion: d, + remainingMs, + canConfirm: d.status === 'pending' && remainingMs <= 0, + confirmingId: null, + cancellingId: null, + }; + }); + }); + + ngOnInit(): void { + this.refresh(); + // Tick every second to update countdowns + this.tickTimer = setInterval(() => { + // Force recomputation by re-setting same value (signal identity change) + this.deletions.update(d => [...d]); + }, 1000); + // Auto-refresh data every 30 seconds + this.refreshTimer = setInterval(() => this.refresh(), 30_000); + } + + ngOnDestroy(): void { + if (this.tickTimer !== null) clearInterval(this.tickTimer); + if (this.refreshTimer !== null) clearInterval(this.refreshTimer); + } + + refresh(): void { + this.loading.set(true); + this.client.listPendingDeletions() + .pipe(catchError(() => of({ items: [] }))) + .subscribe(result => { + this.deletions.set(result.items); + this.lastRefresh.set(new Date().toLocaleTimeString()); + this.loading.set(false); + }); + } + + confirmDeletion(id: string): void { + this.activeAction.set(`confirm:${id}`); + this.client.confirmDeletion(id) + .pipe(catchError(() => of(null))) + .subscribe(() => { + this.activeAction.set(null); + this.refresh(); + }); + } + + cancelDeletion(id: string): void { + this.activeAction.set(`cancel:${id}`); + this.client.cancelDeletion(id) + .pipe(catchError(() => of(null))) + .subscribe(() => { + this.activeAction.set(null); + this.refresh(); + }); + } + + hasCascadeItems(d: PendingDeletion): boolean { + const s = d.cascadeSummary; + return (s.childEnvironments ?? 0) > 0 + || (s.childTargets ?? 0) > 0 + || (s.boundAgents ?? 0) > 0 + || (s.infrastructureBindings ?? 0) > 0 + || (s.activeHealthSchedules ?? 0) > 0 + || (s.pendingDeployments ?? 0) > 0; + } + + formatCascade(d: PendingDeletion): string[] { + const items: string[] = []; + const s = d.cascadeSummary; + if (s.childEnvironments) items.push(`${s.childEnvironments} environments`); + if (s.childTargets) items.push(`${s.childTargets} targets`); + if (s.boundAgents) items.push(`${s.boundAgents} agents`); + if (s.infrastructureBindings) items.push(`${s.infrastructureBindings} bindings`); + if (s.activeHealthSchedules) items.push(`${s.activeHealthSchedules} health schedules`); + if (s.pendingDeployments) items.push(`${s.pendingDeployments} pending deployments`); + return items; + } + + formatEntityType(type: string): string { + return type.replace(/_/g, ' '); + } + + formatDate(iso: string): string { + return new Date(iso).toLocaleString(); + } + + formatDuration(ms: number): string { + if (ms <= 0) return 'now'; + const hours = Math.floor(ms / 3_600_000); + const minutes = Math.floor((ms % 3_600_000) / 60_000); + const seconds = Math.floor((ms % 60_000) / 1_000); + if (hours > 0) return `${hours}h ${minutes}m`; + if (minutes > 0) return `${minutes}m ${seconds}s`; + return `${seconds}s`; + } +} diff --git a/src/Web/StellaOps.Web/src/app/features/topology/readiness-dashboard.component.ts b/src/Web/StellaOps.Web/src/app/features/topology/readiness-dashboard.component.ts new file mode 100644 index 000000000..768412a40 --- /dev/null +++ b/src/Web/StellaOps.Web/src/app/features/topology/readiness-dashboard.component.ts @@ -0,0 +1,210 @@ +import { Component, OnInit, signal, computed, inject, ChangeDetectionStrategy } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { HttpClient } from '@angular/common/http'; +import { catchError, of, forkJoin, interval } from 'rxjs'; + +interface GateResult { + gateName: string; + status: string; // 'pass' | 'fail' | 'skip' | 'pending' + message?: string; +} + +interface ReadinessReport { + targetId: string; + environmentId: string; + isReady: boolean; + gates: GateResult[]; + evaluatedAt: string; +} + +interface Region { + regionId: string; + displayName: string; +} + +interface Environment { + environmentId: string; + displayName: string; + regionId?: string; +} + +interface Target { + targetId: string; + name: string; + environmentId: string; +} + +@Component({ + selector: 'app-readiness-dashboard', + standalone: true, + imports: [CommonModule], + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +
+
+

Topology Readiness

+

Gate status for all targets across environments and regions

+ +
+ + @if (loading() && reports().length === 0) { +
Loading readiness data...
+ } + + @if (!loading() && reports().length === 0) { +
+

No readiness data available. Run validation on targets to see results.

+
+ } + + @for (group of groupedReports(); track group.environmentId) { +
+

{{ group.environmentName }}

+
+
+ Target + @for (gate of gateNames; track gate) { + {{ formatGateName(gate) }} + } + Ready +
+ @for (report of group.reports; track report.targetId) { +
+ {{ getTargetName(report.targetId) }} + @for (gate of gateNames; track gate) { + + {{ getGateIcon(report, gate) }} + + } + + {{ report.isReady ? 'Yes' : 'No' }} + +
+ } +
+
+ } + + @if (lastRefresh()) { +

Last refresh: {{ lastRefresh() }}

+ } +
+ `, + styles: [` + .readiness-dashboard { padding: 1.5rem; } + .dashboard-header { display: flex; align-items: center; gap: 1rem; margin-bottom: 1.5rem; } + .dashboard-header h2 { margin: 0; font-size: 1.25rem; color: var(--color-text-heading, #fff); } + .subtitle { color: var(--color-text-muted, #888); margin: 0; flex: 1; } + .btn { padding: 0.5rem 1rem; border-radius: var(--radius-md, 6px); cursor: pointer; font-size: 0.875rem; border: none; } + .btn--secondary { background: var(--color-surface-secondary, #1e1e2e); color: var(--color-text-primary, #e0e0e0); border: 1px solid var(--color-border-primary, #333); } + .btn--sm { padding: 0.375rem 0.75rem; } + .loading-state, .empty-state { padding: 2rem; text-align: center; color: var(--color-text-muted, #888); } + .env-group { margin-bottom: 2rem; } + .env-group__title { font-size: 1rem; color: var(--color-text-heading, #fff); margin-bottom: 0.75rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--color-border-primary, #333); } + .gate-grid { display: grid; gap: 0; font-size: 0.8125rem; overflow-x: auto; } + .gate-grid__header { display: grid; grid-template-columns: 160px repeat(7, 80px) 60px; gap: 1px; background: var(--color-surface-tertiary, #252535); padding: 0.5rem 0; font-weight: var(--font-weight-semibold, 600); color: var(--color-text-secondary, #aaa); } + .gate-grid__row { display: grid; grid-template-columns: 160px repeat(7, 80px) 60px; gap: 1px; padding: 0.375rem 0; border-bottom: 1px solid var(--color-border-primary, #222); } + .gate-grid__row--ready { background: rgba(34, 197, 94, 0.05); } + .gate-grid__row--not-ready { background: rgba(239, 68, 68, 0.05); } + .gate-grid__cell { padding: 0.25rem 0.5rem; text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + .gate-grid__cell--name { text-align: left; color: var(--color-text-primary, #e0e0e0); } + .gate-grid__cell--ready { font-weight: var(--font-weight-semibold, 600); } + .last-refresh { font-size: 0.75rem; color: var(--color-text-muted, #666); margin-top: 1rem; } + `] +}) +export class ReadinessDashboardComponent implements OnInit { + private readonly http = inject(HttpClient); + + readonly loading = signal(false); + readonly reports = signal([]); + readonly targets = signal>(new Map()); + readonly environments = signal>(new Map()); + readonly lastRefresh = signal(null); + + readonly gateNames = [ + 'agent_bound', 'docker_version_ok', 'docker_ping_ok', + 'registry_pull_ok', 'vault_reachable', 'consul_reachable', 'connectivity_ok' + ]; + + readonly groupedReports = computed(() => { + const envMap = this.environments(); + const grouped = new Map(); + + for (const report of this.reports()) { + if (!grouped.has(report.environmentId)) { + grouped.set(report.environmentId, { + environmentId: report.environmentId, + environmentName: envMap.get(report.environmentId) || report.environmentId, + reports: [], + }); + } + grouped.get(report.environmentId)!.reports.push(report); + } + + return [...grouped.values()]; + }); + + ngOnInit(): void { + this.refresh(); + // Auto-refresh every 30 seconds + interval(30000).subscribe(() => this.refresh()); + } + + refresh(): void { + this.loading.set(true); + + // Fetch environments and their readiness + this.http.get<{ items: { environmentId: string; displayName: string }[] }>('/api/v2/topology/environments') + .pipe(catchError(() => of({ items: [] }))) + .subscribe(envResponse => { + const envMap = new Map(); + envResponse.items.forEach(e => envMap.set(e.environmentId, e.displayName)); + this.environments.set(envMap); + + // For each environment, get readiness + const readinessRequests = envResponse.items.map(env => + this.http.get<{ items: ReadinessReport[] }>(`/api/v1/environments/${env.environmentId}/readiness`) + .pipe(catchError(() => of({ items: [] }))) + ); + + if (readinessRequests.length === 0) { + this.loading.set(false); + return; + } + + forkJoin(readinessRequests).subscribe(results => { + const allReports = results.flatMap(r => r.items); + this.reports.set(allReports); + this.lastRefresh.set(new Date().toLocaleTimeString()); + this.loading.set(false); + }); + }); + } + + getTargetName(targetId: string): string { + return this.targets().get(targetId) || targetId.substring(0, 8); + } + + getGateIcon(report: ReadinessReport, gateName: string): string { + const gate = report.gates.find(g => g.gateName === gateName); + if (!gate) return '-'; + switch (gate.status) { + case 'pass': return 'Pass'; + case 'fail': return 'Fail'; + case 'skip': return 'N/A'; + case 'pending': return '...'; + default: return '-'; + } + } + + getGateMessage(report: ReadinessReport, gateName: string): string { + const gate = report.gates.find(g => g.gateName === gateName); + return gate?.message || ''; + } + + formatGateName(gate: string): string { + return gate.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()).replace(' Ok', ''); + } +} diff --git a/src/Web/StellaOps.Web/src/app/routes/topology.routes.ts b/src/Web/StellaOps.Web/src/app/routes/topology.routes.ts index 5f84949ce..6c5f51f43 100644 --- a/src/Web/StellaOps.Web/src/app/routes/topology.routes.ts +++ b/src/Web/StellaOps.Web/src/app/routes/topology.routes.ts @@ -223,6 +223,32 @@ export const TOPOLOGY_ROUTES: Routes = [ (m) => m.TopologyInventoryPageComponent, ), }, + { + path: 'readiness', + title: 'Readiness Dashboard', + data: { + breadcrumb: 'Readiness', + title: 'Readiness Dashboard', + description: 'Gate status for all targets across environments and regions.', + }, + loadComponent: () => + import('../features/topology/readiness-dashboard.component').then( + (m) => m.ReadinessDashboardComponent, + ), + }, + { + path: 'pending-deletions', + title: 'Pending Deletions', + data: { + breadcrumb: 'Pending Deletions', + title: 'Pending Deletions', + description: 'Active deletion requests with cool-off countdown timers.', + }, + loadComponent: () => + import('../features/topology/pending-deletions-panel.component').then( + (m) => m.PendingDeletionsPanelComponent, + ), + }, ], }, ]; diff --git a/src/Web/StellaOps.Web/src/app/shared/components/delete-confirmation/delete-confirmation.component.ts b/src/Web/StellaOps.Web/src/app/shared/components/delete-confirmation/delete-confirmation.component.ts new file mode 100644 index 000000000..b774199a3 --- /dev/null +++ b/src/Web/StellaOps.Web/src/app/shared/components/delete-confirmation/delete-confirmation.component.ts @@ -0,0 +1,355 @@ +/** + * Delete Confirmation Component + * + * Modal dialog for confirming destructive delete operations. + * Features: + * - Entity name display + * - Cascade summary (list of dependent items that will be affected) + * - Cool-off countdown timer (confirm button disabled until timer expires) + * - Confirm/Cancel buttons + * + * Usage: + * + */ + +import { + Component, + ChangeDetectionStrategy, + Input, + Output, + EventEmitter, + signal, + computed, + HostListener, + OnDestroy, +} from '@angular/core'; + +@Component({ + selector: 'st-delete-confirmation', + standalone: true, + imports: [], + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + @if (isOpen()) { + + } + `, + styles: [` + .delete-overlay { + position: fixed; + inset: 0; + z-index: 1100; + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; + background-color: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(2px); + animation: overlay-fade-in 150ms ease; + } + + @keyframes overlay-fade-in { + from { opacity: 0; } + to { opacity: 1; } + } + + .delete-dialog { + position: relative; + width: 100%; + max-width: 440px; + padding: 1.5rem; + background-color: var(--color-surface-primary); + border-radius: var(--radius-xl); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2); + animation: dialog-scale-in 200ms var(--motion-ease-spring, ease-out); + text-align: center; + } + + @keyframes dialog-scale-in { + from { + opacity: 0; + transform: scale(0.95) translateY(-10px); + } + to { + opacity: 1; + transform: scale(1) translateY(0); + } + } + + .delete-dialog__icon { + display: flex; + align-items: center; + justify-content: center; + width: 48px; + height: 48px; + border-radius: var(--radius-full); + background-color: rgba(220, 38, 38, 0.1); + color: var(--color-severity-critical); + margin: 0 auto 1rem; + } + + .delete-dialog__title { + margin: 0 0 0.5rem; + font-size: 1.125rem; + font-weight: var(--font-weight-semibold); + color: var(--color-text-primary); + } + + .delete-dialog__message { + margin: 0 0 1rem; + font-size: 0.875rem; + color: var(--color-text-secondary); + line-height: 1.5; + + strong { + color: var(--color-text-primary); + font-weight: var(--font-weight-semibold); + } + } + + .delete-dialog__cascade { + text-align: left; + margin-bottom: 1rem; + padding: 0.75rem; + border-radius: var(--radius-md); + background: rgba(220, 38, 38, 0.05); + border: 1px solid rgba(220, 38, 38, 0.15); + } + + .delete-dialog__cascade-title { + margin: 0 0 0.4rem; + font-size: 0.78rem; + font-weight: var(--font-weight-semibold); + color: var(--color-severity-critical); + text-transform: uppercase; + letter-spacing: 0.03em; + } + + .delete-dialog__cascade-list { + margin: 0; + padding-left: 1.25rem; + font-size: 0.78rem; + color: var(--color-text-secondary); + line-height: 1.6; + } + + .delete-dialog__cooloff { + margin-bottom: 1rem; + padding: 0.4rem 0.75rem; + border-radius: var(--radius-md); + background: var(--color-surface-secondary); + color: var(--color-text-muted); + font-size: 0.78rem; + font-weight: var(--font-weight-medium); + } + + .delete-dialog__actions { + display: flex; + justify-content: center; + gap: 0.75rem; + } + + .delete-dialog__btn { + padding: 0.625rem 1.25rem; + font-size: 0.875rem; + font-weight: var(--font-weight-medium); + border: none; + border-radius: var(--radius-md); + cursor: pointer; + transition: background-color 150ms ease, transform 150ms ease; + + &:active { + transform: scale(0.98); + } + } + + .delete-dialog__btn--cancel { + color: var(--color-text-secondary); + background-color: var(--color-surface-tertiary); + + &:hover { + background-color: var(--color-surface-secondary); + } + } + + .delete-dialog__btn--confirm { + background-color: var(--color-severity-critical); + color: white; + + &:hover:not(:disabled) { + background-color: var(--color-status-error-text); + } + + &:disabled { + opacity: 0.55; + cursor: not-allowed; + } + } + + @media (prefers-reduced-motion: reduce) { + .delete-overlay, + .delete-dialog { + animation: none; + } + } + `], +}) +export class DeleteConfirmationComponent implements OnDestroy { + @Input() entityName = ''; + @Input() entityType = 'Item'; + @Input() cascadeSummary: string[] = []; + @Input() coolOffSeconds = 5; + + @Output() confirmed = new EventEmitter(); + @Output() cancelled = new EventEmitter(); + @Output() closed = new EventEmitter(); + + private readonly _isOpen = signal(false); + readonly isOpen = this._isOpen.asReadonly(); + readonly remainingSeconds = signal(0); + + readonly titleId = `delete-title-${Math.random().toString(36).substr(2, 9)}`; + readonly descId = `delete-desc-${Math.random().toString(36).substr(2, 9)}`; + + private countdownTimer: ReturnType | null = null; + + open(): void { + this._isOpen.set(true); + this.startCoolOff(); + } + + close(): void { + this._isOpen.set(false); + this.stopCoolOff(); + this.closed.emit(); + } + + onConfirm(): void { + if (this.remainingSeconds() > 0) return; + this.confirmed.emit(); + this.close(); + } + + onCancel(): void { + this.cancelled.emit(); + this.close(); + } + + onOverlayClick(event: MouseEvent): void { + if (event.target === event.currentTarget) { + this.onCancel(); + } + } + + @HostListener('document:keydown.escape') + onEscape(): void { + if (this._isOpen()) { + this.onCancel(); + } + } + + ngOnDestroy(): void { + this.stopCoolOff(); + } + + private startCoolOff(): void { + this.stopCoolOff(); + this.remainingSeconds.set(this.coolOffSeconds); + + if (this.coolOffSeconds <= 0) return; + + this.countdownTimer = setInterval(() => { + const current = this.remainingSeconds(); + if (current <= 1) { + this.remainingSeconds.set(0); + this.stopCoolOff(); + } else { + this.remainingSeconds.set(current - 1); + } + }, 1000); + } + + private stopCoolOff(): void { + if (this.countdownTimer !== null) { + clearInterval(this.countdownTimer); + this.countdownTimer = null; + } + } +} diff --git a/src/Web/StellaOps.Web/src/app/shared/components/index.ts b/src/Web/StellaOps.Web/src/app/shared/components/index.ts index de2e4bf71..6f1758bfd 100644 --- a/src/Web/StellaOps.Web/src/app/shared/components/index.ts +++ b/src/Web/StellaOps.Web/src/app/shared/components/index.ts @@ -87,3 +87,7 @@ export { VexTrustPopoverComponent } from './vex-trust-popover'; export { TerminalBlockComponent } from './terminal-block'; export { StatsCardComponent, StatsTrend, StatsVariant } from './stats-card'; export { FeatureCardComponent, FeatureCardVariant } from './feature-card'; + +// Inline Edit & Delete Confirmation (Topology Wizard) +export { InlineEditComponent } from './inline-edit/inline-edit.component'; +export { DeleteConfirmationComponent } from './delete-confirmation/delete-confirmation.component'; diff --git a/src/Web/StellaOps.Web/src/app/shared/components/inline-edit/inline-edit.component.ts b/src/Web/StellaOps.Web/src/app/shared/components/inline-edit/inline-edit.component.ts new file mode 100644 index 000000000..2c1b9f47a --- /dev/null +++ b/src/Web/StellaOps.Web/src/app/shared/components/inline-edit/inline-edit.component.ts @@ -0,0 +1,205 @@ +/** + * Inline Edit Component + * + * Click-to-edit component that displays a value as text, and switches to + * an input field on click. Saves on Enter or blur, cancels on Escape. + * Supports error display for validation or conflict scenarios. + * + * Usage: + * + */ + +import { + Component, + ChangeDetectionStrategy, + Input, + Output, + EventEmitter, + signal, + ElementRef, + ViewChild, +} from '@angular/core'; +import { FormsModule } from '@angular/forms'; + +@Component({ + selector: 'st-inline-edit', + standalone: true, + imports: [FormsModule], + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + @if (editing()) { +
+ + @if (error()) { + {{ error() }} + } +
+ } @else { + + } + `, + styles: [` + :host { + display: inline-flex; + max-width: 100%; + } + + .inline-edit { + display: inline-flex; + align-items: center; + gap: 0.3rem; + max-width: 100%; + } + + .inline-edit--display { + background: none; + border: 1px solid transparent; + border-radius: var(--radius-sm); + padding: 0.2rem 0.4rem; + cursor: pointer; + font-size: inherit; + color: var(--color-text-primary); + transition: border-color 150ms ease, background 150ms ease; + + &:hover { + border-color: var(--color-border-primary); + background: var(--color-surface-secondary); + } + } + + .inline-edit--display .inline-edit__icon { + opacity: 0; + color: var(--color-text-muted); + transition: opacity 150ms ease; + flex-shrink: 0; + } + + .inline-edit--display:hover .inline-edit__icon { + opacity: 1; + } + + .inline-edit--empty .inline-edit__value { + color: var(--color-text-muted); + font-style: italic; + } + + .inline-edit__value { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .inline-edit--editing { + flex-direction: column; + align-items: stretch; + width: 100%; + } + + .inline-edit__input { + width: 100%; + padding: 0.3rem 0.5rem; + border: 1px solid var(--color-brand-primary); + border-radius: var(--radius-sm); + background: var(--color-surface-primary); + color: var(--color-text-primary); + font-size: inherit; + box-shadow: 0 0 0 2px var(--color-focus-ring); + box-sizing: border-box; + + &:focus { + outline: none; + } + } + + .inline-edit__input--error { + border-color: var(--color-severity-critical); + box-shadow: 0 0 0 2px rgba(220, 38, 38, 0.2); + } + + .inline-edit__error { + font-size: 0.7rem; + color: var(--color-status-error-text); + margin-top: 0.2rem; + } + `], +}) +export class InlineEditComponent { + @Input() value = ''; + @Input() placeholder = 'Click to edit'; + + @Output() saved = new EventEmitter(); + @Output() cancelled = new EventEmitter(); + + @ViewChild('inputRef') inputRef!: ElementRef; + + readonly editing = signal(false); + readonly editValue = signal(''); + readonly error = signal(null); + + startEditing(): void { + this.editValue.set(this.value); + this.error.set(null); + this.editing.set(true); + // Focus after Angular renders the input + setTimeout(() => { + this.inputRef?.nativeElement?.focus(); + this.inputRef?.nativeElement?.select(); + }); + } + + save(): void { + const trimmed = this.editValue().trim(); + if (trimmed === this.value) { + this.cancel(); + return; + } + this.editing.set(false); + this.saved.emit(trimmed); + } + + cancel(): void { + this.editing.set(false); + this.error.set(null); + this.cancelled.emit(); + } + + onBlur(): void { + // Small delay to allow click events on other elements to fire first + setTimeout(() => { + if (this.editing()) { + this.save(); + } + }, 150); + } + + /** Set an error message externally (e.g., for conflict detection) */ + setError(message: string): void { + this.error.set(message); + this.editing.set(true); + } +} diff --git a/src/Web/StellaOps.Web/src/config/config.json b/src/Web/StellaOps.Web/src/config/config.json index 16b6c8b4f..8bdbd7275 100644 --- a/src/Web/StellaOps.Web/src/config/config.json +++ b/src/Web/StellaOps.Web/src/config/config.json @@ -8,7 +8,7 @@ "redirectUri": "/auth/callback", "silentRefreshRedirectUri": "/auth/silent-refresh", "postLogoutRedirectUri": "/", - "scope": "openid profile email offline_access ui.read ui.admin authority:tenants.read authority:users.read authority:roles.read authority:clients.read authority:tokens.read authority:branding.read authority.audit.read graph:read sbom:read scanner:read policy:read policy:simulate policy:author policy:review policy:approve orch:read analytics.read advisory:read vex:read exceptions:read exceptions:approve aoc:verify findings:read release:read release:write release:publish scheduler:read scheduler:operate notify.viewer notify.operator notify.admin notify.escalate export.viewer export.operator export.admin vuln:view vuln:investigate vuln:operate vuln:audit registry.admin", + "scope": "openid profile email offline_access ui.read ui.admin authority:tenants.read authority:users.read authority:roles.read authority:clients.read authority:tokens.read authority:branding.read authority.audit.read graph:read sbom:read scanner:read policy:read policy:simulate policy:author policy:review policy:approve orch:read analytics.read advisory:read vex:read exceptions:read exceptions:approve aoc:verify findings:read release:read release:write release:publish scheduler:read scheduler:operate notify.viewer notify.operator notify.admin notify.escalate export.viewer export.operator export.admin vuln:view vuln:investigate vuln:operate vuln:audit registry.admin signer:read signer:sign signer:rotate signer:admin trust:read trust:write trust:admin", "audience": "/scanner", "dpopAlgorithms": ["ES256"], "refreshLeewaySeconds": 60 diff --git a/src/Web/StellaOps.Web/src/config/config.sample.json b/src/Web/StellaOps.Web/src/config/config.sample.json index 159b02435..f5da3275e 100644 --- a/src/Web/StellaOps.Web/src/config/config.sample.json +++ b/src/Web/StellaOps.Web/src/config/config.sample.json @@ -7,7 +7,7 @@ "logoutEndpoint": "https://authority.example.dev/connect/logout", "redirectUri": "http://localhost:4400/auth/callback", "postLogoutRedirectUri": "http://localhost:4400/", - "scope": "openid profile email ui.read authority:tenants.read advisory:read vex:read exceptions:read exceptions:approve aoc:verify findings:read orch:read release:read release:write release:publish vuln:view vuln:investigate vuln:operate vuln:audit registry.admin", + "scope": "openid profile email ui.read authority:tenants.read advisory:read vex:read exceptions:read exceptions:approve aoc:verify findings:read orch:read release:read release:write release:publish vuln:view vuln:investigate vuln:operate vuln:audit registry.admin signer:read signer:sign signer:rotate signer:admin trust:read trust:write trust:admin", "audience": "https://scanner.example.dev", "dpopAlgorithms": ["ES256"], "refreshLeewaySeconds": 60 diff --git a/src/Web/StellaOps.Web/tests/e2e/prealpha-canonical-full-sweep.spec.ts b/src/Web/StellaOps.Web/tests/e2e/prealpha-canonical-full-sweep.spec.ts index d4adb24b6..5a1d815c4 100644 --- a/src/Web/StellaOps.Web/tests/e2e/prealpha-canonical-full-sweep.spec.ts +++ b/src/Web/StellaOps.Web/tests/e2e/prealpha-canonical-full-sweep.spec.ts @@ -870,71 +870,26 @@ async function setupHarness(page: Page): Promise { }); }, ); - await page.route('**/api/v1/trust/dashboard**', (route) => - route.fulfill({ + await page.route('**/api/v1/administration/trust-signing', (route) => { + const pathname = new URL(route.request().url()).pathname; + if (!pathname.endsWith('/api/v1/administration/trust-signing')) { + return route.fallback(); + } + + return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ - keys: { - total: 3, - active: 1, - expiringSoon: 1, - expired: 1, - revoked: 0, - pendingRotation: 0, - }, - issuers: { - total: 3, - fullTrust: 2, - partialTrust: 1, - minimalTrust: 0, - untrusted: 0, - blocked: 0, - averageTrustScore: 87.3, - }, - certificates: { - total: 3, - valid: 2, - expiringSoon: 1, - expired: 0, - revoked: 0, - invalidChains: 0, - }, - recentEvents: [], - expiryAlerts: [ - { - keyId: 'key-002', - keyName: 'SBOM Signing Key', - expiresAt: '2026-04-15T00:00:00.000Z', - daysUntilExpiry: 39, - severity: 'warning', - purpose: 'sbom_signing', - suggestedAction: 'Schedule key rotation before expiry.', - }, - ], + inventory: { keys: 3, issuers: 3, certificates: 3 }, + signals: [], + legacyAliases: [], + evidenceConsumerPath: '/evidence-audit/proofs', }), - }), - ); - await page.route('**/api/v1/trust/keys/expiry-alerts**', (route) => - route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify([ - { - keyId: 'key-002', - keyName: 'SBOM Signing Key', - purpose: 'sbom_signing', - expiresAt: '2026-04-15T00:00:00.000Z', - daysUntilExpiry: 39, - severity: 'warning', - suggestedAction: 'Schedule key rotation before expiry.', - }, - ]), - }), - ); - await page.route('**/api/v1/trust/keys**', (route) => { + }); + }); + await page.route('**/api/v1/administration/trust-signing/keys**', (route) => { const pathname = new URL(route.request().url()).pathname; - if (!pathname.endsWith('/api/v1/trust/keys')) { + if (!pathname.endsWith('/trust-signing/keys')) { return route.fallback(); } @@ -945,44 +900,140 @@ async function setupHarness(page: Page): Promise { items: [ { keyId: 'key-001', - tenantId: adminSession.tenant, - name: 'Production Key', - description: 'Main signing key', - keyType: 'asymmetric', + alias: 'Production Key', algorithm: 'RS256', - keySize: 2048, - purpose: 'attestation', status: 'active', - publicKeyFingerprint: 'sha256:prod-key', + currentVersion: 2, createdAt: '2026-01-01T00:00:00.000Z', - expiresAt: '2027-01-01T00:00:00.000Z', - lastUsedAt: '2026-03-06T10:00:00.000Z', - usageCount: 100, + updatedAt: '2026-03-06T10:00:00.000Z', + updatedBy: 'operator', }, { keyId: 'key-002', - tenantId: adminSession.tenant, - name: 'SBOM Signing Key', - description: 'Secondary signing key', - keyType: 'asymmetric', + alias: 'SBOM Signing Key', algorithm: 'RS256', - keySize: 2048, - purpose: 'sbom_signing', status: 'expiring_soon', - publicKeyFingerprint: 'sha256:sbom-key', + currentVersion: 1, createdAt: '2026-01-15T00:00:00.000Z', - expiresAt: '2026-04-15T00:00:00.000Z', - lastUsedAt: '2026-03-05T09:00:00.000Z', - usageCount: 50, + updatedAt: '2026-03-05T09:00:00.000Z', + updatedBy: 'operator', }, ], - totalCount: 2, - pageNumber: 1, - pageSize: 20, - totalPages: 1, + count: 2, + limit: 200, + offset: 0, }), }); }); + await page.route('**/api/v1/administration/trust-signing/issuers**', (route) => { + const pathname = new URL(route.request().url()).pathname; + if (!pathname.endsWith('/trust-signing/issuers')) { + return route.fallback(); + } + + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + items: [ + { + issuerId: 'issuer-001', + name: 'Core Root CA', + issuerUri: 'https://issuer.example/root', + trustLevel: 'full', + status: 'active', + createdAt: '2026-01-01T00:00:00.000Z', + updatedAt: '2026-02-01T00:00:00.000Z', + updatedBy: 'operator', + }, + { + issuerId: 'issuer-002', + name: 'Intermediate CA', + issuerUri: 'https://issuer.example/intermediate', + trustLevel: 'full', + status: 'active', + createdAt: '2026-01-01T00:00:00.000Z', + updatedAt: '2026-02-01T00:00:00.000Z', + updatedBy: 'operator', + }, + { + issuerId: 'issuer-003', + name: 'Partner CA', + issuerUri: 'https://partner.example/ca', + trustLevel: 'partial', + status: 'active', + createdAt: '2026-01-15T00:00:00.000Z', + updatedAt: '2026-02-15T00:00:00.000Z', + updatedBy: 'operator', + }, + ], + count: 3, + limit: 200, + offset: 0, + }), + }); + }); + await page.route('**/api/v1/administration/trust-signing/certificates**', (route) => { + const pathname = new URL(route.request().url()).pathname; + if (!pathname.endsWith('/trust-signing/certificates')) { + return route.fallback(); + } + + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + items: [ + { + certificateId: 'cert-001', + keyId: 'key-001', + issuerId: 'issuer-001', + serialNumber: 'SER-001', + status: 'active', + notBefore: '2026-01-01T00:00:00.000Z', + notAfter: '2027-01-01T00:00:00.000Z', + createdAt: '2026-01-01T00:00:00.000Z', + updatedAt: '2026-02-01T00:00:00.000Z', + updatedBy: 'operator', + }, + { + certificateId: 'cert-002', + keyId: 'key-002', + issuerId: 'issuer-002', + serialNumber: 'SER-002', + status: 'active', + notBefore: '2026-01-15T00:00:00.000Z', + notAfter: '2026-04-15T00:00:00.000Z', + createdAt: '2026-01-15T00:00:00.000Z', + updatedAt: '2026-02-15T00:00:00.000Z', + updatedBy: 'operator', + }, + { + certificateId: 'cert-003', + keyId: null, + issuerId: 'issuer-003', + serialNumber: 'SER-003', + status: 'active', + notBefore: '2026-02-01T00:00:00.000Z', + notAfter: '2027-02-01T00:00:00.000Z', + createdAt: '2026-02-01T00:00:00.000Z', + updatedAt: '2026-03-01T00:00:00.000Z', + updatedBy: 'operator', + }, + ], + count: 3, + limit: 200, + offset: 0, + }), + }); + }); + await page.route('**/api/v1/administration/trust-signing/transparency-log**', (route) => + route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ item: null }), + }), + ); await page.route('**/api/v1/integrations**', (route) => { const request = route.request(); const pathname = new URL(request.url()).pathname; @@ -1435,7 +1486,7 @@ async function setupHarness(page: Page): Promise { requestUrl.includes('/api/v2/security/sbom-explorer') || requestUrl.includes('/policy/api/') || requestUrl.includes('/api/v1/integrations') || - requestUrl.includes('/api/v1/trust/') || + requestUrl.includes('/api/v1/administration/trust-signing') || requestUrl.includes('/api/v1/audit/') || requestUrl.includes('/api/v1/authority/quotas') || requestUrl.includes('/api/v1/gateway/rate-limits') || diff --git a/src/Web/StellaOps.Web/tests/e2e/watchlist-shell.spec.ts b/src/Web/StellaOps.Web/tests/e2e/watchlist-shell.spec.ts index cf8a656af..2e6857145 100644 --- a/src/Web/StellaOps.Web/tests/e2e/watchlist-shell.spec.ts +++ b/src/Web/StellaOps.Web/tests/e2e/watchlist-shell.spec.ts @@ -210,14 +210,79 @@ async function setupHarness(page: Page): Promise { ); await page.route('**/doctor/api/v1/doctor/trends**', (route) => fulfillJson(route, [])); await page.route('**/api/v1/approvals**', (route) => fulfillJson(route, [])); - await page.route('**/api/v1/trust/dashboard**', (route) => - fulfillJson(route, { - keys: { total: 12, active: 9, expiringSoon: 2, expired: 1, revoked: 0, pendingRotation: 1 }, - issuers: { total: 8, fullTrust: 3, partialTrust: 3, minimalTrust: 1, untrusted: 1, blocked: 0, averageTrustScore: 86.4 }, - certificates: { total: 5, valid: 4, expiringSoon: 1, expired: 0, revoked: 0, invalidChains: 0 }, - recentEvents: [], - expiryAlerts: [], - }) + await page.route('**/api/v1/administration/trust-signing', (route) => { + const pathname = new URL(route.request().url()).pathname; + if (!pathname.endsWith('/api/v1/administration/trust-signing')) { + return route.fallback(); + } + return fulfillJson(route, { + inventory: { keys: 12, issuers: 8, certificates: 5 }, + signals: [], + legacyAliases: [], + evidenceConsumerPath: '/evidence-audit/proofs', + }); + }); + await page.route('**/api/v1/administration/trust-signing/keys**', (route) => { + const pathname = new URL(route.request().url()).pathname; + if (!pathname.endsWith('/trust-signing/keys')) return route.fallback(); + return fulfillJson(route, { + items: Array.from({ length: 12 }, (_, i) => ({ + keyId: `key-${String(i + 1).padStart(3, '0')}`, + alias: `Key ${i + 1}`, + algorithm: 'Ed25519', + status: i < 9 ? 'active' : i < 11 ? 'expiring_soon' : 'expired', + currentVersion: 1, + createdAt: '2026-01-01T00:00:00Z', + updatedAt: '2026-02-01T00:00:00Z', + updatedBy: 'operator', + })), + count: 12, + limit: 200, + offset: 0, + }); + }); + await page.route('**/api/v1/administration/trust-signing/issuers**', (route) => { + const pathname = new URL(route.request().url()).pathname; + if (!pathname.endsWith('/trust-signing/issuers')) return route.fallback(); + return fulfillJson(route, { + items: Array.from({ length: 8 }, (_, i) => ({ + issuerId: `issuer-${String(i + 1).padStart(3, '0')}`, + name: `Issuer ${i + 1}`, + issuerUri: `https://issuer.example/${i + 1}`, + trustLevel: i < 3 ? 'full' : i < 6 ? 'partial' : i < 7 ? 'minimal' : 'untrusted', + status: 'active', + createdAt: '2026-01-01T00:00:00Z', + updatedAt: '2026-02-01T00:00:00Z', + updatedBy: 'operator', + })), + count: 8, + limit: 200, + offset: 0, + }); + }); + await page.route('**/api/v1/administration/trust-signing/certificates**', (route) => { + const pathname = new URL(route.request().url()).pathname; + if (!pathname.endsWith('/trust-signing/certificates')) return route.fallback(); + return fulfillJson(route, { + items: Array.from({ length: 5 }, (_, i) => ({ + certificateId: `cert-${String(i + 1).padStart(3, '0')}`, + keyId: `key-${String(i + 1).padStart(3, '0')}`, + issuerId: `issuer-${String(i + 1).padStart(3, '0')}`, + serialNumber: `SER-${String(i + 1).padStart(3, '0')}`, + status: 'active', + notBefore: '2026-01-01T00:00:00Z', + notAfter: '2027-01-01T00:00:00Z', + createdAt: '2026-01-01T00:00:00Z', + updatedAt: '2026-02-01T00:00:00Z', + updatedBy: 'operator', + })), + count: 5, + limit: 200, + offset: 0, + }); + }); + await page.route('**/api/v1/administration/trust-signing/transparency-log**', (route) => + fulfillJson(route, { item: null }) ); await page.route(watchlistEntriesRoute, (route) => fulfillJson(route, { items: watchlistEntries, totalCount: watchlistEntries.length }) diff --git a/src/__Libraries/StellaOps.Infrastructure.Postgres/Migrations/MigrationRunner.cs b/src/__Libraries/StellaOps.Infrastructure.Postgres/Migrations/MigrationRunner.cs index 8ef48f358..9bdf9f599 100644 --- a/src/__Libraries/StellaOps.Infrastructure.Postgres/Migrations/MigrationRunner.cs +++ b/src/__Libraries/StellaOps.Infrastructure.Postgres/Migrations/MigrationRunner.cs @@ -283,6 +283,16 @@ public sealed class MigrationRunner : IMigrationRunner try { + // Bind the search_path to the target module schema for this transaction. + // SET LOCAL scopes the change to the current transaction so that unqualified + // table names in migration SQL resolve to the module schema, not public. + var quotedSchemaLocal = QuoteIdentifier(SchemaName); + await using (var searchPathCommand = new NpgsqlCommand( + $"SET LOCAL search_path TO {quotedSchemaLocal}, public", connection, transaction)) + { + await searchPathCommand.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + } + await using (var command = new NpgsqlCommand(migration.Content, connection, transaction)) { command.CommandTimeout = timeoutSeconds; diff --git a/src/__Libraries/StellaOps.Infrastructure.Postgres/Migrations/StartupMigrationHost.cs b/src/__Libraries/StellaOps.Infrastructure.Postgres/Migrations/StartupMigrationHost.cs index ea7c21f2b..0a3e4eddb 100644 --- a/src/__Libraries/StellaOps.Infrastructure.Postgres/Migrations/StartupMigrationHost.cs +++ b/src/__Libraries/StellaOps.Infrastructure.Postgres/Migrations/StartupMigrationHost.cs @@ -370,6 +370,15 @@ public abstract class StartupMigrationHost : IHostedService try { + // Bind the search_path to the target module schema for this transaction. + // SET LOCAL scopes the change to the current transaction so that unqualified + // table names in migration SQL resolve to the module schema, not public. + await using (var searchPathCommand = new NpgsqlCommand( + $"SET LOCAL search_path TO {quotedSchema}, public", connection, transaction)) + { + await searchPathCommand.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + } + // Execute migration SQL await using (var migrationCommand = new NpgsqlCommand(migration.Content, connection, transaction)) {