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 (
(
(
(