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>
Three root causes addressed:
1. Added <base href="/"> to index.html so all relative asset paths
(css/app.css, js/*.js) resolve from the root regardless of the
current SPA route. Without this, /instance/117 requested
/instance/css/app.css, which hit the SPA fallback and returned
HTML; helmet's nosniff then refused it as a stylesheet.
2. Removed upgrade-insecure-requests from the CSP (useDefaults: false).
This directive told browsers to upgrade HTTP→HTTPS for every asset
request, breaking all resource loading on HTTP-only deployments.
3. Changed script-src-attr from 'none' to 'unsafe-inline' to allow
the inline onclick handlers used throughout the UI.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Dockerfile — creates a non-root app user and runs the process under it
server/routes.js — tailscale_ip validated against IPv4 regex (empty string still allowed)
index.html — sql.js CDN script tag already removed earlier in this session