Skip to content

Bind on the Domains page sends only the domains still offered for binding - #1840

Draft
dawsontoth wants to merge 1 commit into
claude/1630-challenge-cert-poll-backofffrom
claude/1800-bind-only-live-selection
Draft

dawsontoth wants to merge 1 commit into
claude/1630-challenge-cert-poll-backofffrom
claude/1800-bind-only-live-selection

Conversation

@dawsontoth

@dawsontoth dawsontoth commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Closes #1800

Stacked on #1802, which is stacked on #1801: #1801 → #1802 → this PR → #1846 (#1686). Its base is claude/1630-challenge-cert-poll-backoff, so this diff is only the Bind-selection change. Review and merge #1801 and #1802 first.

⊙ Problem

On a cluster's Domains page, the domains ticked for binding are page state that outlived their rows. Tick a domain, then click Remove Domain on it (enabled for any unbound domain, ticked or not): the row disappears, but Bind 1 Domain stays, and clicking it sends the deleted id. Central manager answers 404 Domain <id> not found, and since the selection only clears when a bind succeeds, every later Bind on that page fails the same way until the user leaves it. A ticked domain that gets bound to another cluster meanwhile fails the same way with a 400 (#1800).

❓ Your call: does this need to ship on its own? #1846, the #1686 PR stacked on this one, re-reads the domain list before every domain change and would refuse the stale id with a message. I kept this separate because it needs no extra request, and it fixes what the button counts and sends, which a refusal alone doesn't.

💡 Solution

Bind now counts and sends only the ticked domains that the current list still offers for binding: the selection intersected with the rows canBindDomain accepts, which is the same predicate that renders the checkbox. A domain that was removed, or bound elsewhere, drops out of the count and the request on its own, with no extra state to keep in sync.

Three gaps around a removal are closed as well:

⚖️ Alternatives

  • Prune the selection when the list changes (an effect, or deselect on delete): it is a second copy of the same predicate kept in sync by hand, and it misses a domain that becomes bound elsewhere in a refetch, which no delete callback sees.
  • Disable Remove Domain on a ticked row: it covers the delete but not a domain bound from another tab, and blocks a removal the user is entitled to make.

🔧 Changes

✅ Verification

End-to-end route: not observable end-to-end here — it needs a cluster, verified custom domains and a central manager, and a Bind changes real infrastructure. Verified at the request boundary instead: Management.test.tsx renders the real DomainsManagement and table against a mocked apiClient and asserts the serialized PUT body.

Pre-push review: three full rounds (codex graded, gemini). The Harper-domain adjudicator failed on auth every round and the Cursor legs failed on the 1Password SSH agent, so no round converged. Round 1 found the render-lag click (fixed: Bind reads the list from the query client); round 2 found the reverse race, a Remove queued behind a Bind (fixed: Remove shares isSettingDomainIds). Round 3 returned COMMENTS with no defects. I rejected:

  • "other useDeleteDomain callers break on the new organizationId" (gemini): tableDefinition.tsx is the only caller, and DeleteDomainSchema is only a type;
  • "a double-clicked Bind sends two PUTs" (gemini): Unbinding one custom domain no longer turns off certificates for the others #1801's mutateWhenIdle already refuses a second domain change while one is in flight;
  • the remaining comment nits (both legs): the two kept comments state why the handlers read the query client rather than the render, and why the delete writes the cache, neither of which the code says.

🤖 Generated with Claude Code (Claude Opus 5.5); posted via @dawsontoth.

Related PRs: #1801 overlaps (this PR stacks on it), #1802 overlaps (this PR's base)
Complexity: easy

Review-Coverage: authored=claude; ran=gemini,codex; blocked=cursor-composer(no-receipt),domain(auth),cursor-muse(no-receipt); declined=cursor-grok,cursor-kimi; rounds=3; full=1 @ 00c3b5d

Review-Attention: read ~5m (raised: degraded review) @ 00c3b5d

The Bind selection was page state that outlived its rows. A ticked domain that was then removed,
or bound to another cluster, was still counted and sent, and central manager refused every later
Bind (404 not found, 400 already associated) until the page was left.

Bind now counts and sends the selection intersected with the list's bindable rows, using the
same predicate that renders the checkbox. A removal takes its row out of the cached list as soon
as it succeeds, so the gap before the refetch lands cannot send it, and Bind waits while a
removal is in flight, including a click queued before the controls disabled. Remove likewise
refuses a click queued behind a Bind that is still sending its domain. Bind reads the list from
the query client at click time, since a removal reaches the button a render later.

Closes #1800

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@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 improves the domain binding and removal flow in the cluster domains management feature by handling race conditions between concurrent bind and delete actions. It introduces state checks to ensure only bindable domains are processed, disables controls while mutations are in flight, and optimistically updates the React Query cache upon domain deletion. The review feedback correctly points out that the global queryClient is still being used for query invalidation in Management.tsx and tableDefinition.tsx instead of the newly introduced activeQueryClient hook, which could lead to issues in test environments or custom configurations.

},
});
}, [cluster, selectedDomainIds, setDomainIds]);
}, [activeQueryClient, cluster, organizationId, selectedDomainIds, setDomainIds]);

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.

medium

In onBindClick, queryClient.invalidateQueries is called on line 128 using the global queryClient import instead of the context-provided activeQueryClient. This can cause query invalidation to fail or be ignored in environments with custom query clients (such as during testing). Please update it to use activeQueryClient.invalidateQueries on line 128.

},
});
}, [deleteDomain, cluster.organizationId]);
}, [activeQueryClient, deleteDomain, cluster.organizationId]);

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.

medium

In deleteDomainById, queryClient.invalidateQueries is called on line 54 using the global queryClient import instead of the context-provided activeQueryClient. This can cause query invalidation to fail or be ignored in environments with custom query clients (such as during testing). Please update it to use activeQueryClient.invalidateQueries on line 54. Additionally, consider updating unbindDomainById (on line 68) to also use activeQueryClient and adding activeQueryClient to its dependency array.

@github-actions

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 67.82% 10168 / 14992
🔵 Statements 67.97% 10849 / 15961
🔵 Functions 61.37% 2635 / 4293
🔵 Branches 62.72% 7718 / 12304
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/features/cluster/domains/Management.tsx 78.02% 84.21% 82.35% 79.31% 89, 115, 118-119, 211-237, 245, 251-252
src/features/cluster/domains/canBindDomain.ts 100% 100% 100% 100%
src/features/cluster/domains/constants/tableDefinition.tsx 93.33% 83.87% 93.33% 95.34% 139, 172, 185
src/features/clusters/mutations/setDomainIdsOnCluster.ts 100% 100% 100% 100%
src/features/organization/mutations/deleteDomain.ts 100% 100% 100% 100%
Generated in workflow #2140 for commit 00c3b5d by the Vitest Coverage Report Action

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.

Bind keeps sending a domain that was deleted after it was ticked, and fails until the page is left

1 participant