Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da55367039 | ||
|
|
55786bfa0e | ||
|
|
70908745bd | ||
|
|
0362810681 | ||
|
|
9ec39c9120 | ||
|
|
b7eb76d91a | ||
|
|
5af2762634 | ||
|
|
ddb51a8b4d | ||
|
|
4de2a7fa8e | ||
|
|
90dbf94c18 | ||
|
|
146c17abd8 | ||
|
|
47ce924a90 | ||
|
|
82b9ac30e9 | ||
|
|
c6f13f2e07 | ||
|
|
945da97c97 | ||
|
|
a0e3fc7b1e | ||
|
|
da576bdbdc |
+9
-4
@@ -6,15 +6,20 @@ dist/
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# package-lock.json is platform-specific (rollup native binaries differ between
|
||||
# Windows dev machines and Linux servers). Never commit it — the server generates
|
||||
# its own on each install.
|
||||
package-lock.json
|
||||
|
||||
# Prisma
|
||||
server/prisma/dev.db
|
||||
server/prisma/dev.db-journal
|
||||
|
||||
# Build outputs
|
||||
server/dist/
|
||||
whm/dist/
|
||||
cpanel/dist/
|
||||
nixserver/dist/
|
||||
nixclient/dist/
|
||||
|
||||
# Vite
|
||||
whm/.vite/
|
||||
cpanel/.vite/
|
||||
nixserver/.vite/
|
||||
nixclient/.vite/
|
||||
|
||||
@@ -6,6 +6,93 @@ Pre-1.0 versions treat MINOR as feature releases and PATCH as bug fixes.
|
||||
|
||||
---
|
||||
|
||||
## [0.5.1] — 2026-04-11
|
||||
|
||||
### Bug Fixes
|
||||
- **`install.sh` build steps used `--silent`** — errors during `npm install` and frontend/server builds were suppressed, making failures invisible. Removed `--silent` from all build commands. Also added `rm -rf node_modules package-lock.json` before `npm install` to ensure a clean platform-correct install, consistent with `update.sh`.
|
||||
|
||||
---
|
||||
|
||||
## [0.5.0] — 2026-04-11
|
||||
|
||||
### Bug Fixes
|
||||
- **Rollup native binary missing on Linux** — `package-lock.json` was generated on Windows and committed to git. It included the Windows rollup binary (`@rollup/rollup-win32-x64-msvc`) but not the Linux one (`@rollup/rollup-linux-x64-gnu`), causing the frontend build to fail on the server with `Cannot find module @rollup/rollup-linux-x64-gnu`.
|
||||
- **`package-lock.json` added to `.gitignore`** — lock files are platform-specific and must never be committed. The server now generates its own fresh lock file on every update.
|
||||
- **`update.sh` now does a clean install** — deletes `node_modules/` and `package-lock.json` before `npm install` on every update run to guarantee platform-correct native binaries. Also drops `--silent` from build commands so errors are visible.
|
||||
- **`.gitignore` corrected** — removed stale `whm/` and `cpanel/` dist paths, added correct `nixserver/` and `nixclient/` paths.
|
||||
|
||||
---
|
||||
|
||||
## [0.4.9] — 2026-04-11
|
||||
|
||||
### Bug Fixes
|
||||
- **`update.sh` fails when `package-lock.json` has local changes** — `git pull --ff-only` aborted with "Your local changes would be overwritten by merge" whenever npm had modified the lock file on the server. Fixed by running `git checkout -- package-lock.json` before the pull to discard local changes to the generated lock file.
|
||||
|
||||
---
|
||||
|
||||
## [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,6 +1,6 @@
|
||||
{
|
||||
"name": "@nixpanel/cpanel",
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.3",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nixpanel/whm",
|
||||
"version": "0.4.1",
|
||||
"name": "@nixpanel/nixserver",
|
||||
"version": "0.4.6",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Generated
-6004
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "nixpanel-server",
|
||||
"version": "0.4.1",
|
||||
"version": "0.5.1",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"server",
|
||||
|
||||
+5
-4
@@ -547,7 +547,8 @@ success ".env written"
|
||||
# ── npm install ───────────────────────────────────────────────────────────────
|
||||
step "Installing Node.js dependencies"
|
||||
cd "$INSTALL_DIR"
|
||||
npm install --silent
|
||||
rm -rf node_modules package-lock.json
|
||||
npm install
|
||||
success "npm dependencies installed"
|
||||
|
||||
# ── Prisma generate + db push ─────────────────────────────────────────────────
|
||||
@@ -571,10 +572,10 @@ success "Admin account and default packages seeded"
|
||||
# ── Build ─────────────────────────────────────────────────────────────────────
|
||||
step "Building frontends and server"
|
||||
cd "$INSTALL_DIR"
|
||||
npm run build --workspace=nixserver --silent
|
||||
npm run build --workspace=nixclient --silent
|
||||
npm run build --workspace=nixserver
|
||||
npm run build --workspace=nixclient
|
||||
cd "$INSTALL_DIR/server"
|
||||
npm run build --silent
|
||||
npm run build
|
||||
success "Build complete"
|
||||
|
||||
# ── Set web-readable permissions on frontend assets ───────────────────────────
|
||||
|
||||
+19
-6
@@ -14,29 +14,42 @@ INSTALL_DIR="${1:-/opt/nixpanel}"
|
||||
[[ -d "$INSTALL_DIR/.git" ]] || error "NixPanel not found at $INSTALL_DIR"
|
||||
|
||||
step "Pulling latest code"
|
||||
# package-lock.json is not tracked (platform-specific), but discard any other
|
||||
# locally modified tracked files that could block a fast-forward pull
|
||||
git -C "$INSTALL_DIR" checkout -- . 2>/dev/null || true
|
||||
git -C "$INSTALL_DIR" pull --ff-only
|
||||
success "Code updated"
|
||||
|
||||
step "Installing dependencies"
|
||||
cd "$INSTALL_DIR"
|
||||
npm install --workspaces --include-workspace-root --silent
|
||||
# Delete node_modules and any stale lock file so npm regenerates them fresh
|
||||
# for this platform (avoids the rollup native binary mismatch between
|
||||
# Windows dev machines and Linux servers)
|
||||
rm -rf node_modules package-lock.json
|
||||
npm install --workspaces --include-workspace-root
|
||||
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
|
||||
npm run build --workspace=nixserver
|
||||
npm run build --workspace=nixclient
|
||||
# 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"
|
||||
cd "$INSTALL_DIR/server"
|
||||
npm run build --silent 2>/dev/null || npx tsc 2>/dev/null || true
|
||||
npm run build 2>/dev/null || npx tsc 2>/dev/null || true
|
||||
success "Server rebuilt"
|
||||
|
||||
step "Restarting service"
|
||||
|
||||
+1
-1
@@ -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
@@ -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.5.1',
|
||||
timestamp: new Date().toISOString(),
|
||||
}))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user