Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions src/api/MirrorDashboardApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Mirror dashboard API — single-call replacements for the dashboard's
// per-miner fan-out against `/miners/<id>/issues`. The mirror endpoints
// return roster-blind data; callers blend with the gittensor miner roster
// client-side to filter to subnet authors.
import { useMirrorApiQuery } from './ApiUtils';
import {
type MirrorDashboardIssue,
type MirrorDashboardIssuesResponse,
} from './models';

/**
* Slim issue rows for dashboard trend aggregation.
*
* One bulk call replaces the N per-miner calls previously fanned out via
* `useMinersIssues`. Returns every issue created on or after `since`, plus
* every CLOSED issue closed on or after `since`. Authorship is preserved
* via `author_github_id` so the dashboard can filter to subnet miners.
*/
export const useMirrorDashboardIssues = (since: string, enabled?: boolean) =>
useMirrorApiQuery<MirrorDashboardIssuesResponse, MirrorDashboardIssue[]>(
'useMirrorDashboardIssues',
`/dashboard/issues?since=${encodeURIComponent(since)}`,
{
enabled,
select: (data) => data?.issues ?? [],
},
);
1 change: 1 addition & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export * from './DashboardApi';
export * from './IssuesApi';
export * from './MinerApi';
export * from './MirrorApi';
export * from './MirrorDashboardApi';
export * from './PrsApi';
export * from './ReposApi';
export * from './SearchApi';
Expand Down
21 changes: 21 additions & 0 deletions src/api/models/MirrorDashboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Mirror API (snake_case) — slim issue rows returned by
// `/api/v1/dashboard/issues`. The mirror is intentionally roster-blind: every
// row is returned regardless of author. The dashboard blends with the
// gittensor miner roster client-side to filter to subnet authors.

export interface MirrorDashboardIssue {
repo_full_name: string;
issue_number: number;
author_github_id: string | null;
created_at: string;
closed_at: string | null;
state: 'OPEN' | 'CLOSED' | string;
state_reason: string | null;
solving_pr: { merged_at: string } | null;
}

export interface MirrorDashboardIssuesResponse {
since: string;
generated_at: string;
issues: MirrorDashboardIssue[];
}
1 change: 1 addition & 0 deletions src/api/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export * from './Dashboard';
export * from './DatasetState';
export * from './Issues';
export * from './Miner';
export * from './MirrorDashboard';
export * from './Configurations';
49 changes: 15 additions & 34 deletions src/pages/dashboard/dashboardData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import {
type CommitLog,
type MinerEvaluation,
type MinerIssue,
type MirrorDashboardIssue,
type Repository,
} from '../../api';
import {
Expand Down Expand Up @@ -106,7 +106,7 @@ interface FeaturedWorkConfig {
const HOUR_MS = 60 * 60 * 1000;
const DAY_MS = 24 * HOUR_MS;
const WEEK_MS = 7 * DAY_MS;
const GITTENSOR_START_MS = Date.UTC(2025, 11, 1, 0, 0, 0);
export const GITTENSOR_START_MS = Date.UTC(2025, 11, 1, 0, 0, 0);

const RANGE_CONFIG: Record<
PresetTimeRange,
Expand Down Expand Up @@ -170,40 +170,21 @@ export const getPreviousWindowBounds = (
};
};

// Always send `since` — the mirror's `/issues` endpoint returns only currently
// OPEN issues when `since` is omitted, which silently zeroes the resolved-issues
// trend. Fetch the full subnet history once (back to GITTENSOR_START_MS) so the
// cache key is stable across every range button: switching 1D/7D/35D/All
// re-buckets the already-fetched data instead of re-firing the N mirror calls.
export const getMirrorSinceParam = (): string =>
new Date(GITTENSOR_START_MS).toISOString();

// Dedupe by (repo, number) so an issue surfaced under multiple miners is counted once.
export const flattenMinerIssues = (
responses: ReadonlyArray<ReadonlyArray<MinerIssue>>,
): MinerIssue[] => {
const seen = new Set<string>();
const flattened: MinerIssue[] = [];
responses.forEach((batch) => {
batch.forEach((issue) => {
const key = `${issue.repo_full_name}#${issue.issue_number}`;
if (seen.has(key)) return;
seen.add(key);
flattened.push(issue);
});
});
return flattened;
};

export const isResolvedMinerIssue = (issue: MinerIssue): boolean =>
issue.state === 'CLOSED' && issue.state_reason === 'COMPLETED';
// A "truly resolved" issue: closed as completed AND the linked PR is merged.
// The conjunction matters — state_reason alone misses cases where GitHub
// doesn't set 'completed', and solving_pr.merged_at alone counts not-planned
// closures with stray PR links.
export const isResolvedMinerIssue = (issue: MirrorDashboardIssue): boolean =>
issue.state === 'CLOSED' &&
issue.state_reason === 'COMPLETED' &&
!!issue.solving_pr?.merged_at;

export const isResolvedInWindow = (
issue: MinerIssue,
issue: MirrorDashboardIssue,
window: WindowBounds,
): boolean =>
isResolvedMinerIssue(issue) &&
isWithinWindow(toTimestamp(issue.closed_at), window);
isWithinWindow(toTimestamp(issue.solving_pr?.merged_at), window);

const getUtcWeekStart = (timestamp: number) => {
const date = new Date(timestamp);
Expand Down Expand Up @@ -313,7 +294,7 @@ const formatDelta = (

export const buildDashboardTrendData = (
prs: CommitLog[],
issues: MinerIssue[],
issues: MirrorDashboardIssue[],
range: TrendTimeRange,
now = new Date(),
): { labels: string[]; series: DashboardTrendSeries[] } => {
Expand All @@ -324,7 +305,7 @@ export const buildDashboardTrendData = (
);
const resolvedIssueTimestamps = issues
.filter(isResolvedMinerIssue)
.map((issue) => toTimestamp(issue.closed_at));
.map((issue) => toTimestamp(issue.solving_pr?.merged_at));
const buckets = buildTrendBuckets(
[
...mergedPrTimestamps,
Expand Down Expand Up @@ -653,7 +634,7 @@ export const buildDashboardOverview = (

export const buildDashboardKpis = (
prs: CommitLog[],
issues: MinerIssue[],
issues: MirrorDashboardIssue[],
range: TrendTimeRange,
now = new Date(),
): DashboardKpi[] => {
Expand Down
63 changes: 29 additions & 34 deletions src/pages/dashboard/useDashboardData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ import {
useAllMiners,
useAllPrs,
useIssues,
useMinersIssues,
useMirrorDashboardIssues,
useReposAndWeights,
} from '../../api';
import {
type CommitLog,
type DatasetState,
type IssueBounty,
type MinerEvaluation,
type MinerIssue,
type MirrorDashboardIssue,
type Repository,
} from '../../api/models';
import {
Expand All @@ -30,8 +30,7 @@ import {
buildFeaturedContributors,
buildFeaturedWork,
buildFeaturedDiscoveryContributors,
flattenMinerIssues,
getMirrorSinceParam,
GITTENSOR_START_MS,
type TrendTimeRange,
} from './dashboardData';

Expand All @@ -40,49 +39,45 @@ type DashboardDatasets = {
miners: DatasetState<MinerEvaluation>;
issues: DatasetState<IssueBounty>;
repos: DatasetState<Repository>;
minerIssues: DatasetState<MinerIssue>;
minerIssues: DatasetState<MirrorDashboardIssue>;
};

const hasIssueActivity = (miner: MinerEvaluation): boolean =>
(miner.totalSolvedIssues ?? 0) +
(miner.totalOpenIssues ?? 0) +
(miner.totalClosedIssues ?? 0) >
0;
// Pinned once per module load — same `since` for every dashboard mount keeps
// the React Query cache key stable across renders and route remounts.
const DASHBOARD_ISSUES_SINCE_ISO = new Date(GITTENSOR_START_MS).toISOString();

export const useDashboardData = (range: TrendTimeRange) => {
const prsQuery = useAllPrs();
const minersQuery = useAllMiners();
const issuesQuery = useIssues();
const reposQuery = useReposAndWeights();

// Fan-out per-miner mirror calls; gate on issue activity to bound parallel requests.
const activeMinerGithubIds = useMemo(
() =>
(minersQuery.data ?? [])
.filter(hasIssueActivity)
.map((m) => m.githubId)
.filter((id): id is string => Boolean(id)),
[minersQuery.data],
// Single bulk mirror call replaces the previous per-miner fan-out.
// The mirror is roster-blind; we filter to subnet authors below using the
// gittensor miner roster.
const dashboardIssuesQuery = useMirrorDashboardIssues(
DASHBOARD_ISSUES_SINCE_ISO,
);

// Pin the `since` cutoff once per component lifetime so the cache key never
// changes — range button clicks (1D/7D/35D/All) re-bucket the already-fetched
// data instead of re-firing the N mirror calls.
const minerIssuesSince = useMemo(() => getMirrorSinceParam(), []);
const minerIssuesQueries = useMinersIssues(
activeMinerGithubIds,
activeMinerGithubIds.length > 0,
minerIssuesSince,
);
const minerGithubIdSet = useMemo(() => {
const set = new Set<string>();
(minersQuery.data ?? []).forEach((m) => {
if (m.githubId) set.add(m.githubId);
});
return set;
}, [minersQuery.data]);

const minerIssuesData = useMemo<MinerIssue[]>(
() => flattenMinerIssues(minerIssuesQueries.map((q) => q.data ?? [])),
[minerIssuesQueries],
const minerIssuesData = useMemo<MirrorDashboardIssue[]>(
() =>
(dashboardIssuesQuery.data ?? []).filter(
(issue) =>
!!issue.author_github_id &&
minerGithubIdSet.has(issue.author_github_id),
),
[dashboardIssuesQuery.data, minerGithubIdSet],
);
const isMinerIssuesLoading =
activeMinerGithubIds.length > 0 &&
minerIssuesQueries.some((q) => q.isLoading);
const isMinerIssuesError = minerIssuesQueries.some((q) => q.isError);
const isMinerIssuesLoading = dashboardIssuesQuery.isLoading;
const isMinerIssuesError = dashboardIssuesQuery.isError;

const datasets: DashboardDatasets = {
prs: {
Expand Down
Loading