Identify unknown devices: OUI vendor lookup, mDNS, manual labels
- OUI: mac-oui-lookup package resolves vendor from the MAC prefix (computed on read, no storage needed). Already correctly identifies the LXC host prefix as "Proxmox Server Solutions GmbH" and several "unknown" devices as "Amazon Technologies Inc." -- likely the Echo Dots / Ring gear. - mDNS: discover-devices.sh now runs avahi-resolve per discovered IP (parallel, bounded 2s timeout per host so one non-mDNS device can't stall the run), stored in a new devices.mdns_hostname column. - Manual labels: new device_labels table keyed by MAC (survives DHCP IP changes), PUT/DELETE /api/devices/:mac/label, inline-editable Name cell in the dashboard. Deliberately separate from vendor/mDNS info -- those are shown as an italic *hint* for unlabeled devices, not treated as "known" until the admin actually confirms one. - Fixed the Name column's sort comparator to match what's rendered (name, else vendor/mDNS hint) instead of just the raw name field -- caught while reasoning through what the existing sort test would actually need to assert once hints appear in the column. Part of #15 (OUI/mDNS/manual labels done; on-demand deep-check next). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -41,10 +41,22 @@ export function openDb(path: string): Database.Database {
|
||||
ip TEXT PRIMARY KEY,
|
||||
mac TEXT NOT NULL,
|
||||
known_name TEXT,
|
||||
mdns_hostname TEXT,
|
||||
first_seen TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
last_seen TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
-- Manual admin-assigned names, keyed by MAC (not IP, which can change on
|
||||
-- DHCP renewal). Overrides both known_name and mdns_hostname when present.
|
||||
CREATE TABLE IF NOT EXISTS device_labels (
|
||||
mac TEXT PRIMARY KEY,
|
||||
label TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
`);
|
||||
// Migration for the devices table pre-dating mdns_hostname (CREATE TABLE IF
|
||||
// NOT EXISTS above doesn't touch already-existing tables).
|
||||
db.exec(`ALTER TABLE devices ADD COLUMN IF NOT EXISTS mdns_hostname TEXT`);
|
||||
return db;
|
||||
}
|
||||
|
||||
@@ -140,15 +152,17 @@ export interface DiscoveredDevice {
|
||||
ip: string;
|
||||
mac: string;
|
||||
knownName: string | null;
|
||||
mdnsHostname: 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)
|
||||
INSERT INTO devices (ip, mac, known_name, mdns_hostname)
|
||||
VALUES (@ip, @mac, @knownName, @mdnsHostname)
|
||||
ON CONFLICT(ip) DO UPDATE SET
|
||||
mac = excluded.mac,
|
||||
known_name = excluded.known_name,
|
||||
mdns_hostname = excluded.mdns_hostname,
|
||||
last_seen = datetime('now')
|
||||
`);
|
||||
const tx = db.transaction((items: DiscoveredDevice[]) => {
|
||||
@@ -161,6 +175,8 @@ export interface DeviceRow {
|
||||
ip: string;
|
||||
mac: string;
|
||||
known_name: string | null;
|
||||
mdns_hostname: string | null;
|
||||
manual_label: string | null;
|
||||
first_seen: string;
|
||||
last_seen: string;
|
||||
}
|
||||
@@ -168,12 +184,28 @@ export interface DeviceRow {
|
||||
// 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.
|
||||
// Manual labels are joined by MAC (survives IP changes) and take priority —
|
||||
// see effectiveName() in routes/devices.ts for the full precedence order.
|
||||
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`
|
||||
`SELECT d.ip, d.mac, d.known_name, d.mdns_hostname, l.label AS manual_label,
|
||||
d.first_seen, d.last_seen
|
||||
FROM devices d
|
||||
LEFT JOIN device_labels l ON l.mac = d.mac
|
||||
WHERE d.last_seen > datetime('now', @cutoff)
|
||||
ORDER BY (l.label IS NULL AND d.known_name IS NULL), d.ip`
|
||||
)
|
||||
.all({ cutoff: `-${sinceHours} hours` }) as DeviceRow[];
|
||||
}
|
||||
|
||||
export function setDeviceLabel(db: Database.Database, mac: string, label: string): void {
|
||||
db.prepare(
|
||||
`INSERT INTO device_labels (mac, label) VALUES (@mac, @label)
|
||||
ON CONFLICT(mac) DO UPDATE SET label = excluded.label, updated_at = datetime('now')`
|
||||
).run({ mac, label });
|
||||
}
|
||||
|
||||
export function clearDeviceLabel(db: Database.Database, mac: string): void {
|
||||
db.prepare(`DELETE FROM device_labels WHERE mac = @mac`).run({ mac });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user