| /** | |
| * 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<any>} | |
| */ | |
| 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<Array<{id, name, category, question, estimated_minutes}>>} | |
| */ | |
| 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<number>} 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'); | |
| } | |