From a0e3fc7b1ec2385f5d4fcd34b0c5c2b8e4c3fc56 Mon Sep 17 00:00:00 2001 From: NixReaper Date: Sat, 11 Apr 2026 11:55:25 -0500 Subject: [PATCH] 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. --- CHANGELOG.md | 11 +++ nixclient/package.json | 2 +- nixserver/package.json | 2 +- nixserver/src/components/Layout.tsx | 120 +++++++++++++++++++++++++- package.json | 2 +- server/package.json | 2 +- server/src/app.ts | 2 +- server/src/routes/nixserver/system.ts | 55 ++++++++++++ 8 files changed, 189 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d978d06..925de9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ Pre-1.0 versions treat MINOR as feature releases and PATCH as bug fixes. --- +## [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 diff --git a/nixclient/package.json b/nixclient/package.json index da86089..d2caf60 100644 --- a/nixclient/package.json +++ b/nixclient/package.json @@ -1,6 +1,6 @@ { "name": "@nixpanel/cpanel", - "version": "0.4.2", + "version": "0.4.3", "private": true, "type": "module", "scripts": { diff --git a/nixserver/package.json b/nixserver/package.json index f9e9476..9264bcf 100644 --- a/nixserver/package.json +++ b/nixserver/package.json @@ -1,6 +1,6 @@ { "name": "@nixpanel/whm", - "version": "0.4.2", + "version": "0.4.3", "private": true, "type": "module", "scripts": { diff --git a/nixserver/src/components/Layout.tsx b/nixserver/src/components/Layout.tsx index de33e90..3e2f4c6 100644 --- a/nixserver/src/components/Layout.tsx +++ b/nixserver/src/components/Layout.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo, useEffect } from 'react' +import { useState, useMemo, useEffect, useCallback } from 'react' import { Outlet, Link, useNavigate, useLocation } from 'react-router-dom' import { LayoutDashboard, Settings, HelpCircle, Network, BookUser, UserCog, @@ -6,8 +6,10 @@ import { Users, UserCheck, ArrowRightLeft, Paintbrush, Package, Globe, Database, Wifi, Box, Mail, HeartPulse, Lock, ShoppingBag, Code2, Puzzle, LogOut, Menu, X, - ChevronDown, ChevronRight, Search, RotateCcw + ChevronDown, ChevronRight, Search, RotateCcw, RefreshCcw, + ArrowUpCircle, CheckCircle2, AlertCircle, Loader2 } from 'lucide-react' +import axios from 'axios' import { useAuth } from '../contexts/AuthContext' // ─── Nav data ──────────────────────────────────────────────────────────────── @@ -456,6 +458,37 @@ export default function Layout() { 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(null) + const [upgradeState, setUpgradeState] = useState<'idle' | 'confirm' | 'running' | 'done' | 'error'>('idle') + + const fetchVersion = useCallback(async () => { + try { + const { data } = await axios.get('/api/nixserver/system/version') + setVersionInfo(data.data) + } catch { /* non-fatal */ } + }, []) + + useEffect(() => { fetchVersion() }, [fetchVersion]) + + const handleUpgrade = async () => { + if (upgradeState === 'confirm') { + setUpgradeState('running') + try { + await axios.post('/api/nixserver/system/upgrade') + setUpgradeState('done') + } catch { + setUpgradeState('error') + } + } else { + setUpgradeState('confirm') + } + } + + // ───────────────────────────────────────────────────────────────────────── + const [openCats, setOpenCats] = useState>(() => { // Auto-open the category that contains the active route on first render const active = NAV.find(cat => @@ -618,6 +651,89 @@ export default function Layout() { )} + {/* Version + upgrade */} +
+ {versionInfo ? ( +
+ {/* Version row */} +
+
+ {versionInfo.updateAvailable ? ( + + ) : ( + + )} + + v{versionInfo.currentVersion} + + {versionInfo.updateAvailable && versionInfo.latestVersion && ( + + → v{versionInfo.latestVersion} + + )} +
+ +
+ + {/* Upgrade button */} + {upgradeState === 'done' ? ( +
+ + Upgrade started — panel restarting… +
+ ) : upgradeState === 'error' ? ( +
+ + Upgrade failed — check logs +
+ ) : ( +
+ + {upgradeState === 'confirm' && ( + + )} +
+ )} +
+ ) : ( +
+ + Checking version… +
+ )} +
+ {/* User + logout */}
diff --git a/package.json b/package.json index b76a75c..bc3c0fa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nixpanel-server", - "version": "0.4.2", + "version": "0.4.3", "private": true, "workspaces": [ "server", diff --git a/server/package.json b/server/package.json index 0da7171..d783fda 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "@nixpanel/server", - "version": "0.4.2", + "version": "0.4.3", "private": true, "type": "module", "main": "dist/index.js", diff --git a/server/src/app.ts b/server/src/app.ts index 5a6d9fd..eba77e6 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -73,7 +73,7 @@ export async function buildApp() { fastify.get('/api/health', async () => ({ status: 'ok', - version: '0.4.2', + version: '0.4.3', timestamp: new Date().toISOString(), })) diff --git a/server/src/routes/nixserver/system.ts b/server/src/routes/nixserver/system.ts index cef6e3c..5970621 100644 --- a/server/src/routes/nixserver/system.ts +++ b/server/src/routes/nixserver/system.ts @@ -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) + }) }