feature(devices,topology,tz): devices bulk-add, duplicate safety, and firmware/timezone fixes

Squash-merge of PR #28 from @denis-ev. All three review blockers addressed: encryption key management documented, string truncation replaced with skip+warn validation, and undefined return on duplicate merge fixed with proper 500. Local test stack verified against cloned prod DB — migrations, new features, and backend all clean.
This commit is contained in:
Denis
2026-04-21 16:52:04 -07:00
committed by GitHub
parent 5562ac31dd
commit 30bf691596
23 changed files with 2770 additions and 213 deletions
+47 -1
View File
@@ -39,7 +39,8 @@ router.get('/', async (_req: Request, res: Response) => {
const [devices, allLinks] = await Promise.all([
query(
`SELECT id, name, ip_address, model, device_type, status, ros_version
`SELECT id, name, ip_address, model, device_type, status, ros_version,
ip_addresses_jsonb
FROM devices ORDER BY name ASC`
),
query<LinkRow>(
@@ -70,6 +71,51 @@ router.get('/', async (_req: Request, res: Response) => {
}
const links = Array.from(bestLinks.values());
// ── Resolve neighbor IP → managed device (any IP on that device, not just mgmt) ──
// CDP/MNDP often report only an IP. We cache /ip/address per device during sync;
// if a neighbor address matches any managed device's known address, treat the
// link as device-to-device instead of creating an external node.
const stripCidr = (addr: string): string => {
const a = addr.trim();
const slash = a.indexOf('/');
return slash === -1 ? a : a.slice(0, slash);
};
const normIp = (addr: string): string => stripCidr(addr).toLowerCase();
const ipToDevice = new Map<string, { id: number; name: string }>();
for (const d of devices as {
id: number; name: string; ip_address: string;
ip_addresses_jsonb?: unknown;
}[]) {
if (d.ip_address) {
const k = normIp(d.ip_address);
if (k && !ipToDevice.has(k)) ipToDevice.set(k, { id: d.id, name: d.name });
}
const list = d.ip_addresses_jsonb;
if (Array.isArray(list)) {
for (const entry of list) {
const raw = typeof entry === 'object' && entry && 'address' in entry
? String((entry as { address?: string }).address || '')
: '';
const k = normIp(raw);
if (k && !ipToDevice.has(k)) ipToDevice.set(k, { id: d.id, name: d.name });
}
}
}
for (const link of links) {
if (link.to_device_id) continue;
const na = link.neighbor_address?.trim();
if (!na || na.includes('%')) continue; // skip zone-id style
const key = normIp(na);
if (!key) continue;
const hit = ipToDevice.get(key);
if (hit && hit.id !== link.from_device_id) {
link.to_device_id = hit.id;
link.to_device_name = hit.name;
}
}
// Build synthetic external nodes from unresolved neighbor entries
const externalMap = new Map<string, {
id: string;