diff --git a/frontend-modern/browser-tests/maintenance-evidence.cjs b/frontend-modern/browser-tests/maintenance-evidence.cjs
new file mode 100644
index 0000000000..f20c6d165b
--- /dev/null
+++ b/frontend-modern/browser-tests/maintenance-evidence.cjs
@@ -0,0 +1,416 @@
+// Read-only browser acceptance of the changed report presentation/query flow.
+// Explicit review/rerun business calls are covered by the runtime component
+// suite. Every POST is refused here, so a clock-induced operation fails proof.
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const crypto = require('node:crypto');
+const { chromium, webkit } = require('playwright');
+const root = '/workspace/frontend-modern';
+const output = '/workspace/tmp/maintenance-evidence/browser-read-only';
+const origin = 'http://127.0.0.1:5347';
+const start = Date.parse('2026-10-11T04:00:00Z');
+const iso = (offset) => new Date(start + offset).toISOString();
+const reports = ['healthy', 'pending', 'needs_review', 'failed_verification'].map((status) => ({
+ id: 'report:' + status,
+ resourceId: 'pbs-a',
+ status,
+ trigger: 'maintenance_window_ended',
+ startedAt: iso(-30_000),
+ completedAt: status === 'pending' ? '0001-01-01T00:00:00Z' : iso(-30_000),
+ windowStartedAt: iso(-90_000_000),
+ windowEndedAt: iso(-86_400_000),
+ evidence: {
+ activeCriticalAlerts: status === 'failed_verification' ? 1 : 0,
+ activeWarningAlerts: 2,
+ activeCriticalFindings: 0,
+ activeWarningFindings: 1,
+ failedActionsSinceWindowStart: 0,
+ metricRecovery: { samplesAfterEnd: 3, trend: 'improving' },
+ },
+ linkedFindingIds: [],
+ linkedAlertIds: [],
+ linkedActionIds: [],
+ recommendation: 'Recorded evidence, not a fresh health observation.',
+ ...(status === 'healthy'
+ ? {
+ userOutcome: 'reviewed',
+ reviewedAt: iso(-10_000),
+ reviewedBy: 'fixture-admin',
+ reviewNote: 'Earlier check retained. A completed backup does not prove guest recovery.',
+ }
+ : {}),
+}));
+async function bounded(promise, label) {
+ let timer;
+ try {
+ return await Promise.race([
+ promise,
+ new Promise((_, reject) => {
+ timer = setTimeout(
+ () => reject(new Error('Fixture observation deadline: ' + label)),
+ 10_000,
+ );
+ }),
+ ]);
+ } finally {
+ clearTimeout(timer);
+ }
+}
+(async () => {
+ fs.mkdirSync(output, { recursive: true });
+ const binding = JSON.parse(
+ fs.readFileSync('/workspace/tmp/maintenance-evidence/source-binding.json'),
+ );
+ for (const [file, digest] of Object.entries(binding.content_sha256)) {
+ assert.equal(
+ crypto
+ .createHash('sha256')
+ .update(fs.readFileSync('/workspace/' + file))
+ .digest('hex'),
+ digest,
+ file,
+ );
+ }
+ const playwright = require('playwright/package.json').version;
+ assert.equal(
+ playwright,
+ JSON.parse(fs.readFileSync('/workspace/tests/integration/package-lock.json')).packages[
+ 'node_modules/@playwright/test'
+ ].version,
+ );
+ const result = {
+ source: binding,
+ playwright,
+ cases: [],
+ captures: [],
+ completed: false,
+ browsersClosed: false,
+ serverClosed: false,
+ };
+ const { createServer } = await import(root + '/node_modules/vite/dist/node/index.js');
+ const server = await createServer({
+ root,
+ configFile: root + '/vite.config.ts',
+ cacheDir: output + '/vite-cache',
+ server: { host: '127.0.0.1', port: 5347, strictPort: true, watch: null },
+ });
+ let browser, context;
+ try {
+ await server.listen();
+ for (const [name, engine, width, height, dark] of [
+ ['desktop', chromium, 1365, 900, false],
+ ['phone', webkit, 390, 844, true],
+ ]) {
+ browser = await engine.launch(
+ engine === chromium
+ ? { headless: true, channel: 'chromium', args: ['--no-sandbox'] }
+ : { headless: true },
+ );
+ context = await browser.newContext({
+ viewport: { width, height },
+ isMobile: dark,
+ hasTouch: dark,
+ locale: 'en-GB',
+ timezoneId: 'UTC',
+ });
+ const page = await context.newPage();
+ page.setDefaultTimeout(10_000);
+ page.setDefaultNavigationTimeout(45_000);
+ await page.clock.install({ time: new Date(start - 1000) });
+ await page.clock.pauseAt(new Date(start));
+ await page.addInitScript((dark) => {
+ localStorage.clear();
+ sessionStorage.clear();
+ document.addEventListener(
+ 'DOMContentLoaded',
+ () => document.documentElement.classList.toggle('dark', dark),
+ { once: true },
+ );
+ }, dark);
+ const record = {
+ name,
+ browserVersion: browser.version(),
+ viewport: { width, height },
+ dark,
+ checks: [],
+ reads: [],
+ writes: [],
+ errors: [],
+ unexpected: [],
+ offOrigin: [],
+ completed: false,
+ contextClosed: false,
+ };
+ result.cases.push(record);
+ let mode = 'pending',
+ releaseRead,
+ observeRead;
+ const readStarted = new Promise((resolve) => {
+ observeRead = resolve;
+ });
+ page.on('pageerror', (error) => record.errors.push(error.stack || error.message));
+ await page.route('**/*', async (route) => {
+ const req = route.request(),
+ url = new URL(req.url());
+ if (url.origin !== origin) {
+ record.offOrigin.push(url.origin);
+ return route.abort();
+ }
+ if (!url.pathname.startsWith('/api/')) return route.continue();
+ if (req.method() !== 'GET') {
+ record.writes.push(url.pathname);
+ return route.fulfill({
+ status: 403,
+ json: { error: 'Read-only fixture refuses operations' },
+ });
+ }
+ record.reads.push(url.pathname);
+ let data;
+ if (url.pathname.endsWith('/maintenance-verifications')) {
+ if (url.pathname.includes('/pending-resource/')) {
+ if (mode === 'pending') {
+ await bounded(
+ new Promise((resolve) => {
+ releaseRead = resolve;
+ observeRead();
+ }),
+ 'held new-resource read',
+ );
+ return route.fulfill({
+ status: 503,
+ json: { error: 'Synthetic unavailable reader' },
+ });
+ }
+ if (mode === 'denied')
+ return route.fulfill({ status: 403, json: { error: 'Synthetic access denial' } });
+ return route.fulfill({
+ json: { data: [], meta: { resourceId: 'pending-resource', limit: 25, total: 0 } },
+ });
+ }
+ data = { data: reports, meta: { resourceId: 'pbs-a', limit: 25, total: reports.length } };
+ } else if (url.pathname.endsWith('/operator-state'))
+ data = {
+ configured: true,
+ state: {
+ canonicalId: 'pbs-a',
+ neverAutoRemediate: true,
+ monitoringMode: 'expected_offline',
+ lifecycleState: 'retired',
+ note: 'Keep the saved protection',
+ setAt: iso(-60_000),
+ },
+ };
+ else if (url.pathname.endsWith('/facets'))
+ data = { capabilities: [], relationships: [], recentChanges: [], counts: {} };
+ else if (url.pathname === '/api/ai/intelligence')
+ data = { resourceId: 'pbs-a', findings: [], investigations: [] };
+ else if (url.pathname === '/api/audit/actions')
+ data = { audits: [], count: 0, available: false };
+ else if (url.pathname === '/api/license/runtime-capabilities')
+ data = {
+ capabilities: [],
+ limits: [],
+ hosted_mode: false,
+ runtime: { build: 'community' },
+ blocked_capabilities: [],
+ };
+ else if (url.pathname === '/api/ai/settings') data = { discovery_enabled: false };
+ else if (url.pathname === '/api/security/status')
+ data = { hasAuthentication: true, requiresAuth: false, sessionCapabilities: {} };
+ else {
+ record.unexpected.push(url.pathname);
+ return route.fulfill({ status: 404, json: { error: 'Unspecified synthetic read' } });
+ }
+ return route.fulfill({ json: data });
+ });
+ const check = async (label, action) => {
+ result.phase = name + ': ' + label;
+ console.log(result.phase);
+ await action();
+ record.checks.push(label);
+ };
+ const capture = async (label) => {
+ const file = output + '/' + name + '-' + label + '.png';
+ await page.screenshot({ path: file, fullPage: true, animations: 'disabled' });
+ result.captures.push(file.replace('/workspace/', ''));
+ };
+ result.phase = name + ': mount full drawer';
+ await page.goto(origin + '/browser-tests/maintenance-evidence.html');
+ await page.getByRole('tab', { name: 'Manage', exact: true }).click();
+ const section = page.getByTestId('maintenance-verification-section');
+ await section.getByText('Checked 30s ago', { exact: true }).first().waitFor();
+ await page.getByLabel('Operator note', { exact: true }).fill('Unsaved protected note');
+ await check(
+ 'separate recorded check/window/review ages, unknown pending completion and all four verdicts',
+ async () => {
+ assert.equal(
+ await section.getByText('Window ended 1 day ago', { exact: true }).count(),
+ 4,
+ );
+ assert.equal(await section.getByText('Checked unknown', { exact: true }).count(), 1);
+ assert.equal(
+ await section.getByText('Reviewed 10s ago by fixture-admin', { exact: true }).count(),
+ 1,
+ );
+ assert.match(await section.innerText(), /Reports record past checks, not current health/);
+ for (const status of ['healthy', 'pending', 'needs_review', 'failed_verification'])
+ assert.equal(
+ await section.getByTestId('maintenance-verification-status-' + status).count(),
+ 1,
+ );
+ },
+ );
+ await capture('recorded');
+ const reads = record.reads.length;
+ await page.clock.fastForward(60_000);
+ await check(
+ 'held events age past a minute without reads/writes, changing verdicts or discarding edits',
+ async () => {
+ await section.getByText('Checked 1 min ago', { exact: true }).first().waitFor();
+ assert.equal(
+ await section.getByText('Reviewed 1 min ago by fixture-admin', { exact: true }).count(),
+ 1,
+ );
+ assert.equal(
+ await page.getByLabel('Operator note', { exact: true }).inputValue(),
+ 'Unsaved protected note',
+ );
+ assert.equal(record.reads.length, reads);
+ assert.equal(record.writes.length, 0);
+ },
+ );
+ await page.clock.fastForward(3_600_000);
+ await check(
+ 'an hour-old verdict stays historical with its evidence, review admission and saved restrictions',
+ async () => {
+ await section.getByText('Checked 1 hour ago', { exact: true }).first().waitFor();
+ assert.equal(
+ await section
+ .getByText('Reviewed 1 hour ago by fixture-admin', { exact: true })
+ .count(),
+ 1,
+ );
+ assert.equal(await section.getByTestId('maintenance-verification-review').count(), 3);
+ assert.equal(
+ await page
+ .getByLabel('Never auto-remediate this resource', { exact: true })
+ .getAttribute('aria-pressed'),
+ 'true',
+ );
+ assert.equal(
+ await page.getByLabel('Monitoring', { exact: true }).inputValue(),
+ 'expected_offline',
+ );
+ assert.equal(await page.getByLabel('Lifecycle', { exact: true }).inputValue(), 'retired');
+ assert.equal(record.reads.length, reads);
+ assert.equal(record.writes.length, 0);
+ },
+ );
+ await capture('hour-old');
+ await page.evaluate(() => window.__maintenanceEvidence.select('pending-resource'));
+ // The real drawer deliberately remounts at a different canonical ID and
+ // starts on Overview. Open Manage as an operator would; do not assume a
+ // held tab or misattribute that existing protection to this repair.
+ await page.getByRole('tab', { name: 'Manage', exact: true }).click();
+ await bounded(readStarted, 'new-resource GET observed');
+ await check(
+ 'changing resource immediately withdraws prior reports while its own read waits',
+ async () => {
+ await section.getByText('Loading verification reports…', { exact: true }).waitFor();
+ assert.equal(await section.getByTestId('maintenance-verification-report').count(), 0);
+ assert.equal(await section.getByText(/No verification reports yet/).count(), 0);
+ await page.clock.fastForward(60_000);
+ assert.equal(await section.getByTestId('maintenance-verification-report').count(), 0);
+ },
+ );
+ await capture('other-resource-pending');
+ releaseRead();
+ await check(
+ 'a failed first read is unavailable, not empty history or another resource evidence',
+ async () => {
+ await section.getByTestId('maintenance-verification-load-error').waitFor();
+ assert.equal(await section.getByText(/No verification reports yet/).count(), 0);
+ assert.equal(await section.getByTestId('maintenance-verification-report').count(), 0);
+ const before = record.reads.length;
+ await page.clock.fastForward(60_000);
+ assert.equal(record.reads.length, before);
+ },
+ );
+ await capture('unavailable');
+ mode = 'denied';
+ await section.getByRole('button', { name: 'Retry reports', exact: true }).focus();
+ await section.getByRole('button', { name: 'Retry reports', exact: true }).press('Enter');
+ await check(
+ 'explicit keyboard read retry shows bounded denied guidance without reviving reports',
+ async () => {
+ await section
+ .getByText('Access denied. Check your permissions and license plan.', { exact: true })
+ .waitFor();
+ const before = record.reads.length;
+ await page.clock.fastForward(60_000);
+ assert.equal(record.reads.length, before);
+ assert.equal(await section.getByTestId('maintenance-verification-report').count(), 0);
+ assert.equal(record.writes.length, 0);
+ },
+ );
+ await capture('denied');
+ mode = 'empty';
+ await section.getByRole('button', { name: 'Retry reports', exact: true }).click();
+ await check(
+ 'only a successful empty read is empty history, with no subsequent clock retry',
+ async () => {
+ await section.getByText(/No verification reports yet/).waitFor();
+ assert.equal(await section.getByTestId('maintenance-verification-load-error').count(), 0);
+ const before = record.reads.length;
+ await page.clock.fastForward(60_000);
+ assert.equal(record.reads.length, before);
+ assert.equal(await section.getByTestId('maintenance-verification-report').count(), 0);
+ },
+ );
+ await capture('empty');
+ await check(
+ 'no page error, unspecified/off-origin request, operation or horizontal overflow',
+ async () => {
+ assert.deepEqual(record.errors, []);
+ assert.deepEqual(record.unexpected, []);
+ assert.deepEqual(record.offOrigin, []);
+ assert.deepEqual(record.writes, []);
+ assert.equal(
+ await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth),
+ true,
+ );
+ },
+ );
+ record.completed = true;
+ await context.close();
+ record.contextClosed = true;
+ context = undefined;
+ await browser.close();
+ browser = undefined;
+ }
+ result.completed = true;
+ } catch (error) {
+ result.error = { name: error.name, message: error.message };
+ throw error;
+ } finally {
+ if (context) await context.close();
+ if (browser) await browser.close();
+ result.browsersClosed = true;
+ await server.close();
+ result.serverClosed = true;
+ fs.writeFileSync(output + '/result.json', JSON.stringify(result, null, 2) + '\n');
+ console.log(
+ JSON.stringify({
+ completed: result.completed,
+ phase: result.phase,
+ checks: result.cases.map((c) => ({
+ name: c.name,
+ count: c.checks.length,
+ errors: c.errors,
+ })),
+ captures: result.captures.length,
+ serverClosed: result.serverClosed,
+ }),
+ );
+ }
+})();
diff --git a/frontend-modern/browser-tests/maintenance-evidence.html b/frontend-modern/browser-tests/maintenance-evidence.html
new file mode 100644
index 0000000000..c1a585a96b
--- /dev/null
+++ b/frontend-modern/browser-tests/maintenance-evidence.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Recorded maintenance evidence
+
+
+
+
+
+
diff --git a/frontend-modern/browser-tests/maintenance-evidence.tsx b/frontend-modern/browser-tests/maintenance-evidence.tsx
new file mode 100644
index 0000000000..6ebb688bda
--- /dev/null
+++ b/frontend-modern/browser-tests/maintenance-evidence.tsx
@@ -0,0 +1,53 @@
+// The full production resource drawer, transport/query, shared clock and CSS.
+// Only snapshots and HTTP responses are synthetic. No real operation runs.
+import { createSignal } from 'solid-js';
+import { render } from 'solid-js/web';
+import { ResourceDetailDrawer } from '../src/components/Infrastructure/ResourceDetailDrawer';
+import { DarkModeContext, WebSocketContext } from '../src/contexts/appRuntime';
+import { syncAIRuntimeSettings } from '../src/stores/aiRuntimeState';
+import type { Resource } from '../src/types/resource';
+import '../src/index.css';
+
+syncAIRuntimeSettings({ discovery_enabled: false });
+const [resource, setResource] = createSignal({
+ id: 'pbs-a',
+ name: 'Recorded backup server',
+ displayName: 'Recorded backup server',
+ type: 'pbs',
+ platformId: 'pbs-a',
+ platformType: 'proxmox-pbs',
+ sourceType: 'api',
+ status: 'online',
+ cpu: { current: 12 },
+ lastSeen: Date.parse('2026-10-11T04:00:00Z'),
+});
+(window as any).__maintenanceEvidence = {
+ withdraw: () =>
+ setResource((current) => ({ ...current, id: 'empty-resource', name: 'No reports' })),
+ select: (id: string) =>
+ setResource((current) => ({
+ ...current,
+ id,
+ name: 'Different resource',
+ displayName: 'Different resource',
+ })),
+};
+
+render(
+ () => (
+ true } as any}
+ >
+ document.documentElement.classList.contains('dark')}>
+
+ Recorded maintenance evidence
+
+ Synthetic local responses. No operation reaches a server.
+
+
+
+
+
+ ),
+ document.getElementById('root')!,
+);
diff --git a/frontend-modern/browser-verification/73544eb32505b148.json b/frontend-modern/browser-verification/73544eb32505b148.json
new file mode 100644
index 0000000000..16831e8e69
--- /dev/null
+++ b/frontend-modern/browser-verification/73544eb32505b148.json
@@ -0,0 +1,35 @@
+{
+ "version": 1,
+ "base_sha": "80ef7a2923b5bee25d3aa48ce678f2d4b286725f",
+ "verified_at": "2026-10-11T04:29:14.340458Z",
+ "result": "passed",
+ "changed_paths": [
+ "frontend-modern/src/components/Infrastructure/MaintenanceVerificationSection.tsx"
+ ],
+ "content_sha256": {
+ "frontend-modern/src/components/Infrastructure/MaintenanceVerificationSection.tsx": "af1eac63570ab102272f25fb1ec5cade2ee3508284600a131ff71a9c860efa87"
+ },
+ "routes": ["/browser-tests/maintenance-evidence.html"],
+ "viewports": [
+ {
+ "width": 1365,
+ "height": 900
+ },
+ {
+ "width": 390,
+ "height": 844
+ }
+ ],
+ "states": [
+ "Full production ResourceDetailDrawer Manage, actual report component, query/transport, CSS and shared clock; synthetic local-only snapshots/HTTP. Desktop Chromium 141.0.7390.37/light and phone WebKit 26.0/dark. Playwright 1.56.1 matches integration lock.",
+ "Checked, Window ended and Reviewed remain distinct historical events through minute/hour boundaries; all four verdicts, evidence, operator locks and unsaved note preserved. Go zero-time completion stays unknown.",
+ "Loading, 503 unavailable, 403 denied and successful empty read remain distinct. Explicit keyboard read-only retry; no unsolicited retry or mutation on a tick. The full drawer already remounts at a different ID; opening Manage there shows only its own report state. Component-level held-prop isolation and same-resource failed-refresh retention are separately covered by runtime tests.",
+ "Two completed cases / 16 controls / twelve inspected screenshots, no page error, unspecified/off-origin request, operation or horizontal overflow. No full App, actual authentication, backend review/rerun, appliance, guest recovery, native notification, installed or released acceptance."
+ ],
+ "interactions": [
+ "Advance only the page clock with held reports. Observe stable verdict/counts and saved restrictions; preserve unsaved operator note.",
+ "Synthetic resource selection uses existing keyed drawer reset, then explicitly open Manage. Held GET returns 503, keyboard Retry reports returns 403, another explicit GET returns empty; reports stay absent through ticks.",
+ "All twelve resulting captures visually inspected. Both contexts, browsers and Vite closed; host descendant cleanup is not independently reported. Earlier failed/cancelled/unknown attempts are retained, not rebound or counted here."
+ ],
+ "control_count": 16
+}
diff --git a/frontend-modern/src/components/Infrastructure/MaintenanceVerificationSection.tsx b/frontend-modern/src/components/Infrastructure/MaintenanceVerificationSection.tsx
index 9d3cc78dcb..e1d57f4f88 100644
--- a/frontend-modern/src/components/Infrastructure/MaintenanceVerificationSection.tsx
+++ b/frontend-modern/src/components/Infrastructure/MaintenanceVerificationSection.tsx
@@ -1,6 +1,8 @@
import { Component, For, Show, createMemo, createSignal } from 'solid-js';
import { createNonSuspendingQuery } from '@/hooks/createNonSuspendingQuery';
import { notificationStore } from '@/stores/notifications';
+import { Button } from '@/components/shared/Button';
+import { InlineNotice } from '@/components/shared/InlineNotice';
import {
listMaintenanceVerificationsForResource,
rerunMaintenanceVerification,
@@ -9,6 +11,8 @@ import {
type MaintenanceVerificationStatus,
} from '@/api/maintenanceVerification';
import { formatRelativeTime } from '@/utils/format';
+import { useRelativeTimeNow } from '@/utils/relativeTimeClock';
+import { getAPIReadAccessErrorMessage } from '@/utils/apiAccessError';
/**
* MaintenanceVerificationSection surfaces Maintenance Verification
@@ -50,22 +54,23 @@ const STATUS_CLASSES: Record = {
export const MaintenanceVerificationSection: Component = (
props,
) => {
- const [refreshTick, setRefreshTick] = createSignal(0);
-
+ const now = useRelativeTimeNow();
+ // Age only the recorded events. Ticks neither refresh the report nor change
+ // its verdict, and a rerun's completion time is not the window's end time.
+ const reportAge = (timestamp?: string) => {
+ if (!timestamp || timestamp.startsWith('0001-01-01')) return 'unknown';
+ return formatRelativeTime(timestamp, { now: now() }) || 'unknown';
+ };
const query = createNonSuspendingQuery({
- source: () => (props.resourceId ? `${props.resourceId}:${refreshTick()}` : null),
- fetcher: async () => {
- try {
- const res = await listMaintenanceVerificationsForResource(props.resourceId);
- return res.data ?? [];
- } catch (err) {
- notificationStore.error(
- err instanceof Error ? err.message : 'Failed to load Maintenance Verification Reports',
- );
- return [];
- }
+ source: () => props.resourceId || null,
+ fetcher: async (resourceId) => {
+ const res = await listMaintenanceVerificationsForResource(resourceId);
+ return res.data ?? [];
},
initialValue: [] as MaintenanceVerificationReport[],
+ // A held report belongs only to its resource. The query withdraws denied
+ // reads, but retains last-loaded records on a same-resource transient error.
+ retainPreviousValueOnSourceChange: false,
cacheKey: (key: string) => `maintenance-verifications:${key}`,
});
@@ -75,7 +80,7 @@ export const MaintenanceVerificationSection: Component(null);
- const refresh = () => setRefreshTick((n) => n + 1);
+ const refresh = () => void query.refetch();
const handleRerun = async () => {
if (rerunning() || !props.resourceId) return;
@@ -121,8 +126,8 @@ export const MaintenanceVerificationSection: Component
- Pulse runs deterministic checks each time a maintenance window ends and writes a
- Maintenance Verification Report. The result sticks here for review.
+ Pulse records the result when a maintenance window ends. Reports record past checks, not
+ current health.
+
+
+ Verification reports unavailable
+
+ {getAPIReadAccessErrorMessage(query.error()) ?? 'Could not load verification reports.'}
+
+
+ Last loaded reports remain below.
+
+ {
+ if (!query.loading()) refresh();
+ }}
+ >
+ Retry reports
+
+
+
+
- No verification reports yet. A report is written automatically the next time this
- resource exits a maintenance window.
-
+
+
+ {query.loading()
+ ? 'Loading verification reports…'
+ : 'No verification reports yet. A report is written automatically the next time this resource exits a maintenance window.'}
+
+
}
>
@@ -160,9 +199,9 @@ export const MaintenanceVerificationSection: Component
{STATUS_LABELS[report.status]}
+ Checked {reportAge(report.completedAt)}
- Window ended{' '}
- {report.windowEndedAt ? formatRelativeTime(report.windowEndedAt) : 'unknown'}
+ Window ended {reportAge(report.windowEndedAt)}
@@ -178,7 +217,7 @@ export const MaintenanceVerificationSection: Component
- Reviewed {report.reviewedAt ? formatRelativeTime(report.reviewedAt) : ''}
+ Reviewed {reportAge(report.reviewedAt)}
{report.reviewedBy ? ` by ${report.reviewedBy}` : ''}
diff --git a/frontend-modern/src/components/Infrastructure/__tests__/MaintenanceVerificationSection.render.test.tsx b/frontend-modern/src/components/Infrastructure/__tests__/MaintenanceVerificationSection.render.test.tsx
new file mode 100644
index 0000000000..196d06212b
--- /dev/null
+++ b/frontend-modern/src/components/Infrastructure/__tests__/MaintenanceVerificationSection.render.test.tsx
@@ -0,0 +1,407 @@
+import { cleanup, fireEvent, render, screen, within } from '@solidjs/testing-library';
+import { createSignal } from 'solid-js';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import type { MaintenanceVerificationReport } from '@/api/maintenanceVerification';
+import { resetCreateNonSuspendingQueryCacheForTest } from '@/hooks/createNonSuspendingQuery';
+import { RELATIVE_TIME_TICK_MS } from '@/utils/relativeTimeClock';
+
+const api = vi.hoisted(() => ({ apiFetchJSON: vi.fn() }));
+vi.mock('@/utils/apiClient', async (importOriginal) => ({
+ ...(await importOriginal()),
+ apiFetchJSON: api.apiFetchJSON,
+}));
+vi.mock('@/stores/notifications', () => ({
+ notificationStore: { error: vi.fn(), success: vi.fn() },
+}));
+
+import { MaintenanceVerificationSection } from '../MaintenanceVerificationSection';
+
+const start = Date.parse('2026-10-11T04:00:00Z');
+const iso = (offset: number) => new Date(start + offset).toISOString();
+const resourceId = 'cluster:pve-a:101';
+const report = (
+ overrides: Partial = {},
+): MaintenanceVerificationReport => ({
+ id: 'report:window',
+ resourceId,
+ trigger: 'maintenance_window_ended',
+ status: 'healthy',
+ startedAt: iso(-30_000),
+ completedAt: iso(-30_000),
+ windowStartedAt: iso(-3_600_000),
+ windowEndedAt: iso(-30_000),
+ evidence: {
+ activeCriticalAlerts: 0,
+ activeWarningAlerts: 2,
+ activeCriticalFindings: 0,
+ activeWarningFindings: 1,
+ failedActionsSinceWindowStart: 0,
+ metricRecovery: { samplesAfterEnd: 3, trend: 'improving' },
+ },
+ linkedFindingIds: [],
+ linkedAlertIds: [],
+ linkedActionIds: [],
+ recommendation: 'Recorded recovery evidence',
+ ...overrides,
+});
+const response = (reports: MaintenanceVerificationReport[]) => ({ data: reports });
+const mount = async (reports: MaintenanceVerificationReport[]) => {
+ api.apiFetchJSON.mockResolvedValue(response(reports));
+ const rendered = render(() => );
+ await vi.advanceTimersByTimeAsync(0);
+ return rendered;
+};
+const mutations = () =>
+ api.apiFetchJSON.mock.calls.filter(([, options]) => options?.method === 'POST');
+
+describe('durable maintenance verification evidence', () => {
+ beforeEach(() => {
+ vi.useFakeTimers();
+ resetCreateNonSuspendingQueryCacheForTest();
+ vi.setSystemTime(start);
+ api.apiFetchJSON.mockReset();
+ });
+ afterEach(() => {
+ cleanup();
+ resetCreateNonSuspendingQueryCacheForTest();
+ vi.useRealTimers();
+ });
+
+ it('ages held window and review events without fetching, rewriting or rerunning the report', async () => {
+ const saved = report({
+ userOutcome: 'reviewed',
+ reviewedAt: iso(-30_000),
+ reviewedBy: 'fixture-admin',
+ });
+ const original = JSON.stringify(saved);
+ await mount([saved]);
+ expect(screen.getByText('Window ended 30s ago')).toBeInTheDocument();
+ expect(screen.getByText('Reviewed 30s ago by fixture-admin')).toBeInTheDocument();
+ await vi.advanceTimersByTimeAsync(RELATIVE_TIME_TICK_MS);
+ expect(screen.getByText('Window ended 1 min ago')).toBeInTheDocument();
+ expect(screen.getByText('Reviewed 1 min ago by fixture-admin')).toBeInTheDocument();
+ await vi.advanceTimersByTimeAsync(3_600_000);
+ expect(screen.getByText('Window ended 1 hour ago')).toBeInTheDocument();
+ expect(screen.getByText('Reviewed 1 hour ago by fixture-admin')).toBeInTheDocument();
+ expect(screen.getByTestId('maintenance-verification-status-healthy')).toHaveTextContent(
+ 'Healthy',
+ );
+ expect(screen.queryByTestId('maintenance-verification-review')).not.toBeInTheDocument();
+ expect(api.apiFetchJSON).toHaveBeenCalledTimes(1);
+ expect(mutations()).toEqual([]);
+ expect(JSON.stringify(saved)).toBe(original);
+ });
+
+ it('shows when a check completed, separately from the earlier maintenance window and review', async () => {
+ await mount([
+ report({
+ windowEndedAt: iso(-86_400_000),
+ userOutcome: 'reviewed',
+ reviewedAt: iso(-10_000),
+ }),
+ ]);
+ expect(screen.getByText('Checked 30s ago')).toBeInTheDocument();
+ expect(screen.getByText('Window ended 1 day ago')).toBeInTheDocument();
+ expect(screen.getByText('Reviewed 10s ago')).toBeInTheDocument();
+ expect(
+ screen.getByText(/Reports record past checks, not current health\./),
+ ).toBeInTheDocument();
+ await vi.advanceTimersByTimeAsync(60_000);
+ expect(screen.getByText('Checked 1 min ago')).toBeInTheDocument();
+ expect(screen.getByText('Reviewed 1 min ago')).toBeInTheDocument();
+ expect(api.apiFetchJSON).toHaveBeenCalledTimes(1);
+ });
+
+ it('keeps every recorded verdict and its counts unchanged as several reports age', async () => {
+ const states = ['pending', 'healthy', 'needs_review', 'failed_verification'] as const;
+ const records = states.map((status) => report({ id: `report:${status}`, status }));
+ const original = JSON.stringify(records);
+ await mount(records);
+ await vi.advanceTimersByTimeAsync(120_000);
+ for (const status of states) {
+ const row = screen.getByTestId(`maintenance-verification-status-${status}`).closest('li')!;
+ expect(within(row).getByText('Window ended 2 mins ago')).toBeInTheDocument();
+ expect(within(row).getByText('Checked 2 mins ago')).toBeInTheDocument();
+ expect(within(row).getByText('Warning alerts').nextElementSibling).toHaveTextContent('2');
+ expect(within(row).getByText('Metric samples').nextElementSibling).toHaveTextContent('3');
+ expect(within(row).getByTestId('maintenance-verification-review')).toBeEnabled();
+ }
+ expect(api.apiFetchJSON).toHaveBeenCalledTimes(1);
+ expect(JSON.stringify(records)).toBe(original);
+ });
+
+ it.each(['', 'invalid', '0001-01-01T00:00:00Z'])(
+ 'keeps absent or malformed event times unknown (%j)',
+ async (time) => {
+ await mount([
+ report({
+ windowEndedAt: time,
+ completedAt: time,
+ userOutcome: 'reviewed',
+ reviewedAt: time,
+ }),
+ ]);
+ await vi.advanceTimersByTimeAsync(60_000);
+ expect(screen.getByText('Window ended unknown')).toBeInTheDocument();
+ expect(screen.getByText('Checked unknown')).toBeInTheDocument();
+ expect(screen.getByText('Reviewed unknown')).toBeInTheDocument();
+ expect(api.apiFetchJSON).toHaveBeenCalledTimes(1);
+ },
+ );
+
+ it('keeps clock-skewed past events as ages rather than inventing a scheduled operation', async () => {
+ await mount([report({ completedAt: iso(30_000), windowEndedAt: iso(30_000) })]);
+ expect(screen.getByText('Checked 0s ago')).toBeInTheDocument();
+ await vi.advanceTimersByTimeAsync(90_000);
+ expect(screen.getByText('Checked 1 min ago')).toBeInTheDocument();
+ expect(screen.getByText('Window ended 1 min ago')).toBeInTheDocument();
+ expect(mutations()).toEqual([]);
+ });
+
+ it('withdraws the old resource reports and ignores a late old-resource response across ticks', async () => {
+ let finishOld!: (value: ReturnType) => void;
+ api.apiFetchJSON.mockImplementation((path: string) =>
+ path.includes('old-resource')
+ ? new Promise((resolve) => {
+ finishOld = resolve;
+ })
+ : Promise.resolve(response([])),
+ );
+ const [id, setId] = createSignal('old-resource');
+ render(() => );
+ await vi.advanceTimersByTimeAsync(0);
+ setId('empty-resource');
+ await vi.advanceTimersByTimeAsync(0);
+ finishOld(response([report()]));
+ await vi.advanceTimersByTimeAsync(60_000);
+ expect(screen.queryByTestId('maintenance-verification-report')).not.toBeInTheDocument();
+ expect(screen.getByText(/No verification reports yet/)).toBeInTheDocument();
+ expect(api.apiFetchJSON).toHaveBeenCalledTimes(2);
+ });
+
+ it('does not revive withdrawn reports after an access-denied resource read', async () => {
+ api.apiFetchJSON
+ .mockResolvedValueOnce(response([report()]))
+ .mockRejectedValueOnce(new Error('Access denied'));
+ const [id, setId] = createSignal(resourceId);
+ render(() => );
+ await vi.advanceTimersByTimeAsync(0);
+ expect(screen.getByTestId('maintenance-verification-report')).toBeInTheDocument();
+ setId('denied-resource');
+ await vi.advanceTimersByTimeAsync(0);
+ await vi.advanceTimersByTimeAsync(60_000);
+ expect(screen.queryByTestId('maintenance-verification-report')).not.toBeInTheDocument();
+ expect(api.apiFetchJSON).toHaveBeenCalledTimes(2);
+ });
+
+ it('requires an explicit review, keeps pending admission disabled and uses the returned review fields', async () => {
+ const saved = report();
+ await mount([saved]);
+ let finishReview!: (value: MaintenanceVerificationReport) => void;
+ api.apiFetchJSON.mockImplementationOnce(
+ () =>
+ new Promise((resolve) => {
+ finishReview = resolve;
+ }),
+ );
+ const button = screen.getByTestId('maintenance-verification-review');
+ fireEvent.click(button);
+ await vi.advanceTimersByTimeAsync(60_000);
+ expect(button).toBeDisabled();
+ expect(button).toHaveTextContent('Marking…');
+ fireEvent.click(button);
+ expect(mutations()).toHaveLength(1);
+ expect(mutations()[0][0]).toBe('/api/maintenance-verifications/report%3Awindow/review');
+ expect(screen.getByTestId('maintenance-verification-status-healthy')).toHaveTextContent(
+ 'Healthy',
+ );
+ expect(screen.queryByText(/Reviewed/)).not.toBeInTheDocument();
+ const reviewed = {
+ ...saved,
+ userOutcome: 'reviewed' as const,
+ reviewedAt: iso(60_000),
+ reviewedBy: 'fixture-admin',
+ };
+ api.apiFetchJSON.mockResolvedValue(response([reviewed]));
+ finishReview(reviewed);
+ await vi.advanceTimersByTimeAsync(0);
+ expect(screen.queryByTestId('maintenance-verification-review')).not.toBeInTheDocument();
+ expect(screen.getByText('Reviewed 0s ago by fixture-admin')).toBeInTheDocument();
+ await vi.advanceTimersByTimeAsync(60_000);
+ expect(screen.getByText('Reviewed 1 min ago by fixture-admin')).toBeInTheDocument();
+ expect(api.apiFetchJSON).toHaveBeenCalledTimes(3);
+ });
+
+ it('reruns only after a click, keeps the pending action disabled and retains the old report beside the new check', async () => {
+ const saved = report();
+ await mount([saved]);
+ let finishRerun!: (value: MaintenanceVerificationReport) => void;
+ api.apiFetchJSON.mockImplementationOnce(
+ () =>
+ new Promise((resolve) => {
+ finishRerun = resolve;
+ }),
+ );
+ const button = screen.getByTestId('maintenance-verification-rerun');
+ fireEvent.click(button);
+ await vi.advanceTimersByTimeAsync(60_000);
+ expect(button).toBeDisabled();
+ fireEvent.click(button);
+ expect(mutations()).toHaveLength(1);
+ expect(mutations()[0][0]).toBe(
+ '/api/resources/cluster%3Apve-a%3A101/maintenance-verifications/rerun',
+ );
+ const rerun = report({
+ id: 'report:window-rerun-1',
+ completedAt: iso(60_000),
+ status: 'needs_review',
+ });
+ api.apiFetchJSON.mockResolvedValue(response([rerun, saved]));
+ finishRerun(rerun);
+ await vi.advanceTimersByTimeAsync(0);
+ expect(button).toBeEnabled();
+ expect(screen.getAllByTestId('maintenance-verification-report')).toHaveLength(2);
+ expect(screen.getByText('Checked 0s ago')).toBeInTheDocument();
+ expect(screen.getByTestId('maintenance-verification-status-healthy')).toHaveTextContent(
+ 'Healthy',
+ );
+ expect(screen.getByTestId('maintenance-verification-status-needs_review')).toHaveTextContent(
+ 'Needs review',
+ );
+ await vi.advanceTimersByTimeAsync(60_000);
+ expect(mutations()).toHaveLength(1);
+ expect(api.apiFetchJSON).toHaveBeenCalledTimes(3);
+ });
+
+ it.each(['review', 'rerun'])(
+ 'does not retry a refused %s on a clock tick or alter the report',
+ async (action) => {
+ await mount([report()]);
+ api.apiFetchJSON.mockRejectedValueOnce(new Error('Synthetic write refusal'));
+ fireEvent.click(screen.getByTestId(`maintenance-verification-${action}`));
+ await vi.advanceTimersByTimeAsync(60_000);
+ expect(screen.getByTestId(`maintenance-verification-${action}`)).toBeEnabled();
+ expect(screen.getByTestId('maintenance-verification-status-healthy')).toHaveTextContent(
+ 'Healthy',
+ );
+ expect(screen.getByText('Window ended 1 min ago')).toBeInTheDocument();
+ expect(mutations()).toHaveLength(1);
+ expect(api.apiFetchJSON).toHaveBeenCalledTimes(2);
+ },
+ );
+
+ it('does not describe a pending first read as an empty report history', async () => {
+ let finishRead!: (value: ReturnType) => void;
+ api.apiFetchJSON.mockImplementationOnce(
+ () =>
+ new Promise((resolve) => {
+ finishRead = resolve;
+ }),
+ );
+ render(() => );
+ await vi.advanceTimersByTimeAsync(0);
+ expect(screen.getByText('Loading verification reports…')).toBeInTheDocument();
+ expect(screen.queryByText(/No verification reports yet/)).not.toBeInTheDocument();
+ finishRead(response([]));
+ await vi.advanceTimersByTimeAsync(0);
+ expect(screen.getByText(/No verification reports yet/)).toBeInTheDocument();
+ });
+
+ it('withdraws loaded reports immediately while another resource read is pending', async () => {
+ api.apiFetchJSON
+ .mockResolvedValueOnce(response([report()]))
+ .mockImplementationOnce(() => new Promise(() => {}));
+ const [id, setId] = createSignal(resourceId);
+ render(() => );
+ await vi.advanceTimersByTimeAsync(0);
+ expect(screen.getByTestId('maintenance-verification-report')).toBeInTheDocument();
+ setId('new-resource-pending');
+ await vi.advanceTimersByTimeAsync(60_000);
+ expect(screen.queryByTestId('maintenance-verification-report')).not.toBeInTheDocument();
+ expect(screen.getByText('Loading verification reports…')).toBeInTheDocument();
+ expect(api.apiFetchJSON).toHaveBeenCalledTimes(2);
+ });
+
+ it('distinguishes a failed first read from empty history and retries only after an explicit read request', async () => {
+ api.apiFetchJSON.mockRejectedValueOnce(
+ Object.assign(new Error('Synthetic server internals must not render'), { status: 503 }),
+ );
+ render(() => );
+ await vi.advanceTimersByTimeAsync(0);
+ expect(screen.getByTestId('maintenance-verification-load-error')).toHaveTextContent(
+ 'Verification reports unavailable',
+ );
+ expect(screen.queryByText(/No verification reports yet/)).not.toBeInTheDocument();
+ expect(screen.queryByText(/Synthetic server internals/)).not.toBeInTheDocument();
+ await vi.advanceTimersByTimeAsync(60_000);
+ expect(api.apiFetchJSON).toHaveBeenCalledTimes(1);
+ let finishRetry!: (value: ReturnType) => void;
+ api.apiFetchJSON.mockImplementationOnce(
+ () =>
+ new Promise((resolve) => {
+ finishRetry = resolve;
+ }),
+ );
+ fireEvent.click(screen.getByRole('button', { name: 'Retry reports' }));
+ expect(screen.getByRole('button', { name: 'Retry reports' })).toBeDisabled();
+ await vi.advanceTimersByTimeAsync(60_000);
+ expect(api.apiFetchJSON).toHaveBeenCalledTimes(2);
+ finishRetry(response([report()]));
+ await vi.advanceTimersByTimeAsync(0);
+ expect(screen.queryByTestId('maintenance-verification-load-error')).not.toBeInTheDocument();
+ expect(screen.getByTestId('maintenance-verification-report')).toBeInTheDocument();
+ expect(mutations()).toEqual([]);
+ });
+
+ it('retains qualified historical reports after a same-resource refresh fails, without inventing a successful review', async () => {
+ const saved = report();
+ await mount([saved]);
+ api.apiFetchJSON
+ .mockResolvedValueOnce(saved)
+ .mockRejectedValueOnce(Object.assign(new Error('Synthetic read failure'), { status: 503 }));
+ fireEvent.click(screen.getByTestId('maintenance-verification-review'));
+ await vi.advanceTimersByTimeAsync(60_000);
+ expect(screen.getByTestId('maintenance-verification-load-error')).toHaveTextContent(
+ 'Last loaded reports remain below.',
+ );
+ expect(screen.getByTestId('maintenance-verification-status-healthy')).toHaveTextContent(
+ 'Healthy',
+ );
+ expect(screen.getByText('Window ended 1 min ago')).toBeInTheDocument();
+ expect(screen.queryByText(/Reviewed/)).not.toBeInTheDocument();
+ expect(mutations()).toHaveLength(1);
+ expect(api.apiFetchJSON).toHaveBeenCalledTimes(3);
+ });
+
+ it.each([401, 403])(
+ 'withdraws reports on a denied same-resource refresh (%s), rather than offering them as authorised history',
+ async (status) => {
+ const saved = report();
+ await mount([saved]);
+ api.apiFetchJSON
+ .mockResolvedValueOnce(saved)
+ .mockRejectedValueOnce(
+ Object.assign(new Error('Synthetic access internals must not render'), { status }),
+ );
+ fireEvent.click(screen.getByTestId('maintenance-verification-rerun'));
+ await vi.advanceTimersByTimeAsync(60_000);
+ expect(screen.queryByTestId('maintenance-verification-report')).not.toBeInTheDocument();
+ expect(screen.queryByText(/No verification reports yet/)).not.toBeInTheDocument();
+ expect(screen.getByTestId('maintenance-verification-load-error')).toHaveTextContent(
+ status === 401
+ ? 'Sign in again or check your API token.'
+ : 'Access denied. Check your permissions and license plan.',
+ );
+ expect(screen.queryByText(/Synthetic access internals/)).not.toBeInTheDocument();
+ expect(api.apiFetchJSON).toHaveBeenCalledTimes(3);
+ const reviewed = report({ userOutcome: 'reviewed', reviewedAt: iso(60_000) });
+ api.apiFetchJSON.mockResolvedValueOnce(response([reviewed]));
+ fireEvent.click(screen.getByRole('button', { name: 'Retry reports' }));
+ await vi.advanceTimersByTimeAsync(0);
+ expect(screen.getByText('Reviewed 0s ago')).toBeInTheDocument();
+ expect(screen.queryByTestId('maintenance-verification-load-error')).not.toBeInTheDocument();
+ expect(api.apiFetchJSON).toHaveBeenCalledTimes(4);
+ expect(mutations()).toHaveLength(1);
+ },
+ );
+});