Skip to content

Commit 7dbbc15

Browse files
committed
feat(glook-23): add Other row showing unattributed activity
1 parent f8fab87 commit 7dbbc15

3 files changed

Lines changed: 91 additions & 5 deletions

File tree

src/app/api/project-insights/route.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,20 @@ async function getHandler() {
2626
return NextResponse.json({ available: false });
2727
}
2828

29+
// Actual org totals — used by the client to render an "Other" row showing
30+
// activity not captured in the top-10 project clusters. Fetched before the
31+
// cache check so both the cache-hit and fresh-generation paths can return them.
32+
const [totalsRows] = await db.execute(
33+
`SELECT COALESCE(SUM(total_commits), 0) AS commits, COALESCE(SUM(total_prs), 0) AS prs
34+
FROM developer_stats WHERE report_id = ?`,
35+
[report.id],
36+
) as [any[], any];
37+
const totals = {
38+
commits: Number(totalsRows[0]?.commits ?? 0),
39+
prs: Number(totalsRows[0]?.prs ?? 0),
40+
jiras: Number(jiraCount[0].cnt),
41+
};
42+
2943
// Check cache (reuse report_comparisons table — use report.id for both a and b to distinguish from real comparisons)
3044
// Real comparisons always have different a and b. Project insights use same id for both.
3145
const [cached] = await db.execute(
@@ -43,6 +57,7 @@ async function getHandler() {
4357
available: true,
4458
report: { id: report.id, org: report.org, periodDays: report.period_days, createdAt: report.created_at },
4559
...rest,
60+
totals,
4661
cached: true,
4762
});
4863
}
@@ -218,6 +233,7 @@ ${noJiraData}${inflightBlock}`;
218233
report: { id: report.id, org: report.org, periodDays: report.period_days, createdAt: report.created_at },
219234
projects: toCache.projects,
220235
untracked_work: toCache.untracked_work,
236+
totals,
221237
cached: false,
222238
});
223239
} catch (err) {

src/app/llm-findings.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export default function LlmFindings() {
3535
const [projects, setProjects] = useState<ProjectInsight[]>([]);
3636
const [untrackedWork, setUntrackedWork] = useState<UntrackedWork[]>([]);
3737
const [projectsMeta, setProjectsMeta] = useState<{ id: string; org: string; periodDays: number; createdAt: string } | null>(null);
38+
const [projectTotals, setProjectTotals] = useState<{ commits: number; prs: number; jiras: number } | null>(null);
3839
const [projectsLoading, setProjectsLoading] = useState(true);
3940

4041
// Release notes
@@ -85,6 +86,7 @@ export default function LlmFindings() {
8586
setProjects(data.projects || []);
8687
setUntrackedWork(data.untracked_work || []);
8788
setProjectsMeta({ id: data.report.id, org: data.report.org, periodDays: data.report.periodDays, createdAt: data.report.createdAt });
89+
if (data.totals) setProjectTotals(data.totals);
8890
}
8991
})
9092
.catch(() => {})
@@ -143,6 +145,7 @@ export default function LlmFindings() {
143145
title="Top Projects"
144146
subtitle={projectsMeta ? `${projectsMeta.org} · ${projectsMeta.periodDays}d · ${formatDate(projectsMeta.createdAt)}` : undefined}
145147
developerHref={projectsMeta ? (login) => `/report/${projectsMeta.id}/dev/${login}` : undefined}
148+
actualTotals={projectTotals ?? undefined}
146149
/>
147150
)}
148151

src/components/ProjectsCard.tsx

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ export interface ProjectsCardProps {
3030
/** Optional: link template for developer chips. Receives login, returns href.
3131
* If omitted, chips are not links. */
3232
developerHref?: (login: string) => string;
33+
/** When provided, an "Other (not in top N)" row is appended showing the
34+
* activity not captured by the project clusters. Shown as a peer row with
35+
* the same bar format so users can compare scale directly. */
36+
actualTotals?: { commits: number; prs: number; jiras: number };
3337
/** Collapsible mode (GLOOK-11): header becomes a toggle button styled to
3438
* match <TeamPulseCard>, body hidden when collapsed. Controlled — parent
3539
* owns `expanded` state via `onExpandedChange`. */
@@ -56,12 +60,14 @@ function ProjectsBody({
5660
emptyMessage,
5761
developerHref,
5862
variant,
63+
actualTotals,
5964
}: {
6065
projects: ProjectsCardItem[] | TeamProject[];
6166
loading?: boolean;
6267
emptyMessage: string;
6368
developerHref?: (login: string) => string;
6469
variant: 'standalone' | 'collapsible';
70+
actualTotals?: { commits: number; prs: number; jiras: number };
6571
}) {
6672
if (loading) {
6773
return (
@@ -86,17 +92,34 @@ function ProjectsBody({
8692
[projects],
8793
);
8894

95+
// "Other" row: activity not attributed to any named project cluster.
96+
// Only shown when actualTotals is provided (home page passes org totals from devStats).
97+
const other = useMemo(() => {
98+
if (!actualTotals) return null;
99+
const sumCommits = sorted.reduce((s, p) => s + p.estimated_commits, 0);
100+
const sumPrs = sorted.reduce((s, p) => s + p.estimated_prs, 0);
101+
const sumJiras = sorted.reduce((s, p) => s + p.jira_count, 0);
102+
const o = {
103+
commits: Math.max(0, actualTotals.commits - sumCommits),
104+
prs: Math.max(0, actualTotals.prs - sumPrs),
105+
jiras: Math.max(0, actualTotals.jiras - sumJiras),
106+
};
107+
return (o.commits + o.prs + o.jiras) > 0 ? o : null;
108+
}, [sorted, actualTotals]);
109+
89110
// Bar width = total volume (PRs + Jiras + Commits) / max across all projects.
90111
// Commits are intentionally included here even though they are excluded from the
91112
// sort key — the bar shows the full activity footprint of each project (sort rank
92113
// reflects quality output, bar width reflects overall size).
93-
const maxVolume = useMemo(
94-
() => sorted.reduce((max, p) => Math.max(max, p.estimated_prs + p.jira_count + p.estimated_commits), 1),
95-
[sorted],
96-
);
114+
// If "Other" is present, include it in maxVolume so bars are comparable.
115+
const maxVolume = useMemo(() => {
116+
const projectMax = sorted.reduce((max, p) => Math.max(max, p.estimated_prs + p.jira_count + p.estimated_commits), 1);
117+
const otherTotal = other ? other.commits + other.prs + other.jiras : 0;
118+
return Math.max(projectMax, otherTotal, 1);
119+
}, [sorted, other]);
97120

98121
// Hide Jira legend entry when Jira is disabled (all counts are 0).
99-
const hasJira = useMemo(() => sorted.some(p => p.jira_count > 0), [sorted]);
122+
const hasJira = useMemo(() => sorted.some(p => p.jira_count > 0) || (other?.jiras ?? 0) > 0, [sorted, other]);
100123

101124
return (
102125
<div className={`space-y-3 ${variant === 'collapsible' ? 'mt-3' : 'mt-4'}`}>
@@ -174,6 +197,47 @@ function ProjectsBody({
174197
</div>
175198
);
176199
})}
200+
201+
{/* "Other" row — same visual style as a project, italic label, no summary/devs */}
202+
{other && (() => {
203+
const otherTotal = other.commits + other.prs + other.jiras;
204+
const otherBarPct = (otherTotal / maxVolume) * 100;
205+
return (
206+
<div
207+
className="rounded-lg p-3"
208+
style={{ background: 'rgba(255,255,255,0.01)', border: '1px solid rgba(255,255,255,0.06)' }}
209+
>
210+
<div className="flex items-start justify-between gap-3 mb-1">
211+
<div className="flex items-center gap-2 min-w-0">
212+
<span className="text-xs text-gray-700 w-4 shrink-0 text-right">~</span>
213+
<span className="text-sm text-gray-500 italic">Other (not in top {sorted.length})</span>
214+
</div>
215+
<div className="flex items-center gap-3 shrink-0 text-[11px] text-gray-600">
216+
<span>~{other.jiras} jiras</span>
217+
<span>~{other.commits} commits</span>
218+
<span>~{other.prs} PRs</span>
219+
</div>
220+
</div>
221+
{otherTotal > 0 && (
222+
<div className="pl-6 mb-1.5">
223+
<div
224+
className="h-[5px] rounded-sm overflow-hidden"
225+
style={{ background: 'rgba(255,255,255,0.05)' }}
226+
role="img"
227+
aria-label={`Other: ${other.prs} PRs, ${other.jiras} Jiras, ${other.commits} commits not attributed to a named project`}
228+
>
229+
<div className="h-full flex" style={{ width: `${otherBarPct}%` }}>
230+
<div style={{ flex: other.prs, background: SEGMENT_COLORS.prs }} />
231+
<div style={{ flex: other.jiras, background: SEGMENT_COLORS.jiras }} />
232+
<div style={{ flex: other.commits, background: SEGMENT_COLORS.commits }} />
233+
</div>
234+
</div>
235+
</div>
236+
)}
237+
<div className="text-[10px] text-gray-700 pl-6">Activity not attributed to a named project</div>
238+
</div>
239+
);
240+
})()}
177241
</div>
178242
);
179243
}
@@ -185,6 +249,7 @@ export default function ProjectsCard({
185249
subtitle,
186250
emptyMessage = 'No active projects in this window.',
187251
developerHref,
252+
actualTotals,
188253
collapsible = false,
189254
expanded = true,
190255
onExpandedChange,
@@ -227,6 +292,7 @@ export default function ProjectsCard({
227292
emptyMessage={emptyMessage}
228293
developerHref={developerHref}
229294
variant="collapsible"
295+
actualTotals={actualTotals}
230296
/>
231297
</div>
232298
)}
@@ -251,6 +317,7 @@ export default function ProjectsCard({
251317
emptyMessage={emptyMessage}
252318
developerHref={developerHref}
253319
variant="standalone"
320+
actualTotals={actualTotals}
254321
/>
255322
</div>
256323
);

0 commit comments

Comments
 (0)