rmi-backend/static/admin.html

946 lines
50 KiB
HTML

<!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 */
.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; }
/* Layout */
.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; }
/* Cards */
.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); }
/* Tables */
.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); }
/* Buttons */
.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; }
/* Forms */
.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 */
.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 */
.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); }
/* Modals */
.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; }
/* Charts placeholder */
.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 */
.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); }
/* Responsive */
@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>
<!-- LOGIN SCREEN -->
<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>
<!-- APP -->
<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">
<!-- Content injected by JS -->
</div>
</main>
</div>
<!-- MODALS -->
<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()">&times;</button>
</div>
<div class="modal-body" id="modalBody"></div>
<div class="modal-footer" id="modalFooter"></div>
</div>
</div>
<!-- TOAST -->
<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>