From c12174db125ea69631a8ab530098d24040b31a3f Mon Sep 17 00:00:00 2001 From: "Kai (Tam Nhu) Tran" <61256810+kaitranntt@users.noreply.github.com> Date: Sat, 30 May 2026 14:08:53 -0400 Subject: [PATCH] fix: gate auth monitor stats polling (#1421) --- .../monitoring/auth-monitor/hooks.ts | 27 +++-- .../auth-monitor/auth-monitor-hooks.test.tsx | 98 +++++++++++++++++++ 2 files changed, 117 insertions(+), 8 deletions(-) create mode 100644 ui/tests/unit/ui/components/monitoring/auth-monitor/auth-monitor-hooks.test.tsx diff --git a/ui/src/components/monitoring/auth-monitor/hooks.ts b/ui/src/components/monitoring/auth-monitor/hooks.ts index 0ed653d0..cd944488 100644 --- a/ui/src/components/monitoring/auth-monitor/hooks.ts +++ b/ui/src/components/monitoring/auth-monitor/hooks.ts @@ -4,7 +4,7 @@ import { useState, useMemo, useEffect } from 'react'; import { useCliproxyAuth } from '@/hooks/use-cliproxy'; -import { useCliproxyStats } from '@/hooks/use-cliproxy-stats'; +import { useCliproxyStats, useCliproxyStatus } from '@/hooks/use-cliproxy-stats'; import { buildAccountVisualGroups } from '@/lib/account-visual-groups'; import { getProviderDisplayName } from '@/lib/provider-config'; import type { AuthStatus, OAuthAccount } from '@/lib/api-client'; @@ -26,14 +26,25 @@ export interface AuthMonitorData { /** Hook for computing auth monitor data from CLIProxy auth and stats */ export function useAuthMonitorData(): AuthMonitorData { const { data, isLoading, error } = useCliproxyAuth(); - const { data: statsData, isLoading: statsLoading, dataUpdatedAt } = useCliproxyStats(); + const { data: proxyStatus } = useCliproxyStatus(); + const statsEnabled = proxyStatus?.running === true; + const { + data: statsData, + isLoading: statsLoading, + dataUpdatedAt, + } = useCliproxyStats(statsEnabled); + const activeStatsData = statsEnabled ? statsData : undefined; + const activeStatsUpdatedAt = statsEnabled ? dataUpdatedAt : 0; const [timeSinceUpdate, setTimeSinceUpdate] = useState(''); // Live countdown showing time since last data update useEffect(() => { - if (!dataUpdatedAt) return; + if (!activeStatsUpdatedAt) { + setTimeSinceUpdate(''); + return; + } const updateTime = () => { - const diff = Math.floor((Date.now() - dataUpdatedAt) / 1000); + const diff = Math.floor((Date.now() - activeStatsUpdatedAt) / 1000); if (diff < 60) { setTimeSinceUpdate(`${diff}s ago`); } else { @@ -43,7 +54,7 @@ export function useAuthMonitorData(): AuthMonitorData { updateTime(); const interval = setInterval(updateTime, 1000); return () => clearInterval(interval); - }, [dataUpdatedAt]); + }, [activeStatsUpdatedAt]); // Transform auth status data into account rows const { accounts, totalSuccess, totalFailure, totalRequests, providerStats } = useMemo(() => { @@ -79,7 +90,7 @@ export function useAuthMonitorData(): AuthMonitorData { provider: account.provider || status.provider, })); - buildAccountVisualGroups(normalizedAccounts, statsData).forEach((groupedAccount) => { + buildAccountVisualGroups(normalizedAccounts, activeStatsData).forEach((groupedAccount) => { tSuccess += groupedAccount.successCount; tFailure += groupedAccount.failureCount; providerData.success += groupedAccount.successCount; @@ -131,7 +142,7 @@ export function useAuthMonitorData(): AuthMonitorData { totalRequests: tSuccess + tFailure, providerStats: providerStatsArr, }; - }, [data?.authStatus, statsData]); + }, [data?.authStatus, activeStatsData]); const overallSuccessRate = totalRequests > 0 ? Math.round((totalSuccess / totalRequests) * 100) : 100; @@ -143,7 +154,7 @@ export function useAuthMonitorData(): AuthMonitorData { totalRequests, providerStats, overallSuccessRate, - isLoading: isLoading || statsLoading, + isLoading: isLoading || (statsEnabled && statsLoading), error: error ?? null, timeSinceUpdate, }; diff --git a/ui/tests/unit/ui/components/monitoring/auth-monitor/auth-monitor-hooks.test.tsx b/ui/tests/unit/ui/components/monitoring/auth-monitor/auth-monitor-hooks.test.tsx new file mode 100644 index 00000000..b9c8816b --- /dev/null +++ b/ui/tests/unit/ui/components/monitoring/auth-monitor/auth-monitor-hooks.test.tsx @@ -0,0 +1,98 @@ +import { renderHook } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { useAuthMonitorData } from '@/components/monitoring/auth-monitor/hooks'; + +const { useCliproxyAuthMock, useCliproxyStatsMock, useCliproxyStatusMock } = vi.hoisted(() => ({ + useCliproxyAuthMock: vi.fn(), + useCliproxyStatsMock: vi.fn(), + useCliproxyStatusMock: vi.fn(), +})); + +vi.mock('@/hooks/use-cliproxy', () => ({ + useCliproxyAuth: useCliproxyAuthMock, +})); + +vi.mock('@/hooks/use-cliproxy-stats', () => ({ + useCliproxyStats: useCliproxyStatsMock, + useCliproxyStatus: useCliproxyStatusMock, +})); + +const authStatus = [ + { + provider: 'codex', + displayName: 'OpenAI Codex', + accounts: [ + { + id: 'codex-account', + email: 'codex@example.com', + tokenFile: '/tmp/codex.json', + provider: 'codex', + isDefault: true, + }, + ], + }, +]; + +describe('useAuthMonitorData', () => { + beforeEach(() => { + vi.clearAllMocks(); + useCliproxyAuthMock.mockReturnValue({ + data: { authStatus }, + isLoading: false, + error: null, + }); + useCliproxyStatsMock.mockReturnValue({ + data: undefined, + isLoading: false, + dataUpdatedAt: 0, + }); + }); + + it('keeps account data visible without polling stats when CLIProxy is unavailable', () => { + useCliproxyStatusMock.mockReturnValue({ + data: { running: false }, + isLoading: false, + }); + + const { result } = renderHook(() => useAuthMonitorData()); + + expect(useCliproxyStatsMock).toHaveBeenCalledWith(false); + expect(result.current.isLoading).toBe(false); + expect(result.current.accounts).toHaveLength(1); + expect(result.current.totalRequests).toBe(0); + expect(result.current.providerStats[0]).toMatchObject({ + provider: 'codex', + accountCount: 1, + totalRequests: 0, + }); + }); + + it('enables live stats only after CLIProxy is running', () => { + useCliproxyStatusMock.mockReturnValue({ + data: { running: true }, + isLoading: false, + }); + useCliproxyStatsMock.mockReturnValue({ + data: { + accountStats: { + 'codex:codex@example.com': { + source: 'codex@example.com', + successCount: 8, + failureCount: 2, + totalTokens: 100, + provider: 'codex', + }, + }, + }, + isLoading: false, + dataUpdatedAt: Date.now(), + }); + + const { result } = renderHook(() => useAuthMonitorData()); + + expect(useCliproxyStatsMock).toHaveBeenCalledWith(true); + expect(result.current.totalSuccess).toBe(8); + expect(result.current.totalFailure).toBe(2); + expect(result.current.totalRequests).toBe(10); + }); +});