feat: Tailscale sync jobs
All checks were successful
CI / test (pull_request) Successful in 14s
CI / build-dev (pull_request) Has been skipped

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:
2026-03-28 17:11:40 -04:00
parent 31a5090f4f
commit 47e9c4faf7
8 changed files with 308 additions and 0 deletions

View File

@@ -3,6 +3,7 @@ import {
_resetForTest,
getInstances, getInstance, getDistinctStacks,
createInstance, updateInstance, deleteInstance, importInstances, getInstanceHistory,
getConfig, setConfig,
} from '../server/db.js'
beforeEach(() => _resetForTest());
@@ -269,3 +270,32 @@ describe('test environment boot isolation', () => {
expect(getInstances()).toEqual([]);
});
});
// ── getConfig / setConfig ─────────────────────────────────────────────────────
describe('getConfig / setConfig', () => {
it('returns defaultVal when key does not exist', () => {
expect(getConfig('missing', 'fallback')).toBe('fallback');
});
it('returns empty string by default', () => {
expect(getConfig('missing')).toBe('');
});
it('stores and retrieves a value', () => {
setConfig('tailscale_api_key', 'tskey-test');
expect(getConfig('tailscale_api_key')).toBe('tskey-test');
});
it('overwrites an existing key', () => {
setConfig('tailscale_enabled', '0');
setConfig('tailscale_enabled', '1');
expect(getConfig('tailscale_enabled')).toBe('1');
});
it('config is cleared by _resetForTest', () => {
setConfig('tailscale_api_key', 'tskey-test');
_resetForTest();
expect(getConfig('tailscale_api_key')).toBe('');
});
});