Files
vliaudatandClaude Opus 5 ea945b50d6 feat: add the dashboard
Current state at the top, the panel as it stands beside what is known
about the device, the next seven days resolved with their exceptions,
and alerts when something needs attention.

There is no "push now" button, because in BYOS there is nothing to push:
the panel sleeps on battery and fetches when it wakes. Pretending
otherwise would be the most misleading control on the page. It says so
in plain words instead, and shows when the device is next due back.

Everything about the device is inference from one timestamp and one
interval, so it lives in a tested module rather than in a template. A
panel is only called overdue once it has missed its slot by half its
interval again: firmware wake-ups drift, and an alert that cries wolf
every cycle is an alert nobody reads. Battery falls back to a voltage
estimate, labelled as an estimate, because "about a third left" is the
only thing anyone acts on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd
2026-09-20 21:18:41 +02:00

110 lines
4.2 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from 'vitest';
import { deviceHealth, humaniseSeconds, OVERDUE_FACTOR, type DeviceSnapshot } from './health';
const NOW = new Date('2026-09-22T12:00:00Z');
function snapshot(overrides: Partial<DeviceSnapshot> = {}): DeviceSnapshot {
return {
lastSeenAt: new Date('2026-09-22T11:50:00Z'),
lastRefreshRate: 600,
batteryVoltage: null,
percentCharged: null,
rssi: null,
...overrides,
};
}
describe('deviceHealth', () => {
it('reports a device that has never called', () => {
const health = deviceHealth(snapshot({ lastSeenAt: null }), NOW);
expect(health.neverSeen).toBe(true);
expect(health.nextWakeAt).toBeNull();
expect(health.isOverdue).toBe(false);
});
it('predicts the next wake from the interval the device was given', () => {
const health = deviceHealth(snapshot(), NOW);
expect(health.nextWakeAt?.toISOString()).toBe('2026-09-22T12:00:00.000Z');
expect(health.secondsUntilWake).toBe(0);
});
it('counts down to the next wake', () => {
const health = deviceHealth(snapshot({ lastSeenAt: new Date('2026-09-22T11:55:00Z') }), NOW);
expect(health.secondsUntilWake).toBe(300);
expect(health.isOverdue).toBe(false);
});
it('tolerates a late wake before calling it overdue', () => {
// Firmware wake-ups drift; an alert that cries wolf every cycle is unread.
const justLate = new Date(NOW.getTime() - 600 * 1.2 * 1000);
expect(deviceHealth(snapshot({ lastSeenAt: justLate }), NOW).isOverdue).toBe(false);
});
it('calls a device overdue once it misses by half its interval again', () => {
const wellLate = new Date(NOW.getTime() - 600 * (OVERDUE_FACTOR + 0.1) * 1000);
const health = deviceHealth(snapshot({ lastSeenAt: wellLate }), NOW);
expect(health.isOverdue).toBe(true);
expect(health.secondsUntilWake).toBeLessThan(0);
});
it('says nothing about lateness with no interval to go on', () => {
const health = deviceHealth(snapshot({ lastRefreshRate: null }), NOW);
expect(health.nextWakeAt).toBeNull();
expect(health.isOverdue).toBe(false);
});
it('ignores a nonsensical interval', () => {
expect(deviceHealth(snapshot({ lastRefreshRate: 0 }), NOW).nextWakeAt).toBeNull();
});
it('prefers the reported battery percentage to the voltage', () => {
const health = deviceHealth(snapshot({ percentCharged: 42, batteryVoltage: 4.1 }), NOW);
expect(health.batteryPercent).toBe(42);
});
it('derives a rough percentage from the voltage when that is all there is', () => {
expect(deviceHealth(snapshot({ batteryVoltage: 4.1 }), NOW).batteryPercent).toBe(100);
expect(deviceHealth(snapshot({ batteryVoltage: 3.3 }), NOW).batteryPercent).toBe(0);
expect(deviceHealth(snapshot({ batteryVoltage: 3.7 }), NOW).batteryPercent).toBe(50);
});
it('clamps a voltage outside the usable range', () => {
expect(deviceHealth(snapshot({ batteryVoltage: 4.5 }), NOW).batteryPercent).toBe(100);
expect(deviceHealth(snapshot({ batteryVoltage: 2.9 }), NOW).batteryPercent).toBe(0);
});
it('reports nothing about a battery it was never told about', () => {
expect(deviceHealth(snapshot(), NOW).batteryPercent).toBeNull();
});
it('grades the wifi signal', () => {
expect(deviceHealth(snapshot({ rssi: -55 }), NOW).signal).toBe('good');
expect(deviceHealth(snapshot({ rssi: -72 }), NOW).signal).toBe('fair');
expect(deviceHealth(snapshot({ rssi: -88 }), NOW).signal).toBe('poor');
expect(deviceHealth(snapshot(), NOW).signal).toBeNull();
});
});
describe('humaniseSeconds', () => {
it('looks forward and backward', () => {
expect(humaniseSeconds(300)).toBe('dans 5 minutes');
expect(humaniseSeconds(-300)).toBe('il y a 5 minutes');
});
it('shortens anything under a minute', () => {
expect(humaniseSeconds(30)).toBe('dans moins d’une minute');
expect(humaniseSeconds(0)).toBe('dans moins d’une minute');
});
it('switches unit as the distance grows', () => {
expect(humaniseSeconds(3600)).toBe('dans 1 heure');
expect(humaniseSeconds(7200)).toBe('dans 2 heures');
expect(humaniseSeconds(172_800)).toBe('dans 2 jours');
});
it('keeps the singular', () => {
expect(humaniseSeconds(60)).toBe('dans 1 minute');
});
});