diff --git a/.gitea/workflows/update-readme.yaml b/.gitea/workflows/update-readme.yaml index d66c80c..1441e0e 100644 --- a/.gitea/workflows/update-readme.yaml +++ b/.gitea/workflows/update-readme.yaml @@ -3,6 +3,7 @@ on: push: paths: - 'public/code-stats.json' + - 'public/heatmap.json' workflow_dispatch: jobs: @@ -26,6 +27,11 @@ jobs: elif command -v apt-get &> /dev/null; then apt-get update && apt-get install -y jq; fi fi + if ! command -v python3 &> /dev/null; then + if command -v apk &> /dev/null; then apk add --no-cache python3; + elif command -v apt-get &> /dev/null; then apt-get update && apt-get install -y python3; fi + fi + # Use our own heatmap.json instead of Gitea API HEATMAP_FILE="public/heatmap.json" TOTAL_COMMITS=$(jq '[.days[] | .count] | add' "$HEATMAP_FILE") @@ -37,6 +43,8 @@ jobs: FORMATTED_LOC=$(printf "%'d" $LOC) echo "FORMATTED_LOC=$FORMATTED_LOC" >> $GITHUB_ENV + python3 scripts/generate_heatmap_svg.py public/heatmap.json public/heatmap.svg + sed "s/REPLACE_ME_LOC/$FORMATTED_LOC/g" README.template | \ sed "s/REPLACE_ME_COMMITS/$TOTAL_COMMITS/g" > README.md @@ -44,11 +52,11 @@ jobs: run: | git config user.name "Ares-Bot" git config user.email "ares@beane.me" - git add README.md + git add README.md public/heatmap.svg if git diff --staged --quiet; then echo "No changes to commit" else - git commit -m "chore: sync code velocity to ${{ env.FORMATTED_LOC }} LOC [skip ci]" + git commit -m "chore: sync profile metrics [skip ci]" git push http://patrick:${{ secrets.PAT_TOKEN }}@gitea-http:3000/${{ github.repository }}.git ${{ github.ref_name }} fi diff --git a/README.md b/README.md index 6810fb3..24a25ec 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,10 @@ Actively used across this environment or in adjacent projects: - **Ares:** Ryzen 9 9950X sustaining ~0.06 load avg while running Gitea and a Kubernetes control plane - **Resilience:** Automated failover between AWS and peer nodes +## 📈 Activity + +![Commit heatmap](public/heatmap.svg) + --- ### 🧩 Deployment Patterns diff --git a/README.template b/README.template index 8196743..e0f361d 100644 --- a/README.template +++ b/README.template @@ -77,6 +77,10 @@ Actively used across this environment or in adjacent projects: - **Ares:** Ryzen 9 9950X sustaining ~0.06 load avg while running Gitea and a Kubernetes control plane - **Resilience:** Automated failover between AWS and peer nodes +## 📈 Activity + +![Commit heatmap](public/heatmap.svg) + --- ### 🧩 Deployment Patterns diff --git a/public/heatmap.svg b/public/heatmap.svg new file mode 100644 index 0000000..dfb3386 --- /dev/null +++ b/public/heatmap.svg @@ -0,0 +1,392 @@ + + +Mar +May +Jun +Jul +Aug +Sep +Oct +Nov +Dec +Jan +Feb +Mar +Apr +Mon +Wed +Fri +2025-03-30: 0 commits +2025-03-31: 0 commits +2025-04-01: 0 commits +2025-04-02: 0 commits +2025-04-03: 0 commits +2025-04-04: 0 commits +2025-04-05: 0 commits +2025-04-06: 0 commits +2025-04-07: 0 commits +2025-04-08: 0 commits +2025-04-09: 0 commits +2025-04-10: 0 commits +2025-04-11: 0 commits +2025-04-12: 0 commits +2025-04-13: 0 commits +2025-04-14: 0 commits +2025-04-15: 0 commits +2025-04-16: 0 commits +2025-04-17: 0 commits +2025-04-18: 0 commits +2025-04-19: 0 commits +2025-04-20: 0 commits +2025-04-21: 0 commits +2025-04-22: 0 commits +2025-04-23: 0 commits +2025-04-24: 0 commits +2025-04-25: 0 commits +2025-04-26: 0 commits +2025-04-27: 0 commits +2025-04-28: 0 commits +2025-04-29: 0 commits +2025-04-30: 0 commits +2025-05-01: 0 commits +2025-05-02: 0 commits +2025-05-03: 0 commits +2025-05-04: 0 commits +2025-05-05: 0 commits +2025-05-06: 0 commits +2025-05-07: 0 commits +2025-05-08: 0 commits +2025-05-09: 0 commits +2025-05-10: 0 commits +2025-05-11: 0 commits +2025-05-12: 0 commits +2025-05-13: 0 commits +2025-05-14: 0 commits +2025-05-15: 0 commits +2025-05-16: 0 commits +2025-05-17: 0 commits +2025-05-18: 0 commits +2025-05-19: 0 commits +2025-05-20: 0 commits +2025-05-21: 0 commits +2025-05-22: 0 commits +2025-05-23: 0 commits +2025-05-24: 0 commits +2025-05-25: 0 commits +2025-05-26: 0 commits +2025-05-27: 0 commits +2025-05-28: 0 commits +2025-05-29: 0 commits +2025-05-30: 0 commits +2025-05-31: 0 commits +2025-06-01: 0 commits +2025-06-02: 0 commits +2025-06-03: 0 commits +2025-06-04: 0 commits +2025-06-05: 0 commits +2025-06-06: 0 commits +2025-06-07: 0 commits +2025-06-08: 0 commits +2025-06-09: 0 commits +2025-06-10: 0 commits +2025-06-11: 0 commits +2025-06-12: 0 commits +2025-06-13: 0 commits +2025-06-14: 0 commits +2025-06-15: 0 commits +2025-06-16: 0 commits +2025-06-17: 0 commits +2025-06-18: 0 commits +2025-06-19: 0 commits +2025-06-20: 0 commits +2025-06-21: 0 commits +2025-06-22: 0 commits +2025-06-23: 0 commits +2025-06-24: 0 commits +2025-06-25: 0 commits +2025-06-26: 0 commits +2025-06-27: 0 commits +2025-06-28: 0 commits +2025-06-29: 0 commits +2025-06-30: 0 commits +2025-07-01: 0 commits +2025-07-02: 0 commits +2025-07-03: 0 commits +2025-07-04: 0 commits +2025-07-05: 0 commits +2025-07-06: 0 commits +2025-07-07: 0 commits +2025-07-08: 0 commits +2025-07-09: 0 commits +2025-07-10: 0 commits +2025-07-11: 0 commits +2025-07-12: 0 commits +2025-07-13: 0 commits +2025-07-14: 0 commits +2025-07-15: 0 commits +2025-07-16: 0 commits +2025-07-17: 0 commits +2025-07-18: 0 commits +2025-07-19: 0 commits +2025-07-20: 0 commits +2025-07-21: 0 commits +2025-07-22: 0 commits +2025-07-23: 0 commits +2025-07-24: 0 commits +2025-07-25: 0 commits +2025-07-26: 0 commits +2025-07-27: 0 commits +2025-07-28: 0 commits +2025-07-29: 0 commits +2025-07-30: 0 commits +2025-07-31: 0 commits +2025-08-01: 0 commits +2025-08-02: 0 commits +2025-08-03: 0 commits +2025-08-04: 0 commits +2025-08-05: 0 commits +2025-08-06: 0 commits +2025-08-07: 0 commits +2025-08-08: 0 commits +2025-08-09: 5 commits +2025-08-10: 0 commits +2025-08-11: 0 commits +2025-08-12: 0 commits +2025-08-13: 0 commits +2025-08-14: 0 commits +2025-08-15: 0 commits +2025-08-16: 4 commits +2025-08-17: 0 commits +2025-08-18: 0 commits +2025-08-19: 0 commits +2025-08-20: 0 commits +2025-08-21: 0 commits +2025-08-22: 0 commits +2025-08-23: 0 commits +2025-08-24: 0 commits +2025-08-25: 0 commits +2025-08-26: 0 commits +2025-08-27: 0 commits +2025-08-28: 0 commits +2025-08-29: 0 commits +2025-08-30: 1 commit +2025-08-31: 0 commits +2025-09-01: 0 commits +2025-09-02: 0 commits +2025-09-03: 0 commits +2025-09-04: 0 commits +2025-09-05: 0 commits +2025-09-06: 0 commits +2025-09-07: 0 commits +2025-09-08: 0 commits +2025-09-09: 0 commits +2025-09-10: 0 commits +2025-09-11: 0 commits +2025-09-12: 0 commits +2025-09-13: 0 commits +2025-09-14: 0 commits +2025-09-15: 0 commits +2025-09-16: 0 commits +2025-09-17: 0 commits +2025-09-18: 0 commits +2025-09-19: 0 commits +2025-09-20: 0 commits +2025-09-21: 0 commits +2025-09-22: 0 commits +2025-09-23: 0 commits +2025-09-24: 0 commits +2025-09-25: 0 commits +2025-09-26: 0 commits +2025-09-27: 0 commits +2025-09-28: 0 commits +2025-09-29: 0 commits +2025-09-30: 0 commits +2025-10-01: 0 commits +2025-10-02: 0 commits +2025-10-03: 0 commits +2025-10-04: 6 commits +2025-10-05: 5 commits +2025-10-06: 0 commits +2025-10-07: 0 commits +2025-10-08: 0 commits +2025-10-09: 0 commits +2025-10-10: 0 commits +2025-10-11: 0 commits +2025-10-12: 0 commits +2025-10-13: 0 commits +2025-10-14: 0 commits +2025-10-15: 0 commits +2025-10-16: 0 commits +2025-10-17: 0 commits +2025-10-18: 0 commits +2025-10-19: 0 commits +2025-10-20: 0 commits +2025-10-21: 0 commits +2025-10-22: 0 commits +2025-10-23: 0 commits +2025-10-24: 0 commits +2025-10-25: 0 commits +2025-10-26: 0 commits +2025-10-27: 0 commits +2025-10-28: 0 commits +2025-10-29: 0 commits +2025-10-30: 0 commits +2025-10-31: 0 commits +2025-11-01: 0 commits +2025-11-02: 0 commits +2025-11-03: 0 commits +2025-11-04: 0 commits +2025-11-05: 0 commits +2025-11-06: 0 commits +2025-11-07: 0 commits +2025-11-08: 0 commits +2025-11-09: 0 commits +2025-11-10: 0 commits +2025-11-11: 0 commits +2025-11-12: 0 commits +2025-11-13: 0 commits +2025-11-14: 0 commits +2025-11-15: 0 commits +2025-11-16: 0 commits +2025-11-17: 0 commits +2025-11-18: 0 commits +2025-11-19: 0 commits +2025-11-20: 0 commits +2025-11-21: 0 commits +2025-11-22: 0 commits +2025-11-23: 0 commits +2025-11-24: 0 commits +2025-11-25: 0 commits +2025-11-26: 0 commits +2025-11-27: 0 commits +2025-11-28: 0 commits +2025-11-29: 0 commits +2025-11-30: 0 commits +2025-12-01: 0 commits +2025-12-02: 0 commits +2025-12-03: 0 commits +2025-12-04: 0 commits +2025-12-05: 0 commits +2025-12-06: 0 commits +2025-12-07: 0 commits +2025-12-08: 0 commits +2025-12-09: 0 commits +2025-12-10: 0 commits +2025-12-11: 0 commits +2025-12-12: 0 commits +2025-12-13: 0 commits +2025-12-14: 0 commits +2025-12-15: 0 commits +2025-12-16: 0 commits +2025-12-17: 0 commits +2025-12-18: 0 commits +2025-12-19: 0 commits +2025-12-20: 0 commits +2025-12-21: 0 commits +2025-12-22: 0 commits +2025-12-23: 0 commits +2025-12-24: 0 commits +2025-12-25: 0 commits +2025-12-26: 0 commits +2025-12-27: 0 commits +2025-12-28: 0 commits +2025-12-29: 0 commits +2025-12-30: 0 commits +2025-12-31: 0 commits +2026-01-01: 0 commits +2026-01-02: 0 commits +2026-01-03: 0 commits +2026-01-04: 0 commits +2026-01-05: 0 commits +2026-01-06: 0 commits +2026-01-07: 0 commits +2026-01-08: 10 commits +2026-01-09: 4 commits +2026-01-10: 0 commits +2026-01-11: 1 commit +2026-01-12: 9 commits +2026-01-13: 11 commits +2026-01-14: 23 commits +2026-01-15: 10 commits +2026-01-16: 10 commits +2026-01-17: 3 commits +2026-01-18: 23 commits +2026-01-19: 9 commits +2026-01-20: 28 commits +2026-01-21: 6 commits +2026-01-22: 15 commits +2026-01-23: 17 commits +2026-01-24: 5 commits +2026-01-25: 20 commits +2026-01-26: 56 commits +2026-01-27: 22 commits +2026-01-28: 16 commits +2026-01-29: 28 commits +2026-01-30: 16 commits +2026-01-31: 31 commits +2026-02-01: 0 commits +2026-02-02: 2 commits +2026-02-03: 1 commit +2026-02-04: 40 commits +2026-02-05: 10 commits +2026-02-06: 8 commits +2026-02-07: 1 commit +2026-02-08: 6 commits +2026-02-09: 2 commits +2026-02-10: 27 commits +2026-02-11: 6 commits +2026-02-12: 10 commits +2026-02-13: 2 commits +2026-02-14: 0 commits +2026-02-15: 1 commit +2026-02-16: 1 commit +2026-02-17: 3 commits +2026-02-18: 1 commit +2026-02-19: 4 commits +2026-02-20: 4 commits +2026-02-21: 0 commits +2026-02-22: 1 commit +2026-02-23: 2 commits +2026-02-24: 4 commits +2026-02-25: 5 commits +2026-02-26: 2 commits +2026-02-27: 9 commits +2026-02-28: 19 commits +2026-03-01: 9 commits +2026-03-02: 10 commits +2026-03-03: 2 commits +2026-03-04: 0 commits +2026-03-05: 14 commits +2026-03-06: 29 commits +2026-03-07: 10 commits +2026-03-08: 19 commits +2026-03-09: 2 commits +2026-03-10: 20 commits +2026-03-11: 27 commits +2026-03-12: 0 commits +2026-03-13: 37 commits +2026-03-14: 49 commits +2026-03-15: 9 commits +2026-03-16: 17 commits +2026-03-17: 21 commits +2026-03-18: 14 commits +2026-03-19: 10 commits +2026-03-20: 16 commits +2026-03-21: 7 commits +2026-03-22: 0 commits +2026-03-23: 1 commit +2026-03-24: 19 commits +2026-03-25: 9 commits +2026-03-26: 7 commits +2026-03-27: 0 commits +2026-03-28: 0 commits +2026-03-29: 2 commits +2026-03-30: 9 commits +2026-03-31: 6 commits +2026-04-01: 1 commit +2026-04-02: 10 commits +2026-04-03: 9 commits +2026-04-04: 4 commits + diff --git a/scripts/generate_heatmap_svg.py b/scripts/generate_heatmap_svg.py new file mode 100644 index 0000000..dd2f21f --- /dev/null +++ b/scripts/generate_heatmap_svg.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +import json +import math +import sys +from datetime import datetime, timedelta +from pathlib import Path + + +COLORS = [ + "#ebedf0", + "#9be9a8", + "#40c463", + "#30a14e", + "#216e39", +] + +CELL = 12 +GAP = 2 +RECT = CELL - GAP +LEFT_PAD = 30 +TOP_PAD = 20 +RIGHT_PAD = 10 +BOTTOM_PAD = 10 +FONT_FAMILY = "-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif" +LABEL_COLOR = "#57606a" + + +def load_heatmap(path): + with path.open() as handle: + return json.load(handle) + + +def date_range(start, end): + current = start + while current <= end: + yield current + current += timedelta(days=1) + + +def previous_sunday(day): + return day - timedelta(days=(day.weekday() + 1) % 7) + + +def next_saturday(day): + return day + timedelta(days=(5 - day.weekday()) % 7) + + +def intensity(count, max_count): + if count <= 0 or max_count <= 0: + return 0 + return max(1, min(4, math.ceil((count / max_count) * 4))) + + +def weekday_row(day): + return (day.weekday() + 1) % 7 + + +def month_labels(start, end): + labels = [(0, start.strftime("%b"))] + total_weeks = ((end - start).days // 7) + 1 + + for week in range(total_weeks): + week_start = start + timedelta(days=week * 7) + for offset in range(7): + day = week_start + timedelta(days=offset) + if day.day == 1 and week != 0: + labels.append((week, day.strftime("%b"))) + break + + return labels + + +def build_rect(day, start, counts, max_count): + date_str = day.isoformat() + count = counts.get(date_str, 0) + level = intensity(count, max_count) + x = LEFT_PAD + (((day - start).days // 7) * CELL) + y = TOP_PAD + (weekday_row(day) * CELL) + + commit_label = "commit" if count == 1 else "commits" + + return ( + f'' + f"{date_str}: {count} {commit_label}" + "" + ) + + +def generate_svg(data): + counts = {entry["date"]: entry["count"] for entry in data["days"]} + max_count = data["max_daily_commits"] + + start = datetime.strptime(data["from_date"], "%Y-%m-%d").date() + end = datetime.strptime(data["to_date"], "%Y-%m-%d").date() + aligned_start = previous_sunday(start) + aligned_end = next_saturday(end) + weeks = ((aligned_end - aligned_start).days // 7) + 1 + + width = LEFT_PAD + (weeks * CELL) + RIGHT_PAD + height = TOP_PAD + (7 * CELL) + BOTTOM_PAD + + svg = [ + f'', + '', + ] + + for week, label in month_labels(aligned_start, aligned_end): + x = LEFT_PAD + (week * CELL) + svg.append(f'{label}') + + for label, row in (("Mon", 1), ("Wed", 3), ("Fri", 5)): + y = TOP_PAD + (row * CELL) + 8 + svg.append(f'{label}') + + for day in date_range(aligned_start, aligned_end): + svg.append(build_rect(day, aligned_start, counts, max_count)) + + svg.append("") + return "\n".join(svg) + "\n" + + +def main(): + if len(sys.argv) not in {2, 3}: + print( + "Usage: generate_heatmap_svg.py [output.svg]", + file=sys.stderr, + ) + return 1 + + input_path = Path(sys.argv[1]) + output = generate_svg(load_heatmap(input_path)) + + if len(sys.argv) == 3: + output_path = Path(sys.argv[2]) + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(output) + else: + sys.stdout.write(output) + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())