Repository navigation
fix(cluster): keep only Overview and Usage in a terminated cluster's rail - #1926
Merged
Merged
Conversation
…rail #1830 made a terminated cluster's overview say the cluster is gone, but the cluster rail next to it still offered Scaling, Version, Domains and Instances. None of them can do anything there. Central manager's assertClusterMutable refuses every update to a cluster that isn't RUNNING, and the instances are torn down. Domains has nothing left to unbind either: once the instances are gone, deleteCluster sets clusterId to null on each of the cluster's domains. For a cluster that isDeletedCluster reports gone (TERMINATING, TERMINATED, REMOVED, or FAILED with terminatedAt), the rail now hides those items, the same way it already hides what a member's permissions don't allow. Overview stays, and so does Usage on a managed cluster. On such a cluster, a bookmark to /instances, /scaling, /domains, /edit or /edit/version redirects to the overview instead of opening a form central manager would reject. Starting-up, where Finish-setup sends a cluster that isn't ready, used to wait forever on a terminated cluster. It now says the cluster is gone, but it checks for FAILED first, so a rolled-back build still offers Try Again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dawsontoth
marked this pull request as ready for review
October 13, 2026 14:41
Contributor
There was a problem hiding this comment.
Code Review
This pull request implements a 'terminated' state for clusters, ensuring that users are redirected to the overview page when attempting to access restricted sections (like scaling, domains, or instances) for deleted or terminated clusters. It updates the UI layout to hide these sections and adds navigation guards via beforeLoad hooks in the router. I have kept the review comment regarding the live variable initialization, as it correctly identifies a potential UI flicker issue during the initial data load and provides an actionable code suggestion to improve the user experience.
Coverage Report
File Coverage
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
This was referenced Oct 13, 2026
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.
Problem
#1830 made a terminated cluster's overview say "Cluster Terminated … there is nothing left to sign in to". The cluster rail next to it still offered Scaling, Version, Domains and Instances. Dawson found this while hand-testing the release candidate on stage. Each of those opens a page that edits or operates on a live cluster, and central manager refuses every one of those calls once the cluster is gone.
The open question was Domains: does a user still need it to unbind a custom domain, or does central manager unbind on terminate? I traced it in central-manager at
main(642af5ea):deleteClustersetsclusterId: nullon each domain incluster.domainIds(deleteCluster.js#L133-L141). The domains can then be bound to another cluster or deleted from the organization.domainIdsis left in place (the status patch at#L34-L42only writesstatus,terminatedByUserIdandterminatedAt). Becausedomainsis a@relationship(from: domainIds)(schema.graphql#L150-L151), a terminated cluster still lists its domains even though none of them is bound to it.generateDomainCertsis stamped per instance inservices/updateDomains.js). Those instances are torn down at#L102-L116, and the cluster's Route 53 record and GTM property are removed at#L89-L94.PUT /Cluster/{id}withdomainIds.updateClustercallsassertClusterMutablefirst (updateCluster.js#L70), and that refuses any cluster that isn'tRUNNING(sharedHelpers.js#L587-L593). The same check rejects Scaling and Version.try/catchthat only logs, so a failure leaves a domain bound to the dead cluster. Even then the Domains page couldn't help, for the reason above.DELETE /Domainalso refuses a bound domain (Domain.js#L124-L129), so only support can release it. The empty/self-hosted early return at#L66-L87skips the unbind, but a self-hosted cluster is refused domains (updateCluster.js#L169-L171), and binding readsinstances[0], so a cluster that never had an instance can't hold one either.So hiding Domains costs the user nothing.
Solution
ClusterPageLayout): for a cluster thatisDeletedClusterreports gone, the rail shows only Overview and Usage.isDeletedClusteris Opening a terminated cluster now says it was terminated instead of offering a sign-in that fails with "Network Error" #1830's predicate, unchanged:TERMINATING,TERMINATED,REMOVED, orFAILEDwithterminatedAt. Scaling (Configuration on a self-hosted cluster), Version, Domains and Instances are hidden, not disabled, which matches how the rail already treats an item a member's permissions don't allow: the item is filtered out of the list. A removed self-hosted cluster is left with only Overview, because Usage never shows for self-hosted.redirectAwayFromDeletedClusterbeforeLoadsends/instances,/scaling(the update-progress screen),/domains,/editand/edit/$modeto the cluster overview, which renders Opening a terminated cluster now says it was terminated instead of offering a sign-in that fails with "Network Error" #1830's "Cluster Terminated" state. Before this, those pages opened normally, and a submit failed with central manager's 400. The guard readscontext.clusterfrom the cluster layout'sbeforeLoad, the same way Opening a terminated cluster now says it was terminated instead of offering a sign-in that fails with "Network Error" #1830's sign-in guards do. It lives in its own module becausefeatures/clusters/routes.tscan't import fromfeatures/cluster/routes.tswithout creating an import cycle.resetPassword. The page checksFAILEDfirst, so a build that was rolled back (FAILED+terminatedAt) still offers Try Again, which its cluster card offers too. That's also why Starting-up isn't a redirect: a redirect would swap Try Again for the terminated notice.src/features/cluster/DESIGN.mdgets a section recording the central-manager behavior above.Not changed, on purpose:
allClusterInstancesRunningis false once every instance is deleted, so the page redirects to Starting-up, or to sign-in whenresetPasswordis off.Overlap: none. The diff touches
src/features/cluster/*and the two edit-route definitions insrc/features/clusters/routes.ts. Neither #1780 (david/1778-custom-regions) nor the cluster-form stack (#1842, #1849, #1851, #1870) touches either file. Their shared files areclusters/upsert/*,ClustersList.tsx,clusters/DESIGN.mdand the rootDESIGN.md.Verification
ClusterPageLayout.test.tsxpins the exact rail items and their targets for:TERMINATED,TERMINATING, andFAILED+terminatedAt: Overview and UsageFAILEDwithoutterminatedAt: all six, because it may still be servingredirectAwayFromDeletedCluster.test.tsmatches each bookmark against the real route tree. It runs the real cluster layoutbeforeLoadagainst a seeded query cache, then the matched route'sbeforeLoadwith the context that produced, and asserts where the bookmark lands. That pins the context plumbing and the wiring as well as the guard. For every gone status, the five pages redirect to the overview, while/,/usageand/sign-instill render.RUNNINGand plainFAILEDclusters are not redirected.StartingUp.test.tsx: aTERMINATEDcluster shows "Cluster Terminated". AFAILED+terminatedAtcluster still shows Try Again.Mutation checks, each reverted afterwards:
/domainsand/edit/$modeturned 8 route tests redstatus === 'TERMINATED'turned 15 redclustercontext key turned 20 redGates on Node 24.21.0, with real exit codes captured:
npx vitest run0 (524 files, 5,086 passed);npx tsc -b0;npx oxlint --format stylish .0;npx dprint check0. The pre-commit hook passed too.Cross-model pre-push review: one full round on the first commit (codex graded, gemini, cursor-composer and the Harper domain adjudicator all ran;
independent=true). Adjudicated severity: nit. I acted on four of the findings:context.clusteritself: it now runs the layout'sbeforeLoad.Gemini's major (the guard runs before
context.clusteris filled) and its rail-flash minor were dropped by the adjudicator as factually wrong:clusterLayoutRoute.beforeLoadawaits the cluster before any child runs, and the rail reads that same cache key.I declined one nit with reasons.
ensureQueryDatahands the guard the cached record, and terminating invalidates only[organizationId], so a quick Back to/instancesright after terminating can still render it from stale cache. That only fails open: the deleted states are terminal, central manager still refuses the write, and the rail's own cluster query refetches the stale record on mount (1.9sstaleTime), so the rail corrects itself within one request. The fixes above came after the round and were not re-reviewed.Not browser-verified against a live terminated cluster. Worth a quick look on stage once this deploys: the rail should list only Overview and Usage, and
/domainsshould land on "Cluster Terminated".🤖 Generated with Claude Code