// Unified API client for the dashboard class ApiClient { constructor() { this.baseUrl = '/api'; } async request(path, options = {}) { const url = `${this.baseUrl}${path}`; const headers = { 'Content-Type': 'application/json', ...options.headers }; // Add auth token if available if (window.authManager && window.authManager.token) { headers['Authorization'] = `Bearer ${window.authManager.token}`; } const response = await fetch(url, { ...options, headers }); const text = await response.text(); let result; try { result = JSON.parse(text); } catch (parseErr) { throw new Error(`JSON parse failed for ${url}: ${parseErr.message}`); } if (!response.ok || !result.success) { // Handle authentication errors (session expired, server restarted, etc.) if (response.status === 401 || result.error === 'Session expired or invalid' || result.error === 'Not authenticated' || result.error === 'Admin access required') { if (window.authManager) { // Try to logout to clear local state and show login screen window.authManager.logout(); } } throw new Error(result.error || `HTTP error! status: ${response.status}`); } // Handling X-Refreshed-Token header if (response.headers.get('X-Refreshed-Token') && window.authManager) { window.authManager.token = response.headers.get('X-Refreshed-Token'); if (window.authManager.setToken) { window.authManager.setToken(window.authManager.token); } } return result.data; } async get(path) { return this.request(path, { method: 'GET' }); } async post(path, body) { return this.request(path, { method: 'POST', body: JSON.stringify(body) }); } async put(path, body) { return this.request(path, { method: 'PUT', body: JSON.stringify(body) }); } async delete(path) { return this.request(path, { method: 'DELETE' }); } // Helper for formatting large numbers formatNumber(num) { if (num >= 1000000) { return (num / 1000000).toFixed(1) + 'M'; } if (num >= 1000) { return (num / 1000).toFixed(1) + 'K'; } return num.toString(); } // Helper for formatting currency formatCurrency(amount) { return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 2, maximumFractionDigits: 4 }).format(amount); } // Helper for relative time formatTimeAgo(dateStr) { if (!dateStr) return 'Never'; const date = luxon.DateTime.fromISO(dateStr); return date.toRelative(); } // Helper for escaping HTML escapeHtml(unsafe) { if (unsafe === undefined || unsafe === null) return ''; return unsafe.toString() .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } } window.api = new ApiClient(); // Helper: waits until chartManager is available (defensive against load-order edge cases) window.waitForChartManager = async (timeoutMs = 3000) => { if (window.chartManager) return window.chartManager; const start = Date.now(); while (Date.now() - start < timeoutMs) { await new Promise(r => setTimeout(r, 50)); if (window.chartManager) return window.chartManager; } console.warn('chartManager not available after', timeoutMs, 'ms'); return null; };