feat(#1085): fold Roles page into Analytics tab (#1088)

Red commit: 35e1f46b36 (CI run:
https://github.com/Kpa-clawbot/CoreScope/actions/runs/25367951904)

Fixes #1085

## What changed

The "Roles" page is a stats slice — counts + breakdown by node role. It
belongs in Analytics, not as a top-level nav peer of Map / Channels /
Nodes. This PR folds it in and frees nav space.

### Frontend
- `public/index.html` — drop the `<a data-route="roles">` from the top
nav and the legacy `<script src="roles-page.js">` tag.
- `public/app.js` — backward-compat redirect added at the top of
`navigate()`: `#/roles` (and `#/roles?…`, `#/roles/…`) →
`#/analytics?tab=roles`. Old bookmarks keep working.
- `public/analytics.js` — new `<button data-tab="roles">Roles</button>`
in the tab strip + `case 'roles': await renderRolesTab(el)` in
`renderTab()`. The render function (distribution table + per-role
clock-skew posture) is moved over verbatim from the old standalone page.
- `public/roles-page.js` — deleted; its only consumer was the
now-removed route.

The Analytics tab strip already supports deep-linking via `?tab=…`, so
the redirect target is reached and the Roles tab activates on initial
load with no extra wiring.

## Acceptance criteria (from #1085)

- [x] No "Roles" link in top nav
- [x] Analytics page has a "Roles" tab with the same content
- [x] Old `#/roles` URLs redirect (don't 404)
- [x] Frees nav space for higher-priority pages

## Tests

E2E assertion added: test-e2e-playwright.js:2386 (3 assertions covering
all 3 acceptance criteria).

Also replaces the legacy "Roles page renders distribution table" E2E
test (added for issue #818), which assumed a standalone `/#/roles` SPA
page. The replacement assertions exercise the new fold-in path: nav
scan, Analytics tab click, redirect verification.

## TDD trail

- Red commit `35e1f46` — adds the three failing E2E assertions before
any production change. CI run on the red branch (linked above) shows the
assertions fail when production code hasn't been updated.
- Green commit `2b5715d` — minimal production change to satisfy the
assertions: nav link removed, redirect added, Roles tab + render
function moved into Analytics.

---------

Co-authored-by: clawbot <clawbot@users.noreply.github.com>
This commit is contained in:
Kpa-clawbot
2026-05-05 02:43:41 -07:00
committed by GitHub
co-authored by clawbot
parent 5fa3b56ccb
commit 36ee71d17e
5 changed files with 161 additions and 156 deletions
+46 -33
View File
@@ -2382,40 +2382,53 @@ async function run() {
assert(hasHslPolyline, 'At least one live-packet-trace polyline should have hsl() stroke color from hash');
});
// --- Roles page (issue #818): renders distribution + per-role skew ---
await test('Roles page renders distribution table from /api/analytics/roles', async () => {
await page.goto(BASE + '/#/roles', { waitUntil: 'domcontentloaded' });
// Wait for roles-page.js to mount and the table to render.
await page.waitForSelector('.roles-page[data-page="roles"]', { timeout: 10000 });
await page.waitForFunction(() => {
var el = document.querySelector('#rolesContent');
if (!el) return false;
// Either the table renders, or the empty-state message appears.
return !!el.querySelector('#rolesTable') || /No roles to show|Failed to load/.test(el.textContent);
}, { timeout: 10000 });
var hasTable = await page.$('#rolesTable');
if (!hasTable) {
// Empty fixture is acceptable; at least the page must NOT show the
// generic "Page not yet implemented" placeholder (the bug we fixed).
var bodyText = await page.evaluate(() => document.body.innerText);
assert(!/Page not yet implemented/i.test(bodyText), 'Roles page must not show "Page not yet implemented" placeholder');
return;
}
// With data: header columns and at least one body row must be present.
var headers = await page.$$eval('#rolesTable thead th', ths => ths.map(t => t.textContent.trim()));
assert(headers.includes('Role'), 'Roles table must have a Role column, got ' + JSON.stringify(headers));
assert(headers.some(h => /Median/.test(h)), 'Roles table must have a Median |skew| column, got ' + JSON.stringify(headers));
var rowCount = await page.$$eval('#rolesTable tbody tr', rs => rs.length);
assert(rowCount > 0, 'Roles table should have at least one row when API returns data');
// API contract sanity check: shape matches the page's expectations.
var apiOk = await page.evaluate(async () => {
var r = await fetch('/api/analytics/roles');
if (!r.ok) return { ok: false, status: r.status };
var j = await r.json();
return { ok: true, hasRoles: Array.isArray(j.roles), hasTotal: typeof j.totalNodes === 'number' };
// --- Roles folded into Analytics (issue #1085) ---
// Acceptance criteria:
// 1. "Roles" link does NOT exist in top nav
// 2. Analytics page has a "Roles" tab with the same content
// 3. Old #/roles URL redirects to #/analytics?tab=roles
await test('Roles fold-in (#1085): no "Roles" link in top nav', async () => {
await page.goto(BASE + '/#/home', { waitUntil: 'domcontentloaded' });
await page.waitForSelector('nav.top-nav .nav-links', { timeout: 10000 });
var hasRolesLink = await page.evaluate(() => {
var links = document.querySelectorAll('nav.top-nav .nav-links a.nav-link[data-route="roles"]');
return links.length > 0;
});
assert(apiOk.ok, '/api/analytics/roles must return 200, got ' + JSON.stringify(apiOk));
assert(apiOk.hasRoles && apiOk.hasTotal, '/api/analytics/roles response must have {roles:[], totalNodes:n}, got ' + JSON.stringify(apiOk));
assert(!hasRolesLink, 'Top nav must NOT contain a "Roles" link (data-route="roles")');
});
await test('Roles fold-in (#1085): Analytics page has a "Roles" tab', async () => {
await page.goto(BASE + '/#/analytics', { waitUntil: 'domcontentloaded' });
await page.waitForSelector('#analyticsTabs', { timeout: 10000 });
var rolesTab = await page.$('#analyticsTabs .tab-btn[data-tab="roles"]');
assert(rolesTab, 'Analytics tabs must include a [data-tab="roles"] button');
var label = await page.evaluate(el => el.textContent.trim(), rolesTab);
assert(/roles/i.test(label), 'Roles tab label must say "Roles", got ' + JSON.stringify(label));
// Click the tab and verify the same Roles content renders.
await page.click('#analyticsTabs [data-tab="roles"]');
// Wait for the tab to settle on real content: either the populated
// table (#rolesTable) or the explicit empty-state. "Loading" and
// "Failed to load" are NOT acceptable terminal states (#1085 polish).
await page.waitForFunction(() => {
var el = document.getElementById('analyticsContent');
if (!el) return false;
if (el.querySelector('#rolesTable')) return true;
if (/No roles to show/i.test(el.textContent)) return true;
return false;
}, { timeout: 10000 });
var bodyText = await page.evaluate(() => document.getElementById('analyticsContent').innerText);
assert(!/Page not yet implemented/i.test(bodyText), 'Roles tab must not show SPA placeholder');
assert(!/Failed to load/i.test(bodyText), 'Roles tab must not show "Failed to load" terminal state');
assert(!/Loading…/.test(bodyText), 'Roles tab must not be stuck on "Loading…"');
});
await test('Roles fold-in (#1085): old #/roles URL redirects to #/analytics?tab=roles', async () => {
await page.goto(BASE + '/#/roles', { waitUntil: 'domcontentloaded' });
// Allow router to process the redirect.
await page.waitForFunction(() => /^#\/analytics(\?|$)/.test(location.hash), { timeout: 5000 });
var hash = await page.evaluate(() => location.hash);
assert(/^#\/analytics\?/.test(hash), 'After visiting #/roles, hash must redirect to #/analytics?…, got ' + hash);
assert(/[?&]tab=roles(&|$)/.test(hash), 'Redirect must carry tab=roles, got ' + hash);
});
// --- Geofilter draft: save/load/download buttons (issue #819, rule 18) ---