feat: add NixServer and NixClient React UIs, wire into installer

- nixserver/: React 18 + TypeScript + Vite + Tailwind
  - Login page + placeholder admin dashboard with card grid
  - Blue accent color (#3b82f6)
  - Full Tailwind + PostCSS + TypeScript setup
- nixclient/: identical structure, user-facing panel
  - Green accent color (#10b981)
  - Same build tooling as NixServer
- install.sh: new 'Building NixServer & NixClient UIs' step
  - npm install + npm run build for each
  - Outputs to /opt/nixpanel/nixserver/dist and /opt/nixpanel/nixclient/dist
  - Both panels immediately accessible via Apache on 2087 and 2083
  - Skips gracefully if source not present (early dev)
This commit is contained in:
NixReaper
2026-04-25 09:22:12 -05:00
parent 4994d33fd5
commit e58769a4bd
21 changed files with 506 additions and 3 deletions
+32 -3
View File
@@ -190,19 +190,48 @@ if ! step_done "source"; then
fi
success "Source ready"
# ── 8. Build panel binary ─────────────────────────────────────────────────────
# ── 8. Build panel UIs (React apps) ───────────────────────────────────────────
step "Building NixServer & NixClient UIs"
if ! step_done "build_uis"; then
cd "$SRC_DIR"
# Build NixServer (WHM admin panel)
if [[ -f "nixserver/package.json" ]]; then
info "Building NixServer…"
cd "$SRC_DIR/nixserver"
npm install --legacy-peer-deps --quiet 2>/dev/null || true
npm run build --silent 2>/dev/null
mkdir -p "$INSTALL_DIR/nixserver/dist"
cp -r dist/* "$INSTALL_DIR/nixserver/dist/" 2>/dev/null || true
success "NixServer built"
fi
# Build NixClient (cPanel user panel)
if [[ -f "nixclient/package.json" ]]; then
info "Building NixClient…"
cd "$SRC_DIR/nixclient"
npm install --legacy-peer-deps --quiet 2>/dev/null || true
npm run build --silent 2>/dev/null
mkdir -p "$INSTALL_DIR/nixclient/dist"
cp -r dist/* "$INSTALL_DIR/nixclient/dist/" 2>/dev/null || true
success "NixClient built"
fi
mark_done "build_uis"
fi
# ── 9. Build panel binary ─────────────────────────────────────────────────────
step "Building nixpanel (Rust)"
if ! step_done "build_panel"; then
if [[ -f "$SRC_DIR/Cargo.toml" ]]; then
cd "$SRC_DIR"
$CARGO build --release --quiet
# Copy any produced binaries
[[ -f "target/release/nixpanel" ]] && \
cp target/release/nixpanel "$BIN_DIR/nixpanel" && chmod 755 "$BIN_DIR/nixpanel"
mark_done "build_panel"
success "Panel binary built"
else
warn "Panel Rust source not yet present in repo — skipping build"
warn "Panel Rust backend not yet present in repo — skipping build"
warn "This is expected during early development. Re-run installer when source is ready."
fi
fi
+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NixClient — Your Hosting Account</title>
</head>
<body class="bg-gray-950">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+24
View File
@@ -0,0 +1,24 @@
{
"name": "nixpanel-nixclient",
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@vitejs/plugin-react": "^4.2.0",
"tailwindcss": "^3.4.0",
"postcss": "^8.4.0",
"autoprefixer": "^10.4.0",
"typescript": "^5.3.0",
"vite": "^5.0.0"
}
}
+6
View File
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
+108
View File
@@ -0,0 +1,108 @@
import { useState } from 'react'
export default function App() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [isLoggedIn, setIsLoggedIn] = useState(false)
const handleLogin = (e: React.FormEvent) => {
e.preventDefault()
if (email && password) {
setIsLoggedIn(true)
}
}
if (isLoggedIn) {
return (
<div className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 text-white">
<nav className="border-b border-gray-800 bg-gray-950/80 backdrop-blur-sm sticky top-0 z-40">
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
<h1 className="text-xl font-bold">Nix<span className="text-emerald-500">Client</span></h1>
<button
onClick={() => setIsLoggedIn(false)}
className="px-4 py-2 rounded-lg border border-gray-700 text-gray-300 hover:border-gray-500 hover:text-white transition-colors text-sm"
>
Sign out
</button>
</div>
</nav>
<main className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{[
{ title: 'My Domains', icon: '🌐', count: '0' },
{ title: 'Email Accounts', icon: '📧', count: '0' },
{ title: 'Databases', icon: '💾', count: '0' },
{ title: 'Backups', icon: '💾', count: '0' },
].map((item) => (
<div key={item.title} className="bg-gray-900/50 border border-gray-800 rounded-xl p-6">
<div className="text-4xl mb-3">{item.icon}</div>
<h3 className="font-semibold text-white mb-2">{item.title}</h3>
<p className="text-3xl font-bold text-emerald-500">{item.count}</p>
</div>
))}
</div>
<div className="mt-12 bg-emerald-900/20 border border-emerald-500/20 rounded-2xl p-8">
<h2 className="text-2xl font-bold text-white mb-2">Welcome to Your Account</h2>
<p className="text-gray-400">
Manage your domains, emails, databases, and more — all from one dashboard.
</p>
</div>
</main>
</div>
)
}
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950">
<div className="w-full max-w-md px-4">
<div className="text-center mb-8">
<h1 className="text-4xl font-bold text-white mb-2">
Nix<span className="text-emerald-500">Client</span>
</h1>
<p className="text-gray-400">Your Hosting Control Panel</p>
</div>
<div className="bg-gray-900/50 border border-gray-800 rounded-xl p-8">
<form onSubmit={handleLogin} className="space-y-4">
<div>
<label className="block text-sm text-gray-400 mb-1.5">Email</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="w-full bg-gray-900 border border-gray-700 text-white rounded-lg px-4 py-2.5 text-sm placeholder-gray-500 focus:outline-none focus:border-emerald-500 transition-colors"
placeholder="[email protected]"
/>
</div>
<div>
<label className="block text-sm text-gray-400 mb-1.5">Password</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="w-full bg-gray-900 border border-gray-700 text-white rounded-lg px-4 py-2.5 text-sm placeholder-gray-500 focus:outline-none focus:border-emerald-500 transition-colors"
placeholder="••••••••"
/>
</div>
<button
type="submit"
className="w-full inline-flex items-center justify-center gap-2 px-5 py-2.5 rounded-lg bg-emerald-600 hover:bg-emerald-700 text-white font-semibold text-sm transition-colors mt-6"
>
Sign in
</button>
</form>
</div>
<p className="text-center text-sm text-gray-500 mt-6">
NixPanel Client — Manage your hosting account
</p>
</div>
</div>
)
}
+22
View File
@@ -0,0 +1,22 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
background-color: rgb(3, 7, 30);
color: rgb(229, 231, 235);
}
+10
View File
@@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
+11
View File
@@ -0,0 +1,11 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
+18
View File
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
+10
View File
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
+15
View File
@@ -0,0 +1,15 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
build: {
outDir: 'dist',
sourcemap: false,
minify: 'terser',
},
server: {
port: 3001,
strictPort: true,
},
})
+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NixServer — NixPanel Admin</title>
</head>
<body class="bg-gray-950">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+24
View File
@@ -0,0 +1,24 @@
{
"name": "nixpanel-nixserver",
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@vitejs/plugin-react": "^4.2.0",
"tailwindcss": "^3.4.0",
"postcss": "^8.4.0",
"autoprefixer": "^10.4.0",
"typescript": "^5.3.0",
"vite": "^5.0.0"
}
}
+6
View File
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
+110
View File
@@ -0,0 +1,110 @@
import { useState } from 'react'
export default function App() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [isLoggedIn, setIsLoggedIn] = useState(false)
const handleLogin = (e: React.FormEvent) => {
e.preventDefault()
if (email && password) {
setIsLoggedIn(true)
}
}
if (isLoggedIn) {
return (
<div className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 text-white">
<nav className="border-b border-gray-800 bg-gray-950/80 backdrop-blur-sm sticky top-0 z-40">
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
<h1 className="text-xl font-bold">Nix<span className="text-blue-500">Server</span></h1>
<button
onClick={() => setIsLoggedIn(false)}
className="px-4 py-2 rounded-lg border border-gray-700 text-gray-300 hover:border-gray-500 hover:text-white transition-colors text-sm"
>
Sign out
</button>
</div>
</nav>
<main className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{[
{ title: 'Accounts', icon: '👥', count: '0' },
{ title: 'Domains', icon: '🌐', count: '0' },
{ title: 'Databases', icon: '💾', count: '0' },
{ title: 'Email Accounts', icon: '📧', count: '0' },
{ title: 'SSL Certificates', icon: '🔒', count: '0' },
{ title: 'Backups', icon: '💾', count: '0' },
].map((item) => (
<div key={item.title} className="bg-gray-900/50 border border-gray-800 rounded-xl p-6">
<div className="text-4xl mb-3">{item.icon}</div>
<h3 className="font-semibold text-white mb-2">{item.title}</h3>
<p className="text-3xl font-bold text-blue-500">{item.count}</p>
</div>
))}
</div>
<div className="mt-12 bg-blue-900/20 border border-blue-500/20 rounded-2xl p-8">
<h2 className="text-2xl font-bold text-white mb-2">Welcome to NixServer</h2>
<p className="text-gray-400">
This is a placeholder dashboard. Full server management features are coming soon.
</p>
</div>
</main>
</div>
)
}
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950">
<div className="w-full max-w-md px-4">
<div className="text-center mb-8">
<h1 className="text-4xl font-bold text-white mb-2">
Nix<span className="text-blue-500">Server</span>
</h1>
<p className="text-gray-400">Admin Control Panel</p>
</div>
<div className="bg-gray-900/50 border border-gray-800 rounded-xl p-8">
<form onSubmit={handleLogin} className="space-y-4">
<div>
<label className="block text-sm text-gray-400 mb-1.5">Email</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="w-full bg-gray-900 border border-gray-700 text-white rounded-lg px-4 py-2.5 text-sm placeholder-gray-500 focus:outline-none focus:border-blue-500 transition-colors"
placeholder="[email protected]"
/>
</div>
<div>
<label className="block text-sm text-gray-400 mb-1.5">Password</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="w-full bg-gray-900 border border-gray-700 text-white rounded-lg px-4 py-2.5 text-sm placeholder-gray-500 focus:outline-none focus:border-blue-500 transition-colors"
placeholder="••••••••"
/>
</div>
<button
type="submit"
className="w-full inline-flex items-center justify-center gap-2 px-5 py-2.5 rounded-lg bg-blue-600 hover:bg-blue-700 text-white font-semibold text-sm transition-colors mt-6"
>
Sign in
</button>
</form>
</div>
<p className="text-center text-sm text-gray-500 mt-6">
NixPanel Server Admin — Login with your credentials
</p>
</div>
</div>
)
}
+22
View File
@@ -0,0 +1,22 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
background-color: rgb(3, 7, 30);
color: rgb(229, 231, 235);
}
+10
View File
@@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
+11
View File
@@ -0,0 +1,11 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
+18
View File
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
+10
View File
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
+15
View File
@@ -0,0 +1,15 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
build: {
outDir: 'dist',
sourcemap: false,
minify: 'terser',
},
server: {
port: 3000,
strictPort: true,
},
})