Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions docs/skills/factory-dashboard-content.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
72 changes: 53 additions & 19 deletions scripts/fetch-hive-history.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -107,17 +110,45 @@ 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" };
if (GH_TOKEN) h["Authorization"] = `Bearer ${GH_TOKEN}`;
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) || {};
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -541,4 +573,6 @@ module.exports = {
finalizeContributorStats,
MAX_WEEKLY_SERIES,
MAX_WEEKS,
registryHeaders,
trackedProjectRepos,
};
23 changes: 23 additions & 0 deletions scripts/fetch-hive-history.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ const {
extractMetrics,
finalizeContributorStats,
MAX_WEEKS,
registryHeaders,
trackedProjectRepos,
} = require("./fetch-hive-history.js");

const WEEK = 7 * 86400;
Expand Down Expand Up @@ -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.
Expand Down
16 changes: 10 additions & 6 deletions scripts/leaderboards-standalone.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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(
Expand Down
136 changes: 135 additions & 1 deletion src/components/HiveFactoryDashboard.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand Down
Loading
Loading