feat(miners): refresh page with emission overview and detail modal#231
Conversation
|
Warning Review limit reached
More reviews will be available in 50 minutes and 32 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more credits in the billing tab to continue. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThis PR adds backend APIs (miners activity, miner-works, GitHub bio, SN74 emission, per-repo miners), expands miner DTOs, implements a MinerView derivation library, introduces treemap squarify and stream helpers, adds many shared UI primitives and composed miner feature components (header, treemap, cards, modal, works, palette), and wires everything into a dynamic miners page with TanStack Query. ChangesMiners Activity Feature
🎯 4 (Complex) | ⏱️ ~75 minutes Possibly Related PRs
✨ Finishing Touches🧪 Generate unit tests (beta)
Warning Billing warning: we have not been able to collect payment for this subscription for more than 72 hours. Please update the payment method or pay any pending invoices in Billing to avoid service interruption. |
There was a problem hiding this comment.
Actionable comments posted: 17
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/types/entities.ts (1)
277-310:⚠️ Potential issue | 🟠 Major | ⚡ Quick win
/api/miners/activitynow returns fields that the shared DTOs still hide.The activity route now emits maintainer fields on each miner (
isMaintainer,maintainerRepos,maintainerCut,maintainerTaoShare,maintainerRepoTaoShares) and stamps eachrepoEvaluationwith emission/eligibility fields (issueDiscoveryShare,emissionShare,prTaoShare,issueTaoShare,minPrCred,minIssueCred,minMergedPrs,minSolvedIssues). Those properties are missing from the canonical DTOs here, so the derivation/UI layer no longer has an accurate typed contract for the payload it consumes.Suggested fix
export interface Miner { id: string; uid: number; hotkey: string; githubUsername: string; githubId?: string; + isMaintainer?: boolean; + maintainerRepos?: string[]; + maintainerCut?: number; + maintainerTaoShare?: number; + maintainerRepoTaoShares?: Record<string, number>; isEligible: boolean; isIssueEligible?: boolean; @@ export interface MinerRepoEvaluation { @@ issueDiscoveryScore?: string | number; issue_discovery_score?: string | number; issueTokenScore?: string | number; issue_token_score?: string | number; + issueDiscoveryShare?: number; + emissionShare?: number; + prTaoShare?: number; + issueTaoShare?: number; + minPrCred?: number | null; + minIssueCred?: number | null; + minMergedPrs?: number | null; + minSolvedIssues?: number | null; totalSolvedIssues?: string | number;Also applies to: 312-360
🤖 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/types/entities.ts` around lines 277 - 310, The Miner DTO (interface Miner) and related MinerRepoEvaluation type are missing new fields returned by /api/miners/activity; update the Miner interface to include isMaintainer, maintainerRepos, maintainerCut, maintainerTaoShare, maintainerRepoTaoShares, and update MinerRepoEvaluation to include issueDiscoveryShare, emissionShare, prTaoShare, issueTaoShare, minPrCred, minIssueCred, minMergedPrs, minSolvedIssues (and their snake_case variants if applicable, e.g., repo_evaluations) so the types match the runtime payload consumed by the derivation/UI; add optional typing and appropriate types (number/boolean/string) consistent with other score fields and preserve backwards compatibility by marking them optional.src/app/api/gt/repos/[owner]/[name]/miners/route.ts (1)
77-82:⚠️ Potential issue | 🟠 Major | ⚡ Quick winHandle
full_namehere the same way the sibling miners route does.This route assumes
REPOS_URLalways returnsfullName, butsrc/app/api/miners/activity/route.tsalready normalizesfullName ?? full_namefrom the same upstream. If the payload is snake_case here,repo.fullName.toLowerCase()throws and the repo-miners endpoint falls back to 502.Suggested fix
interface UpstreamRepo { - fullName: string; + fullName?: string | null; + full_name?: string | null; config?: { issueDiscoveryShare?: string | number | null; issue_discovery_share?: string | number | null } | null; issueDiscoveryShare?: string | number | null; issue_discovery_share?: string | number | null; } @@ const issueDiscoveryShareByRepo = new Map<string, number>(); for (const repo of repos) { + const repoName = stringValue(repo.fullName ?? repo.full_name); + if (!repoName) continue; issueDiscoveryShareByRepo.set( - repo.fullName.toLowerCase(), + repoName.toLowerCase(), num(repo.config?.issueDiscoveryShare ?? repo.config?.issue_discovery_share ?? repo.issueDiscoveryShare ?? repo.issue_discovery_share), ); }Also applies to: 129-133
🤖 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/api/gt/repos/`[owner]/[name]/miners/route.ts around lines 77 - 82, The code assumes repo.fullName always exists; update the UpstreamRepo type to include full_name?: string | null and normalize usage the same way as the sibling miners route by replacing direct repo.fullName accesses with a safe normalized value (e.g. const name = repo.fullName ?? repo.full_name) and use (name ?? '').toLowerCase() or skip the repo when name is missing before calling toLowerCase; apply this normalization at both places where repo.fullName is used (the earlier UpstreamRepo usage and the later block around the second occurrence).
🧹 Nitpick comments (1)
src/components/IssueLabels.tsx (1)
144-146: ⚡ Quick winAvoid subscribing every label group to global theme events.
useTheme()installs global listeners and reapplies the document theme per caller. Rendering oneIssueLabelsper row/card will scale those listeners linearly and duplicateapplyThemeAttr()work on each theme change. Prefer acceptingthemefrom a higher-level caller or a shared provider so heavy lists keep a single subscription.🤖 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/components/IssueLabels.tsx` around lines 144 - 146, The IssueLabels component currently calls useTheme() (and thereby sets up global listeners and calls applyThemeAttr()) per instance; change IssueLabels to accept a theme prop (e.g. theme: string | ThemeType) instead of calling useTheme() internally, remove the useTheme() import/usage and any direct calls to applyThemeAttr() from inside IssueLabels, and update callers to pass the shared theme from a higher-level component or provider so a single theme subscription services all label groups; ensure the prop name matches the component signature and update any places that render IssueLabels to forward the shared theme.
🤖 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/api/github-bio/route.ts`:
- Around line 18-27: The cache Map (cache) in GET is unbounded and never evicts
expired entries; update GET to prune expired entries and enforce a size cap
before inserting/reading: iterate cache entries and delete ones where Date.now()
- entry.at >= TTL_MS, and if cache.size exceeds a new MAX_CACHE_ENTRIES constant
remove the oldest entry (lowest entry.at) to keep the map bounded; apply these
steps around where key and hit are computed so subsequent cache.get/set uses the
cleaned map.
In `@src/app/api/miner-works/route.ts`:
- Around line 152-156: The mirror queries use exact-match on repo_full_name
which fails for mixed-case names; modify the lookup to normalize both sides
(e.g., lowercased) so casing differences don't miss rows: when preparing the
statement in getReadDb usage (the SELECT raw_json FROM pulls WHERE
repo_full_name = ? AND number = ? prepared statement used in the prs loop that
calls extractLabels), bind a normalized repo value (lowercase p.repo or
otherwise normalized) and update the SQL to compare against
lower(repo_full_name) (or equivalent DB-normalized column) so mixed-case repos
return their raw_json and closed-at/labels; apply the same normalization change
to the other mirror query location that uses repo_full_name (the similar stmt
used around the other prs handling).
- Around line 526-610: The handler accepts githubId without login but later
logic (mirror supplement, getDbPulls, attachGithubPrLabels, getIssues,
enrichLabelsFromGitHub) still uses login, causing incomplete payloads; fix by
deriving a lookupLogin when login is empty: after fetching allPrs, set const
lookupLogin = login || allPrs.find(p => p.githubId === githubId)?.author ?? '';
then use lookupLogin instead of login for getDbPulls(lookupLogin) (or skip
mirror only if lookupLogin is empty), attachGithubPrLabels(prs, lookupLogin),
getIssues(lookupLogin), and enrichLabelsFromGitHub(prs, issues) so githubId-only
requests get the same mirror, issues, activity and label enrichment as
login-based requests.
In `@src/app/miners/_components/Headline.tsx`:
- Around line 205-218: The treemap tile buttons lack an explicit accessible
name; mirror the computed title string into an aria-label so screen readers get
the same description. Update the button in Headline.tsx (the button that uses
data, formatUsd, shareText, fullPool, useAvatar/useMosaic and onTileClick) to
set aria-label to the same ternary string used for title (the miner vs non-miner
variant), leaving title as-is; keep the existing onMouseEnter/onFocus handlers
(setHoveredKey) intact.
In `@src/app/miners/_components/MinerDistribution.tsx`:
- Around line 56-89: The popover is closed unconditionally by the onBlur={() =>
setHover(null)} on the column, which collapses the dialog when keyboard focus
moves into the popover buttons; update the blur handling on the column wrapper
to only call setHover(null) when focus actually leaves the entire column+popover
subtree (i.e., check the blur event's relatedTarget /
event.nativeEvent.relatedTarget and only clear hover if the new focused element
is not contained within event.currentTarget). Keep references to the existing
symbols: onBlur, setHover, hover, the column wrapper div and the popup div with
role="dialog" (styles.distPop) so you preserve keyboard focus navigation into
the miner buttons and prevent the popover from immediately closing when tabbing
into a miner row.
In `@src/app/miners/_components/MinerModal.tsx`:
- Around line 245-247: The PR recency logic currently falls back from mergedAt
directly to createdAt, omitting closedAt; update both places where this happens
(the loop over works.prs in MinerModal.tsx that computes best/lastActiveIso and
the buildWorkRows logic in MinerWorks.tsx) to prefer mergedAt ?? closedAt ??
createdAt when parsing dates (e.g., Date.parse(mergedAt ?? closedAt ??
createdAt)) and ensure Number.isFinite checks still apply to the parsed
timestamp.
- Around line 245-247: The last-active calculation in MinerModal.tsx iterates
works.prs and currently derives the timestamp using Date.parse(p.mergedAt ??
p.createdAt ?? ''), which ignores closed-but-unmerged PRs; update the logic in
that loop (the code handling p.mergedAt and assigning to best) to prefer
mergedAt, then closedAt, then createdAt (e.g. Date.parse(p.mergedAt ??
p.closedAt ?? p.createdAt ?? '')) and keep the existing Number.isFinite(t) && t
> best check so closed PRs update the "Last active" correctly.
- Around line 382-394: The miner-works query currently throws on non-2xx and the
component only reads works and worksLoading, so backend failures get shown as
"No activity"; update the useQuery call (the one returning { data: works,
isLoading: worksLoading }) to include keepPreviousData: true and retry/backoff
settings and expose isError and error and refetch (e.g. const { data: works,
isLoading: worksLoading, isError: worksError, error: worksErrorObj, refetch:
refetchWorks } = useQuery< MinerWorksResponse >({...})); then update the
MinerModal render to check worksError/worksErrorObj and show a distinct error
state with a retry button that calls refetchWorks (or preserve and render the
last successful works payload when keepPreviousData is present) instead of
falling back to “No activity”.
- Around line 101-121: The code currently creates a stats bucket in stat for
every repo before checking cutoff, causing repos with only stale PRs to get
zeroed; change the logic in the prs loop so you only create/set the Map entry
(stat.set(key, {...})) when a PR actually falls inside the trailing window:
check p.mergedAt and p.closedAt against cutoff first, and only then initialize
or increment the bucket for that key. Keep the rest of the flow (using stat.get
in rows.map and computing mergedPrs/prCred/prEligible) unchanged so repos with
no in-window PRs continue to be left absent from stat.
In `@src/app/miners/_components/MinerWorks.tsx`:
- Around line 1062-1064: The footer link logic (footHref) always constructs a PR
search URL, causing the footer to misrepresent the current view; update footHref
to detect the active tab state (e.g., the component's tab/activeTab variable
used to render Issues vs PRs) and build an issues URL when the Issues tab is
active—use
`https://github.com/${effectiveRepo}/issues?q=author:${encodeURIComponent(login)}`
for repo-scoped issues or `https://github.com/${login}?tab=issues` when no
effectiveRepo, otherwise keep the existing PR search URL; modify the code that
defines footHref in MinerWorks.tsx to branch on the active tab and reference
effectiveRepo and login as shown.
In `@src/app/miners/_components/Palette.tsx`:
- Around line 47-53: The search string built in the matched useMemo only
concatenates repo names from v.rows, so repo names present in maintainer-only
v.topRepos are not matched; update the needle-building logic in the matched
useMemo (inside the views.filter callback) to include repo names from v.topRepos
as well (e.g., safely map and join v.topRepos' repo fields alongside v.rows'
repo fields when constructing hay) so topRepos entries are searchable.
In `@src/app/miners/_components/shared.tsx`:
- Around line 420-432: The repo score badge is omitting the separate
issueTokenScore contribution, so update the displayed and titled score
calculations to include row.issueTokenScore (e.g., change occurrences using
score(row.prScore + row.issueScore) to score(row.prScore + row.issueScore +
row.issueTokenScore) and the formatted fallback used for repoTao titles/labels);
apply this change in both the repoTao block (symbols: repoTao, formatNumber,
repoPool) and the alternate score badge block (symbols: score, row.prScore,
row.issueScore) so the UI reflects issueTokenScore-driven earnings.
In `@src/app/miners/_lib/miners.ts`:
- Around line 308-312: The derived total `issues` currently uses Math.max(...)
and undercounts when repositories have separate closed and completed buckets;
update the computation of the `issues` variable to sum all outcome buckets
(solvedIssues + openIssues + closedIssues + validSolvedIssues) instead of taking
a max so the miner-level fallback includes every bucket; apply the same fix to
the duplicate logic referenced by symbols solvedIssues, openIssues,
closedIssues, validSolvedIssues and the issues assignment at the other
occurrence (the block around the second issues calculation).
In `@src/app/miners/_lib/streams.ts`:
- Around line 25-26: The streamsOf function incorrectly uses the role flag
view.isMaintainer as the maintainer stream; change it to base the maintainer
stream on earned maintainer emission (e.g., view.maintainerEarning or the actual
property that represents earned maintainer cut on MinerView) so Streams reflects
real earned streams rather than roster membership—update streamsOf to return
maintainer: view.maintainerEarning (or the correct earned-cut property) instead
of view.isMaintainer.
In `@src/app/miners/page.tsx`:
- Around line 191-198: The board always uses ranks.activity regardless of the
current sort; update the render to pick the correct rank mapping based on the
current sort key (e.g., use the component's sort/activeSort state) instead of
hardcoding ranks.activity, and when the active sort is not one of the rankable
keys ('activity'|'score'|'earnings'|'repos') hide the `#rank` text and medals;
locate the ranks const and every render site that currently references
ranks.activity (including the render blocks around the `#rank` text and medal
components) and replace with a lookup like ranks[currentSortKey] (or
conditionally render nothing if currentSortKey is not in ranks).
- Around line 233-237: The page is using emission?.minerTaoPerDay (poolTao) as
the denominator for "of pool" percentages, which diverges from the header/repo
math; change the code that defines/uses poolTao/totalTao so the per-card "of
pool" comparisons use the already-derived subnetTao (the active+recycle+treasury
pool) instead of emission?.minerTaoPerDay; find references to view.taoPerDay
being compared to poolTao (or emission?.minerTaoPerDay) in page.tsx and replace
them to use subnetTao so the percentages match the treemap/header calculations.
In `@src/components/ActivityLineChart.tsx`:
- Around line 189-199: The transparent SVG <rect> hitboxes (the element using
x(idx), plotWidth, points.length and the setHoveredIndex handlers) are currently
focusable via tabIndex={0} but have no accessible name; either remove them from
the tab order by changing tabIndex={0} to tabIndex={-1} (making them mouse-only
while preserving onMouseEnter/onMouseLeave behavior), or provide a meaningful
accessible name by adding an aria-label (for example aria-label={`Data point
${idx + 1}`} or a more descriptive label) and keep the existing onFocus/onBlur
handlers so keyboard users can interact; update the <rect> element accordingly
and ensure any focus handlers rely on setHoveredIndex as before.
---
Outside diff comments:
In `@src/app/api/gt/repos/`[owner]/[name]/miners/route.ts:
- Around line 77-82: The code assumes repo.fullName always exists; update the
UpstreamRepo type to include full_name?: string | null and normalize usage the
same way as the sibling miners route by replacing direct repo.fullName accesses
with a safe normalized value (e.g. const name = repo.fullName ?? repo.full_name)
and use (name ?? '').toLowerCase() or skip the repo when name is missing before
calling toLowerCase; apply this normalization at both places where repo.fullName
is used (the earlier UpstreamRepo usage and the later block around the second
occurrence).
In `@src/types/entities.ts`:
- Around line 277-310: The Miner DTO (interface Miner) and related
MinerRepoEvaluation type are missing new fields returned by
/api/miners/activity; update the Miner interface to include isMaintainer,
maintainerRepos, maintainerCut, maintainerTaoShare, maintainerRepoTaoShares, and
update MinerRepoEvaluation to include issueDiscoveryShare, emissionShare,
prTaoShare, issueTaoShare, minPrCred, minIssueCred, minMergedPrs,
minSolvedIssues (and their snake_case variants if applicable, e.g.,
repo_evaluations) so the types match the runtime payload consumed by the
derivation/UI; add optional typing and appropriate types (number/boolean/string)
consistent with other score fields and preserve backwards compatibility by
marking them optional.
---
Nitpick comments:
In `@src/components/IssueLabels.tsx`:
- Around line 144-146: The IssueLabels component currently calls useTheme() (and
thereby sets up global listeners and calls applyThemeAttr()) per instance;
change IssueLabels to accept a theme prop (e.g. theme: string | ThemeType)
instead of calling useTheme() internally, remove the useTheme() import/usage and
any direct calls to applyThemeAttr() from inside IssueLabels, and update callers
to pass the shared theme from a higher-level component or provider so a single
theme subscription services all label groups; ensure the prop name matches the
component signature and update any places that render IssueLabels to forward the
shared theme.
🪄 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: 4dcc13a0-0472-4857-8d71-341742182b5d
📒 Files selected for processing (25)
src/app/api/github-bio/route.tssrc/app/api/gt/repos/[owner]/[name]/miners/route.tssrc/app/api/miner-works/route.tssrc/app/api/miners/activity/route.tssrc/app/api/sn74-emission/route.tssrc/app/dashboard/page.tsxsrc/app/miners/_components/EmissionHeader.tsxsrc/app/miners/_components/Headline.tsxsrc/app/miners/_components/MinerCard.tsxsrc/app/miners/_components/MinerDistribution.tsxsrc/app/miners/_components/MinerListRow.tsxsrc/app/miners/_components/MinerModal.tsxsrc/app/miners/_components/MinerWorks.tsxsrc/app/miners/_components/Palette.tsxsrc/app/miners/_components/StreamTags.tsxsrc/app/miners/_components/shared.tsxsrc/app/miners/_lib/miners.tssrc/app/miners/_lib/squarify.tssrc/app/miners/_lib/streams.tssrc/app/miners/page.module.csssrc/app/miners/page.tsxsrc/components/ActivityLineChart.tsxsrc/components/IssueLabels.tsxsrc/lib/format.tssrc/types/entities.ts
| export async function GET(req: NextRequest) { | ||
| const url = new URL(req.url); | ||
| const login = (url.searchParams.get('login') ?? '').trim(); | ||
| const githubId = (url.searchParams.get('githubId') ?? '').trim(); | ||
| if (!login && !githubId) { | ||
| return NextResponse.json({ error: 'login or githubId required' }, { status: 400 }); | ||
| } | ||
|
|
||
| let allPrs: IndexedPr[] = []; | ||
| try { | ||
| allPrs = await getPrs(); | ||
| } catch { | ||
| allPrs = []; | ||
| } | ||
| const loginLc = login.toLowerCase(); | ||
| const mine = allPrs.filter((p) => (loginLc && p.authorLc === loginLc) || (githubId && p.githubId === githubId)); | ||
|
|
||
| // Supplement with PRs from the mirror that the /prs feed doesn't score (e.g. a | ||
| // maintainer's PRs on their own repo). Deduped against the scored feed set, | ||
| // case-insensitively (the feed lowercases repo names; the mirror keeps GitHub's). | ||
| if (login) { | ||
| const feedKeys = new Set(mine.map((p) => `${p.repo.toLowerCase()}#${p.number}`)); | ||
| for (const p of getDbPulls(login)) { | ||
| if (!feedKeys.has(`${p.repo.toLowerCase()}#${p.number}`)) mine.push(p); | ||
| } | ||
| } | ||
|
|
||
| // Most valuable first: merged (with score) on top, then by score, then recency. | ||
| const stateRank = (s: MinerPr['state']) => (s === 'MERGED' ? 0 : s === 'OPEN' ? 1 : 2); | ||
| mine.sort( | ||
| (a, b) => | ||
| stateRank(a.state) - stateRank(b.state) || | ||
| b.score - a.score || | ||
| (b.createdAt ?? '').localeCompare(a.createdAt ?? ''), | ||
| ); | ||
|
|
||
| const prs: MinerPr[] = mine.slice(0, MAX).map((p) => ({ | ||
| repo: p.repo, | ||
| number: p.number, | ||
| title: p.title, | ||
| state: p.state, | ||
| score: p.score, | ||
| createdAt: p.createdAt, | ||
| mergedAt: p.mergedAt, | ||
| closedAt: p.closedAt, | ||
| additions: p.additions, | ||
| deletions: p.deletions, | ||
| linkedIssueNumber: p.linkedIssueNumber, | ||
| author: p.author, | ||
| hotkey: p.hotkey, | ||
| commitCount: p.commitCount, | ||
| baseScore: p.baseScore, | ||
| collateralScore: p.collateralScore, | ||
| tokenScore: p.tokenScore, | ||
| totalNodesScored: p.totalNodesScored, | ||
| structuralCount: p.structuralCount, | ||
| structuralScore: p.structuralScore, | ||
| leafCount: p.leafCount, | ||
| leafScore: p.leafScore, | ||
| label: p.label, | ||
| labelMultiplier: p.labelMultiplier, | ||
| reviewQualityMultiplier: p.reviewQualityMultiplier, | ||
| labels: p.labels, | ||
| })); | ||
| attachPrLabels(prs); | ||
| // Fill PRs the mirror/feed left label-less (e.g. external-repo PRs with "ci"/"size:L") | ||
| // with their real GitHub labels, so they show in the contributions table — not just the | ||
| // detail view. | ||
| await attachGithubPrLabels(prs, login); | ||
| const issues = getIssues(login); | ||
| // Reconcile labels with each repo's real GitHub palette (fill colors + surface a | ||
| // genuine "other" label where the repo defines one). | ||
| await enrichLabelsFromGitHub(prs, issues); | ||
|
|
||
| const counts = { | ||
| prs: mine.length, | ||
| prMerged: mine.filter((p) => p.state === 'MERGED').length, | ||
| prOpen: mine.filter((p) => p.state === 'OPEN').length, | ||
| prClosed: mine.filter((p) => p.state === 'CLOSED').length, | ||
| issues: issues.length, | ||
| issuesOpen: issues.filter((i) => i.state === 'open').length, | ||
| issuesCompleted: issues.filter((i) => (i.stateReason ?? '').toUpperCase() === 'COMPLETED').length, | ||
| }; | ||
|
|
||
| const activity = buildActivity(mine, issues); |
There was a problem hiding this comment.
githubId-only requests currently return a partial works payload.
The handler accepts githubId without login, but all mirror-backed data below still keys off login. In that path you lose mirrored PRs, issues, activity derived from those issues, and some label enrichment even though the request passed validation.
Suggested fix
export async function GET(req: NextRequest) {
const url = new URL(req.url);
- const login = (url.searchParams.get('login') ?? '').trim();
+ const login = (url.searchParams.get('login') ?? '').trim();
const githubId = (url.searchParams.get('githubId') ?? '').trim();
@@
const loginLc = login.toLowerCase();
const mine = allPrs.filter((p) => (loginLc && p.authorLc === loginLc) || (githubId && p.githubId === githubId));
+ const resolvedLogin =
+ login ||
+ mine.find((p) => githubId && p.githubId === githubId && p.author)?.author ||
+ '';
@@
- if (login) {
+ if (resolvedLogin) {
const feedKeys = new Set(mine.map((p) => `${p.repo.toLowerCase()}#${p.number}`));
- for (const p of getDbPulls(login)) {
+ for (const p of getDbPulls(resolvedLogin)) {
if (!feedKeys.has(`${p.repo.toLowerCase()}#${p.number}`)) mine.push(p);
}
}
@@
- await attachGithubPrLabels(prs, login);
- const issues = getIssues(login);
+ await attachGithubPrLabels(prs, resolvedLogin);
+ const issues = getIssues(resolvedLogin);🤖 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/api/miner-works/route.ts` around lines 526 - 610, The handler accepts
githubId without login but later logic (mirror supplement, getDbPulls,
attachGithubPrLabels, getIssues, enrichLabelsFromGitHub) still uses login,
causing incomplete payloads; fix by deriving a lookupLogin when login is empty:
after fetching allPrs, set const lookupLogin = login || allPrs.find(p =>
p.githubId === githubId)?.author ?? ''; then use lookupLogin instead of login
for getDbPulls(lookupLogin) (or skip mirror only if lookupLogin is empty),
attachGithubPrLabels(prs, lookupLogin), getIssues(lookupLogin), and
enrichLabelsFromGitHub(prs, issues) so githubId-only requests get the same
mirror, issues, activity and label enrichment as login-based requests.
| const solvedIssues = num(row.totalSolvedIssues ?? row.total_solved_issues); | ||
| const openIssues = num(row.totalOpenIssues ?? row.total_open_issues); | ||
| const closedIssues = num(row.totalClosedIssues ?? row.total_closed_issues); | ||
| const validSolvedIssues = num(row.totalValidSolvedIssues ?? row.total_valid_solved_issues); | ||
| const issues = Math.max(solvedIssues + openIssues, closedIssues + openIssues, validSolvedIssues); |
There was a problem hiding this comment.
Count all issue outcome buckets in the derived totals.
issues is currently derived with Math.max(...), and the miner-level fallback only adds solved + open. That undercounts repos/miners that have both separately closed issues and completed issues, even though the rest of this cohort models those as distinct buckets. The result is wrong row.issues / view.totalIssues, which then skews activity sorting and the issue counts shown downstream.
Proposed fix
- const issues = Math.max(solvedIssues + openIssues, closedIssues + openIssues, validSolvedIssues);
+ const issues = solvedIssues + closedIssues + openIssues;
...
- const totalIssues =
- rowIssues ||
- num(miner.totalSolvedIssues ?? wire.total_solved_issues) + num(miner.totalOpenIssues ?? wire.total_open_issues);
+ const totalIssues =
+ rowIssues ||
+ num(miner.totalSolvedIssues ?? wire.total_solved_issues) +
+ num(miner.totalClosedIssues ?? wire.total_closed_issues) +
+ num(miner.totalOpenIssues ?? wire.total_open_issues);Also applies to: 606-608
🤖 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/miners/_lib/miners.ts` around lines 308 - 312, The derived total
`issues` currently uses Math.max(...) and undercounts when repositories have
separate closed and completed buckets; update the computation of the `issues`
variable to sum all outcome buckets (solvedIssues + openIssues + closedIssues +
validSolvedIssues) instead of taking a max so the miner-level fallback includes
every bucket; apply the same fix to the duplicate logic referenced by symbols
solvedIssues, openIssues, closedIssues, validSolvedIssues and the issues
assignment at the other occurrence (the block around the second issues
calculation).
| export function streamsOf(view: MinerView): Streams { | ||
| return { pr: view.prEarning, issue: view.issueEarning, maintainer: view.isMaintainer }; |
There was a problem hiding this comment.
Base the maintainer stream on earned cut, not roster membership.
view.isMaintainer is a role flag, not proof that this miner currently earns maintainer emission. Using it here marks zero-share maintainers as orange/dual across the treemap, badges, and StreamTags, which contradicts the "real earned streams" contract in this file.
Proposed fix
export function streamsOf(view: MinerView): Streams {
- return { pr: view.prEarning, issue: view.issueEarning, maintainer: view.isMaintainer };
+ return {
+ pr: view.prEarning,
+ issue: view.issueEarning,
+ maintainer: view.maintainerTaoShare > 0,
+ };
}🤖 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/miners/_lib/streams.ts` around lines 25 - 26, The streamsOf function
incorrectly uses the role flag view.isMaintainer as the maintainer stream;
change it to base the maintainer stream on earned maintainer emission (e.g.,
view.maintainerEarning or the actual property that represents earned maintainer
cut on MinerView) so Streams reflects real earned streams rather than roster
membership—update streamsOf to return maintainer: view.maintainerEarning (or the
correct earned-cut property) instead of view.isMaintainer.
|
This is really great. You implemented all the things correctly we discussed. Thanks for the work @bitloi. Here are some issues. Correction: the live Gittensor repo feed currently has 19 repos, so the The bigger blocker is Also, |
- case-insensitive mirror lookup for PR labels - live PR stats no longer zero repos with only out-of-window PRs - repo score badge includes issue-solving score - cards rank by the active sort, not always activity - "% of pool" uses subnetTao, matching the treemap - last-active and work dates count closed-unmerged PRs - footer link points to issues on the issues tab
- treemap tiles expose an aria-label - distribution popover stays open when focus moves into it - chart hover hitboxes drop out of the tab order
- declare maintainer/emission fields on the miner DTOs - normalize full_name in the repo-miners route - bound the github-bio in-memory cache - include maintainer-only repos in palette search
same casing mismatch as the label query — missed on the first review pass.
cap concurrency and repo count and trim pagination so opening a modal can't burst the github api from one request; the local mirror stays the primary label source.
derive the live label-lookup login from the matched PRs' own author (skip if the identity is ambiguous), so a request pairing a valid githubId with an arbitrary login can't spend PAT quota under that login or pollute the repo::login cache; also bound that cache.
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/api/github-bio/route.ts`:
- Around line 38-42: The eviction logic currently runs whenever cache.size >=
MAX_CACHE, which will evict the oldest entry even when updating an existing key;
change the logic to only evict when inserting a new key by checking
cache.has(key) first (i.e., if (!cache.has(key) && cache.size >= MAX_CACHE) then
find and delete the oldest), then proceed to cache.set(key, { at: Date.now(),
profile }); reference: cache, MAX_CACHE, key, cache.size, cache.delete,
cache.set.
🪄 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: 33951762-2ed5-4e5f-8db3-99ad12660472
📒 Files selected for processing (13)
src/app/api/github-bio/route.tssrc/app/api/gt/repos/[owner]/[name]/miners/route.tssrc/app/api/miner-works/route.tssrc/app/miners/_components/Headline.tsxsrc/app/miners/_components/MinerDistribution.tsxsrc/app/miners/_components/MinerModal.tsxsrc/app/miners/_components/MinerWorks.tsxsrc/app/miners/_components/Palette.tsxsrc/app/miners/_components/shared.tsxsrc/app/miners/page.module.csssrc/app/miners/page.tsxsrc/components/ActivityLineChart.tsxsrc/types/entities.ts
🚧 Files skipped from review as they are similar to previous changes (11)
- src/app/miners/_components/MinerDistribution.tsx
- src/app/miners/_components/Palette.tsx
- src/app/miners/_components/Headline.tsx
- src/app/miners/_components/MinerModal.tsx
- src/components/ActivityLineChart.tsx
- src/types/entities.ts
- src/app/miners/page.tsx
- src/app/miners/_components/shared.tsx
- src/app/api/gt/repos/[owner]/[name]/miners/route.ts
- src/app/miners/_components/MinerWorks.tsx
- src/app/api/miner-works/route.ts
| if (cache.size >= MAX_CACHE) { | ||
| const oldest = cache.keys().next().value; | ||
| if (oldest !== undefined) cache.delete(oldest); | ||
| } | ||
| cache.set(key, { at: Date.now(), profile }); |
There was a problem hiding this comment.
Avoid evicting on stale refresh of an existing key.
At Line 38, eviction runs whenever cache.size >= MAX_CACHE, even if key already exists. That drops an unrelated entry and can shrink effective cache size on refreshes, reducing hit rate.
Proposed fix
- if (cache.size >= MAX_CACHE) {
+ if (!cache.has(key) && cache.size >= MAX_CACHE) {
const oldest = cache.keys().next().value;
if (oldest !== undefined) cache.delete(oldest);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (cache.size >= MAX_CACHE) { | |
| const oldest = cache.keys().next().value; | |
| if (oldest !== undefined) cache.delete(oldest); | |
| } | |
| cache.set(key, { at: Date.now(), profile }); | |
| if (!cache.has(key) && cache.size >= MAX_CACHE) { | |
| const oldest = cache.keys().next().value; | |
| if (oldest !== undefined) cache.delete(oldest); | |
| } | |
| cache.set(key, { at: Date.now(), profile }); |
🤖 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/api/github-bio/route.ts` around lines 38 - 42, The eviction logic
currently runs whenever cache.size >= MAX_CACHE, which will evict the oldest
entry even when updating an existing key; change the logic to only evict when
inserting a new key by checking cache.has(key) first (i.e., if (!cache.has(key)
&& cache.size >= MAX_CACHE) then find and delete the oldest), then proceed to
cache.set(key, { at: Date.now(), profile }); reference: cache, MAX_CACHE, key,
cache.size, cache.delete, cache.set.
19th registered repo (added to gittensor 2026-06-09); keeps the "in the mines" working-age calc from falling back for contributions there.
Summary
Rebuilds the miners page — a full-viewport treemap headline with a live SN74 emission overview, an accurate per-repo emission and eligibility model, and a new miner detail modal (per-repo earnings, PR/issue works, activity and time-decay charts, and insights). Replaces the old side drawer and compare tray, and adds a shared activity line chart that the dashboard now uses too.
Screenshots
Related Issues
fixes #196
Type of Change
Testing
pnpm buildpassesChecklist
Summary by CodeRabbit
New Features
Improvements