Commit Graph
10 Commits
Author SHA1 Message Date
bobbanandClaude Sonnet 5 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>
2026-09-15 22:17:06 +02:00
bobbanandClaude Sonnet 5 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>
2026-09-15 21:28:26 +02:00
bobbanandClaude Sonnet 5 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>
2026-09-15 20:52:31 +02:00
bobbanandClaude Sonnet 5 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>
2026-09-15 13:12:17 +02:00
bobbanandClaude Sonnet 5 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>
2026-09-15 00:35:21 +02:00
bobbanandClaude Sonnet 5 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>
2026-09-15 00:24:37 +02:00
bobbanandClaude Sonnet 5 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>
2026-09-15 00:14:44 +02:00
bobbanandClaude Sonnet 5 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>
2026-09-15 00:04:34 +02:00
bobbanandClaude Sonnet 5 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>
2026-09-14 23:55:41 +02:00
bobbanandClaude Sonnet 5 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>
2026-09-14 23:31:19 +02:00