/** * Aperture — Backend API Wrapper * All communication with the FastAPI backend goes through this module. */ const BASE = ''; // same-origin; empty string = relative to current host /** * Fetch wrapper with JSON handling and error propagation. * @param {string} path - Relative path, e.g. "/api/eo-products" * @param {object} opts - fetch() options (optional) * @returns {Promise} */ async function apiFetch(path, opts = {}) { const headers = { 'Content-Type': 'application/json', ...(opts.headers || {}) }; const session = JSON.parse(sessionStorage.getItem('aperture_session') || 'null'); if (session) { headers['Authorization'] = `Bearer ${session.email}:${session.token}`; } const res = await fetch(BASE + path, { ...opts, headers }); if (!res.ok) { let detail = res.statusText; try { const body = await res.json(); if (Array.isArray(body.detail)) { detail = body.detail.map(e => e.msg || JSON.stringify(e)).join('; '); } else { detail = body.detail || JSON.stringify(body); } } catch (_) { /* ignore parse errors */ } throw new Error(`API ${res.status}: ${detail}`); } // 204 No Content — nothing to parse if (res.status === 204) return null; return res.json(); } /* ── EO Products ─────────────────────────────────────────── */ /** * List all available EO products. * @returns {Promise>} */ export async function listProducts() { return apiFetch('/api/eo-products'); } /* ── Jobs ────────────────────────────────────────────────── */ /** * Submit a new analysis job. * @param {{aoi, time_range, product_ids, email}} payload * @returns {Promise<{id, status}>} */ export async function submitJob(payload) { return apiFetch('/api/jobs', { method: 'POST', body: JSON.stringify(payload), }); } /** * Get current status and results for a job. * @param {string} jobId * @returns {Promise<{id, status, progress, results, created_at, updated_at, error}>} */ export async function getJob(jobId) { return apiFetch(`/api/jobs/${jobId}`); } /* ── Downloads ───────────────────────────────────────────── */ /** * Return auth headers for the current session (for raw fetch calls). */ export function authHeaders() { const session = JSON.parse(sessionStorage.getItem('aperture_session') || 'null'); if (!session) return {}; return { 'Authorization': `Bearer ${session.email}:${session.token}` }; } /** * Fetch a file with auth and trigger a browser download. * * The anchor must be attached to the DOM for .click() to work * reliably in Safari/Firefox, and the object URL must outlive the * click event long enough for the browser to start the download. * * @param {string} url - File endpoint URL * @param {string} filename - Suggested download filename */ export async function authDownload(url, filename) { const res = await fetch(url, { headers: authHeaders() }); if (!res.ok) { let detail = res.statusText; try { const body = await res.json(); detail = body.detail || detail; } catch (_) { /* ignore */ } throw new Error(`Download failed (${res.status}): ${detail}`); } const blob = await res.blob(); const objectUrl = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = objectUrl; a.download = filename; a.style.display = 'none'; document.body.appendChild(a); a.click(); // Defer cleanup so the browser has time to start the download. setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(objectUrl); }, 1500); } /** * Returns the URL for the PDF report download. * @param {string} jobId * @returns {string} */ export function reportUrl(jobId) { return `${BASE}/api/jobs/${jobId}/report`; } /** * Returns the URL for the data package download. * @param {string} jobId * @returns {string} */ export function packageUrl(jobId) { return `${BASE}/api/jobs/${jobId}/package`; } /** * Returns the URL for an EO product map PNG. * @param {string} jobId * @param {string} productId * @returns {string} */ export function mapUrl(jobId, productId) { return `${BASE}/api/jobs/${jobId}/maps/${productId}`; } /** * Returns the URL for EO product spatial data JSON. * @param {string} jobId * @param {string} productId * @returns {string} */ export function spatialUrl(jobId, productId) { return `${BASE}/api/jobs/${jobId}/spatial/${productId}`; } /** * Fetch composite overview score + compound signals for the dashboard. * @param {string} jobId * @returns {Promise<{overview, compound_signals}>} */ export async function getJobOverview(jobId) { return apiFetch(`/api/jobs/${jobId}/overview`); } /* ── AOI Advisor ───────────────────────────────────────── */ /** * Get Claude-powered region insight for an AOI. * @param {Array} bbox - [minLon, minLat, maxLon, maxLat] * @returns {Promise<{context, recommended_start, recommended_end, product_priorities, reasoning} | null>} */ export async function getAoiAdvice(bbox) { try { const result = await apiFetch('/api/aoi-advice', { method: 'POST', body: JSON.stringify({ bbox }), }); // Treat all-null as failure if (!result || !result.context) return null; return result; } catch { return null; } } /* ── Auth ───────────────────────────────────────────────── */ export async function requestMagicLink(email) { return apiFetch('/api/auth/request', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email }), }); } export async function verifyToken(email, token) { return apiFetch('/api/auth/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, token }), }); } export async function listJobs() { return apiFetch('/api/jobs'); }