Persist traffic settings and support multi-device traffic views
Build and Deploy Gateway / build-and-push (push) Successful in 32s
Build and Deploy Gateway / deploy (push) Successful in 7s

This commit is contained in:
2026-08-31 06:46:59 +03:00
parent 6777422a27
commit 3c2eefe108
18 changed files with 653 additions and 124 deletions
+50 -8
View File
@@ -1,10 +1,16 @@
import type { IncomingMessage, ServerResponse } from 'node:http';
import { isDeepStrictEqual } from 'node:util';
import {
assertLiveTrafficSnapshot,
type LiveTrafficSnapshot,
} from '../../../shared/liveTraffic.js';
import type { StoredState } from '../../../shared/contracts/state.js';
import { HarborError } from '../../../shared/errors.js';
import {
normalizeTrafficSettings,
type TrafficSettings,
} from '../../../shared/trafficSettings.js';
import { sendJson } from '../response.js';
interface LiveTrafficReader {
@@ -15,6 +21,11 @@ interface DeviceInventoryReader {
snapshot(): unknown;
}
interface TrafficSettingsState {
read(): { revision?: unknown; traffic?: TrafficSettings };
update(mutator: (state: StoredState) => Record<string, unknown>): unknown;
}
function record(value: unknown): Record<string, unknown> {
return value && typeof value === 'object' && !Array.isArray(value)
? value as Record<string, unknown>
@@ -53,21 +64,52 @@ export function enrichLiveTrafficDeviceLabels(
export function createLiveTrafficRoute({
traffic,
deviceInventory = null,
settingsState = null,
readBody = null,
sendState = null,
}: {
traffic: LiveTrafficReader | null;
deviceInventory?: DeviceInventoryReader | null;
settingsState?: TrafficSettingsState | null;
readBody?: ((req: IncomingMessage) => Promise<Record<string, unknown>>) | null;
sendState?: ((res: ServerResponse) => Promise<void>) | null;
}) {
return {
async handle(req: IncomingMessage, res: ServerResponse) {
const pathname = new URL(req.url || '/', 'http://localhost').pathname;
if (pathname !== '/api/traffic/live') return false;
if (req.method !== 'GET' || !traffic) throw new HarborError('ENDPOINT_NOT_FOUND');
const snapshot = assertLiveTrafficSnapshot(await traffic.snapshot());
const enriched = deviceInventory
? enrichLiveTrafficDeviceLabels(snapshot, deviceInventory.snapshot())
: snapshot;
sendJson(res, 200, enriched);
return true;
if (pathname === '/api/traffic/live') {
if (req.method !== 'GET' || !traffic) throw new HarborError('ENDPOINT_NOT_FOUND');
const snapshot = assertLiveTrafficSnapshot(await traffic.snapshot());
const enriched = deviceInventory
? enrichLiveTrafficDeviceLabels(snapshot, deviceInventory.snapshot())
: snapshot;
sendJson(res, 200, enriched);
return true;
}
if (pathname === '/api/traffic/settings') {
if (req.method !== 'PUT' || !settingsState || !readBody || !sendState) {
throw new HarborError('ENDPOINT_NOT_FOUND');
}
const body = await readBody(req);
const expectedRevision = body.expectedRevision;
if (!Number.isSafeInteger(expectedRevision) || Number(expectedRevision) < 0) {
throw new HarborError('REQUEST_INVALID');
}
let settings: TrafficSettings;
try {
settings = normalizeTrafficSettings(body.settings, { strict: true });
} catch (cause) {
throw new HarborError('REQUEST_INVALID', { cause });
}
const current = settingsState.read();
if (current.revision !== expectedRevision) throw new HarborError('STATE_CONFLICT');
if (!isDeepStrictEqual(current.traffic, settings)) {
settingsState.update((state) => ({ ...state, traffic: settings }));
}
await sendState(res);
return true;
}
return false;
},
};
}