import { getInstances, updateInstance, getConfig, setConfig } from './db.js'; const TAILSCALE_API = 'https://api.tailscale.com/api/v2'; let _interval = null; export async function runTailscaleSync() { const apiKey = getConfig('tailscale_api_key'); const tailnet = getConfig('tailscale_tailnet'); if (!apiKey || !tailnet) throw new Error('Tailscale not configured'); const res = await fetch( `${TAILSCALE_API}/tailnet/${encodeURIComponent(tailnet)}/devices`, { headers: { Authorization: `Bearer ${apiKey}` } } ); if (!res.ok) throw new Error(`Tailscale API ${res.status}`); const { devices } = await res.json(); // hostname -> first 100.x.x.x address 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); // undefined = not in Tailscale 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) { // Strip db-generated columns — node:sqlite rejects unknown named parameters const { id: _id, created_at: _ca, updated_at: _ua, ...instData } = inst; updateInstance(inst.vmid, { ...instData, tailscale: newTailscale, tailscale_ip: newIp }); updated++; } } return { updated, total: instances.length }; } export function restartJobs() { if (_interval) { clearInterval(_interval); _interval = null; } if (getConfig('tailscale_enabled') !== '1') return; const mins = parseInt(getConfig('tailscale_poll_minutes', '15'), 10); const ms = Math.max(1, Number.isFinite(mins) ? mins : 15) * 60_000; _interval = setInterval(async () => { try { const r = await runTailscaleSync(); setConfig('tailscale_last_run_at', new Date().toISOString()); setConfig('tailscale_last_result', `ok: ${r.updated} updated of ${r.total}`); } catch (e) { setConfig('tailscale_last_run_at', new Date().toISOString()); setConfig('tailscale_last_result', `error: ${e.message}`); } }, ms); }