From 99965330b5373ab075266fb4175948e973b5515e Mon Sep 17 00:00:00 2001 From: null Date: Fri, 22 May 2026 15:19:04 -0500 Subject: [PATCH] feat(provider-credentials): add endpoint to rescan local credentials and update tokens feat(usage): enhance reset time formatting and display full reset date in UI --- backend/app/api/provider_credentials.py | 25 +++++++++++ .../src/app/settings/ai-providers/page.tsx | 42 +++++++++++++++---- .../organisms/ProviderNavbarStatus.tsx | 30 +++++++++---- 3 files changed, 82 insertions(+), 15 deletions(-) diff --git a/backend/app/api/provider_credentials.py b/backend/app/api/provider_credentials.py index c113f39..86cf8c7 100644 --- a/backend/app/api/provider_credentials.py +++ b/backend/app/api/provider_credentials.py @@ -14,6 +14,8 @@ from fastapi import APIRouter, Depends, HTTPException, Query, status from sqlmodel import select from app.api.deps import require_org_admin, require_org_member +from app.core.auth_mode import AuthMode +from app.core.config import settings from app.core.time import utcnow from app.db import crud from app.db.session import get_session @@ -318,6 +320,29 @@ async def get_provider_usage_live( ) +@router.post("/rescan-local", status_code=status.HTTP_200_OK) +async def rescan_local_credentials( + ctx: OrganizationContext = ORG_MEMBER_DEP, +) -> dict: + """Re-read local credential files and upsert updated tokens into the database. + + Only has effect in LOCAL auth mode; returns {"changed": 0} otherwise. + Reads ~/.claude/.credentials.json (Claude) and ~/.codex/auth.json (Codex/GPT) + and updates any rows whose session_key has changed. + """ + if settings.auth_mode != AuthMode.LOCAL: + return {"changed": 0} + try: + from scripts.seed_provider_credentials import seed + changed = await seed(verbose=False) + return {"changed": changed} + except Exception as exc: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Local credential rescan failed: {exc}", + ) + + @router.delete("/{credential_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_provider_credential( credential_id: UUID, diff --git a/frontend/src/app/settings/ai-providers/page.tsx b/frontend/src/app/settings/ai-providers/page.tsx index a720e8e..3eb563a 100644 --- a/frontend/src/app/settings/ai-providers/page.tsx +++ b/frontend/src/app/settings/ai-providers/page.tsx @@ -515,12 +515,26 @@ function fmtTokens(n: number | null | undefined): string { function fmtResetMs(ms: number | null | undefined): string { if (ms == null || ms <= 0) return "< 1m"; - const s = Math.floor(ms / 1000); - if (s < 60) return `${s}s`; - const m = Math.floor(s / 60); - if (m < 60) return `${m}m ${s % 60}s`; - const h = Math.floor(m / 60); - return `${h}h ${m % 60}m`; + const totalMinutes = Math.floor(ms / 60000); + const d = Math.floor(totalMinutes / (60 * 24)); + const h = Math.floor((totalMinutes % (60 * 24)) / 60); + const m = totalMinutes % 60; + const parts: string[] = []; + if (d > 0) parts.push(`${d}d`); + if (h > 0) parts.push(`${h}hrs`); + if (m > 0 || parts.length === 0) parts.push(`${m}m`); + return parts.join(" "); +} + +function fmtResetDate(ms: number | null | undefined): string | null { + if (ms == null || ms <= 0) return null; + const resetAt = new Date(Date.now() + ms); + return resetAt.toLocaleString(undefined, { + month: "short", + day: "numeric", + hour: "numeric", + minute: "2-digit", + }); } function fmtLatencyMs(ms: number | null | undefined): string { @@ -548,6 +562,7 @@ interface UsageWindowBarProps { function UsageWindowBar({ label, pct, resetInMs, badge }: UsageWindowBarProps) { const clamped = Math.max(0, Math.min(100, pct)); + const resetDate = fmtResetDate(resetInMs); return (
@@ -567,7 +582,12 @@ function UsageWindowBar({ label, pct, resetInMs, badge }: UsageWindowBarProps) { style={{ width: `${clamped}%` }} />
-

Resets in {fmtResetMs(resetInMs)}

+

+ Resets in {fmtResetMs(resetInMs)} + {resetDate && ( + · Full reset at {resetDate} + )} +

); } @@ -583,6 +603,12 @@ function UsageStrip({ credentialId, provider }: { credentialId: string; provider setLoading(true); setError(null); try { + if (refresh && provider !== "ollama") { + await customFetch( + "/api/v1/provider-credentials/rescan-local", + { method: "POST" }, + ); + } const res = await getProviderUsageLiveApiV1ProviderCredentialsCredentialIdUsageGet( credentialId, refresh ? { refresh: true } : undefined, @@ -597,7 +623,7 @@ function UsageStrip({ credentialId, provider }: { credentialId: string; provider setLoading(false); } }, - [credentialId], + [credentialId, provider], ); useEffect(() => { diff --git a/frontend/src/components/organisms/ProviderNavbarStatus.tsx b/frontend/src/components/organisms/ProviderNavbarStatus.tsx index 9db7aac..d328896 100644 --- a/frontend/src/components/organisms/ProviderNavbarStatus.tsx +++ b/frontend/src/components/organisms/ProviderNavbarStatus.tsx @@ -44,12 +44,26 @@ function clampPct(pct: number): number { function fmtResetMs(ms: number | null | undefined): string { if (ms == null || ms <= 0) return "< 1m"; - const s = Math.floor(ms / 1000); - if (s < 60) return `${s}s`; - const m = Math.floor(s / 60); - if (m < 60) return `${m}m ${s % 60}s`; - const h = Math.floor(m / 60); - return `${h}h ${m % 60}m`; + const totalMinutes = Math.floor(ms / 60000); + const d = Math.floor(totalMinutes / (60 * 24)); + const h = Math.floor((totalMinutes % (60 * 24)) / 60); + const m = totalMinutes % 60; + const parts: string[] = []; + if (d > 0) parts.push(`${d}d`); + if (h > 0) parts.push(`${h}hrs`); + if (m > 0 || parts.length === 0) parts.push(`${m}m`); + return parts.join(" "); +} + +function fmtResetDate(ms: number | null | undefined): string | null { + if (ms == null || ms <= 0) return null; + const resetAt = new Date(Date.now() + ms); + return resetAt.toLocaleString(undefined, { + month: "short", + day: "numeric", + hour: "numeric", + minute: "2-digit", + }); } function remainingPct(window: SubscriptionWindowRead | null): number | null { @@ -102,7 +116,9 @@ function resetLabel( modelsWindow: SubscriptionWindowRead | null, ): string { const resetInMs = sessionWindow?.reset_in_ms ?? modelsWindow?.reset_in_ms; - return resetInMs == null ? "-" : fmtResetMs(resetInMs); + if (resetInMs == null) return "-"; + const date = fmtResetDate(resetInMs); + return date ? `${fmtResetMs(resetInMs)} · Full reset at ${date}` : fmtResetMs(resetInMs); } function formatRemaining(pct: number | null): string {