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
+30
View File
@@ -0,0 +1,30 @@
import type { FastifyInstance } from "fastify";
import type Database from "better-sqlite3";
import { verifyCredentials } from "../auth/local.js";
declare module "@fastify/session" {
interface FastifySessionObject {
username?: string;
}
}
export function registerAuthRoutes(app: FastifyInstance, db: Database.Database): void {
app.post<{ Body: { username: string; password: string } }>("/api/auth/login", async (req, reply) => {
const { username, password } = req.body ?? {};
if (!username || !password || !verifyCredentials(db, username, password)) {
return reply.code(401).send({ error: "invalid credentials" });
}
req.session.username = username;
return { username };
});
app.post("/api/auth/logout", async (req, reply) => {
await req.session.destroy();
return reply.send({ ok: true });
});
app.get("/api/auth/me", async (req, reply) => {
if (!req.session.username) return reply.code(401).send({ error: "not authenticated" });
return { username: req.session.username };
});
}
+32
View File
@@ -0,0 +1,32 @@
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import type Database from "better-sqlite3";
import { getLatestSnapshots } from "../db/index.js";
async function requireAuth(req: FastifyRequest, reply: FastifyReply) {
if (!req.session.username) {
reply.code(401).send({ error: "not authenticated" });
}
}
export function registerHostRoutes(app: FastifyInstance, db: Database.Database): void {
app.get("/api/hosts", { preHandler: requireAuth }, async () => {
const rows = getLatestSnapshots(db);
return {
hosts: rows.map((r) => ({
hostId: r.host_id,
displayName: r.display_name,
group: r.group_name,
status: r.status,
cpuPct: r.cpu_pct,
memPct: r.mem_pct,
diskPct: r.disk_pct,
memPressurePct: r.mem_pressure_pct,
cpuPressurePct: r.cpu_pressure_pct,
uptimeSec: r.uptime_sec,
lastSeen: r.ts,
})),
};
});
app.get("/api/health", async () => ({ ok: true }));
}