Consolidate Operations UI, rename Policy Packs to Release Policies, add host infrastructure

Five sprints delivered in this change:

Sprint 001 - Ops UI Consolidation:
  Remove Operations Hub, Agents Fleet Dashboard, and Signals Runtime Dashboard
  (31 files deleted). Ops nav goes from 8 to 4 items. Redirects from old routes.

Sprint 002 - Host Infrastructure (Backend):
  Add SshHostConfig and WinRmHostConfig target connection types with validation.
  Implement AgentInventoryCollector (real IInventoryCollector that parses docker ps
  JSON via IRemoteCommandExecutor abstraction). Enrich TopologyHostProjection with
  ProbeStatus/ProbeType/ProbeLastHeartbeat fields.

Sprint 003 - Host UI + Environment Verification:
  Add runtime verification column to environment target list with Verified/Drift/
  Offline/Unmonitored badges. Add container-level verification detail to Deploy
  Status tab showing deployed vs running digests with drift highlighting.

Sprint 004 - Release Policies Rename:
  Move "Policy Packs" from Ops to Release Control as "Release Policies". Remove
  "Risk & Governance" from Security nav. Rename Pack Registry to Automation Catalog.
  Create gate-catalog.ts with 11 gate type display names and descriptions.

Sprint 005 - Policy Builder:
  Create visual policy builder (3-step: name, gates, review) with per-gate-type
  config forms (CVSS threshold slider, signature toggles, freshness days, etc).
  Simplify pack workspace tabs from 6 to 3 (Rules, Test, Activate). Add YAML
  toggle within Rules tab.

59/59 Playwright e2e tests pass across 4 test suites.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-04-01 00:31:38 +03:00
parent db967a54f8
commit 1d7c8fadbd
58 changed files with 2492 additions and 12138 deletions

View File

@@ -0,0 +1,220 @@
/**
* Operations UI Consolidation — Docker Stack E2E Tests
*
* Uses the auth fixture for authenticated page access.
* Run against live Docker stack: npx playwright test --config playwright.e2e.config.ts ops-consolidation
*
* Verifies:
* - Removed pages are gone (Operations Hub, Agent Fleet, Signals)
* - Sidebar Ops section has exactly 5 items
* - Old routes redirect (not 404)
* - Remaining ops pages render
* - Topology pages unaffected
* - No Angular runtime errors
*/
import { test, expect } from './fixtures/auth.fixture';
import { navigateAndWait, assertPageHasContent, getPageHeading } from './helpers/nav.helper';
const SCREENSHOT_DIR = 'e2e/screenshots/ops-consolidation';
function collectNgErrors(page: import('@playwright/test').Page) {
const errors: string[] = [];
page.on('console', (msg) => {
const text = msg.text();
if (msg.type() === 'error' && /NG0\d{3,4}/.test(text)) {
errors.push(text);
}
});
return errors;
}
// ---------------------------------------------------------------------------
// 1. Sidebar: removed items are gone
// ---------------------------------------------------------------------------
test.describe('Ops sidebar cleanup verification', () => {
test('Operations Hub is removed from sidebar', async ({ authenticatedPage: page }) => {
await navigateAndWait(page, '/ops/operations/jobs-queues', { timeout: 30_000 });
const sidebar = page.locator('aside.sidebar');
await expect(sidebar).toBeVisible({ timeout: 15_000 });
await expect(sidebar).not.toContainText('Operations Hub');
await page.screenshot({ path: `${SCREENSHOT_DIR}/01-no-ops-hub.png`, fullPage: true });
});
test('Agent Fleet is removed from sidebar', async ({ authenticatedPage: page }) => {
await navigateAndWait(page, '/ops/operations/jobs-queues', { timeout: 30_000 });
const sidebar = page.locator('aside.sidebar');
await expect(sidebar).toBeVisible({ timeout: 15_000 });
await expect(sidebar).not.toContainText('Agent Fleet');
});
test('Signals is removed from sidebar nav items', async ({ authenticatedPage: page }) => {
await navigateAndWait(page, '/ops/operations/jobs-queues', { timeout: 30_000 });
const navItems = page.locator('aside.sidebar a.nav-item');
const count = await navItems.count();
for (let i = 0; i < count; i++) {
const text = ((await navItems.nth(i).textContent()) ?? '').trim();
expect(text).not.toBe('Signals');
}
});
});
// ---------------------------------------------------------------------------
// 2. Sidebar: expected 5 items present
// ---------------------------------------------------------------------------
test.describe('Ops sidebar has correct items', () => {
const EXPECTED_ITEMS = [
'Policy Packs',
'Scheduled Jobs',
'Feeds & Airgap',
'Scripts',
'Diagnostics',
];
test(`sidebar Ops section contains all ${EXPECTED_ITEMS.length} expected items`, async ({
authenticatedPage: page,
}) => {
await navigateAndWait(page, '/ops/operations/jobs-queues', { timeout: 30_000 });
const sidebarText = (await page.locator('aside.sidebar').textContent()) ?? '';
for (const label of EXPECTED_ITEMS) {
expect(sidebarText, `Should contain "${label}"`).toContain(label);
}
await page.screenshot({ path: `${SCREENSHOT_DIR}/02-ops-nav-5-items.png`, fullPage: true });
});
});
// ---------------------------------------------------------------------------
// 3. Redirects: old routes handled gracefully
// ---------------------------------------------------------------------------
test.describe('Old route redirects', () => {
test('/ops/operations redirects to jobs-queues', async ({ authenticatedPage: page }) => {
await navigateAndWait(page, '/ops/operations', { timeout: 30_000 });
expect(page.url()).toContain('/ops/operations/jobs-queues');
await assertPageHasContent(page);
});
test('/ops/signals redirects to diagnostics', async ({ authenticatedPage: page }) => {
await navigateAndWait(page, '/ops/signals', { timeout: 30_000 });
expect(page.url()).toContain('/ops/operations/doctor');
await assertPageHasContent(page);
});
test('/ops base redirects through to content', async ({ authenticatedPage: page }) => {
await navigateAndWait(page, '/ops', { timeout: 30_000 });
await assertPageHasContent(page);
});
});
// ---------------------------------------------------------------------------
// 4. Remaining ops pages render
// ---------------------------------------------------------------------------
test.describe('Remaining Ops pages render', () => {
const PAGES = [
{ path: '/ops/operations/jobs-queues', name: 'Jobs & Queues' },
{ path: '/ops/operations/feeds-airgap', name: 'Feeds & AirGap' },
{ path: '/ops/operations/doctor', name: 'Diagnostics' },
{ path: '/ops/operations/data-integrity', name: 'Data Integrity' },
{ path: '/ops/operations/event-stream', name: 'Event Stream' },
{ path: '/ops/operations/jobengine', name: 'JobEngine Dashboard' },
];
for (const route of PAGES) {
test(`${route.name} (${route.path}) renders`, async ({ authenticatedPage: page }) => {
const ngErrors = collectNgErrors(page);
await navigateAndWait(page, route.path, { timeout: 30_000 });
await page.waitForTimeout(2000);
await assertPageHasContent(page);
expect(
ngErrors,
`Angular errors on ${route.path}: ${ngErrors.join('\n')}`,
).toHaveLength(0);
});
}
});
// ---------------------------------------------------------------------------
// 5. Topology pages unaffected
// ---------------------------------------------------------------------------
test.describe('Topology pages still work', () => {
const TOPOLOGY_PAGES = [
{ path: '/setup/topology/overview', name: 'Overview' },
{ path: '/setup/topology/hosts', name: 'Hosts' },
{ path: '/setup/topology/targets', name: 'Targets' },
{ path: '/setup/topology/agents', name: 'Agents' },
];
for (const route of TOPOLOGY_PAGES) {
test(`topology ${route.name} (${route.path}) renders`, async ({
authenticatedPage: page,
}) => {
const ngErrors = collectNgErrors(page);
await navigateAndWait(page, route.path, { timeout: 30_000 });
await page.waitForTimeout(2000);
await assertPageHasContent(page);
expect(ngErrors).toHaveLength(0);
});
}
test('operations/agents loads topology agents (not fleet dashboard)', async ({
authenticatedPage: page,
}) => {
const ngErrors = collectNgErrors(page);
await navigateAndWait(page, '/ops/operations/agents', { timeout: 30_000 });
await page.waitForTimeout(2000);
await assertPageHasContent(page);
expect(ngErrors).toHaveLength(0);
await page.screenshot({ path: `${SCREENSHOT_DIR}/03-agents-topology.png`, fullPage: true });
});
});
// ---------------------------------------------------------------------------
// 6. Full journey stability
// ---------------------------------------------------------------------------
test.describe('Ops navigation journey', () => {
test('sequential navigation across all ops routes has no Angular errors', async ({
authenticatedPage: page,
}) => {
test.setTimeout(180_000);
const ngErrors = collectNgErrors(page);
const routes = [
'/ops/operations/jobs-queues',
'/ops/operations/feeds-airgap',
'/ops/operations/doctor',
'/ops/operations/data-integrity',
'/ops/operations/agents',
'/ops/operations/event-stream',
'/ops/integrations',
'/ops/policy',
'/setup/topology/overview',
'/setup/topology/hosts',
'/setup/topology/agents',
];
for (const route of routes) {
await navigateAndWait(page, route, { timeout: 30_000 });
await page.waitForTimeout(1000);
}
expect(
ngErrors,
`Angular errors during journey: ${ngErrors.join('\n')}`,
).toHaveLength(0);
});
test('browser back/forward works across ops routes', async ({
authenticatedPage: page,
}) => {
await navigateAndWait(page, '/ops/operations/jobs-queues', { timeout: 30_000 });
await navigateAndWait(page, '/ops/operations/doctor', { timeout: 30_000 });
await navigateAndWait(page, '/ops/operations/feeds-airgap', { timeout: 30_000 });
await page.goBack();
await page.waitForTimeout(1500);
expect(page.url()).toContain('/doctor');
await page.goForward();
await page.waitForTimeout(1500);
expect(page.url()).toContain('/feeds-airgap');
});
});