Distinguish lost board access when loading parent choices - #2991
Conversation
A 403 on the parent-candidates GET meant board access was revoked after the card editor opened, but the catch discarded the status and rendered the generic "close and reopen this card to retry" copy. Reopening cannot restore access, so the guidance sent the user round a loop that could never succeed. Classify the failure instead: 403 renders explicit permission-loss guidance, every other failure (5xx, network, timeout) keeps the existing retry copy. Both states still disable the selector, and the existing generation guard is unchanged, so an obsolete request's completion - success or failure - never overwrites a newer request's state. Spec covers revoked access, an ordinary read failure, a response-less network failure, both obsolete-completion orderings and the happy path.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Fresh-context independent review at exact head Verified by the reviewer from source (read-only, no execution):
Non-blocking notes, recorded here rather than as commits (law 2c):
Merge gate: waiting for the pending hosted checks at this head and the 3-minute aging floor; will merge with a merge commit once green. |
|
Re-running the single failed job once (Backend Unit windows-latest: |
What
CardParentFieldnow distinguishes lost board access from a transient read failure when it loads the parent candidates for a card.403onGET /boards/{boardId}/cardsrenders explicit permission-loss guidance: "You no longer have access to this board, so parent choices are unavailable. Reopening this card will not restore them. Ask a board admin to restore your access."Why
From the source-confirmed MEDIUM in PR #2965 (comment #2965 (comment)). If board access is revoked after the card editor opens, the candidates GET answers
403. The oldcatch {}discarded the status and always showed the close-and-reopen retry copy, so the user was told to retry something that can never succeed — reopening the card re-issues the same refused read, and the server rejects the parent write too. The defect is misleading permission recovery, not an authorization bypass: writes were and remain server-authorized.Scope is deliberately narrow.
403alone is treated as permission loss — deliberately narrower than the sharedisAccessDeniedError(403 OR 404), following the same reasoning asuseReviewProposals.isForbiddenError: a404from a list endpoint is a routing/board-gone fact, not a permission signal, so it keeps the generic copy.Stale-request isolation is untouched: the
generationcounter still gates every write (the success assignment, the new failure classification and theloadingreset), so an obsolete request's completion — success or failure — never overwrites a newer request's state. Two spec cases pin both orderings.Tests
New spec
frontend/taskdeck-web/src/tests/components/board/CardParentField.spec.ts(the component had none):Checks run (worktree
C:/wt/td-2974, Node 24.x)npx vitest --run --maxWorkers=2 src/tests/components/board/CardParentField.spec.ts— 6 passed.npx vitest --run --maxWorkers=2 src/tests/components/CardModal.spec.ts— 40 passed (the parent field's only consumer).npm run typecheck— clean.npm run build— clean.NOT verified
cardsApi.getCards, as the originating triage also claimed no browser repro.--maxWorkers); CI covers it.CardParentFieldcarries hardcoded English strings today like its siblings incomponents/board/, so the new copy follows the file's existing idiom rather than introducing a first translation key for this component.Closes #2974