mirror of
https://github.com/basicswap/basicswap.git
synced 2025-11-05 10:28:10 +01:00
466 lines
15 KiB
JavaScript
466 lines
15 KiB
JavaScript
const SummaryManager = (function() {
|
|
const config = {
|
|
refreshInterval: window.config?.cacheDuration || 30000,
|
|
summaryEndpoint: '/json',
|
|
retryDelay: 5000,
|
|
maxRetries: 3,
|
|
requestTimeout: 15000,
|
|
debug: false
|
|
};
|
|
|
|
let refreshTimer = null;
|
|
let webSocket = null;
|
|
let fetchRetryCount = 0;
|
|
let lastSuccessfulData = null;
|
|
|
|
function updateElement(elementId, value) {
|
|
const element = document.getElementById(elementId);
|
|
if (!element) return false;
|
|
|
|
const safeValue = (value !== undefined && value !== null)
|
|
? value
|
|
: (element.dataset.lastValue || 0);
|
|
|
|
element.dataset.lastValue = safeValue;
|
|
|
|
if (elementId === 'sent-bids-counter' || elementId === 'recv-bids-counter') {
|
|
const svg = element.querySelector('svg');
|
|
element.textContent = safeValue;
|
|
if (svg) {
|
|
element.insertBefore(svg, element.firstChild);
|
|
}
|
|
} else {
|
|
element.textContent = safeValue;
|
|
}
|
|
|
|
if (['offers-counter', 'bid-requests-counter', 'sent-bids-counter',
|
|
'recv-bids-counter', 'swaps-counter', 'network-offers-counter',
|
|
'watched-outputs-counter'].includes(elementId)) {
|
|
element.classList.remove('bg-blue-500', 'bg-gray-400');
|
|
element.classList.add(safeValue > 0 ? 'bg-blue-500' : 'bg-gray-400');
|
|
}
|
|
|
|
if (elementId === 'swaps-counter') {
|
|
const swapContainer = document.getElementById('swapContainer');
|
|
if (swapContainer) {
|
|
const isSwapping = safeValue > 0;
|
|
if (isSwapping) {
|
|
swapContainer.innerHTML = document.querySelector('#swap-in-progress-green-template').innerHTML || '';
|
|
swapContainer.style.animation = 'spin 2s linear infinite';
|
|
} else {
|
|
swapContainer.innerHTML = document.querySelector('#swap-in-progress-template').innerHTML || '';
|
|
swapContainer.style.animation = 'none';
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function updateUIFromData(data) {
|
|
if (!data) return;
|
|
|
|
updateElement('network-offers-counter', data.num_network_offers);
|
|
updateElement('offers-counter', data.num_sent_active_offers);
|
|
updateElement('offers-counter-mobile', data.num_sent_active_offers);
|
|
updateElement('sent-bids-counter', data.num_sent_active_bids);
|
|
updateElement('recv-bids-counter', data.num_recv_active_bids);
|
|
updateElement('bid-requests-counter', data.num_available_bids);
|
|
updateElement('swaps-counter', data.num_swapping);
|
|
updateElement('watched-outputs-counter', data.num_watched_outputs);
|
|
|
|
updateTooltips(data);
|
|
|
|
const shutdownButtons = document.querySelectorAll('.shutdown-button');
|
|
shutdownButtons.forEach(button => {
|
|
button.setAttribute('data-active-swaps', data.num_swapping);
|
|
if (data.num_swapping > 0) {
|
|
button.classList.add('shutdown-disabled');
|
|
button.setAttribute('data-disabled', 'true');
|
|
button.setAttribute('title', 'Caution: Swaps in progress');
|
|
} else {
|
|
button.classList.remove('shutdown-disabled');
|
|
button.removeAttribute('data-disabled');
|
|
button.removeAttribute('title');
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateTooltips(data) {
|
|
debugLog(`updateTooltips called with data:`, data);
|
|
|
|
const yourOffersTooltip = document.getElementById('tooltip-your-offers');
|
|
debugLog('Looking for tooltip-your-offers element:', yourOffersTooltip);
|
|
|
|
if (yourOffersTooltip) {
|
|
const newContent = `
|
|
<p><b>Total offers:</b> ${data.num_sent_offers || 0}</p>
|
|
<p><b>Active offers:</b> ${data.num_sent_active_offers || 0}</p>
|
|
`;
|
|
|
|
const totalParagraph = yourOffersTooltip.querySelector('p:first-child');
|
|
const activeParagraph = yourOffersTooltip.querySelector('p:last-child');
|
|
|
|
debugLog('Found paragraphs:', { totalParagraph, activeParagraph });
|
|
|
|
if (totalParagraph && activeParagraph) {
|
|
totalParagraph.innerHTML = `<b>Total offers:</b> ${data.num_sent_offers || 0}`;
|
|
activeParagraph.innerHTML = `<b>Active offers:</b> ${data.num_sent_active_offers || 0}`;
|
|
debugLog(`Updated Your Offers tooltip paragraphs: total=${data.num_sent_offers}, active=${data.num_sent_active_offers}`);
|
|
} else {
|
|
yourOffersTooltip.innerHTML = newContent;
|
|
debugLog(`Replaced Your Offers tooltip content: total=${data.num_sent_offers}, active=${data.num_sent_active_offers}`);
|
|
}
|
|
|
|
refreshTooltipInstances('tooltip-your-offers', newContent);
|
|
} else {
|
|
debugLog('Your Offers tooltip element not found - checking all tooltip elements');
|
|
const allTooltips = document.querySelectorAll('[id*="tooltip"]');
|
|
debugLog('All tooltip elements found:', Array.from(allTooltips).map(el => el.id));
|
|
}
|
|
|
|
const bidsTooltip = document.getElementById('tooltip-bids');
|
|
if (bidsTooltip) {
|
|
const newBidsContent = `
|
|
<p><b>Sent bids:</b> ${data.num_sent_bids || 0} (${data.num_sent_active_bids || 0} active)</p>
|
|
<p><b>Received bids:</b> ${data.num_recv_bids || 0} (${data.num_recv_active_bids || 0} active)</p>
|
|
`;
|
|
|
|
const sentParagraph = bidsTooltip.querySelector('p:first-child');
|
|
const recvParagraph = bidsTooltip.querySelector('p:last-child');
|
|
|
|
if (sentParagraph && recvParagraph) {
|
|
sentParagraph.innerHTML = `<b>Sent bids:</b> ${data.num_sent_bids || 0} (${data.num_sent_active_bids || 0} active)`;
|
|
recvParagraph.innerHTML = `<b>Received bids:</b> ${data.num_recv_bids || 0} (${data.num_recv_active_bids || 0} active)`;
|
|
debugLog(`Updated Bids tooltip: sent=${data.num_sent_bids}(${data.num_sent_active_bids}), recv=${data.num_recv_bids}(${data.num_recv_active_bids})`);
|
|
} else {
|
|
bidsTooltip.innerHTML = newBidsContent;
|
|
debugLog(`Replaced Bids tooltip content: sent=${data.num_sent_bids}(${data.num_sent_active_bids}), recv=${data.num_recv_bids}(${data.num_recv_active_bids})`);
|
|
}
|
|
|
|
refreshTooltipInstances('tooltip-bids', newBidsContent);
|
|
} else {
|
|
debugLog('Bids tooltip element not found');
|
|
}
|
|
}
|
|
|
|
function refreshTooltipInstances(tooltipId, newContent) {
|
|
const triggers = document.querySelectorAll(`[data-tooltip-target="${tooltipId}"]`);
|
|
|
|
triggers.forEach(trigger => {
|
|
if (trigger._tippy) {
|
|
trigger._tippy.setContent(newContent);
|
|
debugLog(`Updated Tippy instance content for ${tooltipId}`);
|
|
} else {
|
|
if (window.TooltipManager && typeof window.TooltipManager.create === 'function') {
|
|
window.TooltipManager.create(trigger, newContent, {
|
|
placement: trigger.getAttribute('data-tooltip-placement') || 'top'
|
|
});
|
|
debugLog(`Created new Tippy instance for ${tooltipId}`);
|
|
}
|
|
}
|
|
});
|
|
|
|
if (window.TooltipManager && typeof window.TooltipManager.refreshTooltip === 'function') {
|
|
window.TooltipManager.refreshTooltip(tooltipId, newContent);
|
|
debugLog(`Refreshed tooltip via TooltipManager for ${tooltipId}`);
|
|
}
|
|
|
|
if (window.TooltipManager && typeof window.TooltipManager.initializeTooltips === 'function') {
|
|
CleanupManager.setTimeout(() => {
|
|
window.TooltipManager.initializeTooltips(`[data-tooltip-target="${tooltipId}"]`);
|
|
debugLog(`Re-initialized tooltips for ${tooltipId}`);
|
|
}, 50);
|
|
}
|
|
}
|
|
|
|
function debugLog(message) {
|
|
if (config.debug && console && console.log) {
|
|
console.log(`[SummaryManager] ${message}`);
|
|
}
|
|
}
|
|
|
|
function cacheSummaryData(data) {
|
|
if (!data) return;
|
|
|
|
localStorage.setItem('summary_data_cache', JSON.stringify({
|
|
timestamp: Date.now(),
|
|
data: data
|
|
}));
|
|
}
|
|
|
|
function getCachedSummaryData() {
|
|
let cachedData = null;
|
|
|
|
cachedData = localStorage.getItem('summary_data_cache');
|
|
if (!cachedData) return null;
|
|
|
|
const parsedCache = JSON.parse(cachedData);
|
|
const maxAge = 24 * 60 * 60 * 1000;
|
|
|
|
if (Date.now() - parsedCache.timestamp < maxAge) {
|
|
return parsedCache.data;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function fetchSummaryDataWithTimeout() {
|
|
if (window.ApiManager) {
|
|
return window.ApiManager.makeRequest(config.summaryEndpoint, 'GET', {
|
|
'Accept': 'application/json',
|
|
'Cache-Control': 'no-cache',
|
|
'Pragma': 'no-cache'
|
|
});
|
|
}
|
|
|
|
const controller = new AbortController();
|
|
const timeoutId = CleanupManager.setTimeout(() => controller.abort(), config.requestTimeout);
|
|
|
|
return fetch(config.summaryEndpoint, {
|
|
signal: controller.signal,
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
'Cache-Control': 'no-cache',
|
|
'Pragma': 'no-cache'
|
|
}
|
|
})
|
|
.then(response => {
|
|
if (window.CleanupManager) {
|
|
window.CleanupManager.clearTimeout(timeoutId);
|
|
} else {
|
|
clearTimeout(timeoutId);
|
|
}
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! Status: ${response.status}`);
|
|
}
|
|
|
|
return response.json();
|
|
})
|
|
.catch(error => {
|
|
if (window.CleanupManager) {
|
|
window.CleanupManager.clearTimeout(timeoutId);
|
|
} else {
|
|
clearTimeout(timeoutId);
|
|
}
|
|
throw error;
|
|
});
|
|
}
|
|
|
|
function setupWebSocket() {
|
|
if (webSocket) {
|
|
webSocket.close();
|
|
}
|
|
|
|
const wsPort = window.config?.wsPort ||
|
|
(typeof determineWebSocketPort === 'function' ? determineWebSocketPort() : '11700');
|
|
|
|
const wsUrl = "ws://" + window.location.hostname + ":" + wsPort;
|
|
webSocket = new WebSocket(wsUrl);
|
|
|
|
webSocket.onopen = () => {
|
|
publicAPI.fetchSummaryData()
|
|
.then(() => {})
|
|
.catch(() => {});
|
|
};
|
|
|
|
webSocket.onmessage = (event) => {
|
|
let data;
|
|
|
|
try {
|
|
data = JSON.parse(event.data);
|
|
} catch (error) {
|
|
if (window.logger && window.logger.error) {
|
|
window.logger.error('WebSocket message processing error: ' + error.message);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (data.event) {
|
|
const summaryEvents = ['new_offer', 'offer_revoked', 'new_bid', 'bid_accepted', 'swap_completed'];
|
|
if (summaryEvents.includes(data.event)) {
|
|
publicAPI.fetchSummaryData()
|
|
.then(() => {})
|
|
.catch(() => {});
|
|
}
|
|
|
|
if (window.NotificationManager && typeof window.NotificationManager.handleWebSocketEvent === 'function') {
|
|
window.NotificationManager.handleWebSocketEvent(data);
|
|
}
|
|
}
|
|
};
|
|
|
|
webSocket.onclose = () => {
|
|
CleanupManager.setTimeout(setupWebSocket, 5000);
|
|
};
|
|
}
|
|
|
|
function ensureSwapTemplates() {
|
|
if (!document.getElementById('swap-in-progress-template')) {
|
|
const template = document.createElement('template');
|
|
template.id = 'swap-in-progress-template';
|
|
template.innerHTML = document.querySelector('[id^="swapContainer"]')?.innerHTML || '';
|
|
document.body.appendChild(template);
|
|
}
|
|
|
|
if (!document.getElementById('swap-in-progress-green-template') &&
|
|
document.querySelector('[id^="swapContainer"]')?.innerHTML) {
|
|
const greenTemplate = document.createElement('template');
|
|
greenTemplate.id = 'swap-in-progress-green-template';
|
|
greenTemplate.innerHTML = document.querySelector('[id^="swapContainer"]')?.innerHTML || '';
|
|
document.body.appendChild(greenTemplate);
|
|
}
|
|
}
|
|
|
|
function startRefreshTimer() {
|
|
stopRefreshTimer();
|
|
|
|
publicAPI.fetchSummaryData()
|
|
.then(() => {})
|
|
.catch(() => {});
|
|
|
|
refreshTimer = CleanupManager.setInterval(() => {
|
|
publicAPI.fetchSummaryData()
|
|
.then(() => {})
|
|
.catch(() => {});
|
|
}, config.refreshInterval);
|
|
}
|
|
|
|
function stopRefreshTimer() {
|
|
if (refreshTimer) {
|
|
clearInterval(refreshTimer);
|
|
refreshTimer = null;
|
|
}
|
|
}
|
|
|
|
const publicAPI = {
|
|
initialize: function(options = {}) {
|
|
Object.assign(config, options);
|
|
|
|
ensureSwapTemplates();
|
|
|
|
const cachedData = getCachedSummaryData();
|
|
if (cachedData) {
|
|
updateUIFromData(cachedData);
|
|
}
|
|
|
|
if (window.WebSocketManager && typeof window.WebSocketManager.initialize === 'function') {
|
|
const wsManager = window.WebSocketManager;
|
|
|
|
if (!wsManager.isConnected()) {
|
|
wsManager.connect();
|
|
}
|
|
|
|
wsManager.addMessageHandler('message', (data) => {
|
|
if (data.event) {
|
|
const summaryEvents = ['new_offer', 'offer_revoked', 'new_bid', 'bid_accepted', 'swap_completed'];
|
|
if (summaryEvents.includes(data.event)) {
|
|
this.fetchSummaryData()
|
|
.then(() => {})
|
|
.catch(() => {});
|
|
}
|
|
|
|
if (window.NotificationManager && typeof window.NotificationManager.handleWebSocketEvent === 'function') {
|
|
window.NotificationManager.handleWebSocketEvent(data);
|
|
}
|
|
}
|
|
});
|
|
} else {
|
|
setupWebSocket();
|
|
}
|
|
|
|
startRefreshTimer();
|
|
|
|
if (window.CleanupManager) {
|
|
window.CleanupManager.registerResource('summaryManager', this, (mgr) => mgr.dispose());
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
fetchSummaryData: function() {
|
|
return fetchSummaryDataWithTimeout()
|
|
.then(data => {
|
|
lastSuccessfulData = data;
|
|
cacheSummaryData(data);
|
|
fetchRetryCount = 0;
|
|
|
|
updateUIFromData(data);
|
|
|
|
return data;
|
|
})
|
|
.catch(error => {
|
|
if (window.logger && window.logger.error) {
|
|
window.logger.error('Summary data fetch error: ' + error.message);
|
|
}
|
|
|
|
if (fetchRetryCount < config.maxRetries) {
|
|
fetchRetryCount++;
|
|
|
|
if (window.logger && window.logger.warn) {
|
|
window.logger.warn(`Retrying summary data fetch (${fetchRetryCount}/${config.maxRetries}) in ${config.retryDelay/1000}s`);
|
|
}
|
|
|
|
return new Promise(resolve => {
|
|
CleanupManager.setTimeout(() => {
|
|
resolve(this.fetchSummaryData());
|
|
}, config.retryDelay);
|
|
});
|
|
} else {
|
|
const cachedData = lastSuccessfulData || getCachedSummaryData();
|
|
|
|
if (cachedData) {
|
|
if (window.logger && window.logger.warn) {
|
|
window.logger.warn('Using cached summary data after fetch failures');
|
|
}
|
|
updateUIFromData(cachedData);
|
|
}
|
|
|
|
fetchRetryCount = 0;
|
|
|
|
throw error;
|
|
}
|
|
});
|
|
},
|
|
|
|
updateTooltips: function(data) {
|
|
updateTooltips(data || lastSuccessfulData);
|
|
},
|
|
|
|
updateUI: function(data) {
|
|
updateUIFromData(data || lastSuccessfulData);
|
|
},
|
|
|
|
startRefreshTimer: function() {
|
|
startRefreshTimer();
|
|
},
|
|
|
|
stopRefreshTimer: function() {
|
|
stopRefreshTimer();
|
|
},
|
|
|
|
dispose: function() {
|
|
stopRefreshTimer();
|
|
|
|
if (webSocket && webSocket.readyState === WebSocket.OPEN) {
|
|
webSocket.close();
|
|
}
|
|
|
|
webSocket = null;
|
|
}
|
|
};
|
|
|
|
return publicAPI;
|
|
})();
|
|
|
|
window.SummaryManager = SummaryManager;
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
if (!window.summaryManagerInitialized) {
|
|
window.SummaryManager = SummaryManager.initialize();
|
|
window.summaryManagerInitialized = true;
|
|
}
|
|
});
|
|
|
|
console.log('SummaryManager initialized');
|