feat: Tailscale sync jobs
Adds a background job system that polls the Tailscale API on a configurable interval and syncs tailscale status and IPs to instances by hostname match. - New config table (key/value) in SQLite for persistent server-side settings - New server/jobs.js: runTailscaleSync + restartJobs scheduler - GET/PUT /api/config — read and write Tailscale settings; API key masked as **REDACTED** on GET - POST /api/jobs/tailscale/run — immediate manual sync - Settings modal: new Tailscale Sync section with enable toggle, tailnet, API key, poll interval, Save + Run Now buttons, last-run status Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
58
js/ui.js
58
js/ui.js
@@ -353,6 +353,7 @@ function openSettingsModal() {
|
||||
}
|
||||
}
|
||||
sel.value = getTimezone();
|
||||
loadTailscaleSettings();
|
||||
document.getElementById('settings-modal').classList.add('open');
|
||||
}
|
||||
|
||||
@@ -424,3 +425,60 @@ document.getElementById('tz-select').addEventListener('change', e => {
|
||||
if (m) renderDetailPage(parseInt(m[1], 10));
|
||||
else renderDashboard();
|
||||
});
|
||||
|
||||
// ── Tailscale Settings ────────────────────────────────────────────────────────
|
||||
|
||||
async function loadTailscaleSettings() {
|
||||
try {
|
||||
const res = await fetch('/api/config');
|
||||
if (!res.ok) return;
|
||||
const cfg = await res.json();
|
||||
document.getElementById('ts-enabled').checked = cfg.tailscale_enabled === '1';
|
||||
document.getElementById('ts-tailnet').value = cfg.tailscale_tailnet ?? '';
|
||||
document.getElementById('ts-api-key').value = cfg.tailscale_api_key ?? '';
|
||||
document.getElementById('ts-poll').value = cfg.tailscale_poll_minutes || '15';
|
||||
_updateTsStatus(cfg.tailscale_last_run_at, cfg.tailscale_last_result);
|
||||
} catch { /* silent */ }
|
||||
}
|
||||
|
||||
function _updateTsStatus(lastRun, lastResult) {
|
||||
const el = document.getElementById('ts-status');
|
||||
if (!lastRun) { el.textContent = 'Never run'; return; }
|
||||
el.textContent = `Last run: ${fmtDateFull(lastRun)} — ${lastResult || '—'}`;
|
||||
}
|
||||
|
||||
async function saveTailscaleSettings() {
|
||||
const body = {
|
||||
tailscale_enabled: document.getElementById('ts-enabled').checked ? '1' : '0',
|
||||
tailscale_tailnet: document.getElementById('ts-tailnet').value.trim(),
|
||||
tailscale_api_key: document.getElementById('ts-api-key').value,
|
||||
tailscale_poll_minutes: document.getElementById('ts-poll').value || '15',
|
||||
};
|
||||
const res = await fetch('/api/config', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
showToast(res.ok ? 'Tailscale settings saved' : 'Failed to save settings', res.ok ? 'success' : 'error');
|
||||
}
|
||||
|
||||
async function runTailscaleNow() {
|
||||
const btn = document.getElementById('ts-run-btn');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Running…';
|
||||
try {
|
||||
const res = await fetch('/api/jobs/tailscale/run', { method: 'POST' });
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
showToast(`Sync complete — ${data.updated} updated`, 'success');
|
||||
_updateTsStatus(new Date().toISOString(), `ok: ${data.updated} updated of ${data.total}`);
|
||||
} else {
|
||||
showToast(data.error ?? 'Sync failed', 'error');
|
||||
}
|
||||
} catch {
|
||||
showToast('Sync failed', 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Run Now';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user