/** * API utilities for authenticated requests */ /** * Get backend URL from environment or use relative path (for local dev) * - Production (Vercel): Uses VITE_BACKEND_URL env var (e.g., https://gitpilot-backend.onrender.com) * - Development (local): Uses relative paths (proxied by Vite to localhost:8000) */ const BACKEND_URL = import.meta.env.VITE_BACKEND_URL || ''; /** * Check if backend URL is configured * @returns {boolean} True if backend URL is set */ export function isBackendConfigured() { return BACKEND_URL !== '' && BACKEND_URL !== undefined; } /** * Get the configured backend URL * @returns {string} Backend URL or empty string */ export function getBackendUrl() { return BACKEND_URL; } /** * Construct full API URL * @param {string} path - API endpoint path (e.g., '/api/chat/plan') * @returns {string} Full URL to API endpoint */ export function apiUrl(path) { // Ensure path starts with / const cleanPath = path.startsWith('/') ? path : `/${path}`; return `${BACKEND_URL}${cleanPath}`; } /** * Enhanced fetch with better error handling for JSON parsing * @param {string} url - URL to fetch * @param {Object} options - Fetch options * @returns {Promise} Parsed JSON response */ export async function safeFetchJSON(url, options = {}) { try { // Add timeout to prevent hanging when backend is starting up. // Default raised to 15s to tolerate first-load GitHub API checks. const timeout = options.timeout || 15000; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeout); const fetchOptions = { ...options, signal: options.signal || controller.signal }; delete fetchOptions.timeout; let response; try { response = await fetch(url, fetchOptions); } finally { clearTimeout(timer); } const contentType = response.headers.get('content-type'); // Check if response is actually JSON if (!contentType || !contentType.includes('application/json')) { // If not JSON, it might be an HTML error page const text = await response.text(); // Check if it looks like HTML (starts with } Fetch response */ export async function authFetch(url, options = {}) { const headers = { ...getAuthHeaders(), ...options.headers, }; return fetch(url, { ...options, headers, }); } /** * Make an authenticated JSON request * @param {string} url - API endpoint URL * @param {Object} options - Fetch options * @returns {Promise} Parsed JSON response */ export async function authFetchJSON(url, options = {}) { const headers = { 'Content-Type': 'application/json', ...getAuthHeaders(), ...options.headers, }; const response = await fetch(url, { ...options, headers, }); if (!response.ok) { const error = await response.json().catch(() => ({ detail: 'Request failed' })); throw new Error(error.detail || error.message || 'Request failed'); } return response.json(); } // ─── Redesigned API Endpoints ──────────────────────────── /** * Get normalized server status */ export async function fetchStatus() { return safeFetchJSON(apiUrl("/api/status")); } /** * Get server status with retry (for startup when backend may still be booting). * Retries up to `maxRetries` times with `delayMs` between attempts. * @param {number} maxRetries - Maximum retry attempts (default: 8) * @param {number} delayMs - Delay between retries in ms (default: 2000) * @returns {Promise} Parsed status response or null */ export async function fetchStatusWithRetry(maxRetries = 8, delayMs = 2000) { for (let i = 0; i < maxRetries; i++) { try { return await safeFetchJSON(apiUrl("/api/status"), { timeout: 5000 }); } catch { if (i < maxRetries - 1) { await new Promise((r) => setTimeout(r, delayMs)); } } } return null; } /** * Get detailed provider status */ export async function fetchProviderStatus() { return safeFetchJSON(apiUrl("/api/providers/status")); } /** * Test a provider configuration */ export async function testProvider(providerConfig) { return safeFetchJSON(apiUrl("/api/providers/test"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(providerConfig), }); } /** * Start a session by mode */ export async function startSession(sessionConfig) { return safeFetchJSON(apiUrl("/api/session/start"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(sessionConfig), }); } /** * Send a chat message (redesigned endpoint) */ export async function sendChatMessage(messageConfig) { return safeFetchJSON(apiUrl("/api/chat/send"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(messageConfig), }); } /** * Get workspace summary */ export async function fetchWorkspaceSummary(folderPath) { const query = folderPath ? `?folder_path=${encodeURIComponent(folderPath)}` : ""; return safeFetchJSON(apiUrl(`/api/workspace/summary${query}`)); } /** * Run security scan on workspace */ export async function scanWorkspace(path) { const query = path ? `?path=${encodeURIComponent(path)}` : ""; return safeFetchJSON(apiUrl(`/api/security/scan-workspace${query}`)); }