diff --git a/.gitignore b/.gitignore
index 4f86d12..f09c650 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,5 +6,6 @@ web/static/build/
node_modules/
tmp/
.air/
+.commandcode/
*.db
.DS_Store
diff --git a/web/src/components/CatalogCharts.svelte b/web/src/components/CatalogCharts.svelte
index 5f4aca3..2cc0241 100644
--- a/web/src/components/CatalogCharts.svelte
+++ b/web/src/components/CatalogCharts.svelte
@@ -7,6 +7,7 @@
pulseBars,
quietStarsRank,
rankTraffic,
+ trafficFreshness,
trafficRepos,
uniqueVisitorsByLanguage,
} from "../lib/trafficCharts.js";
@@ -18,7 +19,9 @@
$: totals = catalogTrafficTotals(repos);
$: visitors = rankTraffic(repos, "view_uniques", 10);
$: cloners = rankTraffic(repos, "clone_uniques", 10);
- $: pulse = pulseBars(cutUnsettledDays(dailyCatalogPulse(repos)));
+ $: pulseDays = dailyCatalogPulse(repos);
+ $: freshness = trafficFreshness(pulseDays);
+ $: pulse = pulseBars(cutUnsettledDays(pulseDays));
$: languages = uniqueVisitorsByLanguage(repos);
$: quiet = quietStarsRank(repos).map((row) => ({
...row,
@@ -64,6 +67,15 @@
Last 14 days
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.
+
+ {#if !freshness.lastDay}
+ No traffic recorded in this window yet.
+ {:else if freshness.stale}
+ GitHub has not published new traffic since {freshness.label} ({freshness.daysBehind} days behind).
+ {:else}
+ Data through {freshness.label}.
+ {/if}
+
{#each pulse as day}
diff --git a/web/src/components/CatalogCharts.test.js b/web/src/components/CatalogCharts.test.js
index 9336c31..a105d03 100644
--- a/web/src/components/CatalogCharts.test.js
+++ b/web/src/components/CatalogCharts.test.js
@@ -1,5 +1,5 @@
import { render, screen, within } from "@testing-library/svelte";
-import { describe, expect, test } from "vitest";
+import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import CatalogCharts from "./CatalogCharts.svelte";
const repos = [
@@ -68,3 +68,25 @@ describe("CatalogCharts", () => {
expect(within(pulse).getByTitle("1 unique cloner")).toBeInTheDocument();
});
});
+
+describe("CatalogCharts freshness", () => {
+ beforeEach(() => {
+ vi.useFakeTimers();
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ });
+
+ test("reports the newest traffic day in the pulse", () => {
+ vi.setSystemTime(new Date("2026-08-29T12:00:00Z"));
+ render(CatalogCharts, { props: { repos } });
+ expect(screen.getByText("Data through Aug 27.")).toBeInTheDocument();
+ });
+
+ test("warns when GitHub lags past the settle delay", () => {
+ vi.setSystemTime(new Date("2026-08-30T12:00:00Z"));
+ render(CatalogCharts, { props: { repos } });
+ expect(screen.getByText("GitHub has not published new traffic since Aug 27 (3 days behind).")).toBeInTheDocument();
+ });
+});
diff --git a/web/src/lib/trafficCharts.js b/web/src/lib/trafficCharts.js
index a292e5d..4504018 100644
--- a/web/src/lib/trafficCharts.js
+++ b/web/src/lib/trafficCharts.js
@@ -89,17 +89,34 @@ function pulseHeight(value, peak) {
return `${Math.max(6, Math.round((value / peak) * 100))}%`;
}
+function utcDayLabel(date) {
+ return new Date(`${date}T00:00:00Z`).toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ timeZone: "UTC",
+ });
+}
+
export function pulseBars(days) {
const peak = Math.max(1, ...(days || []).flatMap((day) => [day.viewUniques, day.cloneUniques]));
- return (days || []).map((day) => {
- const utc = new Date(`${day.date}T00:00:00Z`);
- return {
- ...day,
- label: utc.toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" }),
- viewHeight: pulseHeight(day.viewUniques, peak),
- cloneHeight: pulseHeight(day.cloneUniques, peak),
- };
- });
+ return (days || []).map((day) => ({
+ ...day,
+ label: utcDayLabel(day.date),
+ viewHeight: pulseHeight(day.viewUniques, peak),
+ cloneHeight: pulseHeight(day.cloneUniques, peak),
+ }));
+}
+
+// GitHub can lag beyond its usual ~1-2 day settle, and the pulse then freezes
+// silently; report the newest active day so the chart can show the real delay.
+const FRESHNESS_LAG_DAYS = 2;
+
+export function trafficFreshness(days, now = new Date(), lagDays = FRESHNESS_LAG_DAYS) {
+ const lastDay = (days || []).findLast((day) => (day.viewUniques || 0) + (day.cloneUniques || 0) > 0);
+ if (!lastDay) return { lastDay: null, label: "", daysBehind: null, stale: false };
+ const today = Date.parse(`${now.toISOString().slice(0, 10)}T00:00:00Z`);
+ const daysBehind = Math.round((today - Date.parse(`${lastDay.date}T00:00:00Z`)) / 864e5);
+ return { lastDay: lastDay.date, label: utcDayLabel(lastDay.date), daysBehind, stale: daysBehind > lagDays };
}
export function uniqueVisitorsByLanguage(repos, limit = 8) {
diff --git a/web/src/lib/trafficCharts.test.js b/web/src/lib/trafficCharts.test.js
index 8d8f375..a8ceafd 100644
--- a/web/src/lib/trafficCharts.test.js
+++ b/web/src/lib/trafficCharts.test.js
@@ -6,6 +6,7 @@ import {
pulseBars,
quietStarsRank,
rankTraffic,
+ trafficFreshness,
trafficRepos,
uniqueVisitorsByLanguage,
} from "./trafficCharts.js";
@@ -148,6 +149,41 @@ describe("cutUnsettledDays", () => {
});
});
+describe("trafficFreshness", () => {
+ const pulse = [
+ { date: "2026-09-21", viewUniques: 4, cloneUniques: 3 },
+ { date: "2026-09-22", viewUniques: 0, cloneUniques: 0 },
+ { date: "2026-09-23", viewUniques: 2, cloneUniques: 0 },
+ { date: "2026-09-24", viewUniques: 0, cloneUniques: 0 },
+ { date: "2026-09-25", viewUniques: 0, cloneUniques: 0 },
+ ];
+
+ test("reports the newest day with recorded traffic", () => {
+ expect(trafficFreshness(pulse, new Date("2026-09-25T12:00:00Z"))).toEqual({
+ lastDay: "2026-09-23",
+ label: "Sep 23",
+ daysBehind: 2,
+ stale: false,
+ });
+ });
+
+ test("flags a lagging feed once the gap passes the settle delay", () => {
+ const report = trafficFreshness(pulse, new Date("2026-09-28T12:00:00Z"));
+ expect(report.daysBehind).toBe(5);
+ expect(report.stale).toBe(true);
+ });
+
+ test("reports no active day when the window is all zeroes", () => {
+ const quiet = [{ date: "2026-09-25", viewUniques: 0, cloneUniques: 0 }];
+ expect(trafficFreshness(quiet, new Date("2026-09-26T12:00:00Z"))).toEqual({
+ lastDay: null,
+ label: "",
+ daysBehind: null,
+ stale: false,
+ });
+ });
+});
+
describe("pulseBars", () => {
test("labels UTC days and scales both series to the same peak", () => {
const bars = pulseBars(dailyCatalogPulse(catalog));
diff --git a/web/static/css/atlas.css b/web/static/css/atlas.css
index 1db576b..b7ecd95 100644
--- a/web/static/css/atlas.css
+++ b/web/static/css/atlas.css
@@ -1340,6 +1340,32 @@ a.stat:hover {
color: var(--ink-mute);
}
+.chart-freshness {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.4rem;
+}
+
+.chart-freshness::before {
+ content: "";
+ width: 0.4rem;
+ height: 0.4rem;
+ border-radius: 999px;
+ background: var(--moss);
+}
+
+.chart-freshness.is-stale {
+ color: var(--copper-hot);
+}
+
+.chart-freshness.is-stale::before {
+ background: var(--copper);
+}
+
+.chart-freshness.is-empty::before {
+ background: var(--ink-mute);
+}
+
.rank-list {
list-style: none;
margin: 0;