feat(console): poll-immune in-flight guard for start/stop (option A) - #44
Merged
Conversation
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
force-pushed
the
feat/console-scale-inflight-guard
branch
from
August 14, 2026 06:37
4f1176c to
8fd7ccc
Compare
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.
Follow-on to #39. Brett asked whether the start/stop button has debounce — it didn't have a real one. This implements option A: move the in-flight guard off the button DOM node into module state, so it's immune to the 5s roster poll's re-render.
The gap (#39)
scale()setbtn.disabled = true, but that lived on a specific DOM node. The 5stick()poll re-renders the whole roster (innerHTML), replacing the disabled button with a fresh enabled one mid-action. The underlyingdeploy_scaleis idempotent (UpdateService to the same count = no-op), so it was harmless but not an actual guard.Fix
scaling: Map<deploymentKey, targetDesired>in module state. A row's action button renders disabled whenever its key is in the set — so a poll re-render preserves the disabled state (poll-immune).desiredCount(the action landed), or via a 15s safety timeout so a never-observed flip can't wedge a button.repaintRoster()overlays the pending set on the last poll's data without waiting for a fetch.Changes
deploymentKey(d);rosterHtml/renderRostertake an optionalpendingset (defaults empty); a pending row renders a disabled…placeholder with nodata-action, so even a stray click no-ops.scaling/scaleTimersmaps,prunePending()on each tick,repaintRoster();scale()sets the guard, awaits, then prunes viatick()(or clears on error).deploymentKey.Console-only, no Rust touched.
Verification
tsc --noEmit, 38 vitest,vite build(green locally)bundle-macos(desktop.yml) triggers onconsole/**.🤖 Generated with Claude Code