montana/Russian/Site/explorer/index.html
2026-05-28 23:44:50 +03:00

126 lines
6.2 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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