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,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",