From a042cbaaa57f06549991c912630f9481dbd8fe65 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 8 Jun 2026 21:31:10 +1000 Subject: [PATCH] fix(devices): exclude homelab guests (network_hosts + bc:24:11 OUI) from discovery The scan was surfacing every Proxmox container/host as a 'new' device. Filter the scan against the network_hosts inventory and the Proxmox guest OUI so the devices band stays IoT/personal-only, per the spec. Co-Authored-By: Claude Opus 4.8 --- lib/infra/scan_cycle.js | 18 ++++++++++++------ tests/infra/scan_cycle.test.js | 21 ++++++++++++++++++--- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/lib/infra/scan_cycle.js b/lib/infra/scan_cycle.js index c55b697..5bf9c59 100644 --- a/lib/infra/scan_cycle.js +++ b/lib/infra/scan_cycle.js @@ -1,14 +1,20 @@ -// One discovery cycle: scan → upsert → mark-absent → prune. Deps injected for -// tests. Prune only runs after a successful, non-empty scan, so a failed scan -// can never reap rows. +// One discovery cycle: scan → drop homelab guests → upsert → mark-absent → prune. +// Homelab containers/hosts are excluded from the IoT/personal devices band — they +// live in the network_hosts inventory, not here. We drop any MAC that's in +// network_hosts OR carries the Proxmox guest OUI (bc:24:11). Deps injected for +// tests. Prune only runs after a successful, non-empty scan. import { runScan } from './scan.js'; import * as devices from '../db/repos/lan_devices.js'; +import * as netHosts from '../db/repos/network_hosts.js'; import { log } from '../log.js'; -export async function runDeviceScanCycle({ scan = runScan, repo = devices } = {}) { - const rows = await scan(); +const HOMELAB_OUI = 'bc:24:11'; // Proxmox auto-generated guest MAC prefix + +export async function runDeviceScanCycle({ scan = runScan, repo = devices, hosts = netHosts } = {}) { + const inventory = new Set((await hosts.all()).map(h => String(h.mac || '').toLowerCase())); + const rows = (await scan()).filter(r => !inventory.has(r.mac) && !r.mac.startsWith(HOMELAB_OUI)); if (!rows.length) { - log.warn('device scan returned no hosts; skipping upsert/prune'); + log.warn('device scan found no non-homelab hosts; skipping upsert/prune'); return { seen: 0 }; } await repo.upsertScan(rows); diff --git a/tests/infra/scan_cycle.test.js b/tests/infra/scan_cycle.test.js index 4993e8d..cb8f8d3 100644 --- a/tests/infra/scan_cycle.test.js +++ b/tests/infra/scan_cycle.test.js @@ -4,18 +4,18 @@ import { runDeviceScanCycle } from '../../lib/infra/scan_cycle.js'; function fakeRepo() { return { - calls: [], upsertScan: vi.fn(async r => r.length), markAbsent: vi.fn(async () => 1), prune: vi.fn(async () => 2) }; } +const noHosts = { all: async () => [] }; describe('runDeviceScanCycle', () => { it('scan→upsert→markAbsent→prune on a non-empty scan', async () => { const repo = fakeRepo(); const scan = vi.fn(async () => [{ mac: 'aa:bb:cc:dd:ee:ff', ip: '1.2.3.4', vendor: 'x', randomized: false }]); - const res = await runDeviceScanCycle({ scan, repo }); + const res = await runDeviceScanCycle({ scan, repo, hosts: noHosts }); expect(repo.upsertScan).toHaveBeenCalledOnce(); expect(repo.markAbsent).toHaveBeenCalledWith(['aa:bb:cc:dd:ee:ff']); expect(repo.prune).toHaveBeenCalledOnce(); @@ -24,9 +24,24 @@ describe('runDeviceScanCycle', () => { it('skips upsert/prune when the scan returns nothing', async () => { const repo = fakeRepo(); - const res = await runDeviceScanCycle({ scan: async () => [], repo }); + const res = await runDeviceScanCycle({ scan: async () => [], repo, hosts: noHosts }); expect(repo.upsertScan).not.toHaveBeenCalled(); expect(repo.prune).not.toHaveBeenCalled(); expect(res).toEqual({ seen: 0 }); }); + + it('excludes homelab guests (network_hosts inventory + bc:24:11 OUI)', async () => { + const repo = fakeRepo(); + const hosts = { all: async () => [{ mac: 'BC:24:11:9B:B7:3A' }, { mac: '00:E0:4C:0F:36:00' }] }; + const scan = async () => [ + { mac: 'bc:24:11:9b:b7:3a', ip: '192.168.1.216', vendor: '', randomized: false }, // in inventory + { mac: 'bc:24:11:de:ad:00', ip: '192.168.1.99', vendor: '', randomized: false }, // proxmox OUI + { mac: '00:e0:4c:0f:36:00', ip: '192.168.1.124', vendor: '', randomized: false }, // PVE host in inventory + { mac: 'd8:eb:46:77:37:a8', ip: '192.168.1.25', vendor: 'Google', randomized: false } // real device + ]; + const res = await runDeviceScanCycle({ scan, repo, hosts }); + expect(res.seen).toBe(1); + expect(repo.upsertScan).toHaveBeenCalledWith([expect.objectContaining({ mac: 'd8:eb:46:77:37:a8' })]); + expect(repo.markAbsent).toHaveBeenCalledWith(['d8:eb:46:77:37:a8']); + }); });