Skip to content

fix(dashboard): peer catchup row-data lookup + FSM transitions single source (#982)#1015

Merged
oskarszoon merged 2 commits into
bsv-blockchain:mainfrom
oskarszoon:fix/dashboard-982-cleanup
Jun 4, 2026
Merged

fix(dashboard): peer catchup row-data lookup + FSM transitions single source (#982)#1015
oskarszoon merged 2 commits into
bsv-blockchain:mainfrom
oskarszoon:fix/dashboard-982-cleanup

Conversation

@oskarszoon

Copy link
Copy Markdown
Contributor

Resolves the two remaining items of #982 (item 1, the duplicate best_height renderCells 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 document click listener that walked target.closest('tr')row.parentElementindexOf(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 ≠ data order). The table renderCell already receives the row item, so the catchup cell now uses RenderClickableSpan with an onClick that 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/events handler's hardcoded switch, and the dashboard's isEventAllowedForState. Any backend transition change would silently desync the other two.

  • services/blockchain/fsm.go: extracted the inline fsm.Events{} to an exported FSMTransitions var (used by NewFiniteStateMachine) + a pure AvailableEventsForState(state) helper.
  • services/asset/httpimpl/fsm_handler.go: GetFSMEvents derives the list from blockchain.AvailableEventsForState(...) instead of its own switch. Response shape unchanged ({events: [...]}); no proto/gRPC change.
  • ui/dashboard/.../admin/+page.svelte: dropped isEventAllowedForState / getEventDisabledReason — the endpoint is already state-scoped, so the rendered events are authoritative.

services/blockchain/fsm.go is now the single source; the other two derive from it.

Verification

  • go build / go vet clean on services/blockchain + services/asset.
  • go test ./services/blockchain -run 'FSM|AvailableEvents' and ./services/asset/httpimpl -run FSM pass (added a table-driven test for AvailableEventsForState covering all states + unknown).
  • Dashboard: npm run check 0 errors; npm run test:integration 16/16 smoke green.

Closes #982.

…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.
@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

🤖 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:

  • Replaces fragile DOM-index traversal with direct row data capture via closure
  • Uses proper RenderClickableSpan component with accessibility support
  • Removes global event listener dependency

Item 3 — FSM single source of truth:

  • Extracts FSM transitions to FSMTransitions variable in services/blockchain/fsm.go
  • Adds AvailableEventsForState() helper that derives valid events from the transition table
  • Updates services/asset/httpimpl/fsm_handler.go to use the helper (eliminates hardcoded switch)
  • Removes duplicate logic from dashboard admin page
  • Includes comprehensive test coverage for all states
  • Transition table matches docs/state-machine.diagram.md

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.

@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Benchmark Comparison Report

Baseline: main (unknown)

Current: PR-1015 (b58eda1)

Summary

  • Regressions: 0
  • Improvements: 0
  • Unchanged: 144
  • Significance level: p < 0.05
All benchmark results (sec/op)
Benchmark Baseline Current Change p-value
_NewBlockFromBytes-4 1.740µ 1.740µ ~ 1.000
SplitSyncedParentMap_SetIfNotExists/256_buckets-4 61.64n 61.70n ~ 1.000
SplitSyncedParentMap_SetIfNotExists/16_buckets-4 61.66n 61.89n ~ 0.200
SplitSyncedParentMap_SetIfNotExists/1_bucket-4 61.77n 61.75n ~ 0.600
SplitSyncedParentMap_ConcurrentSetIfNotExists/256_buckets... 30.16n 30.79n ~ 0.100
SplitSyncedParentMap_ConcurrentSetIfNotExists/16_buckets_... 52.55n 53.87n ~ 0.400
SplitSyncedParentMap_ConcurrentSetIfNotExists/1_bucket_pa... 105.5n 107.7n ~ 0.200
MiningCandidate_Stringify_Short-4 261.5n 263.4n ~ 0.700
MiningCandidate_Stringify_Long-4 1.935µ 1.920µ ~ 0.300
MiningSolution_Stringify-4 985.1n 978.4n ~ 0.100
BlockInfo_MarshalJSON-4 1.805µ 1.781µ ~ 0.100
NewFromBytes-4 126.8n 129.2n ~ 0.100
AddTxBatchColumnar_Validation-4 2.505µ 2.514µ ~ 0.400
OffsetValidationLoop-4 546.8n 546.4n ~ 1.000
Mine_EasyDifficulty-4 61.10µ 61.11µ ~ 0.700
Mine_WithAddress-4 6.870µ 7.616µ ~ 0.100
BlockAssembler_AddTx-4 0.02689n 0.02599n ~ 1.000
AddNode-4 10.66 10.69 ~ 0.700
AddNodeWithMap-4 11.52 11.37 ~ 0.200
DiskTxMap_SetIfNotExists-4 3.655µ 3.311µ ~ 0.400
DiskTxMap_SetIfNotExists_Parallel-4 3.405µ 3.230µ ~ 0.100
DiskTxMap_ExistenceOnly-4 410.1n 299.4n ~ 0.100
Queue-4 191.9n 185.7n ~ 0.100
AtomicPointer-4 4.144n 5.015n ~ 0.100
ReorgOptimizations/DedupFilterPipeline/Old/10K-4 881.2µ 854.3µ ~ 0.100
ReorgOptimizations/DedupFilterPipeline/New/10K-4 823.7µ 775.4µ ~ 0.100
ReorgOptimizations/AllMarkFalse/Old/10K-4 111.0µ 119.5µ ~ 0.100
ReorgOptimizations/AllMarkFalse/New/10K-4 62.15µ 61.82µ ~ 0.400
ReorgOptimizations/HashSlicePool/Old/10K-4 61.04µ 54.07µ ~ 0.100
ReorgOptimizations/HashSlicePool/New/10K-4 11.83µ 11.54µ ~ 0.700
ReorgOptimizations/NodeFlags/Old/10K-4 5.284µ 4.544µ ~ 0.100
ReorgOptimizations/NodeFlags/New/10K-4 1.562µ 1.545µ ~ 0.700
ReorgOptimizations/DedupFilterPipeline/Old/100K-4 9.202m 9.381m ~ 1.000
ReorgOptimizations/DedupFilterPipeline/New/100K-4 9.481m 9.361m ~ 1.000
ReorgOptimizations/AllMarkFalse/Old/100K-4 1.042m 1.159m ~ 0.100
ReorgOptimizations/AllMarkFalse/New/100K-4 676.7µ 686.3µ ~ 0.700
ReorgOptimizations/HashSlicePool/Old/100K-4 606.8µ 610.5µ ~ 0.400
ReorgOptimizations/HashSlicePool/New/100K-4 299.9µ 301.8µ ~ 1.000
ReorgOptimizations/NodeFlags/Old/100K-4 48.37µ 48.85µ ~ 0.700
ReorgOptimizations/NodeFlags/New/100K-4 16.55µ 17.63µ ~ 0.100
TxMapSetIfNotExists-4 52.28n 52.45n ~ 0.700
TxMapSetIfNotExistsDuplicate-4 39.87n 40.55n ~ 0.100
ChannelSendReceive-4 583.5n 620.2n ~ 0.100
DirectSubtreeAdd/4_per_subtree-4 57.30n 59.55n ~ 0.400
DirectSubtreeAdd/64_per_subtree-4 29.09n 30.28n ~ 0.100
DirectSubtreeAdd/256_per_subtree-4 27.99n 28.57n ~ 0.400
DirectSubtreeAdd/1024_per_subtree-4 26.66n 26.91n ~ 0.200
DirectSubtreeAdd/2048_per_subtree-4 26.20n 26.36n ~ 0.100
SubtreeProcessorAdd/4_per_subtree-4 290.7n 333.8n ~ 0.100
SubtreeProcessorAdd/64_per_subtree-4 297.5n 333.3n ~ 0.100
SubtreeProcessorAdd/256_per_subtree-4 293.0n 307.8n ~ 0.100
SubtreeProcessorAdd/1024_per_subtree-4 281.7n 297.8n ~ 0.100
SubtreeProcessorAdd/2048_per_subtree-4 281.7n 295.3n ~ 0.100
SubtreeProcessorRotate/4_per_subtree-4 285.1n 306.5n ~ 0.100
SubtreeProcessorRotate/64_per_subtree-4 278.1n 295.9n ~ 0.100
SubtreeProcessorRotate/256_per_subtree-4 281.1n 292.4n ~ 0.100
SubtreeProcessorRotate/1024_per_subtree-4 278.8n 289.8n ~ 0.100
SubtreeNodeAddOnly/4_per_subtree-4 55.19n 55.29n ~ 0.500
SubtreeNodeAddOnly/64_per_subtree-4 36.20n 36.25n ~ 0.700
SubtreeNodeAddOnly/256_per_subtree-4 35.19n 35.18n ~ 1.000
SubtreeNodeAddOnly/1024_per_subtree-4 34.59n 34.66n ~ 0.100
SubtreeCreationOnly/4_per_subtree-4 120.6n 113.4n ~ 0.100
SubtreeCreationOnly/64_per_subtree-4 361.4n 385.2n ~ 0.200
SubtreeCreationOnly/256_per_subtree-4 1.276µ 1.274µ ~ 1.000
SubtreeCreationOnly/1024_per_subtree-4 3.991µ 3.895µ ~ 0.700
SubtreeCreationOnly/2048_per_subtree-4 7.275µ 7.155µ ~ 0.700
SubtreeProcessorOverheadBreakdown/64_per_subtree-4 277.2n 287.1n ~ 0.100
SubtreeProcessorOverheadBreakdown/1024_per_subtree-4 277.4n 283.6n ~ 0.100
ParallelGetAndSetIfNotExists/1k_nodes-4 1.999m 2.005m ~ 0.700
ParallelGetAndSetIfNotExists/10k_nodes-4 5.142m 5.220m ~ 0.200
ParallelGetAndSetIfNotExists/50k_nodes-4 7.348m 7.426m ~ 0.400
ParallelGetAndSetIfNotExists/100k_nodes-4 10.62m 10.38m ~ 0.200
SequentialGetAndSetIfNotExists/1k_nodes-4 1.807m 1.773m ~ 0.100
SequentialGetAndSetIfNotExists/10k_nodes-4 4.972m 4.426m ~ 0.100
SequentialGetAndSetIfNotExists/50k_nodes-4 14.60m 13.82m ~ 0.100
SequentialGetAndSetIfNotExists/100k_nodes-4 30.20m 25.32m ~ 0.100
ProcessOwnBlockSubtreeNodesParallel/1k_nodes-4 2.119m 2.092m ~ 0.400
ProcessOwnBlockSubtreeNodesParallel/10k_nodes-4 8.576m 8.588m ~ 1.000
ProcessOwnBlockSubtreeNodesParallel/100k_nodes-4 14.48m 13.23m ~ 0.100
ProcessOwnBlockSubtreeNodesSequential/1k_nodes-4 1.844m 1.807m ~ 0.400
ProcessOwnBlockSubtreeNodesSequential/10k_nodes-4 8.978m 8.126m ~ 0.100
ProcessOwnBlockSubtreeNodesSequential/100k_nodes-4 45.37m 43.71m ~ 0.100
CalcBlockWork-4 481.3n 474.4n ~ 0.200
CalculateWork-4 688.9n 653.2n ~ 1.000
BuildBlockLocatorString_Helpers/Size_10-4 1.402µ 1.394µ ~ 0.700
BuildBlockLocatorString_Helpers/Size_100-4 13.46µ 13.29µ ~ 1.000
BuildBlockLocatorString_Helpers/Size_1000-4 160.5µ 168.2µ ~ 0.400
CatchupWithHeaderCache-4 104.5m 104.6m ~ 1.000
_BufferPoolAllocation/16KB-4 4.159µ 4.084µ ~ 0.400
_BufferPoolAllocation/32KB-4 9.379µ 10.046µ ~ 0.100
_BufferPoolAllocation/64KB-4 22.16µ 21.95µ ~ 0.700
_BufferPoolAllocation/128KB-4 33.18µ 33.53µ ~ 1.000
_BufferPoolAllocation/512KB-4 131.4µ 116.2µ ~ 0.100
_BufferPoolConcurrent/32KB-4 19.48µ 20.25µ ~ 0.700
_BufferPoolConcurrent/64KB-4 31.02µ 31.62µ ~ 0.700
_BufferPoolConcurrent/512KB-4 148.4µ 148.5µ ~ 1.000
_SubtreeDeserializationWithBufferSizes/16KB-4 678.8µ 618.3µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/32KB-4 669.0µ 637.1µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/64KB-4 664.7µ 634.1µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/128KB-4 668.1µ 638.3µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/512KB-4 627.5µ 605.6µ ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/16KB-4 36.45m 36.15m ~ 0.200
_SubtreeDataDeserializationWithBufferSizes/32KB-4 36.08m 36.20m ~ 0.400
_SubtreeDataDeserializationWithBufferSizes/64KB-4 36.09m 36.03m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/128KB-4 35.87m 36.12m ~ 0.700
_SubtreeDataDeserializationWithBufferSizes/512KB-4 35.66m 36.08m ~ 0.100
_PooledVsNonPooled/Pooled-4 738.3n 739.2n ~ 0.800
_PooledVsNonPooled/NonPooled-4 8.726µ 9.003µ ~ 0.200
_MemoryFootprint/Current_512KB_32concurrent-4 6.302µ 6.591µ ~ 0.100
_MemoryFootprint/Proposed_32KB_32concurrent-4 9.438µ 10.550µ ~ 0.100
_MemoryFootprint/Alternative_64KB_32concurrent-4 8.948µ 9.646µ ~ 0.100
_prepareTxsPerLevel-4 415.2m 423.7m ~ 0.700
_prepareTxsPerLevelOrdered-4 4.077m 3.513m ~ 0.100
_prepareTxsPerLevel_Comparison/Original-4 415.1m 411.0m ~ 0.700
_prepareTxsPerLevel_Comparison/Optimized-4 3.930m 3.411m ~ 0.100
SubtreeSizes/10k_tx_4_per_subtree-4 1.295m 1.347m ~ 0.400
SubtreeSizes/10k_tx_16_per_subtree-4 309.6µ 312.5µ ~ 1.000
SubtreeSizes/10k_tx_64_per_subtree-4 73.62µ 74.26µ ~ 0.100
SubtreeSizes/10k_tx_256_per_subtree-4 18.40µ 18.49µ ~ 0.400
SubtreeSizes/10k_tx_512_per_subtree-4 9.126µ 9.116µ ~ 0.400
SubtreeSizes/10k_tx_1024_per_subtree-4 4.580µ 4.550µ ~ 0.400
SubtreeSizes/10k_tx_2k_per_subtree-4 2.252µ 2.245µ ~ 0.300
BlockSizeScaling/10k_tx_64_per_subtree-4 71.62µ 71.50µ ~ 0.700
BlockSizeScaling/10k_tx_256_per_subtree-4 18.20µ 18.14µ ~ 0.700
BlockSizeScaling/10k_tx_1024_per_subtree-4 4.442µ 4.486µ ~ 0.400
BlockSizeScaling/50k_tx_64_per_subtree-4 381.1µ 377.2µ ~ 0.400
BlockSizeScaling/50k_tx_256_per_subtree-4 89.99µ 89.18µ ~ 0.400
BlockSizeScaling/50k_tx_1024_per_subtree-4 22.02µ 22.17µ ~ 1.000
SubtreeAllocations/small_subtrees_exists_check-4 154.2µ 152.3µ ~ 0.700
SubtreeAllocations/small_subtrees_data_fetch-4 162.3µ 162.8µ ~ 0.700
SubtreeAllocations/small_subtrees_full_validation-4 314.9µ 313.7µ ~ 1.000
SubtreeAllocations/medium_subtrees_exists_check-4 9.157µ 9.003µ ~ 0.700
SubtreeAllocations/medium_subtrees_data_fetch-4 9.553µ 9.507µ ~ 0.700
SubtreeAllocations/medium_subtrees_full_validation-4 17.94µ 17.69µ ~ 0.700
SubtreeAllocations/large_subtrees_exists_check-4 2.108µ 2.101µ ~ 1.000
SubtreeAllocations/large_subtrees_data_fetch-4 2.281µ 2.256µ ~ 0.100
SubtreeAllocations/large_subtrees_full_validation-4 4.482µ 4.425µ ~ 0.400
StoreBlock_Sequential/BelowCSVHeight-4 318.2µ 314.0µ ~ 0.400
StoreBlock_Sequential/AboveCSVHeight-4 311.2µ 312.8µ ~ 0.400
GetUtxoHashes-4 284.3n 290.0n ~ 0.700
GetUtxoHashes_ManyOutputs-4 48.53µ 46.33µ ~ 0.100
_NewMetaDataFromBytes-4 213.1n 214.9n ~ 0.200
_Bytes-4 405.1n 398.7n ~ 0.700
_MetaBytes-4 137.4n 139.0n ~ 0.600

Threshold: >10% with p < 0.05 | Generated: 2026-06-02 09:53 UTC

@sonarqubecloud

sonarqubecloud Bot commented Jun 2, 2026

Copy link
Copy Markdown

@ordishs ordishs left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 old RenderSpan (role="button", tabindex, Enter-key handler).
  • The "endpoint is authoritative" claim is correct: fsmEvents is sourced from the state-scoped GetFSMEvents and re-fetched on every state change, so dropping the client-side isEventAllowedForState filter is sound.
  • make([]string, 0) in AvailableEventsForState preserves the old []-not-null JSON shape.
  • Removing the global document click listener also kills a DOM-order-dependent bug class and a lifecycle smell.

Minor, non-blocking:

  • Response ordering for RUNNING changed from [STOP, CATCHUPBLOCKS] to [CATCHUPBLOCKS, STOP] (declaration order). Harmless for the dashboard (re-sorts by event.value), but it is an observable /fsm/events ordering 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 httpimpl FSM test that GetFSMEvents returns the expected set for at least one state, to lock the handler→AvailableEventsForState delegation (the unit test covers the helper, not the wiring).

@oskarszoon oskarszoon merged commit bd84c66 into bsv-blockchain:main Jun 4, 2026
35 checks passed
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.

dashboard: pre-existing cleanup surfaced during #981 smoke-harness review

3 participants