Scaffold homelab-monitor: Fastify API + React dashboard + CI
CI / web (push) Failing after 1m11s
CI / api (push) Successful in 1m19s

Vertical slice for Phase 1 (v1-dashboard milestone): Proxmox collector,
SQLite storage, local auth, and a dashboard UI showing host/container
status cards. Config-driven collector registry so future data sources
(SSH-based hosts, Zabbix, network discovery) plug in without rewiring.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-12 20:18:44 -06:00
commit a075488f4b
33 changed files with 5175 additions and 0 deletions
+66
View File
@@ -0,0 +1,66 @@
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 type { Collector } from "./collectors/types.js";
import { registerAuthRoutes } from "./routes/auth.js";
import { registerHostRoutes } from "./routes/hosts.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,
}),
];
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);
}
const app = Fastify({ logger: true });
await app.register(fastifyCookie);
await app.register(fastifySession, {
secret: cfg.sessionSecret,
cookie: { secure: process.env.NODE_ENV === "production", maxAge: 1000 * 60 * 60 * 12 },
});
registerAuthRoutes(app, db);
registerHostRoutes(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);
});