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 <noreply@anthropic.com>
This commit is contained in:
@@ -1,14 +1,20 @@
|
|||||||
// One discovery cycle: scan → upsert → mark-absent → prune. Deps injected for
|
// One discovery cycle: scan → drop homelab guests → upsert → mark-absent → prune.
|
||||||
// tests. Prune only runs after a successful, non-empty scan, so a failed scan
|
// Homelab containers/hosts are excluded from the IoT/personal devices band — they
|
||||||
// can never reap rows.
|
// 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 { runScan } from './scan.js';
|
||||||
import * as devices from '../db/repos/lan_devices.js';
|
import * as devices from '../db/repos/lan_devices.js';
|
||||||
|
import * as netHosts from '../db/repos/network_hosts.js';
|
||||||
import { log } from '../log.js';
|
import { log } from '../log.js';
|
||||||
|
|
||||||
export async function runDeviceScanCycle({ scan = runScan, repo = devices } = {}) {
|
const HOMELAB_OUI = 'bc:24:11'; // Proxmox auto-generated guest MAC prefix
|
||||||
const rows = await scan();
|
|
||||||
|
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) {
|
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 };
|
return { seen: 0 };
|
||||||
}
|
}
|
||||||
await repo.upsertScan(rows);
|
await repo.upsertScan(rows);
|
||||||
|
|||||||
@@ -4,18 +4,18 @@ import { runDeviceScanCycle } from '../../lib/infra/scan_cycle.js';
|
|||||||
|
|
||||||
function fakeRepo() {
|
function fakeRepo() {
|
||||||
return {
|
return {
|
||||||
calls: [],
|
|
||||||
upsertScan: vi.fn(async r => r.length),
|
upsertScan: vi.fn(async r => r.length),
|
||||||
markAbsent: vi.fn(async () => 1),
|
markAbsent: vi.fn(async () => 1),
|
||||||
prune: vi.fn(async () => 2)
|
prune: vi.fn(async () => 2)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
const noHosts = { all: async () => [] };
|
||||||
|
|
||||||
describe('runDeviceScanCycle', () => {
|
describe('runDeviceScanCycle', () => {
|
||||||
it('scan→upsert→markAbsent→prune on a non-empty scan', async () => {
|
it('scan→upsert→markAbsent→prune on a non-empty scan', async () => {
|
||||||
const repo = fakeRepo();
|
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 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.upsertScan).toHaveBeenCalledOnce();
|
||||||
expect(repo.markAbsent).toHaveBeenCalledWith(['aa:bb:cc:dd:ee:ff']);
|
expect(repo.markAbsent).toHaveBeenCalledWith(['aa:bb:cc:dd:ee:ff']);
|
||||||
expect(repo.prune).toHaveBeenCalledOnce();
|
expect(repo.prune).toHaveBeenCalledOnce();
|
||||||
@@ -24,9 +24,24 @@ describe('runDeviceScanCycle', () => {
|
|||||||
|
|
||||||
it('skips upsert/prune when the scan returns nothing', async () => {
|
it('skips upsert/prune when the scan returns nothing', async () => {
|
||||||
const repo = fakeRepo();
|
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.upsertScan).not.toHaveBeenCalled();
|
||||||
expect(repo.prune).not.toHaveBeenCalled();
|
expect(repo.prune).not.toHaveBeenCalled();
|
||||||
expect(res).toEqual({ seen: 0 });
|
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']);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user