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

+103
View File
@@ -0,0 +1,103 @@
import { useState, type ReactNode } from "react";
import { NavLink } from "react-router-dom";
import {
IconLayoutDashboard,
IconServer2,
IconWorld,
IconNetwork,
IconKey,
IconPlugConnected,
IconUsers,
IconHistory,
IconSettings,
IconMenu2,
} from "@tabler/icons-react";
import type { CurrentUser } from "../api/client";
interface NavItem {
to: string;
label: string;
icon: ReactNode;
minRole?: "operator" | "admin";
}
const NAV_ITEMS: NavItem[] = [
{ to: "/", label: "Dashboard", icon: <IconLayoutDashboard size={20} /> },
{ to: "/servers", label: "Servers & Tasks", icon: <IconServer2 size={20} /> },
{ to: "/dns", label: "DNS", icon: <IconWorld size={20} /> },
{ to: "/ipam", label: "IP Addresses", icon: <IconNetwork size={20} /> },
{ to: "/secrets", label: "Secrets", icon: <IconKey size={20} /> },
{ to: "/integrations", label: "Integrations", icon: <IconPlugConnected size={20} /> },
{ to: "/users", label: "Users", icon: <IconUsers size={20} />, minRole: "admin" },
{ to: "/audit-log", label: "Audit Log", icon: <IconHistory size={20} />, minRole: "operator" },
{ to: "/settings", label: "Settings", icon: <IconSettings size={20} />, minRole: "admin" },
];
const roleRank: Record<CurrentUser["role"], number> = { viewer: 0, operator: 1, admin: 2 };
export default function AppShell({ user, children }: { user: CurrentUser; children: ReactNode }) {
const [sidebarOpen, setSidebarOpen] = useState(false);
const visibleItems = NAV_ITEMS.filter(
(item) => !item.minRole || roleRank[user.role] >= roleRank[item.minRole],
);
return (
<div className="page">
<aside className="navbar navbar-vertical navbar-expand-lg" data-bs-theme="dark">
<div className="container-fluid">
<button
type="button"
className="navbar-toggler"
onClick={() => setSidebarOpen((open) => !open)}
aria-label="Toggle navigation"
>
<IconMenu2 size={20} />
</button>
<h1 className="navbar-brand navbar-brand-autodark">
<NavLink to="/">Homelab Manager</NavLink>
</h1>
<div className={`navbar-collapse collapse ${sidebarOpen ? "show" : ""}`}>
<ul className="navbar-nav pt-lg-3">
{visibleItems.map((item) => (
<li className="nav-item" key={item.to}>
<NavLink
to={item.to}
end={item.to === "/"}
className="nav-link"
onClick={() => setSidebarOpen(false)}
>
<span className="nav-link-icon d-md-none d-lg-inline-block">{item.icon}</span>
<span className="nav-link-title">{item.label}</span>
</NavLink>
</li>
))}
</ul>
</div>
</div>
</aside>
<div className="page-wrapper">
<div className="page-header d-print-none">
<div className="container-xl">
<div className="row g-2 align-items-center">
<div className="col">
<span className="text-secondary">
Signed in as {user.name ?? user.email ?? user.sub} · {user.role}
</span>
</div>
<div className="col-auto ms-auto d-print-none">
<a className="btn btn-outline-secondary btn-sm" href="/auth/logout">
Sign out
</a>
</div>
</div>
</div>
</div>
<div className="page-body">
<div className="container-xl">{children}</div>
</div>
</div>
</div>
);
}