feat(collective-rewards): rebuild the page as a cycle-driven financial dashboard - #2181
feat(collective-rewards): rebuild the page as a cycle-driven financial dashboard#2181sleyter93 wants to merge 4 commits into
Conversation
…l dashboard Replaces the single "rewards at work" section and its dual-axis chart with a dashboard organised around one idea: every figure on the page describes a cycle, and the cycle is selectable. Selecting a row in the history table — or a point on the chart — reloads the backing figure, the distribution panel and the header tiles together. That is the reason for the refactor: the old components each fetched "the current cycle" independently and had no way to describe any other one. Sections: - DashboardMetrics: the ABI figure alongside total backing, all-time distribution with this cycle's delta, participants, and cycle progress. - CycleBackingChart: total stRIF backing indexed by cycle rather than by month, with a range toggle and a band marking the selected cycle. - CycleDistribution: the selected cycle's payout split by token and by role, with per-Backer and per-Builder averages. - CycleHistoryTable: one row per cycle, selectable by mouse or keyboard. - PositionSimulator and the two calls to action. useGetCycleHistory folds four sources into one row per cycle. Only cycles and daily allocations gate loading; the NotifyReward events and the per-cycle Backer counts feed two columns that degrade to a dash on their own, so a slow gauge fetch no longer holds up a table that can already render most of itself. The Backer/Builder split is weighted by fiat value, not token count — a cycle paying 1 rBTC to Backers and 1,000 RIF to Builders is 99.9/0.1, not 50/50. /api/cycles/backers is new. AllocationHistory records allocation changes rather than balances, so counting Backers per cycle needs a running total: a Backer who allocated in cycle 30 and never touched it again is still backing in cycle 46. Its tests cover the query's shape, not its results — that still needs one run against a populated indexer. USD figures for past cycles are valued at spot prices, since no price history is kept. The distribution panel says so rather than implying the numbers are fixed. Also: - ComparativeProgressBar gained a segmented mode and an aria-label, and no longer computes a NaN width when every value is zero. - The 26-cycle year in the ABI formula is now a shared constant, so the simulator's projection cannot drift from the published ABI. Deliberately excluded, per product decision: the ABI boost mechanic (no such mechanic exists on-chain), and the Builders and ABI columns in the history table (no historical data for either).
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
The history table promised that picking a row loads it "above", but only the chart and the distribution panel listened. The metric tiles read cycles[0] and never moved, so selecting cycle 43 left the header still describing cycle 46. Selection now lives in CycleDashboardProvider, above every section. The tiles relabel themselves as they follow it — "paid this cycle" becomes "paid in cycle 43", the day counter becomes the cycle's date window, and the Builder count drops, because today's roster cannot describe a settled cycle. That also means the per-cycle fold runs once for the page instead of once in each of the three components that were calling the hook. Accessibility: - The table was a half-built ARIA grid: role="grid" with every row tabbable and no arrow keys, costing ten tab stops to pass. It now uses a roving tabindex with up/down navigation, and focus draws a visible outline rather than only tinting the background. - The chart range and the simulator presets were rows of aria-pressed buttons, which announces independent toggles for what is a single choice. Both now use a shared SegmentedControl built as a radio group. - Sections carry headings for the document outline even where the design shows none, so the page is navigable by landmark. States: - The distribution column held its width open as an empty 360px gap while the chart loaded beside it. It now shows a placeholder. - An empty history collapsed the chart, the panel and the table into one line of text. Each keeps its own empty state. The spot-price caveat applied to every USD figure on the page but appeared only inside the distribution panel. It now sits once under the section it describes, with the all-time figure carrying it as a tooltip. Builders in the split bar were grey, which reads as disabled and implied they were the lesser half. Two tints of one purple were tried and measured 2.45:1 against each other on the dark card; white against the brand purple measures 3.23:1, with both segments clearing 4.9:1 against the card behind them.
UX review passReviewed the implementation against the design for coherence and pushed the fixes that didn't need a product call. Two findings were left open deliberately — see the end. The page now actually follows the selected cycleThe table promised that picking a row loads it "above", but only the chart and the panel listened; the metric tiles read Selection moved into
The Builder count drops on a settled cycle because today's roster cannot describe a past one. As a side effect the per-cycle fold runs once for the page instead of once in each of the three components that were calling the hook. Accessibility
States
Copy and colourThe spot-price caveat applied to every USD figure on the page but appeared only inside the distribution panel. It now sits once under the section it describes, with the all-time figure carrying it as a tooltip. Builders in the split bar were grey, which reads as disabled and implied they were the lesser half. I tried two tints of one purple and measured 2.45:1 between them on the dark card; white against the brand purple measures 3.23:1, with both segments clearing 4.9:1 against the card. Every bar is also labelled with its percentages, so the split is never carried by colour alone. Left open — these change the design, so they need a call
Both come from the design rather than the implementation, which is why I flagged them instead of changing them. |
Compared the page against the design prototype and closed the gaps that carried the most visual weight. Surfaces: 28px corner radius across the five cards, held in one constant so they cannot drift apart, and 16px gutters in place of 8px. History table: status is a bordered pill instead of loose text, the panel header sits above a rule, and a selected row is a filled row with a short accent bar rather than an outline. The outline had to be assembled per cell — borders on a `<tr>` do not render reliably — which forced `border-separate`, a transparent border on every cell to stop the rows shifting, and corners split across two cells. A background needs none of that. Chart: no gridlines, no visible value axis and no shaded band. The value axis is hidden rather than deleted, because the scale still has to be declared — dropping the element would hand the domain to recharts' defaults and silently change what the filled area means. Its tooltip now writes the figure out in full, since with the axis gone that is the only place the exact number appears. The chart card stretches to the height of the distribution panel beside it, so the row no longer ends ragged. Split bar: RIF blue for Backers, primary orange for Builders. This pairing was avoided before because the distribution panel used coloured dots for USDRIF and rBTC, so the same two hues a few lines above would each have meant two things. Those dots are now the tokens' own logos, which frees the colours. Backing figures use a fixed single decimal. `millify` keeps up to three and drops trailing zeros, so real data rendered `27.59M` directly above `27.422M` and the column read as noise; the fixtures had hidden it by being round.
The newest cycle is not always complete. If its NotifyReward events never landed, its Backer/Builder split is a dash — and since that cycle is what the page loads first, the split is missing from the row a visitor sees before they touch anything. Against the indexed mainnet data this is not hypothetical: cycle 44 is settled and has no split. Fall through to the newest cycle that can answer, leaving explicit selection untouched. A running cycle is exempt. It legitimately has no split for the first hours of its life, and it is the cycle the page is built around, so skipping it would hide the live one behind stale figures.
What
Rebuilds
/collective-rewardsaround one idea: every figure on the page describes a cycle, and the cycle is selectable. Picking a row in the history table — or a point on the chart — reloads the backing figure, the distribution panel and the header tiles together.That selection is the reason for the refactor, not a nicety. The old components each fetched "the current cycle" on their own and had no way to describe any other one.
Sections
DashboardMetricsCycleBackingChartCycleDistributionCycleHistoryTablePositionSimulator+ CTAsEach presentational half has a Storybook story, following the Content/Loader split the repo already uses.
Data
useGetCycleHistoryfolds four sources into one row per cycle. Only cycles and daily allocations gate loading — the NotifyReward events and the per-cycle Backer counts feed two columns that degrade to a dash on their own, so a slow gauge fetch no longer holds up a table that can already render most of itself.The Backer/Builder split reuses events the page was already downloading; no new fetch. It is weighted by fiat value rather than token count, so a cycle paying 1 rBTC to Backers and 1,000 RIF to Builders reads 99.9/0.1 instead of 50/50.
/api/cycles/backersis new.AllocationHistoryrecords allocation changes, not balances, so counting Backers per cycle needs a running total: a Backer who allocated in cycle 30 and never touched it again is still backing in cycle 46.Design alignment
The page was compared against the design prototype and the gaps closed, in rough order of visual weight:
<tr>do not render reliably; that forcedborder-separate, a transparent border on every cell to stop the rows shifting, and corners split across two cells. A background needs none of it, so that scaffolding is gone.Those last two are linked. Blue and orange were avoided at first because the coloured dots already used them for USDRIF and rBTC, so the same two hues a few lines apart would each have meant two things. Replacing the dots with logos freed the colours.
Reviewer notes
/api/cycles/backersreturns plausible per-cycle counts that climb over time (186 → 305), and the whole page renders against it. The earlier caveat on this PR no longer applies.millifykeeps up to three and drops trailing zeros, so real data rendered27.59Mdirectly above27.422Mand the column read as noise. The fixtures had hidden this by being round numbers.formatBackingCompactand the default-cycle fallback are both covered only by eye. Happy to add them if you would rather they were pinned.CollectiveRewardsChart,CallToActionSectionand everything below it,TotalRewardsDistributed,CycleMetrics, and the collective-rewards copies ofTotalBacking/EstimatedRewards/ActiveBuilders/CountMetric. Roughly 20 files and 13 stories. Left in place deliberately so the new dashboard can be validated first; happy to remove in a follow-up.AnnualBackersIncentivesis not in that list —my-rewardsstill uses it.Excluded by product decision
+1.2%, and the 5%/10% split). No such mechanic exists on-chain.Still open against the design
Also
ComparativeProgressBargained a segmented mode and anaria-label, and no longer computes aNaNwidth when every value is zero. It had no production call sites before this.Verification
tscandeslintclean. 125 tests pass, 27 of them new (18 onbuildCycleHistory, 9 on the query).The full page now renders against the local indexer snapshot with live CoinMarketCap prices, and row selection was confirmed end to end: clicking a row moves the chart header, the distribution panel and all five tiles to that cycle together. Chart and distribution cards were measured at equal height. Every component also checked in Storybook.
The simulator reproduces the design's figures exactly — 1,000,000 stRIF at 5% gives 50,000 RIF a year, $4,005, 1,923 per cycle — which is a useful check that the ABI formula and the cycle divisor line up.
Not covered: the running-cycle state. Every cycle in the snapshot is settled (the newest closed 9 Jul), so the "Day X of Y" progress bar, the live-cycle Builder count and the
runningstatus pill were exercised only in Storybook, never against real data.