Delete static directory

This commit is contained in:
DJObleezy 2025-04-09 11:43:06 -07:00 committed by GitHub
parent 35dd182eb2
commit d374bc3ba1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 0 additions and 6843 deletions

View File

@ -1,380 +0,0 @@
/* Styles specific to the blocks page */
/* Block controls */
.block-controls {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
gap: 10px;
}
.block-control-item {
display: flex;
align-items: center;
gap: 10px;
}
.block-input {
background-color: var(--bg-color) !important;
border: 1px solid var(--primary-color) !important;
color: var(--text-color);
padding: 5px 10px;
font-family: var(--terminal-font);
width: 150px;
}
.block-input:focus {
outline: none;
box-shadow: 0 0 8px rgba(247, 147, 26, 0.5);
}
.block-button {
background-color: var(--bg-color);
border: 1px solid var(--primary-color);
color: var(--primary-color);
padding: 5px 15px;
font-family: var(--terminal-font);
cursor: pointer;
transition: all 0.2s ease;
}
.block-button:hover {
background-color: var(--primary-color);
color: var(--bg-color);
box-shadow: 0 0 10px rgba(247, 147, 26, 0.5);
}
/* Latest block stats */
.latest-block-stats {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 15px;
}
.stat-item {
display: flex;
flex-direction: column;
}
.stat-item strong {
color: #f7931a; /* Use the Bitcoin orange color for labels */
}
/* Blocks grid */
.blocks-container {
overflow-x: auto;
}
.blocks-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 15px;
margin-top: 15px;
}
.block-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: 12px;
cursor: pointer;
transition: all 0.2s ease;
}
.block-card:hover {
box-shadow: 0 0 15px rgba(247, 147, 26, 0.5);
transform: translateY(-2px);
}
.block-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;
}
.block-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.block-height {
font-size: 1.2rem;
font-weight: bold;
color: var(--primary-color);
text-shadow: 0 0 5px rgba(247, 147, 26, 0.5);
}
.block-time {
font-size: 0.9rem;
color: #00dfff;
}
.block-info {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px 15px;
}
.block-info-item {
display: flex;
flex-direction: column;
}
.block-info-label {
font-size: 0.8rem;
color: #aaa;
}
.block-info-value {
font-size: 0.9rem;
}
.block-info-value.yellow {
color: #ffd700;
text-shadow: 0 0 8px rgba(255, 215, 0, 0.6);
}
.block-info-value.green {
color: #32CD32;
text-shadow: 0 0 6px rgba(50, 205, 50, 0.6);
}
.block-info-value.blue {
color: #00dfff;
text-shadow: 0 0 6px rgba(0, 223, 255, 0.6);
}
.block-info-value.white {
color: #ffffff;
text-shadow: 0 0 6px rgba(255, 255, 255, 0.6);
}
.block-info-value.red {
color: #ff5555;
text-shadow: 0 0 6px rgba(255, 85, 85, 0.6);
}
/* Loader */
.loader {
text-align: center;
padding: 20px;
grid-column: 1 / -1;
}
.loader-text {
display: inline-block;
margin-right: 5px;
}
/* Modal styles */
.block-modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0, 0, 0, 0.8);
}
.block-modal-content {
background-color: var(--bg-color);
margin: 5% auto;
border: 1px solid var(--primary-color);
box-shadow: 0 0 20px rgba(247, 147, 26, 0.5);
width: 90%;
max-width: 800px;
max-height: 80vh;
overflow-y: auto;
position: relative;
}
.block-modal-content::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: 0;
}
.block-modal-header {
background-color: #000;
color: var(--primary-color);
font-weight: bold;
padding: 0.5rem 1rem;
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);
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
z-index: 1;
}
.block-modal-close {
color: var(--primary-color);
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
.block-modal-close:hover,
.block-modal-close:focus {
color: #ffa500;
text-shadow: 0 0 10px rgba(255, 165, 0, 0.8);
}
.block-modal-body {
padding: 1rem;
position: relative;
z-index: 1;
}
#block-details {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 15px;
}
.block-detail-section {
margin-bottom: 15px;
}
.block-detail-title {
font-size: 1.1rem;
color: var(--primary-color);
margin-bottom: 10px;
text-shadow: 0 0 5px rgba(247, 147, 26, 0.5);
font-weight: bold;
}
.block-detail-item {
margin-bottom: 8px;
}
.block-detail-label {
font-size: 0.9rem;
color: #aaa;
}
.block-detail-value {
font-size: 0.9rem;
word-break: break-all;
}
.block-hash {
font-family: monospace;
font-size: 0.8rem;
color: #00dfff;
word-break: break-all;
}
.transaction-data {
display: flex;
flex-direction: column;
gap: 5px;
}
.fee-bar-container {
height: 5px;
width: 100%;
background-color: rgba(255, 255, 255, 0.1);
margin-top: 5px;
position: relative;
overflow: hidden;
}
.fee-bar {
height: 100%;
width: 0;
background: linear-gradient(90deg, #32CD32, #ffd700);
transition: width 0.5s ease;
}
/* Mining Animation Container */
.mining-animation-container {
padding: 0;
background-color: #0a0a0a;
overflow: hidden;
position: relative;
width: 100%;
}
.mining-animation-container::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;
}
#svg-container {
width: 100%;
height: 300px;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center; /* Add this to center vertically if needed */
}
svg {
max-width: 100%;
height: auto;
display: block; /* Ensures proper centering */
}
/* Make sure the SVG itself takes more width */
#block-mining-animation {
width: 100%;
height: 300px;
/* Fixed height but full width */
}
/* Mobile responsiveness */
@media (max-width: 768px) {
.latest-block-stats {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
.blocks-grid {
grid-template-columns: 1fr;
}
.block-modal-content {
width: 95%;
margin: 10% auto;
}
#block-details {
grid-template-columns: 1fr;
}
#svg-container {
height: 250px;
}
}

View File

@ -1,216 +0,0 @@
/* Base Styles with a subtle radial background for extra depth */
body {
background: linear-gradient(135deg, #121212, #000000);
color: #f7931a;
font-family: 'VT323', monospace;
font-size: 20px;
line-height: 1.4;
margin: 0;
padding: 10px;
overflow-x: hidden;
text-shadow: 0 0 5px rgba(247, 147, 26, 0.4);
height: calc(100vh - 100px);
display: flex;
flex-direction: column;
}
/* 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; }
}
/* Terminal Window with scrolling enabled */
#terminal {
width: 100%;
max-width: 900px;
margin: 0 auto;
white-space: pre-wrap;
word-break: break-word;
animation: flicker 4s infinite;
height: 400px;
overflow-y: auto;
position: relative;
flex: 1;
}
#terminal-content {
position: absolute;
bottom: 0;
width: 100%;
}
.cursor {
display: inline-block;
width: 10px;
height: 16px;
background-color: #f7931a;
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; }
}
/* Neon-inspired color classes */
.green {
color: #39ff14 !important;
text-shadow: 0 0 10px #39ff14, 0 0 20px #39ff14;
}
.blue {
color: #00dfff !important;
text-shadow: 0 0 10px #00dfff, 0 0 20px #00dfff;
}
.yellow {
color: #ffd700 !important;
text-shadow: 0 0 8px #ffd700, 0 0 16px #ffd700;
}
.white {
color: #ffffff !important;
text-shadow: 0 0 8px #ffffff, 0 0 16px #ffffff;
}
.red {
color: #ff2d2d !important;
text-shadow: 0 0 10px #ff2d2d, 0 0 20px #ff2d2d;
}
.magenta {
color: #ff2d95 !important;
text-shadow: 0 0 10px #ff2d95, 0 0 20px #ff2d95;
}
/* Bitcoin Logo styling with extra neon border */
#bitcoin-logo {
display: block;
visibility: hidden;
text-align: center;
margin: 10px auto;
font-size: 10px;
line-height: 1;
color: #f7931a;
text-shadow: 0 0 10px rgba(247, 147, 26, 0.8);
white-space: pre;
width: 260px;
padding: 10px;
border: 2px solid #f7931a;
background-color: #0a0a0a;
box-shadow: 0 0 15px rgba(247, 147, 26, 0.5);
font-family: monospace;
opacity: 0;
transition: opacity 1s ease;
}
/* Skip Button */
#skip-button {
position: fixed;
bottom: 20px;
right: 20px;
background-color: #f7931a;
color: #000;
border: none;
padding: 10px 15px;
border-radius: 5px;
cursor: pointer;
font-family: 'VT323', monospace;
font-size: 16px;
box-shadow: 0 0 8px rgba(247, 147, 26, 0.5);
transition: all 0.2s ease;
}
#skip-button:hover {
background-color: #ffa32e;
box-shadow: 0 0 12px rgba(247, 147, 26, 0.7);
}
/* Prompt Styling */
#prompt-container {
display: none;
white-space: nowrap;
}
#prompt-text {
color: #f7931a;
margin-right: 5px;
text-shadow: 0 0 5px rgba(247, 147, 26, 0.8);
display: inline;
}
#user-input {
background: transparent;
border: none;
color: #f7931a;
font-family: 'VT323', monospace;
font-size: 20px;
caret-color: transparent;
outline: none;
width: 35px;
height: 33px;
padding: 0;
margin: 0;
text-shadow: 0 0 5px rgba(247, 147, 26, 0.8);
display: inline-block;
vertical-align: top;
}
.prompt-cursor {
display: inline-block;
width: 10px;
height: 16px;
background-color: #f7931a;
animation: blink 1s step-end infinite;
vertical-align: middle;
box-shadow: 0 0 5px rgba(247, 147, 26, 0.8);
position: relative;
top: 1px;
margin-left: -2px;
}
/* Mobile Responsiveness */
@media (max-width: 600px) {
body { font-size: 14px; padding: 10px; }
#terminal { margin: 0; }
}
/* Loading and Debug Info */
#loading-message {
text-align: center;
margin-bottom: 10px;
text-shadow: 0 0 5px rgba(247, 147, 26, 0.8);
}
#debug-info {
position: fixed;
bottom: 10px;
left: 10px;
color: #666;
font-size: 12px;
z-index: 100;
}

View File

@ -1,434 +0,0 @@
/* Common styling elements shared across all pages */
: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);
}
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;
}
}
/* 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);
}
/* Top right link */
#topRightLink {
position: absolute;
top: 10px;
right: 10px;
color: grey;
text-decoration: none;
font-size: 0.9rem;
text-shadow: 0 0 5px grey;
}
/* Card styles */
.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;
}
/* 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);
}
/* 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; }
}
/* Container */
.container-fluid {
max-width: 1200px;
margin: 0 auto;
padding-left: 1rem;
padding-right: 1rem;
position: relative;
}
/* Status indicators */
.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; }
}
.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; }
}
/* Color utility classes */
.green-glow, .status-green {
color: #39ff14 !important;
text-shadow: 0 0 10px #39ff14, 0 0 20px #39ff14;
}
.red-glow, .status-red {
color: #ff2d2d !important;
text-shadow: 0 0 10px #ff2d2d, 0 0 20px #ff2d2d;
}
.yellow-glow {
color: #ffd700 !important;
text-shadow: 0 0 6px #ffd700, 0 0 10px #ffd700;
}
.blue-glow {
color: #00dfff !important;
text-shadow: 0 0 6px #00dfff, 0 0 10px #00dfff;
}
.white-glow {
color: #ffffff !important;
text-shadow: 0 0 6px #ffffff, 0 0 10px #ffffff;
}
/* Basic color classes for backward compatibility */
.green {
color: #39ff14 !important;
text-shadow: 0 0 10px #39ff14, 0 0 20px #39ff14;
}
.blue {
color: #00dfff !important;
text-shadow: 0 0 10px #00dfff, 0 0 20px #00dfff;
}
.yellow {
color: #ffd700 !important;
text-shadow: 0 0 8px #ffd700, 0 0 16px #ffd700;
}
.white {
color: #ffffff !important;
text-shadow: 0 0 8px #ffffff, 0 0 16px #ffffff;
}
.red {
color: #ff2d2d !important;
text-shadow: 0 0 10px #ff2d2d, 0 0 20px #ff2d2d;
}
.magenta {
color: #ff2d95 !important;
text-shadow: 0 0 10px #ff2d95, 0 0 20px #ff2d95;
}
/* 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;
}
/* Extra styling for when server update is late */
.waiting-for-update {
animation: waitingPulse 2s infinite !important;
}
@keyframes waitingPulse {
0%, 100% { box-shadow: 0 0 10px #f7931a, 0 0 15px #f7931a; opacity: 0.8; }
50% { box-shadow: 0 0 20px #f7931a, 0 0 35px #f7931a; opacity: 1; }
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.8; }
}
#progress-text {
font-size: 1rem;
color: var(--primary-color);
margin-top: 0.3rem;
text-shadow: 0 0 5px var(--primary-color);
text-align: center;
width: 100%;
}
/* Mobile responsiveness */
@media (max-width: 576px) {
.container-fluid {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.card-body {
padding: 0.5rem;
}
h1 {
font-size: 22px;
}
.card-header {
font-size: 1rem;
}
#topRightLink {
position: static;
display: block;
text-align: right;
margin-bottom: 0.5rem;
}
}
/* Navigation badges for notifications */
.nav-badge {
background-color: var(--primary-color);
color: var(--bg-color);
border-radius: 10px;
font-size: 0.7rem;
padding: 1px 5px;
min-width: 16px;
text-align: center;
display: none;
margin-left: 5px;
vertical-align: middle;
}

View File

@ -1,221 +0,0 @@
/* Specific styles for the main dashboard */
#graphContainer {
background-color: #000;
padding: 0.5rem;
margin-bottom: 1rem;
height: 230px;
border: 1px solid var(--primary-color);
box-shadow: 0 0 10px rgba(247, 147, 26, 0.2);
position: relative;
}
/* Add scanline effect to graph */
#graphContainer::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: repeating-linear-gradient(
0deg,
rgba(0, 0, 0, 0.1),
rgba(0, 0, 0, 0.1) 1px,
transparent 1px,
transparent 2px
);
pointer-events: none;
z-index: 1;
}
/* Override for Payout & Misc card */
#payoutMiscCard {
margin-bottom: 0.5rem;
}
/* Row equal height for card alignment */
.row.equal-height {
display: flex;
flex-wrap: wrap;
margin-bottom: 1rem;
}
.row.equal-height > [class*="col-"] {
display: flex;
margin-bottom: 0.5rem;
}
.row.equal-height > [class*="col-"] .card {
width: 100%;
}
/* Arrow indicator styles */
.arrow {
display: inline-block;
font-weight: bold;
margin-left: 0.5rem;
}
/* Bounce animations for indicators */
@keyframes bounceUp {
0% { transform: translateY(0); }
25% { transform: translateY(-2px); }
50% { transform: translateY(0); }
75% { transform: translateY(-2px); }
100% { transform: translateY(0); }
}
@keyframes bounceDown {
0% { transform: translateY(0); }
25% { transform: translateY(2px); }
50% { transform: translateY(0); }
75% { transform: translateY(2px); }
100% { transform: translateY(0); }
}
.bounce-up {
animation: bounceUp 1s infinite;
}
.bounce-down {
animation: bounceDown 1s infinite;
}
.chevron {
font-size: 0.8rem;
position: relative;
top: 3px;
}
/* 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%;
}
#uptimeTimer strong {
font-weight: bold;
}
#uptimeTimer {
margin-top: 0;
}
/* Metric styling by category */
.metric-value {
color: var(--text-color);
font-weight: bold;
text-shadow: 0 0 6px rgba(255, 255, 255, 0.6);
}
/* Yellow color family (BTC price, sats metrics, time to payout) */
#btc_price,
#daily_mined_sats,
#monthly_mined_sats,
#estimated_earnings_per_day_sats,
#estimated_earnings_next_block_sats,
#estimated_rewards_in_window_sats,
#est_time_to_payout {
color: #ffd700;
text-shadow: 0 0 6px rgba(255, 215, 0, 0.6);
}
/* Green color family (profits, earnings) */
#unpaid_earnings,
#daily_revenue,
#daily_profit_usd,
#monthly_profit_usd {
color: #32CD32;
text-shadow: 0 0 6px rgba(50, 205, 50, 0.6);
}
/* Red color family (costs) */
#daily_power_cost {
color: #ff5555 !important;
text-shadow: 0 0 6px rgba(255, 85, 85, 0.6);
}
/* White metrics (general stats) */
.metric-value.white,
#block_number,
#network_hashrate,
#difficulty,
#workers_hashing,
#last_share,
#blocks_found,
#last_block_height {
color: #ffffff;
text-shadow: 0 0 6px rgba(255, 255, 255, 0.6);
}
/* Blue metrics (time data) */
#last_block_time {
color: #00dfff;
text-shadow: 0 0 6px rgba(0, 223, 255, 0.6);
}
.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;
}
/* Hidden Congrats Message */
#congratsMessage {
display: none;
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
background: #f7931a;
color: #000;
padding: 10px;
border-radius: 5px;
box-shadow: 0 0 15px rgba(247, 147, 26, 0.7);
}
/* Add bottom padding to accommodate minimized system monitor */
.container-fluid {
padding-bottom: 60px !important; /* Enough space for minimized monitor */
}
/* Ensure last card has proper margin to avoid being hidden */
#payoutMiscCard {
margin-bottom: 60px;
}
/* Add these styles to dashboard.css */
@keyframes pulse-block-marker {
0% {
transform: translate(-50%, -50%) rotate(45deg) scale(1);
opacity: 1;
}
50% {
transform: translate(-50%, -50%) rotate(45deg) scale(1.3);
opacity: 0.8;
}
100% {
transform: translate(-50%, -50%) rotate(45deg) scale(1);
opacity: 1;
}
}
.chart-container-relative {
position: relative;
}

View File

@ -1,138 +0,0 @@
:root {
--bg-color: #0a0a0a;
--bg-gradient: linear-gradient(135deg, #0a0a0a, #1a1a1a);
--primary-color: #f7931a;
--text-color: white;
--terminal-font: 'VT323', monospace;
--header-font: 'Orbitron', sans-serif;
}
/* 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: 50px;
font-family: var(--terminal-font);
text-shadow: 0 0 5px rgba(255, 255, 255, 0.3);
}
a.btn-primary {
background-color: var(--primary-color);
border-color: var(--primary-color);
color: black;
margin-top: 20px;
font-family: var(--header-font);
text-shadow: none;
box-shadow: 0 0 10px rgba(247, 147, 26, 0.5);
transition: all 0.3s ease;
}
a.btn-primary:hover {
background-color: #ffa64d;
box-shadow: 0 0 15px rgba(247, 147, 26, 0.7);
}
/* Enhanced error container with scanlines */
.error-container {
max-width: 600px;
margin: 0 auto;
text-align: center;
padding: 2rem;
border: 1px solid var(--primary-color);
border-radius: 0;
background-color: rgba(0, 0, 0, 0.3);
box-shadow: 0 0 15px rgba(247, 147, 26, 0.3);
position: relative;
overflow: hidden;
animation: flicker 4s infinite;
}
/* Scanline effect for error container */
.error-container::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: repeating-linear-gradient(
0deg,
rgba(0, 0, 0, 0.1),
rgba(0, 0, 0, 0.1) 1px,
transparent 1px,
transparent 2px
);
pointer-events: none;
z-index: 1;
}
h1 {
color: var(--primary-color);
margin-bottom: 1rem;
font-family: var(--header-font);
font-weight: bold;
text-shadow: 0 0 10px var(--primary-color);
position: relative;
z-index: 2;
}
p {
margin-bottom: 1.5rem;
font-size: 1.5rem;
position: relative;
z-index: 2;
color: #ff5555;
text-shadow: 0 0 8px rgba(255, 85, 85, 0.6);
}
/* Cursor blink for terminal feel */
.terminal-cursor {
display: inline-block;
width: 10px;
height: 20px;
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; }
}
/* Error code styling */
.error-code {
font-family: var(--terminal-font);
font-size: 1.2rem;
color: #00dfff;
text-shadow: 0 0 10px #00dfff, 0 0 20px #00dfff;
margin-bottom: 1rem;
}

View File

@ -1,324 +0,0 @@
/* notifications.css */
/* Notification Controls */
.notification-controls {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 10px;
}
.full-timestamp {
font-size: 0.8em;
color: #888;
}
.filter-buttons {
display: flex;
flex-wrap: wrap;
gap: 5px;
}
.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;
transition: all 0.3s ease;
}
.filter-button:hover {
background-color: rgba(247, 147, 26, 0.2);
}
.filter-button.active {
background-color: var(--primary-color);
color: var(--bg-color);
box-shadow: 0 0 10px rgba(247, 147, 26, 0.5);
}
.notification-actions {
display: flex;
gap: 5px;
align-items:center;
}
.action-button {
background-color: var(--bg-color);
border: 1px solid var(--primary-color);
color: var(--primary-color);
padding: 6px 12px;
font-family: var(--terminal-font);
cursor: pointer;
transition: all 0.2s ease;
min-width: 80px; /* Set a minimum width to prevent text cutoff */
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 0.9rem; /* Slightly smaller font */
line-height: 1;
}
.action-button:hover {
background-color: rgba(247, 147, 26, 0.2);
}
.action-button.danger {
border-color: #ff5555;
color: #ff5555;
}
.action-button.danger:hover {
background-color: rgba(255, 85, 85, 0.2);
}
/* Card header with unread badge */
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.unread-badge {
background-color: var(--primary-color);
color: var(--bg-color);
padding: 2px 8px;
border-radius: 10px;
font-size: 0.8rem;
min-width: 25px;
text-align: center;
}
.unread-badge:empty {
display: none;
}
/* Notifications Container */
#notifications-container {
min-height: 200px;
position: relative;
}
.loading-message {
text-align: center;
padding: 20px;
color: #888;
}
.empty-state {
text-align: center;
padding: 40px 20px;
color: #888;
}
.empty-state i {
font-size: 3rem;
margin-bottom: 15px;
opacity: 0.5;
}
/* Notification Item */
.notification-item {
display: flex;
padding: 12px;
border-bottom: 1px solid rgba(247, 147, 26, 0.2);
transition: background-color 0.2s ease;
position: relative;
background-color: rgba(0, 0, 0, 0.15);
}
.notification-item:hover {
background-color: rgba(247, 147, 26, 0.05);
}
.notification-item[data-read="true"] {
opacity: 0.6;
}
.notification-item[data-level="success"] {
border-left: 3px solid #32CD32;
}
.notification-item[data-level="info"] {
border-left: 3px solid #00dfff;
}
.notification-item[data-level="warning"] {
border-left: 3px solid #ffd700;
}
.notification-item[data-level="error"] {
border-left: 3px solid #ff5555;
}
.notification-icon {
flex: 0 0 40px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
}
.notification-item[data-level="success"] .notification-icon i {
color: #32CD32;
}
.notification-item[data-level="info"] .notification-icon i {
color: #00dfff;
}
.notification-item[data-level="warning"] .notification-icon i {
color: #ffd700;
}
.notification-item[data-level="error"] .notification-icon i {
color: #ff5555;
}
.notification-content {
flex: 1;
padding: 0 15px;
}
.notification-message {
margin-bottom: 5px;
word-break: break-word;
color: white;
}
.notification-meta {
font-size: 0.8rem;
color: #888;
display: flex;
gap: 15px;
}
.notification-category {
text-transform: uppercase;
font-size: 0.7rem;
color: #aaa;
}
.notification-actions {
flex: 0 0 80px;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 5px;
}
.notification-actions button {
background: none;
border: none;
color: #888;
cursor: pointer;
transition: color 0.2s ease;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 3px;
}
.mark-read-button:hover {
color: #32CD32;
background-color: rgba(50, 205, 50, 0.1);
}
.delete-button:hover {
color: #ff5555;
background-color: rgba(255, 85, 85, 0.1);
}
/* Pagination */
.pagination-controls {
margin-top: 15px;
text-align: center;
}
.load-more-button {
background-color: var(--bg-color);
border: 1px solid var(--primary-color);
color: var(--primary-color);
padding: 5px 15px;
font-family: var(--terminal-font);
cursor: pointer;
transition: all 0.3s ease;
}
.load-more-button:hover {
background-color: rgba(247, 147, 26, 0.2);
}
.load-more-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Notification Animation */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.notification-item {
animation: fadeIn 0.3s ease-out;
}
/* Mobile Responsiveness */
@media (max-width: 768px) {
.notification-actions {
flex-direction: column;
gap: 8px;
}
.action-button {
width: 100%; /* Full width on small screens */
padding: 8px 12px;
font-size: 1rem;
}
.notification-controls {
flex-direction: column;
align-items: stretch;
}
.filter-buttons {
overflow-x: auto;
padding-bottom: 5px;
margin-bottom: 5px;
white-space: nowrap;
display: flex;
flex-wrap: nowrap;
}
.notification-actions {
justify-content: flex-end;
}
.notification-item {
padding: 8px;
}
.notification-icon {
flex: 0 0 30px;
}
.notification-content {
padding: 0 8px;
}
.notification-actions {
flex: 0 0 60px;
}
}

View File

@ -1,441 +0,0 @@
/* Retro Floating Refresh Bar Styles */
:root {
--terminal-bg: #000000;
--terminal-border: #f7931a;
--terminal-text: #f7931a;
--terminal-glow: rgba(247, 147, 26, 0.7);
--terminal-width: 300px;
}
/* Adjust width for desktop */
@media (min-width: 768px) {
:root {
--terminal-width: 340px;
}
}
/* Remove the existing refresh timer container styles */
#refreshUptime {
visibility: hidden !important;
height: 0 !important;
overflow: hidden !important;
margin: 0 !important;
padding: 0 !important;
}
/* Add padding to the bottom of the page to prevent floating bar from covering content
body {
padding-bottom: 100px !important;
}
*/
/* Floating Retro Terminal Container */
#retro-terminal-bar {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
width: var(--terminal-width);
background-color: var(--terminal-bg);
border: 2px solid var(--terminal-border);
/* box-shadow: 0 0 15px var(--terminal-glow); */
z-index: 1000;
font-family: 'VT323', monospace;
overflow: hidden;
padding: 5px;
}
/* Desktop positioning (bottom right) */
@media (min-width: 768px) {
#retro-terminal-bar {
left: auto;
right: 20px;
transform: none;
}
}
/* Terminal header with control buttons */
/* Update the terminal title to match card headers */
.terminal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
border-bottom: 1px solid var(--terminal-border);
padding-bottom: 3px;
background-color: #000; /* Match card header background */
}
.terminal-title {
color: var(--primary-color);
font-weight: bold;
font-size: 1.1rem; /* Match card header font size */
border-bottom: none;
text-shadow: 0 0 5px var(--primary-color);
animation: flicker 4s infinite; /* Add flicker animation from card headers */
font-family: var(--header-font); /* Use the same font variable */
padding: 0.3rem 0; /* Match card header padding */
letter-spacing: 1px;
}
/* Make sure we're using the flicker animation defined in the main CSS */
@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; }
}
.terminal-controls {
display: flex;
gap: 5px;
}
.terminal-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #555;
transition: background-color 0.3s;
}
.terminal-dot:hover {
background-color: #999;
cursor: pointer;
}
.terminal-dot.minimize:hover {
background-color: #ffcc00;
}
.terminal-dot.close:hover {
background-color: #ff3b30;
}
/* Terminal content area */
.terminal-content {
position: relative;
color: #ffffff;
padding: 5px 0;
}
/* Scanline effect for authentic CRT look */
.terminal-content::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: repeating-linear-gradient(
0deg,
rgba(0, 0, 0, 0.15),
rgba(0, 0, 0, 0.15) 1px,
transparent 1px,
transparent 2px
);
pointer-events: none;
z-index: 1;
animation: flicker 0.15s infinite;
}
@keyframes flicker {
0% { opacity: 1.0; }
50% { opacity: 0.98; }
100% { opacity: 1.0; }
}
/* Enhanced Progress Bar with tick marks */
#retro-terminal-bar .bitcoin-progress-container {
width: 100%;
height: 20px;
background-color: #111;
border: 1px solid var(--terminal-border);
margin-bottom: 10px;
position: relative;
overflow: hidden;
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.8);
}
/* Tick marks on progress bar */
.progress-ticks {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: space-between;
padding: 0 5px;
color: rgba(255, 255, 255, 0.6);
font-size: 10px;
pointer-events: none;
z-index: 3;
}
.progress-ticks span {
display: flex;
align-items: flex-end;
height: 100%;
padding-bottom: 2px;
}
.tick-mark {
position: absolute;
top: 0;
width: 1px;
height: 5px;
background-color: rgba(255, 255, 255, 0.4);
}
.tick-mark.major {
height: 8px;
background-color: rgba(255, 255, 255, 0.6);
}
/* The actual progress bar */
#retro-terminal-bar #bitcoin-progress-inner {
height: 100%;
width: 0;
background: linear-gradient(90deg, #f7931a, #ffa500);
position: relative;
transition: width 1s linear;
}
/* Position the original inner container correctly */
#retro-terminal-bar #refreshContainer {
display: block;
width: 100%;
}
/* Blinking scan line animation */
.scan-line {
position: absolute;
height: 2px;
width: 100%;
background-color: rgba(255, 255, 255, 0.7);
animation: scan 3s linear infinite;
box-shadow: 0 0 8px 1px rgba(255, 255, 255, 0.5);
z-index: 2;
}
@keyframes scan {
0% { top: -2px; }
100% { top: 22px; }
}
/* Text styling */
#retro-terminal-bar #progress-text {
font-size: 16px;
color: var(--terminal-text);
text-shadow: 0 0 5px var(--terminal-text);
margin-top: 5px;
text-align: center;
position: relative;
z-index: 2;
}
#retro-terminal-bar #uptimeTimer {
font-size: 16px;
color: var(--terminal-text);
text-shadow: 0 0 5px var(--terminal-text);
text-align: center;
position: relative;
z-index: 2;
border-top: 1px solid rgba(247, 147, 26, 0.3);
padding-top: 5px;
margin-top: 5px;
}
/* Terminal cursor */
#retro-terminal-bar #terminal-cursor {
display: inline-block;
width: 8px;
height: 14px;
background-color: var(--terminal-text);
margin-left: 2px;
animation: blink 1s step-end infinite;
box-shadow: 0 0 8px var(--terminal-text);
}
/* Glowing effect during the last few seconds */
#retro-terminal-bar #bitcoin-progress-inner.glow-effect {
box-shadow: 0 0 15px #f7931a, 0 0 25px #f7931a;
}
#retro-terminal-bar .waiting-for-update {
animation: waitingPulse 2s infinite !important;
}
@keyframes waitingPulse {
0%, 100% { box-shadow: 0 0 10px #f7931a, 0 0 15px #f7931a; opacity: 0.8; }
50% { box-shadow: 0 0 20px #f7931a, 0 0 35px #f7931a; opacity: 1; }
}
/* Status indicators */
.status-indicators {
display: flex;
justify-content: space-between;
margin-bottom: 5px;
font-size: 12px;
color: #aaa;
}
.status-indicator {
display: flex;
align-items: center;
}
.status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
margin-right: 4px;
}
.status-dot.connected {
background-color: #32CD32;
box-shadow: 0 0 5px #32CD32;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { opacity: 0.8; }
50% { opacity: 1; }
100% { opacity: 0.8; }
}
/* Collapse/expand functionality */
#retro-terminal-bar.collapsed .terminal-content {
display: none;
}
#retro-terminal-bar.collapsed {
width: 180px;
}
/* On desktop, move the collapsed bar to bottom right */
@media (min-width: 768px) {
#retro-terminal-bar.collapsed {
right: 20px;
transform: none;
}
}
/* Show button */
#show-terminal-button {
position: fixed;
bottom: 10px;
right: 10px;
z-index: 1000;
background-color: #f7931a;
color: #000;
border: none;
padding: 8px 12px;
cursor: pointer;
font-family: 'VT323', monospace;
font-size: 14px;
box-shadow: 0 0 10px rgba(247, 147, 26, 0.5);
}
#show-terminal-button:hover {
background-color: #ffaa33;
}
/* Mobile responsiveness */
@media (max-width: 576px) {
#retro-terminal-bar {
width: 280px;
bottom: 10px;
}
.terminal-title {
font-size: 12px;
}
.terminal-dot {
width: 6px;
height: 6px;
}
#show-terminal-button {
padding: 6px 10px;
font-size: 12px;
}
}
/* Add these styles to retro-refresh.css to make the progress bar transitions smoother */
/* Smooth transition for progress bar width */
#retro-terminal-bar #bitcoin-progress-inner {
transition: width 0.3s ease-out;
}
/* Add a will-change property to optimize the animation */
#retro-terminal-bar .bitcoin-progress-container {
will-change: contents;
}
/* Smooth transition when changing from waiting state */
#retro-terminal-bar #bitcoin-progress-inner.waiting-for-update {
transition: width 0.3s ease-out, box-shadow 1s ease;
}
/* Ensure the scan line stays smooth during transitions */
#retro-terminal-bar .scan-line {
will-change: transform;
}
/* Improve mobile centering for collapsed system monitor */
@media (max-width: 767px) {
/* Target both possible selectors to ensure we catch the right one */
#retro-terminal-bar.collapsed,
.bitcoin-terminal.collapsed,
.retro-terminal-bar.collapsed,
div[id*="terminal"].collapsed {
left: 50% !important;
right: auto !important;
transform: translateX(-50%) !important;
width: auto !important;
max-width: 300px !important; /* Smaller max-width for mobile */
}
/* Ensure consistent height for minimized view */
.terminal-minimized {
height: 40px;
display: flex;
align-items: center;
}
}
/* Make the terminal draggable in desktop view */
@media (min-width: 768px) {
/* Target both possible selectors to handle all cases */
#bitcoin-terminal,
.bitcoin-terminal,
#retro-terminal-bar {
cursor: grab; /* Show a grab cursor to indicate draggability */
user-select: none; /* Prevent text selection during drag */
}
/* Change cursor during active dragging */
#bitcoin-terminal.dragging,
.bitcoin-terminal.dragging,
#retro-terminal-bar.dragging {
cursor: grabbing;
}
/* Style for drag handle in the header */
.terminal-header {
cursor: grab;
}
.terminal-header::before {
content: "⋮⋮"; /* Add drag indicator */
position: absolute;
left: 10px;
opacity: 0.5;
}
}

View File

@ -1,344 +0,0 @@
/* Styles specific to the workers page */
/* 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);
}
/* 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%;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.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);
}
/* 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;
}
.loading-fade {
opacity: 0.6;
transition: opacity 0.3s ease;
}
/* 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%;
}
}
@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;
}
}
/* Add extra padding at bottom of worker grid to avoid overlap */
.worker-grid {
margin-bottom: 120px;
}
/* Ensure summary stats have proper spacing on mobile */
@media (max-width: 576px) {
.summary-stats {
margin-bottom: 60px;
}
}

View File

@ -1,942 +0,0 @@
/**
* BitcoinMinuteRefresh.js - A minute-based refresh system tied to server uptime
*
* This module creates a Bitcoin-themed terminal that shows server uptime
* and refreshes data only on minute boundaries for better synchronization.
*/
const BitcoinMinuteRefresh = (function () {
// Constants
const STORAGE_KEY = 'bitcoin_last_refresh_time'; // For cross-page sync
// Private variables
let terminalElement = null;
let uptimeElement = null;
let serverTimeOffset = 0;
let serverStartTime = null;
let uptimeInterval = null;
let lastMinuteValue = -1;
let isInitialized = false;
let refreshCallback = null;
/**
* Add dragging functionality to the terminal
*/
function addDraggingBehavior() {
// Find the terminal element (checking both possible selectors)
const terminal = document.getElementById('bitcoin-terminal') ||
document.querySelector('.bitcoin-terminal') ||
document.getElementById('retro-terminal-bar');
if (!terminal) {
console.warn('Terminal element not found for drag behavior');
return;
}
let isDragging = false;
let startX = 0;
let startLeft = 0;
// Function to handle mouse down (drag start)
function handleMouseDown(e) {
// Only enable dragging in desktop view
if (window.innerWidth < 768) return;
// Don't handle drag if clicking on controls
if (e.target.closest('.terminal-dot')) return;
isDragging = true;
terminal.classList.add('dragging');
// Calculate start position
startX = e.clientX;
// Get current left position accounting for different possible styles
const style = window.getComputedStyle(terminal);
if (style.left !== 'auto') {
startLeft = parseInt(style.left) || 0;
} else {
// Calculate from right if left is not set
startLeft = window.innerWidth -
(parseInt(style.right) || 0) -
terminal.offsetWidth;
}
e.preventDefault(); // Prevent text selection
}
// Function to handle mouse move (dragging)
function handleMouseMove(e) {
if (!isDragging) return;
// Calculate the horizontal movement - vertical stays fixed
const deltaX = e.clientX - startX;
let newLeft = startLeft + deltaX;
// Constrain to window boundaries
const maxLeft = window.innerWidth - terminal.offsetWidth;
newLeft = Math.max(0, Math.min(newLeft, maxLeft));
// Update position - only horizontally along bottom
terminal.style.left = newLeft + 'px';
terminal.style.right = 'auto'; // Remove right positioning
terminal.style.transform = 'none'; // Remove transformations
// Bottom position remains fixed
}
// Function to handle mouse up (drag end)
function handleMouseUp() {
if (isDragging) {
isDragging = false;
terminal.classList.remove('dragging');
}
}
// Find the terminal header for dragging
const terminalHeader = terminal.querySelector('.terminal-header');
if (terminalHeader) {
terminalHeader.addEventListener('mousedown', handleMouseDown);
} else {
// If no header found, make the whole terminal draggable
terminal.addEventListener('mousedown', handleMouseDown);
}
// Add mousemove and mouseup listeners to document
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
// Handle window resize to keep terminal visible
window.addEventListener('resize', function () {
if (window.innerWidth < 768) {
// Reset position for mobile view
terminal.style.left = '50%';
terminal.style.right = 'auto';
terminal.style.transform = 'translateX(-50%)';
} else {
// Ensure terminal stays visible in desktop view
const maxLeft = window.innerWidth - terminal.offsetWidth;
const currentLeft = parseInt(window.getComputedStyle(terminal).left) || 0;
if (currentLeft > maxLeft) {
terminal.style.left = maxLeft + 'px';
}
}
});
}
/**
* Create and inject the retro terminal element into the DOM
*/
function createTerminalElement() {
// Container element
terminalElement = document.createElement('div');
terminalElement.id = 'bitcoin-terminal';
terminalElement.className = 'bitcoin-terminal';
// Terminal content
terminalElement.innerHTML = `
<div class="terminal-header">
<div class="terminal-title">SYSTEM MONITOR v.3</div>
<div class="terminal-controls">
<div class="terminal-dot minimize" title="Minimize" onclick="BitcoinMinuteRefresh.toggleTerminal()"></div>
<div class="terminal-dot close" title="Close" onclick="BitcoinMinuteRefresh.hideTerminal()"></div>
</div>
</div>
<div class="terminal-content">
<div class="status-row">
<div class="status-indicator">
<div class="status-dot connected"></div>
<span>LIVE</span>
</div>
<span id="terminal-clock" class="terminal-clock">00:00:00</span>
</div>
<!-- Removed for now
<div class="minute-progress-container">
<div class="minute-labels">
<span>0s</span>
<span>15s</span>
<span>30s</span>
<span>45s</span>
<span>60s</span>
</div>
<div class="minute-progress-bar">
<div id="minute-progress-inner" class="minute-progress-inner">
<div class="minute-progress-fill"></div>
<div class="scan-line"></div>
</div>
</div>
<div id="refresh-status" class="refresh-status">
Next refresh at the top of the minute
</div>
</div>
-->
<div id="uptime-timer" class="uptime-timer">
<div class="uptime-title">UPTIME</div>
<div class="uptime-display">
<div class="uptime-value">
<span id="uptime-hours" class="uptime-number">00</span>
<span class="uptime-label">H</span>
</div>
<div class="uptime-separator">:</div>
<div class="uptime-value">
<span id="uptime-minutes" class="uptime-number">00</span>
<span class="uptime-label">M</span>
</div>
<div class="uptime-separator">:</div>
<div class="uptime-value">
<span id="uptime-seconds" class="uptime-number">00</span>
<span class="uptime-label">S</span>
</div>
</div>
</div>
</div>
<div class="terminal-minimized">
<!-- <div class="minimized-clock">
<span id="minimized-clock-value">00:00:00</span>
</div>
<div class="minimized-separator">|</div> -->
<div class="minimized-uptime">
<span class="mini-uptime-label">UPTIME</span>
<span id="minimized-uptime-value">00:00:00</span>
</div>
<div class="minimized-status-dot connected"></div>
</div>
`;
// Append to body
document.body.appendChild(terminalElement);
// Add dragging behavior
addDraggingBehavior();
// Cache element references
uptimeElement = document.getElementById('uptime-timer');
// Check if terminal was previously collapsed
if (localStorage.getItem('bitcoin_terminal_collapsed') === 'true') {
terminalElement.classList.add('collapsed');
}
// Add custom styles if not already present
if (!document.getElementById('bitcoin-terminal-styles')) {
addStyles();
}
}
/**
* Add CSS styles for the terminal
*/
function addStyles() {
const styleElement = document.createElement('style');
styleElement.id = 'bitcoin-terminal-styles';
styleElement.textContent = `
/* Terminal Container */
.bitcoin-terminal {
position: fixed;
bottom: 20px;
right: 20px;
width: 340px;
background-color: #000000;
border: 1px solid #f7931a; // Changed from 2px to 1px
color: #f7931a;
font-family: 'VT323', monospace;
z-index: 9999;
overflow: hidden;
padding: 8px;
transition: all 0.3s ease;
box-shadow: 0 0 5px rgba(247, 147, 26, 0.3); // Added to match card shadow
}
/* Terminal Header */
.terminal-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #f7931a;
padding-bottom: 5px;
margin-bottom: 8px;
}
.terminal-title {
color: #f7931a;
font-weight: bold;
font-size: 1.1rem;
text-shadow: 0 0 5px rgba(247, 147, 26, 0.8);
animation: terminal-flicker 4s infinite;
}
/* Control Dots */
.terminal-controls {
display: flex;
gap: 5px;
margin-left: 5px;
}
.terminal-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #555;
cursor: pointer;
transition: background-color 0.3s;
}
.terminal-dot.minimize:hover {
background-color: #ffcc00;
}
.terminal-dot.close:hover {
background-color: #ff3b30;
}
/* Terminal Content */
.terminal-content {
position: relative;
}
/* Status Row */
.status-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
/* Status Indicator */
.status-indicator {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.8rem;
}
.status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
}
.status-dot.connected {
background-color: #32CD32;
box-shadow: 0 0 5px #32CD32;
animation: pulse 2s infinite;
}
.terminal-clock {
font-size: 1rem;
font-weight: bold;
text-shadow: 0 0 5px rgba(247, 147, 26, 0.5);
}
/* Minute Progress Bar */
.minute-progress-container {
margin-bottom: 12px;
}
.minute-labels {
display: flex;
justify-content: space-between;
font-size: 0.7rem;
margin-bottom: 2px;
}
.minute-progress-bar {
width: 100%;
height: 15px;
background-color: #111;
border: 1px solid #f7931a;
position: relative;
overflow: hidden;
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.8);
}
.minute-progress-inner {
height: 100%;
width: 0%;
position: relative;
overflow: hidden;
}
.minute-progress-fill {
height: 100%;
width: 100%;
background: linear-gradient(90deg, #f7931a, #ffa500);
transition: width 0.5s ease-out;
}
/* Scan line effect */
.scan-line {
position: absolute;
height: 2px;
width: 100%;
background-color: rgba(255, 255, 255, 0.7);
animation: scan 2s linear infinite;
box-shadow: 0 0 8px 1px rgba(255, 255, 255, 0.5);
z-index: 2;
will-change: transform;
}
/* Refresh status */
.refresh-status {
font-size: 0.8rem;
margin-top: 5px;
text-align: center;
text-shadow: 0 0 5px rgba(247, 147, 26, 0.5);
}
/* Uptime Display - Modern Digital Clock Style (Horizontal) */
.uptime-timer {
display: flex;
flex-direction: column;
align-items: center;
padding: 5px;
background-color: #111;
border: 1px solid rgba(247, 147, 26, 0.5);
margin-top: 5px;
}
.uptime-display {
display: flex;
justify-content: center;
align-items: center;
gap: 2px;
margin-top: 5px;
}
.uptime-value {
display: flex;
align-items: baseline;
}
.uptime-number {
font-size: 1.4rem;
font-weight: bold;
background-color: #000;
padding: 2px 5px;
border-radius: 3px;
min-width: 32px;
display: inline-block;
text-align: center;
letter-spacing: 2px;
text-shadow: 0 0 8px rgba(247, 147, 26, 0.8);
color: #f7931a;
}
.uptime-label {
font-size: 0.7rem;
opacity: 0.7;
margin-left: 2px;
}
.uptime-separator {
font-size: 1.4rem;
font-weight: bold;
padding: 0 2px;
text-shadow: 0 0 8px rgba(247, 147, 26, 0.8);
}
.uptime-title {
font-size: 0.7rem;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 2px;
text-shadow: 0 0 5px rgba(247, 147, 26, 0.8);
margin-bottom: 3px;
}
/* Special effects */
.minute-progress-inner.near-refresh .minute-progress-fill {
box-shadow: 0 0 15px #f7931a, 0 0 25px #f7931a;
animation: pulse-brightness 1s infinite;
}
.minute-progress-inner.refresh-now .minute-progress-fill {
animation: refresh-flash 1s forwards;
}
/* Show button */
#bitcoin-terminal-show {
position: fixed;
bottom: 10px;
right: 10px;
background-color: #f7931a;
color: #000;
border: none;
padding: 8px 12px;
font-family: 'VT323', monospace;
cursor: pointer;
z-index: 9999;
display: none;
box-shadow: 0 0 10px rgba(247, 147, 26, 0.5);
}
/* CRT scanline effect */
.terminal-content::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: repeating-linear-gradient(
0deg,
rgba(0, 0, 0, 0.15),
rgba(0, 0, 0, 0.15) 1px,
transparent 1px,
transparent 2px
);
pointer-events: none;
z-index: 1;
}
/* Minimized view styling */
.terminal-minimized {
display: none;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 4px 10px;
background-color: #000;
position: relative;
}
.terminal-minimized::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: repeating-linear-gradient(
0deg,
rgba(0, 0, 0, 0.15),
rgba(0, 0, 0, 0.15) 1px,
transparent 1px,
transparent 2px
);
pointer-events: none;
z-index: 1;
}
.minimized-clock {
font-size: 1.1rem;
font-weight: bold;
text-shadow: 0 0 5px rgba(247, 147, 26, 0.5);
position: relative;
z-index: 2;
}
.minimized-separator {
margin: 0 10px;
color: rgba(247, 147, 26, 0.5);
font-size: 1.1rem;
position: relative;
z-index: 2;
}
.minimized-uptime {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
z-index: 2;
}
.mini-uptime-label {
font-size: 0.6rem;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 1px;
opacity: 0.7;
margin-left: 45px;
color: #f7931a;
}
#minimized-uptime-value {
font-size: 0.9rem;
font-weight: bold;
text-shadow: 0 0 5px rgba(247, 147, 26, 0.5);
margin-left: 45px;
color: #f7931a;
}
.minimized-status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
margin-left: 10px;
position: relative;
z-index: 2;
}
/* Collapsed state */
.bitcoin-terminal.collapsed {
width: auto;
max-width: 500px;
height: auto;
padding: 5px;
}
.bitcoin-terminal.collapsed .terminal-content {
display: none;
}
.bitcoin-terminal.collapsed .terminal-minimized {
display: flex;
}
.bitcoin-terminal.collapsed .terminal-header {
border-bottom: none;
margin-bottom: 2px;
padding-bottom: 2px;
}
/* Animations */
@keyframes scan {
0% { top: -2px; }
100% { top: 17px; }
}
@keyframes pulse {
0%, 100% { opacity: 0.8; }
50% { opacity: 1; }
}
@keyframes terminal-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; }
}
@keyframes pulse-brightness {
0%, 100% { filter: brightness(1); }
50% { filter: brightness(1.3); }
}
@keyframes refresh-flash {
0% { filter: brightness(1); background-color: #f7931a; }
10% { filter: brightness(1.8); background-color: #fff; }
20% { filter: brightness(1); background-color: #f7931a; }
30% { filter: brightness(1.8); background-color: #fff; }
40% { filter: brightness(1); background-color: #f7931a; }
100% { filter: brightness(1); background-color: #f7931a; }
}
/* Media Queries */
@media (max-width: 768px) {
.bitcoin-terminal {
left: 50%;
right: auto;
transform: translateX(-50%);
width: 90%;
max-width: 320px;
bottom: 10px;
}
.bitcoin-terminal.collapsed {
width: auto;
max-width: 300px;
left: 50%; // Changed from "left: auto"
right: auto; // Changed from "right: 10px"
transform: translateX(-50%); // Changed from "transform: none"
}
}
`;
document.head.appendChild(styleElement);
}
/**
* Update the minute progress bar based on current seconds
*/
function updateMinuteProgress() {
try {
// Get current server time - keep this for other functions that might use it
const now = new Date(Date.now() + (serverTimeOffset || 0));
// We need to keep track of minutes for other functionality
const currentMinute = now.getMinutes();
// Update last minute value (keeping this for any other code that might rely on it)
lastMinuteValue = currentMinute;
// No progress bar updates or effects
} catch (e) {
console.error("BitcoinMinuteRefresh: Error updating progress bar:", e);
}
}
/**
* Update the terminal clock
*/
function updateClock() {
try {
const now = new Date(Date.now() + (serverTimeOffset || 0));
let hours = now.getHours();
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
const ampm = hours >= 12 ? 'PM' : 'AM';
hours = hours % 12;
hours = hours ? hours : 12; // the hour '0' should be '12'
const timeString = `${String(hours).padStart(2, '0')}:${minutes}:${seconds} ${ampm}`;
// Update both clocks (normal and minimized views)
const clockElement = document.getElementById('terminal-clock');
if (clockElement) {
clockElement.textContent = timeString;
}
const minimizedClockElement = document.getElementById('minimized-clock-value');
if (minimizedClockElement) {
minimizedClockElement.textContent = timeString;
}
} catch (e) {
console.error("BitcoinMinuteRefresh: Error updating clock:", e);
}
}
/**
* Update the uptime display
*/
function updateUptime() {
if (serverStartTime) {
try {
const currentServerTime = Date.now() + serverTimeOffset;
const diff = currentServerTime - serverStartTime;
// Calculate hours, minutes, seconds
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);
// Update the main uptime display with digital clock style
const uptimeHoursElement = document.getElementById('uptime-hours');
const uptimeMinutesElement = document.getElementById('uptime-minutes');
const uptimeSecondsElement = document.getElementById('uptime-seconds');
if (uptimeHoursElement) {
uptimeHoursElement.textContent = String(hours).padStart(2, '0');
}
if (uptimeMinutesElement) {
uptimeMinutesElement.textContent = String(minutes).padStart(2, '0');
}
if (uptimeSecondsElement) {
uptimeSecondsElement.textContent = String(seconds).padStart(2, '0');
}
// Update the minimized uptime display
const minimizedUptimeElement = document.getElementById('minimized-uptime-value');
if (minimizedUptimeElement) {
minimizedUptimeElement.textContent = `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
}
} catch (e) {
console.error("BitcoinMinuteRefresh: Error updating uptime:", e);
}
}
}
/**
* Notify other tabs that data has been refreshed
*/
function notifyRefresh() {
const now = Date.now();
localStorage.setItem(STORAGE_KEY, now.toString());
localStorage.setItem('bitcoin_refresh_event', 'refresh-' + now);
console.log("BitcoinMinuteRefresh: Notified other tabs of refresh at " + new Date(now).toISOString());
}
/**
* Initialize the minute refresh system
*/
function initialize(refreshFunc) {
// Store the refresh callback
refreshCallback = refreshFunc;
// Create the terminal element if it doesn't exist
if (!document.getElementById('bitcoin-terminal')) {
createTerminalElement();
} else {
// Get references to existing elements
terminalElement = document.getElementById('bitcoin-terminal');
uptimeElement = document.getElementById('uptime-timer');
}
// NEW CODE: Check if dashboard uptime element exists
const dashboardUptimeElement = document.getElementById('uptimeTimer');
if (dashboardUptimeElement) {
console.log("BitcoinMinuteRefresh: Found dashboard uptime element, will sync with it");
}
// Try to get stored server time information
try {
serverTimeOffset = parseFloat(localStorage.getItem('serverTimeOffset') || '0');
serverStartTime = parseFloat(localStorage.getItem('serverStartTime') || '0');
} catch (e) {
console.error("BitcoinMinuteRefresh: Error reading server time from localStorage:", e);
}
// Clear any existing intervals
if (uptimeInterval) {
clearInterval(uptimeInterval);
}
// Set up intervals for updating at 50ms precision for smooth animation
uptimeInterval = setInterval(function () {
updateClock();
updateUptime();
updateMinuteProgress();
}, 50);
// Listen for storage events to sync across tabs
window.removeEventListener('storage', handleStorageChange);
window.addEventListener('storage', handleStorageChange);
// Handle visibility changes
document.removeEventListener('visibilitychange', handleVisibilityChange);
document.addEventListener('visibilitychange', handleVisibilityChange);
// Initialize last minute value
const now = new Date(Date.now() + serverTimeOffset);
lastMinuteValue = now.getMinutes();
// Log current server time details for debugging
console.log(`BitcoinMinuteRefresh: Server time is ${now.toISOString()}, minute=${lastMinuteValue}, seconds=${now.getSeconds()}`);
// Mark as initialized
isInitialized = true;
console.log("BitcoinMinuteRefresh: Initialized");
}
/**
* Handle storage changes for cross-tab synchronization
*/
function handleStorageChange(event) {
if (event.key === 'bitcoin_refresh_event') {
console.log("BitcoinMinuteRefresh: Detected refresh from another tab");
// If another tab refreshed, consider refreshing this one too
// But don't refresh if it was just refreshed recently (5 seconds)
const lastRefreshTime = parseInt(localStorage.getItem(STORAGE_KEY) || '0');
if (typeof refreshCallback === 'function' && Date.now() - lastRefreshTime > 5000) {
refreshCallback();
}
} else if (event.key === 'serverTimeOffset' || event.key === 'serverStartTime') {
try {
serverTimeOffset = parseFloat(localStorage.getItem('serverTimeOffset') || '0');
serverStartTime = parseFloat(localStorage.getItem('serverStartTime') || '0');
} catch (e) {
console.error("BitcoinMinuteRefresh: Error reading updated server time:", e);
}
}
}
/**
* Handle visibility changes
*/
function handleVisibilityChange() {
if (!document.hidden) {
console.log("BitcoinMinuteRefresh: Page became visible, updating");
// Update immediately when page becomes visible
updateClock();
updateUptime();
updateMinuteProgress();
// Check if we need to do a refresh based on time elapsed
if (typeof refreshCallback === 'function') {
const lastRefreshTime = parseInt(localStorage.getItem(STORAGE_KEY) || '0');
if (Date.now() - lastRefreshTime > 60000) { // More than a minute since last refresh
refreshCallback();
}
}
}
}
/**
* Update server time information
*/
function updateServerTime(timeOffset, startTime) {
serverTimeOffset = timeOffset;
serverStartTime = startTime;
// Store in localStorage for cross-page sharing
localStorage.setItem('serverTimeOffset', serverTimeOffset.toString());
localStorage.setItem('serverStartTime', serverStartTime.toString());
// Update the uptime immediately
updateUptime();
updateMinuteProgress();
console.log("BitcoinMinuteRefresh: Server time updated - offset:", serverTimeOffset, "ms");
}
/**
* Toggle terminal collapsed state
*/
function toggleTerminal() {
if (!terminalElement) return;
terminalElement.classList.toggle('collapsed');
localStorage.setItem('bitcoin_terminal_collapsed', terminalElement.classList.contains('collapsed'));
}
/**
* Hide the terminal and show the restore button
*/
function hideTerminal() {
if (!terminalElement) return;
terminalElement.style.display = 'none';
// Create show button if it doesn't exist
if (!document.getElementById('bitcoin-terminal-show')) {
const showButton = document.createElement('button');
showButton.id = 'bitcoin-terminal-show';
showButton.textContent = 'Show Monitor';
showButton.onclick = showTerminal;
document.body.appendChild(showButton);
}
document.getElementById('bitcoin-terminal-show').style.display = 'block';
}
/**
* Show the terminal and hide the restore button
*/
function showTerminal() {
if (!terminalElement) return;
terminalElement.style.display = 'block';
document.getElementById('bitcoin-terminal-show').style.display = 'none';
}
// Public API
return {
initialize: initialize,
notifyRefresh: notifyRefresh,
updateServerTime: updateServerTime,
toggleTerminal: toggleTerminal,
hideTerminal: hideTerminal,
showTerminal: showTerminal
};
})();
// Auto-initialize when document is ready if a refresh function is available in the global scope
document.addEventListener('DOMContentLoaded', function () {
// Check if manualRefresh function exists in global scope
if (typeof window.manualRefresh === 'function') {
BitcoinMinuteRefresh.initialize(window.manualRefresh);
} else {
console.log("BitcoinMinuteRefresh: No refresh function found, will need to be initialized manually");
}
});

View File

@ -1,384 +0,0 @@
// Bitcoin Block Mining Animation Controller
class BlockMiningAnimation {
constructor(svgContainerId) {
// Get the container element
this.container = document.getElementById(svgContainerId);
if (!this.container) {
console.error("SVG container not found:", svgContainerId);
return;
}
// Get SVG elements
this.blockHeight = document.getElementById("block-height");
this.statusHeight = document.getElementById("status-height");
this.miningPool = document.getElementById("mining-pool");
this.blockTime = document.getElementById("block-time");
this.transactionCount = document.getElementById("transaction-count");
this.miningHash = document.getElementById("mining-hash");
this.nonceValue = document.getElementById("nonce-value");
this.difficultyValue = document.getElementById("difficulty-value");
this.miningStatus = document.getElementById("mining-status");
// Debug element availability
console.log("Animation elements found:", {
blockHeight: !!this.blockHeight,
statusHeight: !!this.statusHeight,
miningPool: !!this.miningPool,
blockTime: !!this.blockTime,
transactionCount: !!this.transactionCount,
miningHash: !!this.miningHash,
nonceValue: !!this.nonceValue,
difficultyValue: !!this.difficultyValue,
miningStatus: !!this.miningStatus
});
// Animation state
this.animationPhase = "collecting"; // collecting, mining, found, adding
this.miningSpeed = 300; // ms between nonce updates
this.nonceCounter = 0;
this.currentBlockData = null;
this.animationInterval = null;
this.apiRetryCount = 0;
this.maxApiRetries = 3;
// Initialize random hash for mining animation
this.updateRandomHash();
}
// Start the animation loop
start() {
if (this.animationInterval) {
clearInterval(this.animationInterval);
}
console.log("Starting block mining animation");
this.animationInterval = setInterval(() => this.animationTick(), this.miningSpeed);
// Start by fetching the latest block
this.fetchLatestBlockWithRetry();
}
// Stop the animation
stop() {
if (this.animationInterval) {
clearInterval(this.animationInterval);
this.animationInterval = null;
}
}
// Main animation tick function
animationTick() {
switch (this.animationPhase) {
case "collecting":
// Simulate collecting transactions
this.updateTransactionAnimation();
break;
case "mining":
// Update nonce and hash values
this.updateMiningAnimation();
break;
case "found":
// Block found phase - brief celebration
this.updateFoundAnimation();
break;
case "adding":
// Adding block to chain
this.updateAddingAnimation();
break;
}
}
// Fetch latest block with retry logic
fetchLatestBlockWithRetry() {
this.apiRetryCount = 0;
this.fetchLatestBlock();
}
// Fetch the latest block data from mempool.space
fetchLatestBlock() {
console.log("Fetching latest block data, attempt #" + (this.apiRetryCount + 1));
// Show that we're fetching
if (this.miningStatus) {
this.miningStatus.textContent = "Connecting to blockchain...";
}
// Use the mempool.space public API
fetch("https://mempool.space/api/v1/blocks/tip/height")
.then(response => {
if (!response.ok) {
throw new Error("Failed to fetch latest block height: " + response.status);
}
return response.json();
})
.then(height => {
console.log("Latest block height:", height);
// Fetch multiple blocks but limit to 1
return fetch(`https://mempool.space/api/v1/blocks?height=${height}&limit=1`);
})
.then(response => {
if (!response.ok) {
throw new Error("Failed to fetch block data: " + response.status);
}
return response.json();
})
.then(blockData => {
console.log("Block data received:", blockData);
// Ensure we have data and use the first block
if (blockData && blockData.length > 0) {
this.currentBlockData = blockData[0];
this.startBlockAnimation();
// Reset retry count on success
this.apiRetryCount = 0;
} else {
throw new Error("No block data received");
}
})
.catch(error => {
console.error("Error fetching block data:", error);
// Retry logic
this.apiRetryCount++;
if (this.apiRetryCount < this.maxApiRetries) {
console.log(`Retrying in 2 seconds... (attempt ${this.apiRetryCount + 1}/${this.maxApiRetries})`);
setTimeout(() => this.fetchLatestBlock(), 2000);
} else {
console.warn("Max retries reached, using placeholder data");
// Use placeholder data if fetch fails after retries
this.usePlaceholderData();
this.startBlockAnimation();
}
});
}
// Start the block animation sequence
startBlockAnimation() {
// Reset animation state
this.animationPhase = "collecting";
this.nonceCounter = 0;
// Update block data display immediately
this.updateBlockDisplay();
// Schedule the animation sequence
setTimeout(() => {
this.animationPhase = "mining";
if (this.miningStatus) {
this.miningStatus.textContent = "Mining in progress...";
}
// After a random mining period, find the block
setTimeout(() => {
this.animationPhase = "found";
if (this.miningStatus) {
this.miningStatus.textContent = "BLOCK FOUND!";
}
// Then move to adding phase
setTimeout(() => {
this.animationPhase = "adding";
if (this.miningStatus) {
this.miningStatus.textContent = "Adding to blockchain...";
}
// After adding, fetch a new block or loop with current one
setTimeout(() => {
// Fetch a new block every time to keep data current
this.fetchLatestBlockWithRetry();
}, 3000);
}, 2000);
}, 5000 + Math.random() * 5000); // Random mining time
}, 3000); // Time for collecting transactions
}
// Update block display with current block data
updateBlockDisplay() {
if (!this.currentBlockData) {
console.error("No block data available to display");
return;
}
// Safely extract and format block data
const blockData = Array.isArray(this.currentBlockData)
? this.currentBlockData[0]
: this.currentBlockData;
console.log("Updating block display with data:", blockData);
try {
// Safely extract and format block height
const height = blockData.height ? blockData.height.toString() : "N/A";
if (this.blockHeight) this.blockHeight.textContent = height;
if (this.statusHeight) this.statusHeight.textContent = height;
// Safely format block timestamp
let formattedTime = "N/A";
if (blockData.timestamp) {
const timestamp = new Date(blockData.timestamp * 1000);
formattedTime = timestamp.toLocaleString();
}
if (this.blockTime) this.blockTime.textContent = formattedTime;
// Safely format transaction count
const txCount = blockData.tx_count ? blockData.tx_count.toString() : "N/A";
if (this.transactionCount) this.transactionCount.textContent = txCount;
// Format mining pool
let poolName = "Unknown";
if (blockData.extras && blockData.extras.pool && blockData.extras.pool.name) {
poolName = blockData.extras.pool.name;
}
if (this.miningPool) this.miningPool.textContent = poolName;
// Format difficulty (simplified)
let difficultyStr = "Unknown";
if (blockData.difficulty) {
// Format as scientific notation for better display
difficultyStr = blockData.difficulty.toExponential(2);
}
if (this.difficultyValue) this.difficultyValue.textContent = difficultyStr;
// Use actual nonce if available
if (this.nonceValue && blockData.nonce) {
this.nonceValue.textContent = blockData.nonce.toString();
// Use this as starting point for animation
this.nonceCounter = blockData.nonce;
}
// Update block hash (if available)
if (this.miningHash && blockData.id) {
const blockHash = blockData.id;
const shortHash = blockHash.substring(0, 8) + "..." + blockHash.substring(blockHash.length - 8);
this.miningHash.textContent = shortHash;
}
console.log("Block display updated successfully");
} catch (error) {
console.error("Error updating block display:", error, "Block data:", blockData);
}
}
// Transaction collection animation
updateTransactionAnimation() {
// Animation for collecting transactions is handled by SVG animation
// We could add additional logic here if needed
}
// Mining animation - update nonce and hash
updateMiningAnimation() {
// Increment nonce
this.nonceCounter += 1 + Math.floor(Math.random() * 1000);
if (this.nonceValue) {
this.nonceValue.textContent = this.nonceCounter.toString().padStart(10, '0');
}
// Update hash value
this.updateRandomHash();
}
// Block found animation - show a hash that matches difficulty
updateFoundAnimation() {
if (!this.miningHash || !this.nonceValue || !this.currentBlockData) return;
try {
// Make the "found" hash start with enough zeros based on difficulty
// Use actual block hash if available
const blockData = Array.isArray(this.currentBlockData)
? this.currentBlockData[0]
: this.currentBlockData;
if (blockData.id) {
const blockHash = blockData.id;
const shortHash = blockHash.substring(0, 8) + "..." + blockHash.substring(blockHash.length - 8);
this.miningHash.textContent = shortHash;
} else {
// Fallback to generated hash
const zeros = Math.min(6, Math.max(2, Math.floor(Math.log10(blockData.difficulty) / 10)));
const zeroPrefix = '0'.repeat(zeros);
const remainingChars = '0123456789abcdef';
let hash = zeroPrefix;
// Fill the rest with random hex characters
for (let i = zeros; i < 8; i++) {
hash += remainingChars.charAt(Math.floor(Math.random() * remainingChars.length));
}
this.miningHash.textContent = hash + "..." + hash;
}
// Use the actual nonce if available
if (blockData.nonce) {
this.nonceValue.textContent = blockData.nonce.toString();
}
} catch (error) {
console.error("Error updating found animation:", error);
}
}
// Adding block to chain animation
updateAddingAnimation() {
// Animation for adding to blockchain is handled by SVG animation
// We could add additional logic here if needed
}
// Generate a random hash string for mining animation
updateRandomHash() {
if (!this.miningHash) return;
const characters = '0123456789abcdef';
let hash = '';
// Generate random 8-char segment
for (let i = 0; i < 8; i++) {
hash += characters.charAt(Math.floor(Math.random() * characters.length));
}
this.miningHash.textContent = hash + "..." + hash;
}
// Use placeholder data if API fetch fails
usePlaceholderData() {
const now = Math.floor(Date.now() / 1000);
this.currentBlockData = {
height: 888888,
timestamp: now,
tx_count: 2500,
difficulty: 50000000000000,
nonce: 123456789,
id: "00000000000000000000b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7",
extras: {
pool: {
name: "Placeholder Pool"
}
}
};
console.log("Using placeholder data:", this.currentBlockData);
}
}
// Initialize and start the animation when the page loads
// document.addEventListener("DOMContentLoaded", function () {
// console.log("DOM content loaded, initializing animation");
// Ensure we give the SVG enough time to be fully rendered and accessible
// setTimeout(() => {
// const svgContainer = document.getElementById("svg-container");
// if (!svgContainer) {
// console.error("SVG container not found in DOM");
// return;
// }
// try {
// const animation = new BlockMiningAnimation("svg-container");
// animation.start();
// console.log("Animation started successfully");
// } catch (error) {
// console.error("Error starting animation:", error);
// }
// }, 1500); // Increased delay to ensure SVG is fully loaded
// });

View File

@ -1,812 +0,0 @@
"use strict";
// Global variables
let currentStartHeight = null;
const mempoolBaseUrl = "https://mempool.space";
let blocksCache = {};
let isLoading = false;
// DOM ready initialization
$(document).ready(function() {
console.log("Blocks page initialized");
// Initialize notification badge
initNotificationBadge();
// Load the latest blocks on page load
loadLatestBlocks();
// Set up event listeners
$("#load-blocks").on("click", function() {
const height = $("#block-height").val();
if (height && !isNaN(height)) {
loadBlocksFromHeight(height);
} else {
showToast("Please enter a valid block height");
}
});
$("#latest-blocks").on("click", loadLatestBlocks);
// Handle Enter key on the block height input
$("#block-height").on("keypress", function(e) {
if (e.which === 13) {
const height = $(this).val();
if (height && !isNaN(height)) {
loadBlocksFromHeight(height);
} else {
showToast("Please enter a valid block height");
}
}
});
// Close the modal when clicking the X or outside the modal
$(".block-modal-close").on("click", closeModal);
$(window).on("click", function(event) {
if ($(event.target).hasClass("block-modal")) {
closeModal();
}
});
// Initialize BitcoinMinuteRefresh if available
if (typeof BitcoinMinuteRefresh !== 'undefined' && BitcoinMinuteRefresh.initialize) {
BitcoinMinuteRefresh.initialize(loadLatestBlocks);
console.log("BitcoinMinuteRefresh initialized with refresh function");
}
});
// Update unread notifications badge in navigation
function updateNotificationBadge() {
$.ajax({
url: "/api/notifications/unread_count",
method: "GET",
success: function (data) {
const unreadCount = data.unread_count;
const badge = $("#nav-unread-badge");
if (unreadCount > 0) {
badge.text(unreadCount).show();
} else {
badge.hide();
}
}
});
}
// Initialize notification badge checking
function initNotificationBadge() {
// Update immediately
updateNotificationBadge();
// Update every 60 seconds
setInterval(updateNotificationBadge, 60000);
}
// Helper function to format timestamps as readable dates
function formatTimestamp(timestamp) {
const date = new Date(timestamp * 1000);
return date.toLocaleString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: true
});
}
// Helper function to format numbers with commas
function numberWithCommas(x) {
if (x == null) return "N/A";
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
// Helper function to format file sizes
function formatFileSize(bytes) {
if (bytes < 1024) return bytes + " B";
else if (bytes < 1048576) return (bytes / 1024).toFixed(2) + " KB";
else return (bytes / 1048576).toFixed(2) + " MB";
}
// Helper function to show toast messages
function showToast(message) {
// Check if we already have a toast container
let toastContainer = $(".toast-container");
if (toastContainer.length === 0) {
// Create a new toast container
toastContainer = $("<div>", {
class: "toast-container",
css: {
position: "fixed",
bottom: "20px",
right: "20px",
zIndex: 9999
}
}).appendTo("body");
}
// Create a new toast
const toast = $("<div>", {
class: "toast",
text: message,
css: {
backgroundColor: "#f7931a",
color: "#000",
padding: "10px 15px",
borderRadius: "5px",
marginTop: "10px",
boxShadow: "0 0 10px rgba(247, 147, 26, 0.5)",
fontFamily: "var(--terminal-font)",
opacity: 0,
transition: "opacity 0.3s ease"
}
}).appendTo(toastContainer);
// Show the toast
setTimeout(() => {
toast.css("opacity", 1);
// Hide and remove the toast after 3 seconds
setTimeout(() => {
toast.css("opacity", 0);
setTimeout(() => toast.remove(), 300);
}, 3000);
}, 100);
}
// Function to load blocks from a specific height
function loadBlocksFromHeight(height) {
if (isLoading) return;
// Convert to integer
height = parseInt(height);
if (isNaN(height) || height < 0) {
showToast("Please enter a valid block height");
return;
}
isLoading = true;
currentStartHeight = height;
// Check if we already have this data in cache
if (blocksCache[height]) {
displayBlocks(blocksCache[height]);
isLoading = false;
return;
}
// Show loading state
$("#blocks-grid").html('<div class="loader"><span class="loader-text">Loading blocks from height ' + height + '<span class="terminal-cursor"></span></span></div>');
// Fetch blocks from the API
$.ajax({
url: `${mempoolBaseUrl}/api/v1/blocks/${height}`,
method: "GET",
dataType: "json",
timeout: 10000,
success: function(data) {
// Cache the data
blocksCache[height] = data;
// Display the blocks
displayBlocks(data);
// Update latest block stats
if (data.length > 0) {
updateLatestBlockStats(data[0]);
}
},
error: function(xhr, status, error) {
console.error("Error fetching blocks:", error);
$("#blocks-grid").html('<div class="error">Error fetching blocks. Please try again later.</div>');
// Show error toast
showToast("Failed to load blocks. Please try again later.");
},
complete: function() {
isLoading = false;
}
});
}
// Function to load the latest blocks and return a promise with the latest block height
function loadLatestBlocks() {
if (isLoading) return Promise.resolve(null);
isLoading = true;
// Show loading state
$("#blocks-grid").html('<div class="loader"><span class="loader-text">Loading latest blocks<span class="terminal-cursor"></span></span></div>');
// Fetch the latest blocks from the API
return $.ajax({
url: `${mempoolBaseUrl}/api/v1/blocks`,
method: "GET",
dataType: "json",
timeout: 10000,
success: function (data) {
// Cache the data (use the first block's height as the key)
if (data.length > 0) {
currentStartHeight = data[0].height;
blocksCache[currentStartHeight] = data;
// Update the block height input with the latest height
$("#block-height").val(currentStartHeight);
// Update latest block stats
updateLatestBlockStats(data[0]);
}
// Display the blocks
displayBlocks(data);
},
error: function (xhr, status, error) {
console.error("Error fetching latest blocks:", error);
$("#blocks-grid").html('<div class="error">Error fetching blocks. Please try again later.</div>');
// Show error toast
showToast("Failed to load latest blocks. Please try again later.");
},
complete: function () {
isLoading = false;
}
}).then(data => data.length > 0 ? data[0].height : null);
}
// Refresh blocks page every 60 seconds if there are new blocks
setInterval(function () {
console.log("Checking for new blocks at " + new Date().toLocaleTimeString());
loadLatestBlocks().then(latestHeight => {
if (latestHeight && latestHeight > currentStartHeight) {
console.log("New blocks detected, refreshing the page");
location.reload();
} else {
console.log("No new blocks detected");
}
});
}, 60000);
// Function to update the latest block stats section
function updateLatestBlockStats(block) {
if (!block) return;
$("#latest-height").text(block.height);
$("#latest-time").text(formatTimestamp(block.timestamp));
$("#latest-tx-count").text(numberWithCommas(block.tx_count));
$("#latest-size").text(formatFileSize(block.size));
$("#latest-difficulty").text(numberWithCommas(Math.round(block.difficulty)));
// Pool info
if (block.extras && block.extras.pool) {
$("#latest-pool").text(block.extras.pool.name);
} else {
$("#latest-pool").text("Unknown");
}
// Average Fee Rate
if (block.extras && block.extras.avgFeeRate) {
$("#latest-fee-rate").text(block.extras.avgFeeRate + " sat/vB");
} else {
$("#latest-fee-rate").text("N/A");
}
}
// Function to display the blocks in the grid
function displayBlocks(blocks) {
const blocksGrid = $("#blocks-grid");
// Clear the grid
blocksGrid.empty();
if (!blocks || blocks.length === 0) {
blocksGrid.html('<div class="no-blocks">No blocks found</div>');
return;
}
// Create a card for each block
blocks.forEach(function(block) {
const blockCard = createBlockCard(block);
blocksGrid.append(blockCard);
});
// Add navigation controls if needed
addNavigationControls(blocks);
}
// Function to create a block card
function createBlockCard(block) {
const timestamp = formatTimestamp(block.timestamp);
const formattedSize = formatFileSize(block.size);
const formattedTxCount = numberWithCommas(block.tx_count);
// Get the pool name or "Unknown"
const poolName = block.extras && block.extras.pool ? block.extras.pool.name : "Unknown";
// Calculate total fees in BTC
const totalFees = block.extras ? (block.extras.totalFees / 100000000).toFixed(8) : "N/A";
// Create the block card
const blockCard = $("<div>", {
class: "block-card",
"data-height": block.height,
"data-hash": block.id
});
// Create the block header
const blockHeader = $("<div>", {
class: "block-header"
});
blockHeader.append($("<div>", {
class: "block-height",
text: "#" + block.height
}));
blockHeader.append($("<div>", {
class: "block-time",
text: timestamp
}));
blockCard.append(blockHeader);
// Create the block info section
const blockInfo = $("<div>", {
class: "block-info"
});
// Add transaction count
const txCountItem = $("<div>", {
class: "block-info-item"
});
txCountItem.append($("<div>", {
class: "block-info-label",
text: "Transactions"
}));
txCountItem.append($("<div>", {
class: "block-info-value white",
text: formattedTxCount
}));
blockInfo.append(txCountItem);
// Add size
const sizeItem = $("<div>", {
class: "block-info-item"
});
sizeItem.append($("<div>", {
class: "block-info-label",
text: "Size"
}));
sizeItem.append($("<div>", {
class: "block-info-value white",
text: formattedSize
}));
blockInfo.append(sizeItem);
// Add miner/pool
const minerItem = $("<div>", {
class: "block-info-item"
});
minerItem.append($("<div>", {
class: "block-info-label",
text: "Miner"
}));
minerItem.append($("<div>", {
class: "block-info-value green",
text: poolName
}));
blockInfo.append(minerItem);
// Add total fees
const feesItem = $("<div>", {
class: "block-info-item"
});
feesItem.append($("<div>", {
class: "block-info-label",
text: "Total Fees"
}));
feesItem.append($("<div>", {
class: "block-info-value yellow",
text: totalFees + " BTC"
}));
blockInfo.append(feesItem);
blockCard.append(blockInfo);
// Add event listener for clicking on the block card
blockCard.on("click", function() {
showBlockDetails(block);
});
return blockCard;
}
// Function to add navigation controls to the blocks grid
function addNavigationControls(blocks) {
// Get the height of the first and last block in the current view
const firstBlockHeight = blocks[0].height;
const lastBlockHeight = blocks[blocks.length - 1].height;
// Create navigation controls
const navControls = $("<div>", {
class: "block-navigation"
});
// Newer blocks button (if not already at the latest blocks)
if (firstBlockHeight !== currentStartHeight) {
const newerButton = $("<button>", {
class: "block-button",
text: "Newer Blocks"
});
newerButton.on("click", function() {
loadBlocksFromHeight(firstBlockHeight + 15);
});
navControls.append(newerButton);
}
// Older blocks button
const olderButton = $("<button>", {
class: "block-button",
text: "Older Blocks"
});
olderButton.on("click", function() {
loadBlocksFromHeight(lastBlockHeight - 1);
});
navControls.append(olderButton);
// Add the navigation controls to the blocks grid
$("#blocks-grid").append(navControls);
}
// Function to show block details in a modal
function showBlockDetails(block) {
const modal = $("#block-modal");
const blockDetails = $("#block-details");
// Clear the details
blockDetails.empty();
// Format the timestamp
const timestamp = formatTimestamp(block.timestamp);
// Create the block header section
const headerSection = $("<div>", {
class: "block-detail-section"
});
headerSection.append($("<div>", {
class: "block-detail-title",
text: "Block #" + block.height
}));
// Add block hash
const hashItem = $("<div>", {
class: "block-detail-item"
});
hashItem.append($("<div>", {
class: "block-detail-label",
text: "Block Hash"
}));
hashItem.append($("<div>", {
class: "block-hash",
text: block.id
}));
headerSection.append(hashItem);
// Add timestamp
const timeItem = $("<div>", {
class: "block-detail-item"
});
timeItem.append($("<div>", {
class: "block-detail-label",
text: "Timestamp"
}));
timeItem.append($("<div>", {
class: "block-detail-value",
text: timestamp
}));
headerSection.append(timeItem);
// Add merkle root
const merkleItem = $("<div>", {
class: "block-detail-item"
});
merkleItem.append($("<div>", {
class: "block-detail-label",
text: "Merkle Root"
}));
merkleItem.append($("<div>", {
class: "block-hash",
text: block.merkle_root
}));
headerSection.append(merkleItem);
// Add previous block hash
const prevHashItem = $("<div>", {
class: "block-detail-item"
});
prevHashItem.append($("<div>", {
class: "block-detail-label",
text: "Previous Block"
}));
prevHashItem.append($("<div>", {
class: "block-hash",
text: block.previousblockhash
}));
headerSection.append(prevHashItem);
blockDetails.append(headerSection);
// Create the mining section
const miningSection = $("<div>", {
class: "block-detail-section"
});
miningSection.append($("<div>", {
class: "block-detail-title",
text: "Mining Details"
}));
// Add miner/pool
const minerItem = $("<div>", {
class: "block-detail-item"
});
minerItem.append($("<div>", {
class: "block-detail-label",
text: "Miner"
}));
const poolName = block.extras && block.extras.pool ? block.extras.pool.name : "Unknown";
minerItem.append($("<div>", {
class: "block-detail-value",
text: poolName
}));
miningSection.append(minerItem);
// Add difficulty
const difficultyItem = $("<div>", {
class: "block-detail-item"
});
difficultyItem.append($("<div>", {
class: "block-detail-label",
text: "Difficulty"
}));
difficultyItem.append($("<div>", {
class: "block-detail-value",
text: numberWithCommas(Math.round(block.difficulty))
}));
miningSection.append(difficultyItem);
// Add nonce
const nonceItem = $("<div>", {
class: "block-detail-item"
});
nonceItem.append($("<div>", {
class: "block-detail-label",
text: "Nonce"
}));
nonceItem.append($("<div>", {
class: "block-detail-value",
text: numberWithCommas(block.nonce)
}));
miningSection.append(nonceItem);
// Add bits
const bitsItem = $("<div>", {
class: "block-detail-item"
});
bitsItem.append($("<div>", {
class: "block-detail-label",
text: "Bits"
}));
bitsItem.append($("<div>", {
class: "block-detail-value",
text: block.bits
}));
miningSection.append(bitsItem);
// Add version
const versionItem = $("<div>", {
class: "block-detail-item"
});
versionItem.append($("<div>", {
class: "block-detail-label",
text: "Version"
}));
versionItem.append($("<div>", {
class: "block-detail-value",
text: "0x" + block.version.toString(16)
}));
miningSection.append(versionItem);
blockDetails.append(miningSection);
// Create the transaction section
const txSection = $("<div>", {
class: "block-detail-section"
});
txSection.append($("<div>", {
class: "block-detail-title",
text: "Transaction Details"
}));
// Add transaction count
const txCountItem = $("<div>", {
class: "block-detail-item"
});
txCountItem.append($("<div>", {
class: "block-detail-label",
text: "Transaction Count"
}));
txCountItem.append($("<div>", {
class: "block-detail-value",
text: numberWithCommas(block.tx_count)
}));
txSection.append(txCountItem);
// Add size
const sizeItem = $("<div>", {
class: "block-detail-item"
});
sizeItem.append($("<div>", {
class: "block-detail-label",
text: "Size"
}));
sizeItem.append($("<div>", {
class: "block-detail-value",
text: formatFileSize(block.size)
}));
txSection.append(sizeItem);
// Add weight
const weightItem = $("<div>", {
class: "block-detail-item"
});
weightItem.append($("<div>", {
class: "block-detail-label",
text: "Weight"
}));
weightItem.append($("<div>", {
class: "block-detail-value",
text: numberWithCommas(block.weight) + " WU"
}));
txSection.append(weightItem);
blockDetails.append(txSection);
// Create the fee section if available
if (block.extras) {
const feeSection = $("<div>", {
class: "block-detail-section"
});
feeSection.append($("<div>", {
class: "block-detail-title",
text: "Fee Details"
}));
// Add total fees
const totalFeesItem = $("<div>", {
class: "block-detail-item"
});
totalFeesItem.append($("<div>", {
class: "block-detail-label",
text: "Total Fees"
}));
const totalFees = (block.extras.totalFees / 100000000).toFixed(8);
totalFeesItem.append($("<div>", {
class: "block-detail-value",
text: totalFees + " BTC"
}));
feeSection.append(totalFeesItem);
// Add reward
const rewardItem = $("<div>", {
class: "block-detail-item"
});
rewardItem.append($("<div>", {
class: "block-detail-label",
text: "Block Reward"
}));
const reward = (block.extras.reward / 100000000).toFixed(8);
rewardItem.append($("<div>", {
class: "block-detail-value",
text: reward + " BTC"
}));
feeSection.append(rewardItem);
// Add median fee
const medianFeeItem = $("<div>", {
class: "block-detail-item"
});
medianFeeItem.append($("<div>", {
class: "block-detail-label",
text: "Median Fee Rate"
}));
medianFeeItem.append($("<div>", {
class: "block-detail-value",
text: block.extras.medianFee + " sat/vB"
}));
feeSection.append(medianFeeItem);
// Add average fee
const avgFeeItem = $("<div>", {
class: "block-detail-item"
});
avgFeeItem.append($("<div>", {
class: "block-detail-label",
text: "Average Fee"
}));
avgFeeItem.append($("<div>", {
class: "block-detail-value",
text: numberWithCommas(block.extras.avgFee) + " sat"
}));
feeSection.append(avgFeeItem);
// Add average fee rate
const avgFeeRateItem = $("<div>", {
class: "block-detail-item"
});
avgFeeRateItem.append($("<div>", {
class: "block-detail-label",
text: "Average Fee Rate"
}));
avgFeeRateItem.append($("<div>", {
class: "block-detail-value",
text: block.extras.avgFeeRate + " sat/vB"
}));
feeSection.append(avgFeeRateItem);
// Add fee range with visual representation
if (block.extras.feeRange && block.extras.feeRange.length > 0) {
const feeRangeItem = $("<div>", {
class: "block-detail-item transaction-data"
});
feeRangeItem.append($("<div>", {
class: "block-detail-label",
text: "Fee Rate Percentiles (sat/vB)"
}));
const feeRangeText = $("<div>", {
class: "block-detail-value",
text: block.extras.feeRange.join(", ")
});
feeRangeItem.append(feeRangeText);
// Add visual fee bar
const feeBarContainer = $("<div>", {
class: "fee-bar-container"
});
const feeBar = $("<div>", {
class: "fee-bar"
});
feeBarContainer.append(feeBar);
feeRangeItem.append(feeBarContainer);
// Animate the fee bar
setTimeout(() => {
feeBar.css("width", "100%");
}, 100);
feeSection.append(feeRangeItem);
}
blockDetails.append(feeSection);
}
// Show the modal
modal.css("display", "block");
}
// Function to close the modal
function closeModal() {
$("#block-modal").css("display", "none");
}

File diff suppressed because it is too large Load Diff

View File

@ -1,406 +0,0 @@
"use strict";
// Global variables
let currentFilter = "all";
let currentOffset = 0;
const pageSize = 20;
let hasMoreNotifications = true;
let isLoading = false;
// Initialize when document is ready
$(document).ready(() => {
console.log("Notification page initializing...");
// Set up filter buttons
$('.filter-button').click(function () {
$('.filter-button').removeClass('active');
$(this).addClass('active');
currentFilter = $(this).data('filter');
resetAndLoadNotifications();
});
// Set up action buttons
$('#mark-all-read').click(markAllAsRead);
$('#clear-read').click(clearReadNotifications);
$('#clear-all').click(clearAllNotifications);
$('#load-more').click(loadMoreNotifications);
// Initial load of notifications
loadNotifications();
// Start polling for unread count
startUnreadCountPolling();
// Initialize BitcoinMinuteRefresh if available
if (typeof BitcoinMinuteRefresh !== 'undefined' && BitcoinMinuteRefresh.initialize) {
BitcoinMinuteRefresh.initialize(refreshNotifications);
console.log("BitcoinMinuteRefresh initialized with refresh function");
}
// Start periodic update of notification timestamps every 30 seconds
setInterval(updateNotificationTimestamps, 30000);
});
// Load notifications with current filter
function loadNotifications() {
if (isLoading) return;
isLoading = true;
showLoading();
const params = {
limit: pageSize,
offset: currentOffset
};
if (currentFilter !== "all") {
params.category = currentFilter;
}
$.ajax({
url: `/api/notifications?${$.param(params)}`,
method: "GET",
dataType: "json",
success: (data) => {
renderNotifications(data.notifications, currentOffset === 0);
updateUnreadBadge(data.unread_count);
// Update load more button state
hasMoreNotifications = data.notifications.length === pageSize;
$('#load-more').prop('disabled', !hasMoreNotifications);
isLoading = false;
},
error: (xhr, status, error) => {
console.error("Error loading notifications:", error);
showError("Failed to load notifications. Please try again.");
isLoading = false;
}
});
}
// Reset offset and load notifications
function resetAndLoadNotifications() {
currentOffset = 0;
loadNotifications();
}
// Load more notifications
function loadMoreNotifications() {
if (!hasMoreNotifications || isLoading) return;
currentOffset += pageSize;
loadNotifications();
}
// Refresh notifications (for periodic updates)
function refreshNotifications() {
// Only refresh if we're on the first page
if (currentOffset === 0) {
resetAndLoadNotifications();
} else {
// Just update the unread count
updateUnreadCount();
}
}
// Update notification timestamps to relative time
function updateNotificationTimestamps() {
$('.notification-item').each(function () {
const timestampStr = $(this).attr('data-timestamp');
if (timestampStr) {
const timestamp = new Date(timestampStr);
const relativeTime = formatTimestamp(timestamp);
$(this).find('.notification-time').text(relativeTime);
}
});
}
// Show loading indicator
function showLoading() {
if (currentOffset === 0) {
// First page load, show loading message
$('#notifications-container').html('<div class="loading-message">Loading notifications<span class="terminal-cursor"></span></div>');
} else {
// Pagination load, show loading below
$('#load-more').prop('disabled', true).text('Loading...');
}
}
// Show error message
function showError(message) {
$('#notifications-container').html(`<div class="error-message">${message}</div>`);
$('#load-more').hide();
}
// Render notifications in the container
function renderNotifications(notifications, isFirstPage) {
const container = $('#notifications-container');
// If first page and no notifications
if (isFirstPage && (!notifications || notifications.length === 0)) {
container.html($('#empty-template').html());
$('#load-more').hide();
return;
}
// If first page, clear container
if (isFirstPage) {
container.empty();
}
// Render each notification
notifications.forEach(notification => {
const notificationElement = createNotificationElement(notification);
container.append(notificationElement);
});
// Show/hide load more button
$('#load-more').show().prop('disabled', !hasMoreNotifications);
}
// Create notification element from template
function createNotificationElement(notification) {
const template = $('#notification-template').html();
const element = $(template);
// Set data attributes
element.attr('data-id', notification.id)
.attr('data-level', notification.level)
.attr('data-category', notification.category)
.attr('data-read', notification.read)
.attr('data-timestamp', notification.timestamp);
// Set icon based on level
const iconElement = element.find('.notification-icon i');
switch (notification.level) {
case 'success':
iconElement.addClass('fa-check-circle');
break;
case 'info':
iconElement.addClass('fa-info-circle');
break;
case 'warning':
iconElement.addClass('fa-exclamation-triangle');
break;
case 'error':
iconElement.addClass('fa-times-circle');
break;
default:
iconElement.addClass('fa-bell');
}
// Append "Z" to indicate UTC if not present
let utcTimestampStr = notification.timestamp;
if (!utcTimestampStr.endsWith('Z')) {
utcTimestampStr += 'Z';
}
const utcDate = new Date(utcTimestampStr);
// Convert UTC date to Los Angeles time with a timezone name for clarity
const fullTimestamp = utcDate.toLocaleString('en-US', {
timeZone: 'America/Los_Angeles',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
year: 'numeric',
month: 'short',
day: 'numeric',
timeZoneName: 'short'
});
// Append the full timestamp to the notification message
const messageWithTimestamp = `${notification.message}<br><span class="full-timestamp">${fullTimestamp}</span>`;
element.find('.notification-message').html(messageWithTimestamp);
// Set metadata for relative time display
element.find('.notification-time').text(formatTimestamp(utcDate));
element.find('.notification-category').text(notification.category);
// Set up action buttons
element.find('.mark-read-button').on('click', (e) => {
e.stopPropagation();
markAsRead(notification.id);
});
element.find('.delete-button').on('click', (e) => {
e.stopPropagation();
deleteNotification(notification.id);
});
// Hide mark as read button if already read
if (notification.read) {
element.find('.mark-read-button').hide();
}
return element;
}
// Format timestamp as relative time
function formatTimestamp(timestamp) {
const now = new Date();
const diffMs = now - timestamp;
const diffSec = Math.floor(diffMs / 1000);
const diffMin = Math.floor(diffSec / 60);
const diffHour = Math.floor(diffMin / 60);
const diffDay = Math.floor(diffHour / 24);
if (diffSec < 60) {
return "just now";
} else if (diffMin < 60) {
return `${diffMin}m ago`;
} else if (diffHour < 24) {
return `${diffHour}h ago`;
} else if (diffDay < 30) {
return `${diffDay}d ago`;
} else {
// Format as date for older notifications
return timestamp.toLocaleDateString('en-US', { timeZone: 'America/Los_Angeles' });
}
}
// Mark a notification as read
function markAsRead(notificationId) {
$.ajax({
url: "/api/notifications/mark_read",
method: "POST",
data: JSON.stringify({ notification_id: notificationId }),
contentType: "application/json",
success: (data) => {
// Update UI
$(`[data-id="${notificationId}"]`).attr('data-read', 'true');
$(`[data-id="${notificationId}"]`).find('.mark-read-button').hide();
// Update unread badge
updateUnreadBadge(data.unread_count);
},
error: (xhr, status, error) => {
console.error("Error marking notification as read:", error);
}
});
}
// Mark all notifications as read
function markAllAsRead() {
$.ajax({
url: "/api/notifications/mark_read",
method: "POST",
data: JSON.stringify({}),
contentType: "application/json",
success: (data) => {
// Update UI
$('.notification-item').attr('data-read', 'true');
$('.mark-read-button').hide();
// Update unread badge
updateUnreadBadge(0);
},
error: (xhr, status, error) => {
console.error("Error marking all notifications as read:", error);
}
});
}
// Delete a notification
function deleteNotification(notificationId) {
$.ajax({
url: "/api/notifications/delete",
method: "POST",
data: JSON.stringify({ notification_id: notificationId }),
contentType: "application/json",
success: (data) => {
// Remove from UI with animation
$(`[data-id="${notificationId}"]`).fadeOut(300, function () {
$(this).remove();
// Check if container is empty now
if ($('#notifications-container').children().length === 0) {
$('#notifications-container').html($('#empty-template').html());
$('#load-more').hide();
}
});
// Update unread badge
updateUnreadBadge(data.unread_count);
},
error: (xhr, status, error) => {
console.error("Error deleting notification:", error);
}
});
}
// Clear read notifications
function clearReadNotifications() {
if (!confirm("Are you sure you want to clear all read notifications?")) {
return;
}
$.ajax({
url: "/api/notifications/clear",
method: "POST",
data: JSON.stringify({
// Special parameter to clear only read notifications
read_only: true
}),
contentType: "application/json",
success: () => {
// Reload notifications
resetAndLoadNotifications();
},
error: (xhr, status, error) => {
console.error("Error clearing read notifications:", error);
}
});
}
// Clear all notifications
function clearAllNotifications() {
if (!confirm("Are you sure you want to clear ALL notifications? This cannot be undone.")) {
return;
}
$.ajax({
url: "/api/notifications/clear",
method: "POST",
data: JSON.stringify({}),
contentType: "application/json",
success: () => {
// Reload notifications
resetAndLoadNotifications();
},
error: (xhr, status, error) => {
console.error("Error clearing all notifications:", error);
}
});
}
// Update unread badge
function updateUnreadBadge(count) {
$('#unread-badge').text(count);
// Add special styling if unread
if (count > 0) {
$('#unread-badge').addClass('has-unread');
} else {
$('#unread-badge').removeClass('has-unread');
}
}
// Update unread count from API
function updateUnreadCount() {
$.ajax({
url: "/api/notifications/unread_count",
method: "GET",
success: (data) => {
updateUnreadBadge(data.unread_count);
},
error: (xhr, status, error) => {
console.error("Error updating unread count:", error);
}
});
}
// Start polling for unread count
function startUnreadCountPolling() {
// Update every 30 seconds
setInterval(updateUnreadCount, 30000);
}

View File

@ -1,492 +0,0 @@
"use strict";
// Global variables for workers dashboard
let workerData = null;
let refreshTimer;
const pageLoadTime = Date.now();
let lastManualRefreshTime = 0;
const 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
const MIN_REFRESH_INTERVAL = 10000; // Minimum 10 seconds between refreshes
// Hashrate Normalization Utilities
// Helper function to normalize hashrate to TH/s for consistent graphing
function normalizeHashrate(value, unit = 'th/s') {
if (!value || isNaN(value)) return 0;
unit = unit.toLowerCase();
const unitConversion = {
'ph/s': 1000,
'eh/s': 1000000,
'gh/s': 1 / 1000,
'mh/s': 1 / 1000000,
'kh/s': 1 / 1000000000,
'h/s': 1 / 1000000000000
};
return unitConversion[unit] !== undefined ? value * unitConversion[unit] : value;
}
// Helper function to format hashrate values for display
function formatHashrateForDisplay(value, unit) {
if (isNaN(value) || value === null || value === undefined) return "N/A";
const normalizedValue = unit ? normalizeHashrate(value, unit) : value;
const unitRanges = [
{ threshold: 1000000, unit: 'EH/s', divisor: 1000000 },
{ threshold: 1000, unit: 'PH/s', divisor: 1000 },
{ threshold: 1, unit: 'TH/s', divisor: 1 },
{ threshold: 0.001, unit: 'GH/s', divisor: 1 / 1000 },
{ threshold: 0, unit: 'MH/s', divisor: 1 / 1000000 }
];
for (const range of unitRanges) {
if (normalizedValue >= range.threshold) {
return (normalizedValue / range.divisor).toFixed(2) + ' ' + range.unit;
}
}
return (normalizedValue * 1000000).toFixed(2) + ' MH/s';
}
// Initialize the page
$(document).ready(function () {
console.log("Worker page initializing...");
initNotificationBadge();
initializePage();
updateServerTime();
window.manualRefresh = fetchWorkerData;
setTimeout(() => {
if (typeof BitcoinMinuteRefresh !== 'undefined' && BitcoinMinuteRefresh.initialize) {
BitcoinMinuteRefresh.initialize(window.manualRefresh);
console.log("BitcoinMinuteRefresh initialized with refresh function");
} else {
console.warn("BitcoinMinuteRefresh not available");
}
}, 500);
fetchWorkerData();
$('.filter-button').click(function () {
$('.filter-button').removeClass('active');
$(this).addClass('active');
filterState.currentFilter = $(this).data('filter');
filterWorkers();
});
$('#worker-search').on('input', function () {
filterState.searchTerm = $(this).val().toLowerCase();
filterWorkers();
});
});
// Initialize page elements
function initializePage() {
console.log("Initializing page elements...");
if (document.getElementById('total-hashrate-chart')) {
initializeMiniChart();
}
$('#worker-grid').html('<div class="text-center p-5"><i class="fas fa-spinner fa-spin"></i> Loading worker data...</div>');
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);
});
}
}
// Update unread notifications badge in navigation
function updateNotificationBadge() {
$.ajax({
url: "/api/notifications/unread_count",
method: "GET",
success: function (data) {
const unreadCount = data.unread_count;
const badge = $("#nav-unread-badge");
if (unreadCount > 0) {
badge.text(unreadCount).show();
} else {
badge.hide();
}
}
});
}
// Initialize notification badge checking
function initNotificationBadge() {
updateNotificationBadge();
setInterval(updateNotificationBadge, 60000);
}
// Server time update via polling - enhanced to use shared storage
function updateServerTime() {
console.log("Updating server time...");
try {
const storedOffset = localStorage.getItem('serverTimeOffset');
const storedStartTime = localStorage.getItem('serverStartTime');
if (storedOffset && storedStartTime) {
serverTimeOffset = parseFloat(storedOffset);
serverStartTime = parseFloat(storedStartTime);
console.log("Using stored server time offset:", serverTimeOffset, "ms");
if (typeof BitcoinMinuteRefresh !== 'undefined' && BitcoinMinuteRefresh.updateServerTime) {
BitcoinMinuteRefresh.updateServerTime(serverTimeOffset, serverStartTime);
}
return;
}
} catch (e) {
console.error("Error reading stored server time:", e);
}
$.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();
localStorage.setItem('serverTimeOffset', serverTimeOffset.toString());
localStorage.setItem('serverStartTime', serverStartTime.toString());
if (typeof BitcoinMinuteRefresh !== 'undefined' && BitcoinMinuteRefresh.updateServerTime) {
BitcoinMinuteRefresh.updateServerTime(serverTimeOffset, serverStartTime);
}
console.log("Server time synchronized. Offset:", serverTimeOffset, "ms");
},
error: function (jqXHR, textStatus, errorThrown) {
console.error("Error fetching server time:", textStatus, errorThrown);
}
});
}
// Utility functions to show/hide loader
function showLoader() {
$("#loader").show();
}
function hideLoader() {
$("#loader").hide();
}
// Fetch worker data from API with pagination, limiting to 10 pages
function fetchWorkerData(forceRefresh = false) {
console.log("Fetching worker data...");
lastManualRefreshTime = Date.now();
$('#worker-grid').addClass('loading-fade');
showLoader();
const maxPages = 10;
const requests = [];
// Create requests for pages 1 through maxPages concurrently
for (let page = 1; page <= maxPages; page++) {
const apiUrl = `/api/workers?page=${page}${forceRefresh ? '&force=true' : ''}`;
requests.push($.ajax({
url: apiUrl,
method: 'GET',
dataType: 'json',
timeout: 15000
}));
}
// Process all requests concurrently
Promise.all(requests)
.then(pages => {
let allWorkers = [];
let aggregatedData = null;
pages.forEach((data, i) => {
if (data && data.workers && data.workers.length > 0) {
allWorkers = allWorkers.concat(data.workers);
if (i === 0) {
aggregatedData = data; // preserve stats from first page
}
} else {
console.warn(`No workers found on page ${i + 1}`);
}
});
// Deduplicate workers if necessary (using worker.name as unique key)
const uniqueWorkers = allWorkers.filter((worker, index, self) =>
index === self.findIndex((w) => w.name === worker.name)
);
workerData = aggregatedData || {};
workerData.workers = uniqueWorkers;
if (typeof BitcoinMinuteRefresh !== 'undefined' && BitcoinMinuteRefresh.notifyRefresh) {
BitcoinMinuteRefresh.notifyRefresh();
}
updateWorkerGrid();
updateSummaryStats();
updateMiniChart();
updateLastUpdated();
$('#retry-button').hide();
connectionRetryCount = 0;
console.log("Worker data updated successfully");
$('#worker-grid').removeClass('loading-fade');
})
.catch(error => {
console.error("Error fetching worker data:", error);
})
.finally(() => {
hideLoader();
});
}
// Refresh worker data every 60 seconds
setInterval(function () {
console.log("Refreshing worker data at " + new Date().toLocaleTimeString());
fetchWorkerData();
}, 60000);
// Update the worker grid with data
function updateWorkerGrid() {
console.log("Updating worker grid...");
if (!workerData || !workerData.workers) {
console.error("No worker data available");
return;
}
const workerGrid = $('#worker-grid');
workerGrid.empty();
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;
}
filteredWorkers.forEach(worker => {
const card = createWorkerCard(worker);
workerGrid.append(card);
});
}
// Create worker card element
function createWorkerCard(worker) {
const card = $('<div class="worker-card"></div>');
card.addClass(worker.status === 'online' ? 'worker-card-online' : 'worker-card-offline');
card.append(`<div class="worker-type">${worker.type}</div>`);
card.append(`<div class="worker-name">${worker.name}</div>`);
card.append(`<div class="status-badge ${worker.status === 'online' ? 'status-badge-online' : 'status-badge-offline'}">${worker.status.toUpperCase()}</div>`);
const maxHashrate = 125; // TH/s - adjust based on your fleet
const normalizedHashrate = normalizeHashrate(worker.hashrate_3hr, worker.hashrate_3hr_unit || 'th/s');
const hashratePercent = Math.min(100, (normalizedHashrate / maxHashrate) * 100);
const formattedHashrate = formatHashrateForDisplay(worker.hashrate_3hr, worker.hashrate_3hr_unit || 'th/s');
card.append(`
<div class="worker-stats-row">
<div class="worker-stats-label">Hashrate (3hr):</div>
<div class="white-glow">${formattedHashrate}</div>
</div>
<div class="stats-bar-container">
<div class="stats-bar" style="width: ${hashratePercent}%"></div>
</div>
`);
card.append(`
<div class="worker-stats">
<div class="worker-stats-row">
<div class="worker-stats-label">Last Share:</div>
<div class="blue-glow">${typeof worker.last_share === 'string' ? worker.last_share.split(' ')[1] || worker.last_share : 'N/A'}</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>
`);
return 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();
const matchesFilter = filterState.currentFilter === 'all' ||
(filterState.currentFilter === 'online' && isOnline) ||
(filterState.currentFilter === 'offline' && !isOnline) ||
(filterState.currentFilter === 'asic' && workerType === 'asic') ||
(filterState.currentFilter === 'bitaxe' && workerType === 'bitaxe');
const matchesSearch = filterState.searchTerm === '' || workerName.includes(filterState.searchTerm);
return matchesFilter && matchesSearch;
});
}
// Apply filter to rendered worker cards
function filterWorkers() {
if (!workerData || !workerData.workers) return;
updateWorkerGrid();
}
// Update summary stats with normalized hashrate display
function updateSummaryStats() {
if (!workerData) return;
$('#workers-count').text(workerData.workers_total || 0);
$('#workers-online').text(workerData.workers_online || 0);
$('#workers-offline').text(workerData.workers_offline || 0);
const onlinePercent = workerData.workers_total > 0 ? workerData.workers_online / workerData.workers_total : 0;
$('.worker-ring').css('--online-percent', onlinePercent);
const formattedHashrate = workerData.total_hashrate !== undefined ?
formatHashrateForDisplay(workerData.total_hashrate, workerData.hashrate_unit || 'TH/s') :
'0.0 TH/s';
$('#total-hashrate').text(formattedHashrate);
$('#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() {
console.log("Initializing mini chart...");
const ctx = document.getElementById('total-hashrate-chart');
if (!ctx) {
console.error("Mini chart canvas not found");
return;
}
const labels = Array(24).fill('').map((_, i) => i);
const data = Array(24).fill(0).map(() => Math.random() * 100 + 700);
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 and normalization
function updateMiniChart() {
if (!miniChart || !workerData || !workerData.hashrate_history) {
console.log("Skipping mini chart update - missing data");
return;
}
const historyData = workerData.hashrate_history;
if (!historyData || historyData.length === 0) {
console.log("No hashrate history data available");
return;
}
const values = historyData.map(item => normalizeHashrate(parseFloat(item.value) || 0, item.unit || workerData.hashrate_unit || 'th/s'));
const labels = historyData.map(item => item.time);
miniChart.data.labels = labels;
miniChart.data.datasets[0].data = values;
const min = Math.min(...values.filter(v => v > 0)) || 0;
const max = Math.max(...values) || 1;
miniChart.options.scales.y.min = min * 0.9;
miniChart.options.scales.y.max = max * 1.1;
miniChart.update('none');
}
// 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, ",");
}