5 Commits
Author SHA1 Message Date
NixReaper 146c17abd8 feat(dashboard): add cPanel-style server info bar and bump to v0.4.4
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.
2026-04-11 14:13:43 -05:00
NixReaper 47ce924a90 fix(update): set web-readable permissions on dist dirs after rebuild 2026-04-11 12:09:31 -05:00
NixReaper 82b9ac30e9 fix(nixserver): widen Lucide icon size prop type to number | string 2026-04-11 12:06:58 -05:00
NixReaper c6f13f2e07 fix(update): use prisma db push instead of migrate deploy 2026-04-11 12:05:30 -05:00
NixReaper 945da97c97 fix(update): remove unsupported --silent flag from prisma generate 2026-04-11 12:04:30 -05:00
6 changed files with 119 additions and 17 deletions
+7
View File
@@ -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
+14 -3
View File
@@ -18,7 +18,7 @@ type NavItem = { label: string; to: string }
type NavCategory = {
id: string
label: string
icon: React.ComponentType<{ size?: number; className?: string }>
icon: React.ComponentType<{ size?: number | string; className?: string }>
items: NavItem[]
}
@@ -462,13 +462,17 @@ export default function Layout() {
// ── Version / upgrade state ──────────────────────────────────────────────
type VersionInfo = { currentVersion: string; latestVersion: string | null; updateAvailable: boolean }
const [versionInfo, setVersionInfo] = useState<VersionInfo | null>(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() {
</div>
)}
</div>
) : versionError ? (
<div className="flex items-center justify-between px-1 py-1">
<span className="text-[#4a5568] text-[10px]">Version unavailable</span>
<button onClick={fetchVersion} title="Retry" className="text-[#4a5568] hover:text-[#94a3b8] transition-colors">
<RefreshCcw size={10} />
</button>
</div>
) : (
<div className="flex items-center gap-1.5 px-1 py-1">
<Loader2 size={10} className="animate-spin text-[#4a5568]" />
+85 -7
View File
@@ -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 (
<div className="flex flex-col min-w-0">
<span className="text-[#4a5568] text-[10px] font-medium uppercase tracking-wide leading-none mb-0.5">
{label}
</span>
<span className="text-[#94a3b8] text-xs font-medium leading-none truncate">
{value}
</span>
</div>
)
}
function Divider() {
return <div className="w-px h-8 bg-[#2a2d3e] flex-shrink-0" />
}
// ─── Component ───────────────────────────────────────────────────────────────
export default function Dashboard() {
const { user } = useAuth()
const [stats, setStats] = useState<SystemStats | null>(null)
const [accountStats, setAccountStats] = useState<AccountStats | null>(null)
const [cpuHistory, setCpuHistory] = useState<{ t: string; cpu: number; mem: number }[]>([])
const [systemInfo, setSystemInfo] = useState<SystemInfo | null>(null)
const [version, setVersion] = useState<string | null>(null)
const [alertCount, setAlertCount] = useState<number | null>(null)
const wsRef = useRef<WebSocket | null>(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 ? (
<span className="text-[#4a5568]">…</span>
) : alertCount === 0 ? (
<span className="flex items-center gap-1 text-emerald-400">
<CheckCircle2 size={11} />
All Clear
</span>
) : (
<span className="flex items-center gap-1 text-amber-400">
<AlertTriangle size={11} />
{alertCount} Alert{alertCount !== 1 ? 's' : ''}
</span>
)
return (
<div className="space-y-6">
<div>
<h1 className="text-white text-xl font-semibold">Dashboard</h1>
<p className="text-[#64748b] text-sm mt-0.5">Server overview and real-time metrics</p>
{/* ── cPanel-style server info bar ─────────────────────────────────── */}
<div className="bg-[#1a1d27] border border-[#2a2d3e] rounded-xl px-5 py-3.5 flex flex-wrap items-center gap-x-5 gap-y-3">
<InfoCell label="Username" value={user?.username ?? '…'} />
<Divider />
<InfoCell label="Hostname" value={systemInfo?.hostname ?? '…'} />
<Divider />
<InfoCell label="OS" value={systemInfo?.os ?? '…'} />
<Divider />
<InfoCell label="NixPanel Version" value={version ? `v${version}` : '…'} />
<Divider />
<InfoCell label="Load Averages" value={loadStr} />
<Divider />
<InfoCell label="Server Monitoring" value={monitoringNode} />
</div>
{/* Stat cards */}
{/* ── Stat cards ───────────────────────────────────────────────────── */}
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4">
{cards.map(card => (
<div key={card.label} className="bg-[#1a1d27] border border-[#2a2d3e] rounded-xl p-4">
@@ -113,7 +191,7 @@ export default function Dashboard() {
))}
</div>
{/* CPU / Memory chart */}
{/* ── CPU / Memory chart ───────────────────────────────────────────── */}
<div className="bg-[#1a1d27] border border-[#2a2d3e] rounded-xl p-4">
<h2 className="text-white text-sm font-medium mb-4">CPU & Memory — Live</h2>
{cpuHistory.length > 0 ? (
@@ -146,7 +224,7 @@ export default function Dashboard() {
)}
</div>
{/* Account status breakdown */}
{/* ── Account status breakdown ─────────────────────────────────────── */}
{accountStats && (
<div className="bg-[#1a1d27] border border-[#2a2d3e] rounded-xl p-4">
<h2 className="text-white text-sm font-medium mb-4">Account Status</h2>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "nixpanel-server",
"version": "0.4.3",
"version": "0.4.4",
"private": true,
"workspaces": [
"server",
+8 -2
View File
@@ -24,14 +24,20 @@ success "Dependencies ready"
step "Running migrations"
cd "$INSTALL_DIR/server"
npx prisma generate --silent
npx prisma migrate deploy
npx prisma generate
npx prisma db push
success "Migrations applied"
step "Rebuilding frontends"
cd "$INSTALL_DIR"
npm run build --workspace=nixserver --silent
npm run build --workspace=nixclient --silent
# Ensure Apache (www-data) can read the rebuilt assets
chmod 755 "$INSTALL_DIR/nixserver" "$INSTALL_DIR/nixclient"
find "$INSTALL_DIR/nixserver/dist" -type d -exec chmod 755 {} \;
find "$INSTALL_DIR/nixserver/dist" -type f -exec chmod 644 {} \;
find "$INSTALL_DIR/nixclient/dist" -type d -exec chmod 755 {} \;
find "$INSTALL_DIR/nixclient/dist" -type f -exec chmod 644 {} \;
success "Frontends rebuilt"
step "Rebuilding server"
+1 -1
View File
@@ -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(),
}))