Sanitized mirror from private repository - 2026-04-05 10:01:52 UTC
Some checks failed
Documentation / Deploy to GitHub Pages (push) Has been cancelled
Documentation / Build Docusaurus (push) Has been cancelled

This commit is contained in:
Gitea Mirror Bot
2026-04-05 10:01:52 +00:00
commit b85d91113b
1394 changed files with 355699 additions and 0 deletions

17
dashboard/ui/lib/api.ts Normal file
View File

@@ -0,0 +1,17 @@
// In the browser, API calls go to the same origin (Next.js rewrites to backend).
// On the server, they go directly to the backend.
const API = typeof window === "undefined"
? (process.env.BACKEND_URL || "http://localhost:18888")
: "";
export async function fetchAPI<T>(path: string): Promise<T> {
const res = await fetch(`${API}${path}`);
if (!res.ok) throw new Error(`API error: ${res.status}`);
return res.json();
}
export async function postAPI<T>(path: string): Promise<T> {
const res = await fetch(`${API}${path}`, { method: "POST" });
if (!res.ok) throw new Error(`API error: ${res.status}`);
return res.json();
}

442
dashboard/ui/lib/themes.ts Normal file
View File

@@ -0,0 +1,442 @@
export interface Theme {
name: string;
label: string;
isDark: boolean;
bgGradient: string;
bodyBg: string;
// Preview swatch colors for the switcher
swatch: [string, string];
vars: Record<string, string>;
}
export const themes: Theme[] = [
// 1. Midnight (default)
{
name: "midnight",
label: "Midnight",
isDark: true,
swatch: ["#3b82f6", "#8b5cf6"],
bodyBg: "linear-gradient(135deg, #060611 0%, #0d1117 40%, #0a0e1a 100%)",
bgGradient:
"radial-gradient(ellipse 80% 50% at 50% -20%, rgba(56, 100, 220, 0.18), transparent), radial-gradient(ellipse 60% 40% at 80% 50%, rgba(139, 92, 246, 0.1), transparent), radial-gradient(ellipse 50% 50% at 20% 80%, rgba(16, 185, 129, 0.07), transparent)",
vars: {
"--background": "230 25% 4%",
"--foreground": "210 40% 93%",
"--card": "220 30% 8% / 0.4",
"--card-foreground": "210 40% 93%",
"--popover": "220 30% 8% / 0.8",
"--popover-foreground": "210 40% 93%",
"--primary": "217 91% 60%",
"--primary-foreground": "210 40% 93%",
"--secondary": "217 33% 12% / 0.5",
"--secondary-foreground": "210 40% 93%",
"--muted": "217 33% 12% / 0.5",
"--muted-foreground": "215 20% 55%",
"--accent": "217 33% 12% / 0.5",
"--accent-foreground": "210 40% 93%",
"--destructive": "0 84% 60%",
"--border": "0 0% 100% / 0.08",
"--input": "0 0% 100% / 0.06",
"--ring": "217 91% 60%",
"--card-bg": "rgba(255, 255, 255, 0.04)",
"--card-border": "rgba(255, 255, 255, 0.08)",
"--card-hover-bg": "rgba(255, 255, 255, 0.07)",
"--card-hover-border": "rgba(255, 255, 255, 0.14)",
"--glass-bg": "rgba(255, 255, 255, 0.03)",
"--glass-border": "rgba(255, 255, 255, 0.06)",
"--glass-hover": "rgba(255, 255, 255, 0.03)",
"--glass-input-bg": "rgba(255, 255, 255, 0.03)",
"--glass-input-border": "rgba(255, 255, 255, 0.08)",
"--glass-input-focus": "rgba(59, 130, 246, 0.3)",
"--glass-input-focus-bg": "rgba(255, 255, 255, 0.05)",
"--glass-table-header": "rgba(255, 255, 255, 0.04)",
"--glass-bar-track": "rgba(255, 255, 255, 0.05)",
"--nav-bg": "rgba(255, 255, 255, 0.02)",
"--nav-border": "rgba(255, 255, 255, 0.05)",
"--nav-active": "rgba(255, 255, 255, 0.06)",
"--nav-hover": "rgba(255, 255, 255, 0.04)",
"--accent-color": "#3b82f6",
"--accent-glow": "rgba(59, 130, 246, 0.3)",
"--card-lift-shadow": "0 8px 40px rgba(0, 0, 0, 0.3), 0 0 40px rgba(59, 130, 246, 0.04)",
"--stat-glow": "0 0 20px rgba(59, 130, 246, 0.15)",
"--nav-active-glow": "0 2px 10px rgba(59, 130, 246, 0.3)",
},
},
// 2. Light
{
name: "light",
label: "Light",
isDark: false,
swatch: ["#2563eb", "#e2e8f0"],
bodyBg: "linear-gradient(135deg, #fafafa 0%, #f1f5f9 40%, #f8fafc 100%)",
bgGradient:
"radial-gradient(ellipse 80% 50% at 50% -20%, rgba(37, 99, 235, 0.06), transparent), radial-gradient(ellipse 60% 40% at 80% 50%, rgba(99, 102, 241, 0.04), transparent)",
vars: {
"--background": "210 20% 98%",
"--foreground": "215 25% 15%",
"--card": "0 0% 100%",
"--card-foreground": "215 25% 15%",
"--popover": "0 0% 100%",
"--popover-foreground": "215 25% 15%",
"--primary": "217 91% 53%",
"--primary-foreground": "0 0% 100%",
"--secondary": "214 32% 91%",
"--secondary-foreground": "215 25% 15%",
"--muted": "214 32% 91%",
"--muted-foreground": "215 16% 47%",
"--accent": "214 32% 91%",
"--accent-foreground": "215 25% 15%",
"--destructive": "0 84% 60%",
"--border": "214 32% 88%",
"--input": "214 32% 88%",
"--ring": "217 91% 53%",
"--card-bg": "rgba(255, 255, 255, 0.9)",
"--card-border": "rgba(0, 0, 0, 0.08)",
"--card-hover-bg": "rgba(255, 255, 255, 1)",
"--card-hover-border": "rgba(0, 0, 0, 0.12)",
"--glass-bg": "rgba(255, 255, 255, 0.7)",
"--glass-border": "rgba(0, 0, 0, 0.06)",
"--glass-hover": "rgba(0, 0, 0, 0.02)",
"--glass-input-bg": "rgba(255, 255, 255, 0.8)",
"--glass-input-border": "rgba(0, 0, 0, 0.1)",
"--glass-input-focus": "rgba(37, 99, 235, 0.3)",
"--glass-input-focus-bg": "rgba(255, 255, 255, 0.95)",
"--glass-table-header": "rgba(0, 0, 0, 0.03)",
"--glass-bar-track": "rgba(0, 0, 0, 0.06)",
"--nav-bg": "rgba(255, 255, 255, 0.8)",
"--nav-border": "rgba(0, 0, 0, 0.06)",
"--nav-active": "rgba(0, 0, 0, 0.05)",
"--nav-hover": "rgba(0, 0, 0, 0.03)",
"--accent-color": "#2563eb",
"--accent-glow": "rgba(37, 99, 235, 0.2)",
"--card-lift-shadow": "0 8px 40px rgba(0, 0, 0, 0.08), 0 0 40px rgba(37, 99, 235, 0.02)",
"--stat-glow": "0 0 20px rgba(37, 99, 235, 0.08)",
"--nav-active-glow": "0 2px 10px rgba(37, 99, 235, 0.15)",
},
},
// 3. Cyberpunk
{
name: "cyberpunk",
label: "Cyberpunk",
isDark: true,
swatch: ["#ec4899", "#06b6d4"],
bodyBg: "linear-gradient(135deg, #0a0a0f 0%, #0d0515 40%, #05080f 100%)",
bgGradient:
"radial-gradient(ellipse 80% 50% at 50% -20%, rgba(236, 72, 153, 0.15), transparent), radial-gradient(ellipse 60% 40% at 80% 50%, rgba(6, 182, 212, 0.12), transparent), radial-gradient(ellipse 50% 50% at 20% 80%, rgba(236, 72, 153, 0.06), transparent)",
vars: {
"--background": "260 30% 4%",
"--foreground": "185 20% 92%",
"--card": "270 30% 8% / 0.4",
"--card-foreground": "185 20% 92%",
"--popover": "270 30% 8% / 0.8",
"--popover-foreground": "185 20% 92%",
"--primary": "330 80% 60%",
"--primary-foreground": "185 20% 95%",
"--secondary": "270 30% 14% / 0.5",
"--secondary-foreground": "185 20% 92%",
"--muted": "270 30% 14% / 0.5",
"--muted-foreground": "200 15% 52%",
"--accent": "185 80% 45%",
"--accent-foreground": "185 20% 95%",
"--destructive": "330 80% 55%",
"--border": "0 0% 100% / 0.06",
"--input": "0 0% 100% / 0.06",
"--ring": "330 80% 60%",
"--card-bg": "rgba(255, 255, 255, 0.03)",
"--card-border": "rgba(236, 72, 153, 0.1)",
"--card-hover-bg": "rgba(255, 255, 255, 0.06)",
"--card-hover-border": "rgba(236, 72, 153, 0.2)",
"--glass-bg": "rgba(255, 255, 255, 0.02)",
"--glass-border": "rgba(236, 72, 153, 0.06)",
"--glass-hover": "rgba(236, 72, 153, 0.03)",
"--glass-input-bg": "rgba(255, 255, 255, 0.03)",
"--glass-input-border": "rgba(236, 72, 153, 0.1)",
"--glass-input-focus": "rgba(6, 182, 212, 0.3)",
"--glass-input-focus-bg": "rgba(255, 255, 255, 0.05)",
"--glass-table-header": "rgba(236, 72, 153, 0.04)",
"--glass-bar-track": "rgba(255, 255, 255, 0.05)",
"--nav-bg": "rgba(10, 10, 15, 0.6)",
"--nav-border": "rgba(236, 72, 153, 0.08)",
"--nav-active": "rgba(236, 72, 153, 0.08)",
"--nav-hover": "rgba(236, 72, 153, 0.04)",
"--accent-color": "#ec4899",
"--accent-glow": "rgba(236, 72, 153, 0.3)",
"--card-lift-shadow": "0 8px 40px rgba(0, 0, 0, 0.3), 0 0 40px rgba(236, 72, 153, 0.06)",
"--stat-glow": "0 0 20px rgba(6, 182, 212, 0.15)",
"--nav-active-glow": "0 2px 10px rgba(236, 72, 153, 0.3)",
},
},
// 4. Steampunk
{
name: "steampunk",
label: "Steampunk",
isDark: true,
swatch: ["#d4a76a", "#b87333"],
bodyBg: "linear-gradient(135deg, #0f0a07 0%, #12100b 40%, #0e0905 100%)",
bgGradient:
"radial-gradient(ellipse 80% 50% at 50% -20%, rgba(212, 167, 106, 0.12), transparent), radial-gradient(ellipse 60% 40% at 80% 50%, rgba(184, 115, 51, 0.1), transparent), radial-gradient(ellipse 50% 50% at 20% 80%, rgba(245, 158, 11, 0.05), transparent)",
vars: {
"--background": "30 30% 4%",
"--foreground": "40 30% 88%",
"--card": "30 25% 10% / 0.4",
"--card-foreground": "40 30% 88%",
"--popover": "30 25% 10% / 0.8",
"--popover-foreground": "40 30% 88%",
"--primary": "35 55% 62%",
"--primary-foreground": "30 40% 10%",
"--secondary": "30 20% 14% / 0.5",
"--secondary-foreground": "40 30% 88%",
"--muted": "30 20% 14% / 0.5",
"--muted-foreground": "35 15% 50%",
"--accent": "25 60% 46%",
"--accent-foreground": "40 30% 92%",
"--destructive": "15 70% 50%",
"--border": "35 30% 50% / 0.08",
"--input": "35 30% 50% / 0.06",
"--ring": "35 55% 62%",
"--card-bg": "rgba(212, 167, 106, 0.03)",
"--card-border": "rgba(212, 167, 106, 0.08)",
"--card-hover-bg": "rgba(212, 167, 106, 0.06)",
"--card-hover-border": "rgba(212, 167, 106, 0.14)",
"--glass-bg": "rgba(212, 167, 106, 0.02)",
"--glass-border": "rgba(212, 167, 106, 0.06)",
"--glass-hover": "rgba(212, 167, 106, 0.03)",
"--glass-input-bg": "rgba(212, 167, 106, 0.03)",
"--glass-input-border": "rgba(212, 167, 106, 0.1)",
"--glass-input-focus": "rgba(184, 115, 51, 0.3)",
"--glass-input-focus-bg": "rgba(212, 167, 106, 0.05)",
"--glass-table-header": "rgba(212, 167, 106, 0.04)",
"--glass-bar-track": "rgba(212, 167, 106, 0.06)",
"--nav-bg": "rgba(15, 10, 7, 0.6)",
"--nav-border": "rgba(212, 167, 106, 0.08)",
"--nav-active": "rgba(212, 167, 106, 0.08)",
"--nav-hover": "rgba(212, 167, 106, 0.04)",
"--accent-color": "#d4a76a",
"--accent-glow": "rgba(212, 167, 106, 0.3)",
"--card-lift-shadow": "0 8px 40px rgba(0, 0, 0, 0.3), 0 0 40px rgba(212, 167, 106, 0.04)",
"--stat-glow": "0 0 20px rgba(212, 167, 106, 0.15)",
"--nav-active-glow": "0 2px 10px rgba(212, 167, 106, 0.3)",
},
},
// 5. Portland
{
name: "portland",
label: "Portland",
isDark: true,
swatch: ["#15803d", "#0e7490"],
bodyBg: "linear-gradient(135deg, #060d08 0%, #081210 40%, #050b08 100%)",
bgGradient:
"radial-gradient(ellipse 80% 50% at 50% -20%, rgba(21, 128, 61, 0.14), transparent), radial-gradient(ellipse 60% 40% at 80% 50%, rgba(14, 116, 144, 0.1), transparent), radial-gradient(ellipse 50% 50% at 20% 80%, rgba(34, 197, 94, 0.05), transparent)",
vars: {
"--background": "140 25% 4%",
"--foreground": "140 15% 88%",
"--card": "150 20% 9% / 0.4",
"--card-foreground": "140 15% 88%",
"--popover": "150 20% 9% / 0.8",
"--popover-foreground": "140 15% 88%",
"--primary": "142 64% 36%",
"--primary-foreground": "140 20% 95%",
"--secondary": "150 18% 14% / 0.5",
"--secondary-foreground": "140 15% 88%",
"--muted": "150 18% 14% / 0.5",
"--muted-foreground": "150 10% 50%",
"--accent": "189 80% 32%",
"--accent-foreground": "140 20% 95%",
"--destructive": "25 80% 50%",
"--border": "140 20% 50% / 0.08",
"--input": "140 20% 50% / 0.06",
"--ring": "142 64% 36%",
"--card-bg": "rgba(21, 128, 61, 0.03)",
"--card-border": "rgba(21, 128, 61, 0.08)",
"--card-hover-bg": "rgba(21, 128, 61, 0.06)",
"--card-hover-border": "rgba(21, 128, 61, 0.14)",
"--glass-bg": "rgba(21, 128, 61, 0.02)",
"--glass-border": "rgba(21, 128, 61, 0.06)",
"--glass-hover": "rgba(21, 128, 61, 0.03)",
"--glass-input-bg": "rgba(21, 128, 61, 0.03)",
"--glass-input-border": "rgba(21, 128, 61, 0.1)",
"--glass-input-focus": "rgba(14, 116, 144, 0.3)",
"--glass-input-focus-bg": "rgba(21, 128, 61, 0.05)",
"--glass-table-header": "rgba(21, 128, 61, 0.04)",
"--glass-bar-track": "rgba(21, 128, 61, 0.06)",
"--nav-bg": "rgba(6, 13, 8, 0.6)",
"--nav-border": "rgba(21, 128, 61, 0.08)",
"--nav-active": "rgba(21, 128, 61, 0.08)",
"--nav-hover": "rgba(21, 128, 61, 0.04)",
"--accent-color": "#15803d",
"--accent-glow": "rgba(21, 128, 61, 0.3)",
"--card-lift-shadow": "0 8px 40px rgba(0, 0, 0, 0.3), 0 0 40px rgba(21, 128, 61, 0.04)",
"--stat-glow": "0 0 20px rgba(21, 128, 61, 0.15)",
"--nav-active-glow": "0 2px 10px rgba(21, 128, 61, 0.3)",
},
},
// 6. Racing
{
name: "racing",
label: "Racing",
isDark: true,
swatch: ["#dc2626", "#a1a1aa"],
bodyBg: "linear-gradient(135deg, #080808 0%, #0c0c0c 40%, #070707 100%)",
bgGradient:
"radial-gradient(ellipse 80% 50% at 50% -20%, rgba(220, 38, 38, 0.12), transparent), radial-gradient(ellipse 60% 40% at 80% 50%, rgba(161, 161, 170, 0.06), transparent), radial-gradient(ellipse 50% 50% at 20% 80%, rgba(220, 38, 38, 0.04), transparent)",
vars: {
"--background": "0 0% 4%",
"--foreground": "0 0% 93%",
"--card": "0 0% 9% / 0.4",
"--card-foreground": "0 0% 93%",
"--popover": "0 0% 9% / 0.8",
"--popover-foreground": "0 0% 93%",
"--primary": "0 72% 51%",
"--primary-foreground": "0 0% 98%",
"--secondary": "0 0% 14% / 0.5",
"--secondary-foreground": "0 0% 93%",
"--muted": "0 0% 14% / 0.5",
"--muted-foreground": "0 0% 55%",
"--accent": "0 0% 63%",
"--accent-foreground": "0 0% 93%",
"--destructive": "0 72% 51%",
"--border": "0 0% 100% / 0.06",
"--input": "0 0% 100% / 0.06",
"--ring": "0 72% 51%",
"--card-bg": "rgba(255, 255, 255, 0.03)",
"--card-border": "rgba(220, 38, 38, 0.08)",
"--card-hover-bg": "rgba(255, 255, 255, 0.05)",
"--card-hover-border": "rgba(220, 38, 38, 0.16)",
"--glass-bg": "rgba(255, 255, 255, 0.02)",
"--glass-border": "rgba(220, 38, 38, 0.05)",
"--glass-hover": "rgba(220, 38, 38, 0.03)",
"--glass-input-bg": "rgba(255, 255, 255, 0.03)",
"--glass-input-border": "rgba(255, 255, 255, 0.08)",
"--glass-input-focus": "rgba(220, 38, 38, 0.3)",
"--glass-input-focus-bg": "rgba(255, 255, 255, 0.05)",
"--glass-table-header": "rgba(255, 255, 255, 0.04)",
"--glass-bar-track": "rgba(255, 255, 255, 0.05)",
"--nav-bg": "rgba(8, 8, 8, 0.7)",
"--nav-border": "rgba(220, 38, 38, 0.08)",
"--nav-active": "rgba(220, 38, 38, 0.08)",
"--nav-hover": "rgba(220, 38, 38, 0.04)",
"--accent-color": "#dc2626",
"--accent-glow": "rgba(220, 38, 38, 0.3)",
"--card-lift-shadow": "0 8px 40px rgba(0, 0, 0, 0.3), 0 0 40px rgba(220, 38, 38, 0.04)",
"--stat-glow": "0 0 20px rgba(220, 38, 38, 0.15)",
"--nav-active-glow": "0 2px 10px rgba(220, 38, 38, 0.3)",
},
},
// 7. Ocean
{
name: "ocean",
label: "Ocean",
isDark: true,
swatch: ["#0284c7", "#2dd4bf"],
bodyBg: "linear-gradient(135deg, #030712 0%, #0a1628 40%, #041020 100%)",
bgGradient:
"radial-gradient(ellipse 80% 50% at 50% -20%, rgba(2, 132, 199, 0.15), transparent), radial-gradient(ellipse 60% 40% at 80% 50%, rgba(45, 212, 191, 0.08), transparent), radial-gradient(ellipse 50% 50% at 20% 80%, rgba(2, 132, 199, 0.06), transparent)",
vars: {
"--background": "220 50% 4%",
"--foreground": "200 20% 90%",
"--card": "215 40% 9% / 0.4",
"--card-foreground": "200 20% 90%",
"--popover": "215 40% 9% / 0.8",
"--popover-foreground": "200 20% 90%",
"--primary": "200 80% 44%",
"--primary-foreground": "200 20% 95%",
"--secondary": "210 30% 14% / 0.5",
"--secondary-foreground": "200 20% 90%",
"--muted": "210 30% 14% / 0.5",
"--muted-foreground": "200 15% 50%",
"--accent": "170 70% 50%",
"--accent-foreground": "200 20% 95%",
"--destructive": "10 70% 55%",
"--border": "200 30% 50% / 0.08",
"--input": "200 30% 50% / 0.06",
"--ring": "200 80% 44%",
"--card-bg": "rgba(2, 132, 199, 0.03)",
"--card-border": "rgba(2, 132, 199, 0.08)",
"--card-hover-bg": "rgba(2, 132, 199, 0.06)",
"--card-hover-border": "rgba(2, 132, 199, 0.14)",
"--glass-bg": "rgba(2, 132, 199, 0.02)",
"--glass-border": "rgba(2, 132, 199, 0.06)",
"--glass-hover": "rgba(2, 132, 199, 0.03)",
"--glass-input-bg": "rgba(2, 132, 199, 0.03)",
"--glass-input-border": "rgba(2, 132, 199, 0.1)",
"--glass-input-focus": "rgba(45, 212, 191, 0.3)",
"--glass-input-focus-bg": "rgba(2, 132, 199, 0.05)",
"--glass-table-header": "rgba(2, 132, 199, 0.04)",
"--glass-bar-track": "rgba(2, 132, 199, 0.06)",
"--nav-bg": "rgba(3, 7, 18, 0.6)",
"--nav-border": "rgba(2, 132, 199, 0.08)",
"--nav-active": "rgba(2, 132, 199, 0.08)",
"--nav-hover": "rgba(2, 132, 199, 0.04)",
"--accent-color": "#0284c7",
"--accent-glow": "rgba(2, 132, 199, 0.3)",
"--card-lift-shadow": "0 8px 40px rgba(0, 0, 0, 0.3), 0 0 40px rgba(2, 132, 199, 0.04)",
"--stat-glow": "0 0 20px rgba(45, 212, 191, 0.15)",
"--nav-active-glow": "0 2px 10px rgba(2, 132, 199, 0.3)",
},
},
// 8. Aurora
{
name: "aurora",
label: "Aurora",
isDark: true,
swatch: ["#4ade80", "#a78bfa"],
bodyBg: "linear-gradient(135deg, #050510 0%, #080818 40%, #050510 100%)",
bgGradient:
"radial-gradient(ellipse 90% 60% at 30% -10%, rgba(74, 222, 128, 0.12), transparent), radial-gradient(ellipse 70% 50% at 70% 30%, rgba(167, 139, 250, 0.1), transparent), radial-gradient(ellipse 60% 40% at 50% 80%, rgba(56, 189, 248, 0.08), transparent), radial-gradient(ellipse 40% 30% at 80% 60%, rgba(74, 222, 128, 0.05), transparent)",
vars: {
"--background": "240 33% 4%",
"--foreground": "220 15% 88%",
"--card": "240 25% 9% / 0.4",
"--card-foreground": "220 15% 88%",
"--popover": "240 25% 9% / 0.8",
"--popover-foreground": "220 15% 88%",
"--primary": "142 70% 58%",
"--primary-foreground": "240 20% 8%",
"--secondary": "240 20% 14% / 0.5",
"--secondary-foreground": "220 15% 88%",
"--muted": "240 20% 14% / 0.5",
"--muted-foreground": "230 12% 52%",
"--accent": "263 70% 72%",
"--accent-foreground": "220 15% 95%",
"--destructive": "45 90% 55%",
"--border": "260 20% 60% / 0.08",
"--input": "260 20% 60% / 0.06",
"--ring": "142 70% 58%",
"--card-bg": "rgba(74, 222, 128, 0.02)",
"--card-border": "rgba(167, 139, 250, 0.08)",
"--card-hover-bg": "rgba(74, 222, 128, 0.04)",
"--card-hover-border": "rgba(167, 139, 250, 0.14)",
"--glass-bg": "rgba(167, 139, 250, 0.02)",
"--glass-border": "rgba(167, 139, 250, 0.06)",
"--glass-hover": "rgba(74, 222, 128, 0.03)",
"--glass-input-bg": "rgba(167, 139, 250, 0.03)",
"--glass-input-border": "rgba(167, 139, 250, 0.1)",
"--glass-input-focus": "rgba(74, 222, 128, 0.3)",
"--glass-input-focus-bg": "rgba(167, 139, 250, 0.05)",
"--glass-table-header": "rgba(167, 139, 250, 0.04)",
"--glass-bar-track": "rgba(167, 139, 250, 0.06)",
"--nav-bg": "rgba(5, 5, 16, 0.6)",
"--nav-border": "rgba(167, 139, 250, 0.08)",
"--nav-active": "rgba(74, 222, 128, 0.06)",
"--nav-hover": "rgba(167, 139, 250, 0.04)",
"--accent-color": "#4ade80",
"--accent-glow": "rgba(74, 222, 128, 0.3)",
"--card-lift-shadow": "0 8px 40px rgba(0, 0, 0, 0.3), 0 0 40px rgba(167, 139, 250, 0.04)",
"--stat-glow": "0 0 20px rgba(74, 222, 128, 0.15)",
"--nav-active-glow": "0 2px 10px rgba(74, 222, 128, 0.3)",
},
},
];
export const DEFAULT_THEME = "midnight";
export function getTheme(name: string): Theme {
return themes.find((t) => t.name === name) ?? themes[0];
}

80
dashboard/ui/lib/types.ts Normal file
View File

@@ -0,0 +1,80 @@
// Types aligned with the actual API response from /api/stats/overview
export interface OverviewStats {
containers: {
total: number;
running?: number;
endpoints?: Record<string, { total: number; running: number; error?: boolean }>;
by_endpoint?: Record<string, { total: number; running: number; error?: boolean }>;
};
gpu: {
available: boolean;
name?: string;
temp_c?: number;
// API may use either naming convention
power_w?: number;
power_draw_w?: number;
power_limit_w?: number;
vram_used_mb?: number;
vram_total_mb?: number;
memory_used_mb?: number;
memory_total_mb?: number;
utilization_pct?: number;
};
// API returns either a number or {gmail, dvish, proton, total}
emails_today?: number | Record<string, number>;
email_today?: number | Record<string, number>;
alerts?: number;
unhealthy_count?: number;
// API returns either an object or a boolean
ollama?: { available: boolean; url: string };
ollama_available?: boolean;
hosts_online?: number;
}
export interface ActivityEvent {
type: string;
timestamp: string;
source: string;
raw: string;
[key: string]: unknown;
}
export interface Container {
id: string;
name: string;
image: string;
state: string;
status: string;
endpoint: string;
error?: string;
}
export interface JellyfinStatus {
version: string;
server_name: string;
libraries: { name: string; type: string; paths: string[] }[];
active_sessions: {
user: string;
device: string;
client: string;
title: string;
type: string;
}[];
idle_sessions: number;
}
export interface EmailStats {
accounts: {
account: string;
today: number;
categories: Record<string, number>;
}[];
sender_cache: Record<string, number>;
}
export interface ExpenseSummary {
month: string;
total: number;
count: number;
top_vendors: { vendor: string; amount: number }[];
}

View File

@@ -0,0 +1,9 @@
import useSWR from "swr";
import { fetchAPI } from "./api";
export function usePoll<T>(path: string, interval: number = 60000) {
return useSWR<T>(path, () => fetchAPI<T>(path), {
refreshInterval: interval,
revalidateOnFocus: false,
});
}

View File

@@ -0,0 +1,52 @@
"use client";
import { useState, useEffect, useRef } from "react";
import type { ActivityEvent } from "./types";
// Use same origin — Next.js rewrites /api/* to backend
export function useSSE(path: string, maxEvents: number = 30) {
const [events, setEvents] = useState<ActivityEvent[]>([]);
const retryTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
let es: EventSource | null = null;
function connect() {
es = new EventSource(path);
// Backend sends "init" with the full batch and "update" with new events
es.addEventListener("init", (e: MessageEvent) => {
try {
const batch: ActivityEvent[] = JSON.parse(e.data);
setEvents(batch.slice(0, maxEvents));
} catch {
// ignore malformed events
}
});
es.addEventListener("update", (e: MessageEvent) => {
try {
const batch: ActivityEvent[] = JSON.parse(e.data);
setEvents((prev) => [...batch, ...prev].slice(0, maxEvents));
} catch {
// ignore malformed events
}
});
es.onerror = () => {
es?.close();
retryTimeout.current = setTimeout(connect, 5000);
};
}
connect();
return () => {
es?.close();
if (retryTimeout.current) clearTimeout(retryTimeout.current);
};
}, [path, maxEvents]);
return events;
}

View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}