rmi-backend/static/rugcharts.html

433 lines
25 KiB
HTML

<!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>