generated from nathanwoodburn/python-webserver-template
feat: Add initial server
This commit is contained in:
@@ -4,57 +4,410 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Nathan.Woodburn/</title>
|
||||
<link rel="icon" href="/assets/img/favicon.png" type="image/png">
|
||||
<title>Firepool | Woodburn</title>
|
||||
<link rel="icon" href="https://explorer.hns.au/assets/img/favicon.png" type="image/png">
|
||||
<link rel="stylesheet" href="/assets/css/index.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="spacer"></div>
|
||||
<div class="centre">
|
||||
<h1>Nathan.Woodburn/</h1>
|
||||
<span>The current date and time is {{datetime}}</span>
|
||||
</div>
|
||||
<div class="bg-orb orb-a"></div>
|
||||
<div class="bg-orb orb-b"></div>
|
||||
<div class="bg-grid"></div>
|
||||
|
||||
<main class="layout">
|
||||
<header class="hero">
|
||||
<div>
|
||||
<p class="kicker">Nathan.Woodburn/</p>
|
||||
<h1>Firepool</h1>
|
||||
<p class="subtitle">Live pool telemetry and miner drill-down for your address.</p>
|
||||
</div>
|
||||
<div class="hero-meta">
|
||||
<span id="lastUpdated">Last update: pending</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="stats-grid" id="poolSummary"></section>
|
||||
|
||||
<section class="panel search-panel">
|
||||
<div class="panel-head">
|
||||
<h2>Miner Search</h2>
|
||||
<p>Search by base address. Workers like <strong>address.rig1</strong> are included automatically.</p>
|
||||
</div>
|
||||
<form id="searchForm" class="search-row">
|
||||
<input id="addressInput" type="text" autocomplete="off" placeholder="Enter wallet address">
|
||||
<button type="submit">Search</button>
|
||||
</form>
|
||||
<div id="searchMessage" class="muted">Enter an address to load individual worker stats.</div>
|
||||
<div class="stats-grid compact" id="minerSummary"></div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-head">
|
||||
<h2>Workers</h2>
|
||||
<p id="workerCountLabel">Pool workers</p>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Hashrate</th>
|
||||
<th>Accepted</th>
|
||||
<th>Rejected</th>
|
||||
<th>Stale</th>
|
||||
<th>Blocks</th>
|
||||
<th>Est. Daily (FBC)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="workersBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-head">
|
||||
<h2>Blocks</h2>
|
||||
<p id="blockCountLabel">Recent block history</p>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Height</th>
|
||||
<th>Status</th>
|
||||
<th>Reward (FBC)</th>
|
||||
<th>Found By</th>
|
||||
<th>Confirmations</th>
|
||||
<th>Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="blocksBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-head">
|
||||
<h2>Balances</h2>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Address</th>
|
||||
<th>Balance (FBC)</th>
|
||||
<th>Total Earned (FBC)</th>
|
||||
<th>Total Paid (FBC)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="balancesBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-head">
|
||||
<h2>Payouts</h2>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Address</th>
|
||||
<th>Amount (FBC)</th>
|
||||
<th>Status</th>
|
||||
<th>TXID</th>
|
||||
<th>Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="payoutsBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
<footer class="footer">
|
||||
<p>Powered by <a href="https://nathan.woodburn.au" target="_blank" rel="noopener">Nathan.Woodburn/</a>
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
<div class="spacer"></div>
|
||||
<div class="centre">
|
||||
<h2 id="test-content-header">Pulling data</h2>
|
||||
<span class="test-content">This is a test content area that will be updated with data from the server.</span>
|
||||
<br>
|
||||
<br>
|
||||
<span class="test-content-timestamp">Timestamp: Waiting to pull data</span>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function fetchData() {
|
||||
// Fetch the data from the server
|
||||
fetch('/api/v1/data')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// Get the data header element
|
||||
const dataHeader = document.getElementById('test-content-header');
|
||||
// Update the header with the fetched data
|
||||
dataHeader.textContent = data.header;
|
||||
// Get the test content element
|
||||
const testContent = document.querySelector('.test-content');
|
||||
// Update the content with the fetched data
|
||||
testContent.textContent = data.content;
|
||||
const refreshMs = 10000;
|
||||
|
||||
// Get the timestamp element
|
||||
const timestampElement = document.querySelector('.test-content-timestamp');
|
||||
// Update the timestamp with the fetched data
|
||||
timestampElement.textContent = `Timestamp: ${data.timestamp}`;
|
||||
})
|
||||
.catch(error => console.error('Error fetching data:', error));
|
||||
function fmtInt(value) {
|
||||
return Number(value || 0).toLocaleString();
|
||||
}
|
||||
|
||||
// Initial fetch after 2 seconds
|
||||
setTimeout(fetchData, 2000);
|
||||
|
||||
// Then fetch every 2 seconds
|
||||
setInterval(fetchData, 2000);
|
||||
</script>
|
||||
function fmtFloat(value, digits = 3) {
|
||||
return Number(value || 0).toLocaleString(undefined, {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: digits
|
||||
});
|
||||
}
|
||||
|
||||
function fmtFbc(value) {
|
||||
return fmtFloat(value, 6);
|
||||
}
|
||||
|
||||
function fmtUptime(seconds) {
|
||||
const total = Number(seconds || 0);
|
||||
const d = Math.floor(total / 86400);
|
||||
const h = Math.floor((total % 86400) / 3600);
|
||||
const m = Math.floor((total % 3600) / 60);
|
||||
const s = total % 60;
|
||||
return `${d}d ${h}h ${m}m ${s}s`;
|
||||
}
|
||||
|
||||
function fmtTime(unixSeconds) {
|
||||
if (!unixSeconds) return "-";
|
||||
return new Date(Number(unixSeconds) * 1000).toLocaleString();
|
||||
}
|
||||
|
||||
function renderCards(containerId, cards) {
|
||||
const el = document.getElementById(containerId);
|
||||
el.innerHTML = cards.map(card => `
|
||||
<article class="stat-card">
|
||||
<span>${card.label}</span>
|
||||
<strong>${card.value}</strong>
|
||||
</article>
|
||||
`).join("");
|
||||
}
|
||||
|
||||
function renderRows(bodyId, rowsHtml, emptyText, cols) {
|
||||
const body = document.getElementById(bodyId);
|
||||
if (!rowsHtml.length) {
|
||||
body.innerHTML = `<tr><td colspan="${cols}" class="empty">${emptyText}</td></tr>`;
|
||||
return;
|
||||
}
|
||||
body.innerHTML = rowsHtml.join("");
|
||||
}
|
||||
|
||||
function shortTxid(txid) {
|
||||
if (!txid) return "-";
|
||||
if (txid.length <= 18) return txid;
|
||||
return `${txid.slice(0, 10)}...${txid.slice(-6)}`;
|
||||
}
|
||||
|
||||
function refreshStamp() {
|
||||
const stamp = document.getElementById("lastUpdated");
|
||||
stamp.textContent = `Last update: ${new Date().toLocaleString()}`;
|
||||
}
|
||||
|
||||
async function loadOverview() {
|
||||
const res = await fetch("/api/v1/pool/overview");
|
||||
if (!res.ok) {
|
||||
throw new Error(`Overview request failed (${res.status})`);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
const summary = data.summary || {};
|
||||
refreshStamp();
|
||||
|
||||
renderCards("poolSummary", [
|
||||
{ label: "Pool Hashrate", value: fmtFloat(summary.hashrate) + " H/s" },
|
||||
{ label: "Network Hashrate", value: fmtFloat(summary.network_hashrate, 2) + " H/s" },
|
||||
{ label: "Block Time", value: fmtInt(summary.block_time_seconds) + " s" },
|
||||
{ label: "Block Reward", value: fmtFbc(summary.current_block_reward_fbc) + " FBC" },
|
||||
{ label: "Blocks / Day", value: fmtInt(summary.blocks_per_day) },
|
||||
{ label: "Network Emission / Day", value: fmtFbc(summary.daily_network_emission_fbc) + " FBC" },
|
||||
{ label: "Est. Reward / H/s / Day", value: fmtFloat(summary.estimated_reward_per_hash_daily_fbc, 2) + " FBC" },
|
||||
{ label: "Active Workers", value: fmtInt(summary.workers) },
|
||||
{ label: "Blocks Found", value: fmtInt(summary.blocks_found) },
|
||||
{ label: "Fee", value: `${fmtFloat(summary.fee, 2)}%` },
|
||||
{ label: "Total Balance (FBC)", value: fmtFbc(summary.total_balance_fbc) },
|
||||
{ label: "Total Paid (FBC)", value: fmtFbc(summary.total_paid_fbc) }
|
||||
]);
|
||||
|
||||
const workers = Array.isArray(data.workers) ? data.workers : [];
|
||||
const blocks = Array.isArray(data.blocks) ? data.blocks : [];
|
||||
const balances = Array.isArray(data.balances) ? data.balances : [];
|
||||
const payouts = Array.isArray(data.payouts) ? data.payouts : [];
|
||||
|
||||
document.getElementById("workerCountLabel").textContent = `${workers.length} workers in pool`;
|
||||
document.getElementById("blockCountLabel").textContent = `${blocks.length} blocks recorded`;
|
||||
|
||||
renderRows(
|
||||
"workersBody",
|
||||
workers.map(w => `
|
||||
<tr>
|
||||
<td data-label="Username">${w.username || "-"}</td>
|
||||
<td data-label="Hashrate">${fmtFloat(w.hashrate, 6)}</td>
|
||||
<td data-label="Accepted">${fmtInt(w.accepted)}</td>
|
||||
<td data-label="Rejected">${fmtInt(w.rejected)}</td>
|
||||
<td data-label="Stale">${fmtInt(w.stale)}</td>
|
||||
<td data-label="Blocks">${fmtInt(w.blocks)}</td>
|
||||
<td data-label="Est. Daily (FBC)">${w.estimated_reward ? fmtFloat(w.estimated_reward.daily_fbc, 2) : "-"}</td>
|
||||
</tr>
|
||||
`),
|
||||
"No workers available.",
|
||||
7
|
||||
);
|
||||
|
||||
renderRows(
|
||||
"blocksBody",
|
||||
blocks.map(b => `
|
||||
<tr>
|
||||
<td data-label="Height">${fmtInt(b.height)}</td>
|
||||
<td data-label="Status"><span class="pill ${String(b.status || "unknown").toLowerCase()}">${b.status || "unknown"}</span></td>
|
||||
<td data-label="Reward (FBC)">${fmtFbc(Number(b.reward || 0) / 1000000)}</td>
|
||||
<td data-label="Found By">${b.found_by || "-"}</td>
|
||||
<td data-label="Confirmations">${fmtInt(b.confirmations)}</td>
|
||||
<td data-label="Time">${fmtTime(b.time)}</td>
|
||||
</tr>
|
||||
`),
|
||||
"No blocks available.",
|
||||
6
|
||||
);
|
||||
|
||||
renderRows(
|
||||
"balancesBody",
|
||||
balances.map(b => `
|
||||
<tr>
|
||||
<td data-label="Address">${b.address || "-"}</td>
|
||||
<td data-label="Balance (FBC)">${fmtFbc(b.fbc ?? Number(b.balance || 0) / 1000000)}</td>
|
||||
<td data-label="Total Earned (FBC)">${fmtFbc(Number(b.total_earned || 0) / 1000000)}</td>
|
||||
<td data-label="Total Paid (FBC)">${fmtFbc(Number(b.total_paid || 0) / 1000000)}</td>
|
||||
</tr>
|
||||
`),
|
||||
"No balances available.",
|
||||
4
|
||||
);
|
||||
|
||||
renderRows(
|
||||
"payoutsBody",
|
||||
payouts.map(p => `
|
||||
<tr>
|
||||
<td data-label="Address">${p.address || "-"}</td>
|
||||
<td data-label="Amount (FBC)">${fmtFbc(Number(p.amount || 0) / 1000000)}</td>
|
||||
<td data-label="Status"><span class="pill ${String(p.status || "pending").toLowerCase()}">${p.status || "pending"}</span></td>
|
||||
<td data-label="TXID" title="${p.txid || ""}">${shortTxid(p.txid)}</td>
|
||||
<td data-label="Time">${fmtTime(p.time)}</td>
|
||||
</tr>
|
||||
`),
|
||||
"No payouts available.",
|
||||
5
|
||||
);
|
||||
}
|
||||
|
||||
async function loadMiner(address) {
|
||||
const msg = document.getElementById("searchMessage");
|
||||
if (!address) {
|
||||
msg.textContent = "Enter an address to load individual worker stats.";
|
||||
document.getElementById("minerSummary").innerHTML = "";
|
||||
return;
|
||||
}
|
||||
|
||||
msg.textContent = `Loading miner data for ${address}...`;
|
||||
|
||||
const res = await fetch(`/api/v1/pool/miner?address=${encodeURIComponent(address)}`);
|
||||
if (!res.ok) {
|
||||
const errText = await res.text();
|
||||
throw new Error(`Miner request failed (${res.status}): ${errText}`);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
const summary = data.summary || {};
|
||||
|
||||
msg.textContent = `Showing ${summary.matching_workers || 0} worker(s) for ${data.address}.`;
|
||||
renderCards("minerSummary", [
|
||||
{ label: "Matching Workers", value: fmtInt(summary.matching_workers) },
|
||||
{ label: "Hashrate", value: fmtFloat(summary.hashrate) },
|
||||
{ label: "Accepted", value: fmtInt(summary.accepted) },
|
||||
{ label: "Rejected", value: fmtInt(summary.rejected) },
|
||||
{ label: "Stale", value: fmtInt(summary.stale) },
|
||||
{ label: "Blocks", value: fmtInt(summary.blocks_found) },
|
||||
{ label: "Est. Daily (FBC)", value: fmtFloat(summary.estimated_daily_reward_fbc, 2) },
|
||||
{ label: "Est. Weekly (FBC)", value: fmtFloat(summary.estimated_weekly_reward_fbc, 2) },
|
||||
{ label: "Pool Share", value: fmtFloat(summary.pool_share_pct, 2) + "%" },
|
||||
{ label: "Balance (FBC)", value: fmtFbc(summary.total_balance_fbc) },
|
||||
{ label: "Paid (FBC)", value: fmtFbc(summary.total_paid_fbc) }
|
||||
]);
|
||||
|
||||
renderRows(
|
||||
"workersBody",
|
||||
(data.workers || []).map(w => `
|
||||
<tr>
|
||||
<td data-label="Username">${w.username || "-"}</td>
|
||||
<td data-label="Hashrate">${fmtFloat(w.hashrate, 6)}</td>
|
||||
<td data-label="Accepted">${fmtInt(w.accepted)}</td>
|
||||
<td data-label="Rejected">${fmtInt(w.rejected)}</td>
|
||||
<td data-label="Stale">${fmtInt(w.stale)}</td>
|
||||
<td data-label="Blocks">${fmtInt(w.blocks)}</td>
|
||||
<td data-label="Est. Daily (FBC)">${w.estimated_reward ? fmtFloat(w.estimated_reward.daily_fbc, 2) : "-"}</td>
|
||||
</tr>
|
||||
`),
|
||||
"No matching workers for this address.",
|
||||
7
|
||||
);
|
||||
|
||||
renderRows(
|
||||
"blocksBody",
|
||||
(data.blocks || []).map(b => `
|
||||
<tr>
|
||||
<td data-label="Height">${fmtInt(b.height)}</td>
|
||||
<td data-label="Status"><span class="pill ${String(b.status || "unknown").toLowerCase()}">${b.status || "unknown"}</span></td>
|
||||
<td data-label="Reward (FBC)">${fmtFbc(Number(b.reward || 0) / 1000000)}</td>
|
||||
<td data-label="Found By">${b.found_by || "-"}</td>
|
||||
<td data-label="Confirmations">${fmtInt(b.confirmations)}</td>
|
||||
<td data-label="Time">${fmtTime(b.time)}</td>
|
||||
</tr>
|
||||
`),
|
||||
"No matching blocks for this address.",
|
||||
6
|
||||
);
|
||||
|
||||
renderRows(
|
||||
"balancesBody",
|
||||
(data.balances || []).map(b => `
|
||||
<tr>
|
||||
<td data-label="Address">${b.address || "-"}</td>
|
||||
<td data-label="Balance (FBC)">${fmtFbc(b.fbc ?? Number(b.balance || 0) / 1000000)}</td>
|
||||
<td data-label="Total Earned (FBC)">${fmtFbc(Number(b.total_earned || 0) / 1000000)}</td>
|
||||
<td data-label="Total Paid (FBC)">${fmtFbc(Number(b.total_paid || 0) / 1000000)}</td>
|
||||
</tr>
|
||||
`),
|
||||
"No matching balances for this address.",
|
||||
4
|
||||
);
|
||||
|
||||
renderRows(
|
||||
"payoutsBody",
|
||||
(data.payouts || []).map(p => `
|
||||
<tr>
|
||||
<td data-label="Address">${p.address || "-"}</td>
|
||||
<td data-label="Amount (FBC)">${fmtFbc(Number(p.amount || 0) / 1000000)}</td>
|
||||
<td data-label="Status"><span class="pill ${String(p.status || "pending").toLowerCase()}">${p.status || "pending"}</span></td>
|
||||
<td data-label="TXID" title="${p.txid || ""}">${shortTxid(p.txid)}</td>
|
||||
<td data-label="Time">${fmtTime(p.time)}</td>
|
||||
</tr>
|
||||
`),
|
||||
"No matching payouts for this address.",
|
||||
5
|
||||
);
|
||||
}
|
||||
|
||||
async function refreshAll() {
|
||||
try {
|
||||
await loadOverview();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
document.getElementById("searchMessage").textContent = `Error loading pool data: ${err.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("searchForm").addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
const address = document.getElementById("addressInput").value.trim();
|
||||
try {
|
||||
await loadMiner(address);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
document.getElementById("searchMessage").textContent = `Error loading miner data: ${err.message}`;
|
||||
}
|
||||
});
|
||||
|
||||
refreshAll();
|
||||
setInterval(refreshAll, refreshMs);
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user