Restore frontend and stable browser contracts

Canonicalize legacy Proxmox backup bookmarks through the shared route tabs,
restore the mobile node web-interface affordance, and strengthen the frontend
source-shape guardrails around shared primitives and canonical workload IDs.
Align stale stable-tier browser selectors with the accessible product contract
while retaining the routing, visibility, history, and tolerance assertions.

Incident: 9c1208a480c48dd6
This commit is contained in:
rcourtman 2026-08-23 20:27:53 +01:00
parent 6794971199
commit a7f3ae6e91
14 changed files with 211 additions and 58 deletions

View file

@ -1,38 +1,40 @@
{
"version": 1,
"base_sha": "10116cc17efb0004e3820f1a250335899d3b9bf0",
"verified_at": "2026-08-23T19:17:24Z",
"base_sha": "679497119925fca16420fdc3cb7b5d940a50578f",
"verified_at": "2026-08-23T19:26:55Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/components/Workloads/useWorkloadViewportSync.ts"
"frontend-modern/src/features/proxmox/ProxmoxBackupsTable.tsx",
"frontend-modern/src/features/proxmox/ProxmoxNodesTable.tsx"
],
"content_sha256": {
"frontend-modern/src/components/Workloads/useWorkloadViewportSync.ts": "f21b70f169cc595b31337584e3df20022342e39d50bdfc9eac96a04770bcf7ba"
"frontend-modern/src/features/proxmox/ProxmoxBackupsTable.tsx": "abd090fa70e905ccdabbca1599088288330ec380b3bcfc7bd84624db3a76c7f3",
"frontend-modern/src/features/proxmox/ProxmoxNodesTable.tsx": "5ac5e1670e9613b4b11a1596602c6f19a0e0df4982287cfbb491a0f09d993a31"
},
"routes": [
"/proxmox/overview"
"/proxmox",
"/proxmox/backups",
"/proxmox/backups/date",
"/proxmox/backups/coverage"
],
"viewports": [
{
"width": 1280,
"height": 800
"width": 1440,
"height": 900
},
{
"width": 390,
"height": 844
"width": 393,
"height": 727
}
],
"states": [
"The connected public mock estate rendered 50 nodes and 929 guests with the grouped workload window active",
"Across 24 consecutive 600-pixel desktop wheel steps from scrollTop 10000 to 24400, every sample retained at least 26 visible guest rows",
"Across 24 consecutive 600-pixel wheel steps at 390x844 from scrollTop 10000 to 24400, every sample retained at least 22 visible guest rows",
"Desktop and narrow verification both retained the 140-row mounted guest budget throughout rapid scrolling",
"The verified route reported no browser error-level runtime logs during responsive and sustained-scroll checks"
"A legacy /proxmox/backups bookmark replaced itself with canonical /proxmox/backups/date, while date selection retained the canonical route plus its day query",
"The one-year backup activity view and trailing backup-server columns remained inside the 1440-pixel desktop viewport",
"At the Pixel 5 viewport, the offline Proxmox node remained visible with dashed stale metrics and a visible safe web-interface link; the unsafe JavaScript URL remained blocked with its warning"
],
"interactions": [
"Reloaded /proxmox/overview and confirmed the complete large-estate mock snapshot and bounded workload window",
"Applied repeated physical wheel input through the workload viewport and sampled visible and mounted rows immediately after every desktop step",
"Repeated the same sustained wheel-input sequence at a 390x844 phone viewport and visually inspected the populated table",
"Inspected browser runtime logs after both responsive scroll sequences"
"Opened the legacy backups bookmark, followed the shared Backups and view-tab links, selected a populated day, and exercised the one-year range",
"Opened the offline node's HTTPS web-interface link in a new page and confirmed that the adjacent control did not expand the node row",
"Verified the mobile link hit target, blocked unsafe URL, retained details control, and absence of horizontal viewport overflow"
]
}

View file

@ -2950,6 +2950,7 @@
"requiredConsumers": [
{ "path": "src/features/docker/DockerPageSurface.tsx" },
{ "path": "src/features/kubernetes/KubernetesPageSurface.tsx" },
{ "path": "src/features/proxmox/ProxmoxBackupsTable.tsx" },
{ "path": "src/features/proxmox/ProxmoxPageSurface.tsx" },
{ "path": "src/features/standalone/StandalonePageSurface.tsx" },
{ "path": "src/features/truenas/TrueNASPageSurface.tsx" },

View file

@ -533,7 +533,8 @@ describe('shared primitive guardrails', () => {
expect(patrolIntelligenceHeaderSource).not.toContain(
'flex-1 py-1.5 px-2 text-xs font-semibold rounded-md transition-all duration-200',
);
expect(proxmoxBackupsTableSource).toContain('FilterSegmentedControl');
expect(proxmoxBackupsTableSource).toContain('PlatformSectionTabs');
expect(proxmoxBackupsTableSource).not.toContain('FilterSegmentedControl');
expect(proxmoxBackupsTableSource).not.toContain('FilterButtonGroup');
expect(proxmoxBackupsTableSource).not.toContain('const viewButtonClass');
expect(proxmoxBackupsTableSource).not.toContain(
@ -7252,6 +7253,7 @@ describe('shared primitive guardrails', () => {
const requiredConsumerPaths = [
'src/features/docker/DockerPageSurface.tsx',
'src/features/kubernetes/KubernetesPageSurface.tsx',
'src/features/proxmox/ProxmoxBackupsTable.tsx',
'src/features/proxmox/ProxmoxPageSurface.tsx',
'src/features/standalone/StandalonePageSurface.tsx',
'src/features/truenas/TrueNASPageSurface.tsx',
@ -7260,6 +7262,7 @@ describe('shared primitive guardrails', () => {
const platformPageSurfaceSources = [
dockerPageSurfaceSource,
kubernetesPageSurfaceSource,
proxmoxBackupsTableSource,
proxmoxPageSurfaceSource,
standalonePageSurfaceSource,
truenasPageSurfaceSource,
@ -7306,7 +7309,7 @@ describe('shared primitive guardrails', () => {
expect(sharedPlatformPageSource).toContain(
'export const PLATFORM_TABLE_NARROW_IDENTITY_WIDTH_PERCENT = 40',
);
expect(proxmoxNodesTableSource).toContain("usesCondensedIdentity() ? '[&>a]:hidden' : ''");
expect(proxmoxNodesTableSource).not.toContain("'[&>a]:hidden'");
expect(proxmoxNodesTableSource).toContain(
"layoutMode() === 'narrow' && nativeNodeName().length",
);

View file

@ -1,6 +1,14 @@
import { Show, createMemo, createResource, createSignal, type Component, type JSX } from 'solid-js';
import {
Show,
createEffect,
createMemo,
createResource,
createSignal,
type Component,
type JSX,
} from 'solid-js';
import ChevronRightIcon from 'lucide-solid/icons/chevron-right';
import { useLocation, useSearchParams } from '@solidjs/router';
import { useLocation, useNavigate, useSearchParams } from '@solidjs/router';
import { FilterBar, type FilterDef, type FilterSelectOption } from '@/components/shared/FilterBar';
import { useBreakpoint } from '@/hooks/useBreakpoint';
import { useProtectionPostures } from '@/hooks/useProtectionPostures';
@ -110,6 +118,7 @@ export const ProxmoxBackupsTable: Component<{
const [pbsBackups] = createResource<PBSBackupsPayload>(fetchPBSBackups);
const { isMobile } = useBreakpoint();
const location = useLocation();
const navigate = useNavigate();
const protectionPostures = useProtectionPostures(() =>
(props.workloads ?? []).map((workload) => workload.id),
);
@ -395,6 +404,17 @@ export const ProxmoxBackupsTable: Component<{
BACKUP_VIEW_TABS.map((tab) => ({ ...tab, path: backupViewPath(tab.id) })),
);
// Keep old bookmarks and saved links working, but replace the legacy base
// route and query-param view with the canonical route segment. Replacement
// avoids adding a dead compatibility URL to browser history.
createEffect(() => {
const normalizedPath = location.pathname.replace(/\/+$/, '');
if (normalizedPath !== PROXMOX_BACKUPS_PATH) return;
navigate(backupViewPath(legacyQueryView() ?? PROXMOX_BACKUPS_DEFAULT_VIEW), {
replace: true,
});
});
// Recoverable artifact feed: PBS snapshots, PVE backup files, and guest snapshot rows.
const RECOVERABLE_SEGMENT_KINDS: readonly BackupActivitySegmentKind[] = [
'pbs',

View file

@ -498,7 +498,7 @@ export const ProxmoxNodesTable: Component<{
<ResourceNameWithWebInterfaceLink
name={name()}
url={externalUrl()}
class={`min-w-0 ${usesCondensedIdentity() ? '[&>a]:hidden' : ''}`}
class="min-w-0"
nameClass="truncate font-semibold text-base-content"
title={`Open ${name()} web interface`}
>

View file

@ -343,6 +343,28 @@ describe('ProxmoxBackupsTable', () => {
expect(screen.queryByRole('button', { name: /pbs artifacts/i })).not.toBeInTheDocument();
});
it('normalizes legacy backup bookmarks to the canonical route without losing filters', async () => {
mockBackupAPIs();
window.history.replaceState(
{},
'',
'/proxmox/backups?view=coverage&q=pbs-docker&source=pbs&day=2026-05-25',
);
renderInRouter(() => (
<ProxmoxBackupsTable emptyIcon={<span />} workloads={[workloadResource]} />
));
await waitFor(() => {
expect(window.location.pathname).toBe('/proxmox/backups/coverage');
const params = new URLSearchParams(window.location.search);
expect(params.get('view')).toBeNull();
expect(params.get('q')).toBe('pbs-docker');
expect(params.get('source')).toBeNull();
expect(params.get('day')).toBeNull();
});
});
it('hydrates the complete saved-view filter state from the URL and clears it atomically', async () => {
mockBackupAPIs();
window.history.replaceState(

View file

@ -295,6 +295,32 @@ describe('ProxmoxNodesTable', () => {
expect(screen.getByText('pve1')).toBeInTheDocument();
});
it('keeps the adjacent web-interface link visible and touch-sized on phones', () => {
render(() => (
<ProxmoxNodesTable
nodes={[
makeNodeResource({
proxmox: {
clusterName: 'homelab',
nodeName: 'pve-node-1',
guestUrl: 'https://pve.example.com:8006',
},
}),
]}
guests={[]}
layoutWidth={() => 390}
emptyIcon={<span />}
emptyTitle="No Proxmox VE nodes"
emptyDescription="No nodes"
/>
));
const link = screen.getByRole('link', { name: 'Open web interface for pve-node-1' });
expect(link).toBeVisible();
expect(link).toHaveClass('min-h-6', 'min-w-6');
expect(link.parentElement).not.toHaveClass('[&>a]:hidden');
});
it('passes alert-backed temperature thresholds into the node temperature gauge', () => {
render(() => (
<ProxmoxNodesTable

View file

@ -602,7 +602,15 @@ describe('frontend resource type boundaries', () => {
expect(workloadsSurfaceSource).not.toContain(
'createMemo(() => getCanonicalWorkloadId(guest()))',
);
expect(workloadPanelSource).toContain('createMemo(() => getCanonicalWorkloadId(guest()))');
expect(workloadPanelSource).toContain(
'new Map(groupGuests().map((guest) => [getCanonicalWorkloadId(guest), guest]))',
);
expect(workloadPanelSource).toContain(
'createMemo(() => groupGuests().map(getCanonicalWorkloadId))',
);
expect(workloadPanelSource).toContain('<For each={groupGuestIds()}');
expect(workloadPanelSource).toContain('groupGuestById().get(keyedGuestId)');
expect(workloadPanelSource).toContain('const guestId = () => keyedGuestId');
expect(workloadsWorkloadTableSource).not.toContain(
'createMemo(() => getCanonicalWorkloadId(guest()))',
);
@ -913,7 +921,15 @@ describe('frontend resource type boundaries', () => {
expect(workloadTableHeaderSource).not.toContain('NodeGroupHeader');
expect(workloadPanelSource).toContain('NodeGroupHeader');
expect(workloadPanelSource).toContain('GuestDrawer');
expect(workloadPanelSource).toContain('createMemo(() => getCanonicalWorkloadId(guest()))');
expect(workloadPanelSource).toContain(
'new Map(groupGuests().map((guest) => [getCanonicalWorkloadId(guest), guest]))',
);
expect(workloadPanelSource).toContain(
'createMemo(() => groupGuests().map(getCanonicalWorkloadId))',
);
expect(workloadPanelSource).toContain('<For each={groupGuestIds()}');
expect(workloadPanelSource).toContain('groupGuestById().get(keyedGuestId)');
expect(workloadPanelSource).toContain('const guestId = () => keyedGuestId');
expect(workloadPanelSource).not.toContain('TableHead');
expect(emptyStateSource).toContain('getEmptyStatePresentation');
expect(emptyStateSource).not.toContain('const iconBgClass: Record<EmptyStateTone, string> =');

View file

@ -15,8 +15,13 @@ async function openProxmoxBackups(page: Page) {
name: "Proxmox sections",
});
await expect(sections).toBeVisible({ timeout: 60_000 });
await sections.getByRole("link", { name: "Backups", exact: true }).click();
await expect(page).toHaveURL(/\/proxmox\/backups$/);
const backupsLink = sections.getByRole("link", {
name: "Backups",
exact: true,
});
await expect(backupsLink).toHaveAttribute("href", "/proxmox/backups/date");
await backupsLink.click();
await expect(page).toHaveURL(/\/proxmox\/backups\/date$/);
}
// Layout guards for the Proxmox Backups section, which replaced the retired
@ -35,13 +40,27 @@ test.describe("Proxmox backups layout guards", () => {
await page.setViewportSize(DESKTOP_VIEWPORT);
await ensureAuthenticated(page);
// Existing bookmarks remain compatible, but the route is normalized to
// the canonical route-segment view before the table is used.
await page.goto("/proxmox/backups", { waitUntil: "domcontentloaded" });
await expect(page).toHaveURL(/\/proxmox\/backups\/date$/, {
timeout: 60_000,
});
await openProxmoxBackups(page);
// The guest-centric Coverage view is the default whenever anything needs
// attention; the day-activity strip lives in the By date view.
const byDateButton = page.getByRole("button", { name: "By date" });
await expect(byDateButton).toBeVisible({ timeout: 60_000 });
await byDateButton.click();
const byDateLink = page.getByRole("link", { name: "By date", exact: true });
const coverageLink = page.getByRole("link", {
name: "Coverage",
exact: true,
});
await expect(byDateLink).toHaveAttribute("href", "/proxmox/backups/date");
await expect(coverageLink).toHaveAttribute(
"href",
"/proxmox/backups/coverage",
);
await byDateLink.click();
await expect(page).toHaveURL(/\/proxmox\/backups\/date$/);
await expect(page.getByText("Backups per day").first()).toBeVisible();
const dayButtons = page.getByRole("button", { name: /: \d+ backups?$/ });
await expect.poll(() => dayButtons.count()).toBeGreaterThanOrEqual(7);
@ -54,8 +73,19 @@ test.describe("Proxmox backups layout guards", () => {
.getByRole("button", { name: /: [1-9]\d* backups?$/ })
.last();
await activeDay.click();
await expect(page).toHaveURL(/\/proxmox\/backups/);
await expect(page).toHaveURL(
/\/proxmox\/backups\/date\?day=\d{4}-\d{2}-\d{2}$/,
);
await expect(page.getByText(/^\d+ of \d+ backups$/).first()).toBeVisible();
const filteredScreenshotPath = testInfo.outputPath(
"proxmox-backups-by-date-filtered.png",
);
await page.screenshot({ path: filteredScreenshotPath, fullPage: true });
await testInfo.attach("proxmox-backups-by-date-filtered", {
path: filteredScreenshotPath,
contentType: "image/png",
});
});
test("long-range activity keeps the page inside the horizontal viewport", async ({
@ -70,9 +100,10 @@ test.describe("Proxmox backups layout guards", () => {
await ensureAuthenticated(page);
await openProxmoxBackups(page);
const byDateButton = page.getByRole("button", { name: "By date" });
await expect(byDateButton).toBeVisible({ timeout: 60_000 });
await byDateButton.click();
const byDateLink = page.getByRole("link", { name: "By date", exact: true });
await expect(byDateLink).toHaveAttribute("href", "/proxmox/backups/date");
await byDateLink.click();
await expect(page).toHaveURL(/\/proxmox\/backups\/date$/);
await page
.getByRole("group", { name: "Activity range" })
@ -112,5 +143,14 @@ test.describe("Proxmox backups layout guards", () => {
expect(dedupBox!.x + dedupBox!.width).toBeLessThanOrEqual(
wrapperBox!.x + wrapperBox!.width + 1,
);
const yearScreenshotPath = testInfo.outputPath(
"proxmox-backups-one-year-layout.png",
);
await page.screenshot({ path: yearScreenshotPath, fullPage: true });
await testInfo.attach("proxmox-backups-one-year-layout", {
path: yearScreenshotPath,
contentType: "image/png",
});
});
});

View file

@ -58,7 +58,10 @@ test.describe('TrueNAS storage disk history', () => {
await page.goto('/truenas/storage', { waitUntil: 'domcontentloaded' });
await page
.getByRole('textbox', { name: /Search TrueNAS/ })
.getByRole('combobox', {
name: 'Search TrueNAS pools, datasets, or disks',
exact: true,
})
.fill('sdc');
const diskRow = page.locator('tr').filter({ hasText: 'sdc' }).first();
@ -90,5 +93,12 @@ test.describe('TrueNAS storage disk history', () => {
// Serial when available, disk:<node>:<device> composite otherwise; both
// must stay canonical metrics-store keys.
expect(resourceId).toMatch(/^(WD-|disk:truenas-main:)/);
const screenshotPath = testInfo.outputPath('truenas-disk-history.png');
await page.screenshot({ path: screenshotPath, fullPage: true });
await testInfo.attach('truenas-disk-history', {
path: screenshotPath,
contentType: 'image/png',
});
});
});

View file

@ -29,7 +29,8 @@ const PAGE_HEADER_ROUTES = [
slug: "patrol",
route: "/patrol",
title: "Patrol",
description: "Patrol checks your infrastructure and shows current issues.",
description:
"See what needs a decision, choose the next step, and keep a verified record.",
},
] as const;
@ -101,7 +102,9 @@ test.describe("Top-level page header consistency", () => {
});
}
test("keeps primary page headings vertically aligned", async ({ page }, testInfo) => {
test("keeps primary page headings vertically aligned", async ({
page,
}, testInfo) => {
test.skip(
testInfo.project.name.startsWith("mobile-"),
"Phone headers use responsive framing and intentionally do not share desktop Y coordinates",

View file

@ -115,7 +115,7 @@ test.describe("Offline Proxmox node visibility", () => {
test("keeps an offline Proxmox node visible on desktop and mobile surfaces", async ({
page,
}) => {
}, testInfo) => {
await page.context().route("https://pve5:8006/**", async (route) => {
await route.fulfill({
status: 200,
@ -198,5 +198,14 @@ test.describe("Offline Proxmox node visibility", () => {
name: "Expand details for Disaster Recovery B",
}),
).toBeVisible();
const screenshotPath = testInfo.outputPath(
"offline-proxmox-node-visible-with-web-interface.png",
);
await page.screenshot({ path: screenshotPath, fullPage: true });
await testInfo.attach("offline-proxmox-node-visible-with-web-interface", {
path: screenshotPath,
contentType: "image/png",
});
});
});

View file

@ -11,8 +11,6 @@ type WorkerFixtures = {
authStorageStatePath: string;
};
const SCREENSHOT_PATH = "/tmp/patrol-assistant-operator-briefing.png";
const test = base.extend<{}, WorkerFixtures>({
storageState: async ({ authStorageStatePath }, use) => {
await use(authStorageStatePath);
@ -44,7 +42,7 @@ test.describe("Patrol Assistant operator briefing", () => {
test("shows attention and operator-decision context in the Assistant drawer", async ({
page,
}) => {
}, testInfo) => {
const approvalRequestedAt = new Date(Date.now() - 60_000).toISOString();
const approvalExpiresAt = new Date(Date.now() + 10 * 60_000).toISOString();
let includePendingApproval = true;
@ -605,9 +603,7 @@ test.describe("Patrol Assistant operator briefing", () => {
await page.goto("/patrol", { waitUntil: "domcontentloaded" });
await page.getByRole("tab", { name: "Activity", exact: true }).click();
await page
.getByRole("button", { name: /Findings and run records/ })
.click();
await page.getByRole("button", { name: /^Patrol records(?:$|\s)/ }).click();
await page.getByText("High CPU usage").click();
const findingReview = page.locator(
@ -634,7 +630,14 @@ test.describe("Patrol Assistant operator briefing", () => {
assistantContext.getByText("systemctl restart workload.service"),
).toHaveCount(0);
await page.screenshot({ path: SCREENSHOT_PATH, fullPage: true });
const screenshotPath = testInfo.outputPath(
"patrol-assistant-operator-briefing.png",
);
await page.screenshot({ path: screenshotPath, fullPage: true });
await testInfo.attach("patrol-assistant-operator-briefing", {
path: screenshotPath,
contentType: "image/png",
});
await page.getByTitle("Pulse Assistant sessions").click();
const highCPUFollowUp = page.getByRole("option", {
@ -744,9 +747,7 @@ test.describe("Patrol Assistant operator briefing", () => {
includePendingApproval = false;
await page.reload({ waitUntil: "domcontentloaded" });
await page.getByRole("tab", { name: "Activity", exact: true }).click();
await page
.getByRole("button", { name: /Findings and run records/ })
.click();
await page.getByRole("button", { name: /^Patrol records(?:$|\s)/ }).click();
const queuedFindingTitle = page.getByText("High CPU usage").first();
await expect(queuedFindingTitle).toBeVisible();
await queuedFindingTitle.click();
@ -777,9 +778,7 @@ test.describe("Patrol Assistant operator briefing", () => {
includeInvestigationProposedFix = true;
await page.reload({ waitUntil: "domcontentloaded" });
await page.getByRole("tab", { name: "Activity", exact: true }).click();
await page
.getByRole("button", { name: /Findings and run records/ })
.click();
await page.getByRole("button", { name: /^Patrol records(?:$|\s)/ }).click();
const expiredFindingTitle = page.getByText("High CPU usage").first();
await expect(expiredFindingTitle).toBeVisible();
await expiredFindingTitle.click();

View file

@ -117,13 +117,13 @@ test("powered-off tolerance preserves inheritance, explicit zero, and strict val
await expect(tolerance).toHaveValue("");
await expect(
page.getByText(
"Blank inherits the existing policy; 0 alerts on the first stopped observation.",
"Blank inherits the existing policy. 0 alerts on the first stopped observation.",
{ exact: true },
),
).toBeVisible();
await expect(page.getByRole("combobox", { name: "Resource" })).toHaveValue(
"vm:1567",
);
await expect(
page.getByRole("combobox", { name: "Resource", exact: true }),
).toHaveValue("vm:1567");
await tolerance.fill("300");
await page.getByRole("button", { name: "Save defaults" }).click();
await expect(
@ -169,8 +169,10 @@ test("powered-off tolerance preserves inheritance, explicit zero, and strict val
graceSeconds: 0,
});
const screenshotPath = testInfo.outputPath("powered-off-tolerance.png");
await page.screenshot({ path: screenshotPath, fullPage: true });
await testInfo.attach("powered-off-tolerance", {
body: await page.screenshot(),
path: screenshotPath,
contentType: "image/png",
});
});