基本功能复刻完成
All checks were successful
build / build (api, amd64, linux) (push) Successful in -47s
build / build (api, arm64, linux) (push) Successful in -47s
build / build (api.exe, amd64, windows) (push) Successful in -48s

This commit is contained in:
CN-JS-HuiBai
2026-04-17 12:24:00 +08:00
parent 06da23fbbc
commit 981ee4f406
37 changed files with 11737 additions and 770 deletions

View File

@@ -1,37 +1,36 @@
:root {
/* Nebula Dark Palette */
--bg: #07101b;
--panel: rgba(9, 20, 35, 0.78);
--panel-strong: rgba(11, 24, 41, 0.92);
--text: #eef6ff;
--text-dim: rgba(227, 239, 255, 0.72);
--text-faint: rgba(227, 239, 255, 0.54);
--border: rgba(152, 192, 255, 0.16);
--shadow: 0 24px 80px rgba(0, 0, 0, 0.34);
--shadow-soft: 0 12px 32px rgba(0, 0, 0, 0.22);
/* Xboard/Ant Design Professional Palette */
--bg: #f0f2f5;
--panel: #ffffff;
--text: #000000d9;
--text-dim: #000000a6;
--text-faint: #00000073;
--border: #d9d9d9;
--border-light: #f0f0f0;
--shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.03), 0 1px 6px -1px rgba(0, 0, 0, 0.02), 0 2px 4px 0 rgba(0, 0, 0, 0.02);
--shadow-card: 0 1px 2px -2px rgba(0, 0, 0, 0.16), 0 3px 6px 0 rgba(0, 0, 0, 0.12), 0 5px 12px 4px rgba(0, 0, 0, 0.09);
/* Brand/Accent */
--accent: #6ce5ff;
--accent-strong: #21b8ff;
--accent-soft: rgba(108, 229, 255, 0.12);
--accent-rgb: 108, 229, 255;
/* Brand/Accent (Ant Design Blue) */
--accent: #1677ff;
--accent-hover: #4096ff;
--accent-active: #0958d9;
--accent-soft: #e6f4ff;
/* Indicators */
--success: #65f0b7;
--warn: #ffca7a;
--danger: #ff8db5;
--success: #52c41a;
--warn: #faad14;
--danger: #ff4d4f;
/* Radius */
--radius-xl: 28px;
--radius-lg: 20px;
--radius-md: 14px;
--radius-lg: 8px;
--radius-md: 6px;
--radius-sm: 4px;
/* Sidebar */
--sidebar: #091221;
--sidebar-active: #ffffff;
--sidebar-muted: rgba(227, 239, 255, 0.5);
--sidebar-bg: #ffffff;
--sidebar-item-active: #e6f4ff;
font-family: "Avenir Next", "Segoe UI Variable Display", "Segoe UI", system-ui, -apple-system, sans-serif;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
-webkit-font-smoothing: antialiased;
}
@@ -41,257 +40,165 @@
html, body {
margin: 0;
min-height: 100%;
background:
radial-gradient(circle at top left, rgba(var(--accent-rgb), 0.12), transparent 30%),
radial-gradient(circle at top right, rgba(255, 201, 122, 0.08), transparent 22%),
linear-gradient(180deg, #050d16 0%, #07101b 100%);
min-height: 100vh;
background: var(--bg);
color: var(--text);
overflow-x: hidden;
}
body::before {
content: "";
position: fixed;
inset: 0;
background-image:
linear-gradient(rgba(155, 192, 255, 0.04) 1px, transparent 1px),
linear-gradient(90deg, rgba(155, 192, 255, 0.04) 1px, transparent 1px);
background-size: 64px 64px;
mask-image: radial-gradient(circle at top center, black, transparent 80%);
pointer-events: none;
z-index: -1;
}
a {
color: inherit;
text-decoration: none;
transition: opacity 0.2s ease;
}
a:hover {
opacity: 0.8;
}
button, input, select, textarea {
font: inherit;
background: transparent;
color: inherit;
}
/* Glass Component Base */
.glass-card {
background: var(--panel);
backdrop-filter: blur(18px);
border: 1px solid var(--border);
box-shadow: var(--shadow-soft);
font-size: 14px;
line-height: 1.5715;
}
/* Main Layout */
.admin-shell {
display: flex;
min-height: 100vh;
display: grid;
grid-template-columns: 280px minmax(0, 1fr);
gap: 0;
}
/* Sidebar */
.admin-sidebar {
position: sticky;
top: 0;
height: 100vh;
background: var(--sidebar);
border-right: 1px solid var(--border);
padding: 32px 20px;
width: 260px;
background: var(--sidebar-bg);
border-right: 1px solid var(--border-light);
display: flex;
flex-direction: column;
gap: 32px;
position: fixed;
height: 100vh;
z-index: 1000;
}
.sidebar-brand {
padding: 0 12px;
}
.sidebar-brand-mark {
display: inline-flex;
padding: 6px 12px;
background: var(--accent-soft);
color: var(--accent);
border: 1px solid rgba(var(--accent-rgb), 0.2);
border-radius: 999px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
margin-bottom: 14px;
.sidebar-header {
padding: 24px 20px;
display: flex;
flex-direction: column;
}
.sidebar-brand strong {
display: block;
font-size: 26px;
font-weight: 800;
letter-spacing: -0.02em;
line-height: 1.1;
font-size: 20px;
font-weight: 600;
color: var(--text);
}
.sidebar-brand span {
display: block;
font-size: 13px;
font-size: 12px;
color: var(--text-faint);
margin-top: 8px;
}
.sidebar-nav-label {
display: block;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--text-faint);
margin: 24px 12px 12px;
}
.sidebar-nav {
display: grid;
gap: 6px;
flex: 1;
padding: 12px 8px;
overflow-y: auto;
}
.sidebar-nav-label {
padding: 8px 16px;
font-size: 12px;
color: var(--text-faint);
text-transform: uppercase;
letter-spacing: 0.1em;
}
.sidebar-item-wrap {
margin-bottom: 4px;
}
.sidebar-item {
display: block;
padding: 12px 16px;
display: flex;
align-items: center;
padding: 10px 16px;
border-radius: var(--radius-md);
color: var(--sidebar-muted);
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
border: 1px solid transparent;
color: var(--text-dim);
text-decoration: none;
transition: all 0.2s;
cursor: pointer;
}
.sidebar-item:hover {
background: rgba(255, 255, 255, 0.04);
background: rgba(0, 0, 0, 0.04);
color: var(--text);
transform: translateX(4px);
}
.sidebar-item.active {
background: var(--accent-soft);
background: var(--sidebar-item-active);
color: var(--accent);
border-color: rgba(var(--accent-rgb), 0.24);
box-shadow: inset 0 0 12px rgba(var(--accent-rgb), 0.08);
font-weight: 500;
}
.sidebar-item strong {
display: block;
font-size: 15px;
font-weight: 700;
}
.sidebar-item span {
display: block;
font-size: 12px;
opacity: 0.7;
margin-top: 2px;
font-size: 14px;
}
/* Main Content */
.admin-main {
padding: 32px 40px;
min-width: 0;
flex: 1;
margin-left: 260px;
padding: 24px;
max-width: 100%;
}
.topbar {
padding: 24px 28px;
border-radius: var(--radius-xl);
margin-bottom: 32px;
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 24px;
margin-bottom: 24px;
}
.topbar-copy h1 {
margin: 0;
font-size: 32px;
font-weight: 800;
letter-spacing: -0.03em;
line-height: 1.1;
font-size: 24px;
font-weight: 600;
color: var(--text);
}
.topbar-copy p {
margin: 10px 0 0;
font-size: 15px;
margin: 4px 0 0;
color: var(--text-dim);
}
.topbar-eyebrow {
display: inline-flex;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.15em;
color: var(--accent);
margin-bottom: 10px;
}
.topbar-meta {
display: flex;
gap: 12px;
margin-top: 16px;
}
.topbar-chip {
padding: 6px 12px;
border-radius: 999px;
background: rgba(255,255,255,0.05);
border: 1px solid var(--border);
font-size: 12px;
color: var(--text-dim);
}
/* Cards & Grid */
.grid {
display: grid;
gap: 20px;
}
.grid-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
/* Cards */
.card {
padding: 28px;
border-radius: var(--radius-xl);
background: var(--panel);
border-radius: var(--radius-lg);
padding: 24px;
border: 1px solid var(--border-light);
box-shadow: var(--shadow);
margin-bottom: 24px;
}
.card h2 {
font-size: 22px;
font-weight: 700;
margin: 0 0 14px;
font-size: 16px;
font-weight: 600;
margin: 0 0 16px;
color: var(--text);
}
/* Stats Grid */
.grid {
display: grid;
gap: 16px;
margin-bottom: 24px;
}
.grid-3 {
grid-template-columns: repeat(3, 1fr);
}
.stat-card {
display: flex;
flex-direction: column;
gap: 12px;
}
.stat-card strong {
font-size: 34px;
font-weight: 800;
letter-spacing: -0.04em;
color: var(--accent);
padding: 16px 24px;
}
.stat-card span {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-faint);
font-size: 14px;
color: var(--text-dim);
}
/* Table Design */
.stat-card strong {
display: block;
font-size: 24px;
font-weight: 600;
margin-top: 4px;
}
/* Tables */
.table-wrap {
background: #fff;
border-radius: var(--radius-lg);
border: 1px solid var(--border);
background: rgba(255, 255, 255, 0.02);
border: 1px solid var(--border-light);
overflow: hidden;
}
@@ -300,56 +207,45 @@ table {
border-collapse: collapse;
}
th, td {
padding: 16px 20px;
th {
background: #fafafa;
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid var(--border);
font-weight: 600;
color: var(--text-dim);
border-bottom: 1px solid var(--border-light);
}
th {
background: rgba(255, 255, 255, 0.04);
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--text-faint);
td {
padding: 12px 16px;
border-bottom: 1px solid var(--border-light);
color: var(--text);
vertical-align: middle;
}
tbody tr:hover {
background: rgba(255, 255, 255, 0.02);
background: #fafafa;
}
tbody tr:last-child td {
border-bottom: 0;
/* Node Hierarchy */
.node-child {
background: #fafafa;
}
/* Forms & Inputs */
.field {
display: grid;
gap: 8px;
margin-bottom: 18px;
.node-child td:first-child {
padding-left: 40px;
position: relative;
}
.field label {
font-size: 13px;
font-weight: 600;
color: var(--text-dim);
}
.field input, .field select, .field textarea {
width: 100%;
padding: 14px 16px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--border);
border-radius: var(--radius-md);
transition: all 0.2s ease;
}
.field input:focus, .field select:focus {
border-color: var(--accent);
background: rgba(255, 255, 255, 0.08);
box-shadow: 0 0 0 4px rgba(var(--accent-rgb), 0.1);
outline: none;
.node-child td:first-child::before {
content: "";
position: absolute;
left: 20px;
top: 0;
bottom: 50%;
width: 10px;
border-left: 1px solid var(--border);
border-bottom: 1px solid var(--border);
}
/* Buttons */
@@ -357,131 +253,251 @@ tbody tr:last-child td {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 12px 24px;
border-radius: var(--radius-md);
font-weight: 700;
height: 32px;
padding: 4px 15px;
font-size: 14px;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: var(--radius-md);
cursor: pointer;
transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
border: 1px solid transparent;
}
.btn:active {
transform: scale(0.98);
user-select: none;
}
.btn-primary {
background: linear-gradient(135deg, var(--accent), var(--accent-strong));
color: #050d16;
box-shadow: 0 12px 32px rgba(var(--accent-rgb), 0.24);
background-color: var(--accent);
color: #fff;
border-color: var(--accent);
}
.btn-primary:hover {
background-color: var(--accent-hover);
border-color: var(--accent-hover);
}
.btn-secondary {
background: rgba(255,255,255,0.05);
background-color: #fff;
border-color: var(--border);
color: var(--text);
}
.btn-ghost {
background: transparent;
.btn-secondary:hover {
color: var(--accent);
border-color: var(--accent);
}
.btn-ghost {
color: var(--accent);
padding: 8px 12px;
}
.btn-ghost:hover {
background: var(--accent-soft);
}
/* Status Badges */
/* Form Elements */
.field {
margin-bottom: 16px;
}
.field label {
display: block;
margin-bottom: 8px;
font-weight: 500;
}
.field input, .field select, .field textarea {
width: 100%;
padding: 4px 11px;
border: 1px solid var(--border);
border-radius: var(--radius-md);
transition: all 0.2s;
}
.field input:focus, .field select:focus {
border-color: var(--accent);
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.1);
outline: 0;
}
/* Toggle Switch (Ant Design Style) */
.switch {
position: relative;
display: inline-block;
width: 44px;
height: 22px;
vertical-align: middle;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.25);
transition: .2s;
border-radius: 100px;
}
.slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 2px;
bottom: 2px;
background-color: white;
transition: .2s;
border-radius: 50%;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
input:checked + .slider {
background-color: var(--accent);
}
input:checked + .slider:before {
transform: translateX(22px);
}
/* Status Pills */
.status-pill {
padding: 6px 12px;
border-radius: 999px;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
display: inline-flex;
padding: 0 7px;
font-size: 12px;
line-height: 20px;
border-radius: 4px;
display: inline-block;
}
.status-ok { background: rgba(101, 240, 183, 0.12); color: var(--success); border: 1px solid rgba(101, 240, 183, 0.2); }
.status-warn { background: rgba(255, 202, 122, 0.12); color: var(--warn); border: 1px solid rgba(255, 202, 122, 0.2); }
.status-danger { background: rgba(255, 141, 181, 0.12); color: var(--danger); border: 1px solid rgba(255, 141, 181, 0.2); }
/* Progress */
.progress-bar {
height: 8px;
background: rgba(255, 255, 255, 0.06);
border-radius: 999px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--accent), var(--accent-strong));
box-shadow: 0 0 12px var(--accent-soft);
}
.status-ok { background: #f6ffed; border: 1px solid #b7eb8f; color: #52c41a; }
.status-warn { background: #fffbe6; border: 1px solid #ffe58f; color: #faad14; }
.status-danger { background: #fff1f0; border: 1px solid #ffa39e; color: #ff4d4f; }
/* Modal */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(8px);
z-index: 100;
display: grid;
place-items: center;
padding: 24px;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1000;
background-color: rgba(0, 0, 0, 0.45);
display: flex;
align-items: flex-start;
justify-content: center;
padding-top: 100px;
}
.modal-card {
width: min(100%, 640px);
animation: modalIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
background: #fff;
border-radius: var(--radius-lg);
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
width: 520px;
overflow: hidden;
animation: modalIn 0.2s cubic-bezier(0.075, 0.82, 0.165, 1);
}
@keyframes modalIn {
from { opacity: 0; transform: scale(0.9) translateY(20px); }
to { opacity: 1; transform: scale(1) translateY(0); }
from { opacity: 0; transform: scale(0.9); }
to { opacity: 1; transform: scale(1); }
}
/* Pagination */
.pagination-shell {
/* Loading busy state */
.btn.busy {
opacity: 0.6;
cursor: not-allowed;
pointer-events: none;
}
/* Sidebar Submenus */
.sidebar-submenu {
margin-top: 2px;
padding-left: 24px;
}
.sidebar-submenu.collapsed {
display: none;
}
.sidebar-nav-item-header {
display: flex;
align-items: center;
gap: 12px;
margin-top: 24px;
justify-content: space-between;
padding: 10px 16px;
cursor: pointer;
border-radius: var(--radius-md);
color: var(--text-dim);
}
.page-btn {
width: 40px;
.sidebar-nav-item-header:hover {
background: rgba(0, 0, 0, 0.04);
}
/* Login Page Styling */
.login-container {
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background: #f0f2f5;
}
.login-card {
width: 400px;
padding: 40px;
background: #fff;
border-radius: var(--radius-lg);
box-shadow: 0 2px 10px rgba(0,0,0,0.08);
}
.login-header h1 {
text-align: center;
font-size: 32px;
font-weight: 600;
margin: 0 0 8px;
}
.login-header p {
text-align: center;
color: var(--text-faint);
margin-bottom: 32px;
}
.login-field {
margin-bottom: 24px;
}
.login-field label {
display: block;
margin-bottom: 8px;
color: var(--text-dim);
}
.input-wrapper input {
height: 40px;
display: grid;
place-items: center;
border-radius: 12px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--border);
transition: all 0.2s ease;
}
.page-btn.active {
background: var(--accent);
color: #050d16;
border-color: var(--accent);
.btn-login {
width: 100%;
height: 40px;
font-size: 16px;
}
/* Responsive */
@media (max-width: 1024px) {
.admin-shell { grid-template-columns: 1fr; }
.admin-sidebar { position: static; height: auto; display: none; } /* Mobile sidebar handled differently */
.admin-main { padding: 24px; }
.grid-3 { grid-template-columns: 1fr; }
}
/* Animation Utilities */
.fade-in {
animation: fadeIn 0.4s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
@media (max-width: 900px) {
.admin-sidebar {
display: none;
}
.admin-main {
margin-left: 0;
}
.grid-3 {
grid-template-columns: 1fr;
}
}

View File

@@ -18,6 +18,12 @@
realname: null,
devices: null,
nodes: null,
expandedNodes: new Set(), // Track IDs of expanded parent nodes
sidebarGroups: {
infrastructure: true,
financials: true,
system: false
},
groups: null,
routes: null,
plans: null,
@@ -56,26 +62,27 @@
async function loadBootstrap() {
try {
state.busy = true;
render();
var loginCheck = unwrap(await request("/api/v1/user/checkLogin", { method: "GET" }));
if (!loginCheck || !loginCheck.is_login || !loginCheck.is_admin) {
clearSession();
return;
if (loginCheck && loginCheck.is_admin) {
state.user = loginCheck;
var results = await Promise.all([
request(api.adminConfig, { method: "GET" }),
request(api.systemStatus, { method: "GET" })
]);
state.config = unwrap(results[0]) || {};
state.system = unwrap(results[1]) || {};
} else {
state.user = null;
}
state.user = loginCheck;
var results = await Promise.all([
request(api.adminConfig, { method: "GET" }),
request(api.systemStatus, { method: "GET" })
]);
state.config = unwrap(results[0]) || {};
state.system = unwrap(results[1]) || {};
} catch (error) {
console.error("Bootstrap failed:", error);
clearSession();
show(error.message || "Failed to load admin data.", "error");
if (state.token) show("Session expired or configuration error.", "error");
} finally {
state.busy = false;
render();
if (state.user) hydrateRoute();
}
}
@@ -127,6 +134,7 @@
}
function onClick(event) {
if (state.busy) return;
var actionEl = event.target.closest("[data-action]");
if (!actionEl) return;
@@ -141,6 +149,7 @@
// Handlers
if (action === "plan-add") { state.modal = { type: "plan", data: {} }; render(); return; }
if (action === "plan-edit") {
if (!state.plans) return;
var plan = state.plans.find(p => p.id == actionEl.getAttribute("data-id"));
state.modal = { type: "plan", data: plan }; render(); return;
}
@@ -167,6 +176,7 @@
}
if (action === "user-edit") {
if (!state.users || !state.users.list) return;
var user = state.users.list.find(u => u.id == actionEl.getAttribute("data-id"));
state.modal = { type: "user", data: user }; render(); return;
}
@@ -183,6 +193,30 @@
return;
}
if (action === "node-expand") {
var id = actionEl.getAttribute("data-id");
if (state.expandedNodes.has(id)) state.expandedNodes.delete(id);
else state.expandedNodes.add(id);
render(); return;
}
if (action === "node-toggle-visible") {
event.preventDefault();
event.stopPropagation();
var id = parseInt(actionEl.getAttribute("data-id"));
var input = actionEl.querySelector("input");
if (input) {
input.checked = !input.checked;
var show = input.checked ? 1 : 0;
adminPost(api.adminBase + "/server/manage/update", { id: id, show: show }).then(hydrateRoute);
}
return;
}
if (action === "sidebar-toggle") {
var id = actionEl.getAttribute("data-id");
state.sidebarGroups[id] = !state.sidebarGroups[id];
render(); return;
}
if (action === "node-add") { state.modal = { type: "node", data: {} }; render(); return; }
if (action === "node-edit") {
var node = state.nodes.find(n => n.id == actionEl.getAttribute("data-id"));
@@ -281,7 +315,7 @@
root.innerHTML = state.user ? renderDashboard() : renderLogin();
if (state.modal) {
var modalShell = document.createElement("div");
modalShell.className = "modal-overlay fade-in";
modalShell.className = "modal-overlay";
modalShell.innerHTML = renderModalContent();
root.appendChild(modalShell);
}
@@ -294,7 +328,7 @@
function renderModalContent() {
var m = state.modal;
var html = '<div class="glass-card modal-content"><button class="btn-close" data-action="modal-close">&times;</button>';
var html = '<div class="modal-card"><div class="card" style="margin:0; border:0; box-shadow:none;"><button class="btn btn-close" style="float:right; margin-top:-10px;" data-action="modal-close">&times;</button>';
if (m.type === "plan") html += renderPlanForm(m.data);
if (m.type === "user") html += renderUserForm(m.data);
if (m.type === "coupon") html += renderCouponForm(m.data);
@@ -351,20 +385,40 @@
function renderLogin() {
return [
'<div class="login-shell">',
'<section class="glass-card card modal-card fade-in">',
'<div style="text-align:center; margin-bottom:32px;">',
'<span class="topbar-eyebrow">SingBox Gopanel</span>',
'<h1 style="font-size:36px; margin:0;">Welcome Back</h1>',
'<p style="color:var(--text-dim); margin-top:8px;">Sign in to manage your high-performance network.</p>',
'<div class="login-container">',
'<div class="login-card">',
'<div class="login-header">',
'<div class="login-logo">',
'<div class="logo-icon">',
'<svg width="32" height="32" viewBox="0 0 32 32" fill="none"><path d="M16 4L4 10L16 16L28 10L16 4Z" fill="currentColor"/><path d="M4 22L16 28L28 22" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M4 16L16 22L28 16" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>',
'</div>',
'</div>',
'<h1>' + escapeHtml(cfg.title || "SingBox Console") + '</h1>',
'<p>Managed Infrastructure & Performance</p>',
'</div>',
state.message ? renderNotice() : "",
'<form data-form="login">',
'<div class="field"><label>Email Address</label><input type="email" name="email" placeholder="admin@example.com" required autocomplete="email" /></div>',
'<div class="field"><label>Secure Password</label><input type="password" name="password" placeholder="••••••••" required autocomplete="current-password" /></div>',
'<button class="btn btn-primary" style="width:100%; height:52px; font-size:16px; margin-top:12px;" type="submit">Unlock Console</button>',
'<form data-form="login" class="login-form">',
'<div class="login-field">',
'<label>Administrative Email</label>',
'<div class="input-wrapper">',
'<input type="email" name="email" placeholder="admin@network.local" required autocomplete="email" />',
'</div>',
'</div>',
'<div class="login-field">',
'<label>Access Key</label>',
'<div class="input-wrapper">',
'<input type="password" name="password" placeholder="••••••••••••" required autocomplete="current-password" />',
'</div>',
'</div>',
'<button type="submit" class="btn btn-primary btn-login">',
'<span>Establish Connection</span>',
'<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg>',
'</button>',
'</form>',
'</section>',
'<div class="login-footer">',
'<p>&copy; ' + new Date().getFullYear() + ' High-Performance Control Plane</p>',
'</div>',
'</div>',
'</div>'
].join("");
}
@@ -387,59 +441,70 @@
function renderSidebar() {
return [
'<aside class="admin-sidebar">',
'<div class="sidebar-header">',
'<div class="sidebar-brand">',
'<span class="sidebar-brand-mark">Nebula</span>',
'<strong>' + escapeHtml(cfg.title || "Admin") + '</strong>',
'<span>/' + escapeHtml(getSecurePath()) + ' Console</span>',
'<span>Control Plane v' + (cfg.version || "1.0") + '</span>',
'</div>',
'</div>',
'<div class="sidebar-nav-label">General</div>',
'<nav class="sidebar-nav">',
navItem("overview", "Overview", "Main statistics"),
navItem("dashboard-node", "Nodes Status", "Real-time metrics"),
'</nav>',
'<div style="margin-top: 24px; display: flex; flex-direction: column; gap: 4px;">',
renderSidebarGroup("general", "Dashboard", [
navItem("overview", "Overview", "Main statistics"),
navItem("dashboard-node", "Nodes Status", "Real-time metrics")
]),
'<div class="sidebar-nav-label">Infrastructure</div>',
'<nav class="sidebar-nav">',
navItem("node-manage", "Server Nodes", "Core infrastructure"),
navItem("node-group", "Permission Groups", "Access control"),
navItem("node-route", "Traffic Routing", "Advanced rules"),
'</nav>',
renderSidebarGroup("infrastructure", "Infrastructure", [
navItem("node-manage", "Server Nodes", "Core infrastructure"),
navItem("node-group", "Permission Groups", "Access control"),
navItem("node-route", "Traffic Routing", "Advanced rules")
]),
'<div class="sidebar-nav-label">Financials</div>',
'<nav class="sidebar-nav">',
navItem("plan-manage", "Packages", "Sales and traffic"),
navItem("order-manage", "Transactions", "Payment history"),
navItem("coupon-manage", "Marketing", "Coupons & gifts"),
'</nav>',
renderSidebarGroup("financials", "Financials", [
navItem("plan-manage", "Packages", "Sales and traffic"),
navItem("order-manage", "Transactions", "Payment history"),
navItem("coupon-manage", "Marketing", "Coupons & gifts")
]),
'<div class="sidebar-nav-label">Operations</div>',
'<nav class="sidebar-nav">',
navItem("user-manage", "User Base", "Accounts & traffic"),
navItem("user-online-devices", "Live Sessions", "Real-time monitoring"),
navItem("ticket-manage", "Service Desk", "Support tickets"),
navItem("realname", "KYC Workflow", "Identity verification"),
'</nav>',
'<div class="sidebar-nav-label">Platform</div>',
'<nav class="sidebar-nav">',
navItem("system-config", "Settings", "System core"),
'</nav>',
renderSidebarGroup("operations", "Operations", [
navItem("user-manage", "User Base", "Accounts & traffic"),
navItem("user-online-devices", "Live Sessions", "Real-time monitoring"),
navItem("ticket-manage", "Support Center", "Tickets & helpdesk")
]),
'<div style="margin-top:auto;" class="sidebar-meta">',
'<span class="sidebar-nav-label">Session</span>',
'<div style="padding:0 12px; font-size:13px; color:var(--text-dim);">' + escapeHtml(state.user.email) + '</div>',
'<div class="toolbar" style="padding:12px;"><button class="btn btn-ghost" style="width:100%; border:1px solid var(--border);" data-action="logout">Logout</button></div>',
renderSidebarGroup("system", "System", [
navItem("system-config", "Global Settings", "Platform parameters")
]),
'</div>',
'<div style="margin-top: auto; padding: 20px 0;">',
'<button class="sidebar-nav-item-header" style="width:100%;" data-action="logout">',
'<strong>Sign Out</strong>',
'</button>',
'</div>',
'</aside>'
].join("");
}
function renderSidebarGroup(id, label, items) {
var isExpanded = state.sidebarGroups[id] !== false;
return [
'<div class="sidebar-nav-group">',
'<div class="sidebar-nav-item-header" data-action="sidebar-toggle" data-id="' + id + '">',
'<span style="font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:0.1em; color:var(--text-faint);">' + escapeHtml(label) + '</span>',
'<svg class="sidebar-arrow ' + (isExpanded ? 'expanded' : '') + '" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" style="width:12px;height:12px;"><path d="M9 18l6-6-6-6"/></svg>',
'</div>',
'<nav class="sidebar-submenu ' + (isExpanded ? '' : 'collapsed') + '">',
items.join(""),
'</nav>',
'</div>'
].join("");
}
function renderTopbar() {
return [
'<header class="topbar glass-card">',
'<header class="topbar card">',
'<div class="topbar-copy">',
'<span class="topbar-eyebrow">Workspace / ' + escapeHtml(getRouteTitle(state.route)) + '</span>',
'<h1>' + escapeHtml(getRouteTitle(state.route)) + '</h1>',
'<p>' + escapeHtml(getRouteDescription(state.route)) + '</p>',
'<div class="topbar-meta">',
@@ -595,19 +660,65 @@
}
function renderNodeManage() {
var rows = state.nodes || [];
var nodes = state.nodes || [];
var roots = nodes.filter(n => !n.parent_id);
var childrenByParent = nodes.reduce((acc, n) => {
if (n.parent_id) {
acc[n.parent_id] = acc[n.parent_id] || [];
acc[n.parent_id].push(n);
}
return acc;
}, {});
var tableRows = [];
roots.forEach(function(node) {
var children = childrenByParent[node.id] || [];
var isExpanded = state.expandedNodes.has(String(node.id));
// Render Root Item
tableRows.push({
isChild: false,
data: node,
hasChildren: children.length > 0,
isExpanded: isExpanded
});
// Render Children if expanded
if (isExpanded) {
children.forEach(function(child) {
tableRows.push({
isChild: true,
data: child,
hasChildren: false,
isExpanded: false
});
});
}
});
return [
'<div class="toolbar" style="margin-bottom:24px;"><button class="btn btn-primary" data-action="node-add">Add New Node</button></div>',
'<div class="table-wrap glass-card">',
renderTable(["ID", "Name/Host", "Type", "Rate", "Visibility", "Actions"], rows.map(function(row) {
return [
escapeHtml(String(row.id)),
'<div><strong>' + escapeHtml(row.name) + '</strong><div style="font-size:12px; color:var(--text-faint);">' + escapeHtml(row.host) + '</div></div>',
escapeHtml(row.type),
'<strong>' + row.rate + 'x</strong>',
renderStatus(row.show ? "visible" : "hidden"),
'<div class="row-actions"><button class="btn btn-ghost" data-action="node-edit" data-id="' + row.id + '">Edit</button><button class="btn btn-ghost" data-action="node-copy" data-id="' + row.id + '">Copy</button><button class="btn btn-ghost" data-action="node-delete" data-id="' + row.id + '">Delete</button></div>'
];
'<div class="table-wrap glass-card" style="background:var(--panel-strong)">',
'<style> .node-toggle { cursor:pointer; margin-right:8px; width:24px; height:24px; display:inline-flex; align-items:center; justify-content:center; border-radius:6px; background:rgba(0,0,0,0.05); } .node-toggle:hover { background:rgba(0,0,0,0.1); } </style>',
renderTable(["ID", "Name/Host", "Type", "Rate", "Visibility", "Actions"], tableRows.map(function(row) {
var n = row.data;
var expandBtn = row.hasChildren ? '<span class="node-toggle" data-action="node-expand" data-id="' + n.id + '">' + (row.isExpanded ? "" : "+") + '</span>' : (row.isChild ? '' : '<span style="display:inline-block;width:32px;"></span>');
var nameContent = '<div style="display:flex; align-items:center;">' + expandBtn + '<div><strong>' + escapeHtml(n.name) + '</strong><div style="font-size:12px; color:var(--text-faint);">' + escapeHtml(n.host) + '</div></div></div>';
return {
className: row.isChild ? "node-child" : "",
cells: [
escapeHtml(String(n.id)),
nameContent,
escapeHtml(n.type),
'<strong>' + n.rate + 'x</strong>',
'<div style="display:flex; align-items:center; gap:8px;">' + renderStatus(n.show ? "visible" : "hidden") +
'<label class="switch" data-action="node-toggle-visible" data-id="' + n.id + '"><input type="checkbox" ' + (n.show ? "checked" : "") + '><span class="slider"></span></label></div>',
'<div class="row-actions"><button class="btn btn-ghost" data-action="node-edit" data-id="' + n.id + '">Edit</button><button class="btn btn-ghost" data-action="node-copy" data-id="' + n.id + '">Copy</button><button class="btn btn-ghost" data-action="node-delete" data-id="' + n.id + '">Delete</button></div>'
]
};
})),
'</div>'
].join("");
@@ -726,12 +837,17 @@
}
function renderTable(headers, rows) {
if (!rows.length) return '<div class="empty-state">No records found.</div>';
if (!rows.length) return '<div class="empty-state" style="padding:48px; text-align:center; color:var(--text-faint);">No records found.</div>';
return [
'<table><thead><tr>',
headers.map(function(h) { return '<th>' + escapeHtml(h) + '</th>'; }).join(""),
'</tr></thead><tbody>',
rows.map(function(row) { return '<tr>' + row.map(function(c) { return '<td>' + c + '</td>'; }).join("") + '</tr>'; }).join(""),
rows.map(function(row) {
var isObj = !Array.isArray(row);
var cells = isObj ? row.cells : row;
var cls = isObj ? (row.className || "") : "";
return '<tr class="' + cls + '">' + cells.map(function(c) { return '<td>' + c + '</td>'; }).join("") + '</tr>';
}).join(""),
'</tbody></table>'
].join("");
}

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff