Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
146c17abd8 | ||
|
|
47ce924a90 | ||
|
|
82b9ac30e9 | ||
|
|
c6f13f2e07 | ||
|
|
945da97c97 |
@@ -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
|
## [0.4.3] — 2026-04-11
|
||||||
|
|
||||||
### New Features
|
### New Features
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ type NavItem = { label: string; to: string }
|
|||||||
type NavCategory = {
|
type NavCategory = {
|
||||||
id: string
|
id: string
|
||||||
label: string
|
label: string
|
||||||
icon: React.ComponentType<{ size?: number; className?: string }>
|
icon: React.ComponentType<{ size?: number | string; className?: string }>
|
||||||
items: NavItem[]
|
items: NavItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,13 +462,17 @@ export default function Layout() {
|
|||||||
// ── Version / upgrade state ──────────────────────────────────────────────
|
// ── Version / upgrade state ──────────────────────────────────────────────
|
||||||
type VersionInfo = { currentVersion: string; latestVersion: string | null; updateAvailable: boolean }
|
type VersionInfo = { currentVersion: string; latestVersion: string | null; updateAvailable: boolean }
|
||||||
const [versionInfo, setVersionInfo] = useState<VersionInfo | null>(null)
|
const [versionInfo, setVersionInfo] = useState<VersionInfo | null>(null)
|
||||||
|
const [versionError, setVersionError] = useState(false)
|
||||||
const [upgradeState, setUpgradeState] = useState<'idle' | 'confirm' | 'running' | 'done' | 'error'>('idle')
|
const [upgradeState, setUpgradeState] = useState<'idle' | 'confirm' | 'running' | 'done' | 'error'>('idle')
|
||||||
|
|
||||||
const fetchVersion = useCallback(async () => {
|
const fetchVersion = useCallback(async () => {
|
||||||
|
setVersionError(false)
|
||||||
try {
|
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)
|
setVersionInfo(data.data)
|
||||||
} catch { /* non-fatal */ }
|
} catch {
|
||||||
|
setVersionError(true)
|
||||||
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => { fetchVersion() }, [fetchVersion])
|
useEffect(() => { fetchVersion() }, [fetchVersion])
|
||||||
@@ -726,6 +730,13 @@ export default function Layout() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</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">
|
<div className="flex items-center gap-1.5 px-1 py-1">
|
||||||
<Loader2 size={10} className="animate-spin text-[#4a5568]" />
|
<Loader2 size={10} className="animate-spin text-[#4a5568]" />
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import React, { useEffect, useState, useRef } from 'react'
|
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 { AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts'
|
||||||
import { api } from '../api/client'
|
import { api } from '../api/client'
|
||||||
|
import { useAuth } from '../contexts/AuthContext'
|
||||||
|
|
||||||
interface SystemStats {
|
interface SystemStats {
|
||||||
cpu: number
|
cpu: number
|
||||||
@@ -18,6 +19,17 @@ interface AccountStats {
|
|||||||
terminated: number
|
terminated: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SystemInfo {
|
||||||
|
hostname: string
|
||||||
|
os: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ServiceStatus {
|
||||||
|
name: string
|
||||||
|
status: 'running' | 'stopped' | 'failed' | 'unknown'
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
interface StatCard {
|
interface StatCard {
|
||||||
label: string
|
label: string
|
||||||
value: string | number
|
value: string | number
|
||||||
@@ -31,15 +43,51 @@ const formatBytes = (bytes: number) => {
|
|||||||
return gb >= 1 ? `${gb.toFixed(1)} GB` : `${(bytes / 1024 / 1024).toFixed(0)} MB`
|
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() {
|
export default function Dashboard() {
|
||||||
|
const { user } = useAuth()
|
||||||
const [stats, setStats] = useState<SystemStats | null>(null)
|
const [stats, setStats] = useState<SystemStats | null>(null)
|
||||||
const [accountStats, setAccountStats] = useState<AccountStats | null>(null)
|
const [accountStats, setAccountStats] = useState<AccountStats | null>(null)
|
||||||
const [cpuHistory, setCpuHistory] = useState<{ t: string; cpu: number; mem: number }[]>([])
|
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)
|
const wsRef = useRef<WebSocket | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api.get('/nixserver/accounts/stats').then(r => setAccountStats(r.data.data)).catch(() => {})
|
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/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
|
// WebSocket live stats
|
||||||
const token = localStorage.getItem('access_token')
|
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 (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
|
||||||
<h1 className="text-white text-xl font-semibold">Dashboard</h1>
|
{/* ── cPanel-style server info bar ─────────────────────────────────── */}
|
||||||
<p className="text-[#64748b] text-sm mt-0.5">Server overview and real-time metrics</p>
|
<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>
|
</div>
|
||||||
|
|
||||||
{/* Stat cards */}
|
{/* ── Stat cards ───────────────────────────────────────────────────── */}
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4">
|
||||||
{cards.map(card => (
|
{cards.map(card => (
|
||||||
<div key={card.label} className="bg-[#1a1d27] border border-[#2a2d3e] rounded-xl p-4">
|
<div key={card.label} className="bg-[#1a1d27] border border-[#2a2d3e] rounded-xl p-4">
|
||||||
@@ -113,7 +191,7 @@ export default function Dashboard() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* CPU / Memory chart */}
|
{/* ── CPU / Memory chart ───────────────────────────────────────────── */}
|
||||||
<div className="bg-[#1a1d27] border border-[#2a2d3e] rounded-xl p-4">
|
<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>
|
<h2 className="text-white text-sm font-medium mb-4">CPU & Memory — Live</h2>
|
||||||
{cpuHistory.length > 0 ? (
|
{cpuHistory.length > 0 ? (
|
||||||
@@ -146,7 +224,7 @@ export default function Dashboard() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Account status breakdown */}
|
{/* ── Account status breakdown ─────────────────────────────────────── */}
|
||||||
{accountStats && (
|
{accountStats && (
|
||||||
<div className="bg-[#1a1d27] border border-[#2a2d3e] rounded-xl p-4">
|
<div className="bg-[#1a1d27] border border-[#2a2d3e] rounded-xl p-4">
|
||||||
<h2 className="text-white text-sm font-medium mb-4">Account Status</h2>
|
<h2 className="text-white text-sm font-medium mb-4">Account Status</h2>
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "nixpanel-server",
|
"name": "nixpanel-server",
|
||||||
"version": "0.4.3",
|
"version": "0.4.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"server",
|
"server",
|
||||||
|
|||||||
+8
-2
@@ -24,14 +24,20 @@ success "Dependencies ready"
|
|||||||
|
|
||||||
step "Running migrations"
|
step "Running migrations"
|
||||||
cd "$INSTALL_DIR/server"
|
cd "$INSTALL_DIR/server"
|
||||||
npx prisma generate --silent
|
npx prisma generate
|
||||||
npx prisma migrate deploy
|
npx prisma db push
|
||||||
success "Migrations applied"
|
success "Migrations applied"
|
||||||
|
|
||||||
step "Rebuilding frontends"
|
step "Rebuilding frontends"
|
||||||
cd "$INSTALL_DIR"
|
cd "$INSTALL_DIR"
|
||||||
npm run build --workspace=nixserver --silent
|
npm run build --workspace=nixserver --silent
|
||||||
npm run build --workspace=nixclient --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"
|
success "Frontends rebuilt"
|
||||||
|
|
||||||
step "Rebuilding server"
|
step "Rebuilding server"
|
||||||
|
|||||||
+1
-1
@@ -73,7 +73,7 @@ export async function buildApp() {
|
|||||||
|
|
||||||
fastify.get('/api/health', async () => ({
|
fastify.get('/api/health', async () => ({
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
version: '0.4.3',
|
version: '0.4.4',
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user