04282232cc
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>
78 lines
2.3 KiB
TypeScript
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);
|
|
});
|