228 lines
11 KiB
HTML
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 Ɉ per window (≈60 s), τ₂ = 20160 windows (≈14 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 s · 1 Ɉ = 10⁹ nɈ
|
|
</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>
|