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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ web/static/build/
node_modules/
tmp/
.air/
.commandcode/
*.db
.DS_Store
14 changes: 13 additions & 1 deletion web/src/components/CatalogCharts.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
pulseBars,
quietStarsRank,
rankTraffic,
trafficFreshness,
trafficRepos,
uniqueVisitorsByLanguage,
} from "../lib/trafficCharts.js";
Expand All @@ -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,
Expand Down Expand Up @@ -64,6 +67,15 @@
<header class="chart-head">
<h3 id="chart-pulse">Last 14 days</h3>
<p>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.</p>
<p class="chart-freshness" class:is-stale={freshness.stale} class:is-empty={!freshness.lastDay}>
{#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}
</p>
</header>
<div class="pulse-chart">
{#each pulse as day}
Expand Down
24 changes: 23 additions & 1 deletion web/src/components/CatalogCharts.test.js
Original file line number Diff line number Diff line change
@@ -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 = [
Expand Down Expand Up @@ -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();
});
});
35 changes: 26 additions & 9 deletions web/src/lib/trafficCharts.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
36 changes: 36 additions & 0 deletions web/src/lib/trafficCharts.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
pulseBars,
quietStarsRank,
rankTraffic,
trafficFreshness,
trafficRepos,
uniqueVisitorsByLanguage,
} from "./trafficCharts.js";
Expand Down Expand Up @@ -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));
Expand Down
26 changes: 26 additions & 0 deletions web/static/css/atlas.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading