Files
GopherGate/static/js/api.js
hobokenchicken 2212f0b736
Some checks failed
CI / Check (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Formatting (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Release Build (push) Has been cancelled
debug(dashboard): add visible API debug panel to diagnose fetch failures in browser
API endpoints return valid data via curl but browser JS shows 'Failed to load'.
Added verbose logging and a fixed-position debug panel to api.js that displays
the exact error type (JSON parse, API error, or network exception) on-page.
Bumped cache-bust to ?v=4.
2026-03-02 13:39:33 -05:00

141 lines
4.9 KiB
JavaScript

// 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;
};