navbar is now explicitly local-first

This commit is contained in:
null 2026-05-24 20:49:45 -05:00
parent c8f8fdb2ec
commit fc24ec933b
4 changed files with 71 additions and 17 deletions

View File

@ -64,6 +64,7 @@ CACHE_TTL_SECONDS = 60
# next auto-poll only fires after the window has cleared. Manual refreshes # next auto-poll only fires after the window has cleared. Manual refreshes
# (force_refresh=True) always bypass this TTL. # (force_refresh=True) always bypass this TTL.
CACHE_TTL_FAILURE_SECONDS = 60 CACHE_TTL_FAILURE_SECONDS = 60
STALE_SUBSCRIPTION_TTL_SECONDS = 10 * 60
REQUEST_TIMEOUT = 8.0 # seconds 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]] = {} _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 # Tracks in-progress fetches so concurrent requests share one result instead
# of each racing to hit the provider API (which triggers 429 cascades). # of each racing to hit the provider API (which triggers 429 cascades).
_inflight: dict[str, asyncio.Future[ProviderUsageLive]] = {} _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) _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: def _secret_fingerprint(value: str | None) -> str:
if not value: if not value:
return "none" return "none"
@ -975,6 +993,9 @@ def _usage_cache_key(
return f"anthropic:api:{normalized_base_url}:{_secret_fingerprint(api_key)}" return f"anthropic:api:{normalized_base_url}:{_secret_fingerprint(api_key)}"
if normalized_provider in ("openai", "codex"): 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: if api_key:
return ( return (
f"{normalized_provider}:api:" f"{normalized_provider}:api:"
@ -982,9 +1003,6 @@ def _usage_cache_key(
) )
if session_key: if session_key:
return f"{normalized_provider}:session:{_secret_fingerprint(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": if normalized_provider == "ollama":
return f"ollama:{normalized_base_url}:{_secret_fingerprint(api_key)}" 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 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 # 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 # (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. # while still preventing the thundering-herd that occurs with no caching at all.

View File

@ -248,7 +248,7 @@ function LineChart({ days, range, onRangeChange, lastPush }: {
<div className="mb-3 flex items-center justify-between"> <div className="mb-3 flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Activity className="h-4 w-4" style={{ color: VIOLET }} /> <Activity className="h-4 w-4" style={{ color: VIOLET }} />
<span className="text-sm font-semibold text-strong">Git Activity</span> <span className="text-sm font-semibold text-strong">Git Commits</span>
<span className="ml-1 rounded px-1.5 py-0.5 text-[11px] font-semibold tabular-nums" <span className="ml-1 rounded px-1.5 py-0.5 text-[11px] font-semibold tabular-nums"
style={{background:"rgba(139,92,246,0.14)", color:VIOLET}}> style={{background:"rgba(139,92,246,0.14)", color:VIOLET}}>
{displayedCount.toLocaleString()} {displayedCount.toLocaleString()}
@ -261,7 +261,7 @@ function LineChart({ days, range, onRangeChange, lastPush }: {
value={range} value={range}
onValueChange={onRangeChange} onValueChange={onRangeChange}
accent="violet" accent="violet"
ariaLabel="Select Git Activity range" ariaLabel="Select Git Commits range"
/> />
</div> </div>
@ -315,7 +315,7 @@ function LineChart({ days, range, onRangeChange, lastPush }: {
onBlur={() => setHoveredPoint(null)} onBlur={() => setHoveredPoint(null)}
style={{fill:"transparent", outline:"none"}} style={{fill:"transparent", outline:"none"}}
> >
<title>{p.date}{p.count > 0 ? `: ${p.count} event${p.count!==1?"s":""}` : ": no activity"}</title> <title>{p.date}{p.count > 0 ? `: ${p.count} commit${p.count!==1?"s":""}` : ": no commits"}</title>
</rect> </rect>
))} ))}
</svg> </svg>
@ -466,7 +466,7 @@ function HeatmapGrid({ days, range, onRangeChange }: {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<LayoutGrid className="h-4 w-4" style={{ color: "rgba(16,185,129,1)" }} /> <LayoutGrid className="h-4 w-4" style={{ color: "rgba(16,185,129,1)" }} />
<span className="text-sm font-semibold text-strong">Activity Heatmap</span> <span className="text-sm font-semibold text-strong">Commit Heatmap</span>
<span className="ml-1 rounded px-1.5 py-0.5 text-[11px] font-semibold tabular-nums" <span className="ml-1 rounded px-1.5 py-0.5 text-[11px] font-semibold tabular-nums"
style={{background:"rgba(16,185,129,0.14)", color:GREEN}}> style={{background:"rgba(16,185,129,0.14)", color:GREEN}}>
{displayedCount.toLocaleString()} {displayedCount.toLocaleString()}
@ -479,12 +479,12 @@ function HeatmapGrid({ days, range, onRangeChange }: {
value={range} value={range}
onValueChange={onRangeChange} onValueChange={onRangeChange}
accent="green" accent="green"
ariaLabel="Select Activity Heatmap range" ariaLabel="Select Commit Heatmap range"
/> />
</div> </div>
{/* SVG */} {/* SVG */}
<svg viewBox={`0 0 ${LVW} ${HVH}`} width="100%" style={{ display:"block", height:LVH }} aria-label={`Activity heatmap last ${range}`}> <svg viewBox={`0 0 ${LVW} ${HVH}`} width="100%" style={{ display:"block", height:LVH }} aria-label={`Commit heatmap last ${range}`}>
{heatmap.mode === "strip" ? ( {heatmap.mode === "strip" ? (
<> <>
{heatmap.daily.map((day, i) => ( {heatmap.daily.map((day, i) => (
@ -507,7 +507,7 @@ function HeatmapGrid({ days, range, onRangeChange }: {
outline: "none", outline: "none",
}} }}
> >
<title>{day.date}{day.count>0?`: ${day.count} event${day.count!==1?"s":""}` : ": no activity"}</title> <title>{day.date}{day.count>0?`: ${day.count} commit${day.count!==1?"s":""}` : ": no commits"}</title>
</rect> </rect>
{(range === "7d" || i % 5 === 0 || i === heatmap.daily.length - 1) && ( {(range === "7d" || i % 5 === 0 || i === heatmap.daily.length - 1) && (
<text <text
@ -547,7 +547,7 @@ function HeatmapGrid({ days, range, onRangeChange }: {
strokeWidth: 1.5, strokeWidth: 1.5,
outline: "none", outline: "none",
}}> }}>
<title>{cell.date}{cell.count>0?`: ${cell.count} event${cell.count!==1?"s":""}` : ": no activity"}</title> <title>{cell.date}{cell.count>0?`: ${cell.count} commit${cell.count!==1?"s":""}` : ": no commits"}</title>
</rect> </rect>
)) ))
)} )}
@ -566,7 +566,7 @@ function HeatmapGrid({ days, range, onRangeChange }: {
{heatmap.totalEvents.toLocaleString()} {heatmap.totalEvents.toLocaleString()}
</span> </span>
<span className="ml-1.5 text-sm" style={{color:W70}}> <span className="ml-1.5 text-sm" style={{color:W70}}>
contributions across all tracked repositories in the last {RANGE_SUMMARY[range]} commits across all tracked repositories in the last {RANGE_SUMMARY[range]}
</span> </span>
</p> </p>

View File

@ -252,7 +252,7 @@ export function ProviderNavbarStatus() {
>({}); >({});
const [isUsageLoading, setIsUsageLoading] = useState(false); const [isUsageLoading, setIsUsageLoading] = useState(false);
const [lastFetchedAt, setLastFetchedAt] = useState<Date | null>(null); const [lastFetchedAt, setLastFetchedAt] = useState<Date | null>(null);
const [now, setNow] = useState(Date.now()); const [now, setNow] = useState(0);
useEffect(() => { useEffect(() => {
const id = setInterval(() => setNow(Date.now()), 1000); const id = setInterval(() => setNow(Date.now()), 1000);
@ -317,7 +317,21 @@ export function ProviderNavbarStatus() {
}), }),
); );
if (!cancelled) { if (!cancelled) {
setUsageByCredentialId(Object.fromEntries(pairs)); setUsageByCredentialId((previous) => {
const next = { ...previous };
for (const [credentialId, usage] of pairs) {
const previousUsage = previous[credentialId];
const nextWindows = usage?.subscription_windows ?? [];
const previousWindows = previousUsage?.subscription_windows ?? [];
next[credentialId] =
usage &&
nextWindows.length === 0 &&
previousWindows.length > 0
? previousUsage
: usage;
}
return next;
});
setLastFetchedAt(new Date()); setLastFetchedAt(new Date());
setIsUsageLoading(false); setIsUsageLoading(false);
} }
@ -366,7 +380,7 @@ export function ProviderNavbarStatus() {
<> <>
<span className="h-3.5 w-px shrink-0 bg-[color:var(--border)]" /> <span className="h-3.5 w-px shrink-0 bg-[color:var(--border)]" />
<span className="tabular-nums text-[color:var(--text-quiet)]"> <span className="tabular-nums text-[color:var(--text-quiet)]">
Updated {fmtElapsed(lastFetchedAt, now)} Updated {fmtElapsed(lastFetchedAt, now || lastFetchedAt.getTime())}
</span> </span>
</> </>
)} )}

View File

@ -4,13 +4,19 @@ export function credentialIsUsable(cred: ProviderCredentialRead): boolean {
return cred.active && (cred.has_api_key || cred.has_session_key || Boolean(cred.base_url)); return cred.active && (cred.has_api_key || cred.has_session_key || Boolean(cred.base_url));
} }
function credentialIsLocal(cred: ProviderCredentialRead): boolean {
return (
cred.account_key === "default" ||
cred.display_name.toLowerCase().includes("local")
);
}
function providerCredentialScore(cred: ProviderCredentialRead): number { function providerCredentialScore(cred: ProviderCredentialRead): number {
let score = 0; let score = 0;
if (cred.active) score += 100; if (cred.active) score += 100;
if (credentialIsLocal(cred)) score += 1000;
if (cred.has_session_key) score += 50; if (cred.has_session_key) score += 50;
if (cred.account_key === "default") score += 40;
if (cred.display_name.toLowerCase().includes("local")) score += 30;
if (cred.has_api_key) score += 10; if (cred.has_api_key) score += 10;
if (cred.base_url) score += 5; if (cred.base_url) score += 5;