Static activity dashboard for tracked OpenSats-funded bitcoin and nostr repositories.
heartbeat365 renders commits, pull requests / merge requests, issues, releases, and NIP-34 nostr repo activity as a git log --oneline-style timeline. This fork is configured with a 365-day data window for annual research.
Live site: https://arvin21m.github.io/heartbeat365/
Live dataset: https://arvin21m.github.io/heartbeat365/data/events.json
Filter tool: https://arvin21m.github.io/heartbeat365/filter.html
This is a fork of OpenSats/heartbeat, extended for longer research windows, multi-host fetching, host-aware event identity, GitHub Pages deployment, strict author filtering, virtualized rendering, browser-side JSON filtering, and monthly repo health checks.
heartbeat365 currently fetches activity from five provider families:
- GitHub — GraphQL provider for bare
owner/nameentries. - Codeberg / Forgejo / Gitea — REST provider for
codeberg:owner/nameand registered self-hosted Forgejo/Gitea instance labels. - GitLab.com — REST v4 provider for
gitlab:group/projectand nested GitLab namespace paths. - Plain Git URLs — commits-only provider for
git:https://...entries, using shallow clone +git log. - NIP-34 nostr repos — nostr provider for
nostr:naddr1...repo announcements, usingnostr-tools, relay queries, and signature verification.
Supported event types are:
commitpr_openedpr_mergedpr_closedissue_openedissue_closedrelease
Notes:
- GitHub, Forgejo/Gitea, Codeberg, and GitLab can provide commits, PRs/MRs, issues, and releases where the host API exposes them.
- Plain Git URL entries are commits-only. Git alone does not expose PRs, issues, releases, comments, or reviews.
- NIP-34 nostr entries currently track patch / PR / issue style events and status events. NIP-34 repo state events are not converted into fake commit events; pair a
nostr:naddr1...entry with agit:https://...entry if you want commits for the same repo. - Mirrors may appear as separate repos if the same project is tracked on multiple hosts. Cross-host mirror deduplication is intentionally not implemented yet.
A build-time script reads the repos*.yml files, routes each repo entry to the correct provider, fetches activity, normalizes the events into one shared schema, and writes:
public/data/events.json
The browser loads that static JSON file. Visitors do not call GitHub, GitLab, Codeberg, Forgejo, Gitea, nostr relays, or Git remotes directly. Normal site usage does not consume provider API rate limits.
The full dataset shape is defined in src/types.ts.
Each event includes host-aware identity fields:
{
id: string
host: string
repoKey: string
repo: string
type: EventType
timestamp: string
actorKey: string
actor: string
title: string
url: string
shortId: string
}repo and actor are kept simple for display and search. repoKey and actorKey include the host and are used internally so same-named repos or users on different hosts do not collide.
Repository entries live in repos*.yml files at the project root.
Supported entry forms:
repos:
# GitHub, implicit
- owner/repo
# Built-in Codeberg / Forgejo
- codeberg:owner/repo
# Built-in GitLab.com
- gitlab:group/project
- gitlab:group/subgroup/project
# Plain Git URL, commits only
- git:https://example.com/owner/repo
- git:https://example.com/owner/repo.git
# NIP-34 nostr repo announcement
- "nostr:naddr1..."
# Self-hosted Forgejo/Gitea instance label from instances.yml
- mygitea:owner/repoRules:
- Bare
owner/namemeans GitHub. github:is intentionally not accepted as an explicit prefix. Use bareowner/name.codeberg:is built in.gitlab:supports nested groups, such asgitlab:group/subgroup/project.git:https://...must use HTTPS and is commits-only.nostr:naddr1...values should be quoted in YAML because of the embedded colon.- Self-hosted Forgejo/Gitea prefixes must be registered in
instances.yml.
Files are merged and deduplicated during the fetch step. Fund buckets are determined from the file name unless the file sets an explicit fund: value.
Examples:
# repos.general.yml
repos:
- bitcoin/bitcoin
- codeberg:joinmarket-ng/joinmarket-ng
- gitlab:gitlab-org/cli# repos.nostr.yml
fund: nostr
repos:
- nostr-dev-kit/ndk
- "nostr:naddr1..."Self-hosted Forgejo/Gitea instances are registered in instances.yml.
Example:
mygitea:
baseUrl: "https://gitea.example.org/api/v1"
tokenEnv: "MYGITEA_TOKEN"
someforgejo:
baseUrl: "https://forgejo.example.org/api/v1"Rules:
- Host labels must be lowercase alphanumeric only.
- Built-in labels such as
codeberg,gitlab,git,nostr, andgithubcannot be redefined. baseUrlshould point at the instance REST API root.tokenEnvis optional. If present, it names the environment variable that holds the token for that instance.- If
tokenEnvis omitted or empty, requests are unauthenticated.
After registering an instance, use the label in any repos*.yml file:
repos:
- mygitea:owner/repoThe deployed site publishes the full dataset alongside the app:
/data/events.json
Current deployed behavior:
- The GitHub Pages workflow fetches a 365-day dataset.
- The workflow runs on pushes to
master. - The workflow refreshes on a scheduled cron every 6 hours.
- The workflow can also be triggered manually from the Actions tab.
- Each successful refresh overwrites the published
events.json. - The published site is static.
- The browser only reads the generated JSON file.
The live events.json file is not a historical archive. If you need durable historical snapshots, download and archive JSON files separately.
All dashboard filters run client-side on the already-loaded dataset. Most filters serialize to the URL, so filtered views can be shared.
- UI:
30d / 60d / 90d / 180d / 365dchips - URL param:
?window=N - Match behavior: shows events from the last
Ndays, limited by the built dataset
- UI: fund chips
- URL param:
?funds=... - Match behavior: repos in the selected fund bucket
- UI:
filter:text input - URL param:
?q=... - Match behavior: substring match across displayed repo paths
- UI: repo chips
- URL param:
?repos=... - Match behavior: exact repo match
- UI:
author:text input - URL param:
?author=... - Match behavior: exact actor match against the event actor
- UI: event-type chips
- URL param:
?types=... - Match behavior: commit, pull request / merge request, issue, and release event subsets
- UI: clicking a username in the timeline
- URL param:
?devs=... - Match behavior: exact event actor selected from the UI
Notes:
?q=is repo-name search only.- It does not search authors or event text.
?author=is the strict author filter.- Use
?author=when preparing single-developer or single-grantee research. - Fund names come from the
repos*.ymlfiles at the project root. - Window chips larger than the built dataset may be disabled in the UI.
- A 365-day view requires the dataset to be fetched with at least
HEARTBEAT_WINDOW_DAYS=365.
The standalone filter tool is available at:
/filter.html
It is useful when the full events.json file is too large for analysis, archiving, or uploading elsewhere.
The filter tool loads the latest deployed data/events.json and lets you filter by:
- fund
- time window
- event types
- exact repo names, one per line
- developer usernames, one per line
The filter tool supports these time windows:
30d60d90d180d365dall
The filter tool defaults to 90d. Choose 365d or all if you want the full annual dataset.
The preview shows:
- events kept
- repos kept
- developers kept
- date range
- estimated download size
The download button creates a local file named like:
events-filtered-YYYY-MM-DD-HH-MM.json
The filtered export keeps the same core event data shape and also adds filter metadata:
{
generatedAt: string
filteredAt: string
windowDays: number | null
filters: {
fund: string | null
repos: string[]
devs: string[]
types: string[]
}
repos: string[]
funds: Record<string, string[]>
events: Event[]
}All filtering happens locally in the browser. Nothing is uploaded anywhere.
Requires Node 22+.
npm install
export GITHUB_TOKEN=ghp_yourtoken
export HEARTBEAT_WINDOW_DAYS=365
npm run fetch
npm run devGITHUB_TOKEN or GH_TOKEN is required if the config includes GitHub repos.
Optional provider tokens:
export CODEBERG_TOKEN=...
export GITLAB_TOKEN=...
export MYGITEA_TOKEN=...For public Codeberg, GitLab, and self-hosted Forgejo/Gitea repos, unauthenticated requests may work, but tokens are useful for rate limits and private/inaccessible repos.
Plain Git URL entries do not use API tokens.
NIP-34 nostr entries do not use API tokens.
If you omit HEARTBEAT_WINDOW_DAYS, the fetch script uses the upstream-compatible default of 90.
To generate the deployed-style annual dataset locally, keep:
export HEARTBEAT_WINDOW_DAYS=365npm run dev- Start the Vite dev server.npm run fetch- Fetch activity and writepublic/data/events.json.npm run build- Type-check and build the static site.npm run preview- Preview the built site locally.npm run typecheck- Run TypeScript checks without building.npm run lint- Run ESLint.npm run format- Format files with Prettier.npm run format:check- Check formatting.npm run vercel-build- Fetch data and build for Vercel.
Environment variables override the fetch defaults.
-
GITHUB_TOKEN- Default: required unless
GH_TOKENis set, when GitHub repos are configured - Purpose: GitHub token used by the GitHub provider
- Default: required unless
-
GH_TOKEN- Default: optional fallback
- Purpose: alternative GitHub token variable
-
CODEBERG_TOKEN- Default: optional
- Purpose: token for the built-in Codeberg provider
-
GITLAB_TOKEN- Default: optional
- Purpose: token for the built-in GitLab.com provider
-
Custom
tokenEnvvalues frominstances.yml- Default: optional
- Purpose: tokens for self-hosted Forgejo/Gitea instances
HEARTBEAT_WINDOW_DAYS- Default:
90 - Purpose: number of days of history to fetch
- Default:
-
HEARTBEAT_COMMITS_PAGE_SIZE- Default: GitHub
100, Forgejo/Gitea50, GitLab50 - Purpose: page size for commits
- Default: GitHub
-
HEARTBEAT_PRS_PAGE_SIZE- Default: GitHub
50, Forgejo/Gitea50, GitLab merge requests50 - Purpose: page size for pull requests / merge requests
- Default: GitHub
-
HEARTBEAT_ISSUES_PAGE_SIZE- Default:
50 - Purpose: page size for issues
- Default:
-
HEARTBEAT_RELEASES_PAGE_SIZE- Default:
20 - Purpose: page size for releases
- Default:
-
HEARTBEAT_COMMITS_MAX_PER_REPO- Default:
5000 - Purpose: hard cap on commits per repo
- Default:
-
HEARTBEAT_PRS_MAX_PER_REPO- Default:
1000 - Purpose: hard cap on pull requests / merge requests per repo
- Default:
-
HEARTBEAT_ISSUES_MAX_PER_REPO- Default:
1000 - Purpose: hard cap on issues per repo
- Default:
-
HEARTBEAT_RELEASES_MAX_PER_REPO- Default:
200 - Purpose: hard cap on releases per repo
- Default:
The caps are safety limits. The normal terminator is the selected HEARTBEAT_WINDOW_DAYS cutoff.
The shared retry helper is used around transient provider failures.
It retries:
- selected HTTP 5xx responses
- network errors such as resets, temporary DNS failures, and timeouts
- fetch network failures
It does not retry:
- HTTP 4xx responses
- 404 not found
- authentication / authorization failures
- programming errors
Default retry budget:
- 3 total attempts
- exponential backoff
- jitter
- roughly 10 seconds maximum wall-time per retried request
Vite uses this base path:
HEARTBEAT_BASE
Default:
/
The GitHub Pages workflow overrides the build base directly with:
npx vite build --base=/heartbeat365/For another static host, set the base path to match where the app will be served.
This fork includes a GitHub Pages workflow at:
.github/workflows/build.yml
It runs on:
- push to
master - scheduled cron every 6 hours
- manual workflow dispatch
The workflow:
- checks out the repo
- installs Node 22 dependencies
- runs
npm run fetch - sets
HEARTBEAT_WINDOW_DAYS=365 - builds with
npx vite build --base=/heartbeat365/ - deploys
dist/to GitHub Pages
Required repo secret:
HEARTBEAT_PAT
Use a GitHub token with access to the tracked GitHub repositories.
Optional repo secrets, depending on configured providers:
CODEBERG_TOKEN
GITLAB_TOKEN
<custom tokenEnv values from instances.yml>
The workflow currently performs a full fetch on every push, even for README or UI-only changes. A previous attempt to decouple fetch from build was reverted. Treat future fetch/build decoupling as a separate design task, not part of normal README or version-bump work.
This fork includes a monthly repo health-check workflow at:
.github/workflows/repo-health.yml
It runs on the 1st of each month and can also be triggered manually from the Actions tab.
Current limitation: the health-check workflow is GitHub-oriented. It checks repo YAML entries against the GitHub API and may not correctly understand non-GitHub prefixed entries such as codeberg:, gitlab:, git:, nostr:, or self-hosted Forgejo/Gitea labels. Treat its results as a GitHub repo-health helper, not a complete multi-provider health checker.
The workflow uses this repo secret:
HEARTBEAT_PAT
If issues are found, it opens a GitHub Issue with the repo-health label. Make sure Issues are enabled on the repository.
If the repository requires labels to exist before use, create the repo-health label.
The upstream-style Vercel build script is still available:
npm run vercel-buildThat runs:
npm run fetch && npm run buildFor Vercel, configure environment variables in the Vercel project settings:
GITHUB_TOKEN
HEARTBEAT_WINDOW_DAYS
Set HEARTBEAT_WINDOW_DAYS=365 if you want an annual dataset. Omit it to use the fetch script's default 90.
For scheduled Vercel refreshes, set this repo secret:
VERCEL_DEPLOY_HOOK_URL
The included workflow at .github/workflows/refresh.yml pings that deploy hook every 6 hours.
You can also build in CI and serve dist/ from any static host.
Example:
- name: Fetch events
run: npm run fetch
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HEARTBEAT_WINDOW_DAYS: '365'
- name: Build
run: npm run buildFor subpath hosting, set the Vite base path appropriately.
Example:
HEARTBEAT_BASE=/heartbeat365/ npm run buildOr pass Vite's base flag directly:
npx vite build --base=/heartbeat365/scripts/
├── fetch.ts
├── lib/
│ └── retry.ts
└── providers/
├── github.ts
├── forgejo.ts
├── gitlab.ts
├── git.ts
└── nostr.ts
src/
└── types.ts
repos.general.yml
repos.nostr.yml
repos.opensats.yml
instances.yml
fetch.ts owns config loading, repo-entry parsing, provider routing, dataset assembly, and writing public/data/events.json.
Provider modules own host-specific fetching and event shaping.
src/types.ts owns the shared Zod schemas for events, datasets, repo config, and self-hosted instance config.
- Plain Git URLs are commits-only.
- NIP-34 repo state events are not converted into commit events.
- Cross-host mirror deduplication is not implemented.
- The monthly repo-health workflow is GitHub-oriented and not a complete multi-provider checker.
- The workflow currently does a full fetch on every push.
package.jsonstill reports version0.1.0; the version has not yet been bumped for the multi-provider upgrade.
Built on OpenSats/heartbeat by the OpenSats team.
MIT — see LICENSE.