merge: chore/cleanup-remove-bloat-and-secrets into main

This commit is contained in:
Crypto Rug Munch 2026-07-02 01:24:22 +07:00
commit bde2f3a97d
1173 changed files with 437609 additions and 0 deletions

946
static/admin.html Normal file
View file

@ -0,0 +1,946 @@
<!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>

110
static/earnings.html Normal file
View file

@ -0,0 +1,110 @@
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RMI Earnings Dashboard</title>
<style>
:root{--bg:#0a0a0f;--fg:#e0e0e0;--accent:#6c5ce7;--green:#27ae60;--card:#14141f;--border:#2a2a3a;--muted:#888}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);color:var(--fg);font-family:system-ui,sans-serif}
header{background:var(--card);border-bottom:1px solid var(--border);padding:16px 24px;display:flex;align-items:center;gap:16px}
.logo{font-size:20px;font-weight:700;color:var(--accent)}
.badge{background:var(--accent);color:#fff;padding:2px 8px;border-radius:12px;font-size:11px}
main{max-width:1100px;margin:0 auto;padding:24px}
h1{font-size:24px;margin-bottom:8px}
.sub{color:var(--muted);margin-bottom:24px;font-size:14px}
.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:16px;margin-bottom:24px}
.card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:20px}
.card-label{font-size:12px;color:var(--muted);text-transform:uppercase;letter-spacing:1px;margin-bottom:8px}
.card-value{font-size:28px;font-weight:700}
.card-value.green{color:var(--green)}
.card-sub{font-size:12px;color:var(--muted);margin-top:4px}
.table-card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:20px;margin-bottom:16px}
.table-card h3{font-size:14px;color:var(--muted);margin-bottom:12px;text-transform:uppercase;letter-spacing:1px}
table{width:100%;border-collapse:collapse;font-size:13px}
th{text-align:left;color:var(--muted);font-weight:500;padding:8px 12px;border-bottom:1px solid var(--border);font-size:11px;text-transform:uppercase}
td{padding:8px 12px;border-bottom:1px solid rgba(255,255,255,0.03)}
tr:hover{background:rgba(255,255,255,0.02)}
.addr{font-family:monospace;font-size:11px;color:var(--accent)}
.spinner{display:inline-block;width:14px;height:14px;border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .6s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
.refresh{background:var(--accent);color:#fff;border:none;padding:8px 16px;border-radius:6px;cursor:pointer;font-size:13px}
.refresh:hover{opacity:.9}
.bar{height:6px;background:var(--accent);border-radius:3px;margin-top:4px}
.bar-bg{background:#1a1a2e;border-radius:3px}
.row2{display:grid;grid-template-columns:1fr 1fr;gap:16px}
</style>
</head>
<body>
<header><span class="logo">RMI</span><span>Earnings Dashboard</span><span class="badge">Revenue</span></header>
<main>
<h1>Revenue Dashboard</h1>
<p class="sub">x402 payment tracking across all chains and facilitators. <button class="refresh" onclick="load()">Refresh</button></p>
<div class="cards" id="summary"></div>
<div class="row2">
<div class="table-card"><h3>Wallets</h3><div id="wallets"></div></div>
<div class="table-card"><h3>By Chain</h3><div id="byChain"></div></div>
</div>
<div class="table-card"><h3>Top Tools</h3><div id="byTool"></div></div>
<div class="table-card"><h3>Recent Activity</h3><div id="recent"></div></div>
</main>
<script>
async function load(){
document.getElementById('summary').innerHTML='<div class="card"><span class="spinner"></span> Loading...</div>';
try{
const r=await fetch('/mcp/earnings');
const d=await r.json();
const w=d.wallet_balances||{};
const s=d.revenue_by_source||{};
// Summary cards
document.getElementById('summary').innerHTML=`
<div class="card"><div class="card-label">Total Tracked</div><div class="card-value green">$${(w.total_usd||0).toFixed(2)}</div><div class="card-sub">Across all payment wallets</div></div>
<div class="card"><div class="card-label">Revenue Events</div><div class="card-value">${s.events||0}</div><div class="card-sub">Tracked tool calls</div></div>
<div class="card"><div class="card-label">Revenue Total</div><div class="card-value green">$${(s.total||0).toFixed(4)}</div><div class="card-sub">From tracked events</div></div>
<div class="card"><div class="card-label">Payment Wallets</div><div class="card-value">${Object.keys(w).filter(k=>k!=='total_usd'&&k!=='updated').length}</div><div class="card-sub">${Object.keys(w).filter(k=>k!=='total_usd'&&k!=='updated').join(', ')}</div></div>`;
// Wallets table
let wh='<table><tr><th>Chain</th><th>Address</th><th>Balance</th><th>USD</th><th>TXs</th></tr>';
for(const [k,v] of Object.entries(w)){
if(k==='total_usd'||k==='updated') continue;
wh+=`<tr><td>${v.chain}</td><td class="addr">${v.address}</td><td>${v.balance?.toFixed(4)||0}</td><td>$${v.balance_usd?.toFixed(2)||0}</td><td>${v.transactions||0}</td></tr>`;
}
wh+='</table>';
document.getElementById('wallets').innerHTML=wh;
// By chain
let ch='<table><tr><th>Chain</th><th>Revenue</th><th>Share</th></tr>';
for(const [k,v] of Object.entries(s.by_chain||{})){
const pct=s.total?((v/s.total)*100).toFixed(1):'0';
ch+=`<tr><td>${k}</td><td>$${v.toFixed(4)}</td><td><div class="bar-bg"><div class="bar" style="width:${pct}%"></div></div>${pct}%</td></tr>`;
}
ch+='</table>';
document.getElementById('byChain').innerHTML=ch;
// By tool
let tt='<table><tr><th>Tool</th><th>Revenue</th><th>Share</th></tr>';
for(const [k,v] of Object.entries(s.by_tool||{})){
const pct=s.total?((v/s.total)*100).toFixed(1):'0';
tt+=`<tr><td>${k}</td><td>$${v.toFixed(4)}</td><td><div class="bar-bg"><div class="bar" style="width:${pct}%"></div></div>${pct}%</td></tr>`;
}
tt+='</table>';
document.getElementById('byTool').innerHTML=tt;
// Recent
let rec='<table><tr><th>Time</th><th>Tool</th><th>Amount</th><th>Chain</th></tr>';
for(const e of (d.earnings_history||[]).reverse()){
rec+=`<tr><td>${e.timestamp?.slice(11,19)||''}</td><td>${e.tool}</td><td>$${e.amount_usd?.toFixed(4)}</td><td>${e.chain}</td></tr>`;
}
rec+='</table>';
document.getElementById('recent').innerHTML=rec;
}catch(e){
document.getElementById('summary').innerHTML=`<div class="card"><div class="card-label">Error</div><div>${e.message}</div></div>`;
}
}
load();
setInterval(load, 60000);
</script>
</body>
</html>

152
static/investigate.html Normal file
View file

@ -0,0 +1,152 @@
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RMI Investigative Framework</title>
<style>
:root{--bg:#0a0a0f;--fg:#e0e0e0;--accent:#6c5ce7;--danger:#e74c3c;--card:#14141f;--border:#2a2a3a;--muted:#888;--success:#27ae60}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);color:var(--fg);font-family:system-ui,sans-serif}
header{background:var(--card);border-bottom:1px solid var(--border);padding:16px 24px;display:flex;align-items:center;gap:16px}
.logo{font-size:20px;font-weight:700;color:var(--accent)}
.badge{background:var(--accent);color:#fff;padding:2px 8px;border-radius:12px;font-size:11px}
main{max-width:960px;margin:0 auto;padding:32px 24px}
h1{font-size:28px;margin-bottom:8px}
.sub{color:var(--muted);margin-bottom:24px}
.card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:24px;margin-bottom:24px}
label{display:block;font-size:13px;color:var(--muted);margin-bottom:6px;font-weight:500}
input,select{width:100%;padding:12px;background:#1a1a2e;border:1px solid var(--border);border-radius:8px;color:var(--fg);font-size:15px}
.row{display:flex;gap:12px}.row>*{flex:1}
button{padding:12px 28px;background:var(--accent);color:#fff;border:none;border-radius:8px;font-size:15px;font-weight:600;cursor:pointer}
button:hover{opacity:.9}button:disabled{opacity:.4;cursor:not-allowed}
.result{animation:fadeIn .3s ease}
@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
.tag{display:inline-block;padding:4px 10px;border-radius:6px;font-size:12px;font-weight:600;margin:2px}
.tag-cex{background:#27ae60;color:#fff}.tag-dex{background:#2980b9;color:#fff}
.tag-mixer{background:#e74c3c;color:#fff}.tag-bridge{background:#8e44ad;color:#fff}
.tag-eoa{background:#7f8c8d;color:#fff}.tag-contract{background:#d35400;color:#fff}
.tag-solana{background:#9945FF;color:#fff}
.hop{padding:12px;background:#1a1a2e;border-radius:8px;margin:8px 0;border-left:3px solid var(--accent)}
.addr{font-family:monospace;font-size:13px;color:var(--accent);word-break:break-all}
.spinner{display:inline-block;width:16px;height:16px;border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .6s linear infinite;margin-right:8px}
@keyframes spin{to{transform:rotate(360deg)}}
.stats{display:flex;gap:16px;flex-wrap:wrap}
.stat{background:#1a1a2e;padding:12px 16px;border-radius:8px}
.stat-value{font-size:20px;font-weight:700}.stat-label{font-size:11px;color:var(--muted)}
.source-badge{font-size:10px;background:#2a2a3a;padding:2px 6px;border-radius:4px;margin-left:8px;color:var(--muted)}
.fallback-notice{background:#1a1a0e;border:1px solid #f39c12;padding:12px;border-radius:8px;font-size:13px;color:#f39c12;margin-top:12px}
</style>
</head>
<body>
<header><span class="logo">RMI</span><span>Investigative Framework</span><span class="badge">Solana + EVM</span></header>
<main>
<h1>Trace Crypto Funding Sources</h1>
<p class="sub">Trace wallets across Solana and 9 EVM chains. Multi-provider fallback ensures you never run out of data.</p>
<div class="card">
<div class="row">
<div><label>Wallet Address</label><input type="text" id="address" placeholder="0x... or Solana address" value="vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg"></div>
<div style="max-width:220px">
<label>Chain</label>
<select id="chain" onchange="updatePlaceholder()">
<option value="solana">Solana</option>
<option value="ethereum">Ethereum</option><option value="bsc">BSC</option>
<option value="polygon">Polygon</option><option value="base">Base</option>
<option value="arbitrum">Arbitrum</option><option value="optimism">Optimism</option>
<option value="avalanche">Avalanche</option><option value="fantom">Fantom</option><option value="gnosis">Gnosis</option>
</select>
</div>
</div>
<div style="margin-top:16px;display:flex;gap:12px">
<button onclick="trace()" id="traceBtn">Trace Funding</button>
<button onclick="scan()" id="scanBtn" style="background:#2a2a3a">Full Investigation</button>
</div>
</div>
<div id="result"></div><div id="error"></div>
</main>
<script>
function updatePlaceholder(){
const chain=document.getElementById('chain').value;
const inp=document.getElementById('address');
if(chain==='solana'){inp.placeholder='Solana address...';if(!inp.value.startsWith('0x'))inp.value=''}
else{inp.placeholder='0x...'}
}
async function api(path,body){
const btns=document.querySelectorAll('button');btns.forEach(b=>b.disabled=true);
document.getElementById('error').innerHTML='';
document.getElementById('result').innerHTML='<p><span class="spinner"></span> Querying all providers...</p>';
try{
const r=await fetch(path,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
const d=await r.json();
if(!r.ok)throw new Error(d.detail||'Request failed');
return d;
}catch(e){
document.getElementById('error').innerHTML=`<div style="background:#2c1010;border:1px solid var(--danger);border-radius:8px;padding:16px;color:#e74c3c">${e.message}</div>`;
document.getElementById('result').innerHTML='';
}finally{btns.forEach(b=>b.disabled=false)}
}
function tagClass(t){const m={cex:'tag-cex',dex:'tag-dex',mixer:'tag-mixer',bridge:'tag-bridge',eoa:'tag-eoa',contract:'tag-contract'};return m[t]||''}
async function trace(){
const chain=document.getElementById('chain').value;
const data=await api('/api/v1/investigate/trace',{address:document.getElementById('address').value.trim(),chain});
if(!data)return;
if(chain==='solana'){
document.getElementById('result').innerHTML=`
<div class="card result"><h3>Solana Wallet Trace</h3>
<div class="stats" style="margin-top:12px">
<div class="stat"><div class="stat-value">${data.first_tx?data.first_tx.slice(0,10)+'...':'N/A'}</div><div class="stat-label">First TX</div></div>
<div class="stat"><div class="stat-value">${(data.balance_change_sol||0).toFixed(4)}</div><div class="stat-label">SOL Change</div></div>
<div class="stat"><div class="stat-value">${data.total_value_usd?('$'+data.total_value_usd.toFixed(2)):'N/A'}</div><div class="stat-label">Portfolio Value</div></div>
</div>
<div style="margin-top:12px">
<span class="tag tag-solana">Solana</span>
<span class="source-badge">via ${data.source||'?'}</span>
${data.label?`<span class="tag" style="background:#2a2a3a">${data.label}</span>`:''}
${data.is_cex?'<span class="tag tag-cex">CEX</span>':''}
</div>
</div>`;
} else {
document.getElementById('result').innerHTML=`
<div class="card result"><h3>EVM Funding Trace</h3>
<div class="stats" style="margin-top:12px">
<div class="stat"><div class="stat-value">${(data.source_type||'?').toUpperCase()}</div><div class="stat-label">Source Type</div></div>
<div class="stat"><div class="stat-value">${data.confidence||0}%</div><div class="stat-label">Confidence</div></div>
<div class="stat"><div class="stat-value">${(data.funding_amount_eth||0).toFixed(6)}</div><div class="stat-label">ETH</div></div>
<div class="stat"><div class="stat-value">${(data.hops||[]).length}</div><div class="stat-label">Hops</div></div>
</div>
${data.source_address?`<div style="margin-top:12px"><label>Source</label><div class="addr">${data.source_address}</div><span class="tag ${tagClass(data.source_type)}">${data.source_type}</span></div>`:''}
${(data.hops||[]).map((h,i)=>`<div class="hop"><strong>Hop ${h.depth}</strong><span class="addr">${h.address}</span> <span style="color:var(--muted)">${(h.amount_eth||0).toFixed(6)} ETH</span></div>`).join('')}
${data.errors?.length?`<div class="fallback-notice">⚠ Fallback used — some providers unavailable</div>`:''}
</div>`;
}
}
async function scan(){
const chain=document.getElementById('chain').value;
const data=await api('/api/v1/investigate/scan',{address:document.getElementById('address').value.trim(),chain});
if(!data)return;
const tr=data.trace||{};
const risk=data.risk||{};
const labels=data.labels||[];
document.getElementById('result').innerHTML=`
<div class="card result"><h3>Full Investigation — ${data.chain.toUpperCase()}</h3>
<div class="stats" style="margin-top:12px">
<div class="stat"><div class="stat-value">${(tr.source_type||tr.source||'?').toUpperCase()}</div><div class="stat-label">Source</div></div>
<div class="stat"><div class="stat-value">${tr.confidence||0}%</div><div class="stat-label">Confidence</div></div>
${risk?`<div class="stat"><div class="stat-value" style="color:${risk.is_honeypot?'var(--danger)':'var(--success)'}">${risk.is_honeypot?'DANGER':'SAFE'}</div><div class="stat-label">Risk</div></div>`:''}
<div class="stat"><div class="stat-value">${labels.length}</div><div class="stat-label">Labels</div></div>
</div>
${risk?.risks?.length?`<div style="margin-top:8px">${risk.risks.map(r=>`<span class="tag" style="background:#2c1010;color:var(--danger)">${r}</span>`).join(' ')}</div>`:''}
${labels.map(l=>`<div class="hop"><span class="addr">${(l.address||'').slice(0,16)}...</span> → ${l.label}</div>`).join('')}
<div class="fallback-notice">🔍 Data sourced from ${tr.source||'multiple providers'} with automatic fallback</div>
</div>`;
}
</script>
</body>
</html>

267
static/news.html Normal file
View file

@ -0,0 +1,267 @@
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RMI News Network — Live Crypto Intelligence</title>
<style>
:root{--bg:#06060f;--fg:#e8e8f0;--accent:#7c6ff7;--green:#2ecc71;--red:#e74c3c;--card:#0f0f1d;--border:#1e1e35;--muted:#888;--yellow:#f1c40f;--orange:#e67e22;--surface:#121224;--glow:rgba(124,111,247,.15);--cyan:#06b6d4}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);color:var(--fg);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif}
header{background:var(--card);border-bottom:1px solid var(--border);padding:10px 24px;display:flex;align-items:center;gap:12px;position:sticky;top:0;z-index:100}
.logo{font-size:20px;font-weight:800}.logo span:first-child{color:var(--accent)}.logo span:last-child{color:var(--fg)}
nav{display:flex;gap:4px}
nav a{color:var(--muted);text-decoration:none;padding:6px 14px;border-radius:6px;font-size:12px;font-weight:500}
nav a:hover,nav a.active{color:var(--fg);background:rgba(255,255,255,.06)}
nav a.active{color:var(--accent)}
.market-ticker{display:flex;gap:16px;margin-left:auto;font-size:12px}
.ticker-item{display:flex;align-items:center;gap:4px}
.ticker-price{font-weight:700;font-family:monospace}
.ticker-change{font-size:11px}
main{max-width:1500px;margin:0 auto;padding:20px;display:grid;grid-template-columns:240px 1fr 340px;gap:20px}
.sidebar{position:sticky;top:70px;height:fit-content;max-height:calc(100vh-90px);overflow-y:auto}
.section-title{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:2px;margin:16px 0 8px;font-weight:700}
.filter-btn{display:block;width:100%;text-align:left;padding:7px 12px;border-radius:6px;color:var(--muted);font-size:12px;cursor:pointer;border:none;background:none;margin:1px 0}
.filter-btn:hover{background:var(--surface);color:var(--fg)}
.filter-btn.active{background:var(--accent);color:#fff;font-weight:600}
.panel{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:14px;margin-bottom:12px}
.panel h4{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:1px;margin-bottom:10px}
.fear-greed-display{text-align:center;padding:12px 0}
.fg-value{font-size:42px;font-weight:800;line-height:1}
.fg-label{font-size:13px;margin-top:4px}
.fg-bar{height:8px;border-radius:4px;margin-top:6px;background:linear-gradient(90deg,#ff0044,#ff8800,#ffd700,#88ff00,#00ff88)}
.fg-marker{width:4px;height:14px;border-radius:2px;background:#fff;position:relative;top:-11px}
.price-row{display:flex;justify-content:space-between;padding:5px 0;font-size:12px;border-bottom:1px solid rgba(255,255,255,.03)}
.price-row:last-child{border:none}
.price-name{color:var(--muted)}
.price-value{font-weight:600;font-family:monospace}
.article-card{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:16px;margin-bottom:8px;transition:.15s;cursor:pointer}
.article-card:hover{border-color:var(--accent);box-shadow:0 4px 20px var(--glow)}
.article-source{font-size:10px;color:var(--accent);font-weight:600;margin-bottom:4px}
.article-title{font-size:15px;font-weight:700;line-height:1.4;margin-bottom:6px}
.article-title a{color:var(--fg);text-decoration:none}
.article-meta{font-size:10px;color:var(--muted)}
.article-card.high-impact{border-left:3px solid var(--red)}
.article-card.medium-impact{border-left:3px solid var(--orange)}
.prediction-item{padding:7px 0;border-bottom:1px solid rgba(255,255,255,.03);font-size:11px}
.prediction-item:last-child{border:none}
.prediction-title{color:var(--fg);margin-bottom:2px}
.prediction-volume{color:var(--muted);font-size:10px}
.loading{text-align:center;padding:40px;color:var(--muted)}
.spinner{display:inline-block;width:20px;height:20px;border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .6s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
.brief-bar{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:10px 14px;margin-bottom:16px;font-size:12px;color:var(--muted)}
.brief-bar b{color:var(--fg)}
.source-badge{display:inline-block;padding:2px 6px;border-radius:3px;font-size:9px;background:rgba(6,182,212,.1);color:var(--cyan);margin-right:4px}
</style>
</head>
<body>
<header>
<div class="logo"><span>RMI</span><span> News</span></div>
<nav>
<a href="/rugcharts">RugCharts</a>
<a href="/rugmaps">RugMaps</a>
<a href="/news" class="active">News</a>
</nav>
<div class="market-ticker" id="tickerBar">
<div class="spinner"></div>
</div>
</header>
<main>
<!-- Left Sidebar -->
<aside class="sidebar">
<div class="panel">
<h4>😨 Fear & Greed</h4>
<div id="fearGreedWidget"><div class="spinner"></div></div>
</div>
<div class="panel">
<h4>💰 Live Prices</h4>
<div id="pricesWidget"><div class="spinner"></div></div>
</div>
<div class="panel">
<h4>🔥 Trending</h4>
<div id="trendingWidget"><div class="spinner"></div></div>
</div>
<div class="section-title">Filters</div>
<button class="filter-btn active" onclick="filterNews('all')">All News</button>
<button class="filter-btn" onclick="filterNews('bitcoin')">Bitcoin</button>
<button class="filter-btn" onclick="filterNews('ethereum')">Ethereum</button>
<button class="filter-btn" onclick="filterNews('defi')">DeFi</button>
<button class="filter-btn" onclick="filterNews('regulation')">Regulation</button>
<button class="filter-btn" onclick="filterNews('security')">Security</button>
</aside>
<!-- Main Feed -->
<div>
<div class="brief-bar" id="briefBar"><div class="spinner"></div> Loading market data...</div>
<div style="display:flex;gap:6px;margin-bottom:12px;flex-wrap:wrap" id="tabBar">
<button class="sort-btn active" onclick="switchTab('all')">📰 All News</button>
<button class="sort-btn" onclick="switchTab('ct')">𝕏 CT Rundown</button>
<button class="sort-btn" onclick="switchTab('weekly')">⭐ Weekly Best</button>
<button class="sort-btn" onclick="switchTab('academic')">📚 Academic</button>
<button class="sort-btn" onclick="switchTab('social')">💬 Social</button>
</div>
<div id="articles"><div class="loading"><div class="spinner"></div> Loading news...</div></div>
<div id="ctRundown" style="display:none"></div>
<div id="weeklyContent" style="display:none"></div>
<div id="academicContent" style="display:none"></div>
<div id="socialContent" style="display:none"></div>
</div>
<!-- Right Sidebar -->
<aside class="sidebar">
<div class="panel">
<h4>🎲 Prediction Markets</h4>
<div id="polyWidget"><div class="spinner"></div></div>
</div>
<div class="panel">
<h4>📡 Data Sources</h4>
<div style="font-size:11px;color:var(--muted);line-height:1.6">
<span class="source-badge">CG</span> CoinGecko<br>
<span class="source-badge">FG</span> Fear & Greed<br>
<span class="source-badge">PM</span> Polymarket<br>
<span class="source-badge">RSS</span> 200+ RSS Feeds<br>
<span class="source-badge">DB</span> DataBus (56 chains)
</div>
</div>
</aside>
</main>
<script>
const DB = '/api/v1/databus';
let allArticles = [];
let currentFilter = 'all';
async function loadAll() {
// Load market data and news in parallel
const [prices, fearGreed, trending, polymarket, brief, newsData] = await Promise.all([
fetch(DB+'/market/prices').then(r=>r.json()).catch(()=>null),
fetch(DB+'/market/fear-greed').then(r=>r.json()).catch(()=>null),
fetch(DB+'/market/trending').then(r=>r.json()).catch(()=>null),
fetch(DB+'/market/prediction-markets').then(r=>r.json()).catch(()=>null),
fetch(DB+'/market/brief').then(r=>r.json()).catch(()=>null),
fetch(DB+'/news/full?limit=30').then(r=>r.json()).catch(()=>null),
]);
// Ticker bar
if (prices?.prices) {
let html = '';
for (const [coin, data] of Object.entries(prices.prices)) {
const change = data.usd_24h_change || 0;
const color = change < -3 ? 'var(--red)' : change < 0 ? 'var(--orange)' : 'var(--green)';
html += `<div class="ticker-item">
<span style="color:var(--muted)">${coin.toUpperCase().slice(0,4)}</span>
<span class="ticker-price">$${(data.usd||0).toLocaleString()}</span>
<span class="ticker-change" style="color:${color}">${change>=0?'+':''}${change.toFixed(1)}%</span>
</div>`;
}
document.getElementById('tickerBar').innerHTML = html;
}
// Fear & Greed
if (fearGreed) {
const v = fearGreed.value || 50;
document.getElementById('fearGreedWidget').innerHTML = `
<div class="fear-greed-display">
<div class="fg-value" style="color:${fearGreed.color}">${v}</div>
<div class="fg-label">${fearGreed.classification}</div>
<div class="fg-bar"><div class="fg-marker" style="left:${v}%"></div></div>
</div>`;
}
// Prices
if (prices?.prices) {
let html = '';
for (const [coin, data] of Object.entries(prices.prices)) {
const change = data.usd_24h_change || 0;
const color = change < 0 ? 'var(--red)' : 'var(--green)';
html += `<div class="price-row">
<span class="price-name">${coin.charAt(0).toUpperCase()+coin.slice(1)}</span>
<span class="price-value">$${(data.usd||0).toLocaleString()} <span style="color:${color};font-size:10px">${change>=0?'+':''}${change.toFixed(1)}%</span></span>
</div>`;
}
document.getElementById('pricesWidget').innerHTML = html;
}
// Trending
if (trending?.trending) {
document.getElementById('trendingWidget').innerHTML = trending.trending.slice(0,8).map(t =>
`<div class="price-row">
<span class="price-name">${t.name}</span>
<span style="font-size:10px;color:var(--muted)">${t.symbol} #${t.market_cap_rank||'?'}</span>
</div>`
).join('');
}
// Polymarket
if (polymarket?.markets?.length) {
document.getElementById('polyWidget').innerHTML = polymarket.markets.map(m =>
`<div class="prediction-item">
<div class="prediction-title">${m.title.slice(0,80)}</div>
<div class="prediction-volume">Vol: $${(m.volume||0).toLocaleString()}</div>
</div>`
).join('');
}
// Brief bar
if (brief?.brief || newsData?.market_brief?.brief) {
const b = brief?.brief || newsData?.market_brief?.brief || '';
document.getElementById('briefBar').innerHTML = `<b>Market:</b> ${b}`;
}
// News articles
const newsContent = newsData?.headlines;
if (newsContent?.articles) {
allArticles = newsContent.articles;
} else {
// Fallback to existing news endpoint
try {
const fallback = await fetch('/news/daily-data').then(r=>r.json());
allArticles = fallback?.articles || [];
} catch(e) { allArticles = []; }
}
renderArticles();
}
function filterNews(cat) {
currentFilter = cat;
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
event.target.classList.add('active');
renderArticles();
}
function renderArticles() {
let filtered = allArticles;
if (currentFilter !== 'all') {
filtered = allArticles.filter(a => {
const text = (a.title + ' ' + (a.source||'') + ' ' + (a.category||'')).toLowerCase();
return text.includes(currentFilter.toLowerCase());
});
}
if (!filtered.length) {
document.getElementById('articles').innerHTML = '<div class="loading">No articles found. Try a different filter.</div>';
return;
}
document.getElementById('articles').innerHTML = filtered.map(a => {
const impact = a.impact || (a.category === 'security' ? 'high' : 'medium');
return `<div class="article-card ${impact}-impact">
<div class="article-source">${a.source||'Unknown'} · ${a.category||'General'} · ${a.published||''}</div>
<div class="article-title"><a href="${a.url||'#'}" target="_blank">${a.title||'Untitled'}</a></div>
<div class="article-meta">${a.summary||a.description||''.slice(0,150)}</div>
</div>`;
}).join('');
}
loadAll();
// Refresh every 2 minutes
setInterval(loadAll, 120000);
</script>
</body>
</html>

433
static/rugcharts.html Normal file
View file

@ -0,0 +1,433 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RugCharts — Token Intelligence</title>
<style>
:root {
--bg: #0a0a0f; --panel: #12121a; --border: #1e1e2e;
--text: #e0e0e0; --dim: #8888aa; --accent: #7c3aed;
--green: #00ff88; --yellow: #ffd700; --orange: #ff8800; --red: #ff0044;
--safe: #00ff88; --low: #88ff00; --medium: #ffd700; --high: #ff8800; --critical: #ff0044;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: var(--bg); color: var(--text); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; min-height: 100vh; }
header { background: var(--panel); border-bottom: 1px solid var(--border); padding: 16px 24px; display: flex; align-items: center; gap: 16px; position: sticky; top: 0; z-index: 100; }
.logo { font-size: 22px; font-weight: 800; letter-spacing: -0.5px; }
.logo span:first-child { color: var(--accent); } .logo span:last-child { color: var(--text); }
.search-bar { flex: 1; max-width: 600px; display: flex; gap: 8px; }
.search-bar input { flex: 1; background: var(--bg); border: 1px solid var(--border); border-radius: 8px; padding: 10px 16px; color: var(--text); font-size: 14px; outline: none; }
.search-bar input:focus { border-color: var(--accent); }
.search-bar select { background: var(--bg); border: 1px solid var(--border); border-radius: 8px; padding: 10px 12px; color: var(--text); font-size: 14px; }
.search-bar button { background: var(--accent); color: white; border: none; border-radius: 8px; padding: 10px 24px; font-size: 14px; font-weight: 600; cursor: pointer; }
.search-bar button:hover { opacity: 0.9; }
.stats-bar { background: var(--panel); border-bottom: 1px solid var(--border); padding: 12px 24px; display: flex; gap: 24px; font-size: 13px; color: var(--dim); }
.stats-bar b { color: var(--text); }
.container { max-width: 1400px; margin: 0 auto; padding: 24px; }
.verdict { background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 24px; margin-bottom: 24px; display: none; }
.verdict.active { display: block; }
.verdict-header { display: flex; align-items: center; gap: 16px; margin-bottom: 12px; }
.risk-badge { padding: 6px 16px; border-radius: 20px; font-weight: 700; font-size: 14px; color: #000; }
.risk-badge.SAFE { background: var(--safe); }
.risk-badge.LOW { background: var(--low); }
.risk-badge.MEDIUM { background: var(--medium); }
.risk-badge.HIGH { background: var(--orange); }
.risk-badge.CRITICAL,.risk-badge.DANGER { background: var(--red); color: #fff; }
.risk-score { font-size: 48px; font-weight: 800; line-height: 1; }
.verdict-text { font-size: 16px; color: var(--dim); margin-top: 4px; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(420px, 1fr)); gap: 20px; }
.panel { background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 20px; }
.panel-title { font-size: 14px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; color: var(--dim); margin-bottom: 16px; display: flex; align-items: center; gap: 8px; }
.panel-title .icon { font-size: 18px; }
.metric-row { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid rgba(255,255,255,0.04); font-size: 13px; }
.metric-row:last-child { border-bottom: none; }
.metric-label { color: var(--dim); }
.metric-value { font-weight: 600; }
.metric-value.green { color: var(--green); }
.metric-value.yellow { color: var(--yellow); }
.metric-value.orange { color: var(--orange); }
.metric-value.red { color: var(--red); }
.tag { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; margin: 2px; }
.tag.pass { background: rgba(0,255,136,0.15); color: var(--green); }
.tag.fail { background: rgba(255,0,68,0.15); color: var(--red); }
.tag.warn { background: rgba(255,136,0,0.15); color: var(--orange); }
.entity-badge { background: rgba(124,58,237,0.15); color: var(--accent); padding: 4px 12px; border-radius: 6px; font-size: 13px; font-weight: 600; }
.loading { text-align: center; padding: 40px; color: var(--dim); }
.spinner { display: inline-block; width: 24px; height: 24px; border: 2px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.6s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
.empty { color: var(--dim); font-size: 13px; padding: 20px; text-align: center; }
.progress-bar { height: 6px; background: var(--border); border-radius: 3px; overflow: hidden; margin-top: 4px; }
.progress-fill { height: 100%; border-radius: 3px; transition: width 0.3s; }
.wallet-list { max-height: 200px; overflow-y: auto; }
.wallet-item { display: flex; justify-content: space-between; padding: 6px 0; font-size: 12px; border-bottom: 1px solid rgba(255,255,255,0.03); font-family: monospace; }
.launch-feed { max-height: 300px; overflow-y: auto; }
.launch-item { padding: 8px 0; border-bottom: 1px solid rgba(255,255,255,0.04); font-size: 12px; display: flex; justify-content: space-between; align-items: center; }
footer { text-align: center; padding: 24px; color: var(--dim); font-size: 12px; }
footer span { color: var(--accent); }
.error-panel { background: rgba(255,0,68,0.1); border: 1px solid var(--red); border-radius: 8px; padding: 12px; color: var(--red); font-size: 13px; display: none; }
</style>
</head>
<body>
<header>
<div class="logo"><span>Rug</span><span>Charts</span></div>
<nav style="display:flex;gap:4px">
<a href="/rugcharts" style="color:var(--text);text-decoration:none;padding:6px 14px;border-radius:6px;font-size:13px;font-weight:500;background:rgba(255,255,255,0.06)">RugCharts</a>
<a href="/rugmaps" style="color:var(--dim);text-decoration:none;padding:6px 14px;border-radius:6px;font-size:13px;font-weight:500">RugMaps</a>
<a href="/news" style="color:var(--dim);text-decoration:none;padding:6px 14px;border-radius:6px;font-size:13px;font-weight:500">News</a>
</nav>
<div class="search-bar">
<input id="tokenInput" type="text" placeholder="Paste token address (Solana or EVM)..." autofocus>
<select id="chainSelect">
<option value="solana">Solana</option>
<option value="ethereum">Ethereum</option>
<option value="base">Base</option>
<option value="bsc">BSC</option>
<option value="arbitrum">Arbitrum</option>
<option value="polygon">Polygon</option>
</select>
<button onclick="scanToken()">Scan</button>
</div>
</header>
<div class="stats-bar" id="statsBar">
<span>DataBus: <b id="chainCount">...</b> chains</span>
<span>Cache: <b id="cacheStatus">checking...</b></span>
<span>Powered by <b>Arkham · Helius · GoPlus · RAG</b></span>
</div>
<div class="container">
<div class="error-panel" id="errorPanel"></div>
<!-- VERDICT BANNER -->
<div class="verdict" id="verdictBanner">
<div class="verdict-header">
<div class="risk-score" id="riskScore">--</div>
<div>
<div class="risk-badge" id="riskBadge">SCAN A TOKEN</div>
<div class="verdict-text" id="verdictText"></div>
</div>
</div>
</div>
<!-- MAIN GRID -->
<div class="grid" id="mainGrid">
<div class="panel">
<div class="panel-title"><span class="icon">🛡️</span> Security Scan</div>
<div id="securityPanel" class="loading"><div class="spinner"></div></div>
</div>
<div class="panel">
<div class="panel-title"><span class="icon">👥</span> Holder Health</div>
<div id="holderPanel" class="loading"><div class="spinner"></div></div>
</div>
<div class="panel">
<div class="panel-title"><span class="icon">💧</span> Liquidity Risk</div>
<div id="liquidityPanel" class="loading"><div class="spinner"></div></div>
</div>
<div class="panel">
<div class="panel-title"><span class="icon">📊</span> Volume Authenticity</div>
<div id="volumePanel" class="loading"><div class="spinner"></div></div>
</div>
<div class="panel">
<div class="panel-title"><span class="icon">🔍</span> Rug Patterns</div>
<div id="rugPanel" class="loading"><div class="spinner"></div></div>
</div>
<div class="panel">
<div class="panel-title"><span class="icon">👤</span> Developer Reputation</div>
<div id="devPanel" class="loading"><div class="spinner"></div></div>
</div>
<div class="panel">
<div class="panel-title"><span class="icon">🔗</span> Cross-Chain Entity</div>
<div id="entityPanel" class="loading"><div class="spinner"></div></div>
</div>
<div class="panel">
<div class="panel-title"><span class="icon">🐋</span> Whale Alerts</div>
<div id="whalePanel" class="loading"><div class="spinner"></div></div>
</div>
<div class="panel">
<div class="panel-title"><span class="icon">🕵️</span> Insider Detection</div>
<div id="insiderPanel" class="loading"><div class="spinner"></div></div>
</div>
<div class="panel">
<div class="panel-title"><span class="icon">🚀</span> Token Launches (Live)</div>
<div id="launchPanel" class="loading"><div class="spinner"></div></div>
</div>
<div class="panel">
<div class="panel-title"><span class="icon">💰</span> Smart Money</div>
<div id="smartMoneyPanel" class="loading"><div class="spinner"></div></div>
</div>
</div>
</div>
<footer>RugCharts · 49 DataBus Chains · <span>No one else shows this data</span></footer>
<script>
const API = '/api/v1/databus';
async function apiGet(path) {
try { const r = await fetch(API + path); return r.ok ? r.json() : null; }
catch(e) { return null; }
}
function riskColor(level) {
const m = {SAFE:'var(--green)',LOW:'var(--low)','LOW RISK':'var(--low)',
MEDIUM:'var(--yellow)','MEDIUM RISK':'var(--yellow)',
HIGH:'var(--orange)','HIGH RISK':'var(--orange)',
CRITICAL:'var(--red)',DANGER:'var(--red)',EXTREME:'var(--red)','NO_HOLDERS':'var(--dim)'};
return m[level] || 'var(--dim)';
}
function riskBadgeClass(level) {
const m = {SAFE:'SAFE',LOW:'LOW','LOW RISK':'LOW',MEDIUM:'MEDIUM','MEDIUM RISK':'MEDIUM',
HIGH:'HIGH','HIGH RISK':'HIGH',CRITICAL:'CRITICAL',DANGER:'CRITICAL'};
return m[level] || 'MEDIUM';
}
function renderSecurity(data) {
if (!data) return '<div class="empty">No security data available</div>';
let html = '';
html += `<div class="metric-row"><span class="metric-label">Risk Score</span><span class="metric-value" style="color:${riskColor(data.band)}">${data.score || '--'}/100</span></div>`;
html += `<div class="metric-row"><span class="metric-label">Risk Band</span><span class="metric-value" style="color:${riskColor(data.band)}">${data.band || '--'}</span></div>`;
html += `<div class="metric-row"><span class="metric-label">Checks Run</span><span class="metric-value">${data.checks_run || 0}</span></div>`;
if (data.highlights && data.highlights.length) {
html += '<div style="margin-top:12px">';
data.highlights.forEach(h => {
const cls = h.startsWith('FAIL') ? 'fail' : 'warn';
html += `<span class="tag ${cls}">${h}</span> `;
});
html += '</div>';
}
return html;
}
function renderHolders(data) {
if (!data) return '<div class="empty">No holder data</div>';
let html = '';
html += `<div class="metric-row"><span class="metric-label">Total Holders</span><span class="metric-value">${(data.total||0).toLocaleString()}</span></div>`;
html += `<div class="metric-row"><span class="metric-label">Gini Coefficient</span><span class="metric-value" style="color:${(data.gini||0)>0.6?'var(--red)':(data.gini||0)>0.4?'var(--yellow)':'var(--green)'}">${(data.gini||0).toFixed(3)}</span></div>`;
html += `<div class="metric-row"><span class="metric-label">Top 10 Concentration</span><span class="metric-value">${data.top10_pct||0}%</span></div>`;
html += `<div class="metric-row"><span class="metric-label">Top Wallet Dominance</span><span class="metric-value" style="color:${(data.top1_pct||0)>50?'var(--red)':'var(--dim)'}">${data.top1_pct||0}%</span></div>`;
html += `<div class="metric-row"><span class="metric-label">Whales (>1%)</span><span class="metric-value">${data.whales||0}</span></div>`;
html += `<div class="metric-row"><span class="metric-label">Decentralization</span><span class="metric-value" style="color:${(data.decentralization||0)>60?'var(--green)':(data.decentralization||0)>30?'var(--yellow)':'var(--red)'}">${data.decentralization||0}/100</span></div>`;
html += `<div class="metric-row"><span class="metric-label">Risk</span><span class="metric-value" style="color:${riskColor(data.risk)}">${data.risk||'--'}</span></div>`;
return html;
}
function renderLiquidity(data) {
if (!data) return '<div class="empty">No liquidity data</div>';
let html = '';
html += `<div class="metric-row"><span class="metric-label">Concentration Risk</span><span class="metric-value" style="color:${riskColor(data.concentration_risk?.toUpperCase())}">${data.concentration_risk||'unknown'}</span></div>`;
html += `<div class="metric-row"><span class="metric-label">Risk Score</span><span class="metric-value" style="color:${riskColor(data.risk_level)}">${data.risk_score||50}/100</span></div>`;
html += `<div class="metric-row"><span class="metric-label">LP Holders</span><span class="metric-value">${data.lp_holders||0}</span></div>`;
if (data.lp_holders_list && data.lp_holders_list.length) {
html += '<div style="margin-top:8px;font-size:11px;color:var(--dim)">Top LP Holders:</div>';
data.lp_holders_list.slice(0,3).forEach(h => {
html += `<div class="wallet-item"><span>${h.address||'...'}</span><span>${h.share_pct||0}% ${h.entity ? '· '+h.entity : ''}</span></div>`;
});
}
return html;
}
function renderVolume(data) {
if (!data) return '<div class="empty">No volume data</div>';
let html = '';
const fake = data.fake_volume_pct || 0;
const auth = data.authentic_score || 100;
const fc = fake > 50 ? 'var(--red)' : fake > 20 ? 'var(--yellow)' : 'var(--green)';
html += `<div class="metric-row"><span class="metric-label">Fake Volume</span><span class="metric-value" style="color:${fc}">${fake}%</span></div>`;
html += `<div class="progress-bar"><div class="progress-fill" style="width:${fake}%;background:${fc}"></div></div>`;
html += `<div class="metric-row"><span class="metric-label">Authentic Score</span><span class="metric-value" style="color:${auth>80?'var(--green)':auth>50?'var(--yellow)':'var(--red)'}">${auth}/100</span></div>`;
html += `<div class="metric-row"><span class="metric-label">Risk Level</span><span class="metric-value" style="color:${riskColor(data.risk_level)}">${data.risk_level||'--'}</span></div>`;
return html;
}
function renderRugPatterns(data) {
if (!data) return '<div class="empty">No pattern data</div>';
let html = '';
html += `<div class="metric-row"><span class="metric-label">Pattern Matches</span><span class="metric-value">${data.total_matches||0}</span></div>`;
html += `<div class="metric-row"><span class="metric-label">Overall Risk</span><span class="metric-value" style="color:${riskColor(data.overall_rug_risk||data.overall_risk)}">${data.overall_rug_risk||data.overall_risk||'--'}</span></div>`;
html += `<div class="metric-row"><span class="metric-label">Top Match</span><span class="metric-value">${data.top_match||'None'}</span></div>`;
if (data.top_score) html += `<div class="metric-row"><span class="metric-label">Match Score</span><span class="metric-value" style="color:${(data.top_score||0)>60?'var(--red)':'var(--yellow)'}">${data.top_score}/100</span></div>`;
if (data.pattern_matches && data.pattern_matches.length) {
data.pattern_matches.slice(0,3).forEach(m => {
html += `<div style="margin-top:4px;font-size:11px"><span class="tag ${m.severity==='CRITICAL'?'fail':'warn'}">${m.pattern_name}</span> ${m.match_score}%</div>`;
});
}
return html;
}
function renderDev(data) {
if (!data) return '<div class="empty">No developer data</div>';
let html = '';
html += `<div class="metric-row"><span class="metric-label">Tokens Deployed</span><span class="metric-value">${data.tokens_deployed||'unknown'}</span></div>`;
html += `<div class="metric-row"><span class="metric-label">Activity Span</span><span class="metric-value">${data.activity_span_days||0} days</span></div>`;
html += `<div class="metric-row"><span class="metric-label">Risk Level</span><span class="metric-value" style="color:${riskColor(data.risk_level)}">${data.risk_level||'--'}</span></div>`;
if (data.known_entity) html += `<div class="metric-row"><span class="metric-label">Entity</span><span class="entity-badge">${data.known_entity}</span></div>`;
if (data.entity_type) html += `<div class="metric-row"><span class="metric-label">Type</span><span class="metric-value">${data.entity_type}</span></div>`;
return html;
}
function renderEntity(data) {
if (!data || !data.entity) return '<div class="empty">No entity data (requires Arkham)</div>';
let html = '';
const e = data.entity;
if (e.entity_name) html += `<div class="entity-badge" style="font-size:16px;margin-bottom:8px">${e.entity_name}</div>`;
html += `<div class="metric-row"><span class="metric-label">Type</span><span class="metric-value">${e.entity_type||'--'}</span></div>`;
html += `<div class="metric-row"><span class="metric-label">Chain</span><span class="metric-value">${e.primary_chain||'--'}</span></div>`;
if (e.label) html += `<div class="metric-row"><span class="metric-label">Label</span><span class="metric-value">${e.label}</span></div>`;
if (e.is_contract) html += `<div class="metric-row"><span class="metric-label">Contract</span><span class="metric-value">Yes</span></div>`;
return html;
}
function renderWhales(data) {
if (!data) return '<div class="empty">No whale activity</div>';
let html = '';
html += `<div class="metric-row"><span class="metric-label">Movements Detected</span><span class="metric-value">${data.total_detected||0}</span></div>`;
if (data.whale_movements && data.whale_movements.length) {
html += '<div class="wallet-list">';
data.whale_movements.slice(0,8).forEach(w => {
html += `<div class="wallet-item"><span>${w.wallet||'...'}</span><span style="color:var(--orange)">${w.amount||0}</span></div>`;
});
html += '</div>';
}
return html;
}
function renderInsider(data) {
if (!data) return '<div class="empty">No insider signals</div>';
let html = '';
html += `<div class="metric-row"><span class="metric-label">Signals Found</span><span class="metric-value">${data.total_signals||0}</span></div>`;
html += `<div class="metric-row"><span class="metric-label">Confidence</span><span class="metric-value" style="color:${data.confidence==='HIGH'?'var(--red)':'var(--yellow)'}">${data.confidence||'--'}</span></div>`;
if (data.insider_signals && data.insider_signals.length) {
data.insider_signals.slice(0,3).forEach(s => {
html += `<div style="margin-top:6px;font-size:11px"><span class="tag ${s.signal_strength==='HIGH'?'fail':'warn'}">${s.signal_strength}</span> ${s.spike_multiplier}x volume spike</div>`;
});
}
return html;
}
function renderLaunches(data) {
if (!data) return '<div class="empty">No launch data</div>';
let html = '';
const rd = data.risk_distribution || {};
html += `<div class="metric-row"><span class="metric-label">New Tokens</span><span class="metric-value">${data.total_detected||0}</span></div>`;
if (rd.CRITICAL) html += `<div class="metric-row"><span class="metric-label">Critical Risk</span><span class="metric-value" style="color:var(--red)">${rd.CRITICAL}</span></div>`;
if (rd.HIGH) html += `<div class="metric-row"><span class="metric-label">High Risk</span><span class="metric-value" style="color:var(--orange)">${rd.HIGH}</span></div>`;
if (data.new_tokens && data.new_tokens.length) {
html += '<div class="launch-feed">';
data.new_tokens.slice(0,10).forEach(t => {
html += `<div class="launch-item"><span style="font-family:monospace;font-size:11px">${t.signature||'...'}</span><span class="tag ${t.risk_level==='CRITICAL'?'fail':t.risk_level==='HIGH'?'warn':'pass'}">${t.risk_level} · ${t.age_minutes}m</span></div>`;
});
html += '</div>';
}
return html;
}
function renderSmartMoney(data) {
if (!data) return '<div class="empty">No smart money data</div>';
let html = '';
html += `<div class="metric-row"><span class="metric-label">Tracked Wallets</span><span class="metric-value">${data.total_tracked||0}</span></div>`;
if (data.smart_money_wallets && data.smart_money_wallets.length) {
html += '<div class="wallet-list">';
data.smart_money_wallets.slice(0,8).forEach(w => {
const label = w.entity ? ` · ${w.entity}` : '';
html += `<div class="wallet-item"><span>${(w.address||'').slice(0,12)}...</span><span>$${(w.estimated_usd||0).toLocaleString()}${label}</span></div>`;
});
html += '</div>';
}
return html;
}
async function scanToken() {
const addr = document.getElementById('tokenInput').value.trim();
const chain = document.getElementById('chainSelect').value;
if (!addr) return;
// Show loading
document.querySelectorAll('.panel > div:last-child').forEach(el => {
if (el.id && el.id.endsWith('Panel')) el.innerHTML = '<div class="spinner"></div>';
});
document.getElementById('verdictBanner').classList.remove('active');
document.getElementById('errorPanel').style.display = 'none';
// Fire all endpoints in parallel
const token = encodeURIComponent(addr);
const [report, security, holders, liquidity, volume, rug, dev, entity, whales, insider, launches, smart] =
await Promise.all([
apiGet(`/premium/token-report/${token}?chain=${chain}`),
apiGet(`/premium/security-scan/${token}?chain=${chain}`),
apiGet(`/premium/holder-health/${token}?chain=${chain}`),
apiGet(`/premium/liquidity-risk/${token}?chain=${chain}`),
apiGet(`/premium/volume-authenticity/${token}?chain=${chain}`),
apiGet(`/premium/rug-patterns/${token}?chain=${chain}`),
apiGet(`/premium/dev-reputation/${token}?chain=${chain}`),
apiGet(`/premium/cross-chain/${token}`),
apiGet(`/premium/whale-alerts/${token}?chain=${chain}`),
apiGet(`/premium/insider-detection/${token}?chain=${chain}`),
apiGet(`/premium/token-launches?chain=${chain}&limit=20`),
apiGet(`/premium/smart-money?chain=${chain}`),
]);
// Render verdict
const v = document.getElementById('verdictBanner');
v.classList.add('active');
const or = report?.overall_risk || {};
document.getElementById('riskScore').textContent = or.score || '--';
document.getElementById('riskScore').style.color = riskColor(or.level);
const badge = document.getElementById('riskBadge');
badge.textContent = or.level || 'UNKNOWN';
badge.className = 'risk-badge ' + riskBadgeClass(or.level);
document.getElementById('verdictText').textContent = report?.quick_verdict || '';
// Render panels
document.getElementById('securityPanel').innerHTML = renderSecurity(report?.sections?.security || security);
document.getElementById('holderPanel').innerHTML = renderHolders(report?.sections?.holders || holders);
document.getElementById('liquidityPanel').innerHTML = renderLiquidity(report?.sections?.liquidity || liquidity);
document.getElementById('volumePanel').innerHTML = renderVolume(report?.sections?.volume || volume);
document.getElementById('rugPanel').innerHTML = renderRugPatterns(report?.sections?.rug_patterns || rug);
document.getElementById('devPanel').innerHTML = renderDev(report?.sections?.developer || dev);
document.getElementById('entityPanel').innerHTML = renderEntity(report?.sections?.entity || entity);
document.getElementById('whalePanel').innerHTML = renderWhales(whales);
document.getElementById('insiderPanel').innerHTML = renderInsider(insider);
document.getElementById('launchPanel').innerHTML = renderLaunches(launches);
document.getElementById('smartMoneyPanel').innerHTML = renderSmartMoney(smart);
// Error handling
if (!report && !security && !holders) {
document.getElementById('errorPanel').style.display = 'block';
document.getElementById('errorPanel').textContent = 'Could not fetch token data. Check the address and chain.';
}
// Update URL
history.pushState({}, '', `?chain=${chain}&token=${addr}`);
}
async function init() {
// Load stats
try {
const health = await fetch(API + '/health').then(r => r.json());
document.getElementById('chainCount').textContent = health?.chains || '49';
document.getElementById('cacheStatus').textContent = health?.cache_hits ? `${health.cache_hits} hits` : 'live';
} catch(e) {}
// Check URL params
const params = new URLSearchParams(window.location.search);
const token = params.get('token');
const chain = params.get('chain');
if (token) {
document.getElementById('tokenInput').value = token;
if (chain) document.getElementById('chainSelect').value = chain;
scanToken();
}
// Enter key
document.getElementById('tokenInput').addEventListener('keydown', e => {
if (e.key === 'Enter') scanToken();
});
}
init();
</script>
</body>
</html>

529
static/rugmaps.html Normal file
View file

@ -0,0 +1,529 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RugMaps — Wallet Cluster Intelligence</title>
<style>
:root {
--bg: #0a0a0f; --panel: #12121a; --border: #1e1e2e;
--text: #e0e0e0; --dim: #8888aa; --accent: #7c3aed; --accent2: #06b6d4;
--green: #00ff88; --yellow: #ffd700; --orange: #ff8800; --red: #ff0044;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: var(--bg); color: var(--text); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; min-height: 100vh; overflow-x: hidden; }
header { background: var(--panel); border-bottom: 1px solid var(--border); padding: 12px 24px; display: flex; align-items: center; gap: 12px; position: sticky; top: 0; z-index: 100; }
.logo { font-size: 20px; font-weight: 800; letter-spacing: -0.5px; white-space: nowrap; }
.logo span:first-child { color: var(--accent2); } .logo span:last-child { color: var(--text); }
nav { display: flex; gap: 4px; }
nav a { color: var(--dim); text-decoration: none; padding: 6px 14px; border-radius: 6px; font-size: 13px; font-weight: 500; }
nav a:hover, nav a.active { color: var(--text); background: rgba(255,255,255,0.06); }
nav a.active { color: var(--accent2); }
.search-bar { flex: 1; max-width: 500px; display: flex; gap: 8px; margin-left: auto; }
.search-bar input { flex: 1; background: var(--bg); border: 1px solid var(--border); border-radius: 8px; padding: 8px 14px; color: var(--text); font-size: 13px; outline: none; }
.search-bar input:focus { border-color: var(--accent2); }
.search-bar select { background: var(--bg); border: 1px solid var(--border); border-radius: 8px; padding: 8px 10px; color: var(--text); font-size: 13px; }
.search-bar button { background: var(--accent2); color: #000; border: none; border-radius: 8px; padding: 8px 18px; font-size: 13px; font-weight: 700; cursor: pointer; }
.search-bar button:hover { opacity: 0.9; }
.container { display: flex; height: calc(100vh - 57px); }
#graphArea { flex: 1; position: relative; background: radial-gradient(ellipse at center, #12122a 0%, var(--bg) 70%); }
#graphArea canvas { position: absolute; top: 0; left: 0; }
#sidebar { width: 380px; background: var(--panel); border-left: 1px solid var(--border); overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 16px; }
.sidebar-section { background: var(--bg); border: 1px solid var(--border); border-radius: 10px; padding: 14px; }
.sidebar-title { font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; color: var(--dim); margin-bottom: 10px; }
.stat { display: flex; justify-content: space-between; padding: 4px 0; font-size: 13px; }
.stat-label { color: var(--dim); }
.stat-value { font-weight: 600; }
.tag { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 10px; font-weight: 600; margin: 1px; }
.tag.high { background: rgba(255,0,68,0.15); color: var(--red); }
.tag.medium { background: rgba(255,136,0,0.15); color: var(--orange); }
.tag.low { background: rgba(0,255,136,0.15); color: var(--green); }
.tag.entity { background: rgba(6,182,212,0.15); color: var(--accent2); }
.bundle-list { max-height: 250px; overflow-y: auto; }
.bundle-item { padding: 8px; border-bottom: 1px solid rgba(255,255,255,0.04); cursor: pointer; border-radius: 4px; margin-bottom: 4px; font-size: 12px; }
.bundle-item:hover { background: rgba(255,255,255,0.04); }
.bundle-item.selected { background: rgba(6,182,212,0.1); border: 1px solid var(--accent2); }
.wallet-badge { font-family: monospace; font-size: 11px; color: var(--dim); }
.legend { display: flex; gap: 12px; flex-wrap: wrap; font-size: 11px; }
.legend-item { display: flex; align-items: center; gap: 4px; }
.legend-dot { width: 8px; height: 8px; border-radius: 50%; }
.tooltip { position: absolute; background: var(--panel); border: 1px solid var(--accent2); border-radius: 8px; padding: 10px 14px; font-size: 12px; pointer-events: none; display: none; z-index: 1000; max-width: 280px; }
.tooltip .addr { font-family: monospace; font-size: 11px; color: var(--accent2); margin-bottom: 4px; }
.tooltip .entity { color: var(--green); font-weight: 600; }
.loading-overlay { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); text-align: center; color: var(--dim); }
.spinner { display: inline-block; width: 32px; height: 32px; border: 3px solid var(--border); border-top-color: var(--accent2); border-radius: 50%; animation: spin 0.8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
.empty-state { text-align: center; color: var(--dim); padding: 40px; }
.empty-state h2 { color: var(--text); margin-bottom: 8px; }
</style>
</head>
<body>
<header>
<div class="logo"><span>Rug</span><span>Maps</span></div>
<nav>
<a href="/rugcharts">RugCharts</a>
<a href="/rugmaps" class="active">RugMaps</a>
<a href="/news">News</a>
</nav>
<div class="search-bar">
<input id="tokenInput" type="text" placeholder="Token address for cluster analysis..." autofocus>
<select id="chainSelect">
<option value="solana">Solana</option>
<option value="ethereum">Ethereum</option>
<option value="base">Base</option>
<option value="bsc">BSC</option>
</select>
<button onclick="scanToken()">Map</button>
</div>
</header>
<div class="container">
<div id="graphArea">
<canvas id="graphCanvas"></canvas>
<div class="loading-overlay" id="loadingOverlay" style="display:none">
<div class="spinner"></div>
<div style="margin-top:12px" id="loadingText">Mapping wallet clusters...</div>
</div>
<div class="empty-state" id="emptyState">
<h2>RugMaps — Wallet Cluster Intelligence</h2>
<p>Paste a token address to visualize wallet clusters,<br>bundle detection, and entity relationships.</p>
<p style="margin-top:8px;font-size:12px;color:var(--dim)">Powered by DataBus: bundle_detect + cluster_map + Arkham</p>
</div>
<div class="tooltip" id="tooltip"></div>
</div>
<div id="sidebar">
<div class="sidebar-section">
<div class="sidebar-title">📊 Cluster Overview</div>
<div id="overviewStats">
<div class="stat"><span class="stat-label">Scanning...</span></div>
</div>
</div>
<div class="sidebar-section">
<div class="sidebar-title">🔗 Wallet Bundles</div>
<div class="legend">
<div class="legend-item"><div class="legend-dot" style="background:var(--red)"></div>High Risk</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--orange)"></div>Medium</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--accent2)"></div>Entity</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--dim)"></div>Wallet</div>
</div>
<div class="bundle-list" id="bundleList"></div>
</div>
<div class="sidebar-section">
<div class="sidebar-title">🏷️ Selected Wallet</div>
<div id="walletDetail" style="font-size:12px;color:var(--dim)">Click a node for details</div>
</div>
</div>
</div>
<script>
const API = '/api/v1/databus';
let nodes = [], edges = [], bundles = [];
let highlightedBundle = null;
async function apiGet(path) {
try { const r = await fetch(API + path); return r.ok ? r.json() : null; }
catch(e) { return null; }
}
async function scanToken() {
const addr = document.getElementById('tokenInput').value.trim();
const chain = document.getElementById('chainSelect').value;
if (!addr) return;
document.getElementById('emptyState').style.display = 'none';
document.getElementById('loadingOverlay').style.display = 'block';
document.getElementById('loadingText').textContent = 'Detecting wallet bundles...';
const token = encodeURIComponent(addr);
// Fetch both bundle detection and cluster map from DataBus
const [bundleData, clusterData] = await Promise.all([
apiGet(`/premium/bundles/${token}?chain=${chain}`),
apiGet(`/premium/clusters/${token}?chain=${chain}&depth=3`),
]);
bundles = bundleData?.bundles || [];
const clusterNodes = clusterData?.nodes || [];
const clusterEdges = clusterData?.edges || [];
document.getElementById('loadingText').textContent = 'Building graph...';
// Build graph from cluster data
nodes = [];
edges = [];
const nodeMap = new Map();
const bundleWallets = new Set();
// Mark bundle wallets
for (const b of bundles) {
for (const w of (b.wallets || [])) {
bundleWallets.add(w);
}
}
// Create nodes
for (const n of clusterNodes) {
const inBundle = bundleWallets.has(n.id);
const hasEntity = n.entity && n.entity.length > 0;
nodes.push({
id: n.id,
entity: n.entity || '',
type: hasEntity ? 'entity' : (inBundle ? 'bundle' : 'wallet'),
depth: n.depth || 0,
x: Math.random() * 600 + 100,
y: Math.random() * 400 + 100,
vx: 0, vy: 0,
});
nodeMap.set(n.id, nodes[nodes.length - 1]);
}
// Add bundle wallets not in cluster
for (const w of bundleWallets) {
if (!nodeMap.has(w)) {
nodes.push({
id: w,
entity: '',
type: 'bundle',
depth: 0,
x: Math.random() * 600 + 100,
y: Math.random() * 400 + 100,
vx: 0, vy: 0,
});
nodeMap.set(w, nodes[nodes.length - 1]);
}
}
// Create edges
for (const e of clusterEdges) {
if (nodeMap.has(e.from) && nodeMap.has(e.to)) {
edges.push({ from: e.from, to: e.to, value: e.value || 1 });
}
}
// Add bundle edges
for (const b of bundles) {
const wallets = b.wallets || [];
for (let i = 0; i < wallets.length; i++) {
for (let j = i + 1; j < wallets.length; j++) {
if (nodeMap.has(wallets[i]) && nodeMap.has(wallets[j])) {
const existingEdge = edges.find(e =>
(e.from === wallets[i] && e.to === wallets[j]) ||
(e.from === wallets[j] && e.to === wallets[i]));
if (!existingEdge) {
edges.push({ from: wallets[i], to: wallets[j], value: 2, bundle: true });
}
}
}
}
}
document.getElementById('loadingOverlay').style.display = 'none';
// Update sidebar
updateSidebar(bundleData, clusterData);
// Render graph
highlightedBundle = null;
renderGraph();
history.pushState({}, '', `?chain=${chain}&token=${addr}`);
}
function updateSidebar(bundleData, clusterData) {
const stats = document.getElementById('overviewStats');
const totalNodes = nodes.length;
const bundleNodes = nodes.filter(n => n.type === 'bundle').length;
const entityNodes = nodes.filter(n => n.type === 'entity').length;
const totalBundles = bundles.length;
const maxBundle = bundles.reduce((m, b) => Math.max(m, (b.wallets||[]).length), 0);
stats.innerHTML = `
<div class="stat"><span class="stat-label">Total Wallets</span><span class="stat-value">${totalNodes}</span></div>
<div class="stat"><span class="stat-label">Clusters Found</span><span class="stat-value">${totalBundles}</span></div>
<div class="stat"><span class="stat-label">Bundle Wallets</span><span class="stat-value" style="color:${bundleNodes>0?'var(--orange)':'var(--dim)'}">${bundleNodes}</span></div>
<div class="stat"><span class="stat-label">Known Entities</span><span class="stat-value" style="color:var(--accent2)">${entityNodes}</span></div>
<div class="stat"><span class="stat-label">Largest Bundle</span><span class="stat-value">${maxBundle} wallets</span></div>
<div class="stat"><span class="stat-label">Total Edges</span><span class="stat-value">${edges.length}</span></div>
<div class="stat"><span class="stat-label">Source</span><span class="stat-value" style="color:var(--accent2)">${clusterData?.source || 'databus'}</span></div>
`;
// Bundle list
const bl = document.getElementById('bundleList');
if (!bundles.length) {
bl.innerHTML = '<div style="color:var(--dim);font-size:12px;padding:8px">No coordinated bundles detected. Wallets appear independent.</div>';
} else {
bl.innerHTML = bundles.map((b, i) => `
<div class="bundle-item" onclick="highlightBundle(${i})" id="bundle-${i}">
<div style="display:flex;justify-content:space-between;align-items:center">
<span>Bundle #${i+1}</span>
<span class="tag ${b.risk_level==='HIGH'?'high':'medium'}">${b.risk_level||'MEDIUM'}</span>
</div>
<div class="wallet-badge">${(b.wallets||[]).length} wallets · score: ${(b.coordination_score||0).toFixed(1)}</div>
${b.pattern ? `<div style="color:var(--dim);font-size:10px">${b.pattern}</div>` : ''}
</div>
`).join('');
}
}
function highlightBundle(idx) {
highlightedBundle = highlightedBundle === idx ? null : idx;
document.querySelectorAll('.bundle-item').forEach(el => el.classList.remove('selected'));
if (highlightedBundle !== null) {
document.getElementById(`bundle-${idx}`)?.classList.add('selected');
}
renderGraph();
}
// ── Force-Directed Graph ──────────────────────────────────────────
const canvas = document.getElementById('graphCanvas');
const ctx = canvas.getContext('2d');
let animationId;
function resize() {
const area = document.getElementById('graphArea');
canvas.width = area.clientWidth;
canvas.height = area.clientHeight;
}
window.addEventListener('resize', () => { resize(); renderGraph(); });
resize();
function nodeColor(node) {
if (node.type === 'entity') return '#06b6d4';
if (node.type === 'bundle') return '#ff8800';
return '#555577';
}
function nodeRadius(node) {
if (node.type === 'entity') return 8;
if (node.type === 'bundle') return 6;
return 4;
}
function renderGraph() {
if (!nodes.length) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
return;
}
// Simulation
const iterations = 50;
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const repulsion = 5000;
const attraction = 0.005;
const damping = 0.85;
for (let iter = 0; iter < iterations; iter++) {
// Repulsion
for (let i = 0; i < nodes.length; i++) {
for (let j = i + 1; j < nodes.length; j++) {
const dx = nodes[i].x - nodes[j].x;
const dy = nodes[i].y - nodes[j].y;
const dist = Math.max(1, Math.sqrt(dx * dx + dy * dy));
const force = repulsion / (dist * dist);
const fx = (dx / dist) * force;
const fy = (dy / dist) * force;
nodes[i].vx += fx;
nodes[i].vy += fy;
nodes[j].vx -= fx;
nodes[j].vy -= fy;
}
}
// Attraction (edges)
for (const e of edges) {
const from = nodes.find(n => n.id === e.from);
const to = nodes.find(n => n.id === e.to);
if (!from || !to) continue;
const dx = to.x - from.x;
const dy = to.y - from.y;
const dist = Math.max(1, Math.sqrt(dx * dx + dy * dy));
const force = dist * attraction * (e.value || 1);
const fx = (dx / dist) * force;
const fy = (dy / dist) * force;
from.vx += fx;
from.vy += fy;
to.vx -= fx;
to.vy -= fy;
}
// Center gravity
for (const n of nodes) {
n.vx += (centerX - n.x) * 0.001;
n.vy += (centerY - n.y) * 0.001;
}
// Apply velocity
for (const n of nodes) {
n.x += n.vx * damping;
n.y += n.vy * damping;
n.vx *= damping;
n.vy *= damping;
// Bounds
n.x = Math.max(20, Math.min(canvas.width - 20, n.x));
n.y = Math.max(20, Math.min(canvas.height - 20, n.y));
}
}
// Draw
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Highlighted bundle highlight area
if (highlightedBundle !== null && bundles[highlightedBundle]) {
const bw = bundles[highlightedBundle].wallets || [];
const bundleNodes = nodes.filter(n => bw.includes(n.id));
if (bundleNodes.length) {
const cx = bundleNodes.reduce((s, n) => s + n.x, 0) / bundleNodes.length;
const cy = bundleNodes.reduce((s, n) => s + n.y, 0) / bundleNodes.length;
const maxR = Math.max(...bundleNodes.map(n =>
Math.sqrt((n.x-cx)**2 + (n.y-cy)**2))) + 30;
ctx.beginPath();
ctx.arc(cx, cy, maxR, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(255,136,0,0.08)';
ctx.fill();
ctx.strokeStyle = 'rgba(255,136,0,0.3)';
ctx.lineWidth = 1;
ctx.stroke();
}
}
// Edges
for (const e of edges) {
const from = nodes.find(n => n.id === e.from);
const to = nodes.find(n => n.id === e.to);
if (!from || !to) continue;
const isBundleEdge = e.bundle;
const isHighlighted = highlightedBundle !== null && bundles[highlightedBundle] &&
bundles[highlightedBundle].wallets?.includes(e.from) &&
bundles[highlightedBundle].wallets?.includes(e.to);
ctx.beginPath();
ctx.moveTo(from.x, from.y);
ctx.lineTo(to.x, to.y);
ctx.strokeStyle = isHighlighted ? 'rgba(255,136,0,0.6)' :
isBundleEdge ? 'rgba(255,136,0,0.2)' : 'rgba(255,255,255,0.06)';
ctx.lineWidth = isHighlighted ? 2 : (isBundleEdge ? 1.5 : 0.5);
ctx.stroke();
}
// Nodes
for (const n of nodes) {
const isHighlighted = highlightedBundle !== null && bundles[highlightedBundle] &&
bundles[highlightedBundle].wallets?.includes(n.id);
ctx.beginPath();
ctx.arc(n.x, n.y, nodeRadius(n) * (isHighlighted ? 1.5 : 1), 0, Math.PI * 2);
ctx.fillStyle = isHighlighted ? '#ff8800' : nodeColor(n);
ctx.fill();
if (isHighlighted || n.type === 'entity') {
ctx.strokeStyle = isHighlighted ? '#fff' : 'rgba(6,182,212,0.5)';
ctx.lineWidth = 2;
ctx.stroke();
}
// Entity label
if (n.entity && (n.type === 'entity' || isHighlighted)) {
ctx.fillStyle = '#fff';
ctx.font = '10px -apple-system, sans-serif';
ctx.fillText(n.entity.slice(0, 12), n.x + 10, n.y + 3);
}
}
}
// ── Interaction ───────────────────────────────────────────────────
const tooltip = document.getElementById('tooltip');
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
let closest = null;
let closestDist = 25;
for (const n of nodes) {
const dx = n.x - mx;
const dy = n.y - my;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < closestDist) {
closestDist = dist;
closest = n;
}
}
if (closest) {
canvas.style.cursor = 'pointer';
tooltip.style.display = 'block';
tooltip.style.left = (e.clientX - rect.left + 15) + 'px';
tooltip.style.top = (e.clientY - rect.top - 10) + 'px';
tooltip.innerHTML = `
<div class="addr">${closest.id.slice(0,8)}...${closest.id.slice(-4)}</div>
${closest.entity ? `<div class="entity">${closest.entity}</div>` : ''}
<div style="color:var(--dim)">Type: ${closest.type} · Depth: ${closest.depth}</div>
`;
document.getElementById('walletDetail').innerHTML = `
<div style="font-family:monospace;color:var(--accent2);margin-bottom:6px">${closest.id}</div>
${closest.entity ? `<div class="tag entity">${closest.entity}</div>` : ''}
<div class="stat"><span class="stat-label">Type</span><span class="stat-value">${closest.type}</span></div>
<div class="stat"><span class="stat-label">Depth</span><span class="stat-value">${closest.depth}</span></div>
<div class="stat"><span class="stat-label">Connections</span><span class="stat-value">${edges.filter(e => e.from===closest.id || e.to===closest.id).length}</span></div>
`;
} else {
canvas.style.cursor = 'default';
tooltip.style.display = 'none';
}
});
canvas.addEventListener('click', (e) => {
const rect = canvas.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
for (const n of nodes) {
const dx = n.x - mx;
const dy = n.y - my;
if (Math.sqrt(dx*dx + dy*dy) < nodeRadius(n) + 5) {
// Find which bundle this wallet belongs to
for (let i = 0; i < bundles.length; i++) {
if ((bundles[i].wallets||[]).includes(n.id)) {
highlightBundle(i);
return;
}
}
// If not in a bundle, clear highlight
highlightedBundle = null;
document.querySelectorAll('.bundle-item').forEach(el => el.classList.remove('selected'));
renderGraph();
return;
}
}
});
async function init() {
const params = new URLSearchParams(window.location.search);
const token = params.get('token');
const chain = params.get('chain');
if (token) {
document.getElementById('tokenInput').value = token;
if (chain) document.getElementById('chainSelect').value = chain;
scanToken();
}
document.getElementById('tokenInput').addEventListener('keydown', e => {
if (e.key === 'Enter') scanToken();
});
}
init();
</script>
</body>
</html>

121
static/rundown.html Normal file
View file

@ -0,0 +1,121 @@
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RMI Daily Market Rundown</title>
<style>
:root{--bg:#0a0a0f;--fg:#e0e0e0;--accent:#6c5ce7;--green:#27ae60;--red:#e74c3c;--card:#14141f;--border:#2a2a3a;--muted:#888;--yellow:#f39c12;--purple:#9945FF}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);color:var(--fg);font-family:system-ui,sans-serif}
header{background:var(--card);border-bottom:1px solid var(--border);padding:16px 24px;display:flex;align-items:center;gap:16px;position:sticky;top:0;z-index:10}
.logo{font-size:20px;font-weight:700;color:var(--accent)}
main{max-width:1300px;margin:0 auto;padding:24px;display:grid;grid-template-columns:240px 1fr 300px;gap:20px}
.sidebar{position:sticky;top:80px;height:fit-content}
.sidebar h3{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:1px;margin:16px 0 8px 0}
.cat-link{display:block;padding:7px 10px;border-radius:6px;color:var(--fg);text-decoration:none;font-size:13px;margin:2px 0;cursor:pointer}
.cat-link:hover,.cat-link.active{background:var(--accent);color:#fff}
.data-card{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:14px;margin-bottom:12px}
.data-card h4{font-size:12px;color:var(--muted);text-transform:uppercase;letter-spacing:1px;margin-bottom:10px}
.data-value{font-size:24px;font-weight:700;margin:4px 0}
.data-label{font-size:11px;color:var(--muted)}
.data-row{display:flex;justify-content:space-between;padding:4px 0;font-size:12px;border-bottom:1px solid rgba(255,255,255,0.03)}
.ai-summary{background:linear-gradient(135deg,#1a1a3e,#2a1a4e);border:1px solid var(--accent);border-radius:12px;padding:20px;margin-bottom:20px}
.ai-summary h2{font-size:20px;margin-bottom:4px;color:var(--accent)}
.ai-badge{background:var(--accent);color:#fff;padding:2px 8px;border-radius:6px;font-size:10px;display:inline-block;margin-bottom:10px}
.ai-summary p{line-height:1.6;color:#c0c0d0;font-size:13px}
.article{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:14px;margin-bottom:10px;transition:border-color .2s}
.article:hover{border-color:var(--accent)}
.article-source{font-size:10px;color:var(--muted);margin-bottom:4px}
.article-title{font-size:14px;font-weight:600;margin-bottom:6px}
.article-title a{color:var(--fg);text-decoration:none}
.article-title a:hover{color:var(--accent)}
.article-summary{font-size:12px;color:#c0c0d0;line-height:1.4;margin-bottom:8px}
.article-meta{display:flex;gap:12px;align-items:center;font-size:11px;color:var(--muted)}
.tag{display:inline-block;padding:2px 6px;border-radius:4px;font-size:10px;margin-right:3px}
.tag-bull{background:rgba(39,174,96,.15);color:var(--green)}
.tag-bear{background:rgba(231,76,60,.15);color:var(--red)}
.tag-neutral{background:rgba(136,136,136,.15);color:var(--muted)}
.tag-cat{background:rgba(108,92,231,.15);color:var(--accent)}
.tag-alert{background:rgba(231,76,60,.2);color:var(--red)}
.vote-btn{background:none;border:1px solid var(--border);color:var(--muted);padding:3px 8px;border-radius:4px;cursor:pointer;font-size:11px}
.vote-btn:hover{background:var(--accent);color:#fff}
.fg-bar{height:6px;border-radius:3px;margin-top:6px;background:linear-gradient(90deg,var(--red),var(--yellow),var(--green))}
.fg-marker{width:4px;height:14px;background:white;border-radius:2px;position:relative}
.spinner{display:inline-block;width:14px;height:14px;border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .6s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
</style>
</head>
<body>
<header><span class="logo">RMI</span><span>Daily Market Rundown</span><span class="badge">AI-Powered</span></header>
<main>
<aside class="sidebar">
<input style="width:100%;padding:8px 10px;background:#1a1a2e;border:1px solid var(--border);border-radius:6px;color:var(--fg);font-size:13px;margin-bottom:12px" placeholder="Search..." id="search" oninput="loadArticles()">
<h3>Categories</h3><div id="categories"></div>
<h3>Sentiment</h3><div id="sentiments"></div>
</aside>
<section id="feed">
<div id="summary"></div>
<div id="articles"><span class="spinner"></span> Loading...</div>
</section>
<aside id="data-panel">
<div class="data-card"><h4>Fear & Greed</h4><div id="fg"></div></div>
<div class="data-card"><h4>Price Action</h4><div id="prices"></div></div>
<div class="data-card"><h4>Security Alerts</h4><div id="alerts"></div></div>
<div class="data-card"><h4>Prediction Markets</h4><div id="pm"></div></div>
</aside>
</main>
<script>
let activeCat='All',activeSent='';
async function load(){
document.getElementById('summary').innerHTML='<div class="ai-summary"><span class="spinner"></span> Generating AI market analysis...</div>';
try{
const s=await fetch('/mcp/news/summary').then(r=>r.json());
document.getElementById('summary').innerHTML=`<div class="ai-summary"><span class="ai-badge">AI Analysis</span><h2>Daily Market Rundown</h2><p>${s.summary}</p></div>`;
}catch(e){document.getElementById('summary').innerHTML='';}
try{const c=await fetch('/mcp/news/categories').then(r=>r.json());
document.getElementById('categories').innerHTML=c.categories.map(c=>`<span class="cat-link ${c.name===activeCat?'active':''}" onclick="setCat('${c.name}')">${c.icon} ${c.name}</span>`).join('');
document.getElementById('sentiments').innerHTML=['bullish','bearish','neutral'].map(s=>`<span class="cat-link ${s===activeSent?'active':''}" onclick="setSent('${s}')">${s==='bullish'?'🟢':s==='bearish'?'🔴':'⚪'} ${s}</span>`).join('');}catch(e){}
loadArticles();
loadDataPanel();
}
async function loadDataPanel(){
try{
const d=await fetch('/mcp/daily-data').then(r=>r.json());
const fg=d.fear_greed||{};
document.getElementById('fg').innerHTML=`<div class="data-value" style="color:${fg.value>60?'var(--green)':fg.value<40?'var(--red)':'var(--yellow)'}">${fg.value||50}</div><div class="data-label">${fg.classification||'Neutral'}</div><div class="fg-bar"><div style="width:${fg.value||50}%;height:6px;background:white;border-radius:3px"></div></div>`;
const pa=d.price_action||{};
let priceHTML='';
for(const [k,v] of Object.entries(pa.major_assets||{})){priceHTML+=`<div class="data-row"><span>${k.toUpperCase()}</span><span>\$${(v.price||0).toFixed(2)}</span></div>`;}
document.getElementById('prices').innerHTML=priceHTML||'<span style="font-size:12px;color:var(--muted)">Loading prices...</span>';
const sa=d.security_alerts||{};
document.getElementById('alerts').innerHTML=sa.count?sa.alerts.slice(0,3).map(a=>`<div style="font-size:11px;padding:6px 0;border-bottom:1px solid rgba(255,255,255,.03)"><span class="tag tag-alert">${a.type}</span> ${a.project||a.amount_lost||''}</div>`).join(''):'<span style="font-size:12px;color:var(--muted)">No major alerts today</span>';
const pm=d.prediction_markets||{};
document.getElementById('pm').innerHTML=pm.top_markets?pm.top_markets.slice(0,3).map(m=>`<div style="font-size:11px;padding:6px 0;border-bottom:1px solid rgba(255,255,255,.03)">${(m.title||'').slice(0,50)}</div>`).join(''):'<span style="font-size:12px;color:var(--muted)">Loading markets...</span>';
}catch(e){console.log(e)}
}
async function loadArticles(){
document.getElementById('articles').innerHTML='<span class="spinner"></span> Loading...';
try{
const params=new URLSearchParams({limit:50});if(activeCat!=='All')params.set('category',activeCat);if(activeSent)params.set('sentiment',activeSent);
const r=await fetch('/mcp/news?'+params).then(r=>r.json());
document.getElementById('articles').innerHTML=r.articles.map(a=>`<div class="article"><div class="article-source">${a.source} · ${a.published?.slice(0,16)||''}</div><div class="article-title"><a href="${a.url}" target="_blank">${a.title}</a></div><div class="article-summary">${a.summary||''}</div><div class="article-meta"><span class="tag tag-${a.sentiment_label?.slice(0,4)||'neutral'}">${a.sentiment_label||'neutral'}</span><span class="tag tag-cat">${a.category}</span><button class="vote-btn" onclick="vote('${a.id}','up')"></button><span>${a.votes_up-a.votes_down}</span><button class="vote-btn" onclick="vote('${a.id}','down')"></button></div></div>`).join('')||'<p>No articles found.</p>';
}catch(e){}
}
function setCat(c){activeCat=c;loadArticles();}
function setSent(s){activeSent=activeSent===s?'':s;loadArticles();}
async function vote(id,dir){await fetch('/mcp/news/vote',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({article_id:id,direction:dir})});loadArticles();}
load();
</script>
</body>
</html>

105
static/x402-api-docs.md Normal file
View file

@ -0,0 +1,105 @@
# RMI x402 API — Crypto Intelligence Micropayments
> **210 tools across 13 chains. Pay per call with USDC on any chain.**
## Quick Start
### 1. Discover Available Tools
```bash
curl https://rugmunch.io/.well-known/x402
```
Returns the full x402 discovery document with all tools, prices, supported chains, and facilitators.
### 2. Browse the Catalog
```bash
curl https://rugmunch.io/api/v1/x402/tools-catalog
```
### 3. Make a Paid Request
```bash
# Example: Check if a token is a honeypot (Base, $0.05 USDC)
curl -X POST https://rugmunch.io/api/v1/x402-tools/honeypot_check \
-H "Content-Type: application/json" \
-H "X-Pay: <your-signed-payment>" \
-d '{"chain": "base", "address": "0x..."}'
```
See the [x402 protocol spec](https://x402.org) for payment header details.
## Supported Chains
| Chain | USDC Contract | Facilitators |
|-------|-------------|--------------|
| **Base** | `0x833589fcd6edb6e08f4c7c32d4f71b54bda02913` | Coinbase CDP, PayAI, EIP-7702 |
| **Ethereum** | `0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48` | Primev, PayAI, EIP-7702 |
| **Solana** | `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v` | PayAI |
| **Arbitrum** | `0xaf88d065e77c8cC2239327C5EDb3A432268e5831` | EIP-7702 |
| **Optimism** | `0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85` | EIP-7702 |
| **Polygon** | `0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359` | EIP-7702 |
| **BSC** | `0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d` | EIP-7702 |
| **Avalanche** | `0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E` | EIP-7702 |
| **Fantom** | `0x04068DA6C83AFCFA0e13ba15A6696662335D5B75` | EIP-7702 |
| **Gnosis** | `0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83` | EIP-7702 |
| **TRON** | USDT/USDC/USDD | Self-verified (fee-free) |
| **Bitcoin** | BTC | Self-verified (fee-free, 1-conf) |
| **SEPA/EUR** | Fiat EUR off-ramp | AsterPay |
## Tool Categories
| Category | Tools | Price Range |
|----------|-------|-------------|
| **Security** | Honeypot Check, Rug Pull Predictor, Audit, MEV Protection | $0.01$0.50 |
| **Intelligence** | OSINT Identity Hunt, Insider Analysis, Whale Tracking | $0.01$0.25 |
| **Trading** | Smart Money Alpha, Sniper Alert, Sentiment | $0.01$0.10 |
| **Forensics** | Deep Scan, Deployer History, Wallet Dossier | $0.05$0.25 |
| **DeFi** | Yield Scanner, Liquidity Depth, Impermanent Loss | $0.01$0.10 |
| **NFT** | Wash Trading Detection, Floor Analytics | $0.10 |
| **Social** | Twitter Profile/Timeline/Search, Discord Alpha | $0.01 |
## Free Trials
Every tool offers free trials without a wallet:
- **No wallet**: 1 free call per tool
- **Connected wallet**: 3 free calls per standard tool, 1 per premium tool
## Facilitators
| Facilitator | Settlement | Fee | Chains |
|-------------|-----------|-----|--------|
| **Coinbase CDP** | Instant | Free | Base, Polygon, Arbitrum, Solana |
| **Primev (mev-commit)** | Pre-confirmed | Free | Ethereum |
| **EIP-7702** | Instant | Free | BSC, Polygon, Avalanche, Fantom, Gnosis, Arbitrum, Optimism, Base |
| **PayAI** | Deferred | Small fee | Base, Solana |
| **TRON Self-Verify** | Instant | Free | TRON |
| **Bitcoin Self-Verify** | 1-conf | Free | Bitcoin |
| **AsterPay** | SEPA off-ramp | Fee | EUR fiats |
## Payment Wallets
- **EVM**: `0x1E3AC01d0fdb976179790BDD02823196A92705C9`
- **Solana**: `Gix4P9AmwcZRGzr2hCEME5m2QAvY86dBfm8c7e7MpFzv`
- **TRON**: `TY27YaMiiXdJ6JcjnLdfvoACxT16V6vu5q`
- **Bitcoin**: `17PiUt1iqfAfM7o7abHaR3GUZaNHBpn7Fz`
## Refund Policy
Full refund if a tool returns no data. Request within 48h via `POST /api/v1/x402/refund` with your transaction hash.
## Protocol Compliance
- [x] x402 v2 discovery at `/.well-known/x402`
- [x] HTTP 402 status for unpaid requests
- [x] Multiple facilitator support
- [x] Free trial tier
- [x] Refund endpoint
## Links
- **Discovery**: https://rugmunch.io/.well-known/x402
- **API Docs**: https://rugmunch.io/docs
- **Health**: https://rugmunch.io/health
- **GitHub**: https://github.com/Rug-Munch-Media-LLC