navbar is now explicitly local-first
This commit is contained in:
parent
c8f8fdb2ec
commit
fc24ec933b
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -248,7 +248,7 @@ function LineChart({ days, range, onRangeChange, lastPush }: {
|
|||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<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"
|
||||
style={{background:"rgba(139,92,246,0.14)", color:VIOLET}}>
|
||||
{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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -315,7 +315,7 @@ function LineChart({ days, range, onRangeChange, lastPush }: {
|
|||
onBlur={() => setHoveredPoint(null)}
|
||||
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>
|
||||
))}
|
||||
</svg>
|
||||
|
|
@ -466,7 +466,7 @@ function HeatmapGrid({ days, range, onRangeChange }: {
|
|||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<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"
|
||||
style={{background:"rgba(16,185,129,0.14)", color:GREEN}}>
|
||||
{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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 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.daily.map((day, i) => (
|
||||
|
|
@ -507,7 +507,7 @@ function HeatmapGrid({ days, range, onRangeChange }: {
|
|||
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>
|
||||
{(range === "7d" || i % 5 === 0 || i === heatmap.daily.length - 1) && (
|
||||
<text
|
||||
|
|
@ -547,7 +547,7 @@ function HeatmapGrid({ days, range, onRangeChange }: {
|
|||
strokeWidth: 1.5,
|
||||
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>
|
||||
))
|
||||
)}
|
||||
|
|
@ -566,7 +566,7 @@ function HeatmapGrid({ days, range, onRangeChange }: {
|
|||
{heatmap.totalEvents.toLocaleString()}
|
||||
</span>
|
||||
<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>
|
||||
</p>
|
||||
|
||||
|
|
|
|||
|
|
@ -252,7 +252,7 @@ export function ProviderNavbarStatus() {
|
|||
>({});
|
||||
const [isUsageLoading, setIsUsageLoading] = useState(false);
|
||||
const [lastFetchedAt, setLastFetchedAt] = useState<Date | null>(null);
|
||||
const [now, setNow] = useState(Date.now());
|
||||
const [now, setNow] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setNow(Date.now()), 1000);
|
||||
|
|
@ -317,7 +317,21 @@ export function ProviderNavbarStatus() {
|
|||
}),
|
||||
);
|
||||
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());
|
||||
setIsUsageLoading(false);
|
||||
}
|
||||
|
|
@ -366,7 +380,7 @@ export function ProviderNavbarStatus() {
|
|||
<>
|
||||
<span className="h-3.5 w-px shrink-0 bg-[color:var(--border)]" />
|
||||
<span className="tabular-nums text-[color:var(--text-quiet)]">
|
||||
Updated {fmtElapsed(lastFetchedAt, now)}
|
||||
Updated {fmtElapsed(lastFetchedAt, now || lastFetchedAt.getTime())}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
||||
function credentialIsLocal(cred: ProviderCredentialRead): boolean {
|
||||
return (
|
||||
cred.account_key === "default" ||
|
||||
cred.display_name.toLowerCase().includes("local")
|
||||
);
|
||||
}
|
||||
|
||||
function providerCredentialScore(cred: ProviderCredentialRead): number {
|
||||
let score = 0;
|
||||
|
||||
if (cred.active) score += 100;
|
||||
if (credentialIsLocal(cred)) score += 1000;
|
||||
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.base_url) score += 5;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue