From dd9681925e709fa5bf89e9697511e7cfc55f20e1 Mon Sep 17 00:00:00 2001 From: null Date: Fri, 22 May 2026 16:19:15 -0500 Subject: [PATCH] feat(forgejo-metrics): adjust line stats caching strategy and improve retry logic for contributor stats --- backend/app/api/forgejo_metrics.py | 6 ++-- backend/app/services/forgejo_client.py | 28 ++++++--------- .../src/components/git/ForgejoHeatmap.tsx | 36 ++++++++----------- 3 files changed, 29 insertions(+), 41 deletions(-) diff --git a/backend/app/api/forgejo_metrics.py b/backend/app/api/forgejo_metrics.py index e2e65a2..b85d124 100644 --- a/backend/app/api/forgejo_metrics.py +++ b/backend/app/api/forgejo_metrics.py @@ -36,7 +36,8 @@ if TYPE_CHECKING: # --------------------------------------------------------------------------- _line_stats_cache: dict[str, tuple[float, int, int, bool]] = {} _line_stats_fetching: set[str] = set() -_LINE_STATS_TTL = 300 # seconds before a re-fetch is triggered +_LINE_STATS_TTL_HIT = 300 # 5 min — re-fetch cadence once real data is cached +_LINE_STATS_TTL_MISS = 30 # 30 s — retry cadence while Forgejo is still computing async def _bg_fetch_line_stats( @@ -383,8 +384,9 @@ async def get_forgejo_heatmap( cached = _line_stats_cache.get(cache_key) now = _time.monotonic() + ttl = _LINE_STATS_TTL_HIT if (cached and cached[3]) else _LINE_STATS_TTL_MISS if cache_key not in _line_stats_fetching and ( - cached is None or now - cached[0] > _LINE_STATS_TTL + cached is None or now - cached[0] > ttl ): # Normalise base_url the same way get_forgejo_client() does, eagerly, # so the background task never touches a potentially-closed session. diff --git a/backend/app/services/forgejo_client.py b/backend/app/services/forgejo_client.py index c328dd4..ec0d013 100644 --- a/backend/app/services/forgejo_client.py +++ b/backend/app/services/forgejo_client.py @@ -353,28 +353,22 @@ class ForgejoAPIClient: async def get_contributor_stats(self, owner: str, repo: str) -> tuple[list[dict], bool]: """Fetch per-contributor weekly stats for a repository. - Returns (contributors, has_data). On the first call Forgejo may return - HTTP 202 ("computing") — we wait 2 s and retry once so the stats are - available on the next dashboard load even if not this one. + Returns (contributors, has_data). Forgejo returns HTTP 202 while it + computes stats — that is not an error, it just means the caller should + retry later. has_data=False signals the 202 case; the application layer + uses a short retry TTL so it probes again within ~30 s. - Each contributor has a ``weeks`` array with ``w`` (Unix timestamp of - week start), ``a`` (additions), and ``d`` (deletions). + Each contributor dict has a ``weeks`` array with ``w`` (Unix timestamp), + ``a`` (additions), and ``d`` (deletions). """ - import asyncio as _asyncio - client = await self._get_client() - url = f"/api/v1/repos/{owner}/{repo}/stats/contributors" - response = await client.get(url) - + response = await client.get( + f"/api/v1/repos/{owner}/{repo}/stats/contributors" + ) if response.status_code == 202: - # Forgejo is computing — wait briefly then try once more - await _asyncio.sleep(2) - response = await client.get(url) - - if response.status_code == 202: - return [], False # still computing after retry + return [], False # still computing — caller will retry shortly if response.status_code == 404: - return [], True # no data, but not a 202 — treat as "has_data" + return [], True # repo exists but no commits; treat as data-present response.raise_for_status() data = response.json() return (data if isinstance(data, list) else []), True diff --git a/frontend/src/components/git/ForgejoHeatmap.tsx b/frontend/src/components/git/ForgejoHeatmap.tsx index 7c01ef9..5e32195 100644 --- a/frontend/src/components/git/ForgejoHeatmap.tsx +++ b/frontend/src/components/git/ForgejoHeatmap.tsx @@ -12,12 +12,12 @@ interface ForgejoHeatmapProps { isLoading?: boolean; } -const CELL = 13; -const GAP = 3; +const CELL = 16; +const GAP = 4; const STRIDE = CELL + GAP; const WEEKS = 27; -const LEFT = 28; -const TOP = 18; +const LEFT = 32; +const TOP = 20; const MONTHS = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]; @@ -33,7 +33,7 @@ const LEVEL_FILL = [ const VIOLET = "rgba(139,92,246,1)"; const GREEN = "rgba(52,211,153,1)"; const RED = "rgba(248,113,113,1)"; -const LABEL_CLR = "rgba(139,92,246,0.65)"; +const LABEL_CLR = "rgba(255,255,255,0.70)"; // white labels function toLevel(count: number): number { if (count === 0) return 0; @@ -166,23 +166,15 @@ export function ForgejoHeatmap({ More - {/* ── Contributions badge ──────────────────────────────────── */} -
-
- - {totalEvents.toLocaleString()} - - - contributions across all tracked repositories in the last 6 months - -
-
+ {/* ── Contributions summary ────────────────────────────────── */} +

+ + {totalEvents.toLocaleString()} + + + contributions across all tracked repositories in the last 6 months + +

{/* ── Line stats ──────────────────────────────────────────── */} {hasLineStats ? (