Merge pull request #193 from gerlofvanek/private-2
Some checks failed
ci / ci (3.12) (push) Has been cancelled

Private orderbook display + Identity stats + Various fixes.
This commit is contained in:
tecnovert 2024-12-26 08:14:13 +00:00 committed by GitHub
commit 7ad92b1bbd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 538 additions and 236 deletions

View file

@ -1329,39 +1329,43 @@ class BasicSwap(BaseApp):
self.closeDB(cursor) self.closeDB(cursor)
def updateIdentityBidState(self, cursor, address: str, bid) -> None: def updateIdentityBidState(self, cursor, address: str, bid) -> None:
identity_stats = self.queryOne(KnownIdentity, cursor, {"address": address}) offer = self.getOffer(bid.offer_id, cursor)
if not identity_stats: addresses_to_update = [offer.addr_from, bid.bid_addr]
identity_stats = KnownIdentity( for addr in addresses_to_update:
active_ind=1, address=address, created_at=self.getTime() identity_stats = self.queryOne(KnownIdentity, cursor, {"address": addr})
) if not identity_stats:
identity_stats = KnownIdentity(
if bid.state == BidStates.SWAP_COMPLETED: active_ind=1,
if bid.was_sent: address=addr,
identity_stats.num_sent_bids_successful = ( created_at=self.getTime()
zeroIfNone(identity_stats.num_sent_bids_successful) + 1
) )
else: is_offer_creator = addr == offer.addr_from
identity_stats.num_recv_bids_successful = ( if bid.state == BidStates.SWAP_COMPLETED:
zeroIfNone(identity_stats.num_recv_bids_successful) + 1 if is_offer_creator:
) old_value = zeroIfNone(identity_stats.num_recv_bids_successful)
elif bid.state in ( identity_stats.num_recv_bids_successful = old_value + 1
BidStates.BID_ERROR, else:
BidStates.XMR_SWAP_FAILED_REFUNDED, old_value = zeroIfNone(identity_stats.num_sent_bids_successful)
BidStates.XMR_SWAP_FAILED_SWIPED, identity_stats.num_sent_bids_successful = old_value + 1
BidStates.XMR_SWAP_FAILED, elif bid.state in (BidStates.BID_ERROR,
BidStates.SWAP_TIMEDOUT, BidStates.XMR_SWAP_FAILED_REFUNDED,
): BidStates.XMR_SWAP_FAILED_SWIPED,
if bid.was_sent: BidStates.XMR_SWAP_FAILED,
identity_stats.num_sent_bids_failed = ( BidStates.SWAP_TIMEDOUT):
zeroIfNone(identity_stats.num_sent_bids_failed) + 1 if is_offer_creator:
) old_value = zeroIfNone(identity_stats.num_recv_bids_failed)
else: identity_stats.num_recv_bids_failed = old_value + 1
identity_stats.num_recv_bids_failed = ( else:
zeroIfNone(identity_stats.num_recv_bids_failed) + 1 old_value = zeroIfNone(identity_stats.num_sent_bids_failed)
) identity_stats.num_sent_bids_failed = old_value + 1
elif bid.state == BidStates.BID_REJECTED:
identity_stats.updated_at = self.getTime() if is_offer_creator:
self.add(identity_stats, cursor, upsert=True) old_value = zeroIfNone(identity_stats.num_recv_bids_rejected)
identity_stats.num_recv_bids_rejected = old_value + 1
else:
old_value = zeroIfNone(identity_stats.num_sent_bids_rejected)
identity_stats.num_sent_bids_rejected = old_value + 1
self.add(identity_stats, cursor, upsert=True)
def getPreFundedTx( def getPreFundedTx(
self, linked_type: int, linked_id: bytes, tx_type: int, cursor=None self, linked_type: int, linked_id: bytes, tx_type: int, cursor=None

View file

@ -250,6 +250,7 @@ def js_offers(self, url_split, post_string, is_json, sent=False) -> bytes:
"is_expired": o.expire_at <= swap_client.getTime(), "is_expired": o.expire_at <= swap_client.getTime(),
"is_own_offer": o.was_sent, "is_own_offer": o.was_sent,
"is_revoked": True if o.active_ind == 2 else False, "is_revoked": True if o.active_ind == 2 else False,
"is_public": o.addr_to == swap_client.network_addr or o.addr_to.strip() == "",
} }
if with_extra_info: if with_extra_info:
offer_data["amount_negotiable"] = o.amount_negotiable offer_data["amount_negotiable"] = o.amount_negotiable
@ -655,6 +656,23 @@ def js_identities(self, url_split, post_string: str, is_json: bool) -> bytes:
swap_client = self.server.swap_client swap_client = self.server.swap_client
swap_client.checkSystemStatus() swap_client.checkSystemStatus()
if len(url_split) > 3:
address = url_split[3]
identity = swap_client.getIdentity(address)
if identity:
return bytes(json.dumps({
"label": identity.label if identity.label is not None else "",
"note": identity.note if identity.note is not None else "",
"automation_override": identity.automation_override if identity.automation_override is not None else 0,
"num_sent_bids_successful": identity.num_sent_bids_successful if identity.num_sent_bids_successful is not None else 0,
"num_recv_bids_successful": identity.num_recv_bids_successful if identity.num_recv_bids_successful is not None else 0,
"num_sent_bids_rejected": identity.num_sent_bids_rejected if identity.num_sent_bids_rejected is not None else 0,
"num_recv_bids_rejected": identity.num_recv_bids_rejected if identity.num_recv_bids_rejected is not None else 0,
"num_sent_bids_failed": identity.num_sent_bids_failed if identity.num_sent_bids_failed is not None else 0,
"num_recv_bids_failed": identity.num_recv_bids_failed if identity.num_recv_bids_failed is not None else 0
}), "UTF-8")
return bytes(json.dumps({}), "UTF-8")
filters = { filters = {
"page_no": 1, "page_no": 1,
"limit": PAGE_LIMIT, "limit": PAGE_LIMIT,
@ -662,10 +680,6 @@ def js_identities(self, url_split, post_string: str, is_json: bool) -> bytes:
"sort_dir": "desc", "sort_dir": "desc",
} }
if len(url_split) > 3:
address = url_split[3]
filters["address"] = address
if post_string != "": if post_string != "":
post_data = getFormData(post_string, is_json) post_data = getFormData(post_string, is_json)

View file

@ -356,4 +356,14 @@ select.disabled-select-enabled {
#toggle-auto-refresh[data-enabled="true"] { #toggle-auto-refresh[data-enabled="true"] {
@apply bg-green-500 hover:bg-green-600 focus:ring-green-300; @apply bg-green-500 hover:bg-green-600 focus:ring-green-300;
} }
[data-popper-placement] {
will-change: transform;
transform: translateZ(0);
}
.tooltip {
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
}

View file

@ -102,7 +102,7 @@ const WebSocketManager = {
}, },
initialize() { initialize() {
console.log('🚀 Initializing WebSocket Manager'); console.log('Initializing WebSocket Manager');
this.setupPageVisibilityHandler(); this.setupPageVisibilityHandler();
this.connect(); this.connect();
this.startHealthCheck(); this.startHealthCheck();
@ -152,7 +152,7 @@ const WebSocketManager = {
performHealthCheck() { performHealthCheck() {
if (!this.isConnected()) { if (!this.isConnected()) {
console.warn('🏥 Health check: Connection lost, attempting reconnect'); console.warn('Health check: Connection lost, attempting reconnect');
this.handleReconnect(); this.handleReconnect();
return; return;
} }
@ -160,13 +160,13 @@ const WebSocketManager = {
const now = Date.now(); const now = Date.now();
const lastCheck = this.connectionState.lastHealthCheck; const lastCheck = this.connectionState.lastHealthCheck;
if (lastCheck && (now - lastCheck) > 60000) { if (lastCheck && (now - lastCheck) > 60000) {
console.warn('🏥 Health check: Connection stale, refreshing'); console.warn('Health check: Connection stale, refreshing');
this.handleReconnect(); this.handleReconnect();
return; return;
} }
this.connectionState.lastHealthCheck = now; this.connectionState.lastHealthCheck = now;
console.log('Health check passed'); console.log('Health check passed');
}, },
connect() { connect() {
@ -183,7 +183,7 @@ const WebSocketManager = {
const wsPort = config.port || window.ws_port || '11700'; const wsPort = config.port || window.ws_port || '11700';
if (!wsPort) { if (!wsPort) {
console.error('WebSocket port not configured'); console.error('WebSocket port not configured');
this.connectionState.isConnecting = false; this.connectionState.isConnecting = false;
return false; return false;
} }
@ -201,7 +201,7 @@ const WebSocketManager = {
return true; return true;
} catch (error) { } catch (error) {
console.error('Error creating WebSocket:', error); console.error('Error creating WebSocket:', error);
this.connectionState.isConnecting = false; this.connectionState.isConnecting = false;
this.handleReconnect(); this.handleReconnect();
return false; return false;
@ -226,13 +226,13 @@ const WebSocketManager = {
const message = JSON.parse(event.data); const message = JSON.parse(event.data);
this.handleMessage(message); this.handleMessage(message);
} catch (error) { } catch (error) {
console.error('Error processing WebSocket message:', error); console.error('Error processing WebSocket message:', error);
updateConnectionStatus('error'); updateConnectionStatus('error');
} }
}; };
this.ws.onerror = (error) => { this.ws.onerror = (error) => {
console.error('WebSocket error:', error); console.error('WebSocket error:', error);
updateConnectionStatus('error'); updateConnectionStatus('error');
}; };
@ -250,7 +250,7 @@ const WebSocketManager = {
handleMessage(message) { handleMessage(message) {
if (this.messageQueue.length >= this.maxQueueSize) { if (this.messageQueue.length >= this.maxQueueSize) {
console.warn('⚠ Message queue full, dropping oldest message'); console.warn('⚠Message queue full, dropping oldest message');
this.messageQueue.shift(); this.messageQueue.shift();
} }
@ -286,7 +286,7 @@ const WebSocketManager = {
this.messageQueue = []; this.messageQueue = [];
} catch (error) { } catch (error) {
console.error('Error processing message queue:', error); console.error('Error processing message queue:', error);
} finally { } finally {
this.processingQueue = false; this.processingQueue = false;
} }
@ -299,7 +299,7 @@ const WebSocketManager = {
this.reconnectAttempts++; this.reconnectAttempts++;
if (this.reconnectAttempts <= this.maxReconnectAttempts) { if (this.reconnectAttempts <= this.maxReconnectAttempts) {
console.log(`🔄 Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})`); console.log(`Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
const delay = Math.min( const delay = Math.min(
this.reconnectDelay * Math.pow(1.5, this.reconnectAttempts - 1), this.reconnectDelay * Math.pow(1.5, this.reconnectAttempts - 1),
@ -312,7 +312,7 @@ const WebSocketManager = {
} }
}, delay); }, delay);
} else { } else {
console.error('Max reconnection attempts reached'); console.error('Max reconnection attempts reached');
updateConnectionStatus('error'); updateConnectionStatus('error');
setTimeout(() => { setTimeout(() => {
@ -323,7 +323,7 @@ const WebSocketManager = {
}, },
cleanup() { cleanup() {
console.log('🧹 Cleaning up WebSocket resources'); console.log('Cleaning up WebSocket resources');
clearTimeout(this.debounceTimeout); clearTimeout(this.debounceTimeout);
clearTimeout(this.reconnectTimeout); clearTimeout(this.reconnectTimeout);
@ -723,7 +723,7 @@ function checkOfferAgainstFilters(offer, filters) {
function initializeFlowbiteTooltips() { function initializeFlowbiteTooltips() {
if (typeof Tooltip === 'undefined') { if (typeof Tooltip === 'undefined') {
//console.warn('Tooltip is not defined. Make sure the required library is loaded.'); console.warn('Tooltip is not defined. Make sure the required library is loaded.');
return; return;
} }
@ -824,6 +824,17 @@ function filterAndSortData() {
let filteredData = [...originalJsonData]; let filteredData = [...originalJsonData];
const sentFromFilter = filters.sent_from || 'any';
filteredData = filteredData.filter(offer => {
if (sentFromFilter === 'public') {
return offer.is_public;
} else if (sentFromFilter === 'private') {
return !offer.is_public;
}
return true;
});
filteredData = filteredData.filter(offer => { filteredData = filteredData.filter(offer => {
if (!isSentOffers && isOfferExpired(offer)) { if (!isSentOffers && isOfferExpired(offer)) {
return false; return false;
@ -1032,7 +1043,7 @@ async function fetchLatestPrices() {
} }
if (data && Object.keys(data).length > 0) { if (data && Object.keys(data).length > 0) {
console.log('Fresh price data received'); console.log('Fresh price data received');
latestPrices = data; latestPrices = data;
@ -1047,7 +1058,7 @@ async function fetchLatestPrices() {
//console.warn('Received empty price data'); //console.warn('Received empty price data');
} }
} catch (error) { } catch (error) {
//console.error('Error fetching prices:', error); //console.error('Error fetching prices:', error);
throw error; throw error;
} }
@ -1055,36 +1066,33 @@ async function fetchLatestPrices() {
} }
async function fetchOffers(manualRefresh = false) { async function fetchOffers(manualRefresh = false) {
const refreshButton = document.getElementById('refreshOffers'); const refreshButton = document.getElementById('refreshOffers');
const refreshIcon = document.getElementById('refreshIcon'); const refreshIcon = document.getElementById('refreshIcon');
const refreshText = document.getElementById('refreshText'); const refreshText = document.getElementById('refreshText');
refreshButton.disabled = true; try {
refreshIcon.classList.add('animate-spin'); refreshButton.disabled = true;
refreshText.textContent = 'Refreshing...'; refreshIcon.classList.add('animate-spin');
refreshButton.classList.add('opacity-75', 'cursor-wait'); refreshText.textContent = 'Refreshing...';
refreshButton.classList.add('opacity-75', 'cursor-wait');
try {
const endpoint = isSentOffers ? '/json/sentoffers' : '/json/offers'; const endpoint = isSentOffers ? '/json/sentoffers' : '/json/offers';
const response = await fetch(endpoint); const response = await fetch(endpoint);
const data = await response.json(); const data = await response.json();
jsonData = formatInitialData(data); jsonData = formatInitialData(data);
originalJsonData = [...jsonData]; originalJsonData = [...jsonData];
await updateOffersTable(); await updateOffersTable();
updateJsonView(); updateJsonView();
updatePaginationInfo(); updatePaginationInfo();
} catch (error) { } catch (error) {
//console.error('[Debug] Error fetching offers:', error); console.error('[Debug] Error fetching offers:', error);
ui.displayErrorMessage('Failed to fetch offers. Please try again later.'); ui.displayErrorMessage('Failed to fetch offers. Please try again later.');
} finally { } finally {
refreshButton.disabled = false; stopRefreshAnimation();
refreshIcon.classList.remove('animate-spin'); }
refreshText.textContent = 'Refresh';
refreshButton.classList.remove('opacity-75', 'cursor-wait');
}
} }
function formatInitialData(data) { function formatInitialData(data) {
@ -1092,6 +1100,7 @@ function formatInitialData(data) {
offer_id: String(offer.offer_id || ''), offer_id: String(offer.offer_id || ''),
swap_type: String(offer.swap_type || 'N/A'), swap_type: String(offer.swap_type || 'N/A'),
addr_from: String(offer.addr_from || ''), addr_from: String(offer.addr_from || ''),
addr_to: String(offer.addr_to || ''),
coin_from: String(offer.coin_from || ''), coin_from: String(offer.coin_from || ''),
coin_to: String(offer.coin_to || ''), coin_to: String(offer.coin_to || ''),
amount_from: String(offer.amount_from || '0'), amount_from: String(offer.amount_from || '0'),
@ -1102,6 +1111,7 @@ function formatInitialData(data) {
is_own_offer: Boolean(offer.is_own_offer), is_own_offer: Boolean(offer.is_own_offer),
amount_negotiable: Boolean(offer.amount_negotiable), amount_negotiable: Boolean(offer.amount_negotiable),
is_revoked: Boolean(offer.is_revoked), is_revoked: Boolean(offer.is_revoked),
is_public: offer.is_public !== undefined ? Boolean(offer.is_public) : false,
unique_id: `${offer.offer_id}_${offer.created_at}_${offer.coin_from}_${offer.coin_to}` unique_id: `${offer.offer_id}_${offer.created_at}_${offer.coin_from}_${offer.coin_to}`
})); }));
} }
@ -1173,6 +1183,23 @@ function updateLastRefreshTime() {
} }
} }
function stopRefreshAnimation() {
const refreshButton = document.getElementById('refreshOffers');
const refreshIcon = document.getElementById('refreshIcon');
const refreshText = document.getElementById('refreshText');
if (refreshButton) {
refreshButton.disabled = false;
refreshButton.classList.remove('opacity-75', 'cursor-wait');
}
if (refreshIcon) {
refreshIcon.classList.remove('animate-spin');
}
if (refreshText) {
refreshText.textContent = 'Refresh';
}
}
function updatePaginationInfo() { function updatePaginationInfo() {
const validOffers = getValidOffers(); const validOffers = getValidOffers();
const totalItems = validOffers.length; const totalItems = validOffers.length;
@ -1280,8 +1307,18 @@ function updateCoinFilterImages() {
function updateClearFiltersButton() { function updateClearFiltersButton() {
const clearButton = document.getElementById('clearFilters'); const clearButton = document.getElementById('clearFilters');
if (clearButton) { if (clearButton) {
clearButton.classList.toggle('opacity-50', !hasActiveFilters()); const hasFilters = hasActiveFilters();
clearButton.disabled = !hasActiveFilters(); clearButton.classList.toggle('opacity-50', !hasFilters);
clearButton.disabled = !hasFilters;
// Update button styles based on state
if (hasFilters) {
clearButton.classList.add('hover:bg-green-600', 'hover:text-white');
clearButton.classList.remove('cursor-not-allowed');
} else {
clearButton.classList.remove('hover:bg-green-600', 'hover:text-white');
clearButton.classList.add('cursor-not-allowed');
}
} }
} }
@ -1292,30 +1329,31 @@ function handleNoOffersScenario() {
filters.coin_from !== 'any' || filters.coin_from !== 'any' ||
(filters.status && filters.status !== 'any'); (filters.status && filters.status !== 'any');
stopRefreshAnimation();
if (hasActiveFilters) { if (hasActiveFilters) {
offersBody.innerHTML = ` offersBody.innerHTML = `
<tr> <tr>
<td colspan="8" class="text-center py-4 text-gray-500 dark:text-white"> <td colspan="9" class="text-center py-8">
No offers match the selected filters. Try different filter options or <div class="flex items-center justify-center text-gray-500 dark:text-white">
<button onclick="clearFilters()" class="text-blue-500 hover:text-blue-700 bold">clear filters</button> No offers match the selected filters. Try different filter options or
<button onclick="clearFilters()" class="ml-1 text-blue-500 hover:text-blue-700 font-semibold">
clear filters
</button>
</div>
</td> </td>
</tr>`; </tr>`;
} else { } else {
offersBody.innerHTML = ` offersBody.innerHTML = `
<tr> <tr>
<td colspan="8" class="text-center py-4 text-gray-500 dark:text-white"> <td colspan="9" class="text-center py-8 text-gray-500 dark:text-white">
No active offers available. ${!isSentOffers ? 'Refreshing data...' : ''} No active offers available.
</td> </td>
</tr>`; </tr>`;
if (!isSentOffers) {
setTimeout(() => fetchOffers(true), 2000);
}
} }
} }
async function updateOffersTable() { async function updateOffersTable() {
//console.log('[Debug] Starting updateOffersTable function');
try { try {
const PRICES_CACHE_KEY = 'prices_coingecko'; const PRICES_CACHE_KEY = 'prices_coingecko';
const cachedPrices = CacheManager.get(PRICES_CACHE_KEY); const cachedPrices = CacheManager.get(PRICES_CACHE_KEY);
@ -1323,27 +1361,25 @@ async function updateOffersTable() {
if (!cachedPrices || !cachedPrices.remainingTime || cachedPrices.remainingTime < 60000) { if (!cachedPrices || !cachedPrices.remainingTime || cachedPrices.remainingTime < 60000) {
console.log('Fetching fresh price data...'); console.log('Fetching fresh price data...');
const priceData = await fetchLatestPrices(); const priceData = await fetchLatestPrices();
if (!priceData) { if (priceData) {
//console.error('Failed to fetch latest prices');
} else {
console.log('Latest prices fetched successfully');
latestPrices = priceData; latestPrices = priceData;
} }
} else { } else {
console.log('Using cached price data (still valid)');
latestPrices = cachedPrices.value; latestPrices = cachedPrices.value;
} }
const totalOffers = originalJsonData.filter(offer => !isOfferExpired(offer)); const validOffers = getValidOffers();
const networkOffersCount = document.getElementById('network-offers-count'); const startIndex = (currentPage - 1) * itemsPerPage;
if (networkOffersCount && !isSentOffers) { const endIndex = Math.min(startIndex + itemsPerPage, validOffers.length);
networkOffersCount.textContent = totalOffers.length; const itemsToDisplay = validOffers.slice(startIndex, endIndex);
}
let validOffers = getValidOffers();
console.log('[Debug] Valid offers:', validOffers.length);
const identityPromises = itemsToDisplay.map(offer =>
offer.addr_from ? getIdentityData(offer.addr_from) : Promise.resolve(null)
);
const identities = await Promise.all(identityPromises);
if (validOffers.length === 0) { if (validOffers.length === 0) {
handleNoOffersScenario(); handleNoOffersScenario();
return; return;
@ -1351,15 +1387,12 @@ async function updateOffersTable() {
const totalPages = Math.max(1, Math.ceil(validOffers.length / itemsPerPage)); const totalPages = Math.max(1, Math.ceil(validOffers.length / itemsPerPage));
currentPage = Math.min(currentPage, totalPages); currentPage = Math.min(currentPage, totalPages);
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = Math.min(startIndex + itemsPerPage, validOffers.length);
const itemsToDisplay = validOffers.slice(startIndex, endIndex);
const fragment = document.createDocumentFragment(); const fragment = document.createDocumentFragment();
const currentOffers = new Set(); itemsToDisplay.forEach((offer, index) => {
itemsToDisplay.forEach(offer => { const identity = identities[index];
const row = createTableRow(offer, isSentOffers); const row = createTableRow(offer, identity);
if (row) { if (row) {
fragment.appendChild(row); fragment.appendChild(row);
} }
@ -1380,22 +1413,14 @@ async function updateOffersTable() {
lastRefreshTime = Date.now(); lastRefreshTime = Date.now();
if (newEntriesCountSpan) { if (newEntriesCountSpan) {
const displayCount = isSentOffers ? jsonData.length : validOffers.length; newEntriesCountSpan.textContent = validOffers.length;
newEntriesCountSpan.textContent = displayCount;
} }
if (lastRefreshTimeSpan) { if (lastRefreshTimeSpan) {
lastRefreshTimeSpan.textContent = new Date(lastRefreshTime).toLocaleTimeString(); lastRefreshTimeSpan.textContent = new Date(lastRefreshTime).toLocaleTimeString();
} }
if (!isSentOffers) {
const nextUpdateTime = getTimeUntilNextExpiration() * 1000;
setTimeout(() => {
updateRowTimes();
}, nextUpdateTime);
}
} catch (error) { } catch (error) {
//console.error('[Debug] Error in updateOffersTable:', error); console.error('[Debug] Error in updateOffersTable:', error);
offersBody.innerHTML = ` offersBody.innerHTML = `
<tr> <tr>
<td colspan="8" class="text-center py-4 text-red-500"> <td colspan="8" class="text-center py-4 text-red-500">
@ -1405,46 +1430,124 @@ async function updateOffersTable() {
} }
} }
function createTableRow(offer, isSentOffers) { async function getIdentityData(address) {
try {
const response = await fetch(`/json/identities/${address}`);
if (!response.ok) {
return null;
}
return await response.json();
} catch (error) {
console.error('Error fetching identity:', error);
return null;
}
}
function getIdentityInfo(address, identity) {
if (!identity) {
return {
displayAddr: address ? `${address.substring(0, 10)}...` : 'Unspecified',
fullAddress: address || '',
label: '',
note: '',
automationOverride: 0,
stats: {
sentBidsSuccessful: 0,
recvBidsSuccessful: 0,
sentBidsRejected: 0,
recvBidsRejected: 0,
sentBidsFailed: 0,
recvBidsFailed: 0
}
};
}
return {
displayAddr: address ? `${address.substring(0, 10)}...` : 'Unspecified',
fullAddress: address || '',
label: identity.label || '',
note: identity.note || '',
automationOverride: identity.automation_override || 0,
stats: {
sentBidsSuccessful: identity.num_sent_bids_successful || 0,
recvBidsSuccessful: identity.num_recv_bids_successful || 0,
sentBidsRejected: identity.num_sent_bids_rejected || 0,
recvBidsRejected: identity.num_recv_bids_rejected || 0,
sentBidsFailed: identity.num_sent_bids_failed || 0,
recvBidsFailed: identity.num_recv_bids_failed || 0
}
};
}
function createTableRow(offer, identity = null) {
const row = document.createElement('tr'); const row = document.createElement('tr');
const uniqueId = `${offer.offer_id}_${offer.created_at}`; const uniqueId = `${offer.offer_id}_${offer.created_at}`;
row.className = `opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600`;
row.className = 'relative opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600';
row.setAttribute('data-offer-id', uniqueId); row.setAttribute('data-offer-id', uniqueId);
const coinFrom = offer.coin_from; const {
const coinTo = offer.coin_to; coin_from: coinFrom,
coin_to: coinTo,
created_at: createdAt,
expire_at: expireAt,
amount_from: amountFrom,
amount_to: amountTo,
is_own_offer: isOwnOffer,
is_revoked: isRevoked,
is_public: isPublic
} = offer;
const coinFromSymbol = coinNameToSymbol[coinFrom] || coinFrom.toLowerCase(); const coinFromSymbol = coinNameToSymbol[coinFrom] || coinFrom.toLowerCase();
const coinToSymbol = coinNameToSymbol[coinTo] || coinTo.toLowerCase(); const coinToSymbol = coinNameToSymbol[coinTo] || coinTo.toLowerCase();
const coinFromDisplay = getDisplayName(coinFrom); const coinFromDisplay = getDisplayName(coinFrom);
const coinToDisplay = getDisplayName(coinTo); const coinToDisplay = getDisplayName(coinTo);
const postedTime = formatTime(createdAt, true);
const postedTime = formatTime(offer.created_at, true); const expiresIn = formatTime(expireAt);
const expiresIn = formatTime(offer.expire_at);
const currentTime = Math.floor(Date.now() / 1000); const currentTime = Math.floor(Date.now() / 1000);
const isActuallyExpired = currentTime > offer.expire_at; const isActuallyExpired = currentTime > expireAt;
const fromAmount = parseFloat(amountFrom) || 0;
const fromAmount = parseFloat(offer.amount_from) || 0; const toAmount = parseFloat(amountTo) || 0;
const toAmount = parseFloat(offer.amount_to) || 0;
// Build row content
row.innerHTML = ` row.innerHTML = `
${!isPublic ? createPrivateIndicator() : '<td class="w-0 p-0 m-0"></td>'}
${createTimeColumn(offer, postedTime, expiresIn)} ${createTimeColumn(offer, postedTime, expiresIn)}
${createDetailsColumn(offer)} ${createDetailsColumn(offer, identity)}
${createTakerAmountColumn(offer, coinTo, coinFrom)} ${createTakerAmountColumn(offer, coinTo, coinFrom)}
${createSwapColumn(offer, coinFromDisplay, coinToDisplay, coinFromSymbol, coinToSymbol)} ${createSwapColumn(offer, coinFromDisplay, coinToDisplay, coinFromSymbol, coinToSymbol)}
${createOrderbookColumn(offer, coinFrom, coinTo)} ${createOrderbookColumn(offer, coinFrom, coinTo)}
${createRateColumn(offer, coinFrom, coinTo)} ${createRateColumn(offer, coinFrom, coinTo)}
${createPercentageColumn(offer)} ${createPercentageColumn(offer)}
${createActionColumn(offer, isActuallyExpired)} ${createActionColumn(offer, isActuallyExpired)}
${createTooltips(offer, offer.is_own_offer, coinFrom, coinTo, fromAmount, toAmount, postedTime, expiresIn, isActuallyExpired, Boolean(offer.is_revoked))} ${createTooltips(
offer,
isOwnOffer,
coinFrom,
coinTo,
fromAmount,
toAmount,
postedTime,
expiresIn,
isActuallyExpired,
Boolean(isRevoked),
identity
)}
`; `;
updateTooltipTargets(row, uniqueId); updateTooltipTargets(row, uniqueId);
updateProfitLoss(row, coinFrom, coinTo, fromAmount, toAmount, offer.is_own_offer); updateProfitLoss(row, coinFrom, coinTo, fromAmount, toAmount, isOwnOffer);
return row; return row;
} }
function createPrivateIndicator() {
return `<td class="relative w-0 p-0 m-0">
<div class="absolute top-0 bottom-0 left-0 w-1 bg-red-700" style="min-height: 100%;"></div>
</td>`;
}
function createTimeColumn(offer, postedTime, expiresIn) { function createTimeColumn(offer, postedTime, expiresIn) {
const now = Math.floor(Date.now() / 1000); const now = Math.floor(Date.now() / 1000);
const timeLeft = offer.expire_at - now; const timeLeft = offer.expire_at - now;
@ -1457,10 +1560,10 @@ function createTimeColumn(offer, postedTime, expiresIn) {
} }
return ` return `
<td class="py-3 pl-6 text-xs"> <td class="py-3 pl-1 pr-2 text-xs whitespace-nowrap">
<div class="flex items-center"> <div class="flex items-center">
<div class="relative" data-tooltip-target="tooltip-active${escapeHtml(offer.offer_id)}"> <div class="relative" data-tooltip-target="tooltip-active${escapeHtml(offer.offer_id)}">
<svg alt="" class="w-5 h-5 rounded-full mr-3 cursor-pointer" xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24"> <svg alt="" class="w-5 h-5 rounded-full mr-4 cursor-pointer" xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24">
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="${strokeColor}" stroke-linejoin="round"> <g stroke-linecap="round" stroke-width="2" fill="none" stroke="${strokeColor}" stroke-linejoin="round">
<circle cx="12" cy="12" r="11"></circle> <circle cx="12" cy="12" r="11"></circle>
<polyline points="12,6 12,12 18,12" stroke="${strokeColor}"></polyline> <polyline points="12,6 12,12 18,12" stroke="${strokeColor}"></polyline>
@ -1468,21 +1571,59 @@ function createTimeColumn(offer, postedTime, expiresIn) {
</svg> </svg>
</div> </div>
<div class="flex flex-col hidden xl:block"> <div class="flex flex-col hidden xl:block">
<div class="text-xs"><span class="bold">Posted:</span> ${escapeHtml(postedTime)}</div> <div class="text-xs whitespace-nowrap"><span class="bold">Posted:</span> ${escapeHtml(postedTime)}</div>
<div class="text-xs"><span class="bold">Expires in:</span> ${escapeHtml(expiresIn)}</div> <div class="text-xs whitespace-nowrap"><span class="bold">Expires in:</span> ${escapeHtml(expiresIn)}</div>
</div> </div>
</div> </div>
</td> </td>
`; `;
} }
function createDetailsColumn(offer) { function shouldShowPublicTag(offers) {
return offers.some(offer => !offer.is_public);
}
function truncateText(text, maxLength = 15) {
if (typeof text !== 'string') return '';
return text.length > maxLength
? text.slice(0, maxLength) + '...'
: text;
}
function createDetailsColumn(offer, identity = null) {
const addrFrom = offer.addr_from || ''; const addrFrom = offer.addr_from || '';
const identityInfo = getIdentityInfo(addrFrom, identity);
const showPublicPrivateTags = originalJsonData.some(o => o.is_public !== offer.is_public);
const tagClass = offer.is_public
? 'bg-green-600 dark:bg-green-600'
: 'bg-red-500 dark:bg-red-500';
const tagText = offer.is_public ? 'Public' : 'Private';
const displayIdentifier = truncateText(
identityInfo.label || addrFrom || 'Unspecified'
);
const identifierTextClass = identityInfo.label
? 'text-white dark:text-white'
: 'monospace';
return ` return `
<td class="py-8 px-4 text-xs text-left hidden xl:block"> <td class="py-8 px-4 text-xs text-left hidden xl:block">
<a data-tooltip-target="tooltip-recipient${escapeHtml(offer.offer_id)}" href="/identity/${escapeHtml(addrFrom)}"> <div class="flex flex-col gap-2 relative">
<span class="bold">Recipient:</span> ${escapeHtml(addrFrom.substring(0, 10))}... ${showPublicPrivateTags ? `<span class="inline-flex pl-6 pr-6 py-1 justify-center text-[10px] w-1/4 font-medium text-gray-100 rounded-md ${tagClass}">${tagText}</span>
</a> ` : ''}
<a data-tooltip-target="tooltip-recipient-${escapeHtml(offer.offer_id)}" href="/identity/${escapeHtml(addrFrom)}" class="flex items-center">
<svg class="w-4 h-4 mr-2 text-gray-400 dark:text-white" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd"></path>
</svg>
<span class="${identifierTextClass} font-semibold">
${escapeHtml(displayIdentifier)}
</span>
</a>
</div>
</td> </td>
`; `;
} }
@ -1635,12 +1776,28 @@ function createActionColumn(offer, isActuallyExpired = false) {
} }
// TOOLTIP FUNCTIONS // TOOLTIP FUNCTIONS
function createTooltips(offer, treatAsSentOffer, coinFrom, coinTo, fromAmount, toAmount, postedTime, expiresIn, isActuallyExpired, isRevoked) { function createTooltips(offer, treatAsSentOffer, coinFrom, coinTo, fromAmount, toAmount, postedTime, expiresIn, isActuallyExpired, isRevoked, identity = null) {
const rate = parseFloat(offer.rate); const rate = parseFloat(offer.rate);
const fromSymbol = getCoinSymbolLowercase(coinFrom); const fromSymbol = getCoinSymbolLowercase(coinFrom);
const toSymbol = getCoinSymbolLowercase(coinTo); const toSymbol = getCoinSymbolLowercase(coinTo);
const uniqueId = `${offer.offer_id}_${offer.created_at}`; const uniqueId = `${offer.offer_id}_${offer.created_at}`;
const addrFrom = offer.addr_from || '';
const identityInfo = getIdentityInfo(addrFrom, identity);
const totalBids = identity ? (
identityInfo.stats.sentBidsSuccessful +
identityInfo.stats.recvBidsSuccessful +
identityInfo.stats.sentBidsFailed +
identityInfo.stats.recvBidsFailed +
identityInfo.stats.sentBidsRejected +
identityInfo.stats.recvBidsRejected
) : 0;
const successRate = totalBids ? (
((identityInfo.stats.sentBidsSuccessful + identityInfo.stats.recvBidsSuccessful) / totalBids) * 100
).toFixed(1) : 0;
const fromPriceUSD = latestPrices[fromSymbol]?.usd || 0; const fromPriceUSD = latestPrices[fromSymbol]?.usd || 0;
const toPriceUSD = latestPrices[toSymbol]?.usd || 0; const toPriceUSD = latestPrices[toSymbol]?.usd || 0;
const rateInUSD = rate * toPriceUSD; const rateInUSD = rate * toPriceUSD;
@ -1689,13 +1846,8 @@ function createTooltips(offer, treatAsSentOffer, coinFrom, coinTo, fromAmount, t
</div> </div>
<div class="tooltip-arrow" data-popper-arrow></div> <div class="tooltip-arrow" data-popper-arrow></div>
</div> </div>
<div id="tooltip-recipient-${uniqueId}" role="tooltip" class="inline-block absolute invisible z-50 py-2 px-3 text-sm font-medium text-white bg-gray-400 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">
<div class="active-revoked-expired"><span class="bold monospace">${offer.addr_from}</span></div>
<div class="tooltip-arrow" data-popper-arrow></div>
</div>
<div id="tooltip-wallet-${uniqueId}" role="tooltip" class="inline-block absolute invisible z-50 py-2 px-3 text-sm font-medium text-white bg-blue-500 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip"> <div id="tooltip-wallet-${uniqueId}" role="tooltip" class="inline-block absolute invisible z-50 py-2 px-3 text-sm font-medium text-white bg-gray-400 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">
<div class="active-revoked-expired"> <div class="active-revoked-expired">
<span class="bold">${treatAsSentOffer ? 'My' : ''} ${coinTo} Wallet</span> <span class="bold">${treatAsSentOffer ? 'My' : ''} ${coinTo} Wallet</span>
</div> </div>
@ -1711,7 +1863,7 @@ function createTooltips(offer, treatAsSentOffer, coinFrom, coinTo, fromAmount, t
<div class="tooltip-arrow pr-6" data-popper-arrow></div> <div class="tooltip-arrow pr-6" data-popper-arrow></div>
</div> </div>
<div id="tooltip-wallet-maker-${uniqueId}" role="tooltip" class="inline-block absolute invisible z-50 py-2 px-3 text-sm font-medium text-white bg-blue-500 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip"> <div id="tooltip-wallet-maker-${uniqueId}" role="tooltip" class="inline-block absolute invisible z-50 py-2 px-3 text-sm font-medium text-white bg-gray-400 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip">
<div class="active-revoked-expired"> <div class="active-revoked-expired">
<span class="bold">${treatAsSentOffer ? 'My' : ''} ${coinFrom} Wallet</span> <span class="bold">${treatAsSentOffer ? 'My' : ''} ${coinFrom} Wallet</span>
</div> </div>
@ -1731,9 +1883,92 @@ function createTooltips(offer, treatAsSentOffer, coinFrom, coinTo, fromAmount, t
</div> </div>
<div class="tooltip-arrow" data-popper-arrow></div> <div class="tooltip-arrow" data-popper-arrow></div>
</div> </div>
${createRecipientTooltip(uniqueId, identityInfo, identity, successRate, totalBids)}
`; `;
} }
function createRecipientTooltip(uniqueId, identityInfo, identity, successRate, totalBids) {
const getSuccessRateColor = (rate) => {
if (rate >= 80) return 'text-green-600';
if (rate >= 60) return 'text-yellow-600';
return 'text-red-600';
};
const truncateText = (text, maxLength) => {
if (text.length <= maxLength) return text;
return text.substring(0, maxLength) + '...';
};
return `
<div id="tooltip-recipient-${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 max-w-sm pointer-events-none">
<div class="identity-info space-y-2">
${identityInfo.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">${escapeHtml(identityInfo.label)}</div>
</div>
` : ''}
<div class="space-y-1">
<div class="text-white text-xs tracking-wide font-semibold">Recipient Address:</div>
<div class="monospace text-xs break-all bg-gray-500 p-2 rounded-md text-white">
${escapeHtml(identityInfo.fullAddress)}
</div>
</div>
${identityInfo.note ? `
<div class="space-y-1 hidden">
<div class="text-white text-xs tracking-wide font-semibold">Note:</div>
<div class="text-white text-sm italic" title="${escapeHtml(identityInfo.note)}">
${escapeHtml(truncateText(identityInfo.note, 150))}
</div>
</div>
` : ''}
${identity ? `
<div class="border-t border-gray-400 pt-2 mt-2">
<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="text-center p-2 bg-gray-500 rounded-md">
<div class="text-lg font-bold ${getSuccessRateColor(successRate)}">${successRate}%</div>
<div class="text-xs text-white">Success Rate</div>
</div>
<div class="text-center p-2 bg-gray-500 rounded-md">
<div class="text-lg font-bold text-blue-500">${totalBids}</div>
<div class="text-xs text-white">Total Trades</div>
</div>
</div>
<div class="grid grid-cols-3 gap-2 mt-2 text-center text-xs">
<div>
<div class="text-green-600 font-semibold">
${identityInfo.stats.sentBidsSuccessful + identityInfo.stats.recvBidsSuccessful}
</div>
<div class="text-white">Successful</div>
</div>
<div>
<div class="text-yellow-600 font-semibold">
${identityInfo.stats.sentBidsRejected + identityInfo.stats.recvBidsRejected}
</div>
<div class="text-white">Rejected</div>
</div>
<div>
<div class="text-red-600 font-semibold">
${identityInfo.stats.sentBidsFailed + identityInfo.stats.recvBidsFailed}
</div>
<div class="text-white">Failed</div>
</div>
</div>
</div>
` : ''}
</div>
<div class="tooltip-arrow" data-popper-arrow></div>
</div>`;
}
function createTooltipContent(isSentOffers, coinFrom, coinTo, fromAmount, toAmount, isOwnOffer) { function createTooltipContent(isSentOffers, coinFrom, coinTo, fromAmount, toAmount, isOwnOffer) {
if (!coinFrom || !coinTo) { if (!coinFrom || !coinTo) {
//console.error(`Invalid coin names: coinFrom=${coinFrom}, coinTo=${coinTo}`); //console.error(`Invalid coin names: coinFrom=${coinFrom}, coinTo=${coinTo}`);
@ -1890,19 +2125,31 @@ function applyFilters() {
filterTimeout = null; filterTimeout = null;
}, 250); }, 250);
} catch (error) { } catch (error) {
//console.error('Error in filter timeout:', error); console.error('Error in filter timeout:', error);
filterTimeout = null; filterTimeout = null;
} }
} }
function clearFilters() { function clearFilters() {
filterForm.reset(); filterForm.reset();
const selectElements = filterForm.querySelectorAll('select');
selectElements.forEach(select => {
select.value = 'any';
// Trigger change event
const event = new Event('change', { bubbles: true });
select.dispatchEvent(event);
});
const statusSelect = document.getElementById('status'); const statusSelect = document.getElementById('status');
if (statusSelect) { if (statusSelect) {
statusSelect.value = 'any'; statusSelect.value = 'any';
} }
jsonData = [...originalJsonData]; jsonData = [...originalJsonData];
currentPage = 1; currentPage = 1;
updateOffersTable(); updateOffersTable();
updateJsonView(); updateJsonView();
updateCoinFilterImages(); updateCoinFilterImages();
@ -1916,26 +2163,18 @@ function hasActiveFilters() {
coin_from: formData.get('coin_from'), coin_from: formData.get('coin_from'),
status: formData.get('status') status: formData.get('status')
}; };
//console.log('Current filters:', filters);
const hasFilters =
filters.coin_to !== 'any' ||
filters.coin_from !== 'any' ||
(filters.status && filters.status !== 'any');
//console.log('Has active filters:', hasFilters);
return hasFilters;
}
function getActiveFilters() { const selectElements = filterForm.querySelectorAll('select');
const formData = new FormData(filterForm); let hasChangedFilters = false;
return {
coin_to: formData.get('coin_to'), selectElements.forEach(select => {
coin_from: formData.get('coin_from'), if (select.value !== 'any') {
status: formData.get('status') hasChangedFilters = true;
}; }
} });
return hasChangedFilters;
}
// UTILITY FUNCTIONS // UTILITY FUNCTIONS
function formatTimeLeft(timestamp) { function formatTimeLeft(timestamp) {
const now = Math.floor(Date.now() / 1000); const now = Math.floor(Date.now() / 1000);
@ -2075,7 +2314,6 @@ function getCoinSymbol(fullName) {
} }
// EVENT LISTENERS // EVENT LISTENERS
document.querySelectorAll('th[data-sortable="true"]').forEach(header => { document.querySelectorAll('th[data-sortable="true"]').forEach(header => {
header.addEventListener('click', () => { header.addEventListener('click', () => {
const columnIndex = parseInt(header.getAttribute('data-column-index')); const columnIndex = parseInt(header.getAttribute('data-column-index'));
@ -2192,12 +2430,24 @@ const timerManager = {
}; };
// INITIALIZATION AND EVENT BINDING // INITIALIZATION AND EVENT BINDING
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
//console.log('DOM content loaded, initializing...'); //console.log('DOM content loaded, initializing...');
console.log('View type:', isSentOffers ? 'sent offers' : 'received offers'); console.log('View type:', isSentOffers ? 'sent offers' : 'received offers');
updateClearFiltersButton(); updateClearFiltersButton();
// Add event listeners for filter controls
const selectElements = filterForm.querySelectorAll('select');
selectElements.forEach(select => {
select.addEventListener('change', () => {
updateClearFiltersButton();
});
});
filterForm.addEventListener('change', () => {
applyFilters();
updateClearFiltersButton();
});
setTimeout(() => { setTimeout(() => {
console.log('Starting WebSocket initialization...'); console.log('Starting WebSocket initialization...');
@ -2215,7 +2465,7 @@ document.addEventListener('DOMContentLoaded', () => {
clearInterval(retryInterval); clearInterval(retryInterval);
continueInitialization(); continueInitialization();
} else if (retryCount >= maxRetries) { } else if (retryCount >= maxRetries) {
//console.error('Failed to load tableRateModule after multiple attempts'); //console.error('Failed to load tableRateModule after multiple attempts');
clearInterval(retryInterval); clearInterval(retryInterval);
continueInitialization(); continueInitialization();
} }
@ -2255,47 +2505,47 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
eventListeners.add(document.getElementById('refreshOffers'), 'click', async () => { eventListeners.add(document.getElementById('refreshOffers'), 'click', async () => {
console.log('Manual refresh initiated'); console.log('Manual refresh initiated');
const refreshButton = document.getElementById('refreshOffers'); const refreshButton = document.getElementById('refreshOffers');
const refreshIcon = document.getElementById('refreshIcon'); const refreshIcon = document.getElementById('refreshIcon');
const refreshText = document.getElementById('refreshText'); const refreshText = document.getElementById('refreshText');
refreshButton.disabled = true; refreshButton.disabled = true;
refreshIcon.classList.add('animate-spin'); refreshIcon.classList.add('animate-spin');
refreshText.textContent = 'Refreshing...'; refreshText.textContent = 'Refreshing...';
refreshButton.classList.add('opacity-75', 'cursor-wait'); refreshButton.classList.add('opacity-75', 'cursor-wait');
try { try {
const endpoint = isSentOffers ? '/json/sentoffers' : '/json/offers'; const endpoint = isSentOffers ? '/json/sentoffers' : '/json/offers';
const response = await fetch(endpoint); const response = await fetch(endpoint);
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
}
const newData = await response.json();
const processedNewData = Array.isArray(newData) ? newData : Object.values(newData);
console.log('Fetched offers:', processedNewData.length);
jsonData = formatInitialData(processedNewData);
originalJsonData = [...jsonData];
await updateOffersTable();
updateJsonView();
updatePaginationInfo();
console.log(' Manual refresh completed successfully');
} catch (error) {
console.error('Error during manual refresh:', error);
ui.displayErrorMessage('Failed to refresh offers. Please try again later.');
} finally {
refreshButton.disabled = false;
refreshIcon.classList.remove('animate-spin');
refreshText.textContent = 'Refresh';
refreshButton.classList.remove('opacity-75', 'cursor-wait');
} }
const newData = await response.json(); });
const processedNewData = Array.isArray(newData) ? newData : Object.values(newData);
console.log('Fetched offers:', processedNewData.length);
jsonData = formatInitialData(processedNewData);
originalJsonData = [...jsonData];
await updateOffersTable();
updateJsonView();
updatePaginationInfo();
console.log('✅ Manual refresh completed successfully');
} catch (error) {
console.error('❌ Error during manual refresh:', error);
ui.displayErrorMessage('Failed to refresh offers. Please try again later.');
} finally {
refreshButton.disabled = false;
refreshIcon.classList.remove('animate-spin');
refreshText.textContent = 'Refresh';
refreshButton.classList.remove('opacity-75', 'cursor-wait');
}
});
eventListeners.add(prevPageButton, 'click', () => { eventListeners.add(prevPageButton, 'click', () => {
if (currentPage > 1) { if (currentPage > 1) {
@ -2332,7 +2582,7 @@ document.addEventListener('DOMContentLoaded', () => {
//console.log('Initial offers fetched'); //console.log('Initial offers fetched');
applyFilters(); applyFilters();
}).catch(error => { }).catch(error => {
console.error('Error fetching initial offers:', error); console.error('Error fetching initial offers:', error);
}); });
const listingLabel = document.querySelector('span[data-listing-label]'); const listingLabel = document.querySelector('span[data-listing-label]');
@ -2351,7 +2601,7 @@ document.addEventListener('DOMContentLoaded', () => {
} }
}); });
console.log('Initialization completed'); console.log('Initialization completed');
}); });
console.log('Offers Table Module fully initialized'); console.log('Offers Table Module fully initialized');

View file

@ -86,7 +86,7 @@
<select class="pl-10 hover:border-blue-500 pl-10 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-white text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" name="addr_to"> <select class="pl-10 hover:border-blue-500 pl-10 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-white text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" name="addr_to">
<option{% if data.addr_to=="-1" %} selected{% endif %} value="-1">Public Network</option> <option{% if data.addr_to=="-1" %} selected{% endif %} value="-1">Public Network</option>
{% for a in addrs_to %} {% for a in addrs_to %}
<option{% if data.addr_to==a[0] %} selected{% endif %} value="{{ a[0] }}">{{ a[0] }} {{ a[1] }}</option> <option{% if data.addr_to==a[0] %} selected{% endif %} value="{{ a[0] }}">{{ a[0] }} ({{ a[1] }})</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
@ -471,7 +471,7 @@ if (document.readyState === 'loading') {
} }
function getRateInferred(event) { function getRateInferred(event) {
event.preventDefault(); // Prevent default form submission behavior event.preventDefault();
const coin_from = document.getElementById('coin_from').value; const coin_from = document.getElementById('coin_from').value;
const coin_to = document.getElementById('coin_to').value; const coin_to = document.getElementById('coin_to').value;
@ -558,7 +558,7 @@ if (document.readyState === 'loading') {
return; return;
} else } else
if (amt_from == '' && amt_to != '') { if (amt_from == '' && amt_to != '') {
if (value_changed == 'amt_from') { // Don't try and set a value just cleared if (value_changed == 'amt_from') {
return; return;
} }
params += '&rate=' + rate + '&amt_to=' + amt_to; params += '&rate=' + rate + '&amt_to=' + amt_to;

View file

@ -290,6 +290,18 @@ function getWebSocketConfig() {
{% endif %} {% endif %}
</div> </div>
</div> </div>
<div class="pt-3 px-3 md:w-auto hover-container">
<div class="flex">
<div class="relative">
{{ input_arrow_down_svg | safe }}
<select name="sent_from" id="sent_from" class="bg-gray-50 text-gray-900 appearance-none pr-10 pl-5 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none block w-full p-2.5 focus:ring-0">
<option value="any" {% if not filters.sent_from %} selected{% endif %}>All Offers</option>
<option value="public" {% if filters.sent_from == 'public' %} selected{% endif %}>Public</option>
<option value="private" {% if filters.sent_from == 'private' %} selected{% endif %}>Private</option>
</select>
</div>
</div>
</div>
<div class="w-full md:w-auto pt-3 px-3"> <div class="w-full md:w-auto pt-3 px-3">
<div class="relative"> <div class="relative">
<button type="button" id="clearFilters" class="transition-opacity duration-200 flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm hover:text-white dark:text-white dark:bg-gray-500 bg-coolGray-200 hover:bg-green-600 hover:border-green-600 rounded-lg transition duration-200 border border-coolGray-200 dark:border-gray-400 rounded-md shadow-button focus:ring-0 focus:outline-none" disabled> <button type="button" id="clearFilters" class="transition-opacity duration-200 flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm hover:text-white dark:text-white dark:bg-gray-500 bg-coolGray-200 hover:bg-green-600 hover:border-green-600 rounded-lg transition duration-200 border border-coolGray-200 dark:border-gray-400 rounded-md shadow-button focus:ring-0 focus:outline-none" disabled>
@ -322,15 +334,20 @@ function getWebSocketConfig() {
<div id="jsonView" class="hidden mb-4"> <div id="jsonView" class="hidden mb-4">
<pre id="jsonContent" class="bg-gray-100 p-4 rounded overflow-auto" style="max-height: 300px;"></pre> <pre id="jsonContent" class="bg-gray-100 p-4 rounded overflow-auto" style="max-height: 300px;"></pre>
</div> </div>
<div class="container mt-5 mx-auto"> <div class="container mt-5 mx-auto px-4">
<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">
<div class="px-0"> <div class="px-0">
<div class="w-auto mt-6 pb-6 overflow-x-auto"> <div class="w-auto mt-6 overflow-x-auto">
<table class="w-full min-w-max"> <table class="w-full min-w-max">
<thead class="uppercase"> <thead class="uppercase">
<tr> <tr>
<th class="p-0" data-sortable="true" data-column-index="0"> <th class="p-0" data-sortable="true" data-column-index="0">
<div class="py-3 pl-4 justify-center rounded-tl-xl bg-coolGray-200 dark:bg-gray-600"> <div class="py-3 pl-4 justify-center rounded-tl-xl bg-coolGray-200 dark:bg-gray-600">
<span class="text-sm mr-1 text-gray-600 dark:text-gray-300 font-semibold"></span>
</div>
</th>
<th class="p-0" data-sortable="true" data-column-index="0">
<div class="py-3 pl-4 justify-center bg-coolGray-200 dark:bg-gray-600">
<span class="text-sm mr-1 text-gray-600 dark:text-gray-300 font-semibold">Time</span> <span class="text-sm mr-1 text-gray-600 dark:text-gray-300 font-semibold">Time</span>
<span class="sort-icon ml-1 text-gray-600 dark:text-gray-400" id="sort-icon-0"></span> <span class="sort-icon ml-1 text-gray-600 dark:text-gray-400" id="sort-icon-0"></span>
</div> </div>
@ -399,19 +416,19 @@ function getWebSocketConfig() {
<div class="rounded-b-md"> <div class="rounded-b-md">
<div class="w-full"> <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 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">
<div class="flex items-center mr-4"> <div class="flex items-center mr-4">
<span id="status-dot" class="w-2.5 h-2.5 rounded-full bg-gray-500 mr-2"></span> <span id="status-dot" class="w-2.5 h-2.5 rounded-full bg-gray-500 mr-2"></span>
<span id="status-text" class="text-sm text-gray-500">Connecting...</span> <span id="status-text" class="text-sm text-gray-500">Connecting...</span>
</div> </div>
<p class="text-sm font-heading dark:text-gray-400 mr-4"> <p class="text-sm font-heading dark:text-gray-400 mr-4">
Last refreshed: <span id="lastRefreshTime">Never</span> Last refreshed: <span id="lastRefreshTime">Never</span>
</p> </p>
<p class="text-sm font-heading dark:text-gray-400 mr-4"> <p class="text-sm font-heading dark:text-gray-400 mr-4">
<span data-listing-label>Network Listings: </span> <span data-listing-label>Network Listings: </span>
<span id="newEntriesCount"></span> <span id="newEntriesCount"></span>
</p> </p>
</div> </div>
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<button type="button" id="prevPage" 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"> <button type="button" id="prevPage" 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">
{{ page_back_svg | safe }} {{ page_back_svg | safe }}

View file

@ -528,6 +528,15 @@ def page_newoffer(self, url_split, post_string):
coins_from, coins_to = listAvailableCoins(swap_client, split_from=True) coins_from, coins_to = listAvailableCoins(swap_client, split_from=True)
addrs_from_raw = swap_client.listSMSGAddresses("offer_send_from")
addrs_to_raw = swap_client.listSMSGAddresses("offer_send_to")
all_addresses = swap_client.listAllSMSGAddresses({})
addr_notes = {addr["addr"]: addr["note"] for addr in all_addresses}
addrs_from = [(addr[0], addr_notes.get(addr[0], "")) for addr in addrs_from_raw]
addrs_to = [(addr[0], addr_notes.get(addr[0], "")) for addr in addrs_to_raw]
automation_filters = {"type_ind": Concepts.OFFER, "sort_by": "label"} automation_filters = {"type_ind": Concepts.OFFER, "sort_by": "label"}
automation_strategies = swap_client.listAutomationStrategies(automation_filters) automation_strategies = swap_client.listAutomationStrategies(automation_filters)
@ -556,8 +565,8 @@ def page_newoffer(self, url_split, post_string):
"err_messages": err_messages, "err_messages": err_messages,
"coins_from": coins_from, "coins_from": coins_from,
"coins": coins_to, "coins": coins_to,
"addrs": swap_client.listSMSGAddresses("offer_send_from"), "addrs": addrs_from,
"addrs_to": swap_client.listSMSGAddresses("offer_send_to"), "addrs_to": addrs_to,
"data": page_data, "data": page_data,
"automation_strategies": automation_strategies, "automation_strategies": automation_strategies,
"summary": summary, "summary": summary,
@ -581,7 +590,7 @@ def page_offer(self, url_split, post_string):
offer, xmr_offer = swap_client.getXmrOffer(offer_id) offer, xmr_offer = swap_client.getXmrOffer(offer_id)
ensure(offer, "Unknown offer ID") ensure(offer, "Unknown offer ID")
extend_data = { # Defaults extend_data = {
"nb_validmins": 10, "nb_validmins": 10,
} }
messages = [] messages = []
@ -598,7 +607,6 @@ def page_offer(self, url_split, post_string):
reverse_bid: bool = True if offer.bid_reversed else False reverse_bid: bool = True if offer.bid_reversed else False
# Set defaults
debugind = -1 debugind = -1
bid_amount = ci_from.format_amount(offer.amount_from) bid_amount = ci_from.format_amount(offer.amount_from)
bid_rate = ci_to.format_amount(offer.rate) bid_rate = ci_to.format_amount(offer.rate)
@ -617,7 +625,6 @@ def page_offer(self, url_split, post_string):
except Exception as ex: except Exception as ex:
err_messages.append("Revoke offer failed: " + str(ex)) err_messages.append("Revoke offer failed: " + str(ex))
elif b"repeat_offer" in form_data: elif b"repeat_offer" in form_data:
# Can't set the post data here as browsers will always resend the original post data when responding to redirects
self.send_response(302) self.send_response(302)
self.send_header("Location", "/newoffer?offer_from=" + offer_id.hex()) self.send_header("Location", "/newoffer?offer_from=" + offer_id.hex())
self.end_headers() self.end_headers()