Adds a gear button to the nav that opens a settings modal with:
- Export: GET /api/export returns all instances as a JSON backup file
with a Content-Disposition attachment header
- Import: POST /api/import validates and bulk-replaces all instances;
client uses FileReader to POST the parsed JSON, with a confirm dialog
before destructive replace
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