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(), }))