diff --git a/docs/superpowers/plans/2026-06-16-glook-23-projects-sort-scale.md b/docs/superpowers/plans/2026-06-16-glook-23-projects-sort-scale.md
new file mode 100644
index 0000000..edcca35
--- /dev/null
+++ b/docs/superpowers/plans/2026-06-16-glook-23-projects-sort-scale.md
@@ -0,0 +1,181 @@
+# GLOOK-23: Projects Card Sort + Volume Bar
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Sort project cards by PRs+Jiras and add a segmented volume bar so users can instantly read project size and composition.
+
+**Architecture:** Single-file change to `src/components/ProjectsCard.tsx`. Inside `ProjectsBody`, sort the incoming array by `(estimated_prs + jira_count)` DESC, compute `maxVolume` across all projects, add a one-line legend before the list, and insert a `6px` segmented bar between the stats row and the summary text. Both the home-page (standalone) and team-page (collapsible) surfaces use the same `ProjectsBody` component and get the change automatically.
+
+**Tech Stack:** React, TypeScript, Tailwind CSS. No new dependencies.
+
+---
+
+## File Map
+
+| File | Change |
+|---|---|
+| `src/components/ProjectsCard.tsx` | Sort by PRs+Jiras; add `maxVolume` computation; add legend; add volume bar per row |
+
+---
+
+### Task 1: Add sort, legend, and volume bar to `ProjectsBody`
+
+**Files:**
+- Modify: `src/components/ProjectsCard.tsx:45-113`
+
+**Context:** `ProjectsBody` receives `projects: ProjectsCardItem[] | TeamProject[]`. Both interfaces already have `jira_count`, `estimated_commits`, and `estimated_prs`. The function currently renders projects in the order they arrive (LLM output order). After this change:
+
+1. Projects are sorted by `estimated_prs + jira_count` DESC before rendering.
+2. `maxVolume = max(estimated_prs + jira_count + estimated_commits)` computed once across all sorted projects.
+3. A small legend (`PRs · Jiras · Commits`) appears before the list.
+4. Each project row gets a `6px` tall bar inserted between the stats line and the summary text. Bar width = `(estimated_prs + jira_count + estimated_commits) / maxVolume * 100%`. Inside the bar three flex segments: PRs (cyan), Jiras (purple), Commits (ghost).
+
+No tests exist for this component — verify visually against the running app.
+
+- [ ] **Step 1: Replace the `ProjectsBody` function with the updated version**
+
+Find and replace the entire `ProjectsBody` function (lines 45–114 in the current file). The new version:
+
+```tsx
+function ProjectsBody({
+ projects,
+ loading,
+ emptyMessage,
+ developerHref,
+ variant,
+}: {
+ projects: ProjectsCardItem[] | TeamProject[];
+ loading?: boolean;
+ emptyMessage: string;
+ developerHref?: (login: string) => string;
+ variant: 'standalone' | 'collapsible';
+}) {
+ if (loading) {
+ return (
+
+
+ Analyzing projects…
+
+ );
+ }
+ if (projects.length === 0) {
+ return
{emptyMessage}
;
+ }
+
+ // Sort by meaningful output (PRs + Jiras). Commits are squashed into PRs so
+ // using raw commit count would inflate commit-heavy, low-PR projects.
+ const sorted = [...projects].sort(
+ (a, b) => (b.estimated_prs + b.jira_count) - (a.estimated_prs + a.jira_count),
+ );
+
+ // Max total volume across all projects — used to normalise bar widths.
+ const maxVolume = Math.max(
+ ...sorted.map(p => p.estimated_prs + p.jira_count + p.estimated_commits),
+ 1,
+ );
+
+ return (
+
+ );
+}
+```
+
+- [ ] **Step 2: Run the full test suite**
+
+```bash
+npm test --no-coverage
+```
+
+Expected: All tests pass (no changes to logic or data — pure UI addition).
+
+- [ ] **Step 3: Start the dev server and verify visually**
+
+```bash
+npm run dev
+```
+
+Open the home page and check:
+- Projects are sorted by PRs+Jiras (largest first, not LLM output order)
+- A small legend appears: `■ PRs ■ Jiras ░ Commits`
+- Each project has a `5px` bar between the stats and summary
+- Bar widths differ — the largest project fills 100%, others are proportionally shorter
+- Segment colours: cyan (PRs), purple (Jiras), near-invisible ghost (Commits)
+- Commit-heavy projects (low PRs/Jiras) rank lower and have large ghost tails
+
+Also open the team page and expand a Current Projects card — same visual should appear.
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add src/components/ProjectsCard.tsx
+git commit -m "feat(glook-23): sort projects by PRs+Jiras, add segmented volume bar"
+```
diff --git a/docs/superpowers/specs/2026-06-16-glook-23-projects-sort-scale-design.md b/docs/superpowers/specs/2026-06-16-glook-23-projects-sort-scale-design.md
new file mode 100644
index 0000000..f8ef32e
--- /dev/null
+++ b/docs/superpowers/specs/2026-06-16-glook-23-projects-sort-scale-design.md
@@ -0,0 +1,90 @@
+# GLOOK-23: Projects Card — Sort by Volume + Visual Scale Bar
+
+## Goal
+
+Sort project cards by meaningful output (PRs + Jiras) and add a visual volume bar so users can instantly read project size and composition without scanning numbers.
+
+## Problem
+
+The home-page and team-page "Current Projects" cards currently render projects in LLM-output order with only text counters (`5 jiras · ~30 commits · ~12 PRs`). There is no visual hierarchy, and commit counts visually dominate even though commits are squashed into PRs and are a lower-signal metric.
+
+## Design (validated with live Smartling data in visual companion)
+
+### Sorting
+
+Sort projects by **PRs + Jiras** descending (meaningful shipped output). Commits are excluded from the sort key because individual commits collapse into PRs — using commit count would inflate commit-heavy, low-PR projects like the Salesforce CC project, which had 116 commits but only 8 PRs and 9 Jiras.
+
+### Volume Bar
+
+Each project row gets a horizontal bar immediately below the stats line:
+
+```
+[cyan: PRs][purple: Jiras][ghost: Commits]
+|←————— bar width = total / max_total ——————→| ←— gray track (100%)
+```
+
+- **Track** (background): full width, `rgba(255,255,255,0.05)`, `6px` tall, rounded
+- **Fill** (foreground): `width = (estimated_prs + jira_count + estimated_commits) / maxVolume * 100%`
+ - Max is computed client-side from the full `projects` array before rendering
+- **Segments** inside fill (using CSS `flex` with raw counts as flex values):
+ - PRs → `#06B6D4` (cyan)
+ - Jiras → `#A855F7` (purple)
+ - Commits → `rgba(255,255,255,0.10)` (ghost — visible context, not dominant)
+- Segment order: PRs → Jiras → Commits (bright metrics first, ghost last)
+
+### Effect on real data (Salesforce CC example)
+
+With the old order, Salesforce CC ranked #2 (116 commits). With PRs+Jiras sort it ranks #8 (8 PRs, 9 Jiras). Its bar is 50% wide (total volume still significant) but the ghost section visually dominates — immediately communicating "this project is commit-heavy with little shipped output." No text change required; the visual explains itself.
+
+---
+
+## Architecture
+
+**One file changed: `src/components/ProjectsCard.tsx`**
+
+The `ProjectsBody` subcomponent receives `projects: ProjectsCardItem[] | TeamProject[]`. Both interfaces already have `jira_count`, `estimated_commits`, `estimated_prs`.
+
+Changes inside `ProjectsBody`:
+1. Sort the incoming `projects` array by `(p.estimated_prs + p.jira_count)` DESC before rendering.
+2. Compute `maxVolume = Math.max(...projects.map(p => p.estimated_prs + p.jira_count + p.estimated_commits), 1)`.
+3. For each project row, add the bar after the stats line and before the summary.
+
+```tsx
+// Sort by PRs + Jiras
+const sorted = [...projects].sort((a, b) =>
+ (b.estimated_prs + b.jira_count) - (a.estimated_prs + a.jira_count)
+);
+const maxVolume = Math.max(...sorted.map(p =>
+ p.estimated_prs + p.jira_count + p.estimated_commits
+), 1);
+
+// Inside the map:
+const totalVol = p.estimated_prs + p.jira_count + p.estimated_commits;
+const barWidth = (totalVol / maxVolume) * 100;
+// Render bar with flex segments: flex={p.estimated_prs}, flex={p.jira_count}, flex={p.estimated_commits}
+```
+
+This applies to **both surfaces** (home page and team page) because both use `ProjectsCard` / `ProjectsBody`.
+
+---
+
+## Legend
+
+No separate legend component needed. The `ProjectsCard` header area (or a small inline legend below the title) shows:
+- `■ PRs ■ Jiras ░ Commits`
+
+Position: a single line of 10px text added to the `ProjectsBody` before the list, only when `projects.length > 0`.
+
+---
+
+## Tests
+
+No unit tests needed — `ProjectsCard` is a pure UI component with no existing test coverage. The sort logic is a one-liner `useMemo` or inline sort; correctness is verified by visual inspection against the live app.
+
+---
+
+## Files Changed
+
+| File | Change |
+|---|---|
+| `src/components/ProjectsCard.tsx` | Sort by PRs+Jiras; add volume bar with ghost commits; add legend |
diff --git a/src/app/api/project-insights/route.ts b/src/app/api/project-insights/route.ts
index 46e35e2..062da50 100644
--- a/src/app/api/project-insights/route.ts
+++ b/src/app/api/project-insights/route.ts
@@ -26,6 +26,20 @@ async function getHandler() {
return NextResponse.json({ available: false });
}
+ // Actual org totals — used by the client to render an "Other" row showing
+ // activity not captured in the top-10 project clusters. Fetched before the
+ // cache check so both the cache-hit and fresh-generation paths can return them.
+ const [totalsRows] = await db.execute(
+ `SELECT COALESCE(SUM(total_commits), 0) AS commits, COALESCE(SUM(total_prs), 0) AS prs
+ FROM developer_stats WHERE report_id = ?`,
+ [report.id],
+ ) as [any[], any];
+ const totals = {
+ commits: Number(totalsRows[0]?.commits ?? 0),
+ prs: Number(totalsRows[0]?.prs ?? 0),
+ jiras: Number(jiraCount[0]?.cnt ?? 0),
+ };
+
// Check cache (reuse report_comparisons table — use report.id for both a and b to distinguish from real comparisons)
// Real comparisons always have different a and b. Project insights use same id for both.
const [cached] = await db.execute(
@@ -43,6 +57,7 @@ async function getHandler() {
available: true,
report: { id: report.id, org: report.org, periodDays: report.period_days, createdAt: report.created_at },
...rest,
+ totals,
cached: true,
});
}
@@ -218,6 +233,7 @@ ${noJiraData}${inflightBlock}`;
report: { id: report.id, org: report.org, periodDays: report.period_days, createdAt: report.created_at },
projects: toCache.projects,
untracked_work: toCache.untracked_work,
+ totals,
cached: false,
});
} catch (err) {
diff --git a/src/app/llm-findings.tsx b/src/app/llm-findings.tsx
index bb598dc..e285bb4 100644
--- a/src/app/llm-findings.tsx
+++ b/src/app/llm-findings.tsx
@@ -35,6 +35,7 @@ export default function LlmFindings() {
const [projects, setProjects] = useState([]);
const [untrackedWork, setUntrackedWork] = useState([]);
const [projectsMeta, setProjectsMeta] = useState<{ id: string; org: string; periodDays: number; createdAt: string } | null>(null);
+ const [projectTotals, setProjectTotals] = useState<{ commits: number; prs: number; jiras: number } | null>(null);
const [projectsLoading, setProjectsLoading] = useState(true);
// Release notes
@@ -85,6 +86,7 @@ export default function LlmFindings() {
setProjects(data.projects || []);
setUntrackedWork(data.untracked_work || []);
setProjectsMeta({ id: data.report.id, org: data.report.org, periodDays: data.report.periodDays, createdAt: data.report.createdAt });
+ if (data.totals) setProjectTotals(data.totals);
}
})
.catch(() => {})
@@ -143,6 +145,7 @@ export default function LlmFindings() {
title="Top Projects"
subtitle={projectsMeta ? `${projectsMeta.org} · ${projectsMeta.periodDays}d · ${formatDate(projectsMeta.createdAt)}` : undefined}
developerHref={projectsMeta ? (login) => `/report/${projectsMeta.id}/dev/${login}` : undefined}
+ actualTotals={projectTotals ?? undefined}
/>
)}
diff --git a/src/components/ProjectsCard.tsx b/src/components/ProjectsCard.tsx
index 7a34da7..7ed2b45 100644
--- a/src/components/ProjectsCard.tsx
+++ b/src/components/ProjectsCard.tsx
@@ -1,6 +1,14 @@
'use client';
+import { useMemo } from 'react';
import type { TeamProject } from '@/lib/team-pulse/types';
+// Segment colors used in both the legend swatches and the bar segments.
+const SEGMENT_COLORS = {
+ prs: '#06B6D4',
+ jiras: '#A855F7',
+ commits: 'rgba(255,255,255,0.10)',
+} as const;
+
/** Older shape used by the home-page LLM project insights — superset of TeamProject.
* Allows the same component to serve both surfaces without a refactor of the home payload. */
export interface ProjectsCardItem {
@@ -22,6 +30,10 @@ export interface ProjectsCardProps {
/** Optional: link template for developer chips. Receives login, returns href.
* If omitted, chips are not links. */
developerHref?: (login: string) => string;
+ /** When provided, an "Other (not in top N)" row is appended showing the
+ * activity not captured by the project clusters. Shown as a peer row with
+ * the same bar format so users can compare scale directly. */
+ actualTotals?: { commits: number; prs: number; jiras: number };
/** Collapsible mode (GLOOK-11): header becomes a toggle button styled to
* match , body hidden when collapsed. Controlled — parent
* owns `expanded` state via `onExpandedChange`. */
@@ -48,12 +60,14 @@ function ProjectsBody({
emptyMessage,
developerHref,
variant,
+ actualTotals,
}: {
projects: ProjectsCardItem[] | TeamProject[];
loading?: boolean;
emptyMessage: string;
developerHref?: (login: string) => string;
variant: 'standalone' | 'collapsible';
+ actualTotals?: { commits: number; prs: number; jiras: number };
}) {
if (loading) {
return (
@@ -69,12 +83,67 @@ function ProjectsBody({
if (projects.length === 0) {
return
{emptyMessage}
;
}
+
+ // Sort by meaningful output (PRs + Jiras). Commits excluded from sort key
+ // because they are squashed into PRs — including them would inflate rank for
+ // commit-heavy, low-PR projects. Commits are still shown in the bar for context.
+ const sorted = useMemo(
+ () => [...projects].sort((a, b) => (b.estimated_prs + b.jira_count) - (a.estimated_prs + a.jira_count)),
+ [projects],
+ );
+
+ // "Other" row: activity not attributed to any named project cluster.
+ // Only shown when actualTotals is provided (home page passes org totals from devStats).
+ const other = useMemo(() => {
+ if (!actualTotals) return null;
+ const sumCommits = sorted.reduce((s, p) => s + p.estimated_commits, 0);
+ const sumPrs = sorted.reduce((s, p) => s + p.estimated_prs, 0);
+ const sumJiras = sorted.reduce((s, p) => s + p.jira_count, 0);
+ const o = {
+ commits: Math.max(0, actualTotals.commits - sumCommits),
+ prs: Math.max(0, actualTotals.prs - sumPrs),
+ jiras: Math.max(0, actualTotals.jiras - sumJiras),
+ };
+ return (o.commits + o.prs + o.jiras) > 0 ? o : null;
+ }, [sorted, actualTotals]);
+
+ // Bar width = total volume (PRs + Jiras + Commits) / max across all projects.
+ // Commits are intentionally included here even though they are excluded from the
+ // sort key — the bar shows the full activity footprint of each project (sort rank
+ // reflects quality output, bar width reflects overall size).
+ // If "Other" is present, include it in maxVolume so bars are comparable.
+ const maxVolume = useMemo(() => {
+ const projectMax = sorted.reduce((max, p) => Math.max(max, p.estimated_prs + p.jira_count + p.estimated_commits), 1);
+ const otherTotal = other ? other.commits + other.prs + other.jiras : 0;
+ return Math.max(projectMax, otherTotal, 1);
+ }, [sorted, other]);
+
+ // Hide Jira legend entry when Jira is disabled (all counts are 0).
+ const hasJira = useMemo(() => sorted.some(p => p.jira_count > 0) || (other?.jiras ?? 0) > 0, [sorted, other]);
+
return (