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,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
template: `
|
template: `
|
||||||
<div class="plan-audit">
|
<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 -->
|
<!-- Filters -->
|
||||||
<div class="plan-audit__toolbar">
|
<div class="plan-audit__toolbar">
|
||||||
<div class="plan-audit__filter">
|
<div class="plan-audit__filter">
|
||||||
@@ -134,6 +147,54 @@ import { PlanAuditEntry, PaginatedResponse } from '../../../core/api/registry-ad
|
|||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
styles: [`
|
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 {
|
.plan-audit__toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -314,6 +375,19 @@ import { PlanAuditEntry, PaginatedResponse } from '../../../core/api/registry-ad
|
|||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
margin-top: 1rem;
|
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 {
|
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-direct-route-defaults.spec.ts",
|
||||||
"src/app/features/policy-simulation/policy-simulation-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/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/registry-admin/registry-admin.component.spec.ts",
|
||||||
"src/app/features/triage/services/ttfs-telemetry.service.spec.ts",
|
"src/app/features/triage/services/ttfs-telemetry.service.spec.ts",
|
||||||
"src/app/features/triage/triage-workspace.component.spec.ts",
|
"src/app/features/triage/triage-workspace.component.spec.ts",
|
||||||
|
|||||||
Reference in New Issue
Block a user