admin / templates /index.html
CORVO-AI's picture
Upload 8 files
1d6a95a verified
Raw
History Blame Contribute Delete
22.1 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, maximum-scale=1">
<meta name="theme-color" content="#0a0a0f">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="mobile-web-app-capable" content="yes">
<title>ServerClass · Admin Console</title>
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%236366f1'/%3E%3Cpath d='M10 12h12M10 16h12M10 20h8' stroke='%23fff' stroke-width='2.5' stroke-linecap='round'/%3E%3C/svg%3E">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body data-unlocked="{{ 'true' if unlocked else 'false' }}">
<!-- ============================================================
LOCK SCREEN
============================================================ -->
<div id="lockScreen" class="lock {% if unlocked %}lock--hidden{% endif %}">
<div class="lock__bg"></div>
<div class="lock__card">
<div class="lock__logo">
<div class="lock__logo-mark">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="11" width="18" height="11" rx="2"/>
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
</svg>
</div>
<div class="lock__brand">
<div class="lock__brand-name">ServerClass</div>
<div class="lock__brand-sub">Admin Console</div>
</div>
</div>
<h1 class="lock__title">Welcome back</h1>
<p class="lock__sub">Enter your master password to unlock the dashboard.</p>
<form id="unlockForm" class="lock__form" autocomplete="off">
<div class="lock__field">
<input
type="password"
id="masterPassword"
class="lock__input"
placeholder="Master password"
autocomplete="current-password"
spellcheck="false"
required>
<button type="button" class="lock__toggle" id="togglePw" aria-label="Show password">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<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>
<button type="submit" class="lock__submit" id="unlockBtn">
<span class="lock__submit-label">Unlock</span>
<svg class="lock__submit-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
</button>
<div id="lockError" class="lock__error"></div>
</form>
<div class="lock__footer">
<span class="dot"></span>
<span class="mono">Secure · End-to-end signed session</span>
</div>
</div>
</div>
<!-- ============================================================
APP SHELL
============================================================ -->
<div id="app" class="app {% if not unlocked %}app--hidden{% endif %}">
<!-- Sidebar (desktop) -->
<aside class="sidebar">
<div class="sidebar__brand">
<div class="sidebar__mark">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="4" width="18" height="6" rx="1.5"/>
<rect x="3" y="14" width="18" height="6" rx="1.5"/>
<circle cx="7" cy="7" r=".8" fill="currentColor"/>
<circle cx="7" cy="17" r=".8" fill="currentColor"/>
</svg>
</div>
<div class="sidebar__name">
<div class="sidebar__title">ServerClass</div>
<div class="sidebar__subtitle mono">ADMIN · v2</div>
</div>
</div>
<nav class="sidebar__nav">
<button class="nav-item active" data-view="overview">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="7" height="9" rx="1.5"/>
<rect x="14" y="3" width="7" height="5" rx="1.5"/>
<rect x="14" y="12" width="7" height="9" rx="1.5"/>
<rect x="3" y="16" width="7" height="5" rx="1.5"/>
</svg>
<span>Overview</span>
</button>
<button class="nav-item" data-view="servers">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="4" width="18" height="6" rx="1.5"/>
<rect x="3" y="14" width="18" height="6" rx="1.5"/>
<circle cx="7" cy="7" r=".8" fill="currentColor"/>
<circle cx="7" cy="17" r=".8" fill="currentColor"/>
</svg>
<span>Servers</span>
<span class="nav-item__count" id="navServersCount"></span>
</button>
<button class="nav-item" data-view="users">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M22 21v-2a4 4 0 0 0-3-3.87"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
<span>Users</span>
<span class="nav-item__count" id="navUsersCount"></span>
</button>
<button class="nav-item" data-view="settings">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<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 0 2.83 2 2 0 0 1-2.83 0l-.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-2 2 2 2 0 0 1-2-2v-.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 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.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 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.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 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>
</svg>
<span>Settings</span>
</button>
</nav>
<div class="sidebar__footer">
<div class="conn-pill" id="connPill" data-state="off">
<span class="conn-pill__dot"></span>
<span class="conn-pill__label" id="connLabel">Connecting</span>
</div>
<button class="sidebar__logout" id="lockBtn" title="Lock dashboard">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
<path d="M16 17l5-5-5-5"/>
<path d="M21 12H9"/>
</svg>
</button>
</div>
</aside>
<!-- Main canvas -->
<main class="main">
<!-- Topbar (mobile) -->
<header class="topbar">
<div class="topbar__left">
<div class="topbar__brand">
<div class="topbar__mark">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="4" width="18" height="6" rx="1.5"/>
<rect x="3" y="14" width="18" height="6" rx="1.5"/>
</svg>
</div>
<div>
<div class="topbar__title" id="topbarTitle">Overview</div>
<div class="topbar__sub mono">
<span class="conn-pill conn-pill--mini" id="connPillMini" data-state="off">
<span class="conn-pill__dot"></span>
<span id="connLabelMini"></span>
</span>
· <span id="lastSync"></span>
</div>
</div>
</div>
</div>
<div class="topbar__right">
<button class="icon-btn" id="refreshBtn" title="Refresh">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 12a9 9 0 0 1 15-6.7L21 8"/>
<path d="M21 3v5h-5"/>
<path d="M21 12a9 9 0 0 1-15 6.7L3 16"/>
<path d="M3 21v-5h5"/>
</svg>
</button>
<button class="icon-btn icon-btn--mobile" id="lockBtnMobile" title="Lock">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="11" width="18" height="11" rx="2"/>
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
</svg>
</button>
</div>
</header>
<!-- Error banner -->
<div id="errorBox" class="error-stack"></div>
<!-- ===== VIEW: OVERVIEW ===== -->
<section class="view view--active" data-view="overview">
<div class="view__head">
<div>
<div class="eyebrow mono">DASHBOARD</div>
<h1 class="view__title">Overview</h1>
<p class="view__sub">Real-time snapshot of your distributed authentication mesh.</p>
</div>
<div class="view__head-meta">
<div class="live-indicator">
<span class="live-indicator__pulse"></span>
<span class="mono">LIVE</span>
</div>
</div>
</div>
<!-- KPI cards -->
<div class="kpi-grid" id="kpiGrid">
<div class="kpi kpi--accent">
<div class="kpi__icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M22 21v-2a4 4 0 0 0-3-3.87"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
</div>
<div class="kpi__label mono">TOTAL USERS</div>
<div class="kpi__value" id="kpiTotalUsers"></div>
<div class="kpi__trend mono"><span id="kpiTotalUsersSub">Registered accounts</span></div>
</div>
<div class="kpi">
<div class="kpi__icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="4" width="18" height="6" rx="1.5"/>
<rect x="3" y="14" width="18" height="6" rx="1.5"/>
<circle cx="7" cy="7" r=".8" fill="currentColor"/>
<circle cx="7" cy="17" r=".8" fill="currentColor"/>
</svg>
</div>
<div class="kpi__label mono">SERVERS</div>
<div class="kpi__value" id="kpiServers"></div>
<div class="kpi__trend mono"><span id="kpiServersSub">Online nodes</span></div>
</div>
<div class="kpi">
<div class="kpi__icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 8v4l3 3"/>
<circle cx="12" cy="12" r="10"/>
</svg>
</div>
<div class="kpi__label mono">RESERVATIONS</div>
<div class="kpi__value" id="kpiReservations"></div>
<div class="kpi__trend mono"><span id="kpiReservationsSub">Pending registrations</span></div>
</div>
<div class="kpi">
<div class="kpi__icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>
</svg>
</div>
<div class="kpi__label mono">CAPACITY</div>
<div class="kpi__value" id="kpiCapacity"><span class="kpi__value-suffix">%</span></div>
<div class="kpi__bar">
<div class="kpi__bar-fill" id="kpiCapacityBar" style="width:0%"></div>
</div>
<div class="kpi__trend mono"><span id="kpiCapacitySub">— / —</span></div>
</div>
</div>
<!-- Capacity chart -->
<div class="card card--chart">
<div class="card__head">
<div>
<div class="card__eyebrow mono">DISTRIBUTION</div>
<h2 class="card__title">Server Load</h2>
</div>
<div class="card__legend">
<span class="legend"><i class="legend__sw legend__sw--ok"></i>Healthy</span>
<span class="legend"><i class="legend__sw legend__sw--warn"></i>High</span>
<span class="legend"><i class="legend__sw legend__sw--full"></i>Full</span>
</div>
</div>
<div class="chart" id="loadChart">
<div class="chart__empty mono">Awaiting data…</div>
</div>
</div>
<!-- Recent users + top servers -->
<div class="split">
<div class="card">
<div class="card__head">
<div>
<div class="card__eyebrow mono">RECENT</div>
<h2 class="card__title">Latest Users</h2>
</div>
<button class="link-btn" data-jump="users">View all →</button>
</div>
<div id="recentUsers" class="recent">
<div class="recent__empty mono">No users yet.</div>
</div>
</div>
<div class="card">
<div class="card__head">
<div>
<div class="card__eyebrow mono">TOP LOAD</div>
<h2 class="card__title">Busiest Servers</h2>
</div>
<button class="link-btn" data-jump="servers">View all →</button>
</div>
<div id="topServers" class="recent">
<div class="recent__empty mono">No data yet.</div>
</div>
</div>
</div>
</section>
<!-- ===== VIEW: SERVERS ===== -->
<section class="view" data-view="servers">
<div class="view__head">
<div>
<div class="eyebrow mono">INFRASTRUCTURE</div>
<h1 class="view__title">Servers</h1>
<p class="view__sub">Live capacity and health across every node.</p>
</div>
</div>
<div class="toolbar">
<div class="search">
<svg class="search__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"/>
<path d="m21 21-4.35-4.35"/>
</svg>
<input type="text" id="serverSearch" class="search__input" placeholder="Filter servers by number, URL, or status…">
<span class="search__hint mono" id="serverCount">— nodes</span>
</div>
<div class="seg" id="serverFilter">
<button data-filter="all" class="seg__btn active">All</button>
<button data-filter="open" class="seg__btn">Open</button>
<button data-filter="full" class="seg__btn">Full</button>
</div>
</div>
<div id="serversContent" class="cards-grid">
<div class="empty mono">Loading servers…</div>
</div>
</section>
<!-- ===== VIEW: USERS ===== -->
<section class="view" data-view="users">
<div class="view__head">
<div>
<div class="eyebrow mono">DIRECTORY</div>
<h1 class="view__title">Users</h1>
<p class="view__sub">Every account, every token, every login.</p>
</div>
</div>
<div class="toolbar">
<div class="search">
<svg class="search__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"/>
<path d="m21 21-4.35-4.35"/>
</svg>
<input type="text" id="userSearch" class="search__input" placeholder="Search username, telegram ID, server…">
<span class="search__hint mono" id="userCount">— records</span>
</div>
<button class="btn btn--ghost" id="exportBtn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="16" height="16">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7 10 12 15 17 10"/>
<line x1="12" y1="15" x2="12" y2="3"/>
</svg>
Export CSV
</button>
</div>
<div id="usersContent" class="users-wrap">
<div class="empty mono">Loading users…</div>
</div>
</section>
<!-- ===== VIEW: SETTINGS ===== -->
<section class="view" data-view="settings">
<div class="view__head">
<div>
<div class="eyebrow mono">CONFIGURATION</div>
<h1 class="view__title">Settings</h1>
<p class="view__sub">Session and connection preferences.</p>
</div>
</div>
<div class="settings">
<div class="setting">
<div>
<div class="setting__label">Auto-refresh interval</div>
<div class="setting__hint">How often the dashboard pulls fresh data from upstream.</div>
</div>
<div class="setting__value mono"><span id="pollInterval">{{ poll_interval }}</span>s</div>
</div>
<div class="setting">
<div>
<div class="setting__label">Upstream API</div>
<div class="setting__hint">The backing ServerClass DB instance.</div>
</div>
<div class="setting__value mono">dooratre-db.hf.space</div>
</div>
<div class="setting">
<div>
<div class="setting__label">Session</div>
<div class="setting__hint">You stay signed in for 30 days on this device.</div>
</div>
<div class="setting__value">
<button class="btn btn--danger" id="lockBtnSettings">Lock dashboard</button>
</div>
</div>
<div class="setting">
<div>
<div class="setting__label">Build</div>
<div class="setting__hint">ServerClass Admin Console</div>
</div>
<div class="setting__value mono">v2.0 · MMXXV</div>
</div>
</div>
</section>
</main>
<!-- Mobile bottom nav -->
<nav class="bottomnav">
<button class="bottomnav__item active" data-view="overview">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="7" height="9" rx="1.5"/>
<rect x="14" y="3" width="7" height="5" rx="1.5"/>
<rect x="14" y="12" width="7" height="9" rx="1.5"/>
<rect x="3" y="16" width="7" height="5" rx="1.5"/>
</svg>
<span>Overview</span>
</button>
<button class="bottomnav__item" data-view="servers">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="4" width="18" height="6" rx="1.5"/>
<rect x="3" y="14" width="18" height="6" rx="1.5"/>
<circle cx="7" cy="7" r=".8" fill="currentColor"/>
<circle cx="7" cy="17" r=".8" fill="currentColor"/>
</svg>
<span>Servers</span>
</button>
<button class="bottomnav__item" data-view="users">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M16 21v-2a4a4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
</svg>
<span>Users</span>
</button>
<button class="bottomnav__item" data-view="settings">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<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 1 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 1 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 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 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 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 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 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
</svg>
<span>Settings</span>
</button>
</nav>
</div>
<!-- Toast container -->
<div id="toasts" class="toasts" aria-live="polite"></div>
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
<script>
window.__POLL_INTERVAL__ = {{ poll_interval }};
window.__UNLOCKED__ = {{ 'true' if unlocked else 'false' }};
</script>
<script src="{{ url_for('static', filename='js/dashboard.js') }}"></script>
</body>
</html>