From 8536a6c7079d55d93e019a04926662cfa3d3db99 Mon Sep 17 00:00:00 2001 From: master <> Date: Mon, 30 Mar 2026 17:26:02 +0300 Subject: [PATCH] Update compose config, policy simulation, and workflow replay - devops/compose: README, docker-compose, hosts updates - Policy simulation: pre-promotion and test-validate panels, routes, dashboard, and spec updates - Workflow visualization: run-graph replay page template update - Claude settings update Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/settings.local.json | 10 +- devops/compose/README.md | 171 +++++++++++++++ devops/compose/docker-compose.stella-ops.yml | 1 + devops/compose/hosts.stellaops.local | 10 + .../policy-simulation.component.spec.ts | 203 ++++-------------- .../policy-simulation.component.ts | 98 ++++----- .../policy-simulation.routes.ts | 157 ++++++-------- .../simulation-dashboard.component.ts | 63 +++--- ...imulation-pre-promotion-panel.component.ts | 128 +++++++++++ ...imulation-test-validate-panel.component.ts | 128 +++++++++++ .../run-graph-replay-page.component.html | 2 +- 11 files changed, 637 insertions(+), 334 deletions(-) create mode 100644 src/Web/StellaOps.Web/src/app/features/policy-simulation/simulation-pre-promotion-panel.component.ts create mode 100644 src/Web/StellaOps.Web/src/app/features/policy-simulation/simulation-test-validate-panel.component.ts diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 7b178328c..7f981e321 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -36,10 +36,16 @@ "mcp__plugin_playwright_playwright__browser_snapshot", "mcp__plugin_playwright_playwright__browser_click", "mcp__plugin_playwright_playwright__browser_navigate", - "mcp__plugin_playwright_playwright__browser_take_screenshot" + "mcp__plugin_playwright_playwright__browser_take_screenshot", + "Bash(git add:*)", + "Bash(git commit:*)" ], "deny": [], - "ask": [] + "ask": [], + "additionalDirectories": [ + "C:\\dev\\serdica-backend4", + "C:\\dev\\serdica-ui" + ] }, "outputStyle": "default" } diff --git a/devops/compose/README.md b/devops/compose/README.md index a29f7a5a5..b43940446 100644 --- a/devops/compose/README.md +++ b/devops/compose/README.md @@ -9,6 +9,9 @@ Consolidated Docker Compose configuration for the StellaOps platform. All profil | Run the full platform | `docker compose -f docker-compose.stella-ops.yml up -d` | | Add observability | `docker compose -f docker-compose.stella-ops.yml -f docker-compose.telemetry.yml up -d` | | Start QA integration fixtures | `docker compose -f docker-compose.integration-fixtures.yml up -d` | +| Start 3rd-party integration services | `docker compose -f docker-compose.integrations.yml up -d` | +| Start GitLab CE (heavy, ~4 GB RAM) | `docker compose -f docker-compose.integrations.yml --profile heavy up -d gitlab` | +| Run integration E2E test suite | See [Integration Test Suite](#integration-test-suite) | | Run CI/testing infrastructure | `docker compose -f docker-compose.testing.yml --profile ci up -d` | | Deploy with China compliance | See [China Compliance](#china-compliance-sm2sm3sm4) | | Deploy with Russia compliance | See [Russia Compliance](#russia-compliance-gost) | @@ -27,6 +30,7 @@ Consolidated Docker Compose configuration for the StellaOps platform. All profil | `docker-compose.telemetry.yml` | **Observability**: OpenTelemetry collector, Prometheus, Tempo, Loki | | `docker-compose.testing.yml` | **CI/Testing**: Test databases, mock services, Gitea for integration tests | | `docker-compose.dev.yml` | **Minimal dev infrastructure**: PostgreSQL, Valkey, RustFS only | +| `docker-compose.integrations.yml` | **Integration services**: Gitea, Jenkins, Nexus, Vault, Docker Registry, MinIO, GitLab | ### Specialized Infrastructure @@ -232,6 +236,173 @@ These fixtures are deterministic QA aids only; they are not production dependenc --- +### Third-Party Integration Services + +Real 3rd-party services for local integration testing. Unlike the QA fixtures above (which are nginx mocks), these are fully functional instances that exercise actual connector plugin code paths. + +```bash +# Start all lightweight integration services (after the main stack is up) +docker compose -f docker-compose.integrations.yml up -d + +# Start specific services only +docker compose -f docker-compose.integrations.yml up -d gitea vault jenkins + +# Start GitLab CE (heavy — requires ~4 GB RAM, ~3 min startup) +docker compose -f docker-compose.integrations.yml --profile heavy up -d gitlab + +# Combine with mock fixtures for full coverage +docker compose \ + -f docker-compose.integrations.yml \ + -f docker-compose.integration-fixtures.yml \ + up -d +``` + +**Hosts file entries** (add to `C:\Windows\System32\drivers\etc\hosts`): +``` +127.1.2.1 gitea.stella-ops.local +127.1.2.2 jenkins.stella-ops.local +127.1.2.3 nexus.stella-ops.local +127.1.2.4 vault.stella-ops.local +127.1.2.5 registry.stella-ops.local +127.1.2.6 minio.stella-ops.local +127.1.2.7 gitlab.stella-ops.local +``` + +**Service reference:** + +| Service | Type | Address | Credentials | Integration Provider | +|---------|------|---------|-------------|---------------------| +| Gitea | SCM | `http://gitea.stella-ops.local:3000` | Create on first login | `Gitea` | +| Jenkins | CI/CD | `http://jenkins.stella-ops.local:8080` | Setup wizard disabled | `Jenkins` | +| Nexus | Registry | `http://nexus.stella-ops.local:8081` | admin / see `admin.password` | `Nexus` | +| Vault | Secrets | `http://vault.stella-ops.local:8200` | Token: `stellaops-dev-root-token-2026` | — | +| Docker Registry | Registry | `http://registry.stella-ops.local:5000` | None (open dev) | `DockerHub` | +| MinIO | S3 Storage | `http://minio.stella-ops.local:9001` | `stellaops` / `Stella2026!` | — | +| GitLab CE | SCM+CI+Registry | `http://gitlab.stella-ops.local:8929` | root / `Stella2026!` | `GitLabServer` | + +**Credential resolution:** Integration connectors resolve secrets via `authref://vault/{path}#{key}` URIs. The Integrations service resolves these from Vault automatically in dev mode. Store credentials with: + +```bash +export VAULT_ADDR=http://vault.stella-ops.local:8200 +export VAULT_TOKEN=stellaops-dev-root-token-2026 + +vault kv put secret/harbor robot-account="harbor-robot-token" +vault kv put secret/github app-private-key="your-key" +vault kv put secret/gitea api-token="your-gitea-token" +vault kv put secret/gitlab access-token="glpat-your-token" +vault kv put secret/jenkins api-token="user:token" +vault kv put secret/nexus admin-password="your-password" +``` + +**Backend connector plugins** (8 total, loaded in Integrations service): + +| Plugin | Type | Provider | Health Endpoint | +|--------|------|----------|-----------------| +| Harbor | Registry | `Harbor` | `/api/v2.0/health` | +| Docker Registry | Registry | `DockerHub` | `/v2/` | +| Nexus | Registry | `Nexus` | `/service/rest/v1/status` | +| GitHub App | SCM | `GitHubApp` | `/api/v3/app` | +| Gitea | SCM | `Gitea` | `/api/v1/version` | +| GitLab | SCM | `GitLabServer` | `/api/v4/version` | +| Jenkins | CI/CD | `Jenkins` | `/api/json` | +| InMemory | Testing | `InMemory` | — (hidden) | + +**Advisory fixture endpoints** (for advisory sources that are unreachable from Docker): + +| Service | Hostname | Port | Mocked Sources | +|---------|----------|------|----------------| +| Advisory fixture | `advisory-fixture.stella-ops.local` | 80 | CERT-In, FSTEC BDU, VEX Hub, StellaOps Mirror, Exploit-DB, AMD, Siemens, Ruby Advisory DB | + +**IP address map:** + +| IP | Service | Port(s) | +|----|---------|---------| +| 127.1.1.6 | harbor-fixture | 80 | +| 127.1.1.7 | github-app-fixture | 80 | +| 127.1.1.8 | advisory-fixture | 80 | +| 127.1.2.1 | gitea | 3000, 2222 | +| 127.1.2.2 | jenkins | 8080, 50000 | +| 127.1.2.3 | nexus | 8081, 8082, 8083 | +| 127.1.2.4 | vault | 8200 | +| 127.1.2.5 | docker-registry | 5000 | +| 127.1.2.6 | minio | 9000, 9001 | +| 127.1.2.7 | gitlab (heavy) | 8929, 2224, 5050 | + +For detailed setup instructions per service, see [`docs/integrations/LOCAL_SERVICES.md`](../../docs/integrations/LOCAL_SERVICES.md). + +### Integration Test Suite + +A Playwright-based E2E test suite validates the full integration lifecycle against the live stack. It covers 5 areas: + +| Area | What it tests | +|------|--------------| +| Compose Health | All fixture + service containers are running and healthy | +| Endpoint Probes | Direct HTTP to each 3rd-party service (Harbor, Gitea, Jenkins, Nexus, Vault, Registry, MinIO) | +| Connector Lifecycle | Create integrations via API, verify auto-activation, test-connection, health-check, cleanup | +| Advisory Sources | All 74 advisory & VEX sources report healthy | +| UI Verification | Hub counts, per-tab list views, tab switching | + +**Prerequisites:** + +```bash +# 1. Main stack must be running +docker compose -f docker-compose.stella-ops.yml up -d + +# 2. Start integration fixtures (mock endpoints) +docker compose -f docker-compose.integration-fixtures.yml up -d + +# 3. Start real 3rd-party services +docker compose -f docker-compose.integrations.yml up -d + +# 4. (Optional) Start GitLab for full SCM coverage +docker compose -f docker-compose.integrations.yml --profile heavy up -d gitlab +``` + +**Run the test suite:** + +```bash +cd src/Web/StellaOps.Web + +# Run all integration tests +E2E_RUN_ID=$(date +%s) \ +PLAYWRIGHT_BASE_URL=https://stella-ops.local \ + npx playwright test --config=playwright.integrations.config.ts + +# Run a specific test group +E2E_RUN_ID=$(date +%s) \ +PLAYWRIGHT_BASE_URL=https://stella-ops.local \ + npx playwright test --config=playwright.integrations.config.ts \ + --grep "Compose Health" + +# Run with verbose output +E2E_RUN_ID=$(date +%s) \ +PLAYWRIGHT_BASE_URL=https://stella-ops.local \ + npx playwright test --config=playwright.integrations.config.ts \ + --reporter=list +``` + +**Environment variables:** + +| Variable | Default | Purpose | +|----------|---------|---------| +| `PLAYWRIGHT_BASE_URL` | `https://stella-ops.local` | Target Stella Ops instance | +| `E2E_RUN_ID` | `run1` | Unique suffix for test integration names (avoids duplicates across runs) | +| `STELLAOPS_ADMIN_USER` | `admin` | Login username | +| `STELLAOPS_ADMIN_PASS` | `Admin@Stella2026!` | Login password | + +**Key files:** + +| File | Purpose | +|------|---------| +| `src/Web/StellaOps.Web/playwright.integrations.config.ts` | Playwright config (no dev server, live stack) | +| `src/Web/StellaOps.Web/tests/e2e/integrations/integrations.e2e.spec.ts` | Test suite (35 tests) | +| `src/Web/StellaOps.Web/tests/e2e/integrations/live-auth.fixture.ts` | Real OIDC login fixture | +| `src/Web/StellaOps.Web/e2e/screenshots/integrations/` | Test screenshots | + +**Note:** Unlike the mocked E2E tests in `tests/e2e/` and `e2e/`, this suite performs real OIDC login and hits the live API. It requires all services to be running and healthy. + +--- + ## Regional Compliance Deployments ### China Compliance (SM2/SM3/SM4) diff --git a/devops/compose/docker-compose.stella-ops.yml b/devops/compose/docker-compose.stella-ops.yml index cbef26e74..8227e39aa 100644 --- a/devops/compose/docker-compose.stella-ops.yml +++ b/devops/compose/docker-compose.stella-ops.yml @@ -2325,6 +2325,7 @@ services: ADVISORYAI__AdvisoryAI__Queue__DirectoryPath: "/var/lib/advisory-ai/queue" ADVISORYAI__AdvisoryAI__Storage__PlanCacheDirectory: "/var/lib/advisory-ai/plans" ADVISORYAI__AdvisoryAI__Storage__OutputDirectory: "/var/lib/advisory-ai/outputs" + ADVISORYAI__AdvisoryAI__Chat__Enabled: "true" ADVISORYAI__AdvisoryAI__Adapters__Llm__Enabled: "${ADVISORY_AI_LLM_ADAPTERS_ENABLED:-true}" ADVISORYAI__AdvisoryAI__LlmProviders__ConfigDirectory: "${ADVISORY_AI_LLM_PROVIDERS_DIRECTORY:-/app/etc/llm-providers}" ADVISORYAI__AdvisoryAI__Inference__Mode: "${ADVISORY_AI_INFERENCE_MODE:-Local}" diff --git a/devops/compose/hosts.stellaops.local b/devops/compose/hosts.stellaops.local index d56a77b2e..753384edb 100644 --- a/devops/compose/hosts.stellaops.local +++ b/devops/compose/hosts.stellaops.local @@ -61,3 +61,13 @@ 127.1.1.5 registry.stella-ops.local 127.1.1.6 harbor-fixture.stella-ops.local 127.1.1.7 github-app-fixture.stella-ops.local +127.1.1.8 advisory-fixture.stella-ops.local + +# ── Third-party integration services ───────────────────────────────────────── +127.1.2.1 gitea.stella-ops.local +127.1.2.2 jenkins.stella-ops.local +127.1.2.3 nexus.stella-ops.local +127.1.2.4 vault.stella-ops.local +127.1.2.5 oci-registry.stella-ops.local +127.1.2.6 minio.stella-ops.local +127.1.2.7 gitlab.stella-ops.local diff --git a/src/Web/StellaOps.Web/src/app/features/policy-simulation/policy-simulation.component.spec.ts b/src/Web/StellaOps.Web/src/app/features/policy-simulation/policy-simulation.component.spec.ts index 54bc47a34..1ac2301cf 100644 --- a/src/Web/StellaOps.Web/src/app/features/policy-simulation/policy-simulation.component.spec.ts +++ b/src/Web/StellaOps.Web/src/app/features/policy-simulation/policy-simulation.component.spec.ts @@ -8,14 +8,14 @@ import { PolicySimulationStudioComponent } from './policy-simulation.component'; @Component({ selector: 'app-shadow-mode-dashboard', template: '', standalone: true }) class MockShadowModeDashboardComponent {} -@Component({ selector: 'app-simulation-console', template: '', standalone: true }) -class MockSimulationConsoleComponent {} +@Component({ selector: 'app-promotion-gate', template: '', standalone: true }) +class MockPromotionGateComponent {} -@Component({ selector: 'app-coverage-fixture', template: '', standalone: true }) -class MockCoverageFixtureComponent {} +@Component({ selector: 'app-simulation-test-validate-panel', template: '', standalone: true }) +class MockSimulationTestValidatePanelComponent {} -@Component({ selector: 'app-policy-audit-log', template: '', standalone: true }) -class MockPolicyAuditLogComponent {} +@Component({ selector: 'app-simulation-pre-promotion-panel', template: '', standalone: true }) +class MockSimulationPrePromotionPanelComponent {} @Component({ selector: 'app-effective-policy-viewer', template: '', standalone: true }) class MockEffectivePolicyViewerComponent {} @@ -23,27 +23,6 @@ class MockEffectivePolicyViewerComponent {} @Component({ selector: 'app-policy-exception', template: '', standalone: true }) class MockPolicyExceptionComponent {} -@Component({ selector: 'app-policy-lint', template: '', standalone: true }) -class MockPolicyLintComponent {} - -@Component({ selector: 'app-promotion-gate', template: '', standalone: true }) -class MockPromotionGateComponent {} - -@Component({ selector: 'app-policy-diff-viewer', template: '', standalone: true }) -class MockPolicyDiffViewerComponent {} - -@Component({ selector: 'app-policy-merge-preview', template: '', standalone: true }) -class MockPolicyMergePreviewComponent {} - -@Component({ selector: 'app-simulation-history', template: '', standalone: true }) -class MockSimulationHistoryComponent {} - -@Component({ selector: 'app-conflict-detection', template: '', standalone: true }) -class MockConflictDetectionComponent {} - -@Component({ selector: 'app-batch-evaluation', template: '', standalone: true }) -class MockBatchEvaluationComponent {} - describe('PolicySimulationStudioComponent', () => { let component: PolicySimulationStudioComponent; let fixture: ComponentFixture; @@ -53,36 +32,22 @@ describe('PolicySimulationStudioComponent', () => { imports: [ PolicySimulationStudioComponent, MockShadowModeDashboardComponent, - MockSimulationConsoleComponent, - MockCoverageFixtureComponent, - MockPolicyAuditLogComponent, + MockPromotionGateComponent, + MockSimulationTestValidatePanelComponent, + MockSimulationPrePromotionPanelComponent, MockEffectivePolicyViewerComponent, MockPolicyExceptionComponent, - MockPolicyLintComponent, - MockPromotionGateComponent, - MockPolicyDiffViewerComponent, - MockPolicyMergePreviewComponent, - MockSimulationHistoryComponent, - MockConflictDetectionComponent, - MockBatchEvaluationComponent, ], }) .overrideComponent(PolicySimulationStudioComponent, { set: { imports: [ MockShadowModeDashboardComponent, - MockSimulationConsoleComponent, - MockCoverageFixtureComponent, - MockPolicyAuditLogComponent, + MockPromotionGateComponent, + MockSimulationTestValidatePanelComponent, + MockSimulationPrePromotionPanelComponent, MockEffectivePolicyViewerComponent, MockPolicyExceptionComponent, - MockPolicyLintComponent, - MockPromotionGateComponent, - MockPolicyDiffViewerComponent, - MockPolicyMergePreviewComponent, - MockSimulationHistoryComponent, - MockConflictDetectionComponent, - MockBatchEvaluationComponent, ], }, }) @@ -103,46 +68,34 @@ describe('PolicySimulationStudioComponent', () => { }); it('should have all tab definitions', () => { - expect(component.tabs.length).toBe(13); + expect(component.tabs.length).toBe(6); }); it('should have tab IDs for all expected features', () => { const tabIds = component.tabs.map((t) => t.id); expect(tabIds).toContain('shadow'); - expect(tabIds).toContain('simulation'); - expect(tabIds).toContain('coverage'); - expect(tabIds).toContain('lint'); - expect(tabIds).toContain('audit'); + expect(tabIds).toContain('promotion'); + expect(tabIds).toContain('testing'); + expect(tabIds).toContain('review'); expect(tabIds).toContain('effective'); expect(tabIds).toContain('exceptions'); - expect(tabIds).toContain('promotion'); - expect(tabIds).toContain('diff'); - expect(tabIds).toContain('merge'); - expect(tabIds).toContain('history'); - expect(tabIds).toContain('conflicts'); - expect(tabIds).toContain('batch'); }); }); describe('Tab Navigation', () => { it('should change active tab on setActiveTab', () => { - component.setActiveTab('simulation'); - expect(component.activeTab()).toBe('simulation'); + component.setActiveTab('promotion'); + expect(component.activeTab()).toBe('promotion'); }); - it('should change active tab to coverage', () => { - component.setActiveTab('coverage'); - expect(component.activeTab()).toBe('coverage'); + it('should change active tab to testing', () => { + component.setActiveTab('testing'); + expect(component.activeTab()).toBe('testing'); }); - it('should change active tab to audit', () => { - component.setActiveTab('audit'); - expect(component.activeTab()).toBe('audit'); - }); - - it('should change active tab to lint', () => { - component.setActiveTab('lint'); - expect(component.activeTab()).toBe('lint'); + it('should change active tab to review', () => { + component.setActiveTab('review'); + expect(component.activeTab()).toBe('review'); }); it('should change active tab to effective', () => { @@ -159,7 +112,7 @@ describe('PolicySimulationStudioComponent', () => { describe('Template Rendering', () => { it('should render navigation tabs', () => { const navTabs = fixture.nativeElement.querySelectorAll('.nav-tab'); - expect(navTabs.length).toBe(13); + expect(navTabs.length).toBe(6); }); it('should mark active tab with active class', () => { @@ -171,12 +124,12 @@ describe('PolicySimulationStudioComponent', () => { it('should display tab labels', () => { const tabLabels = fixture.nativeElement.querySelectorAll('.nav-tab__label'); expect(tabLabels[0].textContent).toContain('Shadow Mode'); - expect(tabLabels[1].textContent).toContain('Simulation Console'); + expect(tabLabels[1].textContent).toContain('Promotion Gate'); }); it('should display tab icons', () => { const tabIcons = fixture.nativeElement.querySelectorAll('.nav-tab__icon'); - expect(tabIcons.length).toBe(13); + expect(tabIcons.length).toBe(6); }); it('should have navigation with tablist role', () => { @@ -186,7 +139,7 @@ describe('PolicySimulationStudioComponent', () => { it('should have buttons with tab role', () => { const tabButtons = fixture.nativeElement.querySelectorAll('button[role="tab"]'); - expect(tabButtons.length).toBe(13); + expect(tabButtons.length).toBe(6); }); it('should set aria-selected on active tab', () => { @@ -207,15 +160,15 @@ describe('PolicySimulationStudioComponent', () => { tick(); fixture.detectChanges(); - expect(component.activeTab()).toBe('simulation'); + expect(component.activeTab()).toBe('promotion'); })); it('should update active class on tab change', fakeAsync(() => { - component.setActiveTab('coverage'); + component.setActiveTab('testing'); fixture.detectChanges(); const activeTab = fixture.nativeElement.querySelector('.nav-tab--active'); - expect(activeTab.textContent).toContain('Coverage'); + expect(activeTab.textContent).toContain('Test & Validate'); })); }); @@ -225,44 +178,34 @@ describe('PolicySimulationStudioComponent', () => { expect(shadowComponent).toBeTruthy(); }); - it('should display simulation console when selected', fakeAsync(() => { - component.setActiveTab('simulation'); + it('should display promotion gate when selected', fakeAsync(() => { + component.setActiveTab('promotion'); fixture.detectChanges(); tick(); fixture.detectChanges(); - const simulationComponent = fixture.debugElement.query(By.directive(MockSimulationConsoleComponent)); - expect(simulationComponent).toBeTruthy(); + const promotionComponent = fixture.debugElement.query(By.directive(MockPromotionGateComponent)); + expect(promotionComponent).toBeTruthy(); })); - it('should display coverage fixture when selected', fakeAsync(() => { - component.setActiveTab('coverage'); + it('should display test & validate panel when selected', fakeAsync(() => { + component.setActiveTab('testing'); fixture.detectChanges(); tick(); fixture.detectChanges(); - const coverageComponent = fixture.debugElement.query(By.directive(MockCoverageFixtureComponent)); - expect(coverageComponent).toBeTruthy(); + const testingComponent = fixture.debugElement.query(By.directive(MockSimulationTestValidatePanelComponent)); + expect(testingComponent).toBeTruthy(); })); - it('should display policy lint when selected', fakeAsync(() => { - component.setActiveTab('lint'); + it('should display pre-promotion review panel when selected', fakeAsync(() => { + component.setActiveTab('review'); fixture.detectChanges(); tick(); fixture.detectChanges(); - const lintComponent = fixture.debugElement.query(By.directive(MockPolicyLintComponent)); - expect(lintComponent).toBeTruthy(); - })); - - it('should display audit log when selected', fakeAsync(() => { - component.setActiveTab('audit'); - fixture.detectChanges(); - tick(); - fixture.detectChanges(); - - const auditComponent = fixture.debugElement.query(By.directive(MockPolicyAuditLogComponent)); - expect(auditComponent).toBeTruthy(); + const reviewComponent = fixture.debugElement.query(By.directive(MockSimulationPrePromotionPanelComponent)); + expect(reviewComponent).toBeTruthy(); })); it('should display effective policies when selected', fakeAsync(() => { @@ -284,66 +227,6 @@ describe('PolicySimulationStudioComponent', () => { const exceptionsComponent = fixture.debugElement.query(By.directive(MockPolicyExceptionComponent)); expect(exceptionsComponent).toBeTruthy(); })); - - it('should display promotion gate when selected', fakeAsync(() => { - component.setActiveTab('promotion'); - fixture.detectChanges(); - tick(); - fixture.detectChanges(); - - const promotionComponent = fixture.debugElement.query(By.directive(MockPromotionGateComponent)); - expect(promotionComponent).toBeTruthy(); - })); - - it('should display diff viewer when selected', fakeAsync(() => { - component.setActiveTab('diff'); - fixture.detectChanges(); - tick(); - fixture.detectChanges(); - - const diffComponent = fixture.debugElement.query(By.directive(MockPolicyDiffViewerComponent)); - expect(diffComponent).toBeTruthy(); - })); - - it('should display merge preview when selected', fakeAsync(() => { - component.setActiveTab('merge'); - fixture.detectChanges(); - tick(); - fixture.detectChanges(); - - const mergeComponent = fixture.debugElement.query(By.directive(MockPolicyMergePreviewComponent)); - expect(mergeComponent).toBeTruthy(); - })); - - it('should display simulation history when selected', fakeAsync(() => { - component.setActiveTab('history'); - fixture.detectChanges(); - tick(); - fixture.detectChanges(); - - const historyComponent = fixture.debugElement.query(By.directive(MockSimulationHistoryComponent)); - expect(historyComponent).toBeTruthy(); - })); - - it('should display conflict detection when selected', fakeAsync(() => { - component.setActiveTab('conflicts'); - fixture.detectChanges(); - tick(); - fixture.detectChanges(); - - const conflictsComponent = fixture.debugElement.query(By.directive(MockConflictDetectionComponent)); - expect(conflictsComponent).toBeTruthy(); - })); - - it('should display batch evaluation when selected', fakeAsync(() => { - component.setActiveTab('batch'); - fixture.detectChanges(); - tick(); - fixture.detectChanges(); - - const batchComponent = fixture.debugElement.query(By.directive(MockBatchEvaluationComponent)); - expect(batchComponent).toBeTruthy(); - })); }); describe('Tab Configuration', () => { diff --git a/src/Web/StellaOps.Web/src/app/features/policy-simulation/policy-simulation.component.ts b/src/Web/StellaOps.Web/src/app/features/policy-simulation/policy-simulation.component.ts index f2f691f12..6445675e3 100644 --- a/src/Web/StellaOps.Web/src/app/features/policy-simulation/policy-simulation.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/policy-simulation/policy-simulation.component.ts @@ -2,40 +2,38 @@ import { Component, ChangeDetectionStrategy, signal } from '@angular/core'; import { ShadowModeDashboardComponent } from './shadow-mode-dashboard.component'; -import { SimulationConsoleComponent } from './simulation-console.component'; -import { CoverageFixtureComponent } from './coverage-fixture.component'; -import { PolicyAuditLogComponent } from './policy-audit-log.component'; +import { PromotionGateComponent } from './promotion-gate.component'; +import { SimulationTestValidatePanelComponent } from './simulation-test-validate-panel.component'; +import { SimulationPrePromotionPanelComponent } from './simulation-pre-promotion-panel.component'; import { EffectivePolicyViewerComponent } from './effective-policy-viewer.component'; import { PolicyExceptionComponent } from './policy-exception.component'; -import { PolicyLintComponent } from './policy-lint.component'; -import { PromotionGateComponent } from './promotion-gate.component'; -import { PolicyDiffViewerComponent } from './policy-diff-viewer.component'; -import { PolicyMergePreviewComponent } from './policy-merge-preview.component'; -import { SimulationHistoryComponent } from './simulation-history.component'; -import { ConflictDetectionComponent } from './conflict-detection.component'; -import { BatchEvaluationComponent } from './batch-evaluation.component'; import { StellaPageTabsComponent, StellaPageTab } from '../../shared/components/stella-page-tabs/stella-page-tabs.component'; /** * Main Policy Simulation Studio component with tabbed navigation. - * Provides access to Shadow Mode, Simulation Console, Coverage, Audit Log, - * Effective Policies, and Exceptions management. + * + * Rationalized from 13 tabs to 6: + * 1. Shadow Mode (default) - unchanged + * 2. Promotion Gate - unchanged + * 3. Test & Validate - merges Simulation Console + Coverage + Lint + * 4. Pre-Promotion Review - merges Diff Viewer + Merge Preview + Conflict Detection + * 5. Effective Policies - unchanged + * 6. Exceptions - unchanged (has badge) + * + * Removed from tabs: + * - Audit Log -> /evidence/audit-log?module=policy-simulation + * - History -> accessible from Shadow Mode via action + * - Batch Evaluation -> accessible from Simulation Console + * * @sprint SPRINT_20251229_021b_FE */ const POLICY_SIM_TABS: readonly StellaPageTab[] = [ { id: 'shadow', label: 'Shadow Mode', icon: 'M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z|||M12 12m-3 0a3 3 0 1 0 6 0 3 3 0 1 0-6 0' }, - { id: 'simulation', label: 'Simulation Console', icon: 'M5 3l14 9-14 9V3z' }, - { id: 'coverage', label: 'Coverage', icon: 'M18 20V10|||M12 20V4|||M6 20v-6' }, - { id: 'lint', label: 'Lint', icon: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z|||M14 2v6h6|||M16 13H8|||M16 17H8|||M10 9H8' }, - { id: 'audit', label: 'Audit Log', icon: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z|||M14 2v6h6|||M16 13H8|||M16 17H8|||M10 9H8' }, + { id: 'promotion', label: 'Promotion Gate', icon: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z' }, + { id: 'testing', label: 'Test & Validate', icon: 'M5 3l14 9-14 9V3z' }, + { id: 'review', label: 'Pre-Promotion Review', icon: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z|||M14 2v6h6' }, { id: 'effective', label: 'Effective Policies', icon: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z' }, { id: 'exceptions', label: 'Exceptions', icon: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z' }, - { id: 'promotion', label: 'Promotion Gate', icon: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z' }, - { id: 'diff', label: 'Diff Viewer', icon: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z|||M14 2v6h6' }, - { id: 'merge', label: 'Merge Preview', icon: 'M6 3v12|||M18 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6z|||M6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6z|||M18 9a9 9 0 0 1-9 9' }, - { id: 'history', label: 'History', icon: 'M12 12m-10 0a10 10 0 1 0 20 0 10 10 0 1 0-20 0|||M12 6v6l4 2' }, - { id: 'conflicts', label: 'Conflicts', icon: 'M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z|||M12 9v4|||M12 17h.01' }, - { id: 'batch', label: 'Batch Evaluation', icon: 'M18 20V10|||M12 20V4|||M6 20v-6' }, ]; @Component({ @@ -43,18 +41,11 @@ const POLICY_SIM_TABS: readonly StellaPageTab[] = [ standalone: true, imports: [ ShadowModeDashboardComponent, - SimulationConsoleComponent, - CoverageFixtureComponent, - PolicyAuditLogComponent, + PromotionGateComponent, + SimulationTestValidatePanelComponent, + SimulationPrePromotionPanelComponent, EffectivePolicyViewerComponent, PolicyExceptionComponent, - PolicyLintComponent, - PromotionGateComponent, - PolicyDiffViewerComponent, - PolicyMergePreviewComponent, - SimulationHistoryComponent, - ConflictDetectionComponent, - BatchEvaluationComponent, StellaPageTabsComponent, ], changeDetection: ChangeDetectionStrategy.OnPush, @@ -70,14 +61,14 @@ const POLICY_SIM_TABS: readonly StellaPageTab[] = [ @if (activeTab() === 'shadow') { } - @if (activeTab() === 'simulation') { - + @if (activeTab() === 'promotion') { + } - @if (activeTab() === 'coverage') { - + @if (activeTab() === 'testing') { + } - @if (activeTab() === 'audit') { - + @if (activeTab() === 'review') { + } @if (activeTab() === 'effective') { @@ -85,27 +76,6 @@ const POLICY_SIM_TABS: readonly StellaPageTab[] = [ @if (activeTab() === 'exceptions') { } - @if (activeTab() === 'lint') { - - } - @if (activeTab() === 'promotion') { - - } - @if (activeTab() === 'diff') { - - } - @if (activeTab() === 'merge') { - - } - @if (activeTab() === 'history') { - - } - @if (activeTab() === 'conflicts') { - - } - @if (activeTab() === 'batch') { - - } `, @@ -131,4 +101,14 @@ export class PolicySimulationStudioComponent { readonly activeTab = signal('shadow'); readonly POLICY_SIM_TABS = POLICY_SIM_TABS; + + /** Alias for spec compatibility. */ + get tabs(): readonly StellaPageTab[] { + return this.POLICY_SIM_TABS; + } + + /** Alias for spec compatibility. */ + setActiveTab(tabId: string): void { + this.activeTab.set(tabId); + } } diff --git a/src/Web/StellaOps.Web/src/app/features/policy-simulation/policy-simulation.routes.ts b/src/Web/StellaOps.Web/src/app/features/policy-simulation/policy-simulation.routes.ts index e1e0e5a4d..f92a6f7e3 100644 --- a/src/Web/StellaOps.Web/src/app/features/policy-simulation/policy-simulation.routes.ts +++ b/src/Web/StellaOps.Web/src/app/features/policy-simulation/policy-simulation.routes.ts @@ -2,36 +2,24 @@ * @file policy-simulation.routes.ts * @sprint SPRINT_20251229_021b_FE * @description Routes for Policy Simulation Studio at /policy/simulation + * + * Rationalized from 13 routes to 6 tab destinations: + * 1. Shadow Mode (default) + * 2. Promotion Gate + * 3. Test & Validate (merges console, coverage, lint) + * 4. Pre-Promotion Review (merges diff, merge, conflicts) + * 5. Effective Policies + * 6. Exceptions + * + * Legacy routes (console, coverage, lint, diff, merge, conflicts) redirect + * to the new merged tabs. Audit Log redirects to the central evidence page. + * History and Batch Evaluation remain accessible as non-tab routes. */ import { Routes } from '@angular/router'; import { requireAuthGuard } from '../../core/auth/auth.guard'; import { StellaOpsScopes } from '../../core/auth/scopes'; -/** - * Policy Simulation Studio Routes - * - * Provides interfaces for policy testing and validation: - * - Dashboard (overview and quick actions) - * - Shadow Mode (A/B policy comparison) - MANDATORY visibility before production promotion - * - Simulation Console (run policy against test data) - * - Policy Lint (syntax and semantic validation) - * - Coverage (test coverage per rule) - * - Effective Policies (which policies apply where) - * - Audit Log (change history) - * - Policy Diff (version comparison) - * - Promotion Gate (checklist enforcement) - * - Exceptions (policy exception management) - * - Merge Preview (pack merge visualization) - * - * Promotion gates require: - * - Shadow duration: 7 days minimum - * - Coverage: 80%+ test coverage - * - Lint: Clean (no errors) - * - Compile: Success - * - Security review: Approved - * - Stakeholder approval: Signed off - */ export const policySimulationRoutes: Routes = [ { path: '', @@ -42,6 +30,7 @@ export const policySimulationRoutes: Routes = [ (m) => m.SimulationDashboardComponent ), children: [ + // ── Default: Shadow Mode ── { path: '', loadComponent: () => @@ -58,54 +47,8 @@ export const policySimulationRoutes: Routes = [ ), data: { requiredScopes: [StellaOpsScopes.POLICY_READ] }, }, - { - path: 'console', - loadComponent: () => - import('./simulation-console.component').then( - (m) => m.SimulationConsoleComponent - ), - data: { requiredScopes: [StellaOpsScopes.POLICY_SIMULATE] }, - }, - { - path: 'lint', - loadComponent: () => - import('./policy-lint.component').then( - (m) => m.PolicyLintComponent - ), - data: { requiredScopes: [StellaOpsScopes.POLICY_READ] }, - }, - { - path: 'coverage', - loadComponent: () => - import('./coverage-fixture.component').then( - (m) => m.CoverageFixtureComponent - ), - data: { requiredScopes: [StellaOpsScopes.POLICY_READ] }, - }, - { - path: 'effective', - loadComponent: () => - import('./effective-policy-viewer.component').then( - (m) => m.EffectivePolicyViewerComponent - ), - data: { requiredScopes: [StellaOpsScopes.POLICY_READ] }, - }, - { - path: 'audit', - loadComponent: () => - import('./policy-audit-log.component').then( - (m) => m.PolicyAuditLogComponent - ), - data: { requiredScopes: [StellaOpsScopes.POLICY_AUDIT] }, - }, - { - path: 'diff/:policyPackId', - loadComponent: () => - import('./policy-diff-viewer.component').then( - (m) => m.PolicyDiffViewerComponent - ), - data: { requiredScopes: [StellaOpsScopes.POLICY_READ] }, - }, + + // ── Promotion Gate ── { path: 'promotion', loadComponent: () => @@ -114,6 +57,38 @@ export const policySimulationRoutes: Routes = [ ), data: { requiredScopes: [StellaOpsScopes.POLICY_APPROVE] }, }, + + // ── Test & Validate (merged: console + coverage + lint) ── + { + path: 'testing', + loadComponent: () => + import('./simulation-test-validate-panel.component').then( + (m) => m.SimulationTestValidatePanelComponent + ), + data: { requiredScopes: [StellaOpsScopes.POLICY_SIMULATE] }, + }, + + // ── Pre-Promotion Review (merged: diff + merge + conflicts) ── + { + path: 'review', + loadComponent: () => + import('./simulation-pre-promotion-panel.component').then( + (m) => m.SimulationPrePromotionPanelComponent + ), + data: { requiredScopes: [StellaOpsScopes.POLICY_READ] }, + }, + + // ── Effective Policies ── + { + path: 'effective', + loadComponent: () => + import('./effective-policy-viewer.component').then( + (m) => m.EffectivePolicyViewerComponent + ), + data: { requiredScopes: [StellaOpsScopes.POLICY_READ] }, + }, + + // ── Exceptions ── { path: 'exceptions', loadComponent: () => @@ -130,14 +105,20 @@ export const policySimulationRoutes: Routes = [ ), data: { requiredScopes: [StellaOpsScopes.POLICY_READ] }, }, - { - path: 'merge', - loadComponent: () => - import('./policy-merge-preview.component').then( - (m) => m.PolicyMergePreviewComponent - ), - data: { requiredScopes: [StellaOpsScopes.POLICY_READ] }, - }, + + // ── Legacy redirects: merged into Test & Validate ── + { path: 'console', redirectTo: 'testing', pathMatch: 'full' }, + { path: 'coverage', redirectTo: 'testing', pathMatch: 'full' }, + { path: 'lint', redirectTo: 'testing', pathMatch: 'full' }, + + // ── Legacy redirects: merged into Pre-Promotion Review ── + { path: 'merge', redirectTo: 'review', pathMatch: 'full' }, + { path: 'conflicts', redirectTo: 'review', pathMatch: 'full' }, + + // ── Audit Log: redirect to central evidence page ── + { path: 'audit', redirectTo: '/evidence/audit-log', pathMatch: 'full' }, + + // ── Non-tab routes (still accessible via actions) ── { path: 'history', loadComponent: () => @@ -146,14 +127,6 @@ export const policySimulationRoutes: Routes = [ ), data: { requiredScopes: [StellaOpsScopes.POLICY_READ] }, }, - { - path: 'conflicts', - loadComponent: () => - import('./conflict-detection.component').then( - (m) => m.ConflictDetectionComponent - ), - data: { requiredScopes: [StellaOpsScopes.POLICY_READ] }, - }, { path: 'batch', loadComponent: () => @@ -162,6 +135,14 @@ export const policySimulationRoutes: Routes = [ ), data: { requiredScopes: [StellaOpsScopes.POLICY_SIMULATE] }, }, + { + path: 'diff/:policyPackId', + loadComponent: () => + import('./policy-diff-viewer.component').then( + (m) => m.PolicyDiffViewerComponent + ), + data: { requiredScopes: [StellaOpsScopes.POLICY_READ] }, + }, ], }, ]; diff --git a/src/Web/StellaOps.Web/src/app/features/policy-simulation/simulation-dashboard.component.ts b/src/Web/StellaOps.Web/src/app/features/policy-simulation/simulation-dashboard.component.ts index 128f44523..838c5649b 100644 --- a/src/Web/StellaOps.Web/src/app/features/policy-simulation/simulation-dashboard.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/policy-simulation/simulation-dashboard.component.ts @@ -5,11 +5,23 @@ import { RouterModule, Router } from '@angular/router'; import { ShadowModeIndicatorComponent } from './shadow-mode-indicator.component'; import { ShadowModeStateService } from './shadow-mode-state.service'; import { StellaPageTabsComponent, StellaPageTab } from '../../shared/components/stella-page-tabs/stella-page-tabs.component'; +import { StellaQuickLinksComponent, type StellaQuickLink } from '../../shared/components/stella-quick-links/stella-quick-links.component'; /** * Main Policy Simulation Studio dashboard component with tabbed navigation. - * Provides access to Shadow Mode, Simulation Console, Coverage, Audit Log, - * Effective Policies, and Exceptions management. + * + * Rationalized from 9 tabs to 6: + * 1. Shadow Mode (default) - unchanged + * 2. Promotion Gate - unchanged + * 3. Test & Validate - merges Simulation Console + Coverage + Lint + * 4. Pre-Promotion Review - merges Diff Viewer + Merge Preview + Conflict Detection + * 5. Effective Policies - unchanged + * 6. Exceptions - unchanged (has badge) + * + * Removed from tabs: + * - Audit Log -> /evidence/audit-log?module=policy-simulation + * - History -> accessible from Shadow Mode via action + * - Batch Evaluation -> accessible from Simulation Console * * MANDATORY: Shadow mode indicator is visible on all policy views before production promotion. * @@ -17,19 +29,16 @@ import { StellaPageTabsComponent, StellaPageTab } from '../../shared/components/ */ const SIMULATION_TABS: readonly StellaPageTab[] = [ { id: 'shadow', label: 'Shadow Mode', icon: 'M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z|||M12 12m-3 0a3 3 0 1 0 6 0 3 3 0 1 0-6 0' }, - { id: 'console', label: 'Simulation Console', icon: 'M5 3l14 9-14 9V3z' }, - { id: 'lint', label: 'Lint', icon: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z|||M14 2v6h6|||M16 13H8|||M16 17H8|||M10 9H8' }, - { id: 'coverage', label: 'Coverage', icon: 'M18 20V10|||M12 20V4|||M6 20v-6', badge: '72%', status: 'warn', statusHint: '72% coverage (80%+ required)' }, - { id: 'effective', label: 'Effective Policies', icon: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z' }, - { id: 'audit', label: 'Audit Log', icon: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z|||M14 2v6h6|||M16 13H8|||M16 17H8|||M10 9H8' }, - { id: 'exceptions', label: 'Exceptions', icon: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z', badge: 3 }, { id: 'promotion', label: 'Promotion Gate', icon: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z' }, - { id: 'merge', label: 'Merge Preview', icon: 'M6 3v12|||M18 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6z|||M6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6z|||M18 9a9 9 0 0 1-9 9' }, + { id: 'testing', label: 'Test & Validate', icon: 'M5 3l14 9-14 9V3z' }, + { id: 'review', label: 'Pre-Promotion Review', icon: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z|||M14 2v6h6' }, + { id: 'effective', label: 'Effective Policies', icon: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z' }, + { id: 'exceptions', label: 'Exceptions', icon: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z', badge: 3 }, ]; @Component({ selector: 'app-simulation-dashboard', - imports: [RouterModule, ShadowModeIndicatorComponent, StellaPageTabsComponent], + imports: [RouterModule, ShadowModeIndicatorComponent, StellaPageTabsComponent, StellaQuickLinksComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
@@ -38,6 +47,9 @@ const SIMULATION_TABS: readonly StellaPageTab[] = [

Policy Simulation

{{ activeSubtitle() }}

+ @@ -192,6 +204,8 @@ const SIMULATION_TABS: readonly StellaPageTab[] = [ .simulation__title { font-size: 1.5rem; font-weight: var(--font-weight-bold, 700); color: var(--color-text-heading); margin: 0 0 0.25rem; } .simulation__subtitle { font-size: 0.8125rem; color: var(--color-text-secondary); margin: 0; } + .page-aside { flex: 0 1 60%; min-width: 0; } + .simulation__shadow-banner { margin-bottom: 1rem; } @@ -296,16 +310,20 @@ export class SimulationDashboardComponent implements OnInit { protected readonly activeTab = signal('shadow'); + readonly quickLinks: readonly StellaQuickLink[] = [ + { label: 'Governance', route: '/ops/policy/governance', description: 'Risk budgets and compliance profiles' }, + { label: 'Policy Packs', route: '/ops/policy/packs', description: 'Author and manage policy pack rules' }, + { label: 'VEX & Exceptions', route: '/ops/policy/vex', description: 'Vulnerability exceptions and waivers' }, + { label: 'Audit Events', route: '/evidence/audit-log', description: 'Cross-module audit trail' }, + ]; + private static readonly SUBTITLES: Record = { shadow: 'Compare shadow policy evaluations against active production policy.', - console: 'Run policies against test data and review evaluations.', - lint: 'Syntax and semantic validation for policy documents.', - coverage: 'Test coverage metrics per rule and policy pack.', - effective: 'Which policies apply to which environments and artifacts.', - audit: 'Simulation and policy change history.', - exceptions: 'Manage temporary policy exception waivers.', promotion: 'Pre-promotion checklist and gate enforcement.', - merge: 'Preview pack merge results before applying.', + testing: 'Run policies against test data, check coverage, and validate syntax.', + review: 'Diff versions, preview merges, and detect conflicts before promotion.', + effective: 'Which policies apply to which environments and artifacts.', + exceptions: 'Manage temporary policy exception waivers.', }; protected readonly activeSubtitle = computed(() => @@ -328,14 +346,11 @@ export class SimulationDashboardComponent implements OnInit { private static readonly TAB_ROUTES: Record = { shadow: './shadow', - console: './console', - lint: './lint', - coverage: './coverage', - effective: './effective', - audit: './audit', - exceptions: './exceptions', promotion: './promotion', - merge: './merge', + testing: './testing', + review: './review', + effective: './effective', + exceptions: './exceptions', }; protected readonly SIMULATION_TABS: readonly StellaPageTab[] = SIMULATION_TABS; diff --git a/src/Web/StellaOps.Web/src/app/features/policy-simulation/simulation-pre-promotion-panel.component.ts b/src/Web/StellaOps.Web/src/app/features/policy-simulation/simulation-pre-promotion-panel.component.ts new file mode 100644 index 000000000..9f1bba4cc --- /dev/null +++ b/src/Web/StellaOps.Web/src/app/features/policy-simulation/simulation-pre-promotion-panel.component.ts @@ -0,0 +1,128 @@ +import { Component, ChangeDetectionStrategy, signal } from '@angular/core'; + +import { PolicyDiffViewerComponent } from './policy-diff-viewer.component'; +import { PolicyMergePreviewComponent } from './policy-merge-preview.component'; +import { ConflictDetectionComponent } from './conflict-detection.component'; + +/** + * Wrapper panel that merges Diff Viewer, Merge Preview, and Conflict Detection + * into a single "Pre-Promotion Review" tab with toggle sub-sections. + * + * Replaces three separate top-level tabs with a single tab and + * toggle buttons to switch between sub-views. + */ +type ReviewSection = 'diff' | 'merge' | 'conflicts'; + +interface SectionOption { + id: ReviewSection; + label: string; + icon: string; +} + +const SECTIONS: readonly SectionOption[] = [ + { + id: 'diff', + label: 'Diff Viewer', + icon: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z M14 2v6h6', + }, + { + id: 'merge', + label: 'Merge Preview', + icon: 'M6 3v12 M18 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6z M6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6z M18 9a9 9 0 0 1-9 9', + }, + { + id: 'conflicts', + label: 'Conflict Detection', + icon: 'M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z M12 9v4 M12 17h.01', + }, +] as const; + +@Component({ + selector: 'app-simulation-pre-promotion-panel', + standalone: true, + imports: [ + PolicyDiffViewerComponent, + PolicyMergePreviewComponent, + ConflictDetectionComponent, + ], + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +
+
+ @for (section of sections; track section.id) { + + } +
+ +
+ @if (activeSection() === 'diff') { + + } + @if (activeSection() === 'merge') { + + } + @if (activeSection() === 'conflicts') { + + } +
+
+ `, + styles: [` + :host { display: block; } + + .panel__toggles { + display: flex; + gap: 0.25rem; + padding: 0.75rem 1rem 0; + border-bottom: 1px solid var(--color-border-subtle, rgba(255,255,255,0.06)); + background: var(--color-surface-secondary, #1a1a2e); + } + + .panel__toggle { + display: inline-flex; + align-items: center; + gap: 0.375rem; + padding: 0.5rem 0.875rem; + border: none; + border-bottom: 2px solid transparent; + background: transparent; + color: var(--color-text-secondary); + font-size: 0.8125rem; + font-weight: var(--font-weight-medium, 500); + cursor: pointer; + transition: color 150ms ease, border-color 150ms ease; + white-space: nowrap; + } + + .panel__toggle:hover { + color: var(--color-text-primary); + } + + .panel__toggle--active { + color: var(--color-brand-primary, #7c5cfc); + border-bottom-color: var(--color-brand-primary, #7c5cfc); + } + + .panel__content { + padding: 0; + } + `], +}) +export class SimulationPrePromotionPanelComponent { + readonly sections = SECTIONS; + readonly activeSection = signal('diff'); +} diff --git a/src/Web/StellaOps.Web/src/app/features/policy-simulation/simulation-test-validate-panel.component.ts b/src/Web/StellaOps.Web/src/app/features/policy-simulation/simulation-test-validate-panel.component.ts new file mode 100644 index 000000000..216d049f3 --- /dev/null +++ b/src/Web/StellaOps.Web/src/app/features/policy-simulation/simulation-test-validate-panel.component.ts @@ -0,0 +1,128 @@ +import { Component, ChangeDetectionStrategy, signal } from '@angular/core'; + +import { SimulationConsoleComponent } from './simulation-console.component'; +import { CoverageFixtureComponent } from './coverage-fixture.component'; +import { PolicyLintComponent } from './policy-lint.component'; + +/** + * Wrapper panel that merges Simulation Console, Coverage, and Lint + * into a single "Test & Validate" tab with toggle sub-sections. + * + * Replaces three separate top-level tabs with a single tab and + * toggle buttons to switch between sub-views. + */ +type TestValidateSection = 'console' | 'coverage' | 'lint'; + +interface SectionOption { + id: TestValidateSection; + label: string; + icon: string; +} + +const SECTIONS: readonly SectionOption[] = [ + { + id: 'console', + label: 'Simulation Console', + icon: 'M5 3l14 9-14 9V3z', + }, + { + id: 'coverage', + label: 'Coverage', + icon: 'M18 20V10 M12 20V4 M6 20v-6', + }, + { + id: 'lint', + label: 'Lint', + icon: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z M14 2v6h6 M16 13H8 M16 17H8 M10 9H8', + }, +] as const; + +@Component({ + selector: 'app-simulation-test-validate-panel', + standalone: true, + imports: [ + SimulationConsoleComponent, + CoverageFixtureComponent, + PolicyLintComponent, + ], + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +
+
+ @for (section of sections; track section.id) { + + } +
+ +
+ @if (activeSection() === 'console') { + + } + @if (activeSection() === 'coverage') { + + } + @if (activeSection() === 'lint') { + + } +
+
+ `, + styles: [` + :host { display: block; } + + .panel__toggles { + display: flex; + gap: 0.25rem; + padding: 0.75rem 1rem 0; + border-bottom: 1px solid var(--color-border-subtle, rgba(255,255,255,0.06)); + background: var(--color-surface-secondary, #1a1a2e); + } + + .panel__toggle { + display: inline-flex; + align-items: center; + gap: 0.375rem; + padding: 0.5rem 0.875rem; + border: none; + border-bottom: 2px solid transparent; + background: transparent; + color: var(--color-text-secondary); + font-size: 0.8125rem; + font-weight: var(--font-weight-medium, 500); + cursor: pointer; + transition: color 150ms ease, border-color 150ms ease; + white-space: nowrap; + } + + .panel__toggle:hover { + color: var(--color-text-primary); + } + + .panel__toggle--active { + color: var(--color-brand-primary, #7c5cfc); + border-bottom-color: var(--color-brand-primary, #7c5cfc); + } + + .panel__content { + padding: 0; + } + `], +}) +export class SimulationTestValidatePanelComponent { + readonly sections = SECTIONS; + readonly activeSection = signal('console'); +} diff --git a/src/Web/StellaOps.Web/src/app/features/workflow-visualization/run-graph-replay-page.component.html b/src/Web/StellaOps.Web/src/app/features/workflow-visualization/run-graph-replay-page.component.html index 75d52686f..581b3fa8d 100644 --- a/src/Web/StellaOps.Web/src/app/features/workflow-visualization/run-graph-replay-page.component.html +++ b/src/Web/StellaOps.Web/src/app/features/workflow-visualization/run-graph-replay-page.component.html @@ -6,7 +6,7 @@ [backLabel]="returnTo() ? 'Return to previous context' : null" (backClick)="returnToSource()" > - Back to release runs + Back to deployments