Make registry-admin audit route self-identifying

This commit is contained in:
master
2026-03-11 19:09:46 +02:00
parent 6afd8f951e
commit 8eec0a9dee
5 changed files with 308 additions and 0 deletions

View File

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

View File

@@ -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&regions=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);
});
}

View File

@@ -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');
});
});

View File

@@ -14,6 +14,19 @@ import { PlanAuditEntry, PaginatedResponse } from '../../../core/api/registry-ad
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="plan-audit">
<header class="plan-audit__header">
<div>
<h2 class="plan-audit__title">Registry Audit Log</h2>
<p class="plan-audit__subtitle">
Review plan mutations, actor activity, and version changes for the registry token service.
</p>
</div>
<div class="plan-audit__summary" aria-label="Audit entry count">
<span class="plan-audit__summary-value">{{ totalCount() }}</span>
<span class="plan-audit__summary-label">Entries</span>
</div>
</header>
<!-- Filters -->
<div class="plan-audit__toolbar">
<div class="plan-audit__filter">
@@ -134,6 +147,54 @@ import { PlanAuditEntry, PaginatedResponse } from '../../../core/api/registry-ad
</div>
`,
styles: [`
.plan-audit__header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 1rem;
margin-bottom: 1.5rem;
}
.plan-audit__title {
margin: 0 0 0.375rem;
font-size: 1.5rem;
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
}
.plan-audit__subtitle {
margin: 0;
max-width: 56rem;
color: var(--color-text-muted);
}
.plan-audit__summary {
display: inline-flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-width: 7rem;
padding: 0.75rem 1rem;
border: 1px solid var(--color-text-heading);
border-radius: var(--radius-lg);
background: rgba(30, 41, 59, 0.55);
}
.plan-audit__summary-value {
font-size: 1.5rem;
font-weight: var(--font-weight-semibold);
color: var(--color-status-info);
line-height: 1;
}
.plan-audit__summary-label {
margin-top: 0.25rem;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--color-text-muted);
}
.plan-audit__toolbar {
display: flex;
justify-content: space-between;
@@ -314,6 +375,19 @@ import { PlanAuditEntry, PaginatedResponse } from '../../../core/api/registry-ad
border-radius: var(--radius-lg);
margin-top: 1rem;
}
@media (max-width: 900px) {
.plan-audit__header,
.plan-audit__toolbar,
.plan-audit__filter {
flex-direction: column;
align-items: stretch;
}
.plan-audit__summary {
align-self: flex-start;
}
}
`]
})
export class PlanAuditComponent implements OnInit {

View File

@@ -25,6 +25,7 @@
"src/app/features/policy-simulation/policy-simulation-direct-route-defaults.spec.ts",
"src/app/features/policy-simulation/policy-simulation-defaults.spec.ts",
"src/app/features/policy-simulation/simulation-dashboard.component.spec.ts",
"src/app/features/registry-admin/components/plan-audit.component.spec.ts",
"src/app/features/registry-admin/registry-admin.component.spec.ts",
"src/app/features/triage/services/ttfs-telemetry.service.spec.ts",
"src/app/features/triage/triage-workspace.component.spec.ts",