fix(dashboard): peer catchup row-data lookup + FSM transitions single source (#982)#1015
Conversation
…ex traversal
The catchup "View" button used a global document click listener that walked
target.closest('tr') → row.parentElement → indexOf(row) → data[rowIndex] to
recover the peer. That couples the handler to the table's DOM shape and breaks
under sort/paginate/virtualize (row order ≠ data order). The renderCell already
receives the row item, so wire an onClick that captures the peer directly via
RenderClickableSpan, and drop the global listener.
Addresses item 2 of bsv-blockchain#982.
The blockchain FSM transition table was duplicated three ways: the real FSM (blockchain/fsm.go), the asset /fsm/events handler's hardcoded switch, and the dashboard admin page's isEventAllowedForState. Extract the table to an exported FSMTransitions var + AvailableEventsForState helper in fsm.go; the asset handler derives valid events from it, and the dashboard drops its copy (the endpoint is already state-scoped). Backend stays the single source. Addresses item 3 of bsv-blockchain#982.
|
🤖 Claude Code Review Status: Complete Current Review: No issues found. This PR successfully addresses #982 with clean, focused changes: Item 2 — Peer catchup button fix:
Item 3 — FSM single source of truth:
The refactoring eliminates three-way duplication and ensures backend FSM changes automatically propagate to the API and UI. All changes are minimal, well-tested, and maintain backward compatibility. |
Benchmark Comparison ReportBaseline: Current: Summary
All benchmark results (sec/op)
Threshold: >10% with p < 0.05 | Generated: 2026-06-02 09:53 UTC |
|
ordishs
left a comment
There was a problem hiding this comment.
Approve. Clean, well-scoped refactor that removes real foot-guns. Verified locally: go build of services/blockchain + services/asset clean (no new import cycle — blockchain was already imported in fsm_handler.go), and go test ./services/blockchain -run 'AvailableEvents|FSM' + ./services/asset/httpimpl -run FSM both pass.
Checks that hold up:
RenderClickableSpan's prop contract (text/onClick/className) matches the new call site, and it's an accessibility upgrade over the oldRenderSpan(role="button",tabindex, Enter-key handler).- The "endpoint is authoritative" claim is correct:
fsmEventsis sourced from the state-scopedGetFSMEventsand re-fetched on every state change, so dropping the client-sideisEventAllowedForStatefilter is sound. make([]string, 0)inAvailableEventsForStatepreserves the old[]-not-nullJSON shape.- Removing the global
documentclick listener also kills a DOM-order-dependent bug class and a lifecycle smell.
Minor, non-blocking:
- Response ordering for
RUNNINGchanged from[STOP, CATCHUPBLOCKS]to[CATCHUPBLOCKS, STOP](declaration order). Harmless for the dashboard (re-sorts byevent.value), but it is an observable/fsm/eventsordering change — fine assuming no consumer depends on order. - Buttons are now disabled only on
fsmLoading, so there's a narrow window where state updated but events haven't re-fetched, allowing a click on a now-invalid event. Backend rejects it (error toast), so blast radius is cosmetic. - Optional: add an assertion in the
httpimplFSM test thatGetFSMEventsreturns the expected set for at least one state, to lock the handler→AvailableEventsForStatedelegation (the unit test covers the helper, not the wiring).



Resolves the two remaining items of #982 (item 1, the duplicate
best_heightrenderCells key, was already fixed in #987).Item 2 — peers catchup button: row data instead of DOM-index traversal
The catchup "View" button used a global
documentclick listener that walkedtarget.closest('tr')→row.parentElement→indexOf(row)→data[rowIndex]to recover the peer. That couples the handler to the table's exact DOM shape and silently breaks under sort / paginate / virtualize (rendered row order ≠dataorder). The table renderCell already receives the rowitem, so the catchup cell now usesRenderClickableSpanwith anonClickthat captures the peer directly — order-independent, no global listener.Item 3 — FSM transitions: single source of truth
The blockchain FSM transition table was duplicated three ways: the real FSM (
services/blockchain/fsm.go), the asset/fsm/eventshandler's hardcodedswitch, and the dashboard'sisEventAllowedForState. Any backend transition change would silently desync the other two.services/blockchain/fsm.go: extracted the inlinefsm.Events{}to an exportedFSMTransitionsvar (used byNewFiniteStateMachine) + a pureAvailableEventsForState(state)helper.services/asset/httpimpl/fsm_handler.go:GetFSMEventsderives the list fromblockchain.AvailableEventsForState(...)instead of its own switch. Response shape unchanged ({events: [...]}); no proto/gRPC change.ui/dashboard/.../admin/+page.svelte: droppedisEventAllowedForState/getEventDisabledReason— the endpoint is already state-scoped, so the rendered events are authoritative.services/blockchain/fsm.gois now the single source; the other two derive from it.Verification
go build/go vetclean onservices/blockchain+services/asset.go test ./services/blockchain -run 'FSM|AvailableEvents'and./services/asset/httpimpl -run FSMpass (added a table-driven test forAvailableEventsForStatecovering all states + unknown).npm run check0 errors;npm run test:integration16/16 smoke green.Closes #982.