diff --git a/backend/app/services/provider_usage.py b/backend/app/services/provider_usage.py
index fe52ce0..8e94a8e 100644
--- a/backend/app/services/provider_usage.py
+++ b/backend/app/services/provider_usage.py
@@ -64,6 +64,7 @@ CACHE_TTL_SECONDS = 60
# next auto-poll only fires after the window has cleared. Manual refreshes
# (force_refresh=True) always bypass this TTL.
CACHE_TTL_FAILURE_SECONDS = 60
+STALE_SUBSCRIPTION_TTL_SECONDS = 10 * 60
REQUEST_TIMEOUT = 8.0 # seconds
@@ -924,6 +925,7 @@ async def _fetch_codex_subscription(session_key: str) -> tuple[list[Subscription
# ---------------------------------------------------------------------------
_cache: dict[str, tuple[datetime, ProviderUsageLive, int]] = {}
+_last_subscription_cache: dict[str, tuple[datetime, ProviderUsageLive]] = {}
# Tracks in-progress fetches so concurrent requests share one result instead
# of each racing to hit the provider API (which triggers 429 cascades).
_inflight: dict[str, asyncio.Future[ProviderUsageLive]] = {}
@@ -944,6 +946,22 @@ def _set_cached(cache_key: str, result: ProviderUsageLive, ttl: int = CACHE_TTL_
_cache[cache_key] = (utcnow(), result, ttl)
+def _get_last_subscription(cache_key: str) -> ProviderUsageLive | None:
+ entry = _last_subscription_cache.get(cache_key)
+ if entry is None:
+ return None
+ cached_at, result = entry
+ if (utcnow() - cached_at).total_seconds() > STALE_SUBSCRIPTION_TTL_SECONDS:
+ del _last_subscription_cache[cache_key]
+ return None
+ return result
+
+
+def _remember_subscription(cache_key: str, result: ProviderUsageLive) -> None:
+ if result.subscription_windows:
+ _last_subscription_cache[cache_key] = (utcnow(), result)
+
+
def _secret_fingerprint(value: str | None) -> str:
if not value:
return "none"
@@ -975,6 +993,9 @@ def _usage_cache_key(
return f"anthropic:api:{normalized_base_url}:{_secret_fingerprint(api_key)}"
if normalized_provider in ("openai", "codex"):
+ local_codex = _read_codex_local_token()
+ if local_codex:
+ return f"{normalized_provider}:local-oauth:{_secret_fingerprint(local_codex)}"
if api_key:
return (
f"{normalized_provider}:api:"
@@ -982,9 +1003,6 @@ def _usage_cache_key(
)
if session_key:
return f"{normalized_provider}:session:{_secret_fingerprint(session_key)}"
- local_codex = _read_codex_local_token()
- if local_codex:
- return f"{normalized_provider}:local-oauth:{_secret_fingerprint(local_codex)}"
if normalized_provider == "ollama":
return f"ollama:{normalized_base_url}:{_secret_fingerprint(api_key)}"
@@ -1108,6 +1126,22 @@ async def _do_fetch_provider_usage(
)
result.account_key = account_key
+ if subscription_attempted and not result.subscription_windows:
+ stale = _get_last_subscription(cache_key)
+ if stale is not None:
+ stale.checked_at = utcnow()
+ stale.account_key = account_key
+ stale.error = None
+ _set_cached(cache_key, stale, ttl=CACHE_TTL_FAILURE_SECONDS)
+ logger.info(
+ "provider_usage.subscription.stale_used provider=%s account=%s error=%s",
+ provider,
+ account_key,
+ result.error,
+ )
+ return stale
+
+ _remember_subscription(cache_key, result)
# Use a short TTL when subscription windows were expected but came back empty
# (e.g. a transient 429 at startup). This avoids persisting a 60s stale result
# while still preventing the thundering-herd that occurs with no caching at all.
diff --git a/frontend/src/components/git/ForgejoHeatmap.tsx b/frontend/src/components/git/ForgejoHeatmap.tsx
index 837e41b..110f43d 100644
--- a/frontend/src/components/git/ForgejoHeatmap.tsx
+++ b/frontend/src/components/git/ForgejoHeatmap.tsx
@@ -248,7 +248,7 @@ function LineChart({ days, range, onRangeChange, lastPush }: {
-
Git Activity
+
Git Commits
{displayedCount.toLocaleString()}
@@ -261,7 +261,7 @@ function LineChart({ days, range, onRangeChange, lastPush }: {
value={range}
onValueChange={onRangeChange}
accent="violet"
- ariaLabel="Select Git Activity range"
+ ariaLabel="Select Git Commits range"
/>
@@ -315,7 +315,7 @@ function LineChart({ days, range, onRangeChange, lastPush }: {
onBlur={() => setHoveredPoint(null)}
style={{fill:"transparent", outline:"none"}}
>
-
{p.date}{p.count > 0 ? `: ${p.count} event${p.count!==1?"s":""}` : ": no activity"}
+
{p.date}{p.count > 0 ? `: ${p.count} commit${p.count!==1?"s":""}` : ": no commits"}
))}
@@ -466,7 +466,7 @@ function HeatmapGrid({ days, range, onRangeChange }: {
- Activity Heatmap
+ Commit Heatmap
{displayedCount.toLocaleString()}
@@ -479,12 +479,12 @@ function HeatmapGrid({ days, range, onRangeChange }: {
value={range}
onValueChange={onRangeChange}
accent="green"
- ariaLabel="Select Activity Heatmap range"
+ ariaLabel="Select Commit Heatmap range"
/>
{/* SVG */}
-