A local-first, backend-free analytics tool for a software team's GitHub activity. A CLI puller fetches + aggregates data into JSON files; a Vite/React SPA reads those files and renders weekly velocity trends and a Team → Week → Person → Repo drill-down, plus a ranked list of the week's most meaningful PRs for status reports.
It describes contribution shape, not individual worth. There is no server, no database, and nothing is deployed — it runs on your machine.
See docs/prd.md for the full specification.
- Bun (the runtime and package manager — never use npm/yarn/pnpm).
- A GitHub Personal Access Token with
repo+read:orgscope (only needed for a real pull).
bun install
bunx playwright install chromium # only needed to run the e2e suite-
Teams & members — copy the example config and edit it:
cp teamvelocity.config.example.json teamvelocity.config.json
githubmay be a bare handle or a full profile URL. Repo scope is hybrid: the GitHub connector auto-discovers repos from members' activity and always includes the explicitreposlist. Config is validated with Zod — an invalid file aborts the pull with a clear message. -
Token — copy the example env file and add your token:
cp .env.example .env # edit .env → GITHUB_TOKEN=ghp_...The token lives only in
.env(git-ignored). It is never written intopublic/data/, never imported by the SPA, and never logged. Under Bun,.envis loaded automatically.
bun run pull # fetch + aggregate → writes public/data/<teamId>/*.json
bun run dev # open the local app, then explore"Fresh data" == re-run bun run pull, then refresh the browser tab. The app itself never calls
GitHub and never sees the token.
- No token yet?
bun run pull(orbun run pull --dry-run) runs the whole pipeline over bundled fixtures and writes valid JSON — so you can try the flow without a token. - Useful flags:
--team <id>,--weeks <N>(backfill depth, default 26),--since/--until,--data-dir <path>,--verbose.
| Script | What it does |
|---|---|
bun run dev |
Start the Vite dev server (reads the committed sample dataset). |
bun run pull |
Fetch + aggregate GitHub data → public/data/ (dry-run over fixtures with no token). |
bun run generate:sample |
Regenerate the committed sample dataset in public/data/sample*. |
bun run build |
Type-check + build the static SPA to dist/. |
bun run preview |
Preview the production build. |
bun run lint |
ESLint (must be clean — 0 errors, 0 warnings). |
bun run typecheck |
tsc --noEmit (strict). |
bun run test |
Vitest unit + integration (timezone-pinned to Europe/Prague). |
bun run test:e2e |
Playwright e2e over the committed sample dataset. |
Two halves share one source-agnostic aggregation library:
teamvelocity.config.json .env (GITHUB_TOKEN)
│ │
▼ ▼
CLI puller (cli/) ── SourceConnector plugins (GitHub via Octokit)
fetch + normalize → shared/aggregate (pure metrics) → writer → public/data/*.json
│ (static JSON)
▼
React SPA (src/) — NO token, NO GitHub calls — fetch('/data/…') → dashboards + drill-down
Invariants (see docs/prd.md §3 / CLAUDE.md):
- The SPA (
src/) never importscli/, Octokit, the config, or the token, and makes no GitHub request — enforced by an ESLintno-restricted-importsboundary. Its only data source isfetchof files under/data/. - Connectors only fetch + normalize — no metric math lives in a connector.
- Velocity, PR-score, and week bucketing are pure functions in
shared/aggregate/(the TDD core), identical across sources. - Everything in
public/data/is Zod-validated — the writer validates before writing, the loader validates after fetching. Fail loud on drift.
- Velocity (0–100) — a team-rhythm indicator, not a grade. Each week's log-dampened weighted
signal is normalized against the team's own rolling median/MAD, so a typical week lands near 50,
a stronger week climbs toward 100, and a quiet/PTO week falls toward 0 automatically. Weights and
color thresholds are configurable per team (
docs/prd.md §7.1). - PR relevance (0–100) — per-PR signals (LOC, review activity, participants, files, commits,
linked issues), log-dampened and min-max normalized within the week's PR set, then weighted
(
docs/prd.md §7.2).
src/ React SPA (never imports cli/ or the token)
cli/ data puller: config, connectors/ (GitHub), writer, pull, sample generator
shared/ pure source-agnostic aggregation + Zod schema (imported by cli and tests)
public/data/ generated (git-ignored) + a committed sample/ (+ sample-solo/) fixture set
e2e/ Playwright specs over the sample dataset
docs/ the PRD
Bitbucket/JIRA are out of v1 scope, but the seam exists: implement SourceConnector
(cli/connectors/types.ts), register it in cli/connectors/index.ts, and give members the
matching handle in the config. No UI or metric changes are needed — aggregation and scoring are
source-agnostic.
- Unit/integration (Vitest): pure logic is TDD'd (week bucketing incl. DST, velocity, PR-score,
config + JSON validation); the GitHub connector runs against recorded fixtures (no live network);
the writer emits Zod-valid JSON. Date tests run under
TZ=Europe/Prague. - E2E (Playwright): team-select (+ single-team auto-skip), the full drill-down and deselect, date-range changes, a PR row linking to GitHub, and the "no data yet" state — all over the committed sample dataset, zero network, no token.
Run everything: `bun run lint && bun run typecheck && bun run test && bun run test:e2e`.

{ "teams": [ { "id": "jdt", // stable slug used in data paths + URLs "name": "JDT", "sprintStartsOn": "Wed", // week boundary: Mon..Sun (default Mon) "sprintStartHour": 0, // hour the boundary flips (default 0) "members": [ { "name": "Jeff", "github": "jeffhandle" }, { "name": "Jack", "github": "https://github.com/jackhandle" } ], "repos": ["acme/backend", "acme/web"], // OPTIONAL — needed for private repos "thresholds": { "red": 40, "green": 70 } // OPTIONAL — velocity color bands } ] }