feat: redesign repositories miner insights#154
Conversation
Replaces the monolithic page with an extracted component/util layout (_components/, _lib/) and adds /api/repos/metadata and /api/sn74-emission to back the new card / drawer / market surfaces. Adds the optional 30d stats fields on GtRepo and the closeOnScroll prop on Dropdown that the new page depends on.
Replace the unclear "subnet" labels with explicit wording: drawer formula reads "TAO (total miners emission)" and the card chip reads "of total emission" to pair with the existing "of pool" chip. Also runs the drawer formula's subnetTAO through formatTAO so it doesn't print 14 decimals.
GitHub's `issues.listForRepo` returns issues *and* PRs combined; with per_page=100, PRs were exhausting the budget on any active repo and older issue creates were silently truncated (e.g. ragflow: ~29 of 240 real issues visible, gittensor: ~31 of 175). Switch to `search.issuesAndPullRequests` with `is:issue created:>=DATE` so PRs are filtered server-side, paginate up to the 1000-row search cap, and route through withRotation's search-quota lane (30/min per PAT, tracked separately from the 5000/h core quota). Also bumps PER_REPO_TIMEOUT_MS to 30s so a paginated fetch + one rate- limit cooldown doesn't fall back to stale cache, and surfaces incomplete_results from the search API when GitHub's index is partial.
- RepoCard / RepoListRow: clickable wrappers were <div onClick> with no keyboard path; add role=button, tabIndex, and Enter/Space handlers. Embedded compare button is excluded from the row handler so it doesn't double-fire. - Palette: add ArrowUp/Down + Enter result navigation with an active highlight, scroll-into-view, and mouse-hover sync. Was previously Escape-only — keyboard users couldn't pick a result. - Dropdown: introduce 'xsmall' (12px font, 24px height) and revert 'small' to the legacy 14px font so existing callers (Pagination, ReposTable) keep their look. The repositories page sort picker opts into xsmall for its row of compact controls.
Adds `isEligible` and `uid` to RepoMiner so the repositories page can
tell which contributors actually earn from a given repo's emission
pool (vs. historical contributors who don't currently pass the
validator's per-repo credibility / token-score gate).
The route now joins three upstream sources:
* /miners — global roster (uid, totalScore)
* /prs — historical merged PRs (for ossContributions score)
* /repos/{full}/miners — per-repo RepoEvaluation rows (isEligible, repo
credibility, base/total/collateral scores)
Per-repo eligibility is fetched on demand, cached for 30s, and in-flight
deduped so a burst of drawer opens doesn't multiply upstream load.
When a repo segment is selected in the emission bar, the inspector now shows a Top Earners row with up to 3 eligible miners (rank chip, avatar, username, score) right next to the existing stats. Filters on `RepoMiner.isEligible === true` from the route change, so historical contributors who no longer pass the gate don't appear. Explicit empty state when no miners are currently eligible on the repo.
Adds a new "Active miners" section between Activity and Languages in
the drawer. Two views toggled via chip group:
* Treemap (default) — hand-laid grid (leader takes half the canvas,
others fill the right half) so 1-5 miners always render with
readable 1.25:1-ish aspect tiles. Eligible miners get the Linear
indigo palette graded by rank; ineligibles get a subtle gray wash
so they fade into the background. Wide-short tiles switch to a
horizontal avatar+name layout so the score/meta don't get clipped.
* Ribbon — vertical leaderboard with accent strip, rank, avatar,
name, share bar, TAO/score. Eligibles first, ineligibles dimmed.
Each tile shows avatar with a credibility ring (Linear green/yellow/
red palette by repo PR cred), uid in the top-right, name below avatar,
score + share/TAO at the bottom. Light-mode palette tuned so white
text stays legible across all eligible tiers and dark text reads on
the muted ineligible tiles. All tile drop/text shadows removed for a
flat Linear-style look.
Combines PR slice, Issue discovery slice, Merge rate, and Resolved into a single 4-column grid under the TAO emission block. Drops the now-duplicate Merge rate / Resolved row that sat at the bottom of the Activity section. Also renames the miner treemap legend's "historical" swatch to "ineligible" so the wording matches the tile text.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughComplete redesign of the repositories explorer: new server routes (per-repo miners, metadata, SN74 emission), enriched DTOs, client-side row/math utilities and squarify treemap, many UI components (market, drawer, compare, palette), and page wiring/styles. ChangesSN74 Repositories Explorer Redesign
Estimated code review effort 🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (2)
src/app/repositories/_lib/squarify.ts (1)
29-29: ⚡ Quick winSort segments by area descending before packing.
Classic squarify assumes largest-first ordering; keeping caller order can noticeably worsen aspect ratios and visual stability.
Proposed fix
- const items = segs.map((seg) => ({ data: seg.data, area: (seg.w / totalWeight) * totalArea })); + const items = segs + .map((seg) => ({ data: seg.data, area: (seg.w / totalWeight) * totalArea })) + .sort((a, b) => b.area - a.area);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/app/repositories/_lib/squarify.ts` at line 29, The items array currently preserves caller order which worsens squarify packing; compute each segment's area as done in the existing expression (const items = segs.map(...) using area: (seg.w / totalWeight) * totalArea) and then sort items by area descending before any packing logic; specifically, after creating items (or inline during creation) apply a sort by (b.area - a.area) so downstream packing routines that consume items (the items variable in this module) receive largest-first ordering.src/app/repositories/_components/MarketSection.tsx (1)
98-101: 💤 Low valueMedia query detection will not update after hydration.
useMemoruns once during render and caches the result. If the user resizes their window or changes device orientation after initial load,isTouchPrimarywon't update. Consider usinguseState+useEffectwith a media query listener, similar to theuseNarrowTreemappattern inDrawer.tsx.♻️ Suggested pattern
- const isTouchPrimary = useMemo(() => { - if (typeof window === 'undefined') return false; - return window.matchMedia('(pointer: coarse), (hover: none)').matches; - }, []); + const [isTouchPrimary, setIsTouchPrimary] = useState(false); + useEffect(() => { + const mq = window.matchMedia('(pointer: coarse), (hover: none)'); + const update = () => setIsTouchPrimary(mq.matches); + update(); + mq.addEventListener('change', update); + return () => mq.removeEventListener('change', update); + }, []);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/app/repositories/_components/MarketSection.tsx` around lines 98 - 101, isTouchPrimary is currently computed with useMemo so it never updates after hydration; replace this with a useState and a useEffect that creates a window.matchMedia('(pointer: coarse), (hover: none)') query, sets the state from query.matches initially (guarding for typeof window === 'undefined'), adds a listener to update the state on change, and removes the listener on cleanup—follow the same pattern used by useNarrowTreemap in Drawer.tsx and reference the isTouchPrimary variable when updating/reading the state.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/app/repositories/_components/CompareTray.tsx`:
- Around line 21-22: The tray is hidden visually but remains keyboard-focusable
when n === 0; update CompareTray so when n === 0 you either don't render the
interactive tray (conditional render based on n) or mark the inner container
(the div with styles.tray inside the div with styles.compareTray) as inert /
remove tab stops for its interactive children (e.g., set tabIndex=-1 on the
Clear/Compare buttons or apply an inert attribute) while keeping aria-hidden;
reference the variable n, the divs using styles.compareTray and styles.tray, and
the Clear/Compare buttons in lines ~87-97 when applying the change.
- Around line 90-95: The CompareTray button (the element with class
styles.priBtn and onClick handler onOpen) currently becomes icon-only on mobile
because the visible text span is hidden and the fallback span uses inline
display:none, leaving no accessible name; fix by adding an explicit accessible
name (e.g., aria-label="Compare side by side" or aria-labelledby pointing to a
visually-hidden span) to the button and replace the inline hidden fallback with
a screen-reader-only span (or use an existing SR-only CSS class) so assistive
tech always receives the label while preserving visual hide on mobile.
In `@src/app/repositories/_components/Drawer.tsx`:
- Line 535: The destructuring in the useMemo call inside Drawer.tsx is pulling
unused variables (totalRows, eligibleCount, ineligibleCount) which triggers
ESLint/CI warnings; edit the useMemo return/destructuring so only used values
are extracted (e.g., keep allRows, totalEligibleScore, top1Pct, conc) and remove
totalRows, eligibleCount, and ineligibleCount from the const { ... } =
useMemo(...) statement and from the returned object in that useMemo block
(adjust any dependent code if necessary).
In `@src/app/repositories/_components/RepoCard.tsx`:
- Around line 177-183: The default-label bar percentage calculation in RepoCard
(the LabelBarRow prop barPct currently set to (r.defaultLabel / 2) * 100) can
exceed 100% for values >2.0; clamp the computed value to the 0–100 range before
passing it to LabelBarRow (e.g., replace the expression with a clamped value
using Math.min/Math.max or a clamp utility) so the bar never overflows the
track.
In `@src/app/repositories/_components/RepoListRow.tsx`:
- Around line 83-86: The code in RepoListRow.tsx calls entries.reduce(...) on
Object.entries(r.labels) which will throw when r.labels is an empty object;
update the r.labels handling to first compute const entries =
Object.entries(r.labels) and then guard entries.length > 0 before calling
entries.reduce (or use a safe fallback) so that when there are no labels you
skip the reduce and set topLabel/topVal to a safe default (and set c =
LABEL_COLORS[...] ?? { fg: 'var(--fg-subtle)', soft: '' }) to avoid runtime
errors; make the check where entries is defined and ensure the rest of the
render logic uses the fallback values when entries is empty.
In `@src/app/repositories/_lib/squarify.ts`:
- Around line 27-30: Guard against zero/invalid weights before dividing:
validate segs' weights (ignore or coerce NaN/non-finite and negative values) and
compute totalWeight safely; if totalWeight <= 0, fall back to equal distribution
(area = totalArea / segs.length) or another defined fallback, then compute items
from segs using that safe weight or fallback. Update the code around
totalWeight, totalArea, and items so the map uses the sanitized weights/
fallback instead of directly dividing by totalWeight to avoid NaN/Infinity in
area calculations.
In `@src/app/repositories/page.module.css`:
- Line 67: Stylelint is flagging CSS Modules pseudo-class syntax like
:global([data-theme='light']) .scope (and other :global(...) occurrences) as
unknown; update the stylelint config to recognize CSS Modules by either
extending the stylelint-config-css-modules preset or adding an allow-list for
the pseudo-class and appropriate parser: add "selector-pseudo-class-no-unknown":
[true, {"ignorePseudoClasses": ["global"]}] and set customSyntax to a PostCSS
parser that supports CSS Modules (e.g., postcss-scss or postcss-modules), so
selectors such as :global(...) .scope no longer trigger
selector-pseudo-class-no-unknown errors.
- Around line 803-805: The linter flags a missing empty line before the
declaration following the custom property; in the CSS rule containing
--cred-color, add a blank line before the normal declarations so that
--cred-color: `#7b8494`; is separated from position: relative; (i.e., ensure an
empty line before the declaration for position: relative; to satisfy
declaration-empty-line-before and keep the custom property (--cred-color)
grouped separately).
In `@src/components/Dropdown.tsx`:
- Around line 117-124: The closeOnScroll listener currently closes the menu on
any scroll because onScroll does setOpen(false) unconditionally; change onScroll
to accept the Event and early-return when the scroll event originated from
inside the menu element (i.e., if the event.target is contained within the
dropdown's DOM node — the component's menuRef or dropdownRef), otherwise call
setOpen(false); ensure the same handler signature is used when adding and
removing the listener (window.addEventListener('scroll', onScroll, true) and
removeEventListener) so internal scrolls (overflowY: 'auto' in the portal) don't
close the dropdown.
---
Nitpick comments:
In `@src/app/repositories/_components/MarketSection.tsx`:
- Around line 98-101: isTouchPrimary is currently computed with useMemo so it
never updates after hydration; replace this with a useState and a useEffect that
creates a window.matchMedia('(pointer: coarse), (hover: none)') query, sets the
state from query.matches initially (guarding for typeof window === 'undefined'),
adds a listener to update the state on change, and removes the listener on
cleanup—follow the same pattern used by useNarrowTreemap in Drawer.tsx and
reference the isTouchPrimary variable when updating/reading the state.
In `@src/app/repositories/_lib/squarify.ts`:
- Line 29: The items array currently preserves caller order which worsens
squarify packing; compute each segment's area as done in the existing expression
(const items = segs.map(...) using area: (seg.w / totalWeight) * totalArea) and
then sort items by area descending before any packing logic; specifically, after
creating items (or inline during creation) apply a sort by (b.area - a.area) so
downstream packing routines that consume items (the items variable in this
module) receive largest-first ordering.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: a12b8614-5b5b-4c94-9801-40042b80d329
📒 Files selected for processing (23)
src/app/api/gt/repos/[owner]/[name]/miners/route.tssrc/app/api/gt/repositories/route.tssrc/app/api/repos/metadata/route.tssrc/app/api/sn74-emission/route.tssrc/app/docs/page.tsxsrc/app/repositories/_components/Avatar.tsxsrc/app/repositories/_components/CompareModal.tsxsrc/app/repositories/_components/CompareTray.tsxsrc/app/repositories/_components/Drawer.tsxsrc/app/repositories/_components/LangIcon.tsxsrc/app/repositories/_components/MarketSection.tsxsrc/app/repositories/_components/Palette.tsxsrc/app/repositories/_components/RefPanels.tsxsrc/app/repositories/_components/RepoCard.tsxsrc/app/repositories/_components/RepoListRow.tsxsrc/app/repositories/_lib/colors.tssrc/app/repositories/_lib/incentives.tssrc/app/repositories/_lib/rows.tssrc/app/repositories/_lib/squarify.tssrc/app/repositories/page.module.csssrc/app/repositories/page.tsxsrc/components/Dropdown.tsxsrc/types/entities.ts
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/app/repositories/_components/Drawer.tsx`:
- Around line 465-471: The current fallback when !hasEligibleMiners still
produces an "Active miners" treemap; instead, stop producing an active-treemap
value and surface a dedicated historical/empty state. Change the branch that
currently returns Math.max(tileScale(baseRepoScore), 0.75) (the
!hasEligibleMiners branch using hasEligibleMiners, tileScale, baseRepoScore,
topEligibleUnit) to return a sentinel that signals "no active miners" (e.g.
null/undefined or a clear flag) and update the Drawer rendering logic that
consumes this value to render the historical/empty state UI rather than the
active-miners treemap. Apply the same change to the other identical path
referenced (the second occurrence using the same variables) so both paths
consistently show the historical/empty state when there are no eligible miners.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: f94cf37c-c1d1-4eab-96cb-4cd525c8a79d
📒 Files selected for processing (2)
src/app/repositories/_components/Drawer.tsxsrc/app/repositories/page.module.css
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/app/repositories/_components/Drawer.tsx (1)
619-627:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAvoid labeling every empty miner state as a benchmark repo.
This branch now fires whenever both active and historical rows are empty, which can also happen for a non-benchmark repo with no synced miner data yet. The current copy will be misleading in that case.
Suggested fix
- Benchmark repo — no miners. + No miner data available for this repo yet.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/app/repositories/_components/Drawer.tsx` around lines 619 - 627, The empty-state message currently shown when displayRows.length === 0 incorrectly always says "Benchmark repo — no miners."; update the Drawer empty-state logic (around displayRows and containerStyle) to choose the copy based on repo type instead of always showing "Benchmark repo" — e.g., if there is an existing flag or prop (use isBenchmarkRepo, repo.isBenchmark, or similar in the parent/component) render "Benchmark repo — no miners.", otherwise render a neutral message like "No miners yet." or "No miner data available." Ensure you reference and use the existing symbols displayRows and containerStyle and add the repo-type check in the same conditional block.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/app/repositories/_components/Drawer.tsx`:
- Around line 455-457: The historical-only UI must use baseScore exclusively;
update minerVisualScore to return only baseScore for historical-only miners
(i.e., when the historical-only condition is true/when m is not eligible)
instead of falling back to score, and ensure any other places that size or order
historical tiles (the sizing/sorting code paths referenced in Drawer.tsx that
currently call minerVisualScore or sort by score/repoWorkScore) are changed to
use baseScore for the historical-only path. Concretely: modify minerVisualScore
to return Math.max(m.baseScore ?? 0, 0) for the historical-only branch (and keep
the current behavior for eligible miners), and audit the other usages mentioned
so historical tiles are both ordered and sized by baseScore only.
---
Outside diff comments:
In `@src/app/repositories/_components/Drawer.tsx`:
- Around line 619-627: The empty-state message currently shown when
displayRows.length === 0 incorrectly always says "Benchmark repo — no miners.";
update the Drawer empty-state logic (around displayRows and containerStyle) to
choose the copy based on repo type instead of always showing "Benchmark repo" —
e.g., if there is an existing flag or prop (use isBenchmarkRepo,
repo.isBenchmark, or similar in the parent/component) render "Benchmark repo —
no miners.", otherwise render a neutral message like "No miners yet." or "No
miner data available." Ensure you reference and use the existing symbols
displayRows and containerStyle and add the repo-type check in the same
conditional block.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 18557bca-c215-429b-9de2-1c2c5723d69f
📒 Files selected for processing (1)
src/app/repositories/_components/Drawer.tsx
0105a0b to
0f66b97
Compare
|
thanks for your contribution. looks much better. |
|
I still don’t think this is ready to merge because the miner insight states are misleading:
Please fix these states so active, ineligible/historical, empty, and error cases are clearly distinct. |
|
so funny guys... |
|
hey, guys. @MkDev11 @bitloi @FairyPigDev |
|
@gazeatcode how I can be same team with @MkDev11 and @bitloi ? |
Haha, “you are stupid too” means you’re admitting that you’re stupid as well lol. Thanks for admitting it yourself, @FairyPigDev. You’re trying really hard to pretend you’re not on the same team as @MkDev11, but you don’t need to. That “self miners…” sentence doesn’t hide anything, in fact, it just makes it more obvious that you’re on the same team. And honestly, it also makes you look pretty stupid. Haha. |
|
@MkDev11 plz make @FairyPigDev calm down!. I think you are super senior, but @FairyPigDev is super junior. plz teach him |
Summary
Redesigns the
/repositoriespage with a richer repo-focused experience: repository cards/list rows, comparison tooling, repository metadata, emission details, and repo-scoped miner insights.Adds an active miners treemap for the selected repository that uses repo-scoped score and credibility data, limits the view to the top five miners, and improves responsive layout for small tiles and language lists.
Related Issues
Closes #153
Type of Change
Screenshots
Testing
pnpm buildpassesAdditional checks run:
git diff --checkcorepack pnpm exec tsc --noEmit --incremental falseChecklist
Summary by CodeRabbit
New Features
Documentation