2026-04-05 10:45:21 -05:00
|
|
|
import React, { useEffect, useState, useRef } from 'react'
|
2026-04-11 14:24:51 -05:00
|
|
|
import { Users, Server, HardDrive, Activity, AlertTriangle, CheckCircle2, Sun, Moon } from 'lucide-react'
|
2026-04-05 10:45:21 -05:00
|
|
|
import { AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts'
|
|
|
|
|
import { api } from '../api/client'
|
2026-04-11 14:13:43 -05:00
|
|
|
import { useAuth } from '../contexts/AuthContext'
|
2026-04-11 14:24:51 -05:00
|
|
|
import { useTheme } from '../contexts/ThemeContext'
|
2026-04-05 10:45:21 -05:00
|
|
|
|
|
|
|
|
interface SystemStats {
|
|
|
|
|
cpu: number
|
|
|
|
|
memory: { total: number; used: number; free: number; percent: number }
|
|
|
|
|
disk: { total: number; used: number; free: number; percent: number }
|
|
|
|
|
load: [number, number, number]
|
|
|
|
|
uptime: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface AccountStats {
|
|
|
|
|
total: number
|
|
|
|
|
active: number
|
|
|
|
|
suspended: number
|
|
|
|
|
terminated: number
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 14:13:43 -05:00
|
|
|
interface SystemInfo {
|
|
|
|
|
hostname: string
|
|
|
|
|
os: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ServiceStatus {
|
|
|
|
|
name: string
|
|
|
|
|
status: 'running' | 'stopped' | 'failed' | 'unknown'
|
|
|
|
|
enabled: boolean
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 10:45:21 -05:00
|
|
|
interface StatCard {
|
|
|
|
|
label: string
|
|
|
|
|
value: string | number
|
|
|
|
|
sub?: string
|
2026-04-05 12:03:33 -05:00
|
|
|
icon: React.ComponentType<{ size?: number | string; className?: string }>
|
2026-04-05 10:45:21 -05:00
|
|
|
color: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const formatBytes = (bytes: number) => {
|
|
|
|
|
const gb = bytes / 1024 / 1024 / 1024
|
|
|
|
|
return gb >= 1 ? `${gb.toFixed(1)} GB` : `${(bytes / 1024 / 1024).toFixed(0)} MB`
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 14:13:43 -05:00
|
|
|
// ─── 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 ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
2026-04-05 10:45:21 -05:00
|
|
|
export default function Dashboard() {
|
2026-04-11 14:13:43 -05:00
|
|
|
const { user } = useAuth()
|
2026-04-11 14:24:51 -05:00
|
|
|
const { theme, toggleTheme } = useTheme()
|
2026-04-05 10:45:21 -05:00
|
|
|
const [stats, setStats] = useState<SystemStats | null>(null)
|
|
|
|
|
const [accountStats, setAccountStats] = useState<AccountStats | null>(null)
|
|
|
|
|
const [cpuHistory, setCpuHistory] = useState<{ t: string; cpu: number; mem: number }[]>([])
|
2026-04-11 14:13:43 -05:00
|
|
|
const [systemInfo, setSystemInfo] = useState<SystemInfo | null>(null)
|
|
|
|
|
const [version, setVersion] = useState<string | null>(null)
|
|
|
|
|
const [alertCount, setAlertCount] = useState<number | null>(null)
|
2026-04-05 10:45:21 -05:00
|
|
|
const wsRef = useRef<WebSocket | null>(null)
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-04-05 10:54:15 -05:00
|
|
|
api.get('/nixserver/accounts/stats').then(r => setAccountStats(r.data.data)).catch(() => {})
|
|
|
|
|
api.get('/nixserver/system/stats').then(r => setStats(r.data.data)).catch(() => {})
|
2026-04-11 14:13:43 -05:00
|
|
|
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) })
|
2026-04-05 10:45:21 -05:00
|
|
|
|
|
|
|
|
// WebSocket live stats
|
|
|
|
|
const token = localStorage.getItem('access_token')
|
|
|
|
|
if (token) {
|
|
|
|
|
const ws = new WebSocket(`ws://${window.location.hostname}:2087/ws/stats?token=${token}`)
|
|
|
|
|
wsRef.current = ws
|
|
|
|
|
ws.onmessage = e => {
|
|
|
|
|
const msg = JSON.parse(e.data)
|
|
|
|
|
if (msg.type === 'stats') {
|
|
|
|
|
setStats(msg.data)
|
|
|
|
|
setCpuHistory(prev => [
|
|
|
|
|
...prev.slice(-29),
|
|
|
|
|
{ t: new Date().toLocaleTimeString(), cpu: msg.data.cpu, mem: msg.data.memory.percent },
|
|
|
|
|
])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return () => wsRef.current?.close()
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
const cards: StatCard[] = [
|
|
|
|
|
{
|
|
|
|
|
label: 'Total Accounts',
|
|
|
|
|
value: accountStats?.total ?? '—',
|
|
|
|
|
sub: `${accountStats?.active ?? 0} active`,
|
|
|
|
|
icon: Users,
|
|
|
|
|
color: 'text-indigo-400',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: 'CPU Usage',
|
|
|
|
|
value: stats ? `${stats.cpu}%` : '—',
|
|
|
|
|
sub: `Load: ${stats?.load[0].toFixed(2) ?? '—'}`,
|
|
|
|
|
icon: Activity,
|
|
|
|
|
color: 'text-emerald-400',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: 'Memory',
|
|
|
|
|
value: stats ? `${stats.memory.percent}%` : '—',
|
|
|
|
|
sub: stats ? `${formatBytes(stats.memory.used)} / ${formatBytes(stats.memory.total)}` : '—',
|
|
|
|
|
icon: Server,
|
|
|
|
|
color: 'text-blue-400',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: 'Disk',
|
|
|
|
|
value: stats ? `${stats.disk.percent}%` : '—',
|
|
|
|
|
sub: stats ? `${formatBytes(stats.disk.used)} / ${formatBytes(stats.disk.total)}` : '—',
|
|
|
|
|
icon: HardDrive,
|
|
|
|
|
color: 'text-amber-400',
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
|
2026-04-11 14:13:43 -05:00
|
|
|
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>
|
|
|
|
|
)
|
|
|
|
|
|
2026-04-05 10:45:21 -05:00
|
|
|
return (
|
|
|
|
|
<div className="space-y-6">
|
2026-04-11 14:13:43 -05:00
|
|
|
|
|
|
|
|
{/* ── 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">
|
2026-04-11 14:24:51 -05:00
|
|
|
<InfoCell label="Username" value={user?.username ?? '…'} />
|
2026-04-11 14:13:43 -05:00
|
|
|
<Divider />
|
2026-04-11 14:24:51 -05:00
|
|
|
<InfoCell label="Hostname" value={systemInfo?.hostname ?? '…'} />
|
2026-04-11 14:13:43 -05:00
|
|
|
<Divider />
|
2026-04-11 14:24:51 -05:00
|
|
|
<InfoCell label="OS" value={systemInfo?.os ?? '…'} />
|
2026-04-11 14:13:43 -05:00
|
|
|
<Divider />
|
2026-04-11 14:24:51 -05:00
|
|
|
<InfoCell label="NixPanel Version" value={version ? `v${version}` : '…'} />
|
2026-04-11 14:13:43 -05:00
|
|
|
<Divider />
|
2026-04-11 14:24:51 -05:00
|
|
|
<InfoCell label="Load Averages" value={loadStr} />
|
2026-04-11 14:13:43 -05:00
|
|
|
<Divider />
|
|
|
|
|
<InfoCell label="Server Monitoring" value={monitoringNode} />
|
2026-04-11 14:24:51 -05:00
|
|
|
|
|
|
|
|
{/* ── Day / Night toggle — far right ── */}
|
|
|
|
|
<div className="ml-auto flex items-center gap-2 flex-shrink-0">
|
|
|
|
|
<Moon
|
|
|
|
|
size={13}
|
|
|
|
|
className={theme === 'dark' ? 'text-indigo-400' : 'text-[#94a3b8]'}
|
|
|
|
|
/>
|
|
|
|
|
<button
|
|
|
|
|
onClick={toggleTheme}
|
|
|
|
|
title={theme === 'dark' ? 'Switch to day mode' : 'Switch to night mode'}
|
|
|
|
|
className={`relative flex items-center w-10 h-5 rounded-full transition-colors cursor-pointer flex-shrink-0 ${
|
|
|
|
|
theme === 'light' ? 'bg-amber-400' : 'bg-[#2a2d3e]'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
<span
|
|
|
|
|
className={`absolute h-4 w-4 rounded-full bg-white shadow-sm transition-transform ${
|
|
|
|
|
theme === 'light' ? 'translate-x-5' : 'translate-x-0.5'
|
|
|
|
|
}`}
|
|
|
|
|
/>
|
|
|
|
|
</button>
|
|
|
|
|
<Sun
|
|
|
|
|
size={13}
|
|
|
|
|
className={theme === 'light' ? 'text-amber-500' : 'text-[#4a5568]'}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2026-04-05 10:45:21 -05:00
|
|
|
</div>
|
|
|
|
|
|
2026-04-11 14:13:43 -05:00
|
|
|
{/* ── Stat cards ───────────────────────────────────────────────────── */}
|
2026-04-05 10:45:21 -05:00
|
|
|
<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">
|
|
|
|
|
<div className="flex items-center justify-between mb-3">
|
|
|
|
|
<span className="text-[#64748b] text-xs font-medium">{card.label}</span>
|
|
|
|
|
<card.icon size={16} className={card.color} />
|
|
|
|
|
</div>
|
|
|
|
|
<p className="text-white text-2xl font-bold">{card.value}</p>
|
|
|
|
|
{card.sub && <p className="text-[#64748b] text-xs mt-1">{card.sub}</p>}
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-04-11 14:13:43 -05:00
|
|
|
{/* ── CPU / Memory chart ───────────────────────────────────────────── */}
|
2026-04-05 10:45:21 -05:00
|
|
|
<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 ? (
|
|
|
|
|
<ResponsiveContainer width="100%" height={160}>
|
|
|
|
|
<AreaChart data={cpuHistory}>
|
|
|
|
|
<defs>
|
|
|
|
|
<linearGradient id="cpu" x1="0" y1="0" x2="0" y2="1">
|
|
|
|
|
<stop offset="5%" stopColor="#6366f1" stopOpacity={0.3} />
|
|
|
|
|
<stop offset="95%" stopColor="#6366f1" stopOpacity={0} />
|
|
|
|
|
</linearGradient>
|
|
|
|
|
<linearGradient id="mem" x1="0" y1="0" x2="0" y2="1">
|
|
|
|
|
<stop offset="5%" stopColor="#10b981" stopOpacity={0.3} />
|
|
|
|
|
<stop offset="95%" stopColor="#10b981" stopOpacity={0} />
|
|
|
|
|
</linearGradient>
|
|
|
|
|
</defs>
|
|
|
|
|
<XAxis dataKey="t" tick={{ fill: '#64748b', fontSize: 10 }} tickLine={false} axisLine={false} />
|
|
|
|
|
<YAxis domain={[0, 100]} tick={{ fill: '#64748b', fontSize: 10 }} tickLine={false} axisLine={false} />
|
|
|
|
|
<Tooltip
|
2026-04-11 14:24:51 -05:00
|
|
|
contentStyle={{
|
|
|
|
|
background: theme === 'light' ? '#ffffff' : '#1e2130',
|
|
|
|
|
border: `1px solid ${theme === 'light' ? '#e2e8f0' : '#2a2d3e'}`,
|
|
|
|
|
borderRadius: 8,
|
|
|
|
|
fontSize: 12,
|
|
|
|
|
color: theme === 'light' ? '#0f172a' : '#e2e8f0',
|
|
|
|
|
}}
|
|
|
|
|
labelStyle={{ color: theme === 'light' ? '#64748b' : '#94a3b8' }}
|
2026-04-05 10:45:21 -05:00
|
|
|
/>
|
|
|
|
|
<Area type="monotone" dataKey="cpu" stroke="#6366f1" fill="url(#cpu)" strokeWidth={2} name="CPU %" />
|
|
|
|
|
<Area type="monotone" dataKey="mem" stroke="#10b981" fill="url(#mem)" strokeWidth={2} name="Mem %" />
|
|
|
|
|
</AreaChart>
|
|
|
|
|
</ResponsiveContainer>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="h-40 flex items-center justify-center text-[#64748b] text-sm">
|
|
|
|
|
Connecting to live stats...
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-04-11 14:13:43 -05:00
|
|
|
{/* ── Account status breakdown ─────────────────────────────────────── */}
|
2026-04-05 10:45:21 -05:00
|
|
|
{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>
|
|
|
|
|
<div className="grid grid-cols-3 gap-4">
|
|
|
|
|
{[
|
2026-04-11 14:13:43 -05:00
|
|
|
{ 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' },
|
2026-04-05 10:45:21 -05:00
|
|
|
].map(s => (
|
|
|
|
|
<div key={s.label} className="text-center">
|
|
|
|
|
<p className={`text-2xl font-bold ${s.color}`}>{s.value}</p>
|
|
|
|
|
<p className="text-[#64748b] text-xs mt-0.5">{s.label}</p>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|