Merge pull request #219 from gerlofvanek/memory

Fix potential sources of mem leaks + Various related fixes.
This commit is contained in:
tecnovert
2025-01-16 19:28:13 +00:00
committed by GitHub
3 changed files with 1215 additions and 718 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -487,6 +487,13 @@ const chartModule = {
chart: null, chart: null,
currentCoin: 'BTC', currentCoin: 'BTC',
loadStartTime: 0, loadStartTime: 0,
cleanup: () => {
if (chartModule.chart) {
chartModule.chart.destroy();
chartModule.chart = null;
}
},
verticalLinePlugin: { verticalLinePlugin: {
id: 'verticalLine', id: 'verticalLine',
beforeDraw: (chart, args, options) => { beforeDraw: (chart, args, options) => {
@@ -509,7 +516,7 @@ const chartModule = {
}, },
initChart: () => { initChart: () => {
const ctx = document.getElementById('coin-chart').getContext('2d'); const ctx = document.getElementById('coin-chart')?.getContext('2d');
if (!ctx) { if (!ctx) {
logger.error('Failed to get chart context. Make sure the canvas element exists.'); logger.error('Failed to get chart context. Make sure the canvas element exists.');
return; return;
@@ -568,7 +575,6 @@ const chartModule = {
callback: function(value) { callback: function(value) {
const date = new Date(value); const date = new Date(value);
if (config.currentResolution === 'day') { if (config.currentResolution === 'day') {
// Convert to AM/PM format
return date.toLocaleTimeString('en-US', { return date.toLocaleTimeString('en-US', {
hour: 'numeric', hour: 'numeric',
minute: '2-digit', minute: '2-digit',
@@ -668,13 +674,10 @@ const chartModule = {
}, },
plugins: [chartModule.verticalLinePlugin] plugins: [chartModule.verticalLinePlugin]
}); });
//console.log('Chart initialized:', chartModule.chart);
}, },
prepareChartData: (coinSymbol, data) => { prepareChartData: (coinSymbol, data) => {
if (!data) { if (!data) {
//console.error(`No data received for ${coinSymbol}`);
return []; return [];
} }
@@ -733,7 +736,6 @@ const chartModule = {
y: price y: price
})); }));
} else { } else {
//console.error(`Unexpected data structure for ${coinSymbol}:`, data);
return []; return [];
} }
@@ -742,7 +744,6 @@ const chartModule = {
y: point.y y: point.y
})); }));
} catch (error) { } catch (error) {
//console.error(`Error preparing chart data for ${coinSymbol}:`, error);
return []; return [];
} }
}, },
@@ -780,21 +781,17 @@ const chartModule = {
if (cachedData && Object.keys(cachedData.value).length > 0) { if (cachedData && Object.keys(cachedData.value).length > 0) {
data = cachedData.value; data = cachedData.value;
//console.log(`Using cached data for ${coinSymbol} (${config.currentResolution})`);
} else { } else {
//console.log(`Fetching fresh data for ${coinSymbol} (${config.currentResolution})`);
const allData = await api.fetchHistoricalDataXHR([coinSymbol]); const allData = await api.fetchHistoricalDataXHR([coinSymbol]);
data = allData[coinSymbol]; data = allData[coinSymbol];
if (!data || Object.keys(data).length === 0) { if (!data || Object.keys(data).length === 0) {
throw new Error(`No data returned for ${coinSymbol}`); throw new Error(`No data returned for ${coinSymbol}`);
} }
//console.log(`Caching new data for ${cacheKey}`);
cache.set(cacheKey, data, config.cacheTTL); cache.set(cacheKey, data, config.cacheTTL);
cachedData = null; cachedData = null;
} }
const chartData = chartModule.prepareChartData(coinSymbol, data); const chartData = chartModule.prepareChartData(coinSymbol, data);
//console.log(`Prepared chart data for ${coinSymbol}:`, chartData.slice(0, 5));
if (chartData.length === 0) { if (chartData.length === 0) {
throw new Error(`No valid chart data for ${coinSymbol}`); throw new Error(`No valid chart data for ${coinSymbol}`);
@@ -826,7 +823,6 @@ const chartModule = {
chartModule.chart.update('active'); chartModule.chart.update('active');
} else { } else {
//console.error('Chart object not initialized');
throw new Error('Chart object not initialized'); throw new Error('Chart object not initialized');
} }
@@ -835,7 +831,6 @@ const chartModule = {
ui.updateLoadTimeAndCache(loadTime, cachedData); ui.updateLoadTimeAndCache(loadTime, cachedData);
} catch (error) { } catch (error) {
//console.error(`Error updating chart for ${coinSymbol}:`, error);
ui.displayErrorMessage(`Failed to update chart for ${coinSymbol}: ${error.message}`); ui.displayErrorMessage(`Failed to update chart for ${coinSymbol}: ${error.message}`);
} finally { } finally {
chartModule.hideChartLoader(); chartModule.hideChartLoader();
@@ -847,7 +842,6 @@ const chartModule = {
const chart = document.getElementById('coin-chart'); const chart = document.getElementById('coin-chart');
if (!loader || !chart) { if (!loader || !chart) {
//console.warn('Chart loader or chart container elements not found');
return; return;
} }
@@ -860,49 +854,61 @@ const chartModule = {
const chart = document.getElementById('coin-chart'); const chart = document.getElementById('coin-chart');
if (!loader || !chart) { if (!loader || !chart) {
//console.warn('Chart loader or chart container elements not found');
return; return;
} }
loader.classList.add('hidden'); loader.classList.add('hidden');
chart.classList.remove('hidden'); chart.classList.remove('hidden');
}, }
}; };
Chart.register(chartModule.verticalLinePlugin); Chart.register(chartModule.verticalLinePlugin);
const volumeToggle = { const volumeToggle = {
isVisible: localStorage.getItem('volumeToggleState') === 'true', isVisible: localStorage.getItem('volumeToggleState') === 'true',
init: () => {
const toggleButton = document.getElementById('toggle-volume');
if (toggleButton) {
toggleButton.addEventListener('click', volumeToggle.toggle);
volumeToggle.updateVolumeDisplay();
}
},
toggle: () => {
volumeToggle.isVisible = !volumeToggle.isVisible;
localStorage.setItem('volumeToggleState', volumeToggle.isVisible.toString());
volumeToggle.updateVolumeDisplay();
},
updateVolumeDisplay: () => {
const volumeDivs = document.querySelectorAll('[id$="-volume-div"]');
volumeDivs.forEach(div => {
div.style.display = volumeToggle.isVisible ? 'flex' : 'none';
});
const toggleButton = document.getElementById('toggle-volume');
if (toggleButton) {
updateButtonStyles(toggleButton, volumeToggle.isVisible, 'green');
}
}
};
function updateButtonStyles(button, isActive, color) { cleanup: () => {
button.classList.toggle('text-' + color + '-500', isActive); const toggleButton = document.getElementById('toggle-volume');
button.classList.toggle('text-gray-600', !isActive); if (toggleButton) {
button.classList.toggle('dark:text-' + color + '-400', isActive); toggleButton.removeEventListener('click', volumeToggle.toggle);
button.classList.toggle('dark:text-gray-400', !isActive); }
},
init: () => {
volumeToggle.cleanup();
const toggleButton = document.getElementById('toggle-volume');
if (toggleButton) {
toggleButton.addEventListener('click', volumeToggle.toggle);
volumeToggle.updateVolumeDisplay();
}
},
toggle: () => {
volumeToggle.isVisible = !volumeToggle.isVisible;
localStorage.setItem('volumeToggleState', volumeToggle.isVisible.toString());
volumeToggle.updateVolumeDisplay();
},
updateVolumeDisplay: () => {
const volumeDivs = document.querySelectorAll('[id$="-volume-div"]');
volumeDivs.forEach(div => {
div.style.display = volumeToggle.isVisible ? 'flex' : 'none';
});
const toggleButton = document.getElementById('toggle-volume');
if (toggleButton) {
updateButtonStyles(toggleButton, volumeToggle.isVisible, 'green');
}
} }
};
function updateButtonStyles(button, isActive, color) {
button.classList.toggle('text-' + color + '-500', isActive);
button.classList.toggle('text-gray-600', !isActive);
button.classList.toggle('dark:text-' + color + '-400', isActive);
button.classList.toggle('dark:text-gray-400', !isActive);
}
const app = { const app = {
btcPriceUSD: 0, btcPriceUSD: 0,
@@ -918,90 +924,175 @@ const app = {
}, },
cacheTTL: 5 * 60 * 1000, // 5 minutes cacheTTL: 5 * 60 * 1000, // 5 minutes
minimumRefreshInterval: 60 * 1000, // 1 minute minimumRefreshInterval: 60 * 1000, // 1 minute
eventListeners: new Map(),
visibilityCleanup: null,
cleanup: () => {
if (app.autoRefreshInterval) {
clearTimeout(app.autoRefreshInterval);
app.autoRefreshInterval = null;
}
if (app.updateNextRefreshTimeRAF) {
cancelAnimationFrame(app.updateNextRefreshTimeRAF);
app.updateNextRefreshTimeRAF = null;
}
if (typeof app.visibilityCleanup === 'function') {
app.visibilityCleanup();
app.visibilityCleanup = null;
}
volumeToggle.cleanup();
app.removeEventListeners();
if (chartModule.chart) {
chartModule.chart.destroy();
chartModule.chart = null;
}
cache.clear();
},
removeEventListeners: () => {
app.eventListeners.forEach((listener, element) => {
if (element && typeof element.removeEventListener === 'function') {
element.removeEventListener(listener.type, listener.fn);
}
});
app.eventListeners.clear();
},
addEventListenerWithCleanup: (element, type, fn) => {
if (element && typeof element.addEventListener === 'function') {
element.addEventListener(type, fn);
app.eventListeners.set(element, { type, fn });
}
},
initResolutionButtons: () => {
const resolutionButtons = document.querySelectorAll('.resolution-button');
resolutionButtons.forEach(button => {
// Remove existing listeners first
const oldListener = button.getAttribute('data-resolution-listener');
if (oldListener && window[oldListener]) {
button.removeEventListener('click', window[oldListener]);
delete window[oldListener];
}
const listener = () => {
const resolution = button.id.split('-')[1];
const currentCoin = chartModule.currentCoin;
if (currentCoin !== 'WOW' || resolution === 'day') {
config.currentResolution = resolution;
chartModule.updateChart(currentCoin, true);
app.updateResolutionButtons(currentCoin);
}
};
const listenerName = `resolutionListener_${button.id}`;
window[listenerName] = listener;
button.setAttribute('data-resolution-listener', listenerName);
button.addEventListener('click', listener);
});
},
setupVisibilityHandler: () => {
const cleanup = () => {
if (window.visibilityHandler) {
document.removeEventListener('visibilitychange', window.visibilityHandler);
delete window.visibilityHandler;
}
};
cleanup();
window.visibilityHandler = () => {
if (!document.hidden && chartModule.chart) {
chartModule.updateChart(chartModule.currentCoin, true);
}
};
document.addEventListener('visibilitychange', window.visibilityHandler);
return cleanup;
},
init: () => { init: () => {
console.log('Initializing app...'); console.log('Initializing app...');
app.cleanup();
window.addEventListener('load', app.onLoad); window.addEventListener('load', app.onLoad);
app.loadLastRefreshedTime(); app.loadLastRefreshedTime();
app.updateAutoRefreshButton(); app.updateAutoRefreshButton();
app.initResolutionButtons();
app.setupVisibilityHandler();
console.log('App initialized'); console.log('App initialized');
}, },
onLoad: async () => { onLoad: async () => {
console.log('App onLoad event triggered'); console.log('App onLoad event triggered');
ui.showLoader(); ui.showLoader();
try { try {
volumeToggle.init(); volumeToggle.init();
await app.updateBTCPrice(); await app.updateBTCPrice();
const chartContainer = document.getElementById('coin-chart'); const chartContainer = document.getElementById('coin-chart');
if (chartContainer) { if (chartContainer) {
chartModule.initChart(); chartModule.initChart();
chartModule.showChartLoader(); chartModule.showChartLoader();
} else { }
//console.warn('Chart container not found, skipping chart initialization');
console.log('Loading all coin data...');
await app.loadAllCoinData();
if (chartModule.chart) {
config.currentResolution = 'day';
await chartModule.updateChart('BTC');
app.updateResolutionButtons('BTC');
}
ui.setActiveContainer('btc-container');
app.setupEventListeners();
app.initializeSelectImages();
app.initAutoRefresh();
} catch (error) {
ui.displayErrorMessage('Failed to initialize the dashboard. Please try refreshing the page.');
} finally {
ui.hideLoader();
if (chartModule.chart) {
chartModule.hideChartLoader();
}
console.log('App onLoad completed');
} }
},
console.log('Loading all coin data...'); loadAllCoinData: async () => {
await app.loadAllCoinData(); try {
const allCoinData = await api.fetchCoinGeckoDataXHR();
if (allCoinData.error) {
throw new Error(allCoinData.error);
}
if (chartModule.chart) { for (const coin of config.coins) {
config.currentResolution = 'day'; const coinData = allCoinData[coin.symbol.toLowerCase()];
await chartModule.updateChart('BTC'); if (coinData) {
app.updateResolutionButtons('BTC'); coinData.displayName = coin.displayName || coin.symbol;
} ui.displayCoinData(coin.symbol, coinData);
ui.setActiveContainer('btc-container'); const cacheKey = `coinData_${coin.symbol}`;
cache.set(cacheKey, coinData);
//console.log('Setting up event listeners and initializations...');
app.setupEventListeners();
app.initializeSelectImages();
app.initAutoRefresh();
} catch (error) {
//console.error('Error during initialization:', error);
ui.displayErrorMessage('Failed to initialize the dashboard. Please try refreshing the page.');
} finally {
ui.hideLoader();
if (chartModule.chart) {
chartModule.hideChartLoader();
}
console.log('App onLoad completed');
}
},
loadAllCoinData: async () => {
//console.log('Loading data for all coins...');
try {
const allCoinData = await api.fetchCoinGeckoDataXHR();
if (allCoinData.error) {
throw new Error(allCoinData.error);
}
for (const coin of config.coins) {
const coinData = allCoinData[coin.symbol.toLowerCase()];
if (coinData) {
coinData.displayName = coin.displayName || coin.symbol;
ui.displayCoinData(coin.symbol, coinData);
const cacheKey = `coinData_${coin.symbol}`;
cache.set(cacheKey, coinData);
} else {
//console.warn(`No data found for ${coin.symbol}`);
}
}
} catch (error) {
//console.error('Error loading all coin data:', error);
ui.displayErrorMessage('Failed to load coin data. Please try refreshing the page.');
} finally {
//console.log('All coin data loaded');
} }
}, }
} catch (error) {
ui.displayErrorMessage('Failed to load coin data. Please try refreshing the page.');
}
},
loadCoinData: async (coin) => { loadCoinData: async (coin) => {
//console.log(`Loading data for ${coin.symbol}...`);
const cacheKey = `coinData_${coin.symbol}`; const cacheKey = `coinData_${coin.symbol}`;
let cachedData = cache.get(cacheKey); let cachedData = cache.get(cacheKey);
let data; let data;
if (cachedData) { if (cachedData) {
//console.log(`Using cached data for ${coin.symbol}`);
data = cachedData.value; data = cachedData.value;
} else { } else {
try { try {
@@ -1014,11 +1105,9 @@ const app = {
if (data.error) { if (data.error) {
throw new Error(data.error); throw new Error(data.error);
} }
//console.log(`Caching new data for ${coin.symbol}`);
cache.set(cacheKey, data); cache.set(cacheKey, data);
cachedData = null; cachedData = null;
} catch (error) { } catch (error) {
//console.error(`Error fetching ${coin.symbol} data:`, error.message);
data = { data = {
error: error.message error: error.message
}; };
@@ -1028,16 +1117,13 @@ const app = {
} }
ui.displayCoinData(coin.symbol, data); ui.displayCoinData(coin.symbol, data);
ui.updateLoadTimeAndCache(0, cachedData); ui.updateLoadTimeAndCache(0, cachedData);
//console.log(`Data loaded for ${coin.symbol}`);
}, },
setupEventListeners: () => { setupEventListeners: () => {
//console.log('Setting up event listeners...');
config.coins.forEach(coin => { config.coins.forEach(coin => {
const container = document.getElementById(`${coin.symbol.toLowerCase()}-container`); const container = document.getElementById(`${coin.symbol.toLowerCase()}-container`);
if (container) { if (container) {
container.addEventListener('click', () => { app.addEventListenerWithCleanup(container, 'click', () => {
//console.log(`${coin.symbol} container clicked`);
ui.setActiveContainer(`${coin.symbol.toLowerCase()}-container`); ui.setActiveContainer(`${coin.symbol.toLowerCase()}-container`);
if (chartModule.chart) { if (chartModule.chart) {
if (coin.symbol === 'WOW') { if (coin.symbol === 'WOW') {
@@ -1052,26 +1138,27 @@ const app = {
const refreshAllButton = document.getElementById('refresh-all'); const refreshAllButton = document.getElementById('refresh-all');
if (refreshAllButton) { if (refreshAllButton) {
refreshAllButton.addEventListener('click', app.refreshAllData); app.addEventListenerWithCleanup(refreshAllButton, 'click', app.refreshAllData);
} }
const headers = document.querySelectorAll('th'); const headers = document.querySelectorAll('th');
headers.forEach((header, index) => { headers.forEach((header, index) => {
header.addEventListener('click', () => app.sortTable(index, header.classList.contains('disabled'))); app.addEventListenerWithCleanup(header, 'click', () =>
app.sortTable(index, header.classList.contains('disabled'))
);
}); });
const closeErrorButton = document.getElementById('close-error'); const closeErrorButton = document.getElementById('close-error');
if (closeErrorButton) { if (closeErrorButton) {
closeErrorButton.addEventListener('click', ui.hideErrorMessage); app.addEventListenerWithCleanup(closeErrorButton, 'click', ui.hideErrorMessage);
} }
//console.log('Event listeners set up');
}, },
initAutoRefresh: () => { initAutoRefresh: () => {
console.log('Initializing auto-refresh...'); console.log('Initializing auto-refresh...');
const toggleAutoRefreshButton = document.getElementById('toggle-auto-refresh'); const toggleAutoRefreshButton = document.getElementById('toggle-auto-refresh');
if (toggleAutoRefreshButton) { if (toggleAutoRefreshButton) {
toggleAutoRefreshButton.addEventListener('click', app.toggleAutoRefresh); app.addEventListenerWithCleanup(toggleAutoRefreshButton, 'click', app.toggleAutoRefresh);
app.updateAutoRefreshButton(); app.updateAutoRefreshButton();
} }
@@ -1100,7 +1187,6 @@ const app = {
earliestExpiration = Math.min(earliestExpiration, cachedItem.expiresAt); earliestExpiration = Math.min(earliestExpiration, cachedItem.expiresAt);
} }
} catch (error) { } catch (error) {
//console.error(`Error parsing cached item ${key}:`, error);
localStorage.removeItem(key); localStorage.removeItem(key);
} }
} }
@@ -1126,65 +1212,56 @@ const app = {
app.updateNextRefreshTime(); app.updateNextRefreshTime();
}, },
refreshAllData: async () => { refreshAllData: async () => {
if (app.isRefreshing) { if (app.isRefreshing) {
console.log('Refresh already in progress, skipping...'); console.log('Refresh already in progress, skipping...');
return; return;
}
console.log('Refreshing all data...');
app.isRefreshing = true;
ui.showLoader();
chartModule.showChartLoader();
try {
cache.clear();
await app.updateBTCPrice();
const allCoinData = await api.fetchCoinGeckoDataXHR();
if (allCoinData.error) {
throw new Error(allCoinData.error);
}
for (const coin of config.coins) {
const symbol = coin.symbol.toLowerCase();
const coinData = allCoinData[symbol];
if (coinData) {
coinData.displayName = coin.displayName || coin.symbol;
ui.displayCoinData(coin.symbol, coinData);
const cacheKey = `coinData_${coin.symbol}`;
cache.set(cacheKey, coinData);
} }
}
console.log('Refreshing all data...'); if (chartModule.currentCoin) {
app.isRefreshing = true; await chartModule.updateChart(chartModule.currentCoin, true);
ui.showLoader(); }
chartModule.showChartLoader();
try { app.lastRefreshedTime = new Date();
localStorage.setItem('lastRefreshedTime', app.lastRefreshedTime.getTime().toString());
ui.updateLastRefreshedTime();
cache.clear(); } catch (error) {
ui.displayErrorMessage('Failed to refresh all data. Please try again.');
await app.updateBTCPrice(); } finally {
ui.hideLoader();
const allCoinData = await api.fetchCoinGeckoDataXHR(); chartModule.hideChartLoader();
if (allCoinData.error) { app.isRefreshing = false;
throw new Error(allCoinData.error); if (app.isAutoRefreshEnabled) {
} app.scheduleNextRefresh();
}
for (const coin of config.coins) { }
const symbol = coin.symbol.toLowerCase(); },
const coinData = allCoinData[symbol];
if (coinData) {
coinData.displayName = coin.displayName || coin.symbol;
ui.displayCoinData(coin.symbol, coinData);
const cacheKey = `coinData_${coin.symbol}`;
cache.set(cacheKey, coinData);
} else {
//console.error(`No data found for ${coin.symbol}`);
}
}
if (chartModule.currentCoin) {
await chartModule.updateChart(chartModule.currentCoin, true);
}
app.lastRefreshedTime = new Date();
localStorage.setItem('lastRefreshedTime', app.lastRefreshedTime.getTime().toString());
ui.updateLastRefreshedTime();
console.log('All data refreshed successfully');
} catch (error) {
//console.error('Error refreshing all data:', error);
ui.displayErrorMessage('Failed to refresh all data. Please try again.');
} finally {
ui.hideLoader();
chartModule.hideChartLoader();
app.isRefreshing = false;
if (app.isAutoRefreshEnabled) {
app.scheduleNextRefresh();
}
}
},
updateNextRefreshTime: () => { updateNextRefreshTime: () => {
console.log('Updating next refresh time display'); console.log('Updating next refresh time display');
@@ -1215,6 +1292,7 @@ const app = {
app.updateNextRefreshTimeRAF = requestAnimationFrame(updateDisplay); app.updateNextRefreshTimeRAF = requestAnimationFrame(updateDisplay);
} }
}; };
updateDisplay(); updateDisplay();
} else { } else {
labelElement.textContent = ''; labelElement.textContent = '';
@@ -1241,7 +1319,6 @@ const app = {
}, },
startSpinAnimation: () => { startSpinAnimation: () => {
//console.log('Starting spin animation on auto-refresh button');
const svg = document.querySelector('#toggle-auto-refresh svg'); const svg = document.querySelector('#toggle-auto-refresh svg');
if (svg) { if (svg) {
svg.classList.add('animate-spin'); svg.classList.add('animate-spin');
@@ -1252,7 +1329,6 @@ const app = {
}, },
stopSpinAnimation: () => { stopSpinAnimation: () => {
//console.log('Stopping spin animation on auto-refresh button');
const svg = document.querySelector('#toggle-auto-refresh svg'); const svg = document.querySelector('#toggle-auto-refresh svg');
if (svg) { if (svg) {
svg.classList.remove('animate-spin'); svg.classList.remove('animate-spin');
@@ -1260,7 +1336,6 @@ const app = {
}, },
updateLastRefreshedTime: () => { updateLastRefreshedTime: () => {
//console.log('Updating last refreshed time');
const lastRefreshedElement = document.getElementById('last-refreshed-time'); const lastRefreshedElement = document.getElementById('last-refreshed-time');
if (lastRefreshedElement && app.lastRefreshedTime) { if (lastRefreshedElement && app.lastRefreshedTime) {
const formattedTime = app.lastRefreshedTime.toLocaleTimeString(); const formattedTime = app.lastRefreshedTime.toLocaleTimeString();
@@ -1277,135 +1352,126 @@ const app = {
} }
}, },
updateBTCPrice: async () => { updateBTCPrice: async () => {
//console.log('Updating BTC price...'); try {
try { const priceData = await api.fetchCoinGeckoDataXHR();
const priceData = await api.fetchCoinGeckoDataXHR(); if (priceData.error) {
if (priceData.error) { app.btcPriceUSD = 0;
//console.error('Error fetching BTC price:', priceData.error); } else if (priceData.btc && priceData.btc.current_price) {
app.btcPriceUSD = 0; app.btcPriceUSD = priceData.btc.current_price;
} else if (priceData.btc && priceData.btc.current_price) { } else {
app.btcPriceUSD = 0;
app.btcPriceUSD = priceData.btc.current_price; }
} else { } catch (error) {
//console.error('Unexpected BTC data structure:', priceData); app.btcPriceUSD = 0;
app.btcPriceUSD = 0;
}
} catch (error) {
//console.error('Error fetching BTC price:', error);
app.btcPriceUSD = 0;
}
//console.log('Current BTC price:', app.btcPriceUSD);
},
sortTable: (columnIndex) => {
//console.log(`Sorting column: ${columnIndex}`);
const sortableColumns = [0, 5, 6, 7]; // 0: Time, 5: Rate, 6: Market +/-, 7: Trade
if (!sortableColumns.includes(columnIndex)) {
//console.log(`Column ${columnIndex} is not sortable`);
return;
}
const table = document.querySelector('table');
if (!table) {
//console.error("Table not found for sorting.");
return;
}
const rows = Array.from(table.querySelectorAll('tbody tr'));
console.log(`Found ${rows.length} rows to sort`);
const sortIcon = document.getElementById(`sort-icon-${columnIndex}`);
if (!sortIcon) {
//console.error("Sort icon not found.");
return;
}
const sortOrder = sortIcon.textContent === '↓' ? 1 : -1;
sortIcon.textContent = sortOrder === 1 ? '↑' : '↓';
const getSafeTextContent = (element) => element ? element.textContent.trim() : '';
rows.sort((a, b) => {
let aValue, bValue;
switch (columnIndex) {
case 1: // Time column
aValue = getSafeTextContent(a.querySelector('td:first-child .text-xs:first-child'));
bValue = getSafeTextContent(b.querySelector('td:first-child .text-xs:first-child'));
//console.log(`Comparing times: "${aValue}" vs "${bValue}"`);
const parseTime = (timeStr) => {
const [value, unit] = timeStr.split(' ');
const numValue = parseFloat(value);
switch(unit) {
case 'seconds': return numValue;
case 'minutes': return numValue * 60;
case 'hours': return numValue * 3600;
case 'days': return numValue * 86400;
default: return 0;
}
};
return (parseTime(bValue) - parseTime(aValue)) * sortOrder;
case 5: // Rate
case 6: // Market +/-
aValue = getSafeTextContent(a.cells[columnIndex]);
bValue = getSafeTextContent(b.cells[columnIndex]);
//console.log(`Comparing values: "${aValue}" vs "${bValue}"`);
aValue = parseFloat(aValue.replace(/[^\d.-]/g, '') || '0');
bValue = parseFloat(bValue.replace(/[^\d.-]/g, '') || '0');
return (aValue - bValue) * sortOrder;
case 7: // Trade
const aCell = a.cells[columnIndex];
const bCell = b.cells[columnIndex];
//console.log('aCell:', aCell ? aCell.outerHTML : 'null');
//console.log('bCell:', bCell ? bCell.outerHTML : 'null');
aValue = getSafeTextContent(aCell.querySelector('a')) ||
getSafeTextContent(aCell.querySelector('button')) ||
getSafeTextContent(aCell);
bValue = getSafeTextContent(bCell.querySelector('a')) ||
getSafeTextContent(bCell.querySelector('button')) ||
getSafeTextContent(bCell);
aValue = aValue.toLowerCase();
bValue = bValue.toLowerCase();
//console.log(`Comparing trade actions: "${aValue}" vs "${bValue}"`);
if (aValue === bValue) return 0;
if (aValue === "swap") return -1 * sortOrder;
if (bValue === "swap") return 1 * sortOrder;
return aValue.localeCompare(bValue) * sortOrder;
default:
aValue = getSafeTextContent(a.cells[columnIndex]);
bValue = getSafeTextContent(b.cells[columnIndex]);
//console.log(`Comparing default values: "${aValue}" vs "${bValue}"`);
return aValue.localeCompare(bValue, undefined, {
numeric: true,
sensitivity: 'base'
}) * sortOrder;
} }
}); },
const tbody = table.querySelector('tbody'); sortTable: (columnIndex) => {
if (tbody) { const sortableColumns = [0, 5, 6, 7]; // 0: Time, 5: Rate, 6: Market +/-, 7: Trade
rows.forEach(row => tbody.appendChild(row)); if (!sortableColumns.includes(columnIndex)) {
} else { return;
//console.error("Table body not found."); }
}
//console.log('Sorting completed'); const table = document.querySelector('table');
}, if (!table) {
return;
}
const rows = Array.from(table.querySelectorAll('tbody tr'));
const sortIcon = document.getElementById(`sort-icon-${columnIndex}`);
if (!sortIcon) {
return;
}
const sortOrder = sortIcon.textContent === '↓' ? 1 : -1;
sortIcon.textContent = sortOrder === 1 ? '↑' : '↓';
const getSafeTextContent = (element) => element ? element.textContent.trim() : '';
rows.sort((a, b) => {
let aValue, bValue;
switch (columnIndex) {
case 1: // Time column
aValue = getSafeTextContent(a.querySelector('td:first-child .text-xs:first-child'));
bValue = getSafeTextContent(b.querySelector('td:first-child .text-xs:first-child'));
const parseTime = (timeStr) => {
const [value, unit] = timeStr.split(' ');
const numValue = parseFloat(value);
switch(unit) {
case 'seconds': return numValue;
case 'minutes': return numValue * 60;
case 'hours': return numValue * 3600;
case 'days': return numValue * 86400;
default: return 0;
}
};
return (parseTime(bValue) - parseTime(aValue)) * sortOrder;
case 5: // Rate
case 6: // Market +/-
aValue = getSafeTextContent(a.cells[columnIndex]);
bValue = getSafeTextContent(b.cells[columnIndex]);
aValue = parseFloat(aValue.replace(/[^\d.-]/g, '') || '0');
bValue = parseFloat(bValue.replace(/[^\d.-]/g, '') || '0');
return (aValue - bValue) * sortOrder;
case 7: // Trade
const aCell = a.cells[columnIndex];
const bCell = b.cells[columnIndex];
aValue = getSafeTextContent(aCell.querySelector('a')) ||
getSafeTextContent(aCell.querySelector('button')) ||
getSafeTextContent(aCell);
bValue = getSafeTextContent(bCell.querySelector('a')) ||
getSafeTextContent(bCell.querySelector('button')) ||
getSafeTextContent(bCell);
aValue = aValue.toLowerCase();
bValue = bValue.toLowerCase();
if (aValue === bValue) return 0;
if (aValue === "swap") return -1 * sortOrder;
if (bValue === "swap") return 1 * sortOrder;
return aValue.localeCompare(bValue) * sortOrder;
default:
aValue = getSafeTextContent(a.cells[columnIndex]);
bValue = getSafeTextContent(b.cells[columnIndex]);
return aValue.localeCompare(bValue, undefined, {
numeric: true,
sensitivity: 'base'
}) * sortOrder;
}
});
const tbody = table.querySelector('tbody');
if (tbody) {
const fragment = document.createDocumentFragment();
rows.forEach(row => fragment.appendChild(row));
tbody.appendChild(fragment);
}
},
initializeSelectImages: () => { initializeSelectImages: () => {
const updateSelectedImage = (selectId) => { const updateSelectedImage = (selectId) => {
const select = document.getElementById(selectId); const select = document.getElementById(selectId);
const button = document.getElementById(`${selectId}_button`); const button = document.getElementById(`${selectId}_button`);
if (!select || !button) { if (!select || !button) {
//console.error(`Elements not found for ${selectId}`);
return; return;
} }
const oldListener = select.getAttribute('data-change-listener');
if (oldListener && window[oldListener]) {
select.removeEventListener('change', window[oldListener]);
delete window[oldListener];
}
const selectedOption = select.options[select.selectedIndex]; const selectedOption = select.options[select.selectedIndex];
const imageURL = selectedOption?.getAttribute('data-image'); const imageURL = selectedOption?.getAttribute('data-image');
requestAnimationFrame(() => { requestAnimationFrame(() => {
if (imageURL) { if (imageURL) {
button.style.backgroundImage = `url('${imageURL}')`; button.style.backgroundImage = `url('${imageURL}')`;
@@ -1419,46 +1485,50 @@ sortTable: (columnIndex) => {
button.style.minHeight = '25px'; button.style.minHeight = '25px';
}); });
}; };
const handleSelectChange = (event) => {
updateSelectedImage(event.target.id);
};
['coin_to', 'coin_from'].forEach(selectId => { ['coin_to', 'coin_from'].forEach(selectId => {
const select = document.getElementById(selectId); const select = document.getElementById(selectId);
if (select) { if (select) {
select.addEventListener('change', handleSelectChange);
const listenerName = `selectChangeListener_${selectId}`;
window[listenerName] = () => updateSelectedImage(selectId);
select.setAttribute('data-change-listener', listenerName);
select.addEventListener('change', window[listenerName]);
updateSelectedImage(selectId); updateSelectedImage(selectId);
} else {
//console.error(`Select element not found for ${selectId}`);
} }
}); });
}, },
updateResolutionButtons: (coinSymbol) => { updateResolutionButtons: (coinSymbol) => {
const resolutionButtons = document.querySelectorAll('.resolution-button'); const resolutionButtons = document.querySelectorAll('.resolution-button');
resolutionButtons.forEach(button => { resolutionButtons.forEach(button => {
const resolution = button.id.split('-')[1]; const resolution = button.id.split('-')[1];
if (coinSymbol === 'WOW') { if (coinSymbol === 'WOW') {
if (resolution === 'day') { if (resolution === 'day') {
button.classList.remove('text-gray-400', 'cursor-not-allowed', 'opacity-50', 'outline-none'); button.classList.remove('text-gray-400', 'cursor-not-allowed', 'opacity-50', 'outline-none');
button.classList.add('active'); button.classList.add('active');
button.disabled = false; button.disabled = false;
} else {
button.classList.add('text-gray-400', 'cursor-not-allowed', 'opacity-50', 'outline-none');
button.classList.remove('active');
button.disabled = true;
}
} else { } else {
button.classList.add('text-gray-400', 'cursor-not-allowed', 'opacity-50', 'outline-none'); button.classList.remove('text-gray-400', 'cursor-not-allowed', 'opacity-50', 'outline-none');
button.classList.remove('active'); button.classList.toggle('active', resolution === config.currentResolution);
button.disabled = true; button.disabled = false;
} }
} else { });
button.classList.remove('text-gray-400', 'cursor-not-allowed', 'opacity-50', 'outline-none'); },
button.classList.toggle('active', resolution === config.currentResolution);
button.disabled = false;
}
});
},
toggleAutoRefresh: () => { toggleAutoRefresh: () => {
console.log('Toggling auto-refresh'); console.log('Toggling auto-refresh');
app.isAutoRefreshEnabled = !app.isAutoRefreshEnabled; app.isAutoRefreshEnabled = !app.isAutoRefreshEnabled;
localStorage.setItem('autoRefreshEnabled', app.isAutoRefreshEnabled.toString()); localStorage.setItem('autoRefreshEnabled', app.isAutoRefreshEnabled.toString());
if (app.isAutoRefreshEnabled) { if (app.isAutoRefreshEnabled) {
console.log('Auto-refresh enabled, scheduling next refresh'); console.log('Auto-refresh enabled, scheduling next refresh');
app.scheduleNextRefresh(); app.scheduleNextRefresh();
@@ -1471,31 +1541,18 @@ updateResolutionButtons: (coinSymbol) => {
app.nextRefreshTime = null; app.nextRefreshTime = null;
localStorage.removeItem('nextRefreshTime'); localStorage.removeItem('nextRefreshTime');
} }
app.updateAutoRefreshButton(); app.updateAutoRefreshButton();
app.updateNextRefreshTime(); app.updateNextRefreshTime();
} }
}; };
const resolutionButtons = document.querySelectorAll('.resolution-button');
resolutionButtons.forEach(button => {
button.addEventListener('click', () => {
const resolution = button.id.split('-')[1];
const currentCoin = chartModule.currentCoin;
if (currentCoin !== 'WOW' || resolution === 'day') {
config.currentResolution = resolution;
chartModule.updateChart(currentCoin, true);
app.updateResolutionButtons(currentCoin);
}
});
});
// LOAD // LOAD
app.init(); app.init();
app.visibilityCleanup = app.setupVisibilityHandler();
document.addEventListener('visibilitychange', () => { window.addEventListener('beforeunload', () => {
if (!document.hidden && chartModule.chart) { console.log('Page unloading, cleaning up...');
console.log('Page became visible, reinitializing chart'); app.cleanup();
chartModule.updateChart(chartModule.currentCoin, true);
}
}); });

View File

@@ -147,7 +147,7 @@ function getWebSocketConfig() {
</button> </button>
<p class="text-red-600 font-semibold text-xl mb-4">Error</p> <p class="text-red-600 font-semibold text-xl mb-4">Error</p>
<p id="error-message" class="text-gray-700 dark:text-gray-300 text-lg mb-6"></p> <p id="error-message" class="text-gray-700 dark:text-gray-300 text-lg mb-6"></p>
<p class="text-sm text-gray-600 dark:text-gray-400">To review or update your Chart API Key(s), navigate to<a href="/settings" class="text-blue-500 hover:underline">Settings & Tools > Settings > General (TAB)</a>. <p class="text-sm text-gray-600 dark:text-gray-400">To review or update your Chart API Key(s), navigate to <a href="/settings" class="text-blue-500 hover:underline">Settings & Tools > Settings > General (TAB)</a>.
</p> </p>
</div> </div>
</div> </div>
@@ -337,7 +337,7 @@ function getWebSocketConfig() {
<div class="container mt-5 mx-auto px-4"> <div class="container mt-5 mx-auto px-4">
<div class="pt-0 pb-6 bg-coolGray-100 dark:bg-gray-500 rounded-xl"> <div class="pt-0 pb-6 bg-coolGray-100 dark:bg-gray-500 rounded-xl">
<div class="px-0"> <div class="px-0">
<div class="w-auto mt-6 overflow-x-auto"> <div class="w-auto mt-6 overflow-hidden md:overflow-hidden sm:overflow-auto">
<table class="w-full min-w-max"> <table class="w-full min-w-max">
<thead class="uppercase"> <thead class="uppercase">
<tr> <tr>