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
This commit is contained in:
null 2026-05-22 15:19:04 -05:00
parent 25abfd3e15
commit 99965330b5
3 changed files with 82 additions and 15 deletions

View File

@ -14,6 +14,8 @@ from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlmodel import select from sqlmodel import select
from app.api.deps import require_org_admin, require_org_member 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.core.time import utcnow
from app.db import crud from app.db import crud
from app.db.session import get_session 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) @router.delete("/{credential_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_provider_credential( async def delete_provider_credential(
credential_id: UUID, credential_id: UUID,

View File

@ -515,12 +515,26 @@ function fmtTokens(n: number | null | undefined): string {
function fmtResetMs(ms: number | null | undefined): string { function fmtResetMs(ms: number | null | undefined): string {
if (ms == null || ms <= 0) return "< 1m"; if (ms == null || ms <= 0) return "< 1m";
const s = Math.floor(ms / 1000); const totalMinutes = Math.floor(ms / 60000);
if (s < 60) return `${s}s`; const d = Math.floor(totalMinutes / (60 * 24));
const m = Math.floor(s / 60); const h = Math.floor((totalMinutes % (60 * 24)) / 60);
if (m < 60) return `${m}m ${s % 60}s`; const m = totalMinutes % 60;
const h = Math.floor(m / 60); const parts: string[] = [];
return `${h}h ${m % 60}m`; 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 { function fmtLatencyMs(ms: number | null | undefined): string {
@ -548,6 +562,7 @@ interface UsageWindowBarProps {
function UsageWindowBar({ label, pct, resetInMs, badge }: UsageWindowBarProps) { function UsageWindowBar({ label, pct, resetInMs, badge }: UsageWindowBarProps) {
const clamped = Math.max(0, Math.min(100, pct)); const clamped = Math.max(0, Math.min(100, pct));
const resetDate = fmtResetDate(resetInMs);
return ( return (
<div className="space-y-1"> <div className="space-y-1">
<div className="flex items-center justify-between text-[11px]"> <div className="flex items-center justify-between text-[11px]">
@ -567,7 +582,12 @@ function UsageWindowBar({ label, pct, resetInMs, badge }: UsageWindowBarProps) {
style={{ width: `${clamped}%` }} style={{ width: `${clamped}%` }}
/> />
</div> </div>
<p className="text-[11px] text-muted">Resets in {fmtResetMs(resetInMs)}</p> <p className="text-[11px] text-muted">
Resets in {fmtResetMs(resetInMs)}
{resetDate && (
<span className="ml-1 text-[color:var(--text-quiet)]">· Full reset at {resetDate}</span>
)}
</p>
</div> </div>
); );
} }
@ -583,6 +603,12 @@ function UsageStrip({ credentialId, provider }: { credentialId: string; provider
setLoading(true); setLoading(true);
setError(null); setError(null);
try { try {
if (refresh && provider !== "ollama") {
await customFetch<unknown>(
"/api/v1/provider-credentials/rescan-local",
{ method: "POST" },
);
}
const res = await getProviderUsageLiveApiV1ProviderCredentialsCredentialIdUsageGet( const res = await getProviderUsageLiveApiV1ProviderCredentialsCredentialIdUsageGet(
credentialId, credentialId,
refresh ? { refresh: true } : undefined, refresh ? { refresh: true } : undefined,
@ -597,7 +623,7 @@ function UsageStrip({ credentialId, provider }: { credentialId: string; provider
setLoading(false); setLoading(false);
} }
}, },
[credentialId], [credentialId, provider],
); );
useEffect(() => { useEffect(() => {

View File

@ -44,12 +44,26 @@ function clampPct(pct: number): number {
function fmtResetMs(ms: number | null | undefined): string { function fmtResetMs(ms: number | null | undefined): string {
if (ms == null || ms <= 0) return "< 1m"; if (ms == null || ms <= 0) return "< 1m";
const s = Math.floor(ms / 1000); const totalMinutes = Math.floor(ms / 60000);
if (s < 60) return `${s}s`; const d = Math.floor(totalMinutes / (60 * 24));
const m = Math.floor(s / 60); const h = Math.floor((totalMinutes % (60 * 24)) / 60);
if (m < 60) return `${m}m ${s % 60}s`; const m = totalMinutes % 60;
const h = Math.floor(m / 60); const parts: string[] = [];
return `${h}h ${m % 60}m`; 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 { function remainingPct(window: SubscriptionWindowRead | null): number | null {
@ -102,7 +116,9 @@ function resetLabel(
modelsWindow: SubscriptionWindowRead | null, modelsWindow: SubscriptionWindowRead | null,
): string { ): string {
const resetInMs = sessionWindow?.reset_in_ms ?? modelsWindow?.reset_in_ms; 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 { function formatRemaining(pct: number | null): string {