File size: 6,369 Bytes
ae74af5 df6bf75 ae74af5 73c5feb 4f2f97a 73c5feb 4f2f97a 73c5feb ae74af5 e8c214d ae74af5 df6bf75 ae74af5 df6bf75 ae74af5 df6bf75 ae74af5 df6bf75 ae74af5 f37d8a6 d2a9a16 f37d8a6 d2a9a16 f37d8a6 d2a9a16 f37d8a6 d2a9a16 f37d8a6 d2a9a16 f37d8a6 d2a9a16 f37d8a6 ae74af5 85f7c19 df6bf75 85f7c19 df6bf75 85f7c19 df6bf75 85f7c19 4f2f97a 337d9e1 df6bf75 337d9e1 df6bf75 337d9e1 df6bf75 337d9e1 57ba197 61feaac df6bf75 61feaac 4f2f97a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 | /**
* 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');
}
|