Syncs patchmon field on instances by querying the Patchmon hosts API and matching hostnames. API token masked as REDACTED in responses. seedJobs now uses INSERT OR IGNORE so new jobs are seeded on existing installs without re-running the full seed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
107 lines
4.2 KiB
JavaScript
107 lines
4.2 KiB
JavaScript
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}` };
|
|
}
|
|
|
|
// ── Patchmon Sync ─────────────────────────────────────────────────────────────
|
|
|
|
async function patchmonSyncHandler(cfg) {
|
|
const { api_url, api_token } = cfg;
|
|
if (!api_url || !api_token) throw new Error('Patchmon not configured — set API URL and token');
|
|
|
|
const res = await fetch(api_url, {
|
|
headers: { Authorization: `Basic ${api_token}` },
|
|
});
|
|
if (!res.ok) throw new Error(`Patchmon API ${res.status}`);
|
|
|
|
const data = await res.json();
|
|
const items = Array.isArray(data) ? data : (data.hosts ?? data.data ?? []);
|
|
const hostSet = new Set(
|
|
items.map(h => (typeof h === 'string' ? h : (h.name ?? h.hostname ?? h.host ?? '')))
|
|
.filter(Boolean)
|
|
);
|
|
|
|
const instances = getInstances();
|
|
let updated = 0;
|
|
for (const inst of instances) {
|
|
const newPatchmon = hostSet.has(inst.name) ? 1 : 0;
|
|
if (newPatchmon !== inst.patchmon) {
|
|
const { id: _id, created_at: _ca, updated_at: _ua, ...instData } = inst;
|
|
updateInstance(inst.vmid, { ...instData, patchmon: newPatchmon });
|
|
updated++;
|
|
}
|
|
}
|
|
return { summary: `${updated} updated of ${instances.length}` };
|
|
}
|
|
|
|
// ── Registry ──────────────────────────────────────────────────────────────────
|
|
|
|
const HANDLERS = {
|
|
tailscale_sync: tailscaleSyncHandler,
|
|
patchmon_sync: patchmonSyncHandler,
|
|
};
|
|
|
|
// ── 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));
|
|
}
|
|
}
|