feat(gov): ops hero, activity, confidence signals, metadata chips (PR 6c)#9
feat(gov): ops hero, activity, confidence signals, metadata chips (PR 6c)#9
Conversation
…s (PR 6c)
Third UX pass on the governance surface. Closes the loop on situational
awareness: a signed-in voter can now see at a glance what they represent,
what their vault has already done, whether their next vote will count,
and which proposals deserve attention right now.
- GovernanceOpsHero + lib/governanceOps: represent / need-vote / passing
counts, progress bar, Jump-to-next-unvoted CTA.
- GovernanceActivity: last-10 receipts card with jump-to-row, backed by
the new GET /gov/receipts/recent endpoint (governanceService
.fetchRecentReceipts).
- Confidence signals:
* verified-chip on ProposalRow, only when the reconciler has fresh
(<5 min) confirmed receipts for this user on this proposal.
* Pre-submit support-shift preview in the vote modal (lib/
governanceSupportShift): net AbsoluteYesCount delta from selected
MNs vs. prior confirmed votes, with replacement count called out
in the detail line.
- Metadata chips (lib/governanceMeta): closing-soon / closing-urgent
tiers off the superblock voting deadline, over-budget rank cut
against the superblock budget, and margin-thin / margin-near within
±1.5% of the 10% pass line.
Made-with: Cursor
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 518e93acd6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| (Number(proposal.AbsoluteYesCount || 0) / enabled) * 100; | ||
| const delta = support - PASSING_SUPPORT_PERCENT; | ||
| if (Math.abs(delta) > MARGIN_WARNING_PERCENT) return null; | ||
| const above = delta >= 0; |
There was a problem hiding this comment.
Classify exactly-10% support as near-passing
The margin chip currently treats delta === 0 as the “above threshold” branch, so a proposal at exactly 10% support gets the margin-thin copy (“just above the 10% pass threshold”) even though the row’s pass logic uses support > 10 and still marks it as not passing. This creates contradictory status signals at the threshold boundary; use a strict delta > 0 check (or a dedicated exact-threshold label) so chip semantics match the actual pass condition.
Useful? React with 👍 / 👎.
| for (const p of proposals) { | ||
| if (p && typeof p.Key === 'string') { | ||
| m.set(p.Key.toLowerCase(), p); |
There was a problem hiding this comment.
Build activity jump lookup from visible proposals
The activity card’s jump affordance is keyed from proposalsByHash, but that map is built from the full proposals list while the table renders visibleProposals. When a user applies a search/filter that hides a proposal they previously voted on, the activity row still renders a jump button, but clicking it cannot scroll to any DOM row because that row is not mounted. This produces a silent no-op CTA in normal filtered views.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Thanks — this is re-flagging the same concern after the round-1 fix. I chose to keep the map built from the full proposals list on purpose and address the silent-no-op failure mode at the click site instead:
jumpToProposal (Governance.js ~420) now detects when the target hash exists in the feed but is hidden by the current filter/query, clears filter + query, and scrolls inside a requestAnimationFrame so React commits the filter reset before getElementById runs. The two tests in Governance.test.js under "jumpToProposal filter-aware behaviour" pin this contract: hidden target → filter clears and row mounts; visible target → filter is preserved (no silent reset).
Filtering the activity card's jump map to only currently-visible proposals is the alternative, but it's strictly worse UX in this context — activity should reflect the user's real voting history regardless of what view filter is currently applied, and hiding jump buttons for activity rows behind an unrelated filter was the original "silent dead-end" complaint in a different shape. Happy to revisit if there's a scenario I'm missing.
- marginChip: use strict delta>0 at the 10% line so exactly-10% support reads as "Close to passing" instead of "Slim margin" (ProposalRow's pass check is `support > 10`, so contradictory copy at the boundary was confusing). - jumpToProposal: when the target proposal exists in the feed but is hidden by the current search/filter, clear filter+query first so the row is mounted before we scroll. Scrolling runs inside requestAnimationFrame so React has a chance to commit the filter reset before getElementById fires. Prevents the silent no-op CTA on filtered activity rows. Made-with: Cursor
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3d90e7b640
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
|
||
| let running = 0; | ||
| for (const row of passing) { | ||
| running += Math.max(0, row.amount); |
There was a problem hiding this comment.
Guard over-budget accumulation against NaN amounts
In computeOverBudgetMap, a malformed payment_amount (for example a non-numeric string from the feed) makes row.amount become NaN, and running += Math.max(0, row.amount) then turns running into NaN. After that, running > ceiling is always false, so lower-ranked passing proposals stop getting over-budget chips even when they should. Coercing non-finite amounts to 0 before adding preserves correct cutline detection for the rest of the list.
Useful? React with 👍 / 👎.
Codex re-raised the "silent no-op jump CTA under an active filter" concern against the filter-clearing fix. The jumpToProposal callback already resets filter+query when the target proposal exists in the feed but is hidden by the current filter; this commit adds explicit coverage so the behaviour is a pinned contract and future readers don't re-litigate the approach: - hidden target → filter state clears, row becomes mountable - visible target → filter state is preserved (no silent reset) The stubs for GovernanceOpsHero / GovernanceActivity expose the onJumpToProposal prop via a test-only button so these page-level tests don't need the real activity fetch. Made-with: Cursor
|
@codex review |
…(Codex round 2) A malformed payment_amount (non-numeric string from the feed) coerces to NaN, which then contaminates the running budget total. Once `running` is NaN every `running > ceiling` check returns false, so the over-budget chip silently disappears from downstream rows that should get it. Clamp non-finite amounts to 0 before adding. Biases toward "no warning" on an unknowable row rather than warning spuriously, while keeping cutline detection working for the rest of the list. Made-with: Cursor
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4e1c4cad7a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| () => | ||
| closingChip({ | ||
| votingDeadline: superblockStats ? superblockStats.voting_deadline : 0, | ||
| }), | ||
| [superblockStats] |
There was a problem hiding this comment.
Recompute closing chip from current time
The closing metadata chip is memoized only on superblockStats, but closingChip() depends on Date.now(). After initial render, the label/kind can stay frozen for the whole session (for example, it never transitions from closing-soon to closing-urgent, and may remain visible after the deadline) unless the stats object itself is refreshed. This produces stale urgency signals for long-lived governance sessions.
Useful? React with 👍 / 👎.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 98277e2c22
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const closing = useMemo( | ||
| () => | ||
| closingChip({ | ||
| votingDeadline: superblockStats ? superblockStats.voting_deadline : 0, | ||
| }), |
There was a problem hiding this comment.
Recompute closing chip as wall-clock time advances
closingChip depends on Date.now(), but this useMemo only invalidates when superblockStats changes, so the “Closes in …” label and urgent/soon tier are frozen to the initial render in any long-lived tab. In practice, users can keep seeing stale countdowns (or miss escalation to urgent/expired) until a manual feed refresh occurs, which makes the new deadline signal inaccurate during normal use.
Useful? React with 👍 / 👎.
…ssions (Codex round 2) The closing-window chip (and the verified-on-chain freshness check) both depend on Date.now(), but the derivations were memoized only on the stats object. That means on a tab that stays open past the chip's boundary: - "closing-soon" (7d window) never escalated to "closing-urgent" at the 48h line; - the chip never disappeared once the deadline passed; - the verified-chip freshness window (5 min) could linger even after staleness. Fix: add a slow-ticking nowMs state at the page level (one update per minute), thread it into the closingChip memo and into ProposalRow so the verified pill uses the same clock. The tick interval is coarse on purpose — chip copy rounds to m/h/d so sub-minute drift is invisible, and 60 re-renders per hour on a quiet page is cheap. Any hard-timed behaviour (server-side deadline enforcement) is unaffected. Adds two tests that advance wall-clock + timers without touching the stats object: soon→urgent escalation, and chip vanishing after the deadline. Made-with: Cursor
|
@codex review |
|
Codex Review: Didn't find any major issues. Nice work! ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Summary
Third (and final) UX pass on the governance surface. PR 6a brought receipts-aware defaults; PR 6b added grouped buckets and exhaustive error copy. 6c closes the loop on situational awareness — a signed-in voter can now see, at a glance:
What changed
Companion PR
Backend: syscoin/sysnode-backend#` `gov-vote-analytics` — adds `GET /gov/receipts/recent`.
Tests
474 / 474 passing (33 suites). New suites cover:
Not in this PR
Test plan
Made with Cursor