// 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}`; } try { console.log(`[API] Fetching ${url}...`); const response = await fetch(url, { ...options, headers }); console.log(`[API] ${url} → status=${response.status} ok=${response.ok} type=${response.headers.get('content-type')}`); const text = await response.text(); console.log(`[API] ${url} → body length=${text.length}, first 200 chars:`, text.substring(0, 200)); let result; try { result = JSON.parse(text); } catch (parseErr) { const msg = `JSON parse failed for ${url}: ${parseErr.message}. Body: ${text.substring(0, 300)}`; console.error(`[API] ${msg}`); this._addDebugEntry(url, 'JSON_PARSE_ERROR', msg); throw new Error(msg); } if (!response.ok || !result.success) { const msg = `API error for ${url}: ok=${response.ok} success=${result.success} error=${result.error} status=${response.status}`; console.error(`[API] ${msg}`); this._addDebugEntry(url, 'API_ERROR', msg); throw new Error(result.error || `HTTP error! status: ${response.status}`); } console.log(`[API] ${url} → SUCCESS, data keys:`, result.data ? Object.keys(result.data) : 'null'); return result.data; } catch (error) { console.error(`[API] Request failed (${path}):`, error); this._addDebugEntry(url, 'EXCEPTION', error.message); throw error; } } // Visible on-page debug panel for diagnosing fetch failures _addDebugEntry(url, status, detail) { let panel = document.getElementById('api-debug-panel'); if (!panel) { panel = document.createElement('div'); panel.id = 'api-debug-panel'; panel.style.cssText = 'position:fixed;bottom:0;left:0;right:0;max-height:200px;overflow-y:auto;background:#1d2021;color:#fbf1c7;font-family:monospace;font-size:11px;padding:8px;z-index:99999;border-top:2px solid #cc241d;'; const title = document.createElement('div'); title.style.cssText = 'font-weight:bold;margin-bottom:4px;color:#fb4934;'; title.textContent = 'API Debug Panel (remove after fixing)'; panel.appendChild(title); document.body.appendChild(panel); } const entry = document.createElement('div'); entry.style.cssText = 'margin:2px 0;padding:2px 4px;background:#282828;border-left:3px solid ' + (status === 'EXCEPTION' ? '#fb4934' : '#fabd2f') + ';'; entry.textContent = `[${status}] ${url}: ${detail}`; panel.appendChild(entry); } 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(); } } 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; };