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