feat: scaffold PulseNode dashboard (M1)

Next.js 16 + TypeScript + Tailwind v4 app with a YAML-driven config
system: schema validation (zod), .env interpolation, and a widget
registry rendering bookmark/search/group widgets. CSS custom
properties (--pn-*) drive theming and are overridable from
config.yml's theme.variables. Config load errors surface as a
readable error page instead of crashing the app.

Hot reload, docker/system/http collectors, and the WebSocket push
layer land in later milestones per the approved plan.
This commit is contained in:
2026-08-17 13:49:55 +02:00
commit 80c2362ac2
27 changed files with 5729 additions and 0 deletions
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+49
View File
@@ -0,0 +1,49 @@
@import "tailwindcss";
:root {
--pn-bg: #0b0d12;
--pn-surface: #12151c;
--pn-surface-raised: #171b24;
--pn-border: #242938;
--pn-fg: #e6e8ee;
--pn-fg-muted: #8a90a2;
--pn-accent: #38bdf8;
--pn-radius: 0.75rem;
--pn-status-up: #34d399;
--pn-status-down: #f87171;
--pn-status-degraded: #fbbf24;
}
:root[data-theme="light"] {
--pn-bg: #f5f6f8;
--pn-surface: #ffffff;
--pn-surface-raised: #f0f1f4;
--pn-border: #e2e4ea;
--pn-fg: #14161c;
--pn-fg-muted: #666b7a;
--pn-accent: #0284c7;
--pn-status-up: #059669;
--pn-status-down: #dc2626;
--pn-status-degraded: #d97706;
}
@theme inline {
--color-bg: var(--pn-bg);
--color-surface: var(--pn-surface);
--color-surface-raised: var(--pn-surface-raised);
--color-border: var(--pn-border);
--color-fg: var(--pn-fg);
--color-fg-muted: var(--pn-fg-muted);
--color-accent: var(--pn-accent);
--color-status-up: var(--pn-status-up);
--color-status-down: var(--pn-status-down);
--color-status-degraded: var(--pn-status-degraded);
--radius-widget: var(--pn-radius);
}
body {
background: var(--pn-bg);
color: var(--pn-fg);
font-family:
ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}
+31
View File
@@ -0,0 +1,31 @@
import type { Metadata } from "next";
import "./globals.css";
import { configStore } from "@/lib/config/loader";
import { ThemeStyleInjector } from "@/components/layout/ThemeStyleInjector";
export const metadata: Metadata = {
title: "PulseNode",
description: "Self-hosted infrastructure dashboard",
};
export default function RootLayout({ children }: LayoutProps<"/">) {
let mode: "dark" | "light" = "dark";
let variables: Record<string, string> = {};
try {
const config = configStore.get();
mode = config.theme.mode === "light" ? "light" : "dark";
variables = config.theme.variables;
} catch {
// page.tsx surfaces the actual config error; layout falls back to defaults
}
return (
<html lang="en" data-theme={mode} className="h-full">
<head>
<ThemeStyleInjector variables={variables} />
</head>
<body className="min-h-full antialiased">{children}</body>
</html>
);
}
+23
View File
@@ -0,0 +1,23 @@
import { configStore, ConfigError } from "@/lib/config/loader";
import { Dashboard } from "@/components/layout/Dashboard";
export const dynamic = "force-dynamic";
export default function Home() {
let config;
try {
config = configStore.get();
} catch (err) {
const message = err instanceof ConfigError ? err.message : "Unknown configuration error";
return (
<main className="mx-auto flex max-w-2xl flex-col gap-4 px-6 py-16">
<h1 className="text-lg font-semibold text-status-down">Configuration Error</h1>
<pre className="rounded-[var(--radius-widget)] border border-status-down bg-surface p-4 text-sm whitespace-pre-wrap text-fg">
{message}
</pre>
</main>
);
}
return <Dashboard config={config} />;
}