Add persistent device traffic history charts
Build and Deploy Gateway / build-and-push (push) Successful in 14s
Build and Deploy Gateway / deploy (push) Successful in 6s

This commit is contained in:
2026-08-07 19:34:59 +03:00
parent cf90fd5b4e
commit ef4847a3e1
7 changed files with 174 additions and 45 deletions
+35 -1
View File
@@ -9,6 +9,7 @@ export const DEVICE_INVENTORY_SCHEMA_VERSION = 2;
const ONLINE_MS = 2 * 60 * 1000;
const RECENT_MS = 24 * 60 * 60 * 1000;
const RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
const TRAFFIC_HISTORY_LIMIT = 120;
const COUNTER_PATTERN = /^\d+$/;
const DEVICE_ID_PATTERN = /^dev_[a-f0-9]{16}$/;
const MAC_PATTERN = /^[0-9a-f]{2}(?::[0-9a-f]{2}){5}$/;
@@ -302,6 +303,36 @@ export function createDeviceInventoryService({
}) {
let refreshPromise = null;
let policyQueue = Promise.resolve();
const trafficHistoryByMac = new Map();
const trafficCursorByMac = new Map();
function captureTrafficHistory(state) {
const knownMacs = new Set(state.devices.map(({ mac }) => mac));
for (const device of state.devices) {
const traffic = state.traffic.totalsByMac[device.mac];
const proxy = state.traffic.proxy.totalsByMac[device.mac];
const signature = `${traffic?.observedAt || ''}|${proxy?.observedAt || ''}`;
if (signature === '|') continue;
const gateway = BigInt(traffic?.uploadBytes || '0') + BigInt(traffic?.downloadBytes || '0');
const proxyTotal = BigInt(proxy?.uploadBytes || '0') + BigInt(proxy?.downloadBytes || '0');
const previous = trafficCursorByMac.get(device.mac);
trafficCursorByMac.set(device.mac, { signature, gateway, proxy: proxyTotal });
if (!previous || previous.signature === signature) continue;
const observedAt = [traffic?.observedAt, proxy?.observedAt].filter(Boolean).sort().at(-1);
const samples = trafficHistoryByMac.get(device.mac) || [];
trafficHistoryByMac.set(device.mac, [...samples, {
observedAt,
gatewayBytes: gateway > previous.gateway ? (gateway - previous.gateway).toString() : '0',
proxyBytes: proxyTotal > previous.proxy ? (proxyTotal - previous.proxy).toString() : '0',
}].slice(-TRAFFIC_HISTORY_LIMIT));
}
for (const mac of trafficCursorByMac.keys()) {
if (!knownMacs.has(mac)) {
trafficCursorByMac.delete(mac);
trafficHistoryByMac.delete(mac);
}
}
}
function serializePolicy(action) {
const result = policyQueue.then(action, action);
@@ -363,6 +394,7 @@ export function createDeviceInventoryService({
proxyUploadBytes: proxyTraffic?.uploadBytes || '0',
proxyDownloadBytes: proxyTraffic?.downloadBytes || '0',
proxyTrafficObservedAt: proxyTraffic?.observedAt || null,
trafficHistory: trafficHistoryByMac.get(device.mac) || [],
desiredPolicy: policy.desired,
appliedPolicy: policy.applied,
policyStatus: policy.status,
@@ -376,6 +408,7 @@ export function createDeviceInventoryService({
));
return {
revision: state.revision,
trafficHistoryCapacity: TRAFFIC_HISTORY_LIMIT,
source: {
kind: 'neighbor',
lastObservedAt: state.lastObservedAt,
@@ -534,7 +567,7 @@ export function createDeviceInventoryService({
identitiesByMac.get(mac).add(`${observation.ip}|${observation.interface || ''}`);
}
return serializePolicy(async () => {
store.update((stored) => {
const nextState = store.update((stored) => {
const state = migrateDeviceInventoryState(stored);
const byMac = new Map(state.devices.map((device) => [device.mac, device]));
for (const observation of observations) {
@@ -752,6 +785,7 @@ export function createDeviceInventoryService({
devices,
};
});
captureTrafficHistory(nextState);
if (policyResult?.transportError) commitPolicyFailure(new Error(policyResult.transportError));
return reconcileLocked(policyResult, false);
});