Make registry-admin audit route self-identifying
This commit is contained in:
@@ -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.
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user