Add backend API, running status and 30-day run history
- 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]>
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user