Files
gitea-heatmap-sidecar/README.md
T
didericis ba66aba286 Add TrueNAS SCALE setup guide to README
Step-by-step instructions for Electric Eel (24.10+): finding the DB
container and network, creating the read-only user, deploying with
docker compose, reverse proxying, and locating the Gitea custom dir.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 15:31:28 -04:00

243 lines
8.1 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
### 1. Create a read-only Postgres user
Run `db/setup.sql` against the Gitea database as a superuser. Edit the
password first.
```bash
psql -U postgres -d gitea -f db/setup.sql
```
Only `SELECT` on `"user"` and `"action"` is granted. If Gitea ever renames
either table in a migration, the service will break loudly — that's the goal.
### 2. Build and run the sidecar
Edit `docker-compose.example.yml`, then:
```bash
docker compose -f docker-compose.example.yml up -d --build
```
Make sure the `networks` block matches your existing Gitea Docker network so
the sidecar can reach `gitea-db` by hostname.
Required env vars:
| Var | Description |
|------------------|----------------------------------------------------------|
| `DATABASE_URL` | `postgres://heatmap_ro:...@host:5432/gitea?sslmode=...` |
| `ALLOWED_USERS` | Comma-separated lowercase usernames (e.g. `didericis`) |
| `ALLOWED_ORIGIN` | CORS origin — must match Gitea's URL |
| `OP_TYPES` | Optional. Comma-separated `op_type` ints. See below. |
| `LISTEN` | Optional. Default `:8080`. |
### 3. Reverse proxy
Expose the service at a hostname Gitea's frontend can reach over HTTPS — e.g.
`heatmap.dideric.is``heatmap:8080`. Use the same TLS setup as Gitea
itself (Caddy/Traefik/nginx).
### 4. Install the profile template override
Copy `templates/user/profile.tmpl` from the Gitea source matching your
running version into `$GITEA_CUSTOM/templates/user/profile.tmpl`, then merge
in the snippet from `templates/profile-snippet.tmpl` near the existing
heatmap block.
Replace `HEATMAP_BASE_URL` in the snippet with your sidecar's public URL
(e.g. `https://heatmap.dideric.is`) and `didericis` with the username you're
exposing.
Restart Gitea, hit the profile page in incognito, and you should see the
heatmap populate.
## TrueNAS SCALE setup
Tested on TrueNAS SCALE 24.10 (Electric Eel) with Gitea installed via the
official app catalog. All commands below run over SSH on the TrueNAS host.
### 1. Find Gitea's DB container and Docker network
```bash
# List Gitea-related containers
docker ps --format '{{.Names}}' | grep -i gitea
# Find the network the Gitea app container is on
docker inspect <gitea-app-container> \
--format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}}{{end}}'
```
Note both the DB container name (typically ends in `-postgres` or `-db`) and
the network name — you'll need them in the steps below.
### 2. Get the Gitea DB password
In the TrueNAS UI: **Apps → Installed Apps → Gitea → Edit → Database
Configuration**. The Postgres password is visible there. You'll use it in
steps 3 and 4.
### 3. Create the read-only DB user
Edit `db/setup.sql` to set a password for `heatmap_ro`, then run it against
the DB container:
```bash
docker exec -i <gitea-db-container> psql -U gitea -d gitea < db/setup.sql
```
### 4. Clone and configure
Put the repo somewhere on persistent storage:
```bash
git clone https://github.com/didericis/gitea-heatmap-sidecar \
/mnt/<pool>/gitea-heatmap-sidecar
cd /mnt/<pool>/gitea-heatmap-sidecar
cp docker-compose.example.yml docker-compose.yml
```
Edit `docker-compose.yml`:
- Set `DATABASE_URL` — use the DB container name as the hostname, e.g.
`postgres://heatmap_ro:PASSWORD@gitea-db-container-name:5432/gitea?sslmode=disable`
- Set `ALLOWED_USERS` and `ALLOWED_ORIGIN`
- Under `networks.gitea`, set `name` to the network name from step 1 and
uncomment `external: true`
### 5. Build and start
```bash
docker compose -f /mnt/<pool>/gitea-heatmap-sidecar/docker-compose.yml up -d --build
```
Confirm it's healthy:
```bash
docker exec gitea-heatmap wget -qO- http://localhost:8080/healthz
```
### 6. Reverse proxy
Expose port 8080 at a public HTTPS hostname. With **Nginx Proxy Manager**
(a common TrueNAS app on the same Docker network):
- Scheme: `http`, Forward hostname: `gitea-heatmap` (the container name),
Port: `8080`
- Enable SSL via Let's Encrypt
For Traefik or Caddy configured as TrueNAS apps, wire it up the same way —
the sidecar is reachable by container name on the shared network.
### 7. Find the Gitea custom directory
In the TrueNAS UI: **Apps → Installed Apps → Gitea → Edit → Storage**.
Find the host path mapped to the Gitea data volume. The custom directory
Gitea reads templates from is the `custom/` subdirectory of that path —
check the `GITEA_CUSTOM` env var in the container if unsure:
```bash
docker exec <gitea-app-container> printenv GITEA_CUSTOM
```
Then follow [step 4 of the main setup](#4-install-the-profile-template-override)
to install the template override into that directory and restart Gitea.
## 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.** 1-hour `Cache-Control` header keeps load trivial. Drop to 5
minutes for snappier updates if needed; the underlying query is cheap
thanks to the existing index on `(user_id, act_user_id, created_unix)`
added in Gitea 1.24.
## 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