- 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]>
130 lines
5.6 KiB
JavaScript
130 lines
5.6 KiB
JavaScript
// Sample data matching the API contract. Timestamps are relative to page load so it always looks "live".
|
||
const now = Date.now();
|
||
const MIN = 60_000;
|
||
const HR = 60 * MIN;
|
||
const DAY = 24 * HR;
|
||
const iso = (offsetMs) => new Date(now + offsetMs).toISOString();
|
||
|
||
// every_h drives the generated run history; it isn't part of the API.
|
||
const JOBS = [
|
||
{
|
||
id: 1, company_name: 'Acme Corp', job_name: 'File Server – Nightly', status: 'success', every_h: 24,
|
||
last_run_at: iso(-6 * HR), next_run_at: iso(18 * HR),
|
||
message: 'Job completed successfully. 1,284 files processed.',
|
||
source_system: 'Veeam B&R', duration_seconds: 2840, size_bytes: 128_849_018_880,
|
||
},
|
||
{
|
||
id: 2, company_name: 'Acme Corp', job_name: 'SQL01 – Transaction Logs', status: 'warning', every_h: 1,
|
||
last_run_at: iso(-25 * MIN), next_run_at: iso(35 * MIN),
|
||
message: 'Processing SQL01 Warning:\nFailed to truncate transaction logs for database "ERP_PROD".\nLog backup completed but truncation was skipped.',
|
||
source_system: 'Veeam B&R', duration_seconds: 312, size_bytes: 2_147_483_648,
|
||
details: { retry_count: 0, repository: 'REPO-ONSITE-01' },
|
||
},
|
||
{
|
||
// Running now; "last run" is still the previous completion
|
||
id: 7, company_name: 'Acme Corp', job_name: 'Hyper-V Hosts – Daily', status: 'running', every_h: 24,
|
||
running_since: iso(-12 * MIN), last_status: 'success',
|
||
last_run_at: iso(-DAY + 40 * MIN), next_run_at: iso(DAY - 12 * MIN),
|
||
message: 'Completed successfully. 6 VMs processed.',
|
||
source_system: 'Veeam B&R', duration_seconds: 3120, size_bytes: 412_316_860_416,
|
||
},
|
||
{
|
||
id: 3, company_name: 'Bluewater Dental', job_name: 'Workstations – Daily', status: 'failed', every_h: 24,
|
||
last_run_at: iso(-3 * HR), next_run_at: iso(21 * HR),
|
||
message: 'Error: Unable to connect to backup repository \\\\NAS02\\Backups.\nThe network path was not found. (0x80070035)\nJob terminated after 3 retries.',
|
||
source_system: 'Datto', duration_seconds: 95,
|
||
details: { retry_count: 3, target: '\\\\NAS02\\Backups' },
|
||
},
|
||
{
|
||
id: 4, company_name: 'Bluewater Dental', job_name: 'Imaging Server – Hourly', status: 'success', every_h: 1,
|
||
last_run_at: iso(-50 * MIN), next_run_at: iso(10 * MIN),
|
||
message: 'Completed.', source_system: 'Datto', duration_seconds: 420, size_bytes: 8_589_934_592,
|
||
},
|
||
{
|
||
// Green status but next run is in the past → should show as Overdue
|
||
id: 5, company_name: 'Northfield Logistics', job_name: 'Exchange – Nightly', status: 'success', every_h: 24,
|
||
last_run_at: iso(-30 * HR), next_run_at: iso(-6 * HR),
|
||
message: 'Completed successfully.', source_system: 'Acronis', duration_seconds: 5400,
|
||
},
|
||
{
|
||
// Never run / unrecognised status → rendered as Unknown
|
||
id: 6, company_name: 'Northfield Logistics', job_name: 'New NAS – Weekly', status: null,
|
||
last_run_at: null, next_run_at: iso(2 * DAY),
|
||
message: '', source_system: 'Synology Hyper Backup',
|
||
},
|
||
];
|
||
|
||
const MESSAGES = {
|
||
success: 'Job completed successfully.',
|
||
warning: 'Completed with warnings: 1 file skipped (file in use).',
|
||
failed: 'Error: Target repository unreachable. Job terminated.',
|
||
};
|
||
|
||
// Deterministic pseudo-random so history doesn't reshuffle on every modal open
|
||
function rng(seed) {
|
||
let x = seed;
|
||
return () => ((x = (x * 1_103_515_245 + 12_345) % 2_147_483_648) / 2_147_483_648);
|
||
}
|
||
|
||
const RUNS = new Map(); // job id → runs, newest first
|
||
for (const job of JOBS) {
|
||
const runs = [];
|
||
if (job.running_since) {
|
||
runs.push({ id: `${job.id}-r`, status: 'running', started_at: job.running_since, finished_at: null, message: 'In progress…' });
|
||
}
|
||
if (job.last_run_at && job.every_h) {
|
||
const rand = rng(job.id * 7919);
|
||
const dur = (job.duration_seconds ?? 600) * 1000;
|
||
let end = Date.parse(job.last_run_at);
|
||
for (let i = 0; end - dur > now - 30 * DAY; i++, end -= job.every_h * HR) {
|
||
const r = rand();
|
||
const status = i === 0 ? (job.last_status ?? job.status) : r < 0.03 ? 'failed' : r < 0.1 ? 'warning' : 'success';
|
||
const thisDur = dur * (0.8 + rand() * 0.4);
|
||
runs.push({
|
||
id: `${job.id}-${i}`,
|
||
status,
|
||
started_at: new Date(end - thisDur).toISOString(),
|
||
finished_at: new Date(end).toISOString(),
|
||
duration_seconds: Math.round(thisDur / 1000),
|
||
size_bytes: status === 'failed' ? null : job.size_bytes,
|
||
message: i === 0 ? job.message : MESSAGES[status],
|
||
details: i === 0 ? job.details ?? {} : {},
|
||
});
|
||
}
|
||
}
|
||
RUNS.set(String(job.id), runs);
|
||
}
|
||
|
||
const delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
||
const summary = ({ details, every_h, last_status, duration_seconds, size_bytes, ...rest }) => rest;
|
||
|
||
export const mockApi = {
|
||
async listJobs() {
|
||
await delay(250);
|
||
return { generated_at: new Date().toISOString(), jobs: JOBS.map(summary) };
|
||
},
|
||
async getJob(id) {
|
||
await delay(200);
|
||
const job = JOBS.find((j) => String(j.id) === String(id));
|
||
if (!job) throw new Error('Job not found');
|
||
const { every_h, ...rest } = job;
|
||
return { last_status: job.status === 'running' ? null : job.status, ...rest, details: job.details ?? {} };
|
||
},
|
||
async listRuns(id, days) {
|
||
await delay(300);
|
||
const since = now - days * DAY;
|
||
const runs = (RUNS.get(String(id)) ?? [])
|
||
.filter((r) => Date.parse(r.started_at) >= since)
|
||
.map(({ message, details, ...r }) => r);
|
||
return { job_id: id, since: new Date(since).toISOString(), runs };
|
||
},
|
||
async getRun(id) {
|
||
await delay(150);
|
||
for (const runs of RUNS.values()) {
|
||
const run = runs.find((r) => r.id === id);
|
||
if (run) return { details: {}, ...run };
|
||
}
|
||
throw new Error('Run not found');
|
||
},
|
||
};
|