Settings → Integrations now has a universal "Icon" field (any integration
type) accepting a pasted URL or an uploaded image, stored as config.iconUrl.
This overrides the built-in icon for that integration's Node Status tile.
Node Status tiles now resolve their icon through a priority chain: custom
iconUrl, then each built-in CDN candidate in order (assets-public first
where available, falling back to the existing dashboard-icons CDN), and
finally the generic per-kind Lucide icon if every candidate 404s. AWS now
tries samuelsjames.github.io/assets-public's aws-logo.svg before the
jsDelivr fallback; SSH gets a Linux logo from the same repo. Proxmox,
Weather, and Remote Desktop have no built-in candidates yet (no matching
assets in that repo) and fall back to the generic icon until added.
Co-authored-by: Claude <noreply@anthropic.com>
Generalizes the Uptime Kuma monitor-grouping pattern to every integration:
Node Status now collapses each integration's resources into one tile (e.g.
30 EC2 instances under one "AWS" tile) instead of flooding the grid, with
members listed in Node Detail on selection. Proxmox stays ungrouped since
its VMs/LXCs are managed individually elsewhere in the app.
Adds integrationType to the /api/integrations/resources response so the
frontend can group/exclude by adapter type rather than resource kind (kind
alone can't distinguish Proxmox VMs from AWS VMs, for example).
Documents the grouping rule in HANDOFF.md and adds a paid-tier roadmap
entry for per-integration node tabs that will show every individual node.
Co-authored-by: Claude <noreply@anthropic.com>
heartbeatList/importantHeartbeatList emit monitor IDs as strings (server
iterates object keys), while monitorList and the live heartbeat event use
numbers. The lastHeartbeat map was keyed by the numeric monitor.id, so
string-keyed lookups from heartbeatList/importantHeartbeatList never hit.
Co-authored-by: Claude <noreply@anthropic.com>
importantHeartbeatList only contains entries for status transitions, so a
monitor that's been continuously up since creation never populates it,
showing as "unknown" in ArchNest despite being healthy in Uptime Kuma.
Co-authored-by: Claude <noreply@anthropic.com>
Integrations whose resources represent many sub-items (Uptime Kuma's
monitors) now collapse into a single tile using the Uptime Kuma CDN
icon, instead of flooding Node Status with one tile per monitor.
Selecting that tile lists every underlying monitor's status in a
scrollable Node Detail panel, so hundreds of monitors stay manageable.
Also drops the temporary debug logging added while diagnosing the
listener-timing bug, now that real monitor/heartbeat data confirmed
coming through.
Listeners for monitorList/importantHeartbeatList/heartbeat were being
attached after the login ack resolved, but the server pushes that
data right after login — sometimes in the same tick as the ack — so
Socket.IO dropped it before a listener existed. Listeners now attach
before login is sent.
Diagnosing why a connected Uptime Kuma instance with real monitors is
producing zero Resources — logs monitor count, active flags, and last
heartbeat per monitor so we can see exactly what the Socket.IO session
returns.
Each adapter now tags its Resources with a kind (vm, container, app,
host, network) so Node Status tiles show the right icon instead of a
generic server glyph — Proxmox LXCs/Docker containers get a container
icon, VMs get a VM icon, Uptime Kuma monitors get an app icon, etc.
Also stops silently swallowing listResources() failures — they're now
logged as warnings, since a connected-but-empty integration (e.g.
Uptime Kuma reporting zero monitors) was previously indistinguishable
from a real adapter error.
Uptime Kuma has no REST API for monitor data; connect over the same
Socket.IO session the web UI uses (login, then read monitorList and
heartbeat events) so connected monitors now surface as Resources.
Switches the integration's credentials from an API key to
username/password, matching what Uptime Kuma's session login expects.
Lets a user pick one of three Starship presets (Nerd Font Symbols,
Pastel Powerline, Tokyo Night) from the Terminal page and install
Starship + a Nerd Font on the active pane's SSH host with one click,
instead of running a script by hand. Idempotent on the host side, and
available to all authenticated users like the rest of the SSH/Docker
tooling.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019hu9pZvJY4BgmcQeAw2ugk
A host can be on the mesh's "side" of a routed network (e.g. a VPC peered
into a NetBird/WireGuard mesh) without holding a local IP in the mesh's
own CIDR. Local-IP-in-CIDR stays the primary check; if it fails, the admin
can supply a known peer/gateway IP on the mesh and we verify by pinging
it instead. Adds iputils to the backend image for the ping binary.
Replace the NetBird-adapter-based "reachable" check with a vendor-agnostic
one: the admin supplies the mesh's IP range (CIDR), and verification just
confirms this host has an address inside it. Works identically for
NetBird, WireGuard, ZeroTier, Tailscale, or any other mesh tech, with no
integration record or vendor API call required.
Implements the design in docs/mesh-prerequisite-gate.md per the user's
DECIDE A-D answers: a permanent admin override, B1 (reachable) verification
with host mesh IP shown informationally, members allowed in with a notice
instead of being blocked, and mesh.required defaulting off so the live
production instance is unaffected.
- system_config kv table + getConfig/setConfig helpers
- /api/system/mesh-status, /mesh/verify, /mesh/override, /mesh/required
- AuthContext gains a 'needs-mesh' status (admins only) and exposes
meshStatus for a member-facing banner
- MeshGate page reuses the integration create+test flow to connect NetBird
Expands the Containers feature with two new ways to see and manage Docker
containers without exposing the Docker Engine TCP socket, plus the docs and
roadmap entries that frame them.
Docker over SSH (management):
- Runs the `docker` CLI on a remote SSH host instead of talking to the Engine
TCP API, reusing the existing SSH transport (jump-host chaining, host-key
verification, key/password auth) via connectTarget + execCommand. No dockerd
socket has to be exposed — the mesh + SSH auth are the gate.
- backend/src/ssh/docker.ts: list/logs/start/stop/restart/pause/unpause/remove
and an interactive `docker exec` shell builder. Container refs are validated
against a strict allowlist and single-quoted to prevent command injection;
action verbs are whitelisted.
- backend/src/routes/dockerSsh.ts: REST routes mirroring the TCP Docker API
shape (mutating actions gated by adminOnly) + a /api/docker-ssh/exec
WebSocket modeled on the terminal PTY plumbing.
- Note: the SSH path uses the ssh2 key/password auth; it does not implement the
OpenSSH-certificate (OPKSSH) fallback that the terminal route has.
Docker push-agent monitoring (self-hosted, read-only):
- A small bash agent (agent/archnest-docker-agent.sh) runs on each Docker VM,
collects a rich snapshot (docker ps + inspect + a stats snapshot), masks
secret-looking env values locally, and POSTs it to ArchNest. VMs need
outbound-only mesh access — no exposed port, no SSH for monitoring.
- backend/src/routes/agents.ts: token-gated ingest
(POST /api/agents/docker/report, ARCHNEST_AGENT_TOKEN, constant-time compare;
503 when unset, so it is disabled by default) plus user-auth read endpoints
(hosts list with staleness flag, per-host containers, single-container
detail). New docker_agent_reports table (latest report per host).
- Ingest stores data only; it never executes anything from the agent.
Containers page:
- Host selector now spans Docker API, SSH, and Agent sources.
- Intra-page tabs: a Containers list plus dynamic, closeable per-container
detail tabs opened by clicking a container name. Agent detail shows
overview/state/stats/ports/networks/mounts/env(masked)/labels; docker/ssh
degrade gracefully. Agent rows are read-only; docker/ssh keep management.
Docs/roadmap:
- docs/docker-agent-monitoring.md (design doc, written before implementation).
- ROADMAP.md: LXC management (paid), Docker monitoring agent tiering
(push self-hosted now / pull-agent paid), terminal grid tiering.
Deferred (documented, not built here): the mesh-prerequisite setup gate, the
paid pull-agent (Option 2), per-host tokens, time-series metrics.
Requires ARCHNEST_AGENT_TOKEN in the backend env to enable agent ingest.
Verified: backend `tsc --noEmit` and frontend `tsc -b && vite build` both pass;
agent jq filters, byte conversion, and `bash -n` checked locally.
Co-authored-by: Samuel James <ssamjame@amazon.com>
Co-authored-by: Kiro <noreply@kiro.dev>
Implements Phase 3 of the auth roadmap: multiple user accounts (cap 10),
an admin/member role model, and admin-only gating of config-mutating
routes. Dashboard data stays shared across all users (per the product
decision in HANDOFF.md — this is a household/self-hosted dashboard, not
a multi-tenant app), so there is no per-user data isolation.
Schema (backend/src/db/index.ts):
- Idempotent migration adds `role` (default 'admin') and `active`
(default 1) columns to `users` when missing. The 'admin' default means
the pre-existing single user is backfilled to admin on deploy and keeps
full access; newly created users are inserted explicitly as 'member'.
Verified against a production-like old schema (columns added, existing
user backfilled to admin/active).
Auth + access control:
- `/api/setup` creates the first user as admin. Login enforces `active`
(deactivated accounts get 403) and embeds the live role in the session.
- `app.authenticate` now reads role+active fresh from the DB on every
request (not from the possibly-stale JWT claim), rejects inactive
accounts, and stashes the role on req.user.
- New `requireAdmin` (auth + role check) and `adminOnly` (role check for
routes already behind the plugin-level authenticate hook) decorators.
User management (admin-only, in auth.ts):
- GET/POST/PUT/DELETE /api/users — list, create (admin sets a temp
password; no public signup), change role, activate/deactivate, delete.
- 10-user cap enforced server-side; guard rails prevent removing the last
active admin (demote/deactivate/delete) and deleting your own account;
deactivating or deleting a user drops their sessions immediately.
Admin-only route gating (members get 403):
- integrations create/update/delete/test, tunnels create/delete, data
export/import. Read routes and tunnel connect/disconnect stay open to
all authenticated users, as do all the SSH/Docker/RDP tools and
bookmarks (members are trusted to use the tooling, per product decision).
Frontend:
- api.ts: listUsers/createUser/updateUser/deleteUser + ManagedUser type;
role+active added to AuthUser.
- Settings: new admin-only "Users" section (create form, role toggle,
activate/deactivate, delete, 10-cap indicator). Nav filters the Users
tab by role and guards ?tab= deep-links. Data & Backup shows an
admin-only notice for members; Integrations shows a read-only banner
for members. (Backend remains the real enforcement boundary.)
Verified end-to-end against a throwaway backend: role assignment,
member 403s on every admin-only route + 200s on shared/read routes,
admin 200/201s, last-admin guards (409/400), deactivation killing an
active session and blocking re-login (then reactivation restoring it),
and the 10-user cap (409 on the 11th). Both frontend and backend
type-check clean.
Co-authored-by: Samuel James <ssamjame@amazon.com>
Co-authored-by: Kiro <noreply@kiro.dev>
Builds out the Settings → Security tab (previously a "coming soon"
placeholder) and the backend behind it. Still single-user; multi-user
and SSO remain Phases 3-4.
Backend:
- New `sessions` table (id, user_id, user_agent, ip, created_at,
last_seen_at) and `login_events` table (user_id, username, ip,
user_agent, success, created_at).
- Login and setup now mint a session row and embed its id as a `sid`
claim in the JWT. The `authenticate` hook validates that the session
still exists (and bumps last_seen_at), so revoking a session genuinely
invalidates its token instead of relying on the JWT signature alone.
Tokens minted before sessions existed have no `sid` and stay valid
until expiry, for backward compatibility.
- Every login attempt (success and failure) is recorded in login_events
for the audit trail.
- New endpoints: PUT /api/auth/password (verifies current via bcrypt,
hashes new at cost 12, revokes all *other* sessions on success),
GET /api/auth/sessions, DELETE /api/auth/sessions/:id (can't revoke
the current one), POST /api/auth/logout (revokes current session),
GET /api/auth/login-events?limit.
- AuthContext.logout() now calls POST /api/auth/logout best-effort so
signing out revokes the server session, not just the local token.
Frontend:
- SecuritySection: change-password form (current/new/confirm with
show/hide and client-side validation), active-sessions list (device
description from user-agent, IP, last-seen relative time, per-session
"Sign out" for non-current sessions), and a recent login-activity feed
(success/failure dot, user, IP, relative time).
- api.ts: changePassword/listSessions/revokeSession/logout/
listLoginEvents + AuthSession/LoginEvent types.
Verified end-to-end against a throwaway backend instance: session
creation, second-device session, failed-login logging, cross-session
revocation invalidating the revoked token, password change keeping the
current session alive while revoking others, and logout invalidating the
current session. Frontend + backend both type-check clean.
Co-authored-by: Samuel James <ssamjame@amazon.com>
Co-authored-by: Kiro <noreply@kiro.dev>
The Terminal page failed to open a shell for one host (Linode) while
Host Metrics worked fine for the same host, and other hosts (pve1/pve2)
worked everywhere. Root cause: the terminal route takes a special
certificate-auth path whenever an SSH integration has ANY `certificate`
secret set, and that path shells out to the system `ssh` binary under a
pty instead of using the ssh2 library. The metrics path always uses
ssh2, which is why it was unaffected.
That host's `certificate` secret was actually a plain public key
(`ssh-ed25519 AAAA...`), not an OpenSSH certificate. ssh discarded it
("is not a certificate") and then could not load the private key under
the container's libcrypto ("error in libcrypto: unsupported"), ending in
"Permission denied (publickey)". With ssh2 (the metrics path), the same
private key authenticates fine.
Two fixes:
- Only take the cert-auth path when the secret is a genuine OpenSSH
certificate (key type ends in `-cert-v01@openssh.com`). A plain public
key now falls through to the normal ssh2 key/password path, which
already works (proven by the metrics endpoint using the same key).
- Add `-o IdentitiesOnly=yes` to the cert-auth ssh invocation so it only
offers the provided key/cert and isn't confused by a stray file.
No server-side or key changes were needed on the affected host; this is
purely a routing/robustness fix in the terminal WebSocket handler.
Co-authored-by: Samuel James <ssamjame@amazon.com>
Co-authored-by: Kiro <noreply@kiro.dev>
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
* Add JSON-converted bookmark import file for Archnest data import
Converts homarr-bookmarks.md into the format expected by /api/data/import.
* Auto-populate bookmark icons via favicon service in import JSON
Each bookmark now points to Google's favicon endpoint for its domain
instead of having no icon at all.
* Add bulk delete-all for bookmarks
Adds DELETE /api/bookmarks to clear every bookmark in one request, and a
"Delete All" button (with confirmation) on the BookNest page so re-imports
don't require deleting dozens of entries one at a time.
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
* Show saved indicator for secret fields instead of appearing blank/deleted
GET /api/integrations never returns decrypted secret values (by design),
so after navigating away and back, secret/key fields rendered empty -
looking exactly like the saved key had been deleted, even though it was
still intact and encrypted in the database. Expose which secret keys
exist (names only, never values) via secretKeys, and use it to label
fields as "saved" with an appropriate placeholder instead of blank.
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
* Enable OpenSSL legacy provider for old-format encrypted SSH keys
OpenSSL 3's default provider disables the MD5-based KDF used by
traditional encrypted PEM keys (BEGIN RSA PRIVATE KEY + DEK-Info
headers), causing "error in libcrypto: unsupported" when the ssh
binary tries to decrypt them for certificate-based auth.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
* Fix integration save wiping untouched config fields
The PUT /api/integrations/:id route fully overwrites config_json with
whatever config object is sent (no merge), but buildPayload only
included fields the user had actually edited. Saving after editing
just one field (e.g. pasting a new SSH key) silently dropped every
other config field. Merge the existing integration's config into the
payload before sending.
* Add collapse/expand for SSH host cards
Click the chevron to collapse a host's card once it's configured.
Collapsed cards keep all field state in memory (just hidden), and
auto-collapse after a successful Save.
* Install openssh-client in backend image for certificate-auth SSH
Certificate-based SSH connections shell out to the system ssh binary
via node-pty (ssh2 has no OpenSSH certificate support), but the
alpine runtime image never installed openssh-client. This caused
'execvp(3) failed: No such file or directory' for any host with an
OPKSSH certificate configured.
---------
Co-authored-by: Claude <noreply@anthropic.com>
Node's global fetch is backed by an internal undici version bundled with Node itself, which differs from the 'undici' npm package used for the insecure Agent (needed for Proxmox's self-signed cert). Passing an Agent from one undici version as the dispatcher for the other's fetch trips an internal handler-shape check, producing 'invalid onRequestStart method'. Importing fetch from the same undici package as the Agent keeps both on the same internal interface.
undici's fetch() collapses DNS/TLS/connection-refused/timeout failures into a vague TypeError. Unwrap err.cause so Test Connection shows the real reason (e.g. ECONNREFUSED, certificate error) instead of just 'fetch failed'.
The frontend calls the API with relative paths (fetch('/api/...')), so in
production those requests hit the nginx frontend container on :8080 — which
previously only served the SPA and would 404 every API call and WebSocket
route. nginx.conf now proxies /api/ to the archnest-backend service with
WebSocket upgrade support, long timeouts for terminals/tunnels/transfers, and
a 1GB body limit matching the backend's upload cap.
The backend Dockerfile now installs python3/make/g++ in both the build and
runtime stages so the native modules (better-sqlite3, ssh2, node-pty) compile
on alpine instead of crashing the container at startup.
The deploy workflow gains a validate job (type-check + build both apps before
touching the host), a pre-flight check that refuses to deploy without the
host-side .env, and a post-deploy health check against /api/health and the
frontend, with concurrency guarding.
GET /api/data/export serializes all integrations (with decrypted secrets, for
cross-instance portability), bookmark categories, bookmarks, and tunnels;
POST /api/data/import restores them additively in a transaction with old->new
id remapping. Wires the Settings "Data & Backup" section to download/upload the
backup file. Verified end-to-end including cross-instance portability under a
different ARCHNEST_SECRET_KEY, plus browser verification of the Settings UI.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbJV5nm8KPVH1oNJYKpnoF
Adds a guacamole/guacd service to docker-compose.yml and points the backend at
it (ARCHNEST_GUACD_HOST/PORT) plus passes through ARCHNEST_GUAC_CRYPT_KEY, with
depends_on ordering. Documents ARCHNEST_GUACD_* in backend/.env.example. Closes
the Phase 5 deployment gap. Compose validated via `docker compose config`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbJV5nm8KPVH1oNJYKpnoF
Ports the core of Termix's host-transfer feature: stream files/directories
between two SSH hosts through the backend via SFTP (read source -> write dest),
with up-front scan for progress totals, recursive directory support, optional
move, and cooperative cancellation. Leaves behind Termix's parallel-segment
workers, tar heuristics, watchdogs and retry orchestration as unjustified at
this scale.
Exposed via REST (start/list/status/cancel) with an in-memory transfer registry,
and surfaced in the Files page as a per-entry "send to another host" action plus
a live transfers progress panel. Verified end-to-end against two real SSH
endpoints: recursive copy (binary md5 match), move (source deleted), error
handling, and mid-stream cancel.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbJV5nm8KPVH1oNJYKpnoF
Ports Termix's per-host metrics collector logic onto ArchNest's own SSH
connection helpers (not its multi-user/cache/session scaffolding), exposed via
a new authenticated REST endpoint and a dedicated /host-metrics page with
client-side polling.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbJV5nm8KPVH1oNJYKpnoF
Adds a remote_desktop integration type and a /api/guacamole websocket
route that drives guacamole-lite's ClientConnection directly (bypassing
its Server class, which would otherwise attach an unfiltered upgrade
listener that conflicts with the existing @fastify/websocket routes).
The frontend RemoteDesktop page renders the Guacamole protocol stream
via guacamole-common-js. Verified end-to-end against a real guacd and
VNC server, including in an actual browser session.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbJV5nm8KPVH1oNJYKpnoF
Extends the existing Engine-API-based docker integration adapter rather than
porting Termix's SSH+CLI approach, since ArchNest's docker integrations only
ever configure a baseUrl. Adds backend/src/docker/{client,exec}.ts and
backend/src/routes/docker.ts (REST + websocket exec-terminal via raw socket
hijack), and a new Containers page wired into the sidebar/router.
Verified end-to-end against a real dockerd instance and a real container in
this sandbox, which caught and fixed a genuine bug: calling /exec/{id}/resize
before starting the exec hangs the daemon indefinitely; fixed by setting the
initial size via ConsoleSize at exec-create time instead.
Ephemeral per-request SFTP connections, whole-file-in-memory view/edit
with a 50MB cap and binary detection, streaming download for files of
any size, multipart upload. No sudo/permission-elevation or
server-to-server transfer in this pass (documented gaps, matching
Termix's own scope for the latter).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbJV5nm8KPVH1oNJYKpnoF
- backend/src/ssh/connect.ts: extracted shared SSH-connect logic
(jump-host chaining, TOFU host-key verification) out of terminal.ts
so tunnels can reuse it.
- backend/src/tunnels/manager.ts + socks5.ts: in-memory tunnel
runtime manager supporting local forward (forwardOut), remote
forward (forwardIn), and dynamic SOCKS5 proxying, with automatic
reconnect/retry and an auto-start-on-boot option. New `tunnels`
table persists configs as the saved presets.
- backend/src/routes/tunnels.ts: REST CRUD + connect/disconnect.
- src/pages/Tunnels.tsx: new /tunnels page (sidebar entry added) to
create, start/stop, and delete tunnels with live status polling.
- Verified end-to-end against a real ssh2 test server handling real
forwardOut/forwardIn requests and a real upstream TCP echo server -
all three tunnel modes moved real data, and disconnect correctly
tore down the local listener.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbJV5nm8KPVH1oNJYKpnoF
- terminal.ts: connectWithCertificate() shells out to system ssh via
node-pty for OpenSSH certificate auth (ssh2 has no native support);
list_tmux WS message + tmuxSession connect param for tmux
attach/create with shell-injection-safe name validation;
sessionLogging config field appends terminal output to disk.
- Settings.tsx: certificate secret field and sessionLogging checkbox
for SSH host integrations.
- Terminal.tsx: tmux session picker in each pane's header.
- Verified end-to-end against a real test SSH server running real
bash/tmux processes (plain shell, tmux create+list, session log
written to disk). Cert auth path type-checks but is unverified in
this sandbox (no ssh CLI available) - documented as a gap in
TERMIX_MIGRATION.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbJV5nm8KPVH1oNJYKpnoF
Terminal connections can now reference a jumpHostIntegrationId on the SSH
integration config; the backend connects to the jump host first and tunnels
to the real target via ssh2's forwardOut(), rather than connecting directly.
Added an ssh_host_keys table and a hostVerifier callback that accepts and
stores a host's fingerprint on first connect, then hard-rejects on any
mismatch on subsequent connects (trust-on-first-use).
Settings previously only ever showed/edited one integration per type, which
silently prevented configuring more than one SSH host at all. Added a
dedicated multi-host SSH section (per-host Save/Test/Delete, Add SSH Host,
and a Jump Host dropdown) so jump-host chaining is actually usable from the UI.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbJV5nm8KPVH1oNJYKpnoF
Implements the minimal-viable terminal described in TERMIX_MIGRATION.md
Phase 1a: a real interactive SSH session in the browser over a
WebSocket, using xterm.js on the frontend and ssh2 on the backend.
Reuses ArchNest's existing SSH integrations (host/port/username/
password/privateKey/passphrase) instead of introducing a second,
duplicate host-management system the way Termix has one.
Backend: new /api/terminal WebSocket route (registered via
@fastify/websocket) handling connect/input/resize/disconnect messages,
authenticated via a JWT passed as a query param (browsers can't set
custom headers on the WS handshake). Extracted the integration secret
loader out of routes/integrations.ts into db/secrets.ts so the new
terminal route can reuse it without duplicating the decrypt logic.
Frontend: new Terminal.tsx page listing configured SSH hosts and
rendering an xterm.js terminal wired to the WebSocket; wired into
App.tsx at /terminal. vite.config.ts's dev proxy now forwards
WebSocket upgrades (ws: true) so this works under `npm run dev`.
Verified end-to-end against a real (test) ssh2-based SSH server:
connect, shell banner, keystroke echo, and prompt redraw all worked
correctly over the actual WebSocket protocol.
Deliberately deferred to Phase 1b/1c per the migration doc: jump-host
chaining, tab/split-pane UI, terminal theme/font settings, OPKSSH cert
auth, tmux session monitor, session recording.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbJV5nm8KPVH1oNJYKpnoF
Proxmox ships with a self-signed cert by default, which Node's fetch
rejected outright; route Proxmox requests through an undici Agent with
rejectUnauthorized disabled so real Proxmox hosts can be connected.
Also bump @fastify/jwt to v10, which pulls in a patched fast-jwt and
resolves the critical advisories (crit-header bypass, algorithm
confusion, cache collision, ReDoS, empty-HMAC-secret auth bypass) that
npm audit flagged on the old v9/fast-jwt<=6.2.3 pairing. Verified auth
still works end-to-end (setup, valid token, rejected bad token) after
the upgrade; npm audit now reports 0 vulnerabilities.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbJV5nm8KPVH1oNJYKpnoF
Many self-hosted machines have no management API, so add an SSH-based
adapter (using ssh2) that connects with password or key auth and probes
hostname/disk/mem/load via a single shell command, surfacing health
status like the other integrations. Also fixes routes/integrations.ts's
hardcoded type enum, which was out of sync with the IntegrationType
union and rejected the new 'ssh' type.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbJV5nm8KPVH1oNJYKpnoF
Implements testConnection via STS GetCallerIdentity and listResources via
EC2 DescribeInstances, mapping instance state to resource health. Verified
end-to-end against real AWS endpoints — invalid credentials return AWS's
actual rejection message rather than crashing. Intended for use with a
dedicated, least-privilege IAM user (ec2:DescribeInstances + sts:GetCallerIdentity).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbJV5nm8KPVH1oNJYKpnoF
Implements testConnection against wttr.in's JSON API using the
configured location, no API key required. listResources is intentionally
omitted since weather conditions don't map to the resource-health model.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbJV5nm8KPVH1oNJYKpnoF
Implements testConnection and listResources against the Cloudflare API,
reporting the configured zone's real status (active/pending/etc) as a
resource. Fixed a bug where non-2xx responses with non-JSON bodies (e.g.
invalid zone ID) threw inside the JSON parse instead of failing cleanly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbJV5nm8KPVH1oNJYKpnoF
Implements testConnection and listResources against the NetBird
Management API (/api/peers), mapping connected/disconnected peers to
resource health. Defaults to the NetBird Cloud API but respects an
optional baseUrl override for self-hosted management servers.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbJV5nm8KPVH1oNJYKpnoF
Implements testConnection (via /api2/json/version) and listResources
(via /api2/json/cluster/resources) using Proxmox's API token auth header,
following the same pattern as the Docker adapter. Verified end-to-end:
graceful failure against an unreachable host, correct event logging, and
exclusion from the resources endpoint when not connected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbJV5nm8KPVH1oNJYKpnoF
TopBar, Sidebar, and the Settings profile form previously showed a hardcoded
"ArchNest Ops" identity, a fake unread-notification count, and a static "All
Systems Operational" indicator. These now use the real logged-in user (with
a new PUT /api/auth/me endpoint to edit display name/email/avatar) and real
integration health for the sidebar status dot.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbJV5nm8KPVH1oNJYKpnoF
Adds an events table + logEvent helper for a genuine activity log, and
a /api/integrations/resources aggregate endpoint backed by a new optional
listResources adapter method (implemented for Docker via its containers API).
StatusCards, MiddleRow, BottomRow, and Infrastructure now render real
integration/resource/event data instead of hardcoded numbers, with empty
states where no data source exists yet (AWS cost, historical trends).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbJV5nm8KPVH1oNJYKpnoF
- Single-user JWT auth with a first-run /api/setup endpoint, gated by
GET /api/system/setup-status, to back an upcoming enrollment page
- SQLite schema for users, integrations, secrets (AES-256-GCM encrypted),
bookmarks, and bookmark categories
- Integration adapter registry with real health-check adapters for
Uptime Kuma and Docker, stubs for the rest, wired to
POST /api/integrations/:id/test
- CRUD routes for integrations and bookmarks
- backend/ as its own Docker service in docker-compose.yml, Vite dev
proxy for /api, .env.example for required secrets
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbJV5nm8KPVH1oNJYKpnoF