Add LAN device discovery (ping sweep + ARP, known/unknown labeling)
Runs as a host-level systemd timer on CT122 (scripts/discover-devices.sh) rather than inside the api container, since real ARP entries live in the host's network namespace, not Docker's bridge network. See docs/device-discovery.md for the full writeup, including why literal passive-only ARP reading was dropped (near-empty result in practice). API reads the resulting JSON file each poll cycle, cross-references config/hosts.yaml's knownDevices list by IP, and serves /api/devices. Dashboard gets a new "Network Devices" table. Closes #9. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,14 @@ export function openDb(path: string): Database.Database {
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_snapshots_host_ts ON metric_snapshots(host_id, ts);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS devices (
|
||||
ip TEXT PRIMARY KEY,
|
||||
mac TEXT NOT NULL,
|
||||
known_name TEXT,
|
||||
first_seen TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
last_seen TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
`);
|
||||
return db;
|
||||
}
|
||||
@@ -100,3 +108,45 @@ export function pruneOldSnapshots(db: Database.Database, retentionHours: number)
|
||||
cutoff: `-${retentionHours} hours`,
|
||||
});
|
||||
}
|
||||
|
||||
export interface DiscoveredDevice {
|
||||
ip: string;
|
||||
mac: string;
|
||||
knownName: string | null;
|
||||
}
|
||||
|
||||
export function upsertDevices(db: Database.Database, devices: DiscoveredDevice[]): void {
|
||||
const upsert = db.prepare(`
|
||||
INSERT INTO devices (ip, mac, known_name)
|
||||
VALUES (@ip, @mac, @knownName)
|
||||
ON CONFLICT(ip) DO UPDATE SET
|
||||
mac = excluded.mac,
|
||||
known_name = excluded.known_name,
|
||||
last_seen = datetime('now')
|
||||
`);
|
||||
const tx = db.transaction((items: DiscoveredDevice[]) => {
|
||||
for (const d of items) upsert.run(d);
|
||||
});
|
||||
tx(devices);
|
||||
}
|
||||
|
||||
export interface DeviceRow {
|
||||
ip: string;
|
||||
mac: string;
|
||||
known_name: string | null;
|
||||
first_seen: string;
|
||||
last_seen: string;
|
||||
}
|
||||
|
||||
// Devices not seen in the last 24h (unplugged, moved, DHCP lease expired) are
|
||||
// dropped from the list rather than shown as permanently "known but offline" —
|
||||
// there's no persistent per-device up/down state to track like hosts have.
|
||||
export function getRecentDevices(db: Database.Database, sinceHours = 24): DeviceRow[] {
|
||||
return db
|
||||
.prepare(
|
||||
`SELECT ip, mac, known_name, first_seen, last_seen FROM devices
|
||||
WHERE last_seen > datetime('now', @cutoff)
|
||||
ORDER BY known_name IS NULL, ip`
|
||||
)
|
||||
.all({ cutoff: `-${sinceHours} hours` }) as DeviceRow[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user