Pricechart + Global Tooltips optimization + wsport fix.

This commit is contained in:
gerlofvanek
2025-03-03 21:09:46 +01:00
parent a5c3c692a0
commit 3489ebe908
6 changed files with 712 additions and 255 deletions

View File

@@ -269,15 +269,30 @@ const WebSocketManager = {
if (this.ws?.readyState === WebSocket.OPEN) return; if (this.ws?.readyState === WebSocket.OPEN) return;
try { try {
const wsPort = window.ws_port || '11700';
let wsPort;
if (typeof getWebSocketConfig === 'function') {
const wsConfig = getWebSocketConfig();
wsPort = wsConfig?.port || wsConfig?.fallbackPort;
}
if (!wsPort && window.config?.port) {
wsPort = window.config.port;
}
if (!wsPort) {
wsPort = window.ws_port || '11701';
}
console.log("Using WebSocket port:", wsPort);
this.ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`); this.ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`);
this.setupEventHandlers(); this.setupEventHandlers();
} catch (error) { } catch (error) {
console.error('WebSocket connection error:', error); console.error('WebSocket connection error:', error);
this.handleReconnect(); this.handleReconnect();
} }
}, },
setupEventHandlers() { setupEventHandlers() {
this.ws.onopen = () => { this.ws.onopen = () => {
state.wsConnected = true; state.wsConnected = true;

View File

@@ -360,14 +360,26 @@ const WebSocketManager = {
if (this.ws?.readyState === WebSocket.OPEN) return; if (this.ws?.readyState === WebSocket.OPEN) return;
try { try {
const wsPort = window.ws_port || '11700'; let wsPort;
if (typeof getWebSocketConfig === 'function') {
const wsConfig = getWebSocketConfig();
wsPort = wsConfig?.port || wsConfig?.fallbackPort;
}
if (!wsPort && window.config?.port) {
wsPort = window.config.port;
}
if (!wsPort) {
wsPort = window.ws_port || '11701';
}
console.log("Using WebSocket port:", wsPort);
this.ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`); this.ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`);
this.setupEventHandlers(); this.setupEventHandlers();
} catch (error) { } catch (error) {
console.error('WebSocket connection error:', error); console.error('WebSocket connection error:', error);
this.handleReconnect(); this.handleReconnect();
} }
}, },
setupEventHandlers() { setupEventHandlers() {
this.ws.onopen = () => { this.ws.onopen = () => {

View File

@@ -256,14 +256,30 @@ const WebSocketManager = {
} }
try { try {
const wsPort = window.ws_port || '11700';
let wsPort;
if (typeof getWebSocketConfig === 'function') {
const wsConfig = getWebSocketConfig();
wsPort = wsConfig?.port || wsConfig?.fallbackPort;
}
if (!wsPort && window.config?.port) {
wsPort = window.config.port;
}
if (!wsPort) {
wsPort = window.ws_port || '11701';
}
console.log("Using WebSocket port:", wsPort);
this.ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`); this.ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`);
this.setupEventHandlers(); this.setupEventHandlers();
} catch (error) { } catch (error) {
console.error('WebSocket connection error:', error); console.error('WebSocket connection error:', error);
this.handleReconnect(); this.handleReconnect();
} }
}, },
setupEventHandlers() { setupEventHandlers() {
if (!this.ws) return; if (!this.ws) return;
@@ -917,7 +933,6 @@ const forceTooltipDOMCleanup = () => {
foundCount += allTooltipElements.length; foundCount += allTooltipElements.length;
allTooltipElements.forEach(element => { allTooltipElements.forEach(element => {
const isDetached = !document.body.contains(element) || const isDetached = !document.body.contains(element) ||
element.classList.contains('hidden') || element.classList.contains('hidden') ||
element.style.display === 'none'; element.style.display === 'none';
@@ -947,7 +962,6 @@ const forceTooltipDOMCleanup = () => {
const tippyRoots = document.querySelectorAll('[data-tippy-root]'); const tippyRoots = document.querySelectorAll('[data-tippy-root]');
foundCount += tippyRoots.length; foundCount += tippyRoots.length;
tippyRoots.forEach(element => { tippyRoots.forEach(element => {
const isOrphan = !element.children.length || const isOrphan = !element.children.length ||
element.children[0].classList.contains('hidden') || element.children[0].classList.contains('hidden') ||
@@ -975,13 +989,10 @@ const forceTooltipDOMCleanup = () => {
} }
} }
}); });
// Handle legacy tooltip elements
document.querySelectorAll('.tooltip').forEach(element => { document.querySelectorAll('.tooltip').forEach(element => {
const isTrulyDetached = !element.parentElement || const isTrulyDetached = !element.parentElement ||
!document.body.contains(element.parentElement) || !document.body.contains(element.parentElement) ||
element.classList.contains('hidden'); element.classList.contains('hidden');
if (isTrulyDetached) { if (isTrulyDetached) {
try { try {
element.remove(); element.remove();
@@ -992,14 +1003,11 @@ const forceTooltipDOMCleanup = () => {
} }
}); });
if (window.TooltipManager && window.TooltipManager.activeTooltips) { if (window.TooltipManager && typeof window.TooltipManager.getActiveTooltipInstances === 'function') {
window.TooltipManager.activeTooltips.forEach((instance, id) => { const activeTooltips = window.TooltipManager.getActiveTooltipInstances();
const tooltipElement = document.getElementById(id.split('tooltip-trigger-')[1]); activeTooltips.forEach(([element, instance]) => {
const triggerElement = document.querySelector(`[data-tooltip-trigger-id="${id}"]`); const tooltipId = element.getAttribute('data-tooltip-trigger-id');
if (!document.body.contains(element)) {
if (!tooltipElement || !triggerElement ||
!document.body.contains(tooltipElement) ||
!document.body.contains(triggerElement)) {
if (instance?.[0]) { if (instance?.[0]) {
try { try {
instance[0].destroy(); instance[0].destroy();
@@ -1007,14 +1015,13 @@ const forceTooltipDOMCleanup = () => {
console.warn('Error destroying tooltip instance:', e); console.warn('Error destroying tooltip instance:', e);
} }
} }
window.TooltipManager.activeTooltips.delete(id);
} }
}); });
} }
if (removedCount > 0) { if (removedCount > 0) {
// console.log(`Tooltip cleanup: found ${foundCount}, removed ${removedCount} detached tooltips`); // console.log(`Tooltip cleanup: found ${foundCount}, removed ${removedCount} detached tooltips`);
} }
}; }
const createTableRow = async (bid) => { const createTableRow = async (bid) => {
const identity = await IdentityManager.getIdentityData(bid.addr_from); const identity = await IdentityManager.getIdentityData(bid.addr_from);

View File

@@ -250,7 +250,15 @@ const WebSocketManager = {
this.connectionState.lastConnectAttempt = Date.now(); this.connectionState.lastConnectAttempt = Date.now();
try { try {
const wsPort = config.port || window.ws_port || '11700'; let wsPort;
if (typeof getWebSocketConfig === 'function') {
const wsConfig = getWebSocketConfig();
wsPort = wsConfig.port || wsConfig.fallbackPort;
console.log("Using WebSocket port:", wsPort);
} else {
wsPort = config?.port || window.ws_port || '11700';
}
if (!wsPort) { if (!wsPort) {
this.connectionState.isConnecting = false; this.connectionState.isConnecting = false;
@@ -273,8 +281,7 @@ const WebSocketManager = {
this.handleReconnect(); this.handleReconnect();
return false; return false;
} }
}, },
setupEventHandlers() { setupEventHandlers() {
if (!this.ws) return; if (!this.ws) return;

View File

@@ -1,3 +1,128 @@
// CLEANUP
const cleanupManager = {
eventListeners: [],
timeouts: [],
intervals: [],
animationFrames: [],
addListener: function(element, type, handler, options) {
if (!element) return null;
element.addEventListener(type, handler, options);
this.eventListeners.push({ element, type, handler, options });
return handler;
},
setTimeout: function(callback, delay) {
const id = setTimeout(callback, delay);
this.timeouts.push(id);
return id;
},
setInterval: function(callback, delay) {
const id = setInterval(callback, delay);
this.intervals.push(id);
return id;
},
requestAnimationFrame: function(callback) {
const id = requestAnimationFrame(callback);
this.animationFrames.push(id);
return id;
},
clearAll: function() {
this.eventListeners.forEach(({ element, type, handler, options }) => {
if (element) {
try {
element.removeEventListener(type, handler, options);
} catch (e) {
console.warn('Error removing event listener:', e);
}
}
});
this.eventListeners = [];
this.timeouts.forEach(id => clearTimeout(id));
this.timeouts = [];
this.intervals.forEach(id => clearInterval(id));
this.intervals = [];
this.animationFrames.forEach(id => cancelAnimationFrame(id));
this.animationFrames = [];
console.log('All resources cleaned up');
},
clearTimeouts: function() {
this.timeouts.forEach(id => clearTimeout(id));
this.timeouts = [];
},
clearIntervals: function() {
this.intervals.forEach(id => clearInterval(id));
this.intervals = [];
},
removeListenersByElement: function(element) {
if (!element) return;
const listenersToRemove = this.eventListeners.filter(
listener => listener.element === element
);
listenersToRemove.forEach(({ element, type, handler, options }) => {
try {
element.removeEventListener(type, handler, options);
} catch (e) {
console.warn('Error removing event listener:', e);
}
});
this.eventListeners = this.eventListeners.filter(
listener => listener.element !== element
);
}
};
// MEMORY
const memoryMonitor = {
isEnabled: true,
lastLogTime: 0,
logInterval: 5 * 60 * 1000,
monitorInterval: null,
startMonitoring: function() {
console.log('Starting memory monitoring');
if (!this.isEnabled) return;
if (this.monitorInterval) {
clearInterval(this.monitorInterval);
}
this.monitorInterval = setInterval(() => {
this.logMemoryUsage();
}, this.logInterval);
this.logMemoryUsage();
},
logMemoryUsage: function() {
console.log('Logging memory usage');
if (window.performance && window.performance.memory) {
const memory = window.performance.memory;
console.log(`Memory Usage: ${Math.round(memory.usedJSHeapSize / (1024 * 1024))}MB / ${Math.round(memory.jsHeapSizeLimit / (1024 * 1024))}MB`);
}
},
stopMonitoring: function() {
if (this.monitorInterval) {
clearInterval(this.monitorInterval);
this.monitorInterval = null;
}
}
};
// CONFIG // CONFIG
const config = { const config = {
apiKeys: getAPIKeys(), apiKeys: getAPIKeys(),
@@ -393,49 +518,138 @@ const rateLimiter = {
// CACHE // CACHE
const cache = { const cache = {
set: (key, value, customTtl = null) => { maxSizeBytes: 10 * 1024 * 1024,
maxItems: 200,
cacheTTL: 5 * 60 * 1000,
set: function(key, value, customTtl = null) {
this.cleanup();
const item = { const item = {
value: value, value: value,
timestamp: Date.now(), timestamp: Date.now(),
expiresAt: Date.now() + (customTtl || app.cacheTTL) expiresAt: Date.now() + (customTtl || this.cacheTTL)
}; };
localStorage.setItem(key, JSON.stringify(item));
//console.log(`Cache set for ${key}, expires in ${(customTtl || app.cacheTTL) / 1000} seconds`); try {
const serialized = JSON.stringify(item);
localStorage.setItem(key, serialized);
} catch (e) {
console.warn('Cache set error:', e);
this.clear();
try {
const serialized = JSON.stringify(item);
localStorage.setItem(key, serialized);
} catch (e2) {
console.error('Failed to store in cache even after cleanup:', e2);
}
}
}, },
get: (key) => {
get: function(key) {
const itemStr = localStorage.getItem(key); const itemStr = localStorage.getItem(key);
if (!itemStr) { if (!itemStr) {
return null; return null;
} }
try { try {
const item = JSON.parse(itemStr); const item = JSON.parse(itemStr);
const now = Date.now(); const now = Date.now();
if (now < item.expiresAt) { if (now < item.expiresAt) {
//console.log(`Cache hit for ${key}, ${(item.expiresAt - now) / 1000} seconds remaining`);
return { return {
value: item.value, value: item.value,
remainingTime: item.expiresAt - now remainingTime: item.expiresAt - now
}; };
} else { } else {
//console.log(`Cache expired for ${key}`);
localStorage.removeItem(key); localStorage.removeItem(key);
} }
} catch (error) { } catch (error) {
//console.error('Error parsing cache item:', error.message); console.error('Error parsing cache item:', error.message);
localStorage.removeItem(key); localStorage.removeItem(key);
} }
return null; return null;
}, },
isValid: (key) => {
return cache.get(key) !== null; isValid: function(key) {
return this.get(key) !== null;
}, },
clear: () => {
Object.keys(localStorage).forEach(key => { clear: function() {
const keysToRemove = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key.startsWith('coinData_') || key.startsWith('chartData_') || key === 'coinGeckoOneLiner') { if (key.startsWith('coinData_') || key.startsWith('chartData_') || key === 'coinGeckoOneLiner') {
localStorage.removeItem(key); keysToRemove.push(key);
} }
}
keysToRemove.forEach(key => {
localStorage.removeItem(key);
}); });
//console.log('Cache cleared');
console.log(`Cache cleared: removed ${keysToRemove.length} items`);
},
cleanup: function() {
let totalSize = 0;
const items = [];
const keysToRemove = [];
const now = Date.now();
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key.startsWith('coinData_') || key.startsWith('chartData_') || key === 'coinGeckoOneLiner') {
try {
const value = localStorage.getItem(key);
const size = new Blob([value]).size;
const item = JSON.parse(value);
if (item.expiresAt && item.expiresAt < now) {
keysToRemove.push(key);
continue;
}
totalSize += size;
items.push({
key,
size,
timestamp: item.timestamp || 0,
expiresAt: item.expiresAt || 0
});
} catch (e) {
keysToRemove.push(key);
}
}
}
keysToRemove.forEach(key => {
localStorage.removeItem(key);
});
if (totalSize > this.maxSizeBytes || items.length > this.maxItems) {
items.sort((a, b) => a.timestamp - b.timestamp);
const itemsToRemove = Math.max(
Math.ceil(items.length * 0.2),
items.length - this.maxItems
);
items.slice(0, itemsToRemove).forEach(item => {
localStorage.removeItem(item.key);
});
console.log(`Cache cleanup: removed ${itemsToRemove} items, freed ${Math.round((totalSize - this.maxSizeBytes) / 1024)}KB`);
}
return {
totalSize,
itemCount: items.length,
removedCount: keysToRemove.length
};
} }
}; };
@@ -631,6 +845,8 @@ const chartModule = {
chart: null, chart: null,
currentCoin: 'BTC', currentCoin: 'BTC',
loadStartTime: 0, loadStartTime: 0,
chartRefs: new WeakMap(),
verticalLinePlugin: { verticalLinePlugin: {
id: 'verticalLine', id: 'verticalLine',
beforeDraw: (chart, args, options) => { beforeDraw: (chart, args, options) => {
@@ -652,15 +868,44 @@ const chartModule = {
} }
}, },
initChart: () => { getChartByElement: function(element) {
const ctx = document.getElementById('coin-chart')?.getContext('2d'); return this.chartRefs.get(element);
},
setChartReference: function(element, chart) {
this.chartRefs.set(element, chart);
},
destroyChart: function() {
if (chartModule.chart) {
try {
chartModule.chart.destroy();
} catch (e) {
console.error('Error destroying chart:', e);
}
chartModule.chart = null;
}
},
initChart: function() {
this.destroyChart();
const canvas = document.getElementById('coin-chart');
if (!canvas) {
logger.error('Chart canvas element not found');
return;
}
const ctx = canvas.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;
} }
const gradient = ctx.createLinearGradient(0, 0, 0, 400); const gradient = ctx.createLinearGradient(0, 0, 0, 400);
gradient.addColorStop(0, 'rgba(77, 132, 240, 0.2)'); gradient.addColorStop(0, 'rgba(77, 132, 240, 0.2)');
gradient.addColorStop(1, 'rgba(77, 132, 240, 0)'); gradient.addColorStop(1, 'rgba(77, 132, 240, 0)');
chartModule.chart = new Chart(ctx, { chartModule.chart = new Chart(ctx, {
type: 'line', type: 'line',
data: { data: {
@@ -811,11 +1056,15 @@ const chartModule = {
}, },
plugins: [chartModule.verticalLinePlugin] plugins: [chartModule.verticalLinePlugin]
}); });
this.setChartReference(canvas, chartModule.chart);
}, },
prepareChartData: (coinSymbol, data) => {
prepareChartData: function(coinSymbol, data) {
if (!data) { if (!data) {
return []; return [];
} }
try { try {
let preparedData; let preparedData;
@@ -825,9 +1074,11 @@ const chartModule = {
const endUnix = endTime.getTime(); const endUnix = endTime.getTime();
const startUnix = endUnix - (24 * 3600000); const startUnix = endUnix - (24 * 3600000);
const hourlyPoints = []; const hourlyPoints = [];
for (let hourUnix = startUnix; hourUnix <= endUnix; hourUnix += 3600000) { for (let hourUnix = startUnix; hourUnix <= endUnix; hourUnix += 3600000) {
const targetHour = new Date(hourUnix); const targetHour = new Date(hourUnix);
targetHour.setUTCMinutes(0, 0, 0); targetHour.setUTCMinutes(0, 0, 0);
const closestPoint = data.reduce((prev, curr) => { const closestPoint = data.reduce((prev, curr) => {
const prevTime = new Date(prev[0]); const prevTime = new Date(prev[0]);
const currTime = new Date(curr[0]); const currTime = new Date(curr[0]);
@@ -868,6 +1119,7 @@ const chartModule = {
y: price y: price
})); }));
} else { } else {
console.warn('Unknown data format for chartData:', data);
return []; return [];
} }
return preparedData.map(point => ({ return preparedData.map(point => ({
@@ -880,7 +1132,7 @@ const chartModule = {
} }
}, },
ensureHourlyData: (data) => { ensureHourlyData: function(data) {
const now = new Date(); const now = new Date();
now.setUTCMinutes(0, 0, 0); now.setUTCMinutes(0, 0, 0);
const twentyFourHoursAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000); const twentyFourHoursAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
@@ -888,21 +1140,28 @@ const chartModule = {
for (let i = 0; i < 24; i++) { for (let i = 0; i < 24; i++) {
const targetTime = new Date(twentyFourHoursAgo.getTime() + i * 60 * 60 * 1000); const targetTime = new Date(twentyFourHoursAgo.getTime() + i * 60 * 60 * 1000);
if (data.length > 0) {
const closestDataPoint = data.reduce((prev, curr) => const closestDataPoint = data.reduce((prev, curr) =>
Math.abs(new Date(curr.x).getTime() - targetTime.getTime()) < Math.abs(new Date(curr.x).getTime() - targetTime.getTime()) <
Math.abs(new Date(prev.x).getTime() - targetTime.getTime()) ? curr : prev Math.abs(new Date(prev.x).getTime() - targetTime.getTime()) ? curr : prev
); );
hourlyData.push({ hourlyData.push({
x: targetTime.getTime(), x: targetTime.getTime(),
y: closestDataPoint.y y: closestDataPoint.y
}); });
} }
}
return hourlyData; return hourlyData;
}, },
updateChart: async (coinSymbol, forceRefresh = false) => { updateChart: async function(coinSymbol, forceRefresh = false) {
try { try {
const currentChartData = chartModule.chart?.data.datasets[0].data || []; if (!chartModule.chart) {
chartModule.initChart();
}
const currentChartData = chartModule.chart?.data?.datasets[0]?.data || [];
if (currentChartData.length === 0) { if (currentChartData.length === 0) {
chartModule.showChartLoader(); chartModule.showChartLoader();
} }
@@ -912,18 +1171,20 @@ const chartModule = {
let data; let data;
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}`);
} else { } else {
try { try {
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}`);
} }
cache.set(cacheKey, data, config.cacheTTL); cache.set(cacheKey, data, config.cacheTTL);
} catch (error) { } catch (error) {
if (error.message.includes('429') && currentChartData.length > 0) { if (error.message.includes('429') && currentChartData.length > 0) {
//console.warn(`Rate limit hit for ${coinSymbol}, maintaining current chart`); console.warn(`Rate limit hit for ${coinSymbol}, maintaining current chart`);
chartModule.hideChartLoader();
return; return;
} }
const expiredCache = localStorage.getItem(cacheKey); const expiredCache = localStorage.getItem(cacheKey);
@@ -931,7 +1192,6 @@ const chartModule = {
try { try {
const parsedCache = JSON.parse(expiredCache); const parsedCache = JSON.parse(expiredCache);
data = parsedCache.value; data = parsedCache.value;
//console.log(`Using expired cache data for ${coinSymbol}`);
} catch (cacheError) { } catch (cacheError) {
throw error; throw error;
} }
@@ -940,6 +1200,11 @@ const chartModule = {
} }
} }
} }
if (chartModule.currentCoin !== coinSymbol) {
chartModule.destroyChart();
chartModule.initChart();
}
const chartData = chartModule.prepareChartData(coinSymbol, data); const chartData = chartModule.prepareChartData(coinSymbol, data);
if (chartData.length > 0 && chartModule.chart) { if (chartData.length > 0 && chartModule.chart) {
chartModule.chart.data.datasets[0].data = chartData; chartModule.chart.data.datasets[0].data = chartData;
@@ -959,65 +1224,92 @@ const chartModule = {
} }
} catch (error) { } catch (error) {
console.error(`Error updating chart for ${coinSymbol}:`, error); console.error(`Error updating chart for ${coinSymbol}:`, error);
if (!(chartModule.chart?.data.datasets[0].data.length > 0)) {
// Keep existing chart data if possible /todo
if (!(chartModule.chart?.data?.datasets[0]?.data?.length > 0)) {
if (!chartModule.chart) {
chartModule.initChart();
}
if (chartModule.chart) {
chartModule.chart.data.datasets[0].data = []; chartModule.chart.data.datasets[0].data = [];
chartModule.chart.update('active'); chartModule.chart.update('active');
} }
}
} finally { } finally {
chartModule.hideChartLoader(); chartModule.hideChartLoader();
} }
}, },
showChartLoader: () => { showChartLoader: function() {
const loader = document.getElementById('chart-loader'); const loader = document.getElementById('chart-loader');
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.remove('hidden'); loader.classList.remove('hidden');
chart.classList.add('hidden'); chart.classList.add('hidden');
}, },
hideChartLoader: () => { hideChartLoader: function() {
const loader = document.getElementById('chart-loader'); const loader = document.getElementById('chart-loader');
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');
}, },
cleanup: function() {
this.destroyChart();
this.currentCoin = null;
this.loadStartTime = 0;
console.log('Chart module cleaned up');
}
}; };
Chart.register(chartModule.verticalLinePlugin); Chart.register(chartModule.verticalLinePlugin);
const volumeToggle = { const volumeToggle = {
isVisible: localStorage.getItem('volumeToggleState') === 'true', isVisible: localStorage.getItem('volumeToggleState') === 'true',
init: () => { init: function() {
const toggleButton = document.getElementById('toggle-volume'); const toggleButton = document.getElementById('toggle-volume');
if (toggleButton) { if (toggleButton) {
if (typeof cleanupManager !== 'undefined') {
cleanupManager.addListener(toggleButton, 'click', volumeToggle.toggle);
} else {
toggleButton.addEventListener('click', volumeToggle.toggle); toggleButton.addEventListener('click', volumeToggle.toggle);
}
volumeToggle.updateVolumeDisplay(); volumeToggle.updateVolumeDisplay();
} }
}, },
toggle: () => {
toggle: function() {
volumeToggle.isVisible = !volumeToggle.isVisible; volumeToggle.isVisible = !volumeToggle.isVisible;
localStorage.setItem('volumeToggleState', volumeToggle.isVisible.toString()); localStorage.setItem('volumeToggleState', volumeToggle.isVisible.toString());
volumeToggle.updateVolumeDisplay(); volumeToggle.updateVolumeDisplay();
}, },
updateVolumeDisplay: () => {
updateVolumeDisplay: function() {
const volumeDivs = document.querySelectorAll('[id$="-volume-div"]'); const volumeDivs = document.querySelectorAll('[id$="-volume-div"]');
volumeDivs.forEach(div => { volumeDivs.forEach(div => {
if (div) {
div.style.display = volumeToggle.isVisible ? 'flex' : 'none'; div.style.display = volumeToggle.isVisible ? 'flex' : 'none';
}
}); });
const toggleButton = document.getElementById('toggle-volume'); const toggleButton = document.getElementById('toggle-volume');
if (toggleButton) { if (toggleButton) {
updateButtonStyles(toggleButton, volumeToggle.isVisible, 'green'); updateButtonStyles(toggleButton, volumeToggle.isVisible, 'green');
} }
},
cleanup: function() {
const toggleButton = document.getElementById('toggle-volume');
if (toggleButton) {
toggleButton.removeEventListener('click', volumeToggle.toggle);
} }
}; }
};
function updateButtonStyles(button, isActive, color) { function updateButtonStyles(button, isActive, color) {
button.classList.toggle('text-' + color + '-500', isActive); button.classList.toggle('text-' + color + '-500', isActive);
@@ -1042,7 +1334,7 @@ const app = {
minimumRefreshInterval: 60 * 1000, // 1 min minimumRefreshInterval: 60 * 1000, // 1 min
init: () => { init: () => {
//console.log('Init'); console.log('Init');
window.addEventListener('load', app.onLoad); window.addEventListener('load', app.onLoad);
app.loadLastRefreshedTime(); app.loadLastRefreshedTime();
app.updateAutoRefreshButton(); app.updateAutoRefreshButton();
@@ -1689,5 +1981,48 @@ resolutionButtons.forEach(button => {
}); });
}); });
// LOAD
const appCleanup = {
init: function() {
memoryMonitor.startMonitoring();
window.addEventListener('beforeunload', this.globalCleanup);
},
globalCleanup: function() {
try {
if (app.autoRefreshInterval) {
clearTimeout(app.autoRefreshInterval);
}
if (chartModule) {
chartModule.cleanup();
}
if (volumeToggle) {
volumeToggle.cleanup();
}
cleanupManager.clearAll();
memoryMonitor.stopMonitoring();
cache.clear();
console.log('Global application cleanup completed');
} catch (error) {
console.error('Error during global cleanup:', error);
}
},
manualCleanup: function() {
this.globalCleanup();
window.location.reload();
}
};
app.init = () => {
//console.log('Init');
window.addEventListener('load', app.onLoad);
appCleanup.init();
app.loadLastRefreshedTime();
app.updateAutoRefreshButton();
memoryMonitor.startMonitoring();
//console.log('App initialized');
};
// LOAD // LOAD
app.init(); app.init();

View File

@@ -1,9 +1,11 @@
class TooltipManager { class TooltipManager {
constructor() { constructor() {
this.activeTooltips = new Map(); this.activeTooltips = new WeakMap();
this.sizeCheckIntervals = new Map(); this.sizeCheckIntervals = new WeakMap();
this.tooltipIdCounter = 0;
this.setupStyles(); this.setupStyles();
this.setupCleanupEvents(); this.setupCleanupEvents();
this.initializeMutationObserver();
} }
static initialize() { static initialize() {
@@ -19,16 +21,26 @@ class TooltipManager {
this.destroy(element); this.destroy(element);
const checkSize = () => { const checkSize = () => {
if (!document.body.contains(element)) {
return;
}
const rect = element.getBoundingClientRect(); const rect = element.getBoundingClientRect();
if (rect.width && rect.height) { if (rect.width && rect.height) {
clearInterval(this.sizeCheckIntervals.get(element)); delete element._tooltipRetryCount;
this.sizeCheckIntervals.delete(element);
this.createTooltip(element, content, options, rect); this.createTooltip(element, content, options, rect);
} else {
const retryCount = element._tooltipRetryCount || 0;
if (retryCount < 5) {
element._tooltipRetryCount = retryCount + 1;
requestAnimationFrame(checkSize);
} else {
delete element._tooltipRetryCount;
}
} }
}; };
this.sizeCheckIntervals.set(element, setInterval(checkSize, 50)); requestAnimationFrame(checkSize);
checkSize();
return null; return null;
} }
@@ -62,6 +74,8 @@ class TooltipManager {
} }
} }
const tooltipId = `tooltip-${++this.tooltipIdCounter}`;
const instance = tippy(element, { const instance = tippy(element, {
content, content,
allowHTML: true, allowHTML: true,
@@ -75,6 +89,28 @@ class TooltipManager {
theme: '', theme: '',
moveTransition: 'none', moveTransition: 'none',
offset: [0, 10], offset: [0, 10],
onShow(instance) {
if (!document.body.contains(element)) {
return false;
}
const rect = element.getBoundingClientRect();
if (!rect.width || !rect.height) {
return false;
}
return true;
},
onMount(instance) {
if (instance.popper.firstElementChild) {
instance.popper.firstElementChild.classList.add(bgClass);
instance.popper.setAttribute('data-for-tooltip-id', tooltipId);
}
const arrow = instance.popper.querySelector('.tippy-arrow');
if (arrow) {
arrow.style.setProperty('color', arrowColor, 'important');
}
},
popperOptions: { popperOptions: {
strategy: 'fixed', strategy: 'fixed',
modifiers: [ modifiers: [
@@ -93,45 +129,11 @@ class TooltipManager {
} }
} }
] ]
},
onCreate(instance) {
instance._originalPlacement = instance.props.placement;
},
onShow(instance) {
if (!document.body.contains(element)) {
return false;
}
const rect = element.getBoundingClientRect();
if (!rect.width || !rect.height) {
return false;
}
instance.setProps({
placement: instance._originalPlacement
});
if (instance.popper.firstElementChild) {
instance.popper.firstElementChild.classList.add(bgClass);
}
return true;
},
onMount(instance) {
if (instance.popper.firstElementChild) {
instance.popper.firstElementChild.classList.add(bgClass);
}
const arrow = instance.popper.querySelector('.tippy-arrow');
if (arrow) {
arrow.style.setProperty('color', arrowColor, 'important');
}
} }
}); });
const id = element.getAttribute('data-tooltip-trigger-id') || element.setAttribute('data-tooltip-trigger-id', tooltipId);
`tooltip-${Math.random().toString(36).substring(7)}`; this.activeTooltips.set(element, instance);
element.setAttribute('data-tooltip-trigger-id', id);
this.activeTooltips.set(id, instance);
return instance; return instance;
} }
@@ -139,40 +141,33 @@ class TooltipManager {
destroy(element) { destroy(element) {
if (!element) return; if (!element) return;
if (this.sizeCheckIntervals.has(element)) { delete element._tooltipRetryCount;
clearInterval(this.sizeCheckIntervals.get(element));
this.sizeCheckIntervals.delete(element);
}
const id = element.getAttribute('data-tooltip-trigger-id'); const id = element.getAttribute('data-tooltip-trigger-id');
if (!id) return; if (!id) return;
const instance = this.activeTooltips.get(id); const instance = this.activeTooltips.get(element);
if (instance?.[0]) { if (instance?.[0]) {
try { try {
instance[0].destroy(); instance[0].destroy();
} catch (e) { } catch (e) {
console.warn('Error destroying tooltip:', e); console.warn('Error destroying tooltip:', e);
const tippyRoot = document.querySelector(`[data-for-tooltip-id="${id}"]`);
if (tippyRoot && tippyRoot.parentNode) {
tippyRoot.parentNode.removeChild(tippyRoot);
} }
} }
this.activeTooltips.delete(id); }
this.activeTooltips.delete(element);
element.removeAttribute('data-tooltip-trigger-id'); element.removeAttribute('data-tooltip-trigger-id');
} }
cleanup() { cleanup() {
this.sizeCheckIntervals.forEach((interval) => clearInterval(interval)); document.querySelectorAll('[data-tooltip-trigger-id]').forEach(element => {
this.sizeCheckIntervals.clear(); this.destroy(element);
this.activeTooltips.forEach((instance, id) => {
if (instance?.[0]) {
try {
instance[0].destroy();
} catch (e) {
console.warn('Error cleaning up tooltip:', e);
}
}
}); });
this.activeTooltips.clear();
document.querySelectorAll('[data-tippy-root]').forEach(element => { document.querySelectorAll('[data-tippy-root]').forEach(element => {
if (element.parentNode) { if (element.parentNode) {
@@ -181,6 +176,63 @@ class TooltipManager {
}); });
} }
getActiveTooltipInstances() {
const result = [];
document.querySelectorAll('[data-tooltip-trigger-id]').forEach(element => {
const instance = this.activeTooltips.get(element);
if (instance) {
result.push([element, instance]);
}
});
return result;
}
initializeMutationObserver() {
if (this.mutationObserver) return;
this.mutationObserver = new MutationObserver(mutations => {
let needsCleanup = false;
mutations.forEach(mutation => {
if (mutation.removedNodes.length) {
Array.from(mutation.removedNodes).forEach(node => {
if (node.nodeType === 1) {
if (node.hasAttribute && node.hasAttribute('data-tooltip-trigger-id')) {
this.destroy(node);
needsCleanup = true;
}
if (node.querySelectorAll) {
node.querySelectorAll('[data-tooltip-trigger-id]').forEach(el => {
this.destroy(el);
needsCleanup = true;
});
}
}
});
}
});
if (needsCleanup) {
document.querySelectorAll('[data-tippy-root]').forEach(element => {
const id = element.getAttribute('data-for-tooltip-id');
if (id && !document.querySelector(`[data-tooltip-trigger-id="${id}"]`)) {
if (element.parentNode) {
element.parentNode.removeChild(element);
}
}
});
}
});
this.mutationObserver.observe(document.body, {
childList: true,
subtree: true
});
}
setupStyles() { setupStyles() {
if (document.getElementById('tooltip-styles')) return; if (document.getElementById('tooltip-styles')) return;
@@ -274,13 +326,22 @@ class TooltipManager {
} }
setupCleanupEvents() { setupCleanupEvents() {
window.addEventListener('beforeunload', () => this.cleanup()); this.boundCleanup = this.cleanup.bind(this);
window.addEventListener('unload', () => this.cleanup()); this.handleVisibilityChange = () => {
document.addEventListener('visibilitychange', () => {
if (document.hidden) { if (document.hidden) {
this.cleanup(); this.cleanup();
} }
}); };
window.addEventListener('beforeunload', this.boundCleanup);
window.addEventListener('unload', this.boundCleanup);
document.addEventListener('visibilitychange', this.handleVisibilityChange);
}
removeCleanupEvents() {
window.removeEventListener('beforeunload', this.boundCleanup);
window.removeEventListener('unload', this.boundCleanup);
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
} }
initializeTooltips(selector = '[data-tooltip-target]') { initializeTooltips(selector = '[data-tooltip-target]') {
@@ -295,6 +356,26 @@ class TooltipManager {
} }
}); });
} }
dispose() {
this.cleanup();
if (this.mutationObserver) {
this.mutationObserver.disconnect();
this.mutationObserver = null;
}
this.removeCleanupEvents();
const styleElement = document.getElementById('tooltip-styles');
if (styleElement && styleElement.parentNode) {
styleElement.parentNode.removeChild(styleElement);
}
if (window.TooltipManager === this) {
window.TooltipManager = null;
}
}
} }
if (typeof module !== 'undefined' && module.exports) { if (typeof module !== 'undefined' && module.exports) {