diff --git a/docs/skills/factory-dashboard-content.md b/docs/skills/factory-dashboard-content.md index 102209cc..770aa7c6 100644 --- a/docs/skills/factory-dashboard-content.md +++ b/docs/skills/factory-dashboard-content.md @@ -70,14 +70,22 @@ Contribution setup and hosted leaderboard links use hosted Hive for interactive contribution flows; do not recreate its setup UI in the docs dashboard. -Individual player records are available at -`/api/leaderboard/contributor/{username}`. Use that verified route for player -cards: the hosted Hive has no public HTML profile route for arbitrary users. +Individual contributor cards and rows link to +`https://hosted-projectbluefin-common-nmq5.hive.hivecommons.dev/contribute/dossier/{username}`. +The dossier owns contributor-specific Hive statistics and milestones. `/leaderboards` is a standalone docs page, not a Factory tab. It owns the shared Hive data provider directly; do not add top-level pages to `FACTORY_ROUTES`. +`scripts/fetch-hive-history.js` derives its contributor scope from the +`projectbluefin` Hive registry entry's `repos` array. Its fallback is only a +last verified registry snapshot for source outages; do not use it as the normal +repository scope. + +The public Hive registry accepts anonymous requests. Do not forward GitHub +authorization to it. + ### countme: match ublue-os/countme, and never trust the seed on its own The adoption numbers come from Fedora's public countme totals CSV diff --git a/scripts/fetch-hive-history.js b/scripts/fetch-hive-history.js index ac42f735..4e901af0 100644 --- a/scripts/fetch-hive-history.js +++ b/scripts/fetch-hive-history.js @@ -75,23 +75,26 @@ const MAX_WEEKS = 52; // leaderboard view (only ~33 contributors are active in a given year). const MAX_WEEKLY_SERIES = 100; -// All active factory repos -const FACTORY_REPOS = [ - "bluefin", +// Last verified registry set. The public registry normally supplies this list. +const FALLBACK_FACTORY_REPOS = [ "common", - "documentation", - "actions", + "bluefin", "bluefin-lts", - "dakota", - "bonedigger", - "bootc-installer", - "knuckle", + "actions", "testsuite", + "server", + "fsdk-containers", + "finpilot", + "dakota-iso", + "utah", + "utah-packages", + "lab", + "documentation", "website", - "brew", - "iso", - "wolfictl", - "fisherman", + "review", + "bootc-installer", + "bluefin-bling", + "knuckle", ]; // GitHub bot accounts to exclude from human contributor lists @@ -107,6 +110,8 @@ const BOT_LOGINS = new Set([ const GH_TOKEN = process.env.GITHUB_TOKEN || process.env.GH_TOKEN || ""; const GH_API = "https://api.github.com"; +const REGISTRY_URL = "https://hive.hivecommons.dev/api/registry"; +const TARGET_ORG = "projectbluefin"; function ghHeaders() { const h = { "User-Agent": "bluefin-hive-history/1.0" }; @@ -114,10 +119,36 @@ function ghHeaders() { return h; } +function registryHeaders() { + return { "User-Agent": "bluefin-hive-history/1.0" }; +} + function safeNum(v) { return typeof v === "number" && isFinite(v) ? v : undefined; } +function trackedProjectRepos(data) { + const hive = data?.hives?.find((entry) => entry?.org === TARGET_ORG); + return Array.isArray(hive?.repos) + ? [...new Set(hive.repos.filter((repo) => typeof repo === "string"))] + : []; +} + +async function fetchTrackedProjectRepos() { + try { + const res = await fetch(REGISTRY_URL, { headers: registryHeaders() }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const repos = trackedProjectRepos(await res.json()); + if (repos.length === 0) throw new Error(`no ${TARGET_ORG} repos`); + return repos; + } catch (err) { + console.warn( + `[hive-history] Hive registry unavailable (${err.message}) — using the last known repository set`, + ); + return FALLBACK_FACTORY_REPOS; + } +} + function extractMetrics(data) { if (!data) return null; const gov = (typeof data.governor === "object" && data.governor) || {}; @@ -157,12 +188,12 @@ function extractMetrics(data) { * handle pagination, aggregate into { login: totalCommits }. * Skips 404s and 403s gracefully. */ -async function fetchContributors() { +async function fetchContributors(repos = FALLBACK_FACTORY_REPOS) { const totals = {}; const byRepo = {}; await Promise.allSettled( - FACTORY_REPOS.map(async (repo) => { + repos.map(async (repo) => { const repoMap = {}; let url = `${GH_API}/repos/projectbluefin/${repo}/contributors?per_page=100&anon=false`; let pages = 0; @@ -342,12 +373,12 @@ function finalizeContributorStats( * Returns: { stats: { [login]: { total, lastWeek, lastMonth, last3Months, byRepo, weeks } }, * weekStarts: number[] } */ -async function fetchContributorWeeklyStats() { +async function fetchContributorWeeklyStats(repos = FALLBACK_FACTORY_REPOS) { const windows = computeStatsWindows(); const acc = createStatsAccumulator(); await Promise.allSettled( - FACTORY_REPOS.map(async (repo) => { + repos.map(async (repo) => { const url = `${GH_API}/repos/projectbluefin/${repo}/stats/contributors`; let attempts = 0; let data = null; @@ -410,6 +441,7 @@ async function main() { if (!history.contributorStats) history.contributorStats = {}; if (!Array.isArray(history.contributorWeekStarts)) history.contributorWeekStarts = []; + const trackedRepos = await fetchTrackedProjectRepos(); // ── Fetch hive snapshot ────────────────────────────────────────────────── let metrics = null; @@ -468,7 +500,7 @@ async function main() { "[hive-history] Fetching all-time contributor counts from factory repos...", ); try { - const { totals, byRepo } = await fetchContributors(); + const { totals, byRepo } = await fetchContributors(trackedRepos); history.contributors = totals; history.contributorsByRepo = byRepo; history.lastContributorFetch = new Date().toISOString(); @@ -497,7 +529,7 @@ async function main() { "[hive-history] Fetching weekly contributor stats (stats/contributors)...", ); try { - const stats = await fetchContributorWeeklyStats(); + const stats = await fetchContributorWeeklyStats(trackedRepos); history.contributorStats = stats.stats; history.contributorWeekStarts = stats.weekStarts; history.lastWeeklyStatsFetch = new Date().toISOString(); @@ -541,4 +573,6 @@ module.exports = { finalizeContributorStats, MAX_WEEKLY_SERIES, MAX_WEEKS, + registryHeaders, + trackedProjectRepos, }; diff --git a/scripts/fetch-hive-history.test.js b/scripts/fetch-hive-history.test.js index 9eb0a0d4..c1be8025 100644 --- a/scripts/fetch-hive-history.test.js +++ b/scripts/fetch-hive-history.test.js @@ -8,6 +8,8 @@ const { extractMetrics, finalizeContributorStats, MAX_WEEKS, + registryHeaders, + trackedProjectRepos, } = require("./fetch-hive-history.js"); const WEEK = 7 * 86400; @@ -269,6 +271,27 @@ test("computeStatsWindows returns ordered unix-second cut-offs", () => { assert.ok(threeMonthsAgo < monthAgo && monthAgo < weekAgo); }); +test("tracked Project Bluefin repositories come from the Hive registry", () => { + assert.deepEqual( + trackedProjectRepos({ + hives: [ + { org: "other", repos: ["ignored"] }, + { + org: "projectbluefin", + repos: ["common", "server", "fsdk-containers"], + }, + ], + }), + ["common", "server", "fsdk-containers"], + ); +}); + +test("Hive registry requests never include GitHub authorization", () => { + assert.deepEqual(registryHeaders(), { + "User-Agent": "bluefin-hive-history/1.0", + }); +}); + // extractMetrics is the sole reader of the live hive payload. Every field it // emits is a history data point, and a wrong-but-plausible value here is not a // crash — it is a wrong chart. These tests pin the fallbacks and the coercion. diff --git a/scripts/leaderboards-standalone.test.js b/scripts/leaderboards-standalone.test.js index 411162cb..77aa2a37 100644 --- a/scripts/leaderboards-standalone.test.js +++ b/scripts/leaderboards-standalone.test.js @@ -90,7 +90,7 @@ function loadRoutes() { return mod.exports; } -test("Hive-only contributors have a hosted player record link", () => { +test("contributor rows link to their hosted dossiers", () => { const { ContributorLeaderboard } = loadDashboard(); assert.equal( typeof ContributorLeaderboard, @@ -131,10 +131,14 @@ test("Hive-only contributors have a hosted player record link", () => { assert.match(html, /hive-only/); assert.match(html, /4 Hive tasks/); - assert.match( - html, - /href="https:\/\/hosted-projectbluefin-knuckle-gjvq\.hive\.hivecommons\.dev\/api\/leaderboard\/contributor\/hive-only"/, - ); + for (const login of ["established", "hive-only"]) { + assert.match( + html, + new RegExp( + `href="https://hosted-projectbluefin-common-nmq5\\.hive\\.hivecommons\\.dev/contribute/dossier/${login}"`, + ), + ); + } }); test("leaderboards stay outside the Factory tab registry", () => { @@ -188,7 +192,7 @@ test("the standalone page includes linked Hive task cards", () => { assert.match(html, /zulu-player/); assert.match( html, - /href="https:\/\/hosted-projectbluefin-knuckle-gjvq\.hive\.hivecommons\.dev\/api\/leaderboard\/contributor\/zulu-player"/, + /href="https:\/\/hosted-projectbluefin-common-nmq5\.hive\.hivecommons\.dev\/contribute\/dossier\/zulu-player"/, ); const taskCards = html.slice(html.indexOf("Hive Task Leaderboard")); assert.ok( diff --git a/src/components/HiveFactoryDashboard.module.css b/src/components/HiveFactoryDashboard.module.css index 6f2dd222..860925b7 100644 --- a/src/components/HiveFactoryDashboard.module.css +++ b/src/components/HiveFactoryDashboard.module.css @@ -2111,6 +2111,137 @@ text-transform: none; } +.leaderboards .contributionLink, +.leaderboards .lbNewcomer, +.leaderboards .hiveTaskCard { + background: var(--ifm-card-background-color); + border-color: var(--ifm-color-emphasis-200); + border-radius: var(--ifm-global-radius); + color: var(--ifm-font-color-base); + padding: 0.9rem 1rem; + transition: + background-color 0.1s ease, + border-color 0.1s ease; +} + +.leaderboards .contributionLink:hover, +.leaderboards .lbNewcomer:hover, +.leaderboards .hiveTaskCard:hover { + background-color: var(--ifm-color-emphasis-100); + border-color: var(--ifm-color-primary); + color: var(--ifm-font-color-base); +} + +.leaderboards .contributionLinkTitle, +.leaderboards .hiveTaskPlayer { + color: var(--ifm-heading-color); +} + +.leaderboards .contributionLinkPath, +.leaderboards .hiveTaskCount, +.leaderboards .lbNewcomerStat { + color: var(--ifm-color-emphasis-700); +} + +.leaderboards .lbNewcomers { + background: var( + --ifm-color-primary-contrast-background, + rgba(74, 105, 189, 0.1) + ); + border-color: var(--ifm-color-primary); + border-radius: var(--ifm-global-radius); + padding: 1rem; +} + +.leaderboards .lbTabs { + border-color: var(--ifm-color-emphasis-200); + gap: 0.75rem; +} + +.leaderboards .lbTab { + border-color: var(--ifm-color-emphasis-300); + border-radius: var(--ifm-global-radius); + color: var(--ifm-color-emphasis-700); + font-size: 0.8rem; + letter-spacing: normal; + padding: 0.45rem 0.85rem; + text-transform: none; + transition: + background-color 0.1s ease, + border-color 0.1s ease, + color 0.1s ease; +} + +.leaderboards .lbTab:hover { + background-color: var(--ifm-color-emphasis-100); + border-color: var(--ifm-color-primary); + color: var(--ifm-color-primary); +} + +.leaderboards .lbTabActive { + background: var( + --ifm-color-primary-contrast-background, + rgba(74, 105, 189, 0.1) + ); + border-color: var(--ifm-color-primary); + color: var(--ifm-color-primary); +} + +.leaderboards .lbTable { + background: var(--ifm-color-emphasis-100); + border: 1px solid var(--ifm-color-emphasis-200); + border-radius: var(--ifm-global-radius); + overflow: hidden; +} + +.leaderboards .lbHeader { + background: var(--ifm-card-background-color); + border-color: var(--ifm-color-emphasis-200); + color: var(--ifm-color-emphasis-600); + font-size: 0.75rem; + letter-spacing: 0.05em; + margin: 0; + padding: 0.6rem 1rem; +} + +.leaderboards .lbRow { + border-bottom: 1px solid var(--ifm-color-emphasis-200); + border-radius: 0; + color: var(--ifm-font-color-base); + padding: 0.55rem 1rem; + transition: background-color 0.1s ease; +} + +.leaderboards .lbRow:last-child { + border-bottom: 0; +} + +.leaderboards .lbRow:hover { + background-color: var(--ifm-color-emphasis-200); + color: var(--ifm-font-color-base); +} + +.leaderboards .lbLogin, +.leaderboards .lbCommitCount { + color: var(--ifm-color-primary); +} + +.leaderboards .lbColRank, +.leaderboards .lbColHive, +.leaderboards .lbRepoChip, +.leaderboards .lbRepoMore, +.leaderboards .lbSparkValue { + color: var(--ifm-color-emphasis-600); +} + +.leaderboards .lbAvatar { + border-color: var(--ifm-color-emphasis-300); +} + +.leaderboards .lbRepoChip { + background: var(--ifm-color-emphasis-200); +} + .contributionLinks { margin-bottom: 1.25rem; } @@ -2307,7 +2438,10 @@ padding: 0.3rem 0.9rem; border-radius: 6px; cursor: pointer; - transition: all 0.15s; + transition: + background-color 0.15s, + border-color 0.15s, + color 0.15s; letter-spacing: 0.03em; text-transform: uppercase; } diff --git a/src/components/HiveFactoryDashboard.tsx b/src/components/HiveFactoryDashboard.tsx index c75477ef..e7679253 100644 --- a/src/components/HiveFactoryDashboard.tsx +++ b/src/components/HiveFactoryDashboard.tsx @@ -532,9 +532,11 @@ interface QueueData { // All fetches fall back gracefully when the user is not logged in. const HOSTED_INSTANCE_URL = "https://hosted-projectbluefin-knuckle-gjvq.hive.hivecommons.dev"; +const HOSTED_DOSSIER_URL = + "https://hosted-projectbluefin-common-nmq5.hive.hivecommons.dev"; -function playerLeaderboardUrl(login: string): string { - return `${HOSTED_INSTANCE_URL}/api/leaderboard/contributor/${encodeURIComponent(login)}`; +function contributorDossierUrl(login: string): string { + return `${HOSTED_DOSSIER_URL}/contribute/dossier/${encodeURIComponent(login)}`; } // Public registry — no auth required, updated every ~15 min by the hub @@ -1422,7 +1424,7 @@ function ContributorWall({ return ( ( ( (