Merge pull request #163 from gerlofvanek/offers-3

ui: Offers: Fixed cache, better filtering on tables, visually on refresh.
This commit is contained in:
tecnovert 2024-11-25 21:46:51 +00:00 committed by GitHub
commit d417a46e67
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -7,8 +7,11 @@ const itemsPerPage = 100;
let lastAppliedFilters = {}; let lastAppliedFilters = {};
const CACHE_KEY = 'latestPricesCache'; const CACHE_KEY = 'latestPricesCache';
const CACHE_DURATION = 10 * 60 * 1000; // 10 min
const MIN_REFRESH_INTERVAL = 30; // 30 sec const MIN_REFRESH_INTERVAL = 60; // 60 sec
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
const FALLBACK_CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours
let jsonData = []; let jsonData = [];
let originalJsonData = []; let originalJsonData = [];
@ -16,6 +19,93 @@ let isInitialLoad = true;
let tableRateModule; let tableRateModule;
const isSentOffers = window.offersTableConfig.isSentOffers; const isSentOffers = window.offersTableConfig.isSentOffers;
let currentSortColumn = 0;
let currentSortDirection = 'desc';
const offerCache = {
set: (key, value, customTtl = null) => {
const item = {
value: value,
timestamp: Date.now(),
expiresAt: Date.now() + (customTtl || CACHE_DURATION)
};
localStorage.setItem(key, JSON.stringify(item));
console.log(`🟢 Cache set for ${key}, expires in ${(customTtl || CACHE_DURATION) / 1000} seconds`);
console.log('Cached data:', {
key: key,
expiresIn: (customTtl || CACHE_DURATION) / 1000,
dataSize: typeof value === 'object' ? Object.keys(value).length : 'not an object'
});
},
get: (key) => {
const itemStr = localStorage.getItem(key);
if (!itemStr) {
console.log(`🔴 No cache found for ${key}`);
return null;
}
try {
const item = JSON.parse(itemStr);
const now = Date.now();
if (now < item.expiresAt) {
const remainingTime = (item.expiresAt - now) / 1000;
console.log(`🟢 Cache hit for ${key}, ${remainingTime.toFixed(1)} seconds remaining`);
return {
value: item.value,
remainingTime: item.expiresAt - now
};
} else {
console.log(`🟡 Cache expired for ${key}`);
localStorage.removeItem(key);
}
} catch (e) {
console.error('❌ Error parsing cache item:', e);
localStorage.removeItem(key);
}
return null;
},
isValid: (key) => {
const result = offerCache.get(key) !== null;
console.log(`🔍 Cache validity check for ${key}: ${result ? 'valid' : 'invalid'}`);
return result;
},
clear: () => {
let clearedItems = [];
Object.keys(localStorage).forEach(key => {
if (key.startsWith('offers_') || key.startsWith('prices_')) {
clearedItems.push(key);
localStorage.removeItem(key);
}
});
console.log(`🧹 Cache cleared: ${clearedItems.length} items removed`);
if (clearedItems.length > 0) {
console.log('Cleared items:', clearedItems);
}
},
debug: () => {
const cacheItems = {};
Object.keys(localStorage).forEach(key => {
if (key.startsWith('offers_') || key.startsWith('prices_')) {
try {
const item = JSON.parse(localStorage.getItem(key));
cacheItems[key] = {
expiresIn: ((item.expiresAt - Date.now()) / 1000).toFixed(1) + ' seconds',
dataSize: typeof item.value === 'object' ? Object.keys(item.value).length : 'not an object'
};
} catch (e) {
cacheItems[key] = 'invalid cache item';
}
}
});
console.log('📊 Current cache status:', cacheItems);
return cacheItems;
}
};
const coinNameToSymbol = { const coinNameToSymbol = {
'Bitcoin': 'bitcoin', 'Bitcoin': 'bitcoin',
'Particl': 'particl', 'Particl': 'particl',
@ -189,6 +279,47 @@ window.tableRateModule = {
} }
}; };
document.querySelectorAll('th[data-sortable="true"]').forEach(header => {
header.addEventListener('click', () => {
const columnIndex = parseInt(header.getAttribute('data-column-index'));
if (currentSortColumn === columnIndex) {
currentSortDirection = currentSortDirection === 'asc' ? 'desc' : 'asc';
} else {
currentSortColumn = columnIndex;
currentSortDirection = 'desc';
}
document.querySelectorAll('.sort-icon').forEach(icon => {
icon.classList.remove('text-blue-500');
icon.textContent = '↓';
});
const sortIcon = document.getElementById(`sort-icon-${columnIndex}`);
if (sortIcon) {
sortIcon.textContent = currentSortDirection === 'asc' ? '↑' : '↓';
sortIcon.classList.add('text-blue-500');
}
document.querySelectorAll('th[data-sortable="true"]').forEach(th => {
const thColumnIndex = parseInt(th.getAttribute('data-column-index'));
if (thColumnIndex === columnIndex) {
th.classList.add('text-blue-500');
} else {
th.classList.remove('text-blue-500');
}
});
localStorage.setItem('tableSortColumn', currentSortColumn);
localStorage.setItem('tableSortDirection', currentSortDirection);
applyFilters();
});
header.classList.add('cursor-pointer', 'hover:bg-gray-100', 'dark:hover:bg-gray-700');
});
function makePostRequest(url, headers = {}) { function makePostRequest(url, headers = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
@ -224,6 +355,16 @@ function makePostRequest(url, headers = {}) {
}); });
} }
function loadSortPreferences() {
const savedColumn = localStorage.getItem('tableSortColumn');
const savedDirection = localStorage.getItem('tableSortDirection');
if (savedColumn !== null) {
currentSortColumn = parseInt(savedColumn);
currentSortDirection = savedDirection || 'desc';
}
}
function escapeHtml(unsafe) { function escapeHtml(unsafe) {
if (typeof unsafe !== 'string') { if (typeof unsafe !== 'string') {
console.warn('escapeHtml received a non-string value:', unsafe); console.warn('escapeHtml received a non-string value:', unsafe);
@ -441,8 +582,18 @@ function setRefreshButtonLoading(isLoading) {
refreshButton.disabled = isLoading; refreshButton.disabled = isLoading;
refreshIcon.classList.toggle('animate-spin', isLoading); refreshIcon.classList.toggle('animate-spin', isLoading);
refreshText.textContent = isLoading ? 'Refresh' : 'Refresh'; refreshText.textContent = isLoading ? 'Refreshing...' : 'Refresh';
// Add visual feedback
if (isLoading) {
refreshButton.classList.add('opacity-75');
refreshButton.classList.add('cursor-wait');
} else {
refreshButton.classList.remove('opacity-75');
refreshButton.classList.remove('cursor-wait');
} }
}
function initializeFlowbiteTooltips() { function initializeFlowbiteTooltips() {
if (typeof Tooltip === 'undefined') { if (typeof Tooltip === 'undefined') {
@ -596,14 +747,15 @@ function updatePaginationControls(totalPages) {
totalPagesSpan.textContent = totalPages; totalPagesSpan.textContent = totalPages;
} }
function checkExpiredAndFetchNew() { async function checkExpiredAndFetchNew() {
if (isSentOffers) return Promise.resolve(); if (isSentOffers) return Promise.resolve();
console.log('Starting checkExpiredAndFetchNew'); console.log('Starting checkExpiredAndFetchNew');
const OFFERS_CACHE_KEY = 'offers_received';
return fetch('/json/offers') try {
.then(response => response.json()) const response = await fetch('/json/offers');
.then(data => { const data = await response.json();
let newListings = Array.isArray(data) ? data : Object.values(data); let newListings = Array.isArray(data) ? data : Object.values(data);
newListings = newListings.map(offer => ({ newListings = newListings.map(offer => ({
@ -626,6 +778,8 @@ function checkExpiredAndFetchNew() {
newListings = newListings.filter(offer => !isOfferExpired(offer)); newListings = newListings.filter(offer => !isOfferExpired(offer));
originalJsonData = newListings; originalJsonData = newListings;
cache.set(OFFERS_CACHE_KEY, newListings, CACHE_DURATION);
const currentFilters = new FormData(filterForm); const currentFilters = new FormData(filterForm);
const hasActiveFilters = currentFilters.get('coin_to') !== 'any' || const hasActiveFilters = currentFilters.get('coin_to') !== 'any' ||
currentFilters.get('coin_from') !== 'any'; currentFilters.get('coin_from') !== 'any';
@ -648,12 +802,11 @@ function checkExpiredAndFetchNew() {
console.log(`Next refresh in ${nextRefreshCountdown} seconds`); console.log(`Next refresh in ${nextRefreshCountdown} seconds`);
return jsonData.length; return jsonData.length;
}) } catch (error) {
.catch(error => {
console.error('Error fetching new listings:', error); console.error('Error fetching new listings:', error);
nextRefreshCountdown = 60; nextRefreshCountdown = 60;
return Promise.reject(error); return Promise.reject(error);
}); }
} }
function createTimeColumn(offer, postedTime, expiresIn) { function createTimeColumn(offer, postedTime, expiresIn) {
@ -1119,33 +1272,30 @@ function clearFilters() {
} }
async function fetchLatestPrices() { async function fetchLatestPrices() {
const MAX_RETRIES = 3; const PRICES_CACHE_KEY = 'prices_coingecko';
const BASE_DELAY = 1000; const cachedData = offerCache.get(PRICES_CACHE_KEY);
const cachedData = getCachedPrices();
if (cachedData) { if (cachedData) {
latestPrices = cachedData; latestPrices = cachedData.value;
return cachedData; return cachedData.value;
} }
const url = 'https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,bitcoin-cash,dash,dogecoin,decred,litecoin,particl,pivx,monero,zano,wownero,zcoin&vs_currencies=USD,BTC'; const url = 'https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,bitcoin-cash,dash,dogecoin,decred,litecoin,particl,pivx,monero,zano,wownero,zcoin&vs_currencies=USD,BTC';
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
try { try {
const data = await makePostRequest(url); const data = await makePostRequest(url);
if (data && Object.keys(data).length > 0) { if (data && Object.keys(data).length > 0) {
latestPrices = data; latestPrices = data;
setCachedPrices(data); offerCache.set(PRICES_CACHE_KEY, data, CACHE_DURATION);
// Store as fallback values with 24h expiry
Object.entries(data).forEach(([coin, prices]) => { Object.entries(data).forEach(([coin, prices]) => {
tableRateModule.setFallbackValue(coin, prices.usd); tableRateModule.setFallbackValue(coin, prices.usd);
}); });
return data; return data;
} }
} catch (error) { } catch (error) {
const delay = Math.min(BASE_DELAY * Math.pow(2, attempt), 10000); console.error('Error fetching prices:', error);
await new Promise(resolve => setTimeout(resolve, delay)); return getFallbackPrices();
}
} }
return getFallbackPrices(); return getFallbackPrices();
@ -1167,11 +1317,28 @@ function getFallbackPrices() {
} }
async function fetchOffers(manualRefresh = false) { async function fetchOffers(manualRefresh = false) {
return new Promise((resolve, reject) => { setRefreshButtonLoading(true);
const OFFERS_CACHE_KEY = `offers_${isSentOffers ? 'sent' : 'received'}`;
if (!manualRefresh) {
const cachedData = offerCache.get(OFFERS_CACHE_KEY);
if (cachedData) {
console.log('Using cached offers data');
jsonData = cachedData.value;
originalJsonData = [...cachedData.value];
updateOffersTable();
updateJsonView();
updatePaginationInfo();
setRefreshButtonLoading(false);
return;
}
}
try {
const endpoint = isSentOffers ? '/json/sentoffers' : '/json/offers'; const endpoint = isSentOffers ? '/json/sentoffers' : '/json/offers';
console.log(`[Debug] Fetching from endpoint: ${endpoint}`); console.log(`[Debug] Fetching from endpoint: ${endpoint}`);
fetch(endpoint, { const response = await fetch(endpoint, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -1180,11 +1347,9 @@ async function fetchOffers(manualRefresh = false) {
with_extra_info: true, with_extra_info: true,
limit: 1000 limit: 1000
}) })
}) });
.then(response => response.json())
.then(data => {
console.log('[Debug] Raw data received:', data);
const data = await response.json();
let newData = Array.isArray(data) ? data : Object.values(data); let newData = Array.isArray(data) ? data : Object.values(data);
newData = newData.map(offer => ({ newData = newData.map(offer => ({
@ -1214,6 +1379,9 @@ async function fetchOffers(manualRefresh = false) {
originalJsonData = [...mergedData]; originalJsonData = [...mergedData];
} }
// Cache the new data
offerCache.set(OFFERS_CACHE_KEY, jsonData, CACHE_DURATION);
if (newEntriesCountSpan) { if (newEntriesCountSpan) {
newEntriesCountSpan.textContent = jsonData.length; newEntriesCountSpan.textContent = jsonData.length;
} }
@ -1222,13 +1390,11 @@ async function fetchOffers(manualRefresh = false) {
updateJsonView(); updateJsonView();
updatePaginationInfo(); updatePaginationInfo();
resolve(); } catch (error) {
})
.catch(error => {
console.error('[Debug] Error fetching offers:', error); console.error('[Debug] Error fetching offers:', error);
reject(error); } finally {
}); setRefreshButtonLoading(false);
}); }
} }
function mergeSentOffers(existingOffers, newOffers) { function mergeSentOffers(existingOffers, newOffers) {
@ -1319,6 +1485,50 @@ function filterAndSortData() {
return true; return true;
}); });
if (currentSortColumn !== null) {
filteredData.sort((a, b) => {
let comparison = 0;
switch(currentSortColumn) {
case 0: // Time
comparison = a.created_at - b.created_at;
break;
case 5: // Rate
comparison = parseFloat(a.rate) - parseFloat(b.rate);
break;
case 6: // Market +/-
// Calculate market differences for comparison
const aFromSymbol = getCoinSymbolLowercase(a.coin_from);
const aToSymbol = getCoinSymbolLowercase(a.coin_to);
const bFromSymbol = getCoinSymbolLowercase(b.coin_from);
const bToSymbol = getCoinSymbolLowercase(b.coin_to);
const aFromPrice = latestPrices[aFromSymbol]?.usd || 0;
const aToPrice = latestPrices[aToSymbol]?.usd || 0;
const bFromPrice = latestPrices[bFromSymbol]?.usd || 0;
const bToPrice = latestPrices[bToSymbol]?.usd || 0;
const aMarketRate = aToPrice / aFromPrice;
const bMarketRate = bToPrice / bFromPrice;
const aOfferedRate = parseFloat(a.rate);
const bOfferedRate = parseFloat(b.rate);
const aPercentDiff = ((aOfferedRate - aMarketRate) / aMarketRate) * 100;
const bPercentDiff = ((bOfferedRate - bMarketRate) / bMarketRate) * 100;
comparison = aPercentDiff - bPercentDiff;
break;
case 7: // Trade
comparison = a.offer_id.localeCompare(b.offer_id);
break;
}
return currentSortDirection === 'desc' ? -comparison : comparison;
});
}
console.log(`[Debug] Filtered data length: ${filteredData.length}`);
return filteredData; return filteredData;
} }
@ -1676,10 +1886,12 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
document.getElementById('refreshOffers').addEventListener('click', () => { document.getElementById('refreshOffers').addEventListener('click', () => {
console.log('Refresh button clicked'); console.log('🔄 Refresh button clicked');
console.log('Clearing cache before refresh...');
offerCache.clear();
console.log('Fetching fresh data...');
fetchOffers(true); fetchOffers(true);
}); });
toggleButton.addEventListener('click', () => { toggleButton.addEventListener('click', () => {
tableView.classList.toggle('hidden'); tableView.classList.toggle('hidden');
jsonView.classList.toggle('hidden'); jsonView.classList.toggle('hidden');