mirror of
https://github.com/Retropex/custom-ocean.xyz-dashboard.git
synced 2025-05-12 19:20:45 +02:00

This commit introduces significant updates to the application, focusing on currency support and improved configuration management. Key changes include: - Added a `config_reset` flag in `update_metrics_job` in `App.py` to manage configuration resets. - Modified `update_config` to handle currency changes and update notifications accordingly. - Updated `MiningDashboardService` to fetch and include exchange rates and configured currency in metrics. - Introduced utility functions for currency formatting and symbol retrieval in `notification_service.py`. - Enhanced UI components in `main.js` and `boot.html` to support currency selection and display. - Adjusted Docker Compose file to remove hardcoded wallet and power settings for better flexibility. These changes enhance usability by allowing users to view financial data in their preferred currency and manage configurations more effectively.
3137 lines
133 KiB
JavaScript
3137 lines
133 KiB
JavaScript
"use strict";
|
|
|
|
/**
|
|
* ArrowIndicator - A clean implementation for managing metric value change indicators
|
|
*
|
|
* This module provides a simple, self-contained system for managing arrow indicators
|
|
* that show whether metric values have increased, decreased, or remained stable
|
|
* between refreshes.
|
|
*/
|
|
class ArrowIndicator {
|
|
constructor() {
|
|
this.previousMetrics = {};
|
|
this.arrowStates = {};
|
|
this.changeThreshold = 0.00001;
|
|
this.debug = false;
|
|
|
|
// Load saved state immediately
|
|
this.loadFromStorage();
|
|
|
|
// DOM ready handling
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', () => this.initializeDOM());
|
|
} else {
|
|
setTimeout(() => this.initializeDOM(), 100);
|
|
}
|
|
|
|
// Handle tab visibility changes
|
|
document.addEventListener("visibilitychange", () => {
|
|
if (!document.hidden) {
|
|
this.loadFromStorage();
|
|
this.forceApplyArrows();
|
|
}
|
|
});
|
|
|
|
// Handle storage changes for cross-tab sync
|
|
window.addEventListener('storage', this.handleStorageEvent.bind(this));
|
|
}
|
|
|
|
initializeDOM() {
|
|
// First attempt to apply arrows
|
|
this.forceApplyArrows();
|
|
|
|
// Set up a detection system to find indicator elements
|
|
this.detectIndicatorElements();
|
|
}
|
|
|
|
detectIndicatorElements() {
|
|
// Scan the DOM for all elements that match our indicator pattern
|
|
const indicatorElements = {};
|
|
|
|
// Look for elements with IDs starting with "indicator_"
|
|
const elements = document.querySelectorAll('[id^="indicator_"]');
|
|
elements.forEach(element => {
|
|
const key = element.id.replace('indicator_', '');
|
|
indicatorElements[key] = element;
|
|
});
|
|
|
|
// Apply arrows to the found elements
|
|
this.applyArrowsToFoundElements(indicatorElements);
|
|
|
|
// Set up a MutationObserver to catch dynamically added elements
|
|
this.setupMutationObserver();
|
|
|
|
// Schedule additional attempts with increasing delays
|
|
[500, 1000, 2000].forEach(delay => {
|
|
setTimeout(() => this.forceApplyArrows(), delay);
|
|
});
|
|
}
|
|
|
|
setupMutationObserver() {
|
|
// Watch for changes to the DOM that might add indicator elements
|
|
const observer = new MutationObserver(mutations => {
|
|
let newElementsFound = false;
|
|
|
|
mutations.forEach(mutation => {
|
|
if (mutation.type === 'childList' && mutation.addedNodes.length) {
|
|
mutation.addedNodes.forEach(node => {
|
|
if (node.nodeType === 1) { // Element node
|
|
// Check the node itself
|
|
if (node.id && node.id.startsWith('indicator_')) {
|
|
newElementsFound = true;
|
|
}
|
|
|
|
// Check children of the node
|
|
const childIndicators = node.querySelectorAll('[id^="indicator_"]');
|
|
if (childIndicators.length) {
|
|
newElementsFound = true;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
if (newElementsFound) {
|
|
this.forceApplyArrows();
|
|
}
|
|
});
|
|
|
|
// Start observing
|
|
observer.observe(document.body, {
|
|
childList: true,
|
|
subtree: true
|
|
});
|
|
}
|
|
|
|
forceApplyArrows() {
|
|
let applied = 0;
|
|
let missing = 0;
|
|
|
|
// Apply arrows to all indicators we know about
|
|
Object.keys(this.arrowStates).forEach(key => {
|
|
const element = document.getElementById(`indicator_${key}`);
|
|
if (element) {
|
|
// Double-check if the element is visible
|
|
const arrowValue = this.arrowStates[key] || "";
|
|
|
|
// Use direct DOM manipulation instead of innerHTML for better reliability
|
|
if (arrowValue) {
|
|
// Clear existing content
|
|
while (element.firstChild) {
|
|
element.removeChild(element.firstChild);
|
|
}
|
|
|
|
// Create the new icon element
|
|
const tmpDiv = document.createElement('div');
|
|
tmpDiv.innerHTML = arrowValue;
|
|
const iconElement = tmpDiv.firstChild;
|
|
|
|
// Make the arrow more visible
|
|
if (iconElement) {
|
|
element.appendChild(iconElement);
|
|
|
|
// Force the arrow to be visible
|
|
iconElement.style.display = "inline-block";
|
|
}
|
|
}
|
|
|
|
applied++;
|
|
} else {
|
|
missing++;
|
|
}
|
|
});
|
|
|
|
return applied;
|
|
}
|
|
|
|
applyArrowsToFoundElements(elements) {
|
|
let applied = 0;
|
|
|
|
Object.keys(elements).forEach(key => {
|
|
if (this.arrowStates[key]) {
|
|
const element = elements[key];
|
|
element.innerHTML = this.arrowStates[key];
|
|
applied++;
|
|
}
|
|
});
|
|
}
|
|
|
|
updateIndicators(newMetrics, forceReset = false) {
|
|
if (!newMetrics) return this.arrowStates;
|
|
|
|
// Define metrics that should have indicators
|
|
const metricKeys = [
|
|
"pool_total_hashrate", "hashrate_24hr", "hashrate_3hr", "hashrate_10min",
|
|
"hashrate_60sec", "block_number", "btc_price", "network_hashrate",
|
|
"difficulty", "daily_revenue", "daily_power_cost", "daily_profit_usd",
|
|
"monthly_profit_usd", "daily_mined_sats", "monthly_mined_sats", "unpaid_earnings",
|
|
"estimated_earnings_per_day_sats", "estimated_earnings_next_block_sats",
|
|
"estimated_rewards_in_window_sats", "workers_hashing"
|
|
];
|
|
|
|
// Clear all arrows if requested
|
|
if (forceReset) {
|
|
metricKeys.forEach(key => {
|
|
this.arrowStates[key] = "";
|
|
});
|
|
}
|
|
|
|
// Current theme affects arrow colors
|
|
const theme = getCurrentTheme();
|
|
const upArrowColor = THEME.SHARED.GREEN;
|
|
const downArrowColor = THEME.SHARED.RED;
|
|
|
|
// Get normalized values and compare with previous metrics
|
|
for (const key of metricKeys) {
|
|
if (newMetrics[key] === undefined) continue;
|
|
|
|
const newValue = this.getNormalizedValue(newMetrics, key);
|
|
if (newValue === null) continue;
|
|
|
|
if (this.previousMetrics[key] !== undefined) {
|
|
const prevValue = this.previousMetrics[key];
|
|
|
|
if (newValue > prevValue * (1 + this.changeThreshold)) {
|
|
this.arrowStates[key] = "<i class='arrow chevron fa-solid fa-angle-double-up bounce-up' style='color: green; display: inline-block !important;'></i>";
|
|
}
|
|
else if (newValue < prevValue * (1 - this.changeThreshold)) {
|
|
this.arrowStates[key] = "<i class='arrow chevron fa-solid fa-angle-double-down bounce-down' style='color: red; position: relative; top: -2px; display: inline-block !important;'></i>";
|
|
}
|
|
}
|
|
|
|
this.previousMetrics[key] = newValue;
|
|
}
|
|
|
|
// Apply arrows to DOM
|
|
this.forceApplyArrows();
|
|
|
|
// Save to localStorage for persistence
|
|
this.saveToStorage();
|
|
|
|
return this.arrowStates;
|
|
}
|
|
|
|
// Get a normalized value for a metric to ensure consistent comparisons
|
|
getNormalizedValue(metrics, key) {
|
|
const value = parseFloat(metrics[key]);
|
|
if (isNaN(value)) return null;
|
|
|
|
// Special handling for hashrate values to normalize units
|
|
if (key.includes('hashrate')) {
|
|
const unit = metrics[key + '_unit'] || 'th/s';
|
|
return this.normalizeHashrate(value, unit);
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
// Normalize hashrate to a common unit (TH/s)
|
|
normalizeHashrate(value, unit) {
|
|
// Use the enhanced global normalizeHashrate function
|
|
return window.normalizeHashrate(value, unit);
|
|
}
|
|
|
|
// Save current state to localStorage
|
|
saveToStorage() {
|
|
try {
|
|
// Save arrow states
|
|
localStorage.setItem('dashboardArrows', JSON.stringify(this.arrowStates));
|
|
|
|
// Save previous metrics for comparison after page reload
|
|
localStorage.setItem('dashboardPreviousMetrics', JSON.stringify(this.previousMetrics));
|
|
} catch (e) {
|
|
console.error("Error saving arrow indicators to localStorage:", e);
|
|
}
|
|
}
|
|
|
|
// Load state from localStorage
|
|
loadFromStorage() {
|
|
try {
|
|
// Load arrow states
|
|
const savedArrows = localStorage.getItem('dashboardArrows');
|
|
if (savedArrows) {
|
|
this.arrowStates = JSON.parse(savedArrows);
|
|
}
|
|
|
|
// Load previous metrics
|
|
const savedMetrics = localStorage.getItem('dashboardPreviousMetrics');
|
|
if (savedMetrics) {
|
|
this.previousMetrics = JSON.parse(savedMetrics);
|
|
}
|
|
} catch (e) {
|
|
console.error("Error loading arrow indicators from localStorage:", e);
|
|
// On error, reset to defaults
|
|
this.arrowStates = {};
|
|
this.previousMetrics = {};
|
|
}
|
|
}
|
|
|
|
// Handle storage events for cross-tab synchronization
|
|
handleStorageEvent(event) {
|
|
if (event.key === 'dashboardArrows') {
|
|
try {
|
|
const newArrows = JSON.parse(event.newValue);
|
|
this.arrowStates = newArrows;
|
|
this.forceApplyArrows();
|
|
} catch (e) {
|
|
console.error("Error handling storage event:", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Reset for new refresh cycle
|
|
prepareForRefresh() {
|
|
Object.keys(this.arrowStates).forEach(key => {
|
|
this.arrowStates[key] = "";
|
|
});
|
|
this.forceApplyArrows();
|
|
}
|
|
|
|
// Clear all indicators
|
|
clearAll() {
|
|
this.arrowStates = {};
|
|
this.previousMetrics = {};
|
|
this.forceApplyArrows();
|
|
this.saveToStorage();
|
|
}
|
|
}
|
|
|
|
// Create the singleton instance
|
|
const arrowIndicator = new ArrowIndicator();
|
|
|
|
// Global timezone configuration
|
|
let dashboardTimezone = 'America/Los_Angeles'; // Default
|
|
window.dashboardTimezone = dashboardTimezone; // Make it globally accessible
|
|
|
|
// Fetch the configured timezone when the page loads
|
|
function fetchTimezoneConfig() {
|
|
fetch('/api/timezone')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data && data.timezone) {
|
|
dashboardTimezone = data.timezone;
|
|
window.dashboardTimezone = dashboardTimezone; // Make it globally accessible
|
|
console.log(`Using configured timezone: ${dashboardTimezone}`);
|
|
}
|
|
})
|
|
.catch(error => console.error('Error fetching timezone config:', error));
|
|
}
|
|
|
|
// Call this on page load
|
|
document.addEventListener('DOMContentLoaded', fetchTimezoneConfig);
|
|
|
|
// Global variables
|
|
let previousMetrics = {};
|
|
let latestMetrics = null;
|
|
let initialLoad = true;
|
|
let trendData = [];
|
|
let trendLabels = [];
|
|
let trendChart = null;
|
|
let connectionRetryCount = 0;
|
|
let maxRetryCount = 10;
|
|
let reconnectionDelay = 1000; // Start with 1 second
|
|
let pingInterval = null;
|
|
let lastPingTime = Date.now();
|
|
let connectionLostTimeout = null;
|
|
|
|
// Server time variables for uptime calculation
|
|
let serverTimeOffset = 0;
|
|
let serverStartTime = null;
|
|
|
|
// Register Chart.js annotation plugin if available
|
|
if (window['chartjs-plugin-annotation']) {
|
|
Chart.register(window['chartjs-plugin-annotation']);
|
|
}
|
|
|
|
// Hashrate Normalization Utilities
|
|
// Enhanced normalizeHashrate function with better error handling for units
|
|
function normalizeHashrate(value, unit) {
|
|
if (!value || isNaN(value)) return 0;
|
|
|
|
// Validate and normalize input value
|
|
value = parseFloat(value);
|
|
|
|
// Standardize unit handling with a lookup table
|
|
const unit_normalized = (unit || 'th/s').toLowerCase().trim();
|
|
|
|
// Lookup table for conversion factors (all relative to TH/s)
|
|
const unitConversions = {
|
|
'ph/s': 1000,
|
|
'p/s': 1000,
|
|
'p': 1000,
|
|
'petahash': 1000,
|
|
'petahash/s': 1000,
|
|
'peta': 1000,
|
|
|
|
'eh/s': 1000000,
|
|
'e/s': 1000000,
|
|
'e': 1000000,
|
|
'exahash': 1000000,
|
|
'exahash/s': 1000000,
|
|
'exa': 1000000,
|
|
|
|
'th/s': 1,
|
|
't/s': 1,
|
|
't': 1,
|
|
'terahash': 1,
|
|
'terahash/s': 1,
|
|
'tera': 1,
|
|
|
|
'gh/s': 1 / 1000,
|
|
'g/s': 1 / 1000,
|
|
'g': 1 / 1000,
|
|
'gigahash': 1 / 1000,
|
|
'gigahash/s': 1 / 1000,
|
|
'giga': 1 / 1000,
|
|
|
|
'mh/s': 1 / 1000000,
|
|
'm/s': 1 / 1000000,
|
|
'm': 1 / 1000000,
|
|
'megahash': 1 / 1000000,
|
|
'megahash/s': 1 / 1000000,
|
|
'mega': 1 / 1000000,
|
|
|
|
'kh/s': 1 / 1000000000,
|
|
'k/s': 1 / 1000000000,
|
|
'k': 1 / 1000000000,
|
|
'kilohash': 1 / 1000000000,
|
|
'kilohash/s': 1 / 1000000000,
|
|
'kilo': 1 / 1000000000,
|
|
|
|
'h/s': 1 / 1000000000000,
|
|
'h': 1 / 1000000000000,
|
|
'hash': 1 / 1000000000000,
|
|
'hash/s': 1 / 1000000000000
|
|
};
|
|
|
|
// Try to find the conversion factor
|
|
let conversionFactor = null;
|
|
|
|
// First try direct lookup
|
|
if (unitConversions.hasOwnProperty(unit_normalized)) {
|
|
conversionFactor = unitConversions[unit_normalized];
|
|
} else {
|
|
// If direct lookup fails, try a fuzzy match
|
|
for (const knownUnit in unitConversions) {
|
|
if (unit_normalized.includes(knownUnit) || knownUnit.includes(unit_normalized)) {
|
|
// Log the unit correction for debugging
|
|
console.log(`Fuzzy matching unit: "${unit}" → interpreted as "${knownUnit}" (conversion: ${unitConversions[knownUnit]})`);
|
|
conversionFactor = unitConversions[knownUnit];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If no conversion factor found, assume TH/s but log a warning
|
|
if (conversionFactor === null) {
|
|
console.warn(`Unrecognized hashrate unit: "${unit}", assuming TH/s. Value: ${value}`);
|
|
// Add additional info to help diagnose incorrect units
|
|
if (value > 1000) {
|
|
console.warn(`NOTE: Value ${value} is quite large for TH/s. Could it be PH/s?`);
|
|
} else if (value < 0.001) {
|
|
console.warn(`NOTE: Value ${value} is quite small for TH/s. Could it be GH/s or MH/s?`);
|
|
}
|
|
return value; // Assume TH/s
|
|
}
|
|
|
|
// Apply conversion and return normalized value
|
|
return value * conversionFactor;
|
|
}
|
|
|
|
// Helper function to format hashrate values for display
|
|
function formatHashrateForDisplay(value, unit) {
|
|
if (isNaN(value) || value === null || value === undefined) return "N/A";
|
|
|
|
// Always normalize to TH/s first if unit is provided
|
|
let normalizedValue = unit ? normalizeHashrate(value, unit) : value;
|
|
|
|
// Select appropriate unit based on magnitude
|
|
if (normalizedValue >= 1000000) { // EH/s range
|
|
return (normalizedValue / 1000000).toFixed(2) + ' EH/s';
|
|
} else if (normalizedValue >= 1000) { // PH/s range
|
|
return (normalizedValue / 1000).toFixed(2) + ' PH/s';
|
|
} else if (normalizedValue >= 1) { // TH/s range
|
|
return normalizedValue.toFixed(2) + ' TH/s';
|
|
} else if (normalizedValue >= 0.001) { // GH/s range
|
|
return (normalizedValue * 1000).toFixed(2) + ' GH/s';
|
|
} else { // MH/s range or smaller
|
|
return (normalizedValue * 1000000).toFixed(2) + ' MH/s';
|
|
}
|
|
}
|
|
|
|
// Function to calculate block finding probability based on hashrate and network hashrate
|
|
function calculateBlockProbability(yourHashrate, yourHashrateUnit, networkHashrate) {
|
|
// First normalize both hashrates to the same unit (TH/s)
|
|
const normalizedYourHashrate = normalizeHashrate(yourHashrate, yourHashrateUnit);
|
|
|
|
// Network hashrate is in EH/s, convert to TH/s (1 EH/s = 1,000,000 TH/s)
|
|
const networkHashrateInTH = networkHashrate * 1000000;
|
|
|
|
// Calculate probability as your_hashrate / network_hashrate
|
|
const probability = normalizedYourHashrate / networkHashrateInTH;
|
|
|
|
// Format the probability for display
|
|
return formatProbability(probability);
|
|
}
|
|
|
|
// Format probability for display
|
|
function formatProbability(probability) {
|
|
// Format as 1 in X chance (more intuitive for small probabilities)
|
|
if (probability > 0) {
|
|
const oneInX = Math.round(1 / probability);
|
|
return `1 : ${numberWithCommas(oneInX)}`;
|
|
} else {
|
|
return "N/A";
|
|
}
|
|
}
|
|
|
|
// Calculate theoretical time to find a block based on hashrate
|
|
function calculateBlockTime(yourHashrate, yourHashrateUnit, networkHashrate) {
|
|
// First normalize both hashrates to the same unit (TH/s)
|
|
const normalizedYourHashrate = normalizeHashrate(yourHashrate, yourHashrateUnit);
|
|
|
|
// Make sure network hashrate is a valid number
|
|
if (typeof networkHashrate !== 'number' || isNaN(networkHashrate) || networkHashrate <= 0) {
|
|
console.error("Invalid network hashrate:", networkHashrate);
|
|
return "N/A";
|
|
}
|
|
|
|
// Network hashrate is in EH/s, convert to TH/s (1 EH/s = 1,000,000 TH/s)
|
|
const networkHashrateInTH = networkHashrate * 1000000;
|
|
|
|
// Calculate the probability of finding a block per hash attempt
|
|
const probability = normalizedYourHashrate / networkHashrateInTH;
|
|
|
|
// Bitcoin produces a block every 10 minutes (600 seconds) on average
|
|
const secondsToFindBlock = 600 / probability;
|
|
|
|
// Log the calculation for debugging
|
|
console.log(`Block time calculation using network hashrate: ${networkHashrate} EH/s`);
|
|
console.log(`Your hashrate: ${yourHashrate} ${yourHashrateUnit} (normalized: ${normalizedYourHashrate} TH/s)`);
|
|
console.log(`Probability: ${normalizedYourHashrate} / (${networkHashrate} * 1,000,000) = ${probability}`);
|
|
console.log(`Time to find block: 600 seconds / ${probability} = ${secondsToFindBlock} seconds`);
|
|
console.log(`Estimated time: ${secondsToFindBlock / 86400} days (${secondsToFindBlock / 86400 / 365.25} years)`);
|
|
|
|
return formatTimeRemaining(secondsToFindBlock);
|
|
}
|
|
|
|
// Format time in seconds to a readable format (similar to est_time_to_payout)
|
|
function formatTimeRemaining(seconds) {
|
|
if (!seconds || seconds <= 0 || !isFinite(seconds)) {
|
|
return "N/A";
|
|
}
|
|
|
|
// Extremely large values (over 100 years) are not useful
|
|
if (seconds > 3153600000) { // 100 years in seconds
|
|
return "Never (statistically)";
|
|
}
|
|
|
|
const minutes = seconds / 60;
|
|
const hours = minutes / 60;
|
|
const days = hours / 24;
|
|
const months = days / 30.44; // Average month length
|
|
const years = days / 365.25; // Account for leap years
|
|
|
|
if (years >= 1) {
|
|
// For very long timeframes, show years and months
|
|
const remainingMonths = Math.floor((years - Math.floor(years)) * 12);
|
|
if (remainingMonths > 0) {
|
|
return `${Math.floor(years)} year${Math.floor(years) !== 1 ? 's' : ''}, ${remainingMonths} month${remainingMonths !== 1 ? 's' : ''}`;
|
|
}
|
|
return `${Math.floor(years)} year${Math.floor(years) !== 1 ? 's' : ''}`;
|
|
} else if (months >= 1) {
|
|
// For months, show months and days
|
|
const remainingDays = Math.floor((months - Math.floor(months)) * 30.44);
|
|
if (remainingDays > 0) {
|
|
return `${Math.floor(months)} month${Math.floor(months) !== 1 ? 's' : ''}, ${remainingDays} day${remainingDays !== 1 ? 's' : ''}`;
|
|
}
|
|
return `${Math.floor(months)} month${Math.floor(months) !== 1 ? 's' : ''}`;
|
|
} else if (days >= 1) {
|
|
// For days, show days and hours
|
|
const remainingHours = Math.floor((days - Math.floor(days)) * 24);
|
|
if (remainingHours > 0) {
|
|
return `${Math.floor(days)} day${Math.floor(days) !== 1 ? 's' : ''}, ${remainingHours} hour${remainingHours !== 1 ? 's' : ''}`;
|
|
}
|
|
return `${Math.floor(days)} day${Math.floor(days) !== 1 ? 's' : ''}`;
|
|
} else if (hours >= 1) {
|
|
// For hours, show hours and minutes
|
|
const remainingMinutes = Math.floor((hours - Math.floor(hours)) * 60);
|
|
if (remainingMinutes > 0) {
|
|
return `${Math.floor(hours)} hour${Math.floor(hours) !== 1 ? 's' : ''}, ${remainingMinutes} minute${remainingMinutes !== 1 ? 's' : ''}`;
|
|
}
|
|
return `${Math.floor(hours)} hour${Math.floor(hours) !== 1 ? 's' : ''}`;
|
|
} else {
|
|
// For minutes, just show minutes
|
|
return `${Math.ceil(minutes)} minute${Math.ceil(minutes) !== 1 ? 's' : ''}`;
|
|
}
|
|
}
|
|
|
|
// Calculate pool luck as a percentage
|
|
function calculatePoolLuck(actualSats, estimatedSats) {
|
|
if (!actualSats || !estimatedSats || estimatedSats === 0) {
|
|
return null;
|
|
}
|
|
|
|
// Calculate luck as a percentage (actual/estimated * 100)
|
|
const luck = (actualSats / estimatedSats) * 100;
|
|
return luck;
|
|
}
|
|
|
|
// Format luck percentage for display with color coding
|
|
function formatLuckPercentage(luckPercentage) {
|
|
if (luckPercentage === null) {
|
|
return "N/A";
|
|
}
|
|
|
|
const formattedLuck = luckPercentage.toFixed(1) + "%";
|
|
|
|
// Don't add classes here, just return the formatted value
|
|
// The styling will be applied separately based on the value
|
|
return formattedLuck;
|
|
}
|
|
|
|
// SSE Connection with Error Handling and Reconnection Logic
|
|
function setupEventSource() {
|
|
console.log("Setting up EventSource connection...");
|
|
|
|
if (window.eventSource) {
|
|
console.log("Closing existing EventSource connection");
|
|
window.eventSource.close();
|
|
window.eventSource = null;
|
|
}
|
|
|
|
// Always use absolute URL with origin to ensure it works from any path
|
|
const baseUrl = window.location.origin;
|
|
const streamUrl = `${baseUrl}/stream`;
|
|
|
|
// Clear any existing ping interval
|
|
if (pingInterval) {
|
|
clearInterval(pingInterval);
|
|
pingInterval = null;
|
|
}
|
|
|
|
// Clear any connection lost timeout
|
|
if (connectionLostTimeout) {
|
|
clearTimeout(connectionLostTimeout);
|
|
connectionLostTimeout = null;
|
|
}
|
|
|
|
try {
|
|
const eventSource = new EventSource(streamUrl);
|
|
|
|
eventSource.onopen = function (e) {
|
|
console.log("EventSource connection opened successfully");
|
|
connectionRetryCount = 0; // Reset retry count on successful connection
|
|
reconnectionDelay = 1000; // Reset reconnection delay
|
|
hideConnectionIssue();
|
|
|
|
// Start ping interval to detect dead connections
|
|
lastPingTime = Date.now();
|
|
pingInterval = setInterval(function () {
|
|
const now = Date.now();
|
|
if (now - lastPingTime > 60000) { // 60 seconds without data
|
|
console.warn("No data received for 60 seconds, reconnecting...");
|
|
showConnectionIssue("Connection stalled");
|
|
eventSource.close();
|
|
setupEventSource();
|
|
}
|
|
}, 30000); // Check every 30 seconds
|
|
};
|
|
|
|
eventSource.onmessage = function (e) {
|
|
lastPingTime = Date.now(); // Update ping time on any message
|
|
|
|
try {
|
|
const data = JSON.parse(e.data);
|
|
|
|
// Handle different message types
|
|
if (data.type === "ping") {
|
|
// Update connection count if available
|
|
if (data.connections !== undefined) {
|
|
console.log(`Active connections: ${data.connections}`);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (data.type === "timeout_warning") {
|
|
console.log(`Connection timeout warning: ${data.remaining}s remaining`);
|
|
// If less than 30 seconds remaining, prepare for reconnection
|
|
if (data.remaining < 30) {
|
|
console.log("Preparing for reconnection due to upcoming timeout");
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (data.type === "timeout") {
|
|
console.log("Connection timeout from server:", data.message);
|
|
eventSource.close();
|
|
// If reconnect flag is true, reconnect immediately
|
|
if (data.reconnect) {
|
|
console.log("Server requested reconnection");
|
|
setTimeout(setupEventSource, 500);
|
|
} else {
|
|
setupEventSource();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (data.error) {
|
|
console.error("Server reported error:", data.error);
|
|
showConnectionIssue(data.error);
|
|
|
|
// If retry time provided, use it, otherwise use default
|
|
const retryTime = data.retry || 5000;
|
|
setTimeout(function () {
|
|
manualRefresh();
|
|
}, retryTime);
|
|
return;
|
|
}
|
|
|
|
// Process regular data update
|
|
latestMetrics = data;
|
|
updateUI();
|
|
hideConnectionIssue();
|
|
|
|
// Notify BitcoinMinuteRefresh that we did a refresh
|
|
BitcoinMinuteRefresh.notifyRefresh();
|
|
} catch (err) {
|
|
console.error("Error processing SSE data:", err);
|
|
showConnectionIssue("Data processing error");
|
|
}
|
|
};
|
|
|
|
eventSource.onerror = function (e) {
|
|
console.error("SSE connection error", e);
|
|
showConnectionIssue("Connection lost");
|
|
|
|
eventSource.close();
|
|
|
|
// Implement exponential backoff for reconnection
|
|
connectionRetryCount++;
|
|
|
|
if (connectionRetryCount > maxRetryCount) {
|
|
console.log("Maximum retry attempts reached, switching to polling mode");
|
|
if (pingInterval) {
|
|
clearInterval(pingInterval);
|
|
pingInterval = null;
|
|
}
|
|
|
|
// Switch to regular polling
|
|
showConnectionIssue("Using polling mode");
|
|
setInterval(manualRefresh, 30000); // Poll every 30 seconds
|
|
manualRefresh(); // Do an immediate refresh
|
|
return;
|
|
}
|
|
|
|
// Exponential backoff with jitter
|
|
const jitter = Math.random() * 0.3 + 0.85; // 0.85-1.15
|
|
reconnectionDelay = Math.min(30000, reconnectionDelay * 1.5 * jitter);
|
|
|
|
console.log(`Reconnecting in ${(reconnectionDelay / 1000).toFixed(1)} seconds... (attempt ${connectionRetryCount}/${maxRetryCount})`);
|
|
setTimeout(setupEventSource, reconnectionDelay);
|
|
};
|
|
|
|
window.eventSource = eventSource;
|
|
|
|
// Set a timeout to detect if connection is established
|
|
connectionLostTimeout = setTimeout(function () {
|
|
if (eventSource.readyState !== 1) { // 1 = OPEN
|
|
console.warn("Connection not established within timeout, switching to manual refresh");
|
|
showConnectionIssue("Connection timeout");
|
|
eventSource.close();
|
|
manualRefresh();
|
|
}
|
|
}, 30000); // 30 seconds timeout to establish connection
|
|
|
|
} catch (error) {
|
|
console.error("Failed to create EventSource:", error);
|
|
showConnectionIssue("Connection setup failed");
|
|
setTimeout(setupEventSource, 5000); // Try again in 5 seconds
|
|
}
|
|
|
|
// Add page visibility change listener
|
|
// This helps reconnect when user returns to the tab after it's been inactive
|
|
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
}
|
|
|
|
// Handle page visibility changes
|
|
function handleVisibilityChange() {
|
|
if (!document.hidden) {
|
|
console.log("Page became visible, checking connection");
|
|
if (!window.eventSource || window.eventSource.readyState !== 1) {
|
|
console.log("Connection not active, reestablishing");
|
|
setupEventSource();
|
|
}
|
|
manualRefresh(); // Always refresh data when page becomes visible
|
|
}
|
|
}
|
|
|
|
// Helper function to show connection issues to the user
|
|
function showConnectionIssue(message) {
|
|
const theme = getCurrentTheme();
|
|
let $connectionStatus = $("#connectionStatus");
|
|
if (!$connectionStatus.length) {
|
|
$("body").append(`<div id="connectionStatus" style="position: fixed; top: 10px; right: 10px; background: rgba(255,0,0,0.7); color: white; padding: 10px; border-radius: 5px; z-index: 9999;"></div>`);
|
|
$connectionStatus = $("#connectionStatus");
|
|
}
|
|
$connectionStatus.html(`<i class="fas fa-exclamation-triangle"></i> ${message}`).show();
|
|
|
|
// Show manual refresh button with theme color
|
|
$("#refreshButton").css('background-color', theme.PRIMARY).show();
|
|
}
|
|
|
|
// Helper function to hide connection issue message
|
|
function hideConnectionIssue() {
|
|
$("#connectionStatus").hide();
|
|
$("#refreshButton").hide();
|
|
}
|
|
|
|
// Improved manual refresh function as fallback
|
|
function manualRefresh() {
|
|
console.log("Manually refreshing data...");
|
|
|
|
// Prepare arrow indicators for a new refresh cycle
|
|
arrowIndicator.prepareForRefresh();
|
|
|
|
$.ajax({
|
|
url: '/api/metrics',
|
|
method: 'GET',
|
|
dataType: 'json',
|
|
timeout: 15000, // 15 second timeout
|
|
success: function (data) {
|
|
console.log("Manual refresh successful");
|
|
lastPingTime = Date.now();
|
|
latestMetrics = data;
|
|
|
|
updateUI();
|
|
hideConnectionIssue();
|
|
|
|
// Notify BitcoinMinuteRefresh that we've refreshed the data
|
|
BitcoinMinuteRefresh.notifyRefresh();
|
|
},
|
|
error: function (xhr, status, error) {
|
|
console.error("Manual refresh failed:", error);
|
|
showConnectionIssue("Manual refresh failed");
|
|
|
|
// Try again with exponential backoff
|
|
const retryDelay = Math.min(30000, 1000 * Math.pow(1.5, Math.min(5, connectionRetryCount)));
|
|
connectionRetryCount++;
|
|
setTimeout(manualRefresh, retryDelay);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Modify the initializeChart function to use blue colors for the chart
|
|
function initializeChart() {
|
|
try {
|
|
const ctx = document.getElementById('trendGraph').getContext('2d');
|
|
if (!ctx) {
|
|
console.error("Could not find trend graph canvas");
|
|
return null;
|
|
}
|
|
|
|
if (!window.Chart) {
|
|
console.error("Chart.js not loaded");
|
|
return null;
|
|
}
|
|
|
|
// Get the current theme colors
|
|
const theme = getCurrentTheme();
|
|
|
|
// Check if Chart.js plugin is available
|
|
const hasAnnotationPlugin = window['chartjs-plugin-annotation'] !== undefined;
|
|
|
|
return new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: [],
|
|
datasets: [{
|
|
label: 'HASHRATE TREND (TH/s)',
|
|
data: [],
|
|
borderWidth: 2,
|
|
borderColor: function (context) {
|
|
const chart = context.chart;
|
|
const { ctx, chartArea } = chart;
|
|
if (!chartArea) {
|
|
return theme.PRIMARY;
|
|
}
|
|
// Create gradient for line
|
|
const gradient = ctx.createLinearGradient(0, 0, 0, chartArea.bottom);
|
|
gradient.addColorStop(0, theme.CHART.GRADIENT_START);
|
|
gradient.addColorStop(1, theme.CHART.GRADIENT_END);
|
|
return gradient;
|
|
},
|
|
backgroundColor: function (context) {
|
|
const chart = context.chart;
|
|
const { ctx, chartArea } = chart;
|
|
if (!chartArea) {
|
|
return `rgba(${theme.PRIMARY_RGB}, 0.1)`;
|
|
}
|
|
// Create gradient for fill
|
|
const gradient = ctx.createLinearGradient(0, 0, 0, chartArea.bottom);
|
|
gradient.addColorStop(0, `rgba(${theme.PRIMARY_RGB}, 0.3)`);
|
|
gradient.addColorStop(0.5, `rgba(${theme.PRIMARY_RGB}, 0.2)`);
|
|
gradient.addColorStop(1, `rgba(${theme.PRIMARY_RGB}, 0.05)`);
|
|
return gradient;
|
|
},
|
|
fill: true,
|
|
tension: 0.3,
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
animation: {
|
|
duration: 0 // Disable animations for better performance
|
|
},
|
|
scales: {
|
|
x: {
|
|
display: true,
|
|
ticks: {
|
|
maxTicksLimit: 8, // Limit number of x-axis labels
|
|
maxRotation: 0, // Don't rotate labels
|
|
autoSkip: true, // Automatically skip some labels
|
|
color: '#FFFFFF',
|
|
font: {
|
|
family: "'VT323', monospace", // Terminal font
|
|
size: 14
|
|
}
|
|
},
|
|
grid: {
|
|
color: '#333333',
|
|
lineWidth: 0.5
|
|
}
|
|
},
|
|
y: {
|
|
title: {
|
|
display: true,
|
|
text: 'HASHRATE (TH/S)',
|
|
color: theme.PRIMARY,
|
|
font: {
|
|
family: "'VT323', monospace",
|
|
size: 16,
|
|
weight: 'bold'
|
|
}
|
|
},
|
|
ticks: {
|
|
color: '#FFFFFF',
|
|
maxTicksLimit: 6, // Limit total number of ticks
|
|
precision: 1, // Control decimal precision
|
|
autoSkip: true, // Skip labels to prevent overcrowding
|
|
autoSkipPadding: 10, // Padding between skipped labels
|
|
font: {
|
|
family: "'VT323', monospace", // Terminal font
|
|
size: 14
|
|
},
|
|
callback: function (value) {
|
|
// For zero, just return 0
|
|
if (value === 0) return '0';
|
|
|
|
// For large values (1000+ TH/s), show in PH/s
|
|
if (value >= 1000) {
|
|
return (value / 1000).toFixed(1) + ' PH';
|
|
}
|
|
// For values between 10 and 1000 TH/s
|
|
else if (value >= 10) {
|
|
return Math.round(value);
|
|
}
|
|
// For small values, limit decimal places
|
|
else if (value >= 1) {
|
|
return value.toFixed(1);
|
|
}
|
|
// For tiny values, use appropriate precision
|
|
else {
|
|
return value.toPrecision(2);
|
|
}
|
|
}
|
|
},
|
|
grid: {
|
|
color: '#333333',
|
|
lineWidth: 0.5,
|
|
drawBorder: false,
|
|
zeroLineColor: '#555555',
|
|
zeroLineWidth: 1,
|
|
drawTicks: false
|
|
}
|
|
}
|
|
},
|
|
plugins: {
|
|
tooltip: {
|
|
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
|
titleColor: theme.PRIMARY,
|
|
bodyColor: '#FFFFFF',
|
|
titleFont: {
|
|
family: "'VT323', monospace",
|
|
size: 16,
|
|
weight: 'bold'
|
|
},
|
|
bodyFont: {
|
|
family: "'VT323', monospace",
|
|
size: 14
|
|
},
|
|
padding: 10,
|
|
cornerRadius: 0,
|
|
displayColors: false,
|
|
callbacks: {
|
|
title: function (tooltipItems) {
|
|
return tooltipItems[0].label.toUpperCase();
|
|
},
|
|
label: function (context) {
|
|
// Format tooltip values with appropriate unit
|
|
const value = context.raw;
|
|
return 'HASHRATE: ' + formatHashrateForDisplay(value).toUpperCase();
|
|
}
|
|
}
|
|
},
|
|
legend: { display: false },
|
|
annotation: hasAnnotationPlugin ? {
|
|
annotations: {
|
|
averageLine: {
|
|
type: 'line',
|
|
yMin: 0,
|
|
yMax: 0,
|
|
borderColor: theme.CHART.ANNOTATION,
|
|
borderWidth: 3,
|
|
borderDash: [8, 4],
|
|
shadowColor: `rgba(${theme.PRIMARY_RGB}, 0.5)`,
|
|
shadowBlur: 8,
|
|
shadowOffsetX: 0,
|
|
shadowOffsetY: 0,
|
|
label: {
|
|
enabled: true,
|
|
content: '24HR AVG: 0 TH/S',
|
|
backgroundColor: 'rgba(0,0,0,0.8)',
|
|
color: theme.CHART.ANNOTATION,
|
|
font: {
|
|
family: "'VT323', monospace",
|
|
size: 16,
|
|
weight: 'bold'
|
|
},
|
|
padding: { top: 4, bottom: 4, left: 8, right: 8 },
|
|
borderRadius: 0,
|
|
position: 'start'
|
|
}
|
|
}
|
|
}
|
|
} : {}
|
|
}
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error("Error initializing chart:", error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Helper function to safely format numbers with commas
|
|
function numberWithCommas(x) {
|
|
if (x == null) return "N/A";
|
|
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
}
|
|
|
|
// Server time update via polling
|
|
function updateServerTime() {
|
|
$.ajax({
|
|
url: "/api/time",
|
|
method: "GET",
|
|
timeout: 5000,
|
|
success: function (data) {
|
|
// Calculate the offset between server time and local time
|
|
serverTimeOffset = new Date(data.server_timestamp).getTime() - Date.now();
|
|
serverStartTime = new Date(data.server_start_time).getTime();
|
|
|
|
// Update BitcoinMinuteRefresh with server time info
|
|
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);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Update UI indicators (arrows) - replaced with ArrowIndicator call
|
|
function updateIndicators(newMetrics) {
|
|
arrowIndicator.updateIndicators(newMetrics);
|
|
}
|
|
|
|
// Helper function to safely update element text content
|
|
function updateElementText(elementId, text) {
|
|
const element = document.getElementById(elementId);
|
|
if (element) {
|
|
element.textContent = text;
|
|
}
|
|
}
|
|
|
|
// Helper function to safely update element HTML content
|
|
function updateElementHTML(elementId, html) {
|
|
const element = document.getElementById(elementId);
|
|
if (element) {
|
|
element.innerHTML = html;
|
|
}
|
|
}
|
|
|
|
// Update workers_hashing value from metrics, but don't try to access worker details
|
|
function updateWorkersCount() {
|
|
if (latestMetrics && latestMetrics.workers_hashing !== undefined) {
|
|
$("#workers_hashing").text(latestMetrics.workers_hashing || 0);
|
|
|
|
// Update miner status with online/offline indicator based on worker count
|
|
if (latestMetrics.workers_hashing > 0) {
|
|
updateElementHTML("miner_status", "<span class='status-green'>ONLINE</span> <span class='online-dot'></span>");
|
|
} else {
|
|
updateElementHTML("miner_status", "<span class='status-red'>OFFLINE</span> <span class='offline-dot'></span>");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for block updates and show congratulatory messages
|
|
function checkForBlockUpdates(data) {
|
|
if (previousMetrics.last_block_height !== undefined &&
|
|
data.last_block_height !== previousMetrics.last_block_height) {
|
|
showCongrats("Congrats! New Block Found: " + data.last_block_height);
|
|
}
|
|
|
|
if (previousMetrics.blocks_found !== undefined &&
|
|
data.blocks_found !== previousMetrics.blocks_found) {
|
|
showCongrats("Congrats! Blocks Found updated: " + data.blocks_found);
|
|
}
|
|
}
|
|
|
|
// Helper function to show congratulatory messages with timestamps
|
|
function showCongrats(message) {
|
|
const $congrats = $("#congratsMessage");
|
|
|
|
// Add timestamp to the message
|
|
const now = new Date(Date.now() + serverTimeOffset); // Use server time offset for accuracy
|
|
const options = {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
second: '2-digit',
|
|
hour12: true
|
|
};
|
|
const timeString = now.toLocaleTimeString('en-US', options);
|
|
|
|
// Format the message with the timestamp
|
|
const messageWithTimestamp = `${message} [${timeString}]`;
|
|
|
|
// Display the message
|
|
$congrats.text(messageWithTimestamp).fadeIn(500, function () {
|
|
setTimeout(function () {
|
|
$congrats.fadeOut(500);
|
|
}, 900000); // 15 minutes fade out
|
|
});
|
|
}
|
|
|
|
// Enhanced Chart Update Function to handle temporary hashrate spikes
|
|
// Modified the updateChartWithNormalizedData function to ensure the 24hr avg line is visible in low hashrate mode
|
|
// Enhanced Chart Update Function with localStorage persistence
|
|
function updateChartWithNormalizedData(chart, data) {
|
|
if (!chart || !data) {
|
|
console.warn("Cannot update chart - chart or data is null");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Try to load lowHashrate state from localStorage first
|
|
const storedLowHashrateState = localStorage.getItem('lowHashrateState');
|
|
|
|
// Initialize mode state by combining stored state with defaults
|
|
if (!chart.lowHashrateState) {
|
|
const defaultState = {
|
|
isLowHashrateMode: false,
|
|
highHashrateSpikeTime: 0,
|
|
spikeCount: 0,
|
|
lowHashrateConfirmTime: 0,
|
|
modeSwitchTimeoutId: null,
|
|
lastModeChange: 0,
|
|
stableModePeriod: 600000
|
|
};
|
|
|
|
// If we have stored state, use it
|
|
if (storedLowHashrateState) {
|
|
try {
|
|
const parsedState = JSON.parse(storedLowHashrateState);
|
|
chart.lowHashrateState = {
|
|
...defaultState,
|
|
...parsedState,
|
|
// Reset any volatile state that shouldn't persist
|
|
highHashrateSpikeTime: parsedState.highHashrateSpikeTime || 0,
|
|
modeSwitchTimeoutId: null
|
|
};
|
|
console.log("Restored low hashrate mode from localStorage:", chart.lowHashrateState.isLowHashrateMode);
|
|
} catch (e) {
|
|
console.error("Error parsing stored low hashrate state:", e);
|
|
chart.lowHashrateState = defaultState;
|
|
}
|
|
} else {
|
|
chart.lowHashrateState = defaultState;
|
|
}
|
|
}
|
|
|
|
// Get values with enhanced stability
|
|
let useHashrate3hr = false;
|
|
const currentTime = Date.now();
|
|
const LOW_HASHRATE_THRESHOLD = 0.01; // TH/s
|
|
const HIGH_HASHRATE_THRESHOLD = 20.0; // TH/s
|
|
const MODE_SWITCH_DELAY = 120000; // Increase to 2 minutes for more stability
|
|
const CONSECUTIVE_SPIKES_THRESHOLD = 3; // Increase to require more consistent high readings
|
|
const MIN_MODE_STABILITY_TIME = 120000; // 2 minutes minimum between mode switches
|
|
|
|
// Check if we changed modes recently - enforce a minimum stability period
|
|
const timeSinceLastModeChange = currentTime - chart.lowHashrateState.lastModeChange;
|
|
const enforceStabilityPeriod = timeSinceLastModeChange < MIN_MODE_STABILITY_TIME;
|
|
|
|
// IMPORTANT: Calculate normalized hashrate values
|
|
const normalizedHashrate60sec = normalizeHashrate(data.hashrate_60sec || 0, data.hashrate_60sec_unit || 'th/s');
|
|
const normalizedHashrate3hr = normalizeHashrate(data.hashrate_3hr || 0, data.hashrate_3hr_unit || 'th/s');
|
|
const normalizedAvg = normalizeHashrate(data.hashrate_24hr || 0, data.hashrate_24hr_unit || 'th/s');
|
|
|
|
// First check if we should use 3hr data based on the stored state
|
|
useHashrate3hr = chart.lowHashrateState.isLowHashrateMode;
|
|
|
|
// Case 1: Currently in low hashrate mode
|
|
if (chart.lowHashrateState.isLowHashrateMode) {
|
|
// Default to staying in low hashrate mode
|
|
useHashrate3hr = true;
|
|
|
|
// If we're enforcing stability, don't even check for mode change
|
|
if (!enforceStabilityPeriod && normalizedHashrate60sec >= HIGH_HASHRATE_THRESHOLD) {
|
|
// Only track spikes if we aren't in stability enforcement period
|
|
if (!chart.lowHashrateState.highHashrateSpikeTime) {
|
|
chart.lowHashrateState.highHashrateSpikeTime = currentTime;
|
|
console.log("High hashrate spike detected in low hashrate mode");
|
|
}
|
|
|
|
// Increment spike counter
|
|
chart.lowHashrateState.spikeCount++;
|
|
console.log(`Spike count: ${chart.lowHashrateState.spikeCount}/${CONSECUTIVE_SPIKES_THRESHOLD}`);
|
|
|
|
// Check if spikes have persisted long enough
|
|
const spikeElapsedTime = currentTime - chart.lowHashrateState.highHashrateSpikeTime;
|
|
|
|
if (chart.lowHashrateState.spikeCount >= CONSECUTIVE_SPIKES_THRESHOLD &&
|
|
spikeElapsedTime > MODE_SWITCH_DELAY) {
|
|
useHashrate3hr = false;
|
|
chart.lowHashrateState.isLowHashrateMode = false;
|
|
chart.lowHashrateState.highHashrateSpikeTime = 0;
|
|
chart.lowHashrateState.spikeCount = 0;
|
|
chart.lowHashrateState.lastModeChange = currentTime;
|
|
console.log("Exiting low hashrate mode after sustained high hashrate");
|
|
|
|
// Save state changes to localStorage
|
|
saveLowHashrateState(chart.lowHashrateState);
|
|
} else {
|
|
console.log(`Remaining in low hashrate mode despite spike (waiting: ${Math.round(spikeElapsedTime / 1000)}/${MODE_SWITCH_DELAY / 1000}s, count: ${chart.lowHashrateState.spikeCount}/${CONSECUTIVE_SPIKES_THRESHOLD})`);
|
|
}
|
|
} else {
|
|
// Don't reset counters immediately on every drop - make the counter more persistent
|
|
if (chart.lowHashrateState.spikeCount > 0 && normalizedHashrate60sec < HIGH_HASHRATE_THRESHOLD) {
|
|
// Don't reset immediately, use a gradual decay approach
|
|
if (Math.random() < 0.2) { // 20% chance to decrement counter each update
|
|
chart.lowHashrateState.spikeCount--;
|
|
console.log("Spike counter decayed to:", chart.lowHashrateState.spikeCount);
|
|
|
|
// Save state changes to localStorage
|
|
saveLowHashrateState(chart.lowHashrateState);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Case 2: Currently in normal mode
|
|
else {
|
|
// Default to staying in normal mode
|
|
useHashrate3hr = false;
|
|
|
|
// Don't switch to low hashrate mode immediately if we recently switched modes
|
|
if (!enforceStabilityPeriod && normalizedHashrate60sec < LOW_HASHRATE_THRESHOLD && normalizedHashrate3hr > LOW_HASHRATE_THRESHOLD) {
|
|
// Record when low hashrate condition was first observed
|
|
if (!chart.lowHashrateState.lowHashrateConfirmTime) {
|
|
chart.lowHashrateState.lowHashrateConfirmTime = currentTime;
|
|
console.log("Low hashrate condition detected");
|
|
|
|
// Save state changes to localStorage
|
|
saveLowHashrateState(chart.lowHashrateState);
|
|
}
|
|
|
|
// Require at least 60 seconds of low hashrate before switching modes
|
|
const lowHashrateTime = currentTime - chart.lowHashrateState.lowHashrateConfirmTime;
|
|
if (lowHashrateTime > 60000) { // 1 minute
|
|
useHashrate3hr = true;
|
|
chart.lowHashrateState.isLowHashrateMode = true;
|
|
chart.lowHashrateState.lastModeChange = currentTime;
|
|
console.log("Entering low hashrate mode after persistent low hashrate condition");
|
|
|
|
// Save state changes to localStorage
|
|
saveLowHashrateState(chart.lowHashrateState);
|
|
} else {
|
|
console.log(`Low hashrate detected but waiting for persistence: ${Math.round(lowHashrateTime / 1000)}/60s`);
|
|
}
|
|
} else {
|
|
// Only reset the confirmation timer if we've been above threshold consistently
|
|
if (chart.lowHashrateState.lowHashrateConfirmTime &&
|
|
currentTime - chart.lowHashrateState.lowHashrateConfirmTime > 30000) { // 30 seconds above threshold
|
|
chart.lowHashrateState.lowHashrateConfirmTime = 0;
|
|
console.log("Low hashrate condition cleared after consistent normal hashrate");
|
|
|
|
// Save state changes to localStorage
|
|
saveLowHashrateState(chart.lowHashrateState);
|
|
} else if (chart.lowHashrateState.lowHashrateConfirmTime) {
|
|
console.log("Brief hashrate spike, maintaining low hashrate detection timer");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Helper function to save lowHashrateState to localStorage
|
|
function saveLowHashrateState(state) {
|
|
try {
|
|
// Create a clean copy without circular references or functions
|
|
const stateToSave = {
|
|
isLowHashrateMode: state.isLowHashrateMode,
|
|
highHashrateSpikeTime: state.highHashrateSpikeTime,
|
|
spikeCount: state.spikeCount,
|
|
lowHashrateConfirmTime: state.lowHashrateConfirmTime,
|
|
lastModeChange: state.lastModeChange,
|
|
stableModePeriod: state.stableModePeriod
|
|
};
|
|
localStorage.setItem('lowHashrateState', JSON.stringify(stateToSave));
|
|
console.log("Saved low hashrate state:", state.isLowHashrateMode);
|
|
} catch (e) {
|
|
console.error("Error saving low hashrate state to localStorage:", e);
|
|
}
|
|
}
|
|
|
|
// Process history data with enhanced validation and error handling
|
|
if (data.arrow_history && data.arrow_history.hashrate_60sec) {
|
|
// Validate history data
|
|
try {
|
|
// Log 60sec data
|
|
console.log("60sec history data received:", data.arrow_history.hashrate_60sec);
|
|
|
|
// Also log 3hr data if available
|
|
if (data.arrow_history.hashrate_3hr) {
|
|
console.log("3hr history data received:", data.arrow_history.hashrate_3hr);
|
|
} else {
|
|
console.log("3hr history data not available in API response");
|
|
}
|
|
|
|
// If we're using 3hr average, try to use that history if available
|
|
const historyData = useHashrate3hr && data.arrow_history.hashrate_3hr ?
|
|
data.arrow_history.hashrate_3hr : data.arrow_history.hashrate_60sec;
|
|
|
|
console.log("Using history data:", useHashrate3hr ? "3hr data" : "60sec data");
|
|
|
|
if (historyData && historyData.length > 0) {
|
|
// Format time labels
|
|
chart.data.labels = historyData.map(item => {
|
|
const timeStr = item.time;
|
|
try {
|
|
// Parse and format the time (existing code)...
|
|
let timeParts;
|
|
if (timeStr.length === 8 && timeStr.indexOf(':') !== -1) {
|
|
// Format: HH:MM:SS
|
|
timeParts = timeStr.split(':');
|
|
} else if (timeStr.length === 5 && timeStr.indexOf(':') !== -1) {
|
|
// Format: HH:MM
|
|
timeParts = timeStr.split(':');
|
|
timeParts.push('00'); // Add seconds
|
|
} else {
|
|
return timeStr; // Use as-is if format is unexpected
|
|
}
|
|
|
|
// Format in 12-hour time with timezone support
|
|
const now = new Date();
|
|
const timeDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(),
|
|
parseInt(timeParts[0]), parseInt(timeParts[1]), parseInt(timeParts[2] || 0));
|
|
|
|
let formattedTime = timeDate.toLocaleTimeString('en-US', {
|
|
timeZone: dashboardTimezone || 'America/Los_Angeles',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
hour12: true
|
|
});
|
|
|
|
// Remove the AM/PM part
|
|
formattedTime = formattedTime.replace(/\s[AP]M$/i, '');
|
|
return formattedTime;
|
|
} catch (e) {
|
|
console.error("Error formatting time:", e, timeStr);
|
|
return timeStr; // Use original on error
|
|
}
|
|
});
|
|
|
|
// Process and normalize hashrate values with validation
|
|
const hashrateValues = [];
|
|
const validatedData = historyData.map((item, index) => {
|
|
try {
|
|
// Safety conversion in case value is a string
|
|
const val = parseFloat(item.value || 0);
|
|
if (isNaN(val)) {
|
|
console.warn(`Invalid value at index ${index}: ${item.value}`);
|
|
return 0;
|
|
}
|
|
|
|
// Validate the unit
|
|
const unit = item.unit || 'th/s';
|
|
const normalizedValue = normalizeHashrate(val, unit);
|
|
|
|
// Collect valid values for statistics
|
|
if (normalizedValue > 0) {
|
|
hashrateValues.push(normalizedValue);
|
|
}
|
|
|
|
return normalizedValue;
|
|
} catch (err) {
|
|
console.error(`Error processing hashrate at index ${index}:`, err);
|
|
return 0;
|
|
}
|
|
});
|
|
|
|
chart.data.datasets[0].data = validatedData;
|
|
|
|
// Calculate statistics for anomaly detection
|
|
if (hashrateValues.length > 1) {
|
|
const mean = hashrateValues.reduce((sum, val) => sum + val, 0) / hashrateValues.length;
|
|
const max = Math.max(...hashrateValues);
|
|
const min = Math.min(...hashrateValues);
|
|
|
|
// Check for outliers that might indicate incorrect units
|
|
if (max > mean * 10 || min < mean / 10) {
|
|
console.warn("WARNING: Wide hashrate variance detected in chart data. Possible unit inconsistency.");
|
|
console.warn(`Min: ${min.toFixed(2)} TH/s, Max: ${max.toFixed(2)} TH/s, Mean: ${mean.toFixed(2)} TH/s`);
|
|
}
|
|
}
|
|
|
|
// Update chart dataset label to indicate which average we're displaying
|
|
chart.data.datasets[0].label = useHashrate3hr ?
|
|
'Hashrate Trend (3HR AVG)' : 'Hashrate Trend (60SEC AVG)';
|
|
|
|
// Calculate appropriate y-axis range with safeguards for outliers and ensure 24hr avg line is visible
|
|
const values = chart.data.datasets[0].data.filter(v => !isNaN(v) && v !== null && v > 0);
|
|
if (values.length > 0) {
|
|
const max = Math.max(...values);
|
|
const min = Math.min(...values) || 0;
|
|
|
|
// MODIFICATION: When in low hashrate mode, ensure the y-axis includes the 24hr average
|
|
if (useHashrate3hr && normalizedAvg > 0) {
|
|
// Ensure the 24-hour average is visible on the chart
|
|
const yMin = Math.min(min * 0.8, normalizedAvg * 0.5);
|
|
const yMax = Math.max(max * 1.2, normalizedAvg * 1.5);
|
|
|
|
chart.options.scales.y.min = yMin;
|
|
chart.options.scales.y.max = yMax;
|
|
console.log(`Low hashrate mode: Adjusting y-axis to include 24hr avg: [${yMin.toFixed(2)}, ${yMax.toFixed(2)}]`);
|
|
} else {
|
|
// Normal mode scaling
|
|
chart.options.scales.y.min = min * 0.8;
|
|
chart.options.scales.y.max = max * 1.2;
|
|
}
|
|
|
|
// Set appropriate step size based on range
|
|
const range = chart.options.scales.y.max - chart.options.scales.y.min;
|
|
|
|
// Calculate an appropriate stepSize that won't exceed Chart.js tick limits
|
|
// Aim for approximately 5-10 ticks (Chart.js recommends ~5-6 ticks for readability)
|
|
let stepSize;
|
|
const targetTicks = 6; // Target number of ticks we want to display
|
|
|
|
if (range <= 0.1) {
|
|
// For very small ranges (< 0.1 TH/s)
|
|
stepSize = 0.01;
|
|
} else if (range <= 1) {
|
|
// For small ranges (0.1 - 1 TH/s)
|
|
stepSize = 0.1;
|
|
} else if (range <= 10) {
|
|
// For medium ranges (1 - 10 TH/s)
|
|
stepSize = 1;
|
|
} else if (range <= 50) {
|
|
stepSize = 5;
|
|
} else if (range <= 100) {
|
|
stepSize = 10;
|
|
} else if (range <= 500) {
|
|
stepSize = 50;
|
|
} else if (range <= 1000) {
|
|
stepSize = 100;
|
|
} else if (range <= 5000) {
|
|
stepSize = 500;
|
|
} else {
|
|
// For very large ranges, calculate stepSize that will produce ~targetTicks ticks
|
|
stepSize = Math.ceil(range / targetTicks);
|
|
|
|
// Round to a nice number (nearest power of 10 multiple)
|
|
const magnitude = Math.pow(10, Math.floor(Math.log10(stepSize)));
|
|
stepSize = Math.ceil(stepSize / magnitude) * magnitude;
|
|
|
|
// Safety check for extremely large ranges
|
|
if (range / stepSize > 1000) {
|
|
console.warn(`Y-axis range (${range.toFixed(2)}) requires extremely large stepSize.
|
|
Adjusting to limit ticks to 1000.`);
|
|
stepSize = Math.ceil(range / 1000);
|
|
}
|
|
}
|
|
|
|
// Set the calculated stepSize
|
|
chart.options.scales.y.ticks.stepSize = stepSize;
|
|
|
|
// Log the chosen stepSize for debugging
|
|
console.log(`Y-axis range: ${range.toFixed(2)}, using stepSize: ${stepSize}`);
|
|
}
|
|
} else {
|
|
console.warn("No valid history data items available");
|
|
}
|
|
} catch (historyError) {
|
|
console.error("Error processing hashrate history data:", historyError);
|
|
// Fall back to single datapoint if history processing fails
|
|
useSingleDataPoint();
|
|
}
|
|
} else {
|
|
// No history data, use single datapoint
|
|
useSingleDataPoint();
|
|
}
|
|
|
|
// Handle single datapoint display when no history is available
|
|
function useSingleDataPoint() {
|
|
try {
|
|
// Format current time
|
|
const now = new Date();
|
|
let currentTime;
|
|
try {
|
|
currentTime = now.toLocaleTimeString('en-US', {
|
|
timeZone: dashboardTimezone || 'America/Los_Angeles',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
hour12: true
|
|
}).replace(/\s[AP]M$/i, '');
|
|
} catch (e) {
|
|
console.error("Error formatting current time:", e);
|
|
currentTime = now.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit' });
|
|
}
|
|
|
|
// Choose which current hashrate to display with validation
|
|
let currentValue, currentUnit, normalizedValue;
|
|
|
|
if (useHashrate3hr) {
|
|
currentValue = parseFloat(data.hashrate_3hr || 0);
|
|
currentUnit = data.hashrate_3hr_unit || 'th/s';
|
|
chart.data.datasets[0].label = 'Hashrate Trend (3HR AVG)';
|
|
} else {
|
|
currentValue = parseFloat(data.hashrate_60sec || 0);
|
|
currentUnit = data.hashrate_60sec_unit || 'th/s';
|
|
chart.data.datasets[0].label = 'Hashrate Trend (60SEC AVG)';
|
|
}
|
|
|
|
// Guard against invalid values
|
|
if (isNaN(currentValue)) {
|
|
console.warn("Invalid hashrate value, using 0");
|
|
normalizedValue = 0;
|
|
} else {
|
|
normalizedValue = normalizeHashrate(currentValue, currentUnit);
|
|
}
|
|
|
|
chart.data.labels = [currentTime];
|
|
chart.data.datasets[0].data = [normalizedValue];
|
|
|
|
// MODIFICATION: For single datapoint in low hashrate mode, ensure 24hr avg is visible
|
|
if (useHashrate3hr && normalizedAvg > 0) {
|
|
const yMin = Math.min(normalizedValue * 0.8, normalizedAvg * 0.5);
|
|
const yMax = Math.max(normalizedValue * 1.2, normalizedAvg * 1.5);
|
|
|
|
chart.options.scales.y.min = yMin;
|
|
chart.options.scales.y.max = yMax;
|
|
console.log(`Low hashrate mode (single point): Adjusting y-axis to include 24hr avg: [${yMin.toFixed(2)}, ${yMax.toFixed(2)}]`);
|
|
}
|
|
} catch (err) {
|
|
console.error("Error setting up single datapoint:", err);
|
|
chart.data.labels = ["Now"];
|
|
chart.data.datasets[0].data = [0];
|
|
}
|
|
}
|
|
|
|
// Show low hashrate indicator as needed
|
|
if (useHashrate3hr) {
|
|
// Add indicator text to the chart
|
|
if (!chart.lowHashrateIndicator) {
|
|
// Create the indicator element if it doesn't exist
|
|
const graphContainer = document.getElementById('graphContainer');
|
|
if (graphContainer) {
|
|
const theme = getCurrentTheme();
|
|
const indicator = document.createElement('div');
|
|
indicator.id = 'lowHashrateIndicator';
|
|
indicator.style.position = 'absolute';
|
|
indicator.style.top = '10px';
|
|
indicator.style.right = '10px';
|
|
indicator.style.background = 'rgba(0,0,0,0.7)';
|
|
indicator.style.color = theme.PRIMARY;
|
|
indicator.style.padding = '5px 10px';
|
|
indicator.style.borderRadius = '3px';
|
|
indicator.style.fontSize = '12px';
|
|
indicator.style.zIndex = '10';
|
|
indicator.style.fontWeight = 'bold';
|
|
indicator.textContent = 'LOW HASHRATE MODE: SHOWING 3HR AVG';
|
|
graphContainer.appendChild(indicator);
|
|
chart.lowHashrateIndicator = indicator;
|
|
}
|
|
} else {
|
|
chart.lowHashrateIndicator.style.color = getCurrentTheme().PRIMARY;
|
|
chart.lowHashrateIndicator.style.display = 'block';
|
|
}
|
|
} else if (chart.lowHashrateIndicator) {
|
|
chart.lowHashrateIndicator.style.display = 'none';
|
|
}
|
|
|
|
// UPDATE THE 24HR AVERAGE LINE ANNOTATION - THIS WAS MISSING
|
|
if (chart.options && chart.options.plugins && chart.options.plugins.annotation &&
|
|
chart.options.plugins.annotation.annotations && chart.options.plugins.annotation.annotations.averageLine) {
|
|
|
|
// Get current theme for styling
|
|
const theme = getCurrentTheme();
|
|
|
|
// Update the position of the average line to match the 24hr hashrate
|
|
chart.options.plugins.annotation.annotations.averageLine.yMin = normalizedAvg;
|
|
chart.options.plugins.annotation.annotations.averageLine.yMax = normalizedAvg;
|
|
|
|
// Update the annotation label
|
|
const formattedAvg = formatHashrateForDisplay(data.hashrate_24hr, data.hashrate_24hr_unit);
|
|
chart.options.plugins.annotation.annotations.averageLine.label.content =
|
|
`24HR AVG: ${formattedAvg}`;
|
|
|
|
// Set the color based on current theme
|
|
chart.options.plugins.annotation.annotations.averageLine.borderColor = theme.CHART.ANNOTATION;
|
|
chart.options.plugins.annotation.annotations.averageLine.label.color = theme.CHART.ANNOTATION;
|
|
|
|
console.log(`Updated 24hr average line: ${normalizedAvg.toFixed(2)} TH/s`);
|
|
} else {
|
|
console.warn("Chart annotation plugin not properly configured");
|
|
}
|
|
|
|
// Finally update the chart with a safe non-animating update
|
|
chart.update('none');
|
|
} catch (chartError) {
|
|
console.error("Error updating chart:", chartError);
|
|
}
|
|
}
|
|
|
|
// Modify the pool fee calculation to use actual last block earnings
|
|
function calculatePoolFeeInSats(poolFeePercentage, lastBlockEarnings) {
|
|
if (poolFeePercentage === undefined || poolFeePercentage === null ||
|
|
lastBlockEarnings === undefined || lastBlockEarnings === null) {
|
|
return null;
|
|
}
|
|
|
|
// Log the raw values for debugging
|
|
console.log("Pool Fee %:", poolFeePercentage, "Last Block Earnings:", lastBlockEarnings);
|
|
|
|
// Calculate how many SATS were taken as fees from the last block
|
|
// Pool fee is a percentage, so we divide by 100 to get the actual rate
|
|
const feeAmount = (poolFeePercentage / 100) * lastBlockEarnings;
|
|
|
|
// Return as a negative number since it represents a cost
|
|
return -Math.round(feeAmount);
|
|
}
|
|
|
|
// Main UI update function with currency support
|
|
function updateUI() {
|
|
function ensureElementStyles() {
|
|
// Create a style element if it doesn't exist
|
|
if (!document.getElementById('customMetricStyles')) {
|
|
const styleEl = document.createElement('style');
|
|
styleEl.id = 'customMetricStyles';
|
|
styleEl.textContent = `
|
|
/* Ensure rows have consistent layout */
|
|
.card-body p {
|
|
position: relative;
|
|
display: grid;
|
|
grid-template-columns: auto auto 1fr;
|
|
align-items: center;
|
|
margin: 0.25rem 0;
|
|
line-height: 1.2;
|
|
gap: 0.25rem;
|
|
}
|
|
|
|
/* Label style */
|
|
.card-body strong {
|
|
grid-column: 1;
|
|
}
|
|
|
|
/* Main metric container */
|
|
.main-metric {
|
|
grid-column: 2;
|
|
display: flex;
|
|
align-items: center;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* All dividers */
|
|
.metric-divider-container {
|
|
grid-column: 3;
|
|
justify-self: end;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.metric-divider {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
margin-left: 1rem;
|
|
padding-left: 0.75rem;
|
|
height: 1.5em;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.metric-divider-value {
|
|
font-size: 0.85em;
|
|
font-weight: normal;
|
|
margin-right: 0.5rem;
|
|
}
|
|
|
|
.metric-divider-note {
|
|
font-size: 0.75em;
|
|
opacity: 0.8;
|
|
color: white;
|
|
font-weight: normal;
|
|
}
|
|
|
|
span[id^="indicator_"] {
|
|
margin-left: 0.25rem;
|
|
width: 1rem;
|
|
display: inline-flex;
|
|
}
|
|
`;
|
|
document.head.appendChild(styleEl);
|
|
}
|
|
}
|
|
|
|
// Helper function to create dividers with consistent horizontal alignment
|
|
function createDivider(valueId, valueText, labelText, valueClass = "yellow") {
|
|
const dividerContainer = document.createElement("span");
|
|
dividerContainer.className = "metric-divider";
|
|
|
|
// Value element
|
|
const valueSpan = document.createElement("span");
|
|
valueSpan.id = valueId;
|
|
valueSpan.className = `metric-value metric-divider-value ${valueClass}`;
|
|
valueSpan.textContent = valueText;
|
|
dividerContainer.appendChild(valueSpan);
|
|
|
|
// Label element
|
|
const labelSpan = document.createElement("span");
|
|
labelSpan.className = "metric-divider-note";
|
|
labelSpan.textContent = labelText;
|
|
dividerContainer.appendChild(labelSpan);
|
|
|
|
return dividerContainer;
|
|
}
|
|
|
|
if (!latestMetrics) {
|
|
console.warn("No metrics data available");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const data = latestMetrics;
|
|
|
|
// Get currency and exchange rate information
|
|
const currency = data.currency || 'USD';
|
|
const exchangeRate = data.exchange_rates && data.exchange_rates[currency] ?
|
|
data.exchange_rates[currency] : 1.0;
|
|
|
|
// Update currency-related labels
|
|
const earningsHeader = document.querySelector('.card-header');
|
|
if (earningsHeader && earningsHeader.textContent.includes("EARNINGS")) {
|
|
// Find the card header that says "USD EARNINGS" and update it
|
|
const headers = document.querySelectorAll('.card-header');
|
|
headers.forEach(header => {
|
|
if (header.textContent.includes("EARNINGS") &&
|
|
header.textContent.match(/[A-Z]{3} EARNINGS/)) {
|
|
header.textContent = `${currency} EARNINGS`;
|
|
}
|
|
});
|
|
}
|
|
|
|
// If this is the initial load, force a reset of all arrows
|
|
if (initialLoad) {
|
|
arrowIndicator.forceApplyArrows();
|
|
initialLoad = false;
|
|
}
|
|
|
|
// Format each hashrate with proper normalization
|
|
// Pool Hashrate
|
|
let formattedPoolHashrate = "N/A";
|
|
if (data.pool_total_hashrate != null) {
|
|
formattedPoolHashrate = formatHashrateForDisplay(
|
|
data.pool_total_hashrate,
|
|
data.pool_total_hashrate_unit || 'th/s'
|
|
);
|
|
}
|
|
updateElementText("pool_total_hashrate", formattedPoolHashrate);
|
|
|
|
// Add pool luck calculation right after pool_total_hashrate
|
|
if (data.daily_mined_sats && data.estimated_earnings_per_day_sats) {
|
|
const poolLuck = calculatePoolLuck(
|
|
parseFloat(data.daily_mined_sats),
|
|
parseFloat(data.estimated_earnings_per_day_sats)
|
|
);
|
|
|
|
// Add pool_luck to the metrics data for arrow indicators
|
|
if (poolLuck !== null) {
|
|
data.pool_luck = poolLuck;
|
|
}
|
|
|
|
const poolLuckValue = poolLuck !== null ? formatLuckPercentage(poolLuck) : "N/A";
|
|
|
|
// Get the pool_total_hashrate element's parent paragraph
|
|
const poolHashratePara = document.getElementById("pool_total_hashrate").parentNode;
|
|
|
|
// Ensure grid layout and structure
|
|
ensureElementStyles();
|
|
|
|
// Structure parent for proper grid layout (similar to the other metrics)
|
|
if (!poolHashratePara.querySelector('.main-metric')) {
|
|
const poolHashrate = document.getElementById("pool_total_hashrate");
|
|
const indicatorPoolHashrate = document.getElementById("indicator_pool_total_hashrate");
|
|
|
|
// Create the main metric container
|
|
const mainMetric = document.createElement("span");
|
|
mainMetric.className = "main-metric";
|
|
|
|
// Move the metric and its indicator inside the container
|
|
if (poolHashrate && indicatorPoolHashrate) {
|
|
// Clear any existing text nodes between the elements
|
|
let node = poolHashrate.nextSibling;
|
|
while (node && node !== indicatorPoolHashrate) {
|
|
const nextNode = node.nextSibling;
|
|
if (node.nodeType === 3) { // Text node
|
|
poolHashratePara.removeChild(node);
|
|
}
|
|
node = nextNode;
|
|
}
|
|
|
|
poolHashrate.parentNode.insertBefore(mainMetric, poolHashrate);
|
|
mainMetric.appendChild(poolHashrate);
|
|
mainMetric.appendChild(indicatorPoolHashrate);
|
|
}
|
|
|
|
// Create divider container for pool hashrate row
|
|
const dividerContainer = document.createElement("span");
|
|
dividerContainer.className = "metric-divider-container";
|
|
poolHashratePara.appendChild(dividerContainer);
|
|
}
|
|
|
|
// Get or create the divider container
|
|
let poolDividerContainer = poolHashratePara.querySelector('.metric-divider-container');
|
|
if (!poolDividerContainer) {
|
|
poolDividerContainer = document.createElement("span");
|
|
poolDividerContainer.className = "metric-divider-container";
|
|
poolHashratePara.appendChild(poolDividerContainer);
|
|
}
|
|
|
|
// Check if the "pool_luck" element already exists
|
|
const existingLuck = document.getElementById("pool_luck");
|
|
if (existingLuck) {
|
|
// Update existing element
|
|
existingLuck.textContent = poolLuckValue;
|
|
|
|
// Apply appropriate color class based on luck value
|
|
existingLuck.className = "metric-value metric-divider-value";
|
|
if (poolLuck !== null) {
|
|
if (poolLuck > 110) {
|
|
existingLuck.classList.add("very-lucky");
|
|
} else if (poolLuck > 100) {
|
|
existingLuck.classList.add("lucky");
|
|
} else if (poolLuck >= 90) {
|
|
existingLuck.classList.add("normal-luck");
|
|
} else {
|
|
existingLuck.classList.add("unlucky");
|
|
}
|
|
}
|
|
} else {
|
|
// Create the divider if it doesn't exist
|
|
const poolLuckDiv = createDivider("pool_luck", poolLuckValue, "Earnings Efficiency");
|
|
|
|
// Apply appropriate color class
|
|
const valueSpan = poolLuckDiv.querySelector('#pool_luck');
|
|
if (valueSpan && poolLuck !== null) {
|
|
if (poolLuck > 110) {
|
|
valueSpan.classList.add("very-lucky");
|
|
} else if (poolLuck > 100) {
|
|
valueSpan.classList.add("lucky");
|
|
} else if (poolLuck >= 90) {
|
|
valueSpan.classList.add("normal-luck");
|
|
} else {
|
|
valueSpan.classList.add("unlucky");
|
|
}
|
|
}
|
|
|
|
// Add to divider container
|
|
poolDividerContainer.appendChild(poolLuckDiv);
|
|
}
|
|
}
|
|
|
|
// Update pool fees in SATS (as negative value)
|
|
if (data.pool_fees_percentage !== undefined && data.last_block_earnings !== undefined) {
|
|
// Parse the last_block_earnings (removing any "+" prefix if present)
|
|
const lastBlockEarnings = parseFloat(data.last_block_earnings.toString().replace(/^\+/, ''));
|
|
|
|
const poolFeeSats = calculatePoolFeeInSats(
|
|
parseFloat(data.pool_fees_percentage),
|
|
lastBlockEarnings
|
|
);
|
|
|
|
// Find the pool_fees_percentage element
|
|
const poolFeesPercentage = document.getElementById("pool_fees_percentage");
|
|
|
|
if (poolFeesPercentage) {
|
|
// Format the pool fee in SATS with commas
|
|
const formattedPoolFee = poolFeeSats !== null ?
|
|
numberWithCommas(poolFeeSats) + " SATS" : "N/A";
|
|
|
|
// Check if pool_fees_sats span already exists
|
|
let poolFeesSats = document.getElementById("pool_fees_sats");
|
|
|
|
if (!poolFeesSats) {
|
|
// Create a new span for the fees in SATS if it doesn't exist
|
|
poolFeesSats = document.createElement("span");
|
|
poolFeesSats.id = "pool_fees_sats";
|
|
poolFeesSats.className = "metric-value";
|
|
|
|
// Find the indicator element that comes right after pool_fees_percentage
|
|
const indicatorPoolFees = document.getElementById("indicator_pool_fees_percentage");
|
|
|
|
// Insert before the indicator element
|
|
if (indicatorPoolFees) {
|
|
poolFeesPercentage.parentNode.insertBefore(poolFeesSats, indicatorPoolFees);
|
|
} else {
|
|
// If no indicator, append to the parent
|
|
poolFeesPercentage.parentNode.appendChild(poolFeesSats);
|
|
}
|
|
}
|
|
|
|
// Update the text and styling
|
|
poolFeesSats.textContent = " (" + formattedPoolFee + ")";
|
|
poolFeesSats.setAttribute("style", "color: #ff5555 !important; font-weight: bold !important; margin-left: 6px;");
|
|
}
|
|
}
|
|
|
|
// 24hr Hashrate
|
|
let formatted24hrHashrate = "N/A";
|
|
if (data.hashrate_24hr != null) {
|
|
formatted24hrHashrate = formatHashrateForDisplay(
|
|
data.hashrate_24hr,
|
|
data.hashrate_24hr_unit || 'th/s'
|
|
);
|
|
}
|
|
updateElementText("hashrate_24hr", formatted24hrHashrate);
|
|
|
|
// Update the block time section with consistent addition logic
|
|
let blockTime = "N/A"; // Default value
|
|
if (data.hashrate_24hr != null && data.network_hashrate != null) {
|
|
blockTime = calculateBlockTime(
|
|
data.hashrate_24hr,
|
|
data.hashrate_24hr_unit || 'th/s',
|
|
data.network_hashrate
|
|
);
|
|
}
|
|
|
|
// Find the hashrate_24hr element's parent paragraph
|
|
const hashrate24hrPara = document.getElementById("hashrate_24hr").parentNode;
|
|
|
|
// Structure parent for proper grid layout
|
|
if (!hashrate24hrPara.querySelector('.main-metric')) {
|
|
const hashrate24hr = document.getElementById("hashrate_24hr");
|
|
const indicator24hr = document.getElementById("indicator_hashrate_24hr");
|
|
|
|
// Create the main metric container
|
|
const mainMetric = document.createElement("span");
|
|
mainMetric.className = "main-metric";
|
|
|
|
// Move the metric and its indicator inside the container
|
|
if (hashrate24hr && indicator24hr) {
|
|
// Clear any existing text nodes between the elements
|
|
let node = hashrate24hr.nextSibling;
|
|
while (node && node !== indicator24hr) {
|
|
const nextNode = node.nextSibling;
|
|
if (node.nodeType === 3) { // Text node
|
|
hashrate24hrPara.removeChild(node);
|
|
}
|
|
node = nextNode;
|
|
}
|
|
|
|
hashrate24hr.parentNode.insertBefore(mainMetric, hashrate24hr);
|
|
mainMetric.appendChild(hashrate24hr);
|
|
mainMetric.appendChild(indicator24hr);
|
|
}
|
|
|
|
// Create divider container
|
|
const dividerContainer = document.createElement("span");
|
|
dividerContainer.className = "metric-divider-container";
|
|
hashrate24hrPara.appendChild(dividerContainer);
|
|
}
|
|
|
|
// Get or create the divider container
|
|
let dividerContainer = hashrate24hrPara.querySelector('.metric-divider-container');
|
|
if (!dividerContainer) {
|
|
dividerContainer = document.createElement("span");
|
|
dividerContainer.className = "metric-divider-container";
|
|
hashrate24hrPara.appendChild(dividerContainer);
|
|
}
|
|
|
|
// Check if the "block_time" element already exists
|
|
const existingBlockTime = document.getElementById("block_time");
|
|
if (existingBlockTime) {
|
|
// Find the containing metric-divider
|
|
let dividerElement = existingBlockTime.closest('.metric-divider');
|
|
if (dividerElement) {
|
|
// Just update the text
|
|
existingBlockTime.textContent = blockTime;
|
|
} else {
|
|
// If structure is broken, recreate it
|
|
const blockTimeDiv = createDivider("block_time", blockTime, "[Time to ₿]");
|
|
dividerContainer.innerHTML = ''; // Clear container
|
|
dividerContainer.appendChild(blockTimeDiv);
|
|
}
|
|
} else {
|
|
// Create the "Time to ₿" divider
|
|
const blockTimeDiv = createDivider("block_time", blockTime, "[Time to ₿]");
|
|
dividerContainer.appendChild(blockTimeDiv);
|
|
}
|
|
|
|
// 3hr Hashrate
|
|
let formatted3hrHashrate = "N/A";
|
|
if (data.hashrate_3hr != null) {
|
|
formatted3hrHashrate = formatHashrateForDisplay(
|
|
data.hashrate_3hr,
|
|
data.hashrate_3hr_unit || 'th/s'
|
|
);
|
|
}
|
|
updateElementText("hashrate_3hr", formatted3hrHashrate);
|
|
|
|
// Same for 3hr data with blockOdds
|
|
const hashrate3hrPara = document.getElementById("hashrate_3hr").parentNode;
|
|
|
|
// Structure parent for proper grid layout
|
|
if (!hashrate3hrPara.querySelector('.main-metric')) {
|
|
const hashrate3hr = document.getElementById("hashrate_3hr");
|
|
const indicator3hr = document.getElementById("indicator_hashrate_3hr");
|
|
|
|
// Create the main metric container
|
|
const mainMetric = document.createElement("span");
|
|
mainMetric.className = "main-metric";
|
|
|
|
// Move the metric and its indicator inside the container
|
|
if (hashrate3hr && indicator3hr) {
|
|
// Clear any existing text nodes between the elements
|
|
let node = hashrate3hr.nextSibling;
|
|
while (node && node !== indicator3hr) {
|
|
const nextNode = node.nextSibling;
|
|
if (node.nodeType === 3) { // Text node
|
|
hashrate3hrPara.removeChild(node);
|
|
}
|
|
node = nextNode;
|
|
}
|
|
|
|
hashrate3hr.parentNode.insertBefore(mainMetric, hashrate3hr);
|
|
mainMetric.appendChild(hashrate3hr);
|
|
mainMetric.appendChild(indicator3hr);
|
|
}
|
|
|
|
// Create divider container
|
|
const dividerContainer = document.createElement("span");
|
|
dividerContainer.className = "metric-divider-container";
|
|
hashrate3hrPara.appendChild(dividerContainer);
|
|
}
|
|
|
|
// Get or create the divider container
|
|
let odds3hrContainer = hashrate3hrPara.querySelector('.metric-divider-container');
|
|
if (!odds3hrContainer) {
|
|
odds3hrContainer = document.createElement("span");
|
|
odds3hrContainer.className = "metric-divider-container";
|
|
hashrate3hrPara.appendChild(odds3hrContainer);
|
|
}
|
|
|
|
// Apply the same consistent approach for the block odds section
|
|
if (data.hashrate_24hr != null && data.network_hashrate != null) {
|
|
const blockProbability = calculateBlockProbability(
|
|
data.hashrate_24hr,
|
|
data.hashrate_24hr_unit || 'th/s',
|
|
data.network_hashrate
|
|
);
|
|
|
|
// Update the element if it already exists
|
|
const existingProbability = document.getElementById("block_odds_3hr");
|
|
if (existingProbability) {
|
|
existingProbability.textContent = blockProbability;
|
|
} else {
|
|
// For block odds after 3hr hashrate
|
|
const blockOddsDiv = createDivider("block_odds_3hr", blockProbability, "[₿ Odds]");
|
|
odds3hrContainer.appendChild(blockOddsDiv);
|
|
}
|
|
}
|
|
|
|
// 10min Hashrate
|
|
let formatted10minHashrate = "N/A";
|
|
if (data.hashrate_10min != null) {
|
|
formatted10minHashrate = formatHashrateForDisplay(
|
|
data.hashrate_10min,
|
|
data.hashrate_10min_unit || 'th/s'
|
|
);
|
|
}
|
|
updateElementText("hashrate_10min", formatted10minHashrate);
|
|
|
|
// 60sec Hashrate
|
|
let formatted60secHashrate = "N/A";
|
|
if (data.hashrate_60sec != null) {
|
|
formatted60secHashrate = formatHashrateForDisplay(
|
|
data.hashrate_60sec,
|
|
data.hashrate_60sec_unit || 'th/s'
|
|
);
|
|
}
|
|
updateElementText("hashrate_60sec", formatted60secHashrate);
|
|
|
|
// Update other non-hashrate metrics
|
|
updateElementText("block_number", numberWithCommas(data.block_number));
|
|
|
|
// Update BTC price with currency conversion and symbol
|
|
if (data.btc_price != null) {
|
|
const btcPriceValue = data.btc_price * exchangeRate;
|
|
const symbol = getCurrencySymbol(currency);
|
|
|
|
updateElementText("btc_price", formatCurrencyValue(btcPriceValue, currency));
|
|
} else {
|
|
updateElementText("btc_price", formatCurrencyValue(0, currency));
|
|
}
|
|
|
|
// Update last block earnings
|
|
if (data.last_block_earnings !== undefined) {
|
|
// Format with "+" prefix and "SATS" suffix
|
|
updateElementText("last_block_earnings",
|
|
"+" + numberWithCommas(data.last_block_earnings) + " SATS");
|
|
}
|
|
|
|
// Network hashrate (already in EH/s but verify)
|
|
// Improved version with ZH/s support:
|
|
if (data.network_hashrate >= 1000) {
|
|
// Convert to Zettahash if over 1000 EH/s
|
|
updateElementText("network_hashrate",
|
|
(data.network_hashrate / 1000).toFixed(2) + " ZH/s");
|
|
} else {
|
|
// Use regular EH/s formatting
|
|
updateElementText("network_hashrate",
|
|
numberWithCommas(Math.round(data.network_hashrate)) + " EH/s");
|
|
}
|
|
updateElementText("difficulty", numberWithCommas(Math.round(data.difficulty)));
|
|
|
|
// Daily revenue with currency conversion
|
|
if (data.daily_revenue != null) {
|
|
const dailyRevenue = data.daily_revenue * exchangeRate;
|
|
updateElementText("daily_revenue", formatCurrencyValue(dailyRevenue, currency));
|
|
} else {
|
|
updateElementText("daily_revenue", formatCurrencyValue(0, currency));
|
|
}
|
|
|
|
// Daily power cost with currency conversion
|
|
if (data.daily_power_cost != null) {
|
|
const dailyPowerCost = data.daily_power_cost * exchangeRate;
|
|
updateElementText("daily_power_cost", formatCurrencyValue(dailyPowerCost, currency));
|
|
} else {
|
|
updateElementText("daily_power_cost", formatCurrencyValue(0, currency));
|
|
}
|
|
|
|
// Daily profit with currency conversion and color
|
|
if (data.daily_profit_usd != null) {
|
|
const dailyProfit = data.daily_profit_usd * exchangeRate;
|
|
const dailyProfitElement = document.getElementById("daily_profit_usd");
|
|
if (dailyProfitElement) {
|
|
dailyProfitElement.textContent = formatCurrencyValue(dailyProfit, currency);
|
|
if (dailyProfit < 0) {
|
|
// Use setAttribute to properly set the style with !important
|
|
dailyProfitElement.setAttribute("style", "color: #ff5555 !important; font-weight: bold !important;");
|
|
} else {
|
|
// Clear the style attribute completely
|
|
dailyProfitElement.removeAttribute("style");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Monthly profit with currency conversion and color
|
|
if (data.monthly_profit_usd != null) {
|
|
const monthlyProfit = data.monthly_profit_usd * exchangeRate;
|
|
const monthlyProfitElement = document.getElementById("monthly_profit_usd");
|
|
if (monthlyProfitElement) {
|
|
monthlyProfitElement.textContent = formatCurrencyValue(monthlyProfit, currency);
|
|
if (monthlyProfit < 0) {
|
|
// Use setAttribute to properly set the style with !important
|
|
monthlyProfitElement.setAttribute("style", "color: #ff5555 !important; font-weight: bold !important;");
|
|
} else {
|
|
// Clear the style attribute completely
|
|
monthlyProfitElement.removeAttribute("style");
|
|
}
|
|
}
|
|
}
|
|
|
|
updateElementText("daily_mined_sats", numberWithCommas(data.daily_mined_sats) + " SATS");
|
|
updateElementText("monthly_mined_sats", numberWithCommas(data.monthly_mined_sats) + " SATS");
|
|
|
|
// Update worker count from metrics (just the number, not full worker data)
|
|
updateWorkersCount();
|
|
|
|
updateElementText("unpaid_earnings", data.unpaid_earnings.toFixed(8) + " BTC");
|
|
|
|
// Update payout estimation with color coding
|
|
const payoutText = data.est_time_to_payout;
|
|
updateElementText("est_time_to_payout", payoutText);
|
|
|
|
// Check for "next block" in any case format
|
|
if (payoutText && /next\s+block/i.test(payoutText)) {
|
|
$("#est_time_to_payout").attr("style", "color: #32CD32 !important; animation: pulse 1s infinite !important; text-transform: uppercase !important;");
|
|
} else {
|
|
// Trim any extra whitespace
|
|
const cleanText = payoutText ? payoutText.trim() : "";
|
|
// Update your regex to handle hours-only format as well
|
|
const regex = /(?:(\d+)\s*days?(?:,?\s*(\d+)\s*hours?)?)|(?:(\d+)\s*hours?)/i;
|
|
const match = cleanText.match(regex);
|
|
|
|
let totalDays = NaN;
|
|
if (match) {
|
|
if (match[1]) {
|
|
// Format: "X days" or "X days, Y hours"
|
|
const days = parseFloat(match[1]);
|
|
const hours = match[2] ? parseFloat(match[2]) : 0;
|
|
totalDays = days + (hours / 24);
|
|
} else if (match[3]) {
|
|
// Format: "X hours"
|
|
const hours = parseFloat(match[3]);
|
|
totalDays = hours / 24;
|
|
}
|
|
console.log("Total days computed:", totalDays); // Debug output
|
|
}
|
|
|
|
if (!isNaN(totalDays)) {
|
|
if (totalDays < 4) {
|
|
$("#est_time_to_payout").attr("style", "color: #32CD32 !important; animation: none !important;");
|
|
} else if (totalDays > 20) {
|
|
$("#est_time_to_payout").attr("style", "color: #ff5555 !important; animation: none !important;");
|
|
} else {
|
|
$("#est_time_to_payout").attr("style", "color: #ffd700 !important; animation: none !important;");
|
|
}
|
|
} else {
|
|
$("#est_time_to_payout").attr("style", "color: #ffd700 !important; animation: none !important;");
|
|
}
|
|
}
|
|
|
|
updateElementText("last_block_height", data.last_block_height ? numberWithCommas(data.last_block_height) : "N/A");
|
|
updateElementText("last_block_time", data.last_block_time || "");
|
|
updateElementText("blocks_found", data.blocks_found || "0");
|
|
updateElementText("last_share", data.total_last_share || "");
|
|
|
|
// Update Estimated Earnings metrics
|
|
updateElementText("estimated_earnings_per_day_sats", numberWithCommas(data.estimated_earnings_per_day_sats) + " SATS");
|
|
updateElementText("estimated_earnings_next_block_sats", numberWithCommas(data.estimated_earnings_next_block_sats) + " SATS");
|
|
updateElementText("estimated_rewards_in_window_sats", numberWithCommas(data.estimated_rewards_in_window_sats) + " SATS");
|
|
|
|
// Update last updated timestamp
|
|
try {
|
|
// Get the configured timezone with fallback
|
|
const configuredTimezone = window.dashboardTimezone || 'America/Los_Angeles';
|
|
|
|
// Use server timestamp from metrics if available, otherwise use adjusted local time
|
|
const timestampToUse = latestMetrics && latestMetrics.server_timestamp ?
|
|
new Date(latestMetrics.server_timestamp) :
|
|
new Date(Date.now() + (serverTimeOffset || 0));
|
|
|
|
// Format with explicit timezone
|
|
const options = {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
second: '2-digit',
|
|
hour12: true,
|
|
timeZone: configuredTimezone // Explicitly set timezone
|
|
};
|
|
|
|
// Update the lastUpdated element
|
|
updateElementHTML("lastUpdated",
|
|
"<strong>Last Updated:</strong> " +
|
|
timestampToUse.toLocaleString('en-US', options) +
|
|
"<span id='terminal-cursor'></span>");
|
|
|
|
console.log(`Last updated timestamp shown using timezone: ${configuredTimezone}`);
|
|
} catch (error) {
|
|
console.error("Error formatting last updated timestamp:", error);
|
|
// Fallback to basic timestamp if there's an error
|
|
const now = new Date();
|
|
updateElementHTML("lastUpdated",
|
|
"<strong>Last Updated:</strong> " +
|
|
now.toLocaleString() +
|
|
"<span id='terminal-cursor'></span>");
|
|
}
|
|
|
|
// Update chart with normalized data if it exists
|
|
if (trendChart) {
|
|
// Use the enhanced chart update function with normalization
|
|
updateChartWithNormalizedData(trendChart, data);
|
|
}
|
|
|
|
// Update indicators and check for block updates
|
|
updateIndicators(data);
|
|
checkForBlockUpdates(data);
|
|
|
|
// Store current metrics for next comparison
|
|
previousMetrics = { ...data };
|
|
|
|
} catch (error) {
|
|
console.error("Error updating UI:", error);
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
// Modify the resetDashboardChart function
|
|
function resetDashboardChart() {
|
|
console.log("Resetting dashboard chart data");
|
|
|
|
if (trendChart) {
|
|
// Reset chart data arrays first (always succeeds)
|
|
trendChart.data.labels = [];
|
|
trendChart.data.datasets[0].data = [];
|
|
trendChart.update('none');
|
|
|
|
// Show immediate feedback
|
|
showConnectionIssue("Resetting chart data...");
|
|
|
|
// Then call the API to clear underlying data
|
|
$.ajax({
|
|
url: '/api/reset-chart-data',
|
|
method: 'POST',
|
|
success: function (response) {
|
|
console.log("Server data reset:", response);
|
|
showConnectionIssue("Chart data reset successfully");
|
|
setTimeout(hideConnectionIssue, 3000);
|
|
},
|
|
error: function (xhr, status, error) {
|
|
console.error("Error resetting chart data:", error);
|
|
showConnectionIssue("Chart display reset (backend error: " + error + ")");
|
|
setTimeout(hideConnectionIssue, 5000);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
// Add keyboard event listener for Alt+W to reset wallet address
|
|
$(document).keydown(function (event) {
|
|
// Check if Alt+W is pressed (key code 87 is 'W')
|
|
if (event.altKey && event.keyCode === 87) {
|
|
resetWalletAddress();
|
|
|
|
// Prevent default browser behavior
|
|
event.preventDefault();
|
|
}
|
|
});
|
|
|
|
// Function to reset wallet address in configuration and clear chart data
|
|
function resetWalletAddress() {
|
|
if (confirm("Are you sure you want to reset your wallet address? This will also clear all chart data and redirect you to the configuration page.")) {
|
|
// First clear chart data using the existing API endpoint
|
|
$.ajax({
|
|
url: '/api/reset-chart-data',
|
|
method: 'POST',
|
|
success: function () {
|
|
console.log("Chart data reset successfully");
|
|
|
|
// Then reset the chart display locally
|
|
if (trendChart) {
|
|
trendChart.data.labels = [];
|
|
trendChart.data.datasets[0].data = [];
|
|
trendChart.update('none');
|
|
}
|
|
|
|
// Then reset wallet address
|
|
fetch('/api/config')
|
|
.then(response => response.json())
|
|
.then(config => {
|
|
// Reset the wallet address to default
|
|
config.wallet = "yourwallethere";
|
|
// Add special flag to indicate config reset
|
|
config.config_reset = true;
|
|
|
|
// Save the updated configuration
|
|
return fetch('/api/config', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(config)
|
|
});
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
console.log("Wallet address reset successfully:", data);
|
|
// Also clear arrow indicator states
|
|
arrowIndicator.clearAll();
|
|
// Redirect to the boot page for reconfiguration
|
|
window.location.href = window.location.origin + "/";
|
|
})
|
|
.catch(error => {
|
|
console.error("Error resetting wallet address:", error);
|
|
alert("There was an error resetting your wallet address. Please try again.");
|
|
});
|
|
},
|
|
error: function (xhr, status, error) {
|
|
console.error("Error clearing chart data:", error);
|
|
// Continue with wallet reset even if chart reset fails
|
|
resetWalletAddressOnly();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Fallback function if chart reset fails
|
|
function resetWalletAddressOnly() {
|
|
fetch('/api/config')
|
|
.then(response => response.json())
|
|
.then(config => {
|
|
config.wallet = "yourwallethere";
|
|
return fetch('/api/config', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(config)
|
|
});
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
console.log("Wallet address reset successfully (without chart reset):", data);
|
|
window.location.href = window.location.origin + "/";
|
|
})
|
|
.catch(error => {
|
|
console.error("Error resetting wallet address:", error);
|
|
alert("There was an error resetting your wallet address. Please try again.");
|
|
});
|
|
}
|
|
|
|
// Function to show a helpful notification to the user about hashrate normalization
|
|
function showHashrateNormalizeNotice() {
|
|
// Only show if the notification doesn't already exist on the page
|
|
if ($("#hashrateNormalizeNotice").length === 0) {
|
|
const theme = getCurrentTheme();
|
|
|
|
// Create notification element with theme-appropriate styling
|
|
const notice = $(`
|
|
<div id="hashrateNormalizeNotice" style="
|
|
position: fixed;
|
|
bottom: 30px;
|
|
right: 30px;
|
|
background-color: rgba(0, 0, 0, 0.85);
|
|
color: ${theme.PRIMARY};
|
|
border: 1px solid ${theme.PRIMARY};
|
|
padding: 15px 20px;
|
|
border-radius: 4px;
|
|
z-index: 9999;
|
|
max-width: 300px;
|
|
font-family: 'VT323', monospace;
|
|
font-size: 16px;
|
|
box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);
|
|
">
|
|
<div style="display: flex; align-items: flex-start;">
|
|
<div style="margin-right: 10px;">
|
|
<i class="fas fa-chart-line" style="font-size: 22px;"></i>
|
|
</div>
|
|
<div>
|
|
<div style="font-weight: bold; margin-bottom: 5px; text-transform: uppercase;">Hashrate Chart Notice</div>
|
|
<div>Please wait 2-3 minutes for the chart to collect data and normalize for your hashrate pattern.</div>
|
|
</div>
|
|
</div>
|
|
<div style="text-align: right; margin-top: 10px;">
|
|
<button id="hashrateNoticeClose" style="
|
|
background: none;
|
|
border: none;
|
|
color: ${theme.PRIMARY};
|
|
cursor: pointer;
|
|
font-family: inherit;
|
|
text-decoration: underline;
|
|
">Dismiss</button>
|
|
<label style="margin-left: 10px;">
|
|
<input type="checkbox" id="dontShowAgain" style="vertical-align: middle;">
|
|
<span style="font-size: 0.8em; vertical-align: middle;">Don't show again</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
`);
|
|
|
|
// Add to body and handle close button
|
|
$("body").append(notice);
|
|
|
|
// Handler for the close button
|
|
$("#hashrateNoticeClose").on("click", function () {
|
|
// Check if "Don't show again" is checked
|
|
if ($("#dontShowAgain").is(":checked")) {
|
|
// Remember permanently in localStorage
|
|
localStorage.setItem('hideHashrateNotice', 'true');
|
|
console.log("User chose to permanently hide hashrate notice");
|
|
} else {
|
|
// Only remember for this session
|
|
sessionStorage.setItem('hideHashrateNoticeSession', 'true');
|
|
console.log("User dismissed hashrate notice for this session");
|
|
}
|
|
|
|
// Hide and remove the notice
|
|
$("#hashrateNormalizeNotice").fadeOut(300, function () {
|
|
$(this).remove();
|
|
});
|
|
});
|
|
|
|
// Auto-hide after 60 seconds
|
|
setTimeout(function () {
|
|
if ($("#hashrateNormalizeNotice").length) {
|
|
$("#hashrateNormalizeNotice").fadeOut(500, function () {
|
|
$(this).remove();
|
|
});
|
|
}
|
|
}, 60000); // Changed to 60 seconds for better visibility
|
|
}
|
|
}
|
|
|
|
// Helper function to check if we should show the notice (call this during page initialization)
|
|
function checkAndShowHashrateNotice() {
|
|
// Check if user has permanently hidden the notice
|
|
const permanentlyHidden = localStorage.getItem('hideHashrateNotice') === 'true';
|
|
|
|
// Check if user has hidden the notice for this session
|
|
const sessionHidden = sessionStorage.getItem('hideHashrateNoticeSession') === 'true';
|
|
|
|
// Also check low hashrate mode state (to potentially show a different message)
|
|
const inLowHashrateMode = localStorage.getItem('lowHashrateState') ?
|
|
JSON.parse(localStorage.getItem('lowHashrateState')).isLowHashrateMode : false;
|
|
|
|
if (!permanentlyHidden && !sessionHidden) {
|
|
// Show the notice with a short delay to ensure the page is fully loaded
|
|
setTimeout(function () {
|
|
showHashrateNormalizeNotice();
|
|
}, 2000);
|
|
} else {
|
|
console.log("Hashrate notice will not be shown: permanently hidden = " +
|
|
permanentlyHidden + ", session hidden = " + sessionHidden);
|
|
}
|
|
}
|
|
|
|
// Currency conversion functions
|
|
function getCurrencySymbol(currency) {
|
|
const symbols = {
|
|
'USD': '$',
|
|
'EUR': '€',
|
|
'GBP': '£',
|
|
'JPY': '¥',
|
|
'CAD': 'CA$',
|
|
'AUD': 'A$',
|
|
'CNY': '¥',
|
|
'KRW': '₩',
|
|
'BRL': 'R$',
|
|
'CHF': 'Fr'
|
|
};
|
|
return symbols[currency] || '$';
|
|
}
|
|
|
|
function formatCurrencyValue(value, currency) {
|
|
if (value == null || isNaN(value)) return "N/A";
|
|
|
|
const symbol = getCurrencySymbol(currency);
|
|
|
|
// For JPY and KRW, show without decimal places
|
|
if (currency === 'JPY' || currency === 'KRW') {
|
|
return `${symbol}${numberWithCommas(Math.round(value))}`;
|
|
}
|
|
|
|
return `${symbol}${numberWithCommas(value.toFixed(2))}`;
|
|
}
|
|
|
|
// Update the BTC price and earnings card header with the selected currency
|
|
function updateCurrencyLabels(currency) {
|
|
const earningsHeader = document.querySelector('.card-header:contains("USD EARNINGS")');
|
|
if (earningsHeader) {
|
|
earningsHeader.textContent = `${currency} EARNINGS`;
|
|
}
|
|
}
|
|
|
|
$(document).ready(function () {
|
|
// Apply theme based on stored preference - moved to beginning for better initialization
|
|
try {
|
|
const useDeepSea = localStorage.getItem('useDeepSeaTheme') === 'true';
|
|
if (useDeepSea) {
|
|
applyDeepSeaTheme();
|
|
}
|
|
// Setup theme change listener
|
|
setupThemeChangeListener();
|
|
} catch (e) {
|
|
console.error("Error handling theme:", e);
|
|
}
|
|
|
|
// Modify the initializeChart function to use blue colors for the chart
|
|
function initializeChart() {
|
|
try {
|
|
const ctx = document.getElementById('trendGraph').getContext('2d');
|
|
if (!ctx) {
|
|
console.error("Could not find trend graph canvas");
|
|
return null;
|
|
}
|
|
|
|
if (!window.Chart) {
|
|
console.error("Chart.js not loaded");
|
|
return null;
|
|
}
|
|
|
|
// Get the current theme colors
|
|
const theme = getCurrentTheme();
|
|
|
|
// Check if Chart.js plugin is available
|
|
const hasAnnotationPlugin = window['chartjs-plugin-annotation'] !== undefined;
|
|
|
|
return new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: [],
|
|
datasets: [{
|
|
label: 'HASHRATE TREND (TH/s)',
|
|
data: [],
|
|
borderWidth: 2,
|
|
borderColor: function (context) {
|
|
const chart = context.chart;
|
|
const { ctx, chartArea } = chart;
|
|
if (!chartArea) {
|
|
return theme.PRIMARY;
|
|
}
|
|
// Create gradient for line
|
|
const gradient = ctx.createLinearGradient(0, 0, 0, chartArea.bottom);
|
|
gradient.addColorStop(0, theme.CHART.GRADIENT_START);
|
|
gradient.addColorStop(1, theme.CHART.GRADIENT_END);
|
|
return gradient;
|
|
},
|
|
backgroundColor: function (context) {
|
|
const chart = context.chart;
|
|
const { ctx, chartArea } = chart;
|
|
if (!chartArea) {
|
|
return `rgba(${theme.PRIMARY_RGB}, 0.1)`;
|
|
}
|
|
// Create gradient for fill
|
|
const gradient = ctx.createLinearGradient(0, 0, 0, chartArea.bottom);
|
|
gradient.addColorStop(0, `rgba(${theme.PRIMARY_RGB}, 0.3)`);
|
|
gradient.addColorStop(0.5, `rgba(${theme.PRIMARY_RGB}, 0.2)`);
|
|
gradient.addColorStop(1, `rgba(${theme.PRIMARY_RGB}, 0.05)`);
|
|
return gradient;
|
|
},
|
|
fill: true,
|
|
tension: 0.3,
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
animation: {
|
|
duration: 0 // Disable animations for better performance
|
|
},
|
|
scales: {
|
|
x: {
|
|
display: true,
|
|
ticks: {
|
|
maxTicksLimit: 8, // Limit number of x-axis labels
|
|
maxRotation: 0, // Don't rotate labels
|
|
autoSkip: true, // Automatically skip some labels
|
|
color: '#FFFFFF',
|
|
font: {
|
|
family: "'VT323', monospace", // Terminal font
|
|
size: 14
|
|
}
|
|
},
|
|
grid: {
|
|
color: '#333333',
|
|
lineWidth: 0.5
|
|
}
|
|
},
|
|
y: {
|
|
title: {
|
|
display: true,
|
|
text: 'HASHRATE (TH/S)',
|
|
color: theme.PRIMARY,
|
|
font: {
|
|
family: "'VT323', monospace",
|
|
size: 16,
|
|
weight: 'bold'
|
|
}
|
|
},
|
|
ticks: {
|
|
color: '#FFFFFF',
|
|
maxTicksLimit: 6, // Limit total number of ticks
|
|
precision: 1, // Control decimal precision
|
|
autoSkip: true, // Skip labels to prevent overcrowding
|
|
autoSkipPadding: 10, // Padding between skipped labels
|
|
font: {
|
|
family: "'VT323', monospace", // Terminal font
|
|
size: 14
|
|
},
|
|
callback: function (value) {
|
|
// For zero, just return 0
|
|
if (value === 0) return '0';
|
|
|
|
// For very large values (1M+ TH/s = 1000+ PH/s)
|
|
if (value >= 1000000) {
|
|
return (value / 1000000).toFixed(1) + 'E'; // Show as EH/s
|
|
}
|
|
// For large values (1000+ TH/s), show in PH/s
|
|
else if (value >= 1000) {
|
|
return (value / 1000).toFixed(1) + 'P'; // Show as PH/s
|
|
}
|
|
// For values between 10 and 1000 TH/s
|
|
else if (value >= 10) {
|
|
return Math.round(value);
|
|
}
|
|
// For small values, limit decimal places
|
|
else if (value >= 1) {
|
|
return value.toFixed(1);
|
|
}
|
|
// For tiny values, use appropriate precision
|
|
else {
|
|
return value.toPrecision(2);
|
|
}
|
|
}
|
|
},
|
|
grid: {
|
|
color: '#333333',
|
|
lineWidth: 0.5,
|
|
drawBorder: false,
|
|
zeroLineColor: '#555555',
|
|
zeroLineWidth: 1,
|
|
drawTicks: false
|
|
}
|
|
}
|
|
},
|
|
plugins: {
|
|
tooltip: {
|
|
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
|
titleColor: theme.PRIMARY,
|
|
bodyColor: '#FFFFFF',
|
|
titleFont: {
|
|
family: "'VT323', monospace",
|
|
size: 16,
|
|
weight: 'bold'
|
|
},
|
|
bodyFont: {
|
|
family: "'VT323', monospace",
|
|
size: 14
|
|
},
|
|
padding: 10,
|
|
cornerRadius: 0,
|
|
displayColors: false,
|
|
callbacks: {
|
|
title: function (tooltipItems) {
|
|
return tooltipItems[0].label.toUpperCase();
|
|
},
|
|
label: function (context) {
|
|
// Format tooltip values with appropriate unit
|
|
const value = context.raw;
|
|
return 'HASHRATE: ' + formatHashrateForDisplay(value).toUpperCase();
|
|
}
|
|
}
|
|
},
|
|
legend: { display: false },
|
|
annotation: hasAnnotationPlugin ? {
|
|
annotations: {
|
|
averageLine: {
|
|
type: 'line',
|
|
yMin: 0,
|
|
yMax: 0,
|
|
borderColor: theme.CHART.ANNOTATION,
|
|
borderWidth: 3,
|
|
borderDash: [8, 4],
|
|
shadowColor: `rgba(${theme.PRIMARY_RGB}, 0.5)`,
|
|
shadowBlur: 8,
|
|
shadowOffsetX: 0,
|
|
shadowOffsetY: 0,
|
|
label: {
|
|
enabled: true,
|
|
content: '24HR AVG: 0 TH/S',
|
|
backgroundColor: 'rgba(0,0,0,0.8)',
|
|
color: theme.CHART.ANNOTATION,
|
|
font: {
|
|
family: "'VT323', monospace",
|
|
size: 16,
|
|
weight: 'bold'
|
|
},
|
|
padding: { top: 4, bottom: 4, left: 8, right: 8 },
|
|
borderRadius: 0,
|
|
position: 'start'
|
|
}
|
|
}
|
|
}
|
|
} : {}
|
|
}
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error("Error initializing chart:", error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Add this function to the document ready section
|
|
function setupThemeChangeListener() {
|
|
window.addEventListener('storage', function (event) {
|
|
if (event.key === 'useDeepSeaTheme') {
|
|
if (trendChart) {
|
|
// Save all font configurations
|
|
const fontConfig = {
|
|
xTicks: { ...trendChart.options.scales.x.ticks.font },
|
|
yTicks: { ...trendChart.options.scales.y.ticks.font },
|
|
yTitle: { ...trendChart.options.scales.y.title.font },
|
|
tooltip: {
|
|
title: { ...trendChart.options.plugins.tooltip.titleFont },
|
|
body: { ...trendChart.options.plugins.tooltip.bodyFont }
|
|
}
|
|
};
|
|
|
|
// No need to create a copy of lowHashrateState here,
|
|
// as we'll load it from localStorage after chart recreation
|
|
|
|
// Save the low hashrate indicator element if it exists
|
|
const wasInLowHashrateMode = trendChart.lowHashrateState &&
|
|
trendChart.lowHashrateState.isLowHashrateMode;
|
|
|
|
// Check if we're on mobile (viewport width < 768px)
|
|
const isMobile = window.innerWidth < 768;
|
|
|
|
// Store the original sizes before destroying chart
|
|
const xTicksFontSize = fontConfig.xTicks.size || 14;
|
|
const yTicksFontSize = fontConfig.yTicks.size || 14;
|
|
const yTitleFontSize = fontConfig.yTitle.size || 16;
|
|
|
|
// Recreate the chart with new theme colors
|
|
trendChart.destroy();
|
|
trendChart = initializeChart();
|
|
|
|
// The state will be automatically loaded from localStorage in updateChartWithNormalizedData
|
|
|
|
// Ensure font sizes are explicitly set to original values
|
|
// This is especially important for mobile
|
|
if (isMobile) {
|
|
// On mobile, set explicit font sizes (based on the originals)
|
|
trendChart.options.scales.x.ticks.font = {
|
|
...fontConfig.xTicks,
|
|
size: xTicksFontSize
|
|
};
|
|
|
|
trendChart.options.scales.y.ticks.font = {
|
|
...fontConfig.yTicks,
|
|
size: yTicksFontSize
|
|
};
|
|
|
|
trendChart.options.scales.y.title.font = {
|
|
...fontConfig.yTitle,
|
|
size: yTitleFontSize
|
|
};
|
|
|
|
// Also set tooltip font sizes explicitly
|
|
trendChart.options.plugins.tooltip.titleFont = {
|
|
...fontConfig.tooltip.title,
|
|
size: fontConfig.tooltip.title.size || 16
|
|
};
|
|
|
|
trendChart.options.plugins.tooltip.bodyFont = {
|
|
...fontConfig.tooltip.body,
|
|
size: fontConfig.tooltip.body.size || 14
|
|
};
|
|
|
|
console.log('Mobile device detected: Setting explicit font sizes for chart labels');
|
|
} else {
|
|
// On desktop, use the full font config objects as before
|
|
trendChart.options.scales.x.ticks.font = fontConfig.xTicks;
|
|
trendChart.options.scales.y.ticks.font = fontConfig.yTicks;
|
|
trendChart.options.scales.y.title.font = fontConfig.yTitle;
|
|
trendChart.options.plugins.tooltip.titleFont = fontConfig.tooltip.title;
|
|
trendChart.options.plugins.tooltip.bodyFont = fontConfig.tooltip.body;
|
|
}
|
|
|
|
// Update with data and force an immediate chart update
|
|
updateChartWithNormalizedData(trendChart, latestMetrics);
|
|
trendChart.update('none');
|
|
}
|
|
|
|
// Update refresh button color
|
|
updateRefreshButtonColor();
|
|
|
|
// Trigger custom event
|
|
$(document).trigger('themeChanged');
|
|
}
|
|
});
|
|
}
|
|
|
|
setupThemeChangeListener();
|
|
|
|
// Remove the existing refreshUptime container to avoid duplicates
|
|
$('#refreshUptime').hide();
|
|
|
|
// Create a shared timing object that both systems can reference
|
|
window.sharedTimingData = {
|
|
serverTimeOffset: serverTimeOffset,
|
|
serverStartTime: serverStartTime,
|
|
lastRefreshTime: Date.now()
|
|
};
|
|
|
|
// Override the updateServerTime function to update the shared object
|
|
const originalUpdateServerTime = updateServerTime;
|
|
updateServerTime = function () {
|
|
originalUpdateServerTime();
|
|
|
|
// Update shared timing data after the original function runs
|
|
setTimeout(function () {
|
|
window.sharedTimingData.serverTimeOffset = serverTimeOffset;
|
|
window.sharedTimingData.serverStartTime = serverStartTime;
|
|
|
|
// Make sure BitcoinMinuteRefresh uses the same timing information
|
|
if (typeof BitcoinMinuteRefresh !== 'undefined' && BitcoinMinuteRefresh.updateServerTime) {
|
|
BitcoinMinuteRefresh.updateServerTime(serverTimeOffset, serverStartTime);
|
|
}
|
|
}, 100);
|
|
};
|
|
|
|
// Add this to your $(document).ready() function in main.js
|
|
function fixLastBlockLine() {
|
|
// Add the style to fix the Last Block line
|
|
$("<style>")
|
|
.prop("type", "text/css")
|
|
.html(`
|
|
/* Fix for Last Block line to keep all elements on one line */
|
|
.card-body p.last-block-line {
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.card-body p.last-block-line > strong {
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.card-body p.last-block-line > span,
|
|
.card-body p.last-block-line > #indicator_last_block {
|
|
display: inline-block;
|
|
margin-right: 5px;
|
|
}
|
|
`)
|
|
.appendTo("head");
|
|
|
|
// Apply the class to the Last Block line
|
|
$("#payoutMiscCard .card-body p").each(function () {
|
|
const strongElem = $(this).find("strong");
|
|
if (strongElem.length && strongElem.text().includes("Last Block")) {
|
|
$(this).addClass("last-block-line");
|
|
}
|
|
});
|
|
}
|
|
|
|
// Call this function
|
|
fixLastBlockLine();
|
|
|
|
// Check if we should show the hashrate normalization notice
|
|
checkAndShowHashrateNotice();
|
|
|
|
// Also show notice when entering low hashrate mode for the first time in a session
|
|
// Track when we enter low hashrate mode to show specialized notification
|
|
const originalUpdateChartWithNormalizedData = updateChartWithNormalizedData;
|
|
window.updateChartWithNormalizedData = function (chart, data) {
|
|
const wasInLowHashrateMode = chart && chart.lowHashrateState &&
|
|
chart.lowHashrateState.isLowHashrateMode;
|
|
|
|
// Call original function
|
|
originalUpdateChartWithNormalizedData(chart, data);
|
|
|
|
// Check if we just entered low hashrate mode
|
|
if (chart && chart.lowHashrateState &&
|
|
chart.lowHashrateState.isLowHashrateMode && !wasInLowHashrateMode) {
|
|
|
|
console.log("Entered low hashrate mode - showing notification");
|
|
|
|
// Show the notice if it hasn't been permanently hidden
|
|
if (localStorage.getItem('hideHashrateNotice') !== 'true' &&
|
|
!$("#hashrateNormalizeNotice").length) {
|
|
showHashrateNormalizeNotice();
|
|
}
|
|
}
|
|
};
|
|
|
|
// Load timezone setting early
|
|
(function loadTimezoneEarly() {
|
|
// First try to get from localStorage for instant access
|
|
try {
|
|
const storedTimezone = localStorage.getItem('dashboardTimezone');
|
|
if (storedTimezone) {
|
|
window.dashboardTimezone = storedTimezone;
|
|
console.log(`Using cached timezone: ${storedTimezone}`);
|
|
}
|
|
} catch (e) {
|
|
console.error("Error reading timezone from localStorage:", e);
|
|
}
|
|
|
|
// Then fetch from server to ensure we have the latest setting
|
|
fetch('/api/timezone')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data && data.timezone) {
|
|
window.dashboardTimezone = data.timezone;
|
|
console.log(`Set timezone from server: ${data.timezone}`);
|
|
|
|
// Cache for future use
|
|
try {
|
|
localStorage.setItem('dashboardTimezone', data.timezone);
|
|
} catch (e) {
|
|
console.error("Error storing timezone in localStorage:", e);
|
|
}
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error("Error fetching timezone:", error);
|
|
});
|
|
})();
|
|
|
|
// Override the manualRefresh function to update the shared lastRefreshTime
|
|
const originalManualRefresh = manualRefresh;
|
|
window.manualRefresh = function () {
|
|
// Update the shared timing data
|
|
window.sharedTimingData.lastRefreshTime = Date.now();
|
|
|
|
// Call the original function
|
|
originalManualRefresh();
|
|
|
|
// Notify BitcoinMinuteRefresh about the refresh
|
|
if (typeof BitcoinMinuteRefresh !== 'undefined' && BitcoinMinuteRefresh.notifyRefresh) {
|
|
BitcoinMinuteRefresh.notifyRefresh();
|
|
}
|
|
};
|
|
|
|
// Initialize the chart
|
|
trendChart = initializeChart();
|
|
|
|
// Add keyboard event listener for Shift+R
|
|
$(document).keydown(function (event) {
|
|
// Check if Shift+R is pressed (key code 82 is 'R')
|
|
if (event.shiftKey && event.keyCode === 82) {
|
|
resetDashboardChart();
|
|
|
|
// Prevent default browser behavior (e.g., reload with Shift+R in some browsers)
|
|
event.preventDefault();
|
|
}
|
|
});
|
|
|
|
// Apply any saved arrows to DOM on page load
|
|
arrowIndicator.forceApplyArrows();
|
|
|
|
// Initialize BitcoinMinuteRefresh with our refresh function
|
|
if (typeof BitcoinMinuteRefresh !== 'undefined' && BitcoinMinuteRefresh.initialize) {
|
|
BitcoinMinuteRefresh.initialize(window.manualRefresh);
|
|
|
|
// Immediately update it with our current server time information
|
|
if (serverTimeOffset && serverStartTime) {
|
|
BitcoinMinuteRefresh.updateServerTime(serverTimeOffset, serverStartTime);
|
|
}
|
|
}
|
|
|
|
// Update BitcoinProgressBar theme when theme changes
|
|
$(document).on('themeChanged', function () {
|
|
if (typeof BitcoinMinuteRefresh !== 'undefined' &&
|
|
typeof BitcoinMinuteRefresh.updateTheme === 'function') {
|
|
BitcoinMinuteRefresh.updateTheme();
|
|
}
|
|
});
|
|
|
|
// Set up event source for SSE
|
|
setupEventSource();
|
|
|
|
// Start server time polling
|
|
updateServerTime();
|
|
setInterval(updateServerTime, 30000);
|
|
|
|
// Update the manual refresh button color
|
|
$("body").append('<button id="refreshButton" style="position: fixed; bottom: 20px; left: 20px; z-index: 1000; background: #0088cc; color: white; border: none; padding: 8px 16px; display: none; border-radius: 4px; cursor: pointer;">Refresh Data</button>');
|
|
|
|
$("#refreshButton").on("click", function () {
|
|
$(this).text("Refreshing...");
|
|
$(this).prop("disabled", true);
|
|
manualRefresh();
|
|
setTimeout(function () {
|
|
$("#refreshButton").text("Refresh Data");
|
|
$("#refreshButton").prop("disabled", false);
|
|
}, 5000);
|
|
});
|
|
|
|
// Force a data refresh when the page loads
|
|
manualRefresh();
|
|
|
|
// Add emergency refresh button functionality
|
|
$("#forceRefreshBtn").show().on("click", function () {
|
|
$(this).text("Refreshing...");
|
|
$(this).prop("disabled", true);
|
|
|
|
$.ajax({
|
|
url: '/api/force-refresh',
|
|
method: 'POST',
|
|
timeout: 15000,
|
|
success: function (data) {
|
|
console.log("Force refresh successful:", data);
|
|
manualRefresh(); // Immediately get the new data
|
|
$("#forceRefreshBtn").text("Force Refresh").prop("disabled", false);
|
|
},
|
|
error: function (xhr, status, error) {
|
|
console.error("Force refresh failed:", error);
|
|
$("#forceRefreshBtn").text("Force Refresh").prop("disabled", false);
|
|
alert("Refresh failed: " + error);
|
|
}
|
|
});
|
|
});
|
|
|
|
// Add stale data detection
|
|
setInterval(function () {
|
|
if (latestMetrics && latestMetrics.server_timestamp) {
|
|
const lastUpdate = new Date(latestMetrics.server_timestamp);
|
|
const timeSinceUpdate = Math.floor((Date.now() - lastUpdate.getTime()) / 1000);
|
|
if (timeSinceUpdate > 120) { // More than 2 minutes
|
|
showConnectionIssue(`Data stale (${timeSinceUpdate}s old). Use Force Refresh.`);
|
|
$("#forceRefreshBtn").show();
|
|
}
|
|
}
|
|
}, 30000); // Check every 30 seconds
|
|
|
|
// Initialize notification badge
|
|
initNotificationBadge();
|
|
});
|