Initial: Gitea heatmap sidecar with private contributions

This commit is contained in:
didericis
2026-05-05 19:15:19 +00:00
commit 9bc2429422
9 changed files with 585 additions and 0 deletions
+98
View File
@@ -0,0 +1,98 @@
{{/*
Drop this into $GITEA_CUSTOM/templates/user/profile.tmpl as part of a full
profile.tmpl override. Place near the existing heatmap section (search for
"heatmap" in the upstream profile.tmpl for your Gitea version).
The {{if eq .ContextUser.LowerName ...}} guard means only the named user
gets the override; everyone else keeps the stock heatmap.
The HEATMAP_BASE_URL placeholder must be replaced at install time with the
reverse-proxied URL of the sidecar service, e.g. https://heatmap.dideric.is
*/}}
{{if eq .ContextUser.LowerName "didericis"}}
<div class="ui attached segment private-heatmap-wrap">
<h4 class="ui header">Contributions (incl. private)</h4>
<div id="private-heatmap"
data-user="{{.ContextUser.LowerName}}"
data-source="HEATMAP_BASE_URL/heatmap/{{.ContextUser.LowerName}}.json">
<noscript>JavaScript is required to render this heatmap.</noscript>
</div>
</div>
<style>
.private-heatmap-wrap #private-heatmap {
display: grid;
grid-template-columns: repeat(53, 11px);
grid-template-rows: repeat(7, 11px);
gap: 2px;
overflow-x: auto;
padding: 4px 0;
}
.private-heatmap-wrap .cell {
border-radius: 2px;
background: var(--color-secondary, #161b22);
}
.private-heatmap-wrap .cell[data-level="1"] { background: #0e4429; }
.private-heatmap-wrap .cell[data-level="2"] { background: #006d32; }
.private-heatmap-wrap .cell[data-level="3"] { background: #26a641; }
.private-heatmap-wrap .cell[data-level="4"] { background: #39d353; }
.private-heatmap-wrap .cell.future { background: transparent; }
</style>
<script>
(async () => {
const el = document.getElementById('private-heatmap');
if (!el) return;
let data = [];
try {
const res = await fetch(el.dataset.source, { credentials: 'omit' });
if (!res.ok) throw new Error('HTTP ' + res.status);
data = await res.json();
} catch (e) {
el.textContent = 'Heatmap unavailable.';
console.warn('private-heatmap fetch failed:', e);
return;
}
const byDate = Object.fromEntries(data.map(d => [d.date, d.count]));
const bucket = c =>
c === 0 ? 0 : c < 3 ? 1 : c < 6 ? 2 : c < 10 ? 3 : 4;
const today = new Date();
today.setUTCHours(0, 0, 0, 0);
// Anchor rightmost column to the most recent Saturday (>= today).
const end = new Date(today);
while (end.getUTCDay() !== 6) end.setUTCDate(end.getUTCDate() + 1);
const start = new Date(end);
start.setUTCDate(start.getUTCDate() - (53 * 7 - 1));
const fmt = d => d.toISOString().slice(0, 10);
const frag = document.createDocumentFragment();
for (let i = 0; i < 53 * 7; i++) {
const d = new Date(start);
d.setUTCDate(d.getUTCDate() + i);
const cell = document.createElement('div');
cell.className = 'cell';
cell.style.gridColumn = Math.floor(i / 7) + 1;
cell.style.gridRow = (i % 7) + 1;
if (d > today) {
cell.classList.add('future');
} else {
const c = byDate[fmt(d)] || 0;
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'}`;
}
frag.appendChild(cell);
}
el.appendChild(frag);
})();
</script>
{{end}}