// Add this flag at the top of your file, outside the function let isApplyingTheme = false; "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] = ""; } else if (newValue < prevValue * (1 - this.changeThreshold)) { this.arrowStates[key] = ""; } } 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) { if (!value || isNaN(value)) return 0; unit = (unit || 'th/s').toLowerCase(); if (unit.includes('ph/s')) { return value * 1000; // Convert PH/s to TH/s } else if (unit.includes('eh/s')) { return value * 1000000; // Convert EH/s to TH/s } else if (unit.includes('gh/s')) { return value / 1000; // Convert GH/s to TH/s } else if (unit.includes('mh/s')) { return value / 1000000; // Convert MH/s to TH/s } else if (unit.includes('kh/s')) { return value / 1000000000; // Convert KH/s to TH/s } else if (unit.includes('h/s') && !unit.includes('th/s') && !unit.includes('ph/s') && !unit.includes('eh/s') && !unit.includes('gh/s') && !unit.includes('mh/s') && !unit.includes('kh/s')) { return value / 1000000000000; // Convert H/s to TH/s } else { // Assume TH/s if unit is not recognized return value; } } // 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 // Helper function to normalize hashrate to TH/s for consistent graphing function normalizeHashrate(value, unit) { if (!value || isNaN(value)) return 0; unit = (unit || 'th/s').toLowerCase(); if (unit.includes('ph/s')) { return value * 1000; // Convert PH/s to TH/s } else if (unit.includes('eh/s')) { return value * 1000000; // Convert EH/s to TH/s } else if (unit.includes('gh/s')) { return value / 1000; // Convert GH/s to TH/s } else if (unit.includes('mh/s')) { return value / 1000000; // Convert MH/s to TH/s } else if (unit.includes('kh/s')) { return value / 1000000000; // Convert KH/s to TH/s } else if (unit.includes('h/s') && !unit.includes('th/s') && !unit.includes('ph/s') && !unit.includes('eh/s') && !unit.includes('gh/s') && !unit.includes('mh/s') && !unit.includes('kh/s')) { return value / 1000000000000; // Convert H/s to TH/s } else { // Assume TH/s if unit is not recognized return value; } } // 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(`
`); $connectionStatus = $("#connectionStatus"); } $connectionStatus.html(` ${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 very large values (1M+) if (value >= 1000000) { return (value / 1000000).toFixed(1) + 'M'; } // For large values (1K+) else if (value >= 1000) { return (value / 1000).toFixed(1) + 'K'; } // For values between 10 and 1000 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", "ONLINE "); } else { updateElementHTML("miner_status", "OFFLINE "); } } } // 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 with Dynamic Hashrate Selection function updateChartWithNormalizedData(chart, data) { if (!chart || !data) { console.warn("Cannot update chart - chart or data is null"); return; } try { // Always update the 24hr average line even if we don't have data points yet const avg24hr = parseFloat(data.hashrate_24hr || 0); const avg24hrUnit = data.hashrate_24hr_unit ? data.hashrate_24hr_unit.toLowerCase() : 'th/s'; const normalizedAvg = normalizeHashrate(avg24hr, avg24hrUnit); if (!isNaN(normalizedAvg) && chart.options.plugins.annotation && chart.options.plugins.annotation.annotations && chart.options.plugins.annotation.annotations.averageLine) { const annotation = chart.options.plugins.annotation.annotations.averageLine; annotation.yMin = normalizedAvg; annotation.yMax = normalizedAvg; annotation.label.content = '24HR AVG: ' + normalizedAvg.toFixed(1) + ' TH/S'; } // Detect low hashrate devices (Bitaxe < 2 TH/s) const hashrate60sec = parseFloat(data.hashrate_60sec || 0); const hashrate60secUnit = data.hashrate_60sec_unit ? data.hashrate_60sec_unit.toLowerCase() : 'th/s'; const normalizedHashrate60sec = normalizeHashrate(hashrate60sec, hashrate60secUnit); const hashrate3hr = parseFloat(data.hashrate_3hr || 0); const hashrate3hrUnit = data.hashrate_3hr_unit ? data.hashrate_3hr_unit.toLowerCase() : 'th/s'; const normalizedHashrate3hr = normalizeHashrate(hashrate3hr, hashrate3hrUnit); // Choose which hashrate average to display based on device characteristics let useHashrate3hr = false; // For devices with hashrate under 2 TH/s, use the 3hr average if: // 1. Their 60sec average is zero (appears offline) AND // 2. Their 3hr average shows actual mining activity if (normalizedHashrate3hr < 2.0) { if (normalizedHashrate60sec < 0.01 && normalizedHashrate3hr > 0.01) { useHashrate3hr = true; console.log("Low hashrate device detected. Using 3hr average instead of 60sec average."); } } // Process history data if available if (data.arrow_history && data.arrow_history.hashrate_60sec) { console.log("History data received:", data.arrow_history.hashrate_60sec); // 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; if (historyData && historyData.length > 0) { // Add day info to labels if they cross midnight let prevHour = -1; let dayCount = 0; chart.data.labels = historyData.map(item => { const timeStr = item.time; // Convert the time string to a Date object in Los Angeles timezone 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 { // Use current date if format is unexpected return timeStr; } // Create a date object for today with the time const now = new Date(); const timeDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), parseInt(timeParts[0]), parseInt(timeParts[1]), parseInt(timeParts[2] || 0)); // Format in 12-hour time for Los Angeles (Pacific Time) // The options define Pacific Time and 12-hour format without AM/PM try { let formattedTime = timeDate.toLocaleTimeString('en-US', { timeZone: dashboardTimezone, 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); return timeStr.substring(0, 5); // Fallback to original format } }); chart.data.datasets[0].data = historyData.map(item => { const val = parseFloat(item.value); const unit = item.unit || 'th/s'; // Ensure unit is assigned return normalizeHashrate(val, unit); }); // 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)'; const values = chart.data.datasets[0].data.filter(v => !isNaN(v) && v !== null); if (values.length > 0) { const max = Math.max(...values); const min = Math.min(...values.filter(v => v > 0)) || 0; chart.options.scales.y.min = min * 0.8; chart.options.scales.y.max = max * 1.2; const range = max - min; if (range > 1000) { chart.options.scales.y.ticks.stepSize = 500; } else if (range > 100) { chart.options.scales.y.ticks.stepSize = 50; } else if (range > 10) { chart.options.scales.y.ticks.stepSize = 5; } else { chart.options.scales.y.ticks.stepSize = 1; } } } } else { // No history data, just use the current point // Format current time in 12-hour format for Los Angeles timezone without AM/PM const now = new Date(); let currentTime; try { currentTime = now.toLocaleTimeString('en-US', { timeZone: dashboardTimezone, 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 based on our earlier logic let currentValue, currentUnit; if (useHashrate3hr) { currentValue = parseFloat(data.hashrate_3hr || 0); currentUnit = data.hashrate_3hr_unit ? data.hashrate_3hr_unit.toLowerCase() : 'th/s'; chart.data.datasets[0].label = 'Hashrate Trend (3HR AVG)'; } else { currentValue = parseFloat(data.hashrate_60sec || 0); currentUnit = data.hashrate_60sec_unit ? data.hashrate_60sec_unit.toLowerCase() : 'th/s'; chart.data.datasets[0].label = 'Hashrate Trend (60SEC AVG)'; } const normalizedValue = normalizeHashrate(currentValue, currentUnit); chart.data.labels = [currentTime]; chart.data.datasets[0].data = [normalizedValue]; } // In updateChartWithNormalizedData function 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.bottom = '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 { // Update color based on current theme chart.lowHashrateIndicator.style.color = getCurrentTheme().PRIMARY; // Show the indicator if it already exists chart.lowHashrateIndicator.style.display = 'block'; } } else if (chart.lowHashrateIndicator) { // Hide the indicator when not in low hashrate mode chart.lowHashrateIndicator.style.display = 'none'; } chart.update('none'); } catch (chartError) { console.error("Error updating chart:", chartError); } } // Main UI update function with hashrate normalization 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; // If this is the initial load, force a reset of all arrows if (initialLoad) { arrowIndicator.forceApplyArrows(); initialLoad = false; } // Cache jQuery selectors for performance and use safe update methods // 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); } } // 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)); updateElementText("btc_price", data.btc_price != null ? "$" + numberWithCommas(parseFloat(data.btc_price).toFixed(2)) : "N/A" ); // 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 updateElementText("daily_revenue", "$" + numberWithCommas(data.daily_revenue.toFixed(2))); // Daily power cost updateElementText("daily_power_cost", "$" + numberWithCommas(data.daily_power_cost.toFixed(2))); // Daily profit USD - Add red color if negative const dailyProfitUSD = data.daily_profit_usd; const dailyProfitElement = document.getElementById("daily_profit_usd"); if (dailyProfitElement) { dailyProfitElement.textContent = "$" + numberWithCommas(dailyProfitUSD.toFixed(2)); if (dailyProfitUSD < 0) { // Use setAttribute to properly set the style with !important dailyProfitElement.setAttribute("style", "color: #ff5555 !important; font-weight: bold !important; text-shadow: 0 0 6px rgba(255, 85, 85, 0.6) !important;"); } else { // Clear the style attribute completely instead of setting it to empty dailyProfitElement.removeAttribute("style"); } } // Monthly profit USD - Add red color if negative const monthlyProfitUSD = data.monthly_profit_usd; const monthlyProfitElement = document.getElementById("monthly_profit_usd"); if (monthlyProfitElement) { monthlyProfitElement.textContent = "$" + numberWithCommas(monthlyProfitUSD.toFixed(2)); if (monthlyProfitUSD < 0) { // Use setAttribute to properly set the style with !important monthlyProfitElement.setAttribute("style", "color: #ff5555 !important; font-weight: bold !important; text-shadow: 0 0 6px rgba(255, 85, 85, 0.6) !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 + " 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; text-shadow: 0 0 10px rgba(50, 205, 50, 0.6) !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; text-shadow: 0 0 6px rgba(50, 205, 50, 0.6) !important; animation: none !important;"); } else if (totalDays > 20) { $("#est_time_to_payout").attr("style", "color: #ff5555 !important; text-shadow: 0 0 6px rgba(255, 85, 85, 0.6) !important; animation: none !important;"); } else { $("#est_time_to_payout").attr("style", "color: #ffd700 !important; text-shadow: 0 0 6px rgba(255, 215, 0, 0.6) !important; animation: none !important;"); } } else { $("#est_time_to_payout").attr("style", "color: #ffd700 !important; text-shadow: 0 0 6px rgba(255, 215, 0, 0.6) !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", "Last Updated: " + timestampToUse.toLocaleString('en-US', options) + ""); 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", "Last Updated: " + now.toLocaleString() + ""); } // 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); } }); } } // Replace your entire function with this version function applyDeepSeaTheme() { // Check if we're already applying the theme to prevent recursion if (isApplyingTheme) { console.log("Theme application already in progress, avoiding recursion"); return; } // Set the guard flag isApplyingTheme = true; try { console.log("Applying DeepSea theme..."); // Create or update CSS variables for the DeepSea theme const styleElement = document.createElement('style'); styleElement.id = 'deepSeaThemeStyles'; // Give it an ID so we can check if it exists // Enhanced CSS with your requested changes styleElement.textContent = ` /* Base theme variables */ :root { --primary-color: #0088cc !important; --bitcoin-orange: #0088cc !important; --bitcoin-orange-rgb: 0, 136, 204 !important; --bg-gradient: linear-gradient(135deg, #0a0a0a, #131b20) !important; --accent-color: #00b3ff !important; --header-bg: linear-gradient(to right, #0088cc, #005580) !important; --card-header-bg: linear-gradient(to right, #0088cc, #006699) !important; --progress-bar-color: #0088cc !important; --link-color: #0088cc !important; --link-hover-color: #00b3ff !important; /* Standardized text shadow values */ --blue-text-shadow: 0 0 10px rgba(0, 136, 204, 0.8), 0 0 5px rgba(0, 136, 204, 0.5); --yellow-text-shadow: 0 0 10px rgba(255, 215, 0, 0.8), 0 0 5px rgba(255, 215, 0, 0.5); --green-text-shadow: 0 0 10px rgba(50, 205, 50, 0.8), 0 0 5px rgba(50, 205, 50, 0.5); --red-text-shadow: 0 0 10px rgba(255, 85, 85, 0.8), 0 0 5px rgba(255, 85, 85, 0.5); --white-text-shadow: 0 0 10px rgba(255, 255, 255, 0.8), 0 0 5px rgba(255, 255, 255, 0.5); --cyan-text-shadow: 0 0 10px rgba(0, 255, 255, 0.8), 0 0 5px rgba(0, 255, 255, 0.5); } /* Blue elements - main theme elements */ .card-header, .card > .card-header, .container-fluid .card > .card-header { background: linear-gradient(to right, #0088cc, #006699) !important; border-bottom: 1px solid #0088cc !important; text-shadow: var(--blue-text-shadow) !important; color: #fff !important; } .card { border: 1px solid #0088cc !important; box-shadow: 0 0 5px rgba(0, 136, 204, 0.3) !important; } /* Navigation and interface elements */ .nav-link { border: 1px solid #0088cc !important; color: #0088cc !important; } .nav-link:hover, .nav-link.active { background-color: #0088cc !important; color: #fff !important; box-shadow: 0 0 10px rgba(0, 136, 204, 0.5) !important; } #terminal-cursor { background-color: #0088cc !important; box-shadow: 0 0 5px rgba(0, 136, 204, 0.8) !important; } #lastUpdated { color: #0088cc !important; } /* Chart and progress elements */ .bitcoin-progress-inner { background: linear-gradient(90deg, #0088cc, #00b3ff) !important; } .bitcoin-progress-container { border: 1px solid #0088cc !important; box-shadow: 0 0 8px rgba(0, 136, 204, 0.5) !important; } h1, .text-center h1 { color: #0088cc !important; text-shadow: var(--blue-text-shadow) !important; } .nav-badge { background-color: #0088cc !important; } /* ===== COLOR SPECIFIC STYLING ===== */ /* YELLOW - SATOSHI EARNINGS & BTC PRICE */ /* All Satoshi earnings in yellow with consistent text shadow */ #daily_mined_sats, #monthly_mined_sats, #estimated_earnings_per_day_sats, #estimated_earnings_next_block_sats, #estimated_rewards_in_window_sats, #btc_price, /* BTC Price in yellow */ .card:contains('SATOSHI EARNINGS') span.metric-value { color: #ffd700 !important; /* Bitcoin gold/yellow */ text-shadow: var(--yellow-text-shadow) !important; } /* More specific selectors for Satoshi values */ span.metric-value[id$="_sats"] { color: #ffd700 !important; text-shadow: var(--yellow-text-shadow) !important; } /* Retaining original yellow for specific elements */ .est_time_to_payout:not(.green):not(.red) { color: #ffd700 !important; text-shadow: var(--yellow-text-shadow) !important; } /* GREEN - POSITIVE USD VALUES */ /* USD earnings that are positive should be green */ .metric-value.green, span.green, #daily_revenue:not([style*="color: #ff"]), #monthly_profit_usd:not([style*="color: #ff"]), #daily_profit_usd:not([style*="color: #ff"]) { color: #32CD32 !important; /* Lime green */ text-shadow: var(--green-text-shadow) !important; } /* Status indicators remain green */ .status-green { color: #32CD32 !important; text-shadow: var(--green-text-shadow) !important; } .online-dot { background: #32CD32 !important; box-shadow: 0 0 10px #32CD32, 0 0 20px #32CD32 !important; } /* RED - NEGATIVE USD VALUES & WARNINGS */ /* Red for negative values and warnings */ .metric-value.red, span.red, .status-red, #daily_power_cost { color: #ff5555 !important; text-shadow: var(--red-text-shadow) !important; } .offline-dot { background: #ff5555 !important; box-shadow: 0 0 10px #ff5555, 0 0 20px #ff5555 !important; } /* WHITE - Network stats and worker data */ #block_number, #difficulty, #network_hashrate, #pool_fees_percentage, #workers_hashing, #last_share, #blocks_found, #last_block_height { color: #ffffff !important; text-shadow: var(--white-text-shadow) !important; } /* CYAN - Time ago in last block */ #last_block_time { color: #00ffff !important; /* Cyan */ text-shadow: var(--cyan-text-shadow) !important; } /* BLUE - Pool statistics */ #pool_total_hashrate { color: #0088cc !important; text-shadow: var(--blue-text-shadow) !important; } /* Hashrate values are white */ #hashrate_24hr, #hashrate_3hr, #hashrate_10min, #hashrate_60sec { color: white !important; text-shadow: var(--white-text-shadow) !important; } /* Pool luck/efficiency colors - PRESERVE EXISTING */ #pool_luck.very-lucky { color: #32CD32 !important; /* Very lucky - bright green */ text-shadow: var(--green-text-shadow) !important; } #pool_luck.lucky { color: #90EE90 !important; /* Lucky - light green */ text-shadow: 0 0 10px rgba(144, 238, 144, 0.8), 0 0 5px rgba(144, 238, 144, 0.5) !important; } #pool_luck.normal-luck { color: #F0E68C !important; /* Normal - khaki */ text-shadow: 0 0 10px rgba(240, 230, 140, 0.8), 0 0 5px rgba(240, 230, 140, 0.5) !important; } #pool_luck.unlucky { color: #ff5555 !important; /* Unlucky - red */ text-shadow: var(--red-text-shadow) !important; } /* Congrats message */ #congratsMessage { background: #0088cc !important; box-shadow: 0 0 15px rgba(0, 136, 204, 0.7) !important; } /* Animations */ @keyframes waitingPulse { 0%, 100% { box-shadow: 0 0 10px #0088cc, 0 0 15px #0088cc !important; opacity: 0.8; } 50% { box-shadow: 0 0 20px #0088cc, 0 0 35px #0088cc !important; opacity: 1; } } @keyframes glow { 0%, 100% { box-shadow: 0 0 10px #0088cc, 0 0 15px #0088cc !important; } 50% { box-shadow: 0 0 15px #0088cc, 0 0 25px #0088cc !important; } } `; // Check if our style element already exists const existingStyle = document.getElementById('deepSeaThemeStyles'); if (existingStyle) { existingStyle.parentNode.removeChild(existingStyle); } // Add our new style element to the head document.head.appendChild(styleElement); // Update page title document.title = document.title.replace("BTC-OS", "DeepSea"); document.title = document.title.replace("Bitcoin", "DeepSea"); // Update header text const headerElement = document.querySelector('h1'); if (headerElement) { headerElement.innerHTML = headerElement.innerHTML.replace("BTC-OS", "DeepSea"); headerElement.innerHTML = headerElement.innerHTML.replace("BITCOIN", "DEEPSEA"); } console.log("DeepSea theme applied with color adjustments"); } finally { // Always reset the guard flag when done, even if there's an error isApplyingTheme = false; } } // Make the function accessible globally so main.js can check for it window.applyDeepSeaTheme = applyDeepSeaTheme; $(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+) if (value >= 1000000) { return (value / 1000000).toFixed(1) + 'M'; } // For large values (1K+) else if (value >= 1000) { return (value / 1000).toFixed(1) + 'K'; } // For values between 10 and 1000 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() { // Listen for storage events to detect theme changes from other tabs/windows window.addEventListener('storage', function (event) { if (event.key === 'useDeepSeaTheme') { // Update chart with new theme colors if (trendChart) { // Trigger chart update with new colors trendChart.destroy(); trendChart = initializeChart(); updateChartWithNormalizedData(trendChart, latestMetrics); } // Update other UI elements that depend on theme colors updateRefreshButtonColor(); // Trigger a custom event that other components can listen to $(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 $("