Sanitized mirror from private repository - 2026-04-05 10:01:52 UTC
This commit is contained in:
17
dashboard/ui/lib/api.ts
Normal file
17
dashboard/ui/lib/api.ts
Normal 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
442
dashboard/ui/lib/themes.ts
Normal 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
80
dashboard/ui/lib/types.ts
Normal 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 }[];
|
||||
}
|
||||
9
dashboard/ui/lib/use-poll.ts
Normal file
9
dashboard/ui/lib/use-poll.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
52
dashboard/ui/lib/use-sse.ts
Normal file
52
dashboard/ui/lib/use-sse.ts
Normal 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;
|
||||
}
|
||||
6
dashboard/ui/lib/utils.ts
Normal file
6
dashboard/ui/lib/utils.ts
Normal 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))
|
||||
}
|
||||
Reference in New Issue
Block a user