diff --git a/web/src/components/CatalogCharts.svelte b/web/src/components/CatalogCharts.svelte index 49b3aa3..5f4aca3 100644 --- a/web/src/components/CatalogCharts.svelte +++ b/web/src/components/CatalogCharts.svelte @@ -2,6 +2,7 @@ import { formatCount } from "../lib/format.js"; import { catalogTrafficTotals, + cutUnsettledDays, dailyCatalogPulse, pulseBars, quietStarsRank, @@ -17,7 +18,7 @@ $: totals = catalogTrafficTotals(repos); $: visitors = rankTraffic(repos, "view_uniques", 10); $: cloners = rankTraffic(repos, "clone_uniques", 10); - $: pulse = pulseBars(dailyCatalogPulse(repos)); + $: pulse = pulseBars(cutUnsettledDays(dailyCatalogPulse(repos))); $: languages = uniqueVisitorsByLanguage(repos); $: quiet = quietStarsRank(repos).map((row) => ({ ...row, @@ -62,7 +63,7 @@

Last 14 days

-

Unique visitors and cloners summed across this catalog.

+

Unique visitors and cloners summed across this catalog. GitHub settles traffic with a ~1–2 day delay, so the newest days appear once they are final.

{#each pulse as day} diff --git a/web/src/lib/trafficCharts.js b/web/src/lib/trafficCharts.js index f7843b8..a292e5d 100644 --- a/web/src/lib/trafficCharts.js +++ b/web/src/lib/trafficCharts.js @@ -75,6 +75,15 @@ export function dailyCatalogPulse(repos) { return fillDaySpan(rows); } +// GitHub finalizes a traffic day roughly 36h after it closes, so the newest +// buckets come back empty or partial. Hide them so the pulse ends on settled data. +const SETTLE_HOURS = 36; + +export function cutUnsettledDays(days, now = new Date(), settleHours = SETTLE_HOURS) { + const cutoff = now.getTime() - settleHours * 3600 * 1000; + return (days || []).filter((day) => Date.parse(`${day.date}T00:00:00Z`) <= cutoff); +} + function pulseHeight(value, peak) { if (value <= 0) return "0%"; return `${Math.max(6, Math.round((value / peak) * 100))}%`; diff --git a/web/src/lib/trafficCharts.test.js b/web/src/lib/trafficCharts.test.js index ba2cfd9..8d8f375 100644 --- a/web/src/lib/trafficCharts.test.js +++ b/web/src/lib/trafficCharts.test.js @@ -1,6 +1,7 @@ import { describe, expect, test } from "vitest"; import { catalogTrafficTotals, + cutUnsettledDays, dailyCatalogPulse, pulseBars, quietStarsRank, @@ -119,6 +120,34 @@ describe("dailyCatalogPulse", () => { }); }); +describe("cutUnsettledDays", () => { + const pulse = [ + { date: "2026-09-16", viewUniques: 5, cloneUniques: 2 }, + { date: "2026-09-17", viewUniques: 3, cloneUniques: 1 }, + { date: "2026-09-18", viewUniques: 0, cloneUniques: 0 }, + { date: "2026-09-19", viewUniques: 0, cloneUniques: 0 }, + ]; + + test("drops trailing days GitHub has not settled yet", () => { + const now = new Date("2026-09-19T03:00:00Z"); + expect(cutUnsettledDays(pulse, now).map((day) => day.date)).toEqual(["2026-09-16", "2026-09-17"]); + }); + + test("keeps a day once it is older than the settle window", () => { + const now = new Date("2026-09-19T13:00:00Z"); + expect(cutUnsettledDays(pulse, now).map((day) => day.date)).toEqual([ + "2026-09-16", + "2026-09-17", + "2026-09-18", + ]); + }); + + test("returns an empty list when every day is unsettled", () => { + const fresh = [{ date: "2026-09-19", viewUniques: 1, cloneUniques: 0 }]; + expect(cutUnsettledDays(fresh, new Date("2026-09-19T03:00:00Z"))).toEqual([]); + }); +}); + describe("pulseBars", () => { test("labels UTC days and scales both series to the same peak", () => { const bars = pulseBars(dailyCatalogPulse(catalog));