mirror of
https://github.com/Retropex/custom-ocean.xyz-dashboard.git
synced 2025-05-13 03:30:46 +02:00
Add files via upload
This commit is contained in:
parent
6fcbfa806d
commit
8c5c39d435
852
static/js/BitcoinProgressBar.js
Normal file
852
static/js/BitcoinProgressBar.js
Normal file
@ -0,0 +1,852 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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>
|
||||||
|
|
||||||
|
<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);
|
||||||
|
|
||||||
|
// 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
|
||||||
|
const now = new Date(Date.now() + (serverTimeOffset || 0));
|
||||||
|
const seconds = now.getSeconds();
|
||||||
|
const milliseconds = now.getMilliseconds();
|
||||||
|
|
||||||
|
// Calculate precise progress within the minute (0-1)
|
||||||
|
const progress = (seconds + (milliseconds / 1000)) / 60;
|
||||||
|
|
||||||
|
// Update the progress bar
|
||||||
|
const progressBar = document.getElementById('minute-progress-inner');
|
||||||
|
if (progressBar) {
|
||||||
|
// Set the width based on the current progress through the minute
|
||||||
|
progressBar.style.width = (progress * 100) + "%";
|
||||||
|
|
||||||
|
// Add effects when close to the end of the minute
|
||||||
|
if (seconds >= 50) {
|
||||||
|
progressBar.classList.add('near-refresh');
|
||||||
|
document.getElementById('refresh-status').textContent = `Refresh in ${60 - seconds} seconds...`;
|
||||||
|
} else {
|
||||||
|
progressBar.classList.remove('near-refresh');
|
||||||
|
document.getElementById('refresh-status').textContent = 'Next refresh at the top of the minute';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flash when reaching 00 seconds (new minute)
|
||||||
|
if (seconds === 0 && milliseconds < 500) {
|
||||||
|
progressBar.classList.add('refresh-now');
|
||||||
|
document.getElementById('refresh-status').textContent = 'Refreshing data...';
|
||||||
|
|
||||||
|
// Remove the class after animation completes
|
||||||
|
setTimeout(() => {
|
||||||
|
progressBar.classList.remove('refresh-now');
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
progressBar.classList.remove('refresh-now');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we've crossed into a new minute
|
||||||
|
const currentMinute = now.getMinutes();
|
||||||
|
if (lastMinuteValue !== -1 && currentMinute !== lastMinuteValue && seconds === 0) {
|
||||||
|
// Trigger refresh on minute change (only when seconds are 0)
|
||||||
|
if (typeof refreshCallback === 'function') {
|
||||||
|
console.log('New minute started - triggering refresh...');
|
||||||
|
refreshCallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update last minute value
|
||||||
|
lastMinuteValue = currentMinute;
|
||||||
|
|
||||||
|
} 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));
|
||||||
|
const hours = String(now.getHours()).padStart(2, '0');
|
||||||
|
const minutes = String(now.getMinutes()).padStart(2, '0');
|
||||||
|
const seconds = String(now.getSeconds()).padStart(2, '0');
|
||||||
|
const timeString = `${hours}:${minutes}:${seconds}`;
|
||||||
|
|
||||||
|
// 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
|
||||||
|
document.getElementById('uptime-hours').textContent = String(hours).padStart(2, '0');
|
||||||
|
document.getElementById('uptime-minutes').textContent = String(minutes).padStart(2, '0');
|
||||||
|
document.getElementById('uptime-seconds').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');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
}
|
||||||
|
});
|
384
static/js/block-animation.js
Normal file
384
static/js/block-animation.js
Normal file
@ -0,0 +1,384 @@
|
|||||||
|
// 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
|
||||||
|
});
|
762
static/js/blocks.js
Normal file
762
static/js/blocks.js
Normal file
@ -0,0 +1,762 @@
|
|||||||
|
"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");
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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
|
||||||
|
function loadLatestBlocks() {
|
||||||
|
if (isLoading) return;
|
||||||
|
|
||||||
|
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
|
||||||
|
$.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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user