Seamless cloud plan upgrades + over-quota downgrade guardrails (0216) - #250
Merged
Conversation
added 7 commits
June 24, 2026 09:32
…over-quota guardrails
- Add 'over-quota' variant to PlanChangeResult; changePlan now refuses a capacity reduction that would shrink the quota below stored data (or whose usage can't be measured), instead of silently flipping it down. - currentUsageBytes() reads fresh hub /health; injectable for tests. - wipeAndChangePlan(): destroy + re-provision an empty hub at the smaller plan (the 'start fresh' escape hatch); preserves billing + data identity. - Fix FileService to enforce the plan quota (config.defaultQuota) instead of its hardcoded 5 GiB default, so files count against the same quota as backups. Exploration 0216.
… banner - POST /account/plan now renders an over-quota notice (free up space, or wipe & start fresh) instead of redirecting, when changePlan reports 'over-quota'. - New POST /account/plan/wipe (explicit confirm required) calls wipeAndChangePlan. - renderOverQuotaNotice: usage vs target, reclaim amount, distinct copy when usage couldn't be measured (cold hub), and a double-confirmed wipe form. - planChangeCard copy reframed: upgrades = instant more space; downgrades must fit. - DashboardLive.overQuota signal (storagePct saturates at 100) + a read-only banner on the hub card when over quota. - buildControlPlane gains an injectable readUsageBytes seam (tests). Exploration 0216.
Contributor
|
Preview removed for PR #250. |
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.
Implements exploration 0216 — make the managed-hub upgrade/downgrade path clear, clean, and safe.
The problem
changePlanflipped the storage quota in place with no usage check, so a downgrade silently shrank the quota under live data. A Family tenant (250 GiB) holding 200 GiB who switched to Personal (25 GiB) just had their quota set to 25 GiB — and the hub started 507-ing the next write, with no warning.What this does
Upgrades stay seamless. An in-tier quota increase is still a pure live entitlement flip — no usage read, no migration prompt. Your data just gets more room.
Downgrades are guarded (Layer 1 — block, don't surprise).
PlanChangeResultgains anover-quotavariant.changePlannow refuses a capacity reduction that would shrink the quota below stored data — or whose usage can't be measured (cold/asleep hub) — instead of silently flipping it down.currentUsageBytes()reads a fresh, uncached hub/health(injectablereadUsageBytesseam for tests).POST /account/planrenders an over-quota notice with two honest exits: Free up space (recommended) or Wipe & start fresh. NewPOST /account/plan/wipe(explicitconfirm=wipe) callswipeAndChangePlan— destroy + re-provision an empty hub at the smaller plan, preserving billing + data identity. The dashboard double-confirms before posting.Graceful over-quota signal (Layer 2 — never auto-delete).
DashboardLive.overQuota(becausestoragePctsaturates at 100) drives a read-only banner on the hub card: "Your data is safe, but new writes are paused until you free up space or upgrade." This is the Google One model, explicitly not Dropbox's delete-your-oldest-files model. Cancellation already routes tosuspendTenant, which retains the R2 replica.Correctness fix.
FileServicewas constructed without the plan quota, so uploads checked against a hardcoded 5 GiB instead ofconfig.defaultQuota. Now wired to the plan quota, so files count against the same quota the dashboard meter shows.Tests
control-plane.test.ts: over-quota block (measured + unmeasurable), downgrade-fits flip, upgrade never reads usage,wipeAndChangePlan(same-tier and cross-tier).server.test.ts: over-quota notice (no flip, tenant unchanged), wipe route requires confirm.hub-status.test.ts:overQuotatrue/false/null.files.quota.test.ts: FileService enforces the configured quota.xnet-cloud/@xnetjs/hub/@xnetjs/entitlementstypecheck clean; prettier + eslint clean.Deferred (called out in the doc)
🤖 Generated with Claude Code