#220 fix: the replication poll yields to the operator's click on both screens - #225
Merged
Merged
Conversation
… actions The poll and both row actions swap one wrapper, so the response that lands second finds its target detached and htmx discards it - swap, focus script and HX-Trigger toast alike. Unsynced, the discarded one was often the operator's. `poll_sync_violations` (tests/dashboard/conftest.py) states the invariant against a poll fragment: the wrapper's poll is `this:abort` and disinherits hx-sync; every request-issuing element inside syncs on the wrapper with `drop`. test_poll_sync.py holds the checker to the markups #220 weighed, so an empty list means safe rather than unexamined. Both screens assert it - the RepSpec twin carries the identical defect, which the issue did not name. Red at this commit on both screens. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… screens The issue had the ordering inverted. A poll landing after the action is harmless: its target was already swapped out, so htmx drops it. The bug is the poll landing first. The action's target is then detached, and its swap, focus script and HX-Trigger toast are all discarded. If the poll read before the commit and no other command was open, its render stopped polling, and the section stayed on pre-issuance state until reload - with a fresh, enabled Replicate now button and no toast, inviting a second occasion. hx-confirm makes that ordering the likely one: it blocks the page, the poll timer runs overdue behind the dialog, and it fires just after the POST. Wrapper: hx-sync="this:abort" + hx-disinherit="hx-sync", inside the poll block. Actions: hx-sync="closest #<section>:drop". htmx takes the lock before confirm(), so an in-flight poll is aborted at the click and a tick during the action is dropped; `every` needs no re-arming. hx-disinherit because hx-sync inherits: without it, anything inside - boosted links included - takes `this:abort` and is dropped mid-poll. Applied to the RepSpec twin too. Verified in Chromium against the dev server's real templates, with latency injected at the browser's network layer rather than a shortened interval, so each ordering is deterministic. Before: both screens lost the POST render, toast and focus, and ended stale and idle when the open command closed during the dialog. After: poll aborted, POST render shown, toast delivered, focus on the heading, ticks continue after a cancelled confirm, boosted links still navigate. Docs: UI.md gains the shared pattern; INFO_ITEM_DETAIL.md and PAGES.md point at it. Also corrects the section header's claim that Replicate now swaps only its row, untrue since #171 CR #37. Closes #220 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
poll_sync_violations holds the templates to the attributes, not htmx to what it does with them. The load-bearing rule is htmx 2.0.8's implementation rather than its documented contract: the docs say `drop` ignores a request while one is in flight, which read literally drops the click behind a poll. 2.0.8 exempts an in-flight request issued `abort` and aborts it instead - so a vendor bump could undo #220 with every Python test green. tests/js/htmx-poll-sync.test.js drives the vendored library through a stubbed XHR held in flight, as htmx-error-trigger.test.js does for #178: an action aborts an in-flight poll and is sent; a tick during an action is dropped; an element inheriting `this:abort` is dropped mid-poll unless the wrapper disinherits; a response whose target was swapped out is discarded, HX-Trigger toast included. Mutating the shipped strategies (action `abort`, wrapper `drop`) fails it. UI.md names the pin. happy-dom's document is not `instanceof Document`, which htmx's plain- selector hx-target lookup requires; the file shims that for its own run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three places said `queue` "re-issues from a detached element that never fires", and test_poll_sync said both strategies "lose a second action's response". Measured in Chromium: - `queue` parks the second action on the wrapper's htmx data, and the first action's swap wipes that data, queue included. When the first action fails without swapping, the queued one does fire - behind a second confirm. A maintainer "fixing" the stated reason would get that surprise dialog. - `replace` aborts the *first* action's in-flight request: the server commits it, but its render and toast are lost. The conclusion - `drop` - stands; only the reasons were wrong. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…othing It said the server refreshes a sub-section by sending HX-Reswap / HX-Retarget headers, citing the rep-spec assignment row. No first-party route or script sends either header, and the per-row swap was retired by #171 CR #37 - the same claim this branch corrected in the section's template header, left standing in a doc the branch edits. Nothing links to the heading. The swaps that do exist are documented by the action-swaps-card and polling-section patterns below it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t a control The discard test pins two negatives - no toast, and the action's render never lands - but nothing in the file showed either positive through this stub XHR. The only answered response carried no HX-Trigger, so "no toast" would also have held had htmx never read the header here. The abort test now answers the action too: its render swaps in and its showFlash reaches document. That is #220's first acceptance bullet - the click's response is what the operator ends up looking at - and the control that makes the discard test's absences mean something. Its first draft compared the whole event detail and failed on the `elt` htmx adds, so it asserts the fields that matter, as htmx-error-trigger.test.js does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CR 2 rewrote this docstring to say `queue` "never sends the second". Measured, it does when the first action fails without swapping - later, behind a second confirm - as UI.md says. The docstring now claims only what holds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Sep 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.
Closes #220.
What was wrong
The issue had the ordering inverted. A poll landing after Replicate now is harmless: its target was already swapped out, so htmx drops it. The bug is the poll landing first. The action's target (resolved at click time) is then detached, and its swap, focus script and
HX-Triggertoast are all discarded. If that poll read before the commit and no other command was open, its render stopped polling, and the section stayed on pre-issuance state until reload - with a fresh, enabled Replicate now button and no toast, inviting a second occasion.hx-confirmmakes that ordering the likely one: the poll timer runs overdue behind the dialog and fires just after the POST. The RepSpec detail page had the identical defect.The fix (both screens)
hx-sync="this:abort" hx-disinherit="hx-sync".hx-sync="closest #<section>:drop".htmx takes the lock before
confirm(), so an in-flight poll is aborted at the click, and a tick during the action is dropped;everyneeds no re-arming.hx-disinheritbecausehx-syncinherits: without it, anything inside the section - boosted links included - takesthis:abortand is dropped mid-poll.Tests
poll_sync_violations(tests/dashboard/conftest.py) checks the rule on each screen's poll fragment.test_poll_sync.pyholds the checker to the markups the issue weighed, so an empty list means safe, not unexamined. Red at1d64ec5, green atab4584c.tests/js/htmx-poll-sync.test.jsdrives the vendored htmx 2.0.8 through a stubbed XHR held in flight. Thedrop-aborts-an-abortable-poll rule is htmx's implementation, not its documented contract, so an htmx upgrade could undo Replication poll and Replicate now race for the same swap target #220 with every Python test green. Setting the actions toabortor the wrapper todropfails it.Browser verification (#220 acceptance)
Run in Chromium against the dev server's real templates (8001, dev DB, bus off). Instead of shortening the poll interval, latency was injected in the browser, so each ordering happens every time rather than by chance.
Review rounds
Two CR rounds, six findings, all in their own commits. One correction worth reading: the #220 comment called
queue all"deferred and hidden". Measured,queueparks the second action on the wrapper, where the first action's swap wipes it; only when the first fails without swapping does the queued one fire, late, behind a second confirm.replaceaborts the first action's request: the server commits it, but its render and toast are lost.dropstands.Also in this PR
0085e96 chore: update skills submodules- the SessionStart auto-refresh bump, which was on localmainwhen this branch was cut. It rides along asec800b2did in #213 docs: index REGISTER.md; bring BUS.md, PAGES.md and AGENTS.md under budget #221.HX-Reswap/HX-Retargetheaders no first-party code sends, citing the per-row swap Dashboard replication state + streams table: retire the hand-edited public_url #171 CR API Keys - updates to workflow and list #37 retired.No contract-visible paths change, so no CHANGELOG entry. After merge:
sudo systemctl restart archiver.🤖 Generated with Claude Code