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
2026-05-05 15:24:55 -04:00

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.

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:

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.isheatmap: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

# 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:

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:

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

docker compose -f /mnt/<pool>/gitea-heatmap-sidecar/docker-compose.yml up -d --build

Confirm it's healthy:

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:

docker exec <gitea-app-container> printenv GITEA_CUSTOM

Then follow step 4 of the main setup 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

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:

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

S
Description
No description provided
Readme 71 KiB
Languages
Go 68.5%
Go Template 28.3%
Dockerfile 3.2%