Repair setup admin branding and action routes

This commit is contained in:
master
2026-03-11 17:05:49 +02:00
parent dc98d5a758
commit 8cf132798d
18 changed files with 719 additions and 188 deletions

View File

@@ -0,0 +1,231 @@
#!/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 statePath = path.join(outputDirectory, 'live-frontdoor-auth-state.json');
const reportPath = path.join(outputDirectory, 'live-frontdoor-auth-report.json');
const resultPath = path.join(outputDirectory, 'live-setup-admin-action-sweep.json');
const scopeQuery = 'tenant=demo-prod&regions=us-east&environments=stage&timeWindow=7d';
function createRuntime() {
return {
consoleErrors: [],
pageErrors: [],
requestFailures: [],
responseErrors: [],
};
}
function attachRuntimeListeners(page, runtime) {
page.on('console', (message) => {
if (message.type() === 'error') {
runtime.consoleErrors.push({
timestamp: Date.now(),
page: page.url(),
text: message.text(),
});
}
});
page.on('pageerror', (error) => {
runtime.pageErrors.push({
timestamp: Date.now(),
page: page.url(),
message: error.message,
});
});
page.on('requestfailed', (request) => {
const url = request.url();
if (/\.(?:css|js|map|png|jpg|jpeg|svg|woff2?)(?:$|\?)/i.test(url)) {
return;
}
const errorText = request.failure()?.errorText ?? 'unknown';
if (errorText === 'net::ERR_ABORTED') {
return;
}
runtime.requestFailures.push({
timestamp: Date.now(),
page: page.url(),
method: request.method(),
url,
error: errorText,
});
});
page.on('response', (response) => {
const url = response.url();
if (!url.includes('/api/') && !url.includes('/console/')) {
return;
}
if (response.status() >= 400) {
runtime.responseErrors.push({
timestamp: Date.now(),
page: page.url(),
method: response.request().method(),
status: response.status(),
url,
});
}
});
}
async function captureSnapshot(page, label) {
const heading = await page.locator('h1,h2').first().textContent().catch(() => '');
const alerts = await page.locator('[role="alert"], .alert, .toast').allTextContents().catch(() => []);
return {
label,
url: page.url(),
title: await page.title(),
heading: (heading || '').trim(),
alerts: alerts.map((text) => text.trim()).filter(Boolean),
};
}
async function gotoRoute(page, route) {
await page.goto(`https://stella-ops.local${route}?${scopeQuery}`, {
waitUntil: 'domcontentloaded',
timeout: 30_000,
});
await page.waitForTimeout(2_000);
}
async function main() {
mkdirSync(outputDirectory, { recursive: true });
const authReport = await authenticateFrontdoor({
statePath,
reportPath,
});
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 runtime = createRuntime();
attachRuntimeListeners(page, runtime);
const startedAt = Date.now();
const results = [];
await gotoRoute(page, '/setup/tenant-branding');
const brandingTitleInput = page.locator('#title').first();
const applyChangesButton = page.getByRole('button', { name: 'Apply Changes', exact: true }).first();
const brandingBefore = await captureSnapshot(page, 'branding-before');
await brandingTitleInput.waitFor({ state: 'visible', timeout: 10_000 });
const applyDisabledBefore = await applyChangesButton.isDisabled().catch(() => true);
const titleEditable = await brandingTitleInput.isEditable().catch(() => false);
let applyDisabledAfter = applyDisabledBefore;
if (titleEditable) {
const originalTitle = await brandingTitleInput.inputValue();
await brandingTitleInput.fill(`${originalTitle} QA`);
await page.waitForTimeout(300);
applyDisabledAfter = await applyChangesButton.isDisabled().catch(() => true);
}
const brandingAfter = await captureSnapshot(page, 'branding-after-edit');
results.push({
action: 'tenant-branding-editor',
ok: brandingBefore.url.includes('/setup/tenant-branding')
&& /branding configuration/i.test(brandingBefore.heading)
&& !brandingBefore.alerts.some((alert) => /failed to load branding/i.test(alert))
&& (
(titleEditable && applyDisabledBefore && !applyDisabledAfter)
|| (!titleEditable
&& applyDisabledBefore
&& applyDisabledAfter
&& brandingAfter.alerts.some((alert) => /read-only for this session/i.test(alert)))
)
&& !brandingAfter.alerts.some((alert) => /failed to load branding/i.test(alert)),
titleEditable,
applyDisabledBefore,
applyDisabledAfter,
snapshot: brandingAfter,
});
await gotoRoute(page, '/setup/notifications');
await page.getByRole('button', { name: 'Create Rule', exact: true }).click({ timeout: 10_000 });
await page.waitForTimeout(2_000);
results.push({
action: 'notifications-create-rule',
ok: page.url().includes('/setup/notifications/rules/new'),
snapshot: await captureSnapshot(page, 'notifications-create-rule'),
});
await gotoRoute(page, '/setup/usage');
await page.getByRole('link', { name: 'Configure Quotas', exact: true }).click({ timeout: 10_000 });
await page.waitForTimeout(2_000);
results.push({
action: 'usage-configure-quotas',
ok: page.url().includes('/ops/operations/quotas'),
snapshot: await captureSnapshot(page, 'usage-configure-quotas'),
});
const systemActions = [
{ name: 'View Details', expected: '/ops/operations/system-health' },
{ name: 'Run Doctor', expected: '/ops/operations/doctor' },
{ name: 'View SLOs', expected: '/ops/operations/health-slo' },
{ name: 'View Jobs', expected: '/ops/operations/jobs-queues' },
];
for (const action of systemActions) {
await gotoRoute(page, '/setup/system');
await page.getByRole('link', { name: action.name, exact: true }).click({ timeout: 10_000 });
await page.waitForTimeout(2_000);
results.push({
action: `system-${action.name.toLowerCase().replace(/\s+/g, '-')}`,
ok: page.url().includes(action.expected),
snapshot: await captureSnapshot(page, `system-${action.name}`),
});
}
const runtimeIssues = [
...runtime.consoleErrors.map((entry) => `console:${entry.text}`),
...runtime.pageErrors.map((entry) => `pageerror:${entry.message}`),
...runtime.requestFailures.map((entry) => `requestfailed:${entry.method} ${entry.url} ${entry.error}`),
...runtime.responseErrors.map((entry) => `response:${entry.status} ${entry.method} ${entry.url}`),
];
const result = {
generatedAtUtc: new Date().toISOString(),
durationMs: Date.now() - startedAt,
results,
runtime,
failedActionCount: results.filter((entry) => !entry.ok).length,
runtimeIssueCount: runtimeIssues.length,
runtimeIssues,
};
writeFileSync(resultPath, `${JSON.stringify(result, null, 2)}\n`, 'utf8');
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
await context.close();
await browser.close();
if (result.failedActionCount > 0 || result.runtimeIssueCount > 0) {
process.exitCode = 1;
}
}
main().catch((error) => {
process.stderr.write(`[live-setup-admin-action-sweep] ${error instanceof Error ? error.message : String(error)}\n`);
process.exit(1);
});