From d57a148ff4dba6ceebda596bb746cf165260ad33 Mon Sep 17 00:00:00 2001 From: gerlofvanek Date: Sun, 4 May 2025 19:51:22 +0200 Subject: [PATCH 1/5] Fix: Price Tiles volume/btc display + Better memory clean / tooltip manager. --- basicswap/static/js/modules/api-manager.js | 63 +- basicswap/static/js/modules/memory-manager.js | 854 +++++++++++++----- .../static/js/modules/tooltips-manager.js | 551 +++++------ basicswap/static/js/offers.js | 51 +- basicswap/static/js/pricechart.js | 149 +-- 5 files changed, 1129 insertions(+), 539 deletions(-) diff --git a/basicswap/static/js/modules/api-manager.js b/basicswap/static/js/modules/api-manager.js index e38440c..bac3f22 100644 --- a/basicswap/static/js/modules/api-manager.js +++ b/basicswap/static/js/modules/api-manager.js @@ -260,30 +260,80 @@ const ApiManager = (function() { fetchVolumeData: async function() { return this.rateLimiter.queueRequest('coingecko', async () => { try { - const coins = (window.config && window.config.coins) ? + let coinList = (window.config && window.config.coins) ? window.config.coins .filter(coin => coin.usesCoinGecko) - .map(coin => getCoinBackendId ? getCoinBackendId(coin.name) : coin.name) + .map(coin => { + return window.config.getCoinBackendId ? + window.config.getCoinBackendId(coin.name) : + (typeof getCoinBackendId === 'function' ? + getCoinBackendId(coin.name) : coin.name.toLowerCase()); + }) .join(',') : - 'bitcoin,monero,particl,bitcoin-cash,pivx,firo,dash,litecoin,dogecoin,decred,namecoin'; + 'bitcoin,monero,particl,bitcoin-cash,pivx,firo,dash,litecoin,dogecoin,decred,namecoin,wownero'; - const url = `https://api.coingecko.com/api/v3/simple/price?ids=${coins}&vs_currencies=usd&include_24hr_vol=true&include_24hr_change=true`; + if (!coinList.includes('zcoin') && coinList.includes('firo')) { + coinList = coinList + ',zcoin'; + } + const url = `https://api.coingecko.com/api/v3/simple/price?ids=${coinList}&vs_currencies=usd&include_24hr_vol=true&include_24hr_change=true`; + const response = await this.makePostRequest(url, { 'User-Agent': 'Mozilla/5.0', 'Accept': 'application/json' }); + if (!response || typeof response !== 'object') { + throw new Error('Invalid response from CoinGecko API'); + } + const volumeData = {}; + Object.entries(response).forEach(([coinId, data]) => { - if (data && data.usd_24h_vol) { + if (data && data.usd_24h_vol !== undefined) { volumeData[coinId] = { - total_volume: data.usd_24h_vol, + total_volume: data.usd_24h_vol || 0, price_change_percentage_24h: data.usd_24h_change || 0 }; } }); + const coinMappings = { + 'firo': ['firo', 'zcoin'], + 'zcoin': ['zcoin', 'firo'], + 'bitcoin-cash': ['bitcoin-cash', 'bitcoincash', 'bch'], + 'bitcoincash': ['bitcoincash', 'bitcoin-cash', 'bch'], + 'particl': ['particl', 'part'] + }; + + if (response['zcoin'] && (!volumeData['firo'] || volumeData['firo'].total_volume === 0)) { + volumeData['firo'] = { + total_volume: response['zcoin'].usd_24h_vol || 0, + price_change_percentage_24h: response['zcoin'].usd_24h_change || 0 + }; + } + + if (response['bitcoin-cash'] && (!volumeData['bitcoincash'] || volumeData['bitcoincash'].total_volume === 0)) { + volumeData['bitcoincash'] = { + total_volume: response['bitcoin-cash'].usd_24h_vol || 0, + price_change_percentage_24h: response['bitcoin-cash'].usd_24h_change || 0 + }; + } + + for (const [mainCoin, alternativeIds] of Object.entries(coinMappings)) { + if (!volumeData[mainCoin] || volumeData[mainCoin].total_volume === 0) { + for (const altId of alternativeIds) { + if (response[altId] && response[altId].usd_24h_vol) { + volumeData[mainCoin] = { + total_volume: response[altId].usd_24h_vol, + price_change_percentage_24h: response[altId].usd_24h_change || 0 + }; + break; + } + } + } + } + return volumeData; } catch (error) { console.error("Error fetching volume data:", error); @@ -364,7 +414,6 @@ const ApiManager = (function() { }, dispose: function() { - // Clear any pending requests or resources rateLimiter.requestQueue = {}; rateLimiter.lastRequestTime = {}; state.isInitialized = false; diff --git a/basicswap/static/js/modules/memory-manager.js b/basicswap/static/js/modules/memory-manager.js index fd5489f..8a96323 100644 --- a/basicswap/static/js/modules/memory-manager.js +++ b/basicswap/static/js/modules/memory-manager.js @@ -1,219 +1,663 @@ const MemoryManager = (function() { + const config = { + tooltipCleanupInterval: 60000, + maxTooltipsThreshold: 100, + diagnosticsInterval: 300000, + tooltipLifespan: 240000, + debug: false, + autoCleanup: true, + elementVerificationInterval: 50000, + tooltipSelectors: [ + '[data-tippy-root]', + '[data-tooltip-trigger-id]', + '.tooltip', + '.tippy-box', + '.tippy-content' + ] + }; - const state = { - isMonitoringEnabled: false, - monitorInterval: null, - cleanupInterval: null - }; + let mutationObserver = null; - const config = { - monitorInterval: 30000, - cleanupInterval: 60000, - debug: false - }; + const safeGet = (obj, path, defaultValue = null) => { + if (!obj) return defaultValue; + const pathParts = path.split('.'); + let result = obj; + for (const part of pathParts) { + if (result === null || result === undefined) return defaultValue; + result = result[part]; + } + return result !== undefined ? result : defaultValue; + }; - function log(message, ...args) { - if (config.debug) { - console.log(`[MemoryManager] ${message}`, ...args); + const state = { + intervals: new Map(), + trackedTooltips: new Map(), + trackedElements: new WeakMap(), + startTime: Date.now(), + lastCleanupTime: Date.now(), + metrics: { + tooltipsCreated: 0, + tooltipsDestroyed: 0, + orphanedTooltipsRemoved: 0, + elementsProcessed: 0, + cleanupRuns: 0, + manualCleanupRuns: 0, + lastMemoryUsage: null + } + }; + + const log = (message, ...args) => { + if (!config.debug) return; + const now = new Date().toISOString(); + console.log(`[MemoryManager ${now}]`, message, ...args); + }; + + const logError = (message, error) => { + console.error(`[MemoryManager] ${message}`, error); + }; + + const trackTooltip = (element, tooltipInstance) => { + try { + if (!element || !tooltipInstance) return; + + const timestamp = Date.now(); + const tooltipId = element.getAttribute('data-tooltip-trigger-id') || `tooltip_${timestamp}_${Math.random().toString(36).substring(2, 9)}`; + + state.trackedTooltips.set(tooltipId, { + timestamp, + element, + instance: tooltipInstance, + processed: false + }); + + state.metrics.tooltipsCreated++; + + setTimeout(() => { + if (state.trackedTooltips.has(tooltipId)) { + destroyTooltip(tooltipId); } + }, config.tooltipLifespan); + + return tooltipId; + } catch (error) { + logError('Error tracking tooltip:', error); + return null; + } + }; + + const destroyTooltip = (tooltipId) => { + try { + const tooltipInfo = state.trackedTooltips.get(tooltipId); + if (!tooltipInfo) return false; + + const { element, instance } = tooltipInfo; + + if (instance && typeof instance.destroy === 'function') { + instance.destroy(); + } + + if (element && element.removeAttribute) { + element.removeAttribute('data-tooltip-trigger-id'); + element.removeAttribute('aria-describedby'); + } + + const tippyRoot = document.querySelector(`[data-for-tooltip-id="${tooltipId}"]`); + if (tippyRoot && tippyRoot.parentNode) { + tippyRoot.parentNode.removeChild(tippyRoot); + } + + state.trackedTooltips.delete(tooltipId); + state.metrics.tooltipsDestroyed++; + + return true; + } catch (error) { + logError(`Error destroying tooltip ${tooltipId}:`, error); + return false; + } + }; + + const removeOrphanedTooltips = () => { + try { + const tippyRoots = document.querySelectorAll('[data-tippy-root]'); + let removed = 0; + + tippyRoots.forEach(root => { + const tooltipId = root.getAttribute('data-for-tooltip-id'); + + const trigger = tooltipId ? + document.querySelector(`[data-tooltip-trigger-id="${tooltipId}"]`) : + null; + + if (!trigger || !document.body.contains(trigger)) { + if (root.parentNode) { + root.parentNode.removeChild(root); + removed++; + } + } + }); + + document.querySelectorAll('[data-tooltip-trigger-id]').forEach(trigger => { + const tooltipId = trigger.getAttribute('data-tooltip-trigger-id'); + const root = document.querySelector(`[data-for-tooltip-id="${tooltipId}"]`); + + if (!root) { + trigger.removeAttribute('data-tooltip-trigger-id'); + trigger.removeAttribute('aria-describedby'); + removed++; + } + }); + + state.metrics.orphanedTooltipsRemoved += removed; + return removed; + } catch (error) { + logError('Error removing orphaned tooltips:', error); + return 0; + } + }; + + const checkMemoryUsage = () => { + if (window.performance && window.performance.memory) { + const memoryUsage = { + usedJSHeapSize: window.performance.memory.usedJSHeapSize, + totalJSHeapSize: window.performance.memory.totalJSHeapSize, + jsHeapSizeLimit: window.performance.memory.jsHeapSizeLimit, + percentUsed: (window.performance.memory.usedJSHeapSize / window.performance.memory.jsHeapSizeLimit * 100).toFixed(2) + }; + + state.metrics.lastMemoryUsage = memoryUsage; + return memoryUsage; } - const publicAPI = { - enableMonitoring: function(interval = config.monitorInterval) { - if (state.monitorInterval) { - clearInterval(state.monitorInterval); - } + return null; + }; - state.isMonitoringEnabled = true; - config.monitorInterval = interval; + const checkForDisconnectedElements = () => { + try { + const disconnectedElements = new Set(); - this.logMemoryUsage(); - - state.monitorInterval = setInterval(() => { - this.logMemoryUsage(); - }, interval); - - console.log(`Memory monitoring enabled - reporting every ${interval/1000} seconds`); - return true; - }, - - disableMonitoring: function() { - if (state.monitorInterval) { - clearInterval(state.monitorInterval); - state.monitorInterval = null; - } - - state.isMonitoringEnabled = false; - console.log('Memory monitoring disabled'); - return true; - }, - - logMemoryUsage: function() { - const timestamp = new Date().toLocaleTimeString(); - console.log(`=== Memory Monitor [${timestamp}] ===`); - - if (window.performance && window.performance.memory) { - console.log('Memory usage:', { - usedJSHeapSize: (window.performance.memory.usedJSHeapSize / 1024 / 1024).toFixed(2) + ' MB', - totalJSHeapSize: (window.performance.memory.totalJSHeapSize / 1024 / 1024).toFixed(2) + ' MB' - }); - } - - if (navigator.deviceMemory) { - console.log('Device memory:', navigator.deviceMemory, 'GB'); - } - - const nodeCount = document.querySelectorAll('*').length; - console.log('DOM node count:', nodeCount); - - if (window.CleanupManager) { - const counts = CleanupManager.getResourceCounts(); - console.log('Managed resources:', counts); - } - - if (window.TooltipManager) { - const tooltipInstances = document.querySelectorAll('[data-tippy-root]').length; - const tooltipTriggers = document.querySelectorAll('[data-tooltip-trigger-id]').length; - console.log('Tooltip instances:', tooltipInstances, '- Tooltip triggers:', tooltipTriggers); - } - - if (window.CacheManager && window.CacheManager.getStats) { - const cacheStats = CacheManager.getStats(); - console.log('Cache stats:', cacheStats); - } - - if (window.IdentityManager && window.IdentityManager.getStats) { - const identityStats = window.IdentityManager.getStats(); - console.log('Identity cache stats:', identityStats); - } - - console.log('=============================='); - }, - - enableAutoCleanup: function(interval = config.cleanupInterval) { - if (state.cleanupInterval) { - clearInterval(state.cleanupInterval); - } - - config.cleanupInterval = interval; - - this.forceCleanup(); - - state.cleanupInterval = setInterval(() => { - this.forceCleanup(); - }, interval); - - log('Auto-cleanup enabled every', interval/1000, 'seconds'); - return true; - }, - - disableAutoCleanup: function() { - if (state.cleanupInterval) { - clearInterval(state.cleanupInterval); - state.cleanupInterval = null; - } - - console.log('Memory auto-cleanup disabled'); - return true; - }, - - forceCleanup: function() { - if (config.debug) { - console.log('Running memory cleanup...', new Date().toLocaleTimeString()); - } - - if (window.CacheManager && CacheManager.cleanup) { - CacheManager.cleanup(true); - } - - if (window.TooltipManager && TooltipManager.cleanup) { - window.TooltipManager.cleanup(); - } - - document.querySelectorAll('[data-tooltip-trigger-id]').forEach(element => { - if (window.TooltipManager && TooltipManager.destroy) { - window.TooltipManager.destroy(element); - } - }); - - if (window.chartModule && chartModule.cleanup) { - chartModule.cleanup(); - } - - if (window.gc) { - window.gc(); - } else { - const arr = new Array(1000); - for (let i = 0; i < 1000; i++) { - arr[i] = new Array(10000).join('x'); - } - } - - if (config.debug) { - console.log('Memory cleanup completed'); - } - - return true; - }, - - setDebugMode: function(enabled) { - config.debug = Boolean(enabled); - return `Debug mode ${config.debug ? 'enabled' : 'disabled'}`; - }, - - getStatus: function() { - return { - monitoring: { - enabled: Boolean(state.monitorInterval), - interval: config.monitorInterval - }, - autoCleanup: { - enabled: Boolean(state.cleanupInterval), - interval: config.cleanupInterval - }, - debug: config.debug - }; - }, - - initialize: function(options = {}) { - if (options.debug !== undefined) { - this.setDebugMode(options.debug); - } - - if (options.enableMonitoring) { - this.enableMonitoring(options.monitorInterval || config.monitorInterval); - } - - if (options.enableAutoCleanup) { - this.enableAutoCleanup(options.cleanupInterval || config.cleanupInterval); - } - - if (window.CleanupManager) { - window.CleanupManager.registerResource('memoryManager', this, (mgr) => mgr.dispose()); - } - - log('MemoryManager initialized'); - return this; - }, - - dispose: function() { - this.disableMonitoring(); - this.disableAutoCleanup(); - log('MemoryManager disposed'); + state.trackedTooltips.forEach((info, id) => { + const { element } = info; + if (element && !document.body.contains(element)) { + disconnectedElements.add(id); } - }; + }); - return publicAPI; + disconnectedElements.forEach(id => { + destroyTooltip(id); + }); + + return disconnectedElements.size; + } catch (error) { + logError('Error checking for disconnected elements:', error); + return 0; + } + }; + + const setupMutationObserver = () => { + if (mutationObserver) { + mutationObserver.disconnect(); + } + + 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')) { + const tooltipId = node.getAttribute('data-tooltip-trigger-id'); + destroyTooltip(tooltipId); + needsCleanup = true; + } + + if (node.querySelectorAll) { + const tooltipTriggers = node.querySelectorAll('[data-tooltip-trigger-id]'); + if (tooltipTriggers.length > 0) { + tooltipTriggers.forEach(el => { + const tooltipId = el.getAttribute('data-tooltip-trigger-id'); + destroyTooltip(tooltipId); + }); + needsCleanup = true; + } + } + } + }); + } + }); + + if (needsCleanup) { + removeOrphanedTooltips(); + } + }); + + mutationObserver.observe(document.body, { + childList: true, + subtree: true + }); + + return mutationObserver; + }; + + const performCleanup = (force = false) => { + try { + log('Starting tooltip cleanup' + (force ? ' (forced)' : '')); + + state.lastCleanupTime = Date.now(); + state.metrics.cleanupRuns++; + + if (force) { + state.metrics.manualCleanupRuns++; + } + + document.querySelectorAll('[data-tippy-root]').forEach(root => { + const instance = safeGet(root, '_tippy'); + if (instance && instance._animationFrame) { + cancelAnimationFrame(instance._animationFrame); + instance._animationFrame = null; + } + }); + + const orphanedRemoved = removeOrphanedTooltips(); + + const disconnectedRemoved = checkForDisconnectedElements(); + + const tooltipCount = document.querySelectorAll('[data-tippy-root]').length; + const triggerCount = document.querySelectorAll('[data-tooltip-trigger-id]').length; + + if (force || tooltipCount > config.maxTooltipsThreshold) { + if (tooltipCount > config.maxTooltipsThreshold) { + document.querySelectorAll('[data-tooltip-trigger-id]').forEach(trigger => { + const tooltipId = trigger.getAttribute('data-tooltip-trigger-id'); + destroyTooltip(tooltipId); + }); + + document.querySelectorAll('[data-tippy-root]').forEach(root => { + if (root.parentNode) { + root.parentNode.removeChild(root); + } + }); + } + + document.querySelectorAll('[data-tooltip-trigger-id], [aria-describedby]').forEach(el => { + if (window.CleanupManager && window.CleanupManager.removeListenersByElement) { + window.CleanupManager.removeListenersByElement(el); + } else { + if (el.parentNode) { + const clone = el.cloneNode(true); + el.parentNode.replaceChild(clone, el); + } + } + }); + } + + if (window.gc) { + window.gc(); + } else if (force) { + const arr = new Array(1000); + for (let i = 0; i < 1000; i++) { + arr[i] = new Array(10000).join('x'); + } + } + + checkMemoryUsage(); + + const result = { + orphanedRemoved, + disconnectedRemoved, + tooltipCount: document.querySelectorAll('[data-tippy-root]').length, + triggerCount: document.querySelectorAll('[data-tooltip-trigger-id]').length, + memoryUsage: state.metrics.lastMemoryUsage + }; + + log('Cleanup completed', result); + return result; + } catch (error) { + logError('Error during cleanup:', error); + return { error: error.message }; + } + }; + + const runDiagnostics = () => { + try { + log('Running memory diagnostics'); + + const memoryUsage = checkMemoryUsage(); + const tooltipCount = document.querySelectorAll('[data-tippy-root]').length; + const triggerCount = document.querySelectorAll('[data-tooltip-trigger-id]').length; + + const diagnostics = { + time: new Date().toISOString(), + uptime: Date.now() - state.startTime, + memoryUsage, + elementsCount: { + tippyRoots: tooltipCount, + tooltipTriggers: triggerCount, + orphanedTriggers: triggerCount - tooltipCount > 0 ? triggerCount - tooltipCount : 0, + orphanedTooltips: tooltipCount - triggerCount > 0 ? tooltipCount - triggerCount : 0 + }, + metrics: { ...state.metrics }, + issues: [] + }; + + if (tooltipCount > config.maxTooltipsThreshold) { + diagnostics.issues.push({ + severity: 'high', + message: `Excessive tooltip count: ${tooltipCount} (threshold: ${config.maxTooltipsThreshold})`, + recommendation: 'Run cleanup and check for tooltip creation loops' + }); + } + + if (Math.abs(tooltipCount - triggerCount) > 10) { + diagnostics.issues.push({ + severity: 'medium', + message: `Mismatch between tooltips (${tooltipCount}) and triggers (${triggerCount})`, + recommendation: 'Remove orphaned tooltips and tooltip triggers' + }); + } + + if (memoryUsage && memoryUsage.percentUsed > 80) { + diagnostics.issues.push({ + severity: 'high', + message: `High memory usage: ${memoryUsage.percentUsed}%`, + recommendation: 'Force garbage collection and check for memory leaks' + }); + } + + if (config.autoCleanup && diagnostics.issues.some(issue => issue.severity === 'high')) { + log('Critical issues detected, triggering automatic cleanup'); + performCleanup(true); + } + + return diagnostics; + } catch (error) { + logError('Error running diagnostics:', error); + return { error: error.message }; + } + }; + + const patchTooltipManager = () => { + try { + if (!window.TooltipManager) { + log('TooltipManager not found'); + return false; + } + + log('Patching TooltipManager'); + + const originalCreate = window.TooltipManager.create; + const originalDestroy = window.TooltipManager.destroy; + const originalCleanup = window.TooltipManager.cleanup; + + window.TooltipManager.create = function(element, content, options = {}) { + if (!element) return null; + + try { + const result = originalCreate.call(this, element, content, options); + const tooltipId = element.getAttribute('data-tooltip-trigger-id'); + + if (tooltipId) { + const tippyInstance = safeGet(element, '_tippy') || null; + trackTooltip(element, tippyInstance); + } + + return result; + } catch (error) { + logError('Error in patched create:', error); + return originalCreate.call(this, element, content, options); + } + }; + + window.TooltipManager.destroy = function(element) { + if (!element) return; + + try { + const tooltipId = element.getAttribute('data-tooltip-trigger-id'); + + originalDestroy.call(this, element); + + if (tooltipId) { + state.trackedTooltips.delete(tooltipId); + state.metrics.tooltipsDestroyed++; + } + } catch (error) { + logError('Error in patched destroy:', error); + originalDestroy.call(this, element); + } + }; + + window.TooltipManager.cleanup = function() { + try { + originalCleanup.call(this); + removeOrphanedTooltips(); + } catch (error) { + logError('Error in patched cleanup:', error); + originalCleanup.call(this); + } + }; + + return true; + } catch (error) { + logError('Error patching TooltipManager:', error); + return false; + } + }; + + const patchTippy = () => { + try { + if (typeof tippy !== 'function') { + log('tippy.js not found globally'); + return false; + } + + log('Patching global tippy'); + + const originalTippy = window.tippy; + + window.tippy = function(...args) { + const result = originalTippy.apply(this, args); + + if (Array.isArray(result)) { + result.forEach(instance => { + const reference = instance.reference; + + if (reference) { + const originalShow = instance.show; + const originalHide = instance.hide; + const originalDestroy = instance.destroy; + + instance.show = function(...showArgs) { + return originalShow.apply(this, showArgs); + }; + + instance.hide = function(...hideArgs) { + return originalHide.apply(this, hideArgs); + }; + + instance.destroy = function(...destroyArgs) { + return originalDestroy.apply(this, destroyArgs); + }; + } + }); + } + + return result; + }; + + Object.assign(window.tippy, originalTippy); + + return true; + } catch (error) { + logError('Error patching tippy:', error); + return false; + } + }; + + const startMonitoring = () => { + try { + stopMonitoring(); + + state.intervals.set('cleanup', setInterval(() => { + performCleanup(); + }, config.tooltipCleanupInterval)); + + state.intervals.set('diagnostics', setInterval(() => { + runDiagnostics(); + }, config.diagnosticsInterval)); + + state.intervals.set('elementVerification', setInterval(() => { + checkForDisconnectedElements(); + }, config.elementVerificationInterval)); + + setupMutationObserver(); + + log('Monitoring started'); + return true; + } catch (error) { + logError('Error starting monitoring:', error); + return false; + } + }; + + const stopMonitoring = () => { + try { + state.intervals.forEach((interval, key) => { + clearInterval(interval); + }); + + state.intervals.clear(); + + if (mutationObserver) { + mutationObserver.disconnect(); + mutationObserver = null; + } + + log('Monitoring stopped'); + return true; + } catch (error) { + logError('Error stopping monitoring:', error); + return false; + } + }; + + const autoFix = () => { + try { + log('Running auto-fix'); + + performCleanup(true); + + document.querySelectorAll('[data-tooltip-trigger-id]').forEach(element => { + const tooltipId = element.getAttribute('data-tooltip-trigger-id'); + const duplicates = document.querySelectorAll(`[data-tooltip-trigger-id="${tooltipId}"]`); + + if (duplicates.length > 1) { + for (let i = 1; i < duplicates.length; i++) { + duplicates[i].removeAttribute('data-tooltip-trigger-id'); + duplicates[i].removeAttribute('aria-describedby'); + } + } + }); + + const tippyRoots = document.querySelectorAll('[data-tippy-root]'); + tippyRoots.forEach(root => { + if (!document.body.contains(root) && root.parentNode) { + root.parentNode.removeChild(root); + } + }); + + if (window.TooltipManager && window.TooltipManager.getInstance) { + const manager = window.TooltipManager.getInstance(); + if (manager && manager.chartRefs && manager.chartRefs.clear) { + manager.chartRefs.clear(); + } + + if (manager && manager.tooltipElementsMap && manager.tooltipElementsMap.clear) { + manager.tooltipElementsMap.clear(); + } + } + + patchTooltipManager(); + patchTippy(); + + return true; + } catch (error) { + logError('Error during auto-fix:', error); + return false; + } + }; + + const initialize = (options = {}) => { + try { + Object.assign(config, options); + + if (document.head) { + const metaCache = document.createElement('meta'); + metaCache.setAttribute('http-equiv', 'Cache-Control'); + metaCache.setAttribute('content', 'no-store, max-age=0'); + document.head.appendChild(metaCache); + } + + patchTooltipManager(); + patchTippy(); + + startMonitoring(); + + if (window.CleanupManager && window.CleanupManager.registerResource) { + window.CleanupManager.registerResource('memorymanager', MemoryManager, (optimizer) => { + optimizer.dispose(); + }); + } + + log('Memory Optimizer initialized', config); + + setTimeout(() => { + runDiagnostics(); + }, 5000); + + return MemoryManager; + } catch (error) { + logError('Error initializing Memory Optimizer:', error); + return null; + } + }; + + const dispose = () => { + try { + log('Disposing Memory Optimizer'); + + performCleanup(true); + + stopMonitoring(); + + state.trackedTooltips.clear(); + + return true; + } catch (error) { + logError('Error disposing Memory Optimizer:', error); + return false; + } + }; + + return { + initialize, + dispose, + performCleanup, + runDiagnostics, + autoFix, + getConfig: () => ({ ...config }), + getMetrics: () => ({ ...state.metrics }), + setDebugMode: (enabled) => { + config.debug = Boolean(enabled); + return config.debug; + } + }; })(); +if (typeof document !== 'undefined') { + document.addEventListener('DOMContentLoaded', function() { + MemoryManager.initialize(); + }); +} + window.MemoryManager = MemoryManager; - -document.addEventListener('DOMContentLoaded', function() { - if (!window.memoryManagerInitialized) { - MemoryManager.initialize(); - window.memoryManagerInitialized = true; - } -}); - -//console.log('MemoryManager initialized with methods:', Object.keys(MemoryManager)); -console.log('MemoryManager initialized'); +console.log('Memory Manager initialized'); diff --git a/basicswap/static/js/modules/tooltips-manager.js b/basicswap/static/js/modules/tooltips-manager.js index 6af1b6e..69219f0 100644 --- a/basicswap/static/js/modules/tooltips-manager.js +++ b/basicswap/static/js/modules/tooltips-manager.js @@ -1,46 +1,52 @@ const TooltipManager = (function() { let instance = null; + const tooltipInstanceMap = new WeakMap(); + class TooltipManagerImpl { constructor() { - if (instance) { return instance; } - - this.activeTooltips = new WeakMap(); - this.tooltipIdCounter = 0; this.pendingAnimationFrames = new Set(); - this.tooltipElementsMap = new Map(); - this.maxTooltips = 300; - this.cleanupThreshold = 1.3; + this.pendingTimeouts = new Set(); + this.tooltipIdCounter = 0; + this.maxTooltips = 200; + this.cleanupThreshold = 1.2; this.disconnectedCheckInterval = null; - + this.cleanupInterval = null; + this.mutationObserver = null; + this.debug = false; + this.tooltipData = new WeakMap(); this.setupStyles(); - this.setupCleanupEvents(); - this.initializeMutationObserver(); + this.setupMutationObserver(); + this.startPeriodicCleanup(); this.startDisconnectedElementsCheck(); - instance = this; } + log(message, ...args) { + if (this.debug) { + console.log(`[TooltipManager] ${message}`, ...args); + } + } + create(element, content, options = {}) { - if (!element) return null; + if (!element || !document.body.contains(element)) return null; + + if (!document.contains(element)) { + this.log('Tried to create tooltip for detached element'); + return null; + } this.destroy(element); - if (this.tooltipElementsMap.size > this.maxTooltips * this.cleanupThreshold) { - const oldestEntries = Array.from(this.tooltipElementsMap.entries()) - .sort((a, b) => a[1].timestamp - b[1].timestamp) - .slice(0, 20); - - oldestEntries.forEach(([el]) => { - this.destroy(el); - }); + const currentTooltipCount = document.querySelectorAll('[data-tooltip-trigger-id]').length; + if (currentTooltipCount > this.maxTooltips * this.cleanupThreshold) { + this.cleanupOrphanedTooltips(); + this.performPeriodicCleanup(true); } - const originalContent = content; - const rafId = requestAnimationFrame(() => { this.pendingAnimationFrames.delete(rafId); @@ -48,81 +54,50 @@ const TooltipManager = (function() { const rect = element.getBoundingClientRect(); if (rect.width > 0 && rect.height > 0) { - this.createTooltip(element, originalContent, options, rect); + this.createTooltipInstance(element, content, options); } else { let retryCount = 0; + const maxRetries = 3; + const retryCreate = () => { const newRect = element.getBoundingClientRect(); - if ((newRect.width > 0 && newRect.height > 0) || retryCount >= 3) { + if ((newRect.width > 0 && newRect.height > 0) || retryCount >= maxRetries) { if (newRect.width > 0 && newRect.height > 0) { - this.createTooltip(element, originalContent, options, newRect); + this.createTooltipInstance(element, content, options); } } else { retryCount++; - const newRafId = requestAnimationFrame(retryCreate); - this.pendingAnimationFrames.add(newRafId); + const timeoutId = setTimeout(() => { + this.pendingTimeouts.delete(timeoutId); + const newRafId = requestAnimationFrame(retryCreate); + this.pendingAnimationFrames.add(newRafId); + }, 100); + this.pendingTimeouts.add(timeoutId); } }; - const initialRetryId = requestAnimationFrame(retryCreate); - this.pendingAnimationFrames.add(initialRetryId); + + const initialTimeoutId = setTimeout(() => { + this.pendingTimeouts.delete(initialTimeoutId); + const retryRafId = requestAnimationFrame(retryCreate); + this.pendingAnimationFrames.add(retryRafId); + }, 100); + this.pendingTimeouts.add(initialTimeoutId); } }); this.pendingAnimationFrames.add(rafId); return null; } - - createTooltip(element, content, options, rect) { - const targetId = element.getAttribute('data-tooltip-target'); - let bgClass = 'bg-gray-400'; - let arrowColor = 'rgb(156 163 175)'; - - if (targetId?.includes('tooltip-offer-') && window.jsonData) { - try { - const offerId = targetId.split('tooltip-offer-')[1]; - let actualOfferId = offerId; - - if (offerId.includes('_')) { - [actualOfferId] = offerId.split('_'); - } - - let offer = null; - if (Array.isArray(window.jsonData)) { - for (let i = 0; i < window.jsonData.length; i++) { - const o = window.jsonData[i]; - if (o && (o.unique_id === offerId || o.offer_id === actualOfferId)) { - offer = o; - break; - } - } - } - - if (offer) { - if (offer.is_revoked) { - bgClass = 'bg-red-500'; - arrowColor = 'rgb(239 68 68)'; - } else if (offer.is_own_offer) { - bgClass = 'bg-gray-300'; - arrowColor = 'rgb(209 213 219)'; - } else { - bgClass = 'bg-green-700'; - arrowColor = 'rgb(21 128 61)'; - } - } - } catch (e) { - console.warn('Error finding offer for tooltip:', e); - } + + createTooltipInstance(element, content, options = {}) { + if (!element || !document.body.contains(element) || !window.tippy) { + return null; } - const tooltipId = `tooltip-${++this.tooltipIdCounter}`; - try { - if (typeof tippy !== 'function') { - console.error('Tippy.js is not loaded. Cannot create tooltip.'); - return null; - } + const tooltipId = `tooltip-${++this.tooltipIdCounter}`; - const instance = tippy(element, { + const tooltipOptions = { content: content, allowHTML: true, placement: options.placement || 'top', @@ -143,14 +118,25 @@ const TooltipManager = (function() { }, onMount(instance) { if (instance.popper && instance.popper.firstElementChild) { + const bgClass = options.bgClass || 'bg-gray-400'; instance.popper.firstElementChild.classList.add(bgClass); instance.popper.setAttribute('data-for-tooltip-id', tooltipId); } const arrow = instance.popper.querySelector('.tippy-arrow'); if (arrow) { + const arrowColor = options.arrowColor || 'rgb(156 163 175)'; arrow.style.setProperty('color', arrowColor, 'important'); } }, + onHidden(instance) { + if (!document.body.contains(element)) { + setTimeout(() => { + if (instance && instance.destroy) { + instance.destroy(); + } + }, 100); + } + }, popperOptions: { strategy: 'fixed', modifiers: [ @@ -170,19 +156,27 @@ const TooltipManager = (function() { } ] } - }); + }; - element.setAttribute('data-tooltip-trigger-id', tooltipId); - this.activeTooltips.set(element, instance); + const tippyInstance = window.tippy(element, tooltipOptions); - this.tooltipElementsMap.set(element, { - timestamp: Date.now(), - id: tooltipId - }); + if (tippyInstance && Array.isArray(tippyInstance) && tippyInstance[0]) { + this.tooltipData.set(element, { + id: tooltipId, + instance: tippyInstance[0], + timestamp: Date.now() + }); - return instance; - } catch (e) { - console.error('Error creating tooltip:', e); + element.setAttribute('data-tooltip-trigger-id', tooltipId); + + tooltipInstanceMap.set(element, tippyInstance[0]); + + return tippyInstance[0]; + } + + return null; + } catch (error) { + console.error('Error creating tooltip:', error); return null; } } @@ -190,35 +184,49 @@ const TooltipManager = (function() { destroy(element) { if (!element) return; - const id = element.getAttribute('data-tooltip-trigger-id'); - if (!id) return; + try { + const tooltipId = element.getAttribute('data-tooltip-trigger-id'); + if (!tooltipId) return; - const instance = this.activeTooltips.get(element); - if (instance?.[0]) { - try { - instance[0].destroy(); - } catch (e) { - console.warn('Error destroying tooltip:', e); + const tooltipData = this.tooltipData.get(element); + const instance = tooltipData?.instance || tooltipInstanceMap.get(element); - const tippyRoot = document.querySelector(`[data-for-tooltip-id="${id}"]`); - if (tippyRoot && tippyRoot.parentNode) { - tippyRoot.parentNode.removeChild(tippyRoot); + if (instance) { + try { + instance.destroy(); + } catch (e) { + console.warn('Error destroying tooltip instance:', e); } } + + element.removeAttribute('data-tooltip-trigger-id'); + element.removeAttribute('aria-describedby'); + + const tippyRoot = document.querySelector(`[data-for-tooltip-id="${tooltipId}"]`); + if (tippyRoot && tippyRoot.parentNode) { + tippyRoot.parentNode.removeChild(tippyRoot); + } + + this.tooltipData.delete(element); + tooltipInstanceMap.delete(element); + } catch (error) { + console.error('Error destroying tooltip:', error); } - - this.activeTooltips.delete(element); - this.tooltipElementsMap.delete(element); - - element.removeAttribute('data-tooltip-trigger-id'); } cleanup() { + this.log('Running tooltip cleanup'); + this.pendingAnimationFrames.forEach(id => { cancelAnimationFrame(id); }); this.pendingAnimationFrames.clear(); + this.pendingTimeouts.forEach(id => { + clearTimeout(id); + }); + this.pendingTimeouts.clear(); + const elements = document.querySelectorAll('[data-tooltip-trigger-id]'); const batchSize = 20; @@ -236,26 +244,144 @@ const TooltipManager = (function() { }); this.pendingAnimationFrames.add(rafId); } else { - this.cleanupOrphanedTippyElements(); + this.cleanupOrphanedTooltips(); } }; if (elements.length > 0) { processElementsBatch(0); } else { - this.cleanupOrphanedTippyElements(); + this.cleanupOrphanedTooltips(); } - - this.tooltipElementsMap.clear(); } - cleanupOrphanedTippyElements() { + cleanupOrphanedTooltips() { const tippyElements = document.querySelectorAll('[data-tippy-root]'); + let removed = 0; + tippyElements.forEach(element => { - if (element.parentNode) { - element.parentNode.removeChild(element); + const tooltipId = element.getAttribute('data-for-tooltip-id'); + const trigger = tooltipId ? + document.querySelector(`[data-tooltip-trigger-id="${tooltipId}"]`) : + null; + + if (!trigger || !document.body.contains(trigger)) { + if (element.parentNode) { + element.parentNode.removeChild(element); + removed++; + } } }); + + if (removed > 0) { + this.log(`Removed ${removed} orphaned tooltip elements`); + } + + return removed; + } + + setupMutationObserver() { + if (this.mutationObserver) { + this.mutationObserver.disconnect(); + } + + this.mutationObserver = new MutationObserver(mutations => { + let needsCleanup = false; + + mutations.forEach(mutation => { + if (mutation.removedNodes.length) { + Array.from(mutation.removedNodes).forEach(node => { + if (node.nodeType === Node.ELEMENT_NODE) { + if (node.hasAttribute && node.hasAttribute('data-tooltip-trigger-id')) { + this.destroy(node); + needsCleanup = true; + } + + if (node.querySelectorAll) { + const tooltipTriggers = node.querySelectorAll('[data-tooltip-trigger-id]'); + if (tooltipTriggers.length > 0) { + tooltipTriggers.forEach(trigger => { + this.destroy(trigger); + }); + needsCleanup = true; + } + } + } + }); + } + }); + + if (needsCleanup) { + this.cleanupOrphanedTooltips(); + } + }); + + this.mutationObserver.observe(document.body, { + childList: true, + subtree: true + }); + + return this.mutationObserver; + } + + startDisconnectedElementsCheck() { + if (this.disconnectedCheckInterval) { + clearInterval(this.disconnectedCheckInterval); + } + + this.disconnectedCheckInterval = setInterval(() => { + this.checkForDisconnectedElements(); + }, 60000); + } + + checkForDisconnectedElements() { + const elements = document.querySelectorAll('[data-tooltip-trigger-id]'); + let removedCount = 0; + + elements.forEach(element => { + if (!document.body.contains(element)) { + this.destroy(element); + removedCount++; + } + }); + + if (removedCount > 0) { + this.log(`Removed ${removedCount} tooltips for disconnected elements`); + this.cleanupOrphanedTooltips(); + } + } + + startPeriodicCleanup() { + if (this.cleanupInterval) { + clearInterval(this.cleanupInterval); + } + + this.cleanupInterval = setInterval(() => { + this.performPeriodicCleanup(); + }, 120000); + } + + performPeriodicCleanup(force = false) { + this.cleanupOrphanedTooltips(); + + this.checkForDisconnectedElements(); + + const tooltipCount = document.querySelectorAll('[data-tippy-root]').length; + + if (force || tooltipCount > this.maxTooltips) { + this.log(`Performing aggressive cleanup (${tooltipCount} tooltips)`); + + this.cleanup(); + + if (window.gc) { + window.gc(); + } else { + const arr = new Array(1000); + for (let i = 0; i < 1000; i++) { + arr[i] = new Array(10000).join('x'); + } + } + } } setupStyles() { @@ -350,136 +476,11 @@ const TooltipManager = (function() { `); } - setupCleanupEvents() { - this.boundCleanup = this.cleanup.bind(this); - this.handleVisibilityChange = () => { - if (document.hidden) { - this.cleanup(); - - if (window.MemoryManager) { - window.MemoryManager.forceCleanup(); - } - } - }; - - window.addEventListener('beforeunload', this.boundCleanup); - window.addEventListener('unload', this.boundCleanup); - document.addEventListener('visibilitychange', this.handleVisibilityChange); - - if (window.CleanupManager) { - window.CleanupManager.registerResource('tooltipManager', this, (tm) => tm.dispose()); - } - - this.cleanupInterval = setInterval(() => { - this.performPeriodicCleanup(); - }, 120000); - } - - startDisconnectedElementsCheck() { - - if (this.disconnectedCheckInterval) { - clearInterval(this.disconnectedCheckInterval); - } - - this.disconnectedCheckInterval = setInterval(() => { - this.checkForDisconnectedElements(); - }, 60000); - } - - checkForDisconnectedElements() { - if (this.tooltipElementsMap.size === 0) return; - - const elementsToCheck = Array.from(this.tooltipElementsMap.keys()); - let removedCount = 0; - - elementsToCheck.forEach(element => { - - if (!document.body.contains(element)) { - this.destroy(element); - removedCount++; - } - }); - - if (removedCount > 0) { - this.cleanupOrphanedTippyElements(); - } - } - - performPeriodicCleanup() { - this.cleanupOrphanedTippyElements(); - this.checkForDisconnectedElements(); - - if (this.tooltipElementsMap.size > this.maxTooltips * this.cleanupThreshold) { - const sortedTooltips = Array.from(this.tooltipElementsMap.entries()) - .sort((a, b) => a[1].timestamp - b[1].timestamp); - - const tooltipsToRemove = sortedTooltips.slice(0, sortedTooltips.length - this.maxTooltips); - tooltipsToRemove.forEach(([element]) => { - this.destroy(element); - }); - } - } - - removeCleanupEvents() { - window.removeEventListener('beforeunload', this.boundCleanup); - window.removeEventListener('unload', this.boundCleanup); - document.removeEventListener('visibilitychange', this.handleVisibilityChange); - - if (this.cleanupInterval) { - clearInterval(this.cleanupInterval); - this.cleanupInterval = null; - } - - if (this.disconnectedCheckInterval) { - clearInterval(this.disconnectedCheckInterval); - this.disconnectedCheckInterval = null; - } - } - - 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) { - const tooltipTriggers = node.querySelectorAll('[data-tooltip-trigger-id]'); - if (tooltipTriggers.length > 0) { - tooltipTriggers.forEach(el => { - this.destroy(el); - }); - needsCleanup = true; - } - } - } - }); - } - }); - - if (needsCleanup) { - this.cleanupOrphanedTippyElements(); - } - }); - - this.mutationObserver.observe(document.body, { - childList: true, - subtree: true - }); - } - initializeTooltips(selector = '[data-tooltip-target]') { document.querySelectorAll(selector).forEach(element => { const targetId = element.getAttribute('data-tooltip-target'); + if (!targetId) return; + const tooltipContent = document.getElementById(targetId); if (tooltipContent) { @@ -491,6 +492,8 @@ const TooltipManager = (function() { } dispose() { + this.log('Disposing TooltipManager'); + this.cleanup(); this.pendingAnimationFrames.forEach(id => { @@ -498,31 +501,50 @@ const TooltipManager = (function() { }); this.pendingAnimationFrames.clear(); + this.pendingTimeouts.forEach(id => { + clearTimeout(id); + }); + this.pendingTimeouts.clear(); + + if (this.disconnectedCheckInterval) { + clearInterval(this.disconnectedCheckInterval); + this.disconnectedCheckInterval = null; + } + + if (this.cleanupInterval) { + clearInterval(this.cleanupInterval); + this.cleanupInterval = null; + } + 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); } - this.activeTooltips = new WeakMap(); - this.tooltipElementsMap.clear(); - instance = null; + return true; + } + + setDebugMode(enabled) { + this.debug = Boolean(enabled); + return this.debug; } initialize(options = {}) { - if (options.maxTooltips) { this.maxTooltips = options.maxTooltips; } - console.log('TooltipManager initialized'); + if (options.debug !== undefined) { + this.setDebugMode(options.debug); + } + + this.log('TooltipManager initialized'); return this; } } @@ -538,7 +560,7 @@ const TooltipManager = (function() { getInstance: function() { if (!instance) { - const manager = new TooltipManagerImpl(); + this.initialize(); } return instance; }, @@ -562,6 +584,11 @@ const TooltipManager = (function() { const manager = this.getInstance(); return manager.initializeTooltips(...args); }, + + setDebugMode: function(enabled) { + const manager = this.getInstance(); + return manager.setDebugMode(enabled); + }, dispose: function(...args) { const manager = this.getInstance(); @@ -570,19 +597,37 @@ const TooltipManager = (function() { }; })(); -window.TooltipManager = TooltipManager; +function installTooltipManager() { + const originalTooltipManager = window.TooltipManager; -document.addEventListener('DOMContentLoaded', function() { - if (!window.tooltipManagerInitialized) { - TooltipManager.initialize(); - TooltipManager.initializeTooltips(); - window.tooltipManagerInitialized = true; + window.TooltipManager = TooltipManager; + + window.TooltipManager.initialize({ + maxTooltips: 200, + debug: false + }); + + document.addEventListener('DOMContentLoaded', function() { + if (!window.tooltipManagerInitialized) { + window.TooltipManager.initializeTooltips(); + window.tooltipManagerInitialized = true; + } + }); + + return originalTooltipManager; +} + +if (typeof document !== 'undefined') { + if (document.readyState === 'complete' || document.readyState === 'interactive') { + installTooltipManager(); + } else { + document.addEventListener('DOMContentLoaded', installTooltipManager); } -}); +} if (typeof module !== 'undefined' && module.exports) { module.exports = TooltipManager; } -//console.log('TooltipManager initialized with methods:', Object.keys(TooltipManager)); +window.TooltipManager = TooltipManager; console.log('TooltipManager initialized'); diff --git a/basicswap/static/js/offers.js b/basicswap/static/js/offers.js index 0993312..8cf7f71 100644 --- a/basicswap/static/js/offers.js +++ b/basicswap/static/js/offers.js @@ -2343,10 +2343,31 @@ function cleanup() { } } - if (window.TooltipManager) { - if (typeof window.TooltipManager.cleanup === 'function') { - window.TooltipManager.cleanup(); - } + const offersBody = document.getElementById('offers-body'); + if (offersBody) { + const existingRows = Array.from(offersBody.querySelectorAll('tr')); + existingRows.forEach(row => { + const tooltipTriggers = row.querySelectorAll('[data-tooltip-trigger-id]'); + tooltipTriggers.forEach(trigger => { + if (window.TooltipManager) { + window.TooltipManager.destroy(trigger); + } + }); + + if (window.CleanupManager) { + window.CleanupManager.removeListenersByElement(row); + } + + while (row.attributes && row.attributes.length > 0) { + row.removeAttribute(row.attributes[0].name); + } + + while (row.firstChild) { + row.removeChild(row.firstChild); + } + }); + + offersBody.innerHTML = ''; } const filterForm = document.getElementById('filterForm'); @@ -2358,21 +2379,21 @@ function cleanup() { }); } - const paginationButtons = document.querySelectorAll('#prevPage, #nextPage'); - paginationButtons.forEach(button => { - CleanupManager.removeListenersByElement(button); - }); - - document.querySelectorAll('th[data-sortable="true"]').forEach(header => { - CleanupManager.removeListenersByElement(header); - }); - - cleanupTable(); - jsonData = null; originalJsonData = null; latestPrices = null; + if (window.TooltipManager) { + window.TooltipManager.cleanup(); + } + + if (window.MemoryManager) { + if (window.MemoryManager.cleanupTooltips) { + window.MemoryManager.cleanupTooltips(true); + } + window.MemoryManager.forceCleanup(); + } + console.log('Offers.js cleanup completed'); } catch (error) { console.error('Error during cleanup:', error); diff --git a/basicswap/static/js/pricechart.js b/basicswap/static/js/pricechart.js index 3b2b15a..8b56ba4 100644 --- a/basicswap/static/js/pricechart.js +++ b/basicswap/static/js/pricechart.js @@ -120,18 +120,33 @@ const api = { fetchCoinGeckoDataXHR: async () => { try { const priceData = await window.PriceManager.getPrices(); - const transformedData = {}; + const btcPriceUSD = priceData.bitcoin?.usd || 0; + if (btcPriceUSD > 0) { + window.btcPriceUSD = btcPriceUSD; + } + window.config.coins.forEach(coin => { const symbol = coin.symbol.toLowerCase(); const coinData = priceData[symbol] || priceData[coin.name.toLowerCase()]; if (coinData && coinData.usd) { + let priceBtc; + if (symbol === 'btc') { + priceBtc = 1; + } else if (window.btcPriceUSD && window.btcPriceUSD > 0) { + priceBtc = coinData.usd / window.btcPriceUSD; + } else { + priceBtc = coinData.btc || 0; + } + transformedData[symbol] = { current_price: coinData.usd, - price_btc: coinData.btc || (priceData.bitcoin ? coinData.usd / priceData.bitcoin.usd : 0), - displayName: coin.displayName || coin.symbol + price_btc: priceBtc, + displayName: coin.displayName || coin.symbol, + total_volume: coinData.total_volume, + price_change_percentage_24h: coinData.price_change_percentage_24h }; } }); @@ -274,63 +289,72 @@ const rateLimiter = { const ui = { displayCoinData: (coin, data) => { - let priceUSD, priceBTC, priceChange1d, volume24h; - const updateUI = (isError = false) => { - const priceUsdElement = document.querySelector(`#${coin.toLowerCase()}-price-usd`); - const volumeDiv = document.querySelector(`#${coin.toLowerCase()}-volume-div`); - const volumeElement = document.querySelector(`#${coin.toLowerCase()}-volume-24h`); - const btcPriceDiv = document.querySelector(`#${coin.toLowerCase()}-btc-price-div`); - const priceBtcElement = document.querySelector(`#${coin.toLowerCase()}-price-btc`); - - if (priceUsdElement) { - priceUsdElement.textContent = isError ? 'N/A' : `$ ${ui.formatPrice(coin, priceUSD)}`; - } - - if (volumeDiv && volumeElement) { - if (isError || volume24h === null || volume24h === undefined) { - volumeElement.textContent = 'N/A'; - } else { - volumeElement.textContent = `${utils.formatNumber(volume24h, 0)} USD`; - } - volumeDiv.style.display = volumeToggle.isVisible ? 'flex' : 'none'; - } - - if (btcPriceDiv && priceBtcElement) { - if (coin === 'BTC') { - btcPriceDiv.style.display = 'none'; - } else { - priceBtcElement.textContent = isError ? 'N/A' : `${priceBTC.toFixed(8)}`; - btcPriceDiv.style.display = 'flex'; - } - } - - ui.updatePriceChangeContainer(coin, isError ? null : priceChange1d); - }; - - try { - if (data.error) { - throw new Error(data.error); - } - - if (!data || !data.current_price) { - throw new Error(`Invalid data structure for ${coin}`); - } - - priceUSD = data.current_price; - priceBTC = coin === 'BTC' ? 1 : data.price_btc || (data.current_price / app.btcPriceUSD); - priceChange1d = data.price_change_percentage_24h || 0; - volume24h = data.total_volume || 0; - - if (isNaN(priceUSD) || isNaN(priceBTC)) { - throw new Error(`Invalid numeric values in data for ${coin}`); - } - - updateUI(false); - } catch (error) { - logger.error(`Failed to display data for ${coin}:`, error.message); - updateUI(true); + let priceUSD, priceBTC, priceChange1d, volume24h; + const updateUI = (isError = false) => { + const priceUsdElement = document.querySelector(`#${coin.toLowerCase()}-price-usd`); + const volumeDiv = document.querySelector(`#${coin.toLowerCase()}-volume-div`); + const volumeElement = document.querySelector(`#${coin.toLowerCase()}-volume-24h`); + const btcPriceDiv = document.querySelector(`#${coin.toLowerCase()}-btc-price-div`); + const priceBtcElement = document.querySelector(`#${coin.toLowerCase()}-price-btc`); + if (priceUsdElement) { + priceUsdElement.textContent = isError ? 'N/A' : `$ ${ui.formatPrice(coin, priceUSD)}`; } - }, + if (volumeDiv && volumeElement) { + if (isError || volume24h === null || volume24h === undefined) { + volumeElement.textContent = 'N/A'; + } else { + volumeElement.textContent = `${utils.formatNumber(volume24h, 0)} USD`; + } + volumeDiv.style.display = volumeToggle.isVisible ? 'flex' : 'none'; + } + if (btcPriceDiv && priceBtcElement) { + if (coin === 'BTC') { + btcPriceDiv.style.display = 'none'; + } else { + priceBtcElement.textContent = isError ? 'N/A' : `${priceBTC.toFixed(8)}`; + btcPriceDiv.style.display = 'flex'; + } + } + ui.updatePriceChangeContainer(coin, isError ? null : priceChange1d); + }; + try { + if (data.error) { + throw new Error(data.error); + } + if (!data || !data.current_price) { + throw new Error(`Invalid data structure for ${coin}`); + } + priceUSD = data.current_price; + + if (coin === 'BTC') { + priceBTC = 1; + } else { + + if (data.price_btc !== undefined && data.price_btc !== null) { + priceBTC = data.price_btc; + } + else if (window.btcPriceUSD && window.btcPriceUSD > 0) { + priceBTC = priceUSD / window.btcPriceUSD; + } + else if (app && app.btcPriceUSD && app.btcPriceUSD > 0) { + priceBTC = priceUSD / app.btcPriceUSD; + } + else { + priceBTC = 0; + } + } + + priceChange1d = data.price_change_percentage_24h || 0; + volume24h = data.total_volume || 0; + if (isNaN(priceUSD) || isNaN(priceBTC)) { + throw new Error(`Invalid numeric values in data for ${coin}`); + } + updateUI(false); + } catch (error) { + logger.error(`Failed to display data for ${coin}:`, error.message); + updateUI(true); + } +}, showLoader: () => { const loader = document.getElementById('loader'); @@ -1746,7 +1770,14 @@ document.addEventListener('DOMContentLoaded', () => { app.init(); if (window.MemoryManager) { + if (typeof MemoryManager.enableAutoCleanup === 'function') { MemoryManager.enableAutoCleanup(); + } else { + MemoryManager.initialize({ + autoCleanup: true, + debug: false + }); + } } CleanupManager.setInterval(() => { From 868b2475c130446498019c0c22b5c6ed27da0aed Mon Sep 17 00:00:00 2001 From: gerlofvanek Date: Thu, 8 May 2025 21:01:02 +0200 Subject: [PATCH 2/5] Fix: Better memory/tooltip/clean-up managers, various fixes. --- basicswap/static/js/bids_sentreceived.js | 10 - .../static/js/modules/cleanup-manager.js | 262 +++- basicswap/static/js/modules/memory-manager.js | 1053 ++++++++--------- basicswap/static/js/modules/price-manager.js | 4 +- .../static/js/modules/tooltips-manager.js | 732 ++++++++---- basicswap/static/js/offers.js | 52 +- basicswap/static/js/pricechart.js | 9 +- 7 files changed, 1266 insertions(+), 856 deletions(-) diff --git a/basicswap/static/js/bids_sentreceived.js b/basicswap/static/js/bids_sentreceived.js index decbc6f..52b91e5 100644 --- a/basicswap/static/js/bids_sentreceived.js +++ b/basicswap/static/js/bids_sentreceived.js @@ -311,7 +311,6 @@ CleanupManager.addListener(document, 'visibilitychange', () => { window.TooltipManager.cleanup(); } - // Run memory optimization if (window.MemoryManager) { MemoryManager.forceCleanup(); } @@ -1883,7 +1882,6 @@ function setupMemoryMonitoring() { }, { once: true }); } -// Init function initialize() { const filterElements = { stateSelect: document.getElementById('state'), @@ -1901,8 +1899,6 @@ function initialize() { if (filterElements.coinFrom) filterElements.coinFrom.value = 'any'; if (filterElements.coinTo) filterElements.coinTo.value = 'any'; - setupMemoryMonitoring(); - setTimeout(() => { WebSocketManager.initialize(); setupEventListeners(); @@ -1921,12 +1917,6 @@ function initialize() { updateBidsTable(); }, 100); - setInterval(() => { - if ((state.data.sent.length + state.data.received.length) > 1000) { - optimizeMemoryUsage(); - } - }, 5 * 60 * 1000); // Check every 5 minutes - window.cleanupBidsTable = cleanup; } diff --git a/basicswap/static/js/modules/cleanup-manager.js b/basicswap/static/js/modules/cleanup-manager.js index d179ee9..7fcb32a 100644 --- a/basicswap/static/js/modules/cleanup-manager.js +++ b/basicswap/static/js/modules/cleanup-manager.js @@ -1,12 +1,12 @@ const CleanupManager = (function() { - const state = { eventListeners: [], timeouts: [], intervals: [], animationFrames: [], resources: new Map(), - debug: false + debug: false, + memoryOptimizationInterval: null }; function log(message, ...args) { @@ -232,6 +232,229 @@ const CleanupManager = (function() { }; }, + setupMemoryOptimization: function(options = {}) { + const memoryCheckInterval = options.interval || 2 * 60 * 1000; // Default: 2 minutes + const maxCacheSize = options.maxCacheSize || 100; + const maxDataSize = options.maxDataSize || 1000; + + if (state.memoryOptimizationInterval) { + this.clearInterval(state.memoryOptimizationInterval); + } + + this.addListener(document, 'visibilitychange', () => { + if (document.hidden) { + log('Tab hidden - running memory optimization'); + this.optimizeMemory({ + maxCacheSize: maxCacheSize, + maxDataSize: maxDataSize + }); + } else if (window.TooltipManager) { + window.TooltipManager.cleanup(); + } + }); + + state.memoryOptimizationInterval = this.setInterval(() => { + if (document.hidden) { + log('Periodic memory optimization'); + this.optimizeMemory({ + maxCacheSize: maxCacheSize, + maxDataSize: maxDataSize + }); + } + }, memoryCheckInterval); + + log('Memory optimization setup complete'); + return state.memoryOptimizationInterval; + }, + + optimizeMemory: function(options = {}) { + log('Running memory optimization'); + + if (window.TooltipManager && typeof window.TooltipManager.cleanup === 'function') { + window.TooltipManager.cleanup(); + } + + if (window.IdentityManager && typeof window.IdentityManager.limitCacheSize === 'function') { + window.IdentityManager.limitCacheSize(options.maxCacheSize || 100); + } + + this.cleanupOrphanedResources(); + + if (window.gc) { + try { + window.gc(); + log('Forced garbage collection'); + } catch (e) { + } + } + + document.dispatchEvent(new CustomEvent('memoryOptimized', { + detail: { + timestamp: Date.now(), + maxDataSize: options.maxDataSize || 1000 + } + })); + + log('Memory optimization complete'); + }, + + cleanupOrphanedResources: function() { + let removedListeners = 0; + const validListeners = []; + + for (let i = 0; i < state.eventListeners.length; i++) { + const listener = state.eventListeners[i]; + if (!listener.element) { + removedListeners++; + continue; + } + + try { + + const isDetached = !(listener.element instanceof Node) || + !document.body.contains(listener.element) || + (listener.element.classList && listener.element.classList.contains('hidden')) || + (listener.element.style && listener.element.style.display === 'none'); + + if (isDetached) { + try { + if (listener.element instanceof Node) { + listener.element.removeEventListener(listener.type, listener.handler, listener.options); + } + removedListeners++; + } catch (e) { + + } + } else { + validListeners.push(listener); + } + } catch (e) { + + log(`Error checking listener (removing): ${e.message}`); + removedListeners++; + } + } + + if (removedListeners > 0) { + state.eventListeners = validListeners; + log(`Removed ${removedListeners} event listeners for detached/hidden elements`); + } + + let removedResources = 0; + const resourcesForRemoval = []; + + state.resources.forEach((info, id) => { + const resource = info.resource; + + try { + + if (resource instanceof Element && !document.body.contains(resource)) { + resourcesForRemoval.push(id); + } + + if (resource && resource.element) { + + if (resource.element instanceof Node && !document.body.contains(resource.element)) { + resourcesForRemoval.push(id); + } + } + } catch (e) { + log(`Error checking resource ${id}: ${e.message}`); + } + }); + + resourcesForRemoval.forEach(id => { + this.unregisterResource(id); + removedResources++; + }); + + if (removedResources > 0) { + log(`Removed ${removedResources} orphaned resources`); + } + + if (window.TooltipManager) { + if (typeof window.TooltipManager.cleanupOrphanedTooltips === 'function') { + try { + window.TooltipManager.cleanupOrphanedTooltips(); + } catch (e) { + + if (typeof window.TooltipManager.cleanup === 'function') { + try { + window.TooltipManager.cleanup(); + } catch (err) { + log(`Error cleaning up tooltips: ${err.message}`); + } + } + } + } else if (typeof window.TooltipManager.cleanup === 'function') { + try { + window.TooltipManager.cleanup(); + } catch (e) { + log(`Error cleaning up tooltips: ${e.message}`); + } + } + } + + try { + this.cleanupTooltipDOM(); + } catch (e) { + log(`Error in cleanupTooltipDOM: ${e.message}`); + } + }, + + cleanupTooltipDOM: function() { + let removedElements = 0; + + try { + + const tooltipSelectors = [ + '[role="tooltip"]', + '[id^="tooltip-"]', + '.tippy-box', + '[data-tippy-root]' + ]; + + tooltipSelectors.forEach(selector => { + try { + const elements = document.querySelectorAll(selector); + + elements.forEach(element => { + try { + + if (!(element instanceof Element)) return; + + const isDetached = !element.parentElement || + !document.body.contains(element.parentElement) || + element.classList.contains('hidden') || + element.style.display === 'none' || + element.style.visibility === 'hidden'; + + if (isDetached) { + try { + element.remove(); + removedElements++; + } catch (e) { + + } + } + } catch (err) { + + } + }); + } catch (err) { + + log(`Error querying for ${selector}: ${err.message}`); + } + }); + } catch (e) { + log(`Error in tooltip DOM cleanup: ${e.message}`); + } + + if (removedElements > 0) { + log(`Removed ${removedElements} detached tooltip elements`); + } + }, + setDebugMode: function(enabled) { state.debug = Boolean(enabled); log(`Debug mode ${state.debug ? 'enabled' : 'disabled'}`); @@ -247,6 +470,17 @@ const CleanupManager = (function() { if (options.debug !== undefined) { this.setDebugMode(options.debug); } + + if (typeof window !== 'undefined' && !options.noAutoCleanup) { + this.addListener(window, 'beforeunload', () => { + this.clearAll(); + }); + } + + if (typeof window !== 'undefined' && !options.noMemoryOptimization) { + this.setupMemoryOptimization(options.memoryOptions || {}); + } + log('CleanupManager initialized'); return this; } @@ -255,16 +489,20 @@ const CleanupManager = (function() { return publicAPI; })(); +if (typeof module !== 'undefined' && module.exports) { + module.exports = CleanupManager; +} -window.CleanupManager = CleanupManager; +if (typeof window !== 'undefined') { + window.CleanupManager = CleanupManager; +} - -document.addEventListener('DOMContentLoaded', function() { - if (!window.cleanupManagerInitialized) { - CleanupManager.initialize(); - window.cleanupManagerInitialized = true; +if (typeof window !== 'undefined' && typeof document !== 'undefined') { + if (document.readyState === 'complete' || document.readyState === 'interactive') { + CleanupManager.initialize({ debug: false }); + } else { + document.addEventListener('DOMContentLoaded', () => { + CleanupManager.initialize({ debug: false }); + }, { once: true }); } -}); - -//console.log('CleanupManager initialized with methods:', Object.keys(CleanupManager)); -console.log('CleanupManager initialized'); +} diff --git a/basicswap/static/js/modules/memory-manager.js b/basicswap/static/js/modules/memory-manager.js index 8a96323..4c7687f 100644 --- a/basicswap/static/js/modules/memory-manager.js +++ b/basicswap/static/js/modules/memory-manager.js @@ -1,132 +1,169 @@ const MemoryManager = (function() { const config = { - tooltipCleanupInterval: 60000, + tooltipCleanupInterval: 300000, + diagnosticsInterval: 600000, + elementVerificationInterval: 300000, maxTooltipsThreshold: 100, - diagnosticsInterval: 300000, - tooltipLifespan: 240000, + maxTooltips: 300, + cleanupThreshold: 1.5, + minTimeBetweenCleanups: 180000, + memoryGrowthThresholdMB: 100, debug: false, - autoCleanup: true, - elementVerificationInterval: 50000, - tooltipSelectors: [ - '[data-tippy-root]', - '[data-tooltip-trigger-id]', - '.tooltip', - '.tippy-box', - '.tippy-content' + protectedWebSockets: ['wsPort', 'ws_port'], + interactiveSelectors: [ + 'tr:hover', + '[data-tippy-root]:hover', + '.tooltip:hover', + '[data-tooltip-trigger-id]:hover', + '[data-tooltip-target]:hover' + ], + protectedContainers: [ + '#sent-tbody', + '#received-tbody', + '#offers-body' ] }; - let mutationObserver = null; - - const safeGet = (obj, path, defaultValue = null) => { - if (!obj) return defaultValue; - const pathParts = path.split('.'); - let result = obj; - for (const part of pathParts) { - if (result === null || result === undefined) return defaultValue; - result = result[part]; - } - return result !== undefined ? result : defaultValue; - }; - const state = { - intervals: new Map(), - trackedTooltips: new Map(), - trackedElements: new WeakMap(), - startTime: Date.now(), + pendingAnimationFrames: new Set(), + pendingTimeouts: new Set(), + cleanupInterval: null, + diagnosticsInterval: null, + elementVerificationInterval: null, + mutationObserver: null, lastCleanupTime: Date.now(), + startTime: Date.now(), + isCleanupRunning: false, metrics: { - tooltipsCreated: 0, - tooltipsDestroyed: 0, - orphanedTooltipsRemoved: 0, - elementsProcessed: 0, + tooltipsRemoved: 0, cleanupRuns: 0, - manualCleanupRuns: 0, - lastMemoryUsage: null + lastMemoryUsage: null, + lastCleanupDetails: {}, + history: [] + }, + originalTooltipFunctions: {} + }; + + function log(message, ...args) { + if (config.debug) { + console.log(`[MemoryManager] ${message}`, ...args); } - }; + } - const log = (message, ...args) => { - if (!config.debug) return; - const now = new Date().toISOString(); - console.log(`[MemoryManager ${now}]`, message, ...args); - }; + function preserveTooltipFunctions() { + if (window.TooltipManager && !state.originalTooltipFunctions.destroy) { + state.originalTooltipFunctions = { + destroy: window.TooltipManager.destroy, + cleanup: window.TooltipManager.cleanup, + create: window.TooltipManager.create + }; + } + } - const logError = (message, error) => { - console.error(`[MemoryManager] ${message}`, error); - }; + function isInProtectedContainer(element) { + if (!element) return false; + + for (const selector of config.protectedContainers) { + if (element.closest && element.closest(selector)) { + return true; + } + } + + return false; + } + + function shouldSkipCleanup() { + if (state.isCleanupRunning) return true; + + const selector = config.interactiveSelectors.join(', '); + const hoveredElements = document.querySelectorAll(selector); + + return hoveredElements.length > 0; + } + + function performCleanup(force = false) { + if (shouldSkipCleanup() && !force) { + return false; + } + + if (state.isCleanupRunning) { + return false; + } + + const now = Date.now(); + if (!force && now - state.lastCleanupTime < config.minTimeBetweenCleanups) { + return false; + } - const trackTooltip = (element, tooltipInstance) => { try { - if (!element || !tooltipInstance) return; + state.isCleanupRunning = true; + state.lastCleanupTime = now; + state.metrics.cleanupRuns++; - const timestamp = Date.now(); - const tooltipId = element.getAttribute('data-tooltip-trigger-id') || `tooltip_${timestamp}_${Math.random().toString(36).substring(2, 9)}`; + const startTime = performance.now(); + const startMemory = checkMemoryUsage(); - state.trackedTooltips.set(tooltipId, { - timestamp, - element, - instance: tooltipInstance, - processed: false + state.pendingAnimationFrames.forEach(id => { + cancelAnimationFrame(id); }); + state.pendingAnimationFrames.clear(); - state.metrics.tooltipsCreated++; + state.pendingTimeouts.forEach(id => { + clearTimeout(id); + }); + state.pendingTimeouts.clear(); - setTimeout(() => { - if (state.trackedTooltips.has(tooltipId)) { - destroyTooltip(tooltipId); - } - }, config.tooltipLifespan); + const tooltipsResult = removeOrphanedTooltips(); + state.metrics.tooltipsRemoved += tooltipsResult; - return tooltipId; - } catch (error) { - logError('Error tracking tooltip:', error); - return null; - } - }; + const disconnectedResult = checkForDisconnectedElements(); - const destroyTooltip = (tooltipId) => { - try { - const tooltipInfo = state.trackedTooltips.get(tooltipId); - if (!tooltipInfo) return false; + tryRunGarbageCollection(false); - const { element, instance } = tooltipInfo; + const endTime = performance.now(); + const endMemory = checkMemoryUsage(); - if (instance && typeof instance.destroy === 'function') { - instance.destroy(); + const runStats = { + timestamp: new Date().toISOString(), + duration: endTime - startTime, + tooltipsRemoved: tooltipsResult, + disconnectedRemoved: disconnectedResult, + memoryBefore: startMemory ? startMemory.usedMB : null, + memoryAfter: endMemory ? endMemory.usedMB : null, + memorySaved: startMemory && endMemory ? + (startMemory.usedMB - endMemory.usedMB).toFixed(2) : null + }; + + state.metrics.history.unshift(runStats); + if (state.metrics.history.length > 10) { + state.metrics.history.pop(); } - if (element && element.removeAttribute) { - element.removeAttribute('data-tooltip-trigger-id'); - element.removeAttribute('aria-describedby'); - } + state.metrics.lastCleanupDetails = runStats; - const tippyRoot = document.querySelector(`[data-for-tooltip-id="${tooltipId}"]`); - if (tippyRoot && tippyRoot.parentNode) { - tippyRoot.parentNode.removeChild(tippyRoot); + if (config.debug) { + log(`Cleanup completed in ${runStats.duration.toFixed(2)}ms, removed ${tooltipsResult} tooltips`); } - state.trackedTooltips.delete(tooltipId); - state.metrics.tooltipsDestroyed++; - return true; } catch (error) { - logError(`Error destroying tooltip ${tooltipId}:`, error); + console.error("Error during cleanup:", error); return false; + } finally { + state.isCleanupRunning = false; } - }; + } - const removeOrphanedTooltips = () => { + function removeOrphanedTooltips() { try { - const tippyRoots = document.querySelectorAll('[data-tippy-root]'); + + const tippyRoots = document.querySelectorAll('[data-tippy-root]:not(:hover)'); let removed = 0; tippyRoots.forEach(root => { const tooltipId = root.getAttribute('data-for-tooltip-id'); - const trigger = tooltipId ? - document.querySelector(`[data-tooltip-trigger-id="${tooltipId}"]`) : - null; + document.querySelector(`[data-tooltip-trigger-id="${tooltipId}"]`) : null; if (!trigger || !document.body.contains(trigger)) { if (root.parentNode) { @@ -136,528 +173,410 @@ const MemoryManager = (function() { } }); - document.querySelectorAll('[data-tooltip-trigger-id]').forEach(trigger => { - const tooltipId = trigger.getAttribute('data-tooltip-trigger-id'); - const root = document.querySelector(`[data-for-tooltip-id="${tooltipId}"]`); - - if (!root) { - trigger.removeAttribute('data-tooltip-trigger-id'); - trigger.removeAttribute('aria-describedby'); - removed++; - } - }); - - state.metrics.orphanedTooltipsRemoved += removed; return removed; } catch (error) { - logError('Error removing orphaned tooltips:', error); + console.error("Error removing orphaned tooltips:", error); return 0; } - }; + } - const checkMemoryUsage = () => { - if (window.performance && window.performance.memory) { - const memoryUsage = { - usedJSHeapSize: window.performance.memory.usedJSHeapSize, - totalJSHeapSize: window.performance.memory.totalJSHeapSize, - jsHeapSizeLimit: window.performance.memory.jsHeapSizeLimit, - percentUsed: (window.performance.memory.usedJSHeapSize / window.performance.memory.jsHeapSizeLimit * 100).toFixed(2) - }; - - state.metrics.lastMemoryUsage = memoryUsage; - return memoryUsage; - } - - return null; - }; - - const checkForDisconnectedElements = () => { + function checkForDisconnectedElements() { try { + + const tooltipTriggers = document.querySelectorAll('[data-tooltip-trigger-id]:not(:hover)'); const disconnectedElements = new Set(); - state.trackedTooltips.forEach((info, id) => { - const { element } = info; - if (element && !document.body.contains(element)) { - disconnectedElements.add(id); + tooltipTriggers.forEach(el => { + if (!document.body.contains(el)) { + const tooltipId = el.getAttribute('data-tooltip-trigger-id'); + disconnectedElements.add(tooltipId); } }); + const tooltipRoots = document.querySelectorAll('[data-for-tooltip-id]'); + let removed = 0; + disconnectedElements.forEach(id => { - destroyTooltip(id); + for (const root of tooltipRoots) { + if (root.getAttribute('data-for-tooltip-id') === id && root.parentNode) { + root.parentNode.removeChild(root); + removed++; + break; + } + } }); return disconnectedElements.size; } catch (error) { - logError('Error checking for disconnected elements:', error); + console.error("Error checking for disconnected elements:", error); return 0; } - }; + } - const setupMutationObserver = () => { - if (mutationObserver) { - mutationObserver.disconnect(); + function tryRunGarbageCollection(aggressive = false) { + setTimeout(() => { + + const cache = {}; + for (let i = 0; i < 100; i++) { + cache[`key${i}`] = {}; + } + + for (const key in cache) { + delete cache[key]; + } + }, 100); + + return true; + } + + function checkMemoryUsage() { + const result = { + usedJSHeapSize: 0, + totalJSHeapSize: 0, + jsHeapSizeLimit: 0, + percentUsed: "0", + usedMB: "0", + totalMB: "0", + limitMB: "0" + }; + + if (window.performance && window.performance.memory) { + result.usedJSHeapSize = window.performance.memory.usedJSHeapSize; + result.totalJSHeapSize = window.performance.memory.totalJSHeapSize; + result.jsHeapSizeLimit = window.performance.memory.jsHeapSizeLimit; + result.percentUsed = (result.usedJSHeapSize / result.jsHeapSizeLimit * 100).toFixed(2); + result.usedMB = (result.usedJSHeapSize / (1024 * 1024)).toFixed(2); + result.totalMB = (result.totalJSHeapSize / (1024 * 1024)).toFixed(2); + result.limitMB = (result.jsHeapSizeLimit / (1024 * 1024)).toFixed(2); + } else { + result.usedMB = "Unknown"; + result.totalMB = "Unknown"; + result.limitMB = "Unknown"; + result.percentUsed = "Unknown"; } - mutationObserver = new MutationObserver(mutations => { - let needsCleanup = false; + state.metrics.lastMemoryUsage = result; + return result; + } - 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')) { - const tooltipId = node.getAttribute('data-tooltip-trigger-id'); - destroyTooltip(tooltipId); - needsCleanup = true; - } - - if (node.querySelectorAll) { - const tooltipTriggers = node.querySelectorAll('[data-tooltip-trigger-id]'); - if (tooltipTriggers.length > 0) { - tooltipTriggers.forEach(el => { - const tooltipId = el.getAttribute('data-tooltip-trigger-id'); - destroyTooltip(tooltipId); - }); - needsCleanup = true; - } - } - } - }); - } - }); - - if (needsCleanup) { - removeOrphanedTooltips(); - } - }); - - mutationObserver.observe(document.body, { - childList: true, - subtree: true - }); - - return mutationObserver; - }; - - const performCleanup = (force = false) => { - try { - log('Starting tooltip cleanup' + (force ? ' (forced)' : '')); - - state.lastCleanupTime = Date.now(); - state.metrics.cleanupRuns++; - - if (force) { - state.metrics.manualCleanupRuns++; - } - - document.querySelectorAll('[data-tippy-root]').forEach(root => { - const instance = safeGet(root, '_tippy'); - if (instance && instance._animationFrame) { - cancelAnimationFrame(instance._animationFrame); - instance._animationFrame = null; - } - }); - - const orphanedRemoved = removeOrphanedTooltips(); - - const disconnectedRemoved = checkForDisconnectedElements(); - - const tooltipCount = document.querySelectorAll('[data-tippy-root]').length; - const triggerCount = document.querySelectorAll('[data-tooltip-trigger-id]').length; - - if (force || tooltipCount > config.maxTooltipsThreshold) { - if (tooltipCount > config.maxTooltipsThreshold) { - document.querySelectorAll('[data-tooltip-trigger-id]').forEach(trigger => { - const tooltipId = trigger.getAttribute('data-tooltip-trigger-id'); - destroyTooltip(tooltipId); - }); - - document.querySelectorAll('[data-tippy-root]').forEach(root => { - if (root.parentNode) { - root.parentNode.removeChild(root); - } - }); - } - - document.querySelectorAll('[data-tooltip-trigger-id], [aria-describedby]').forEach(el => { - if (window.CleanupManager && window.CleanupManager.removeListenersByElement) { - window.CleanupManager.removeListenersByElement(el); - } else { - if (el.parentNode) { - const clone = el.cloneNode(true); - el.parentNode.replaceChild(clone, el); - } - } - }); - } - - if (window.gc) { - window.gc(); - } else if (force) { - const arr = new Array(1000); - for (let i = 0; i < 1000; i++) { - arr[i] = new Array(10000).join('x'); - } - } - - checkMemoryUsage(); - - const result = { - orphanedRemoved, - disconnectedRemoved, - tooltipCount: document.querySelectorAll('[data-tippy-root]').length, - triggerCount: document.querySelectorAll('[data-tooltip-trigger-id]').length, - memoryUsage: state.metrics.lastMemoryUsage - }; - - log('Cleanup completed', result); - return result; - } catch (error) { - logError('Error during cleanup:', error); - return { error: error.message }; + function handleVisibilityChange() { + if (document.hidden) { + removeOrphanedTooltips(); + checkForDisconnectedElements(); } - }; + } - const runDiagnostics = () => { - try { - log('Running memory diagnostics'); - - const memoryUsage = checkMemoryUsage(); - const tooltipCount = document.querySelectorAll('[data-tippy-root]').length; - const triggerCount = document.querySelectorAll('[data-tooltip-trigger-id]').length; - - const diagnostics = { - time: new Date().toISOString(), - uptime: Date.now() - state.startTime, - memoryUsage, - elementsCount: { - tippyRoots: tooltipCount, - tooltipTriggers: triggerCount, - orphanedTriggers: triggerCount - tooltipCount > 0 ? triggerCount - tooltipCount : 0, - orphanedTooltips: tooltipCount - triggerCount > 0 ? tooltipCount - triggerCount : 0 - }, - metrics: { ...state.metrics }, - issues: [] - }; - - if (tooltipCount > config.maxTooltipsThreshold) { - diagnostics.issues.push({ - severity: 'high', - message: `Excessive tooltip count: ${tooltipCount} (threshold: ${config.maxTooltipsThreshold})`, - recommendation: 'Run cleanup and check for tooltip creation loops' - }); - } - - if (Math.abs(tooltipCount - triggerCount) > 10) { - diagnostics.issues.push({ - severity: 'medium', - message: `Mismatch between tooltips (${tooltipCount}) and triggers (${triggerCount})`, - recommendation: 'Remove orphaned tooltips and tooltip triggers' - }); - } - - if (memoryUsage && memoryUsage.percentUsed > 80) { - diagnostics.issues.push({ - severity: 'high', - message: `High memory usage: ${memoryUsage.percentUsed}%`, - recommendation: 'Force garbage collection and check for memory leaks' - }); - } - - if (config.autoCleanup && diagnostics.issues.some(issue => issue.severity === 'high')) { - log('Critical issues detected, triggering automatic cleanup'); - performCleanup(true); - } - - return diagnostics; - } catch (error) { - logError('Error running diagnostics:', error); - return { error: error.message }; + function setupMutationObserver() { + if (state.mutationObserver) { + state.mutationObserver.disconnect(); + state.mutationObserver = null; } - }; - const patchTooltipManager = () => { - try { - if (!window.TooltipManager) { - log('TooltipManager not found'); - return false; + let processingScheduled = false; + let lastProcessTime = 0; + const MIN_PROCESS_INTERVAL = 10000; + + const processMutations = (mutations) => { + const now = Date.now(); + + if (now - lastProcessTime < MIN_PROCESS_INTERVAL || processingScheduled) { + return; } - log('Patching TooltipManager'); - - const originalCreate = window.TooltipManager.create; - const originalDestroy = window.TooltipManager.destroy; - const originalCleanup = window.TooltipManager.cleanup; - - window.TooltipManager.create = function(element, content, options = {}) { - if (!element) return null; - - try { - const result = originalCreate.call(this, element, content, options); - const tooltipId = element.getAttribute('data-tooltip-trigger-id'); - - if (tooltipId) { - const tippyInstance = safeGet(element, '_tippy') || null; - trackTooltip(element, tippyInstance); - } - - return result; - } catch (error) { - logError('Error in patched create:', error); - return originalCreate.call(this, element, content, options); - } - }; - - window.TooltipManager.destroy = function(element) { - if (!element) return; - - try { - const tooltipId = element.getAttribute('data-tooltip-trigger-id'); - - originalDestroy.call(this, element); - - if (tooltipId) { - state.trackedTooltips.delete(tooltipId); - state.metrics.tooltipsDestroyed++; - } - } catch (error) { - logError('Error in patched destroy:', error); - originalDestroy.call(this, element); - } - }; - - window.TooltipManager.cleanup = function() { - try { - originalCleanup.call(this); - removeOrphanedTooltips(); - } catch (error) { - logError('Error in patched cleanup:', error); - originalCleanup.call(this); - } - }; - - return true; - } catch (error) { - logError('Error patching TooltipManager:', error); - return false; - } - }; - - const patchTippy = () => { - try { - if (typeof tippy !== 'function') { - log('tippy.js not found globally'); - return false; - } - - log('Patching global tippy'); - - const originalTippy = window.tippy; - - window.tippy = function(...args) { - const result = originalTippy.apply(this, args); - - if (Array.isArray(result)) { - result.forEach(instance => { - const reference = instance.reference; - - if (reference) { - const originalShow = instance.show; - const originalHide = instance.hide; - const originalDestroy = instance.destroy; - - instance.show = function(...showArgs) { - return originalShow.apply(this, showArgs); - }; - - instance.hide = function(...hideArgs) { - return originalHide.apply(this, hideArgs); - }; - - instance.destroy = function(...destroyArgs) { - return originalDestroy.apply(this, destroyArgs); - }; - } - }); - } - - return result; - }; - - Object.assign(window.tippy, originalTippy); - - return true; - } catch (error) { - logError('Error patching tippy:', error); - return false; - } - }; - - const startMonitoring = () => { - try { - stopMonitoring(); - - state.intervals.set('cleanup', setInterval(() => { - performCleanup(); - }, config.tooltipCleanupInterval)); - - state.intervals.set('diagnostics', setInterval(() => { - runDiagnostics(); - }, config.diagnosticsInterval)); - - state.intervals.set('elementVerification', setInterval(() => { - checkForDisconnectedElements(); - }, config.elementVerificationInterval)); - - setupMutationObserver(); - - log('Monitoring started'); - return true; - } catch (error) { - logError('Error starting monitoring:', error); - return false; - } - }; - - const stopMonitoring = () => { - try { - state.intervals.forEach((interval, key) => { - clearInterval(interval); - }); - - state.intervals.clear(); - - if (mutationObserver) { - mutationObserver.disconnect(); - mutationObserver = null; - } - - log('Monitoring stopped'); - return true; - } catch (error) { - logError('Error stopping monitoring:', error); - return false; - } - }; - - const autoFix = () => { - try { - log('Running auto-fix'); - - performCleanup(true); - - document.querySelectorAll('[data-tooltip-trigger-id]').forEach(element => { - const tooltipId = element.getAttribute('data-tooltip-trigger-id'); - const duplicates = document.querySelectorAll(`[data-tooltip-trigger-id="${tooltipId}"]`); - - if (duplicates.length > 1) { - for (let i = 1; i < duplicates.length; i++) { - duplicates[i].removeAttribute('data-tooltip-trigger-id'); - duplicates[i].removeAttribute('aria-describedby'); - } - } - }); - - const tippyRoots = document.querySelectorAll('[data-tippy-root]'); - tippyRoots.forEach(root => { - if (!document.body.contains(root) && root.parentNode) { - root.parentNode.removeChild(root); - } - }); - - if (window.TooltipManager && window.TooltipManager.getInstance) { - const manager = window.TooltipManager.getInstance(); - if (manager && manager.chartRefs && manager.chartRefs.clear) { - manager.chartRefs.clear(); - } - - if (manager && manager.tooltipElementsMap && manager.tooltipElementsMap.clear) { - manager.tooltipElementsMap.clear(); - } - } - - patchTooltipManager(); - patchTippy(); - - return true; - } catch (error) { - logError('Error during auto-fix:', error); - return false; - } - }; - - const initialize = (options = {}) => { - try { - Object.assign(config, options); - - if (document.head) { - const metaCache = document.createElement('meta'); - metaCache.setAttribute('http-equiv', 'Cache-Control'); - metaCache.setAttribute('content', 'no-store, max-age=0'); - document.head.appendChild(metaCache); - } - - patchTooltipManager(); - patchTippy(); - - startMonitoring(); - - if (window.CleanupManager && window.CleanupManager.registerResource) { - window.CleanupManager.registerResource('memorymanager', MemoryManager, (optimizer) => { - optimizer.dispose(); - }); - } - - log('Memory Optimizer initialized', config); + processingScheduled = true; setTimeout(() => { - runDiagnostics(); + processingScheduled = false; + lastProcessTime = Date.now(); + + if (state.isCleanupRunning) { + return; + } + + const tooltipSelectors = ['[data-tippy-root]', '[data-tooltip-trigger-id]', '.tooltip']; + let tooltipCount = 0; + + tooltipCount = document.querySelectorAll(tooltipSelectors.join(', ')).length; + + if (tooltipCount > config.maxTooltipsThreshold && + (Date.now() - state.lastCleanupTime > config.minTimeBetweenCleanups)) { + + removeOrphanedTooltips(); + checkForDisconnectedElements(); + state.lastCleanupTime = Date.now(); + } }, 5000); + }; - return MemoryManager; - } catch (error) { - logError('Error initializing Memory Optimizer:', error); - return null; + state.mutationObserver = new MutationObserver(processMutations); + + state.mutationObserver.observe(document.body, { + childList: true, + subtree: true, + attributes: false, + characterData: false + }); + + return state.mutationObserver; + } + + function enhanceTooltipManager() { + if (!window.TooltipManager || window.TooltipManager._memoryManagerEnhanced) return false; + + preserveTooltipFunctions(); + + const originalDestroy = window.TooltipManager.destroy; + const originalCleanup = window.TooltipManager.cleanup; + + window.TooltipManager.destroy = function(element) { + if (!element) return; + + try { + const tooltipId = element.getAttribute('data-tooltip-trigger-id'); + + if (isInProtectedContainer(element)) { + if (originalDestroy) { + return originalDestroy.call(window.TooltipManager, element); + } + return; + } + + if (tooltipId) { + if (originalDestroy) { + originalDestroy.call(window.TooltipManager, element); + } + + const tooltipRoot = document.querySelector(`[data-for-tooltip-id="${tooltipId}"]`); + if (tooltipRoot && tooltipRoot.parentNode) { + tooltipRoot.parentNode.removeChild(tooltipRoot); + } + + element.removeAttribute('data-tooltip-trigger-id'); + element.removeAttribute('aria-describedby'); + + if (element._tippy) { + try { + element._tippy.destroy(); + element._tippy = null; + } catch (e) {} + } + } + } catch (error) { + console.error('Error in enhanced tooltip destroy:', error); + + if (originalDestroy) { + originalDestroy.call(window.TooltipManager, element); + } + } + }; + + window.TooltipManager.cleanup = function() { + try { + if (originalCleanup) { + originalCleanup.call(window.TooltipManager); + } + + removeOrphanedTooltips(); + } catch (error) { + console.error('Error in enhanced tooltip cleanup:', error); + + if (originalCleanup) { + originalCleanup.call(window.TooltipManager); + } + } + }; + + window.TooltipManager._memoryManagerEnhanced = true; + window.TooltipManager._originalDestroy = originalDestroy; + window.TooltipManager._originalCleanup = originalCleanup; + + return true; + } + + function initializeScheduledCleanups() { + if (state.cleanupInterval) { + clearInterval(state.cleanupInterval); + state.cleanupInterval = null; } - }; - const dispose = () => { - try { - log('Disposing Memory Optimizer'); - - performCleanup(true); - - stopMonitoring(); - - state.trackedTooltips.clear(); - - return true; - } catch (error) { - logError('Error disposing Memory Optimizer:', error); - return false; + if (state.diagnosticsInterval) { + clearInterval(state.diagnosticsInterval); + state.diagnosticsInterval = null; } - }; + + if (state.elementVerificationInterval) { + clearInterval(state.elementVerificationInterval); + state.elementVerificationInterval = null; + } + + state.cleanupInterval = setInterval(() => { + removeOrphanedTooltips(); + checkForDisconnectedElements(); + }, config.tooltipCleanupInterval); + + state.diagnosticsInterval = setInterval(() => { + checkMemoryUsage(); + }, config.diagnosticsInterval); + + state.elementVerificationInterval = setInterval(() => { + checkForDisconnectedElements(); + }, config.elementVerificationInterval); + + document.removeEventListener('visibilitychange', handleVisibilityChange); + document.addEventListener('visibilitychange', handleVisibilityChange); + + setupMutationObserver(); + + return true; + } + + function initialize(options = {}) { + preserveTooltipFunctions(); + + if (options) { + Object.assign(config, options); + } + + enhanceTooltipManager(); + + if (window.WebSocketManager && !window.WebSocketManager.cleanupOrphanedSockets) { + window.WebSocketManager.cleanupOrphanedSockets = function() { + return 0; + }; + } + + const manager = window.ApiManager || window.Api; + if (manager && !manager.abortPendingRequests) { + manager.abortPendingRequests = function() { + return 0; + }; + } + + initializeScheduledCleanups(); + + setTimeout(() => { + removeOrphanedTooltips(); + checkForDisconnectedElements(); + }, 5000); + + return this; + } + + function dispose() { + if (state.cleanupInterval) { + clearInterval(state.cleanupInterval); + state.cleanupInterval = null; + } + + if (state.diagnosticsInterval) { + clearInterval(state.diagnosticsInterval); + state.diagnosticsInterval = null; + } + + if (state.elementVerificationInterval) { + clearInterval(state.elementVerificationInterval); + state.elementVerificationInterval = null; + } + + if (state.mutationObserver) { + state.mutationObserver.disconnect(); + state.mutationObserver = null; + } + + document.removeEventListener('visibilitychange', handleVisibilityChange); + + return true; + } + + function displayStats() { + const stats = getDetailedStats(); + + console.group('Memory Manager Stats'); + console.log('Memory Usage:', stats.memory ? + `${stats.memory.usedMB}MB / ${stats.memory.limitMB}MB (${stats.memory.percentUsed}%)` : + 'Not available'); + console.log('Total Cleanups:', stats.metrics.cleanupRuns); + console.log('Total Tooltips Removed:', stats.metrics.tooltipsRemoved); + console.log('Current Tooltips:', stats.tooltips.total); + console.log('Last Cleanup:', stats.metrics.lastCleanupDetails); + console.log('Cleanup History:', stats.metrics.history); + console.groupEnd(); + + return stats; + } + + function getDetailedStats() { + + const allTooltipElements = document.querySelectorAll('[data-tippy-root], [data-tooltip-trigger-id], .tooltip'); + + const tooltips = { + roots: document.querySelectorAll('[data-tippy-root]').length, + triggers: document.querySelectorAll('[data-tooltip-trigger-id]').length, + tooltipElements: document.querySelectorAll('.tooltip').length, + total: allTooltipElements.length, + protectedContainers: {} + }; + + config.protectedContainers.forEach(selector => { + const container = document.querySelector(selector); + if (container) { + tooltips.protectedContainers[selector] = { + tooltips: container.querySelectorAll('.tooltip').length, + triggers: container.querySelectorAll('[data-tooltip-trigger-id]').length, + roots: document.querySelectorAll(`[data-tippy-root][data-for-tooltip-id]`).length + }; + } + }); + + return { + memory: checkMemoryUsage(), + metrics: { ...state.metrics }, + tooltips, + config: { ...config } + }; + } return { initialize, - dispose, - performCleanup, - runDiagnostics, - autoFix, - getConfig: () => ({ ...config }), - getMetrics: () => ({ ...state.metrics }), - setDebugMode: (enabled) => { + cleanup: performCleanup, + forceCleanup: function() { + return performCleanup(true); + }, + fullCleanup: function() { + return performCleanup(true); + }, + getStats: getDetailedStats, + displayStats, + setDebugMode: function(enabled) { config.debug = Boolean(enabled); return config.debug; - } + }, + addProtectedContainer: function(selector) { + if (!config.protectedContainers.includes(selector)) { + config.protectedContainers.push(selector); + } + return config.protectedContainers; + }, + removeProtectedContainer: function(selector) { + const index = config.protectedContainers.indexOf(selector); + if (index !== -1) { + config.protectedContainers.splice(index, 1); + } + return config.protectedContainers; + }, + dispose }; })(); -if (typeof document !== 'undefined') { - document.addEventListener('DOMContentLoaded', function() { - MemoryManager.initialize(); +document.addEventListener('DOMContentLoaded', function() { + const isDevMode = window.location.hostname === 'localhost' || + window.location.hostname === '127.0.0.1'; + + MemoryManager.initialize({ + debug: isDevMode }); -} + + console.log('Memory Manager initialized'); +}); window.MemoryManager = MemoryManager; -console.log('Memory Manager initialized'); diff --git a/basicswap/static/js/modules/price-manager.js b/basicswap/static/js/modules/price-manager.js index 17fe22a..b109bc0 100644 --- a/basicswap/static/js/modules/price-manager.js +++ b/basicswap/static/js/modules/price-manager.js @@ -59,7 +59,7 @@ const PriceManager = (function() { return fetchPromise; } - console.log('PriceManager: Fetching latest prices.'); + //console.log('PriceManager: Fetching latest prices.'); lastFetchTime = Date.now(); fetchPromise = this.fetchPrices() .then(prices => { @@ -89,7 +89,7 @@ const PriceManager = (function() { ? window.config.coins.map(c => c.symbol).filter(symbol => symbol && symbol.trim() !== '') : ['BTC', 'XMR', 'PART', 'BCH', 'PIVX', 'FIRO', 'DASH', 'LTC', 'DOGE', 'DCR', 'NMC', 'WOW']); - console.log('PriceManager: lookupFiatRates ' + coinSymbols.join(', ')); + //console.log('PriceManager: lookupFiatRates ' + coinSymbols.join(', ')); if (!coinSymbols.length) { throw new Error('No valid coins configured'); diff --git a/basicswap/static/js/modules/tooltips-manager.js b/basicswap/static/js/modules/tooltips-manager.js index 69219f0..59f70b2 100644 --- a/basicswap/static/js/modules/tooltips-manager.js +++ b/basicswap/static/js/modules/tooltips-manager.js @@ -1,6 +1,5 @@ const TooltipManager = (function() { let instance = null; - const tooltipInstanceMap = new WeakMap(); class TooltipManagerImpl { @@ -8,20 +7,22 @@ const TooltipManager = (function() { if (instance) { return instance; } - this.pendingAnimationFrames = new Set(); - this.pendingTimeouts = new Set(); + this.tooltipIdCounter = 0; this.maxTooltips = 200; this.cleanupThreshold = 1.2; - this.disconnectedCheckInterval = null; - this.cleanupInterval = null; - this.mutationObserver = null; this.debug = false; this.tooltipData = new WeakMap(); - this.setupStyles(); - this.setupMutationObserver(); - this.startPeriodicCleanup(); - this.startDisconnectedElementsCheck(); + this.resources = {}; + + if (window.CleanupManager) { + CleanupManager.registerResource( + 'tooltipManager', + this, + (manager) => manager.dispose() + ); + } + instance = this; } @@ -33,7 +34,7 @@ const TooltipManager = (function() { create(element, content, options = {}) { if (!element || !document.body.contains(element)) return null; - + if (!document.contains(element)) { this.log('Tried to create tooltip for detached element'); return null; @@ -47,9 +48,7 @@ const TooltipManager = (function() { this.performPeriodicCleanup(true); } - const rafId = requestAnimationFrame(() => { - this.pendingAnimationFrames.delete(rafId); - + const createTooltip = () => { if (!document.body.contains(element)) return; const rect = element.getBoundingClientRect(); @@ -58,7 +57,7 @@ const TooltipManager = (function() { } else { let retryCount = 0; const maxRetries = 3; - + const retryCreate = () => { const newRect = element.getBoundingClientRect(); if ((newRect.width > 0 && newRect.height > 0) || retryCount >= maxRetries) { @@ -67,30 +66,29 @@ const TooltipManager = (function() { } } else { retryCount++; - const timeoutId = setTimeout(() => { - this.pendingTimeouts.delete(timeoutId); - const newRafId = requestAnimationFrame(retryCreate); - this.pendingAnimationFrames.add(newRafId); + CleanupManager.setTimeout(() => { + CleanupManager.requestAnimationFrame(retryCreate); }, 100); - this.pendingTimeouts.add(timeoutId); } }; - const initialTimeoutId = setTimeout(() => { - this.pendingTimeouts.delete(initialTimeoutId); - const retryRafId = requestAnimationFrame(retryCreate); - this.pendingAnimationFrames.add(retryRafId); + CleanupManager.setTimeout(() => { + CleanupManager.requestAnimationFrame(retryCreate); }, 100); - this.pendingTimeouts.add(initialTimeoutId); } - }); + }; - this.pendingAnimationFrames.add(rafId); + CleanupManager.requestAnimationFrame(createTooltip); return null; } - + createTooltipInstance(element, content, options = {}) { - if (!element || !document.body.contains(element) || !window.tippy) { + if (!element || !document.body.contains(element)) { + return null; + } + + if (typeof window.tippy !== 'function') { + console.error('Tippy.js is not available.'); return null; } @@ -130,7 +128,7 @@ const TooltipManager = (function() { }, onHidden(instance) { if (!document.body.contains(element)) { - setTimeout(() => { + CleanupManager.setTimeout(() => { if (instance && instance.destroy) { instance.destroy(); } @@ -168,9 +166,26 @@ const TooltipManager = (function() { }); element.setAttribute('data-tooltip-trigger-id', tooltipId); - tooltipInstanceMap.set(element, tippyInstance[0]); + const resourceId = CleanupManager.registerResource( + 'tooltip', + { element, instance: tippyInstance[0] }, + (resource) => { + try { + if (resource.instance && resource.instance.destroy) { + resource.instance.destroy(); + } + if (resource.element) { + resource.element.removeAttribute('data-tooltip-trigger-id'); + resource.element.removeAttribute('aria-describedby'); + } + } catch (e) { + console.warn('Error destroying tooltip during cleanup:', e); + } + } + ); + return tippyInstance[0]; } @@ -214,181 +229,292 @@ const TooltipManager = (function() { } } + getActiveTooltipInstances() { + const result = []; + try { + document.querySelectorAll('[data-tooltip-trigger-id]').forEach(element => { + const instance = element._tippy ? [element._tippy] : null; + if (instance) { + result.push([element, instance]); + } + }); + } catch (error) { + console.error('Error getting active tooltip instances:', error); + } + return result; + } + cleanup() { this.log('Running tooltip cleanup'); - this.pendingAnimationFrames.forEach(id => { - cancelAnimationFrame(id); - }); - this.pendingAnimationFrames.clear(); - - this.pendingTimeouts.forEach(id => { - clearTimeout(id); - }); - this.pendingTimeouts.clear(); - - const elements = document.querySelectorAll('[data-tooltip-trigger-id]'); - const batchSize = 20; - - const processElementsBatch = (startIdx) => { - const endIdx = Math.min(startIdx + batchSize, elements.length); - - for (let i = startIdx; i < endIdx; i++) { - this.destroy(elements[i]); + try { + if ((window.location.pathname.includes('/offers') || window.location.pathname.includes('/bids')) && + (document.querySelector('[data-tippy-root]:hover') || document.querySelector('[data-tooltip-trigger-id]:hover'))) { + console.log('Skipping tooltip cleanup - tooltip is being hovered'); + return; } - if (endIdx < elements.length) { - const rafId = requestAnimationFrame(() => { - this.pendingAnimationFrames.delete(rafId); - processElementsBatch(endIdx); - }); - this.pendingAnimationFrames.add(rafId); + const elements = document.querySelectorAll('[data-tooltip-trigger-id]:not(:hover)'); + const batchSize = 20; + + const processElementsBatch = (startIdx) => { + const endIdx = Math.min(startIdx + batchSize, elements.length); + + for (let i = startIdx; i < endIdx; i++) { + this.destroy(elements[i]); + } + + if (endIdx < elements.length) { + CleanupManager.requestAnimationFrame(() => { + processElementsBatch(endIdx); + }); + } else { + this.cleanupOrphanedTooltips(); + } + }; + + if (elements.length > 0) { + processElementsBatch(0); } else { this.cleanupOrphanedTooltips(); } - }; + } catch (error) { + console.error('Error during cleanup:', error); + } + } - if (elements.length > 0) { - processElementsBatch(0); - } else { - this.cleanupOrphanedTooltips(); + thoroughCleanup() { + this.log('Running thorough tooltip cleanup'); + + try { + this.cleanup(); + this.cleanupAllTooltips(); + this.log('Thorough tooltip cleanup completed'); + } catch (error) { + console.error('Error in thorough tooltip cleanup:', error); + } + } + + cleanupAllTooltips() { + this.log('Cleaning up all tooltips'); + + try { + if ((window.location.pathname.includes('/offers') || window.location.pathname.includes('/bids')) && + document.querySelector('#offers-body tr:hover')) { + this.log('Skipping all tooltips cleanup on offers/bids page with row hover'); + return; + } + + const tooltipRoots = document.querySelectorAll('[data-tippy-root]'); + const tooltipTriggers = document.querySelectorAll('[data-tooltip-trigger-id]'); + const tooltipElements = document.querySelectorAll('.tooltip'); + + const isHovered = element => { + try { + return element.matches && element.matches(':hover'); + } catch (e) { + + return false; + } + }; + + tooltipRoots.forEach(root => { + if (!isHovered(root) && root.parentNode) { + root.parentNode.removeChild(root); + } + }); + + tooltipTriggers.forEach(trigger => { + if (!isHovered(trigger)) { + trigger.removeAttribute('data-tooltip-trigger-id'); + trigger.removeAttribute('aria-describedby'); + + if (trigger._tippy) { + try { + trigger._tippy.destroy(); + trigger._tippy = null; + } catch (e) {} + } + } + }); + + tooltipElements.forEach(tooltip => { + if (!isHovered(tooltip) && tooltip.parentNode) { + let closestHoveredRow = false; + + try { + if (tooltip.closest && tooltip.closest('tr') && isHovered(tooltip.closest('tr'))) { + closestHoveredRow = true; + } + } catch (e) {} + + if (!closestHoveredRow) { + const style = window.getComputedStyle(tooltip); + const isVisible = style.display !== 'none' && + style.visibility !== 'hidden' && + style.opacity !== '0'; + + if (!isVisible) { + tooltip.parentNode.removeChild(tooltip); + } + } + } + }); + } catch (error) { + console.error('Error cleaning up all tooltips:', error); } } cleanupOrphanedTooltips() { - const tippyElements = document.querySelectorAll('[data-tippy-root]'); - let removed = 0; + try { + const tippyElements = document.querySelectorAll('[data-tippy-root]'); + let removed = 0; - tippyElements.forEach(element => { - const tooltipId = element.getAttribute('data-for-tooltip-id'); - const trigger = tooltipId ? - document.querySelector(`[data-tooltip-trigger-id="${tooltipId}"]`) : - null; + tippyElements.forEach(element => { + const tooltipId = element.getAttribute('data-for-tooltip-id'); + const trigger = tooltipId ? + document.querySelector(`[data-tooltip-trigger-id="${tooltipId}"]`) : + null; - if (!trigger || !document.body.contains(trigger)) { - if (element.parentNode) { - element.parentNode.removeChild(element); - removed++; + if (!trigger || !document.body.contains(trigger)) { + if (element.parentNode) { + element.parentNode.removeChild(element); + removed++; + } } + }); + + if (removed > 0) { + this.log(`Removed ${removed} orphaned tooltip elements`); } - }); - if (removed > 0) { - this.log(`Removed ${removed} orphaned tooltip elements`); + return removed; + } catch (error) { + console.error('Error cleaning up orphaned tooltips:', error); + return 0; } - - return removed; } setupMutationObserver() { - if (this.mutationObserver) { - this.mutationObserver.disconnect(); - } + try { + const mutationObserver = new MutationObserver(mutations => { + let needsCleanup = false; - this.mutationObserver = new MutationObserver(mutations => { - let needsCleanup = false; - - mutations.forEach(mutation => { - if (mutation.removedNodes.length) { - Array.from(mutation.removedNodes).forEach(node => { - if (node.nodeType === Node.ELEMENT_NODE) { - if (node.hasAttribute && node.hasAttribute('data-tooltip-trigger-id')) { - this.destroy(node); - needsCleanup = true; - } - - if (node.querySelectorAll) { - const tooltipTriggers = node.querySelectorAll('[data-tooltip-trigger-id]'); - if (tooltipTriggers.length > 0) { - tooltipTriggers.forEach(trigger => { - this.destroy(trigger); - }); + mutations.forEach(mutation => { + if (mutation.removedNodes.length) { + Array.from(mutation.removedNodes).forEach(node => { + if (node.nodeType === Node.ELEMENT_NODE) { + if (node.hasAttribute && node.hasAttribute('data-tooltip-trigger-id')) { + this.destroy(node); needsCleanup = true; } + + if (node.querySelectorAll) { + const tooltipTriggers = node.querySelectorAll('[data-tooltip-trigger-id]'); + if (tooltipTriggers.length > 0) { + tooltipTriggers.forEach(trigger => { + this.destroy(trigger); + }); + needsCleanup = true; + } + } } - } - }); + }); + } + }); + + if (needsCleanup) { + this.cleanupOrphanedTooltips(); } }); - - if (needsCleanup) { - this.cleanupOrphanedTooltips(); - } - }); - this.mutationObserver.observe(document.body, { - childList: true, - subtree: true - }); + mutationObserver.observe(document.body, { + childList: true, + subtree: true + }); - return this.mutationObserver; + this.resources.mutationObserver = CleanupManager.registerResource( + 'mutationObserver', + mutationObserver, + (observer) => observer.disconnect() + ); + + return mutationObserver; + } catch (error) { + console.error('Error setting up mutation observer:', error); + return null; + } } startDisconnectedElementsCheck() { - if (this.disconnectedCheckInterval) { - clearInterval(this.disconnectedCheckInterval); + try { + this.resources.disconnectedCheckInterval = CleanupManager.setInterval(() => { + this.checkForDisconnectedElements(); + }, 60000); + } catch (error) { + console.error('Error starting disconnected elements check:', error); } - - this.disconnectedCheckInterval = setInterval(() => { - this.checkForDisconnectedElements(); - }, 60000); } checkForDisconnectedElements() { - const elements = document.querySelectorAll('[data-tooltip-trigger-id]'); - let removedCount = 0; + try { + const elements = document.querySelectorAll('[data-tooltip-trigger-id]'); + let removedCount = 0; - elements.forEach(element => { - if (!document.body.contains(element)) { - this.destroy(element); - removedCount++; + elements.forEach(element => { + if (!document.body.contains(element)) { + this.destroy(element); + removedCount++; + } + }); + + if (removedCount > 0) { + this.log(`Removed ${removedCount} tooltips for disconnected elements`); + this.cleanupOrphanedTooltips(); } - }); - - if (removedCount > 0) { - this.log(`Removed ${removedCount} tooltips for disconnected elements`); - this.cleanupOrphanedTooltips(); + } catch (error) { + console.error('Error checking for disconnected elements:', error); } } startPeriodicCleanup() { - if (this.cleanupInterval) { - clearInterval(this.cleanupInterval); + try { + this.resources.cleanupInterval = CleanupManager.setInterval(() => { + this.performPeriodicCleanup(); + }, 120000); + } catch (error) { + console.error('Error starting periodic cleanup:', error); } - - this.cleanupInterval = setInterval(() => { - this.performPeriodicCleanup(); - }, 120000); } performPeriodicCleanup(force = false) { - this.cleanupOrphanedTooltips(); - - this.checkForDisconnectedElements(); - - const tooltipCount = document.querySelectorAll('[data-tippy-root]').length; - - if (force || tooltipCount > this.maxTooltips) { - this.log(`Performing aggressive cleanup (${tooltipCount} tooltips)`); - - this.cleanup(); - - if (window.gc) { - window.gc(); - } else { - const arr = new Array(1000); - for (let i = 0; i < 1000; i++) { - arr[i] = new Array(10000).join('x'); - } + try { + if ((window.location.pathname.includes('/offers') || window.location.pathname.includes('/bids')) && + !force) { + return; } + + this.cleanupOrphanedTooltips(); + this.checkForDisconnectedElements(); + + const tooltipCount = document.querySelectorAll('[data-tippy-root]').length; + + if (force || tooltipCount > this.maxTooltips) { + this.log(`Performing aggressive cleanup (${tooltipCount} tooltips)`); + this.cleanup(); + } + } catch (error) { + console.error('Error performing periodic cleanup:', error); } } setupStyles() { if (document.getElementById('tooltip-styles')) return; - document.head.insertAdjacentHTML('beforeend', ` - - `); + `; + document.head.appendChild(style); + + this.resources.tooltipStyles = CleanupManager.registerResource( + 'tooltipStyles', + style, + (styleElement) => { + if (styleElement && styleElement.parentNode) { + styleElement.parentNode.removeChild(styleElement); + } + } + ); + } catch (error) { + console.error('Error setting up styles:', error); + try { + document.head.insertAdjacentHTML('beforeend', ` + + `); + + const styleElement = document.getElementById('tooltip-styles'); + if (styleElement) { + this.resources.tooltipStyles = CleanupManager.registerResource( + 'tooltipStyles', + styleElement, + (elem) => { + if (elem && elem.parentNode) { + elem.parentNode.removeChild(elem); + } + } + ); + } + } catch (e) { + console.error('Failed to add tooltip styles:', e); + } + } } initializeTooltips(selector = '[data-tooltip-target]') { - document.querySelectorAll(selector).forEach(element => { - const targetId = element.getAttribute('data-tooltip-target'); - if (!targetId) return; + try { + document.querySelectorAll(selector).forEach(element => { + const targetId = element.getAttribute('data-tooltip-target'); + if (!targetId) return; - const tooltipContent = document.getElementById(targetId); + const tooltipContent = document.getElementById(targetId); - if (tooltipContent) { - this.create(element, tooltipContent.innerHTML, { - placement: element.getAttribute('data-tooltip-placement') || 'top' - }); - } - }); + if (tooltipContent) { + this.create(element, tooltipContent.innerHTML, { + placement: element.getAttribute('data-tooltip-placement') || 'top' + }); + } + }); + } catch (error) { + console.error('Error initializing tooltips:', error); + } } dispose() { this.log('Disposing TooltipManager'); - - this.cleanup(); - this.pendingAnimationFrames.forEach(id => { - cancelAnimationFrame(id); - }); - this.pendingAnimationFrames.clear(); + try { + this.cleanup(); - this.pendingTimeouts.forEach(id => { - clearTimeout(id); - }); - this.pendingTimeouts.clear(); + Object.values(this.resources).forEach(resourceId => { + if (resourceId) { + CleanupManager.unregisterResource(resourceId); + } + }); - if (this.disconnectedCheckInterval) { - clearInterval(this.disconnectedCheckInterval); - this.disconnectedCheckInterval = null; + this.resources = {}; + + instance = null; + return true; + } catch (error) { + console.error('Error disposing TooltipManager:', error); + return false; } - - if (this.cleanupInterval) { - clearInterval(this.cleanupInterval); - this.cleanupInterval = null; - } - - if (this.mutationObserver) { - this.mutationObserver.disconnect(); - this.mutationObserver = null; - } - - const styleElement = document.getElementById('tooltip-styles'); - if (styleElement && styleElement.parentNode) { - styleElement.parentNode.removeChild(styleElement); - } - - instance = null; - return true; } setDebugMode(enabled) { @@ -536,16 +766,26 @@ const TooltipManager = (function() { } initialize(options = {}) { - if (options.maxTooltips) { - this.maxTooltips = options.maxTooltips; - } + try { + if (options.maxTooltips) { + this.maxTooltips = options.maxTooltips; + } - if (options.debug !== undefined) { - this.setDebugMode(options.debug); - } + if (options.debug !== undefined) { + this.setDebugMode(options.debug); + } - this.log('TooltipManager initialized'); - return this; + this.setupStyles(); + this.setupMutationObserver(); + this.startPeriodicCleanup(); + this.startDisconnectedElementsCheck(); + + this.log('TooltipManager initialized'); + return this; + } catch (error) { + console.error('Error initializing TooltipManager:', error); + return this; + } } } @@ -580,6 +820,11 @@ const TooltipManager = (function() { return manager.cleanup(...args); }, + thoroughCleanup: function() { + const manager = this.getInstance(); + return manager.thoroughCleanup(); + }, + initializeTooltips: function(...args) { const manager = this.getInstance(); return manager.initializeTooltips(...args); @@ -590,6 +835,11 @@ const TooltipManager = (function() { return manager.setDebugMode(enabled); }, + getActiveTooltipInstances: function() { + const manager = this.getInstance(); + return manager.getActiveTooltipInstances(); + }, + dispose: function(...args) { const manager = this.getInstance(); return manager.dispose(...args); @@ -597,37 +847,53 @@ const TooltipManager = (function() { }; })(); -function installTooltipManager() { - const originalTooltipManager = window.TooltipManager; - - window.TooltipManager = TooltipManager; - - window.TooltipManager.initialize({ - maxTooltips: 200, - debug: false - }); - - document.addEventListener('DOMContentLoaded', function() { - if (!window.tooltipManagerInitialized) { - window.TooltipManager.initializeTooltips(); - window.tooltipManagerInitialized = true; - } - }); - - return originalTooltipManager; -} - -if (typeof document !== 'undefined') { - if (document.readyState === 'complete' || document.readyState === 'interactive') { - installTooltipManager(); - } else { - document.addEventListener('DOMContentLoaded', installTooltipManager); - } -} - if (typeof module !== 'undefined' && module.exports) { module.exports = TooltipManager; } -window.TooltipManager = TooltipManager; -console.log('TooltipManager initialized'); +if (typeof window !== 'undefined') { + window.TooltipManager = TooltipManager; +} + +if (typeof window !== 'undefined' && typeof document !== 'undefined') { + function initializeTooltipManager() { + if (!window.tooltipManagerInitialized) { + + if (!window.CleanupManager) { + console.warn('CleanupManager not found. TooltipManager will run with limited functionality.'); + + window.CleanupManager = window.CleanupManager || { + registerResource: (type, resource, cleanup) => { + return Math.random().toString(36).substring(2, 9); + }, + unregisterResource: () => {}, + setTimeout: (callback, delay) => setTimeout(callback, delay), + setInterval: (callback, delay) => setInterval(callback, delay), + requestAnimationFrame: (callback) => requestAnimationFrame(callback), + addListener: (element, type, handler, options) => { + element.addEventListener(type, handler, options); + return handler; + } + }; + } + + window.TooltipManager.initialize({ + maxTooltips: 200, + debug: false + }); + + window.TooltipManager.initializeTooltips(); + window.tooltipManagerInitialized = true; + } + } + + if (document.readyState === 'complete' || document.readyState === 'interactive') { + initializeTooltipManager(); + } else { + document.addEventListener('DOMContentLoaded', initializeTooltipManager, { once: true }); + } +} + +if (typeof window !== 'undefined' && typeof console !== 'undefined') { + console.log('TooltipManager initialized'); +} diff --git a/basicswap/static/js/offers.js b/basicswap/static/js/offers.js index 8cf7f71..18ba205 100644 --- a/basicswap/static/js/offers.js +++ b/basicswap/static/js/offers.js @@ -990,7 +990,7 @@ function createTableRow(offer, identity = null) { } let coinFromDisplay, coinToDisplay; - + if (window.CoinManager) { coinFromDisplay = window.CoinManager.getDisplayName(coinFrom) || coinFrom; coinToDisplay = window.CoinManager.getDisplayName(coinTo) || coinTo; @@ -1000,7 +1000,7 @@ function createTableRow(offer, identity = null) { if (coinFromDisplay.toLowerCase() === 'zcoin') coinFromDisplay = 'Firo'; if (coinToDisplay.toLowerCase() === 'zcoin') coinToDisplay = 'Firo'; } - + const postedTime = formatTime(createdAt, true); const expiresIn = formatTime(expireAt); const currentTime = Math.floor(Date.now() / 1000); @@ -1370,7 +1370,6 @@ function createRecipientTooltip(uniqueId, identityInfo, identity, successRate, t return 'text-red-600'; }; - const truncateText = (text, maxLength) => { if (text.length <= maxLength) return text; return text.substring(0, maxLength) + '...'; @@ -1454,7 +1453,7 @@ function createTooltipContent(isSentOffers, coinFrom, coinTo, fromAmount, toAmou const getPriceKey = (coin) => { if (!coin) return null; - + const lowerCoin = coin.toLowerCase(); if (lowerCoin === 'zcoin') return 'firo'; @@ -2167,6 +2166,18 @@ document.addEventListener('DOMContentLoaded', async function() { tableRateModule.init(); } + document.addEventListener('memoryOptimized', (e) => { + if (jsonData && jsonData.length > e.detail.maxDataSize) { + console.log(`Trimming offers data from ${jsonData.length} to ${e.detail.maxDataSize} items`); + jsonData = jsonData.slice(0, e.detail.maxDataSize); + } + + if (originalJsonData && originalJsonData.length > e.detail.maxDataSize) { + console.log(`Trimming original offers data from ${originalJsonData.length} to ${e.detail.maxDataSize} items`); + originalJsonData = originalJsonData.slice(0, e.detail.maxDataSize); + } + }); + await initializeTableAndData(); if (window.PriceManager) { @@ -2179,7 +2190,7 @@ document.addEventListener('DOMContentLoaded', async function() { if (window.WebSocketManager) { WebSocketManager.addMessageHandler('message', async (data) => { if (data.event === 'new_offer' || data.event === 'offer_revoked') { - console.log('WebSocket event received:', data.event); + //console.log('WebSocket event received:', data.event); try { const previousPrices = latestPrices; @@ -2188,7 +2199,7 @@ document.addEventListener('DOMContentLoaded', async function() { if (!offersResponse.ok) { throw new Error(`HTTP error! status: ${offersResponse.status}`); } - + const newData = await offersResponse.json(); const processedNewData = Array.isArray(newData) ? newData : Object.values(newData); jsonData = formatInitialData(processedNewData); @@ -2200,7 +2211,7 @@ document.addEventListener('DOMContentLoaded', async function() { } else { priceData = await fetchLatestPrices(); } - + if (priceData) { latestPrices = priceData; CacheManager.set('prices_coingecko', priceData, 'prices'); @@ -2230,7 +2241,7 @@ document.addEventListener('DOMContentLoaded', async function() { updatePaginationInfo(); - console.log('WebSocket-triggered refresh completed successfully'); + //console.log('WebSocket-triggered refresh completed successfully'); } catch (error) { console.error('Error during WebSocket-triggered refresh:', error); NetworkManager.handleNetworkError(error); @@ -2357,28 +2368,10 @@ function cleanup() { if (window.CleanupManager) { window.CleanupManager.removeListenersByElement(row); } - - while (row.attributes && row.attributes.length > 0) { - row.removeAttribute(row.attributes[0].name); - } - - while (row.firstChild) { - row.removeChild(row.firstChild); - } }); - offersBody.innerHTML = ''; } - const filterForm = document.getElementById('filterForm'); - if (filterForm) { - CleanupManager.removeListenersByElement(filterForm); - - filterForm.querySelectorAll('select').forEach(select => { - CleanupManager.removeListenersByElement(select); - }); - } - jsonData = null; originalJsonData = null; latestPrices = null; @@ -2388,13 +2381,12 @@ function cleanup() { } if (window.MemoryManager) { - if (window.MemoryManager.cleanupTooltips) { - window.MemoryManager.cleanupTooltips(true); + if (window.MemoryManager.forceCleanup) { + window.MemoryManager.forceCleanup(); } - window.MemoryManager.forceCleanup(); } - console.log('Offers.js cleanup completed'); + //console.log('Offers.js cleanup completed'); } catch (error) { console.error('Error during cleanup:', error); } diff --git a/basicswap/static/js/pricechart.js b/basicswap/static/js/pricechart.js index 8b56ba4..1508ba1 100644 --- a/basicswap/static/js/pricechart.js +++ b/basicswap/static/js/pricechart.js @@ -578,7 +578,7 @@ const chartModule = { this.chartRefs.set(element, chart); }, - destroyChart: function() { +destroyChart: function() { if (chartModule.chart) { try { const chartInstance = chartModule.chart; @@ -592,12 +592,17 @@ const chartModule = { if (canvas) { chartModule.chartRefs.delete(canvas); + + const ctx = canvas.getContext('2d'); + if (ctx) { + ctx.clearRect(0, 0, canvas.width, canvas.height); + } } } catch (e) { console.error('Error destroying chart:', e); } } - }, +}, initChart: function() { this.destroyChart(); From ece9d7fb4bfdc3f8f10ba7bea34bb52930fa58ec Mon Sep 17 00:00:00 2001 From: gerlofvanek Date: Thu, 8 May 2025 21:09:24 +0200 Subject: [PATCH 3/5] Removed repeating console.log(s) --- basicswap/static/js/modules/cache-manager.js | 2 +- basicswap/static/js/pricechart.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/basicswap/static/js/modules/cache-manager.js b/basicswap/static/js/modules/cache-manager.js index 1deddc8..8211a89 100644 --- a/basicswap/static/js/modules/cache-manager.js +++ b/basicswap/static/js/modules/cache-manager.js @@ -328,7 +328,7 @@ const CacheManager = (function() { .filter(key => isCacheKey(key)) .forEach(key => memoryCache.delete(key)); - console.log("Cache cleared successfully"); + //console.log("Cache cleared successfully"); return true; }, diff --git a/basicswap/static/js/pricechart.js b/basicswap/static/js/pricechart.js index 1508ba1..015f579 100644 --- a/basicswap/static/js/pricechart.js +++ b/basicswap/static/js/pricechart.js @@ -1377,7 +1377,7 @@ const app = { }, refreshAllData: async function() { - console.log('Price refresh started at', new Date().toLocaleTimeString()); + //console.log('Price refresh started at', new Date().toLocaleTimeString()); if (app.isRefreshing) { console.log('Refresh already in progress, skipping...'); @@ -1411,7 +1411,7 @@ refreshAllData: async function() { return; } - console.log('Starting refresh of all data...'); + //console.log('Starting refresh of all data...'); app.isRefreshing = true; app.updateNextRefreshTime(); ui.showLoader(); @@ -1478,7 +1478,7 @@ refreshAllData: async function() { const cacheKey = `coinData_${coin.symbol}`; CacheManager.set(cacheKey, coinData, 'prices'); - console.log(`Updated price for ${coin.symbol}: $${coinData.current_price}`); + //console.log(`Updated price for ${coin.symbol}: $${coinData.current_price}`); } catch (coinError) { console.warn(`Failed to update ${coin.symbol}: ${coinError.message}`); @@ -1549,7 +1549,7 @@ refreshAllData: async function() { app.scheduleNextRefresh(); } - console.log(`Refresh process finished at ${new Date().toLocaleTimeString()}, next refresh scheduled: ${app.isAutoRefreshEnabled ? 'yes' : 'no'}`); + //console.log(`Refresh process finished at ${new Date().toLocaleTimeString()}, next refresh scheduled: ${app.isAutoRefreshEnabled ? 'yes' : 'no'}`); } }, From b3c0ad7e9cc28468fcd074ca14f35400a7459f2b Mon Sep 17 00:00:00 2001 From: gerlofvanek Date: Thu, 8 May 2025 22:31:39 +0200 Subject: [PATCH 4/5] Add clickable bid counters in the header that navigate to the sent/received tabs + small fix. --- basicswap/static/js/bids_sentreceived.js | 71 ++++++++++ basicswap/static/js/pricechart.js | 2 +- basicswap/static/js/ui/bids-tab-navigation.js | 127 ++++++++++++++++++ basicswap/templates/header.html | 107 +++++++-------- 4 files changed, 248 insertions(+), 59 deletions(-) create mode 100644 basicswap/static/js/ui/bids-tab-navigation.js diff --git a/basicswap/static/js/bids_sentreceived.js b/basicswap/static/js/bids_sentreceived.js index 52b91e5..3ad6af4 100644 --- a/basicswap/static/js/bids_sentreceived.js +++ b/basicswap/static/js/bids_sentreceived.js @@ -1925,3 +1925,74 @@ if (document.readyState === 'loading') { } else { initialize(); } + +(function() { + function handleBidsTabFromHash() { + if (window.location.pathname !== '/bids') { + return; + } + + const hash = window.location.hash; + + if (hash) { + const tabName = hash.substring(1); + let tabId; + switch (tabName.toLowerCase()) { + case 'sent': + tabId = '#sent'; + break; + case 'received': + tabId = '#received'; + break; + default: + tabId = '#sent'; + } + switchTab(tabId); + } else { + switchTab('#sent'); + } + } + + function switchTab(tabId) { + const targetTabBtn = document.querySelector(`[data-tabs-target="${tabId}"]`); + if (targetTabBtn) { + targetTabBtn.click(); + } + } + + function setupBidsTabNavigation() { + handleBidsTabFromHash(); + window.addEventListener('hashchange', handleBidsTabFromHash); + const originalSwitchTab = window.switchTab || null; + + window.switchTab = function(tabId) { + const newTabName = tabId.replace('#', ''); + if (window.location.hash !== `#${newTabName}`) { + history.replaceState(null, null, `#${newTabName}`); + } + if (originalSwitchTab && typeof originalSwitchTab === 'function') { + originalSwitchTab(tabId); + } else { + const targetTabBtn = document.querySelector(`[data-tabs-target="${tabId}"]`); + if (targetTabBtn) { + targetTabBtn.click(); + } + } + }; + + const tabButtons = document.querySelectorAll('[data-tabs-target]'); + tabButtons.forEach(btn => { + btn.addEventListener('click', function() { + const tabId = this.getAttribute('data-tabs-target'); + const tabName = tabId.replace('#', ''); + history.replaceState(null, null, `#${tabName}`); + }); + }); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', setupBidsTabNavigation); + } else { + setupBidsTabNavigation(); + } +})(); diff --git a/basicswap/static/js/pricechart.js b/basicswap/static/js/pricechart.js index 015f579..4c0906c 100644 --- a/basicswap/static/js/pricechart.js +++ b/basicswap/static/js/pricechart.js @@ -1518,7 +1518,7 @@ refreshAllData: async function() { } }, 1000); } - console.log(`Price refresh completed at ${new Date().toLocaleTimeString()}. Updated ${window.config.coins.length - failedCoins.length}/${window.config.coins.length} coins.`); + //console.log(`Price refresh completed at ${new Date().toLocaleTimeString()}. Updated ${window.config.coins.length - failedCoins.length}/${window.config.coins.length} coins.`); } catch (error) { console.error('Critical error during refresh:', error); diff --git a/basicswap/static/js/ui/bids-tab-navigation.js b/basicswap/static/js/ui/bids-tab-navigation.js new file mode 100644 index 0000000..d1acb93 --- /dev/null +++ b/basicswap/static/js/ui/bids-tab-navigation.js @@ -0,0 +1,127 @@ +(function() { + 'use strict'; + + document.addEventListener('DOMContentLoaded', initBidsTabNavigation); + window.addEventListener('load', handleHashChange); + window.addEventListener('hashchange', preventScrollOnHashChange); + + function initBidsTabNavigation() { + const sentTabButton = document.getElementById('sent-tab'); + const receivedTabButton = document.getElementById('received-tab'); + + if (!sentTabButton || !receivedTabButton) { + return; + } + + document.querySelectorAll('.bids-tab-link').forEach(link => { + link.addEventListener('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + + const targetTabId = this.getAttribute('data-tab-target'); + + if (targetTabId) { + if (window.location.pathname === '/bids') { + const oldScrollPosition = window.scrollY; + + activateTab(targetTabId); + + setTimeout(function() { + window.scrollTo(0, oldScrollPosition); + + history.replaceState(null, null, '#' + targetTabId.replace('#', '')); + }, 0); + } else { + localStorage.setItem('bidsTabToActivate', targetTabId.replace('#', '')); + window.location.href = '/bids'; + } + } + }); + }); + + const tabToActivate = localStorage.getItem('bidsTabToActivate'); + if (tabToActivate) { + localStorage.removeItem('bidsTabToActivate'); + activateTab('#' + tabToActivate); + } else if (window.location.pathname === '/bids' && !window.location.hash) { + activateTab('#sent'); + } + } + + function preventScrollOnHashChange(e) { + if (window.location.pathname !== '/bids') { + return; + } + + e.preventDefault(); + + const oldScrollPosition = window.scrollY; + const hash = window.location.hash; + + if (hash) { + const tabId = `#${hash.replace('#', '')}`; + activateTab(tabId); + } else { + activateTab('#sent'); + } + + setTimeout(function() { + window.scrollTo(0, oldScrollPosition); + }, 0); + } + + function handleHashChange() { + if (window.location.pathname !== '/bids') { + return; + } + + const oldScrollPosition = window.scrollY; + const hash = window.location.hash; + + if (hash) { + const tabId = `#${hash.replace('#', '')}`; + activateTab(tabId); + } else { + activateTab('#sent'); + } + + setTimeout(function() { + window.scrollTo(0, oldScrollPosition); + }, 0); + } + + function activateTab(tabId) { + if (tabId !== '#sent' && tabId !== '#received') { + tabId = '#sent'; + } + + const tabButtonId = tabId === '#sent' ? 'sent-tab' : 'received-tab'; + const tabButton = document.getElementById(tabButtonId); + + if (tabButton) { + const oldScrollPosition = window.scrollY; + + tabButton.click(); + + setTimeout(function() { + window.scrollTo(0, oldScrollPosition); + }, 0); + } + } + + window.navigateToBidsTab = function(tabId) { + if (window.location.pathname === '/bids') { + const oldScrollPosition = window.scrollY; + + activateTab('#' + (tabId === 'sent' || tabId === 'received' ? tabId : 'sent')); + + setTimeout(function() { + window.scrollTo(0, oldScrollPosition); + history.replaceState(null, null, '#' + tabId); + }, 0); + } else { + localStorage.setItem('bidsTabToActivate', tabId); + window.location.href = '/bids'; + } + }; +})(); diff --git a/basicswap/templates/header.html b/basicswap/templates/header.html index 5b811c5..0f84a8d 100644 --- a/basicswap/templates/header.html +++ b/basicswap/templates/header.html @@ -63,6 +63,7 @@ + @@ -84,6 +85,7 @@ + @@ -981,11 +1136,11 @@ const createTableRow = async (bid) => {
@@ -1001,12 +1156,12 @@ const createTableRow = async (bid) => {
${state.currentTab === 'sent' ? bid.coin_to : bid.coin_from}
-
${state.currentTab === 'sent' ? bid.amount_to : bid.amount_from}
-
${state.currentTab === 'sent' ? bid.coin_to : bid.coin_from}
+
${isSent ? bid.amount_to : bid.amount_from}
+
${isSent ? bid.coin_to : bid.coin_from}
@@ -1015,12 +1170,12 @@ const createTableRow = async (bid) => {
${state.currentTab === 'sent' ? bid.coin_from : bid.coin_to}
-
${state.currentTab === 'sent' ? bid.amount_from : bid.amount_to}
-
${state.currentTab === 'sent' ? bid.coin_from : bid.coin_to}
+
${isSent ? bid.amount_from : bid.amount_to}
+
${isSent ? bid.coin_from : bid.coin_to}
@@ -1045,10 +1200,9 @@ const createTableRow = async (bid) => { - @@ -1071,6 +1225,131 @@ const createTableRow = async (bid) => { `; }; +function cleanupOffscreenTooltips() { + if (!window.TooltipManager) return; + + const selector = '#' + state.currentTab + ' [data-tooltip-target]'; + const tooltipTriggers = document.querySelectorAll(selector); + + const farOffscreenTriggers = Array.from(tooltipTriggers).filter(trigger => { + const rect = trigger.getBoundingClientRect(); + return (rect.bottom < -window.innerHeight * 2 || + rect.top > window.innerHeight * 3); + }); + + farOffscreenTriggers.forEach(trigger => { + const targetId = trigger.getAttribute('data-tooltip-target'); + if (targetId) { + const tooltipElement = document.getElementById(targetId); + if (tooltipElement) { + window.TooltipManager.destroy(trigger); + trigger.addEventListener('mouseenter', () => { + createTooltipForTrigger(trigger); + }, { once: true }); + } + } + }); +} + +function implementVirtualizedRows() { + const tbody = elements[`${state.currentTab}BidsBody`]; + if (!tbody) return; + + const tableRows = tbody.querySelectorAll('tr'); + if (tableRows.length < 30) return; + + Array.from(tableRows).forEach(row => { + const rect = row.getBoundingClientRect(); + const isVisible = ( + rect.bottom >= 0 && + rect.top <= window.innerHeight + ); + + if (!isVisible && (rect.bottom < -window.innerHeight || rect.top > window.innerHeight * 2)) { + const tooltipTriggers = row.querySelectorAll('[data-tooltip-target]'); + tooltipTriggers.forEach(trigger => { + if (window.TooltipManager) { + window.TooltipManager.destroy(trigger); + } + }); + } + }); +} + +async function fetchBids(type = state.currentTab) { + if (type === 'all') { + return fetchAllBids(); + } + + try { + if (activeFetchController) { + activeFetchController.abort(); + } + activeFetchController = new AbortController(); + const endpoint = type === 'sent' ? '/json/sentbids' : '/json/bids'; + const withExpiredSelect = document.getElementById('with_expired'); + const includeExpired = withExpiredSelect ? withExpiredSelect.value === 'true' : true; + + //console.log(`Fetching ${type} bids, include expired:`, includeExpired); + + const timeoutId = setTimeout(() => { + if (activeFetchController) { + activeFetchController.abort(); + } + }, 30000); + + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify({ + sort_by: state.filters.sort_by || 'created_at', + sort_dir: state.filters.sort_dir || 'desc', + with_expired: true, + state: state.filters.state ?? -1, + with_extra_info: true + }), + signal: activeFetchController.signal + }); + + clearTimeout(timeoutId); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + //console.log(`Received raw ${type} data:`, data.length, 'bids'); + + state.filters.with_expired = includeExpired; + + let processedData; + if (data.length > 500) { + processedData = await new Promise(resolve => { + setTimeout(() => { + const filtered = filterAndSortData(data); + resolve(filtered); + }, 10); + }); + } else { + processedData = filterAndSortData(data); + } + + return processedData; + } catch (error) { + if (error.name === 'AbortError') { + console.log('Fetch request was aborted'); + } else { + console.error(`Error in fetch${type.charAt(0).toUpperCase() + type.slice(1)}Bids:`, error); + } + throw error; + } finally { + activeFetchController = null; + } +} + const updateTableContent = async (type) => { const tbody = elements[`${type}BidsBody`]; if (!tbody) return; @@ -1220,146 +1499,6 @@ const createTooltipForTrigger = (trigger) => { } }; -function optimizeForLargeDatasets() { - if (state.data[state.currentTab]?.length > 50) { - - const simplifyTooltips = tooltipIdsToCleanup.size > 50; - - implementVirtualizedRows(); - - let scrollTimeout; - window.addEventListener('scroll', () => { - clearTimeout(scrollTimeout); - scrollTimeout = setTimeout(() => { - cleanupOffscreenTooltips(); - }, 150); - }, { passive: true }); - } -} - -function cleanupOffscreenTooltips() { - if (!window.TooltipManager) return; - - const selector = '#' + state.currentTab + ' [data-tooltip-target]'; - const tooltipTriggers = document.querySelectorAll(selector); - - const farOffscreenTriggers = Array.from(tooltipTriggers).filter(trigger => { - const rect = trigger.getBoundingClientRect(); - return (rect.bottom < -window.innerHeight * 2 || - rect.top > window.innerHeight * 3); - }); - - farOffscreenTriggers.forEach(trigger => { - const targetId = trigger.getAttribute('data-tooltip-target'); - if (targetId) { - const tooltipElement = document.getElementById(targetId); - if (tooltipElement) { - window.TooltipManager.destroy(trigger); - trigger.addEventListener('mouseenter', () => { - createTooltipForTrigger(trigger); - }, { once: true }); - } - } - }); -} - -function implementVirtualizedRows() { - const tbody = elements[`${state.currentTab}BidsBody`]; - if (!tbody) return; - - const tableRows = tbody.querySelectorAll('tr'); - if (tableRows.length < 30) return; - - Array.from(tableRows).forEach(row => { - const rect = row.getBoundingClientRect(); - const isVisible = ( - rect.bottom >= 0 && - rect.top <= window.innerHeight - ); - - if (!isVisible && (rect.bottom < -window.innerHeight || rect.top > window.innerHeight * 2)) { - const tooltipTriggers = row.querySelectorAll('[data-tooltip-target]'); - tooltipTriggers.forEach(trigger => { - if (window.TooltipManager) { - window.TooltipManager.destroy(trigger); - } - }); - } - }); -} - -let activeFetchController = null; - -const fetchBids = async () => { - try { - if (activeFetchController) { - activeFetchController.abort(); - } - activeFetchController = new AbortController(); - const endpoint = state.currentTab === 'sent' ? '/json/sentbids' : '/json/bids'; - const withExpiredSelect = document.getElementById('with_expired'); - const includeExpired = withExpiredSelect ? withExpiredSelect.value === 'true' : true; - - //console.log('Fetching bids, include expired:', includeExpired); - - const timeoutId = setTimeout(() => { - if (activeFetchController) { - activeFetchController.abort(); - } - }, 30000); - - const response = await fetch(endpoint, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json' - }, - body: JSON.stringify({ - sort_by: state.filters.sort_by || 'created_at', - sort_dir: state.filters.sort_dir || 'desc', - with_expired: true, - state: state.filters.state ?? -1, - with_extra_info: true - }), - signal: activeFetchController.signal - }); - - clearTimeout(timeoutId); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const data = await response.json(); - //console.log('Received raw data:', data.length, 'bids'); - - state.filters.with_expired = includeExpired; - - let processedData; - if (data.length > 500) { - processedData = await new Promise(resolve => { - setTimeout(() => { - const filtered = filterAndSortData(data); - resolve(filtered); - }, 10); - }); - } else { - processedData = filterAndSortData(data); - } - - return processedData; - } catch (error) { - if (error.name === 'AbortError') { - console.log('Fetch request was aborted'); - } else { - console.error('Error in fetchBids:', error); - } - throw error; - } finally { - activeFetchController = null; - } -}; - const updateBidsTable = async () => { if (state.isLoading) { return; @@ -1369,7 +1508,13 @@ const updateBidsTable = async () => { state.isLoading = true; updateLoadingState(true); - const bids = await fetchBids(); + let bids; + + if (state.currentTab === 'all') { + bids = await fetchAllBids(); + } else { + bids = await fetchBids(); + } // Add identity preloading if we're searching if (state.filters.searchQuery && state.filters.searchQuery.length > 0) { @@ -1416,7 +1561,11 @@ const updatePaginationControls = (type) => { } if (currentPageSpan) { - currentPageSpan.textContent = totalPages > 0 ? state.currentPage[type] : 0; + if (totalPages > 0) { + currentPageSpan.innerHTML = `${state.currentPage[type]} of ${totalPages}`; + } else { + currentPageSpan.textContent = "0"; + } } if (prevButton) { @@ -1581,7 +1730,7 @@ function setupFilterEventListeners() { } const setupRefreshButtons = () => { - ['Sent', 'Received'].forEach(type => { + ['All', 'Sent', 'Received'].forEach(type => { const refreshButton = elements[`refresh${type}Bids`]; if (refreshButton) { EventManager.add(refreshButton, 'click', async () => { @@ -1597,30 +1746,35 @@ const setupRefreshButtons = () => { state.isLoading = true; updateLoadingState(true); - const response = await fetch(state.currentTab === 'sent' ? '/json/sentbids' : '/json/bids', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - sort_by: state.filters.sort_by, - sort_dir: state.filters.sort_dir, - with_expired: state.filters.with_expired, - state: state.filters.state, - with_extra_info: true - }) - }); + if (lowerType === 'all') { + state.data.all = await fetchAllBids(); + } else { + const response = await fetch(lowerType === 'sent' ? '/json/sentbids' : '/json/bids', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + sort_by: state.filters.sort_by, + sort_dir: state.filters.sort_dir, + with_expired: state.filters.with_expired, + state: state.filters.state, + with_extra_info: true + }) + }); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + if (!Array.isArray(data)) { + throw new Error('Invalid response format'); + } + + state.data[lowerType] = data; } - - const data = await response.json(); - if (!Array.isArray(data)) { - throw new Error('Invalid response format'); - } - - state.data[lowerType] = data; + await updateTableContent(lowerType); updatePaginationControls(lowerType); @@ -1648,8 +1802,10 @@ const switchTab = (tabId) => { tooltipIdsToCleanup.clear(); - state.currentTab = tabId === '#sent' ? 'sent' : 'received'; + state.currentTab = tabId === '#all' ? 'all' : + (tabId === '#sent' ? 'sent' : 'received'); + elements.allContent.classList.add('hidden'); elements.sentContent.classList.add('hidden'); elements.receivedContent.classList.add('hidden'); @@ -1669,11 +1825,31 @@ const switchTab = (tabId) => { tab.classList.add('hover:text-gray-600', 'hover:bg-gray-50', 'dark:hover:bg-gray-500'); } }); + setTimeout(() => { updateBidsTable(); }, 10); }; +window.switchTab = switchTab; + +function optimizeForLargeDatasets() { + if (state.data[state.currentTab]?.length > 50) { + + const simplifyTooltips = tooltipIdsToCleanup.size > 50; + + implementVirtualizedRows(); + + let scrollTimeout; + window.addEventListener('scroll', () => { + clearTimeout(scrollTimeout); + scrollTimeout = setTimeout(() => { + cleanupOffscreenTooltips(); + }, 150); + }, { passive: true }); + } +} + const setupEventListeners = () => { const filterControls = document.querySelector('.flex.flex-wrap.justify-center'); if (filterControls) { @@ -1708,10 +1884,12 @@ const setupEventListeners = () => { } }); + elements.allContent.classList.toggle('hidden', targetId !== '#all'); elements.sentContent.classList.toggle('hidden', targetId !== '#sent'); elements.receivedContent.classList.toggle('hidden', targetId !== '#received'); - state.currentTab = targetId === '#sent' ? 'sent' : 'received'; + state.currentTab = targetId === '#all' ? 'all' : + (targetId === '#sent' ? 'sent' : 'received'); state.currentPage[state.currentTab] = 1; if (window.TooltipManager) { @@ -1724,7 +1902,7 @@ const setupEventListeners = () => { }); } - ['Sent', 'Received'].forEach(type => { + ['All', 'Sent', 'Received'].forEach(type => { const lowerType = type.toLowerCase(); if (elements[`prevPage${type}`]) { @@ -1863,6 +2041,11 @@ function setupMemoryMonitoring() { window.TooltipManager.cleanup(); } + if (state.data.all.length > 1000) { + console.log('Trimming all bids data'); + state.data.all = state.data.all.slice(0, 1000); + } + if (state.data.sent.length > 1000) { console.log('Trimming sent bids data'); state.data.sent = state.data.sent.slice(0, 1000); @@ -1912,11 +2095,13 @@ function initialize() { setTimeout(() => { updateClearFiltersButton(); - state.currentTab = 'sent'; + state.currentTab = 'all'; state.filters.state = -1; updateBidsTable(); }, 100); + setupMemoryMonitoring(); + window.cleanupBidsTable = cleanup; } @@ -1925,74 +2110,3 @@ if (document.readyState === 'loading') { } else { initialize(); } - -(function() { - function handleBidsTabFromHash() { - if (window.location.pathname !== '/bids') { - return; - } - - const hash = window.location.hash; - - if (hash) { - const tabName = hash.substring(1); - let tabId; - switch (tabName.toLowerCase()) { - case 'sent': - tabId = '#sent'; - break; - case 'received': - tabId = '#received'; - break; - default: - tabId = '#sent'; - } - switchTab(tabId); - } else { - switchTab('#sent'); - } - } - - function switchTab(tabId) { - const targetTabBtn = document.querySelector(`[data-tabs-target="${tabId}"]`); - if (targetTabBtn) { - targetTabBtn.click(); - } - } - - function setupBidsTabNavigation() { - handleBidsTabFromHash(); - window.addEventListener('hashchange', handleBidsTabFromHash); - const originalSwitchTab = window.switchTab || null; - - window.switchTab = function(tabId) { - const newTabName = tabId.replace('#', ''); - if (window.location.hash !== `#${newTabName}`) { - history.replaceState(null, null, `#${newTabName}`); - } - if (originalSwitchTab && typeof originalSwitchTab === 'function') { - originalSwitchTab(tabId); - } else { - const targetTabBtn = document.querySelector(`[data-tabs-target="${tabId}"]`); - if (targetTabBtn) { - targetTabBtn.click(); - } - } - }; - - const tabButtons = document.querySelectorAll('[data-tabs-target]'); - tabButtons.forEach(btn => { - btn.addEventListener('click', function() { - const tabId = this.getAttribute('data-tabs-target'); - const tabName = tabId.replace('#', ''); - history.replaceState(null, null, `#${tabName}`); - }); - }); - } - - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', setupBidsTabNavigation); - } else { - setupBidsTabNavigation(); - } -})(); diff --git a/basicswap/static/js/bids_sentreceived_export.js b/basicswap/static/js/bids_sentreceived_export.js index acee851..3021267 100644 --- a/basicswap/static/js/bids_sentreceived_export.js +++ b/basicswap/static/js/bids_sentreceived_export.js @@ -4,17 +4,18 @@ const BidExporter = { return 'No data to export'; } - const isSent = type === 'sent'; + const isAllTab = type === 'all'; const headers = [ 'Date/Time', 'Bid ID', 'Offer ID', 'From Address', - isSent ? 'You Send Amount' : 'You Receive Amount', - isSent ? 'You Send Coin' : 'You Receive Coin', - isSent ? 'You Receive Amount' : 'You Send Amount', - isSent ? 'You Receive Coin' : 'You Send Coin', + ...(isAllTab ? ['Type'] : []), + 'You Send Amount', + 'You Send Coin', + 'You Receive Amount', + 'You Receive Coin', 'Status', 'Created At', 'Expires At' @@ -23,11 +24,13 @@ const BidExporter = { let csvContent = headers.join(',') + '\n'; bids.forEach(bid => { + const isSent = isAllTab ? (bid.source === 'sent') : (type === 'sent'); const row = [ `"${formatTime(bid.created_at)}"`, `"${bid.bid_id}"`, `"${bid.offer_id}"`, `"${bid.addr_from}"`, + ...(isAllTab ? [`"${bid.source}"`] : []), isSent ? bid.amount_from : bid.amount_to, `"${isSent ? bid.coin_from : bid.coin_to}"`, isSent ? bid.amount_to : bid.amount_from, @@ -103,6 +106,15 @@ const BidExporter = { document.addEventListener('DOMContentLoaded', function() { setTimeout(function() { if (typeof state !== 'undefined' && typeof EventManager !== 'undefined') { + const exportAllButton = document.getElementById('exportAllBids'); + if (exportAllButton) { + EventManager.add(exportAllButton, 'click', (e) => { + e.preventDefault(); + state.currentTab = 'all'; + BidExporter.exportCurrentView(); + }); + } + const exportSentButton = document.getElementById('exportSentBids'); if (exportSentButton) { EventManager.add(exportSentButton, 'click', (e) => { @@ -128,9 +140,14 @@ const originalCleanup = window.cleanup || function(){}; window.cleanup = function() { originalCleanup(); + const exportAllButton = document.getElementById('exportAllBids'); const exportSentButton = document.getElementById('exportSentBids'); const exportReceivedButton = document.getElementById('exportReceivedBids'); + if (exportAllButton && typeof EventManager !== 'undefined') { + EventManager.remove(exportAllButton, 'click'); + } + if (exportSentButton && typeof EventManager !== 'undefined') { EventManager.remove(exportSentButton, 'click'); } diff --git a/basicswap/static/js/ui/bids-tab-navigation.js b/basicswap/static/js/ui/bids-tab-navigation.js index d1acb93..c88d732 100644 --- a/basicswap/static/js/ui/bids-tab-navigation.js +++ b/basicswap/static/js/ui/bids-tab-navigation.js @@ -1,36 +1,39 @@ (function() { 'use strict'; + const originalOnload = window.onload; - document.addEventListener('DOMContentLoaded', initBidsTabNavigation); - window.addEventListener('load', handleHashChange); - window.addEventListener('hashchange', preventScrollOnHashChange); + window.onload = function() { + if (typeof originalOnload === 'function') { + originalOnload(); + } + + setTimeout(function() { + initBidsTabNavigation(); + handleInitialNavigation(); + }, 100); + }; + + document.addEventListener('DOMContentLoaded', function() { + initBidsTabNavigation(); + }); + + window.addEventListener('hashchange', handleHashChange); + + window.bidsTabNavigationInitialized = false; function initBidsTabNavigation() { - const sentTabButton = document.getElementById('sent-tab'); - const receivedTabButton = document.getElementById('received-tab'); - - if (!sentTabButton || !receivedTabButton) { + if (window.bidsTabNavigationInitialized) { return; } - + document.querySelectorAll('.bids-tab-link').forEach(link => { link.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); - const targetTabId = this.getAttribute('data-tab-target'); - if (targetTabId) { if (window.location.pathname === '/bids') { - const oldScrollPosition = window.scrollY; - - activateTab(targetTabId); - - setTimeout(function() { - window.scrollTo(0, oldScrollPosition); - - history.replaceState(null, null, '#' + targetTabId.replace('#', '')); - }, 0); + navigateToTabDirectly(targetTabId); } else { localStorage.setItem('bidsTabToActivate', targetTabId.replace('#', '')); window.location.href = '/bids'; @@ -39,35 +42,28 @@ }); }); - const tabToActivate = localStorage.getItem('bidsTabToActivate'); - if (tabToActivate) { - localStorage.removeItem('bidsTabToActivate'); - activateTab('#' + tabToActivate); - } else if (window.location.pathname === '/bids' && !window.location.hash) { - activateTab('#sent'); - } + window.bidsTabNavigationInitialized = true; + console.log('Bids tab navigation initialized'); } - function preventScrollOnHashChange(e) { + function handleInitialNavigation() { if (window.location.pathname !== '/bids') { return; } + + const tabToActivate = localStorage.getItem('bidsTabToActivate'); - e.preventDefault(); - - const oldScrollPosition = window.scrollY; - const hash = window.location.hash; - - if (hash) { - const tabId = `#${hash.replace('#', '')}`; - activateTab(tabId); + if (tabToActivate) { + //console.log('Activating tab from localStorage:', tabToActivate); + localStorage.removeItem('bidsTabToActivate'); + activateTabWithRetry('#' + tabToActivate); + } else if (window.location.hash) { + //console.log('Activating tab from hash:', window.location.hash); + activateTabWithRetry(window.location.hash); } else { - activateTab('#sent'); + //console.log('Activating default tab: #all'); + activateTabWithRetry('#all'); } - - setTimeout(function() { - window.scrollTo(0, oldScrollPosition); - }, 0); } function handleHashChange() { @@ -75,50 +71,141 @@ return; } - const oldScrollPosition = window.scrollY; const hash = window.location.hash; - if (hash) { - const tabId = `#${hash.replace('#', '')}`; - activateTab(tabId); + //console.log('Hash changed, activating tab:', hash); + activateTabWithRetry(hash); } else { - activateTab('#sent'); + //console.log('Hash cleared, activating default tab: #all'); + activateTabWithRetry('#all'); } + } + + function activateTabWithRetry(tabId, retryCount = 0) { + const normalizedTabId = tabId.startsWith('#') ? tabId : '#' + tabId; + + if (normalizedTabId !== '#all' && normalizedTabId !== '#sent' && normalizedTabId !== '#received') { + //console.log('Invalid tab ID, defaulting to #all'); + activateTabWithRetry('#all'); + return; + } + + const tabButtonId = normalizedTabId === '#all' ? 'all-tab' : + (normalizedTabId === '#sent' ? 'sent-tab' : 'received-tab'); + const tabButton = document.getElementById(tabButtonId); + + if (!tabButton) { + if (retryCount < 5) { + //console.log('Tab button not found, retrying...', retryCount + 1); + setTimeout(() => { + activateTabWithRetry(normalizedTabId, retryCount + 1); + }, 100); + } else { + //console.error('Failed to find tab button after retries'); + } + return; + } + + //console.log('Activating tab:', normalizedTabId); + + tabButton.click(); + + if (window.Tabs) { + const tabsEl = document.querySelector('[data-tabs-toggle="#bidstab"]'); + if (tabsEl) { + const allTabs = Array.from(tabsEl.querySelectorAll('[role="tab"]')); + const targetTab = allTabs.find(tab => tab.getAttribute('data-tabs-target') === normalizedTabId); + + if (targetTab) { + + allTabs.forEach(tab => { + tab.setAttribute('aria-selected', tab === targetTab ? 'true' : 'false'); + + if (tab === targetTab) { + tab.classList.add('bg-gray-100', 'dark:bg-gray-600', 'text-gray-900', 'dark:text-white'); + tab.classList.remove('hover:text-gray-600', 'hover:bg-gray-50', 'dark:hover:bg-gray-500'); + } else { + tab.classList.remove('bg-gray-100', 'dark:bg-gray-600', 'text-gray-900', 'dark:text-white'); + tab.classList.add('hover:text-gray-600', 'hover:bg-gray-50', 'dark:hover:bg-gray-500'); + } + }); + + const allContent = document.getElementById('all'); + const sentContent = document.getElementById('sent'); + const receivedContent = document.getElementById('received'); + + if (allContent && sentContent && receivedContent) { + allContent.classList.toggle('hidden', normalizedTabId !== '#all'); + sentContent.classList.toggle('hidden', normalizedTabId !== '#sent'); + receivedContent.classList.toggle('hidden', normalizedTabId !== '#received'); + } + } + } + } + + const allPanel = document.getElementById('all'); + const sentPanel = document.getElementById('sent'); + const receivedPanel = document.getElementById('received'); + + if (allPanel && sentPanel && receivedPanel) { + allPanel.classList.toggle('hidden', normalizedTabId !== '#all'); + sentPanel.classList.toggle('hidden', normalizedTabId !== '#sent'); + receivedPanel.classList.toggle('hidden', normalizedTabId !== '#received'); + } + + const newHash = normalizedTabId.replace('#', ''); + if (window.location.hash !== '#' + newHash) { + history.replaceState(null, null, '#' + newHash); + } + + triggerDataLoad(normalizedTabId); + } + + function triggerDataLoad(tabId) { + setTimeout(() => { + if (window.state) { + window.state.currentTab = tabId === '#all' ? 'all' : + (tabId === '#sent' ? 'sent' : 'received'); + + if (typeof window.updateBidsTable === 'function') { + //console.log('Triggering data load for', tabId); + window.updateBidsTable(); + } + } + + const event = new CustomEvent('tabactivated', { + detail: { + tabId: tabId, + type: tabId === '#all' ? 'all' : + (tabId === '#sent' ? 'sent' : 'received') + } + }); + document.dispatchEvent(event); + + if (window.TooltipManager && typeof window.TooltipManager.cleanup === 'function') { + setTimeout(() => { + window.TooltipManager.cleanup(); + if (typeof window.initializeTooltips === 'function') { + window.initializeTooltips(); + } + }, 200); + } + }, 100); + } + + function navigateToTabDirectly(tabId) { + const oldScrollPosition = window.scrollY; + + activateTabWithRetry(tabId); setTimeout(function() { window.scrollTo(0, oldScrollPosition); }, 0); } - function activateTab(tabId) { - if (tabId !== '#sent' && tabId !== '#received') { - tabId = '#sent'; - } - - const tabButtonId = tabId === '#sent' ? 'sent-tab' : 'received-tab'; - const tabButton = document.getElementById(tabButtonId); - - if (tabButton) { - const oldScrollPosition = window.scrollY; - - tabButton.click(); - - setTimeout(function() { - window.scrollTo(0, oldScrollPosition); - }, 0); - } - } - window.navigateToBidsTab = function(tabId) { if (window.location.pathname === '/bids') { - const oldScrollPosition = window.scrollY; - - activateTab('#' + (tabId === 'sent' || tabId === 'received' ? tabId : 'sent')); - - setTimeout(function() { - window.scrollTo(0, oldScrollPosition); - history.replaceState(null, null, '#' + tabId); - }, 0); + navigateToTabDirectly('#' + tabId); } else { localStorage.setItem('bidsTabToActivate', tabId); window.location.href = '/bids'; diff --git a/basicswap/templates/bids.html b/basicswap/templates/bids.html index 3cd6ebc..2bd172a 100644 --- a/basicswap/templates/bids.html +++ b/basicswap/templates/bids.html @@ -10,7 +10,7 @@
-

Sent Bids / Received Bids

+

All Bids / Sent Bids / Received Bids

View, and manage bids.

@@ -28,7 +28,12 @@
  • - +
  • +
  • +
  • @@ -167,7 +172,106 @@
    -
    + +
    +
    +
    +
    +
    +
    + + + + + + + + + + + + + +
    +
    + Date/Time +
    +
    +
    + You Send +
    +
    +
    + You Receive +
    +
    +
    + Status +
    +
    +
    + Actions +
    +
    +
    +
    +
    +
    +
    +
    + + Connecting... +
    +

    + All Bids: 0 +

    + {% if debug_ui_mode == true %} + + {% endif %} + + + +
    + +
    +
    +
    +
    +
    +
    +
    +
    + + +