Skip to content

fix(cluster): keep only Overview and Usage in a terminated cluster's rail - #1926

Merged
dawsontoth merged 1 commit into
stagefrom
claude/terminated-cluster-rail
Oct 13, 2026
Merged

dawsontoth merged 1 commit into
stagefrom
claude/terminated-cluster-rail

Conversation

@dawsontoth

Copy link
Copy Markdown
Contributor

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):

  • Terminate unbinds every domain. After it tears down the instances and commits, deleteCluster sets clusterId: null on each domain in cluster.domainIds (deleteCluster.js#L133-L141). The domains can then be bound to another cluster or deleted from the organization.
  • The cluster's own domainIds is left in place (the status patch at #L34-L42 only writes status, terminatedByUserId and terminatedAt). Because domains is a @relationship(from: domainIds) (schema.graphql#L150-L151), a terminated cluster still lists its domains even though none of them is bound to it.
  • Certificates: central manager keeps no certificate record of its own. Custom-domain certs are issued on the instances (generateDomainCerts is stamped per instance in services/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.
  • Domains couldn't unbind anything anyway. Studio's unbind is PUT /Cluster/{id} with domainIds. updateCluster calls assertClusterMutable first (updateCluster.js#L70), and that refuses any cluster that isn't RUNNING (sharedHelpers.js#L587-L593). The same check rejects Scaling and Version.
  • Edge case: the unbind is best-effort. It runs after the commit inside a try/catch that 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 /Domain also refuses a bound domain (Domain.js#L124-L129), so only support can release it. The empty/self-hosted early return at #L66-L87 skips the unbind, but a self-hosted cluster is refused domains (updateCluster.js#L169-L171), and binding reads instances[0], so a cluster that never had an instance can't hold one either.

So hiding Domains costs the user nothing.

Solution

  • Rail (ClusterPageLayout): for a cluster that isDeletedCluster reports gone, the rail shows only Overview and Usage. isDeletedCluster is 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, or FAILED with terminatedAt. 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.
  • Bookmarks: a new redirectAwayFromDeletedCluster beforeLoad sends /instances, /scaling (the update-progress screen), /domains, /edit and /edit/$mode to 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 reads context.cluster from the cluster layout's beforeLoad, 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 because features/clusters/routes.ts can't import from features/cluster/routes.ts without creating an import cycle.
  • Starting-up: for a deleted cluster this page now renders the same "Cluster Terminated" state, where before it showed "Here we go! … spinning up" forever. Finish-setup sends a cluster that isn't ready here, and a terminated cluster can still carry resetPassword. The page checks FAILED first, 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.md gets a section recording the central-manager behavior above.

Not changed, on purpose:

Overlap: none. The diff touches src/features/cluster/* and the two edit-route definitions in src/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 are clusters/upsert/*, ClustersList.tsx, clusters/DESIGN.md and the root DESIGN.md.

Verification

  • ClusterPageLayout.test.tsx pins the exact rail items and their targets for:

    • running managed: all six items
    • TERMINATED, TERMINATING, and FAILED + terminatedAt: Overview and Usage
    • FAILED without terminatedAt: all six, because it may still be serving
    • removed self-hosted: Overview only
    • running self-hosted: Overview, Configuration, Instances
    • view-only member: unchanged
  • redirectAwayFromDeletedCluster.test.ts matches each bookmark against the real route tree. It runs the real cluster layout beforeLoad against a seeded query cache, then the matched route's beforeLoad with 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 /, /usage and /sign-in still render. RUNNING and plain FAILED clusters are not redirected.

  • StartingUp.test.tsx: a TERMINATED cluster shows "Cluster Terminated". A FAILED + terminatedAt cluster still shows Try Again.

  • Mutation checks, each reverted afterwards:

    • forcing the rail gate open turned 4 rail tests red
    • removing the guard from /domains and /edit/$mode turned 8 route tests red
    • narrowing the guard to status === 'TERMINATED' turned 15 red
    • renaming the layout's cluster context key turned 20 red
    • dropping Starting-up's deleted branch turned 1 red
    • checking deleted before failed (which loses Try Again) turned 1 red
  • Gates on Node 24.21.0, with real exit codes captured: npx vitest run 0 (524 files, 5,086 passed); npx tsc -b 0; npx oxlint --format stylish . 0; npx dprint check 0. 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:

    • Starting-up dead-ended on a terminated cluster: fixed as above.
    • DESIGN.md claimed Usage for every deleted cluster, but a self-hosted one never shows it: corrected.
    • The route test handed in context.cluster itself: it now runs the layout's beforeLoad.
    • Narrating docblocks: trimmed.

    Gemini's major (the guard runs before context.cluster is filled) and its rail-flash minor were dropped by the adjudicator as factually wrong: clusterLayoutRoute.beforeLoad awaits the cluster before any child runs, and the rail reads that same cache key.

    I declined one nit with reasons. ensureQueryData hands the guard the cached record, and terminating invalidates only [organizationId], so a quick Back to /instances right 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.9s staleTime), 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 /domains should land on "Cluster Terminated".

🤖 Generated with Claude Code

…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
dawsontoth marked this pull request as ready for review October 13, 2026 14:41
@dawsontoth
dawsontoth requested a review from a team as a code owner October 13, 2026 14:41

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/features/cluster/components/ClusterPageLayout.tsx
@github-actions

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 77.43% 13302 / 17179
🔵 Statements 77.37% 14120 / 18248
🔵 Functions 72.25% 3500 / 4844
🔵 Branches 72.39% 10451 / 14436
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/features/cluster/StartingUp.tsx 85% 84.61% 66.66% 85% 39-40, 82-99
src/features/cluster/redirectAwayFromDeletedCluster.ts 100% 100% 100% 100%
src/features/cluster/routes.ts 80% 100% 60% 80% 19, 27, 34, 42, 50, 112, 120, 130
src/features/cluster/components/ClusterPageLayout.tsx 100% 95.23% 100% 100%
src/features/clusters/routes.ts 75% 100% 55.55% 75% 16, 23, 30, 38
Generated in workflow #2325 for commit 1c9d79e by the Vitest Coverage Report Action

@dawsontoth
dawsontoth merged commit 3c38787 into stage Oct 13, 2026
3 checks passed
@dawsontoth
dawsontoth deleted the claude/terminated-cluster-rail branch October 13, 2026 14:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant