[WIP] feat(admin): Leaderboard — top earners per month + MSO comp - #145
Draft
hhff wants to merge 12 commits into
Draft
[WIP] feat(admin): Leaderboard — top earners per month + MSO comp#145hhff wants to merge 12 commits into
hhff wants to merge 12 commits into
Conversation
Adds an admin-only ActiveAdmin page listing the top X earners for each month, styled as a ledger. Earnings are the aggregate of a contributor's ContributorPayouts across all of their ledgers (a contributor has one ledger per enterprise), bucketed on invoice_passes.start_of_month to match the canonical dating used by ContributorPayout#effective_on_for_display. Trueups are excluded. A Trueup is what tops a leadership role up to the average of the top earners, so counting it would let the roles being topped up appear at the top of the board they are benchmarked against. Because Trueups live in their own table rather than as a type column on a shared ledger table, the exclusion is structural: we never sum them. X defaults to 5 and is overridable via ?limit=, clamped to 1..50 so a hostile or fat-fingered param cannot blow up the page. The global AuthorizationAdapter admits project leads to most pages, so the page additionally gates on current_admin_user.is_admin? in a before_action, and hides itself from the menu for non-admins. Verified against the restored production dataset: April 2026 reproduces the finance team's independently-maintained figures to the cent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nthly average Swaps the hand-rolled inline "ledger paper" styling for the same markup the Money (payable QBO bills) screen uses, so the two read as one system: a .title_bar per group with the heading on the left and a .pill.complete on the right, over a standard .index_table.index table with even/odd rows and .text-right amounts. The Arbre block becomes an ERB partial, matching how the other content-heavy register_pages (e.g. OKR Explorer) render. The right-hand pill now shows the AVERAGE earned across the listed top-N contributors rather than the month's total — this is the "average top-N earner" figure the leadership compensation model multiplies against. The total moves to a footer row on the table, so no information is lost. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…header copy - Each contributor name now links through to their contributor page via link_to admin_contributor_path, matching the Money screen. Entry already carries contributor_id, so this costs no extra queries. - The limit toggles become the same nag-pill tab bar the Money page uses, with .complete marking the active limit. - Removes the explanatory paragraph above the table. The reasoning for excluding Trueups lives in the code comments and the PR, not the chrome. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds an "Export CSV" action to the Leaderboard, rendered top-right in a .title_bar the way the Money page renders "Sync all to QBO". The export is a page_action, so it runs through the same require_admin! before_action as the page itself — a non-admin cannot pull the data by hitting the URL directly. Stacks::Leaderboard.to_csv emits one row per ranked contributor with the month's average and total repeated on each row, so the file pivots cleanly. Amounts are plain decimals rather than currency strings so spreadsheets read them as numbers. The export honors ?limit=, and the filename records both the limit and the export date. The page now sits under the Money tab and is hidden from the nav for anyone who isn't an admin. Menu visibility is presentation, not access control, so the before_action remains the actual gate; the menu proc uses &. so it stays falsy rather than raising for a logged-out visitor. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ActiveAdmin's `csv do ... end` DSL lives on ResourceDSL and needs an
ActiveRecord-backed collection, so a register_page can't use it (PageDSL
exposes only content/page_action/belongs_to). We get the same convention by
hand instead of inventing our own:
- The index action now responds to .csv, so the download lives at the
conventional /admin/leaderboard.csv?limit=N instead of a bespoke
/admin/leaderboard/export_csv page_action.
- The bespoke "Export CSV" action_item is replaced by the standard
#index_footer > .download_links markup ActiveAdmin renders under an index
table ("Download: CSV"), so it inherits existing styling and sits where
admins already look for it.
Access is unchanged in substance: require_admin! still guards every format.
Note that AA's own authorize_access! runs first and answers a non-HTML format
with 401 rather than a redirect, so the CSV test asserts the security property
(never 200, no rows emitted) rather than a specific status code.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Both are derivable from the exported rows, so repeating them on every line was denormalised noise in a spreadsheet. The CSV is now Month, Rank, Contributor, Earnings. The on-screen pill and table footer still show the average and total, where they're the point. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Placeholder section (body is TKTKTK) sitting between each month's heading and its table. Uses the same markup ActiveAdmin's Arbre `panel` builder emits — div.panel > h3 + div.panel_contents — so it inherits the standard panel styling from an ERB template, where the Arbre builder isn't available. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fills the MSO Compensation panel with the three leadership tiers, each paying the month's average top-N earner times its multiplier: Heads 0.8x, Chiefs 1x, Founders 1.2x. Tiers live in Stacks::Leaderboard::MSO_TIERS with a mso_compensation helper rather than being hardcoded in the view, so the multipliers are single-sourced and unit-testable. The multiplier label strips a trailing .0 so Chiefs reads "1x" rather than "1.0x". This is the other half of the Trueup exclusion: a Trueup is the payment that lifts one of these roles up to the figure computed here, so counting Trueups as earnings would feed the benchmark back into itself. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SVPs sit exactly halfway between Heads and Chiefs — (0.7 + 1.0) / 2 — which a test now asserts against the constant rather than the literal, so the midpoint relationship survives any future change to either neighbour. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Renames the top MSO tier and moves it to 1.15x so the ladder steps evenly: 0.7 / 0.85 / 1.0 / 1.15, a uniform +0.15 between neighbours. The step is named (MSO_TIER_STEP) and asserted across the whole ladder, so a future change to any single tier that breaks the even spacing fails loudly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a top-4 view alongside 3/5/10/20. Top-4 is the basis the original leadership compensation spreadsheet used, so this makes the page directly comparable to it without hand-editing the query string. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
hhff
marked this pull request as draft
July 28, 2026 20:14
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.
What
Adds a Leaderboard page under the Money tab (admins only): the top X earners for each month, styled as a native ActiveAdmin index. Above each month's table sits an MSO Compensation panel that turns that month's average top-X earner into what each leadership tier would be paid.
How earnings are defined
ContributorPayoutamounts across all of a contributor's ledgers. A contributor has oneLedgerperEnterprise, so payouts are grouped byledgers.contributor_id.invoice_passes.start_of_month, matching the canonical dating used byContributorPayout#effective_on_for_display(a payout has no date column of its own).ContributorPayoutisacts_as_paranoid, so soft-deleted rows are already excluded by its default scope.Verified against restored production data: April 2026 reproduces the finance team's independently-maintained figures to the cent (james $28,720.96, owen $25,537.20, andy $23,569.60, zachary $20,577.38).
MSO Compensation
Each tier is paid that month's average top-X earner × the tier multiplier. The ladder steps by a uniform +0.15 per rung:
The tiers live in
Stacks::Leaderboard::MSO_TIERS(with aMSO_TIER_STEPconstant), single-sourced so the view, labels, and math all follow one definition — renaming a tier or changing a multiplier is a one-line edit. A test asserts the ladder stays evenly stepped, so a future change that breaks the +0.15 spacing fails loudly.This is the other half of the Trueup exclusion: the Trueup is the payment that lifts one of these roles up to the figure computed here.
Behaviour
Xdefaults to 5;?limit=Noverrides it via pill toggles (3 / 4 / 5 / 10 / 20), clamped to1..50so a hostile or fat-fingered param can't blow up the page. Top-4 matches the basis of the original leadership-comp spreadsheet.Styling
Matches the Money (payable QBO bills) screen so the two read as one system —
.title_barheaders,.index_table.indextables,.pill.completebadges, and the standard#index_footer > .download_linksdownload row. The MSO panel reuses ActiveAdmin'sdiv.panel > h3 + div.panel_contentsmarkup (hand-written, since the Arbrepanelbuilder isn't available in an ERB partial).CSV export — the ActiveAdmin way
The index responds to
.csv, so the download lives at the conventional/admin/leaderboard.csv?limit=Nwith the standard "Download: CSV" footer link. (AA'scsv do…endDSL isResourceDSL-only and needs an AR collection, so aregister_pagecan't use it directly — this reproduces the same convention by hand.) Columns:Month, Rank, Contributor, Earnings.Access control
Nested under Money and hidden from the nav for non-admins. Menu visibility is presentation, not access control, so a
before_action :require_admin!is the real gate across every format — a non-admin can't pull the CSV by hitting the URL. (AA's ownauthorize_access!runs first and answers a non-HTML format with 401 rather than a redirect; the CSV test asserts the security property — never 200, no rows emitted — rather than a specific status.)Verification
test/lib/stacks/leaderboard_test.rb— ranking/limit, Trueup exclusion, cross-ledger aggregation, zero-earning omission, name formatting,sanitize_limitclamping, CSV (incl. comma-escaping), MSO tier math + even-ladder invariant.test/integration/admin_leaderboard_test.rb— renders,?limit=toggles, out-of-range/junk limits, contributor links, MSO panel,.csvdownload, non-admin block.test/integration+test/lib/stackssuites: 267 runs, 787 assertions, 0 failures.Not in this PR
The Google-Drive explainer spreadsheet (
MSO-Compensation-Model.xlsx) is a separate deliverable, not code.🤖 Generated with Claude Code