Merge pull request #307 from gerlofvanek/fixes-17

Fix: Price Tiles / (JS) Better memory cleanup/manager / Improved header / ALL tab/table on bids page + Various fixes.
This commit is contained in:
tecnovert
2025-05-10 19:41:53 +00:00
committed by GitHub
13 changed files with 2432 additions and 920 deletions

View File

@@ -1,15 +1,19 @@
const PAGE_SIZE = 50; const PAGE_SIZE = 50;
let activeFetchController = null;
const state = { const state = {
currentPage: { currentPage: {
all: 1,
sent: 1, sent: 1,
received: 1 received: 1
}, },
isLoading: false, isLoading: false,
isRefreshing: false, isRefreshing: false,
currentTab: 'sent', currentTab: 'all',
wsConnected: false, wsConnected: false,
refreshPromise: null, refreshPromise: null,
data: { data: {
all: [],
sent: [], sent: [],
received: [] 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 = { const STATE_MAP = {
1: ['Sent'], 1: ['Sent'],
2: ['Receiving'], 2: ['Receiving'],
@@ -61,33 +75,43 @@ const STATE_MAP = {
}; };
const elements = { const elements = {
sentBidsBody: document.querySelector('#sent tbody'), allBidsBody: document.querySelector('#all-tbody'),
receivedBidsBody: document.querySelector('#received tbody'), sentBidsBody: document.querySelector('#sent-tbody'),
receivedBidsBody: document.querySelector('#received-tbody'),
filterForm: document.querySelector('form'), filterForm: document.querySelector('form'),
stateSelect: document.querySelector('select[name="state"]'), stateSelect: document.querySelector('select[name="state"]'),
sortBySelect: document.querySelector('select[name="sort_by"]'), sortBySelect: document.querySelector('select[name="sort_by"]'),
sortDirSelect: document.querySelector('select[name="sort_dir"]'), sortDirSelect: document.querySelector('select[name="sort_dir"]'),
withExpiredSelect: document.querySelector('select[name="with_expired"]'), withExpiredSelect: document.querySelector('select[name="with_expired"]'),
tabButtons: document.querySelectorAll('#myTab button'), tabButtons: document.querySelectorAll('#myTab button'),
allContent: document.getElementById('all'),
sentContent: document.getElementById('sent'), sentContent: document.getElementById('sent'),
receivedContent: document.getElementById('received'), receivedContent: document.getElementById('received'),
allPaginationControls: document.getElementById('pagination-controls-all'),
sentPaginationControls: document.getElementById('pagination-controls-sent'), sentPaginationControls: document.getElementById('pagination-controls-sent'),
receivedPaginationControls: document.getElementById('pagination-controls-received'), receivedPaginationControls: document.getElementById('pagination-controls-received'),
prevPageAll: document.getElementById('prevPageAll'),
prevPageSent: document.getElementById('prevPageSent'), prevPageSent: document.getElementById('prevPageSent'),
nextPageSent: document.getElementById('nextPageSent'),
prevPageReceived: document.getElementById('prevPageReceived'), prevPageReceived: document.getElementById('prevPageReceived'),
nextPageAll: document.getElementById('nextPageAll'),
nextPageSent: document.getElementById('nextPageSent'),
nextPageReceived: document.getElementById('nextPageReceived'), nextPageReceived: document.getElementById('nextPageReceived'),
currentPageAll: document.getElementById('currentPageAll'),
currentPageSent: document.getElementById('currentPageSent'), currentPageSent: document.getElementById('currentPageSent'),
currentPageReceived: document.getElementById('currentPageReceived'), currentPageReceived: document.getElementById('currentPageReceived'),
allBidsCount: document.getElementById('allBidsCount'),
sentBidsCount: document.getElementById('sentBidsCount'), sentBidsCount: document.getElementById('sentBidsCount'),
receivedBidsCount: document.getElementById('receivedBidsCount'), receivedBidsCount: document.getElementById('receivedBidsCount'),
statusDotAll: document.getElementById('status-dot-all'),
statusTextAll: document.getElementById('status-text-all'),
statusDotSent: document.getElementById('status-dot-sent'), statusDotSent: document.getElementById('status-dot-sent'),
statusTextSent: document.getElementById('status-text-sent'), statusTextSent: document.getElementById('status-text-sent'),
statusDotReceived: document.getElementById('status-dot-received'), statusDotReceived: document.getElementById('status-dot-received'),
statusTextReceived: document.getElementById('status-text-received'), statusTextReceived: document.getElementById('status-text-received'),
refreshAllBids: document.getElementById('refreshAllBids'),
refreshSentBids: document.getElementById('refreshSentBids'), refreshSentBids: document.getElementById('refreshSentBids'),
refreshReceivedBids: document.getElementById('refreshReceivedBids') refreshReceivedBids: document.getElementById('refreshReceivedBids')
}; };
@@ -213,6 +237,7 @@ function cleanup() {
} }
}; };
cleanupTableBody('all-tbody');
cleanupTableBody('sent-tbody'); cleanupTableBody('sent-tbody');
cleanupTableBody('received-tbody'); cleanupTableBody('received-tbody');
@@ -234,11 +259,13 @@ function cleanup() {
clearAllAnimationFrames(); clearAllAnimationFrames();
state.data = { state.data = {
all: [],
sent: [], sent: [],
received: [] received: []
}; };
state.currentPage = { state.currentPage = {
all: 1,
sent: 1, sent: 1,
received: 1 received: 1
}; };
@@ -283,7 +310,7 @@ function cleanup() {
if (window.CleanupManager) CleanupManager.clearAll(); if (window.CleanupManager) CleanupManager.clearAll();
if (window.WebSocketManager) WebSocketManager.disconnect(); if (window.WebSocketManager) WebSocketManager.disconnect();
state.data = { sent: [], received: [] }; state.data = { all: [], sent: [], received: [] };
state.isLoading = false; state.isLoading = false;
Object.keys(elements).forEach(key => { Object.keys(elements).forEach(key => {
@@ -311,7 +338,6 @@ CleanupManager.addListener(document, 'visibilitychange', () => {
window.TooltipManager.cleanup(); window.TooltipManager.cleanup();
} }
// Run memory optimization
if (window.MemoryManager) { if (window.MemoryManager) {
MemoryManager.forceCleanup(); MemoryManager.forceCleanup();
} }
@@ -367,7 +393,7 @@ function cleanupRow(row) {
function optimizeMemoryUsage() { function optimizeMemoryUsage() {
const MAX_BIDS_IN_MEMORY = 500; 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) { 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}`); 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); state.data[type] = state.data[type].slice(0, MAX_BIDS_IN_MEMORY);
@@ -529,7 +555,9 @@ function filterAndSortData(bids) {
const coinName = selectedOption?.textContent.trim(); const coinName = selectedOption?.textContent.trim();
if (coinName) { 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)) { if (!coinMatches(coinToMatch, coinName)) {
return false; return false;
} }
@@ -542,7 +570,10 @@ function filterAndSortData(bids) {
const coinName = selectedOption?.textContent.trim(); const coinName = selectedOption?.textContent.trim();
if (coinName) { 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)) { if (!coinMatches(coinToMatch, coinName)) {
return false; return false;
} }
@@ -583,7 +614,8 @@ function filterAndSortData(bids) {
let matchesDisplayedLabel = false; let matchesDisplayedLabel = false;
if (!matchesLabel && document) { if (!matchesLabel && document) {
try { 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/"]`); const cells = document.querySelectorAll(`#${tableId} a[href^="/identity/"]`);
for (const cell of cells) { for (const cell of cells) {
@@ -681,7 +713,7 @@ function updateCoinFilterImages() {
const updateLoadingState = (isLoading) => { const updateLoadingState = (isLoading) => {
state.isLoading = isLoading; state.isLoading = isLoading;
['Sent', 'Received'].forEach(type => { ['All', 'Sent', 'Received'].forEach(type => {
const refreshButton = elements[`refresh${type}Bids`]; const refreshButton = elements[`refresh${type}Bids`];
const refreshText = refreshButton?.querySelector(`#refresh${type}Text`); const refreshText = refreshButton?.querySelector(`#refresh${type}Text`);
const refreshIcon = refreshButton?.querySelector('svg'); const refreshIcon = refreshButton?.querySelector('svg');
@@ -732,7 +764,7 @@ const updateConnectionStatus = (status) => {
const config = statusConfig[status] || statusConfig.connected; 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 dot = elements[`statusDot${type.charAt(0).toUpperCase() + type.slice(1)}`];
const text = elements[`statusText${type.charAt(0).toUpperCase() + type.slice(1)}`]; const text = elements[`statusText${type.charAt(0).toUpperCase() + type.slice(1)}`];
@@ -770,10 +802,12 @@ const processIdentityStats = (identity) => {
const createIdentityTooltipContent = (identity) => { const createIdentityTooltipContent = (identity) => {
if (!identity) return ''; if (!identity) return '';
const address = identity.address || '';
let statsSection = '';
try {
const stats = processIdentityStats(identity); const stats = processIdentityStats(identity);
if (!stats) return ''; if (stats) {
const getSuccessRateColor = (rate) => { const getSuccessRateColor = (rate) => {
const numRate = parseFloat(rate); const numRate = parseFloat(rate);
if (numRate >= 80) return 'text-green-600'; if (numRate >= 80) return 'text-green-600';
@@ -781,29 +815,7 @@ const createIdentityTooltipContent = (identity) => {
return 'text-red-600'; return 'text-red-600';
}; };
return ` statsSection = `
<div class="identity-info space-y-2">
${identity.label ? `
<div class="border-b border-gray-400 pb-2">
<div class="text-white text-xs tracking-wide font-semibold">Label:</div>
<div class="text-white">${identity.label}</div>
</div>
` : ''}
<div class="space-y-1">
<div class="text-white text-xs tracking-wide font-semibold">Bid From Address:</div>
<div class="monospace text-xs break-all bg-gray-500 p-2 rounded-md text-white">
${identity.address || ''}
</div>
</div>
${identity.note ? `
<div class="space-y-1">
<div class="text-white text-xs tracking-wide font-semibold">Note:</div>
<div class="text-white text-sm italic">${identity.note}</div>
</div>
` : ''}
<div class="pt-2 mt-2"> <div class="pt-2 mt-2">
<div class="text-white text-xs tracking-wide font-semibold mb-2">Swap History:</div> <div class="text-white text-xs tracking-wide font-semibold mb-2">Swap History:</div>
<div class="grid grid-cols-2 gap-2"> <div class="grid grid-cols-2 gap-2">
@@ -839,6 +851,40 @@ const createIdentityTooltipContent = (identity) => {
</div> </div>
</div> </div>
</div> </div>
`;
}
} catch (e) {
console.warn('Error processing identity stats:', e);
}
const addressSection = `
<div class="space-y-1">
<div class="text-white text-xs tracking-wide font-semibold">Bid From Address:</div>
<div class="monospace text-xs break-all bg-gray-500 p-2 rounded-md text-white">
${address || identity.address || ''}
</div>
</div>
`;
return `
<div class="identity-info space-y-2">
${identity.label ? `
<div class="border-b border-gray-400 pb-2">
<div class="text-white text-xs tracking-wide font-semibold">Label:</div>
<div class="text-white">${identity.label}</div>
</div>
` : ''}
${addressSection}
${identity.note ? `
<div class="space-y-1">
<div class="text-white text-xs tracking-wide font-semibold">Note:</div>
<div class="text-white text-sm italic">${identity.note}</div>
</div>
` : ''}
${statsSection}
</div> </div>
`; `;
}; };
@@ -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 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()}`; const uniqueId = `${bid.bid_id}_${Date.now()}`;
tooltipIdsToCleanup.add(`tooltip-identity-${uniqueId}`); tooltipIdsToCleanup.add(`tooltip-identity-${uniqueId}`);
tooltipIdsToCleanup.add(`tooltip-status-${uniqueId}`); tooltipIdsToCleanup.add(`tooltip-status-${uniqueId}`);
const timeColor = getTimeStrokeColor(bid.expire_at); const timeColor = getTimeStrokeColor(bid.expire_at);
const currentTabIsAll = state.currentTab === 'all';
const isSent = currentTabIsAll ? (bid.source === 'sent') : (state.currentTab === 'sent');
const sourceIndicator = currentTabIsAll ?
`<span class="ml-1 px-1.5 py-0.5 text-xs font-medium ${isSent ? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300' : 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300'} rounded">
${isSent ? 'Sent' : 'Received'}
</span>` : '';
let tooltipContent = '';
try {
tooltipContent = createIdentityTooltipContent(identity);
} catch (e) {
console.warn('Error creating tooltip content:', e);
}
if (!tooltipContent) {
tooltipContent = `
<div class="identity-info space-y-2">
<div class="space-y-1">
<div class="text-white text-xs tracking-wide font-semibold">Bid From Address:</div>
<div class="monospace text-xs break-all bg-gray-500 p-2 rounded-md text-white">
${rawAddress}
</div>
</div>
</div>
`;
}
return ` return `
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600"> <tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
@@ -973,7 +1124,10 @@ const createTableRow = async (bid) => {
<polyline points="12,6 12,12 18,12"></polyline> <polyline points="12,6 12,12 18,12"></polyline>
</g> </g>
</svg> </svg>
<div class="text-xs">${formatTime(bid.created_at)}</div> <div class="text-xs flex items-center">
${formatTime(bid.created_at)}
${sourceIndicator}
</div>
</div> </div>
</td> </td>
@@ -982,11 +1136,11 @@ const createTableRow = async (bid) => {
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex items-center min-w-max"> <div class="flex items-center min-w-max">
<div class="relative" data-tooltip-target="tooltip-identity-${uniqueId}"> <div class="relative" data-tooltip-target="tooltip-identity-${uniqueId}">
<a href="/identity/${bid.addr_from}" class="text-xs font-mono"> <a href="/identity/${rawAddress}" class="text-xs font-mono">
<span> <span>
${state.currentTab === 'sent' ? 'Out:' : 'In:'} ${isSent ? 'Out:' : 'In:'}
</span> </span>
${identity?.label || formatAddressSMSG(bid.addr_from)} ${identity?.label || formatAddressSMSG(rawAddress)}
</a> </a>
</div> </div>
</div> </div>
@@ -1002,12 +1156,12 @@ const createTableRow = async (bid) => {
<td class="p-3"> <td class="p-3">
<div class="flex items-center min-w-max"> <div class="flex items-center min-w-max">
<img class="w-8 h-8 mr-2" <img class="w-8 h-8 mr-2"
src="/static/images/coins/${state.currentTab === 'sent' ? bid.coin_to.replace(' ', '-') : bid.coin_from.replace(' ', '-')}.png" src="/static/images/coins/${isSent ? bid.coin_to.replace(' ', '-') : bid.coin_from.replace(' ', '-')}.png"
alt="${state.currentTab === 'sent' ? bid.coin_to : bid.coin_from}" alt="${isSent ? bid.coin_to : bid.coin_from}"
onerror="this.src='/static/images/coins/default.png'"> onerror="this.src='/static/images/coins/default.png'">
<div> <div>
<div class="text-sm font-medium monospace">${state.currentTab === 'sent' ? bid.amount_to : bid.amount_from}</div> <div class="text-sm font-medium monospace">${isSent ? bid.amount_to : bid.amount_from}</div>
<div class="text-xs opacity-75 monospace">${state.currentTab === 'sent' ? bid.coin_to : bid.coin_from}</div> <div class="text-xs opacity-75 monospace">${isSent ? bid.coin_to : bid.coin_from}</div>
</div> </div>
</div> </div>
</td> </td>
@@ -1016,12 +1170,12 @@ const createTableRow = async (bid) => {
<td class="p-3"> <td class="p-3">
<div class="flex items-center min-w-max"> <div class="flex items-center min-w-max">
<img class="w-8 h-8 mr-2" <img class="w-8 h-8 mr-2"
src="/static/images/coins/${state.currentTab === 'sent' ? bid.coin_from.replace(' ', '-') : bid.coin_to.replace(' ', '-')}.png" src="/static/images/coins/${isSent ? bid.coin_from.replace(' ', '-') : bid.coin_to.replace(' ', '-')}.png"
alt="${state.currentTab === 'sent' ? bid.coin_from : bid.coin_to}" alt="${isSent ? bid.coin_from : bid.coin_to}"
onerror="this.src='/static/images/coins/default.png'"> onerror="this.src='/static/images/coins/default.png'">
<div> <div>
<div class="text-sm font-medium monospace">${state.currentTab === 'sent' ? bid.amount_from : bid.amount_to}</div> <div class="text-sm font-medium monospace">${isSent ? bid.amount_from : bid.amount_to}</div>
<div class="text-xs opacity-75 monospace">${state.currentTab === 'sent' ? bid.coin_from : bid.coin_to}</div> <div class="text-xs opacity-75 monospace">${isSent ? bid.coin_from : bid.coin_to}</div>
</div> </div>
</div> </div>
</td> </td>
@@ -1046,10 +1200,9 @@ const createTableRow = async (bid) => {
</td> </td>
</tr> </tr>
<!-- Tooltips --> <!-- Tooltips -->
<div id="tooltip-identity-${uniqueId}" role="tooltip" class="fixed z-50 py-3 px-4 text-sm font-medium text-white bg-gray-400 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip dark:bg-gray-600 max-w-sm pointer-events-none"> <div id="tooltip-identity-${uniqueId}" role="tooltip" class="fixed z-50 py-3 px-4 text-sm font-medium text-white bg-gray-400 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip dark:bg-gray-600 max-w-sm pointer-events-none">
${createIdentityTooltipContent(identity)} ${tooltipContent}
<div class="tooltip-arrow" data-popper-arrow></div> <div class="tooltip-arrow" data-popper-arrow></div>
</div> </div>
@@ -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 updateTableContent = async (type) => {
const tbody = elements[`${type}BidsBody`]; const tbody = elements[`${type}BidsBody`];
if (!tbody) return; 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 () => { const updateBidsTable = async () => {
if (state.isLoading) { if (state.isLoading) {
return; return;
@@ -1370,7 +1508,13 @@ const updateBidsTable = async () => {
state.isLoading = true; state.isLoading = true;
updateLoadingState(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 // Add identity preloading if we're searching
if (state.filters.searchQuery && state.filters.searchQuery.length > 0) { if (state.filters.searchQuery && state.filters.searchQuery.length > 0) {
@@ -1417,7 +1561,11 @@ const updatePaginationControls = (type) => {
} }
if (currentPageSpan) { if (currentPageSpan) {
currentPageSpan.textContent = totalPages > 0 ? state.currentPage[type] : 0; if (totalPages > 0) {
currentPageSpan.innerHTML = `<span>${state.currentPage[type]}</span> of <span>${totalPages}</span>`;
} else {
currentPageSpan.textContent = "0";
}
} }
if (prevButton) { if (prevButton) {
@@ -1582,7 +1730,7 @@ function setupFilterEventListeners() {
} }
const setupRefreshButtons = () => { const setupRefreshButtons = () => {
['Sent', 'Received'].forEach(type => { ['All', 'Sent', 'Received'].forEach(type => {
const refreshButton = elements[`refresh${type}Bids`]; const refreshButton = elements[`refresh${type}Bids`];
if (refreshButton) { if (refreshButton) {
EventManager.add(refreshButton, 'click', async () => { EventManager.add(refreshButton, 'click', async () => {
@@ -1598,7 +1746,10 @@ const setupRefreshButtons = () => {
state.isLoading = true; state.isLoading = true;
updateLoadingState(true); updateLoadingState(true);
const response = await fetch(state.currentTab === 'sent' ? '/json/sentbids' : '/json/bids', { if (lowerType === 'all') {
state.data.all = await fetchAllBids();
} else {
const response = await fetch(lowerType === 'sent' ? '/json/sentbids' : '/json/bids', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@@ -1622,6 +1773,8 @@ const setupRefreshButtons = () => {
} }
state.data[lowerType] = data; state.data[lowerType] = data;
}
await updateTableContent(lowerType); await updateTableContent(lowerType);
updatePaginationControls(lowerType); updatePaginationControls(lowerType);
@@ -1649,8 +1802,10 @@ const switchTab = (tabId) => {
tooltipIdsToCleanup.clear(); 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.sentContent.classList.add('hidden');
elements.receivedContent.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'); tab.classList.add('hover:text-gray-600', 'hover:bg-gray-50', 'dark:hover:bg-gray-500');
} }
}); });
setTimeout(() => { setTimeout(() => {
updateBidsTable(); updateBidsTable();
}, 10); }, 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 setupEventListeners = () => {
const filterControls = document.querySelector('.flex.flex-wrap.justify-center'); const filterControls = document.querySelector('.flex.flex-wrap.justify-center');
if (filterControls) { if (filterControls) {
@@ -1709,10 +1884,12 @@ const setupEventListeners = () => {
} }
}); });
elements.allContent.classList.toggle('hidden', targetId !== '#all');
elements.sentContent.classList.toggle('hidden', targetId !== '#sent'); elements.sentContent.classList.toggle('hidden', targetId !== '#sent');
elements.receivedContent.classList.toggle('hidden', targetId !== '#received'); 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; state.currentPage[state.currentTab] = 1;
if (window.TooltipManager) { if (window.TooltipManager) {
@@ -1725,7 +1902,7 @@ const setupEventListeners = () => {
}); });
} }
['Sent', 'Received'].forEach(type => { ['All', 'Sent', 'Received'].forEach(type => {
const lowerType = type.toLowerCase(); const lowerType = type.toLowerCase();
if (elements[`prevPage${type}`]) { if (elements[`prevPage${type}`]) {
@@ -1864,6 +2041,11 @@ function setupMemoryMonitoring() {
window.TooltipManager.cleanup(); 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) { if (state.data.sent.length > 1000) {
console.log('Trimming sent bids data'); console.log('Trimming sent bids data');
state.data.sent = state.data.sent.slice(0, 1000); state.data.sent = state.data.sent.slice(0, 1000);
@@ -1883,7 +2065,6 @@ function setupMemoryMonitoring() {
}, { once: true }); }, { once: true });
} }
// Init
function initialize() { function initialize() {
const filterElements = { const filterElements = {
stateSelect: document.getElementById('state'), stateSelect: document.getElementById('state'),
@@ -1901,8 +2082,6 @@ function initialize() {
if (filterElements.coinFrom) filterElements.coinFrom.value = 'any'; if (filterElements.coinFrom) filterElements.coinFrom.value = 'any';
if (filterElements.coinTo) filterElements.coinTo.value = 'any'; if (filterElements.coinTo) filterElements.coinTo.value = 'any';
setupMemoryMonitoring();
setTimeout(() => { setTimeout(() => {
WebSocketManager.initialize(); WebSocketManager.initialize();
setupEventListeners(); setupEventListeners();
@@ -1916,16 +2095,12 @@ function initialize() {
setTimeout(() => { setTimeout(() => {
updateClearFiltersButton(); updateClearFiltersButton();
state.currentTab = 'sent'; state.currentTab = 'all';
state.filters.state = -1; state.filters.state = -1;
updateBidsTable(); updateBidsTable();
}, 100); }, 100);
setInterval(() => { setupMemoryMonitoring();
if ((state.data.sent.length + state.data.received.length) > 1000) {
optimizeMemoryUsage();
}
}, 5 * 60 * 1000); // Check every 5 minutes
window.cleanupBidsTable = cleanup; window.cleanupBidsTable = cleanup;
} }

View File

@@ -4,17 +4,18 @@ const BidExporter = {
return 'No data to export'; return 'No data to export';
} }
const isSent = type === 'sent'; const isAllTab = type === 'all';
const headers = [ const headers = [
'Date/Time', 'Date/Time',
'Bid ID', 'Bid ID',
'Offer ID', 'Offer ID',
'From Address', 'From Address',
isSent ? 'You Send Amount' : 'You Receive Amount', ...(isAllTab ? ['Type'] : []),
isSent ? 'You Send Coin' : 'You Receive Coin', 'You Send Amount',
isSent ? 'You Receive Amount' : 'You Send Amount', 'You Send Coin',
isSent ? 'You Receive Coin' : 'You Send Coin', 'You Receive Amount',
'You Receive Coin',
'Status', 'Status',
'Created At', 'Created At',
'Expires At' 'Expires At'
@@ -23,11 +24,13 @@ const BidExporter = {
let csvContent = headers.join(',') + '\n'; let csvContent = headers.join(',') + '\n';
bids.forEach(bid => { bids.forEach(bid => {
const isSent = isAllTab ? (bid.source === 'sent') : (type === 'sent');
const row = [ const row = [
`"${formatTime(bid.created_at)}"`, `"${formatTime(bid.created_at)}"`,
`"${bid.bid_id}"`, `"${bid.bid_id}"`,
`"${bid.offer_id}"`, `"${bid.offer_id}"`,
`"${bid.addr_from}"`, `"${bid.addr_from}"`,
...(isAllTab ? [`"${bid.source}"`] : []),
isSent ? bid.amount_from : bid.amount_to, isSent ? bid.amount_from : bid.amount_to,
`"${isSent ? bid.coin_from : bid.coin_to}"`, `"${isSent ? bid.coin_from : bid.coin_to}"`,
isSent ? bid.amount_to : bid.amount_from, isSent ? bid.amount_to : bid.amount_from,
@@ -103,6 +106,15 @@ const BidExporter = {
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
setTimeout(function() { setTimeout(function() {
if (typeof state !== 'undefined' && typeof EventManager !== 'undefined') { 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'); const exportSentButton = document.getElementById('exportSentBids');
if (exportSentButton) { if (exportSentButton) {
EventManager.add(exportSentButton, 'click', (e) => { EventManager.add(exportSentButton, 'click', (e) => {
@@ -128,9 +140,14 @@ const originalCleanup = window.cleanup || function(){};
window.cleanup = function() { window.cleanup = function() {
originalCleanup(); originalCleanup();
const exportAllButton = document.getElementById('exportAllBids');
const exportSentButton = document.getElementById('exportSentBids'); const exportSentButton = document.getElementById('exportSentBids');
const exportReceivedButton = document.getElementById('exportReceivedBids'); const exportReceivedButton = document.getElementById('exportReceivedBids');
if (exportAllButton && typeof EventManager !== 'undefined') {
EventManager.remove(exportAllButton, 'click');
}
if (exportSentButton && typeof EventManager !== 'undefined') { if (exportSentButton && typeof EventManager !== 'undefined') {
EventManager.remove(exportSentButton, 'click'); EventManager.remove(exportSentButton, 'click');
} }

View File

@@ -260,30 +260,80 @@ const ApiManager = (function() {
fetchVolumeData: async function() { fetchVolumeData: async function() {
return this.rateLimiter.queueRequest('coingecko', async () => { return this.rateLimiter.queueRequest('coingecko', async () => {
try { try {
const coins = (window.config && window.config.coins) ? let coinList = (window.config && window.config.coins) ?
window.config.coins window.config.coins
.filter(coin => coin.usesCoinGecko) .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(',') : .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, { const response = await this.makePostRequest(url, {
'User-Agent': 'Mozilla/5.0', 'User-Agent': 'Mozilla/5.0',
'Accept': 'application/json' 'Accept': 'application/json'
}); });
if (!response || typeof response !== 'object') {
throw new Error('Invalid response from CoinGecko API');
}
const volumeData = {}; const volumeData = {};
Object.entries(response).forEach(([coinId, data]) => { Object.entries(response).forEach(([coinId, data]) => {
if (data && data.usd_24h_vol) { if (data && data.usd_24h_vol !== undefined) {
volumeData[coinId] = { volumeData[coinId] = {
total_volume: data.usd_24h_vol, total_volume: data.usd_24h_vol || 0,
price_change_percentage_24h: data.usd_24h_change || 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; return volumeData;
} catch (error) { } catch (error) {
console.error("Error fetching volume data:", error); console.error("Error fetching volume data:", error);
@@ -364,7 +414,6 @@ const ApiManager = (function() {
}, },
dispose: function() { dispose: function() {
// Clear any pending requests or resources
rateLimiter.requestQueue = {}; rateLimiter.requestQueue = {};
rateLimiter.lastRequestTime = {}; rateLimiter.lastRequestTime = {};
state.isInitialized = false; state.isInitialized = false;

View File

@@ -328,7 +328,7 @@ const CacheManager = (function() {
.filter(key => isCacheKey(key)) .filter(key => isCacheKey(key))
.forEach(key => memoryCache.delete(key)); .forEach(key => memoryCache.delete(key));
console.log("Cache cleared successfully"); //console.log("Cache cleared successfully");
return true; return true;
}, },

View File

@@ -1,12 +1,12 @@
const CleanupManager = (function() { const CleanupManager = (function() {
const state = { const state = {
eventListeners: [], eventListeners: [],
timeouts: [], timeouts: [],
intervals: [], intervals: [],
animationFrames: [], animationFrames: [],
resources: new Map(), resources: new Map(),
debug: false debug: false,
memoryOptimizationInterval: null
}; };
function log(message, ...args) { 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) { setDebugMode: function(enabled) {
state.debug = Boolean(enabled); state.debug = Boolean(enabled);
log(`Debug mode ${state.debug ? 'enabled' : 'disabled'}`); log(`Debug mode ${state.debug ? 'enabled' : 'disabled'}`);
@@ -247,6 +470,17 @@ const CleanupManager = (function() {
if (options.debug !== undefined) { if (options.debug !== undefined) {
this.setDebugMode(options.debug); 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'); log('CleanupManager initialized');
return this; return this;
} }
@@ -255,16 +489,20 @@ const CleanupManager = (function() {
return publicAPI; return publicAPI;
})(); })();
if (typeof module !== 'undefined' && module.exports) {
module.exports = CleanupManager;
}
window.CleanupManager = CleanupManager; if (typeof window !== 'undefined') {
window.CleanupManager = CleanupManager;
}
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
document.addEventListener('DOMContentLoaded', function() { if (document.readyState === 'complete' || document.readyState === 'interactive') {
if (!window.cleanupManagerInitialized) { CleanupManager.initialize({ debug: false });
CleanupManager.initialize(); } else {
window.cleanupManagerInitialized = true; document.addEventListener('DOMContentLoaded', () => {
CleanupManager.initialize({ debug: false });
}, { once: true });
} }
}); }
//console.log('CleanupManager initialized with methods:', Object.keys(CleanupManager));
console.log('CleanupManager initialized');

View File

@@ -1,15 +1,47 @@
const MemoryManager = (function() { const MemoryManager = (function() {
const config = {
const state = { tooltipCleanupInterval: 300000,
isMonitoringEnabled: false, diagnosticsInterval: 600000,
monitorInterval: null, elementVerificationInterval: 300000,
cleanupInterval: null 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 config = { const state = {
monitorInterval: 30000, pendingAnimationFrames: new Set(),
cleanupInterval: 60000, pendingTimeouts: new Set(),
debug: false 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: {}
}; };
function log(message, ...args) { function log(message, ...args) {
@@ -18,202 +50,533 @@ const MemoryManager = (function() {
} }
} }
const publicAPI = { function preserveTooltipFunctions() {
enableMonitoring: function(interval = config.monitorInterval) { if (window.TooltipManager && !state.originalTooltipFunctions.destroy) {
if (state.monitorInterval) { state.originalTooltipFunctions = {
clearInterval(state.monitorInterval); destroy: window.TooltipManager.destroy,
cleanup: window.TooltipManager.cleanup,
create: window.TooltipManager.create
};
}
} }
state.isMonitoringEnabled = true; function isInProtectedContainer(element) {
config.monitorInterval = interval; if (!element) return false;
this.logMemoryUsage(); for (const selector of config.protectedContainers) {
if (element.closest && element.closest(selector)) {
state.monitorInterval = setInterval(() => {
this.logMemoryUsage();
}, interval);
console.log(`Memory monitoring enabled - reporting every ${interval/1000} seconds`);
return true; return true;
}, }
disableMonitoring: function() {
if (state.monitorInterval) {
clearInterval(state.monitorInterval);
state.monitorInterval = null;
} }
state.isMonitoringEnabled = false; return false;
console.log('Memory monitoring disabled'); }
return true;
},
logMemoryUsage: function() { function shouldSkipCleanup() {
const timestamp = new Date().toLocaleTimeString(); if (state.isCleanupRunning) return true;
console.log(`=== Memory Monitor [${timestamp}] ===`);
const selector = config.interactiveSelectors.join(', ');
const hoveredElements = document.querySelectorAll(selector);
return hoveredElements.length > 0;
}
function performCleanup(force = false) {
if (shouldSkipCleanup() && !force) {
return false;
}
if (state.isCleanupRunning) {
return false;
}
const now = Date.now();
if (!force && now - state.lastCleanupTime < config.minTimeBetweenCleanups) {
return false;
}
try {
state.isCleanupRunning = true;
state.lastCleanupTime = now;
state.metrics.cleanupRuns++;
const startTime = performance.now();
const startMemory = checkMemoryUsage();
state.pendingAnimationFrames.forEach(id => {
cancelAnimationFrame(id);
});
state.pendingAnimationFrames.clear();
state.pendingTimeouts.forEach(id => {
clearTimeout(id);
});
state.pendingTimeouts.clear();
const tooltipsResult = removeOrphanedTooltips();
state.metrics.tooltipsRemoved += tooltipsResult;
const disconnectedResult = checkForDisconnectedElements();
tryRunGarbageCollection(false);
const endTime = performance.now();
const endMemory = checkMemoryUsage();
const runStats = {
timestamp: new Date().toISOString(),
duration: endTime - startTime,
tooltipsRemoved: tooltipsResult,
disconnectedRemoved: disconnectedResult,
memoryBefore: startMemory ? startMemory.usedMB : null,
memoryAfter: endMemory ? endMemory.usedMB : null,
memorySaved: startMemory && endMemory ?
(startMemory.usedMB - endMemory.usedMB).toFixed(2) : null
};
state.metrics.history.unshift(runStats);
if (state.metrics.history.length > 10) {
state.metrics.history.pop();
}
state.metrics.lastCleanupDetails = runStats;
if (config.debug) {
log(`Cleanup completed in ${runStats.duration.toFixed(2)}ms, removed ${tooltipsResult} tooltips`);
}
return true;
} catch (error) {
console.error("Error during cleanup:", error);
return false;
} finally {
state.isCleanupRunning = false;
}
}
function removeOrphanedTooltips() {
try {
const tippyRoots = document.querySelectorAll('[data-tippy-root]:not(:hover)');
let removed = 0;
tippyRoots.forEach(root => {
const tooltipId = root.getAttribute('data-for-tooltip-id');
const trigger = tooltipId ?
document.querySelector(`[data-tooltip-trigger-id="${tooltipId}"]`) : null;
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"
};
if (window.performance && window.performance.memory) { if (window.performance && window.performance.memory) {
console.log('Memory usage:', { result.usedJSHeapSize = window.performance.memory.usedJSHeapSize;
usedJSHeapSize: (window.performance.memory.usedJSHeapSize / 1024 / 1024).toFixed(2) + ' MB', result.totalJSHeapSize = window.performance.memory.totalJSHeapSize;
totalJSHeapSize: (window.performance.memory.totalJSHeapSize / 1024 / 1024).toFixed(2) + ' MB' 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;
} }
if (navigator.deviceMemory) { function enhanceTooltipManager() {
console.log('Device memory:', navigator.deviceMemory, 'GB'); 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;
} }
const nodeCount = document.querySelectorAll('*').length; if (tooltipId) {
console.log('DOM node count:', nodeCount); if (originalDestroy) {
originalDestroy.call(window.TooltipManager, element);
if (window.CleanupManager) {
const counts = CleanupManager.getResourceCounts();
console.log('Managed resources:', counts);
} }
if (window.TooltipManager) { const tooltipRoot = document.querySelector(`[data-for-tooltip-id="${tooltipId}"]`);
const tooltipInstances = document.querySelectorAll('[data-tippy-root]').length; if (tooltipRoot && tooltipRoot.parentNode) {
const tooltipTriggers = document.querySelectorAll('[data-tooltip-trigger-id]').length; tooltipRoot.parentNode.removeChild(tooltipRoot);
console.log('Tooltip instances:', tooltipInstances, '- Tooltip triggers:', tooltipTriggers);
} }
if (window.CacheManager && window.CacheManager.getStats) { element.removeAttribute('data-tooltip-trigger-id');
const cacheStats = CacheManager.getStats(); element.removeAttribute('aria-describedby');
console.log('Cache stats:', cacheStats);
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);
} }
if (window.IdentityManager && window.IdentityManager.getStats) { removeOrphanedTooltips();
const identityStats = window.IdentityManager.getStats(); } catch (error) {
console.log('Identity cache stats:', identityStats); console.error('Error in enhanced tooltip cleanup:', error);
if (originalCleanup) {
originalCleanup.call(window.TooltipManager);
} }
console.log('==============================');
},
enableAutoCleanup: function(interval = config.cleanupInterval) {
if (state.cleanupInterval) {
clearInterval(state.cleanupInterval);
} }
};
config.cleanupInterval = interval; window.TooltipManager._memoryManagerEnhanced = true;
window.TooltipManager._originalDestroy = originalDestroy;
window.TooltipManager._originalCleanup = originalCleanup;
this.forceCleanup();
state.cleanupInterval = setInterval(() => {
this.forceCleanup();
}, interval);
log('Auto-cleanup enabled every', interval/1000, 'seconds');
return true; return true;
}, }
disableAutoCleanup: function() { function initializeScheduledCleanups() {
if (state.cleanupInterval) { if (state.cleanupInterval) {
clearInterval(state.cleanupInterval); clearInterval(state.cleanupInterval);
state.cleanupInterval = null; state.cleanupInterval = null;
} }
console.log('Memory auto-cleanup disabled'); 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; return true;
},
forceCleanup: function() {
if (config.debug) {
console.log('Running memory cleanup...', new Date().toLocaleTimeString());
} }
if (window.CacheManager && CacheManager.cleanup) { function initialize(options = {}) {
CacheManager.cleanup(true); preserveTooltipFunctions();
if (options) {
Object.assign(config, options);
} }
if (window.TooltipManager && TooltipManager.cleanup) { enhanceTooltipManager();
window.TooltipManager.cleanup();
if (window.WebSocketManager && !window.WebSocketManager.cleanupOrphanedSockets) {
window.WebSocketManager.cleanupOrphanedSockets = function() {
return 0;
};
} }
document.querySelectorAll('[data-tooltip-trigger-id]').forEach(element => { const manager = window.ApiManager || window.Api;
if (window.TooltipManager && TooltipManager.destroy) { if (manager && !manager.abortPendingRequests) {
window.TooltipManager.destroy(element); 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
};
} }
}); });
if (window.chartModule && chartModule.cleanup) { return {
chartModule.cleanup(); memory: checkMemoryUsage(),
metrics: { ...state.metrics },
tooltips,
config: { ...config }
};
} }
if (window.gc) { return {
window.gc(); initialize,
} else { cleanup: performCleanup,
const arr = new Array(1000); forceCleanup: function() {
for (let i = 0; i < 1000; i++) { return performCleanup(true);
arr[i] = new Array(10000).join('x');
}
}
if (config.debug) {
console.log('Memory cleanup completed');
}
return true;
}, },
fullCleanup: function() {
return performCleanup(true);
},
getStats: getDetailedStats,
displayStats,
setDebugMode: function(enabled) { setDebugMode: function(enabled) {
config.debug = Boolean(enabled); config.debug = Boolean(enabled);
return `Debug mode ${config.debug ? 'enabled' : 'disabled'}`; return config.debug;
}, },
addProtectedContainer: function(selector) {
getStatus: function() { if (!config.protectedContainers.includes(selector)) {
return { config.protectedContainers.push(selector);
monitoring: { }
enabled: Boolean(state.monitorInterval), return config.protectedContainers;
interval: config.monitorInterval
}, },
autoCleanup: { removeProtectedContainer: function(selector) {
enabled: Boolean(state.cleanupInterval), const index = config.protectedContainers.indexOf(selector);
interval: config.cleanupInterval if (index !== -1) {
config.protectedContainers.splice(index, 1);
}
return config.protectedContainers;
}, },
debug: config.debug dispose
}; };
},
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');
}
};
return publicAPI;
})(); })();
window.MemoryManager = MemoryManager;
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
if (!window.memoryManagerInitialized) { const isDevMode = window.location.hostname === 'localhost' ||
MemoryManager.initialize(); window.location.hostname === '127.0.0.1';
window.memoryManagerInitialized = true;
} MemoryManager.initialize({
debug: isDevMode
});
console.log('Memory Manager initialized');
}); });
//console.log('MemoryManager initialized with methods:', Object.keys(MemoryManager)); window.MemoryManager = MemoryManager;
console.log('MemoryManager initialized');

View File

@@ -59,7 +59,7 @@ const PriceManager = (function() {
return fetchPromise; return fetchPromise;
} }
console.log('PriceManager: Fetching latest prices.'); //console.log('PriceManager: Fetching latest prices.');
lastFetchTime = Date.now(); lastFetchTime = Date.now();
fetchPromise = this.fetchPrices() fetchPromise = this.fetchPrices()
.then(prices => { .then(prices => {
@@ -89,7 +89,7 @@ const PriceManager = (function() {
? window.config.coins.map(c => c.symbol).filter(symbol => symbol && symbol.trim() !== '') ? window.config.coins.map(c => c.symbol).filter(symbol => symbol && symbol.trim() !== '')
: ['BTC', 'XMR', 'PART', 'BCH', 'PIVX', 'FIRO', 'DASH', 'LTC', 'DOGE', 'DCR', 'NMC', 'WOW']); : ['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) { if (!coinSymbols.length) {
throw new Error('No valid coins configured'); throw new Error('No valid coins configured');

File diff suppressed because it is too large Load Diff

View File

@@ -1370,7 +1370,6 @@ function createRecipientTooltip(uniqueId, identityInfo, identity, successRate, t
return 'text-red-600'; return 'text-red-600';
}; };
const truncateText = (text, maxLength) => { const truncateText = (text, maxLength) => {
if (text.length <= maxLength) return text; if (text.length <= maxLength) return text;
return text.substring(0, maxLength) + '...'; return text.substring(0, maxLength) + '...';
@@ -2167,6 +2166,18 @@ document.addEventListener('DOMContentLoaded', async function() {
tableRateModule.init(); 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(); await initializeTableAndData();
if (window.PriceManager) { if (window.PriceManager) {
@@ -2179,7 +2190,7 @@ document.addEventListener('DOMContentLoaded', async function() {
if (window.WebSocketManager) { if (window.WebSocketManager) {
WebSocketManager.addMessageHandler('message', async (data) => { WebSocketManager.addMessageHandler('message', async (data) => {
if (data.event === 'new_offer' || data.event === 'offer_revoked') { if (data.event === 'new_offer' || data.event === 'offer_revoked') {
console.log('WebSocket event received:', data.event); //console.log('WebSocket event received:', data.event);
try { try {
const previousPrices = latestPrices; const previousPrices = latestPrices;
@@ -2230,7 +2241,7 @@ document.addEventListener('DOMContentLoaded', async function() {
updatePaginationInfo(); updatePaginationInfo();
console.log('WebSocket-triggered refresh completed successfully'); //console.log('WebSocket-triggered refresh completed successfully');
} catch (error) { } catch (error) {
console.error('Error during WebSocket-triggered refresh:', error); console.error('Error during WebSocket-triggered refresh:', error);
NetworkManager.handleNetworkError(error); NetworkManager.handleNetworkError(error);
@@ -2343,37 +2354,39 @@ function 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) { if (window.TooltipManager) {
if (typeof window.TooltipManager.cleanup === 'function') { window.TooltipManager.destroy(trigger);
window.TooltipManager.cleanup();
} }
}
const filterForm = document.getElementById('filterForm');
if (filterForm) {
CleanupManager.removeListenersByElement(filterForm);
filterForm.querySelectorAll('select').forEach(select => {
CleanupManager.removeListenersByElement(select);
});
}
const paginationButtons = document.querySelectorAll('#prevPage, #nextPage');
paginationButtons.forEach(button => {
CleanupManager.removeListenersByElement(button);
}); });
document.querySelectorAll('th[data-sortable="true"]').forEach(header => { if (window.CleanupManager) {
CleanupManager.removeListenersByElement(header); window.CleanupManager.removeListenersByElement(row);
}
}); });
offersBody.innerHTML = '';
cleanupTable(); }
jsonData = null; jsonData = null;
originalJsonData = null; originalJsonData = null;
latestPrices = 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) { } catch (error) {
console.error('Error during cleanup:', error); console.error('Error during cleanup:', error);
} }

View File

@@ -120,18 +120,33 @@ const api = {
fetchCoinGeckoDataXHR: async () => { fetchCoinGeckoDataXHR: async () => {
try { try {
const priceData = await window.PriceManager.getPrices(); const priceData = await window.PriceManager.getPrices();
const transformedData = {}; const transformedData = {};
const btcPriceUSD = priceData.bitcoin?.usd || 0;
if (btcPriceUSD > 0) {
window.btcPriceUSD = btcPriceUSD;
}
window.config.coins.forEach(coin => { window.config.coins.forEach(coin => {
const symbol = coin.symbol.toLowerCase(); const symbol = coin.symbol.toLowerCase();
const coinData = priceData[symbol] || priceData[coin.name.toLowerCase()]; const coinData = priceData[symbol] || priceData[coin.name.toLowerCase()];
if (coinData && coinData.usd) { 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] = { transformedData[symbol] = {
current_price: coinData.usd, current_price: coinData.usd,
price_btc: coinData.btc || (priceData.bitcoin ? coinData.usd / priceData.bitcoin.usd : 0), price_btc: priceBtc,
displayName: coin.displayName || coin.symbol displayName: coin.displayName || coin.symbol,
total_volume: coinData.total_volume,
price_change_percentage_24h: coinData.price_change_percentage_24h
}; };
} }
}); });
@@ -281,11 +296,9 @@ const ui = {
const volumeElement = document.querySelector(`#${coin.toLowerCase()}-volume-24h`); const volumeElement = document.querySelector(`#${coin.toLowerCase()}-volume-24h`);
const btcPriceDiv = document.querySelector(`#${coin.toLowerCase()}-btc-price-div`); const btcPriceDiv = document.querySelector(`#${coin.toLowerCase()}-btc-price-div`);
const priceBtcElement = document.querySelector(`#${coin.toLowerCase()}-price-btc`); const priceBtcElement = document.querySelector(`#${coin.toLowerCase()}-price-btc`);
if (priceUsdElement) { if (priceUsdElement) {
priceUsdElement.textContent = isError ? 'N/A' : `$ ${ui.formatPrice(coin, priceUSD)}`; priceUsdElement.textContent = isError ? 'N/A' : `$ ${ui.formatPrice(coin, priceUSD)}`;
} }
if (volumeDiv && volumeElement) { if (volumeDiv && volumeElement) {
if (isError || volume24h === null || volume24h === undefined) { if (isError || volume24h === null || volume24h === undefined) {
volumeElement.textContent = 'N/A'; volumeElement.textContent = 'N/A';
@@ -294,7 +307,6 @@ const ui = {
} }
volumeDiv.style.display = volumeToggle.isVisible ? 'flex' : 'none'; volumeDiv.style.display = volumeToggle.isVisible ? 'flex' : 'none';
} }
if (btcPriceDiv && priceBtcElement) { if (btcPriceDiv && priceBtcElement) {
if (coin === 'BTC') { if (coin === 'BTC') {
btcPriceDiv.style.display = 'none'; btcPriceDiv.style.display = 'none';
@@ -303,34 +315,46 @@ const ui = {
btcPriceDiv.style.display = 'flex'; btcPriceDiv.style.display = 'flex';
} }
} }
ui.updatePriceChangeContainer(coin, isError ? null : priceChange1d); ui.updatePriceChangeContainer(coin, isError ? null : priceChange1d);
}; };
try { try {
if (data.error) { if (data.error) {
throw new Error(data.error); throw new Error(data.error);
} }
if (!data || !data.current_price) { if (!data || !data.current_price) {
throw new Error(`Invalid data structure for ${coin}`); throw new Error(`Invalid data structure for ${coin}`);
} }
priceUSD = data.current_price; priceUSD = data.current_price;
priceBTC = coin === 'BTC' ? 1 : data.price_btc || (data.current_price / app.btcPriceUSD);
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; priceChange1d = data.price_change_percentage_24h || 0;
volume24h = data.total_volume || 0; volume24h = data.total_volume || 0;
if (isNaN(priceUSD) || isNaN(priceBTC)) { if (isNaN(priceUSD) || isNaN(priceBTC)) {
throw new Error(`Invalid numeric values in data for ${coin}`); throw new Error(`Invalid numeric values in data for ${coin}`);
} }
updateUI(false); updateUI(false);
} catch (error) { } catch (error) {
logger.error(`Failed to display data for ${coin}:`, error.message); logger.error(`Failed to display data for ${coin}:`, error.message);
updateUI(true); updateUI(true);
} }
}, },
showLoader: () => { showLoader: () => {
const loader = document.getElementById('loader'); const loader = document.getElementById('loader');
@@ -554,7 +578,7 @@ const chartModule = {
this.chartRefs.set(element, chart); this.chartRefs.set(element, chart);
}, },
destroyChart: function() { destroyChart: function() {
if (chartModule.chart) { if (chartModule.chart) {
try { try {
const chartInstance = chartModule.chart; const chartInstance = chartModule.chart;
@@ -568,12 +592,17 @@ const chartModule = {
if (canvas) { if (canvas) {
chartModule.chartRefs.delete(canvas); chartModule.chartRefs.delete(canvas);
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
} }
} catch (e) { } catch (e) {
console.error('Error destroying chart:', e); console.error('Error destroying chart:', e);
} }
} }
}, },
initChart: function() { initChart: function() {
this.destroyChart(); this.destroyChart();
@@ -1348,7 +1377,7 @@ const app = {
}, },
refreshAllData: async function() { refreshAllData: async function() {
console.log('Price refresh started at', new Date().toLocaleTimeString()); //console.log('Price refresh started at', new Date().toLocaleTimeString());
if (app.isRefreshing) { if (app.isRefreshing) {
console.log('Refresh already in progress, skipping...'); console.log('Refresh already in progress, skipping...');
@@ -1382,7 +1411,7 @@ refreshAllData: async function() {
return; return;
} }
console.log('Starting refresh of all data...'); //console.log('Starting refresh of all data...');
app.isRefreshing = true; app.isRefreshing = true;
app.updateNextRefreshTime(); app.updateNextRefreshTime();
ui.showLoader(); ui.showLoader();
@@ -1449,7 +1478,7 @@ refreshAllData: async function() {
const cacheKey = `coinData_${coin.symbol}`; const cacheKey = `coinData_${coin.symbol}`;
CacheManager.set(cacheKey, coinData, 'prices'); 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) { } catch (coinError) {
console.warn(`Failed to update ${coin.symbol}: ${coinError.message}`); console.warn(`Failed to update ${coin.symbol}: ${coinError.message}`);
@@ -1489,7 +1518,7 @@ refreshAllData: async function() {
} }
}, 1000); }, 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) { } catch (error) {
console.error('Critical error during refresh:', error); console.error('Critical error during refresh:', error);
@@ -1520,7 +1549,7 @@ refreshAllData: async function() {
app.scheduleNextRefresh(); 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(); app.init();
if (window.MemoryManager) { if (window.MemoryManager) {
if (typeof MemoryManager.enableAutoCleanup === 'function') {
MemoryManager.enableAutoCleanup(); MemoryManager.enableAutoCleanup();
} else {
MemoryManager.initialize({
autoCleanup: true,
debug: false
});
}
} }
CleanupManager.setInterval(() => { CleanupManager.setInterval(() => {

View File

@@ -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';
}
};
})();

View File

@@ -10,7 +10,7 @@
<img class="absolute h-64 left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 object-cover" src="/static/images/elements/wave.svg" alt=""> <img class="absolute h-64 left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 object-cover" src="/static/images/elements/wave.svg" alt="">
<div class="relative z-20 flex flex-wrap items-center -m-3"> <div class="relative z-20 flex flex-wrap items-center -m-3">
<div class="w-full md:w-1/2 p-3"> <div class="w-full md:w-1/2 p-3">
<h2 class="mb-3 text-2xl font-bold text-white tracking-tighter">Sent Bids / Received Bids</h2> <h2 class="mb-3 text-2xl font-bold text-white tracking-tighter">All Bids / Sent Bids / Received Bids</h2>
<p class="font-normal text-coolGray-200 dark:text-white">View, and manage bids.</p> <p class="font-normal text-coolGray-200 dark:text-white">View, and manage bids.</p>
</div> </div>
</div> </div>
@@ -28,7 +28,12 @@
<div class="mb-4 border-b pb-5 border-gray-200 dark:border-gray-500"> <div class="mb-4 border-b pb-5 border-gray-200 dark:border-gray-500">
<ul class="flex flex-wrap text-sm font-medium text-center text-gray-500 dark:text-gray-400" id="myTab" data-tabs-toggle="#bidstab" role="tablist"> <ul class="flex flex-wrap text-sm font-medium text-center text-gray-500 dark:text-gray-400" id="myTab" data-tabs-toggle="#bidstab" role="tablist">
<li class="mr-2"> <li class="mr-2">
<button class="inline-block px-4 py-3 rounded-lg hover:text-gray-900 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white focus:outline-none focus:ring-0" id="sent-tab" data-tabs-target="#sent" type="button" role="tab" aria-controls="sent" aria-selected="true"> <button class="inline-block px-4 py-3 rounded-lg hover:text-gray-900 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white focus:outline-none focus:ring-0" id="all-tab" data-tabs-target="#all" type="button" role="tab" aria-controls="all" aria-selected="true">
All Bids <span class="text-gray-500 dark:text-gray-400">({{ sent_bids_count + received_bids_count }})</span>
</button>
</li>
<li class="mr-2">
<button class="inline-block px-4 py-3 rounded-lg hover:text-gray-900 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white focus:outline-none focus:ring-0" id="sent-tab" data-tabs-target="#sent" type="button" role="tab" aria-controls="sent" aria-selected="false">
Sent Bids <span class="text-gray-500 dark:text-gray-400">({{ sent_bids_count }})</span> Sent Bids <span class="text-gray-500 dark:text-gray-400">({{ sent_bids_count }})</span>
</button> </button>
</li> </li>
@@ -167,7 +172,106 @@
</section> </section>
<div id="bidstab"> <div id="bidstab">
<div class="rounded-lg lg:px-6" id="sent" role="tabpanel" aria-labelledby="sent-tab"> <!-- All Bids Tab -->
<div class="rounded-lg lg:px-6" id="all" role="tabpanel" aria-labelledby="all-tab">
<div id="all-content">
<div class="xl:container mx-auto lg:px-0">
<div class="pt-0 pb-6 bg-coolGray-100 dark:bg-gray-500 rounded-xl">
<div class="px-0">
<div class="w-auto overflow-auto lg:overflow-hidden">
<table class="w-full lg:min-w-max">
<thead class="uppercase">
<tr class="text-left">
<th class="p-0">
<div class="py-3 pl-16 rounded-tl-xl bg-coolGray-200 dark:bg-gray-600">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Date/Time</span>
</div>
</th>
<th class="p-0 hidden lg:block">
<div class="p-3 bg-coolGray-200 dark:bg-gray-600">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Details</span>
</div>
</th>
<th class="p-0">
<div class="p-3 bg-coolGray-200 dark:bg-gray-600">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">You Send</span>
</div>
</th>
<th class="p-0">
<div class="p-3 bg-coolGray-200 dark:bg-gray-600">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">You Receive</span>
</div>
</th>
<th class="p-0">
<div class="p-3 text-center bg-coolGray-200 dark:bg-gray-600">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Status</span>
</div>
</th>
<th class="p-0">
<div class="p-3 pr-6 text-center rounded-tr-xl bg-coolGray-200 dark:bg-gray-600">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Actions</span>
</div>
</th>
</tr>
</thead>
<tbody id="all-tbody">
</tbody>
</table>
</div>
<div class="rounded-b-md">
<div class="w-full">
<div class="flex flex-wrap justify-between items-center pl-6 pt-6 pr-6 border-t border-gray-100 dark:border-gray-400">
<div class="flex items-center">
<div class="flex items-center mr-4">
<span id="status-dot-all" class="w-2.5 h-2.5 rounded-full bg-gray-500 mr-2"></span>
<span id="status-text-all" class="text-sm text-gray-500">Connecting...</span>
</div>
<p class="text-sm font-heading dark:text-gray-400">
All Bids: <span id="allBidsCount">0</span>
</p>
{% if debug_ui_mode == true %}
<button id="refreshAllBids" class="ml-4 inline-flex items-center px-4 py-2.5 font-medium text-sm text-white bg-blue-600 hover:bg-green-600 hover:border-green-600 rounded-lg transition duration-200 border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
<span id="refreshAllText">Refresh</span>
</button>
{% endif %}
<button id="exportAllBids" class="ml-4 inline-flex items-center px-4 py-2.5 font-medium text-sm text-white bg-green-600 hover:bg-green-700 hover:border-green-700 rounded-lg transition duration-200 border border-green-600 rounded-md shadow-button focus:ring-0 focus:outline-none">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
<span>Export CSV</span>
</button>
</div>
<div id="pagination-controls-all" class="flex items-center space-x-2" style="display: none;">
<button id="prevPageAll" class="inline-flex items-center h-9 py-1 px-4 text-xs text-blue-50 font-semibold bg-blue-500 hover:bg-green-600 rounded-lg transition duration-200 focus:ring-0 focus:outline-none">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
</svg>
Previous
</button>
<p class="text-sm font-heading dark:text-white">Page <span id="currentPageAll">1</span></p>
<button id="nextPageAll" class="inline-flex items-center h-9 py-1 px-4 text-xs text-blue-50 font-semibold bg-blue-500 hover:bg-green-600 rounded-lg transition duration-200 focus:ring-0 focus:outline-none">
Next
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
</svg>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Sent Bids Tab -->
<div class="hidden rounded-lg lg:px-6" id="sent" role="tabpanel" aria-labelledby="sent-tab">
<div id="sent-content"> <div id="sent-content">
<div class="xl:container mx-auto lg:px-0"> <div class="xl:container mx-auto lg:px-0">
<div class="pt-0 pb-6 bg-coolGray-100 dark:bg-gray-500 rounded-xl"> <div class="pt-0 pb-6 bg-coolGray-100 dark:bg-gray-500 rounded-xl">
@@ -208,7 +312,7 @@
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody id="sent-tbody">
</tbody> </tbody>
</table> </table>
</div> </div>
@@ -264,6 +368,7 @@
</div> </div>
</div> </div>
<!-- Received Bids Tab -->
<div class="hidden rounded-lg lg:px-6" id="received" role="tabpanel" aria-labelledby="received-tab"> <div class="hidden rounded-lg lg:px-6" id="received" role="tabpanel" aria-labelledby="received-tab">
<div id="received-content"> <div id="received-content">
<div class="xl:container mx-auto lg:px-0"> <div class="xl:container mx-auto lg:px-0">
@@ -305,7 +410,7 @@
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody id="received-tbody">
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@@ -63,6 +63,7 @@
<script src="/static/js/libs/tippy.js"></script> <script src="/static/js/libs/tippy.js"></script>
<!-- UI Components --> <!-- UI Components -->
<script src="/static/js/ui/tabs.js"></script> <script src="/static/js/ui/tabs.js"></script>
<script src="/static/js/ui/bids-tab-navigation.js"></script>
<script src="/static/js/ui/dropdown.js"></script> <script src="/static/js/ui/dropdown.js"></script>
<!-- Core functionality --> <!-- Core functionality -->
<script src="/static/js/modules/coin-manager.js"></script> <script src="/static/js/modules/coin-manager.js"></script>
@@ -84,6 +85,7 @@
<script src="/static/js/modules/memory-manager.js"></script> <script src="/static/js/modules/memory-manager.js"></script>
<!-- Main application script --> <!-- Main application script -->
<script src="/static/js/global.js"></script> <script src="/static/js/global.js"></script>
</head> </head>
<body class="dark:bg-gray-700"> <body class="dark:bg-gray-700">
<div id="shutdownModal" tabindex="-1" class="hidden fixed inset-0 z-50 overflow-y-auto overflow-x-hidden"> <div id="shutdownModal" tabindex="-1" class="hidden fixed inset-0 z-50 overflow-y-auto overflow-x-hidden">
@@ -422,26 +424,17 @@
<!-- Bids --> <!-- Bids -->
<li> <li>
<a href="/bids" data-tooltip-target="tooltip-bids" class="flex items-center text-sm text-gray-400 <a href="/bids" data-tooltip-target="tooltip-bids" class="flex items-center text-sm text-gray-400 hover:text-gray-600 dark:text-gray-100 dark:hover:text-gray-100">
hover:text-gray-600 dark:text-gray-100 dark:hover:text-gray-100">
<span class="inline-block mr-2">{{ bids_sent_svg | safe }}</span> <span class="inline-block mr-2">{{ bids_sent_svg | safe }}</span>
<span>Bids</span> <span class="my-auto">Bids</span>
<span class="flex items-center ml-2"> <span class="flex items-center ml-2 my-auto">
<span id="sent-bids-counter" class="inline-flex items-center text-xs font-semibold px-2.5 py-1 text-white {% if summary.num_sent_active_bids > 0 %}bg-blue-500{% else %}bg-gray-400{% endif %} rounded-full bids-tab-link cursor-pointer" data-tab-target="#sent">
<!-- Outgoing bids counter arrow -->
<span id="sent-bids-counter" class="inline-flex items-center text-xs font-semibold px-2.5 py-1
text-white {% if summary.num_sent_active_bids > 0 %}bg-blue-500{% else %}bg-gray-400{% endif %}
rounded-full">
<svg class="w-3 h-3 mr-1" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg class="w-3 h-3 mr-1" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 19V5L19 12L5 19Z" fill="currentColor" transform="rotate(-90 12 12)"/> <path d="M5 19V5L19 12L5 19Z" fill="currentColor" transform="rotate(-90 12 12)"/>
</svg> </svg>
{{ summary.num_sent_active_bids }} {{ summary.num_sent_active_bids }}
</span> </span>
<span id="recv-bids-counter" class="inline-flex items-center text-xs font-semibold ml-2 px-2.5 py-1 text-white {% if summary.num_recv_active_bids > 0 %}bg-blue-500{% else %}bg-gray-400{% endif %} rounded-full bids-tab-link cursor-pointer" data-tab-target="#received">
<!-- Incoming bids counter arrow -->
<span id="recv-bids-counter" class="inline-flex items-center text-xs font-semibold ml-2 px-2.5
py-1 text-white {% if summary.num_recv_active_bids > 0 %}bg-blue-500{% else %}bg-gray-400
{% endif %} rounded-full">
<svg class="w-3 h-3 mr-1" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg class="w-3 h-3 mr-1" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 19V5L19 12L5 19Z" fill="currentColor" transform="rotate(90 12 12)"/> <path d="M5 19V5L19 12L5 19Z" fill="currentColor" transform="rotate(90 12 12)"/>
</svg> </svg>
@@ -449,14 +442,11 @@
</span> </span>
</span> </span>
</a> </a>
<div id="tooltip-bids" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-sm <div id="tooltip-bids" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-sm font-medium text-white bg-blue-500 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">
font-medium text-white bg-blue-500 rounded-lg shadow-sm opacity-0 transition-opacity duration-300
tooltip">
<p><b>Sent bids:</b> {{ sent_bids_count }} ({{ summary.num_sent_active_bids }} active)</p> <p><b>Sent bids:</b> {{ sent_bids_count }} ({{ summary.num_sent_active_bids }} active)</p>
<p><b>Received bids:</b> {{ received_bids_count }} ({{ summary.num_recv_active_bids }} active)</p> <p><b>Received bids:</b> {{ received_bids_count }} ({{ summary.num_recv_active_bids }} active)</p>
</div> </div>
</li> </li>
<li> <li>
<span class="text-gray-300">|</span> <span class="text-gray-300">|</span>
</li> </li>
@@ -556,11 +546,12 @@
<li> <li>
<a class="flex items-center pl-3 py-3 pr-4 text-gray-50 hover:bg-gray-900 rounded" href="/bids"> <a class="flex items-center pl-3 py-3 pr-4 text-gray-50 hover:bg-gray-900 rounded" href="/bids">
{{ bids_received_svg | safe }} {{ bids_received_svg | safe }}
<span>Bids</span> <span class="my-auto">Bids</span>
<div class="flex ml-auto"> <div class="flex items-center ml-auto my-auto">
<span id="sent-bids-counter" class="inline-flex items-center text-xs font-semibold px-2.5 <span id="sent-bids-counter" class="inline-flex items-center text-xs font-semibold px-2.5
py-1 text-white {% if summary.num_sent_active_bids and summary.num_sent_active_bids > 0 %} py-1 text-white {% if summary.num_sent_active_bids and summary.num_sent_active_bids > 0 %}
bg-blue-500{% else %}bg-gray-400{% endif %} rounded-full mr-2"> bg-blue-500{% else %}bg-gray-400{% endif %} rounded-full mr-2 bids-tab-link cursor-pointer"
data-tab-target="#sent">
<svg class="w-3 h-3 mr-1" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg class="w-3 h-3 mr-1" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 19V5L19 12L5 19Z" fill="currentColor" transform="rotate(-90 12 12)"/> <path d="M5 19V5L19 12L5 19Z" fill="currentColor" transform="rotate(-90 12 12)"/>
</svg> </svg>
@@ -568,7 +559,8 @@
</span> </span>
<span id="recv-bids-counter" class="inline-flex items-center text-xs font-semibold px-2.5 <span id="recv-bids-counter" class="inline-flex items-center text-xs font-semibold px-2.5
py-1 text-white {% if summary.num_recv_active_bids and summary.num_recv_active_bids > 0 %} py-1 text-white {% if summary.num_recv_active_bids and summary.num_recv_active_bids > 0 %}
bg-blue-500{% else %}bg-gray-400{% endif %} rounded-full"> bg-blue-500{% else %}bg-gray-400{% endif %} rounded-full bids-tab-link cursor-pointer"
data-tab-target="#received">
<svg class="w-3 h-3 mr-1" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg class="w-3 h-3 mr-1" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 19V5L19 12L5 19Z" fill="currentColor" transform="rotate(90 12 12)"/> <path d="M5 19V5L19 12L5 19Z" fill="currentColor" transform="rotate(90 12 12)"/>
</svg> </svg>
@@ -576,7 +568,6 @@
</span> </span>
</div> </div>
</a> </a>
</li>
<li> <li>
<a class="flex items-center pl-3 py-3 pr-4 text-gray-50 hover:bg-gray-900 rounded" href="/active"> <a class="flex items-center pl-3 py-3 pr-4 text-gray-50 hover:bg-gray-900 rounded" href="/active">
<div id="swapContainer" class="inline-flex center-spin mr-2" <div id="swapContainer" class="inline-flex center-spin mr-2"