diff --git a/platform/lib/translations.ts b/platform/lib/translations.ts index 7d8f1aa..940c10d 100644 --- a/platform/lib/translations.ts +++ b/platform/lib/translations.ts @@ -448,6 +448,8 @@ export const translations = { "Contributors with high velocity or 80%+ AI adoption across the org", badge: "Hyper Engineer", repos: "{count} repos", + identified: "Identified", + unidentified: "Unidentified (no linked GitHub account)", }, repoList: { empty: "No repositories yet.", @@ -1868,6 +1870,8 @@ export const translations = { "Contribuidores com alta velocidade ou 80%+ de adoção de IA na organização", badge: "Hyper Engineer", repos: "{count} repos", + identified: "Identificados", + unidentified: "Não identificados (sem conta do GitHub vinculada)", }, repoList: { empty: "Nenhum repositório ainda.", diff --git a/platform/src/app/[tenant]/dashboard/sections/HyperEngineers.tsx b/platform/src/app/[tenant]/dashboard/sections/HyperEngineers.tsx index a58661c..896cb4e 100644 --- a/platform/src/app/[tenant]/dashboard/sections/HyperEngineers.tsx +++ b/platform/src/app/[tenant]/dashboard/sections/HyperEngineers.tsx @@ -1,60 +1,102 @@ -'use client'; +"use client"; -import { GitHubAvatar } from '@/app/[tenant]/repos/[repoName]/github-avatar'; -import { useTranslation } from '@/hooks/useTranslation'; -import type { HyperEngineer } from '@/types/org-summary'; +import { GitHubAvatar } from "@/app/[tenant]/repos/[repoName]/github-avatar"; +import { useTranslation } from "@/hooks/useTranslation"; +import type { HyperEngineer } from "@/types/org-summary"; interface HyperEngineersProps { engineers: HyperEngineer[]; } +function EngineerCard({ + eng, + t, +}: { + eng: HyperEngineer; + t: ReturnType["t"]; +}) { + return ( +
+ {eng.github ? ( + + ) : ( +
+ {eng.name.charAt(0).toUpperCase()} +
+ )} + {eng.github ? ( + + {eng.name} + + ) : ( + {eng.name} + )} + 🏆 + {eng.repos > 1 && ( + + {t("dashboard.hyperEngineers.repos", { count: eng.repos })} + + )} +
+ ); +} + export function HyperEngineers({ engineers }: HyperEngineersProps) { const { t } = useTranslation(); if (engineers.length === 0) return null; + // Split by whether we could resolve a real GitHub identity. Two people + // (or two aliases of the same person iris/cli.py couldn't tie together — + // e.g. a personal email that isn't linked/verified on their GitHub + // account) can share a display name, so "identified" is the group we can + // actually confirm and link to a profile; "unidentified" is everyone else, + // shown separately rather than mixed in or hidden outright. + const identified = engineers.filter((eng) => eng.github); + const unidentified = engineers.filter((eng) => !eng.github); + return (
-

{t('dashboard.hyperEngineers.title')}

+

+ {t("dashboard.hyperEngineers.title")} +

- {t('dashboard.hyperEngineers.subtitle')} + {t("dashboard.hyperEngineers.subtitle")}

-
- {engineers.map((eng) => ( -
- {eng.github ? ( - - ) : ( -
- {eng.name.charAt(0).toUpperCase()} -
- )} - {eng.github ? ( - - {eng.name} - - ) : ( - {eng.name} - )} - 🏆 - {eng.repos > 1 && ( - - {t('dashboard.hyperEngineers.repos', { count: eng.repos })} - - )} + {identified.length > 0 && ( +
+ {unidentified.length > 0 && ( +

+ {t("dashboard.hyperEngineers.identified")} +

+ )} +
+ {identified.map((eng) => ( + + ))}
- ))} -
+
+ )} + + {unidentified.length > 0 && ( +
+

+ {t("dashboard.hyperEngineers.unidentified")} +

+
+ {unidentified.map((eng) => ( + + ))} +
+
+ )}
); } diff --git a/platform/tests/org-summary.test.ts b/platform/tests/org-summary.test.ts index 23af816..495933b 100644 --- a/platform/tests/org-summary.test.ts +++ b/platform/tests/org-summary.test.ts @@ -391,7 +391,7 @@ describe("computeHyperEngineers — dedupes the same person across name variants expect(result[0].repos).toBe(2); }); - it("merges two display-name variants sharing a GitHub noreply email", () => { + it("merges two display-name variants sharing a GitHub noreply email, surfacing github from userMap once merged", () => { const payloads = new Map([ [ "repo-a", @@ -420,13 +420,22 @@ describe("computeHyperEngineers — dedupes the same person across name variants }), ], ]); + // Nothing in nameToGithub — the shared noreply email is what ties the + // two name variants into one group; userMap (keyed by the normalized + // email, which for a noreply address IS the github username) is what + // then supplies the github field for display. + const userMap = new Map([ + [ + "renatoguimaraescb", + { name: "Renato Guimarães", github: "renatoguimaraescb" }, + ], + ]); - // No github resolved anywhere — only the shared noreply email ties - // the two name variants together. - const result = computeHyperEngineers(payloads, new Map(), new Map()); + const result = computeHyperEngineers(payloads, userMap, new Map()); expect(result).toHaveLength(1); expect(result[0].repos).toBe(2); + expect(result[0].github).toBe("renatoguimaraescb"); }); it("keeps genuinely different people separate", () => { @@ -448,9 +457,38 @@ describe("computeHyperEngineers — dedupes the same person across name variants }), ], ]); + const nameToGithub = new Map([ + ["alice", "alice-gh"], + ["bob", "bob-gh"], + ]); - const result = computeHyperEngineers(payloads, new Map(), new Map()); + const result = computeHyperEngineers(payloads, new Map(), nameToGithub); expect(result).toHaveLength(2); }); + + it("still includes an engineer with no resolved GitHub username, with github left undefined", () => { + // computeHyperEngineers doesn't filter these out — the "identified" vs + // "unidentified" split is a display concern, done in HyperEngineers.tsx. + const payloads = new Map([ + [ + "repo-a", + payload({ + author_velocity: { + authors: [ + hyperAuthor({ + name: "Mystery Person", + email: "mystery@corp.com", + }), + ], + }, + }), + ], + ]); + + const result = computeHyperEngineers(payloads, new Map(), new Map()); + + expect(result).toHaveLength(1); + expect(result[0].github).toBeUndefined(); + }); });