14 Commits
Author SHA1 Message Date
NixReaper 0362810681 fix(login): fetch version from /api/health instead of hardcoding, bump v0.4.8
Login page had 'NixServer v0.3.2 · Port 2087' hardcoded and subtitle
said 'Web Host Manager'. Now fetches version from GET /api/health on
mount (no auth needed) and shows it dynamically. Changed subtitle to
'Admin Panel'.
2026-04-11 15:02:18 -05:00
NixReaper 9ec39c9120 fix: regenerate package-lock.json after @nixpanel/whm → @nixpanel/nixserver rename, bump v0.4.7
The old package-lock.json still referenced @nixpanel/whm as a workspace.
After the package rename in v0.4.6, npm install failed to resolve the
workspace correctly, causing the nixserver frontend build to fail and
breaking login. Deleted and regenerated package-lock.json cleanly.
2026-04-11 15:01:46 -05:00
NixReaper b7eb76d91a chore: remove WHM trademark references, rename package, bump v0.4.6
WHM is a registered trademark of cPanel, L.L.C. Replace all occurrences:
- Sidebar: 'Basic WebHost Manager® Setup' → 'Basic NixServer Setup'
- Sidebar: 'WHM Marketplace' → 'NixServer Marketplace'
- Sidebar: 'OpenAPI Documentation for WHM' → 'OpenAPI Documentation for NixServer'
- nixserver/package.json: @nixpanel/whm → @nixpanel/nixserver
- server/src/app.ts: whmDist variable → nixserverDist
2026-04-11 14:45:50 -05:00
NixReaper 5af2762634 fix(version): cache-bust refresh + clear stale state on manual check, bump to v0.4.5
The refresh/retry button called fetchVersion() without clearing
versionInfo first, so the old value stayed displayed with no feedback.
On manual refresh it now immediately sets versionInfo to null (shows
spinner) then fetches with ?_t=Date.now() to bypass any browser cache
of the API response.

Bump version to 0.4.5 to include all fixes since 0.4.4 (day/night
toggle, uptime/kill allowlist, auto-retry on version error).
2026-04-11 14:27:19 -05:00
NixReaper ddb51a8b4d feat: day/night mode toggle with theme context
Add ThemeContext (dark/light, persisted to localStorage, applied as
data-theme on <html>). Theme toggle is a sun/moon flip switch at the
right end of the dashboard info bar.

CSS overrides in index.css target every hardcoded Tailwind arbitrary
colour class used across all pages via [data-theme="light"] selectors,
so the entire panel switches without touching individual page files.

Light palette: page #f0f4f8, cards/sidebar #ffffff, hover #f1f5f9,
borders #e2e8f0, text #0f172a. Chart tooltip is made theme-aware via
inline-style interpolation since Tailwind CSS overrides cannot reach
Recharts inline styles.
2026-04-11 14:24:51 -05:00
NixReaper 4de2a7fa8e fix: auto-retry version fetch + add uptime/kill to exec allowlist
Layout.tsx: when versionError is true, automatically retry fetchVersion
after 5 s. This handles the case where the initial fetch runs during
the service-restart window after an upgrade (server not yet up), which
caused the sidebar footer to permanently show 'Version unavailable'
even though the server came back online shortly after.

exec.ts: add 'uptime' and 'kill' to ALLOWED_COMMANDS. 'uptime' was
missing, causing exec('uptime', ['-p']) in GET /api/nixserver/system/info
to throw 'Command not allowed', which rejected the entire Promise.all
and returned 500 — leaving Hostname and OS as '...' on the dashboard.
2026-04-11 14:19:24 -05:00
NixReaper 90dbf94c18 fix(layout): use authenticated api client for version/upgrade calls
Plain axios was called without the Authorization header, so the
requireAdmin middleware rejected every request with 401. The catch
block set versionError but the error state was also not yet deployed,
leaving the spinner stuck on 'Checking version…' forever.

Switch both axios.get('/api/nixserver/system/version') and
axios.post('/api/nixserver/system/upgrade') to the api client from
../api/client, which automatically attaches Bearer token from
localStorage on every request.
2026-04-11 14:15:48 -05:00
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
NixReaper a0e3fc7b1e feat(nixserver): version display and one-click panel upgrade
Add version badge to sidebar footer showing installed vs latest version,
fetched from /api/nixserver/system/version. Badge shows green when up to
date or amber with target version when an update is available.

Upgrade button triggers POST /api/nixserver/system/upgrade which spawns
scripts/update.sh detached (git pull + npm install + prisma migrate +
build + restart). Response is returned before the service restarts so
the client receives confirmation. Panel comes back online in ~60 s.
2026-04-11 11:55:25 -05:00
NixReaper da576bdbdc feat(nixserver): WHM-style collapsible category sidebar
Replace the flat 14-item nav with a full category sidebar matching
the WHM layout. 29 collapsible categories, ~210 menu items, live
search that filters across all categories and auto-expands results,
active route auto-expands the matching category, sidebar toggle
hides/shows the full panel.
2026-04-11 11:51:18 -05:00
16 changed files with 1105 additions and 2083 deletions
+63
View File
@@ -6,6 +6,69 @@ Pre-1.0 versions treat MINOR as feature releases and PATCH as bug fixes.
---
## [0.4.8] — 2026-04-11
### Bug Fixes
- **Login page showing stale hardcoded version** — "NixServer v0.3.2 · Port 2087" was hardcoded. Now fetches version live from `GET /api/health` (no auth required) on mount.
- **Login page subtitle** — "Web Host Manager" replaced with "Admin Panel" to remove any WHM trademark association.
---
## [0.4.7] — 2026-04-11
### Bug Fixes
- **Login broken after v0.4.6 update** — `package-lock.json` still mapped the workspace as `@nixpanel/whm`. After renaming the package to `@nixpanel/nixserver`, npm workspace resolution failed during `npm install`, causing the frontend build to break and leaving the panel unresponsive. Fixed by regenerating `package-lock.json` with the correct package name.
---
## [0.4.6] — 2026-04-11
### Changes
- **Removed all WHM trademark references** — "WHM" is a registered trademark of cPanel, L.L.C. All occurrences replaced with "NixServer":
- Sidebar menu item "Basic WebHost Manager® Setup" → "Basic NixServer Setup"
- Sidebar menu item "WHM Marketplace" → "NixServer Marketplace"
- Sidebar menu item "OpenAPI Documentation for WHM" → "OpenAPI Documentation for NixServer"
- Internal npm package name `@nixpanel/whm` → `@nixpanel/nixserver`
---
## [0.4.5] — 2026-04-11
### Bug Fixes
- **Sidebar version refresh not updating** — clicking the refresh icon now clears the stale version immediately (shows spinner) and adds a `?_t=` cache-bust parameter so the browser never serves a cached response. Same fix applied to the "Retry" button in the error state.
- **`uptime` and `kill` missing from exec allowlist** — caused `GET /api/nixserver/system/info` to throw "Command not allowed", returning 500 and leaving Hostname/OS as `…` on the dashboard.
- **Version fetch auto-retry** — when the version API fails (e.g. during service restart after upgrade), the sidebar now automatically retries every 5 s until it succeeds instead of permanently showing "Version unavailable".
### New Features
- **Day/Night mode toggle** — sun/moon flip switch at the right end of the dashboard info bar. Light palette: page `#f0f4f8`, cards `#ffffff`. Preference persisted to `localStorage`. All pages switch via seven `[data-theme="light"]` CSS overrides in `index.css` without touching individual page files.
---
## [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
- **Version display in sidebar** — the sidebar footer shows the currently installed version (e.g. `v0.4.3`), fetched live from the API. A green checkmark indicates up to date; an amber alert indicates an update is available.
- **One-click panel upgrade** — "Upgrade panel" button in the sidebar footer. When a newer version is available it highlights amber and shows the target version. Clicking requires a confirmation step before running. Calls `POST /api/nixserver/system/upgrade` which fires `scripts/update.sh` in the background (git pull → npm install → prisma migrate → build frontends + server → restart service). The panel comes back online automatically after ~60 s.
- **Check for updates button** — small refresh icon next to the version badge re-fetches version info on demand.
- **Backend: `GET /api/nixserver/system/version`** — returns `currentVersion` (read from `/opt/nixpanel/package.json`), `latestVersion` (fetched from GitHub raw content with a 5 s timeout), and `updateAvailable` boolean.
- **Backend: `POST /api/nixserver/system/upgrade`** — admin-only; responds immediately then spawns `scripts/update.sh` detached so the HTTP response is flushed before the service restarts.
---
## [0.4.2] — 2026-04-11
### Changes
- **NixServer sidebar redesigned** — replaced the flat 14-item nav with a full WHM-style collapsible category sidebar (29 categories, ~210 menu items). Categories fold/unfold on click; the active route's category auto-expands on navigation. A live search box at the top of the sidebar filters items across all categories and auto-expands matching results. Sidebar can be hidden entirely via the topbar toggle. All existing routes are wired to their correct menu items; future feature items route to the closest related hub page.
---
## [0.4.1] — 2026-04-11
### Bug Fixes (Installer)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@nixpanel/cpanel",
"version": "0.4.1",
"version": "0.4.3",
"private": true,
"type": "module",
"scripts": {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@nixpanel/whm",
"version": "0.4.1",
"name": "@nixpanel/nixserver",
"version": "0.4.6",
"private": true,
"type": "module",
"scripts": {
+6 -3
View File
@@ -1,6 +1,7 @@
import React from 'react'
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
import { AuthProvider, useAuth } from './contexts/AuthContext'
import { ThemeProvider } from './contexts/ThemeContext'
import Layout from './components/Layout'
import Login from './pages/Login'
import Dashboard from './pages/Dashboard'
@@ -233,9 +234,11 @@ function PageLoader() {
export default function App() {
return (
<BrowserRouter>
<AuthProvider>
<AppRoutes />
</AuthProvider>
<ThemeProvider>
<AuthProvider>
<AppRoutes />
</AuthProvider>
</ThemeProvider>
</BrowserRouter>
)
}
+744 -63
View File
@@ -1,33 +1,553 @@
import { useState } from 'react'
import { Outlet, NavLink, useNavigate } from 'react-router-dom'
import { useState, useMemo, useEffect, useCallback } from 'react'
import { Outlet, Link, useNavigate, useLocation } from 'react-router-dom'
import {
LayoutDashboard, Users, Package, UserCog, Globe, Mail, Shield,
Server, HardDrive, Settings, LogOut, Menu, X,
Activity, Lock, BarChart3, Cpu
LayoutDashboard, Settings, HelpCircle, Network, BookUser, UserCog,
Wrench, Languages, HardDrive, GitFork, RefreshCw, Activity,
Users, UserCheck, ArrowRightLeft, Paintbrush, Package,
Globe, Database, Wifi, Box, Mail, HeartPulse, Lock,
ShoppingBag, Code2, Puzzle, LogOut, Menu, X,
ChevronDown, ChevronRight, Search, RotateCcw, RefreshCcw,
ArrowUpCircle, CheckCircle2, AlertCircle, Loader2
} from 'lucide-react'
import { useAuth } from '../contexts/AuthContext'
import { api } from '../api/client'
const nav = [
{ label: 'Dashboard', to: '/', icon: LayoutDashboard },
{ label: 'Accounts', to: '/accounts', icon: Users },
{ label: 'Packages', to: '/packages', icon: Package },
{ label: 'Resellers', to: '/resellers', icon: UserCog },
{ label: 'DNS', to: '/dns', icon: Globe },
{ label: 'Email', to: '/email', icon: Mail },
{ label: 'SSL', to: '/ssl', icon: Lock },
{ label: 'PHP', to: '/php', icon: Cpu },
{ label: 'Web Server', to: '/webserver', icon: Server },
{ label: 'Services', to: '/services', icon: Activity },
{ label: 'Backup', to: '/backup', icon: HardDrive },
{ label: 'Security', to: '/security', icon: Shield },
{ label: 'System', to: '/system', icon: BarChart3 },
{ label: 'Settings', to: '/settings', icon: Settings },
// ─── Nav data ────────────────────────────────────────────────────────────────
type NavItem = { label: string; to: string }
type NavCategory = {
id: string
label: string
icon: React.ComponentType<{ size?: number | string; className?: string }>
items: NavItem[]
}
const NAV: NavCategory[] = [
{
id: 'server-config',
label: 'Server Configuration',
icon: Settings,
items: [
{ label: 'Basic NixServer Setup', to: '/settings' },
{ label: 'Change Root Password', to: '/security' },
{ label: 'Configure Analytics', to: '/settings' },
{ label: 'Configure Cron Jobs', to: '/settings' },
{ label: 'Initial Quota Setup', to: '/settings' },
{ label: 'Link Server Nodes', to: '/settings' },
{ label: 'Server Profile', to: '/settings' },
{ label: 'Server Time', to: '/system' },
{ label: 'Statistics Software', to: '/settings' },
{ label: 'Terminal', to: '/system' },
{ label: 'Tweak Settings', to: '/settings' },
{ label: 'Update Preferences', to: '/settings' },
{ label: 'NixServer Marketplace', to: '/settings' },
],
},
{
id: 'support',
label: 'Support',
icon: HelpCircle,
items: [
{ label: 'Grant NixPanel Support Access', to: '/security' },
{ label: 'Support Center', to: '/settings' },
],
},
{
id: 'networking',
label: 'Networking Setup',
icon: Network,
items: [
{ label: 'Change Hostname', to: '/security' },
{ label: 'Resolver Configuration', to: '/security' },
{ label: 'Security Center', to: '/security' },
{ label: 'Apache mod_userdir Tweak', to: '/webserver' },
{ label: 'SMTP Restrictions', to: '/email' },
{ label: 'Compiler Access', to: '/security' },
{ label: 'Configure Security Policies', to: '/security' },
{ label: 'mXSTOP Brute Force Protection', to: '/security' },
{ label: 'Host Access Control', to: '/security' },
{ label: 'Manage External Authentications', to: '/security' },
{ label: "Manage root's SSH Keys", to: '/security' },
{ label: 'Manage Wheel Group Users', to: '/security' },
{ label: 'ModSecurity™ Configuration', to: '/security' },
{ label: 'ModSecurity™ Tools', to: '/security' },
{ label: 'ModSecurity™ Vendors', to: '/security' },
{ label: 'Password Strength Configuration', to: '/security' },
{ label: 'Security Advisor', to: '/security' },
{ label: 'Security Questions', to: '/security' },
{ label: 'Shell Fork Bomb Protection', to: '/security' },
{ label: 'SSH Password Authorization Tweak', to: '/security' },
{ label: 'Traceroute Enable/Disable', to: '/security' },
{ label: 'Two-Factor Authentication', to: '/security' },
],
},
{
id: 'contacts',
label: 'Server Contacts',
icon: BookUser,
items: [
{ label: 'Contact Manager', to: '/settings' },
{ label: 'Edit System Mail Preferences', to: '/email' },
],
},
{
id: 'resellers',
label: 'Resellers',
icon: UserCog,
items: [
{ label: 'Change Ownership of an Account', to: '/resellers' },
{ label: 'Change Ownership of Multiple Accounts', to: '/resellers' },
{ label: 'Edit Reseller Nameservers and Privileges', to: '/resellers' },
{ label: 'Email All Resellers', to: '/resellers' },
{ label: "Manage Reseller's IP Delegation", to: '/resellers' },
{ label: "Manage Reseller's Shared IP", to: '/resellers' },
{ label: 'Reseller Center', to: '/resellers' },
{ label: 'Reset Resellers', to: '/resellers' },
{ label: 'Show Reseller Accounts', to: '/resellers' },
{ label: 'View Reseller Usage', to: '/resellers' },
],
},
{
id: 'service-config',
label: 'Service Configuration',
icon: Wrench,
items: [
{ label: 'Apache Configuration', to: '/webserver' },
{ label: 'Log Rotation Configuration', to: '/settings' },
{ label: 'Web Disk Configuration', to: '/settings' },
{ label: 'Web Services Configuration', to: '/settings' },
{ label: 'Exim Configuration Manager', to: '/email' },
{ label: 'FTP Server Selection', to: '/settings' },
{ label: 'Mailserver Configuration', to: '/email' },
{ label: 'Manage Service SSL Certificates', to: '/ssl' },
{ label: 'Nameserver Selection', to: '/dns' },
{ label: 'Service Manager', to: '/services' },
],
},
{
id: 'locales',
label: 'Locales',
icon: Languages,
items: [
{ label: 'Configure Application Locales', to: '/settings' },
{ label: 'Copy a Locale', to: '/settings' },
{ label: 'Delete a Locale', to: '/settings' },
{ label: 'Edit a Locale', to: '/settings' },
{ label: 'Locale XML Download', to: '/settings' },
{ label: 'Locale XML Upload', to: '/settings' },
{ label: 'View Available Locales', to: '/settings' },
],
},
{
id: 'backup',
label: 'Backup',
icon: HardDrive,
items: [
{ label: 'Backup Configuration', to: '/backup' },
{ label: 'Backup Restoration', to: '/backup' },
{ label: 'Backup User Selection', to: '/backup' },
{ label: 'File and Directory Restoration', to: '/backup' },
],
},
{
id: 'clusters',
label: 'Clusters',
icon: GitFork,
items: [
{ label: 'Configuration Cluster', to: '/settings' },
{ label: 'DNS Cluster', to: '/dns' },
{ label: 'Remote Access Key', to: '/security' },
],
},
{
id: 'reboot',
label: 'System Reboot',
icon: RotateCcw,
items: [
{ label: 'Forceful Server Reboot', to: '/system' },
{ label: 'Graceful Server Reboot', to: '/system' },
],
},
{
id: 'server-status',
label: 'Server Status',
icon: Activity,
items: [
{ label: 'Apache Status', to: '/webserver' },
{ label: 'Daily Process Log', to: '/system' },
{ label: 'Server Information',to: '/system' },
{ label: 'Service Status', to: '/services' },
{ label: 'Task Queue Monitor',to: '/system' },
],
},
{
id: 'account-info',
label: 'Account Information',
icon: Users,
items: [
{ label: 'List Accounts', to: '/accounts/list' },
{ label: 'List Parked Domains', to: '/accounts/parked-domains'},
{ label: 'List Subdomains', to: '/accounts/subdomains' },
{ label: 'List Suspended Accounts', to: '/accounts/suspended' },
{ label: 'Show Accounts Over Quota', to: '/accounts/over-quota' },
{ label: 'View Bandwidth Usage', to: '/accounts/bandwidth' },
],
},
{
id: 'account-functions',
label: 'Account Functions',
icon: UserCheck,
items: [
{ label: "Change Site's IP Address", to: '/accounts/change-ip' },
{ label: 'Create a New Account', to: '/accounts/create' },
{ label: 'Email All Users', to: '/accounts/email-users' },
{ label: 'Force Password Change', to: '/accounts/force-password' },
{ label: 'Limit Bandwidth Usage', to: '/accounts/limit-bandwidth' },
{ label: 'Manage Account Suspension', to: '/accounts/suspension' },
{ label: 'Manage Demo Mode', to: '/accounts/demo-mode' },
{ label: 'Manage Shell Access', to: '/accounts/shell-access' },
{ label: 'Modify an Account', to: '/accounts/modify' },
{ label: 'Password Modification', to: '/accounts/password' },
{ label: 'Quota Modification', to: '/accounts/quota' },
{ label: 'Raw Apache Log Download', to: '/accounts/apache-logs' },
{ label: 'Raw NGINX® Log Download', to: '/accounts/nginx-logs' },
{ label: 'Rearrange an Account', to: '/accounts/rearrange' },
{ label: 'Reset Account Bandwidth', to: '/accounts/reset-bandwidth' },
{ label: 'Terminate Accounts', to: '/accounts/terminate' },
{ label: 'Unsuspend Bandwidth Exceeders', to: '/accounts/unsuspend-bandwidth' },
{ label: 'Upgrade/Downgrade an Account', to: '/accounts/upgrade' },
{ label: 'Web Template Editor', to: '/accounts/web-template' },
],
},
{
id: 'multi-account',
label: 'Multi Account Functions',
icon: Users,
items: [
{ label: "Change Multiple Sites' IP Addresses", to: '/accounts/bulk-ip' },
{ label: 'Modify/Upgrade Multiple Accounts', to: '/accounts/bulk-modify' },
],
},
{
id: 'transfers',
label: 'Transfers',
icon: ArrowRightLeft,
items: [
{ label: 'Convert Addon Domain to Account', to: '/accounts' },
{ label: 'Review Transfers and Restores', to: '/accounts' },
{ label: 'Transfer or Restore a cPanel Account', to: '/accounts' },
{ label: 'Transfer Tool', to: '/accounts' },
],
},
{
id: 'themes',
label: 'Themes',
icon: Paintbrush,
items: [
{ label: 'Theme Manager', to: '/settings' },
],
},
{
id: 'packages',
label: 'Packages',
icon: Package,
items: [
{ label: 'Add a Package', to: '/packages' },
{ label: 'Delete a Package', to: '/packages' },
{ label: 'Edit a Package', to: '/packages' },
{ label: 'Feature Manager', to: '/packages' },
],
},
{
id: 'dns',
label: 'DNS Functions',
icon: Globe,
items: [
{ label: 'Add a DNS Zone', to: '/dns' },
{ label: 'Add an A Entry for Hostname', to: '/dns' },
{ label: 'Delete a DNS Zone', to: '/dns' },
{ label: 'DNS Zone Manager', to: '/dns' },
{ label: 'Edit Zone Templates', to: '/dns' },
{ label: 'Email DNS Record Manager', to: '/dns' },
{ label: 'Email Routing Configuration', to: '/email' },
{ label: 'Enable DKIM/SPF Globally', to: '/email' },
{ label: 'Nameserver Record Report', to: '/dns' },
{ label: 'Park a Domain', to: '/dns' },
{ label: 'Perform a DNS Cleanup', to: '/dns' },
{ label: 'Set Zone TTL', to: '/dns' },
{ label: 'Setup/Edit Domain Forwarding',to: '/dns' },
{ label: 'Synchronize DNS Records', to: '/dns' },
],
},
{
id: 'databases',
label: 'Database Services',
icon: Database,
items: [
{ label: 'Change Database Root Password', to: '/settings' },
{ label: 'Change Database User Password', to: '/settings' },
{ label: 'Database Map Tool', to: '/settings' },
{ label: 'Edit Database Configuration', to: '/settings' },
{ label: 'Manage Database Access Hosts', to: '/settings' },
{ label: 'Manage Database Profiles', to: '/settings' },
{ label: 'Manage Database Users', to: '/settings' },
{ label: 'Manage Databases', to: '/settings' },
{ label: 'phpMyAdmin', to: '/settings' },
{ label: 'Repair Databases', to: '/settings' },
{ label: 'Show Database Processes', to: '/settings' },
{ label: 'Upgrade Database Version', to: '/settings' },
],
},
{
id: 'ip',
label: 'IP Functions',
icon: Wifi,
items: [
{ label: 'IPv6 Ranges', to: '/security' },
{ label: 'Add a New IP Address', to: '/security' },
{ label: 'Assign IPv6 Address', to: '/security' },
{ label: 'Configure Remote Service IPs', to: '/security' },
{ label: 'IP Migration Wizard', to: '/security' },
{ label: 'Rebuild the IP Address Pool', to: '/security' },
{ label: 'Show IP Address Usage', to: '/security' },
{ label: 'Show or Delete Current IPs', to: '/security' },
{ label: 'Show/Edit Reserved IPs', to: '/security' },
],
},
{
id: 'software',
label: 'Software',
icon: Box,
items: [
{ label: 'Install a Perl Module', to: '/system' },
{ label: 'Install Distro Packages', to: '/system' },
{ label: 'Module Installers', to: '/system' },
{ label: 'MultiPHP INI Editor', to: '/php' },
{ label: 'MultiPHP Manager', to: '/php' },
{ label: 'NGINX® Manager', to: '/webserver' },
{ label: 'Rebuild RPM Database', to: '/system' },
{ label: 'System Update', to: '/system' },
{ label: 'Update Server Software', to: '/system' },
{ label: 'EasyApache 4', to: '/webserver' },
],
},
{
id: 'email',
label: 'Email',
icon: Mail,
items: [
{ label: 'Email Deliverability', to: '/email' },
{ label: 'Filter Incoming Emails by Country', to: '/email' },
{ label: 'Filter Incoming Emails by Domain', to: '/email' },
{ label: 'Greylisting', to: '/email' },
{ label: 'Mail Delivery Reports', to: '/email' },
{ label: 'Mail Queue Manager', to: '/email' },
{ label: 'Mail Troubleshooter', to: '/email' },
{ label: 'Mailbox Conversion', to: '/email' },
{ label: 'Repair Mailbox Permissions', to: '/email' },
{ label: 'Spamd Startup Configuration', to: '/email' },
{ label: 'View Mail Statistics Summary', to: '/email' },
{ label: 'View Relayers', to: '/email' },
{ label: 'View Sent Summary', to: '/email' },
],
},
{
id: 'system-health',
label: 'System Health',
icon: HeartPulse,
items: [
{ label: 'Background Process Killer', to: '/system' },
{ label: 'Process Manager', to: '/system' },
{ label: 'Show Current Disk Usage', to: '/system' },
{ label: 'Show Current Running Processes', to: '/system' },
],
},
{
id: 'nixpanel',
label: 'NixPanel',
icon: LayoutDashboard,
items: [
{ label: 'Change Log', to: '/settings' },
{ label: 'Customization', to: '/settings' },
{ label: 'Manage Plugins', to: '/settings' },
{ label: 'Modify NixPanel News', to: '/settings' },
{ label: 'Reset a Mailman Password', to: '/email' },
{ label: 'Upgrade to Latest Version', to: '/settings' },
],
},
{
id: 'ssl',
label: 'SSL/TLS',
icon: Lock,
items: [
{ label: 'Generate an SSL Certificate and Signing Request', to: '/ssl' },
{ label: 'Install an SSL Certificate on a Domain', to: '/ssl' },
{ label: 'Manage AutoSSL', to: '/ssl' },
{ label: 'Manage SSL Hosts', to: '/ssl' },
{ label: 'Purchase and Install an SSL Certificate', to: '/ssl' },
{ label: 'SSL Storage Manager', to: '/ssl' },
{ label: 'SSL/TLS Configuration', to: '/ssl' },
],
},
{
id: 'market',
label: 'Market',
icon: ShoppingBag,
items: [
{ label: 'Market Provider Manager', to: '/settings' },
],
},
{
id: 'restart-services',
label: 'Restart Services',
icon: RefreshCw,
items: [
{ label: 'Database Server', to: '/services' },
{ label: 'DNS Server', to: '/services' },
{ label: 'HTTP Server (Apache)', to: '/services' },
{ label: 'IMAP Server', to: '/services' },
{ label: 'Mail Server (Exim)', to: '/services' },
{ label: 'Mailing List Manager (Mailman)', to: '/services' },
{ label: 'PHP-FPM service for Apache', to: '/services' },
{ label: 'SSH Server (OpenSSH)', to: '/services' },
],
},
{
id: 'development',
label: 'Development',
icon: Code2,
items: [
{ label: 'Apps Managed by AppConfig', to: '/settings' },
{ label: 'NixPanel Development Forum', to: '/settings' },
{ label: 'NixPanel Plugin File Generator', to: '/settings' },
{ label: 'Developer Documentation', to: '/settings' },
{ label: 'Manage API Tokens', to: '/settings' },
{ label: 'Manage Hooks', to: '/settings' },
{ label: 'OpenAPI Documentation for NixPanel', to: '/settings' },
{ label: 'OpenAPI Documentation for NixServer', to: '/settings' },
],
},
{
id: 'plugins',
label: 'Plugins',
icon: Puzzle,
items: [
{ label: 'Backuply by Softaculous', to: '/backup' },
{ label: 'ImunifyAV', to: '/security' },
{ label: 'Server Monitoring', to: '/system' },
{ label: 'SitePad Website Builder', to: '/settings' },
{ label: 'Softaculous - Instant Installs', to: '/settings' },
{ label: 'WP Toolkit', to: '/settings' },
],
},
]
// ─── Helpers ─────────────────────────────────────────────────────────────────
function isItemActive(to: string, pathname: string) {
if (to === '/') return pathname === '/'
return pathname === to || pathname.startsWith(to + '/')
}
// ─── Component ───────────────────────────────────────────────────────────────
export default function Layout() {
const { user, logout } = useAuth()
const navigate = useNavigate()
const location = useLocation()
const [sidebarOpen, setSidebarOpen] = useState(true)
const [search, setSearch] = useState('')
// ── 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 (manual = false) => {
setVersionError(false)
// On a manual refresh, clear the stale value so the spinner shows
if (manual) setVersionInfo(null)
try {
const { data } = await api.get('/nixserver/system/version', {
timeout: 10_000,
// Cache-bust so the browser never serves a stale version response
params: { _t: Date.now() },
})
setVersionInfo(data.data)
} catch {
setVersionError(true)
}
}, [])
// Initial fetch
useEffect(() => { fetchVersion() }, [fetchVersion])
// Auto-retry every 5 s while in error state (handles the service-restart window after an upgrade)
useEffect(() => {
if (!versionError) return
const t = setTimeout(fetchVersion, 5_000)
return () => clearTimeout(t)
}, [versionError, fetchVersion])
const handleUpgrade = async () => {
if (upgradeState === 'confirm') {
setUpgradeState('running')
try {
await api.post('/nixserver/system/upgrade')
setUpgradeState('done')
} catch {
setUpgradeState('error')
}
} else {
setUpgradeState('confirm')
}
}
// ─────────────────────────────────────────────────────────────────────────
const [openCats, setOpenCats] = useState<Set<string>>(() => {
// Auto-open the category that contains the active route on first render
const active = NAV.find(cat =>
cat.items.some(item => isItemActive(item.to, location.pathname))
)
return active ? new Set([active.id]) : new Set()
})
// When the route changes (e.g. user navigates), auto-open the matching category
useEffect(() => {
const active = NAV.find(cat =>
cat.items.some(item => isItemActive(item.to, location.pathname))
)
if (active) {
setOpenCats(prev => prev.has(active.id) ? prev : new Set([...prev, active.id]))
}
}, [location.pathname])
const toggleCat = (id: string) => {
setOpenCats(prev => {
const next = new Set(prev)
next.has(id) ? next.delete(id) : next.add(id)
return next
})
}
// Filter nav based on search query
const filteredNav = useMemo(() => {
const q = search.trim().toLowerCase()
if (!q) return NAV
return NAV.map(cat => ({
...cat,
items: cat.items.filter(item => item.label.toLowerCase().includes(q)),
})).filter(cat =>
cat.items.length > 0 || cat.label.toLowerCase().includes(q)
)
}, [search])
// When searching, all matching categories are forced open
const effectiveOpen = (id: string) =>
search.trim() ? filteredNav.some(c => c.id === id) : openCats.has(id)
const handleLogout = async () => {
await logout()
@@ -36,68 +556,229 @@ export default function Layout() {
return (
<div className="flex h-screen overflow-hidden bg-[#0f1117]">
{/* Sidebar */}
{/* ── Sidebar ─────────────────────────────────────────────────────── */}
<aside
className={`flex flex-col transition-all duration-200 bg-[#1a1d27] border-r border-[#2a2d3e] ${
sidebarOpen ? 'w-56' : 'w-14'
className={`flex flex-col transition-all duration-200 bg-[#1a1d27] border-r border-[#2a2d3e] overflow-hidden flex-shrink-0 ${
sidebarOpen ? 'w-72' : 'w-0'
}`}
>
{/* Logo */}
<div className="flex items-center gap-2 px-4 py-4 border-b border-[#2a2d3e] min-h-[57px]">
{/* Branding */}
<div className="flex items-center gap-2 px-4 py-4 border-b border-[#2a2d3e] min-h-[57px] flex-shrink-0">
<div className="w-7 h-7 rounded-lg bg-indigo-600 flex items-center justify-center flex-shrink-0">
<span className="text-white text-xs font-bold">N</span>
</div>
{sidebarOpen && (
<div className="overflow-hidden">
<p className="text-white text-sm font-semibold leading-tight">NixServer</p>
<p className="text-[#64748b] text-xs">Admin Panel</p>
<div className="overflow-hidden">
<p className="text-white text-sm font-semibold leading-tight whitespace-nowrap">NixServer</p>
<p className="text-[#64748b] text-xs whitespace-nowrap">Admin Panel</p>
</div>
</div>
{/* Search */}
<div className="px-3 py-2 border-b border-[#2a2d3e] flex-shrink-0">
<div className="relative">
<Search size={13} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-[#4a5568] pointer-events-none" />
<input
type="text"
placeholder="Search..."
value={search}
onChange={e => setSearch(e.target.value)}
className="w-full bg-[#0f1117] border border-[#2a2d3e] rounded-md text-xs text-[#94a3b8] placeholder-[#4a5568] pl-7 pr-3 py-1.5 focus:outline-none focus:border-indigo-500 transition-colors"
/>
{search && (
<button
onClick={() => setSearch('')}
className="absolute right-2 top-1/2 -translate-y-1/2 text-[#4a5568] hover:text-[#94a3b8]"
>
<X size={11} />
</button>
)}
</div>
</div>
{/* Nav */}
<nav className="flex-1 overflow-y-auto py-1 scrollbar-thin">
{/* Dashboard — always at the top */}
<Link
to="/"
className={`flex items-center gap-2.5 px-3 py-1.5 mx-2 my-0.5 rounded-md text-xs transition-colors ${
location.pathname === '/'
? 'bg-indigo-600/20 text-indigo-400'
: 'text-[#94a3b8] hover:bg-[#1e2130] hover:text-white'
}`}
>
<LayoutDashboard size={13} className="flex-shrink-0" />
<span className="truncate">Dashboard</span>
</Link>
{/* Categories */}
{filteredNav.map(cat => {
const isOpen = effectiveOpen(cat.id)
const Icon = cat.icon
const hasActive = cat.items.some(item => isItemActive(item.to, location.pathname))
return (
<div key={cat.id} className="mx-2 my-0.5">
{/* Category header */}
<button
onClick={() => !search.trim() && toggleCat(cat.id)}
className={`flex items-center gap-2 w-full px-2 py-1.5 rounded-md text-xs font-medium transition-colors ${
hasActive && !search.trim()
? 'text-indigo-300'
: 'text-[#64748b] hover:text-[#94a3b8]'
} ${search.trim() ? 'cursor-default' : ''}`}
>
<Icon size={13} className="flex-shrink-0" />
<span className="flex-1 text-left truncate">{cat.label}</span>
{!search.trim() && (
isOpen
? <ChevronDown size={11} className="flex-shrink-0 opacity-60" />
: <ChevronRight size={11} className="flex-shrink-0 opacity-60" />
)}
</button>
{/* Items */}
{isOpen && (
<div className="ml-3 pl-2 border-l border-[#2a2d3e] mt-0.5 mb-1">
{cat.items.map(item => {
const active = isItemActive(item.to, location.pathname)
return (
<Link
key={item.label}
to={item.to}
className={`block px-2 py-1 rounded text-xs transition-colors truncate ${
active
? 'text-indigo-400 bg-indigo-600/10'
: 'text-[#64748b] hover:text-[#94a3b8] hover:bg-[#1e2130]'
}`}
>
{item.label}
</Link>
)
})}
</div>
)}
</div>
)
})}
{/* Empty search state */}
{search.trim() && filteredNav.length === 0 && (
<p className="text-center text-[#4a5568] text-xs py-6">No results</p>
)}
</nav>
{/* Version + upgrade */}
<div className="border-t border-[#2a2d3e] px-3 pt-2.5 pb-1 flex-shrink-0">
{versionInfo ? (
<div className="space-y-1.5">
{/* Version row */}
<div className="flex items-center justify-between px-1">
<div className="flex items-center gap-1.5">
{versionInfo.updateAvailable ? (
<AlertCircle size={11} className="text-amber-400 flex-shrink-0" />
) : (
<CheckCircle2 size={11} className="text-emerald-500 flex-shrink-0" />
)}
<span className="text-[#64748b] text-[10px]">
v{versionInfo.currentVersion}
</span>
{versionInfo.updateAvailable && versionInfo.latestVersion && (
<span className="text-amber-400 text-[10px]">
→ v{versionInfo.latestVersion}
</span>
)}
</div>
<button
onClick={() => fetchVersion(true)}
title="Check for updates"
className="text-[#4a5568] hover:text-[#94a3b8] transition-colors"
>
<RefreshCcw size={10} />
</button>
</div>
{/* Upgrade button */}
{upgradeState === 'done' ? (
<div className="flex items-center gap-1.5 px-2 py-1.5 rounded-md bg-emerald-600/10 text-emerald-400 text-[10px]">
<CheckCircle2 size={11} />
<span>Upgrade started — panel restarting…</span>
</div>
) : upgradeState === 'error' ? (
<div className="flex items-center gap-1.5 px-2 py-1.5 rounded-md bg-red-600/10 text-red-400 text-[10px]">
<AlertCircle size={11} />
<span>Upgrade failed — check logs</span>
</div>
) : (
<div className="flex gap-1.5">
<button
onClick={handleUpgrade}
disabled={upgradeState === 'running'}
className={`flex-1 flex items-center justify-center gap-1.5 px-2 py-1.5 rounded-md text-[10px] font-medium transition-colors ${
upgradeState === 'confirm'
? 'bg-amber-500/20 text-amber-300 hover:bg-amber-500/30'
: versionInfo.updateAvailable
? 'bg-indigo-600/20 text-indigo-300 hover:bg-indigo-600/30'
: 'bg-[#1e2130] text-[#64748b] hover:text-[#94a3b8]'
}`}
>
{upgradeState === 'running' ? (
<><Loader2 size={10} className="animate-spin" /><span>Upgrading…</span></>
) : upgradeState === 'confirm' ? (
<><ArrowUpCircle size={10} /><span>Confirm upgrade</span></>
) : versionInfo.updateAvailable ? (
<><ArrowUpCircle size={10} /><span>Upgrade available</span></>
) : (
<><ArrowUpCircle size={10} /><span>Upgrade panel</span></>
)}
</button>
{upgradeState === 'confirm' && (
<button
onClick={() => setUpgradeState('idle')}
className="px-2 py-1.5 rounded-md bg-[#1e2130] text-[#64748b] hover:text-[#94a3b8] text-[10px] transition-colors"
>
Cancel
</button>
)}
</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(true)} 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]" />
<span className="text-[#4a5568] text-[10px]">Checking version…</span>
</div>
)}
</div>
{/* Nav */}
<nav className="flex-1 overflow-y-auto py-2">
{nav.map(({ label, to, icon: Icon }) => (
<NavLink
key={to}
to={to}
end={to === '/'}
className={({ isActive }) =>
`flex items-center gap-3 px-4 py-2 mx-2 my-0.5 rounded-lg text-sm transition-colors ${
isActive
? 'bg-indigo-600/20 text-indigo-400'
: 'text-[#94a3b8] hover:bg-[#1e2130] hover:text-white'
}`
}
>
<Icon size={16} className="flex-shrink-0" />
{sidebarOpen && <span className="truncate">{label}</span>}
</NavLink>
))}
</nav>
{/* User + logout */}
<div className="border-t border-[#2a2d3e] p-3">
{sidebarOpen && (
<div className="px-1 mb-2">
<p className="text-white text-xs font-medium truncate">{user?.username}</p>
<p className="text-[#64748b] text-xs truncate">{user?.role}</p>
</div>
)}
<div className="border-t border-[#2a2d3e] p-3 flex-shrink-0">
<div className="px-1 mb-2">
<p className="text-white text-xs font-medium truncate">{user?.username}</p>
<p className="text-[#64748b] text-xs truncate">{user?.role}</p>
</div>
<button
onClick={handleLogout}
className="flex items-center gap-2 px-3 py-2 w-full rounded-lg text-[#94a3b8] hover:bg-[#1e2130] hover:text-red-400 transition-colors text-sm"
className="flex items-center gap-2 px-3 py-2 w-full rounded-lg text-[#94a3b8] hover:bg-[#1e2130] hover:text-red-400 transition-colors text-xs"
>
<LogOut size={16} className="flex-shrink-0" />
{sidebarOpen && <span>Logout</span>}
<LogOut size={14} className="flex-shrink-0" />
<span>Logout</span>
</button>
</div>
</aside>
{/* Main */}
<div className="flex-1 flex flex-col overflow-hidden">
{/* ── Main ────────────────────────────────────────────────────────── */}
<div className="flex-1 flex flex-col overflow-hidden min-w-0">
{/* Topbar */}
<header className="flex items-center gap-4 px-6 py-3 bg-[#1a1d27] border-b border-[#2a2d3e] min-h-[57px]">
<header className="flex items-center gap-4 px-6 py-3 bg-[#1a1d27] border-b border-[#2a2d3e] min-h-[57px] flex-shrink-0">
<button
onClick={() => setSidebarOpen(v => !v)}
className="text-[#64748b] hover:text-white transition-colors"
+35
View File
@@ -0,0 +1,35 @@
import { createContext, useContext, useEffect, useState } from 'react'
type Theme = 'dark' | 'light'
interface ThemeContextValue {
theme: Theme
toggleTheme: () => void
}
const ThemeContext = createContext<ThemeContextValue | null>(null)
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>(() =>
(localStorage.getItem('nixpanel-theme') as Theme) ?? 'dark'
)
useEffect(() => {
document.documentElement.setAttribute('data-theme', theme)
localStorage.setItem('nixpanel-theme', theme)
}, [theme])
const toggleTheme = () => setTheme(t => (t === 'dark' ? 'light' : 'dark'))
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
)
}
export function useTheme() {
const ctx = useContext(ThemeContext)
if (!ctx) throw new Error('useTheme must be used inside ThemeProvider')
return ctx
}
+42
View File
@@ -45,3 +45,45 @@ body {
::-webkit-scrollbar-thumb:hover {
background: #3a3d4e;
}
/* ── Light theme ─────────────────────────────────────────────────────────── */
[data-theme="light"] {
--bg-primary: #f0f4f8;
--bg-secondary: #ffffff;
--bg-card: #f1f5f9;
--border: #e2e8f0;
--text-primary: #0f172a;
--text-muted: #64748b;
}
[data-theme="light"] body {
background-color: #f0f4f8;
color: #0f172a;
}
/* Scrollbar */
[data-theme="light"] ::-webkit-scrollbar-track { background: #f1f5f9; }
[data-theme="light"] ::-webkit-scrollbar-thumb { background: #cbd5e1; }
[data-theme="light"] ::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
/* ── Background overrides ──────────────────────────────────────────────── */
/* Page background (darkest) */
[data-theme="light"] .bg-\[\#0f1117\] { background-color: #f0f4f8 !important; }
/* Cards / sidebar */
[data-theme="light"] .bg-\[\#1a1d27\] { background-color: #ffffff !important; }
/* Hover / alternate rows */
[data-theme="light"] .bg-\[\#1e2130\] { background-color: #f1f5f9 !important; }
/* ── Border overrides ──────────────────────────────────────────────────── */
[data-theme="light"] .border-\[\#2a2d3e\] { border-color: #e2e8f0 !important; }
/* ── Text overrides ────────────────────────────────────────────────────── */
[data-theme="light"] .text-white { color: #0f172a !important; }
[data-theme="light"] .text-\[\#94a3b8\] { color: #475569 !important; }
[data-theme="light"] .text-\[\#64748b\] { color: #64748b !important; }
[data-theme="light"] .text-\[\#4a5568\] { color: #94a3b8 !important; }
/* ── Input fields ──────────────────────────────────────────────────────── */
[data-theme="light"] input { color: #0f172a !important; }
[data-theme="light"] input::placeholder { color: #94a3b8 !important; }
+123 -12
View File
@@ -1,7 +1,9 @@
import React, { useEffect, useState, useRef } from 'react'
import { Users, Server, HardDrive, Activity } from 'lucide-react'
import { Users, Server, HardDrive, Activity, AlertTriangle, CheckCircle2, Sun, Moon } from 'lucide-react'
import { AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts'
import { api } from '../api/client'
import { useAuth } from '../contexts/AuthContext'
import { useTheme } from '../contexts/ThemeContext'
interface SystemStats {
cpu: number
@@ -18,6 +20,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 +44,52 @@ 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 { theme, toggleTheme } = useTheme()
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 +142,69 @@ 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} />
{/* ── 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>
</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 +218,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 ? (
@@ -132,8 +237,14 @@ export default function Dashboard() {
<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
contentStyle={{ background: '#1e2130', border: '1px solid #2a2d3e', borderRadius: 8, fontSize: 12 }}
labelStyle={{ color: '#94a3b8' }}
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' }}
/>
<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 %" />
@@ -146,15 +257,15 @@ 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>
<div className="grid grid-cols-3 gap-4">
{[
{ 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 => (
<div key={s.label} className="text-center">
<p className={`text-2xl font-bold ${s.color}`}>{s.value}</p>
+11 -3
View File
@@ -1,7 +1,8 @@
import React, { useState } from 'react'
import React, { useState, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { useAuth } from '../contexts/AuthContext'
import { Shield } from 'lucide-react'
import axios from 'axios'
export default function Login() {
const { login } = useAuth()
@@ -10,6 +11,13 @@ export default function Login() {
const [password, setPassword] = useState('')
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
const [version, setVersion] = useState<string | null>(null)
useEffect(() => {
axios.get('/api/health', { timeout: 5_000 })
.then(r => setVersion(r.data.version ?? null))
.catch(() => {})
}, [])
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
@@ -35,7 +43,7 @@ export default function Login() {
<Shield size={24} className="text-white" />
</div>
<h1 className="text-white text-xl font-bold">NixServer</h1>
<p className="text-[#64748b] text-sm mt-1">Web Host Manager</p>
<p className="text-[#64748b] text-sm mt-1">Admin Panel</p>
</div>
{/* Form */}
@@ -81,7 +89,7 @@ export default function Login() {
</form>
<p className="text-center text-[#64748b] text-xs mt-4">
NixServer v0.3.2 · Port 2087
NixServer{version ? ` v${version}` : ''} · Port 2087
</p>
</div>
</div>
+10 -1992
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "nixpanel-server",
"version": "0.4.1",
"version": "0.4.8",
"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
@@ -1,6 +1,6 @@
{
"name": "@nixpanel/server",
"version": "0.4.1",
"version": "0.4.3",
"private": true,
"type": "module",
"main": "dist/index.js",
+2 -2
View File
@@ -60,7 +60,7 @@ export async function buildApp() {
// ─── Static files ────────────────────────────────────────────────────────────
// NixServer frontend
const whmDist = path.resolve(__dirname, '../../nixserver/dist')
const nixserverDist = path.resolve(__dirname, '../../nixserver/dist')
const cpanelDist = path.resolve(__dirname, '../../nixclient/dist')
// ─── API Routes ──────────────────────────────────────────────────────────────
@@ -73,7 +73,7 @@ export async function buildApp() {
fastify.get('/api/health', async () => ({
status: 'ok',
version: '0.4.1',
version: '0.4.8',
timestamp: new Date().toISOString(),
}))
+1 -1
View File
@@ -24,7 +24,7 @@ const ALLOWED_COMMANDS = new Set([
// Network
'ss', 'ip', 'hostname',
// System
'uname', 'free', 'ps',
'uname', 'uptime', 'free', 'ps', 'kill',
// PHP
'php8.2', 'php8.3',
// Fail2ban
+55
View File
@@ -1,10 +1,13 @@
import type { FastifyInstance } from 'fastify'
import { spawn } from 'child_process'
import prisma from '../../db/client.js'
import { requireAdmin } from '../../middleware/auth.js'
import { getSystemStats } from '../../core/system.js'
import { exec } from '../../core/exec.js'
import { readFile } from 'fs/promises'
const INSTALL_DIR = process.env.INSTALL_DIR ?? '/opt/nixpanel'
export default async function systemRoutes(fastify: FastifyInstance) {
// GET /api/nixserver/system/stats
fastify.get('/stats', { preHandler: [requireAdmin] }, async (request, reply) => {
@@ -121,4 +124,56 @@ export default async function systemRoutes(fastify: FastifyInstance) {
return reply.send({ success: true, data: { message: 'Settings updated' } })
})
// GET /api/nixserver/system/version
// Returns the installed version and checks GitHub for the latest available version.
fastify.get('/version', { preHandler: [requireAdmin] }, async (_request, reply) => {
// Read installed version from package.json
let currentVersion = '0.0.0'
try {
const pkg = JSON.parse(await readFile(`${INSTALL_DIR}/package.json`, 'utf8'))
currentVersion = pkg.version ?? '0.0.0'
} catch { /* running in dev or non-standard path */ }
// Check GitHub for latest version (5 s timeout, non-fatal)
let latestVersion: string | null = null
try {
const resp = await fetch(
'https://raw.githubusercontent.com/NixReaper/nixpanel-server/main/package.json',
{ signal: AbortSignal.timeout(5_000) }
)
if (resp.ok) {
const json = await resp.json() as { version?: string }
latestVersion = json.version ?? null
}
} catch { /* offline or rate-limited */ }
return reply.send({
success: true,
data: {
currentVersion,
latestVersion,
updateAvailable: latestVersion !== null && latestVersion !== currentVersion,
},
})
})
// POST /api/nixserver/system/upgrade
// Fires the update script in the background and returns immediately.
// The service will restart ~60 s later once the build finishes.
fastify.post('/upgrade', { preHandler: [requireAdmin] }, async (_request, reply) => {
reply.send({
success: true,
data: { message: 'Upgrade started. The panel will be back online in about 60 seconds.' },
})
// Delay slightly to ensure the HTTP response is flushed before the service restarts
setTimeout(() => {
const child = spawn('bash', [`${INSTALL_DIR}/scripts/update.sh`], {
detached: true,
stdio: 'ignore',
})
child.unref()
}, 800)
})
}