fix(nodes): SSH join-node fix + Test Connection button

Root cause:
  gateway binary was built before SwarmJoinNodeViaSSH was added → 404 on
  /api/swarm/join-node → tRPC threw 'Gateway unavailable' error.

Gateway fixes:
  - Rebuilt gateway image (golang:1.23-alpine + go mod tidy → clean go.sum)
  - SwarmJoinNodeViaSSH now live in binary (was already coded, not compiled)
  - NEW: POST /api/swarm/ssh-test handler (SwarmSSHTest)
      • Dials SSH with 10s timeout
      • Runs 'docker version' on remote to check Docker availability
      • Returns {ok, sshOk, dockerOk, dockerVersion, error, step}
  - Route registered: r.Post("/swarm/ssh-test", h.SwarmSSHTest)

Server fixes:
  - gateway-proxy.ts: added testSSHConnection() → POST /api/swarm/ssh-test
  - routers.ts: added nodes.sshTest mutation (input: host/port/user/password)

UI (client/src/pages/Nodes.tsx) — AddNodeDialog rewritten:
  - Separate state for testResult and joinResult (two independent panels)
  - NEW: yellow 'Test Connection' button → calls nodes.sshTest
      shows SSH OK + Docker version, or human-readable error
  - 'Join Swarm' button remains independent
  - Human-readable error messages per step:
      ssh_connect → 'Cannot connect — check IP, port, SSH running'
      docker_join → 'docker swarm join failed: ...'
      trpc        → 'Gateway unavailable — check gateway container'
  - Input changes clear stale test/join results
  - ✕ close button in dialog header
  - Disabled state unified (busy || joinDone)
This commit is contained in:
bboxwtf
2026-03-21 22:37:59 +00:00
parent 37e0a21ec3
commit 3be643992d
7 changed files with 251 additions and 82 deletions

View File

@@ -716,6 +716,27 @@ export async function joinSwarmNodeViaSSH(opts: {
} catch { return null; }
}
/**
* Test SSH connectivity and Docker availability on a remote host (no swarm join).
*/
export async function testSSHConnection(opts: {
host: string;
port?: number;
user: string;
password: string;
}): Promise<{ ok: boolean; sshOk?: boolean; dockerOk?: boolean; dockerVersion?: string; error?: string; step?: string } | null> {
try {
const res = await fetch(`${GATEWAY_BASE_URL}/api/swarm/ssh-test`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(opts),
signal: AbortSignal.timeout(20_000),
});
if (!res.ok) return null;
return res.json();
} catch { return null; }
}
/** Add a label to a swarm node */
export async function addSwarmNodeLabel(nodeId: string, key: string, value: string): Promise<boolean> {
try {

View File

@@ -35,6 +35,7 @@ import {
stopSwarmAgent,
getOllamaModelInfo,
joinSwarmNodeViaSSH,
testSSHConnection,
} from "./gateway-proxy";
// Shared system user id for non-authenticated agent management
@@ -1067,6 +1068,22 @@ export const appRouter = router({
return result;
}),
/**
* Test SSH connectivity and Docker availability on a remote host — no swarm join performed.
*/
sshTest: publicProcedure
.input(z.object({
host: z.string().min(1),
port: z.number().int().min(1).max(65535).default(22),
user: z.string().min(1),
password: z.string().min(1),
}))
.mutation(async ({ input }) => {
const result = await testSSHConnection(input);
if (!result) throw new Error("Gateway unavailable — cannot reach SSH test endpoint");
return result;
}),
/**
* Get live container stats (CPU%, RAM) for all running containers.
*/