fix(ui): history fan — stable expansion and newest-first ordering - #66
Open
MA1503 wants to merge 2 commits into
Open
fix(ui): history fan — stable expansion and newest-first ordering#66MA1503 wants to merge 2 commits into
MA1503 wants to merge 2 commits into
Conversation
The history endpoint returns projects unsorted, so the card fan showed runs in arbitrary order — a fresh run could appear anywhere in the stack instead of at the front. Sort client-side by created_at descending (ISO strings compare lexicographically, so localeCompare is stable and needs no date parsing). Records with a missing created_at sort last. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Expanding the simulation-records fan grows the observed container itself (~550px fan -> 1600px+ grid once the record count exceeds one grid row), so its intersectionRatio drops below the lowest observer threshold (0.4) and Chromium reports isIntersecting=false while a large part of the section is still on screen — isIntersecting is derived from the crossed threshold index, not from actual visibility. The section then collapsed itself roughly 850ms after opening, with no scroll movement at all. Fix: hysteresis. The initial expansion still requires ratio >= 0.4, but staying expanded only requires any visibility. A 0 threshold is added, which is mandatory for isIntersecting to be reported as true below 0.4. Collapse on scroll-out and card clicks are unaffected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
Two independent bugs in
frontend/src/components/HistoryDatabase.vue, one commit each.1. History records were shown in arbitrary order
loadHistory()assigned the API response straight toprojects:The history endpoint does not guarantee an order, so the card fan could show older
runs in front of newer ones — a run you just finished ended up somewhere in the
middle of the stack instead of at the front, which is the one place you look for it.
Fixed by sorting client-side, newest first.
created_atvalues are ISO strings, sothey compare correctly lexicographically and no date parsing is needed; records with
a missing
created_atsort last:.slice()keeps the sort off the response object. No other behaviour changes.2. The fan collapsed itself immediately after expanding
Opening the records section made it snap shut again about 850ms later, with no
scroll movement. It reproduces reliably once the record count exceeds a single grid
row (roughly 5+ records); with fewer records the section barely grows and the bug
stays hidden, which is why it can look intermittent.
Why it happens
The
IntersectionObserverobserves the very container that expansion resizes:The visible slice of the page does not change, so
intersectionRatiofalls from0.45 to 0.375 purely as a side effect of the element growing. That drops below
the observer's lowest threshold (
0.4), and this is the part that turns a cosmeticdetail into a bug: Blink derives
entry.isIntersectingfrom the crossed thresholdindex, not from actual visibility. With
threshold: [0.4, 0.6, 0.8]any ratiobelow 0.4 is therefore reported as
isIntersecting: false, even though severalhundred pixels of the section are plainly on screen.
The callback used that value directly:
so expanding produced the observer event that collapsed it again. The existing
debounce and animation lock cannot help here — the event is not flicker, it is a
single, confident, wrong "not visible" report arriving right after the animation
lock releases.
The fix
Hysteresis, with separate conditions for entering and staying in the expanded state:
Opening still requires a decent share of the section to be visible, so the section
does not pop open when it is only peeking into the viewport. Staying open requires
only that it is visible at all, so the element's own growth can no longer close it.
The threshold list gains a
0entry:This is required, not cosmetic: without a 0 threshold there is no crossing for
Blink to report below 0.4, so
isIntersectingwould never become true again in thatrange and the hysteresis branch would never see the state it exists to handle. The
0.4 literal is now the named
EXPAND_RATIOso the gate and the threshold cannotdrift apart.
rootMargin, the debounce delays, the animation lock and the collapse-on-scroll-outbehaviour are all untouched.
Verification
Measured in Chromium via Playwright, counting expand/collapse transitions per run:
The remaining 3 are the intended ones (scroll in, scroll out, card click). Collapse
on scroll-out and on card clicks still work; expansion no longer triggers below the
intended visibility share.
The sort and the hysteresis fix are independent — the ordering change did not cause
the collapse, the record count crossing one grid row did.
🤖 Generated with Claude Code