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');
}