import { useState, useEffect, useCallback } from 'react' import * as api from './api' import type { DashboardData, ServiceInfo, Account } from './api' /* ── Types ──────────────────────────────────────────────────────────────── */ type Page = | 'dashboard' | 'accounts' | 'createAccount' | 'listAccounts' | 'domains' | 'dns' | 'databases' | 'email' | 'ssl' | 'backups' | 'firewall' | 'apache' | 'php' | 'services' | 'settings' | 'logs' | 'packages' /* ── Nav ─────────────────────────────────────────────────────────────────── */ type NavGroup = { label: string; items: { id: Page; label: string; icon: string }[] } const NAV_GROUPS: NavGroup[] = [ { label: 'Overview', items: [{ id: 'dashboard', label: 'Dashboard', icon: '▦' }] }, { label: 'Account Management',items: [ { id: 'listAccounts', label: 'List Accounts', icon: '📋' }, { id: 'createAccount',label: 'Create Account', icon: '➕' }, { id: 'packages', label: 'Feature Packages', icon: '📦' }, ]}, { label: 'Domains & DNS', items: [ { id: 'domains', label: 'Zone Manager', icon: '🌐' }, { id: 'dns', label: 'DNS Records', icon: '🔗' }, ]}, { label: 'Databases', items: [{ id: 'databases', label: 'MySQL / MariaDB', icon: '🗄️' }] }, { label: 'Email', items: [{ id: 'email', label: 'Email Routing', icon: '✉️' }] }, { label: 'Security', items: [ { id: 'ssl', label: 'SSL / TLS', icon: '🔒' }, { id: 'firewall', label: 'Firewall (CSF)', icon: '🛡️' }, ]}, { label: 'Server', items: [ { id: 'apache', label: 'Apache Config', icon: '⚙️' }, { id: 'php', label: 'PHP Manager', icon: '🐘' }, { id: 'services', label: 'Service Manager', icon: '🔧' }, { id: 'backups', label: 'Backup Manager', icon: '💾' }, { id: 'logs', label: 'System Logs', icon: '📜' }, { id: 'settings', label: 'Server Config', icon: '🔩' }, ]}, ] /* ── Sidebar ────────────────────────────────────────────────────────────── */ function Sidebar({ current, onNav, onLogout, collapsed, onToggle, username }: { current: Page; onNav: (p: Page) => void; onLogout: () => void collapsed: boolean; onToggle: () => void; username: string }) { return ( ) } /* ── Stat bar ────────────────────────────────────────────────────────────── */ function StatBar({ data }: { data: DashboardData | null }) { const s = data?.stats const si = data?.sysinfo const ramPct = si && si.ram_total_mb > 0 ? Math.round((si.ram_used_mb / si.ram_total_mb) * 100) : null const diskPct = si && si.disk_total_gb > 0 ? Math.round((si.disk_used_gb / si.disk_total_gb) * 100) : null function barColor(pct: number) { return pct > 90 ? 'bg-red-500' : pct > 70 ? 'bg-yellow-500' : 'bg-emerald-500' } const stats = [ { label: 'Hosting Accounts', value: s?.accounts ?? '—', unit: '', color: 'text-orange-400', bar: null }, { label: 'Domains', value: s?.domains ?? '—', unit: '', color: 'text-blue-400', bar: null }, { label: 'Databases', value: s?.databases ?? '—', unit: '', color: 'text-purple-400', bar: null }, { label: 'Email Accounts', value: s?.email_accounts ?? '—', unit: '', color: 'text-cyan-400', bar: null }, { label: 'Disk Used', value: si ? `${si.disk_used_gb}` : '—', unit: 'GB', color: diskPct && diskPct > 80 ? 'text-red-400' : 'text-emerald-400', bar: diskPct }, { label: 'RAM Used', value: si ? `${Math.round(si.ram_used_mb / 1024 * 10) / 10}` : '—', unit: 'GB', color: ramPct && ramPct > 80 ? 'text-red-400' : 'text-yellow-400', bar: ramPct }, ] return (
{stats.map((s, i) => (

{s.label}

{s.value}{s.unit}

{s.bar !== null && (
)}
))}
) } /* ── Service table ───────────────────────────────────────────────────────── */ function ServiceTable({ services, loading, onAction }: { services: ServiceInfo[] loading: boolean onAction: (name: string, action: 'start' | 'stop' | 'restart') => void }) { const badge: Record = { running: 'bg-emerald-500/20 text-emerald-400 border border-emerald-500/30', stopped: 'bg-red-500/20 text-red-400 border border-red-500/30', warning: 'bg-yellow-500/20 text-yellow-400 border border-yellow-500/30', } const running = services.filter(s => s.status === 'running').length return (

Service Manager

{loading ? Loading… : {running} / {services.length} running }
{['Service', 'Status', 'PID', 'Uptime', 'Actions'].map(h => ( ))} {loading ? ( ) : services.length === 0 ? ( ) : services.map(svc => ( ))}
{h}
Loading services…
No service data available
{svc.display} {svc.status.charAt(0).toUpperCase() + svc.status.slice(1)} {svc.pid ?? '—'} {svc.uptime ?? '—'}
) } /* ── Quick action grid ───────────────────────────────────────────────────── */ function QuickActions({ onNav }: { onNav: (p: Page) => void }) { const actions = [ { label: 'Create Account', icon: '👤', page: 'createAccount' as Page, desc: 'Add hosting account' }, { label: 'Zone Manager', icon: '🌐', page: 'domains' as Page, desc: 'Manage DNS zones' }, { label: 'New Database', icon: '🗄️', page: 'databases' as Page, desc: 'Create MariaDB DB' }, { label: 'Issue SSL', icon: '🔒', page: 'ssl' as Page, desc: "Let's Encrypt cert" }, { label: 'Run Backup', icon: '💾', page: 'backups' as Page, desc: 'Full server backup' }, { label: 'PHP Manager', icon: '🐘', page: 'php' as Page, desc: 'Switch PHP version' }, { label: 'Firewall', icon: '🛡️', page: 'firewall' as Page, desc: 'Edit CSF rules' }, { label: 'System Logs', icon: '📜', page: 'logs' as Page, desc: 'Apache, error logs' }, ] return (

Quick Actions

{actions.map((a, i) => ( ))}
) } /* ── Server info bar ─────────────────────────────────────────────────────── */ function ServerInfoBar({ data }: { data: DashboardData | null }) { const si = data?.sysinfo const v = data?.versions const info = [ { label: 'Hostname', value: si?.hostname ?? '—' }, { label: 'OS', value: si?.os ?? '—' }, { label: 'Kernel', value: si?.kernel ?? '—' }, { label: 'PHP', value: v?.php ?? '—' }, { label: 'MariaDB', value: v?.mariadb ?? '—' }, { label: 'Apache', value: v?.apache ?? '—' }, { label: 'NixPanel', value: v?.nixpanel ?? '—' }, ] return (
{info.map(i => (
{i.label}: {i.value}
))} {si && (
Load: {si.load_avg[0].toFixed(2)}
)}
) } /* ── Dashboard page ──────────────────────────────────────────────────────── */ function Dashboard({ onNav }: { onNav: (p: Page) => void }) { const [dashData, setDashData] = useState(null) const [services, setServices] = useState([]) const [svcLoading, setSvcLoading] = useState(true) const [actionMsg, setActionMsg] = useState('') useEffect(() => { api.getDashboard().then(setDashData).catch(console.error) }, []) useEffect(() => { api.getServices().then(s => { setServices(s); setSvcLoading(false) }).catch(() => setSvcLoading(false)) }, []) const handleServiceAction = useCallback(async (name: string, action: 'start' | 'stop' | 'restart') => { setActionMsg(`${action}ing ${name}…`) try { await api.serviceAction(name, action) // Refresh services const updated = await api.getServices() setServices(updated) setActionMsg(`${name} ${action}ed`) } catch (e: any) { setActionMsg(`Error: ${e.message}`) } setTimeout(() => setActionMsg(''), 3000) }, []) return (

Server Overview

{dashData?.sysinfo ? `${dashData.sysinfo.hostname} · ${dashData.sysinfo.ip}` : 'Welcome back, admin — NixServer WHM'}

{actionMsg && ( {actionMsg} )} All systems operational
) } /* ── List accounts ───────────────────────────────────────────────────────── */ function ListAccounts({ onNav }: { onNav: (p: Page) => void }) { const [accounts, setAccounts] = useState([]) const [loading, setLoading] = useState(true) const [search, setSearch] = useState('') useEffect(() => { api.getAccounts() .then(setAccounts) .catch(console.error) .finally(() => setLoading(false)) }, []) const filtered = accounts.filter(a => a.username.includes(search) || a.domain.includes(search) ) return (

Hosting Accounts

setSearch(e.target.value)} placeholder="Search accounts, domains…" className="flex-1 bg-[#0f1520] border border-[#2a3044] text-white rounded px-3 py-2 text-sm placeholder-gray-600 focus:outline-none focus:border-orange-500/50" />
{['Username', 'Domain', 'Package', 'Disk Quota', 'Status', 'Created', 'Actions'].map(h => ( ))} {loading ? ( ) : filtered.length === 0 ? ( ) : filtered.map(acc => ( ))}
{h}
Loading…
👤

{search ? 'No accounts match your search.' : <>No accounts yet. }

{acc.username} {acc.domain} {acc.package_name} {acc.disk_quota_mb === 0 ? '∞' : `${acc.disk_quota_mb} MB`} {acc.status.charAt(0).toUpperCase() + acc.status.slice(1)} {new Date(acc.created_at).toLocaleDateString()}
) } /* ── Create account form ─────────────────────────────────────────────────── */ function CreateAccount({ onNav }: { onNav: (p: Page) => void }) { const [form, setForm] = useState({ username: '', domain: '', email: '', password: '', package_name: 'Default', disk_quota_mb: 10240, bandwidth_mb: 0, }) const [loading, setLoading] = useState(false) const [error, setError] = useState('') const [success, setSuccess] = useState('') const set = (k: string, v: string | number) => setForm(f => ({ ...f, [k]: v })) const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() setLoading(true); setError(''); setSuccess('') try { const acc = await api.createAccount({ ...form, disk_quota_mb: Number(form.disk_quota_mb), bandwidth_mb: Number(form.bandwidth_mb), }) setSuccess(`Account "${acc.username}" created for ${acc.domain}`) setTimeout(() => onNav('listAccounts'), 1500) } catch (e: any) { setError(e.message) } finally { setLoading(false) } } const Field = ({ label, name, type = 'text', placeholder = '' }: { label: string; name: string; type?: string; placeholder?: string }) => (
set(name, e.target.value)} placeholder={placeholder} className="w-full bg-[#0f1520] border border-[#2a3044] text-white rounded px-3 py-2 text-sm placeholder-gray-600 focus:outline-none focus:border-orange-500/60" />
) return (

Create Hosting Account

{error &&
⚠️ {error}
} {success &&
✓ {success}
}

Domain Information

Account Credentials

Resource Limits

set('disk_quota_mb', e.target.value)} className="w-full bg-[#0f1520] border border-[#2a3044] text-white rounded px-3 py-2 text-sm focus:outline-none focus:border-orange-500/60" />
set('bandwidth_mb', e.target.value)} className="w-full bg-[#0f1520] border border-[#2a3044] text-white rounded px-3 py-2 text-sm focus:outline-none focus:border-orange-500/60" />
) } /* ── Placeholder ─────────────────────────────────────────────────────────── */ function Placeholder({ title, icon }: { title: string; icon: string }) { return (
{icon}

{title}

This module is under active development and will be available in an upcoming release.

Coming Soon
) } const PAGE_META: Record = { dashboard: { title: 'Server Dashboard', icon: '▦' }, accounts: { title: 'Accounts', icon: '👤' }, listAccounts: { title: 'List Accounts', icon: '📋' }, createAccount: { title: 'Create Account', icon: '➕' }, packages: { title: 'Feature Packages', icon: '📦' }, domains: { title: 'Zone Manager', icon: '🌐' }, dns: { title: 'DNS Records', icon: '🔗' }, databases: { title: 'MySQL / MariaDB', icon: '🗄️' }, email: { title: 'Email Routing', icon: '✉️' }, ssl: { title: 'SSL / TLS Manager', icon: '🔒' }, backups: { title: 'Backup Manager', icon: '💾' }, firewall: { title: 'Firewall (CSF)', icon: '🛡️' }, apache: { title: 'Apache Config', icon: '⚙️' }, php: { title: 'PHP Manager', icon: '🐘' }, services: { title: 'Service Manager', icon: '🔧' }, settings: { title: 'Server Config', icon: '🔩' }, logs: { title: 'System Logs', icon: '📜' }, } /* ── Admin panel shell ───────────────────────────────────────────────────── */ function AdminPanel({ username, onLogout }: { username: string; onLogout: () => void }) { const [page, setPage] = useState('dashboard') const [collapsed, setCollapsed] = useState(false) const meta = PAGE_META[page] function renderPage() { switch (page) { case 'dashboard': return case 'listAccounts': return case 'createAccount': return default: return } } return (
setCollapsed(c => !c)} username={username} />
{meta.icon}

{meta.title}

Ubuntu 24.04
{username.charAt(0).toUpperCase()}
{username}
{page !== 'dashboard' && (
/ {meta.title}
)}
{renderPage()}
NixPanel WHM v0.1.0 · Ubuntu 24.04 LTS © 2025 NixPanel
) } /* ── Login ───────────────────────────────────────────────────────────────── */ export default function App() { const [username, setUsername] = useState('') const [password, setPassword] = useState('') const [loggedInAs, setLoggedInAs] = useState(null) const [error, setError] = useState('') const [loading, setLoading] = useState(false) // Restore session from localStorage useEffect(() => { if (api.isLoggedIn()) { const stored = localStorage.getItem('nixpanel_username') setLoggedInAs(stored ?? 'admin') } }, []) const handleLogin = async (e: React.FormEvent) => { e.preventDefault() setLoading(true); setError('') try { const result = await api.login(username, password) localStorage.setItem('nixpanel_username', result.user.username) setLoggedInAs(result.user.username) } catch (err: any) { setError(err.message === 'Unauthorized' ? 'Invalid username or password.' : err.message) } finally { setLoading(false) } } const handleLogout = () => { api.clearToken() localStorage.removeItem('nixpanel_username') setLoggedInAs(null) setUsername(''); setPassword('') } if (loggedInAs) return return (
{/* Left branding panel */}
N
NixServer
WHM Control Panel
{[ { icon: '🌐', title: 'Full DNS Control', desc: 'PowerDNS-backed zone management' }, { icon: '🔒', title: 'Auto SSL/TLS', desc: "Let's Encrypt for all domains" }, { icon: '🛡️', title: 'Built-in Firewall', desc: 'CSF/Fail2ban integration' }, { icon: '💾', title: 'Automated Backups', desc: 'Scheduled full & incremental' }, { icon: '🐘', title: 'Multi-PHP Support', desc: 'PHP 8.0 – 8.3 per account' }, ].map(f => (
{f.icon}

{f.title}

{f.desc}

))}

NixPanel WHM v0.1.0 · Secured by Fail2ban

{/* Login form */}
NixServer

WHM Control Panel

Administrator Login

NixServer WHM · Root Access

{error && (
⚠️ {error}
)}
setUsername(e.target.value)} required autoFocus autoComplete="username" placeholder="admin" className="w-full bg-[#0f1520] border border-[#2a3044] text-white rounded-lg px-4 py-2.5 text-sm placeholder-gray-700 focus:outline-none focus:border-orange-500/60 focus:ring-1 focus:ring-orange-500/20 transition-all" />
setPassword(e.target.value)} required autoComplete="current-password" placeholder="••••••••••••" className="w-full bg-[#0f1520] border border-[#2a3044] text-white rounded-lg px-4 py-2.5 text-sm placeholder-gray-700 focus:outline-none focus:border-orange-500/60 focus:ring-1 focus:ring-orange-500/20 transition-all" />
Secured by Fail2ban NixPanel v0.1
) }