mirror of
https://github.com/myronblair/novacpx
synced 2026-06-30 17:50:41 -05:00
feat: NovaCPX v1.0.0 initial scaffold
Full hosting control panel with 3 tiers: Admin, Reseller, User. - install.sh: unattended installer for Ubuntu 20/22/24 + Debian 11/12 - PHP multi-version (7.4/8.1/8.2/8.3), Apache2/nginx choice, MySQL, PostgreSQL - BIND9 DNS, Postfix+Dovecot mail, ProFTPD, Certbot SSL, UFW, Fail2Ban - 18-table DB schema with audit log and version tracking - PHP REST API (auth, system/updates, server stats, service control) - Admin panel: dark dashboard, service manager, git-based update system - User panel: usage rings + feature card grid (distinct from cPanel) - VERSION file: git-tracked; Admin > Updates panel shows/applies git commits Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
Options -Indexes
|
||||
RewriteEngine On
|
||||
|
||||
# Route API calls
|
||||
RewriteRule ^api/(.*)$ api/index.php [QSA,L]
|
||||
|
||||
# Panel routes — serve index.php for SPA navigation within each panel tier
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule ^(admin|reseller|user)/.*$ $1/index.php [QSA,L]
|
||||
|
||||
# Security headers
|
||||
Header always set X-Frame-Options "SAMEORIGIN"
|
||||
Header always set X-Content-Type-Options "nosniff"
|
||||
Header always set X-XSS-Protection "1; mode=block"
|
||||
Header always set Referrer-Policy "strict-origin-when-cross-origin"
|
||||
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
|
||||
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
// NovaCPX Admin Panel — Datacenter/Server Manager
|
||||
// Equivalent to WHM (WebHost Manager)
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>NovaCPX Admin</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/assets/img/favicon.svg">
|
||||
<link rel="stylesheet" href="/assets/css/nova.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="panel-layout" id="app" style="display:none">
|
||||
|
||||
<!-- Sidebar -->
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<div class="sidebar-brand">
|
||||
<svg class="logo-icon" viewBox="0 0 40 40" fill="none">
|
||||
<circle cx="20" cy="20" r="18" stroke="url(#lg1)" stroke-width="2"/>
|
||||
<path d="M12 28 L20 8 L28 28" stroke="url(#lg2)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M14 22 H26" stroke="url(#lg2)" stroke-width="2" stroke-linecap="round"/>
|
||||
<defs>
|
||||
<linearGradient id="lg1" x1="2" y1="2" x2="38" y2="38"><stop offset="0%" stop-color="#6366f1"/><stop offset="100%" stop-color="#0ea5e9"/></linearGradient>
|
||||
<linearGradient id="lg2" x1="12" y1="8" x2="28" y2="28"><stop offset="0%" stop-color="#6366f1"/><stop offset="100%" stop-color="#0ea5e9"/></linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<span class="logo-text">Nova<strong>CPX</strong> <small style="font-size:.65rem;color:var(--text-muted)">Admin</small></span>
|
||||
</div>
|
||||
|
||||
<nav>
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-label">Overview</div>
|
||||
<a href="#" class="sidebar-link active" data-page="dashboard">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>
|
||||
Dashboard
|
||||
</a>
|
||||
<a href="#" class="sidebar-link" data-page="server-status">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="8" rx="2"/><rect x="2" y="14" width="20" height="8" rx="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg>
|
||||
Server Status
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-label">Accounts</div>
|
||||
<a href="#" class="sidebar-link" data-page="accounts">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
||||
All Accounts
|
||||
</a>
|
||||
<a href="#" class="sidebar-link" data-page="resellers">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="8" r="4"/><path d="M2 20c0-4 4-7 10-7s10 3 10 7"/></svg>
|
||||
Resellers
|
||||
</a>
|
||||
<a href="#" class="sidebar-link" data-page="packages">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg>
|
||||
Packages
|
||||
</a>
|
||||
<a href="#" class="sidebar-link" data-page="create-account">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><line x1="19" y1="8" x2="19" y2="14"/><line x1="22" y1="11" x2="16" y2="11"/></svg>
|
||||
Create Account
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-label">DNS</div>
|
||||
<a href="#" class="sidebar-link" data-page="dns-zones">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
||||
DNS Zones
|
||||
</a>
|
||||
<a href="#" class="sidebar-link" data-page="nameservers">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
|
||||
Nameservers
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-label">Services</div>
|
||||
<a href="#" class="sidebar-link" data-page="web-server">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>
|
||||
Web Server
|
||||
</a>
|
||||
<a href="#" class="sidebar-link" data-page="php-manager">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
|
||||
PHP Manager
|
||||
</a>
|
||||
<a href="#" class="sidebar-link" data-page="mysql-manager">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/></svg>
|
||||
MySQL / PgSQL
|
||||
</a>
|
||||
<a href="#" class="sidebar-link" data-page="mail-server">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>
|
||||
Mail Server
|
||||
</a>
|
||||
<a href="#" class="sidebar-link" data-page="ftp-server">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
|
||||
FTP Server
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-label">Security</div>
|
||||
<a href="#" class="sidebar-link" data-page="ssl-manager">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
|
||||
SSL Manager
|
||||
</a>
|
||||
<a href="#" class="sidebar-link" data-page="firewall">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
||||
Firewall / Fail2Ban
|
||||
</a>
|
||||
<a href="#" class="sidebar-link" data-page="audit-log">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>
|
||||
Audit Log
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-label">System</div>
|
||||
<a href="#" class="sidebar-link" data-page="updates">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>
|
||||
Updates <span id="update-badge" class="badge badge-yellow" style="display:none"></span>
|
||||
</a>
|
||||
<a href="#" class="sidebar-link" data-page="backups">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
|
||||
Backups
|
||||
</a>
|
||||
<a href="#" class="sidebar-link" data-page="settings">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
|
||||
Settings
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-user">
|
||||
<div class="sidebar-user-info">
|
||||
<div class="avatar" id="user-avatar">A</div>
|
||||
<div>
|
||||
<div class="user-name" id="user-name">Admin</div>
|
||||
<div class="user-role">Administrator</div>
|
||||
</div>
|
||||
<a href="#" id="logout-btn" class="btn btn-ghost btn-sm btn-icon" title="Logout" style="margin-left:auto">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="main-content">
|
||||
<header class="topbar">
|
||||
<button class="btn btn-ghost btn-icon" id="sidebar-toggle" style="display:none">☰</button>
|
||||
<div class="topbar-title" id="page-title">Dashboard</div>
|
||||
<div class="topbar-actions">
|
||||
<span id="server-ip" class="text-muted text-sm"></span>
|
||||
<div id="alert-indicator" style="display:none">
|
||||
<span class="badge badge-red" id="alert-count"></span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="page-content" id="page-content">
|
||||
<!-- Loaded by JS -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Auth guard -->
|
||||
<div id="auth-check" style="display:flex;align-items:center;justify-content:center;min-height:100vh">
|
||||
<div style="text-align:center;color:var(--text-muted)">Verifying session…</div>
|
||||
</div>
|
||||
|
||||
<script src="/assets/js/nova.js"></script>
|
||||
<script src="/assets/js/admin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,302 @@
|
||||
/* NovaCPX Design System */
|
||||
:root {
|
||||
--bg: #0d0f17;
|
||||
--bg2: #131520;
|
||||
--bg3: #1a1d2e;
|
||||
--border: #252840;
|
||||
--text: #e2e4f0;
|
||||
--text-muted: #7c7f9a;
|
||||
--primary: #6366f1;
|
||||
--primary-h: #4f52e8;
|
||||
--sky: #0ea5e9;
|
||||
--green: #10b981;
|
||||
--yellow: #f59e0b;
|
||||
--red: #ef4444;
|
||||
--radius: 10px;
|
||||
--shadow: 0 4px 24px rgba(0,0,0,.4);
|
||||
--font: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
html { font-size: 15px; }
|
||||
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: var(--font);
|
||||
line-height: 1.6;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* ── Login Page ─────────────────────────────────────────────────────────────── */
|
||||
.login-page {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
background: radial-gradient(ellipse at 30% 20%, rgba(99,102,241,.15) 0%, transparent 60%),
|
||||
radial-gradient(ellipse at 80% 80%, rgba(14,165,233,.1) 0%, transparent 60%),
|
||||
var(--bg);
|
||||
}
|
||||
|
||||
.login-wrap { width: 100%; max-width: 420px; padding: 1.5rem; }
|
||||
|
||||
.login-brand {
|
||||
display: flex; align-items: center; gap: .75rem;
|
||||
justify-content: center; margin-bottom: 2rem;
|
||||
}
|
||||
.logo-icon { width: 42px; height: 42px; }
|
||||
.logo-text { font-size: 1.8rem; font-weight: 300; letter-spacing: -.5px; }
|
||||
.logo-text strong { font-weight: 700; background: linear-gradient(135deg, #6366f1, #0ea5e9); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
|
||||
|
||||
.login-card {
|
||||
background: var(--bg2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
.login-card h1 { font-size: 1.4rem; margin-bottom: .25rem; }
|
||||
.login-sub { color: var(--text-muted); font-size: .875rem; margin-bottom: 1.5rem; }
|
||||
|
||||
.login-footer {
|
||||
text-align: center; margin-top: 1.25rem;
|
||||
font-size: .8rem; color: var(--text-muted);
|
||||
}
|
||||
.login-footer a { color: var(--primary); text-decoration: none; }
|
||||
|
||||
/* ── Forms ──────────────────────────────────────────────────────────────────── */
|
||||
.form-group { margin-bottom: 1rem; }
|
||||
.form-group label { display: block; font-size: .85rem; font-weight: 500; margin-bottom: .4rem; color: var(--text-muted); }
|
||||
|
||||
input[type="text"], input[type="password"], input[type="email"],
|
||||
input[type="number"], input[type="url"], select, textarea {
|
||||
width: 100%; padding: .65rem .9rem;
|
||||
background: var(--bg3); border: 1px solid var(--border);
|
||||
border-radius: var(--radius); color: var(--text);
|
||||
font-family: var(--font); font-size: .9rem;
|
||||
transition: border-color .15s;
|
||||
outline: none;
|
||||
}
|
||||
input:focus, select:focus, textarea:focus { border-color: var(--primary); }
|
||||
|
||||
.input-with-icon { position: relative; }
|
||||
.input-with-icon input { padding-right: 2.5rem; }
|
||||
.eye-toggle {
|
||||
position: absolute; right: .75rem; top: 50%; transform: translateY(-50%);
|
||||
background: none; border: none; cursor: pointer; color: var(--text-muted);
|
||||
padding: 0; display: flex; align-items: center;
|
||||
}
|
||||
.eye-toggle svg { width: 18px; height: 18px; }
|
||||
|
||||
/* ── Buttons ─────────────────────────────────────────────────────────────────── */
|
||||
.btn {
|
||||
display: inline-flex; align-items: center; gap: .4rem;
|
||||
padding: .6rem 1.25rem; border: none; border-radius: var(--radius);
|
||||
font-family: var(--font); font-size: .9rem; font-weight: 500;
|
||||
cursor: pointer; transition: all .15s; text-decoration: none;
|
||||
}
|
||||
.btn-primary { background: var(--primary); color: #fff; }
|
||||
.btn-primary:hover { background: var(--primary-h); }
|
||||
.btn-sky { background: var(--sky); color: #fff; }
|
||||
.btn-green { background: var(--green); color: #fff; }
|
||||
.btn-red { background: var(--red); color: #fff; }
|
||||
.btn-ghost { background: transparent; border: 1px solid var(--border); color: var(--text); }
|
||||
.btn-ghost:hover { border-color: var(--primary); color: var(--primary); }
|
||||
.btn-full { width: 100%; justify-content: center; padding: .75rem; }
|
||||
.btn:disabled { opacity: .6; cursor: not-allowed; }
|
||||
.btn-sm { padding: .35rem .8rem; font-size: .82rem; }
|
||||
.btn-icon { padding: .5rem; border-radius: 8px; }
|
||||
|
||||
/* ── Alerts ──────────────────────────────────────────────────────────────────── */
|
||||
.alert { padding: .75rem 1rem; border-radius: var(--radius); font-size: .875rem; margin-bottom: 1rem; }
|
||||
.alert-error { background: rgba(239,68,68,.12); border: 1px solid rgba(239,68,68,.3); color: #fca5a5; }
|
||||
.alert-success { background: rgba(16,185,129,.12); border: 1px solid rgba(16,185,129,.3); color: #6ee7b7; }
|
||||
.alert-warning { background: rgba(245,158,11,.12); border: 1px solid rgba(245,158,11,.3); color: #fcd34d; }
|
||||
.alert-info { background: rgba(99,102,241,.12); border: 1px solid rgba(99,102,241,.3); color: #a5b4fc; }
|
||||
|
||||
/* ── Panel Layout ────────────────────────────────────────────────────────────── */
|
||||
.panel-layout { display: flex; min-height: 100vh; }
|
||||
|
||||
.sidebar {
|
||||
width: 240px; min-width: 240px; background: var(--bg2);
|
||||
border-right: 1px solid var(--border);
|
||||
display: flex; flex-direction: column;
|
||||
position: fixed; height: 100vh; overflow-y: auto; z-index: 100;
|
||||
}
|
||||
.sidebar-brand {
|
||||
display: flex; align-items: center; gap: .6rem;
|
||||
padding: 1.25rem 1.25rem 1rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.sidebar-brand .logo-text { font-size: 1.1rem; }
|
||||
.sidebar-brand .logo-icon { width: 28px; height: 28px; }
|
||||
|
||||
.sidebar-section { padding: .75rem 0; }
|
||||
.sidebar-section-label {
|
||||
font-size: .7rem; font-weight: 700; letter-spacing: .08em;
|
||||
text-transform: uppercase; color: var(--text-muted);
|
||||
padding: .25rem 1.25rem .5rem;
|
||||
}
|
||||
.sidebar-link {
|
||||
display: flex; align-items: center; gap: .75rem;
|
||||
padding: .55rem 1.25rem; text-decoration: none;
|
||||
color: var(--text-muted); font-size: .88rem;
|
||||
border-left: 3px solid transparent;
|
||||
transition: all .12s;
|
||||
}
|
||||
.sidebar-link:hover { color: var(--text); background: var(--bg3); }
|
||||
.sidebar-link.active { color: var(--primary); background: rgba(99,102,241,.1); border-left-color: var(--primary); }
|
||||
.sidebar-link svg { width: 18px; height: 18px; flex-shrink: 0; }
|
||||
|
||||
.sidebar-user {
|
||||
margin-top: auto; padding: 1rem 1.25rem;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.sidebar-user-info { display: flex; align-items: center; gap: .75rem; }
|
||||
.avatar {
|
||||
width: 36px; height: 36px; border-radius: 50%;
|
||||
background: linear-gradient(135deg, var(--primary), var(--sky));
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-weight: 700; font-size: .9rem; flex-shrink: 0;
|
||||
}
|
||||
.user-name { font-size: .88rem; font-weight: 600; }
|
||||
.user-role { font-size: .75rem; color: var(--text-muted); text-transform: capitalize; }
|
||||
|
||||
.main-content { margin-left: 240px; flex: 1; display: flex; flex-direction: column; }
|
||||
|
||||
.topbar {
|
||||
background: var(--bg2); border-bottom: 1px solid var(--border);
|
||||
padding: .75rem 1.5rem; display: flex; align-items: center;
|
||||
gap: 1rem; position: sticky; top: 0; z-index: 50;
|
||||
}
|
||||
.topbar-title { font-size: 1rem; font-weight: 600; flex: 1; }
|
||||
.topbar-actions { display: flex; align-items: center; gap: .5rem; }
|
||||
|
||||
.page-content { padding: 1.5rem; flex: 1; }
|
||||
|
||||
/* ── Cards ───────────────────────────────────────────────────────────────────── */
|
||||
.card {
|
||||
background: var(--bg2); border: 1px solid var(--border);
|
||||
border-radius: var(--radius); overflow: hidden;
|
||||
}
|
||||
.card-header {
|
||||
padding: 1rem 1.25rem; border-bottom: 1px solid var(--border);
|
||||
display: flex; align-items: center; gap: .75rem;
|
||||
}
|
||||
.card-title { font-size: .95rem; font-weight: 600; flex: 1; }
|
||||
.card-body { padding: 1.25rem; }
|
||||
|
||||
/* ── Stats Cards ─────────────────────────────────────────────────────────────── */
|
||||
.stats-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 1rem; margin-bottom: 1.5rem; }
|
||||
.stat-card {
|
||||
background: var(--bg2); border: 1px solid var(--border);
|
||||
border-radius: var(--radius); padding: 1.25rem;
|
||||
}
|
||||
.stat-label { font-size: .78rem; text-transform: uppercase; letter-spacing: .05em; color: var(--text-muted); margin-bottom: .5rem; }
|
||||
.stat-value { font-size: 1.8rem; font-weight: 700; line-height: 1; }
|
||||
.stat-sub { font-size: .78rem; color: var(--text-muted); margin-top: .3rem; }
|
||||
.stat-green { color: var(--green); }
|
||||
.stat-red { color: var(--red); }
|
||||
.stat-yellow{ color: var(--yellow); }
|
||||
.stat-blue { color: var(--sky); }
|
||||
|
||||
/* ── Progress bar ────────────────────────────────────────────────────────────── */
|
||||
.progress { background: var(--bg3); border-radius: 999px; height: 6px; overflow: hidden; }
|
||||
.progress-bar { height: 100%; border-radius: 999px; transition: width .3s; }
|
||||
.progress-bar.green { background: var(--green); }
|
||||
.progress-bar.yellow { background: var(--yellow); }
|
||||
.progress-bar.red { background: var(--red); }
|
||||
|
||||
/* ── Tables ──────────────────────────────────────────────────────────────────── */
|
||||
.table-wrap { overflow-x: auto; }
|
||||
table { width: 100%; border-collapse: collapse; font-size: .88rem; }
|
||||
th { text-align: left; font-size: .75rem; text-transform: uppercase; letter-spacing: .05em;
|
||||
color: var(--text-muted); padding: .65rem 1rem; border-bottom: 1px solid var(--border); }
|
||||
td { padding: .75rem 1rem; border-bottom: 1px solid var(--border); }
|
||||
tr:last-child td { border-bottom: none; }
|
||||
tr:hover td { background: var(--bg3); }
|
||||
|
||||
/* ── Badges ──────────────────────────────────────────────────────────────────── */
|
||||
.badge { display: inline-block; padding: .2rem .55rem; border-radius: 999px; font-size: .72rem; font-weight: 600; }
|
||||
.badge-green { background: rgba(16,185,129,.15); color: #6ee7b7; }
|
||||
.badge-red { background: rgba(239,68,68,.15); color: #fca5a5; }
|
||||
.badge-yellow { background: rgba(245,158,11,.15); color: #fcd34d; }
|
||||
.badge-blue { background: rgba(99,102,241,.15); color: #a5b4fc; }
|
||||
.badge-sky { background: rgba(14,165,233,.15); color: #7dd3fc; }
|
||||
.badge-gray { background: rgba(148,163,184,.15); color: #94a3b8; }
|
||||
|
||||
/* ── Modal ───────────────────────────────────────────────────────────────────── */
|
||||
.modal-overlay {
|
||||
display: none; position: fixed; inset: 0;
|
||||
background: rgba(0,0,0,.7); z-index: 1000;
|
||||
align-items: center; justify-content: center;
|
||||
}
|
||||
.modal-overlay.open { display: flex; }
|
||||
.modal {
|
||||
background: var(--bg2); border: 1px solid var(--border);
|
||||
border-radius: 14px; width: 100%; max-width: 500px;
|
||||
max-height: 90vh; overflow-y: auto;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,.6);
|
||||
}
|
||||
.modal-header {
|
||||
padding: 1.25rem 1.5rem; border-bottom: 1px solid var(--border);
|
||||
display: flex; align-items: center;
|
||||
}
|
||||
.modal-title { font-size: 1rem; font-weight: 600; flex: 1; }
|
||||
.modal-close { background: none; border: none; cursor: pointer; color: var(--text-muted); font-size: 1.25rem; }
|
||||
.modal-body { padding: 1.5rem; }
|
||||
.modal-footer { padding: 1rem 1.5rem; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: .5rem; }
|
||||
|
||||
/* ── Tabs ────────────────────────────────────────────────────────────────────── */
|
||||
.tabs { display: flex; gap: 0; border-bottom: 1px solid var(--border); margin-bottom: 1.5rem; }
|
||||
.tab-btn {
|
||||
padding: .65rem 1.25rem; border: none; background: none;
|
||||
color: var(--text-muted); font-size: .88rem; cursor: pointer;
|
||||
border-bottom: 2px solid transparent; margin-bottom: -1px;
|
||||
transition: color .12s;
|
||||
}
|
||||
.tab-btn:hover { color: var(--text); }
|
||||
.tab-btn.active { color: var(--primary); border-bottom-color: var(--primary); }
|
||||
.tab-pane { display: none; }
|
||||
.tab-pane.active { display: block; }
|
||||
|
||||
/* ── Grid helpers ────────────────────────────────────────────────────────────── */
|
||||
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
|
||||
.grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1rem; }
|
||||
.grid-4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; }
|
||||
@media (max-width: 768px) {
|
||||
.sidebar { transform: translateX(-100%); transition: transform .2s; }
|
||||
.sidebar.open { transform: translateX(0); }
|
||||
.main-content { margin-left: 0; }
|
||||
.grid-2,.grid-3,.grid-4 { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
/* ── Services status ─────────────────────────────────────────────────────────── */
|
||||
.service-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
|
||||
.service-dot.active { background: var(--green); box-shadow: 0 0 6px var(--green); }
|
||||
.service-dot.inactive { background: var(--red); }
|
||||
.service-dot.unknown { background: var(--text-muted); }
|
||||
|
||||
/* ── Code / Terminal ─────────────────────────────────────────────────────────── */
|
||||
code { font-family: 'JetBrains Mono', 'Fira Code', monospace; font-size: .85em; background: var(--bg3); padding: .15em .4em; border-radius: 4px; }
|
||||
.terminal {
|
||||
background: #050508; border: 1px solid var(--border); border-radius: var(--radius);
|
||||
padding: 1rem; font-family: monospace; font-size: .82rem; line-height: 1.7;
|
||||
color: #a6e22e; max-height: 300px; overflow-y: auto;
|
||||
}
|
||||
|
||||
/* ── Scrollbar ───────────────────────────────────────────────────────────────── */
|
||||
::-webkit-scrollbar { width: 5px; }
|
||||
::-webkit-scrollbar-track { background: var(--bg); }
|
||||
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 999px; }
|
||||
|
||||
/* ── Utility ─────────────────────────────────────────────────────────────────── */
|
||||
.flex { display: flex; } .items-center { align-items: center; } .justify-between { justify-content: space-between; }
|
||||
.gap-1 { gap: .5rem; } .gap-2 { gap: 1rem; } .gap-3 { gap: 1.5rem; }
|
||||
.mb-1 { margin-bottom: .5rem; } .mb-2 { margin-bottom: 1rem; } .mb-3 { margin-bottom: 1.5rem; }
|
||||
.mt-1 { margin-top: .5rem; } .mt-2 { margin-top: 1rem; }
|
||||
.text-muted { color: var(--text-muted); } .text-sm { font-size: .82rem; }
|
||||
.text-right { text-align: right; } .font-bold { font-weight: 700; }
|
||||
.w-full { width: 100%; } .hidden { display: none; }
|
||||
@@ -0,0 +1,315 @@
|
||||
/**
|
||||
* NovaCPX Admin Panel — page controllers
|
||||
*/
|
||||
(async () => {
|
||||
// ── Auth guard ─────────────────────────────────────────────────────────────
|
||||
const me = await Nova.api('auth', 'me');
|
||||
if (!me?.success || me.data.role !== 'admin') {
|
||||
location.href = '/?redirect=/admin/';
|
||||
return;
|
||||
}
|
||||
document.getElementById('auth-check').style.display = 'none';
|
||||
document.getElementById('app').style.display = '';
|
||||
document.getElementById('user-name').textContent = me.data.username;
|
||||
document.getElementById('user-avatar').textContent = me.data.username[0].toUpperCase();
|
||||
|
||||
// ── Logout ─────────────────────────────────────────────────────────────────
|
||||
document.getElementById('logout-btn').addEventListener('click', async e => {
|
||||
e.preventDefault();
|
||||
await Nova.api('auth', 'logout', { method: 'POST' });
|
||||
location.href = '/';
|
||||
});
|
||||
|
||||
// ── Page definitions ───────────────────────────────────────────────────────
|
||||
const pages = {
|
||||
dashboard,
|
||||
'server-status': serverStatus,
|
||||
accounts,
|
||||
resellers,
|
||||
packages,
|
||||
'create-account': createAccount,
|
||||
'dns-zones': dnsZones,
|
||||
nameservers,
|
||||
'web-server': webServer,
|
||||
'php-manager': phpManager,
|
||||
'mysql-manager': mysqlManager,
|
||||
'mail-server': mailServer,
|
||||
'ftp-server': ftpServer,
|
||||
'ssl-manager': sslManager,
|
||||
firewall,
|
||||
'audit-log': auditLog,
|
||||
updates,
|
||||
backups,
|
||||
settings,
|
||||
};
|
||||
|
||||
Nova.initNav(pages);
|
||||
await Nova.loadPage('dashboard', pages);
|
||||
checkUpdates();
|
||||
|
||||
// ── Dashboard ──────────────────────────────────────────────────────────────
|
||||
async function dashboard() {
|
||||
const [stats, version] = await Promise.all([
|
||||
Nova.api('system', 'stats'),
|
||||
Nova.api('system', 'version'),
|
||||
]);
|
||||
const s = stats?.data || {};
|
||||
const v = version?.data || {};
|
||||
|
||||
document.getElementById('server-ip').textContent = '';
|
||||
|
||||
return `
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">CPU Usage</div>
|
||||
<div class="stat-value ${s.cpu?.pct > 80 ? 'stat-red' : 'stat-green'}">${s.cpu?.pct ?? 0}%</div>
|
||||
<div class="stat-sub">Load: ${(s.cpu?.load || [0,0,0]).join(' / ')}</div>
|
||||
<div class="mt-1">${Nova.progressBar(s.cpu?.pct || 0)}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Memory</div>
|
||||
<div class="stat-value ${s.ram?.pct > 80 ? 'stat-red' : 'stat-blue'}">${s.ram?.pct ?? 0}%</div>
|
||||
<div class="stat-sub">${Nova.bytes((s.ram?.used_kb||0)*1024)} / ${Nova.bytes((s.ram?.total_kb||0)*1024)}</div>
|
||||
<div class="mt-1">${Nova.progressBar(s.ram?.pct || 0)}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Disk</div>
|
||||
<div class="stat-value ${s.disk?.pct > 85 ? 'stat-red' : 'stat-yellow'}">${s.disk?.pct ?? 0}%</div>
|
||||
<div class="stat-sub">${Nova.bytes(s.disk?.total - s.disk?.free || 0)} used</div>
|
||||
<div class="mt-1">${Nova.progressBar(s.disk?.pct || 0)}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Uptime</div>
|
||||
<div class="stat-value stat-green" style="font-size:1rem;padding-top:.4rem">${s.uptime || '—'}</div>
|
||||
<div class="stat-sub">PHP ${v.php_version || '—'}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-2 gap-2">
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-title">Services</span></div>
|
||||
<div class="card-body">
|
||||
<table><tbody>
|
||||
${Object.entries(s.services || {}).map(([svc, status]) => `
|
||||
<tr>
|
||||
<td>${Nova.serviceDot(status)} ${svc}</td>
|
||||
<td>${Nova.badge(status, status === 'active' ? 'green' : 'red')}</td>
|
||||
<td class="text-right">
|
||||
<button class="btn btn-ghost btn-sm" onclick="adminServiceAction('${svc}','restart')">Restart</button>
|
||||
<button class="btn btn-ghost btn-sm" onclick="adminServiceAction('${svc}','stop')">Stop</button>
|
||||
</td>
|
||||
</tr>`).join('')}
|
||||
</tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-title">NovaCPX Version</span></div>
|
||||
<div class="card-body">
|
||||
<table><tbody>
|
||||
<tr><td class="text-muted">Installed</td><td><strong>${v.installed_version || '—'}</strong></td></tr>
|
||||
<tr><td class="text-muted">Branch</td><td><code>${v.git_branch || 'main'}</code></td></tr>
|
||||
<tr><td class="text-muted">Commit</td><td><code>${v.git_commit || '—'}</code>${v.git_dirty ? ' <span class="badge badge-yellow">dirty</span>' : ''}</td></tr>
|
||||
<tr><td class="text-muted">PHP</td><td>${v.php_version || '—'}</td></tr>
|
||||
<tr><td class="text-muted">OS</td><td>${v.os || '—'}</td></tr>
|
||||
</tbody></table>
|
||||
<div class="mt-2">
|
||||
<button class="btn btn-primary btn-sm" onclick="adminPage('updates')">Check for Updates</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── Server Status ──────────────────────────────────────────────────────────
|
||||
async function serverStatus() {
|
||||
const res = await Nova.api('system', 'stats');
|
||||
const s = res?.data || {};
|
||||
return `
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-title">Real-Time Server Status</span>
|
||||
<button class="btn btn-ghost btn-sm" onclick="adminPage('server-status')">↻ Refresh</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="grid-3">
|
||||
<div><p class="text-muted text-sm mb-1">CPU</p><h2>${s.cpu?.pct}%</h2>${Nova.progressBar(s.cpu?.pct||0)}</div>
|
||||
<div><p class="text-muted text-sm mb-1">RAM</p><h2>${s.ram?.pct}%</h2>${Nova.progressBar(s.ram?.pct||0)}</div>
|
||||
<div><p class="text-muted text-sm mb-1">Disk</p><h2>${s.disk?.pct}%</h2>${Nova.progressBar(s.disk?.pct||0)}</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<p class="text-muted text-sm mb-1">Load Average</p>
|
||||
<p>${(s.cpu?.load||[]).join(' / ')}</p>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<p class="text-muted text-sm mb-1">Uptime</p>
|
||||
<p>${s.uptime}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── Updates ────────────────────────────────────────────────────────────────
|
||||
async function updates() {
|
||||
const [ver, check] = await Promise.all([
|
||||
Nova.api('system', 'version'),
|
||||
Nova.api('system', 'check-update'),
|
||||
]);
|
||||
const v = ver?.data || {};
|
||||
const upd = check?.data || {};
|
||||
const count = upd.updates_available || 0;
|
||||
|
||||
return `
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">NovaCPX Updates</span>
|
||||
${count > 0 ? Nova.badge(count + ' update' + (count > 1 ? 's' : '') + ' available', 'yellow') : Nova.badge('Up to date', 'green')}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="grid-2 mb-3">
|
||||
<div><p class="text-muted text-sm">Installed Version</p><p class="font-bold">${v.installed_version}</p></div>
|
||||
<div><p class="text-muted text-sm">Git Commit</p><code>${v.git_commit || '—'}</code></div>
|
||||
<div><p class="text-muted text-sm">Branch</p><code>${v.git_branch || 'main'}</code></div>
|
||||
<div><p class="text-muted text-sm">Dirty Working Tree</p><p>${v.git_dirty ? Nova.badge('Yes','yellow') : Nova.badge('No','green')}</p></div>
|
||||
</div>
|
||||
|
||||
${count > 0 ? `
|
||||
<div class="card mb-2" style="background:var(--bg3)">
|
||||
<div class="card-header"><span class="card-title">Pending Commits</span></div>
|
||||
<div class="card-body terminal">
|
||||
${upd.commits?.map(c => `<div>${c}</div>`).join('') || 'None'}
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="applyUpdate()">Apply Update</button>
|
||||
` : `<p class="text-muted">NovaCPX is up to date.</p>`}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── Audit Log ──────────────────────────────────────────────────────────────
|
||||
async function auditLog() {
|
||||
const res = await Nova.api('system', 'audit-log', { params: { per_page: 50 } });
|
||||
const rows = res?.data || [];
|
||||
return `
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-title">Audit Log</span></div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>Time</th><th>User</th><th>Action</th><th>Resource</th><th>IP</th></tr></thead>
|
||||
<tbody>
|
||||
${rows.map(r => `
|
||||
<tr>
|
||||
<td class="text-muted text-sm">${Nova.relTime(r.created_at)}</td>
|
||||
<td>${r.username || '—'}</td>
|
||||
<td><code>${r.action}</code></td>
|
||||
<td>${r.resource || '—'}</td>
|
||||
<td class="text-muted text-sm">${r.ip_address || '—'}</td>
|
||||
</tr>`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── PHP Manager ────────────────────────────────────────────────────────────
|
||||
async function phpManager() {
|
||||
return `
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-title">PHP Version Manager</span></div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted mb-2">Manage installed PHP versions and global extensions.</p>
|
||||
<div class="grid-4">
|
||||
${['7.4','8.1','8.2','8.3'].map(v => `
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">PHP ${v}</div>
|
||||
<div class="stat-value" style="font-size:1rem">${Nova.badge('Active','green')}</div>
|
||||
<div class="mt-2 flex gap-1">
|
||||
<button class="btn btn-ghost btn-sm" onclick="phpAction('${v}','fpm-restart')">Restart FPM</button>
|
||||
</div>
|
||||
</div>`).join('')}
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<h4 class="mb-1">Global PHP Extensions</h4>
|
||||
<p class="text-muted text-sm">Extensions installed across all PHP versions: mbstring, curl, gd, xml, zip, opcache, redis, imagick, pdo, pdo_mysql, pdo_pgsql</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── Settings ───────────────────────────────────────────────────────────────
|
||||
async function settings() {
|
||||
return `
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-title">Panel Settings</span></div>
|
||||
<div class="card-body">
|
||||
<form id="settings-form">
|
||||
<div class="grid-2">
|
||||
<div class="form-group"><label>Panel Name</label><input type="text" name="panel_name" value="NovaCPX"></div>
|
||||
<div class="form-group"><label>Default PHP Version</label>
|
||||
<select name="default_php">
|
||||
${['7.4','8.1','8.2','8.3'].map(v => `<option value="${v}" ${v==='8.3'?'selected':''}>${v}</option>`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group"><label>Primary Nameserver</label><input type="text" name="default_nameserver1" value="ns1.example.com"></div>
|
||||
<div class="form-group"><label>Secondary Nameserver</label><input type="text" name="default_nameserver2" value="ns2.example.com"></div>
|
||||
<div class="form-group"><label>Update Channel</label>
|
||||
<select name="update_channel"><option value="stable">Stable</option><option value="beta">Beta</option></select>
|
||||
</div>
|
||||
<div class="form-group"><label>Git Remote</label><input type="url" name="git_remote" value="https://github.com/myronblair/novacpx.git"></div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Save Settings</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── Stub pages ─────────────────────────────────────────────────────────────
|
||||
function stubPage(title, desc) {
|
||||
return `<div class="card"><div class="card-header"><span class="card-title">${title}</span></div>
|
||||
<div class="card-body"><p class="text-muted">${desc}</p>
|
||||
<div class="mt-2">${Nova.badge('Coming Soon','yellow')}</div></div></div>`;
|
||||
}
|
||||
function accounts() { return stubPage('All Accounts', 'View and manage all hosting accounts on this server.'); }
|
||||
function resellers() { return stubPage('Resellers', 'Create and manage reseller accounts with custom packages and resource limits.'); }
|
||||
function packages() { return stubPage('Packages', 'Define hosting packages with disk, bandwidth, email, FTP, and database limits.'); }
|
||||
function createAccount() { return stubPage('Create Account', 'Create a new hosting account and assign it a package.'); }
|
||||
function dnsZones() { return stubPage('DNS Zones', 'View, add, and edit all DNS zones on this nameserver.'); }
|
||||
function nameservers() { return stubPage('Nameservers', 'Configure primary and secondary nameservers for all hosted domains.'); }
|
||||
function webServer() { return stubPage('Web Server', 'Manage Apache2 / nginx virtual hosts, modules, and configuration.'); }
|
||||
function mysqlManager() { return stubPage('MySQL / PostgreSQL', 'Create databases, users, and manage remote access.'); }
|
||||
function mailServer() { return stubPage('Mail Server', 'Manage Postfix/Dovecot configuration, spam filters, and mail queues.'); }
|
||||
function ftpServer() { return stubPage('FTP Server', 'Configure ProFTPD, manage FTP accounts and access rules.'); }
|
||||
function sslManager() { return stubPage('SSL Manager', 'Issue, install, and auto-renew Let\'s Encrypt SSL certificates for all domains.'); }
|
||||
function firewall() { return stubPage('Firewall / Fail2Ban', 'Manage UFW rules and review Fail2Ban bans.'); }
|
||||
function backups() { return stubPage('Backups', 'Configure automated backups, restore accounts, and manage backup storage.'); }
|
||||
|
||||
// ── Global action helpers ──────────────────────────────────────────────────
|
||||
window.adminPage = (page) => Nova.loadPage(page, pages);
|
||||
window.applyUpdate = async () => {
|
||||
Nova.confirm('Apply all pending updates? The panel may restart.', async () => {
|
||||
Nova.toast('Applying update…', 'info', 8000);
|
||||
const res = await Nova.api('system', 'apply-update', { method: 'POST' });
|
||||
if (res?.data?.updated) {
|
||||
Nova.toast(`Updated to ${res.data.to_commit}`, 'success');
|
||||
Nova.loadPage('updates', pages);
|
||||
} else {
|
||||
Nova.toast(res?.data?.pull_output || 'Already up to date', 'info');
|
||||
}
|
||||
});
|
||||
};
|
||||
window.adminServiceAction = async (svc, cmd) => {
|
||||
const res = await Nova.api('system', 'service', { method: 'POST', body: { service: svc, command: cmd } });
|
||||
Nova.toast(`${svc}: ${cmd} → ${res?.success ? 'OK' : res?.message}`, res?.success ? 'success' : 'error');
|
||||
};
|
||||
window.phpAction = async (ver, cmd) => {
|
||||
const svc = `php${ver}-fpm`;
|
||||
await window.adminServiceAction(svc, 'restart');
|
||||
};
|
||||
|
||||
// ── Check for updates badge ────────────────────────────────────────────────
|
||||
async function checkUpdates() {
|
||||
const res = await Nova.api('system', 'check-update');
|
||||
const n = res?.data?.updates_available || 0;
|
||||
const badge = document.getElementById('update-badge');
|
||||
if (badge && n > 0) { badge.textContent = n; badge.style.display = ''; }
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* NovaCPX — Shared JS utilities
|
||||
*/
|
||||
|
||||
window.Nova = (() => {
|
||||
// ── API ───────────────────────────────────────────────────────────────────
|
||||
async function api(endpoint, action, opts = {}) {
|
||||
const { method = 'GET', body, params } = opts;
|
||||
let url = `/api/${endpoint}/${action}`;
|
||||
if (params) url += '?' + new URLSearchParams(params);
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
credentials: 'include',
|
||||
headers: body ? { 'Content-Type': 'application/json' } : {},
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
if (res.status === 401) { location.href = '/?redirect=' + encodeURIComponent(location.pathname); return null; }
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// ── Toast ─────────────────────────────────────────────────────────────────
|
||||
let toastEl = null;
|
||||
function toast(msg, type = 'info', duration = 3500) {
|
||||
if (!toastEl) {
|
||||
toastEl = document.createElement('div');
|
||||
toastEl.style.cssText = 'position:fixed;bottom:1.5rem;right:1.5rem;z-index:9999;display:flex;flex-direction:column;gap:.5rem;max-width:380px';
|
||||
document.body.appendChild(toastEl);
|
||||
}
|
||||
const el = document.createElement('div');
|
||||
el.className = `alert alert-${type}`;
|
||||
el.style.cssText = 'animation:fadeIn .2s;cursor:pointer;box-shadow:var(--shadow)';
|
||||
el.textContent = msg;
|
||||
el.addEventListener('click', () => el.remove());
|
||||
toastEl.appendChild(el);
|
||||
setTimeout(() => el.remove(), duration);
|
||||
}
|
||||
|
||||
// ── Modal ─────────────────────────────────────────────────────────────────
|
||||
function modal(title, bodyHtml, footerHtml = '') {
|
||||
const ov = document.createElement('div');
|
||||
ov.className = 'modal-overlay open';
|
||||
ov.innerHTML = `<div class="modal">
|
||||
<div class="modal-header">
|
||||
<span class="modal-title">${title}</span>
|
||||
<button class="modal-close" onclick="this.closest('.modal-overlay').remove()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">${bodyHtml}</div>
|
||||
${footerHtml ? `<div class="modal-footer">${footerHtml}</div>` : ''}
|
||||
</div>`;
|
||||
ov.addEventListener('click', e => { if (e.target === ov) ov.remove(); });
|
||||
document.body.appendChild(ov);
|
||||
return ov;
|
||||
}
|
||||
|
||||
// ── Confirm dialog ────────────────────────────────────────────────────────
|
||||
function confirm(msg, onYes, danger = false) {
|
||||
const ov = modal('Confirm', `<p>${msg}</p>`,
|
||||
`<button class="btn btn-ghost" onclick="this.closest('.modal-overlay').remove()">Cancel</button>
|
||||
<button class="btn btn-${danger ? 'red' : 'primary'}" id="confirm-yes">Confirm</button>`
|
||||
);
|
||||
ov.querySelector('#confirm-yes').onclick = () => { ov.remove(); onYes(); };
|
||||
}
|
||||
|
||||
// ── Sidebar navigation ────────────────────────────────────────────────────
|
||||
function initNav(pages) {
|
||||
document.querySelectorAll('[data-page]').forEach(link => {
|
||||
link.addEventListener('click', e => {
|
||||
e.preventDefault();
|
||||
const page = link.dataset.page;
|
||||
document.querySelectorAll('[data-page]').forEach(l => l.classList.remove('active'));
|
||||
link.classList.add('active');
|
||||
const titleEl = document.getElementById('page-title');
|
||||
if (titleEl) titleEl.textContent = link.textContent.trim();
|
||||
loadPage(page, pages);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function loadPage(page, pages) {
|
||||
const content = document.getElementById('page-content');
|
||||
if (!content) return;
|
||||
const fn = pages[page];
|
||||
if (fn) {
|
||||
content.innerHTML = '<div style="padding:2rem;color:var(--text-muted);text-align:center">Loading…</div>';
|
||||
Promise.resolve(fn()).then(html => { if (html) content.innerHTML = html; });
|
||||
} else {
|
||||
content.innerHTML = `<div class="card"><div class="card-body"><p class="text-muted">Page "${page}" coming soon.</p></div></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Progress bar helper ───────────────────────────────────────────────────
|
||||
function progressBar(pct) {
|
||||
const color = pct >= 90 ? 'red' : pct >= 70 ? 'yellow' : 'green';
|
||||
return `<div class="progress"><div class="progress-bar ${color}" style="width:${pct}%"></div></div>`;
|
||||
}
|
||||
|
||||
// ── Format helpers ────────────────────────────────────────────────────────
|
||||
function bytes(n) {
|
||||
if (n >= 1073741824) return (n / 1073741824).toFixed(1) + ' GB';
|
||||
if (n >= 1048576) return (n / 1048576).toFixed(1) + ' MB';
|
||||
if (n >= 1024) return (n / 1024).toFixed(1) + ' KB';
|
||||
return n + ' B';
|
||||
}
|
||||
function relTime(dateStr) {
|
||||
const diff = (Date.now() - new Date(dateStr)) / 1000;
|
||||
if (diff < 60) return 'just now';
|
||||
if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
|
||||
if (diff < 86400) return Math.floor(diff / 3600) + 'h ago';
|
||||
return Math.floor(diff / 86400) + 'd ago';
|
||||
}
|
||||
function badge(text, type = 'blue') {
|
||||
return `<span class="badge badge-${type}">${text}</span>`;
|
||||
}
|
||||
function serviceDot(status) {
|
||||
const cls = status === 'active' ? 'active' : status === 'inactive' ? 'inactive' : 'unknown';
|
||||
return `<span class="service-dot ${cls}"></span>`;
|
||||
}
|
||||
|
||||
// Inject global CSS animation
|
||||
const style = document.createElement('style');
|
||||
style.textContent = '@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}';
|
||||
document.head.appendChild(style);
|
||||
|
||||
return { api, toast, modal, confirm, initNav, loadPage, progressBar, bytes, relTime, badge, serviceDot };
|
||||
})();
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
// NovaCPX entry point — redirect based on role or show login
|
||||
session_start();
|
||||
$redirect = $_GET['redirect'] ?? '';
|
||||
$safeRedirect = preg_match('#^/(user|reseller|admin)#', $redirect) ? $redirect : '';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>NovaCPX — Login</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/assets/img/favicon.svg">
|
||||
<link rel="stylesheet" href="/assets/css/nova.css">
|
||||
</head>
|
||||
<body class="login-page">
|
||||
|
||||
<div class="login-wrap">
|
||||
<div class="login-brand">
|
||||
<svg class="logo-icon" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="20" cy="20" r="18" stroke="url(#lg1)" stroke-width="2"/>
|
||||
<path d="M12 28 L20 8 L28 28" stroke="url(#lg2)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M14 22 H26" stroke="url(#lg2)" stroke-width="2" stroke-linecap="round"/>
|
||||
<defs>
|
||||
<linearGradient id="lg1" x1="2" y1="2" x2="38" y2="38">
|
||||
<stop offset="0%" stop-color="#6366f1"/>
|
||||
<stop offset="100%" stop-color="#0ea5e9"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="lg2" x1="12" y1="8" x2="28" y2="28">
|
||||
<stop offset="0%" stop-color="#6366f1"/>
|
||||
<stop offset="100%" stop-color="#0ea5e9"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<span class="logo-text">Nova<strong>CPX</strong></span>
|
||||
</div>
|
||||
|
||||
<div class="login-card">
|
||||
<h1>Sign In</h1>
|
||||
<p class="login-sub">Linux Web Hosting Control Panel</p>
|
||||
|
||||
<div id="login-error" class="alert alert-error" style="display:none"></div>
|
||||
|
||||
<form id="login-form">
|
||||
<div class="form-group">
|
||||
<label for="username">Username or Email</label>
|
||||
<input type="text" id="username" name="username" autocomplete="username" autofocus required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<div class="input-with-icon">
|
||||
<input type="password" id="password" name="password" autocomplete="current-password" required>
|
||||
<button type="button" class="eye-toggle" data-target="password">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-full" id="login-btn">
|
||||
<span class="btn-text">Sign In</span>
|
||||
<span class="btn-spinner" style="display:none">Signing in…</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="login-footer">
|
||||
NovaCPX v<span id="panel-version">1.0.0</span> |
|
||||
<a href="/api/system/version" target="_blank">System Info</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const REDIRECT = <?= json_encode($safeRedirect) ?>;
|
||||
|
||||
document.getElementById('login-form').addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
const btn = document.getElementById('login-btn');
|
||||
const err = document.getElementById('login-error');
|
||||
btn.querySelector('.btn-text').style.display = 'none';
|
||||
btn.querySelector('.btn-spinner').style.display = '';
|
||||
btn.disabled = true;
|
||||
err.style.display = 'none';
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type':'application/json'},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
username: document.getElementById('username').value,
|
||||
password: document.getElementById('password').value,
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!data.success) throw new Error(data.message || 'Login failed');
|
||||
|
||||
const role = data.data.user.role;
|
||||
const dest = REDIRECT || (role === 'admin' ? '/admin/' : role === 'reseller' ? '/reseller/' : '/user/');
|
||||
location.href = dest;
|
||||
} catch (ex) {
|
||||
err.textContent = ex.message;
|
||||
err.style.display = '';
|
||||
btn.querySelector('.btn-text').style.display = '';
|
||||
btn.querySelector('.btn-spinner').style.display = 'none';
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Password toggle
|
||||
document.querySelectorAll('.eye-toggle').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const inp = document.getElementById(btn.dataset.target);
|
||||
inp.type = inp.type === 'password' ? 'text' : 'password';
|
||||
});
|
||||
});
|
||||
|
||||
// Fetch version
|
||||
fetch('/api/auth/me', {credentials:'include'}).then(r => r.json()).then(d => {
|
||||
if (d.success) {
|
||||
const role = d.data.role;
|
||||
location.href = role === 'admin' ? '/admin/' : role === 'reseller' ? '/reseller/' : '/user/';
|
||||
}
|
||||
});
|
||||
fetch('/api/system/version', {credentials:'include'})
|
||||
.then(r=>r.json()).then(d=>{ if(d.data?.installed_version) document.getElementById('panel-version').textContent=d.data.installed_version; });
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,269 @@
|
||||
<?php
|
||||
// NovaCPX User Panel — End-user hosting dashboard
|
||||
// Design: Horizontal feature cards with usage rings, NOT cPanel icon grid
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>NovaCPX — My Hosting</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/assets/img/favicon.svg">
|
||||
<link rel="stylesheet" href="/assets/css/nova.css">
|
||||
<style>
|
||||
/* ── User panel specific ─────────────────────────────── */
|
||||
.feature-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
.feature-card {
|
||||
background: var(--bg2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 1.25rem;
|
||||
text-decoration: none;
|
||||
color: var(--text);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
transition: border-color .15s, transform .1s;
|
||||
cursor: pointer;
|
||||
}
|
||||
.feature-card:hover {
|
||||
border-color: var(--primary);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.feature-icon {
|
||||
width: 44px; height: 44px; flex-shrink: 0;
|
||||
border-radius: 10px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.fi-purple { background: rgba(99,102,241,.15); color: var(--primary); }
|
||||
.fi-sky { background: rgba(14,165,233,.15); color: var(--sky); }
|
||||
.fi-green { background: rgba(16,185,129,.15); color: var(--green); }
|
||||
.fi-yellow { background: rgba(245,158,11,.15); color: var(--yellow); }
|
||||
.fi-red { background: rgba(239,68,68,.15); color: var(--red); }
|
||||
.fi-pink { background: rgba(236,72,153,.15); color: #f472b6; }
|
||||
.fi-teal { background: rgba(20,184,166,.15); color: #2dd4bf; }
|
||||
.fi-orange { background: rgba(249,115,22,.15); color: #fb923c; }
|
||||
.feature-icon svg { width: 22px; height: 22px; }
|
||||
.feature-info { flex: 1; min-width: 0; }
|
||||
.feature-name { font-weight: 600; font-size: .9rem; margin-bottom: .2rem; }
|
||||
.feature-desc { font-size: .78rem; color: var(--text-muted); line-height: 1.4; }
|
||||
.feature-meta { font-size: .75rem; color: var(--primary); margin-top: .3rem; }
|
||||
|
||||
/* Usage ring */
|
||||
.usage-rings {
|
||||
display: flex; gap: 2rem; align-items: center;
|
||||
background: var(--bg2); border: 1px solid var(--border);
|
||||
border-radius: 12px; padding: 1.25rem 1.5rem; margin-bottom: 1.5rem;
|
||||
}
|
||||
.ring-item { text-align: center; }
|
||||
.ring-label { font-size: .72rem; text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted); margin-top: .5rem; }
|
||||
.ring-val { font-size: .85rem; font-weight: 600; margin-top: .15rem; }
|
||||
svg.ring { transform: rotate(-90deg); }
|
||||
svg.ring circle { transition: stroke-dashoffset .5s; }
|
||||
|
||||
/* Breadcrumb / section tabs */
|
||||
.section-header { display: flex; align-items: center; gap: 1rem; margin-bottom: 1.25rem; }
|
||||
.section-header h2 { font-size: 1rem; font-weight: 700; flex: 1; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="panel-layout" id="app" style="display:none">
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<div class="sidebar-brand">
|
||||
<svg class="logo-icon" viewBox="0 0 40 40" fill="none">
|
||||
<circle cx="20" cy="20" r="18" stroke="url(#ulg1)" stroke-width="2"/>
|
||||
<path d="M12 28 L20 8 L28 28" stroke="url(#ulg2)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M14 22 H26" stroke="url(#ulg2)" stroke-width="2" stroke-linecap="round"/>
|
||||
<defs>
|
||||
<linearGradient id="ulg1" x1="2" y1="2" x2="38" y2="38"><stop offset="0%" stop-color="#6366f1"/><stop offset="100%" stop-color="#0ea5e9"/></linearGradient>
|
||||
<linearGradient id="ulg2" x1="12" y1="8" x2="28" y2="28"><stop offset="0%" stop-color="#6366f1"/><stop offset="100%" stop-color="#0ea5e9"/></linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<span class="logo-text">Nova<strong>CPX</strong></span>
|
||||
</div>
|
||||
<nav>
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-label">My Account</div>
|
||||
<a href="#" class="sidebar-link active" data-page="home">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg> Home
|
||||
</a>
|
||||
<a href="#" class="sidebar-link" data-page="domains">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg> Domains
|
||||
</a>
|
||||
<a href="#" class="sidebar-link" data-page="files">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg> File Manager
|
||||
</a>
|
||||
</div>
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-label">Email</div>
|
||||
<a href="#" class="sidebar-link" data-page="email-accounts">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg> Email Accounts
|
||||
</a>
|
||||
<a href="#" class="sidebar-link" data-page="forwarders">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg> Forwarders
|
||||
</a>
|
||||
<a href="#" class="sidebar-link" data-page="autoresponders">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg> Autoresponders
|
||||
</a>
|
||||
</div>
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-label">Databases</div>
|
||||
<a href="#" class="sidebar-link" data-page="mysql">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/></svg> MySQL
|
||||
</a>
|
||||
<a href="#" class="sidebar-link" data-page="postgres">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/></svg> PostgreSQL
|
||||
</a>
|
||||
</div>
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-label">Advanced</div>
|
||||
<a href="#" class="sidebar-link" data-page="ftp">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg> FTP Accounts
|
||||
</a>
|
||||
<a href="#" class="sidebar-link" data-page="dns">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="16.5" y1="9.4" x2="7.5" y2="4.21"/><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg> DNS Editor
|
||||
</a>
|
||||
<a href="#" class="sidebar-link" data-page="ssl">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg> SSL / TLS
|
||||
</a>
|
||||
<a href="#" class="sidebar-link" data-page="php-config">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg> PHP Config
|
||||
</a>
|
||||
<a href="#" class="sidebar-link" data-page="cron">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg> Cron Jobs
|
||||
</a>
|
||||
<a href="#" class="sidebar-link" data-page="backups">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg> Backups
|
||||
</a>
|
||||
<a href="#" class="sidebar-link" data-page="logs">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/></svg> Error Logs
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="sidebar-user">
|
||||
<div class="sidebar-user-info">
|
||||
<div class="avatar" id="user-avatar">U</div>
|
||||
<div><div class="user-name" id="user-name">User</div><div class="user-role" id="user-domain">example.com</div></div>
|
||||
<a href="#" id="logout-btn" class="btn btn-ghost btn-sm btn-icon" title="Logout" style="margin-left:auto">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="main-content">
|
||||
<header class="topbar">
|
||||
<div class="topbar-title" id="page-title">My Hosting</div>
|
||||
<div class="topbar-actions">
|
||||
<span id="account-domain" class="text-muted text-sm"></span>
|
||||
</div>
|
||||
</header>
|
||||
<div class="page-content" id="page-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="auth-check" style="display:flex;align-items:center;justify-content:center;min-height:100vh">
|
||||
<div style="text-align:center;color:var(--text-muted)">Loading…</div>
|
||||
</div>
|
||||
|
||||
<script src="/assets/js/nova.js"></script>
|
||||
<script>
|
||||
(async () => {
|
||||
const me = await Nova.api('auth', 'me');
|
||||
if (!me?.success) { location.href = '/?redirect=/user/'; return; }
|
||||
|
||||
document.getElementById('auth-check').style.display = 'none';
|
||||
document.getElementById('app').style.display = '';
|
||||
document.getElementById('user-name').textContent = me.data.username;
|
||||
document.getElementById('user-avatar').textContent = me.data.username[0].toUpperCase();
|
||||
|
||||
document.getElementById('logout-btn').addEventListener('click', async e => {
|
||||
e.preventDefault();
|
||||
await Nova.api('auth', 'logout', { method: 'POST' });
|
||||
location.href = '/';
|
||||
});
|
||||
|
||||
function ring(pct, color, r = 28) {
|
||||
const circ = 2 * Math.PI * r;
|
||||
const offset = circ * (1 - pct / 100);
|
||||
return `<svg class="ring" width="${r*2+8}" height="${r*2+8}" viewBox="0 0 ${r*2+8} ${r*2+8}">
|
||||
<circle cx="${r+4}" cy="${r+4}" r="${r}" fill="none" stroke="var(--border)" stroke-width="5"/>
|
||||
<circle cx="${r+4}" cy="${r+4}" r="${r}" fill="none" stroke="${color}" stroke-width="5"
|
||||
stroke-dasharray="${circ}" stroke-dashoffset="${offset}" stroke-linecap="round"/>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
const homePage = () => `
|
||||
<div class="usage-rings">
|
||||
<div>
|
||||
<h2 style="font-size:1.1rem;font-weight:700">My Hosting</h2>
|
||||
<p class="text-muted text-sm">example.com · Active</p>
|
||||
</div>
|
||||
<div style="margin-left:auto;display:flex;gap:2rem">
|
||||
<div class="ring-item">${ring(45,'#6366f1')}<div class="ring-label">Disk</div><div class="ring-val">2.3 GB / 5 GB</div></div>
|
||||
<div class="ring-item">${ring(22,'#0ea5e9')}<div class="ring-label">Bandwidth</div><div class="ring-val">2.2 GB / 10 GB</div></div>
|
||||
<div class="ring-item">${ring(60,'#10b981')}<div class="ring-label">Email</div><div class="ring-val">6 / 10</div></div>
|
||||
<div class="ring-item">${ring(30,'#f59e0b')}<div class="ring-label">Databases</div><div class="ring-val">3 / 10</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-header"><h2>Quick Access</h2></div>
|
||||
<div class="feature-grid">
|
||||
${[
|
||||
{ page:'files', icon:'folder', color:'fi-yellow', name:'File Manager', desc:'Upload, edit, and manage your website files and directories.' },
|
||||
{ page:'email-accounts',icon:'mail', color:'fi-sky', name:'Email Accounts', desc:'Create and manage mailboxes for your domains.' },
|
||||
{ page:'mysql', icon:'db', color:'fi-green', name:'MySQL Databases', desc:'Create databases and users for your PHP applications.' },
|
||||
{ page:'postgres', icon:'db', color:'fi-teal', name:'PostgreSQL', desc:'Manage PostgreSQL databases and connections.' },
|
||||
{ page:'domains', icon:'globe', color:'fi-purple', name:'Domains', desc:'Add subdomains, addon domains, and domain aliases.' },
|
||||
{ page:'dns', icon:'dns', color:'fi-orange', name:'DNS Editor', desc:'Manage A, CNAME, MX, TXT, and SRV records.' },
|
||||
{ page:'ssl', icon:'lock', color:'fi-green', name:'SSL / TLS', desc:'Issue free Let\'s Encrypt certificates for your domains.' },
|
||||
{ page:'ftp', icon:'ftp', color:'fi-pink', name:'FTP Accounts', desc:'Create FTP users with directory access controls.' },
|
||||
{ page:'php-config', icon:'code', color:'fi-purple', name:'PHP Config', desc:'Switch PHP version and configure php.ini settings per domain.' },
|
||||
{ page:'cron', icon:'clock', color:'fi-yellow', name:'Cron Jobs', desc:'Schedule automated tasks on any interval.' },
|
||||
{ page:'forwarders', icon:'forward', color:'fi-sky', name:'Email Forwarders', desc:'Forward emails from one address to another.' },
|
||||
{ page:'backups', icon:'backup', color:'fi-red', name:'Backups', desc:'Create full or partial backups and restore files.' },
|
||||
].map(f => `
|
||||
<div class="feature-card" onclick="loadUserPage('${f.page}')">
|
||||
<div class="feature-icon ${f.color}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
${svgPath(f.icon)}
|
||||
</svg>
|
||||
</div>
|
||||
<div class="feature-info">
|
||||
<div class="feature-name">${f.name}</div>
|
||||
<div class="feature-desc">${f.desc}</div>
|
||||
</div>
|
||||
</div>`).join('')}
|
||||
</div>`;
|
||||
|
||||
function svgPath(icon) {
|
||||
const p = {
|
||||
folder:'<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>',
|
||||
mail:'<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/>',
|
||||
db:'<ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/>',
|
||||
globe:'<circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>',
|
||||
dns:'<line x1="16.5" y1="9.4" x2="7.5" y2="4.21"/><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>',
|
||||
lock:'<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>',
|
||||
ftp:'<polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/>',
|
||||
code:'<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>',
|
||||
clock:'<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>',
|
||||
forward:'<polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/>',
|
||||
backup:'<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/>',
|
||||
};
|
||||
return p[icon] || '';
|
||||
}
|
||||
|
||||
const pages = { home: homePage };
|
||||
Nova.initNav(pages);
|
||||
document.getElementById('page-content').innerHTML = homePage();
|
||||
|
||||
window.loadUserPage = (page) => Nova.loadPage(page, pages);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user