Skip to content

fix(ui): align workstation web contract with gateway API shape#1461

Open
eddydao wants to merge 1 commit into
nextlevelbuilder:devfrom
eddydao:fix/1258-workstation-contract-casing
Open

fix(ui): align workstation web contract with gateway API shape#1461
eddydao wants to merge 1 commit into
nextlevelbuilder:devfrom
eddydao:fix/1258-workstation-contract-casing

Conversation

@eddydao

@eddydao eddydao commented Jul 21, 2026

Copy link
Copy Markdown

Fixes #1258.

Problem

The workstations page was written against snake_case field names the gateway never served. Go's json decoder drops unknown members silently rather than erroring, so the mismatch surfaced as confusing validation failures instead of decode errors:

  • Create failed every time. The dialog sent workstation_key / backend_type; the handler reads workstationKey / backendType (internal/gateway/methods/workstations.go:125-127). Both landed as zero values → "workstationKey is required", regardless of what was typed.
  • List rendered garbage. The Workstation interface declared workstation_key / backend_type / created_at, but the API serializes camelCase (internal/store/workstation_store.go:14-26) → blank key column, backend.undefined, Invalid Date.
  • SSH credentials never validated. The dialog sent metadata.identity_file, which store.SSHMetadata does not accept; UnmarshalSSHMetadata requires privateKey or password.

Fix

Aligned the TypeScript contract on the camelCase shape the WS handler, HTTP handler, and store model already share. No backend changes — per the maintainer's decision on the issue, adding snake_case aliases in Go would leave list rendering and future update flows exposed to a second parallel contract.

  • Renamed Workstation / CreateWorkstationParams fields and every read on the page.
  • Replaced the dead Identity File input with a private key (PEM) or password credential path.
  • Extracted buildWorkstationCreatePayload() so the wire contract is unit-testable without mounting React, following the existing cli-credentials helper precedent.
  • Moved validation strings into the en/vi/zh catalogs (89 keys, parity verified).

Two findings beyond the issue text

1. Docker create was equally broken — not mentioned anywhere in #1258. The dialog sent {container, docker_host}, but store.DockerMetadata requires image plus host or socketPath, and stores the container name in host (surfaced back as metadataSummary.containerName). Every Docker workstation creation failed with "image is required". Added the missing image field and mapped container → host, daemon endpoint → socketPath.

Flagging this explicitly because it expands the issue's stated scope. Fixing only what #1258 describes would have closed it with Docker creation still 100% broken. Happy to split it into its own PR if you'd prefer the boundary kept tight.

2. Update was broken for a second, independent reason. useWorkstations.updateWorkstation sent {id, ...params} flattened, but the handler reads a nested updates map (internal/gateway/methods/workstations.go:180-182) → "no updates provided". Item 2 of the issue's scope covers this, but the cause was never diagnosed, so a rename-only fix would have left update dead.

Tests

New workstation-contract.test.ts — 7 tests covering create payload shape, SSH credential mapping, Docker metadata, and list rendering against a verbatim SanitizedView() response body.

Verified the guards actually fail without the fix: reintroducing the snake_case payload produces expected [ 'backend_type', … ] to deeply equal [ 'backendType', … ].

Full-suite baseline comparison — 6 failing files / 2 failing tests both before and after, an identical pre-existing set (jsdom localStorage and network stubs in http-client, channels, cli-credentials). This change adds +7 passing and introduces no new failures. tsc --noEmit, eslint --max-warnings=0, and pnpm build all clean.

Surface parity

  • Web UI — fixed.
  • Gateway / API contract — N/A, unchanged by design; the UI was the wrong side.
  • CLI / runtime package — N/A, workstations.create has exactly one caller in the repo (ui/web/src/pages/workstations/hooks/use-workstations.ts); no CLI or desktop surface exists.
  • Docs — N/A, no documentation describes this dialog.

The workstations page was written against snake_case field names the
gateway never served. Go's json decoder drops unknown members silently,
so create requests arrived with an empty WorkstationKey and were rejected
with "workstationKey is required", while list rows read undefined and
rendered a blank key, "backend.undefined" and "Invalid Date".

Align the TypeScript contract on the camelCase shape that both the WS
handler and store model already use:

- Rename Workstation, CreateWorkstationParams and every field read on the
  workstations page to workstationKey/backendType/createdAt/updatedAt.
- Nest update params under `updates`; a flattened body decoded to an empty
  map and was rejected with "no updates provided".
- Replace the Identity File input, which mapped to a field SSHMetadata does
  not accept, with a private key or password credential path.
- Add the image field required by DockerMetadata and map the container name
  onto host and the daemon endpoint onto socketPath, so Docker workstations
  can be created at all.
- Extract the payload builder so the wire contract is unit-testable, and
  move validation strings into the en/vi/zh catalogs.

Covered by tests asserting the create payload shape and list rendering
against a verbatim gateway response body.
@eddydao
eddydao force-pushed the fix/1258-workstation-contract-casing branch from f133f8a to 5568421 Compare July 21, 2026 07:23

@clark-cant clark-cant left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — github-maintain

Summary: Aligns the workstation web UI TypeScript contract with the actual gateway API shape (camelCase), fixes Docker create metadata, and replaces the dead Identity File input with proper privateKey/password auth. Closes #1258.

Risk level: Low — UI-only changes, no backend modifications, well-scoped with regression tests.

Mandatory gates

  • Duplicate / prior implementation: clear — no prior PR addresses this contract mismatch
  • Project standards: docs found — follows existing patterns (cli-credentials helper precedent for extracted payload builder, i18n catalog structure matches other pages)
  • Strategic necessity: clear value — the workstation create flow was 100% broken for both SSH (snake_case silently dropped by Go decoder) and Docker (missing image field). This is a real user-facing bug.

Findings

Positive observations:

  • Excellent PR description with root cause analysis and scope expansion disclosure (Docker create was equally broken — flagged explicitly)
  • buildWorkstationCreatePayload() extracted into a pure, testable helper — follows the existing cli-credentials pattern
  • 7 regression tests that actually verify the wire contract, including a test that proves the guards fail without the fix
  • i18n parity across en/vi/zh with 89 keys verified
  • Update endpoint fix ({ id, updates: params } instead of flattened) addresses the second half of #1258 that was never diagnosed
  • No anti-slop patterns: no unnecessary abstractions, no defensive paranoia, no phantom coverage

Suggestion:

  • The privateKeyHint i18n string says "encrypted at rest" — ensure this is actually true in the backend storage layer before shipping. If not, soften to "stored securely".

Verdict: Approve — clean, well-tested fix for a completely broken workflow. The scope expansion beyond #1258 is well-justified and explicitly flagged.

Merge deferred: mergeStateStatus=UNSTABLE, no CI checks reported yet. Will merge once CI is green.

Reviewed by github-maintain at $(date -u +'%Y-%m-%dT%H:%M:%SZ')

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.

Add Workstation dialog is broken: snake_case/camelCase field mismatch + dead identity_file field

2 participants