Found while verifying against the user's real tailnet (25 devices): the
Tailscale device object has no "online" or "isExitNode" field at all — this
was inherited from Sloth Manager's original adapter, which apparently never
had this checked against live data either. With the old mapping, every
device showed online:null and isExitNode was always false regardless of
reality.
Fixed by deriving both from fields that actually exist:
- online <- connectedToControl (whether the device currently has an active
session with Tailscale's control plane)
- isExitNode <- enabledRoutes containing both 0.0.0.0/0 and ::/0 (a device
can *advertise* exit-node routes without them being approved; checking
enabledRoutes instead of advertisedRoutes reflects whether it's actually
acting as one right now)
Both fields come back from the list endpoint via `?fields=all`, so this
adds no extra requests.
Verified against the real tailnet: 25 devices, 24 online / 1 offline (a
phone last seen 9 days ago — correct), and the one device actually
configured as an exit node identified correctly. This is the last of the
five previously-unverified integrations now confirmed against real
infrastructure; combined with the earlier Synology (HTTP vs HTTPS) and
Proxmox (async task timing) findings, every integration has now had at
least one real bug shaken out by testing against the user's actual homelab
rather than mocks alone.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Found while verifying against the user's real DSM: the adapter hardcoded
node:https for every request, but the user's NAS is reached over plain
HTTP on port 5000 inside the LAN (HTTPS on 5001 also works, but nothing
about the integration should assume one or the other). Now picks http vs
https based on the configured URL's own protocol, with the right default
port per protocol (5000/5001) and TLS options only applied for https.
Verified against the user's real Synology (10.200.5.35): ping, login, and
SYNO.Storage.CGI.Storage load_info all confirmed working end-to-end through
the actual HTTP route layer — 1 volume (normal, SHR, ~11.5TB/~3.9TB used),
4 disks (all normal, 34-39°C). This is the first of the five previously-
unverified integrations confirmed against real hardware.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
Ports cron/systemd task tracking across Debian/Raspbian servers, including
the Linux push agent (install/report/uninstall scripts, rebranded from
"schedule-task-manager-agent" to "homelab-manager-agent") and the
manual-task-entry flow for things an agent can't see (Docker jobs, backups).
The schema (servers/scheduled_tasks tables) was already in place from the
foundation pass, so this is mostly a straight port of the original's
services/routes.
Deliberate change from the original: server/token management (which mints
agent credentials) is now admin-only rather than open to any logged-in user,
and manual task CRUD is gated to operator+ — consistent with how Secrets,
IPAM, and DNS already split "configure credentials" from "everyday edits"
across roles. All mutations are audit-logged.
- server/src/services/tokens.ts, taskSync.ts: ported near-verbatim (agent
token hashing, the agent-sync-marks-missing-as-stale-not-deleted logic).
- server/src/routes/servers.ts, tasks.ts, agentReport.ts: same contract as
the original (agent auth is a per-server bearer token, independent of the
session-based requireAuth used everywhere else).
- web: a single Servers & Tasks page (filter bar, task table grouped by
server/schedule type, manual task form, and an admin-only server
management panel with token reveal + copyable install/uninstall commands),
replacing the original's two separate pages/apps.
Verified: full build passes; a scripted HTTP test against a running server
covers unauthenticated access, role gating at each tier (admin-only server
mgmt, operator+ task mgmt), agent bearer-token auth (valid/invalid/rotated),
manual-vs-agent task edit protection, stale-marking on re-sync, and cascade
delete — 22/22 checks passing. Real agent installation on an actual
Debian/Raspbian host still needs to be tried on the user's network.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ports zone/record management across Cloudflare, Loopia, Pi-hole, Azure DNS,
cPanel, and Technitium onto the new stack. Unlike the original (one instance
per provider configured via env vars), providers are now configured through
the UI and support multiple named instances per type, with API
credentials encrypted at rest via the integration_credentials table.
- server/src/dns/adapters/*: each provider ported to a config-based factory
(no more process.env reads), preserving each provider's original quirks
(Pi-hole session auth, Azure record-set merging, Loopia XML-RPC, cPanel
UAPI/API2 fallbacks, Technitium composite record IDs).
- server/src/routes/dns.ts: provider CRUD (admin), a "test connection"
endpoint, and zone/record browsing+sync+CRUD (operator+), all audit-logged.
- dns_zones_cache/dns_records_cache tables replace Sloth Manager's
dns-cache.json file, keeping the same "cache is the source of truth for
display, sync fetches fresh from the provider" behavior.
- web: a DNS page with provider management, zone browsing, and a record
editor, plus a reusable dynamic provider-config form.
Verified: full build (tsc + vite) passes; a scripted HTTP-layer test against
a running server exercises auth, role gating (403 for viewer), validation
(400 on missing config fields), provider CRUD, credential non-leakage in
list responses, and adapter error propagation (502 against an unreachable
host) — all passing. Real provider connectivity still needs to be checked
against the user's actual DNS accounts.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Monorepo (Express+TS+Drizzle/libSQL server, React+Vite+Tabler web) matching
the stack used by ScheduleTaskManager and Sloth Manager. Includes Authentik
OIDC login with local admin/operator/viewer roles (first user becomes admin),
a generalized audit log, encrypted-at-rest storage for future integration API
tokens, the DB schema for all planned modules, and the Tabler-styled app
shell/nav. Also ports the Secrets (expiry tracker) and IP Addresses (IPAM)
modules from Sloth Manager onto the new stack.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>