Skip to content

CR round 3 on #212: the detail page never started polling - #218

Merged
gregoryfoster merged 6 commits into
mainfrom
cr/212-round3
Sep 11, 2026
Merged

gregoryfoster merged 6 commits into
mainfrom
cr/212-round3

Conversation

@gregoryfoster

Copy link
Copy Markdown
Contributor

Round-3 review of 88d3dfd..a8ab532 (the #212 work). Six findings applied, one held.

# Finding Action
12 🔴 The InfoItem detail page never passed poll — a full page load with an open command was inert Fixed 508711b
13 {% if poll is defined %} turned the missing key into silent non-polling Fixed 620a54c
14 Both partials' contract comments omitted poll / poll_interval_seconds Fixed 5f63556
15 No Cache-Control on a fragment fetched every 2s Fixed 66723c6
16 The stalled notice was duplicated verbatim across two templates Fixed df85c72
17 Mapping[object, ReplicationCommand] where keys are ULIDs Fixed 1aea43b
18 Poll/click swap race Held — the fix is speculative

Finding 12: #212's own defect survived on the most ordinary path

The section reaches a browser by four routes, not three: the deactivate swap, the Replicate now swap, the poll — and the detail page itself. Only the three swaps went through the renderer that supplies poll, so a full page load rendered the section with no polling context at all.

Click Replicate now, press F5 before it settles, and the outcome is back to requiring a manual reload. Arriving at the item from the list while a command is in flight behaves the same. The RepSpec twin routed all four of its sites from the start, so the two screens disagreed — and the broken one is the screen the operator actually used.

Two tests pin it: the page polls with a command open, and does not with nothing open.

Finding 13 is why it shipped green

{% if poll is defined and poll.active %} — under Jinja's default undefined, a missing poll is falsy. Every test exercised a path that passed the key, and the one path without it degraded silently instead of failing. A defensive guard that converts a missing contract into the original defect is worse than no guard.

Finding 18, held

A poll firing while a Replicate now POST is in flight gives two swaps racing for one target; a poll response landing after the POST's would briefly show pre-issuance state. It self-corrects within one interval and both renders are server-authoritative, so the impact is a flicker. hx-sync is the obvious lever, but which element declares it and which strategy gives the right precedence — the poll must lose to the click, never the reverse — needs verifying in a browser rather than reasoning about. Not applied under a blanket directive.

Verification

ruff check and ruff format --check clean over 440 files; 1791 passed, 2 skipped, 3 deselected. Five net-new tests.

🤖 Generated with Claude Code

gregoryfoster and others added 6 commits September 11, 2026 18:58
The section reaches a browser by four routes, not three: the deactivate swap,
the Replicate now swap, the poll - and the detail page itself. Only the three
swaps went through the renderer that supplies `poll`, so a full page load
rendered the section with no polling context at all.

That is archiver#212's own defect surviving on the most ordinary path to this
screen. Click Replicate now, press F5 before it settles, and the outcome is
back to requiring a manual reload. Arriving at the item from the list while a
command is in flight behaves the same way. The RepSpec twin routed all four of
its sites from the start, so the two screens disagreed - and the one that was
wrong is the one the operator actually used.

One context builder now serves every site here too. Two tests pin it: the page
polls with a command open, and does not with nothing open - a page that always
polled would be worse than one that never did.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both templates read `{% if poll is defined and poll.active %}`. Under Jinja's
default undefined a missing `poll` is falsy, so a render site that forgot the
key produced a section which silently never polled - and that is exactly how CR
12 shipped green: every test exercised a path that passed the key, and the one
path without it degraded instead of failing.

A defensive guard that turns a missing contract into the original defect is
worse than no guard. Every site supplies the keys now, so the templates say so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…needs

Both headers declared their required context and stopped short of `poll` and
`poll_interval_seconds`. That comment is the only statement of what a caller
must supply, and it is the document CR 12 would have been checked against - the
detail page omitted both keys and nothing said it was wrong.

The hub's note also records why the `is defined` guard is gone, since that is
the part a future reader is most likely to want to reinstate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both poll fragments returned a plain 200 with no cache headers. Heuristic
caching has little to work with here - no Last-Modified, no Expires - so the
present risk is low, but a fragment fetched every two seconds is the one
response in this app that must never be served from a cache: a stale body would
freeze the section on a state that reads as authoritative, which is #212's
defect wearing a different hat. One header removes the class.

On the GETs only. The swap responses are POST and DELETE, which are not
cacheable by method.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The paragraph was duplicated verbatim across both partials, with the comment
explaining why it says what it says in only one of them. The two screens render
the same section from the same live_poll decision, so they have to describe
"open, no longer watching" identically - and the copy without the reasoning
beside it is the one a later edit would reword.

Lives in _macros.html beside replication_state, which the same two screens
already share for the same reason.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every caller passes a mapping keyed by assignment ULID; `object` accepted
anything and documented nothing, in a module whose other signatures are exact.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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