| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>RMI Darkroom β Admin Control Panel</title> |
| <style> |
| :root { |
| --bg: #0a0a0f; --bg2: #12121a; --bg3: #1a1a2e; --bg4: #242438; |
| --fg: #e0e0e0; --fg2: #a0a0b0; --accent: #00d4ff; --accent2: #ff6b35; |
| --green: #00ff88; --red: #ff4444; --yellow: #ffcc00; --purple: #b388ff; |
| --border: #2a2a40; --shadow: 0 4px 24px rgba(0,0,0,0.5); |
| --radius: 8px; --font: 'Segoe UI', system-ui, sans-serif; |
| } |
| * { margin:0; padding:0; box-sizing:border-box; } |
| body { font-family:var(--font); background:var(--bg); color:var(--fg); min-height:100vh; } |
| |
| |
| .login-screen { display:flex; align-items:center; justify-content:center; min-height:100vh; background:linear-gradient(135deg, var(--bg) 0%, var(--bg3) 100%); } |
| .login-box { background:var(--bg2); padding:40px; border-radius:16px; border:1px solid var(--border); width:100%; max-width:420px; box-shadow:var(--shadow); } |
| .login-box h1 { font-size:28px; margin-bottom:8px; color:var(--accent); } |
| .login-box p { color:var(--fg2); margin-bottom:24px; font-size:14px; } |
| .login-box input { width:100%; padding:12px 16px; margin-bottom:12px; background:var(--bg3); border:1px solid var(--border); border-radius:var(--radius); color:var(--fg); font-size:15px; } |
| .login-box input:focus { outline:none; border-color:var(--accent); } |
| .login-box button { width:100%; padding:14px; background:var(--accent); color:var(--bg); border:none; border-radius:var(--radius); font-size:16px; font-weight:600; cursor:pointer; transition:opacity 0.2s; } |
| .login-box button:hover { opacity:0.9; } |
| .login-box .error { color:var(--red); font-size:13px; margin-top:8px; } |
| |
| |
| .app { display:none; } |
| .app.active { display:flex; min-height:100vh; } |
| .sidebar { width:260px; 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-header { padding:20px; border-bottom:1px solid var(--border); } |
| .sidebar-header h2 { font-size:18px; color:var(--accent); } |
| .sidebar-header span { font-size:12px; color:var(--fg2); } |
| .sidebar-nav { flex:1; padding:12px; } |
| .nav-item { display:flex; align-items:center; gap:12px; padding:10px 14px; border-radius:var(--radius); cursor:pointer; transition:all 0.2s; color:var(--fg2); font-size:14px; margin-bottom:2px; } |
| .nav-item:hover { background:var(--bg3); color:var(--fg); } |
| .nav-item.active { background:var(--bg3); color:var(--accent); border-left:3px solid var(--accent); } |
| .nav-item svg { width:18px; height:18px; flex-shrink:0; } |
| .nav-section { font-size:11px; text-transform:uppercase; color:var(--fg2); padding:16px 14px 8px; letter-spacing:1px; } |
| .sidebar-footer { padding:16px; border-top:1px solid var(--border); font-size:12px; color:var(--fg2); } |
| .sidebar-footer button { width:100%; padding:8px; margin-top:8px; background:var(--bg3); border:1px solid var(--border); color:var(--fg2); border-radius:var(--radius); cursor:pointer; } |
| |
| .main { flex:1; margin-left:260px; min-height:100vh; } |
| .topbar { display:flex; align-items:center; justify-content:space-between; padding:16px 24px; background:var(--bg2); border-bottom:1px solid var(--border); position:sticky; top:0; z-index:50; } |
| .topbar h1 { font-size:20px; font-weight:500; } |
| .topbar-right { display:flex; align-items:center; gap:16px; } |
| .topbar .badge { padding:4px 12px; border-radius:20px; font-size:12px; font-weight:500; } |
| .badge-admin { background:var(--accent); color:var(--bg); } |
| .badge-super { background:var(--purple); color:var(--bg); } |
| .badge-mod { background:var(--yellow); color:var(--bg); } |
| |
| .content { padding:24px; } |
| |
| |
| .card-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(280px, 1fr)); gap:16px; margin-bottom:24px; } |
| .stat-card { background:var(--bg2); border:1px solid var(--border); border-radius:var(--radius); padding:20px; } |
| .stat-card h3 { font-size:12px; color:var(--fg2); text-transform:uppercase; letter-spacing:1px; margin-bottom:8px; } |
| .stat-card .value { font-size:32px; font-weight:700; color:var(--accent); } |
| .stat-card .change { font-size:13px; color:var(--green); margin-top:4px; } |
| .stat-card .change.down { color:var(--red); } |
| |
| |
| .table-wrap { background:var(--bg2); border:1px solid var(--border); border-radius:var(--radius); overflow:hidden; } |
| .table-wrap table { width:100%; border-collapse:collapse; } |
| .table-wrap th { text-align:left; padding:12px 16px; font-size:12px; text-transform:uppercase; color:var(--fg2); background:var(--bg3); border-bottom:1px solid var(--border); } |
| .table-wrap td { padding:12px 16px; border-bottom:1px solid var(--border); font-size:14px; } |
| .table-wrap tr:hover td { background:var(--bg3); } |
| .table-wrap .status { padding:4px 10px; border-radius:12px; font-size:11px; font-weight:500; } |
| .status-published { background:rgba(0,255,136,0.15); color:var(--green); } |
| .status-draft { background:rgba(255,204,0,0.15); color:var(--yellow); } |
| .status-archived { background:rgba(255,68,68,0.15); color:var(--red); } |
| .status-review { background:rgba(0,212,255,0.15); color:var(--accent); } |
| .status-scheduled { background:rgba(179,136,255,0.15); color:var(--purple); } |
| |
| |
| .btn { padding:8px 16px; border-radius:var(--radius); border:none; font-size:13px; cursor:pointer; transition:opacity 0.2s; font-weight:500; } |
| .btn-primary { background:var(--accent); color:var(--bg); } |
| .btn-danger { background:var(--red); color:#fff; } |
| .btn-secondary { background:var(--bg3); color:var(--fg); border:1px solid var(--border); } |
| .btn-sm { padding:4px 12px; font-size:12px; } |
| .btn:hover { opacity:0.85; } |
| |
| |
| .form-group { margin-bottom:16px; } |
| .form-group label { display:block; font-size:13px; color:var(--fg2); margin-bottom:6px; } |
| .form-group input, .form-group textarea, .form-group select { width:100%; padding:10px 14px; background:var(--bg3); border:1px solid var(--border); border-radius:var(--radius); color:var(--fg); font-size:14px; font-family:inherit; } |
| .form-group input:focus, .form-group textarea:focus, .form-group select:focus { outline:none; border-color:var(--accent); } |
| .form-group textarea { min-height:120px; resize:vertical; } |
| .form-row { display:grid; grid-template-columns:1fr 1fr; gap:16px; } |
| |
| |
| .tabs { display:flex; gap:4px; margin-bottom:20px; border-bottom:1px solid var(--border); } |
| .tab { padding:10px 20px; cursor:pointer; font-size:14px; color:var(--fg2); border-bottom:2px solid transparent; transition:all 0.2s; } |
| .tab:hover { color:var(--fg); } |
| .tab.active { color:var(--accent); border-bottom-color:var(--accent); } |
| |
| |
| .search-bar { display:flex; gap:12px; margin-bottom:20px; } |
| .search-bar input { flex:1; padding:10px 16px; background:var(--bg3); border:1px solid var(--border); border-radius:var(--radius); color:var(--fg); } |
| .search-bar input:focus { outline:none; border-color:var(--accent); } |
| |
| |
| .modal-overlay { display:none; position:fixed; inset:0; background:rgba(0,0,0,0.7); z-index:200; align-items:center; justify-content:center; } |
| .modal-overlay.active { display:flex; } |
| .modal { background:var(--bg2); border:1px solid var(--border); border-radius:16px; width:90%; max-width:700px; max-height:90vh; overflow-y:auto; box-shadow:var(--shadow); } |
| .modal-header { padding:20px 24px; border-bottom:1px solid var(--border); display:flex; justify-content:space-between; align-items:center; } |
| .modal-header h2 { font-size:18px; } |
| .modal-body { padding:24px; } |
| .modal-footer { padding:16px 24px; border-top:1px solid var(--border); display:flex; justify-content:flex-end; gap:12px; } |
| |
| |
| .chart-container { background:var(--bg2); border:1px solid var(--border); border-radius:var(--radius); padding:20px; height:300px; display:flex; align-items:center; justify-content:center; color:var(--fg2); } |
| |
| |
| .toast { position:fixed; bottom:24px; right:24px; padding:12px 20px; border-radius:var(--radius); color:#fff; font-size:14px; z-index:300; transform:translateY(100px); opacity:0; transition:all 0.3s; } |
| .toast.show { transform:translateY(0); opacity:1; } |
| .toast-success { background:var(--green); color:var(--bg); } |
| .toast-error { background:var(--red); } |
| |
| |
| @media (max-width:768px) { |
| .sidebar { width:60px; } |
| .sidebar .nav-item span, .sidebar-header span, .nav-section { display:none; } |
| .main { margin-left:60px; } |
| .form-row { grid-template-columns:1fr; } |
| } |
| </style> |
| </head> |
| <body> |
|
|
| |
| <div class="login-screen" id="loginScreen"> |
| <div class="login-box"> |
| <h1>Darkroom</h1> |
| <p>RugMunch Intelligence β Admin Control Panel</p> |
| <input type="email" id="loginEmail" placeholder="Admin email" value=""> |
| <input type="password" id="loginPassword" placeholder="Password"> |
| <input type="text" id="loginTOTP" placeholder="2FA code (if enabled)" style="display:none;"> |
| <button onclick="doLogin()">Sign In</button> |
| <div class="error" id="loginError"></div> |
| </div> |
| </div> |
|
|
| |
| <div class="app" id="app"> |
| <aside class="sidebar"> |
| <div class="sidebar-header"> |
| <h2>Darkroom</h2> |
| <span id="adminRoleBadge">ADMIN</span> |
| </div> |
| <nav class="sidebar-nav"> |
| <div class="nav-section">Overview</div> |
| <div class="nav-item active" onclick="navTo('dashboard')" 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> |
| <span>Dashboard</span> |
| </div> |
| <div class="nav-item" onclick="navTo('system')" data-page="system"> |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg> |
| <span>System Health</span> |
| </div> |
| <div class="nav-item" onclick="navTo('analytics')" data-page="analytics"> |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 20V10"/><path d="M12 20V4"/><path d="M6 20v-6"/></svg> |
| <span>Analytics</span> |
| </div> |
|
|
| <div class="nav-section">Users & Security</div> |
| <div class="nav-item" onclick="navTo('users')" data-page="users"> |
| <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"/></svg> |
| <span>User Management</span> |
| </div> |
| <div class="nav-item" onclick="navTo('security')" data-page="security"> |
| <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> |
| <span>Security Center</span> |
| </div> |
| <div class="nav-item" onclick="navTo('audit')" data-page="audit"> |
| <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"/></svg> |
| <span>Audit Logs</span> |
| </div> |
| <div class="nav-item" onclick="navTo('admins')" data-page="admins"> |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg> |
| <span>Admin Accounts</span> |
| </div> |
|
|
| <div class="nav-section">Content</div> |
| <div class="nav-item" onclick="navTo('bulletin')" data-page="bulletin"> |
| <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"/><path d="M22 6l-10 7L2 6"/></svg> |
| <span>Bulletin Board</span> |
| </div> |
| <div class="nav-item" onclick="navTo('announcements')" data-page="announcements"> |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg> |
| <span>Announcements</span> |
| </div> |
|
|
| <div class="nav-section">Platform</div> |
| <div class="nav-item" onclick="navTo('tokens')" data-page="tokens"> |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg> |
| <span>Token Deployer</span> |
| </div> |
| <div class="nav-item" onclick="navTo('airdrop')" data-page="airdrop"> |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg> |
| <span>Airdrop Manager</span> |
| </div> |
| <div class="nav-item" onclick="navTo('financial')" data-page="financial"> |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg> |
| <span>Financial</span> |
| </div> |
| <div class="nav-item" onclick="navTo('apikeys')" data-page="apikeys"> |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/></svg> |
| <span>API Keys</span> |
| </div> |
| <div class="nav-item" onclick="navTo('webhooks')" data-page="webhooks"> |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg> |
| <span>Webhooks</span> |
| </div> |
| <div class="nav-item" onclick="navTo('config')" data-page="config"> |
| <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 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 4.68 15a1.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.68 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 9 4.68a1.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 19.32 9a1.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>Configuration</span> |
| </div> |
| </nav> |
| <div class="sidebar-footer"> |
| <div id="adminEmail">admin@rugmunch.io</div> |
| <button onclick="doLogout()">Logout</button> |
| </div> |
| </aside> |
|
|
| <main class="main"> |
| <div class="topbar"> |
| <h1 id="pageTitle">Dashboard</h1> |
| <div class="topbar-right"> |
| <span class="badge badge-admin" id="roleBadge">ADMIN</span> |
| <span id="currentTime" style="font-size:13px;color:var(--fg2);"></span> |
| </div> |
| </div> |
| <div class="content" id="mainContent"> |
| |
| </div> |
| </main> |
| </div> |
|
|
| |
| <div class="modal-overlay" id="modalOverlay"> |
| <div class="modal" id="modal"> |
| <div class="modal-header"> |
| <h2 id="modalTitle">Modal</h2> |
| <button class="btn btn-secondary btn-sm" onclick="closeModal()">×</button> |
| </div> |
| <div class="modal-body" id="modalBody"></div> |
| <div class="modal-footer" id="modalFooter"></div> |
| </div> |
| </div> |
|
|
| |
| <div class="toast" id="toast"></div> |
|
|
| <script> |
| // ββ State βββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| let session = null; |
| let admin = null; |
| let currentPage = 'dashboard'; |
| const API_BASE = '/api/v1'; |
| |
| // ββ Auth ββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| async function doLogin() { |
| const email = document.getElementById('loginEmail').value; |
| const password = document.getElementById('loginPassword').value; |
| const totp = document.getElementById('loginTOTP').value; |
| const errorEl = document.getElementById('loginError'); |
| errorEl.textContent = ''; |
| |
| try { |
| const res = await fetch(`${API_BASE}/admin/backend/auth/login`, { |
| method: 'POST', |
| headers: {'Content-Type': 'application/json'}, |
| body: JSON.stringify({email, password, totp_code: totp || undefined}) |
| }); |
| const data = await res.json(); |
| if (!res.ok) throw new Error(data.detail || 'Login failed'); |
| |
| session = data.session_id; |
| admin = data.admin; |
| localStorage.setItem('admin_session', session); |
| showApp(); |
| } catch (e) { |
| errorEl.textContent = e.message; |
| } |
| } |
| |
| function doLogout() { |
| localStorage.removeItem('admin_session'); |
| session = null; |
| admin = null; |
| location.reload(); |
| } |
| |
| async function checkSession() { |
| const saved = localStorage.getItem('admin_session'); |
| if (!saved) return; |
| session = saved; |
| try { |
| const res = await fetch(`${API_BASE}/admin/backend/auth/me`, { |
| headers: {'X-Admin-Session': session} |
| }); |
| if (!res.ok) throw new Error('Session expired'); |
| const data = await res.json(); |
| admin = data.admin; |
| showApp(); |
| } catch (e) { |
| localStorage.removeItem('admin_session'); |
| } |
| } |
| |
| function showApp() { |
| document.getElementById('loginScreen').style.display = 'none'; |
| document.getElementById('app').classList.add('active'); |
| document.getElementById('adminEmail').textContent = admin?.email || 'Admin'; |
| document.getElementById('roleBadge').textContent = (admin?.role || 'ADMIN').toUpperCase(); |
| document.getElementById('adminRoleBadge').textContent = (admin?.role || 'ADMIN').toUpperCase(); |
| navTo('dashboard'); |
| } |
| |
| // ββ Navigation ββββββββββββββββββββββββββββββββββββββββββββββββ |
| function navTo(page) { |
| currentPage = page; |
| document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active')); |
| document.querySelector(`[data-page="${page}"]`)?.classList.add('active'); |
| document.getElementById('pageTitle').textContent = page.charAt(0).toUpperCase() + page.slice(1); |
| |
| const content = document.getElementById('mainContent'); |
| content.innerHTML = ''; |
| |
| switch(page) { |
| case 'dashboard': renderDashboard(content); break; |
| case 'system': renderSystem(content); break; |
| case 'analytics': renderAnalytics(content); break; |
| case 'users': renderUsers(content); break; |
| case 'security': renderSecurity(content); break; |
| case 'audit': renderAudit(content); break; |
| case 'admins': renderAdmins(content); break; |
| case 'bulletin': renderBulletin(content); break; |
| case 'announcements': renderAnnouncements(content); break; |
| case 'tokens': window.location.href = '/darkroom'; break; |
| case 'airdrop': window.location.href = '/darkroom'; break; |
| case 'financial': renderFinancial(content); break; |
| case 'apikeys': renderAPIKeys(content); break; |
| case 'webhooks': renderWebhooks(content); break; |
| case 'config': renderConfig(content); break; |
| default: renderDashboard(content); |
| } |
| } |
| |
| // ββ Dashboard βββββββββββββββββββββββββββββββββββββββββββββββββ |
| async function renderDashboard(el) { |
| el.innerHTML = ` |
| <div class="card-grid"> |
| <div class="stat-card"><h3>Total Users</h3><div class="value" id="dashUsers">-</div><div class="change">+12% this week</div></div> |
| <div class="stat-card"><h3>Active Sessions</h3><div class="value" id="dashSessions">-</div></div> |
| <div class="stat-card"><h3>Blocked IPs</h3><div class="value" id="dashBlocked">-</div></div> |
| <div class="stat-card"><h3>System Health</h3><div class="value" id="dashHealth" style="color:var(--green)">OK</div></div> |
| <div class="stat-card"><h3>Token Deployments</h3><div class="value" id="dashTokens">-</div></div> |
| <div class="stat-card"><h3>X402 Revenue</h3><div class="value" id="dashRevenue">$0</div></div> |
| </div> |
| <div class="chart-container">Charts & Analytics β Coming Soon</div> |
| `; |
| try { |
| const res = await fetch(`${API_BASE}/admin/backend/dashboard`, {headers:{'X-Admin-Session':session}}); |
| if(res.ok) { |
| const data = await res.json(); |
| document.getElementById('dashUsers').textContent = data.stats?.total_users || 0; |
| document.getElementById('dashSessions').textContent = data.stats?.active_sessions || 0; |
| document.getElementById('dashBlocked').textContent = data.stats?.blocked_ips || 0; |
| document.getElementById('dashTokens').textContent = data.stats?.total_deployments || 0; |
| const health = data.health?.cpu?.percent || 0; |
| document.getElementById('dashHealth').textContent = health > 80 ? 'HIGH' : 'OK'; |
| document.getElementById('dashHealth').style.color = health > 80 ? 'var(--red)' : 'var(--green)'; |
| } |
| } catch(e) {} |
| } |
| |
| // ββ System Health ββββββββββββββββββββββββββββββββββββββββββββ |
| async function renderSystem(el) { |
| el.innerHTML = `<div class="chart-container">Loading system health...</div>`; |
| try { |
| const res = await fetch(`${API_BASE}/admin/backend/system/health`, {headers:{'X-Admin-Session':session}}); |
| const data = await res.json(); |
| el.innerHTML = ` |
| <div class="card-grid"> |
| <div class="stat-card"><h3>CPU Usage</h3><div class="value">${data.cpu?.percent || 0}%</div><div class="change">${data.cpu?.count} cores</div></div> |
| <div class="stat-card"><h3>Memory</h3><div class="value">${data.memory?.percent || 0}%</div><div class="change">${data.memory?.used_gb}GB / ${data.memory?.total_gb}GB</div></div> |
| <div class="stat-card"><h3>Disk</h3><div class="value">${data.disk?.percent || 0}%</div><div class="change">${data.disk?.free_gb}GB free</div></div> |
| <div class="stat-card"><h3>Backend Memory</h3><div class="value">${data.backend?.memory_mb || 0}MB</div><div class="change">${data.backend?.threads} threads</div></div> |
| </div> |
| <div class="table-wrap"> |
| <table><thead><tr><th>Service</th><th>Status</th><th>Details</th></tr></thead> |
| <tbody> |
| <tr><td>Backend</td><td><span class="status status-published">Running</span></td><td>PID ${data.backend?.pid}</td></tr> |
| <tr><td>Redis</td><td><span class="status ${data.services?.redis?.status==='connected'?'status-published':'status-archived'}">${data.services?.redis?.status}</span></td><td>${data.services?.redis?.version || ''}</td></tr> |
| <tr><td>Supabase</td><td><span class="status ${data.services?.supabase?.status==='connected'?'status-published':'status-archived'}">${data.services?.supabase?.status}</span></td><td></td></tr> |
| </tbody> |
| </table> |
| </div> |
| `; |
| } catch(e) { el.innerHTML = `<div class="error">Failed to load: ${e.message}</div>`; } |
| } |
| |
| // ββ Users ββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| async function renderUsers(el) { |
| el.innerHTML = ` |
| <div class="search-bar"> |
| <input type="text" id="userSearch" placeholder="Search users..." onkeyup="if(event.key==='Enter')loadUsers()"> |
| <button class="btn btn-primary" onclick="loadUsers()">Search</button> |
| </div> |
| <div class="table-wrap" id="usersTable"><table><thead><tr><th>Email</th><th>Tier</th><th>Role</th><th>Created</th><th>Actions</th></tr></thead><tbody><tr><td colspan="5" style="text-align:center;color:var(--fg2)">Loading...</td></tr></tbody></table></div> |
| `; |
| loadUsers(); |
| } |
| |
| async function loadUsers() { |
| const search = document.getElementById('userSearch')?.value || ''; |
| try { |
| const res = await fetch(`${API_BASE}/admin/backend/users?limit=100&search=${encodeURIComponent(search)}`, {headers:{'X-Admin-Session':session}}); |
| const data = await res.json(); |
| const tbody = data.users?.map(u => ` |
| <tr> |
| <td>${u.email}</td> |
| <td><span class="status ${u.tier==='FREE'?'status-draft':'status-published'}">${u.tier}</span></td> |
| <td>${u.role || 'USER'}</td> |
| <td>${new Date(u.created_at).toLocaleDateString()}</td> |
| <td> |
| <button class="btn btn-sm btn-secondary" onclick="banUser('${u.id}', ${!u.banned})">${u.banned ? 'Unban' : 'Ban'}</button> |
| <button class="btn btn-sm btn-secondary" onclick="showUserTier('${u.id}', '${u.tier}')">Tier</button> |
| </td> |
| </tr> |
| `).join('') || '<tr><td colspan="5" style="text-align:center">No users</td></tr>'; |
| document.getElementById('usersTable').innerHTML = `<table><thead><tr><th>Email</th><th>Tier</th><th>Role</th><th>Created</th><th>Actions</th></tr></thead><tbody>${tbody}</tbody></table>`; |
| } catch(e) {} |
| } |
| |
| async function banUser(userId, banned) { |
| try { |
| await fetch(`${API_BASE}/admin/backend/users/${userId}/ban`, { |
| method: 'POST', headers: {'X-Admin-Session': session, 'Content-Type': 'application/json'}, |
| body: JSON.stringify({banned}) |
| }); |
| showToast(banned ? 'User banned' : 'User unbanned', 'success'); |
| loadUsers(); |
| } catch(e) { showToast(e.message, 'error'); } |
| } |
| |
| // ββ Security βββββββββββββββββββββββββββββββββββββββββββββββββ |
| async function renderSecurity(el) { |
| el.innerHTML = ` |
| <div class="tabs"> |
| <div class="tab active" onclick="showSecurityTab('blocked')">Blocked IPs</div> |
| <div class="tab" onclick="showSecurityTab('threats')">Threats</div> |
| </div> |
| <div id="securityContent"> |
| <div class="search-bar"> |
| <input type="text" id="blockIP" placeholder="IP to block"> |
| <input type="text" id="blockReason" placeholder="Reason" style="flex:2"> |
| <button class="btn btn-danger" onclick="blockIP()">Block IP</button> |
| </div> |
| <div class="table-wrap" id="blockedTable">Loading...</div> |
| </div> |
| `; |
| loadBlockedIPs(); |
| } |
| |
| async function loadBlockedIPs() { |
| try { |
| const res = await fetch(`${API_BASE}/admin/backend/security/blocked-ips`, {headers:{'X-Admin-Session':session}}); |
| const data = await res.json(); |
| const tbody = data.blocked_ips?.map(ip => ` |
| <tr><td>${ip.ip}</td><td>${ip.reason}</td><td>${new Date(ip.blocked_at).toLocaleString()}</td><td>${ip.duration_hours}h</td> |
| <td><button class="btn btn-sm btn-secondary" onclick="unblockIP('${ip.ip}')">Unblock</button></td></tr> |
| `).join('') || '<tr><td colspan="5" style="text-align:center">No blocked IPs</td></tr>'; |
| document.getElementById('blockedTable').innerHTML = `<table><thead><tr><th>IP</th><th>Reason</th><th>Blocked At</th><th>Duration</th><th>Actions</th></tr></thead><tbody>${tbody}</tbody></table>`; |
| } catch(e) {} |
| } |
| |
| async function blockIP() { |
| const ip = document.getElementById('blockIP').value; |
| const reason = document.getElementById('blockReason').value; |
| if(!ip) return; |
| try { |
| await fetch(`${API_BASE}/admin/backend/security/block-ip`, { |
| method: 'POST', headers: {'X-Admin-Session': session, 'Content-Type': 'application/json'}, |
| body: JSON.stringify({ip, reason, duration_hours: 24}) |
| }); |
| showToast('IP blocked', 'success'); |
| loadBlockedIPs(); |
| } catch(e) { showToast(e.message, 'error'); } |
| } |
| |
| async function unblockIP(ip) { |
| try { |
| await fetch(`${API_BASE}/admin/backend/security/unblock-ip`, { |
| method: 'POST', headers: {'X-Admin-Session': session, 'Content-Type': 'application/json'}, |
| body: JSON.stringify({ip}) |
| }); |
| showToast('IP unblocked', 'success'); |
| loadBlockedIPs(); |
| } catch(e) { showToast(e.message, 'error'); } |
| } |
| |
| function showSecurityTab(tab) { |
| document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); |
| event.target.classList.add('active'); |
| if(tab === 'blocked') loadBlockedIPs(); |
| } |
| |
| // ββ Audit Logs βββββββββββββββββββββββββββββββββββββββββββββββ |
| async function renderAudit(el) { |
| el.innerHTML = ` |
| <div class="search-bar"> |
| <input type="text" id="auditAction" placeholder="Action filter"> |
| <button class="btn btn-primary" onclick="loadAudit()">Refresh</button> |
| </div> |
| <div class="table-wrap" id="auditTable">Loading...</div> |
| `; |
| loadAudit(); |
| } |
| |
| async function loadAudit() { |
| try { |
| const res = await fetch(`${API_BASE}/admin/backend/security/audit-logs?limit=100`, {headers:{'X-Admin-Session':session}}); |
| const data = await res.json(); |
| const tbody = data.logs?.map(l => ` |
| <tr><td>${new Date(l.timestamp).toLocaleString()}</td><td>${l.admin_email}</td><td>${l.action}</td> |
| <td>${l.resource_type}</td><td><span class="status ${l.status==='success'?'status-published':'status-archived'}">${l.status}</span></td></tr> |
| `).join('') || '<tr><td colspan="5" style="text-align:center">No logs</td></tr>'; |
| document.getElementById('auditTable').innerHTML = `<table><thead><tr><th>Time</th><th>Admin</th><th>Action</th><th>Resource</th><th>Status</th></tr></thead><tbody>${tbody}</tbody></table>`; |
| } catch(e) {} |
| } |
| |
| // ββ Admins βββββββββββββββββββββββββββββββββββββββββββββββββββ |
| async function renderAdmins(el) { |
| el.innerHTML = ` |
| <div style="margin-bottom:16px"><button class="btn btn-primary" onclick="showCreateAdmin()">+ Create Admin</button></div> |
| <div class="table-wrap" id="adminsTable">Loading...</div> |
| `; |
| loadAdmins(); |
| } |
| |
| async function loadAdmins() { |
| try { |
| const res = await fetch(`${API_BASE}/admin/backend/admins`, {headers:{'X-Admin-Session':session}}); |
| const data = await res.json(); |
| const tbody = data.admins?.map(a => ` |
| <tr><td>${a.email}</td><td><span class="badge badge-${a.role}">${a.role}</span></td> |
| <td>${a.is_active ? 'Active' : 'Inactive'}</td><td>${a.last_login ? new Date(a.last_login).toLocaleString() : 'Never'}</td> |
| <td><button class="btn btn-sm btn-secondary" onclick="resetAdminPassword('${a.id}')">Reset PW</button></td></tr> |
| `).join('') || '<tr><td colspan="5" style="text-align:center">No admins</td></tr>'; |
| document.getElementById('adminsTable').innerHTML = `<table><thead><tr><th>Email</th><th>Role</th><th>Status</th><th>Last Login</th><th>Actions</th></tr></thead><tbody>${tbody}</tbody></table>`; |
| } catch(e) {} |
| } |
| |
| function showCreateAdmin() { |
| document.getElementById('modalTitle').textContent = 'Create Admin'; |
| document.getElementById('modalBody').innerHTML = ` |
| <div class="form-group"><label>Email</label><input type="email" id="newAdminEmail"></div> |
| <div class="form-group"><label>Password</label><input type="password" id="newAdminPassword"></div> |
| <div class="form-group"><label>Role</label><select id="newAdminRole"><option value="viewer">Viewer</option><option value="moderator">Moderator</option><option value="admin">Admin</option><option value="superadmin">Superadmin</option></select></div> |
| `; |
| document.getElementById('modalFooter').innerHTML = ` |
| <button class="btn btn-secondary" onclick="closeModal()">Cancel</button> |
| <button class="btn btn-primary" onclick="createAdmin()">Create</button> |
| `; |
| document.getElementById('modalOverlay').classList.add('active'); |
| } |
| |
| async function createAdmin() { |
| const email = document.getElementById('newAdminEmail').value; |
| const password = document.getElementById('newAdminPassword').value; |
| const role = document.getElementById('newAdminRole').value; |
| try { |
| await fetch(`${API_BASE}/admin/backend/admins`, { |
| method: 'POST', headers: {'X-Admin-Session': session, 'Content-Type': 'application/json'}, |
| body: JSON.stringify({email, password, role}) |
| }); |
| closeModal(); |
| showToast('Admin created', 'success'); |
| loadAdmins(); |
| } catch(e) { showToast(e.message, 'error'); } |
| } |
| |
| // ββ BULLETIN BOARD βββββββββββββββββββββββββββββββββββββββββββ |
| async function renderBulletin(el) { |
| el.innerHTML = ` |
| <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;"> |
| <div class="tabs"> |
| <div class="tab active" onclick="showBulletinTab('all')">All Posts</div> |
| <div class="tab" onclick="showBulletinTab('published')">Published</div> |
| <div class="tab" onclick="showBulletinTab('draft')">Drafts</div> |
| <div class="tab" onclick="showBulletinTab('scheduled')">Scheduled</div> |
| <div class="tab" onclick="showBulletinTab('archived')">Archived</div> |
| </div> |
| <button class="btn btn-primary" onclick="showCreatePost()">+ New Post</button> |
| </div> |
| <div class="search-bar"> |
| <input type="text" id="postSearch" placeholder="Search posts..." onkeyup="if(event.key==='Enter')loadPosts()"> |
| <select id="postCategory" onchange="loadPosts()"> |
| <option value="">All Categories</option> |
| <option value="news">News</option> |
| <option value="alert">Alert</option> |
| <option value="update">Update</option> |
| <option value="promo">Promo</option> |
| <option value="system">System</option> |
| <option value="community">Community</option> |
| <option value="announcement">Announcement</option> |
| <option value="tutorial">Tutorial</option> |
| </select> |
| <button class="btn btn-primary" onclick="loadPosts()">Search</button> |
| </div> |
| <div class="table-wrap" id="postsTable">Loading...</div> |
| `; |
| loadPosts(); |
| } |
| |
| let currentBulletinTab = 'all'; |
| |
| function showBulletinTab(tab) { |
| currentBulletinTab = tab; |
| document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); |
| event.target.classList.add('active'); |
| loadPosts(); |
| } |
| |
| async function loadPosts() { |
| const search = document.getElementById('postSearch')?.value || ''; |
| const category = document.getElementById('postCategory')?.value || ''; |
| const status = currentBulletinTab === 'all' ? '' : currentBulletinTab; |
| try { |
| const res = await fetch(`${API_BASE}/admin/bulletin/posts?limit=50&search=${encodeURIComponent(search)}&category=${category}&status=${status}&sort_by=created_at&sort_order=desc`, {headers:{'X-Admin-Session':session}}); |
| const data = await res.json(); |
| const tbody = data.posts?.map(p => ` |
| <tr> |
| <td><strong>${p.title}</strong><br><small style="color:var(--fg2)">${p.summary?.substring(0,80)}...</small></td> |
| <td><span class="status status-${p.status}">${p.status}</span></td> |
| <td><span class="badge" style="background:var(--bg3);color:var(--fg2)">${p.category}</span></td> |
| <td>${p.pinned ? 'Pinned' : '-'}</td> |
| <td>${p.author_name}</td> |
| <td>${new Date(p.created_at).toLocaleDateString()}</td> |
| <td> |
| ${p.status === 'draft' || p.status === 'review' || p.status === 'scheduled' ? `<button class="btn btn-sm btn-primary" onclick="publishPost('${p.post_id}')">Publish</button>` : ''} |
| <button class="btn btn-sm btn-secondary" onclick="editPost('${p.post_id}')">Edit</button> |
| <button class="btn btn-sm btn-danger" onclick="deletePost('${p.post_id}')">Delete</button> |
| </td> |
| </tr> |
| `).join('') || '<tr><td colspan="7" style="text-align:center">No posts</td></tr>'; |
| document.getElementById('postsTable').innerHTML = `<table><thead><tr><th>Title</th><th>Status</th><th>Category</th><th>Pin</th><th>Author</th><th>Date</th><th>Actions</th></tr></thead><tbody>${tbody}</tbody></table>`; |
| } catch(e) { document.getElementById('postsTable').innerHTML = `<div class="error">Error: ${e.message}</div>`; } |
| } |
| |
| function showCreatePost() { |
| document.getElementById('modalTitle').textContent = 'New Bulletin Post'; |
| document.getElementById('modalBody').innerHTML = ` |
| <div class="form-group"><label>Title</label><input type="text" id="postTitle" maxlength="200"></div> |
| <div class="form-group"><label>Content</label><textarea id="postContent"></textarea></div> |
| <div class="form-row"> |
| <div class="form-group"><label>Category</label><select id="postCat"><option value="news">News</option><option value="alert">Alert</option><option value="update">Update</option><option value="promo">Promo</option><option value="system">System</option><option value="community">Community</option><option value="announcement">Announcement</option><option value="tutorial">Tutorial</option></select></div> |
| <div class="form-group"><label>Priority</label><select id="postPriority"><option value="low">Low</option><option value="normal" selected>Normal</option><option value="high">High</option><option value="critical">Critical</option></select></div> |
| </div> |
| <div class="form-row"> |
| <div class="form-group"><label>Status</label><select id="postStatus"><option value="draft" selected>Draft</option><option value="published">Published</option><option value="scheduled">Scheduled</option></select></div> |
| <div class="form-group"><label>Target Audience</label><select id="postAudience"><option value="all">All</option><option value="free">Free</option><option value="premium">Premium</option><option value="pro">Pro</option><option value="enterprise">Enterprise</option><option value="admins">Admins</option></select></div> |
| </div> |
| <div class="form-group"><label>Tags (comma separated)</label><input type="text" id="postTags"></div> |
| <div class="form-row"> |
| <div class="form-group"><label>Schedule At (optional)</label><input type="datetime-local" id="postSchedule"></div> |
| <div class="form-group"><label>Expires At (optional)</label><input type="datetime-local" id="postExpires"></div> |
| </div> |
| <div class="form-group"><label><input type="checkbox" id="postPinned"> Pin this post</label></div> |
| <div class="form-group"><label><input type="checkbox" id="postComments"> Allow comments</label></div> |
| `; |
| document.getElementById('modalFooter').innerHTML = ` |
| <button class="btn btn-secondary" onclick="closeModal()">Cancel</button> |
| <button class="btn btn-primary" onclick="createPost()">Create Post</button> |
| `; |
| document.getElementById('modalOverlay').classList.add('active'); |
| } |
| |
| async function createPost() { |
| const body = { |
| title: document.getElementById('postTitle').value, |
| content: document.getElementById('postContent').value, |
| category: document.getElementById('postCat').value, |
| priority: document.getElementById('postPriority').value, |
| target_audience: document.getElementById('postAudience').value, |
| status: document.getElementById('postStatus').value, |
| tags: document.getElementById('postTags').value.split(',').map(t => t.trim()).filter(t => t), |
| scheduled_at: document.getElementById('postSchedule').value || null, |
| expires_at: document.getElementById('postExpires').value || null, |
| pinned: document.getElementById('postPinned').checked, |
| allow_comments: document.getElementById('postComments').checked, |
| }; |
| try { |
| await fetch(`${API_BASE}/admin/bulletin/posts`, { |
| method: 'POST', headers: {'X-Admin-Session': session, 'Content-Type': 'application/json'}, |
| body: JSON.stringify(body) |
| }); |
| closeModal(); |
| showToast('Post created', 'success'); |
| loadPosts(); |
| } catch(e) { showToast(e.message, 'error'); } |
| } |
| |
| async function publishPost(postId) { |
| try { |
| await fetch(`${API_BASE}/admin/bulletin/posts/${postId}/publish`, { |
| method: 'POST', headers: {'X-Admin-Session': session} |
| }); |
| showToast('Post published', 'success'); |
| loadPosts(); |
| } catch(e) { showToast(e.message, 'error'); } |
| } |
| |
| async function deletePost(postId) { |
| if(!confirm('Archive this post?')) return; |
| try { |
| await fetch(`${API_BASE}/admin/bulletin/posts/${postId}`, { |
| method: 'DELETE', headers: {'X-Admin-Session': session} |
| }); |
| showToast('Post archived', 'success'); |
| loadPosts(); |
| } catch(e) { showToast(e.message, 'error'); } |
| } |
| |
| // ββ Announcements (simplified) ββββββββββββββββββββββββββββββββ |
| async function renderAnnouncements(el) { |
| el.innerHTML = ` |
| <div style="margin-bottom:16px"><button class="btn btn-primary" onclick="showCreateAnnouncement()">+ New Announcement</button></div> |
| <div class="table-wrap" id="announcementsTable">Loading...</div> |
| `; |
| try { |
| const res = await fetch(`${API_BASE}/admin/backend/content/announcements?active_only=false`, {headers:{'X-Admin-Session':session}}); |
| const data = await res.json(); |
| const tbody = data.announcements?.map(a => ` |
| <tr><td><strong>${a.title}</strong></td><td>${a.type}</td><td>${a.target_audience}</td> |
| <td>${new Date(a.created_at).toLocaleDateString()}</td><td>${a.active ? 'Active' : 'Inactive'}</td></tr> |
| `).join('') || '<tr><td colspan="5" style="text-align:center">No announcements</td></tr>'; |
| document.getElementById('announcementsTable').innerHTML = `<table><thead><tr><th>Title</th><th>Type</th><th>Audience</th><th>Created</th><th>Status</th></tr></thead><tbody>${tbody}</tbody></table>`; |
| } catch(e) {} |
| } |
| |
| function showCreateAnnouncement() { |
| document.getElementById('modalTitle').textContent = 'New Announcement'; |
| document.getElementById('modalBody').innerHTML = ` |
| <div class="form-group"><label>Title</label><input type="text" id="annTitle"></div> |
| <div class="form-group"><label>Content</label><textarea id="annContent"></textarea></div> |
| <div class="form-row"> |
| <div class="form-group"><label>Type</label><select id="annType"><option value="info">Info</option><option value="warning">Warning</option><option value="critical">Critical</option><option value="update">Update</option></select></div> |
| <div class="form-group"><label>Audience</label><select id="annAudience"><option value="all">All</option><option value="users">Users</option><option value="admins">Admins</option><option value="premium">Premium</option></select></div> |
| </div> |
| `; |
| document.getElementById('modalFooter').innerHTML = ` |
| <button class="btn btn-secondary" onclick="closeModal()">Cancel</button> |
| <button class="btn btn-primary" onclick="createAnnouncement()">Create</button> |
| `; |
| document.getElementById('modalOverlay').classList.add('active'); |
| } |
| |
| async function createAnnouncement() { |
| const body = { |
| title: document.getElementById('annTitle').value, |
| content: document.getElementById('annContent').value, |
| type: document.getElementById('annType').value, |
| target_audience: document.getElementById('annAudience').value, |
| }; |
| try { |
| await fetch(`${API_BASE}/admin/backend/content/announcements`, { |
| method: 'POST', headers: {'X-Admin-Session': session, 'Content-Type': 'application/json'}, |
| body: JSON.stringify(body) |
| }); |
| closeModal(); |
| showToast('Announcement created', 'success'); |
| navTo('announcements'); |
| } catch(e) { showToast(e.message, 'error'); } |
| } |
| |
| // ββ Financial ββββββββββββββββββββββββββββββββββββββββββββββββ |
| async function renderFinancial(el) { |
| el.innerHTML = ` |
| <div class="card-grid"> |
| <div class="stat-card"><h3>X402 Revenue (30d)</h3><div class="value">$0</div></div> |
| <div class="stat-card"><h3>Subscriptions</h3><div class="value">$0</div></div> |
| <div class="stat-card"><h3>API Usage</h3><div class="value">$0</div></div> |
| <div class="stat-card"><h3>Total Revenue</h3><div class="value">$0</div></div> |
| </div> |
| <div class="chart-container">Financial charts β Coming Soon</div> |
| `; |
| } |
| |
| // ββ API Keys ββββββββββββββββββββββββββββββββββββββββββββββββ |
| async function renderAPIKeys(el) { |
| el.innerHTML = ` |
| <div style="margin-bottom:16px"><button class="btn btn-primary" onclick="showCreateAPIKey()">+ New API Key</button></div> |
| <div class="table-wrap" id="apiKeysTable">Loading...</div> |
| `; |
| try { |
| const res = await fetch(`${API_BASE}/admin/backend/api-keys`, {headers:{'X-Admin-Session':session}}); |
| const data = await res.json(); |
| const tbody = data.api_keys?.map(k => ` |
| <tr><td>${k.name}</td><td><code>${k.key}</code></td><td>${k.scopes?.join(', ') || 'all'}</td> |
| <td>${k.active ? 'Active' : 'Revoked'}</td><td>${new Date(k.created_at).toLocaleDateString()}</td> |
| <td><button class="btn btn-sm btn-danger" onclick="revokeKey('${k.id}')">Revoke</button></td></tr> |
| `).join('') || '<tr><td colspan="6" style="text-align:center">No API keys</td></tr>'; |
| document.getElementById('apiKeysTable').innerHTML = `<table><thead><tr><th>Name</th><th>Key</th><th>Scopes</th><th>Status</th><th>Created</th><th>Actions</th></tr></thead><tbody>${tbody}</tbody></table>`; |
| } catch(e) {} |
| } |
| |
| function showCreateAPIKey() { |
| document.getElementById('modalTitle').textContent = 'New API Key'; |
| document.getElementById('modalBody').innerHTML = ` |
| <div class="form-group"><label>Name</label><input type="text" id="keyName"></div> |
| <div class="form-group"><label>Scopes (comma separated)</label><input type="text" id="keyScopes" placeholder="read, write, deploy"></div> |
| <div class="form-group"><label>Expires (days)</label><input type="number" id="keyExpires" value="30"></div> |
| `; |
| document.getElementById('modalFooter').innerHTML = ` |
| <button class="btn btn-secondary" onclick="closeModal()">Cancel</button> |
| <button class="btn btn-primary" onclick="createAPIKey()">Create</button> |
| `; |
| document.getElementById('modalOverlay').classList.add('active'); |
| } |
| |
| async function createAPIKey() { |
| const body = { |
| name: document.getElementById('keyName').value, |
| scopes: document.getElementById('keyScopes').value.split(',').map(s => s.trim()).filter(s => s), |
| expires_days: parseInt(document.getElementById('keyExpires').value) || 30, |
| }; |
| try { |
| const res = await fetch(`${API_BASE}/admin/backend/api-keys`, { |
| method: 'POST', headers: {'X-Admin-Session': session, 'Content-Type': 'application/json'}, |
| body: JSON.stringify(body) |
| }); |
| const data = await res.json(); |
| closeModal(); |
| if(data.api_key) { |
| document.getElementById('modalTitle').textContent = 'API Key Created'; |
| document.getElementById('modalBody').innerHTML = `<div style="background:var(--bg3);padding:16px;border-radius:8px;"><p style="color:var(--fg2);margin-bottom:8px;">Copy this key now β it won't be shown again:</p><code style="font-size:16px;word-break:break-all;">${data.api_key}</code></div>`; |
| document.getElementById('modalFooter').innerHTML = `<button class="btn btn-primary" onclick="closeModal();navTo('apikeys');">Done</button>`; |
| document.getElementById('modalOverlay').classList.add('active'); |
| } |
| } catch(e) { showToast(e.message, 'error'); } |
| } |
| |
| async function revokeKey(keyId) { |
| if(!confirm('Revoke this API key?')) return; |
| try { |
| await fetch(`${API_BASE}/admin/backend/api-keys/${keyId}/revoke`, { |
| method: 'POST', headers: {'X-Admin-Session': session} |
| }); |
| showToast('Key revoked', 'success'); |
| navTo('apikeys'); |
| } catch(e) { showToast(e.message, 'error'); } |
| } |
| |
| // ββ Webhooks βββββββββββββββββββββββββββββββββββββββββββββββββ |
| async function renderWebhooks(el) { |
| el.innerHTML = ` |
| <div style="margin-bottom:16px"><button class="btn btn-primary" onclick="showCreateWebhook()">+ New Webhook</button></div> |
| <div class="table-wrap" id="webhooksTable">Loading...</div> |
| `; |
| try { |
| const res = await fetch(`${API_BASE}/admin/backend/webhooks`, {headers:{'X-Admin-Session':session}}); |
| const data = await res.json(); |
| const tbody = data.webhooks?.map(w => ` |
| <tr><td>${w.url}</td><td>${w.events?.join(', ')}</td><td>${w.active ? 'Active' : 'Inactive'}</td> |
| <td>${new Date(w.created_at).toLocaleDateString()}</td></tr> |
| `).join('') || '<tr><td colspan="4" style="text-align:center">No webhooks</td></tr>'; |
| document.getElementById('webhooksTable').innerHTML = `<table><thead><tr><th>URL</th><th>Events</th><th>Status</th><th>Created</th></tr></thead><tbody>${tbody}</tbody></table>`; |
| } catch(e) {} |
| } |
| |
| function showCreateWebhook() { |
| document.getElementById('modalTitle').textContent = 'New Webhook'; |
| document.getElementById('modalBody').innerHTML = ` |
| <div class="form-group"><label>URL</label><input type="url" id="whUrl"></div> |
| <div class="form-group"><label>Events (comma separated)</label><input type="text" id="whEvents" placeholder="token.deploy, user.signup, payment.received"></div> |
| <div class="form-group"><label>Secret</label><input type="text" id="whSecret"></div> |
| `; |
| document.getElementById('modalFooter').innerHTML = ` |
| <button class="btn btn-secondary" onclick="closeModal()">Cancel</button> |
| <button class="btn btn-primary" onclick="createWebhook()">Create</button> |
| `; |
| document.getElementById('modalOverlay').classList.add('active'); |
| } |
| |
| async function createWebhook() { |
| const body = { |
| url: document.getElementById('whUrl').value, |
| events: document.getElementById('whEvents').value.split(',').map(s => s.trim()).filter(s => s), |
| secret: document.getElementById('whSecret').value, |
| }; |
| try { |
| await fetch(`${API_BASE}/admin/backend/webhooks`, { |
| method: 'POST', headers: {'X-Admin-Session': session, 'Content-Type': 'application/json'}, |
| body: JSON.stringify(body) |
| }); |
| closeModal(); |
| showToast('Webhook created', 'success'); |
| navTo('webhooks'); |
| } catch(e) { showToast(e.message, 'error'); } |
| } |
| |
| // ββ Config βββββββββββββββββββββββββββββββββββββββββββββββββββ |
| async function renderConfig(el) { |
| el.innerHTML = ` |
| <div class="table-wrap"> |
| <table><thead><tr><th>Key</th><th>Value</th><th>Category</th><th>Actions</th></tr></thead> |
| <tbody> |
| <tr><td>ENVIRONMENT</td><td>production</td><td>general</td><td><button class="btn btn-sm btn-secondary">Edit</button></td></tr> |
| <tr><td>BACKEND_PORT</td><td>8000</td><td>general</td><td><button class="btn btn-sm btn-secondary">Edit</button></td></tr> |
| <tr><td>REDIS_HOST</td><td>localhost</td><td>database</td><td><button class="btn btn-sm btn-secondary">Edit</button></td></tr> |
| <tr><td>X402_ENABLED</td><td>true</td><td>payments</td><td><button class="btn btn-sm btn-secondary">Edit</button></td></tr> |
| </tbody> |
| </table> |
| </div> |
| `; |
| } |
| |
| // ββ Utilities ββββββββββββββββββββββββββββββββββββββββββββββββ |
| function closeModal() { |
| document.getElementById('modalOverlay').classList.remove('active'); |
| } |
| |
| function showToast(msg, type='success') { |
| const t = document.getElementById('toast'); |
| t.textContent = msg; |
| t.className = `toast toast-${type} show`; |
| setTimeout(() => t.classList.remove('show'), 3000); |
| } |
| |
| function updateTime() { |
| document.getElementById('currentTime').textContent = new Date().toLocaleTimeString(); |
| } |
| setInterval(updateTime, 1000); |
| updateTime(); |
| |
| // Init |
| checkSession(); |
| </script> |
| </body> |
| </html> |
|
|