Add Worker Overview files

This commit is contained in:
DJObleezy 2025-03-22 21:05:41 -07:00 committed by GitHub
parent 691e0aed25
commit 9904393b94
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 1536 additions and 0 deletions

937
workers.html Normal file
View File

@ -0,0 +1,937 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<!-- Custom Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&family=VT323&display=swap" rel="stylesheet">
<!-- Meta viewport for responsive scaling -->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Workers Overview - Ocean.xyz Pool Mining Dashboard v 0.2</title>
<!-- Font Awesome CDN for icon support -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
:root {
--bg-color: #0a0a0a;
--bg-gradient: linear-gradient(135deg, #0a0a0a, #1a1a1a);
--primary-color: #f7931a;
--accent-color: #00ffff;
--text-color: #ffffff;
--card-padding: 0.5rem;
--text-size-base: 16px;
--terminal-font: 'VT323', monospace;
--header-font: 'Orbitron', sans-serif;
}
@media (min-width: 768px) {
:root {
--card-padding: 0.75rem;
--text-size-base: 18px;
}
}
/* CRT Screen Effect */
body::before {
content: " ";
display: block;
position: fixed;
top: 0; left: 0; bottom: 0; right: 0;
background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.1) 50%),
linear-gradient(90deg, rgba(255, 0, 0, 0.03), rgba(0, 255, 0, 0.02), rgba(0, 0, 255, 0.03));
background-size: 100% 2px, 3px 100%;
pointer-events: none;
z-index: 2;
opacity: 0.15;
}
/* Flicker Animation */
@keyframes flicker {
0% { opacity: 0.97; }
5% { opacity: 0.95; }
10% { opacity: 0.97; }
15% { opacity: 0.94; }
20% { opacity: 0.98; }
50% { opacity: 0.95; }
80% { opacity: 0.96; }
90% { opacity: 0.94; }
100% { opacity: 0.98; }
}
body {
background: var(--bg-gradient);
color: var(--text-color);
padding-top: 0.5rem;
font-size: var(--text-size-base);
font-family: var(--terminal-font);
text-shadow: 0 0 5px rgba(255, 255, 255, 0.3);
}
h1 {
font-size: 24px;
font-weight: bold;
color: var(--primary-color);
font-family: var(--header-font);
letter-spacing: 1px;
text-shadow: 0 0 10px var(--primary-color);
animation: flicker 4s infinite;
}
@media (min-width: 768px) {
h1 {
font-size: 26px;
}
}
.card,
.card-header,
.card-body,
.card-footer {
border-radius: 0 !important;
}
/* Enhanced card with scanlines */
.card {
background-color: var(--bg-color);
border: 1px solid var(--primary-color);
margin-bottom: 0.5rem;
padding: var(--card-padding);
flex: 1;
position: relative;
overflow: hidden;
box-shadow: 0 0 5px rgba(247, 147, 26, 0.3);
}
/* Scanline effect for cards */
.card::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: repeating-linear-gradient(
0deg,
rgba(0, 0, 0, 0.05),
rgba(0, 0, 0, 0.05) 1px,
transparent 1px,
transparent 2px
);
pointer-events: none;
z-index: 1;
}
.card-header {
background-color: #000;
color: var(--primary-color);
font-weight: bold;
padding: 0.3rem 0.5rem;
font-size: 1.1rem;
border-bottom: 1px solid var(--primary-color);
text-shadow: 0 0 5px var(--primary-color);
animation: flicker 4s infinite;
font-family: var(--header-font);
}
.card-body hr {
border-top: 1px solid var(--primary-color);
margin: 0.25rem 0;
}
.arrow {
display: inline-block;
font-weight: bold;
margin-left: 0.5rem;
}
/* Bounce Up Animation for Up Chevron */
@keyframes bounceUp {
0% { transform: translateY(0); }
25% { transform: translateY(-2px); }
50% { transform: translateY(0); }
75% { transform: translateY(-2px); }
100% { transform: translateY(0); }
}
/* Bounce Down Animation for Down Chevron */
@keyframes bounceDown {
0% { transform: translateY(0); }
25% { transform: translateY(2px); }
50% { transform: translateY(0); }
75% { transform: translateY(2px); }
100% { transform: translateY(0); }
}
/* Apply bounce animations */
.bounce-up {
animation: bounceUp 1s infinite;
}
.bounce-down {
animation: bounceDown 1s infinite;
}
/* Make chevrons slightly smaller */
.chevron {
font-size: 0.8rem;
position: relative;
top: 3px;
}
/* Enhanced Online dot with more glow */
.online-dot {
display: inline-block;
width: 8px;
height: 8px;
background: #32CD32;
border-radius: 50%;
margin-left: 0.5em;
position: relative;
top: -2px;
animation: glow 1s infinite;
box-shadow: 0 0 10px #32CD32, 0 0 20px #32CD32;
}
@keyframes glow {
0%, 100% { box-shadow: 0 0 10px #32CD32, 0 0 15px #32CD32; }
50% { box-shadow: 0 0 15px #32CD32, 0 0 25px #32CD32; }
}
/* Enhanced Offline dot with more glow */
.offline-dot {
display: inline-block;
width: 8px;
height: 8px;
background: red;
border-radius: 50%;
margin-left: 0.5em;
animation: glowRed 1s infinite;
box-shadow: 0 0 10px red, 0 0 20px red;
}
@keyframes glowRed {
0%, 100% { box-shadow: 0 0 10px red, 0 0 15px red; }
50% { box-shadow: 0 0 15px red, 0 0 25px red; }
}
/* Refresh timer container */
#refreshUptime {
text-align: center;
margin-top: 0.5rem;
}
#refreshContainer {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
}
/* Enhance metric value styling with consistent glow */
.metric-value {
color: var(--text-color);
font-weight: bold;
text-shadow: 0 0 6px rgba(255, 255, 255, 0.6);
}
/* Standardized glow effects for all metrics */
/* Yellow color family (BTC price, sats metrics, time to payout) */
.metric-value.yellow,
.yellow-glow {
color: #ffd700;
text-shadow: 0 0 6px rgba(255, 215, 0, 0.6);
}
/* Green color family (profits, earnings) */
.metric-value.green,
.green-glow {
color: #32CD32;
text-shadow: 0 0 6px rgba(50, 205, 50, 0.6);
}
/* Red color family (costs) */
.metric-value.red,
.red-glow {
color: #ff5555 !important;
text-shadow: 0 0 6px rgba(255, 85, 85, 0.6);
}
/* White metrics (general stats) */
.metric-value.white,
.white-glow {
color: #ffffff;
text-shadow: 0 0 6px rgba(255, 255, 255, 0.6);
}
/* Blue metrics (time data) */
.metric-value.blue,
.blue-glow {
color: #00dfff;
text-shadow: 0 0 6px rgba(0, 223, 255, 0.6);
}
/* Special stronger glow only for online/offline indicators */
.status-green {
color: #39ff14 !important;
text-shadow: 0 0 10px #39ff14, 0 0 20px #39ff14;
}
.status-red {
color: #ff2d2d !important;
text-shadow: 0 0 10px #ff2d2d, 0 0 20px #ff2d2d;
}
/* Card body elements */
.card-body strong {
color: var(--primary-color);
margin-right: 0.25rem;
text-shadow: 0 0 2px var(--primary-color);
}
.card-body p {
margin: 0.25rem 0;
line-height: 1.2;
}
.container-fluid {
max-width: 1200px;
margin: 0 auto;
padding-left: 1rem;
padding-right: 1rem;
position: relative;
}
#topRightLink {
position: absolute;
top: 10px;
right: 10px;
color: grey;
text-decoration: none;
font-size: 0.9rem;
text-shadow: 0 0 5px grey;
}
#uptimeTimer strong {
font-weight: bold;
}
#uptimeTimer {
margin-top: 0;
}
/* Connection status indicator */
#connectionStatus {
display: none;
position: fixed;
top: 10px;
right: 10px;
background: rgba(255,0,0,0.7);
color: white;
padding: 10px;
border-radius: 5px;
z-index: 9999;
font-size: 0.9rem;
text-shadow: 0 0 5px rgba(255, 0, 0, 0.8);
box-shadow: 0 0 10px rgba(255, 0, 0, 0.5);
}
/* Worker grid for worker cards */
.worker-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 10px;
margin-top: 10px;
}
/* Worker card styles */
.worker-card {
background-color: var(--bg-color);
border: 1px solid var(--primary-color);
box-shadow: 0 0 5px rgba(247, 147, 26, 0.3);
position: relative;
overflow: hidden;
padding: 10px;
height: 100%;
}
.worker-card::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: repeating-linear-gradient(
0deg,
rgba(0, 0, 0, 0.05),
rgba(0, 0, 0, 0.05) 1px,
transparent 1px,
transparent 2px
);
pointer-events: none;
z-index: 1;
}
.worker-card-online {
border-color: #32CD32;
box-shadow: 0 0 8px rgba(50, 205, 50, 0.4);
}
.worker-card-offline {
border-color: #ff5555;
box-shadow: 0 0 8px rgba(255, 85, 85, 0.4);
}
.worker-name {
color: var(--primary-color);
font-weight: bold;
font-size: 1.2rem;
text-shadow: 0 0 5px var(--primary-color);
margin-bottom: 5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
z-index: 2;
position: relative;
}
.worker-stats {
margin-top: 8px;
font-size: 0.9rem;
z-index: 2;
position: relative;
}
.worker-stats-row {
display: flex;
justify-content: space-between;
margin-bottom: 4px;
}
.worker-stats-label {
color: #aaa;
}
.hashrate-bar {
height: 4px;
background: linear-gradient(90deg, #1137F5, #39ff14);
margin-top: 4px;
margin-bottom: 8px;
position: relative;
z-index: 2;
}
/* Worker badge */
.worker-type {
position: absolute;
top: 10px;
right: 10px;
font-size: 0.7rem;
background-color: rgba(0, 0, 0, 0.6);
border: 1px solid var(--primary-color);
color: var(--primary-color);
padding: 1px 5px;
z-index: 2;
}
/* Status badges */
.status-badge {
display: inline-block;
font-size: 0.8rem;
padding: 2px 8px;
border-radius: 3px;
z-index: 2;
position: relative;
}
.status-badge-online {
background-color: rgba(50, 205, 50, 0.2);
border: 1px solid #32CD32;
color: #32CD32;
text-shadow: 0 0 5px rgba(50, 205, 50, 0.8);
}
.status-badge-offline {
background-color: rgba(255, 85, 85, 0.2);
border: 1px solid #ff5555;
color: #ff5555;
text-shadow: 0 0 5px rgba(255, 85, 85, 0.8);
}
/* Stats bars */
.stats-bar-container {
width: 100%;
height: 4px;
background-color: rgba(255, 255, 255, 0.1);
margin-top: 2px;
margin-bottom: 5px;
position: relative;
z-index: 2;
}
.stats-bar {
height: 100%;
background: linear-gradient(90deg, #1137F5, #39ff14);
}
/* Search and filter controls */
.controls-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
flex-wrap: wrap;
gap: 10px;
}
.search-box {
background-color: var(--bg-color);
border: 1px solid var(--primary-color);
color: var(--text-color);
padding: 5px 10px;
font-family: var(--terminal-font);
min-width: 200px;
}
.search-box:focus {
outline: none;
box-shadow: 0 0 8px rgba(247, 147, 26, 0.5);
}
.filter-button {
background-color: var(--bg-color);
border: 1px solid var(--primary-color);
color: var(--primary-color);
padding: 5px 10px;
font-family: var(--terminal-font);
cursor: pointer;
}
.filter-button.active {
background-color: var(--primary-color);
color: var(--bg-color);
}
/* Bitcoin Progress Bar Styles */
.bitcoin-progress-container {
width: 100%;
max-width: 300px;
height: 20px;
background-color: #111;
border: 1px solid var(--primary-color);
border-radius: 0;
margin: 0.5rem auto;
position: relative;
overflow: hidden;
box-shadow: 0 0 8px rgba(247, 147, 26, 0.5);
align-self: center;
}
.bitcoin-progress-inner {
height: 100%;
width: 0;
background: linear-gradient(90deg, #f7931a, #ffa500);
border-radius: 0;
transition: width 0.3s ease;
position: relative;
overflow: hidden;
}
.bitcoin-progress-inner::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(90deg,
rgba(255, 255, 255, 0.1) 0%,
rgba(255, 255, 255, 0.2) 20%,
rgba(255, 255, 255, 0.1) 40%);
animation: shimmer 2s infinite;
}
@keyframes shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
.bitcoin-icons {
position: absolute;
top: 50%;
left: 0;
width: 100%;
transform: translateY(-50%);
display: flex;
justify-content: space-around;
font-size: 12px;
color: rgba(0, 0, 0, 0.7);
}
.glow-effect {
box-shadow: 0 0 15px #f7931a, 0 0 25px #f7931a;
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.8; }
}
#progress-text {
font-size: 0.9rem;
color: var(--primary-color);
margin-top: 0.3rem;
text-shadow: 0 0 5px var(--primary-color);
text-align: center;
width: 100%;
}
/* Last Updated text with subtle animation */
#lastUpdated {
animation: flicker 5s infinite;
text-align: center;
}
/* Cursor blink for terminal feel */
#terminal-cursor {
display: inline-block;
width: 10px;
height: 16px;
background-color: #f7931a;
margin-left: 2px;
animation: blink 1s step-end infinite;
vertical-align: middle;
box-shadow: 0 0 5px rgba(247, 147, 26, 0.8);
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
/* Summary stats in the header */
.summary-stats {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
gap: 15px;
margin: 15px 0;
}
.summary-stat {
text-align: center;
min-width: 120px;
}
.summary-stat-value {
font-size: 1.6rem;
font-weight: bold;
margin-bottom: 5px;
}
.summary-stat-label {
font-size: 0.9rem;
color: #aaa;
}
/* Worker count ring */
.worker-ring {
width: 90px;
height: 90px;
border-radius: 50%;
position: relative;
margin: 0 auto;
background: conic-gradient(
#32CD32 0% calc(var(--online-percent) * 100%),
#ff5555 calc(var(--online-percent) * 100%) 100%
);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0 15px rgba(247, 147, 26, 0.3);
}
.worker-ring-inner {
width: 70px;
height: 70px;
border-radius: 50%;
background-color: var(--bg-color);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
font-weight: bold;
color: var(--text-color);
}
/* Mini hashrate chart */
.mini-chart {
height: 40px;
width: 100%;
margin-top: 5px;
position: relative;
z-index: 2;
}
/* Mobile responsiveness */
@media (max-width: 576px) {
.controls-bar {
flex-direction: column;
align-items: stretch;
}
.search-box {
width: 100%;
}
.filter-buttons {
display: flex;
justify-content: space-between;
}
.worker-grid {
grid-template-columns: 1fr;
}
.summary-stats {
flex-direction: column;
align-items: center;
}
.summary-stat {
width: 100%;
}
}
/* Navigation links */
.navigation-links {
display: flex;
justify-content: center;
margin-top: 10px;
margin-bottom: 15px;
}
.nav-link {
padding: 5px 15px;
margin: 0 10px;
background-color: var(--bg-color);
border: 1px solid var(--primary-color);
color: var(--primary-color);
text-decoration: none;
font-family: var(--terminal-font);
transition: all 0.3s ease;
}
.nav-link:hover {
background-color: var(--primary-color);
color: var(--bg-color);
box-shadow: 0 0 10px rgba(247, 147, 26, 0.5);
}
.nav-link.active {
background-color: var(--primary-color);
color: var(--bg-color);
box-shadow: 0 0 10px rgba(247, 147, 26, 0.5);
}
.loading-fade {
opacity: 0.6;
transition: opacity 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.worker-card {
animation: fadeIn 0.3s ease;
}
@media (max-width: 768px) {
/* Fix for "Made by" link collision with title */
#topRightLink {
position: static !important;
display: block !important;
text-align: right !important;
margin-bottom: 0.5rem !important;
margin-top: 0 !important;
font-size: 0.8rem !important;
}
/* Adjust heading for better mobile display */
h1 {
font-size: 20px !important;
line-height: 1.2 !important;
margin-top: 0.5rem !important;
padding-top: 0 !important;
}
/* Improve container padding for mobile */
.container-fluid {
padding-left: 0.5rem !important;
padding-right: 0.5rem !important;
}
/* Ensure top section has appropriate spacing */
.row.mb-3 {
margin-top: 0.5rem !important;
}
}
/* Add a more aggressive breakpoint for very small screens */
@media (max-width: 380px) {
#topRightLink {
margin-bottom: 0.75rem !important;
font-size: 0.7rem !important;
}
h1 {
font-size: 18px !important;
margin-bottom: 0.5rem !important;
}
/* Further reduce container padding */
.container-fluid {
padding-left: 0.3rem !important;
padding-right: 0.3rem !important;
}
}
</style>
</head>
<body>
<div class="container-fluid">
<!-- Connection status indicator -->
<div id="connectionStatus"></div>
<!-- Updated section to fix mobile layout issues -->
<div class="top-section">
<!-- Top right link - moved to top for better mobile flow -->
<a href="https://x.com/DJObleezy" id="topRightLink" target="_blank" rel="noopener noreferrer">Made by @DJO₿leezy</a>
<!-- Title with margin to avoid collision -->
<h1 class="text-center">Workers Overview</h1>
<p class="text-center" id="lastUpdated"><strong>Last Updated:</strong> {{ current_time }}<span id="terminal-cursor"></span></p>
</div>
<!-- Navigation links -->
<div class="navigation-links">
<a href="/dashboard" class="nav-link">Main Dashboard</a>
<a href="/workers" class="nav-link active">Workers Overview</a>
</div>
<!-- Summary statistics -->
<div class="row mb-3">
<div class="col-md-12">
<div class="card">
<div class="card-header">Fleet Summary</div>
<div class="card-body">
<div class="summary-stats">
<div class="summary-stat">
<div class="worker-ring" style="--online-percent: {{ workers_online / workers_total if workers_total > 0 else 0 }}">
<div class="worker-ring-inner">
<span id="workers-count">{{ workers_total }}</span>
</div>
</div>
<div class="summary-stat-label">Workers</div>
<div>
<span class="green-glow" id="workers-online">{{ workers_online }}</span> /
<span class="red-glow" id="workers-offline">{{ workers_offline }}</span>
</div>
</div>
<div class="summary-stat">
<div class="summary-stat-value white-glow" id="total-hashrate">
{% if total_hashrate is defined %}
{{ "%.1f"|format(total_hashrate) }} {{ hashrate_unit }}
{% else %}
N/A
{% endif %}
</div>
<div class="summary-stat-label">Total Hashrate</div>
<div class="mini-chart">
<canvas id="total-hashrate-chart"></canvas>
</div>
</div>
<div class="summary-stat">
<div class="summary-stat-value green-glow" id="total-earnings">
{% if total_earnings is defined %}
{{ "%.8f"|format(total_earnings) }} BTC
{% else %}
N/A
{% endif %}
</div>
<div class="summary-stat-label">Lifetime Earnings</div>
</div>
<div class="summary-stat">
<div class="summary-stat-value yellow-glow" id="daily-sats">
{% if daily_sats is defined %}
{{ daily_sats|commafy }} sats
{% else %}
N/A
{% endif %}
</div>
<div class="summary-stat-label">Daily Sats</div>
</div>
<div class="summary-stat">
<div class="summary-stat-value white-glow" id="avg-acceptance-rate">
{% if avg_acceptance_rate is defined %}
{{ "%.2f"|format(avg_acceptance_rate) }}%
{% else %}
N/A
{% endif %}
</div>
<div class="summary-stat-label">Acceptance Rate</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Controls bar -->
<div class="controls-bar">
<input type="text" class="search-box" id="worker-search" placeholder="Search workers...">
<div class="filter-buttons">
<button class="filter-button active" data-filter="all">All Workers</button>
<button class="filter-button" data-filter="online">Online</button>
<button class="filter-button" data-filter="offline">Offline</button>
<button class="filter-button" data-filter="asic">ASIC</button>
<button class="filter-button" data-filter="fpga">FPGA</button>
</div>
</div>
<!-- Workers grid -->
<div class="worker-grid" id="worker-grid">
<!-- Worker cards will be generated here via JavaScript -->
</div>
<!-- Bitcoin-themed Progress Bar and Uptime -->
<div id="refreshUptime" class="text-center mt-4">
<div id="refreshContainer">
<!-- Bitcoin-themed progress bar -->
<div class="bitcoin-progress-container">
<div id="bitcoin-progress-inner" class="bitcoin-progress-inner" style="width: 0%">
<!-- Small Bitcoin icons inside the bar -->
<div class="bitcoin-icons">
<i class="fab fa-bitcoin"></i>
<i class="fab fa-bitcoin"></i>
<i class="fab fa-bitcoin"></i>
</div>
</div>
</div>
<div id="progress-text">60s to next update</div>
</div>
<div id="uptimeTimer"><strong>Uptime:</strong> 0h 0m 0s</div>
</div>
</div>
<!-- External JavaScript libraries -->
<script src="https://code.jquery.com/jquery-3.7.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="/static/js/workers.js"></script>
</body>
</html>

599
workers.js Normal file
View File

@ -0,0 +1,599 @@
"use strict";
// Global variables for workers dashboard
let workerData = null;
let refreshTimer;
let pageLoadTime = Date.now();
let currentProgress = 0;
const PROGRESS_MAX = 60; // 60 seconds for a complete cycle
let lastUpdateTime = Date.now();
let filterState = {
currentFilter: 'all',
searchTerm: ''
};
let miniChart = null;
let connectionRetryCount = 0;
// Server time variables for uptime calculation - synced with main dashboard
let serverTimeOffset = 0;
let serverStartTime = null;
// New variable to track custom refresh timing
let lastManualRefreshTime = 0;
const MIN_REFRESH_INTERVAL = 10000; // Minimum 10 seconds between refreshes
// Initialize the page
$(document).ready(function() {
// Set up initial UI
initializePage();
// Get server time for uptime calculation
updateServerTime();
// Set up refresh synchronization with main dashboard
setupRefreshSync();
// Fetch worker data immediately on page load
fetchWorkerData();
// Set up refresh timer
setInterval(updateProgressBar, 1000);
// Set up uptime timer - synced with main dashboard
setInterval(updateUptime, 1000);
// Start server time polling - same as main dashboard
setInterval(updateServerTime, 30000);
// Auto-refresh worker data - aligned with main dashboard if possible
setInterval(function() {
// Check if it's been at least PROGRESS_MAX seconds since last update
const timeSinceLastUpdate = Date.now() - lastUpdateTime;
if (timeSinceLastUpdate >= PROGRESS_MAX * 1000) {
// Check if there was a recent manual refresh
const timeSinceManualRefresh = Date.now() - lastManualRefreshTime;
if (timeSinceManualRefresh >= MIN_REFRESH_INTERVAL) {
console.log("Auto-refresh triggered after time interval");
fetchWorkerData();
}
}
}, 10000); // Check every 10 seconds to align better with main dashboard
// Set up filter button click handlers
$('.filter-button').click(function() {
$('.filter-button').removeClass('active');
$(this).addClass('active');
filterState.currentFilter = $(this).data('filter');
filterWorkers();
});
// Set up search input handler
$('#worker-search').on('input', function() {
filterState.searchTerm = $(this).val().toLowerCase();
filterWorkers();
});
});
// Set up refresh synchronization with main dashboard
function setupRefreshSync() {
// Listen for storage events (triggered by main dashboard)
window.addEventListener('storage', function(event) {
// Check if this is our dashboard refresh event
if (event.key === 'dashboardRefreshEvent') {
console.log("Detected dashboard refresh event");
// Prevent too frequent refreshes
const now = Date.now();
const timeSinceLastRefresh = now - lastUpdateTime;
if (timeSinceLastRefresh >= MIN_REFRESH_INTERVAL) {
console.log("Syncing refresh with main dashboard");
// Reset progress bar and immediately fetch
resetProgressBar();
// Refresh the worker data
fetchWorkerData();
} else {
console.log("Skipping too-frequent refresh", timeSinceLastRefresh);
// Just reset the progress bar to match main dashboard
resetProgressBar();
}
}
});
// On page load, check if we should align with main dashboard timing
try {
const lastDashboardRefresh = localStorage.getItem('dashboardRefreshTime');
if (lastDashboardRefresh) {
const lastRefreshTime = parseInt(lastDashboardRefresh);
const timeSinceLastDashboardRefresh = Date.now() - lastRefreshTime;
// If main dashboard refreshed recently, adjust our timer
if (timeSinceLastDashboardRefresh < PROGRESS_MAX * 1000) {
console.log("Adjusting timer to align with main dashboard");
currentProgress = Math.floor(timeSinceLastDashboardRefresh / 1000);
updateProgressBar(currentProgress);
// Calculate when next update will happen (roughly 60 seconds from last dashboard refresh)
const timeUntilNextRefresh = (PROGRESS_MAX * 1000) - timeSinceLastDashboardRefresh;
// Schedule a one-time check near the expected refresh time
if (timeUntilNextRefresh > 0) {
console.log(`Scheduling coordinated refresh in ${Math.floor(timeUntilNextRefresh/1000)} seconds`);
setTimeout(function() {
// Check if a refresh happened in the last few seconds via localStorage event
const newLastRefresh = parseInt(localStorage.getItem('dashboardRefreshTime') || '0');
const secondsSinceLastRefresh = (Date.now() - newLastRefresh) / 1000;
// If dashboard hasn't refreshed in the last 5 seconds, do our own refresh
if (secondsSinceLastRefresh > 5) {
console.log("Coordinated refresh time reached, fetching data");
fetchWorkerData();
} else {
console.log("Dashboard already refreshed recently, skipping coordinated refresh");
}
}, timeUntilNextRefresh);
}
}
}
} catch (e) {
console.error("Error reading dashboard refresh time:", e);
}
// Check for dashboard refresh periodically
setInterval(function() {
try {
const lastDashboardRefresh = parseInt(localStorage.getItem('dashboardRefreshTime') || '0');
const now = Date.now();
const timeSinceLastRefresh = (now - lastUpdateTime) / 1000;
const timeSinceDashboardRefresh = (now - lastDashboardRefresh) / 1000;
// If dashboard refreshed more recently than we did and we haven't refreshed in at least 10 seconds
if (lastDashboardRefresh > lastUpdateTime && timeSinceLastRefresh > 10) {
console.log("Catching up with dashboard refresh");
resetProgressBar();
fetchWorkerData();
}
} catch (e) {
console.error("Error in periodic dashboard check:", e);
}
}, 5000); // Check every 5 seconds
}
// Server time update via polling - same as main.js
function updateServerTime() {
$.ajax({
url: "/api/time",
method: "GET",
timeout: 5000,
success: function(data) {
serverTimeOffset = new Date(data.server_timestamp).getTime() - Date.now();
serverStartTime = new Date(data.server_start_time).getTime();
},
error: function(jqXHR, textStatus, errorThrown) {
console.error("Error fetching server time:", textStatus, errorThrown);
}
});
}
// Update uptime display - synced with main dashboard
function updateUptime() {
if (serverStartTime) {
const currentServerTime = Date.now() + serverTimeOffset;
const diff = currentServerTime - serverStartTime;
const hours = Math.floor(diff / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
$("#uptimeTimer").html("<strong>Uptime:</strong> " + hours + "h " + minutes + "m " + seconds + "s");
}
}
// Initialize page elements
function initializePage() {
// Initialize mini chart for total hashrate if the element exists
if (document.getElementById('total-hashrate-chart')) {
initializeMiniChart();
}
// Show loading state
$('#worker-grid').html('<div class="text-center p-5"><i class="fas fa-spinner fa-spin"></i> Loading worker data...</div>');
// Add retry button (hidden by default)
if (!$('#retry-button').length) {
$('body').append('<button id="retry-button" style="position: fixed; bottom: 20px; left: 20px; z-index: 1000; background: #f7931a; color: black; border: none; padding: 8px 16px; display: none; border-radius: 4px; cursor: pointer;">Retry Loading Data</button>');
$('#retry-button').on('click', function() {
$(this).text('Retrying...').prop('disabled', true);
fetchWorkerData(true);
setTimeout(() => {
$('#retry-button').text('Retry Loading Data').prop('disabled', false);
}, 3000);
});
}
}
// Fetch worker data from API
function fetchWorkerData(forceRefresh = false) {
// Track this as a manual refresh for throttling purposes
lastManualRefreshTime = Date.now();
$('#worker-grid').addClass('loading-fade');
// Update progress bar to show data is being fetched
resetProgressBar();
// Choose API URL based on whether we're forcing a refresh
const apiUrl = `/api/workers${forceRefresh ? '?force=true' : ''}`;
$.ajax({
url: apiUrl,
method: 'GET',
dataType: 'json',
timeout: 15000, // 15 second timeout
success: function(data) {
workerData = data;
lastUpdateTime = Date.now();
// Update UI with new data
updateWorkerGrid();
updateSummaryStats();
updateMiniChart();
updateLastUpdated();
// Hide retry button
$('#retry-button').hide();
// Reset connection retry count
connectionRetryCount = 0;
console.log("Worker data updated successfully");
},
error: function(xhr, status, error) {
console.error("Error fetching worker data:", error);
// Show error in worker grid
$('#worker-grid').html(`
<div class="text-center p-5 text-danger">
<i class="fas fa-exclamation-triangle"></i>
<p>Error loading worker data: ${error || 'Unknown error'}</p>
</div>
`);
// Show retry button
$('#retry-button').show();
// Implement exponential backoff for automatic retry
connectionRetryCount++;
const delay = Math.min(30000, 1000 * Math.pow(1.5, Math.min(5, connectionRetryCount)));
console.log(`Will retry in ${delay/1000} seconds (attempt ${connectionRetryCount})`);
setTimeout(() => {
fetchWorkerData(true); // Force refresh on retry
}, delay);
},
complete: function() {
$('#worker-grid').removeClass('loading-fade');
}
});
}
// Update the worker grid with data
function updateWorkerGrid() {
if (!workerData || !workerData.workers) {
console.error("No worker data available");
return;
}
const workerGrid = $('#worker-grid');
workerGrid.empty();
// Apply current filters before rendering
const filteredWorkers = filterWorkersData(workerData.workers);
if (filteredWorkers.length === 0) {
workerGrid.html(`
<div class="text-center p-5">
<i class="fas fa-search"></i>
<p>No workers match your filter criteria</p>
</div>
`);
return;
}
// Generate worker cards
filteredWorkers.forEach(worker => {
// Create worker card
const card = $('<div class="worker-card"></div>');
// Add class based on status
if (worker.status === 'online') {
card.addClass('worker-card-online');
} else {
card.addClass('worker-card-offline');
}
// Add worker type badge
card.append(`<div class="worker-type">${worker.type}</div>`);
// Add worker name
card.append(`<div class="worker-name">${worker.name}</div>`);
// Add status badge
if (worker.status === 'online') {
card.append('<div class="status-badge status-badge-online">ONLINE</div>');
} else {
card.append('<div class="status-badge status-badge-offline">OFFLINE</div>');
}
// Add hashrate bar
const maxHashrate = 200; // TH/s - adjust based on your fleet
const hashratePercent = Math.min(100, (worker.hashrate_3hr / maxHashrate) * 100);
card.append(`
<div class="worker-stats-row">
<div class="worker-stats-label">Hashrate (3hr):</div>
<div class="white-glow">${worker.hashrate_3hr} ${worker.hashrate_3hr_unit}</div>
</div>
<div class="stats-bar-container">
<div class="stats-bar" style="width: ${hashratePercent}%"></div>
</div>
`);
// Add additional stats
card.append(`
<div class="worker-stats">
<div class="worker-stats-row">
<div class="worker-stats-label">Last Share:</div>
<div class="blue-glow">${worker.last_share.split(' ')[1]}</div>
</div>
<div class="worker-stats-row">
<div class="worker-stats-label">Earnings:</div>
<div class="green-glow">${worker.earnings.toFixed(8)}</div>
</div>
<div class="worker-stats-row">
<div class="worker-stats-label">Accept Rate:</div>
<div class="white-glow">${worker.acceptance_rate}%</div>
</div>
<div class="worker-stats-row">
<div class="worker-stats-label">Temp:</div>
<div class="${worker.temperature > 65 ? 'red-glow' : 'white-glow'}">${worker.temperature > 0 ? worker.temperature + '°C' : 'N/A'}</div>
</div>
</div>
`);
// Add card to grid
workerGrid.append(card);
});
}
// Filter worker data based on current filter state
function filterWorkersData(workers) {
if (!workers) return [];
return workers.filter(worker => {
const workerName = worker.name.toLowerCase();
const isOnline = worker.status === 'online';
const workerType = worker.type.toLowerCase();
// Check if worker matches filter
let matchesFilter = false;
if (filterState.currentFilter === 'all') {
matchesFilter = true;
} else if (filterState.currentFilter === 'online' && isOnline) {
matchesFilter = true;
} else if (filterState.currentFilter === 'offline' && !isOnline) {
matchesFilter = true;
} else if (filterState.currentFilter === 'asic' && workerType === 'asic') {
matchesFilter = true;
} else if (filterState.currentFilter === 'fpga' && workerType === 'fpga') {
matchesFilter = true;
}
// Check if worker matches search term
const matchesSearch = workerName.includes(filterState.searchTerm);
return matchesFilter && matchesSearch;
});
}
// Apply filter to rendered worker cards
function filterWorkers() {
if (!workerData || !workerData.workers) return;
// Re-render the worker grid with current filters
updateWorkerGrid();
}
// Modified updateSummaryStats function for workers.js
function updateSummaryStats() {
if (!workerData) return;
// Update worker counts
$('#workers-count').text(workerData.workers_total || 0);
$('#workers-online').text(workerData.workers_online || 0);
$('#workers-offline').text(workerData.workers_offline || 0);
// Update worker ring percentage
const onlinePercent = workerData.workers_total > 0 ?
workerData.workers_online / workerData.workers_total : 0;
$('.worker-ring').css('--online-percent', onlinePercent);
// IMPORTANT: Update total hashrate using EXACT format matching main dashboard
// This ensures the displayed value matches exactly what's on the main page
if (workerData.total_hashrate !== undefined) {
// Format with exactly 1 decimal place - matches main dashboard format
const formattedHashrate = Number(workerData.total_hashrate).toFixed(1);
$('#total-hashrate').text(`${formattedHashrate} ${workerData.hashrate_unit || 'TH/s'}`);
} else {
$('#total-hashrate').text(`0.0 ${workerData.hashrate_unit || 'TH/s'}`);
}
// Update other summary stats
$('#total-earnings').text(`${(workerData.total_earnings || 0).toFixed(8)} BTC`);
$('#daily-sats').text(`${numberWithCommas(workerData.daily_sats || 0)} sats`);
$('#avg-acceptance-rate').text(`${(workerData.avg_acceptance_rate || 0).toFixed(2)}%`);
}
// Initialize mini chart
function initializeMiniChart() {
const ctx = document.getElementById('total-hashrate-chart').getContext('2d');
// Generate some sample data to start
const labels = Array(24).fill('').map((_, i) => i);
const data = [750, 760, 755, 770, 780, 775, 760, 765, 770, 775, 780, 790, 785, 775, 770, 765, 780, 785, 775, 770, 775, 780, 775, 774.8];
miniChart = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [{
data: data,
borderColor: '#1137F5',
backgroundColor: 'rgba(57, 255, 20, 0.1)',
fill: true,
tension: 0.3,
borderWidth: 1.5,
pointRadius: 0
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: { display: false },
y: {
display: false,
min: Math.min(...data) * 0.9,
max: Math.max(...data) * 1.1
}
},
plugins: {
legend: { display: false },
tooltip: { enabled: false }
},
animation: false,
elements: {
line: {
tension: 0.4
}
}
}
});
}
// Update mini chart with real data
function updateMiniChart() {
if (!miniChart || !workerData || !workerData.hashrate_history) return;
// Extract hashrate data from history
const historyData = workerData.hashrate_history;
if (!historyData || historyData.length === 0) return;
// Get the values for the chart
const values = historyData.map(item => parseFloat(item.value) || 0);
const labels = historyData.map(item => item.time);
// Update chart data
miniChart.data.labels = labels;
miniChart.data.datasets[0].data = values;
// Update y-axis range
const min = Math.min(...values);
const max = Math.max(...values);
miniChart.options.scales.y.min = min * 0.9;
miniChart.options.scales.y.max = max * 1.1;
// Update the chart
miniChart.update('none');
}
// Update progress bar
function updateProgressBar() {
if (currentProgress < PROGRESS_MAX) {
currentProgress++;
const progressPercent = (currentProgress / PROGRESS_MAX) * 100;
$("#bitcoin-progress-inner").css("width", progressPercent + "%");
// Add glowing effect when close to completion
if (progressPercent > 80) {
$("#bitcoin-progress-inner").addClass("glow-effect");
} else {
$("#bitcoin-progress-inner").removeClass("glow-effect");
}
// Update remaining seconds text
let remainingSeconds = PROGRESS_MAX - currentProgress;
if (remainingSeconds <= 0) {
$("#progress-text").text("Waiting for update...");
$("#bitcoin-progress-inner").addClass("waiting-for-update");
} else {
$("#progress-text").text(remainingSeconds + "s to next update");
$("#bitcoin-progress-inner").removeClass("waiting-for-update");
}
// Check for main dashboard refresh near the end to ensure sync
if (currentProgress >= 55) { // When we're getting close to refresh time
try {
const lastDashboardRefresh = parseInt(localStorage.getItem('dashboardRefreshTime') || '0');
const secondsSinceDashboardRefresh = (Date.now() - lastDashboardRefresh) / 1000;
// If main dashboard just refreshed (within last 5 seconds)
if (secondsSinceDashboardRefresh <= 5) {
console.log("Detected recent dashboard refresh, syncing now");
resetProgressBar();
fetchWorkerData();
return;
}
} catch (e) {
console.error("Error checking dashboard refresh status:", e);
}
}
} else {
// Reset progress bar if it's time to refresh
// But first check if the main dashboard refreshed recently
try {
const lastDashboardRefresh = parseInt(localStorage.getItem('dashboardRefreshTime') || '0');
const secondsSinceDashboardRefresh = (Date.now() - lastDashboardRefresh) / 1000;
// If dashboard refreshed in the last 10 seconds, wait for it instead of refreshing ourselves
if (secondsSinceDashboardRefresh < 10) {
console.log("Waiting for dashboard refresh event instead of refreshing independently");
return;
}
} catch (e) {
console.error("Error checking dashboard refresh status:", e);
}
// If main dashboard hasn't refreshed recently, do our own refresh
if (Date.now() - lastUpdateTime > PROGRESS_MAX * 1000) {
console.log("Progress bar expired, fetching data");
fetchWorkerData();
}
}
}
// Reset progress bar
function resetProgressBar() {
currentProgress = 0;
$("#bitcoin-progress-inner").css("width", "0%");
$("#bitcoin-progress-inner").removeClass("glow-effect");
$("#bitcoin-progress-inner").removeClass("waiting-for-update");
$("#progress-text").text(PROGRESS_MAX + "s to next update");
}
// Update the last updated timestamp
function updateLastUpdated() {
if (!workerData || !workerData.timestamp) return;
try {
const timestamp = new Date(workerData.timestamp);
$("#lastUpdated").html("<strong>Last Updated:</strong> " +
timestamp.toLocaleString() + "<span id='terminal-cursor'></span>");
} catch (e) {
console.error("Error formatting timestamp:", e);
}
}
// Format numbers with commas
function numberWithCommas(x) {
if (x == null) return "N/A";
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}