From 44ffdba5223afa37cbdbda32d631b6dbbe3c7ab5 Mon Sep 17 00:00:00 2001 From: DJObleezy <156115770+Djobleezy@users.noreply.github.com> Date: Thu, 27 Mar 2025 09:49:55 -0700 Subject: [PATCH] Update main.js --- static/js/main.js | 573 +++++++++++++++++++++++++++++++++------------- 1 file changed, 411 insertions(+), 162 deletions(-) diff --git a/static/js/main.js b/static/js/main.js index dd38d63..d6570f6 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -1,10 +1,320 @@ "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] = ""; + }); + } + + // 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 variables let previousMetrics = {}; -let persistentArrows = {}; -let serverTimeOffset = 0; -let serverStartTime = null; let latestMetrics = null; let initialLoad = true; let trendData = []; @@ -17,6 +327,10 @@ 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']); @@ -83,9 +397,6 @@ function setupEventSource() { const baseUrl = window.location.origin; const streamUrl = `${baseUrl}/stream`; - console.log("Current path:", window.location.pathname); - console.log("Using stream URL:", streamUrl); - // Clear any existing ping interval if (pingInterval) { clearInterval(pingInterval); @@ -117,11 +428,10 @@ function setupEventSource() { eventSource.close(); setupEventSource(); } - }, 10000); // Check every 10 seconds + }, 30000); // Check every 30 seconds }; eventSource.onmessage = function (e) { - console.log("SSE message received"); lastPingTime = Date.now(); // Update ping time on any message try { @@ -129,7 +439,6 @@ function setupEventSource() { // Handle different message types if (data.type === "ping") { - console.log("Ping received:", data); // Update connection count if available if (data.connections !== undefined) { console.log(`Active connections: ${data.connections}`); @@ -216,7 +525,6 @@ function setupEventSource() { }; window.eventSource = eventSource; - console.log("EventSource setup complete"); // Set a timeout to detect if connection is established connectionLostTimeout = setTimeout(function () { @@ -275,6 +583,9 @@ function hideConnectionIssue() { function manualRefresh() { console.log("Manually refreshing data..."); + // Prepare arrow indicators for a new refresh cycle + arrowIndicator.prepareForRefresh(); + $.ajax({ url: '/api/metrics', method: 'GET', @@ -282,8 +593,9 @@ function manualRefresh() { timeout: 15000, // 15 second timeout success: function (data) { console.log("Manual refresh successful"); - lastPingTime = Date.now(); // Update ping time + lastPingTime = Date.now(); latestMetrics = data; + updateUI(); hideConnectionIssue(); @@ -462,85 +774,9 @@ function updateServerTime() { }); } -// FIXED: Update UI indicators (arrows) with unit normalization +// Update UI indicators (arrows) - replaced with ArrowIndicator call function updateIndicators(newMetrics) { - console.log("Updating indicators with new metrics"); - - const keys = [ - "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" - ]; - - keys.forEach(function (key) { - const newVal = parseFloat(newMetrics[key]); - if (isNaN(newVal)) return; - - // First try to calculate arrow based on comparison with previous value - let arrowCalculated = false; - const oldVal = parseFloat(previousMetrics[key]); - - if (!isNaN(oldVal)) { - // For hashrate values, normalize both values to the same unit before comparison - if (key.includes('hashrate')) { - const newUnit = newMetrics[key + '_unit'] || 'th/s'; - const oldUnit = previousMetrics[key + '_unit'] || 'th/s'; - - const normalizedNewVal = normalizeHashrate(newVal, newUnit); - const normalizedOldVal = normalizeHashrate(oldVal, oldUnit); - - // Lower threshold to 0.1% to catch more changes - if (normalizedNewVal > normalizedOldVal * 1.001) { - persistentArrows[key] = ""; - arrowCalculated = true; - } else if (normalizedNewVal < normalizedOldVal * 0.999) { - persistentArrows[key] = ""; - arrowCalculated = true; - } - } else { - // Lower threshold to 0.1% for non-hashrate values too - if (newVal > oldVal * 1.001) { - persistentArrows[key] = ""; - arrowCalculated = true; - } else if (newVal < oldVal * 0.999) { - persistentArrows[key] = ""; - arrowCalculated = true; - } - } - } - - // If we couldn't calculate arrow from comparison or initial load - // Try to use arrow_history - if (!arrowCalculated && newMetrics.arrow_history && newMetrics.arrow_history[key] && newMetrics.arrow_history[key].length > 0) { - const historyArr = newMetrics.arrow_history[key]; - for (let i = historyArr.length - 1; i >= 0; i--) { - if (historyArr[i].arrow !== "") { - if (historyArr[i].arrow === "↑") { - persistentArrows[key] = ""; - arrowCalculated = true; - } else if (historyArr[i].arrow === "↓") { - persistentArrows[key] = ""; - arrowCalculated = true; - } - break; - } - } - } - - // Update indicator in DOM - const indicator = document.getElementById("indicator_" + key); - if (indicator) { - indicator.innerHTML = persistentArrows[key] || ""; - } else { - console.warn(`Missing indicator element for: ${key}`); - } - }); - - // Store current metrics for next comparison - previousMetrics = { ...newMetrics }; + arrowIndicator.updateIndicators(newMetrics); } // Helper function to safely update element text content @@ -625,8 +861,6 @@ function updateChartWithNormalizedData(chart, data) { if (data.arrow_history && data.arrow_history.hashrate_60sec) { const historyData = data.arrow_history.hashrate_60sec; if (historyData && historyData.length > 0) { - console.log(`Updating chart with ${historyData.length} data points`); - // Store the current unit for reference const currentUnit = data.hashrate_60sec_unit ? data.hashrate_60sec_unit.toLowerCase() : 'th/s'; @@ -676,12 +910,8 @@ function updateChartWithNormalizedData(chart, data) { chart.options.scales.y.ticks.stepSize = 1; } } - } else { - console.log("No history data points available yet"); } } else { - console.log("No hashrate_60sec history available yet"); - // If there's no history data, create a starting point using current hashrate if (data.hashrate_60sec) { const currentTime = new Date().toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit' }); @@ -690,7 +920,6 @@ function updateChartWithNormalizedData(chart, data) { chart.data.labels = [currentTime]; chart.data.datasets[0].data = [normalizedValue]; - console.log("Created initial data point with current hashrate"); } } @@ -711,39 +940,9 @@ function updateUI() { try { const data = latestMetrics; - // If there's execution time data, log it - if (data.execution_time) { - console.log(`Server metrics fetch took ${data.execution_time.toFixed(2)}s`); - } - - // Ensure persistentArrows has initial values from arrow_history on first load - if (initialLoad && data.arrow_history) { - console.log("First load - ensuring arrows are initialized from history"); - for (const key in data.arrow_history) { - const historyArr = data.arrow_history[key]; - if (historyArr && historyArr.length > 0) { - // Look for the most recent arrow in history - for (let i = historyArr.length - 1; i >= 0; i--) { - if (historyArr[i].arrow !== "") { - if (historyArr[i].arrow === "↑") { - persistentArrows[key] = ""; - } else if (historyArr[i].arrow === "↓") { - persistentArrows[key] = ""; - } - break; - } - } - } - } - - // Immediately apply the first set of arrows - Object.keys(persistentArrows).forEach(key => { - const indicator = document.getElementById("indicator_" + key); - if (indicator) { - indicator.innerHTML = persistentArrows[key] || ""; - } - }); - + // If this is the initial load, force a reset of all arrows + if (initialLoad) { + arrowIndicator.forceApplyArrows(); initialLoad = false; } @@ -871,18 +1070,100 @@ function updateUI() { 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); +} + // Document ready initialization $(document).ready(function () { + // 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); + }; + + // 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(); + // Apply any saved arrows to DOM on page load + arrowIndicator.forceApplyArrows(); + // Initialize BitcoinMinuteRefresh with our refresh function - BitcoinMinuteRefresh.initialize(manualRefresh); + 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); + } + } // Set up event source for SSE setupEventSource(); @@ -940,38 +1221,6 @@ $(document).ready(function () { } } }, 30000); // Check every 30 seconds -}); - -// 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 30 seconds - setInterval(updateNotificationBadge, 30000); -} - -// Add to document ready -$(document).ready(function () { - // Existing code... // Initialize notification badge initNotificationBadge();