Skip to content

fix(archive): keep dismissed confirmations closed across delayed responses - #3010

Merged
Chris0Jeky merged 3 commits into
mainfrom
codex/issue-2996-archive-request-ownership
Sep 12, 2026
Merged

Chris0Jeky merged 3 commits into
mainfrom
codex/issue-2996-archive-request-ownership

Conversation

@Chris0Jeky

@Chris0Jeky Chris0Jeky commented Sep 11, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #2996.

CardArchiveAction.refreshChildren checked confirming only on entry and never re-checked after its await. Dismiss the confirmation (Escape/backdrop) while an in-dialog child refresh is in flight and the late response reopened the dialog the user closed, cleared the retained error, and pulled focus back into the modal. The mirror case overwrote the page-level message and stole focus after the user had moved on.

  • Two ownership counters. Card identity change and unmount invalidate the component context; Escape, backdrop and Cancel additionally invalidate the confirmation. Every preview success and failure re-checks both owners after its await, so an obsolete promise can neither repopulate the preview nor overwrite retained recovery state.
  • Scheduled focus recovery re-checks ownership after nextTick, not only when it is scheduled, so dismissed work cannot pull keyboard focus back.
  • An old context cannot clear a newer context's busy.
  • Dismissal is not cancellation of a submitted write: a successful setCardArchived for the same card still emits changed, a failed one still raises the page-level error without stealing focus. Escape stays unconditional — a request that never settles must not trap a keyboard user.
  • Eight native Vue component regression cases, plus the issue's diagnosis/qualification note.

Rebase onto current main

The original draft was based on 4c479a7ff and went DIRTY when #3015 (GH-2969) landed the archived prop override in the same component. Merged with main at 5396fb6cc; the ownership model is re-applied on top of main's component, not the other way round. Both conflicts were additive and resolved keeping main's semantics intact:

  • change() sends !archived.value (main's settled-state override), then the context guard runs.
  • requestChange() keeps main's if (archived.value) return change() entry.

git diff origin/main...HEAD touches exactly three files; no other file in main's version of the component is altered.

Verification actually executed

Real Vitest on the merged head — the draft's dependency-free Node probe is superseded and is not evidence for this change.

Command (from frontend/taskdeck-web) Result
npx vitest --run --maxWorkers=2 src/tests/components/CardArchiveAction.requestOwnership.spec.ts src/tests/components/CardArchiveAction.spec.ts src/tests/components/BoardCardArchive.spec.ts 18/18 pass (the 8 new cases named individually under --reporter=verbose)
npx vitest --run --maxWorkers=2 src/tests/components/CardModal.spec.ts src/tests/components/CardModalAssignmentSave.spec.ts src/tests/views/paper/PaperBoardCard.spec.ts 75/75 pass (consumers of this component, including #3015's kept-draft paths)
npm run typecheck clean
npm run build clean
node scripts/check-docs-governance.mjs / node scripts/check-doc-links.mjs (repo root) both pass

Regression value measured, not assumed. With the component reverted to main's version and the spec untouched, 5 of the 8 fail: both late-refresh-after-Escape cases, the late write failure after Escape, and both card-switch cases. The other three — late write success after Escape, same-card prop replacement, and unmount before the write settles — pass against the unfixed component too, so they pin invariants the repair must not break rather than reproducing the reported defect. Said plainly here and in the note rather than presented as eight reproducers.

NOT verified

  • Real-browser Paper/Legacy archive-recovery journeys and screen-reader announcement order. The same manual gap the existing archive specs carry; not introduced here.
  • The full frontend suite is not run locally (it exhausts memory on this box). The hosted Frontend Unit job is the broad evidence.
  • No backend change, so no backend suite was run.

Scope

CardArchiveAction.vue, its new spec, and docs/analysis/2026-09-11-archive-request-ownership.md (rewritten: the draft's note claimed the frontend commands were NOT RUN, which is no longer true). No endpoint, payload, permission rule, expected-state token, retry behaviour or confirmation wording changes. No CardModal, BoardCardArchive or CI control-path edits. STATUS/MASTERPLAN unchanged — a bounded defect repair inside one already-shipped control, the same basis on which #3015 landed.

#2969 (newer unsaved draft disposal) and #2997 (assignment-save close paths) remain separate. #3023 (kept-draft notice residuals from #3015's review) is untouched by this PR.

Review focus

Escape during a deferred refresh/write in both success and failure; the interaction between the new onBeforeUnmount invalidation and CardModal's :key="card.updatedAt" remount of this control; same-ID prop replacement during a write; context switches with overlapping reads; and the shared control in both Paper and Legacy hosts.

…e receipts

Refs #2996. Add deferred-response component regressions and an explicit verification handoff.

Copy link
Copy Markdown
Owner Author

Exact-head qualification update for 1e891252083adba98994d5c3d5f8b16c79979925: hosted frontend lint/typecheck/build/coverage and backend-unit jobs have passed on both Windows and Linux; Linux API integration and docs governance also passed. CI run: https://github.com/Chris0Jeky/Taskdeck/actions/runs/34634840225. Windows API integration and browser smoke were still running when inspected, so this remains draft; full CI and independent review are not claimed complete. Local diagnostic was rerun: baseline 4/12 pass, fix 12/12 pass (script/lifecycle stubs, not native Vue). Full lane release and remaining ownership boundaries: #2996 (comment) .

…ive-request-ownership

# Conflicts:
#	frontend/taskdeck-web/src/components/board/CardArchiveAction.vue
…uite evidence

The draft's qualification note recorded the repair as unqualified and listed
the frontend commands as NOT RUN, because the authoring environment had no
installed frontend dependencies. Both statements are now false: the eight
regression cases, the existing archive specs, the consumer specs, typecheck
and build all ran on the merged head.

Also records what the cases are actually worth: reverted to main's component,
five of the eight fail and three pass, so three of them pin invariants rather
than reproducing the defect. Real-browser and screen-reader journeys stay
marked NOT RUN.
@Chris0Jeky
Chris0Jeky marked this pull request as ready for review September 11, 2026 23:34
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@Chris0Jeky

Copy link
Copy Markdown
Owner Author

Review round 1 — disposition

Codex connector did not review this head. Its only comment is an informational notice that the account's code-review usage limit is reached; that is a non-finding, not a clean outcome, so under global law 2g the review gate is carried by one fresh-context independent adversarial pass instead. That pass ran against the merged head 66fd0ab20 with the diff, the issue text, and both consuming components (CardModal.vue, BoardCardArchive.vue) in scope.

Result: nothing at CRITICAL or HIGH. One MEDIUM, four LOWs, one vacuous test. All non-blocking under law 2a; tracked in #3033 rather than fixed here, so the reviewed head is the merged head.

Confirmed sound

  • The merge against fix(frontend): preserve unsaved card drafts across archive recovery and completion #3015 is faithful. archived survives in the prop defaults block, the requestChange entry (if (archived.value) return change()), the button label, and the !archived.value argument to setCardArchived. None of main's lines were reverted by the conflict resolution; CardModal.spec.ts's completed-archive-over-a-kept-draft case still passes.
  • The finally/try asymmetry is correct. The try/catch bodies guard on context and confirmation; the finally guards on context only. After Escape-during-refresh the request is no longer in flight, so busy must clear or the opener renders "Saving…" forever. busy was traced and cannot get stuck true: every contextGeneration bump comes either from the watcher (which sets busy = false in the same synchronous callback) or from unmount.
  • flush: 'sync' is load-bearing, not incidental. Both watch sources are prop getters so the callback cannot re-trigger itself; with the default pre flush there is a microtask window in which a response could land under the new card.
  • focusRecovery's default-parameter evaluation is not off by one. closeConfirmation bumps first and then calls focusRecovery() with post-bump values, so the recovery it schedules for itself validates while one scheduled by an earlier catch correctly aborts.

Declined here, tracked in #3033

  • MEDIUM — a write failure after Escape can leave focus on document.body. change()'s catch passes the captured confirmation, so the recovery focus move is suppressed on the premise that the user moved to another control; in the real DOM (Confirm goes disabled on busy, so focus drops to body; TdDialog's restoreFocus no-ops against the disabled opener) the user is on nothing, and the page-level "Refresh card state" never receives focus. A narrow a11y regression against main's unconditional focusRecovery() in that catch — not correctness, security or data loss, so it does not meet law 2a's merge-blocking bar. It is also not reproduced: happy-dom's focus() does honour disabled, but the "focus falls to body" step was reasoned about, not run, and the spec's focusElsewhere() helper manufactures a focused element this path cannot produce. Reproducing it is the first step in [Frontend][Archive] Write-failure-after-Escape can strand focus on body; request-ownership residuals (from PR #3010 review) #3033, before any fix.
  • LOW ×4 — the vacuous unmount case (Vue's emit() already short-circuits on an unmounted instance, so that assertion holds with or without the generation bump, and the analysis note overstates what the guard adds); a write failure after a card switch being reported nowhere; the same-card double-submit window opened by resetting busy on a card switch (server-rejected on the precondition, so a confusing error rather than data loss); and confirmationGeneration never being bumped on open, which is sound only while busy serialises the three request paths.
  • INFO — the new spec mocks boardStore as a plain non-reactive object while CardArchiveAction.spec.ts uses real Pinia; fine for these cases, noted in [Frontend][Archive] Write-failure-after-Escape can strand focus on body; request-ownership residuals (from PR #3010 review) #3033 so a future permission-transition case is not silently inert.

Evidence at this head

Hosted Frontend Unit passed on both ubuntu-latest and windows-latest — that is the broad frontend evidence the local box cannot produce. Backend Unit (both platforms), Backend Architecture, Docs Governance, Migration Validation, SAST, Container Images and Smart CI Required Gate all passed. Locally on the merged head: the three archive specs 18/18, the three consumer specs 75/75, typecheck clean, build clean, docs governance and doc links clean — with the eight new cases' regression value measured by reverting the component (5 of 8 fail, 3 pin invariants), stated plainly in the body rather than presented as eight reproducers.

@Chris0Jeky
Chris0Jeky merged commit 9ffacf2 into main Sep 12, 2026
52 of 53 checks passed
@Chris0Jeky
Chris0Jeky deleted the codex/issue-2996-archive-request-ownership branch September 12, 2026 00:24
@github-project-automation github-project-automation Bot moved this from Pending to Done in Taskdeck Execution Sep 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[Frontend][Archive] In-dialog child-list refresh has no request ownership: a late response resurrects a dismissed confirmation (from PR #2995 review)

1 participant