188 lines
4.6 KiB
HTML
188 lines
4.6 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Homelab Devnet Explorer</title>
|
|
<style>
|
|
:root {
|
|
color-scheme: light dark;
|
|
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
body {
|
|
margin: 0;
|
|
background: #111827;
|
|
color: #f9fafb;
|
|
}
|
|
|
|
main {
|
|
width: min(980px, calc(100% - 32px));
|
|
margin: 0 auto;
|
|
padding: 32px 0;
|
|
}
|
|
|
|
h1 {
|
|
margin: 0 0 8px;
|
|
font-size: 1.8rem;
|
|
}
|
|
|
|
.toolbar,
|
|
.grid,
|
|
pre {
|
|
border: 1px solid #374151;
|
|
background: #1f2937;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.toolbar {
|
|
display: flex;
|
|
gap: 8px;
|
|
align-items: center;
|
|
padding: 12px;
|
|
margin: 24px 0;
|
|
}
|
|
|
|
input {
|
|
flex: 1;
|
|
min-width: 0;
|
|
padding: 10px;
|
|
border: 1px solid #4b5563;
|
|
border-radius: 6px;
|
|
background: #111827;
|
|
color: #f9fafb;
|
|
}
|
|
|
|
button {
|
|
padding: 10px 14px;
|
|
border: 0;
|
|
border-radius: 6px;
|
|
background: #38bdf8;
|
|
color: #0f172a;
|
|
font-weight: 700;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
gap: 1px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.metric {
|
|
padding: 16px;
|
|
background: #111827;
|
|
}
|
|
|
|
.metric span {
|
|
display: block;
|
|
color: #9ca3af;
|
|
font-size: 0.78rem;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.metric strong {
|
|
display: block;
|
|
margin-top: 6px;
|
|
font-size: 1.2rem;
|
|
word-break: break-word;
|
|
}
|
|
|
|
pre {
|
|
padding: 16px;
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
min-height: 120px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<main>
|
|
<h1>Homelab Devnet Explorer</h1>
|
|
<p>Local Anvil chain view. Use only dev wallets and dev keys.</p>
|
|
|
|
<section class="toolbar">
|
|
<input id="rpcUrl" value="http://127.0.0.1:8545" aria-label="JSON-RPC URL">
|
|
<button id="refresh">Refresh</button>
|
|
</section>
|
|
|
|
<section class="grid" aria-label="Devnet metrics">
|
|
<div class="metric">
|
|
<span>Chain ID</span>
|
|
<strong id="chainId">unknown</strong>
|
|
</div>
|
|
<div class="metric">
|
|
<span>Latest Block</span>
|
|
<strong id="blockNumber">unknown</strong>
|
|
</div>
|
|
<div class="metric">
|
|
<span>Gas Price</span>
|
|
<strong id="gasPrice">unknown</strong>
|
|
</div>
|
|
<div class="metric">
|
|
<span>Peer Count</span>
|
|
<strong id="peerCount">unknown</strong>
|
|
</div>
|
|
</section>
|
|
|
|
<h2>Latest Block</h2>
|
|
<pre id="latestBlock">Waiting for refresh...</pre>
|
|
</main>
|
|
|
|
<script>
|
|
const fields = {
|
|
chainId: document.querySelector("#chainId"),
|
|
blockNumber: document.querySelector("#blockNumber"),
|
|
gasPrice: document.querySelector("#gasPrice"),
|
|
peerCount: document.querySelector("#peerCount"),
|
|
latestBlock: document.querySelector("#latestBlock"),
|
|
rpcUrl: document.querySelector("#rpcUrl")
|
|
};
|
|
|
|
async function rpc(method, params = []) {
|
|
const response = await fetch(fields.rpcUrl.value, {
|
|
method: "POST",
|
|
headers: { "content-type": "application/json" },
|
|
body: JSON.stringify({ jsonrpc: "2.0", method, params, id: Date.now() })
|
|
});
|
|
const payload = await response.json();
|
|
if (payload.error) {
|
|
throw new Error(payload.error.message || JSON.stringify(payload.error));
|
|
}
|
|
return payload.result;
|
|
}
|
|
|
|
function hexToDecimal(value) {
|
|
return Number.parseInt(value, 16).toString();
|
|
}
|
|
|
|
async function refresh() {
|
|
fields.latestBlock.textContent = "Loading...";
|
|
try {
|
|
const [chainId, blockNumber, gasPrice, peerCount] = await Promise.all([
|
|
rpc("eth_chainId"),
|
|
rpc("eth_blockNumber"),
|
|
rpc("eth_gasPrice"),
|
|
rpc("net_peerCount")
|
|
]);
|
|
fields.chainId.textContent = hexToDecimal(chainId);
|
|
fields.blockNumber.textContent = hexToDecimal(blockNumber);
|
|
fields.gasPrice.textContent = `${hexToDecimal(gasPrice)} wei`;
|
|
fields.peerCount.textContent = hexToDecimal(peerCount);
|
|
|
|
const block = await rpc("eth_getBlockByNumber", [blockNumber, false]);
|
|
fields.latestBlock.textContent = JSON.stringify(block, null, 2);
|
|
} catch (error) {
|
|
fields.latestBlock.textContent = error.message;
|
|
}
|
|
}
|
|
|
|
document.querySelector("#refresh").addEventListener("click", refresh);
|
|
refresh();
|
|
setInterval(refresh, 10000);
|
|
</script>
|
|
</body>
|
|
</html>
|