diff --git a/docs/skills/factory-dashboard-content.md b/docs/skills/factory-dashboard-content.md index 0a7b88282..5a048f689 100644 --- a/docs/skills/factory-dashboard-content.md +++ b/docs/skills/factory-dashboard-content.md @@ -156,35 +156,28 @@ authorization to it. ### CountMe: first-party stream counts only -`/analytics` reads `https://countme.projectbluefin.io/counts.json` at runtime. -The Worker aggregates D1 events; `scripts/lib/countme-sources.mjs` allows only -`dakota` as a first-party published repo today. This image is labeled -**Bluefin**. The older upstream artifact is **Bluefin Classic** and remains -separate. Fedora's `totals.csv` and the tracked `countme-history.json` are not -sources for a Project Bluefin image count. - -- The Dakota client reports one successful check-in per UTC Monday-anchored - week, with a booted `stable` or `testing` tag. Its daily calendar retry does - not send another ping after a successful report that week. No machine ID is - transmitted, so the totals estimate active systems rather than count - distinct devices. -- `/counts.json` keeps the all-tag Dakota total for existing consumers, but - additionally publishes nullable `dakotaStable`, `dakotaTesting` and - `dakotaUnclassified` readings. Only exact tags belong to named streams; - `latest`, missing or ambiguous tags stay unclassified. Game mode is a subset - of check-ins, not a separate image or a third stream. -- The first chart plots stable and testing only, on one zero-anchored domain. - Its text dates each last measured reading and labels the in-progress UTC - week using `generatedAt`. Missing is `null`, never zero; unclassified pings - are disclosed separately rather than attributed to either line. -- The cards read **Bluefin**, **Bluefin Utah**, **Bluefin Classic**, in that - order. Bluefin Utah is fed by `https://countme.projectbluefin.io/v1/daily.json` - (daily `projectbluefin-countme` pings, one row per day and image), read with - `familyDaily(rows, "utah")` in `firstPartyCountme.ts`. It plots systems - active **per day** by stream; never add days together into weekly users. - -Verify suspicious numbers against the read-only D1 rows grouped by `repo`, -`tag` and UTC week, then compare `/counts.json`. Re-running a fetcher only +`/analytics` reads `https://countme.projectbluefin.io/v1/daily.json` at +runtime: one row per UTC day and image, `{day, image, n}`, where `image` is +`/:`. The rows come from +`projectbluefin-countme`, shipped by `projectbluefin/common`, which pings at +most once per day with the booted `stable` or `testing` tag (else `unknown`). +No machine ID is transmitted, so `n` estimates systems active that day rather +than counting distinct devices. The older upstream artifact is **Bluefin +Classic** and remains separate. Fedora's `totals.csv` and the tracked +`countme-history.json` are not sources for a Project Bluefin image count. + +- The cards read **Bluefin** (`dakota…` images), **Bluefin Utah** (`utah…`), + **Bluefin Classic**, in that order. Both first-party cards are one + `DailyPanel`, fed by `familyDaily(rows, family)` in `firstPartyCountme.ts`. +- They plot systems active **per day** by stream on one zero-anchored domain. + The day axis is continuous, so an unreported day is a `null` gap, never a + zero and never bridged. Never add days together into weekly users. +- An empty card says "No raptors reporting in, life finds a way"; a failed + fetch shows the generic pending reason instead. +- The Bluefin Classic comparison chip shows the latest Bluefin daily total. + +Verify suspicious numbers against the read-only D1 `daily_pings` rows grouped +by `day` and `image`, then compare `/v1/daily.json`. Re-running a fetcher only proves determinism. Never send a synthetic production ping to test a chart. ### Charts follow the site theme @@ -342,8 +335,8 @@ change. - [ ] `node --test scripts/factory-theming.test.js scripts/tests-panels.test.js` passes. - [ ] Panel copy matches [`/press-kit`](/press-kit) vocabulary. -- [ ] Named stream counts agree with exact `stable`/`testing` D1 tags; other - Dakota tags remain unclassified. +- [ ] Per-stream daily counts agree with the D1 `daily_pings` rows for that + image family. - [ ] The dashboard was opened in **both** light and dark mode. - [ ] Every unavailable panel states a reason — `scripts/panel-unavailability.test.js` passes. @@ -357,7 +350,7 @@ change. ## Sources - [`/press-kit`](/press-kit) — brand vocabulary for panel copy. -- `workers/countme-proxy/counts.mjs`, +- `workers/countme-proxy/ping.mjs`, `src/components/analytics/firstPartyCountme.ts`, and `scripts/lib/countme-sources.mjs` — first-party counting contract. - [`ublue-os/countme`](https://github.com/ublue-os/countme) — upstream source diff --git a/scripts/countme-analytics-charts.test.js b/scripts/countme-analytics-charts.test.js index e0085e309..0f7adf2a9 100644 --- a/scripts/countme-analytics-charts.test.js +++ b/scripts/countme-analytics-charts.test.js @@ -78,7 +78,7 @@ const { } = mod; // The first-party reader is its own module; see the note in the component. -const { latestReading, familyDaily } = loadTsxModule( +const { familyDaily } = loadTsxModule( path.join( __dirname, "..", @@ -149,26 +149,25 @@ test("the catalogue names every family common ships into", () => { assert.deepEqual(ids, ["bluefin", "bluefin-lts", "dakota", "utah", "server"]); }); -test("the count panel says it lacks data without naming infrastructure", () => { - // Presentation rule 6 requires the panel to say it is unavailable and why. +test("empty count cards say no raptors are reporting, naming no infrastructure", () => { + // Presentation rule 6 requires each panel to say it is unavailable and why. // AGENTS.md requires that it never emit a host address or an internal URL. - // Both hold: it speaks about the data, not the plumbing. const html = renderToStaticMarkup( React.createElement(CountmeAnalyticsCharts, { registry: REGISTRY_FIXTURE, + daily: { days: [] }, }), ); - - const panel = html.match( - /data-what="Weekly active systems"[^>]*data-reason="([^"]*)"/, - ); - assert.ok( - panel, - "the weekly active systems panel must say it is unavailable", - ); - assert.match(panel[1], /not published yet/); - assert.doesNotMatch(panel[1], /projectbluefin\.io/); - assert.doesNotMatch(panel[1], /endpoint/i); + for (const what of ["Bluefin", "Bluefin Utah"]) { + const panel = html.match( + new RegExp( + `data-what="${what} daily active systems"[^>]*data-reason="([^"]*)"`, + ), + ); + assert.ok(panel, `${what} must say it is unavailable`); + assert.equal(panel[1], "No raptors reporting in, life finds a way"); + } + assert.doesNotMatch(html, /data-reason="[^"]*projectbluefin\.io/); }); const PROMOTED = [ @@ -295,198 +294,7 @@ test("the matrix says why it is empty rather than rendering nothing", () => { }); /** - * Weekly active systems, served by the first-party service. - * Only Dakota's exact stable/testing tags are charted. An unclassified tag is - * disclosed in text instead of being silently assigned to either stream. - * Null means no reading; zero is a real measured value. - */ -const COUNTS_FIXTURE = { - generatedAt: "2026-09-23T12:00:00.000Z", - source: "https://countme.projectbluefin.io", - method: "first-party-d1-v2", - unit: "estimated weekly active systems", - variants: ["dakota"], - weeks: [ - { - week: "2026-09-07", - dakota: 4, - dakotaStable: null, - dakotaTesting: 1, - dakotaUnclassified: 3, - }, - { - week: "2026-09-14", - dakota: 3, - dakotaStable: 2, - dakotaTesting: null, - dakotaUnclassified: 1, - }, - { - week: "2026-09-21", - dakota: 1, - dakotaStable: null, - dakotaTesting: null, - dakotaUnclassified: 1, - }, - ], -}; - -function renderCounts(counts) { - return renderToStaticMarkup( - React.createElement(CountmeAnalyticsCharts, { - registry: REGISTRY_FIXTURE, - counts, - }), - ); -} - -function chartOption(html) { - const option = html.match( - /data-title="Weekly active systems"[\s\S]*?data-option="([^"]*)"/, - ); - assert.ok(option, "weekly active systems must render a chart"); - return JSON.parse(option[1].replace(/"/g, '"').replace(/&/g, "&")); -} - -test("a trailing gap reports the last week each stream actually measured", () => { - assert.deepEqual(latestReading(COUNTS_FIXTURE.weeks, "dakotaStable"), { - value: 2, - week: "2026-09-14", - }); - assert.deepEqual(latestReading(COUNTS_FIXTURE.weeks, "dakotaTesting"), { - value: 1, - week: "2026-09-07", - }); -}); - -test("only the exact Dakota streams appear, not all-tag or LTS totals", () => { - const html = renderCounts(COUNTS_FIXTURE); - const option = chartOption(html); - assert.deepEqual( - option.series.map((series) => series.name), - ["Bluefin :stable", "Bluefin :testing"], - ); - assert.deepEqual( - option.series.map((series) => series.data), - [ - [null, 2, null], - [1, null, null], - ], - ); - assert.match(html, /Bluefin :stable: 2 \(week 2026-09-14\)/); - assert.match(html, /Bluefin :testing: 1 \(week 2026-09-07\)/); - assert.match(html, /Unclassified: 1 \(week 2026-09-21, partial\)/); - assert.match(html, /Bluefin \(all tags\): 1 \(week 2026-09-21, partial\)/); - assert.match(html, /Bluefin Classic \(ublue-os\/bluefin\)/); - assert.match(html, /estimates from weekly check-ins/i); -}); - -test("a measured zero is shown as zero rather than an accumulating gap", () => { - const counts = { - ...COUNTS_FIXTURE, - weeks: [ - { - week: "2026-09-21", - dakota: 0, - dakotaStable: 0, - dakotaTesting: null, - dakotaUnclassified: 0, - }, - ], - }; - const html = renderCounts(counts); - assert.deepEqual( - chartOption(html).series.map((s) => s.data), - [[0], [null]], - ); - assert.match(html, /Bluefin :stable: 0 \(week 2026-09-21, partial\)/); - assert.match(html, /Bluefin :testing: accumulating/); - assert.match(html, /Unclassified: 0 \(week 2026-09-21, partial\)/); -}); - -test("the chart breaks at missing weeks rather than inventing zeros or bridges", () => { - const option = chartOption(renderCounts(COUNTS_FIXTURE)); - assert.deepEqual( - option.series.map((series) => series.data), - [ - [null, 2, null], - [1, null, null], - ], - ); - for (const series of option.series) { - assert.equal(series.connectNulls, false, "a gap must break the line"); - assert.equal(series.smooth, false, "a spline invents values"); - } - assert.equal(option.yAxis.min, 0, "the floor must not exaggerate readings"); -}); - -test("the chart summary dates the readings instead of calling totals a stream", () => { - const html = renderCounts(COUNTS_FIXTURE); - const summary = html.match( - /data-title="Weekly active systems"[\s\S]*?data-summary="([^"]*)"/, - )[1]; - assert.match(summary, /Bluefin :stable 2 \(week 2026-09-14\)/); - assert.match(summary, /Bluefin :testing 1 \(week 2026-09-07\)/); - assert.doesNotMatch(summary, /Bluefin LTS|Bluefin \d|game mode/); -}); - -test("the panel stays unavailable when the service reports no weeks", () => { - const html = renderCounts({ - unavailable: true, - stateReason: "Weekly active systems are not published yet.", - weeks: [], - }); - const panel = html.match( - /data-what="Weekly active systems"[^>]*data-reason="([^"]*)"/, - ); - assert.ok(panel, "an empty aggregate must still render a reasoned panel"); - assert.match(panel[1], /not published yet/); - assert.doesNotMatch(panel[1], /projectbluefin\.io/); -}); -test("unclassified-only weeks disclose their count and absence of named streams", () => { - const html = renderCounts({ - generatedAt: "2026-09-23T12:00:00.000Z", - stateReason: "Named stream check-ins have not reported yet.", - weeks: [ - { - week: "2026-09-21", - dakota: 1, - dakotaStable: null, - dakotaTesting: null, - dakotaUnclassified: 1, - }, - ], - }); - assert.match(html, /Unclassified: 1 \(week 2026-09-21, partial\)/); - assert.match(html, /Bluefin :stable: accumulating data/); - assert.match(html, /Bluefin :testing: accumulating data/); - assert.match( - html, - /data-reason="Named stream check-ins have not reported yet\."/, - ); - assert.doesNotMatch(html, /data-title="Weekly active systems"/); -}); - -test("the axis shortens its ticks but keeps the full date in the data", () => { - // Nine ISO dates on one axis repeat the year nine times and crowd each other - // out. The tooltip and the numbers table read xAxis.data, so the full date - // has to survive there even though the tick text does not show it. - const option = chartOption(renderCounts(COUNTS_FIXTURE)); - - assert.deepEqual(option.xAxis.data, [ - "2026-09-07", - "2026-09-14", - "2026-09-21", - ]); - assert.equal(compactWeek("2026-08-17"), "Aug 17"); - assert.equal(compactWeek("2026-01-05"), "Jan 5"); - // A value that is not a date is passed through rather than rendered as - // "Invalid Date" on the axis. - assert.equal(compactWeek("not-a-date"), "not-a-date"); -}); - -/** - * Bluefin Utah: daily pings from projectbluefin-countme, read from + * Bluefin and Bluefin Utah: daily pings from projectbluefin-countme, read from * /v1/daily.json rows of `/:`. */ const DAILY_FIXTURE = { @@ -503,7 +311,6 @@ test("the three count cards read Bluefin, Bluefin Utah, Bluefin Classic in order const html = renderToStaticMarkup( React.createElement(CountmeAnalyticsCharts, { registry: REGISTRY_FIXTURE, - counts: COUNTS_FIXTURE, daily: DAILY_FIXTURE, }), ); @@ -529,7 +336,6 @@ test("the Utah card charts its streams and states each latest value", () => { const html = renderToStaticMarkup( React.createElement(CountmeAnalyticsCharts, { registry: REGISTRY_FIXTURE, - counts: COUNTS_FIXTURE, daily: DAILY_FIXTURE, }), ); @@ -551,11 +357,10 @@ test("the Utah card charts its streams and states each latest value", () => { assert.match(html, /Bluefin Utah :unknown: accumulating data/); }); -test("the Utah card says why it is empty before any Utah system reports", () => { +test("the Utah card stays empty while only Dakota reports", () => { const html = renderToStaticMarkup( React.createElement(CountmeAnalyticsCharts, { registry: REGISTRY_FIXTURE, - counts: COUNTS_FIXTURE, daily: { days: [{ day: "2026-09-22", image: "dakota/main:stable", n: 9 }], }, @@ -563,7 +368,7 @@ test("the Utah card says why it is empty before any Utah system reports", () => ); assert.match( html, - /data-what="Bluefin Utah daily active systems"[^>]*data-reason="No Bluefin Utah system has reported yet\."/, + /data-what="Bluefin Utah daily active systems"[^>]*data-reason="No raptors reporting in, life finds a way"/, ); }); @@ -578,3 +383,52 @@ test("a Utah day with no reports stays on the axis as a gap", () => { assert.deepEqual(days, ["2026-09-20", "2026-09-21", "2026-09-22"]); assert.deepEqual(streams.stable, [4, null, 5]); }); + +function renderDaily(daily) { + return renderToStaticMarkup( + React.createElement(CountmeAnalyticsCharts, { + registry: REGISTRY_FIXTURE, + daily, + }), + ); +} + +function dailyOption(html, label) { + const m = html.match( + new RegExp( + `data-title="${label} daily active systems"[^>]*data-option="([^"]*)"`, + ), + ); + assert.ok(m, `${label} must render a chart`); + return JSON.parse(m[1].replace(/"/g, '"').replace(/&/g, "&")); +} + +test("the Bluefin card charts Dakota pings per stream and never Utah", () => { + const option = dailyOption(renderDaily(DAILY_FIXTURE), "Bluefin"); + assert.deepEqual( + option.series.map((s) => s.name), + ["Bluefin :stable", "Bluefin :testing", "Bluefin :unknown"], + ); + assert.deepEqual(option.xAxis.data, ["2026-09-22"]); + assert.deepEqual( + option.series.map((s) => s.data), + [[9], [null], [null]], + ); +}); + +test("the Bluefin Classic comparison uses the latest Bluefin daily total", () => { + assert.match(renderDaily(DAILY_FIXTURE), /Bluefin: 9 \(2026-09-22\)/); + assert.match(renderDaily({ days: [] }), /Bluefin: accumulating data/); +}); + +test("the day axis shortens its ticks but keeps the full date in the data", () => { + const option = dailyOption(renderDaily(DAILY_FIXTURE), "Bluefin Utah"); + assert.deepEqual(option.xAxis.data, ["2026-09-22", "2026-09-23"]); + for (const series of option.series) { + assert.equal(series.connectNulls, false, "a gap must break the line"); + assert.equal(series.smooth, false, "a spline invents values"); + } + assert.equal(option.yAxis.min, 0, "the floor must not exaggerate readings"); + assert.equal(compactWeek("2026-08-17"), "Aug 17"); + assert.equal(compactWeek("not-a-date"), "not-a-date"); +}); diff --git a/src/components/analytics/CountmeAnalyticsCharts.tsx b/src/components/analytics/CountmeAnalyticsCharts.tsx index 871a8d430..68b850c16 100644 --- a/src/components/analytics/CountmeAnalyticsCharts.tsx +++ b/src/components/analytics/CountmeAnalyticsCharts.tsx @@ -12,17 +12,14 @@ import { } from "../factory/chartTheme"; import { FIRST_PARTY_PENDING_REASON } from "@site/scripts/lib/countme-sources.mjs"; import { - COUNTS_URL, DAILY_STREAMS, DAILY_URL, FIRST_PARTY_ORIGIN, familyDaily, - latestReading, - measuredWeekCount, - repoSeries, - weekLabels, - type CountmeDataset, + latestDaily, + measuredDays, type DailyDataset, + type FamilyDaily, } from "./firstPartyCountme"; import { useFactoryTheme } from "../factory/useFactoryTheme"; import "../factory/tokens.css"; @@ -36,7 +33,7 @@ import styles from "./CountmeAnalyticsCharts.module.css"; const REGISTRY_URL = "/data/ghcr-packages.json"; /** - * Weekly active systems come from the first-party service and nothing else. + * Active-system counts come from the first-party service and nothing else. * * The reader lives in `./firstPartyCountme`, and is imported rather than * re-exported from here. This file holds the image catalogue, and @@ -61,22 +58,6 @@ const REGISTRY_URL = "/data/ghcr-packages.json"; const LEGACY_CHART_URL = `${FIRST_PARTY_ORIGIN}/legacy/bluefin.svg`; const LEGACY_BADGE_URL = `${FIRST_PARTY_ORIGIN}/badge-endpoints/bluefin.json`; -/** Only exact Dakota tags are named chart series; all other tags stay unclassified. */ -const DAKOTA_STREAMS = [ - { key: "dakotaStable", label: "Bluefin :stable" }, - { key: "dakotaTesting", label: "Bluefin :testing" }, -] as const; -const DAKOTA_STREAM_KEYS = DAKOTA_STREAMS.map(({ key }) => key); - -function readingText( - reading: ReturnType, - currentWeek: string | null, -): string { - return reading - ? `${reading.value.toLocaleString()} (week ${reading.week}${reading.week === currentWeek ? ", partial" : ""})` - : "accumulating data"; -} - /** * `2026-07-13` as `Jul 13`. * @@ -345,17 +326,122 @@ export function buildStreamMatrix( return cells; } +/** What an empty daily card says until its image family reports. */ +export const NO_RAPTORS = "No raptors reporting in, life finds a way"; + +/** + * One image family's systems active per day, by stream: the latest value of + * each stream as text, then the chart, or the reason there is no chart. + */ +function DailyPanel({ + daily, + label, + reason, + cat, +}: { + daily: FamilyDaily; + label: string; + reason: string; + cat: string[]; +}): React.JSX.Element { + const measured = measuredDays(daily); + const readings = DAILY_STREAMS.map((stream) => { + const values = daily.streams[stream]; + for (let i = values.length - 1; i >= 0; i -= 1) { + if (values[i] !== null) { + return { + stream, + text: `${values[i]!.toLocaleString()} (${daily.days[i]})`, + }; + } + } + return { stream, text: "accumulating data" }; + }); + const option = useMemo( + () => ({ + grid: { left: 56, right: 24, top: 16, bottom: 48, containLabel: true }, + tooltip: { trigger: "axis" }, + xAxis: { + type: "category", + data: daily.days, + axisLabel: { + fontSize: 13, + hideOverlap: true, + formatter: (value: string) => compactWeek(value), + }, + }, + // Anchored at zero: a floating floor turns a flat series into a cliff. + yAxis: { + type: "value", + min: 0, + minInterval: 1, + axisLabel: { fontSize: 13 }, + }, + legend: { textStyle: { fontSize: 13 }, itemGap: 18 }, + series: DAILY_STREAMS.map((stream, i) => ({ + name: `${label} :${stream}`, + type: "line", + // Gaps break the line rather than being bridged or turned into zero. + smooth: false, + connectNulls: false, + showSymbol: true, + symbolSize: 7, + symbol: SERIES_SYMBOLS[i], + data: daily.streams[stream], + itemStyle: { color: cat[i % cat.length] }, + lineStyle: { + width: 2, + color: cat[i % cat.length], + type: seriesDash(i), + }, + })), + }), + [daily, label, cat], + ); + return ( + <> +

+ {readings.map(({ stream, text }, i) => ( + + + {label} :{stream}: {text} + + ))} +

+ {measured > 0 ? ( + `${label} :${r.stream} ${r.text}`).join(", ")}.`} + points={measured} + minPoints={2} + height={300} + tableCaption={`${label} systems active per day by stream`} + /> + ) : ( + // Rule 6: unavailability is visible and carries its reason. + + )} + + ); +} + export interface CountmeAnalyticsChartsProps { registry?: GhcrDataset; - /** Injected by tests; production fetches the first-party aggregate. */ - counts?: CountmeDataset; /** Injected by tests; production fetches the daily ping counts. */ daily?: DailyDataset; } export default function CountmeAnalyticsCharts({ registry, - counts, daily, }: CountmeAnalyticsChartsProps = {}): React.JSX.Element { const [themeRef, fxTheme] = useFactoryTheme(); @@ -384,45 +470,6 @@ export default function CountmeAnalyticsCharts({ })(); }, [base, registry]); - // ── Weekly active systems, first-party only ──────────────────────────── - const [fetchedCounts, setFetchedCounts] = useState( - null, - ); - const [countsReason, setCountsReason] = useState(null); - - useEffect(() => { - if (counts) return; - void (async () => { - try { - const res = await fetch(COUNTS_URL); - if (!res.ok) throw new Error(`HTTP ${res.status}`); - setFetchedCounts((await res.json()) as CountmeDataset); - } catch { - // The reason is deliberately generic. A panel reason is published copy, - // and commit 5a5269bc removed internal service posture from this page. - setCountsReason(FIRST_PARTY_PENDING_REASON); - } - })(); - }, [counts]); - - const countsData = counts ?? fetchedCounts; - const countmeWeeks = countsData?.weeks ?? []; - const generatedAt = countsData?.generatedAt - ? new Date(countsData.generatedAt) - : null; - const currentWeek = - generatedAt && Number.isFinite(generatedAt.valueOf()) - ? new Date( - Date.UTC( - generatedAt.getUTCFullYear(), - generatedAt.getUTCMonth(), - generatedAt.getUTCDate() - ((generatedAt.getUTCDay() + 6) % 7), - ), - ) - .toISOString() - .slice(0, 10) - : null; - // ── Upstream image, the one permitted legacy series ──────────────────── // // UPSTREAM_ALLOWED is the single exception to the first-party rule: @@ -444,7 +491,7 @@ export default function CountmeAnalyticsCharts({ })(); }, []); - // ── Bluefin Utah: daily pings from projectbluefin-countme ────────────── + // ── Bluefin and Bluefin Utah: daily pings from projectbluefin-countme ─ const [fetchedDaily, setFetchedDaily] = useState(null); const [dailyReason, setDailyReason] = useState(null); @@ -456,134 +503,23 @@ export default function CountmeAnalyticsCharts({ if (!res.ok) throw new Error(`HTTP ${res.status}`); setFetchedDaily((await res.json()) as DailyDataset); } catch { + // A panel reason is published copy: generic, no service posture. setDailyReason(FIRST_PARTY_PENDING_REASON); } })(); }, [daily]); - const utah = useMemo( - () => familyDaily((daily ?? fetchedDaily)?.days ?? [], "utah"), - [daily, fetchedDaily], - ); - const utahMeasured = utah.days.filter((_, i) => - DAILY_STREAMS.some((stream) => utah.streams[stream][i] !== null), - ).length; - const utahReadings = DAILY_STREAMS.map((stream) => { - const values = utah.streams[stream]; - for (let i = values.length - 1; i >= 0; i -= 1) { - if (values[i] !== null) - return { stream, value: values[i], day: utah.days[i] }; - } - return { stream, value: null, day: null }; - }); - const utahText = ({ value, day }: (typeof utahReadings)[number]) => - value === null ? "accumulating data" : `${value.toLocaleString()} (${day})`; - const utahOption = useMemo( - () => ({ - grid: { left: 56, right: 24, top: 16, bottom: 48, containLabel: true }, - tooltip: { trigger: "axis" }, - xAxis: { - type: "category", - data: utah.days, - axisLabel: { - fontSize: 13, - hideOverlap: true, - formatter: (value: string) => compactWeek(value), - }, - }, - yAxis: { - type: "value", - min: 0, - minInterval: 1, - axisLabel: { fontSize: 13 }, - }, - legend: { textStyle: { fontSize: 13 }, itemGap: 18 }, - series: DAILY_STREAMS.map((stream, i) => ({ - name: `Bluefin Utah :${stream}`, - type: "line", - smooth: false, - connectNulls: false, - showSymbol: true, - symbolSize: 7, - symbol: SERIES_SYMBOLS[i], - data: utah.streams[stream], - itemStyle: { color: cat[i % cat.length] }, - lineStyle: { - width: 2, - color: cat[i % cat.length], - type: seriesDash(i), - }, - })), - }), - [utah, cat], - ); - - const countmeReadings = useMemo( - () => - DAKOTA_STREAMS.map((stream) => ({ - ...stream, - reading: latestReading(countmeWeeks, stream.key), - })), - [countmeWeeks], + const dailyData = daily ?? fetchedDaily; + const emptyReason = dailyData?.stateReason ?? dailyReason ?? NO_RAPTORS; + const bluefin = useMemo( + () => familyDaily(dailyData?.days ?? [], "dakota"), + [dailyData], ); - const unclassifiedReading = latestReading(countmeWeeks, "dakotaUnclassified"); - // The legacy comparison uses the existing all-tag aggregate, never a stream. - const allTagReading = latestReading(countmeWeeks, "dakota"); - const countmePoints = measuredWeekCount(countmeWeeks, DAKOTA_STREAM_KEYS); - - const countmeOption = useMemo( - () => ({ - grid: { left: 56, right: 24, top: 16, bottom: 48, containLabel: true }, - tooltip: { trigger: "axis" }, - xAxis: { - type: "category", - // Full ISO dates stay in the data, so the tooltip and the numbers table - // keep them; only the tick text is shortened. - data: weekLabels(countmeWeeks), - axisLabel: { - fontSize: 13, - hideOverlap: true, - formatter: (value: string) => compactWeek(value), - }, - }, - // Anchored at zero: a floating floor turns a flat series into a cliff. - yAxis: { - type: "value", - min: 0, - minInterval: 1, - axisLabel: { fontSize: 13 }, - }, - legend: { textStyle: { fontSize: 13 }, itemGap: 18 }, - series: DAKOTA_STREAMS.map(({ key, label }, i) => ({ - name: label, - type: "line", - // Discrete weekly readings: gaps break the line rather than being - // bridged or converted to zero. - smooth: false, - connectNulls: false, - showSymbol: true, - symbolSize: 7, - symbol: SERIES_SYMBOLS[i], - data: repoSeries(countmeWeeks, key), - itemStyle: { color: cat[i % cat.length] }, - lineStyle: { - width: 2, - color: cat[i % cat.length], - type: seriesDash(i), - }, - })), - }), - [countmeWeeks, cat], + const utah = useMemo( + () => familyDaily(dailyData?.days ?? [], "utah"), + [dailyData], ); - - /** The chart's accessible summary dates every measured stream reading. */ - const countmeSummary = `Estimated weekly active systems from check-ins across ${countmePoints} measured week${ - countmePoints === 1 ? "" : "s" - } — ${countmeReadings - .map( - ({ label, reading }) => `${label} ${readingText(reading, currentWeek)}`, - ) - .join(", ")}.`; + const bluefinLatest = latestDaily(bluefin); const ghcr = registry ?? fetchedRegistry; @@ -678,55 +614,20 @@ export default function CountmeAnalyticsCharts({ return (
- {/* ── 1. Bluefin: weekly active systems ────────────────────────────── */} + {/* ── 1. Bluefin: daily active systems ─────────────────────────────── */}
Bluefin -

Weekly active systems.

+

Systems active per day.

- -

- {countmeReadings.map(({ key, label, reading }, i) => ( - - - {label}: {readingText(reading, currentWeek)} - - ))} -

- {countmePoints > 0 ? ( - - ) : ( - // Rule 6: unavailability is visible and carries its reason. - - )} -

- Unclassified: {readingText(unclassifiedReading, currentWeek)}. These - are estimates from weekly check-ins; other or unknown tags are not - assigned to :stable or :testing. -

+
{/* ── 2. Bluefin Utah: daily active systems ────────────────────────── */} @@ -737,44 +638,12 @@ export default function CountmeAnalyticsCharts({

Systems active per day.

-

- {utahReadings.map((reading, i) => ( - - - Bluefin Utah :{reading.stream}: {utahText(reading)} - - ))} -

- {utahMeasured > 0 ? ( - `Bluefin Utah :${r.stream} ${utahText(r)}`) - .join(", ")}.`} - points={utahMeasured} - minPoints={2} - height={300} - tableCaption="Bluefin Utah systems active per day by stream" - /> - ) : ( - - )} + {/* ── 3. Upstream image, for watching the migration ────────────────── */} @@ -803,7 +672,10 @@ export default function CountmeAnalyticsCharts({ - Bluefin (all tags): {readingText(allTagReading, currentWeek)} + Bluefin:{" "} + {bluefinLatest + ? `${bluefinLatest.value.toLocaleString()} (${bluefinLatest.day})` + : "accumulating data"}

diff --git a/src/components/analytics/firstPartyCountme.ts b/src/components/analytics/firstPartyCountme.ts index 82c5915e3..409cf9242 100644 --- a/src/components/analytics/firstPartyCountme.ts +++ b/src/components/analytics/firstPartyCountme.ts @@ -1,7 +1,7 @@ import { FIRST_PARTY } from "@site/scripts/lib/countme-sources.mjs"; /** - * Reading weekly active systems from the first-party countme service. + * Reading daily active systems from the first-party countme service. * * This module is deliberately separate from the component that renders it. * `scripts/countme-first-party.test.js` forbids a file that holds the image @@ -21,83 +21,6 @@ import { FIRST_PARTY } from "@site/scripts/lib/countme-sources.mjs"; /** The first-party origin. Every route below is served by our own worker. */ export const FIRST_PARTY_ORIGIN: string = FIRST_PARTY.origin; -export const COUNTS_URL = `${FIRST_PARTY_ORIGIN}/counts.json`; - -/** One week of first-party counts. A null stream is a gap, never zero. */ -export interface CountmeWeek { - week: string; - [repo: string]: string | number | null | undefined; -} - -export interface CountmeDataset { - generatedAt?: string; - source?: string; - method?: string; - unit?: string; - variants?: string[]; - weeks?: CountmeWeek[]; - unavailable?: boolean; - stateReason?: string | null; -} - -/** - * Parse a raw count, preserving 0 as a real measurement. - * - * `0` and `null` are different claims: "nobody was running it" against "nobody - * reported". Returns null for undefined, null, empty string, and non-finite. - */ -export function parseReading(val: unknown): number | null { - if (val === null || val === undefined || val === "") return null; - const n = typeof val === "number" ? val : Number(val); - return Number.isFinite(n) ? n : null; -} - -/** - * Latest real reading for a repo, with the week it belongs to. - * - * Reads backwards so a trailing gap does not read as "no data": the number is - * the most recent one actually measured, and the caller states its week rather - * than implying it is current. - */ -export function latestReading( - weeks: CountmeWeek[], - repo: string, -): { value: number; week: string } | null { - for (let i = weeks.length - 1; i >= 0; i -= 1) { - const value = parseReading(weeks[i]?.[repo]); - if (value !== null) return { value, week: String(weeks[i].week) }; - } - return null; -} - -/** A repo's series across the week axis, gaps preserved as null. */ -export function repoSeries( - weeks: CountmeWeek[], - repo: string, -): Array { - return weeks.map((w) => parseReading(w[repo])); -} - -/** The week axis, as category labels. */ -export function weekLabels(weeks: CountmeWeek[]): string[] { - return weeks.map((w) => String(w.week)); -} - -/** - * Weeks carrying at least one real reading. - * - * This is the count presentation rule 5 tests against, so an axis padded with - * empty weeks cannot pass for accumulated data. - */ -export function measuredWeekCount( - weeks: CountmeWeek[], - repos: string[], -): number { - return weeks.filter((w) => - repos.some((repo) => parseReading(w[repo]) !== null), - ).length; -} - /** Systems active per UTC day and image, from `projectbluefin-countme` pings. */ export const DAILY_URL = `${FIRST_PARTY_ORIGIN}/v1/daily.json`; @@ -152,3 +75,28 @@ export function familyDaily( } return { days, streams }; } + +export type FamilyDaily = ReturnType; + +/** Days on which at least one stream reported, as `measuredWeekCount` does. */ +export function measuredDays(daily: FamilyDaily): number { + return daily.days.filter((_, i) => + DAILY_STREAMS.some((stream) => daily.streams[stream][i] !== null), + ).length; +} + +/** The last day any stream reported, with that day's total across streams. */ +export function latestDaily( + daily: FamilyDaily, +): { value: number; day: string } | null { + for (let i = daily.days.length - 1; i >= 0; i -= 1) { + const values = DAILY_STREAMS.map((stream) => daily.streams[stream][i]); + if (values.some((v) => v !== null)) { + return { + value: values.reduce((sum, v) => sum + (v ?? 0), 0), + day: daily.days[i], + }; + } + } + return null; +}