montana/Russian/Site/explorer/index.html

126 lines
6.2 KiB
HTML
Raw Normal View History

2026-05-28 23:44:50 +03:00
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Montana Explorer</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
:root { --bg:#000; --fg:#e8e8e8; --acc:#d4af37; --dim:#666; --card:#0d0d0d; --hl:#1a1a1a; }
* { box-sizing:border-box; }
body { background:var(--bg); color:var(--fg); font:14px/1.5 -apple-system,system-ui,sans-serif; margin:0; padding:0; }
header { padding:24px; border-bottom:1px solid var(--hl); display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; }
h1 { margin:0; color:var(--acc); font-weight:300; font-size:24px; letter-spacing:1px; }
h2 { color:var(--acc); font-weight:400; font-size:16px; margin:24px 0 8px; }
.grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(180px,1fr)); gap:12px; padding:24px; }
.stat { background:var(--card); padding:16px; border-radius:4px; border:1px solid var(--hl); }
.stat .label { color:var(--dim); font-size:11px; text-transform:uppercase; letter-spacing:1px; }
.stat .value { color:var(--acc); font-size:28px; font-weight:300; margin-top:4px; font-variant-numeric:tabular-nums; }
.section { padding:0 24px 24px; }
table { width:100%; border-collapse:collapse; background:var(--card); border:1px solid var(--hl); border-radius:4px; overflow:hidden; }
th, td { text-align:left; padding:10px 12px; border-bottom:1px solid var(--hl); font-variant-numeric:tabular-nums; }
th { background:var(--hl); color:var(--acc); font-weight:500; font-size:11px; text-transform:uppercase; letter-spacing:1px; }
td.mono { font-family:'SF Mono',Menlo,monospace; font-size:12px; color:var(--dim); }
.hex { display:inline-block; max-width:120px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; vertical-align:bottom; }
tr:hover td { background:var(--hl); }
.online { color:#3a7; }
.search { padding:8px 12px; background:var(--card); border:1px solid var(--hl); color:var(--fg); border-radius:4px; width:240px; }
footer { color:var(--dim); padding:24px; text-align:center; font-size:11px; border-top:1px solid var(--hl); }
a { color:var(--acc); text-decoration:none; }
a:hover { text-decoration:underline; }
#detail { display:none; }
.tag { display:inline-block; padding:2px 8px; background:var(--hl); color:var(--acc); border-radius:3px; font-size:11px; }
</style>
</head>
<body>
<header>
<h1>Ɉ MONTANA EXPLORER</h1>
<input class="search" id="searchBox" placeholder="окно (window number)" type="number" min="0">
</header>
<div class="grid" id="stats"></div>
<div class="section">
<h2>Узлы (NodeTable)</h2>
<table id="nodes-tbl">
<thead><tr><th>node_id</th><th>chain_length</th><th>start_window</th><th>last_conf</th></tr></thead>
<tbody></tbody>
</table>
</div>
<div class="section">
<h2>Последние Proposals</h2>
<table id="props-tbl">
<thead><tr><th>window</th><th>proposer</th><th>winner</th><th>state_root</th><th>envelope</th><th>bundles</th></tr></thead>
<tbody></tbody>
</table>
</div>
<div class="section" id="detail">
<h2>Детали окна <span id="detail-w"></span></h2>
<pre id="detail-body" style="background:var(--card);padding:16px;border-radius:4px;border:1px solid var(--hl);overflow-x:auto;font-size:11px;"></pre>
</div>
<footer>
Montana Reference Implementation · обновление каждые 5с · <span class="online" id="liveness"></span> <span id="ts"></span>
</footer>
<script>
const API = "/montana-api";
const hex16 = h => h ? `<span class="hex" title="${h}">${h.slice(0,16)}…</span>` : "";
const fmt = n => Number(n).toLocaleString("ru-RU");
async function load() {
try {
const st = await (await fetch(`${API}/status`)).json();
document.getElementById("stats").innerHTML = `
<div class="stat"><div class="label">Текущее окно</div><div class="value">${fmt(st.current_window)}</div></div>
<div class="stat"><div class="label">Цементировано</div><div class="value">${fmt(st.last_cemented_window)}</div></div>
<div class="stat"><div class="label">Узлов в NodeTable</div><div class="value">${fmt(st.nodes)}</div></div>
<div class="stat"><div class="label">Кандидатов</div><div class="value">${fmt(st.candidates)}</div></div>
<div class="stat"><div class="label">Аккаунтов</div><div class="value">${fmt(st.accounts)}</div></div>
<div class="stat"><div class="label">Архив Proposals</div><div class="value">${fmt(st.proposals_archived)}</div></div>
`;
const ns = await (await fetch(`${API}/nodes`)).json();
document.querySelector("#nodes-tbl tbody").innerHTML = ns.nodes.map(n => `
<tr><td class="mono">${hex16(n.node_id)}</td><td>${fmt(n.chain_length)}</td><td>${fmt(n.start_window)}</td><td>${fmt(n.last_confirmation_window)}</td></tr>
`).join("");
const ps = await (await fetch(`${API}/proposals?limit=25`)).json();
document.querySelector("#props-tbl tbody").innerHTML = ps.proposals.map(p => `
<tr onclick="showDetail(${p.window_index})" style="cursor:pointer">
<td><a href="#w${p.window_index}">${fmt(p.window_index)}</a></td>
<td class="mono">${hex16(p.proposer_node_id)}</td>
<td class="mono">${hex16(p.winner_id)}</td>
<td class="mono">${hex16(p.state_root)}</td>
<td>${fmt(p.envelope_size)} B</td>
<td><span class="tag">${p.bundle_count}</span></td>
</tr>
`).join("");
document.getElementById("liveness").className = "online";
document.getElementById("ts").textContent = new Date(st.ts * 1000).toLocaleString("ru-RU");
} catch(e) {
document.getElementById("liveness").className = "";
document.getElementById("liveness").style.color = "#a33";
document.getElementById("ts").textContent = "offline: " + e.message;
}
}
async function showDetail(w) {
try {
const d = await (await fetch(`${API}/proposal/${w}`)).json();
document.getElementById("detail-w").textContent = w;
document.getElementById("detail-body").textContent = JSON.stringify(d, null, 2);
document.getElementById("detail").style.display = "block";
} catch(e) {
alert("ошибка: " + e.message);
}
}
document.getElementById("searchBox").addEventListener("keydown", e => {
if (e.key === "Enter") { const v = parseInt(e.target.value); if (!isNaN(v)) showDetail(v); }
});
load(); setInterval(load, 5000);
</script>
</body>
</html>