Scaffold Homelab Manager foundation

Monorepo (Express+TS+Drizzle/libSQL server, React+Vite+Tabler web) matching
the stack used by ScheduleTaskManager and Sloth Manager. Includes Authentik
OIDC login with local admin/operator/viewer roles (first user becomes admin),
a generalized audit log, encrypted-at-rest storage for future integration API
tokens, the DB schema for all planned modules, and the Tabler-styled app
shell/nav. Also ports the Secrets (expiry tracker) and IP Addresses (IPAM)
modules from Sloth Manager onto the new stack.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
bobbanandClaude Sonnet 5 committed 2026-09-14 21:57:13 +02:00
commit 6bd2ed52c1
52 files changed
+8103

No files matched your search

+24
View File
@@ -0,0 +1,24 @@
import { db } from "../db/client.js";
import { auditLog, users } from "../db/schema.js";
type CurrentUser = typeof users.$inferSelect;
/** Records one audit-log entry. Call this from any route that mutates state or takes an action. */
export async function recordAudit(params: {
actor: CurrentUser;
category: string;
action: string;
targetType?: string;
targetId?: string | number;
detail?: unknown;
}) {
await db.insert(auditLog).values({
actorUserId: params.actor.id,
actorLabel: params.actor.name ?? params.actor.email ?? params.actor.oidcSub,
category: params.category,
action: params.action,
targetType: params.targetType,
targetId: params.targetId !== undefined ? String(params.targetId) : undefined,
detail: params.detail !== undefined ? JSON.stringify(params.detail) : undefined,
});
}
+19
View File
@@ -0,0 +1,19 @@
export type SecretStatus = "ok" | "expiring" | "expired";
/** Computes status + days-left for a secret, matching Sloth Manager's original semantics. */
export function computeSecretStatus(
expiryDate: string,
warnDays: number,
now: Date = new Date(),
): { status: SecretStatus; daysLeft: number } {
const expiry = new Date(`${expiryDate}T00:00:00Z`);
const today = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
const daysLeft = Math.round((expiry.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
let status: SecretStatus;
if (daysLeft < 0) status = "expired";
else if (daysLeft <= warnDays) status = "expiring";
else status = "ok";
return { status, daysLeft };
}