f92f8de96e39ed07f66eea481d830c58f43c0824
10
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |