From 146c17abd8bca9c2996acd4105909ba04c546841 Mon Sep 17 00:00:00 2001 From: NixReaper Date: Sat, 11 Apr 2026 14:13:43 -0500 Subject: [PATCH] feat(dashboard): add cPanel-style server info bar and bump to v0.4.4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a compact info strip at the top of the NixPanel dashboard showing Username, Hostname, OS, NixPanel Version, Load Averages (live via WebSocket), and Server Monitoring alert count. Alert count is derived from GET /api/nixserver/services — green "All Clear" when all services are running, amber "N Alerts" for any stopped/failed service. Also fixes the version footer error state in Layout.tsx (versionError → "Version unavailable" fallback with retry button) so the spinner no longer hangs indefinitely when the version API is unreachable. --- CHANGELOG.md | 7 +++ nixserver/src/components/Layout.tsx | 15 ++++- nixserver/src/pages/Dashboard.tsx | 98 ++++++++++++++++++++++++++--- package.json | 2 +- server/src/app.ts | 2 +- 5 files changed, 110 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 925de9b..7a8dc1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ Pre-1.0 versions treat MINOR as feature releases and PATCH as bug fixes. --- +## [0.4.4] — 2026-04-11 + +### New Features +- **Server info bar on Dashboard** — cPanel/WHM-style info strip at the top of the NixPanel home page. Shows Username, Hostname, OS, NixPanel Version, Load Averages (live from WebSocket), and Server Monitoring alert count. Alerts are colour-coded: green "All Clear" when all managed services are running; amber "N Alerts" with a count of stopped/failed services. + +--- + ## [0.4.3] — 2026-04-11 ### New Features diff --git a/nixserver/src/components/Layout.tsx b/nixserver/src/components/Layout.tsx index 55ca917..913bc55 100644 --- a/nixserver/src/components/Layout.tsx +++ b/nixserver/src/components/Layout.tsx @@ -462,13 +462,17 @@ export default function Layout() { // ── Version / upgrade state ────────────────────────────────────────────── type VersionInfo = { currentVersion: string; latestVersion: string | null; updateAvailable: boolean } const [versionInfo, setVersionInfo] = useState(null) + const [versionError, setVersionError] = useState(false) const [upgradeState, setUpgradeState] = useState<'idle' | 'confirm' | 'running' | 'done' | 'error'>('idle') const fetchVersion = useCallback(async () => { + setVersionError(false) try { - const { data } = await axios.get('/api/nixserver/system/version') + const { data } = await axios.get('/api/nixserver/system/version', { timeout: 10_000 }) setVersionInfo(data.data) - } catch { /* non-fatal */ } + } catch { + setVersionError(true) + } }, []) useEffect(() => { fetchVersion() }, [fetchVersion]) @@ -726,6 +730,13 @@ export default function Layout() { )} + ) : versionError ? ( +
+ Version unavailable + +
) : (
diff --git a/nixserver/src/pages/Dashboard.tsx b/nixserver/src/pages/Dashboard.tsx index ef58bf7..6881290 100644 --- a/nixserver/src/pages/Dashboard.tsx +++ b/nixserver/src/pages/Dashboard.tsx @@ -1,7 +1,8 @@ import React, { useEffect, useState, useRef } from 'react' -import { Users, Server, HardDrive, Activity } from 'lucide-react' +import { Users, Server, HardDrive, Activity, AlertTriangle, CheckCircle2 } from 'lucide-react' import { AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts' import { api } from '../api/client' +import { useAuth } from '../contexts/AuthContext' interface SystemStats { cpu: number @@ -18,6 +19,17 @@ interface AccountStats { terminated: number } +interface SystemInfo { + hostname: string + os: string +} + +interface ServiceStatus { + name: string + status: 'running' | 'stopped' | 'failed' | 'unknown' + enabled: boolean +} + interface StatCard { label: string value: string | number @@ -31,15 +43,51 @@ const formatBytes = (bytes: number) => { return gb >= 1 ? `${gb.toFixed(1)} GB` : `${(bytes / 1024 / 1024).toFixed(0)} MB` } +// ─── Server Info Bar ───────────────────────────────────────────────────────── + +interface InfoCellProps { + label: string + value: React.ReactNode +} + +function InfoCell({ label, value }: InfoCellProps) { + return ( +
+ + {label} + + + {value} + +
+ ) +} + +function Divider() { + return
+} + +// ─── Component ─────────────────────────────────────────────────────────────── + export default function Dashboard() { + const { user } = useAuth() const [stats, setStats] = useState(null) const [accountStats, setAccountStats] = useState(null) const [cpuHistory, setCpuHistory] = useState<{ t: string; cpu: number; mem: number }[]>([]) + const [systemInfo, setSystemInfo] = useState(null) + const [version, setVersion] = useState(null) + const [alertCount, setAlertCount] = useState(null) const wsRef = useRef(null) useEffect(() => { api.get('/nixserver/accounts/stats').then(r => setAccountStats(r.data.data)).catch(() => {}) api.get('/nixserver/system/stats').then(r => setStats(r.data.data)).catch(() => {}) + api.get('/nixserver/system/info').then(r => setSystemInfo(r.data.data)).catch(() => {}) + api.get('/nixserver/system/version').then(r => setVersion(r.data.data?.currentVersion ?? null)).catch(() => {}) + api.get('/nixserver/services').then(r => { + const services: ServiceStatus[] = r.data.data ?? [] + setAlertCount(services.filter(s => s.status !== 'running').length) + }).catch(() => { setAlertCount(0) }) // WebSocket live stats const token = localStorage.getItem('access_token') @@ -92,14 +140,44 @@ export default function Dashboard() { }, ] + const loadStr = stats + ? `${stats.load[0].toFixed(2)}\u00a0\u00a0${stats.load[1].toFixed(2)}\u00a0\u00a0${stats.load[2].toFixed(2)}` + : '…' + + const monitoringNode = + alertCount === null ? ( + … + ) : alertCount === 0 ? ( + + + All Clear + + ) : ( + + + {alertCount} Alert{alertCount !== 1 ? 's' : ''} + + ) + return (
-
-

Dashboard

-

Server overview and real-time metrics

+ + {/* ── cPanel-style server info bar ─────────────────────────────────── */} +
+ + + + + + + + + + +
- {/* Stat cards */} + {/* ── Stat cards ───────────────────────────────────────────────────── */}
{cards.map(card => (
@@ -113,7 +191,7 @@ export default function Dashboard() { ))}
- {/* CPU / Memory chart */} + {/* ── CPU / Memory chart ───────────────────────────────────────────── */}

CPU & Memory — Live

{cpuHistory.length > 0 ? ( @@ -146,15 +224,15 @@ export default function Dashboard() { )}
- {/* Account status breakdown */} + {/* ── Account status breakdown ─────────────────────────────────────── */} {accountStats && (

Account Status

{[ - { label: 'Active', value: accountStats.active, color: 'text-emerald-400' }, - { label: 'Suspended', value: accountStats.suspended, color: 'text-amber-400' }, - { label: 'Terminated', value: accountStats.terminated, color: 'text-red-400' }, + { label: 'Active', value: accountStats.active, color: 'text-emerald-400' }, + { label: 'Suspended', value: accountStats.suspended, color: 'text-amber-400' }, + { label: 'Terminated', value: accountStats.terminated, color: 'text-red-400' }, ].map(s => (

{s.value}

diff --git a/package.json b/package.json index bc3c0fa..c895fc9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nixpanel-server", - "version": "0.4.3", + "version": "0.4.4", "private": true, "workspaces": [ "server", diff --git a/server/src/app.ts b/server/src/app.ts index eba77e6..8cffc30 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -73,7 +73,7 @@ export async function buildApp() { fastify.get('/api/health', async () => ({ status: 'ok', - version: '0.4.3', + version: '0.4.4', timestamp: new Date().toISOString(), }))