Add host temperature telemetry and history

This commit is contained in:
2026-10-13 23:39:04 -04:00
parent 09396ab584
commit 171f657876
28 changed files with 1466 additions and 32 deletions
+110
View File
@@ -0,0 +1,110 @@
import { afterEach, expect, it } from 'vitest';
import { flushSync, mount, unmount } from 'svelte';
import TemperatureChart from '../../src/lib/ui/components/capacity/TemperatureChart.svelte';
import type { CapacityHistory } from '../../src/lib/capacity.js';
let component: ReturnType<typeof mount> | undefined;
afterEach(async () => {
if (component) await unmount(component);
component = undefined;
document.body.replaceChildren();
});
function history(): CapacityHistory {
return {
range: '1h',
startAt: 1000,
endAt: 4000,
bucketMs: 1000,
points: [1000, 2000, 3000].map((at) => ({
at,
cpuCoresAvailable: null,
memoryBytesAvailable: null,
reportingHosts: 0,
expectedHosts: 1
})),
temperatures: [
{
id: 'host:cpu',
hostId: 'host',
label: 'CPU temperature',
kind: 'cpu',
points: [65, null, 67].map((celsius, i) => ({ at: 1000 + i * 1000, celsius }))
},
{
id: 'host:drive',
hostId: 'host',
label: 'NVMe drive 1',
kind: 'nvme',
points: [49, 50.9, 52].map((celsius, i) => ({ at: 1000 + i * 1000, celsius }))
}
]
};
}
function show(report = history()) {
component = mount(TemperatureChart, {
target: document.body,
props: { history: report, scope: 'ec-2' }
});
flushSync();
}
it('defaults to CPU and toggles device lines and readouts together', () => {
show();
const buttons = document.querySelectorAll<HTMLButtonElement>('.legend button');
expect(buttons[0].getAttribute('aria-pressed')).toBe('true');
expect(buttons[1].getAttribute('aria-pressed')).toBe('false');
expect(document.querySelectorAll('[data-series]')).toHaveLength(1);
// A missing middle bucket splits the CPU line into two isolated segments.
expect(document.querySelectorAll('[data-series="host:cpu"] path')).toHaveLength(2);
buttons[1].click();
flushSync();
expect(document.querySelectorAll('[data-series]')).toHaveLength(2);
expect(document.querySelector('.readout')?.textContent).toContain('52 °C');
buttons[0].click();
buttons[1].click();
flushSync();
expect(document.querySelectorAll('[data-series]')).toHaveLength(0);
expect(document.body.textContent).toContain('Select a sensor to show history');
expect(document.querySelector('.readout')).toBeNull();
});
it('supports keyboard and pointer inspection without turning missing readings into zero', () => {
show();
const plot = document.querySelector<HTMLElement>('[role="slider"]')!;
expect(plot.getAttribute('aria-valuenow')).toBe('2');
plot.dispatchEvent(new KeyboardEvent('keydown', { key: 'Home', bubbles: true }));
flushSync();
expect(plot.getAttribute('aria-valuetext')).toContain('65 °C');
plot.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }));
flushSync();
expect(plot.getAttribute('aria-valuetext')).toContain('Unavailable');
expect(document.querySelector('.readout')?.textContent).not.toContain('0 °C');
Object.defineProperty(plot, 'getBoundingClientRect', {
value: () => ({ left: 0, width: 1000 })
});
plot.dispatchEvent(
new PointerEvent('pointerdown', { clientX: 8, pointerType: 'touch', bubbles: true })
);
flushSync();
expect(plot.getAttribute('aria-valuenow')).toBe('0');
expect(document.querySelector('.readout')?.textContent).toContain('65 °C');
});
it('shows unsupported hosts as unavailable', () => {
const report = history();
report.temperatures[0].points = report.temperatures[0].points.map((point) => ({
...point,
celsius: null
}));
show(report);
expect(document.body.textContent).toContain('No temperature readings in this period');
expect(document.body.textContent).toContain('Unavailable');
});
it('plots a genuine zero-degree reading', () => {
const report = history();
report.temperatures[0].points[2].celsius = 0;
show(report);
expect(document.querySelector('.readout')?.textContent).toContain('0 °C');
expect(document.querySelector('.empty-chart')).toBeNull();
});