Files
homelab-monitor/apps/api/src/index.ts
T
jhodgkin 04282232cc
CI / web (push) Successful in 17s
CI / api (push) Successful in 23s
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>
2026-07-12 20:49:22 -06:00

78 lines
2.3 KiB
TypeScript

import "dotenv/config";
import Fastify from "fastify";
import fastifyCookie from "@fastify/cookie";
import fastifySession from "@fastify/session";
import { loadConfig } from "./config/index.js";
import { openDb, upsertSnapshots, pruneOldSnapshots } from "./db/index.js";
import { seedAdminUser } from "./auth/local.js";
import { ProxmoxCollector } from "./collectors/proxmox.js";
import { SshHostCollector } from "./collectors/sshHost.js";
import type { Collector } from "./collectors/types.js";
import { registerAuthRoutes } from "./routes/auth.js";
import { registerHostRoutes } from "./routes/hosts.js";
import { registerDeviceRoutes } from "./routes/devices.js";
import { refreshDevices } from "./discovery/index.js";
const HOSTS_CONFIG_PATH = process.env.HOSTS_CONFIG_PATH ?? "../../config/hosts.yaml";
async function main() {
const cfg = loadConfig(HOSTS_CONFIG_PATH);
const db = openDb(cfg.dbPath);
if (cfg.adminPassword) {
seedAdminUser(db, cfg.adminUsername, cfg.adminPassword);
}
const collectors: Collector[] = [
new ProxmoxCollector({
host: cfg.proxmox.host,
node: cfg.proxmox.node,
tokenId: cfg.proxmox.tokenId,
tokenSecret: cfg.proxmox.tokenSecret,
}),
...cfg.sshHosts.map((h) => new SshHostCollector(h)),
];
async function pollOnce() {
for (const collector of collectors) {
try {
const snapshots = await collector.collect();
upsertSnapshots(db, snapshots);
} catch (err) {
app.log.error({ err, collector: collector.id }, "collector failed");
}
}
pruneOldSnapshots(db, cfg.snapshotRetentionHours);
try {
await refreshDevices(db, cfg.discoveryFilePath, cfg.knownDevices);
} catch (err) {
app.log.error({ err }, "device discovery refresh failed");
}
}
const app = Fastify({ logger: true });
await app.register(fastifyCookie);
await app.register(fastifySession, {
secret: cfg.sessionSecret,
cookie: { secure: cfg.cookieSecure, maxAge: 1000 * 60 * 60 * 12 },
});
registerAuthRoutes(app, db);
registerHostRoutes(app, db);
registerDeviceRoutes(app, db);
await pollOnce();
setInterval(() => {
void pollOnce();
}, cfg.pollIntervalSeconds * 1000);
await app.listen({ port: cfg.port, host: "0.0.0.0" });
}
main().catch((err) => {
console.error(err);
process.exit(1);
});