montana/_internal-private/explorer-snapshot/index.html
2026-05-26 21:14:51 +03:00

228 lines
11 KiB
HTML

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Montana Network Explorer</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, system-ui, "SF Pro", "Segoe UI", Roboto, sans-serif;
background: #0a0a0a;
color: #e0e0e0;
min-height: 100vh;
padding: 24px;
}
.container { max-width: 1200px; margin: 0 auto; }
header {
display: flex; align-items: baseline; justify-content: space-between;
border-bottom: 1px solid #2a2a2a; padding-bottom: 16px; margin-bottom: 24px;
flex-wrap: wrap; gap: 12px;
}
h1 { font-size: 28px; font-weight: 700; letter-spacing: -0.5px; }
h1 .accent { color: #f0c060; }
.updated { font-size: 13px; color: #888; font-family: "SF Mono", Menlo, monospace; }
.updated .live { color: #4ade80; }
.updated .stale { color: #f97316; }
.summary {
display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px; margin-bottom: 32px;
}
.stat {
background: #151515; border: 1px solid #2a2a2a; border-radius: 8px;
padding: 16px;
}
.stat-label { font-size: 11px; color: #888; text-transform: uppercase; letter-spacing: 0.5px; }
.stat-value { font-size: 26px; font-weight: 600; margin-top: 4px; font-family: "SF Mono", Menlo, monospace; }
.stat-value .accent { color: #f0c060; }
.nodes {
display: grid; grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
gap: 16px;
}
.node {
background: #141414; border: 1px solid #2a2a2a; border-radius: 12px;
padding: 20px; transition: border-color 0.2s;
}
.node:hover { border-color: #404040; }
.node-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
.node-label { font-size: 18px; font-weight: 700; }
.node-host { font-family: "SF Mono", Menlo, monospace; font-size: 12px; color: #888; }
.badge { padding: 4px 10px; border-radius: 999px; font-size: 11px; font-weight: 600; }
.badge.active { background: rgba(74, 222, 128, 0.15); color: #4ade80; }
.badge.unreachable { background: rgba(248, 113, 113, 0.15); color: #f87171; }
.badge.no_data { background: rgba(251, 191, 36, 0.15); color: #fbbf24; }
.row { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #1f1f1f; }
.row:last-child { border-bottom: none; }
.row-label { color: #888; font-size: 13px; }
.row-value { font-family: "SF Mono", Menlo, monospace; font-size: 13px; text-align: right; }
.row-value.window { color: #f0c060; font-weight: 600; }
.row-value.balance { color: #4ade80; }
.row-value.id { font-size: 11px; color: #777; }
footer {
margin-top: 48px; padding-top: 16px; border-top: 1px solid #2a2a2a;
font-size: 12px; color: #666; line-height: 1.7;
}
footer a { color: #888; text-decoration: none; border-bottom: 1px dotted #444; }
footer a:hover { color: #f0c060; }
.pulse {
display: inline-block; width: 8px; height: 8px; border-radius: 50%;
background: #4ade80; margin-right: 6px;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
.error { color: #f87171; font-size: 12px; margin-top: 8px; font-family: "SF Mono", Menlo, monospace; }
</style>
<link rel="icon" type="image/svg+xml" href="https://montana.quest/messenger/favicon.svg">
<link rel="icon" type="image/png" sizes="32x32" href="https://montana.quest/messenger/favicon-32.png">
<link rel="apple-touch-icon" sizes="180x180" href="https://montana.quest/messenger/apple-touch-icon.png">
<meta name="theme-color" content="#0c0a08">
</head>
<body><a class="home-link" href="/home"><span class="home-logo">Ɉ</span><span class="home-text">Монтана</span></a>
<style>
.home-link {
position: fixed;
top: calc(env(safe-area-inset-top, 0) + 0.75rem);
left: calc(env(safe-area-inset-left, 0) + 0.75rem);
z-index: 200;
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.875rem;
border: 1px solid #2a2520;
border-radius: 0.625rem;
background: rgba(20,17,13,0.9);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
color: #e8e0d0;
text-decoration: none;
font-family: ui-serif, Georgia, serif;
font-size: 0.9375rem;
transition: background 0.15s, border-color 0.15s;
}
.home-link:hover { background: rgba(34,28,22,0.95); border-color: #8e6824; }
.home-link .home-logo { color: #ca9335; font-size: 1.25rem; line-height: 1; }
.home-link .home-text { letter-spacing: 0.02em; }
@media (max-width: 480px) {
.home-link { padding: 0.5rem 0.625rem; font-size: 0.875rem; }
.home-link .home-text { display: none; }
}
</style>
<div class="container">
<header>
<h1>Montana <span class="accent">Network</span> Explorer</h1>
<div class="updated">
<span class="pulse"></span>
<span id="updated">loading…</span>
</div>
</header>
<div class="summary" id="summary"></div>
<div class="nodes" id="nodes"></div>
<h2 id="discovered-title" style="font-size:18px;font-weight:600;margin:32px 0 12px 0;letter-spacing:-0.3px;color:#f0c060;display:none;">Discovered peers</h2>
<div class="discovered" id="discovered"></div>
<footer>
Montana Genesis cohort — three nodes in Moscow, Helsinki and Frankfurt, full 6/6 pairwise mesh over Noise_PQ XX
(ML-KEM-768 + ML-DSA-65 + ChaCha20-Poly1305). Emission 13&nbsp;Ɉ per window (≈60&nbsp;s), τ₂&nbsp;=&nbsp;20160 windows (≈14&nbsp;days).
Newly-joining nodes auto-appear in the "Discovered peers" panel below once the live mesh witnesses them.<br>
<a href="https://github.com/efir369999/Montana" target="_blank" rel="noopener">github.com/efir369999/Montana</a>
· refresh every 60&nbsp;s · 1&nbsp;Ɉ&nbsp;=&nbsp;10⁹&nbsp;
</footer>
</div>
<script>
const fmt = {
num: n => n.toLocaleString('ru-RU'),
ij: nj => (nj / 1e9).toLocaleString('ru-RU', { maximumFractionDigits: 3 }) + ' Ɉ',
ago: ts => {
const s = Math.max(0, Math.floor(Date.now() / 1000) - ts);
if (s < 60) return s + ' s ago';
if (s < 3600) return Math.floor(s / 60) + ' min ' + (s % 60) + ' s ago';
return Math.floor(s / 3600) + ' h ' + Math.floor((s % 3600) / 60) + ' min назад';
},
shortHex: h => h ? h.slice(0, 16) + '…' + h.slice(-8) : '—',
};
async function refresh() {
try {
const r = await fetch('data.json?t=' + Date.now());
const d = await r.json();
const stale = (Math.floor(Date.now() / 1000) - d.updated_unix) > 120;
const upEl = document.getElementById('updated');
upEl.innerHTML = `<span class="${stale ? 'stale' : 'live'}">${fmt.ago(d.updated_unix)}</span>`;
const sum = d.network_summary;
const totalSupplyIj = (sum.total_supply_nj / 1e9).toLocaleString('ru-RU', { maximumFractionDigits: 0 });
document.getElementById('summary').innerHTML = `
<div class="stat"><div class="stat-label">Active Genesis</div><div class="stat-value"><span class="accent">${sum.active_nodes}</span> / ${sum.total_nodes}</div></div>
<div class="stat"><div class="stat-label">Network window</div><div class="stat-value"><span class="accent">${fmt.num(sum.max_window)}</span></div></div>
<div class="stat"><div class="stat-label">Emission (closed-form)</div><div class="stat-value"><span class="accent">${totalSupplyIj}</span> Ɉ</div></div>
<div class="stat"><div class="stat-label">Discovered peers</div><div class="stat-value"><span class="accent">${sum.discovered_peer_count || 0}</span></div></div>
`;
document.getElementById('nodes').innerHTML = d.nodes.map(n => {
if (n.status !== 'active') {
return `<div class="node">
<div class="node-head">
<span><span class="node-label">${n.label}</span> <span class="node-host">${n.host}</span></span>
<span class="badge ${n.status}">${n.status}</span>
</div>
<div class="error">${n.error || 'no data'}</div>
</div>`;
}
return `<div class="node">
<div class="node-head">
<span><span class="node-label">${n.label}</span> <span class="node-host">${n.host}</span></span>
<span class="badge active">${n.phase}</span>
</div>
<div class="row"><span class="row-label">Current window</span><span class="row-value window">${fmt.num(n.current_window)}</span></div>
<div class="row"><span class="row-label">D (SHA-256 iterations)</span><span class="row-value">${fmt.num(n.D)}</span></div>
<div class="row"><span class="row-label">Operator balance</span><span class="row-value balance">${fmt.ij(n.balance_nj)}</span></div>
<div class="row"><span class="row-label">Supply (closed-form)</span><span class="row-value">${fmt.ij(n.supply_nj)}</span></div>
<div class="row"><span class="row-label">AccountTable</span><span class="row-value">${fmt.num(n.account_table)} records</span></div>
<div class="row"><span class="row-label">NodeTable</span><span class="row-value">${fmt.num(n.node_table)} records</span></div>
<div class="row"><span class="row-label">account_id</span><span class="row-value id">${fmt.shortHex(n.account_id)}</span></div>
<div class="row"><span class="row-label">node_id</span><span class="row-value id">${fmt.shortHex(n.node_id)}</span></div>
</div>`;
}).join('');
// Discovered peers (auto-detected via journals of the Genesis cohort).
const dpEl = document.getElementById('discovered');
const dpTitleEl = document.getElementById('discovered-title');
const dp = Array.isArray(d.discovered_peers) ? d.discovered_peers : [];
if (dp.length === 0) {
dpEl.innerHTML = '';
dpTitleEl.style.display = 'none';
} else {
dpTitleEl.style.display = '';
dpEl.innerHTML = dp.map(p => {
const ageStr = p.last_heartbeat_seconds_ago == null
? '—'
: (p.last_heartbeat_seconds_ago < 60
? p.last_heartbeat_seconds_ago + ' s ago'
: Math.floor(p.last_heartbeat_seconds_ago / 60) + ' min ago');
const witnessStr = Array.isArray(p.witnessed_by) ? p.witnessed_by.join(', ') : '—';
return `<div class="node">
<div class="node-head">
<span><span class="node-label">${p.peer_id.slice(0, 12)}${p.peer_id.slice(-8)}</span> <span class="node-host">${p.remote_ip}</span></span>
<span class="badge ${p.status === 'active' ? 'active' : 'no_data'}">${p.status}</span>
</div>
<div class="row"><span class="row-label">Last heartbeat</span><span class="row-value">${ageStr}</span></div>
<div class="row"><span class="row-label">Witnessed by</span><span class="row-value">${witnessStr}</span></div>
<div class="row"><span class="row-label">XX PeerId</span><span class="row-value id">${p.peer_id}</span></div>
</div>`;
}).join('');
}
} catch (e) {
document.getElementById('updated').innerHTML = '<span class="stale">⚠ load error</span>';
}
}
refresh();
setInterval(refresh, 60000);
</script>
</body>
</html>