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) <noreply@anthropic.com>
This commit is contained in:
master
2026-03-30 17:26:02 +03:00
parent 260fce8ef8
commit 8536a6c707
11 changed files with 637 additions and 334 deletions

View File

@@ -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"
}

View File

@@ -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)

View File

@@ -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}"

View File

@@ -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

View File

@@ -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<PolicySimulationStudioComponent>;
@@ -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', () => {

View File

@@ -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') {
<app-shadow-mode-dashboard />
}
@if (activeTab() === 'simulation') {
<app-simulation-console />
@if (activeTab() === 'promotion') {
<app-promotion-gate />
}
@if (activeTab() === 'coverage') {
<app-coverage-fixture />
@if (activeTab() === 'testing') {
<app-simulation-test-validate-panel />
}
@if (activeTab() === 'audit') {
<app-policy-audit-log />
@if (activeTab() === 'review') {
<app-simulation-pre-promotion-panel />
}
@if (activeTab() === 'effective') {
<app-effective-policy-viewer />
@@ -85,27 +76,6 @@ const POLICY_SIM_TABS: readonly StellaPageTab[] = [
@if (activeTab() === 'exceptions') {
<app-policy-exception />
}
@if (activeTab() === 'lint') {
<app-policy-lint />
}
@if (activeTab() === 'promotion') {
<app-promotion-gate />
}
@if (activeTab() === 'diff') {
<app-policy-diff-viewer />
}
@if (activeTab() === 'merge') {
<app-policy-merge-preview />
}
@if (activeTab() === 'history') {
<app-simulation-history />
}
@if (activeTab() === 'conflicts') {
<app-conflict-detection />
}
@if (activeTab() === 'batch') {
<app-batch-evaluation />
}
</stella-page-tabs>
</div>
`,
@@ -131,4 +101,14 @@ export class PolicySimulationStudioComponent {
readonly activeTab = signal<string>('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);
}
}

View File

@@ -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] },
},
],
},
];

View File

@@ -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: `
<section class="simulation">
@@ -38,6 +47,9 @@ const SIMULATION_TABS: readonly StellaPageTab[] = [
<h1 class="simulation__title">Policy Simulation</h1>
<p class="simulation__subtitle">{{ activeSubtitle() }}</p>
</div>
<aside class="page-aside">
<stella-quick-links [links]="quickLinks" label="Quick Links" layout="strip" />
</aside>
</header>
<!-- MANDATORY: Shadow Mode Indicator - Visible on all policy views -->
@@ -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<string>('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<string, string> = {
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<string, string> = {
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;

View File

@@ -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: `
<div class="panel">
<div class="panel__toggles" role="group" aria-label="Pre-promotion review sections">
@for (section of sections; track section.id) {
<button
class="panel__toggle"
[class.panel__toggle--active]="activeSection() === section.id"
[attr.aria-pressed]="activeSection() === section.id"
(click)="activeSection.set(section.id)"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
@for (d of section.icon.split(' M'); track $index) {
<path [attr.d]="$index === 0 ? d : 'M' + d" />
}
</svg>
<span>{{ section.label }}</span>
</button>
}
</div>
<div class="panel__content">
@if (activeSection() === 'diff') {
<app-policy-diff-viewer />
}
@if (activeSection() === 'merge') {
<app-policy-merge-preview />
}
@if (activeSection() === 'conflicts') {
<app-conflict-detection />
}
</div>
</div>
`,
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<ReviewSection>('diff');
}

View File

@@ -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: `
<div class="panel">
<div class="panel__toggles" role="group" aria-label="Test and validate sections">
@for (section of sections; track section.id) {
<button
class="panel__toggle"
[class.panel__toggle--active]="activeSection() === section.id"
[attr.aria-pressed]="activeSection() === section.id"
(click)="activeSection.set(section.id)"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
@for (d of section.icon.split(' M'); track $index) {
<path [attr.d]="$index === 0 ? d : 'M' + d" />
}
</svg>
<span>{{ section.label }}</span>
</button>
}
</div>
<div class="panel__content">
@if (activeSection() === 'console') {
<app-simulation-console />
}
@if (activeSection() === 'coverage') {
<app-coverage-fixture />
}
@if (activeSection() === 'lint') {
<app-policy-lint />
}
</div>
</div>
`,
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<TestValidateSection>('console');
}

View File

@@ -6,7 +6,7 @@
[backLabel]="returnTo() ? 'Return to previous context' : null"
(backClick)="returnToSource()"
>
<a header-actions routerLink="/releases/runs" class="run-workspace__back-link">Back to release runs</a>
<a header-actions routerLink="/releases/deployments" class="run-workspace__back-link">Back to deployments</a>
</app-context-header>
<app-tabbed-nav