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
|
# 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.
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue