generated from nathanwoodburn/python-webserver-template
feat: Add initial server
This commit is contained in:
@@ -1,41 +1,392 @@
|
||||
:root {
|
||||
--bg: #0a0e1a;
|
||||
--bg-alt: #141829;
|
||||
--panel: rgba(20, 24, 41, 0.75);
|
||||
--panel-border: rgba(255, 107, 53, 0.25);
|
||||
--text: #f5f5f5;
|
||||
--muted: #a0a0a0;
|
||||
--accent: #ff6b35;
|
||||
--warning: #ffa500;
|
||||
--danger: #ff4444;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #000000;
|
||||
color: #ffffff;
|
||||
}
|
||||
h1 {
|
||||
font-size: 50px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: var(--text);
|
||||
background: radial-gradient(circle at top left, #2a2a3e 0%, var(--bg) 48%, #0a0a0f 100%);
|
||||
font-family: "Space Grotesk", "Segoe UI", sans-serif;
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
}
|
||||
.centre {
|
||||
margin-top: 10%;
|
||||
|
||||
.bg-grid {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
background-image: linear-gradient(rgba(255, 107, 53, 0.06) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255, 107, 53, 0.06) 1px, transparent 1px);
|
||||
background-size: 42px 42px;
|
||||
mask-image: radial-gradient(circle at center, black 30%, transparent 100%);
|
||||
}
|
||||
|
||||
.bg-orb {
|
||||
position: fixed;
|
||||
border-radius: 999px;
|
||||
filter: blur(70px);
|
||||
opacity: 0.5;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.orb-a {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
background: #ff6b35;
|
||||
top: 7%;
|
||||
left: 4%;
|
||||
}
|
||||
|
||||
.orb-b {
|
||||
width: 260px;
|
||||
height: 260px;
|
||||
background: #ff8c42;
|
||||
right: 7%;
|
||||
bottom: 10%;
|
||||
}
|
||||
|
||||
.layout {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 28px 16px 48px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.hero {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
align-items: end;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.kicker {
|
||||
margin: 0;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--accent);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 6px 0;
|
||||
font-size: clamp(30px, 5vw, 52px);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.hero-meta {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.stats-grid.compact {
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(180deg, rgba(9, 45, 79, 0.84), var(--panel));
|
||||
padding: 10px 12px;
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.stat-card span {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.stat-card strong {
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
letter-spacing: 0.02em;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.panel {
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 16px;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
background: var(--panel);
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.panel.split {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.panel-head h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.panel-head p {
|
||||
margin: 2px 0 10px;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.search-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 10px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
color: var(--text);
|
||||
font: inherit;
|
||||
padding: 10px 11px;
|
||||
}
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
background: linear-gradient(120deg, #ff6b35, #ff8c42);
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.table-wrap {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
min-width: 700px;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
text-align: left;
|
||||
padding: 8px 10px;
|
||||
border-bottom: 1px solid rgba(141, 206, 255, 0.13);
|
||||
font-size: 13px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
td {
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
th {
|
||||
color: #ff8c42;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.pill {
|
||||
border-radius: 99px;
|
||||
padding: 2px 9px;
|
||||
display: inline-block;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.pill.immature,
|
||||
.pill.pending {
|
||||
background: rgba(249, 202, 104, 0.25);
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.pill.confirmed,
|
||||
.pill.paid,
|
||||
.pill.success {
|
||||
background: rgba(120, 230, 200, 0.2);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.pill.orphan,
|
||||
.pill.failed,
|
||||
.pill.rejected {
|
||||
background: rgba(255, 127, 136, 0.2);
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
@media (min-width: 920px) {
|
||||
.panel.split {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 850px) {
|
||||
.layout {
|
||||
padding: 20px 12px 36px;
|
||||
}
|
||||
|
||||
.hero {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.hero-meta {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(145px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.search-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.stats-grid,
|
||||
.stats-grid.compact {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 620px) {
|
||||
h1 {
|
||||
font-size: clamp(26px, 8vw, 36px);
|
||||
}
|
||||
|
||||
.stats-grid,
|
||||
.stats-grid.compact {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.panel {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.table-wrap {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
table {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
thead {
|
||||
display: none;
|
||||
}
|
||||
|
||||
tbody,
|
||||
tr,
|
||||
td {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
tr {
|
||||
border: 1px solid rgba(255, 107, 53, 0.2);
|
||||
border-radius: 12px;
|
||||
margin-bottom: 10px;
|
||||
padding: 6px 8px;
|
||||
background: rgba(40, 30, 20, 0.4);
|
||||
}
|
||||
|
||||
td {
|
||||
border: 0;
|
||||
border-bottom: 1px dashed rgba(255, 107, 53, 0.15);
|
||||
padding: 7px 0;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(90px, 35%) 1fr;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
td:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
td::before {
|
||||
content: attr(data-label);
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.empty {
|
||||
display: block;
|
||||
border: 0;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.empty::before {
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
a {
|
||||
color: #ffffff;
|
||||
.footer a {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Mike section styling */
|
||||
.mike-section {
|
||||
margin-top: 30px;
|
||||
max-width: 600px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding: 20px;
|
||||
background-color: rgba(50, 50, 50, 0.3);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.mike-section h2 {
|
||||
color: #f0f0f0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.mike-section p {
|
||||
line-height: 1.6;
|
||||
margin-bottom: 15px;
|
||||
font-weight: 500;
|
||||
}
|
||||
@@ -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