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:
@@ -2,7 +2,9 @@ import { Router } from 'express';
|
||||
import {
|
||||
getInstances, getInstance, getDistinctStacks,
|
||||
createInstance, updateInstance, deleteInstance, importInstances, getInstanceHistory, getAllHistory,
|
||||
getConfig, setConfig,
|
||||
} from './db.js';
|
||||
import { runTailscaleSync, restartJobs } from './jobs.js';
|
||||
|
||||
export const router = Router();
|
||||
|
||||
@@ -12,6 +14,12 @@ const VALID_STATES = ['deployed', 'testing', 'degraded'];
|
||||
const VALID_STACKS = ['production', 'development'];
|
||||
const SERVICE_KEYS = ['atlas', 'argus', 'semaphore', 'patchmon', 'tailscale', 'andromeda'];
|
||||
|
||||
const CONFIG_KEYS = [
|
||||
'tailscale_api_key', 'tailscale_tailnet', 'tailscale_poll_minutes',
|
||||
'tailscale_enabled', 'tailscale_last_run_at', 'tailscale_last_result',
|
||||
];
|
||||
const REDACTED = '**REDACTED**';
|
||||
|
||||
function validate(body) {
|
||||
const errors = [];
|
||||
if (!body.name || typeof body.name !== 'string' || !body.name.trim())
|
||||
@@ -160,3 +168,38 @@ router.delete('/instances/:vmid', (req, res) => {
|
||||
handleDbError('DELETE /api/instances/:vmid', e, res);
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/config
|
||||
router.get('/config', (_req, res) => {
|
||||
const cfg = {};
|
||||
for (const key of CONFIG_KEYS) {
|
||||
const val = getConfig(key);
|
||||
cfg[key] = (key === 'tailscale_api_key' && val) ? REDACTED : val;
|
||||
}
|
||||
res.json(cfg);
|
||||
});
|
||||
|
||||
// PUT /api/config
|
||||
router.put('/config', (req, res) => {
|
||||
for (const key of CONFIG_KEYS) {
|
||||
if (!(key in (req.body ?? {}))) continue;
|
||||
if (key === 'tailscale_api_key' && req.body[key] === REDACTED) continue;
|
||||
setConfig(key, req.body[key]);
|
||||
}
|
||||
try { restartJobs(); } catch (e) { console.error('PUT /api/config restartJobs', e); }
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
// POST /api/jobs/tailscale/run
|
||||
router.post('/jobs/tailscale/run', async (req, res) => {
|
||||
if (!getConfig('tailscale_api_key') || !getConfig('tailscale_tailnet'))
|
||||
return res.status(400).json({ error: 'Tailscale not configured' });
|
||||
try {
|
||||
const result = await runTailscaleSync();
|
||||
setConfig('tailscale_last_run_at', new Date().toISOString());
|
||||
setConfig('tailscale_last_result', `ok: ${result.updated} updated of ${result.total}`);
|
||||
res.json(result);
|
||||
} catch (e) {
|
||||
handleDbError('POST /api/jobs/tailscale/run', e, res);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user