Skip to content

feat(ssh): tunnel cards, in-place tunnel creation, startup commands - #377

Merged
attson merged 2 commits into
mainfrom
feat/ssh-panel-tunnels-startup-command
Sep 9, 2026
Merged

feat(ssh): tunnel cards, in-place tunnel creation, startup commands#377
attson merged 2 commits into
mainfrom
feat/ssh-panel-tunnels-startup-command

Conversation

@attson

@attson attson commented Sep 9, 2026

Copy link
Copy Markdown
Owner

Summary

  • Tunnels tab is now a card grid matching Hosts and Keys, with the state pill and actions stacked in the card's right column so the route text keeps a full line.
  • Tunnels can be created, edited and deleted from the Tunnels tab. The new drawer picks the SSH host the tunnel rides — the bastion, when the target is behind one — and saves the rule onto that host. Saving does not open a listener; there is a separate "Save and start".
  • Hosts gain a startup command: lines typed at whatever prompts a bastion shows before handing over a shell. Built for JumpServer, which asks for the asset at Opt> and then the system user at ID>. Each line is followed by a separate carriage return — "text\r" in one write reads as a paste to a program in raw mode (the bug this repo has re-fixed three times on the quick-template path, fix quick input enter dispatch #63 / chore(desktop): remove the legacy quickInput plugin (superseded by QuickTemplate) #110 / fix(templates): split text and Enter so Codex reads them as keystrokes, not paste #129).
  • Tag field commits on blur. Leaving by anything other than Enter or comma used to silently discard the typed tag.
  • Key drawer can reveal the stored private key behind an eye button (fetched on demand via a new RevealSSHKey binding, never on open), with a copy button beside it.

Notes on two deliberate choices

SSHConnectReq.StartupCommand is tagged json:"-". It types arbitrary input into a remote shell, so only NewSshSessionByID fills it from a saved host — a marshalled field would make "type this into that machine" part of the bound-method surface NewSshSession exposes to the frontend.

Every tunnel write sends the whole host record. UpdateSSHHost lets the caller's value win for UI-owned fields, so a payload built from the rule alone would clear the rest of the host — the same shape as the item-25 bug that stripped proxy_jump.

Test plan

  • go test ./desktop/... passes (4 packages)
  • go vet ./desktop/... clean
  • npx vitest run passes (177 files / 2347 tests; SshHostsPanel.test.ts 93 → 108)
  • npx vue-tsc --noEmit clean
  • vite build succeeds
  • Tunnels tab renders as cards; start / stop / dismiss still work on a live tunnel
  • New Tunnel drawer: pick a host, save, confirm the rule appears under that host and the host's other fields survive
  • "Save and start" brings the listener up on the rule just created
  • Edit a tunnel: host picker is locked, save replaces rather than appends
  • Delete a running tunnel: listener stops before the rule is removed
  • Host drawer: type a two-line startup command against a JumpServer bastion, confirm it lands on the target machine
  • Startup command survives an unrelated host edit; clearing it sticks
  • Tag field: type a tag, click elsewhere, confirm the chip appears and saves
  • Key drawer: eye button reveals the stored PEM, second click clears it, copy button works

The SSH panel's Tunnels tab was read-only: rules could only be created
from inside a host's drawer, which is backwards when what you are doing
is "I need a tunnel" rather than "I am editing this host". It now has a
New Tunnel drawer that picks the host the tunnel rides — the bastion,
when the target is behind one — and writes the rule back as one of that
host's forwarding rules. Rules can also be edited and deleted from the
card. Every write sends the whole host record, because UpdateSSHHost lets
the caller's value win and a partial payload would clear the rest.

The tab itself moves from a row list to the same card grid the Hosts and
Keys tabs use, with the state pill and buttons stacked in the card's
right column so the route text keeps a full line to itself.

Hosts gain a startup command: the lines typed at whatever prompts a
bastion shows before it hands over a shell. JumpServer is the case it
was built for — it asks for the asset at `Opt>`, then for the system
user at `ID>`, neither of which is a shell command you could pass on a
command line. Each line is written and followed by a *separate* carriage
return, because "text\r" in one write reads as a paste to a program in
raw mode. SSHConnectReq carries the field as `json:"-"`: only
NewSshSessionByID fills it from a saved host, so an ad-hoc connect
cannot use it to type arbitrary input into a remote shell.

Two smaller fixes in the same panel. The tag field now commits what is
in it when it loses focus — leaving by any route other than Enter or
comma silently threw the tag away. And the key drawer can reveal the
stored private key behind an eye button (fetched on demand, never on
open), with a copy button beside it; the panel only mounts on desktop,
so the PEM goes from the OS keychain to a webview in the same process
and never touches the relay.
internal/relay/web-dist/ is built from web/src AND desktop/frontend/src,
so the SSH panel changes in the previous commit left it stale and CI's
"verify embed has no drift" gate failed. Rebuilt with the pinned
toolchain (Node 20.20.2, Go 1.23.12) that the gate requires — only the
content hashes and the files that reference them move.
@attson
attson merged commit 2658b07 into main Sep 9, 2026
7 checks passed
@attson
attson deleted the feat/ssh-panel-tunnels-startup-command branch September 9, 2026 15:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant