<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PoCX Aggregator Dashboard</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #333;
padding: 20px;
min-height: 100vh;
}
.container {
max-width: 1600px;
margin: 0 auto;
}
h1 {
color: white;
text-align: center;
margin-bottom: 30px;
font-size: 2.5em;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.stat-card {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
transition: transform 0.3s ease;
}
.stat-card:hover {
transform: translateY(-5px);
}
.stat-label {
font-size: 0.85em;
color: #666;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 8px;
}
.stat-value {
font-size: 1.8em;
font-weight: bold;
color: #667eea;
}
.current-best-section {
background: white;
border-radius: 15px;
padding: 20px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
margin-bottom: 20px;
}
.current-best-section h2 {
margin-bottom: 15px;
color: #667eea;
font-size: 1.3em;
}
.best-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
}
.best-item {
padding: 10px;
background: #f9fafb;
border-radius: 8px;
}
.best-item-label {
font-size: 0.75em;
color: #666;
text-transform: uppercase;
margin-bottom: 5px;
}
.best-item-value {
font-size: 1.3em;
font-weight: bold;
color: #667eea;
font-family: 'Courier New', monospace;
}
.section {
background: white;
border-radius: 15px;
padding: 25px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
margin-bottom: 20px;
}
.section h2 {
margin-bottom: 20px;
color: #667eea;
border-bottom: 3px solid #667eea;
padding-bottom: 10px;
font-size: 1.5em;
}
.data-table {
width: 100%;
border-collapse: collapse;
}
.data-table th {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 12px;
text-align: left;
font-weight: 600;
font-size: 0.9em;
}
.data-table td {
padding: 10px 12px;
border-bottom: 1px solid #e5e7eb;
font-size: 0.9em;
}
.data-table tr:hover {
background: #f9fafb;
}
.data-table tr.parent-row {
font-weight: 600;
cursor: pointer;
}
.data-table tr.child-row {
background: #f3f4f6;
}
.data-table tr.child-row td {
padding-left: 40px;
font-size: 0.85em;
}
.status-indicator {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 8px;
}
.status-active {
background: #10b981;
box-shadow: 0 0 10px #10b981;
}
.status-inactive {
background: #ef4444;
}
.expand-icon {
display: inline-block;
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 7px solid #667eea;
margin-right: 10px;
transition: transform 0.2s;
}
.expand-icon.collapsed {
transform: rotate(-90deg);
}
.status-cell {
display: flex;
align-items: center;
gap: 8px;
}
.account-id {
font-family: 'Courier New', monospace;
color: #6366f1;
font-weight: 500;
}
.last-update {
text-align: center;
color: white;
margin-top: 20px;
font-size: 0.9em;
}
.no-data {
text-align: center;
padding: 40px;
color: #9ca3af;
font-size: 1.2em;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.loading {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
body.dark-mode {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
}
.dark-mode .stat-card {
background: #2d2d44;
color: #e4e4e7;
}
.dark-mode .stat-label {
color: #a1a1aa;
}
.dark-mode .stat-value {
color: #818cf8;
}
.dark-mode .current-best-section,
.dark-mode .section {
background: #2d2d44;
}
.dark-mode .section h2,
.dark-mode .current-best-section h2 {
color: #818cf8;
border-bottom-color: #818cf8;
}
.dark-mode .best-item {
background: #1f1f2e;
}
.dark-mode .best-item-value {
color: #818cf8;
}
.dark-mode .data-table th {
background: linear-gradient(135deg, #4338ca 0%, #6366f1 100%);
}
.dark-mode .data-table td {
border-bottom-color: #3f3f46;
color: #e4e4e7;
}
.dark-mode .data-table tr:hover {
background: #27272a;
}
.dark-mode .data-table tr.child-row {
background: #1f1f2e;
}
.dark-mode .expand-icon {
border-top-color: #818cf8;
}
.dark-mode .account-id {
color: #a78bfa;
}
.dark-mode .no-data {
color: #71717a;
}
.theme-toggle {
position: fixed;
top: 20px;
right: 20px;
background: white;
border: none;
border-radius: 50%;
width: 50px;
height: 50px;
cursor: pointer;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
font-size: 24px;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.2s;
z-index: 1000;
}
.theme-toggle:hover {
transform: scale(1.1);
}
.dark-mode .theme-toggle {
background: #2d2d44;
}
@media (max-width: 768px) {
body {
padding: 10px;
font-size: 14px;
}
h1 {
font-size: 1.5em;
margin-bottom: 15px;
}
.theme-toggle {
width: 40px;
height: 40px;
font-size: 20px;
top: 10px;
right: 10px;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
gap: 10px;
margin-bottom: 15px;
}
.stat-card {
padding: 12px;
}
.stat-label {
font-size: 0.65em;
margin-bottom: 5px;
}
.stat-value {
font-size: 1.3em;
}
.current-best-section {
padding: 12px;
}
.current-best-section h2 {
font-size: 1em;
margin-bottom: 10px;
}
.best-grid {
grid-template-columns: repeat(2, 1fr);
gap: 8px;
}
.best-item {
padding: 8px;
}
.best-item-label {
font-size: 0.65em;
}
.best-item-value {
font-size: 0.9em;
}
.section {
padding: 12px;
}
.section h2 {
font-size: 1.1em;
margin-bottom: 12px;
padding-bottom: 8px;
}
.data-table thead {
display: none;
}
.data-table,
.data-table tbody,
.data-table tr {
display: block;
}
.data-table tr.parent-row {
margin-bottom: 12px;
border: 2px solid #e5e7eb;
border-radius: 8px;
padding: 12px;
background: #f9fafb;
}
.dark-mode .data-table tr.parent-row {
border-color: #3f3f46;
background: #27272a;
}
.data-table tr.parent-row:hover {
background: #f3f4f6;
}
.dark-mode .data-table tr.parent-row:hover {
background: #2d2d3a;
}
.data-table tr.child-row {
margin-left: 20px;
margin-bottom: 8px;
padding: 10px;
border-radius: 6px;
font-size: 0.85em;
}
.data-table td {
display: block;
text-align: left;
padding: 6px 0;
border: none;
font-size: 0.85em;
}
.data-table td:first-child {
display: flex !important;
margin-bottom: 8px;
}
.data-table tr.parent-row td:nth-child(2)::before {
content: "ID: ";
font-weight: bold;
color: #666;
}
.dark-mode .data-table tr.parent-row td:nth-child(2)::before {
color: #a1a1aa;
}
.data-table tr.parent-row td:nth-child(3)::before {
content: "Count: ";
font-weight: bold;
color: #666;
}
.dark-mode .data-table tr.parent-row td:nth-child(3)::before {
color: #a1a1aa;
}
.data-table tr.parent-row td:nth-child(4)::before {
content: "Capacity: ";
font-weight: bold;
color: #666;
}
.dark-mode .data-table tr.parent-row td:nth-child(4)::before {
color: #a1a1aa;
}
.data-table tr.parent-row td:nth-child(5)::before {
content: "Subs 24h: ";
font-weight: bold;
color: #666;
}
.dark-mode .data-table tr.parent-row td:nth-child(5)::before {
color: #a1a1aa;
}
.data-table tr.parent-row td:nth-child(6)::before {
content: "Last: ";
font-weight: bold;
color: #666;
}
.dark-mode .data-table tr.parent-row td:nth-child(6)::before {
color: #a1a1aa;
}
.data-table tr.child-row td:nth-child(2)::before {
content: "ID: ";
font-weight: bold;
color: #888;
}
.dark-mode .data-table tr.child-row td:nth-child(2)::before {
color: #71717a;
}
.no-data {
padding: 20px;
font-size: 1em;
}
.last-update {
font-size: 0.75em;
margin-top: 15px;
}
}
@media (min-width: 769px) and (max-width: 1024px) {
.stats-grid {
grid-template-columns: repeat(3, 1fr);
}
.best-grid {
grid-template-columns: repeat(2, 1fr);
}
}
</style>
</head>
<body>
<button class="theme-toggle" id="theme-toggle" aria-label="Toggle dark mode">🌙</button>
<div class="container">
<h1>⛏️ PoCX Aggregator Dashboard</h1>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">Connected Machines</div>
<div class="stat-value" id="connected-machines">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Mining Accounts</div>
<div class="stat-value" id="total-accounts">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Total Miner Capacity</div>
<div class="stat-value" id="total-capacity">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Network Capacity</div>
<div class="stat-value" id="network-capacity">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Current Height</div>
<div class="stat-value" id="current-height">-</div>
</div>
</div>
<div class="current-best-section">
<h2>🏆 Current Block Best Submission</h2>
<div class="best-grid" id="best-grid">
<div class="best-item">
<div class="best-item-label">Best PoC Time</div>
<div class="best-item-value" id="best-poc-time">-</div>
</div>
<div class="best-item">
<div class="best-item-label">Best Quality</div>
<div class="best-item-value" id="best-quality">-</div>
</div>
<div class="best-item">
<div class="best-item-label">Best Account</div>
<div class="best-item-value" id="best-account">-</div>
</div>
<div class="best-item">
<div class="best-item-label">Machine</div>
<div class="best-item-value" id="best-machine">-</div>
</div>
</div>
</div>
<div class="section">
<h2>🖥️ Connected Machines</h2>
<table class="data-table">
<thead>
<tr>
<th style="width: 40px;"></th>
<th>Machine ID (IP)</th>
<th>Accounts</th>
<th>Capacity (Est 1h)</th>
<th>Avg Submissions</th>
<th>Last Seen</th>
</tr>
</thead>
<tbody id="machines-tbody">
<tr>
<td colspan="6" class="no-data loading">Loading machines...</td>
</tr>
</tbody>
</table>
</div>
<div class="section">
<h2>👤 Mining Accounts</h2>
<table class="data-table">
<thead>
<tr>
<th style="width: 40px;"></th>
<th>Account ID</th>
<th>Machines</th>
<th>Capacity (Est 1h)</th>
<th>Avg Submissions</th>
<th>Last Seen</th>
</tr>
</thead>
<tbody id="accounts-tbody">
<tr>
<td colspan="6" class="no-data loading">Loading accounts...</td>
</tr>
</tbody>
</table>
</div>
<div class="last-update">
Uptime: <span id="uptime">-</span> | Last updated: <span id="last-update">Never</span>
</div>
</div>
<script>
const STATS_ENDPOINT = '/stats';
const UPDATE_INTERVAL = 2000;
const expandedMachines = new Set();
const expandedAccounts = new Set();
const themeToggle = document.getElementById('theme-toggle');
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'dark') {
document.body.classList.add('dark-mode');
themeToggle.textContent = '☀️';
}
themeToggle.addEventListener('click', () => {
document.body.classList.toggle('dark-mode');
const isDark = document.body.classList.contains('dark-mode');
themeToggle.textContent = isDark ? '☀️' : '🌙';
localStorage.setItem('theme', isDark ? 'dark' : 'light');
});
function formatUptime(seconds) {
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
if (days > 0) {
return `${days}d ${hours}h ${minutes}m`;
} else if (hours > 0) {
return `${hours}h ${minutes}m ${secs}s`;
} else if (minutes > 0) {
return `${minutes}m ${secs}s`;
} else {
return `${secs}s`;
}
}
function formatLastSeen(secondsAgo) {
if (secondsAgo < 60) {
return `${secondsAgo}s ago`;
} else if (secondsAgo < 3600) {
return `${Math.floor(secondsAgo / 60)}m ago`;
} else if (secondsAgo < 86400) {
return `${Math.floor(secondsAgo / 3600)}h ago`;
} else {
return `${Math.floor(secondsAgo / 86400)}d ago`;
}
}
function formatPocTime(pocTimeSecs) {
if (!pocTimeSecs) return 'N/A';
if (pocTimeSecs >= 3600) {
const hours = Math.floor(pocTimeSecs / 3600);
const mins = Math.floor((pocTimeSecs % 3600) / 60);
const secs = pocTimeSecs % 60;
return `${hours}h ${mins}m ${secs}s`;
} else if (pocTimeSecs >= 60) {
const mins = Math.floor(pocTimeSecs / 60);
const secs = pocTimeSecs % 60;
return `${mins}m ${secs}s`;
} else {
return `${pocTimeSecs}s`;
}
}
function truncateAccountId(id) {
if (!id || id.length <= 20) return id;
return id.substring(0, 10) + '...' + id.substring(id.length - 10);
}
function toggleMachine(machineId) {
if (expandedMachines.has(machineId)) {
expandedMachines.delete(machineId);
} else {
expandedMachines.add(machineId);
}
}
function toggleAccount(accountId) {
if (expandedAccounts.has(accountId)) {
expandedAccounts.delete(accountId);
} else {
expandedAccounts.add(accountId);
}
}
async function updateStats() {
try {
const response = await fetch(STATS_ENDPOINT);
const data = await response.json();
document.getElementById('connected-machines').textContent =
`${data.active_machines} / ${data.unique_machines}`;
document.getElementById('total-accounts').textContent = data.unique_miners;
document.getElementById('total-capacity').textContent = data.total_capacity;
document.getElementById('network-capacity').textContent = data.network_capacity;
document.getElementById('current-height').textContent = data.current_height.toLocaleString();
document.getElementById('uptime').textContent = formatUptime(data.uptime_secs);
const best = data.current_block_best;
if (best.best_poc_time) {
document.getElementById('best-poc-time').textContent = formatPocTime(best.best_poc_time);
document.getElementById('best-quality').textContent = best.best_quality.toLocaleString();
document.getElementById('best-account').textContent = truncateAccountId(best.best_account_id);
document.getElementById('best-machine').textContent = best.best_machine_id;
} else {
document.getElementById('best-poc-time').textContent = '⏳ Waiting...';
document.getElementById('best-quality').textContent = '-';
document.getElementById('best-account').textContent = '-';
document.getElementById('best-machine').textContent = '-';
}
const machinesTbody = document.getElementById('machines-tbody');
if (data.machines && data.machines.length > 0) {
let html = '';
data.machines.forEach(machine => {
const isActive = machine.is_active;
const statusClass = isActive ? 'status-active' : 'status-inactive';
const isExpanded = expandedMachines.has(machine.machine_id);
const expandClass = isExpanded ? '' : 'collapsed';
html += `
<tr class="parent-row" onclick="toggleMachine('${machine.machine_id}'); updateStats();">
<td class="status-cell">
<span class="expand-icon ${expandClass}"></span>
<span class="status-indicator ${statusClass}"></span>
</td>
<td><strong>${machine.machine_id}</strong></td>
<td>${machine.account_count}</td>
<td>${machine.total_capacity_tib.toFixed(2)} TiB</td>
<td>Avg: ${machine.submission_percentage.toFixed(1)}%</td>
<td>${formatLastSeen(machine.last_seen_secs_ago)}</td>
</tr>
`;
if (isExpanded) {
machine.accounts.forEach(account => {
const childStatusClass = account.is_active ? 'status-active' : 'status-inactive';
html += `
<tr class="child-row">
<td><span class="status-indicator ${childStatusClass}"></span></td>
<td class="account-id">${truncateAccountId(account.account_id)}</td>
<td></td>
<td>${account.capacity_tib.toFixed(2)} TiB</td>
<td>${account.submissions_24h} (${account.submission_percentage.toFixed(1)}%)</td>
<td>${formatLastSeen(account.last_seen_secs_ago)}</td>
</tr>
`;
});
}
});
machinesTbody.innerHTML = html;
} else {
machinesTbody.innerHTML = '<tr><td colspan="6" class="no-data">No machines connected</td></tr>';
}
const accountsTbody = document.getElementById('accounts-tbody');
if (data.accounts && data.accounts.length > 0) {
let html = '';
data.accounts.forEach(account => {
const isActive = account.is_active;
const statusClass = isActive ? 'status-active' : 'status-inactive';
const isExpanded = expandedAccounts.has(account.account_id);
const expandClass = isExpanded ? '' : 'collapsed';
html += `
<tr class="parent-row" onclick="toggleAccount('${account.account_id}'); updateStats();">
<td class="status-cell">
<span class="expand-icon ${expandClass}"></span>
<span class="status-indicator ${statusClass}"></span>
</td>
<td class="account-id"><strong>${truncateAccountId(account.account_id)}</strong></td>
<td>${account.machine_count}</td>
<td>${account.total_capacity_tib.toFixed(2)} TiB</td>
<td>Avg: ${account.submission_percentage.toFixed(1)}%</td>
<td>${formatLastSeen(account.last_seen_secs_ago)}</td>
</tr>
`;
if (isExpanded) {
account.machines.forEach(machine => {
const childStatusClass = machine.is_active ? 'status-active' : 'status-inactive';
html += `
<tr class="child-row">
<td><span class="status-indicator ${childStatusClass}"></span></td>
<td>${machine.machine_id}</td>
<td></td>
<td>${machine.capacity_tib.toFixed(2)} TiB</td>
<td>${machine.submissions_24h} (${machine.submission_percentage.toFixed(1)}%)</td>
<td>${formatLastSeen(machine.last_seen_secs_ago)}</td>
</tr>
`;
});
}
});
accountsTbody.innerHTML = html;
} else {
accountsTbody.innerHTML = '<tr><td colspan="6" class="no-data">No accounts connected</td></tr>';
}
document.getElementById('last-update').textContent = new Date().toLocaleTimeString();
} catch (error) {
console.error('Failed to fetch stats:', error);
document.getElementById('last-update').textContent = 'Error: ' + error.message;
}
}
updateStats();
setInterval(updateStats, UPDATE_INTERVAL);
</script>
</body>
</html>