feat(web): rebuild the console around the fused control plane

Render the Hub feature set in the existing Animal Island styling: fleet
messages, notification centre, log centre, organisation panel, per-instance
device module panels and the settings backup, connection and maintenance
surfaces.
This commit is contained in:
chick
2026-09-05 18:53:04 +08:00
parent f9186bd851
commit a75badeb58
77 changed files with 20578 additions and 435 deletions
@@ -48,6 +48,16 @@ describe('fleet table view model', () => {
expect(fleetStatusKind({ reachable: true, authenticated: true })).toBe('online');
});
it('treats a device the heartbeat has never reached as unknown, not offline', () => {
expect(fleetStatusKind({ probed: false, reachable: false, authenticated: false })).toBe(
'unknown',
);
expect(fleetStatusKind({ probed: true, reachable: false, authenticated: false })).toBe(
'offline',
);
expect(fleetStatusKind({ probed: true, reachable: true, authenticated: true })).toBe('online');
});
it('builds stable rows and defaults to ascending display-name order', () => {
const model = buildFleetTableViewModel(instances, statuses);
@@ -197,6 +207,74 @@ describe('fleet table view model', () => {
capabilities: ['sms', 'ussd'],
versions: ['2.4.1'],
tags: ['east', 'west'],
groups: [],
});
});
it('filters, sorts, and searches by device group', () => {
const grouped: readonly FleetInstance[] = [
{ id: 'one', name: 'One', url: 'https://one.example', groupId: 'group-field' },
{ id: 'two', name: 'Two', url: 'https://two.example', groupId: 'group-office' },
{ id: 'three', name: 'Three', url: 'https://three.example', groupId: null },
{ id: 'four', name: 'Four', url: 'https://four.example' },
];
const groupNames = new Map([
['group-field', 'Field Ops'],
['group-office', 'Office'],
]);
const model = buildFleetTableViewModel(grouped, new Map(), { groupNames });
expect(model.facets.groups).toEqual(['group-field', 'group-office']);
expect(model.rows.map((row) => [row.id, row.groupId])).toEqual([
['four', null],
['one', 'group-field'],
['three', null],
['two', 'group-office'],
]);
expect(
buildFleetTableViewModel(grouped, new Map(), {
group: 'group-field',
groupNames,
}).rows.map((row) => row.id),
).toEqual(['one']);
// Group names are searchable, not just the raw identifier.
expect(
buildFleetTableViewModel(grouped, new Map(), { query: 'office', groupNames }).rows.map(
(row) => row.id,
),
).toEqual(['two']);
expect(
buildFleetTableViewModel(grouped, new Map(), {
query: '外场',
groupNames: new Map([['group-field', '外场']]),
}).rows.map((row) => row.id),
).toEqual(['one']);
expect(
buildFleetTableViewModel(grouped, new Map(), {
sort: { column: 'group', direction: 'asc' },
groupNames,
}).rows.map((row) => row.id),
).toEqual(['one', 'two', 'four', 'three']);
// Sorting follows the visible group name, never the opaque identifier.
expect(
buildFleetTableViewModel(grouped, new Map(), {
sort: { column: 'group', direction: 'asc' },
groupNames: new Map([
['group-field', 'Zeta'],
['group-office', 'Alpha'],
]),
}).rows.map((row) => row.id),
).toEqual(['two', 'one', 'four', 'three']);
expect(
buildFleetTableViewModel(grouped, new Map(), {
group: 'group-missing',
groupNames,
}).emptyReason,
).toBe('filter');
});
});