- FastAPI + SQLite backend: tenants, jobs and runs, with an idempotent /api/v1/ingest endpoint authenticated by per-collector API keys - Runs store started_at and finished_at; "Last Run" is the most recent finish, and in-progress runs show as Running - Job modal shows a 30-day daily strip, success rate, and a filterable run list with per-run messages - nginx: HTTPS plus basic auth, ingest endpoint exempt; systemd unit - manage.py for API keys, job cleanup and demo data; pytest suite Co-Authored-By: Claude Opus 5.5 <[email protected]>
61 lines
2.1 KiB
JavaScript
61 lines
2.1 KiB
JavaScript
import { CONFIG } from './config.js';
|
|
|
|
export class ApiError extends Error {
|
|
constructor(message, status = null) {
|
|
super(message);
|
|
this.name = 'ApiError';
|
|
this.status = status;
|
|
}
|
|
}
|
|
|
|
async function request(path) {
|
|
const controller = new AbortController();
|
|
const timer = setTimeout(() => controller.abort(), CONFIG.requestTimeoutMs);
|
|
try {
|
|
const res = await fetch(`${CONFIG.apiBaseUrl}${path}`, {
|
|
headers: { Accept: 'application/json' },
|
|
credentials: 'same-origin', // ready for cookie/session auth behind nginx
|
|
signal: controller.signal,
|
|
});
|
|
// 401 means the nginx/SSO session lapsed; a reload sends the user back through sign-in
|
|
if (res.status === 401) throw new ApiError('Your session has expired. Reload the page to sign in again', 401);
|
|
if (!res.ok) throw new ApiError(`Server returned HTTP ${res.status}`, res.status);
|
|
return await res.json();
|
|
} catch (err) {
|
|
if (err.name === 'AbortError') throw new ApiError('Request timed out');
|
|
if (err instanceof ApiError) throw err;
|
|
throw new ApiError(`Network error: ${err.message}`);
|
|
} finally {
|
|
clearTimeout(timer);
|
|
}
|
|
}
|
|
|
|
const httpApi = {
|
|
/** GET /jobs → { generated_at, jobs: Job[] } */
|
|
async listJobs() {
|
|
const data = await request('/jobs');
|
|
if (!Array.isArray(data?.jobs)) throw new ApiError('Unexpected response shape from /jobs');
|
|
return data;
|
|
},
|
|
/** GET /jobs/:id → JobDetail */
|
|
getJob(id) {
|
|
return request(`/jobs/${encodeURIComponent(id)}`);
|
|
},
|
|
/** GET /jobs/:id/runs?days=N → { job_id, since, runs: RunSummary[] } (newest first, no messages) */
|
|
listRuns(id, days) {
|
|
return request(`/jobs/${encodeURIComponent(id)}/runs?days=${encodeURIComponent(days)}`);
|
|
},
|
|
/** GET /runs/:id → Run (with message and details) */
|
|
getRun(id) {
|
|
return request(`/runs/${encodeURIComponent(id)}`);
|
|
},
|
|
};
|
|
|
|
// Mock is loaded lazily so it never ships in the production request path.
|
|
export async function getApi() {
|
|
if (CONFIG.useMock) {
|
|
const { mockApi } = await import('./mock.js');
|
|
return mockApi;
|
|
}
|
|
return httpApi;
|
|
} |