MyDU Market Browser

Advanced market analysis and trading opportunities

Checking system status...
Data age: Unknown
Markets: 0
Orders: 0
Market Name Planet Orders Distance Actions
Loading markets...
Item Type Quantity Price Market Player Expires
Loading orders...
Loading profit opportunities...
Planet Name Markets Distance from Origin Actions
Loading planets...
Loading statistics...
// Global state let currentTab = 'markets'; let currentData = { markets: { data: [], page: 1, totalPages: 1, sortBy: 'name', sortOrder: 'asc' }, orders: { data: [], page: 1, totalPages: 1, sortBy: 'unitPrice', sortOrder: 'asc' }, profits: { data: [], page: 1, totalPages: 1, sortBy: 'totalProfit', sortOrder: 'desc' }, planets: { data: [], sortBy: 'name', sortOrder: 'asc' } }; let systemStatus = null; // Initialize the application document.addEventListener('DOMContentLoaded', function() { console.log('Frontend initialized, loading data...'); loadSystemStatus(); loadPlanets(); loadMarkets(); }); // Tab Management function showTab(tabName) { // Update navigation document.querySelectorAll('.nav-tab').forEach(tab => tab.classList.remove('active')); document.querySelector(`[onclick="showTab('${tabName}')"]`).classList.add('active'); // Update content panels document.querySelectorAll('.content-panel').forEach(panel => panel.classList.remove('active')); document.getElementById(`${tabName}-panel`).classList.add('active'); currentTab = tabName; // Load data for the tab if not already loaded switch(tabName) { case 'markets': if (currentData.markets.data.length === 0) loadMarkets(); break; case 'orders': if (currentData.orders.data.length === 0) loadOrders(); break; case 'profits': if (currentData.profits.data.length === 0) loadProfits(); break; case 'planets': if (currentData.planets.data.length === 0) loadPlanets(); break; case 'statistics': loadStatistics(); break; } } // System Status Management async function loadSystemStatus() { try { console.log('Loading system status...'); const response = await fetch('/api/market/status'); console.log('Status response:', response.status); systemStatus = await response.json(); console.log('System status data:', systemStatus); updateStatusBar(); } catch (error) { console.error('Error loading system status:', error); updateStatusBar(true); } } function updateStatusBar(hasError = false) { const statusIndicator = document.getElementById('system-status'); const statusText = document.getElementById('system-status-text'); const dataAge = document.getElementById('data-age'); const marketCount = document.getElementById('market-count'); const orderCount = document.getElementById('order-count'); if (hasError || !systemStatus) { statusIndicator.className = 'status-indicator error'; statusText.textContent = 'System Error'; dataAge.textContent = 'Data age: Unknown'; marketCount.textContent = 'Markets: Unknown'; orderCount.textContent = 'Orders: Unknown'; return; } // Update status indicator if (systemStatus.isHealthy) { statusIndicator.className = 'status-indicator'; } else if (systemStatus.warnings && systemStatus.warnings.length > 0) { statusIndicator.className = 'status-indicator warning'; } else { statusIndicator.className = 'status-indicator error'; } // Update status text statusText.textContent = systemStatus.status || 'Unknown'; // Update data age if (systemStatus.dataAge) { const minutes = Math.floor(systemStatus.dataAge.totalMinutes || 0); dataAge.textContent = `Data age: ${minutes}m`; } // Update counts marketCount.textContent = `Markets: ${systemStatus.marketCount || 0}`; orderCount.textContent = `Orders: ${systemStatus.orderCount || 0}`; } async function refreshData() { try { const response = await fetch('/api/market/refresh', { method: 'POST' }); const result = await response.json(); if (response.ok) { showNotification('Data refresh completed successfully', 'success'); await loadSystemStatus(); // Reload current tab data switch(currentTab) { case 'markets': await loadMarkets(); break; case 'orders': await loadOrders(); break; case 'profits': await loadProfits(); break; case 'planets': await loadPlanets(); break; case 'statistics': await loadStatistics(); break; } } else { showNotification('Data refresh failed: ' + result.message, 'error'); } } catch (error) { console.error('Error refreshing data:', error); showNotification('Data refresh failed: Network error', 'error'); } } // Markets Management async function loadMarkets(page = 1) { const tableBody = document.getElementById('markets-table-body'); tableBody.innerHTML = ' Loading markets... '; try { const params = new URLSearchParams({ page: page, pageSize: 20, sortBy: currentData.markets.sortBy, sortOrder: currentData.markets.sortOrder }); // Add filters const marketSearch = document.getElementById('market-search')?.value; const planetFilter = document.getElementById('market-planet')?.value; if (marketSearch) params.append('marketName', marketSearch); if (planetFilter) params.append('planetName', planetFilter); const response = await fetch(`/api/market/markets?${params}`); const result = await response.json(); if (response.ok) { currentData.markets = { data: result.data, page: result.page, totalPages: result.totalPages, sortBy: currentData.markets.sortBy, sortOrder: currentData.markets.sortOrder }; renderMarketsTable(); renderPagination('markets', result); } else { tableBody.innerHTML = ` Error loading markets: ${result.error} `; } } catch (error) { console.error('Error loading markets:', error); tableBody.innerHTML = ' Network error loading markets '; } } function renderMarketsTable() { const tableBody = document.getElementById('markets-table-body'); if (currentData.markets.data.length === 0) { tableBody.innerHTML = ' No markets found '; return; } tableBody.innerHTML = currentData.markets.data.map(market => ` ${escapeHtml(market.name)} ${escapeHtml(market.planetName)} ${market.orderCount} ${formatDistance(market.distanceFromOrigin)} `).join(''); } function sortMarkets(column) { if (currentData.markets.sortBy === column) { currentData.markets.sortOrder = currentData.markets.sortOrder === 'asc' ? 'desc' : 'asc'; } else { currentData.markets.sortBy = column; currentData.markets.sortOrder = 'asc'; } updateSortHeaders('markets', column, currentData.markets.sortOrder); loadMarkets(currentData.markets.page); } function searchMarkets() { currentData.markets.page = 1; loadMarkets(1); } function clearMarketFilters() { document.getElementById('market-search').value = ''; document.getElementById('market-planet').value = ''; searchMarkets(); } function viewMarketOrders(marketId) { showTab('orders'); document.getElementById('order-market').value = currentData.markets.data.find(m => m.marketId === marketId)?.name || ''; searchOrders(); } // Orders Management async function loadOrders(page = 1) { const tableBody = document.getElementById('orders-table-body'); tableBody.innerHTML = ' Loading orders... '; try { const params = new URLSearchParams({ page: page, pageSize: 20, sortBy: currentData.orders.sortBy, sortOrder: currentData.orders.sortOrder }); // Add filters const itemName = document.getElementById('order-item')?.value; const orderType = document.getElementById('order-type')?.value; const marketName = document.getElementById('order-market')?.value; const minPrice = document.getElementById('order-min-price')?.value; const maxPrice = document.getElementById('order-max-price')?.value; const playerName = document.getElementById('order-player')?.value; if (itemName) params.append('itemName', itemName); if (orderType) params.append('orderType', orderType); if (marketName) params.append('marketName', marketName); if (minPrice) params.append('minPrice', minPrice); if (maxPrice) params.append('maxPrice', maxPrice); if (playerName) params.append('playerName', playerName); const response = await fetch(`/api/market/orders?${params}`); const result = await response.json(); if (response.ok) { currentData.orders = { data: result.data, page: result.page, totalPages: result.totalPages, sortBy: currentData.orders.sortBy, sortOrder: currentData.orders.sortOrder }; renderOrdersTable(); renderPagination('orders', result); } else { tableBody.innerHTML = ` Error loading orders: ${result.error} `; } } catch (error) { console.error('Error loading orders:', error); tableBody.innerHTML = ' Network error loading orders '; } } function renderOrdersTable() { const tableBody = document.getElementById('orders-table-body'); if (currentData.orders.data.length === 0) { tableBody.innerHTML = ' No orders found '; return; } tableBody.innerHTML = currentData.orders.data.map(order => ` ${escapeHtml(order.itemName)} ${order.orderType.toUpperCase()} ${formatNumber(order.quantity)} ${formatCurrency(order.unitPrice)} ${escapeHtml(order.marketName)}
${escapeHtml(order.planetName)} ${escapeHtml(order.playerName)} ${formatDate(order.expirationDate)} `).join(''); } function sortOrders(column) { if (currentData.orders.sortBy === column) { currentData.orders.sortOrder = currentData.orders.sortOrder === 'asc' ? 'desc' : 'asc'; } else { currentData.orders.sortBy = column; currentData.orders.sortOrder = column === 'unitPrice' ? 'asc' : 'asc'; } updateSortHeaders('orders', column, currentData.orders.sortOrder); loadOrders(currentData.orders.page); } function searchOrders() { currentData.orders.page = 1; loadOrders(1); } function clearOrderFilters() { document.getElementById('order-item').value = ''; document.getElementById('order-type').value = ''; document.getElementById('order-market').value = ''; document.getElementById('order-min-price').value = ''; document.getElementById('order-max-price').value = ''; document.getElementById('order-player').value = ''; searchOrders(); } // Profit Opportunities Management async function loadProfits(page = 1) { const container = document.getElementById('profits-container'); container.innerHTML = '
Loading profit opportunities...
'; try { const params = new URLSearchParams({ page: page, pageSize: 10, sortBy: currentData.profits.sortBy, sortOrder: currentData.profits.sortOrder }); // Add filters const itemName = document.getElementById('profit-item')?.value; const minMargin = document.getElementById('profit-min-margin')?.value; const maxDistance = document.getElementById('profit-max-distance')?.value; if (itemName) params.append('itemName', itemName); if (minMargin) params.append('minProfitMargin', parseFloat(minMargin) / 100); if (maxDistance) params.append('maxDistance', maxDistance); const response = await fetch(`/api/market/profits?${params}`); const result = await response.json(); if (response.ok) { currentData.profits = { data: result.data, page: result.page, totalPages: result.totalPages, sortBy: currentData.profits.sortBy, sortOrder: currentData.profits.sortOrder }; renderProfitsCards(); renderPagination('profits', result); } else { container.innerHTML = `
Error loading profit opportunities: ${result.error}
`; } } catch (error) { console.error('Error loading profits:', error); container.innerHTML = '
Network error loading profit opportunities
'; } } function renderProfitsCards() { const container = document.getElementById('profits-container'); if (currentData.profits.data.length === 0) { container.innerHTML = '
No profit opportunities found
'; return; } container.innerHTML = currentData.profits.data.map(profit => `
${formatCurrency({amount: profit.totalProfit})}
Total Profit
${(profit.profitMargin * 100).toFixed(1)}%
Margin
${formatNumber(profit.maxQuantity)}
Max Quantity
${formatDistance(profit.distance)}
Distance

${escapeHtml(profit.itemName)}

${formatCurrency({amount: profit.profitPerUnit})} per unit

Buy From

Market: ${escapeHtml(profit.buyOrder.marketName)}

Planet: ${escapeHtml(profit.buyOrder.planetName)}

Price: ${formatCurrency(profit.buyOrder.unitPrice)}

Quantity: ${formatNumber(profit.buyOrder.quantity)}

Player: ${escapeHtml(profit.buyOrder.playerName)}

Sell To

Market: ${escapeHtml(profit.sellOrder.marketName)}

Planet: ${escapeHtml(profit.sellOrder.planetName)}

Price: ${formatCurrency(profit.sellOrder.unitPrice)}

Quantity: ${formatNumber(profit.sellOrder.quantity)}

Player: ${escapeHtml(profit.sellOrder.playerName)}

`).join(''); } function searchProfits() { currentData.profits.page = 1; loadProfits(1); } function clearProfitFilters() { document.getElementById('profit-item').value = ''; document.getElementById('profit-min-margin').value = ''; document.getElementById('profit-max-distance').value = ''; searchProfits(); } // Planets Management async function loadPlanets() { const tableBody = document.getElementById('planets-table-body'); tableBody.innerHTML = ' Loading planets... '; try { const response = await fetch('/api/market/planets'); const planets = await response.json(); if (response.ok) { currentData.planets.data = planets; renderPlanetsTable(); // Populate planet filter dropdown const planetSelect = document.getElementById('market-planet'); if (planetSelect) { planetSelect.innerHTML = '' + planets.map(planet => ` `).join(''); } } else { tableBody.innerHTML = ` Error loading planets: ${planets.error} `; } } catch (error) { console.error('Error loading planets:', error); tableBody.innerHTML = ' Network error loading planets '; } } function renderPlanetsTable() { const tableBody = document.getElementById('planets-table-body'); if (currentData.planets.data.length === 0) { tableBody.innerHTML = ' No planets found '; return; } // Sort planets const sortedPlanets = [...currentData.planets.data].sort((a, b) => { const aVal = a[currentData.planets.sortBy]; const bVal = b[currentData.planets.sortBy]; const modifier = currentData.planets.sortOrder === 'asc' ? 1 : -1; if (typeof aVal === 'string') { return aVal.localeCompare(bVal) * modifier; } return (aVal - bVal) * modifier; }); tableBody.innerHTML = sortedPlanets.map(planet => ` ${escapeHtml(planet.name)} ${planet.marketCount} ${formatDistance(planet.distanceFromOrigin)} `).join(''); } function sortPlanets(column) { if (currentData.planets.sortBy === column) { currentData.planets.sortOrder = currentData.planets.sortOrder === 'asc' ? 'desc' : 'asc'; } else { currentData.planets.sortBy = column; currentData.planets.sortOrder = 'asc'; } updateSortHeaders('planets', column, currentData.planets.sortOrder); renderPlanetsTable(); } function filterByPlanet(planetName) { showTab('markets'); document.getElementById('market-planet').value = planetName; searchMarkets(); } // Statistics Management async function loadStatistics() { const container = document.getElementById('statistics-container'); container.innerHTML = '
Loading statistics...
'; try { const response = await fetch('/api/market/stats'); const stats = await response.json(); if (response.ok) { renderStatistics(stats); } else { container.innerHTML = `
Error loading statistics: ${stats.error}
`; } } catch (error) { console.error('Error loading statistics:', error); container.innerHTML = '
Network error loading statistics
'; } } function renderStatistics(stats) { const container = document.getElementById('statistics-container'); container.innerHTML = `

Market Statistics

${stats.markets.total}
Total Markets
${stats.markets.withOrders}
Markets with Orders
${stats.markets.averageOrdersPerMarket.toFixed(1)}
Avg Orders per Market

Order Statistics

${stats.orders.total}
Total Orders
${stats.orders.buyOrders}
Buy Orders
${stats.orders.sellOrders}
Sell Orders
${stats.orders.uniqueItems}
Unique Items
${stats.orders.uniquePlayers}
Unique Players
${formatNumber(stats.orders.totalVolume)}
Total Volume

Markets by Planet

${Object.entries(stats.markets.byPlanet).map(([planet, count]) => ` `).join('')}
Planet Market Count
${escapeHtml(planet)} ${count}

Cache Information

${stats.cache.isStale ? 'Stale' : 'Fresh'}
Cache Status
${Math.floor(stats.cache.age)}m
Cache Age
${stats.cache.consecutiveFailures}
Consecutive Failures
`; } // Utility Functions function renderPagination(type, result) { const paginationContainer = document.getElementById(`${type}-pagination`); if (result.totalPages <= 1) { paginationContainer.innerHTML='' ; return; } const currentPage=result.page; const totalPages=result.totalPages; let paginationHTML=`
Showing ${((currentPage - 1) * result.pageSize) + 1}-${Math.min(currentPage * result.pageSize, result.totalCount)} of ${result.totalCount} items
`; // Previous button paginationHTML += ` `; // Page numbers const startPage = Math.max(1, currentPage - 2); const endPage = Math.min(totalPages, currentPage + 2); if (startPage > 1) { paginationHTML += ``; if (startPage > 2) { paginationHTML += `...`; } } for (let i = startPage; i <= endPage; i++) { paginationHTML +=` `; } if (endPage < totalPages) { if (endPage < totalPages - 1) { paginationHTML +=`...`; } paginationHTML += ``; } // Next button paginationHTML += ` `; paginationHTML += '
'; paginationContainer.innerHTML = paginationHTML; } function changePage(type, page) { switch(type) { case 'markets': loadMarkets(page); break; case 'orders': loadOrders(page); break; case 'profits': loadProfits(page); break; } } function updateSortHeaders(tableType, column, order) { const table = document.querySelector(`#${tableType}-panel .data-table`); if (!table) return; // Reset all headers table.querySelectorAll('th').forEach(th => { th.classList.remove('sort-asc', 'sort-desc'); }); // Find and update the sorted column header const headers = table.querySelectorAll('th'); headers.forEach(th => { const onclick = th.getAttribute('onclick'); if (onclick && onclick.includes(`'${column}'`)) { th.classList.add(order === 'asc' ? 'sort-asc' : 'sort-desc'); } }); } function formatCurrency(currency) { if (!currency || typeof currency.amount !== 'number') return 'N/A'; return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 0, maximumFractionDigits: 0 }).format(currency.amount).replace('$', ''); } function formatNumber(num) { if (typeof num !== 'number') return 'N/A'; return new Intl.NumberFormat('en-US').format(num); } function formatDistance(distance) { if (typeof distance !== 'number') return 'N/A'; if (distance < 1000) return `${distance.toFixed(0)}m`; if (distance < 1000000) return `${(distance / 1000).toFixed(1)}km`; return `${(distance / 1000000).toFixed(1)}Mm`; } function formatDate(dateString) { if (!dateString) return 'N/A' ; const date=new Date(dateString); const now=new Date(); const diffMs=date.getTime() - now.getTime(); const diffDays=Math.ceil(diffMs / (1000 * 60 * 60 * 24)); if (diffDays < 0) return 'Expired' ; if (diffDays===0) return 'Today' ; if (diffDays===1) return 'Tomorrow' ; return `${diffDays} days`; } function escapeHtml(text) { if (!text) return '' ; const div=document.createElement('div'); div.textContent=text; return div.innerHTML; } function showNotification(message, type='info' ) { // Create notification element const notification=document.createElement('div'); notification.className=`notification notification-${type}`; notification.style.cssText=` position: fixed; top: 20px; right: 20px; padding: 15px 20px; border-radius: 8px; color: white; font-weight: 600; z-index: 1000; max-width: 400px; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2); transform: translateX(100%); transition: transform 0.3s ease; `; // Set background color based on type switch(type) { case 'success' : notification.style.background='#28a745' ; break; case 'error' : notification.style.background='#dc3545' ; break; case 'warning' : notification.style.background='#ffc107' ; break; default: notification.style.background='#1e3c72' ; break; } notification.textContent=message; document.body.appendChild(notification); // Animate in setTimeout(()=> { notification.style.transform = 'translateX(0)'; }, 100); // Auto remove after 5 seconds setTimeout(() => { notification.style.transform = 'translateX(100%)'; setTimeout(() => { if (notification.parentNode) { notification.parentNode.removeChild(notification); } }, 300); }, 5000); } // Keyboard shortcuts document.addEventListener('keydown', function(e) { if (e.ctrlKey || e.metaKey) { switch(e.key) { case '1': e.preventDefault(); showTab('markets'); break; case '2': e.preventDefault(); showTab('orders'); break; case '3': e.preventDefault(); showTab('profits'); break; case '4': e.preventDefault(); showTab('planets'); break; case '5': e.preventDefault(); showTab('statistics'); break; case 'r': e.preventDefault(); refreshData(); break; } } }); // Auto-refresh every 5 minutes setInterval(loadSystemStatus, 5 * 60 * 1000);