Add outbound traffic breakdown to device charts
Build and Deploy Gateway / build-and-push (push) Successful in 20s
Build and Deploy Gateway / deploy (push) Successful in 14s

This commit is contained in:
2026-08-12 22:18:08 +03:00
parent 9e52ccc24d
commit 9d43e74d97
10 changed files with 403 additions and 69 deletions
+76
View File
@@ -504,6 +504,82 @@ test('device traffic history stays in bounded service memory and survives client
assert.deepEqual(createService().snapshot().traffic.history, []);
});
test('device outbound history keeps sing-box routes and kernel Direct on separate reset-safe series', async (t) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-device-outbound-history-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const filePath = path.join(directory, 'devices.json');
const store = createJsonStore({ filePath, defaultValue: {}, migrate: migrateDeviceInventoryState });
const mac = '00:11:22:33:44:55';
const id = deviceId(mac);
let observedAt = '2026-08-12T12:00:00.000Z';
let epoch = 'epoch-a';
let vpn = ['100', '200'];
let trackedDirect = ['10', '20'];
let unknown = ['1', '2'];
let directIpv4 = ['40', '60'];
const service = createDeviceInventoryService({
store,
observe: () => ({
observedAt,
error: null,
observations: [{ ip: '192.168.50.7', mac, interface: 'eth0', observedAt, active: true }],
}),
observeTraffic: () => ({
epoch,
generation: 'rules-a',
observedAt,
source: { error: null },
direct: { uploadBytes: directIpv4[0], downloadBytes: directIpv4[1] },
devices: [{
ip: '192.168.50.7', mac, interface: 'eth0', uploadBytes: '100', downloadBytes: '200',
directUploadBytes: directIpv4[0], directDownloadBytes: directIpv4[1],
}],
}),
observeDomainTraffic: () => ({
epoch,
observedAt,
source: { error: null },
routes: [
{ deviceId: id, source: 'gateway', outbound: 'vpn', uploadBytes: vpn[0], downloadBytes: vpn[1] },
{ deviceId: id, source: 'proxy', outbound: 'direct', uploadBytes: trackedDirect[0], downloadBytes: trackedDirect[1] },
{ deviceId: id, source: 'gateway', outbound: 'unknown', uploadBytes: unknown[0], downloadBytes: unknown[1] },
],
series: [],
}),
});
assert.deepEqual((await service.refresh()).devices[0].outboundTrafficHistory, []);
observedAt = '2026-08-12T12:00:15.000Z';
vpn = ['150', '250'];
trackedDirect = ['30', '50'];
unknown = ['4', '8'];
directIpv4 = ['70', '90'];
let snapshot = await service.refresh();
assert.deepEqual(snapshot.devices[0].outboundTrafficHistory, [{
observedAt,
vpnBytes: '100',
directTrackedBytes: '50',
directIpv4Bytes: '60',
unknownBytes: '9',
}]);
observedAt = '2026-08-12T12:00:30.000Z';
epoch = 'epoch-b';
vpn = ['3', '4'];
trackedDirect = ['1', '2'];
unknown = ['0', '1'];
directIpv4 = ['5', '6'];
snapshot = await service.refresh();
assert.deepEqual(snapshot.devices[0].outboundTrafficHistory.at(-1), {
observedAt,
vpnBytes: '0',
directTrackedBytes: '0',
directIpv4Bytes: '0',
unknownBytes: '0',
});
assert.doesNotMatch(fs.readFileSync(filePath, 'utf8'), /outboundTrafficHistory/);
});
test('global traffic stays monotonic when a device expires and returns in the same epoch', async (t) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-global-traffic-retention-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));