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.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.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 @@
-
-
+ -
+
Sent Bids ({{ sent_bids_count }})
@@ -167,7 +172,106 @@
-
+
+
+
+
+
+
+
+
+
+
+ |
+
+ Date/Time
+
+ |
+
+
+ Details
+
+ |
+
+
+ You Send
+
+ |
+
+
+ You Receive
+
+ |
+
+
+ Status
+
+ |
+
+
+ Actions
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+ Connecting...
+
+
+ All Bids: 0
+
+ {% if debug_ui_mode == true %}
+
+
+ Refresh
+
+ {% endif %}
+
+
+
+ Export CSV
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -208,7 +312,7 @@
-
+
@@ -264,6 +368,7 @@
+
@@ -305,7 +410,7 @@
-
+
diff --git a/basicswap/templates/header.html b/basicswap/templates/header.html
index 5b811c5..0f84a8d 100644
--- a/basicswap/templates/header.html
+++ b/basicswap/templates/header.html
@@ -63,6 +63,7 @@
+
@@ -84,6 +85,7 @@
+