Conversation
After submitting a vote, the cohort chip and "Last N votes" activity
badge stayed on "Pending / Submitted" indefinitely. `/gov/receipts/summary`
is a pure SELECT, and the only code path that flips receipts from
`relayed` to `confirmed` is `POST /gov/receipts/reconcile`, which was
only fired by `useOwnedMasternodes` when the vote modal opened. Users
had to navigate into a proposal (reopening its modal) to trigger a
reconcile, then come back to the Governance page before the UI caught
up with chain state.
The 30s background poll in `useGovernanceReceipts` now runs a reconcile
sweep before each summary refetch: it scans the last-known summary for
rows with `relayed > 0`, fans out `reconcileReceipts(hash)` in parallel
(allSettled so one flaky proposal can't stall the sweep), and then calls
`load()` to surface any transitions. The callback reads `summary` via
a ref so its identity stays stable and `useBackgroundPoll` doesn't
reset its cadence on every state change.
RPC cost is bounded by two existing backend layers:
* Route-level freshness check short-circuits reconcile with
`reconciled: false` (no RPC) when every row for a proposal is
already confirmed inside `receiptsFreshnessMs`.
* `currentVotesCache` (2 min TTL) collapses concurrent
`gobject_getcurrentvotes` calls across users to one RPC per
proposal per TTL window.
* `POST /gov/vote` already invalidates that cache for the just-voted
proposal, so the first reconcile after a relay hits the chain
cleanly instead of returning a stale snapshot.
Manual `refresh()` stays pure — the modal-open reconcile already covers
the just-voted proposal, so closeVoteModal doesn't need to fan out
again. Feature-detected: services without `reconcileReceipts` degrade
to a plain load so legacy test fixtures keep passing.
Covered by 5 new tests: reconcile fires for relayed>0 rows and surfaces
transitions, sweep is a no-op when nothing is pending, one failed
reconcile doesn't block the rest, enabled=false gates everything,
legacy service shape degrades gracefully. Plus a guard that manual
refresh() stays pure.
Made-with: Cursor
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.
Problem
After submitting a vote, the per-proposal cohort chip (e.g.
Voted 1/10) and the "Last N votes" activity-card badges sat on their pre-confirm state (Pending/Submitted) indefinitely. The user had to navigate into a proposal (which reopened its vote modal and implicitly triggered a reconcile viauseOwnedMasternodes) and then come back to the Governance page before anyrelayed → confirmedtransition surfaced.Root cause:
GET /gov/receipts/summaryis documented as a pure SELECT — no RPC, no reconciliation — andPOST /gov/receipts/reconcile(the only path that flips receipts fromrelayedtoconfirmedviagobject_getcurrentvotes) was only fired when the modal opened. The 30s Governance background poll only refetched the pure-read summary, so stalerelayedrows never got touched.Fix
The background poll in
useGovernanceReceiptsnow runs a reconcile sweep before each summary refetch:useBackgroundPolldoesn't reset its cadence on every state change) for proposals withrelayed > 0.governanceService.reconcileReceipts(hash)in parallel.Promise.allSettledso one flaky proposal can't stall the sweep.load()to surface any transitions the reconciles just wrote.RPC cost
Bounded by three existing backend layers, no new server-side work needed:
/gov/receipts/reconcileshort-circuits withreconciled: false(no RPC) when every row for a proposal is already confirmed insidereceiptsFreshnessMs.currentVotesCache(2 min TTL) collapses concurrentgobject_getcurrentvotescalls across all logged-in users to one RPC per proposal per TTL window.POST /gov/votealready invalidates that cache for the just-voted proposal, so the first post-relay reconcile hits the chain cleanly instead of returning a stale snapshot.What stays the same
/gov/receipts/summarystays pure — only the client sweep issues reconciles.refresh()(called byGovernance.closeVoteModal) stays pure.useOwnedMasternodesalready reconciled the just-voted proposal when the modal opened, so closeVoteModal doesn't need to fan out again. Guarded by a dedicated test.GovernanceActivity's independent 30s poll is untouched — it reads the same DB the sweep just updated, so badges flip on its next tick.reconcileReceiptsdegrade to a plain load (legacy test fixtures keep passing — dedicated test).Test plan
5 new tests added in
useGovernanceReceipts.test.js:relayed > 0and re-loads/summaryto surfacerelayed → confirmedtransitions.relayedrows skips reconcile entirely and only re-loads/summary.enabled=falsesuppresses the background sweep so anonymous / gated-off sessions never issue reconcile RPC.reconcileReceiptsstill polls/summarycleanly (back-compat).Plus one guard on the existing surface:
refresh()does NOT fire reconcile — reconciliation belongs only to the background cadence so modal-close doesn't double up on the RPC the vote modal already issued.Verified locally
useGovernanceReceipts.test.js— 17/17 pass.Governance.test.js,GovernanceOpsHero.test.js,GovernanceActivity.test.js,ProposalVoteModal.test.js— 105/105 pass combined across all governance-related suites.After deploy
Submit a vote, leave the Governance page open. Within ~30s the cohort chip on that proposal row should flip from
PendingtoVoted X/Y, and the activity card should flip its badge fromSubmittedtoOn-chain— no page reload, no navigation required.Made with Cursor