Files
didericis 81cd3bd919 Add in-memory result cache with 1-hour TTL
DB is now queried at most once per hour per user; subsequent requests
within the window are served from memory. TTL matches the existing
Cache-Control header. Restart clears the cache immediately.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 17:00:33 -04:00

102 lines
3.5 KiB
Markdown

# gitea-heatmap-sidecar
A tiny HTTP service that exposes daily contribution counts for an allowlisted
Gitea user — **including private repo activity** — for rendering a
GitHub-style heatmap on a public profile page.
Stock Gitea (and Forgejo) intentionally do not expose private contribution
counts to anonymous viewers. This is a sidecar workaround: read directly from
Gitea's `action` table with a read-only DB user, return per-day counts as
JSON, and let a custom profile template render the squares client-side.
## What it shows / what it doesn't
- ✅ Daily counts (the green squares)
- ✅ Hover tooltips with the date, count, and repo names
- ❌ Commit messages, branches, file content — none of that ever leaves the
database.
## Architecture
```
[anonymous visitor] → GET /didericis (Gitea)
↓ profile.tmpl override loads
<script> fetches https://heatmap.dideric.is/heatmap/didericis.json
[this service] → SELECT FROM action WHERE act_user_id=? → JSON
```
## Setup
See [docs/truenas.md](docs/truenas.md) for the full TrueNAS SCALE setup guide
(Custom App Wizard + Gitea template override).
## Op type reference
Gitea's `action.op_type` is an integer enum. Defaults are commits-only
(`5,9,18`), which is the closest analog to what GitHub counts as a
"contribution" in their heatmap. Common values:
| Value | Meaning |
|-------|--------------------------|
| 1 | Create repo |
| 5 | Push commits |
| 6 | Create issue |
| 7 | Create pull request |
| 9 | Push tag |
| 11 | Merge pull request |
| 18 | Mirror sync push |
| 24 | Publish release |
Set `OP_TYPES=5,6,7,9,11,18,24` for a more inclusive count.
## Endpoints
- `GET /heatmap/{username}.json` — JSON `[{"date":"YYYY-MM-DD","count":N,"repos":["name", ...]}, ...]`
for the past ~53 weeks. 1-hour cache header.
- `GET /healthz` — 200 if the DB is reachable, 503 otherwise.
## Maintenance
- **Gitea schema changes.** The query reads from `"action"` and `"user"`.
These tables have been stable for many Gitea versions. After each Gitea
upgrade, hit `/healthz` and the JSON endpoint to confirm.
- **Template drift.** The harder part. Gitea's `profile.tmpl` does change
between versions — diff the new upstream against your override on each
upgrade and re-merge the snippet.
- **Cache.** Results are cached in memory for 1 hour (matching the
`Cache-Control: max-age=3600` response header), so the DB is only hit
once per hour per user regardless of traffic. Restart the service to
clear the cache immediately.
## Local dev
```bash
go run . # requires DATABASE_URL and ALLOWED_USERS in env
```
`go build ./...` produces a binary named `gitea-heatmap-sidecar` (derived from
the module name). The Dockerfile overrides this with `-o /heatmap`; the two
names differ but refer to the same program.
On NixOS:
```bash
nix-shell -p go_1_23 postgresql
```
## Limitations
- Postgres only. MySQL support would mean swapping `to_timestamp(...)` for
`FROM_UNIXTIME(...)` and adjusting identifier quoting — straightforward
but not done here.
- Single-tenant by allowlist. The service is meant for a personal Gitea
instance with a small known set of users opting in. Don't expose it to
arbitrary usernames; that would leak private activity counts for users
who haven't consented.
- No auth on the JSON endpoint by design — the data is intentionally public.
## License
MIT