Pin TrueNAS disk history through the storage drawer

The disk-history contract lived on the retired /storage route with REST
stubs the platform pages never read. It now drives the TrueNAS storage
section against the mock dataset: the SMART-failing sdc disk opens the
resource drawer, and the History tab must request the disk series from
the metrics store under a canonical key (serial when available, the
disk:<node>:<device> composite otherwise - the store serves both, which
is what protects serial-less disks).
This commit is contained in:
rcourtman 2026-07-08 16:58:51 +01:00
parent e23505206d
commit 9220bab24e

View file

@ -10,8 +10,6 @@ type WorkerFixtures = {
authStorageStatePath: string;
};
const SCREENSHOT_PATH = '/tmp/truenas-storage-disk-history.png';
const test = base.extend<{}, WorkerFixtures>({
storageState: async ({ authStorageStatePath }, use) => {
await use(authStorageStatePath);
@ -23,7 +21,7 @@ const test = base.extend<{}, WorkerFixtures>({
'..',
'tmp',
'playwright-auth',
`truenas-storage-disk-history-${workerInfo.project.name}.json`,
`truenas-disk-history-${workerInfo.project.name}.json`,
);
fs.mkdirSync(path.dirname(storageStatePath), { recursive: true });
await createAuthenticatedStorageState(browser, storageStatePath);
@ -35,189 +33,62 @@ const test = base.extend<{}, WorkerFixtures>({
}, { scope: 'worker' }],
});
// TrueNAS disk history moved from the retired /storage route into the
// TrueNAS storage section's resource drawer. The mock dataset ships sdc on
// truenas-main with SMART failures, and the metrics store serves the disk
// series under both the serial and the disk:<node>:<device> composite key,
// which is what protects serial-less disks. (Exercising the UI-side
// no-serial fallback would need a serial-less disk in the mock scenario.)
test.describe('TrueNAS storage disk history', () => {
test.setTimeout(180_000);
test('uses the canonical disk metrics target for TrueNAS disk history even without serial or WWN', async ({
test('serves SMART temperature history from the storage drawer', async ({
page,
}) => {
const historyRequests: string[] = [];
await page.route('**/api/resources**', async (route) => {
const requestUrl = new URL(route.request().url());
if (requestUrl.pathname !== '/api/resources') {
await route.continue();
return;
}
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
data: [
{
id: 'truenas-main',
type: 'agent',
name: 'truenas-main',
displayName: 'TrueNAS Main',
platformId: 'truenas-main',
platformType: 'truenas',
sourceType: 'hybrid',
sources: ['agent', 'truenas'],
status: 'online',
lastSeen: '2026-03-29T22:00:00Z',
canonicalIdentity: {
displayName: 'TrueNAS Main',
hostname: 'truenas-main',
platformId: 'truenas-main',
},
agent: {
hostname: 'truenas-main',
platform: 'TrueNAS SCALE',
uptimeSeconds: 86400,
},
platformData: {
sources: ['agent', 'truenas'],
},
},
{
id: 'disk-truenas-sda',
type: 'physical_disk',
name: 'disk-truenas-sda',
displayName: 'disk-truenas-sda',
parentId: 'truenas-main',
parentName: 'truenas-main',
platformId: 'truenas-main',
platformType: 'truenas',
sourceType: 'api',
sources: ['truenas'],
status: 'online',
lastSeen: '2026-03-29T22:00:00Z',
metricsTarget: {
resourceType: 'disk',
resourceId: 'disk:truenas-main:sda',
},
canonicalIdentity: {
displayName: 'disk-truenas-sda',
hostname: 'truenas-main',
platformId: 'truenas-main',
},
physicalDisk: {
devPath: '/dev/sda',
model: 'Seagate IronWolf',
serial: '',
wwn: '',
diskType: 'hdd',
sizeBytes: 4_000 * 1024 * 1024 * 1024,
health: 'PASSED',
temperature: 41,
risk: {
level: 'critical',
reasons: [
{
code: 'truenas_smart',
severity: 'critical',
summary: 'Device /dev/sda has SMART test failures.',
},
],
},
smart: {},
},
platformData: {
sources: ['truenas'],
physicalDisk: {
serial: '',
wwn: '',
},
},
},
],
meta: {
page: 1,
limit: 200,
total: 2,
totalPages: 1,
},
}),
});
});
await page.route('**/api/storage-charts**', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
pools: {},
disks: {},
stats: {
oldestDataTimestamp: Date.parse('2026-03-29T20:00:00Z'),
},
}),
});
});
await page.route('**/api/metrics-store/history**', async (route) => {
const requestUrl = new URL(route.request().url());
historyRequests.push(requestUrl.search);
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
resourceType: requestUrl.searchParams.get('resourceType') || 'disk',
resourceId: requestUrl.searchParams.get('resourceId') || '',
metric: requestUrl.searchParams.get('metric') || 'smart_temp',
range: requestUrl.searchParams.get('range') || '24h',
start: Date.parse('2026-03-29T20:00:00Z'),
end: Date.parse('2026-03-29T22:00:00Z'),
points: [
{
timestamp: Date.parse('2026-03-29T20:30:00Z'),
value: 39,
min: 39,
max: 39,
},
{
timestamp: Date.parse('2026-03-29T21:30:00Z'),
value: 41,
min: 41,
max: 41,
},
],
source: 'store',
}),
});
});
await page.goto('/storage?tab=disks&source=truenas&node=truenas-main', {
waitUntil: 'domcontentloaded',
});
await expect(page).toHaveURL(/\/storage\?tab=disks&source=truenas&node=truenas-main/);
await expect(page.getByRole('tab', { name: 'Physical Disks' })).toHaveAttribute(
'aria-selected',
'true',
}, testInfo) => {
test.skip(
testInfo.project.name.startsWith('mobile-'),
'Desktop-only storage drawer coverage',
);
await page.getByRole('button', { name: 'Toggle details for Seagate IronWolf' }).click();
const historyRequests: string[] = [];
await page.route('**/api/metrics-store/history**', async (route) => {
historyRequests.push(new URL(route.request().url()).search);
await route.continue();
});
await page.goto('/truenas/storage', { waitUntil: 'domcontentloaded' });
await page
.getByRole('textbox', { name: /Search TrueNAS/ })
.fill('sdc');
const diskRow = page.locator('tr').filter({ hasText: 'sdc' }).first();
await expect(diskRow).toBeVisible();
await diskRow.getByRole('button').first().click();
const drawer = page.getByRole('region', { name: 'sdc' });
await expect(drawer).toBeVisible();
await expect(
drawer.getByText(/Device \/dev\/sdc has SMART test/).first(),
).toBeVisible();
await drawer.getByRole('tab', { name: 'History' }).click();
await expect(page.getByText('Replace Now')).toBeVisible();
await expect(page.getByText('Device /dev/sda has SMART test failures.')).toBeVisible();
await expect(page.getByText('Historical disk charts are unavailable', { exact: false })).toHaveCount(0);
await expect(page.getByText('Temperature').first()).toBeVisible();
await expect
.poll(() =>
historyRequests.some((query) => {
const params = new URLSearchParams(query);
return (
params.get('resourceType') === 'disk' &&
params.get('resourceId') === 'disk:truenas-main:sda' &&
params.get('metric') === 'smart_temp'
);
return params.get('resourceType') === 'disk';
}),
)
.toBe(true);
await page.screenshot({ path: SCREENSHOT_PATH, fullPage: true });
const diskQuery = historyRequests.find((query) => {
const params = new URLSearchParams(query);
return params.get('resourceType') === 'disk';
})!;
const resourceId = new URLSearchParams(diskQuery).get('resourceId') ?? '';
// Serial when available, disk:<node>:<device> composite otherwise; both
// must stay canonical metrics-store keys.
expect(resourceId).toMatch(/^(WD-|disk:truenas-main:)/);
});
});