1a2dd19736e892afeff0b41ccf49d678d4dd1f52
18
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1a2dd19736 |
Add an Uptime Kuma integration: monitor status and which server each one watches
New integration, following the existing pattern: config in-app (URL + API key, credentials encrypted at rest), its own Uptime Kuma page, an Integrations list entry, a Dashboard widget, and diagnostic-log/ integration-down-alert coverage for free via the shared withDiagLogging wrapper. Read-only -- no start/stop equivalent exists for a monitor. Uptime Kuma has no conventional REST API (the dashboard talks to it over Socket.IO); researched before writing any code, since guessing wrong here would have cost real time. The one machine-readable, authenticated endpoint that lists every monitor is its Prometheus exporter at GET /metrics, gated by HTTP Basic auth -- an API key as the password with the username left blank on current installs, or the real dashboard login on installs from before the API-key feature existed. This adapter authenticates the same way and parses that endpoint's text-exposition format itself (metrics: monitor_status, monitor_response_time, monitor_cert_days_remaining, monitor_uptime_ratio; labels: monitor_id, monitor_name, monitor_type, monitor_url, monitor_hostname, monitor_port), verified against the documented metric/label set and the actual upstream source (server/prometheus.js). A malformed line is skipped rather than failing the whole scrape. "What server is being monitored for what": each monitor's target (an IP for TCP checks, or the hostname out of the URL for HTTP/keyword checks) is matched against your servers' own IPs and hostnames -- reusing the same kind of match already used in the consistency report -- and linked to that server's page. Monitors with no single network target (groups, push monitors, DNS/keyword checks with a complex URL) are left unmatched rather than guessed at. Uptime Kuma's tags aren't read, since the Prometheus endpoint doesn't reliably distinguish a tag label from any other label it might add later. The username field is the first genuinely optional integration config field this app has had; IntegrationField gained an `optional` flag (server validation and both the add/edit web forms honor it) rather than special-casing Uptime Kuma. Verified with 48 backend checks (Prometheus text parsing including escaped quotes, decimals, negative numbers, and malformed lines; TCP vs. HTTP target/port extraction; every documented status code; server matching by IP, hostname, and short name, including no-match cases; the route's real HTTP round trip against a fake Uptime Kuma server, wrong credentials, upstream failures, roles, wrong/disabled/missing integration, diagnostic-log entries; the optional-field validation rule) and by driving the real page and the real Dashboard widget in a browser against the real routers, including CSV export and column sorting. Real dev database mtime untouched. Not verified: a real Uptime Kuma instance. Everything here was checked against Uptime Kuma's documented metric format, its actual upstream source, and a fake server built to match both -- not against a live installation. If your instance's /metrics output differs from what's documented (older version, unusual monitor types), the parser should degrade to an empty or partial monitor list rather than error, but that degradation itself hasn't been observed against the real thing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
5c1376cf1c |
Make search results open the actual entry, not just its list page
Previously a search result for a secret/IP/integration/DNS record only filtered or scrolled to the right page — you still had to find and click it yourself. Now: - Secrets/IPAM results pass ?editId= and the page auto-opens that row's existing edit form on load (viewers still just get the ?q= filter, since they can't edit). - Integration results now link to /integrations?editId= (which auto-opens IntegrationEditForm for that row) instead of the type's live dashboard page (/proxmox, /tailscale, etc.) — the dashboard can't distinguish between two integrations of the same type anyway, so it wasn't landing on "the" entry when more than one existed. - DNS record results now also pass recordType/recordName/recordContent so Dns.tsx, after auto-selecting the right zone, finds that exact record by (type, name, content) and opens its edit form too. That triple was used instead of an id because the records-list endpoint returns both the cache table's internal integer id and the provider's own string record id under different field names, and matching by content sidesteps that ambiguity entirely rather than risking picking the wrong one. Servers and DNS zones/providers already landed on their real entry (server detail page; zone/provider auto-selected) so those are unchanged. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
7564796c39 |
Add a per-row Test button to the Integrations list
The add/edit forms already had "Test connection", backed by existing POST /api/integrations/:id/test (admin-only, pings with the stored config) and POST /api/integrations/test (for a not-yet-saved one) -- but there was no way to re-test an already-configured integration without opening its edit form. Add a "Test" button next to Edit/ Delete on each row, showing an OK/latency or Failed/error badge inline once it returns. Pure frontend wiring to the existing endpoint, no server changes needed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
d5b6c55383 |
Simplify Integrations to a plain list + Add button
Flagged back when Proxmox/Synology got their own pages: once all six integration types had moved to dedicated pages, the "browsing" dropdown here only ever showed one of six near-identical "go manage this on its own page" redirects, and its final fallback branch was dead code (every IntegrationType was already covered). Now that all the actual data views have moved out, drop that mode entirely. The page is now just what "Manage integrations" already was: a sortable, CSV-exportable table of configured integrations (name/type/ status), visible to every role since none of that is sensitive, with an admin-only "Add integration" button and per-row edit/enable-disable/ delete actions. No more mode toggle, no provider-picker dropdown, no per-type redirect cards -- the six dedicated pages (and the sidebar nav that already points at them) are how you actually use each integration now. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
f4274c1ad8 |
Make every table sortable and exportable to CSV
Applies the useSortable/SortableTh infrastructure introduced in the Diagnostic Log commit to the rest of the app's tables: Audit Log, Users, Servers (manage table), server task tables (grouped by schedule type, on both the all-servers and per-server views), DNS providers/zones/records, Docker containers, Gitea repos, Integrations (manage table), IP Addresses, Proxmox guests, Secrets (already had CSV, gained sorting), Semaphore templates, Synology volumes/disks, and Tailscale devices. Every column header is now click-to-sort (again on the raw field, not its formatted display -- a byte count sorts numerically even though the cell shows "1.2 GB", a date sorts chronologically even though the cell shows "5d 15h 2m"), and every table got an "Export CSV" button next to its Refresh button, exporting whatever's currently sorted/filtered via the existing downloadCsv util. Grouped tables (ServerTaskTable renders one sub-table per schedule type) can't call the useSortable hook per group without breaking the Rules of Hooks, so extracted its comparison core as a standalone sortItems() function, driven by one shared sort-state pair at the component's top level and applied per group. Deliberately left two small (1-6 row) tables embedded inside stat cards unsorted -- Proxmox's per-node storage list and ServerDetail's agent-reported disk list -- since they're secondary detail inside an already-scannable card, not primary list content; happy to add if useful in practice. Verified the shared sort core (sortItems) directly: numeric-aware string compare (so "item2" sorts before "item10"), numbers, booleans, and that null/undefined always sort to the end regardless of direction. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
95bd831d81 |
Move Proxmox and Synology to their own top-level pages
Same move as Docker/Tailscale/Semaphore/Gitea: each was buried inside
the generic Integrations browsing view. Give them dedicated pages too
— this was the last pair, so every integration type with a browsing UI
now has its own page.
New web/src/pages/{Proxmox,Synology}.tsx reuse the existing, unchanged
API routes and adapters (no server changes) with their own integration
picker scoped to just that type. Added matching nav items and routes.
Synology stays read-only, matching its original design.
Removed the corresponding state/handlers/tables from Integrations.tsx
(296 lines). It's now down to just the "Manage integrations" CRUD
table plus a browsing dropdown whose six branches are all redirects to
the type's own page — genuinely pointless now that every type has
moved out, worth simplifying separately.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
||
|
|
8fef68c27a |
Move Tailscale, Semaphore, and Gitea to their own top-level pages
Same move as Docker: each was buried inside the generic Integrations
browsing view, sharing one "pick any integration" dropdown across six
unrelated types. Give them dedicated pages instead.
New web/src/pages/{Tailscale,Semaphore,Gitea}.tsx each reuse the
existing, unchanged API routes and adapters (no server changes) with
their own integration picker scoped to just that type. Added matching
nav items (Tailscale, Semaphore, Gitea, right after Docker) and routes.
Removed the corresponding state/handlers/tables from Integrations.tsx
entirely (374 lines). Adding, editing, enabling/disabling, and
deleting each credential itself still happens under Integrations ->
Manage integrations, same as Proxmox and Synology, which stay on the
Integrations page. Selecting one of these three types in Integrations'
generic browsing dropdown now points to its new page instead of
rendering a table there too.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
||
|
|
52dc7ed1f5 |
Move Dockhand container management to its own top-level Docker page
Container management was buried inside the generic Integrations browsing view, mixed in with five unrelated integration types behind a single "pick any integration" dropdown. Give it a dedicated page, matching how Servers & Tasks and DNS already get their own top-level spot instead of living inside Integrations. New web/src/pages/Docker.tsx reuses the existing, unchanged Dockhand API routes and adapter (no server changes) — it just has its own integration picker scoped to Dockhand only, rather than sharing Integrations' any-type dropdown. Added a Docker nav item (right after Integrations) and route. Removed the Dockhand-specific state/handlers/table from Integrations entirely; adding, editing, enabling/disabling, and deleting the Dockhand credential itself still happens under Integrations -> Manage integrations like every other integration. Selecting a Dockhand row in Integrations' generic browsing dropdown now points to the Docker page instead of rendering a container table there too. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
f92f8de96e |
Add a Date & Time setting and fix inconsistent date formats app-wide
Different tables used different date formats: Servers & Tasks used a fixed "YYYY-MM-DD HH:mm:ss" (24h), while Audit Log, DNS, Integrations (Tailscale/Semaphore), and Users called plain .toLocaleString() with no options, which renders using the browser's own locale — different per browser/OS, and inconsistent with the other pages' fixed format. - New Settings -> Date & Time page: pick date order (YYYY-MM-DD, DD/MM/YYYY, MM/DD/YYYY) and 12h vs 24h clock, with a live preview. - utils/date.ts's formatDateTime() now reads these settings instead of being hardcoded to sv-SE/24h; the setting is fetched once at app startup (alongside /api/me) via a new non-secret GET /api/settings/display (any signed-in user, same rationale as the badge-color endpoints) and applied immediately on save too, without needing a page reload. - Switched every remaining raw new Date(...).toLocaleString() call (Audit Log, DNS zone sync time, Tailscale/Semaphore last-seen, Users' last login) over to the shared formatter, so every table now renders dates identically. Verified the formatter's date-order x 12h logic against all six combinations plus the midnight/noon 12h edge cases, and the settings endpoints end-to-end (defaults, partial updates, validation rejection, audit logging) against the real dev server. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
ad6783b6a1 |
Extend badge color settings to cover integration types too
Settings → DNS Badges only let you customize DNS provider badge colors. Expand it into a general Badges page with a second section for the six integration types (Tailscale, Proxmox, Synology, Semaphore, Gitea, Dockhand), applied to the type badge in the Manage integrations table. - New integrationColors key on AppSettings, stored/merged the same way as providerColors via the existing settingsStore. - New GET /api/settings/integration-colors — non-secret, any signed-in user, mirroring /provider-colors — so the badge color can be read without needing admin access to the full settings payload. - Renamed DnsBadgeSettings.tsx -> BadgeSettings.tsx (route /settings/dns-badges -> /settings/badges, sub-nav label "DNS Badges" -> "Badges") with both color sections saved together. Verified end-to-end against the real dev server: PUT persists integration colors independently of provider colors, the public integration-colors endpoint reflects updates immediately, and the change is audit-logged. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
a6ab69316d |
Add a graceful Shutdown action alongside Proxmox's Start/Restart/Stop
Stop maps to Proxmox's hard power-off (/status/stop) — fine for a
crashed guest, but risky for anything with a filesystem that'd rather
flush cleanly first. Proxmox exposes a separate /status/shutdown
endpoint that asks the guest to power itself down (ACPI event for a
VM, SIGTERM-then-wait for a container), so add it as its own action
rather than overloading Stop.
- New shutdownGuest() on the Proxmox adapter, calling /status/shutdown.
- The existing generic action route/loop already dispatches by
${action}Guest, so adding "shutdown" to that list was enough on the
server side — no new route needed.
- New Shutdown button next to Restart/Stop on both the Integrations
page's guest table and a Proxmox-linked server's detail page.
Stop's confirm prompt now explicitly points at Shutdown as the
gentler alternative.
Verified end-to-end against a mock Proxmox server: the shutdown call
hits /status/shutdown (never /status/stop) and is audit-logged as
shutdown_guest.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
||
|
|
f5d3c25c89 |
Add editing existing integrations, so an expired secret can be rotated
Manage integrations only supported add/toggle/delete — fixing an expired API token meant deleting and recreating the whole integration. - New GET /api/integrations/:id/config returns only the non-secret config fields (never the decrypted secret) so an edit form can pre-fill URL/tailnet/etc. fields. - New POST /api/integrations/:id/test merges the stored, decrypted config with any freshly-typed overrides and pings the real adapter — lets "Test connection" work during an edit without ever sending the current secret back to the browser. - New IntegrationEditForm component: secret fields render blank with a "leave blank to keep the current value" placeholder; submitting only sends the fields that were actually filled in, so a name/URL edit can't accidentally wipe a secret and a secret rotation can't touch anything else. Reuses the existing PATCH /:id route, which already merged partial config updates correctly. Verified end-to-end against the real dev server: confirmed via direct DB decryption that a non-secret-only edit leaves the stored secret byte-for-byte unchanged, and that a secret-only edit rotates it without touching other config; the test route was confirmed to make a real network call (got a genuine "API token invalid" from Tailscale's API against a fake key). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
a83a4b3b11 |
Add Synology integration — completes all six planned live integrations
Sixth and final live integration: volume and disk health across the NAS, read-only per the delivery plan (DSM write actions are riskier and stayed out of scope). Adapter built against the same discover-then-call pattern used by hacf-fr/synologydsm-api (the library behind Home Assistant's Synology integration) since DSM's API paths/versions vary by release and the user's NAS isn't reachable from here to check directly: - GET query.cgi?api=SYNO.API.Info&method=query&query=all once per adapter instance, to learn the real path/version for SYNO.API.Auth and SYNO.Storage.CGI.Storage rather than hardcoding them - SYNO.API.Auth login (account/passwd/format=sid) for a session id, re-used across calls and refreshed on session-related error codes (105/106/119) - SYNO.Storage.CGI.Storage's `load_info` method, which returns disks, volumes, and pools in one call — verified against that library's storage.py field mapping (size.total/used, status, smart_status, temp, exceed_bad_sector_thr, below_remain_life_thr) Uses node:https directly (like Proxmox and the cPanel DNS adapter) for an "allow self-signed certificate" option, since DSM ships one by default. No 2FA support — login surfaces a clear error if the account requires it rather than failing silently. Verified: full build passes. Unreachable from here, so ran an 11-check HTTP test against the live server: confirms all six integration types are now registered, role gating, credential (password) non-leakage, disabled- integration blocking, and the wrong_type crash-safety check — server stays up throughout. Real volume/disk data still needs verification once this app can reach the user's NAS. This completes the integrations phase from the original plan: Tailscale, Gitea, Dockhand, Semaphore, Proxmox, Synology are all built, each following the same config-in-UI + encrypted-credentials pattern. Combined with the foundation, Secrets, IPAM, DNS, and Servers & Tasks modules from earlier, Homelab Manager now covers every feature area from the user's original request. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
bc72b9e152 |
Add Proxmox integration
Fifth live integration: VM/LXC status across every online node in the
cluster, with start/stop/restart actions — matching the "dashboard + basic
actions" depth from the plan. Adapter built against Proxmox VE's own
published API tree (pve.proxmox.com/pve-docs/api-viewer/apidoc.js — parsed
its ~4MB ExtJS tree structure directly since it's not plain JSON) plus
community-verified docs for the token auth header format, since the user's
instance isn't reachable from here.
server/src/integrations/proxmox/adapter.ts: GET /nodes for online nodes,
then GET /nodes/{node}/{qemu,lxc} per node in parallel (Promise.all) and
flattened, tagging each guest with its node and type; POST
/nodes/{node}/{qemu,lxc}/{vmid}/status/{start,stop,reboot} for actions (all
confirmed token-auth-eligible via the spec's "allowtoken" flag). Uses
node:https directly (like the cPanel DNS adapter) rather than fetch, since
Proxmox commonly runs a self-signed certificate in homelab setups — added
an "Allow self-signed certificate" checkbox field for that. Auth is
`Authorization: PVEAPIToken=<tokenId>=<tokenSecret>`, a different shape
from the other three integrations' single bearer token, so the field
schema splits it into two fields (a non-secret token ID like
"root@pam!homelab-manager" and a secret token value).
Verified: full build passes. Unreachable from here, so ran a 16-check HTTP
test against the live server: role gating, credential non-leakage,
disabled-integration blocking, invalid-guest-type and non-numeric-vmid
rejection, checkbox-only config correctly still failing required-field
validation, and the wrong_type crash-safety check for this fifth adapter
type — server stays up throughout. Real node/VM data and the actual
start/stop/restart actions still need verification once this app can reach
the user's Proxmox cluster.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
||
|
|
abb5335a52 |
Add Semaphore integration
Fourth live integration: Ansible run history per template with a
trigger-a-run action — matching the "dashboard + basic actions" depth from
the plan. Adapter built against Semaphore's official published OpenAPI spec
(github.com/semaphoreui/semaphore/blob/develop/api-docs.yml) plus its Go
source directly for the task-status enum, which the spec itself leaves
untyped (db/Task.go, pkg/task_logger/task_logger.go) — instance wasn't
reachable from here (semaphore.int.toolabs.se doesn't resolve outside the
user's LAN), so verified against the real spec/source rather than guessing.
server/src/integrations/semaphore/adapter.ts: GET /api/projects, then GET
/api/project/{id}/templates?sort=name&order=asc per project — each template
in that response already embeds its last_task, so listing every template's
current status across every project needs only one call per project (no
N+1 per-template lookup, unlike Gitea where the run history isn't embedded
in the repo list). POST /api/project/{id}/tasks {template_id} triggers a
run. Follows the same config-in-UI + encrypted-credential pattern as the
other three integrations.
Verified: full build passes. Same as Dockhand — unreachable, so ran a
14-check HTTP test against the live server (real target URL, bogus token):
role gating, credential non-leakage, disabled-integration blocking, and the
wrong_type crash-safety check for this fourth adapter type, confirming the
server stays up throughout. Real template/run data and the trigger-a-run
action still need verification once this app can reach the user's LAN.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
||
|
|
9c45a806c8 |
Add Dockhand integration
Third live integration: container status across every Docker host Dockhand manages, with start/stop/restart actions — matching the "dashboard + basic actions" depth from the plan. Talks to Dockhand's own aggregating REST API (bearer tokens, dh_...) rather than the raw Docker Engine API on each host directly, so one credential covers every host Dockhand is already connected to. Adapter built against Dockhand's real published OpenAPI spec (fetched from https://github.com/strausmann/mcp-dockhand/blob/main/docs/dockhand-openapi.json, 257 documented paths) since the instance itself isn't reachable from here — same "verify against the real shape, don't guess" approach as Gitea, just via the spec instead of a live instance: - GET /api/environments — the Docker hosts Dockhand knows about - GET /api/containers?env=<id>&all=true — containers per host ({id, name, image, state, status}) - POST /api/containers/{id}/{start,stop,restart}?env=<id> server/src/integrations/dockhand/adapter.ts fans the environments call out to one containers call per host (Promise.all) and flattens the result, tagging each container with its environment; one unreachable host returns an empty list for that host rather than failing the whole dashboard view. Follows the same config-in-UI + encrypted-credential pattern as Tailscale and Gitea. Verified: full build passes. Since Dockhand isn't reachable from here, ran a 14-check HTTP test against the live server instead of the real API — role gating, credential non-leakage, disabled-integration blocking, and (critically) re-confirmed the wrong_type crash-safety pattern holds for this third adapter type: hitting the Dockhand routes on a differently-typed integration row returns a clean 400 rather than crashing the process, and the server keeps responding to /health afterward. Real container data and the start/stop/restart actions still need verification once this app runs on the user's LAN where Dockhand is reachable. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
79710aa7a5 |
Add Gitea integration; fix .env never being loaded outside Docker
Second live integration: repo list with last CI run status, and re-running
failed jobs on a workflow run — matching the "dashboard + basic actions"
depth from the plan. Adapter built directly against the real Gitea 1.27
swagger spec (fetched from the user's own instance) rather than guessing at
the API shape: GET /user/repos for the repo list, GET
/repos/{owner}/{repo}/actions/runs?limit=1 for the latest run per repo (only
for repos with Actions enabled), and POST .../rerun-failed-jobs for retrying
just the failed jobs in a run. Follows the same config-in-UI +
encrypted-credential pattern as Tailscale and DNS providers.
Since Gitea collects its own base URL as a config field (unlike Tailscale,
which always talks to a fixed api.tailscale.com), generalized the
"integrations.baseUrl" bookkeeping into resolveBaseUrl() instead of the
one-fixed-URL-per-type map used previously.
Also fixed a real gap found while setting this up: server/src/env.ts reads
process.env directly, but nothing in the app ever loaded .env into
process.env for plain `node dist/index.js` / `tsx src/index.ts` runs — only
Docker's `env_file` config populated it, by injecting vars before Node even
starts. Every local (non-Docker) run silently had every setting at its
insecure default. Added server/src/loadEnv.ts (dotenv, pointed at the
repo-root .env) as the first import in both server/src/index.ts and
server/src/db/migrate.ts's standalone entrypoint.
Verified against the user's real, reachable services — not mocks:
- Authentik (auth.labsconnect.se): full OIDC login completed by the user
through the real UI; confirmed their account landed as admin (first user).
- Gitea (gitea.labsconnect.se): the compiled adapter run directly against a
real API token correctly listed all 11 real repos; a full HTTP-layer test
against the live server (8 checks) additionally covered a real
test-connection ping, credential non-leakage in list responses, and role
gating (403) on the rerun-failed-jobs action even with a valid token
behind it. None of the real repos have any workflow run history yet, so
the success/failure status badge and the rerun action itself are
implemented per the swagger spec but not yet exercised against a real run
— worth checking once one of those repos has actual CI activity.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
||
|
|
9c2742c30f |
Add Tailscale integration and harden all routes against crash-on-throw
First of the six planned live integrations (Proxmox, Synology, Semaphore, Tailscale, Gitea, Dockhand), reusing Sloth Manager's existing Tailscale adapter logic. Generalizes the "integrations" table already scaffolded in the foundation pass into a working config-in-UI + encrypted-credentials flow, following the same pattern as the DNS providers module — an "Add integration" form only offers types with an implemented adapter (currently just Tailscale), so the framework is ready for the next five integrations without further schema/plumbing changes. - server/src/integrations/tailscale/adapter.ts: ported from Sloth Manager's backend/src/adapters/tailscale.js — listDevices/setAuthorized/deleteDevice against the Tailscale API, now config-based (tailnet + apiKey) instead of reading process.env, and returning a ping() result instead of throwing. - server/src/routes/integrations.ts: generic integration CRUD (admin) + a "test connection" endpoint, plus Tailscale-specific device routes (dashboard-and-basic-actions depth per the plan: authorize/deauthorize/ remove, gated to operator+, audit-logged). - web: an Integrations page (provider-style manage/browse split, matching the DNS page's UX) with a device table, and a live Tailscale widget on the Dashboard. Bug found and fixed while testing: hitting the Tailscale device routes on a non-Tailscale integration row crashed the ENTIRE server process, not just that request — the generic adapter registry throws for unimplemented types, and that throw happened inside an async handler with no surrounding try/catch, which Express 4 doesn't catch, so it became an unhandled rejection that (on modern Node) kills the process. Fixed at the source (check the row's type before ever constructing an adapter) and, since the same "a helper throws before any local try/catch runs" shape existed wherever a route calls into loadDnsProviderConfig/loadIntegrationConfig (both call decryptSecret, which throws if CREDENTIALS_ENCRYPTION_KEY is ever wrong/missing after data was already encrypted with a different key), added a small asyncHandler() wrapper and applied it to every route handler across every router — a single bad request should never be able to take the whole app down for every user. Verified: full build passes. Fresh HTTP-layer tests against a running server (17 checks) cover not-implemented-type rejection, missing-field validation, a real network call to api.tailscale.com with a bogus key (clean ok:false, not a crash), role gating at every tier, credential non-leakage, disabled-integration blocking, and the wrong_type case that originally crashed the server — confirmed it now returns 400 cleanly and the server stays up. Re-ran the existing DNS (13 checks) and Servers/Tasks suites afterward to confirm the asyncHandler sweep didn't regress anything — all passing. Authorizing/removing a real device still needs a real Tailscale API key to verify end-to-end. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |