Skip to content

feat(console): start/stop buttons on the roster (scale 0/1) - #39

Merged
brettchien merged 1 commit into
mainfrom
feat/console-start-stop
Aug 14, 2026
Merged

feat(console): start/stop buttons on the roster (scale 0/1)#39
brettchien merged 1 commit into
mainfrom
feat/console-start-stop

Conversation

@brettchien

Copy link
Copy Markdown
Contributor

What

Adds a Start / Stop action to each deployment row in the console, wired to the sidecar's existing deploy_scale MCP tool.

  • Stop = deploy_scale(size=0), Start = deploy_scale(size=1) — ADR-2 §5 write model (stop = scale→0, start = scale→1).
  • One contextual button per row: Start when a deployment is off (desired == 0), Stop when on.

Why this needs no state store

stop keeps the Spec — ECS retains the service at desiredCount 0, so it's reversible with no durable store. The Fleet Store (#18) / reconcile loop (ADR-4) are not required for start/stop; the whole scale pipeline (oabctl → studio-cp → oab-mcp deploy_scale) already existed. This PR is only the console wiring.

Changes

  • render.ts — contextual action button per row; carries name + namespace via data-*. Service is oab-{namespace}-{name}; the managing credential is per-cluster, so the row needs no cluster.
  • source.tsscaleDeployment(name, size, namespace, cluster?) on the Source contract. Tauri impl invokes deploy_scale; mock no-ops (browser preview).
  • main.ts — delegated roster listener. Start executes on click; Stop is disruptive, so it arms on the first click and executes on a confirming second click within 3s — webview-safe, no dialog plugin. On success tick() re-renders.
  • src-tauri/lib.rsdeploy_scale bridge command (mirrors fleet_config_write). Passes namespace explicitly — the MCP handler defaults it to "default", which would target oab-default-{name} instead of the real oab-prod-{name}.
  • styles + tests.

Scope / follow-ons

Verification

  • ✅ console tsc --noEmit, 34 vitest, vite build (all green locally)
  • The Rust bridge is compiled by CI's macOS tauri build (desktop.yml, triggers on src-tauri/** + console/**).
  • ⚠️ Live click-test needs the macOS app (nightly build or local tauri dev) — can't drive the desktop webview headless.

🤖 Generated with Claude Code

…store)

Wire a Start/Stop action per deployment row to the sidecar's existing
`deploy_scale` MCP tool. Stop = scale→0, Start = scale→1 (ADR-2 §5 write
model): the Spec is kept by ECS at desiredCount 0, so it's reversible and
needs no durable state store (ADR-4 / Fleet Store #18 not required).

- render.ts: one contextual action button per row — Start when a deployment
  is off (desired 0), Stop when on. Carries name + namespace via data-* (the
  service is oab-{namespace}-{name}; the managing credential is per-cluster,
  so the row needs no cluster).
- source.ts: `scaleDeployment(name, size, namespace, cluster?)` on the Source
  contract; Tauri impl invokes `deploy_scale`, mock no-ops (browser preview).
- main.ts: delegated roster listener. Start executes on click; Stop is
  disruptive so it arms on the first click and executes on a confirming second
  click within 3s — webview-safe, no dialog plugin. On success tick() re-renders.
- src-tauri: `deploy_scale` bridge command (mirrors fleet_config_write), passes
  namespace explicitly so prod services resolve (handler defaults to "default").
- styles + tests.

Note: namespace MUST be sent — the MCP handler defaults it to "default", which
would target oab-default-{name} instead of the real oab-prod-{name}.

Verified: console typecheck + 34 vitest + vite build green. The Rust bridge is
compiled by CI's macOS `tauri build` (desktop.yml). Live click-test needs the
macOS app.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@brettchien
brettchien merged commit 468d445 into main Aug 14, 2026
2 checks passed
@brettchien
brettchien deleted the feat/console-start-stop branch August 14, 2026 05:16
brettchien pushed a commit that referenced this pull request Aug 14, 2026
The #39 disable lived on the button DOM node, so the 5s roster poll could
re-render and hand back a fresh enabled button mid-action (the underlying
deploy_scale is idempotent, so it was harmless but not a real guard).

Move the guard to module state: `scaling` maps deploymentKey → the desiredCount
we're driving toward. The action button renders disabled whenever its key is in
the set, so a poll re-render preserves the disabled state. The key clears when
the roster observes the target count (poll-immune), or by a 15s safety timeout
so a never-observed flip can't wedge a button. Re-entry is refused while a key
is in flight.

- render.ts: `deploymentKey(d)`; rosterHtml/renderRoster take an optional
  `pending` set; a pending row renders a disabled `…` placeholder (no
  data-action, so even a stray click no-ops).
- main.ts: `scaling`/`scaleTimers` maps, prunePending on each tick, repaint for
  instant feedback; scale() sets the guard, awaits, prunes via tick() or errors
  out clearing it.
- tests: pending → disabled placeholder; others stay live; deploymentKey.

Verified: tsc + 38 vitest + vite build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
brettchien added a commit that referenced this pull request Aug 14, 2026
…ix) (#44)

The #39 disable lived on the button DOM node, so the 5s roster poll could
re-render and hand back a fresh enabled button mid-action (the underlying
deploy_scale is idempotent, so it was harmless but not a real guard).

Move the guard to module state: `scaling` maps deploymentKey → the desiredCount
we're driving toward. The action button renders disabled whenever its key is in
the set, so a poll re-render preserves the disabled state. The key clears when
the roster observes the target count (poll-immune), or by a 15s safety timeout
so a never-observed flip can't wedge a button. Re-entry is refused while a key
is in flight.

- render.ts: `deploymentKey(d)`; rosterHtml/renderRoster take an optional
  `pending` set; a pending row renders a disabled `…` placeholder (no
  data-action, so even a stray click no-ops).
- main.ts: `scaling`/`scaleTimers` maps, prunePending on each tick, repaint for
  instant feedback; scale() sets the guard, awaits, prunes via tick() or errors
  out clearing it.
- tests: pending → disabled placeholder; others stay live; deploymentKey.

Verified: tsc + 38 vitest + vite build green.

Co-authored-by: Orca (ecs-claude) <orca@ecs.local>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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