import { getJobs, getJob, getInstances, updateInstance, createJobRun, completeJobRun } from './db.js'; // ── Handlers ────────────────────────────────────────────────────────────────── const TAILSCALE_API = 'https://api.tailscale.com/api/v2'; async function tailscaleSyncHandler(cfg) { const { api_key, tailnet } = cfg; if (!api_key || !tailnet) throw new Error('Tailscale not configured — set API key and tailnet'); const res = await fetch( `${TAILSCALE_API}/tailnet/${encodeURIComponent(tailnet)}/devices`, { headers: { Authorization: `Bearer ${api_key}` } } ); if (!res.ok) throw new Error(`Tailscale API ${res.status}`); const { devices } = await res.json(); const tsMap = new Map( devices.map(d => [d.hostname, (d.addresses ?? []).find(a => a.startsWith('100.')) ?? '']) ); const instances = getInstances(); let updated = 0; for (const inst of instances) { const tsIp = tsMap.get(inst.name); const matched = tsIp !== undefined; const newTailscale = matched ? 1 : (inst.tailscale === 1 ? 0 : inst.tailscale); const newIp = matched ? tsIp : (inst.tailscale === 1 ? '' : inst.tailscale_ip); if (newTailscale !== inst.tailscale || newIp !== inst.tailscale_ip) { const { id: _id, created_at: _ca, updated_at: _ua, ...instData } = inst; updateInstance(inst.vmid, { ...instData, tailscale: newTailscale, tailscale_ip: newIp }); updated++; } } return { summary: `${updated} updated of ${instances.length}` }; } // ── Registry ────────────────────────────────────────────────────────────────── const HANDLERS = { tailscale_sync: tailscaleSyncHandler, }; // ── Public API ──────────────────────────────────────────────────────────────── export async function runJob(jobId) { const job = getJob(jobId); if (!job) throw new Error('Job not found'); const handler = HANDLERS[job.key]; if (!handler) throw new Error(`No handler for '${job.key}'`); const cfg = JSON.parse(job.config || '{}'); const runId = createJobRun(jobId); try { const result = await handler(cfg); completeJobRun(runId, 'success', result.summary ?? ''); return result; } catch (e) { completeJobRun(runId, 'error', e.message); throw e; } } const _intervals = new Map(); export function restartJobs() { for (const iv of _intervals.values()) clearInterval(iv); _intervals.clear(); for (const job of getJobs()) { if (!job.enabled) continue; const ms = Math.max(1, job.schedule || 15) * 60_000; const id = job.id; _intervals.set(id, setInterval(() => runJob(id).catch(() => {}), ms)); } }