"use strict"; // Global variables for workers dashboard let workerData = null; let refreshTimer; const pageLoadTime = Date.now(); let lastManualRefreshTime = 0; const filterState = { currentFilter: 'all', searchTerm: '' }; let miniChart = null; let connectionRetryCount = 0; // Server time variables for uptime calculation - synced with main dashboard let serverTimeOffset = 0; let serverStartTime = null; // New variable to track custom refresh timing const MIN_REFRESH_INTERVAL = 10000; // Minimum 10 seconds between refreshes // Hashrate Normalization Utilities // Helper function to normalize hashrate to TH/s for consistent graphing function normalizeHashrate(value, unit = 'th/s') { if (!value || isNaN(value)) return 0; unit = unit.toLowerCase(); const unitConversion = { 'ph/s': 1000, 'eh/s': 1000000, 'gh/s': 1 / 1000, 'mh/s': 1 / 1000000, 'kh/s': 1 / 1000000000, 'h/s': 1 / 1000000000000 }; return unitConversion[unit] !== undefined ? value * unitConversion[unit] : value; } // Helper function to format hashrate values for display function formatHashrateForDisplay(value, unit) { if (isNaN(value) || value === null || value === undefined) return "N/A"; const normalizedValue = unit ? normalizeHashrate(value, unit) : value; const unitRanges = [ { threshold: 1000000, unit: 'EH/s', divisor: 1000000 }, { threshold: 1000, unit: 'PH/s', divisor: 1000 }, { threshold: 1, unit: 'TH/s', divisor: 1 }, { threshold: 0.001, unit: 'GH/s', divisor: 1 / 1000 }, { threshold: 0, unit: 'MH/s', divisor: 1 / 1000000 } ]; for (const range of unitRanges) { if (normalizedValue >= range.threshold) { return (normalizedValue / range.divisor).toFixed(2) + ' ' + range.unit; } } return (normalizedValue * 1000000).toFixed(2) + ' MH/s'; } // Initialize the page $(document).ready(function () { console.log("Worker page initializing..."); initNotificationBadge(); initializePage(); updateServerTime(); window.manualRefresh = fetchWorkerData; setTimeout(() => { if (typeof BitcoinMinuteRefresh !== 'undefined' && BitcoinMinuteRefresh.initialize) { BitcoinMinuteRefresh.initialize(window.manualRefresh); console.log("BitcoinMinuteRefresh initialized with refresh function"); } else { console.warn("BitcoinMinuteRefresh not available"); } }, 500); fetchWorkerData(); $('.filter-button').click(function () { $('.filter-button').removeClass('active'); $(this).addClass('active'); filterState.currentFilter = $(this).data('filter'); filterWorkers(); }); $('#worker-search').on('input', function () { filterState.searchTerm = $(this).val().toLowerCase(); filterWorkers(); }); }); // 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); }); })(); // Initialize page elements function initializePage() { console.log("Initializing page elements..."); if (document.getElementById('total-hashrate-chart')) { initializeMiniChart(); } $('#worker-grid').html('
Loading worker data...
'); if (!$('#retry-button').length) { $('body').append(''); $('#retry-button').on('click', function () { $(this).text('Retrying...').prop('disabled', true); fetchWorkerData(true); setTimeout(() => { $('#retry-button').text('Retry Loading Data').prop('disabled', false); }, 3000); }); } } // Update unread notifications badge in navigation function updateNotificationBadge() { $.ajax({ url: "/api/notifications/unread_count", method: "GET", success: function (data) { const unreadCount = data.unread_count; const badge = $("#nav-unread-badge"); if (unreadCount > 0) { badge.text(unreadCount).show(); } else { badge.hide(); } } }); } // Initialize notification badge checking function initNotificationBadge() { updateNotificationBadge(); setInterval(updateNotificationBadge, 60000); } // Server time update via polling - enhanced to use shared storage function updateServerTime() { console.log("Updating server time..."); try { const storedOffset = localStorage.getItem('serverTimeOffset'); const storedStartTime = localStorage.getItem('serverStartTime'); if (storedOffset && storedStartTime) { serverTimeOffset = parseFloat(storedOffset); serverStartTime = parseFloat(storedStartTime); console.log("Using stored server time offset:", serverTimeOffset, "ms"); if (typeof BitcoinMinuteRefresh !== 'undefined' && BitcoinMinuteRefresh.updateServerTime) { BitcoinMinuteRefresh.updateServerTime(serverTimeOffset, serverStartTime); } return; } } catch (e) { console.error("Error reading stored server time:", e); } $.ajax({ url: "/api/time", method: "GET", timeout: 5000, success: function (data) { serverTimeOffset = new Date(data.server_timestamp).getTime() - Date.now(); serverStartTime = new Date(data.server_start_time).getTime(); localStorage.setItem('serverTimeOffset', serverTimeOffset.toString()); localStorage.setItem('serverStartTime', serverStartTime.toString()); if (typeof BitcoinMinuteRefresh !== 'undefined' && BitcoinMinuteRefresh.updateServerTime) { BitcoinMinuteRefresh.updateServerTime(serverTimeOffset, serverStartTime); } console.log("Server time synchronized. Offset:", serverTimeOffset, "ms"); }, error: function (jqXHR, textStatus, errorThrown) { console.error("Error fetching server time:", textStatus, errorThrown); } }); } // Utility functions to show/hide loader function showLoader() { $("#loader").show(); } function hideLoader() { $("#loader").hide(); } // Fetch worker data with better pagination and progressive loading function fetchWorkerData(forceRefresh = false) { console.log("Fetching worker data..."); lastManualRefreshTime = Date.now(); $('#worker-grid').addClass('loading-fade'); showLoader(); // For large datasets, better to use streaming or chunked approach // First fetch just the summary data and first page const initialRequest = $.ajax({ url: `/api/workers?summary=true&page=1${forceRefresh ? '&force=true' : ''}`, method: 'GET', dataType: 'json', timeout: 15000 }); initialRequest.then(summaryData => { // Store summary stats immediately to update UI workerData = summaryData || {}; workerData.workers = workerData.workers || []; // Update summary stats while we load more workers updateSummaryStats(); updateMiniChart(); updateLastUpdated(); const totalPages = Math.ceil((workerData.workers_total || 0) / 100); // Assuming 100 workers per page const pagesToFetch = Math.min(totalPages, 20); // Limit to 20 pages max if (pagesToFetch <= 1) { // We already have all the data from the first request finishWorkerLoad(); return; } // Progress indicator const progressBar = $('
Loading workers: 1/' + pagesToFetch + '
'); $('#worker-grid').html(progressBar); // Load remaining pages in batches to avoid overwhelming the browser loadWorkerPages(2, pagesToFetch, progressBar); }).catch(error => { console.error("Error fetching initial worker data:", error); $('#worker-grid').html('
Error loading workers.
'); $('.retry-btn').on('click', () => fetchWorkerData(true)); hideLoader(); $('#worker-grid').removeClass('loading-fade'); }); } // Load worker pages in batches function loadWorkerPages(startPage, totalPages, progressBar) { const BATCH_SIZE = 3; // Number of pages to load in parallel const endPage = Math.min(startPage + BATCH_SIZE - 1, totalPages); const requests = []; for (let page = startPage; page <= endPage; page++) { requests.push( $.ajax({ url: `/api/workers?page=${page}`, method: 'GET', dataType: 'json', timeout: 15000 }) ); } Promise.all(requests) .then(pages => { // Process each page pages.forEach(pageData => { if (pageData && pageData.workers && pageData.workers.length > 0) { // Append new workers to our list efficiently workerData.workers = workerData.workers.concat(pageData.workers); } }); // Update progress const progress = Math.min(endPage / totalPages * 100, 100); progressBar.find('.progress-bar').css('width', progress + '%'); progressBar.find('.progress-text').text(`Loading workers: ${endPage}/${totalPages}`); if (endPage < totalPages) { // Continue with next batch setTimeout(() => loadWorkerPages(endPage + 1, totalPages, progressBar), 100); } else { // All pages loaded finishWorkerLoad(); } }) .catch(error => { console.error(`Error fetching worker pages ${startPage}-${endPage}:`, error); // Continue with what we have so far finishWorkerLoad(); }); } // Finish loading process with optimized rendering function finishWorkerLoad() { // Deduplicate workers more efficiently with a Map const uniqueWorkersMap = new Map(); workerData.workers.forEach(worker => { if (worker.name) { uniqueWorkersMap.set(worker.name, worker); } }); workerData.workers = Array.from(uniqueWorkersMap.values()); // Notify BitcoinMinuteRefresh if (typeof BitcoinMinuteRefresh !== 'undefined' && BitcoinMinuteRefresh.notifyRefresh) { BitcoinMinuteRefresh.notifyRefresh(); } // Efficiently render workers with virtualized list approach renderWorkersList(); $('#retry-button').hide(); connectionRetryCount = 0; console.log(`Worker data updated successfully: ${workerData.workers.length} workers`); $('#worker-grid').removeClass('loading-fade'); hideLoader(); } // Virtualized list rendering for large datasets function renderWorkersList() { if (!workerData || !workerData.workers) { console.error("No worker data available"); return; } const workerGrid = $('#worker-grid'); workerGrid.empty(); const filteredWorkers = filterWorkersData(workerData.workers); if (filteredWorkers.length === 0) { workerGrid.html(`

No workers match your filter criteria

`); return; } // Performance optimization for large lists if (filteredWorkers.length > 200) { // For very large lists, render in batches const workerBatch = 100; const totalBatches = Math.ceil(filteredWorkers.length / workerBatch); console.log(`Rendering ${filteredWorkers.length} workers in ${totalBatches} batches`); // Render first batch immediately renderWorkerBatch(filteredWorkers.slice(0, workerBatch), workerGrid); // Render remaining batches with setTimeout to avoid UI freezing for (let i = 1; i < totalBatches; i++) { const start = i * workerBatch; const end = Math.min(start + workerBatch, filteredWorkers.length); setTimeout(() => { renderWorkerBatch(filteredWorkers.slice(start, end), workerGrid); // Update "loading more" message with progress const loadingMsg = workerGrid.find('.loading-more-workers'); if (loadingMsg.length) { if (i === totalBatches - 1) { loadingMsg.remove(); } else { loadingMsg.text(`Loading more workers... ${Math.min((i + 1) * workerBatch, filteredWorkers.length)}/${filteredWorkers.length}`); } } }, i * 50); // 50ms delay between batches } // Add "loading more" indicator at the bottom if (totalBatches > 1) { workerGrid.append(`
Loading more workers... ${workerBatch}/${filteredWorkers.length}
`); } } else { // For smaller lists, render all at once renderWorkerBatch(filteredWorkers, workerGrid); } } // Render a batch of workers efficiently function renderWorkerBatch(workers, container) { // Create a document fragment for better performance const fragment = document.createDocumentFragment(); // Calculate max hashrate once for this batch const maxHashrate = calculateMaxHashrate(); workers.forEach(worker => { const card = createWorkerCard(worker, maxHashrate); fragment.appendChild(card[0]); }); container.append(fragment); } // Calculate max hashrate once to avoid recalculating for each worker function calculateMaxHashrate() { let maxHashrate = 125; // Default fallback // First check if global hashrate data is available if (workerData && workerData.hashrate_24hr) { const globalHashrate = normalizeHashrate(workerData.hashrate_24hr, workerData.hashrate_24hr_unit || 'th/s'); if (globalHashrate > 0) { return Math.max(5, globalHashrate * 1.2); } } // If no global data, calculate from workers efficiently if (workerData && workerData.workers && workerData.workers.length > 0) { const onlineWorkers = workerData.workers.filter(w => w.status === 'online'); if (onlineWorkers.length > 0) { let maxWorkerHashrate = 0; // Find maximum hashrate without logging every worker onlineWorkers.forEach(w => { const hashrateValue = w.hashrate_24hr || w.hashrate_3hr || 0; const hashrateUnit = w.hashrate_24hr ? (w.hashrate_24hr_unit || 'th/s') : (w.hashrate_3hr_unit || 'th/s'); const normalizedRate = normalizeHashrate(hashrateValue, hashrateUnit); if (normalizedRate > maxWorkerHashrate) { maxWorkerHashrate = normalizedRate; } }); if (maxWorkerHashrate > 0) { return Math.max(5, maxWorkerHashrate * 1.2); } } } // Fallback to total hashrate if (workerData && workerData.total_hashrate) { const totalHashrate = normalizeHashrate(workerData.total_hashrate, workerData.hashrate_unit || 'th/s'); if (totalHashrate > 0) { return Math.max(5, totalHashrate * 1.2); } } return maxHashrate; } // Optimized worker card creation (removed debug logging) function createWorkerCard(worker, maxHashrate) { const card = $('
'); card.addClass(worker.status === 'online' ? 'worker-card-online' : 'worker-card-offline'); card.append(`
${worker.type}
`); card.append(`
${worker.name}
`); card.append(`
${worker.status.toUpperCase()}
`); // Use 3hr hashrate for display as in original code const normalizedHashrate = normalizeHashrate(worker.hashrate_3hr, worker.hashrate_3hr_unit || 'th/s'); const hashratePercent = Math.min(100, (normalizedHashrate / maxHashrate) * 100); const formattedHashrate = formatHashrateForDisplay(worker.hashrate_3hr, worker.hashrate_3hr_unit || 'th/s'); card.append(`
Hashrate (3hr):
${formattedHashrate}
`); // Format the last share using the proper method for timezone conversion let formattedLastShare = 'N/A'; if (worker.last_share && typeof worker.last_share === 'string') { try { const dateWithoutTZ = new Date(worker.last_share + 'Z'); formattedLastShare = dateWithoutTZ.toLocaleString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true, timeZone: window.dashboardTimezone || 'America/Los_Angeles' }); } catch (e) { formattedLastShare = worker.last_share; } } card.append(`
Last Share:
${formattedLastShare}
Earnings:
${worker.earnings.toFixed(8)}
`); return card; } // Filter worker data based on current filter state function filterWorkersData(workers) { if (!workers) return []; return workers.filter(worker => { const workerName = (worker.name || '').toLowerCase(); const isOnline = worker.status === 'online'; const workerType = (worker.type || '').toLowerCase(); const matchesFilter = filterState.currentFilter === 'all' || (filterState.currentFilter === 'online' && isOnline) || (filterState.currentFilter === 'offline' && !isOnline) || (filterState.currentFilter === 'asic' && workerType === 'asic') || (filterState.currentFilter === 'bitaxe' && workerType === 'bitaxe'); const matchesSearch = filterState.searchTerm === '' || workerName.includes(filterState.searchTerm); return matchesFilter && matchesSearch; }); } // Apply filter to rendered worker cards function filterWorkers() { if (!workerData || !workerData.workers) return; updateWorkerGrid(); } // Update summary stats with normalized hashrate display function updateSummaryStats() { if (!workerData) return; $('#workers-count').text(workerData.workers_total || 0); $('#workers-online').text(workerData.workers_online || 0); $('#workers-offline').text(workerData.workers_offline || 0); const onlinePercent = workerData.workers_total > 0 ? workerData.workers_online / workerData.workers_total : 0; $('.worker-ring').css('--online-percent', onlinePercent); const formattedHashrate = workerData.total_hashrate !== undefined ? formatHashrateForDisplay(workerData.total_hashrate, workerData.hashrate_unit || 'TH/s') : '0.0 TH/s'; $('#total-hashrate').text(formattedHashrate); $('#total-earnings').text(`${(workerData.total_earnings || 0).toFixed(8)} BTC`); $('#daily-sats').text(`${numberWithCommas(workerData.daily_sats || 0)} SATS`); } // Initialize mini chart function initializeMiniChart() { console.log("Initializing mini chart..."); const ctx = document.getElementById('total-hashrate-chart'); if (!ctx) { console.error("Mini chart canvas not found"); return; } const labels = Array(24).fill('').map((_, i) => i); const data = Array(24).fill(0).map(() => Math.random() * 100 + 700); miniChart = new Chart(ctx, { type: 'line', data: { labels: labels, datasets: [{ data: data, borderColor: '#1137F5', backgroundColor: 'rgba(57, 255, 20, 0.1)', fill: true, tension: 0.3, borderWidth: 1.5, pointRadius: 0 }] }, options: { responsive: true, maintainAspectRatio: false, scales: { x: { display: false }, y: { display: false, min: Math.min(...data) * 0.9, max: Math.max(...data) * 1.1 } }, plugins: { legend: { display: false }, tooltip: { enabled: false } }, animation: false, elements: { line: { tension: 0.4 } } } }); } // Update mini chart with real data and normalization function updateMiniChart() { if (!miniChart || !workerData || !workerData.hashrate_history) { console.log("Skipping mini chart update - missing data"); return; } const historyData = workerData.hashrate_history; if (!historyData || historyData.length === 0) { console.log("No hashrate history data available"); return; } const values = historyData.map(item => normalizeHashrate(parseFloat(item.value) || 0, item.unit || workerData.hashrate_unit || 'th/s')); const labels = historyData.map(item => item.time); miniChart.data.labels = labels; miniChart.data.datasets[0].data = values; const min = Math.min(...values.filter(v => v > 0)) || 0; const max = Math.max(...values) || 1; miniChart.options.scales.y.min = min * 0.9; miniChart.options.scales.y.max = max * 1.1; miniChart.update('none'); } // Update the last updated timestamp function updateLastUpdated() { if (!workerData || !workerData.timestamp) return; try { const timestamp = new Date(workerData.timestamp); // Get the configured timezone with a fallback const configuredTimezone = window.dashboardTimezone || 'America/Los_Angeles'; // Format with the configured timezone const options = { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true, timeZone: configuredTimezone // Explicitly use the configured timezone }; // Format the timestamp and update the DOM const formattedTime = timestamp.toLocaleString('en-US', options); $("#lastUpdated").html("Last Updated: " + formattedTime + ""); console.log(`Last updated timestamp using timezone: ${configuredTimezone}`); } catch (e) { console.error("Error formatting timestamp:", e); // Fallback to basic timestamp if there's an error $("#lastUpdated").html("Last Updated: " + new Date().toLocaleString() + ""); } } // Format numbers with commas function numberWithCommas(x) { if (x == null) return "N/A"; return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); }