# MyDU Market Browser

Advanced market analysis and trading opportunities

Checking system status...
Data age: Unknown
Markets: 0
Orders: 0
Market Name
Planet

Market Name | Planet | Orders | Distance | Actions
--- | --- | --- | --- | ---
Loading markets...

Item Name
Order Type
Market
Min Price
Max Price
Player Name

Item | Type | Quantity | Price | Market | Player | Expires
--- | --- | --- | --- | --- | --- | ---
Loading orders...

Item Name
Min Profit Margin (%)
Max Distance
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 = 'All Planets' + planets.map(planet => `${escapeHtml(planet.name)} `).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

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

### 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);