diff --git a/basicswap/static/js/bids_sentreceived.js b/basicswap/static/js/bids_sentreceived.js index decbc6f..ae4effd 100644 --- a/basicswap/static/js/bids_sentreceived.js +++ b/basicswap/static/js/bids_sentreceived.js @@ -1,15 +1,19 @@ const PAGE_SIZE = 50; +let activeFetchController = null; + const state = { currentPage: { + all: 1, sent: 1, received: 1 }, isLoading: false, isRefreshing: false, - currentTab: 'sent', + currentTab: 'all', wsConnected: false, refreshPromise: null, data: { + all: [], sent: [], received: [] }, @@ -24,6 +28,16 @@ const state = { } }; +document.addEventListener('tabactivated', function(event) { + if (event.detail && event.detail.tabId) { + const tabType = event.detail.type || (event.detail.tabId === '#all' ? 'all' : + (event.detail.tabId === '#sent' ? 'sent' : 'received')); + //console.log('Tab activation event received for:', tabType); + state.currentTab = tabType; + updateBidsTable(); + } +}); + const STATE_MAP = { 1: ['Sent'], 2: ['Receiving'], @@ -61,33 +75,43 @@ const STATE_MAP = { }; const elements = { - sentBidsBody: document.querySelector('#sent tbody'), - receivedBidsBody: document.querySelector('#received tbody'), + allBidsBody: document.querySelector('#all-tbody'), + sentBidsBody: document.querySelector('#sent-tbody'), + receivedBidsBody: document.querySelector('#received-tbody'), filterForm: document.querySelector('form'), stateSelect: document.querySelector('select[name="state"]'), sortBySelect: document.querySelector('select[name="sort_by"]'), sortDirSelect: document.querySelector('select[name="sort_dir"]'), withExpiredSelect: document.querySelector('select[name="with_expired"]'), tabButtons: document.querySelectorAll('#myTab button'), + allContent: document.getElementById('all'), sentContent: document.getElementById('sent'), receivedContent: document.getElementById('received'), + allPaginationControls: document.getElementById('pagination-controls-all'), sentPaginationControls: document.getElementById('pagination-controls-sent'), receivedPaginationControls: document.getElementById('pagination-controls-received'), + prevPageAll: document.getElementById('prevPageAll'), prevPageSent: document.getElementById('prevPageSent'), - nextPageSent: document.getElementById('nextPageSent'), prevPageReceived: document.getElementById('prevPageReceived'), + nextPageAll: document.getElementById('nextPageAll'), + nextPageSent: document.getElementById('nextPageSent'), nextPageReceived: document.getElementById('nextPageReceived'), + currentPageAll: document.getElementById('currentPageAll'), currentPageSent: document.getElementById('currentPageSent'), currentPageReceived: document.getElementById('currentPageReceived'), + allBidsCount: document.getElementById('allBidsCount'), sentBidsCount: document.getElementById('sentBidsCount'), receivedBidsCount: document.getElementById('receivedBidsCount'), + statusDotAll: document.getElementById('status-dot-all'), + statusTextAll: document.getElementById('status-text-all'), statusDotSent: document.getElementById('status-dot-sent'), statusTextSent: document.getElementById('status-text-sent'), statusDotReceived: document.getElementById('status-dot-received'), statusTextReceived: document.getElementById('status-text-received'), + refreshAllBids: document.getElementById('refreshAllBids'), refreshSentBids: document.getElementById('refreshSentBids'), refreshReceivedBids: document.getElementById('refreshReceivedBids') }; @@ -213,6 +237,7 @@ function cleanup() { } }; + cleanupTableBody('all-tbody'); cleanupTableBody('sent-tbody'); cleanupTableBody('received-tbody'); @@ -234,11 +259,13 @@ function cleanup() { clearAllAnimationFrames(); state.data = { + all: [], sent: [], received: [] }; state.currentPage = { + all: 1, sent: 1, received: 1 }; @@ -283,7 +310,7 @@ function cleanup() { if (window.CleanupManager) CleanupManager.clearAll(); if (window.WebSocketManager) WebSocketManager.disconnect(); - state.data = { sent: [], received: [] }; + state.data = { all: [], sent: [], received: [] }; state.isLoading = false; Object.keys(elements).forEach(key => { @@ -311,7 +338,6 @@ CleanupManager.addListener(document, 'visibilitychange', () => { window.TooltipManager.cleanup(); } - // Run memory optimization if (window.MemoryManager) { MemoryManager.forceCleanup(); } @@ -367,7 +393,7 @@ function cleanupRow(row) { function optimizeMemoryUsage() { const MAX_BIDS_IN_MEMORY = 500; - ['sent', 'received'].forEach(type => { + ['all', 'sent', 'received'].forEach(type => { if (state.data[type] && state.data[type].length > MAX_BIDS_IN_MEMORY) { console.log(`Trimming ${type} bids data from ${state.data[type].length} to ${MAX_BIDS_IN_MEMORY}`); state.data[type] = state.data[type].slice(0, MAX_BIDS_IN_MEMORY); @@ -529,7 +555,9 @@ function filterAndSortData(bids) { const coinName = selectedOption?.textContent.trim(); if (coinName) { - const coinToMatch = state.currentTab === 'sent' ? bid.coin_to : bid.coin_from; + const coinToMatch = state.currentTab === 'all' + ? (bid.source === 'sent' ? bid.coin_to : bid.coin_from) + : (state.currentTab === 'sent' ? bid.coin_to : bid.coin_from); if (!coinMatches(coinToMatch, coinName)) { return false; } @@ -542,7 +570,10 @@ function filterAndSortData(bids) { const coinName = selectedOption?.textContent.trim(); if (coinName) { - const coinToMatch = state.currentTab === 'sent' ? bid.coin_from : bid.coin_to; + const coinToMatch = state.currentTab === 'all' + ? (bid.source === 'sent' ? bid.coin_from : bid.coin_to) + : (state.currentTab === 'sent' ? bid.coin_from : bid.coin_to); + if (!coinMatches(coinToMatch, coinName)) { return false; } @@ -583,7 +614,8 @@ function filterAndSortData(bids) { let matchesDisplayedLabel = false; if (!matchesLabel && document) { try { - const tableId = state.currentTab === 'sent' ? 'sent' : 'received'; + const tableId = state.currentTab === 'sent' ? 'sent' : + (state.currentTab === 'received' ? 'received' : 'all'); const cells = document.querySelectorAll(`#${tableId} a[href^="/identity/"]`); for (const cell of cells) { @@ -681,7 +713,7 @@ function updateCoinFilterImages() { const updateLoadingState = (isLoading) => { state.isLoading = isLoading; - ['Sent', 'Received'].forEach(type => { + ['All', 'Sent', 'Received'].forEach(type => { const refreshButton = elements[`refresh${type}Bids`]; const refreshText = refreshButton?.querySelector(`#refresh${type}Text`); const refreshIcon = refreshButton?.querySelector('svg'); @@ -732,7 +764,7 @@ const updateConnectionStatus = (status) => { const config = statusConfig[status] || statusConfig.connected; - ['sent', 'received'].forEach(type => { + ['all', 'sent', 'received'].forEach(type => { const dot = elements[`statusDot${type.charAt(0).toUpperCase() + type.slice(1)}`]; const text = elements[`statusText${type.charAt(0).toUpperCase() + type.slice(1)}`]; @@ -770,16 +802,69 @@ const processIdentityStats = (identity) => { const createIdentityTooltipContent = (identity) => { if (!identity) return ''; + const address = identity.address || ''; + let statsSection = ''; - const stats = processIdentityStats(identity); - if (!stats) return ''; + try { + const stats = processIdentityStats(identity); + if (stats) { + const getSuccessRateColor = (rate) => { + const numRate = parseFloat(rate); + if (numRate >= 80) return 'text-green-600'; + if (numRate >= 60) return 'text-yellow-600'; + return 'text-red-600'; + }; - const getSuccessRateColor = (rate) => { - const numRate = parseFloat(rate); - if (numRate >= 80) return 'text-green-600'; - if (numRate >= 60) return 'text-yellow-600'; - return 'text-red-600'; - }; + statsSection = ` +
+
Swap History:
+
+
+
+ ${stats.successRate}% +
+
Success Rate
+
+
+
${stats.totalBids}
+
Total Trades
+
+
+
+
+
+ ${stats.totalSuccessful} +
+
Successful
+
+
+
+ ${stats.totalRejected} +
+
Rejected
+
+
+
+ ${stats.totalFailed} +
+
Failed
+
+
+
+ `; + } + } catch (e) { + console.warn('Error processing identity stats:', e); + } + + const addressSection = ` +
+
Bid From Address:
+
+ ${address || identity.address || ''} +
+
+ `; return `
@@ -790,12 +875,7 @@ const createIdentityTooltipContent = (identity) => {
` : ''} -
-
Bid From Address:
-
- ${identity.address || ''} -
-
+ ${addressSection} ${identity.note ? `
@@ -804,41 +884,7 @@ const createIdentityTooltipContent = (identity) => {
` : ''} -
-
Swap History:
-
-
-
- ${stats.successRate}% -
-
Success Rate
-
-
-
${stats.totalBids}
-
Total Trades
-
-
-
-
-
- ${stats.totalSuccessful} -
-
Successful
-
-
-
- ${stats.totalRejected} -
-
Rejected
-
-
-
- ${stats.totalFailed} -
-
Failed
-
-
-
+ ${statsSection} `; }; @@ -955,12 +1001,117 @@ const forceTooltipDOMCleanup = () => { } } +async function fetchAllBids() { + try { + const sentController = new AbortController(); + const receivedController = new AbortController(); + + const sentPromise = fetch('/json/sentbids', { + 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: sentController.signal + }); + + const receivedPromise = fetch('/json/bids', { + 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: receivedController.signal + }); + + const timeoutId = setTimeout(() => { + sentController.abort(); + receivedController.abort(); + }, 30000); + + const [sentResponse, receivedResponse] = await Promise.all([sentPromise, receivedPromise]); + + clearTimeout(timeoutId); + + if (!sentResponse.ok || !receivedResponse.ok) { + throw new Error(`HTTP error! Sent status: ${sentResponse.status}, Received status: ${receivedResponse.status}`); + } + + const [sentData, receivedData] = await Promise.all([ + sentResponse.json(), + receivedResponse.json() + ]); + + const filteredSentData = filterAndSortData(sentData).map(bid => ({ ...bid, source: 'sent' })); + const filteredReceivedData = filterAndSortData(receivedData).map(bid => ({ ...bid, source: 'received' })); + + const combined = [...filteredSentData, ...filteredReceivedData].sort((a, b) => { + const direction = state.filters.sort_dir === 'asc' ? 1 : -1; + return direction * (a.created_at - b.created_at); + }); + + return combined; + } catch (error) { + console.error('Error fetching all bids:', error); + throw error; + } +} + const createTableRow = async (bid) => { - const identity = await IdentityManager.getIdentityData(bid.addr_from); + const rawAddress = bid.addr_from || ''; + let identity = null; + try { + identity = await IdentityManager.getIdentityData(rawAddress); + } catch (e) { + console.warn('Error loading identity for bid:', bid.bid_id, e); + } + if (!identity) { + identity = { address: rawAddress, label: null }; + } else if (!identity.address) { + identity.address = rawAddress; + } const uniqueId = `${bid.bid_id}_${Date.now()}`; tooltipIdsToCleanup.add(`tooltip-identity-${uniqueId}`); tooltipIdsToCleanup.add(`tooltip-status-${uniqueId}`); const timeColor = getTimeStrokeColor(bid.expire_at); + const currentTabIsAll = state.currentTab === 'all'; + const isSent = currentTabIsAll ? (bid.source === 'sent') : (state.currentTab === 'sent'); + const sourceIndicator = currentTabIsAll ? + ` + ${isSent ? 'Sent' : 'Received'} + ` : ''; + let tooltipContent = ''; + try { + tooltipContent = createIdentityTooltipContent(identity); + } catch (e) { + console.warn('Error creating tooltip content:', e); + } + if (!tooltipContent) { + tooltipContent = ` +
+
+
Bid From Address:
+
+ ${rawAddress} +
+
+
+ `; + } return ` @@ -973,7 +1124,10 @@ const createTableRow = async (bid) => { -
${formatTime(bid.created_at)}
+
+ ${formatTime(bid.created_at)} + ${sourceIndicator} +
@@ -982,11 +1136,11 @@ const createTableRow = async (bid) => {
@@ -1002,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}
@@ -1016,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}
@@ -1046,10 +1200,9 @@ const createTableRow = async (bid) => { - @@ -1072,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; @@ -1221,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; @@ -1370,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) { @@ -1417,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) { @@ -1582,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 () => { @@ -1598,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); @@ -1649,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'); @@ -1670,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) { @@ -1709,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) { @@ -1725,7 +1902,7 @@ const setupEventListeners = () => { }); } - ['Sent', 'Received'].forEach(type => { + ['All', 'Sent', 'Received'].forEach(type => { const lowerType = type.toLowerCase(); if (elements[`prevPage${type}`]) { @@ -1864,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); @@ -1883,7 +2065,6 @@ function setupMemoryMonitoring() { }, { once: true }); } -// Init function initialize() { const filterElements = { stateSelect: document.getElementById('state'), @@ -1901,8 +2082,6 @@ function initialize() { if (filterElements.coinFrom) filterElements.coinFrom.value = 'any'; if (filterElements.coinTo) filterElements.coinTo.value = 'any'; - setupMemoryMonitoring(); - setTimeout(() => { WebSocketManager.initialize(); setupEventListeners(); @@ -1916,17 +2095,13 @@ function initialize() { setTimeout(() => { updateClearFiltersButton(); - state.currentTab = 'sent'; + state.currentTab = 'all'; state.filters.state = -1; updateBidsTable(); }, 100); - setInterval(() => { - if ((state.data.sent.length + state.data.received.length) > 1000) { - optimizeMemoryUsage(); - } - }, 5 * 60 * 1000); // Check every 5 minutes - + setupMemoryMonitoring(); + window.cleanupBidsTable = cleanup; } 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/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/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/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 fd5489f..4c7687f 100644 --- a/basicswap/static/js/modules/memory-manager.js +++ b/basicswap/static/js/modules/memory-manager.js @@ -1,219 +1,582 @@ const MemoryManager = (function() { + const config = { + tooltipCleanupInterval: 300000, + diagnosticsInterval: 600000, + elementVerificationInterval: 300000, + maxTooltipsThreshold: 100, + maxTooltips: 300, + cleanupThreshold: 1.5, + minTimeBetweenCleanups: 180000, + memoryGrowthThresholdMB: 100, + debug: false, + 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' + ] + }; - const state = { - isMonitoringEnabled: false, - monitorInterval: null, - cleanupInterval: null - }; + const state = { + pendingAnimationFrames: new Set(), + pendingTimeouts: new Set(), + cleanupInterval: null, + diagnosticsInterval: null, + elementVerificationInterval: null, + mutationObserver: null, + lastCleanupTime: Date.now(), + startTime: Date.now(), + isCleanupRunning: false, + metrics: { + tooltipsRemoved: 0, + cleanupRuns: 0, + lastMemoryUsage: null, + lastCleanupDetails: {}, + history: [] + }, + originalTooltipFunctions: {} + }; - const config = { - monitorInterval: 30000, - cleanupInterval: 60000, - debug: false - }; + function log(message, ...args) { + if (config.debug) { + console.log(`[MemoryManager] ${message}`, ...args); + } + } - function log(message, ...args) { - if (config.debug) { - console.log(`[MemoryManager] ${message}`, ...args); - } + function preserveTooltipFunctions() { + if (window.TooltipManager && !state.originalTooltipFunctions.destroy) { + state.originalTooltipFunctions = { + destroy: window.TooltipManager.destroy, + cleanup: window.TooltipManager.cleanup, + create: window.TooltipManager.create + }; + } + } + + function isInProtectedContainer(element) { + if (!element) return false; + + for (const selector of config.protectedContainers) { + if (element.closest && element.closest(selector)) { + return true; + } } - const publicAPI = { - enableMonitoring: function(interval = config.monitorInterval) { - if (state.monitorInterval) { - clearInterval(state.monitorInterval); - } + return false; + } - state.isMonitoringEnabled = true; - config.monitorInterval = interval; + function shouldSkipCleanup() { + if (state.isCleanupRunning) return true; + + const selector = config.interactiveSelectors.join(', '); + const hoveredElements = document.querySelectorAll(selector); + + return hoveredElements.length > 0; + } - this.logMemoryUsage(); + function performCleanup(force = false) { + if (shouldSkipCleanup() && !force) { + return false; + } - state.monitorInterval = setInterval(() => { - this.logMemoryUsage(); - }, interval); + if (state.isCleanupRunning) { + return false; + } - console.log(`Memory monitoring enabled - reporting every ${interval/1000} seconds`); - return true; - }, + const now = Date.now(); + if (!force && now - state.lastCleanupTime < config.minTimeBetweenCleanups) { + return false; + } - disableMonitoring: function() { - if (state.monitorInterval) { - clearInterval(state.monitorInterval); - state.monitorInterval = null; - } + try { + state.isCleanupRunning = true; + state.lastCleanupTime = now; + state.metrics.cleanupRuns++; - state.isMonitoringEnabled = false; - console.log('Memory monitoring disabled'); - return true; - }, + const startTime = performance.now(); + const startMemory = checkMemoryUsage(); - logMemoryUsage: function() { - const timestamp = new Date().toLocaleTimeString(); - console.log(`=== Memory Monitor [${timestamp}] ===`); + state.pendingAnimationFrames.forEach(id => { + cancelAnimationFrame(id); + }); + state.pendingAnimationFrames.clear(); - 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' - }); - } + state.pendingTimeouts.forEach(id => { + clearTimeout(id); + }); + state.pendingTimeouts.clear(); - if (navigator.deviceMemory) { - console.log('Device memory:', navigator.deviceMemory, 'GB'); - } + const tooltipsResult = removeOrphanedTooltips(); + state.metrics.tooltipsRemoved += tooltipsResult; - const nodeCount = document.querySelectorAll('*').length; - console.log('DOM node count:', nodeCount); + const disconnectedResult = checkForDisconnectedElements(); - if (window.CleanupManager) { - const counts = CleanupManager.getResourceCounts(); - console.log('Managed resources:', counts); - } + tryRunGarbageCollection(false); - 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); - } + const endTime = performance.now(); + const endMemory = checkMemoryUsage(); - if (window.CacheManager && window.CacheManager.getStats) { - const cacheStats = CacheManager.getStats(); - console.log('Cache stats:', cacheStats); - } + 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 + }; - if (window.IdentityManager && window.IdentityManager.getStats) { - const identityStats = window.IdentityManager.getStats(); - console.log('Identity cache stats:', identityStats); - } + state.metrics.history.unshift(runStats); + if (state.metrics.history.length > 10) { + state.metrics.history.pop(); + } - console.log('=============================='); - }, + state.metrics.lastCleanupDetails = runStats; - enableAutoCleanup: function(interval = config.cleanupInterval) { - if (state.cleanupInterval) { - clearInterval(state.cleanupInterval); - } + if (config.debug) { + log(`Cleanup completed in ${runStats.duration.toFixed(2)}ms, removed ${tooltipsResult} tooltips`); + } - config.cleanupInterval = interval; + return true; + } catch (error) { + console.error("Error during cleanup:", error); + return false; + } finally { + state.isCleanupRunning = false; + } + } - this.forceCleanup(); + function removeOrphanedTooltips() { + try { - state.cleanupInterval = setInterval(() => { - this.forceCleanup(); - }, interval); + const tippyRoots = document.querySelectorAll('[data-tippy-root]:not(:hover)'); + let removed = 0; - log('Auto-cleanup enabled every', interval/1000, 'seconds'); - return true; - }, + tippyRoots.forEach(root => { + const tooltipId = root.getAttribute('data-for-tooltip-id'); + const trigger = tooltipId ? + document.querySelector(`[data-tooltip-trigger-id="${tooltipId}"]`) : null; - 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'); + if (!trigger || !document.body.contains(trigger)) { + if (root.parentNode) { + root.parentNode.removeChild(root); + removed++; + } } + }); + + return removed; + } catch (error) { + console.error("Error removing orphaned tooltips:", error); + return 0; + } + } + + function checkForDisconnectedElements() { + try { + + const tooltipTriggers = document.querySelectorAll('[data-tooltip-trigger-id]:not(:hover)'); + const disconnectedElements = new Set(); + + 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 => { + 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) { + console.error("Error checking for disconnected elements:", error); + return 0; + } + } + + 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" }; - return publicAPI; + 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"; + } + + state.metrics.lastMemoryUsage = result; + return result; + } + + function handleVisibilityChange() { + if (document.hidden) { + removeOrphanedTooltips(); + checkForDisconnectedElements(); + } + } + + function setupMutationObserver() { + if (state.mutationObserver) { + state.mutationObserver.disconnect(); + state.mutationObserver = null; + } + + 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; + } + + processingScheduled = true; + + setTimeout(() => { + 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); + }; + + 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; + } + + 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, + 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 + }; })(); -window.MemoryManager = MemoryManager; - document.addEventListener('DOMContentLoaded', function() { - if (!window.memoryManagerInitialized) { - MemoryManager.initialize(); - window.memoryManagerInitialized = true; - } + const isDevMode = window.location.hostname === 'localhost' || + window.location.hostname === '127.0.0.1'; + + MemoryManager.initialize({ + debug: isDevMode + }); + + console.log('Memory Manager initialized'); }); -//console.log('MemoryManager initialized with methods:', Object.keys(MemoryManager)); -console.log('MemoryManager initialized'); +window.MemoryManager = MemoryManager; 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 6af1b6e..59f70b2 100644 --- a/basicswap/static/js/modules/tooltips-manager.js +++ b/basicswap/static/js/modules/tooltips-manager.js @@ -1,128 +1,101 @@ 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.disconnectedCheckInterval = null; + this.maxTooltips = 200; + this.cleanupThreshold = 1.2; + this.debug = false; + this.tooltipData = new WeakMap(); + this.resources = {}; - this.setupStyles(); - this.setupCleanupEvents(); - this.initializeMutationObserver(); - this.startDisconnectedElementsCheck(); + if (window.CleanupManager) { + CleanupManager.registerResource( + 'tooltipManager', + this, + (manager) => manager.dispose() + ); + } 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); - + const createTooltip = () => { if (!document.body.contains(element)) return; 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); + CleanupManager.setTimeout(() => { + CleanupManager.requestAnimationFrame(retryCreate); + }, 100); } }; - const initialRetryId = requestAnimationFrame(retryCreate); - this.pendingAnimationFrames.add(initialRetryId); - } - }); - this.pendingAnimationFrames.add(rafId); + CleanupManager.setTimeout(() => { + CleanupManager.requestAnimationFrame(retryCreate); + }, 100); + } + }; + + CleanupManager.requestAnimationFrame(createTooltip); 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)) { + return null; } - const tooltipId = `tooltip-${++this.tooltipIdCounter}`; + if (typeof window.tippy !== 'function') { + console.error('Tippy.js is not available.'); + return null; + } 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 +116,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)) { + CleanupManager.setTimeout(() => { + if (instance && instance.destroy) { + instance.destroy(); + } + }, 100); + } + }, popperOptions: { strategy: 'fixed', modifiers: [ @@ -170,19 +154,44 @@ 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]); + + 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]; + } + + return null; + } catch (error) { + console.error('Error creating tooltip:', error); return null; } } @@ -190,79 +199,322 @@ 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'); + 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.pendingAnimationFrames.forEach(id => { - cancelAnimationFrame(id); - }); - this.pendingAnimationFrames.clear(); + this.log('Running tooltip cleanup'); - 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.cleanupOrphanedTippyElements(); + this.cleanupOrphanedTooltips(); } - }; - - if (elements.length > 0) { - processElementsBatch(0); - } else { - this.cleanupOrphanedTippyElements(); + } catch (error) { + console.error('Error during cleanup:', error); } - - this.tooltipElementsMap.clear(); } - cleanupOrphanedTippyElements() { - const tippyElements = document.querySelectorAll('[data-tippy-root]'); - tippyElements.forEach(element => { - if (element.parentNode) { - element.parentNode.removeChild(element); + 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() { + 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; + + 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; + } catch (error) { + console.error('Error cleaning up orphaned tooltips:', error); + return 0; + } + } + + setupMutationObserver() { + try { + const 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(); + } + }); + + mutationObserver.observe(document.body, { + childList: true, + subtree: true + }); + + 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() { + try { + this.resources.disconnectedCheckInterval = CleanupManager.setInterval(() => { + this.checkForDisconnectedElements(); + }, 60000); + } catch (error) { + console.error('Error starting disconnected elements check:', error); + } + } + + checkForDisconnectedElements() { + try { + 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(); + } + } catch (error) { + console.error('Error checking for disconnected elements:', error); + } + } + + startPeriodicCleanup() { + try { + this.resources.cleanupInterval = CleanupManager.setInterval(() => { + this.performPeriodicCleanup(); + }, 120000); + } catch (error) { + console.error('Error starting periodic cleanup:', error); + } + } + + performPeriodicCleanup(force = false) { + 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); - setupCleanupEvents() { - this.boundCleanup = this.cleanup.bind(this); - this.handleVisibilityChange = () => { - if (document.hidden) { - this.cleanup(); - - if (window.MemoryManager) { - window.MemoryManager.forceCleanup(); + 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', ` + + `); - 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; - } + const styleElement = document.getElementById('tooltip-styles'); + if (styleElement) { + this.resources.tooltipStyles = CleanupManager.registerResource( + 'tooltipStyles', + styleElement, + (elem) => { + if (elem && elem.parentNode) { + elem.parentNode.removeChild(elem); } } - }); + ); } - }); - - if (needsCleanup) { - this.cleanupOrphanedTippyElements(); + } catch (e) { + console.error('Failed to add tooltip styles:', e); } - }); - - 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'); - const tooltipContent = document.getElementById(targetId); + try { + document.querySelectorAll(selector).forEach(element => { + const targetId = element.getAttribute('data-tooltip-target'); + if (!targetId) return; - if (tooltipContent) { - this.create(element, tooltipContent.innerHTML, { - placement: element.getAttribute('data-tooltip-placement') || 'top' - }); - } - }); + const tooltipContent = document.getElementById(targetId); + + if (tooltipContent) { + this.create(element, tooltipContent.innerHTML, { + placement: element.getAttribute('data-tooltip-placement') || 'top' + }); + } + }); + } catch (error) { + console.error('Error initializing tooltips:', error); + } } dispose() { - this.cleanup(); + this.log('Disposing TooltipManager'); - this.pendingAnimationFrames.forEach(id => { - cancelAnimationFrame(id); - }); - this.pendingAnimationFrames.clear(); + try { + this.cleanup(); - if (this.mutationObserver) { - this.mutationObserver.disconnect(); - this.mutationObserver = null; + Object.values(this.resources).forEach(resourceId => { + if (resourceId) { + CleanupManager.unregisterResource(resourceId); + } + }); + + this.resources = {}; + + instance = null; + return true; + } catch (error) { + console.error('Error disposing TooltipManager:', error); + return false; } + } - 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; + setDebugMode(enabled) { + this.debug = Boolean(enabled); + return this.debug; } initialize(options = {}) { + try { + if (options.maxTooltips) { + this.maxTooltips = options.maxTooltips; + } - if (options.maxTooltips) { - this.maxTooltips = options.maxTooltips; + if (options.debug !== undefined) { + this.setDebugMode(options.debug); + } + + this.setupStyles(); + this.setupMutationObserver(); + this.startPeriodicCleanup(); + this.startDisconnectedElementsCheck(); + + this.log('TooltipManager initialized'); + return this; + } catch (error) { + console.error('Error initializing TooltipManager:', error); + return this; } - - console.log('TooltipManager initialized'); - return this; } } @@ -538,7 +800,7 @@ const TooltipManager = (function() { getInstance: function() { if (!instance) { - const manager = new TooltipManagerImpl(); + this.initialize(); } return instance; }, @@ -558,10 +820,25 @@ 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); }, + + setDebugMode: function(enabled) { + const manager = this.getInstance(); + return manager.setDebugMode(enabled); + }, + + getActiveTooltipInstances: function() { + const manager = this.getInstance(); + return manager.getActiveTooltipInstances(); + }, dispose: function(...args) { const manager = this.getInstance(); @@ -570,19 +847,53 @@ const TooltipManager = (function() { }; })(); -window.TooltipManager = TooltipManager; - -document.addEventListener('DOMContentLoaded', function() { - if (!window.tooltipManagerInitialized) { - TooltipManager.initialize(); - TooltipManager.initializeTooltips(); - window.tooltipManagerInitialized = true; - } -}); - if (typeof module !== 'undefined' && module.exports) { module.exports = TooltipManager; } -//console.log('TooltipManager initialized with methods:', Object.keys(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 0993312..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); @@ -2343,37 +2354,39 @@ 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); + } + }); - const filterForm = document.getElementById('filterForm'); - if (filterForm) { - CleanupManager.removeListenersByElement(filterForm); - - filterForm.querySelectorAll('select').forEach(select => { - CleanupManager.removeListenersByElement(select); + if (window.CleanupManager) { + window.CleanupManager.removeListenersByElement(row); + } }); + offersBody.innerHTML = ''; } - 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; - console.log('Offers.js cleanup completed'); + if (window.TooltipManager) { + window.TooltipManager.cleanup(); + } + + if (window.MemoryManager) { + if (window.MemoryManager.forceCleanup) { + 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..4c0906c 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'); @@ -554,7 +578,7 @@ const chartModule = { this.chartRefs.set(element, chart); }, - destroyChart: function() { +destroyChart: function() { if (chartModule.chart) { try { const chartInstance = chartModule.chart; @@ -568,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(); @@ -1348,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...'); @@ -1382,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(); @@ -1449,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}`); @@ -1489,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); @@ -1520,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'}`); } }, @@ -1746,7 +1775,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(() => { 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..c88d732 --- /dev/null +++ b/basicswap/static/js/ui/bids-tab-navigation.js @@ -0,0 +1,214 @@ +(function() { + 'use strict'; + const originalOnload = window.onload; + + 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() { + 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') { + navigateToTabDirectly(targetTabId); + } else { + localStorage.setItem('bidsTabToActivate', targetTabId.replace('#', '')); + window.location.href = '/bids'; + } + } + }); + }); + + window.bidsTabNavigationInitialized = true; + console.log('Bids tab navigation initialized'); + } + + function handleInitialNavigation() { + if (window.location.pathname !== '/bids') { + return; + } + + const tabToActivate = localStorage.getItem('bidsTabToActivate'); + + 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 { + //console.log('Activating default tab: #all'); + activateTabWithRetry('#all'); + } + } + + function handleHashChange() { + if (window.location.pathname !== '/bids') { + return; + } + + const hash = window.location.hash; + if (hash) { + //console.log('Hash changed, activating tab:', hash); + activateTabWithRetry(hash); + } else { + //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); + } + + window.navigateToBidsTab = function(tabId) { + if (window.location.pathname === '/bids') { + 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 @@