mirror of
https://github.com/Retropex/custom-ocean.xyz-dashboard.git
synced 2025-05-12 19:20:45 +02:00
Delete static directory
This commit is contained in:
parent
35dd182eb2
commit
d374bc3ba1
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
});
|
@ -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
|
||||
// });
|
@ -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");
|
||||
}
|
1309
static/js/main.js
1309
static/js/main.js
File diff suppressed because it is too large
Load Diff
@ -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);
|
||||
}
|
@ -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, ",");
|
||||
}
|
Loading…
Reference in New Issue
Block a user