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:
@@ -1,6 +1,7 @@
|
||||
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
||||
import type Database from "better-sqlite3";
|
||||
import { getRecentDevices } from "../db/index.js";
|
||||
import { getVendor } from "mac-oui-lookup";
|
||||
import { getRecentDevices, setDeviceLabel, clearDeviceLabel, type DeviceRow } from "../db/index.js";
|
||||
|
||||
async function requireAuth(req: FastifyRequest, reply: FastifyReply) {
|
||||
if (!req.session.username) {
|
||||
@@ -8,18 +9,53 @@ async function requireAuth(req: FastifyRequest, reply: FastifyReply) {
|
||||
}
|
||||
}
|
||||
|
||||
// Manual label wins (deliberately set by the admin), then the static
|
||||
// config/hosts.yaml known list, then the passively-resolved mDNS hostname —
|
||||
// which is informational only and does NOT make a device "known" on its own,
|
||||
// since nobody has actually vetted it yet.
|
||||
function toApiDevice(r: DeviceRow) {
|
||||
const name = r.manual_label ?? r.known_name ?? null;
|
||||
return {
|
||||
ip: r.ip,
|
||||
mac: r.mac,
|
||||
name,
|
||||
known: r.manual_label !== null || r.known_name !== null,
|
||||
vendor: getVendor(r.mac),
|
||||
mdnsHostname: r.mdns_hostname,
|
||||
firstSeen: r.first_seen,
|
||||
lastSeen: r.last_seen,
|
||||
};
|
||||
}
|
||||
|
||||
const MAC_RE = /^[0-9a-f]{2}(:[0-9a-f]{2}){5}$/i;
|
||||
|
||||
export function registerDeviceRoutes(app: FastifyInstance, db: Database.Database): void {
|
||||
app.get("/api/devices", { preHandler: requireAuth }, async () => {
|
||||
const rows = getRecentDevices(db);
|
||||
return {
|
||||
devices: rows.map((r) => ({
|
||||
ip: r.ip,
|
||||
mac: r.mac,
|
||||
name: r.known_name,
|
||||
known: r.known_name !== null,
|
||||
firstSeen: r.first_seen,
|
||||
lastSeen: r.last_seen,
|
||||
})),
|
||||
};
|
||||
return { devices: rows.map(toApiDevice) };
|
||||
});
|
||||
|
||||
app.put<{ Params: { mac: string }; Body: { label: string } }>(
|
||||
"/api/devices/:mac/label",
|
||||
{ preHandler: requireAuth },
|
||||
async (req, reply) => {
|
||||
const mac = req.params.mac.toLowerCase();
|
||||
const label = req.body?.label?.trim();
|
||||
if (!MAC_RE.test(mac)) return reply.code(400).send({ error: "invalid MAC address" });
|
||||
if (!label) return reply.code(400).send({ error: "label must not be empty" });
|
||||
setDeviceLabel(db, mac, label);
|
||||
return { ok: true };
|
||||
}
|
||||
);
|
||||
|
||||
app.delete<{ Params: { mac: string } }>(
|
||||
"/api/devices/:mac/label",
|
||||
{ preHandler: requireAuth },
|
||||
async (req, reply) => {
|
||||
const mac = req.params.mac.toLowerCase();
|
||||
if (!MAC_RE.test(mac)) return reply.code(400).send({ error: "invalid MAC address" });
|
||||
clearDeviceLabel(db, mac);
|
||||
return { ok: true };
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user