Wire up on-demand deep-check: API route + dashboard button
CI / web (push) Successful in 18s
CI / api (push) Successful in 24s

POST /api/devices/:ip/deep-check runs deep-check-device.sh on the
CT122 host via SSH (reaches its own LAN IP), returns mDNS/SSDP/port
scan results. "Deep check" button on unknown device rows in the
dashboard shows results inline below the row.

Verified end-to-end via SSH before wiring into the API: correctly
identified Home Assistant via SSDP (friendlyName/manufacturer/model),
and confirmed both a shell-injection attempt and an out-of-subnet IP
get rejected cleanly by the forced command's input validation.

Closes #15 (all four pieces: OUI, mDNS, manual labels, deep check).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-12 23:15:24 -06:00
parent e0ef618f0a
commit 542a3d8ce0
9 changed files with 421 additions and 28 deletions
+28 -1
View File
@@ -7,6 +7,8 @@ import type Database from "better-sqlite3";
import macOuiLookup from "mac-oui-lookup";
const { getVendor } = macOuiLookup;
import { getRecentDevices, setDeviceLabel, clearDeviceLabel, type DeviceRow } from "../db/index.js";
import { deepCheckDevice } from "../discovery/deepCheck.js";
import type { DeepCheckHostConfig } from "../config/index.js";
async function requireAuth(req: FastifyRequest, reply: FastifyReply) {
if (!req.session.username) {
@@ -33,8 +35,13 @@ function toApiDevice(r: DeviceRow) {
}
const MAC_RE = /^[0-9a-f]{2}(:[0-9a-f]{2}){5}$/i;
const IP_RE = /^192\.168\.1\.([0-9]{1,3})$/;
export function registerDeviceRoutes(app: FastifyInstance, db: Database.Database): void {
export function registerDeviceRoutes(
app: FastifyInstance,
db: Database.Database,
deepCheckConfig: DeepCheckHostConfig | undefined
): void {
app.get("/api/devices", { preHandler: requireAuth }, async () => {
const rows = getRecentDevices(db);
return { devices: rows.map(toApiDevice) };
@@ -63,4 +70,24 @@ export function registerDeviceRoutes(app: FastifyInstance, db: Database.Database
return { ok: true };
}
);
// Admin-triggered, single-device, on-demand -- not automatic, to avoid the
// noise/risk of doing this for the whole subnet on every poll. Runs on the
// CT122 host via SSH (see docs/device-discovery.md); can take up to ~15s
// (SSDP listen window + bounded port scan).
app.post<{ Params: { ip: string } }>(
"/api/devices/:ip/deep-check",
{ preHandler: requireAuth },
async (req, reply) => {
if (!deepCheckConfig) return reply.code(501).send({ error: "deep check not configured" });
if (!IP_RE.test(req.params.ip)) return reply.code(400).send({ error: "invalid IP address" });
try {
const result = await deepCheckDevice(deepCheckConfig, req.params.ip);
return result;
} catch (err) {
req.log.error({ err, ip: req.params.ip }, "deep check failed");
return reply.code(502).send({ error: err instanceof Error ? err.message : "deep check failed" });
}
}
);
}