Repository navigation
settings: give unsubscribed shards a reachable resize control - #51
Conversation
resizeShard() implemented the immediate resize path for shards without a subscription, but nothing ever called it: no template binding, no other call site. The selection panel rendered only the PayPal revise container, and renderResizeButton() returns early unless resizeNeedsApproval, so on a trial shard the panel offered nothing but Cancel. Render a Confirm button bound to resizeShard() when !resizeNeedsApproval, and branch the copy — there is no price to approve without a subscription. The immediate path now settles its own state. The subscribed path leaves waitingForRestart set and lets the hasPendingResize watcher clear it once the UPDATED webhook commits the resize. An unsubscribed resize never creates a pending_vm_size, so that watcher cannot fire for it: on error the method clears waitingForRestart itself and keeps the selection so the user can retry or cancel, and on success it clears the selection and keeps waitingForRestart set while handing over to the /restart interstitial, so the size buttons cannot fire a second resize against a shard that is going down. The navigation is no longer awaited inside the try — a rejected route transition would otherwise be reported as a failed resize. Refs #49
The size buttons were gated only against max_vm_size, so every smaller tier stayed selectable while the controller rejects any downsize with a 409. After the previous commit made the resize control reachable, that turned an inert panel into an offer the system cannot honour. The controller's rule comes from OVH: a classic flavor may only move to one with a similar or bigger local disk, and ours run 25/50/50/100/200 GB across xs..xl. Rather than restate that reasoning at the button, the card now says a Shard can only be made larger, so the greyed-out tiers are explained instead of merely disabled.
|
Added the UI follow-through, What changed
const index = this.resize.sizes.indexOf(size);
return index <= this.resize.sizes.indexOf(profile.max_vm_size)
&& index > this.resize.sizes.indexOf(profile.vm_size);The Size card also gains one muted line — "A Shard can only be made larger, never smaller." — so the greyed-out buttons are explained rather than just dead. Why the backend refuses, for the recordNot arbitrary. OVH's guide states "Only upscaling is possible for classic models": a classic flavor may only move to one with a similar or bigger local disk, and only Flex instances (locked 50 GB) resize both ways. Our flavors are classic
VerifiedRe-run on the branch after this commit, all actually executed:
Still not exercised against a live shard. Note for the reviewerThis makes the PR's behaviour honest, but it does not deliver the original motivation in #49 — a trial user on |
Closes #49.
Cause
resizeShard()insrc/views/Settings.vuewas fully implemented and completely unreachable.grep -n resizeShard src/returned exactly one line onmain— its own definition. No template binding, no other call site.The only action control the selection panel rendered was the PayPal revise container (
<div ref="resizeButton">), andrenderResizeButton()returns early unlessresizeNeedsApproval— which isprofile.billing_enabled && subscription && subscription.status === 'active', false on a trial shard. So the PayPal path declined to render and the free path had nothing to render. The panel was inert by construction: the copy said "Approve the new price to resize to XS", and the only thing to click was Cancel.What changed
One file,
src/views/Settings.vue.Template. The selection panel now branches on
resizeNeedsApproval:<div ref="resizeButton">for the PayPal revise button.Confirmbutton bound toresizeShard(), disabled whileresize.waitingForRestart, and the copy drops the price clause — "Resize to XS. The shard restarts afterwards." There is no subscription, so there is no price to approve.ref="resizeButton"now sits behindv-if="resizeNeedsApproval". BothrenderResizeButton()andteardownResizeButton()already guard onthis.$refs.resizeButton, so the ref being absent on the unsubscribed path is handled.resizeShard(). The method now settles its own post-resize state. Details below, since this is the part that differs most between the two paths.How the two paths now differ after a resize
The subscribed path leaves
waitingForRestartset and relies on thehasPendingResizewatcher: the PayPalUPDATEDwebhook promotes the price, clearspending_vm_sizeand resizes the VM, the interstitial poll picks the profile change up, and the watcher clears bothwaitingForRestartandselectedSize.An unsubscribed resize never creates a
pending_vm_size— the controller'sPOST /api/shards/self/resizecallsbegin_resize_shard()directly whenshard.subscription_id is Noneand returns an emptyResizeResponse(freeshard-controller-backend/freeshard_controller/api/shards.py, read atorigin/main). So that watcher can never fire for this path, and the method has to clear its own state:waitingForRestart, keepselectedSize. The panel stays up with the toast, so the user can retry or Cancel.selectedSize, deliberately leavewaitingForRestartset, then navigate to/restart. The shard is going down; re-enabling the size buttons would let a second resize be fired at a VM that is being deallocated. Settings unmounts on navigation anyway, so the flag only matters in the case where the navigation does not happen — which is exactly the case where the guard is wanted.The
$router.replace('/restart')also moved out of thetry. Awaiting it inside meant a rejected route transition (vue-router 3 rejects on aborted/redundant navigation) would have surfaced as "Error during resize" for a resize that had already succeeded. It is now.catch(() => {}), matching the idiom already used for thesubquery cleanup in the same file.Verification
Ran in a fresh clone of
mainwithnpm ci:npx vue-cli-service lint --no-fix— "No lint errors found!"npx vue-cli-service test:unit— 3 suites, 19 tests, all passed (none of them cover Settings.vue; the existing suites are store, pricing and paypal-sdk).NODE_OPTIONS=--openssl-legacy-provider npx vue-cli-service build— "Build complete."Not verified against a live shard. There is no component test for Settings.vue in this repo and I did not add one.
Uncertain / worth a second opinion
The controller refuses downsizing, so the issue's motivating scenario still fails.
_check_resize_preconditionsraises 409"Resize must increase size: current is 's', requested 'xs'"whennew_vm_size <= shard.vm_size, for subscribed and unsubscribed shards alike (api/shards.pyatorigin/main;VmSizeordersXS < S < M < L < XL). The frontend'ssizeIsAvailable()does not enforce that — it only checks the tier againstmax_vm_size— so a trial user on S can still select XS, and will now get a toast carrying that 409 detail instead of an inert panel.That is a strict improvement over a dead end, but it does not deliver "a trial user who only needs XS can get there before subscribing" from the issue text. Reaching that needs a controller change to allow downsizing, which is out of scope here and in a different repo. An upsize on a trial shard (S → M, say) works with this PR. Tell me if you want the downsize case handled on the frontend in the meantime (grey out smaller sizes rather than let the request 409) or an issue opened against the controller.
Button label and layout.
Confirmwithvariant="primary" size="sm"and anmr-2so it does not sit flush against the Cancel link. Not styled beyond that, and #50 reworks these cards anyway.Out of scope and untouched: #48 (no price shown on trial shards) and #50 (subscription/size/disk card rework).