// 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
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 = `
${stats.markets.total}
Total Markets
${stats.markets.withOrders}
Markets with Orders
${stats.markets.averageOrdersPerMarket.toFixed(1)}
Avg Orders per Market
${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
| Planet |
Market Count |
${Object.entries(stats.markets.byPlanet).map(([planet, count]) => `
| ${escapeHtml(planet)} |
${count} |
`).join('')}
${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=`
';
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);