diff --git a/docs/implplan/SPRINT_20260311_008_FE_live_registry_admin_audit_route_identity.md b/docs/implplan/SPRINT_20260311_008_FE_live_registry_admin_audit_route_identity.md new file mode 100644 index 000000000..8d1df1015 --- /dev/null +++ b/docs/implplan/SPRINT_20260311_008_FE_live_registry_admin_audit_route_identity.md @@ -0,0 +1,68 @@ +# Sprint 20260311_008 - FE Live Registry Admin Audit Route Identity + +## Topic & Scope +- Prove whether the `registry-admin` audit tab failure is a real route break or a weak post-navigation page identity. +- Keep the fix inside the web workspace by making the audit child route render explicit audit-specific content after navigation. +- Add focused regression coverage and rerun the live changed-surfaces sweep on `https://stella-ops.local`. +- Working directory: `src/Web/StellaOps.Web`. +- Expected evidence: root-cause notes, focused Angular spec, rebuilt web bundle, live Playwright pass for the registry-admin audit action. + +## Dependencies & Concurrency +- Depends on the live compose stack being healthy and reachable. +- Safe parallelism: limited to `src/Web/StellaOps.Web` plus this sprint file. + +## Documentation Prerequisites +- `AGENTS.md` +- `docs/qa/feature-checks/FLOW.md` + +## Delivery Tracker + +### FE-REGISTRY-AUDIT-001 - Root-cause the audit-tab live failure +Status: DONE +Dependency: none +Owners: QA, 3rd line support +Task description: +- Reproduce the failing `audit-tab` action from the live changed-surfaces sweep and determine whether the click fails, the route fails, or the target route lacks truthful audit-specific content. + +Completion criteria: +- [x] Live authenticated Playwright proves whether `/ops/integrations/registry-admin/audit` is reached. +- [x] Root cause is recorded with concrete evidence. + +### FE-REGISTRY-AUDIT-002 - Make the audit route self-identifying +Status: DONE +Dependency: FE-REGISTRY-AUDIT-001 +Owners: Product Manager, Architect, Developer +Task description: +- Update the registry-admin audit child view so the audit route renders an explicit title/description/count summary. The target page must clearly indicate that the user is in the audit trail, not just inside the generic registry shell. + +Completion criteria: +- [x] The audit child view renders an audit-specific heading. +- [x] The audit state remains clear in loading, empty, and populated cases. +- [x] Focused component tests cover the visible route identity. + +### FE-REGISTRY-AUDIT-003 - Reverify the live registry-admin action flow +Status: DONE +Dependency: FE-REGISTRY-AUDIT-002 +Owners: QA +Task description: +- Rebuild the web bundle, sync it into the live stack, and rerun the changed-surfaces or focused registry-admin Playwright flow to confirm the audit tab now produces truthful post-navigation evidence. + +Completion criteria: +- [x] Web build passes. +- [x] Live Playwright confirms the audit tab lands on `/registry-admin/audit`. +- [x] Live Playwright confirms the page exposes audit-specific visible text. + +## Execution Log +| Date (UTC) | Update | Owner | +| --- | --- | --- | +| 2026-03-11 | Sprint created after the changed-surfaces sweep flagged the registry-admin audit tab. | QA | +| 2026-03-11 | Authenticated Playwright repro proved the click navigates correctly to `/ops/integrations/registry-admin/audit`; the real defect is that the target child view exposes no audit-specific heading, so the route is not self-identifying. | QA / 3rd line support | +| 2026-03-11 | Added explicit audit title/summary content to `PlanAuditComponent`, covered it with focused Angular specs (`2/2` across the registry-admin shell and audit view), rebuilt the web bundle, synced it into `compose_console-dist`, restarted `stellaops-router-gateway`, and passed the focused live Playwright proof in `live-registry-admin-audit-check.mjs` with `actionOk=true` and zero runtime errors. | Product / Architect / Developer / QA | + +## Decisions & Risks +- Decision: treat this as a product defect, not a harness change. The audit route is real, but without audit-specific visible identity the user cannot reliably confirm the navigation succeeded. +- Risk: only checking URL would hide regressions where the shell changes but the intended audit view does not become obvious to the user. +- Decision: use a focused live Playwright check for this slice instead of re-running the full changed-surfaces matrix after every small route-identity fix. That preserves truthful verification while staying within the low-churn runtime budget. + +## Next Checkpoints +- Move to the next live route/action defect from the changed-surfaces and broader action sweeps after committing this registry-admin repair. diff --git a/src/Web/StellaOps.Web/scripts/live-registry-admin-audit-check.mjs b/src/Web/StellaOps.Web/scripts/live-registry-admin-audit-check.mjs new file mode 100644 index 000000000..1d0d83373 --- /dev/null +++ b/src/Web/StellaOps.Web/scripts/live-registry-admin-audit-check.mjs @@ -0,0 +1,111 @@ +#!/usr/bin/env node + +import { mkdirSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { chromium } from 'playwright'; + +import { authenticateFrontdoor, createAuthenticatedContext } from './live-frontdoor-auth.mjs'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const webRoot = path.resolve(__dirname, '..'); +const outputDirectory = path.join(webRoot, 'output', 'playwright'); +const outputPath = path.join(outputDirectory, 'live-registry-admin-audit-check.json'); +const baseUrl = process.env.STELLAOPS_FRONTDOOR_BASE_URL?.trim() || 'https://stella-ops.local'; +const statePath = path.join(outputDirectory, 'live-frontdoor-auth-state.json'); + +function trimText(value, maxLength = 400) { + const normalized = value.replace(/\s+/g, ' ').trim(); + return normalized.length > maxLength ? `${normalized.slice(0, maxLength)}...` : normalized; +} + +async function collectHeadings(page) { + return page.locator('h1, main h1, main h2, h2').evaluateAll((elements) => + elements + .map((element) => (element.textContent || '').replace(/\s+/g, ' ').trim()) + .filter((text, index, values) => text.length > 0 && values.indexOf(text) === index), + ).catch(() => []); +} + +async function main() { + mkdirSync(outputDirectory, { recursive: true }); + + const authReport = await authenticateFrontdoor({ + statePath, + reportPath: path.join(outputDirectory, 'live-frontdoor-auth-report.json'), + }); + + const browser = await chromium.launch({ + headless: true, + args: ['--disable-dev-shm-usage'], + }); + + const context = await createAuthenticatedContext(browser, authReport, { statePath }); + const page = await context.newPage(); + + const report = { + generatedAtUtc: new Date().toISOString(), + baseUrl, + startUrl: `${baseUrl}/ops/integrations/registry-admin?tenant=demo-prod®ions=apac,eu-west,us-east,us-west`, + finalUrl: '', + headings: [], + auditHeadingMatched: false, + actionOk: false, + errors: [], + }; + + page.on('console', (message) => { + if (message.type() === 'error') { + report.errors.push(`console:${message.text()}`); + } + }); + + page.on('pageerror', (error) => { + report.errors.push(`page:${error.message}`); + }); + + try { + await page.goto(report.startUrl, { + waitUntil: 'domcontentloaded', + timeout: 30_000, + }); + await page.waitForTimeout(4_000); + + const auditTab = page.locator('nav[role="tablist"] a:has-text("Audit Log")').first(); + await auditTab.click(); + await page.waitForTimeout(4_000); + + report.finalUrl = page.url(); + report.headings = await collectHeadings(page); + const bodyText = trimText(await page.locator('body').innerText().catch(() => ''), 1200); + report.auditHeadingMatched = report.headings.some((heading) => /audit/i.test(heading)) + || /registry audit log/i.test(bodyText); + report.actionOk = report.finalUrl.includes('/registry-admin/audit') + && report.finalUrl.includes('tenant=') + && report.finalUrl.includes('regions=') + && report.auditHeadingMatched; + + await page.screenshot({ + path: path.join(outputDirectory, 'live-registry-admin-audit-check.png'), + fullPage: true, + }).catch(() => {}); + } finally { + await browser.close(); + } + + writeFileSync(outputPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8'); + process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); + + if (!report.actionOk || report.errors.length > 0) { + process.exitCode = 1; + } +} + +if (process.argv[1] && path.resolve(process.argv[1]) === __filename) { + main().catch((error) => { + process.stderr.write(`[live-registry-admin-audit-check] ${error instanceof Error ? error.message : String(error)}\n`); + process.exit(1); + }); +} diff --git a/src/Web/StellaOps.Web/src/app/features/registry-admin/components/plan-audit.component.spec.ts b/src/Web/StellaOps.Web/src/app/features/registry-admin/components/plan-audit.component.spec.ts new file mode 100644 index 000000000..c96d1da74 --- /dev/null +++ b/src/Web/StellaOps.Web/src/app/features/registry-admin/components/plan-audit.component.spec.ts @@ -0,0 +1,54 @@ +import { TestBed } from '@angular/core/testing'; +import { of } from 'rxjs'; + +import { REGISTRY_ADMIN_API } from '../../../core/api/registry-admin.client'; +import { PlanAuditComponent } from './plan-audit.component'; + +describe('PlanAuditComponent', () => { + const registryAdminApiStub = { + getAuditHistory: () => + of({ + items: [ + { + id: 'audit-1', + timestamp: '2026-03-11T10:15:00Z', + action: 'Updated', + planId: 'plan-gold', + actor: 'admin', + summary: 'Adjusted repository scopes', + previousVersion: 2, + newVersion: 3, + }, + ], + totalCount: 1, + page: 1, + pageSize: 20, + }), + }; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PlanAuditComponent], + }) + .overrideComponent(PlanAuditComponent, { + set: { + providers: [{ provide: REGISTRY_ADMIN_API, useValue: registryAdminApiStub }], + }, + }) + .compileComponents(); + }); + + it('renders an explicit audit heading and entry count', async () => { + const fixture = TestBed.createComponent(PlanAuditComponent); + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); + + const text = fixture.nativeElement.textContent.replace(/\s+/g, ' ').trim(); + + expect(text).toContain('Registry Audit Log'); + expect(text).toContain('Review plan mutations, actor activity, and version changes'); + expect(text).toContain('Entries'); + expect(text).toContain('Adjusted repository scopes'); + }); +}); diff --git a/src/Web/StellaOps.Web/src/app/features/registry-admin/components/plan-audit.component.ts b/src/Web/StellaOps.Web/src/app/features/registry-admin/components/plan-audit.component.ts index 3adf69c18..8e625d431 100644 --- a/src/Web/StellaOps.Web/src/app/features/registry-admin/components/plan-audit.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/registry-admin/components/plan-audit.component.ts @@ -14,6 +14,19 @@ import { PlanAuditEntry, PaginatedResponse } from '../../../core/api/registry-ad changeDetection: ChangeDetectionStrategy.OnPush, template: `
+ Review plan mutations, actor activity, and version changes for the registry token service. +
+