From e94d2f64816d9338e8bf7eb70db15151ecfe21db Mon Sep 17 00:00:00 2001 From: didericis Date: Tue, 5 May 2026 15:24:55 -0400 Subject: [PATCH] Add repo names to heatmap tooltip Extends the JSON response to include per-day repo names via array_agg on action.repo_name, and surfaces them in the hover tooltip below the contribution count. Co-Authored-By: Claude Sonnet 4.6 --- README.md | 8 ++++---- main.go | 22 +++++++++++++++------- templates/profile-snippet.tmpl | 9 ++++++--- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 03ed44b..a6bead8 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,9 @@ 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 and count -- ❌ Repo names, commit messages, branches, file content — none of that ever - leaves the database. Only counts. +- ✅ Hover tooltips with the date, count, and repo names +- ❌ Commit messages, branches, file content — none of that ever leaves the + database. ## Architecture @@ -102,7 +102,7 @@ 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}, ...]` +- `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. diff --git a/main.go b/main.go index edef855..cd6a39a 100644 --- a/main.go +++ b/main.go @@ -1,8 +1,10 @@ // Package main is a tiny HTTP service that exposes daily contribution counts -// for a single allowlisted Gitea user, including activity from private repos. +// and repo names for a single allowlisted Gitea user, including activity from +// private repos. // // It reads directly from the Gitea database (the `action` table) using a -// read-only DB user. Output is JSON: [{"date":"2026-05-04","count":3}, ...]. +// read-only DB user. Output is JSON: +// [{"date":"2026-05-04","count":3,"repos":["myrepo"]}, ...]. // // Pair with templates/profile-snippet.tmpl on the Gitea side to render a // GitHub-style heatmap with hover tooltips on a user profile page. @@ -22,7 +24,7 @@ import ( "syscall" "time" - _ "github.com/lib/pq" + "github.com/lib/pq" ) // ---- config --------------------------------------------------------------- @@ -90,8 +92,9 @@ type heatmapHandler struct { } type dayCount struct { - Date string `json:"date"` - Count int `json:"count"` + Date string `json:"date"` + Count int `json:"count"` + Repos []string `json:"repos"` } func (h *heatmapHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -154,7 +157,12 @@ func (h *heatmapHandler) queryHeatmap(ctx context.Context, username string) ([]d // PostgreSQL doesn't accept lists as a single placeholder. query := fmt.Sprintf(` SELECT to_char(to_timestamp(created_unix) AT TIME ZONE 'UTC', 'YYYY-MM-DD') AS day, - COUNT(*)::int + COUNT(*)::int, + COALESCE( + array_agg(DISTINCT repo_name ORDER BY repo_name) + FILTER (WHERE repo_name IS NOT NULL AND repo_name != ''), + '{}' + ) AS repos FROM "action" WHERE act_user_id = $1 AND created_unix >= $2 @@ -172,7 +180,7 @@ func (h *heatmapHandler) queryHeatmap(ctx context.Context, username string) ([]d var out []dayCount for rows.Next() { var d dayCount - if err := rows.Scan(&d.Date, &d.Count); err != nil { + if err := rows.Scan(&d.Date, &d.Count, pq.Array(&d.Repos)); err != nil { return nil, fmt.Errorf("scan: %w", err) } out = append(out, d) diff --git a/templates/profile-snippet.tmpl b/templates/profile-snippet.tmpl index 97310cf..9f41315 100644 --- a/templates/profile-snippet.tmpl +++ b/templates/profile-snippet.tmpl @@ -56,7 +56,7 @@ return; } - const byDate = Object.fromEntries(data.map(d => [d.date, d.count])); + const byDate = Object.fromEntries(data.map(d => [d.date, d])); const bucket = c => c === 0 ? 0 : c < 3 ? 1 : c < 6 ? 2 : c < 10 ? 3 : 4; @@ -85,10 +85,13 @@ if (d > today) { cell.classList.add('future'); } else { - const c = byDate[fmt(d)] || 0; + const entry = byDate[fmt(d)]; + const c = entry ? entry.count : 0; + const repos = entry && entry.repos && entry.repos.length ? entry.repos : []; cell.dataset.level = String(bucket(c)); // Native browser tooltip. For richer ones, swap in Tippy.js etc. - cell.title = `${fmt(d)}: ${c} contribution${c === 1 ? '' : 's'}`; + const repoLine = repos.length ? '\n' + repos.join('\n') : ''; + cell.title = `${fmt(d)}: ${c} contribution${c === 1 ? '' : 's'}${repoLine}`; } frag.appendChild(cell); }