feat(console): start/stop buttons on the roster (scale 0/1) - #39
Merged
Conversation
…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
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds a Start / Stop action to each deployment row in the console, wired to the sidecar's existing
deploy_scaleMCP tool.deploy_scale(size=0), Start =deploy_scale(size=1)— ADR-2 §5 write model (stop = scale→0,start = scale→1).desired == 0), Stop when on.Why this needs no state store
stopkeeps the Spec — ECS retains the service atdesiredCount 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-mcpdeploy_scale) already existed. This PR is only the console wiring.Changes
name+namespaceviadata-*. Service isoab-{namespace}-{name}; the managing credential is per-cluster, so the row needs nocluster.scaleDeployment(name, size, namespace, cluster?)on theSourcecontract. Tauri impl invokesdeploy_scale; mock no-ops (browser preview).tick()re-renders.deploy_scalebridge command (mirrorsfleet_config_write). Passesnamespaceexplicitly — the MCP handler defaults it to"default", which would targetoab-default-{name}instead of the realoab-prod-{name}.Scope / follow-ons
cordon/Paused(drain-but-alive). A true pause needs the durable admission flag from Fleet Store (ADR: Fleet Store & Control-Plane State Ownership #18) and is intentionally out of scope.Unhealthyuntil health wiring / ADR: Fleet Store & Control-Plane State Ownership #18 lands — that's the status readout, not this action.Verification
tsc --noEmit, 34 vitest,vite build(all green locally)tauri build(desktop.yml, triggers onsrc-tauri/**+console/**).tauri dev) — can't drive the desktop webview headless.🤖 Generated with Claude Code