fix: gate auth monitor stats polling (#1421)

This commit is contained in:
Kai (Tam Nhu) Tran
2026-05-30 14:08:53 -04:00
committed by GitHub
parent f76a9c03b4
commit c12174db12
2 changed files with 117 additions and 8 deletions
@@ -4,7 +4,7 @@
import { useState, useMemo, useEffect } from 'react'; import { useState, useMemo, useEffect } from 'react';
import { useCliproxyAuth } from '@/hooks/use-cliproxy'; 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 { buildAccountVisualGroups } from '@/lib/account-visual-groups';
import { getProviderDisplayName } from '@/lib/provider-config'; import { getProviderDisplayName } from '@/lib/provider-config';
import type { AuthStatus, OAuthAccount } from '@/lib/api-client'; 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 */ /** Hook for computing auth monitor data from CLIProxy auth and stats */
export function useAuthMonitorData(): AuthMonitorData { export function useAuthMonitorData(): AuthMonitorData {
const { data, isLoading, error } = useCliproxyAuth(); 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(''); const [timeSinceUpdate, setTimeSinceUpdate] = useState('');
// Live countdown showing time since last data update // Live countdown showing time since last data update
useEffect(() => { useEffect(() => {
if (!dataUpdatedAt) return; if (!activeStatsUpdatedAt) {
setTimeSinceUpdate('');
return;
}
const updateTime = () => { const updateTime = () => {
const diff = Math.floor((Date.now() - dataUpdatedAt) / 1000); const diff = Math.floor((Date.now() - activeStatsUpdatedAt) / 1000);
if (diff < 60) { if (diff < 60) {
setTimeSinceUpdate(`${diff}s ago`); setTimeSinceUpdate(`${diff}s ago`);
} else { } else {
@@ -43,7 +54,7 @@ export function useAuthMonitorData(): AuthMonitorData {
updateTime(); updateTime();
const interval = setInterval(updateTime, 1000); const interval = setInterval(updateTime, 1000);
return () => clearInterval(interval); return () => clearInterval(interval);
}, [dataUpdatedAt]); }, [activeStatsUpdatedAt]);
// Transform auth status data into account rows // Transform auth status data into account rows
const { accounts, totalSuccess, totalFailure, totalRequests, providerStats } = useMemo(() => { const { accounts, totalSuccess, totalFailure, totalRequests, providerStats } = useMemo(() => {
@@ -79,7 +90,7 @@ export function useAuthMonitorData(): AuthMonitorData {
provider: account.provider || status.provider, provider: account.provider || status.provider,
})); }));
buildAccountVisualGroups(normalizedAccounts, statsData).forEach((groupedAccount) => { buildAccountVisualGroups(normalizedAccounts, activeStatsData).forEach((groupedAccount) => {
tSuccess += groupedAccount.successCount; tSuccess += groupedAccount.successCount;
tFailure += groupedAccount.failureCount; tFailure += groupedAccount.failureCount;
providerData.success += groupedAccount.successCount; providerData.success += groupedAccount.successCount;
@@ -131,7 +142,7 @@ export function useAuthMonitorData(): AuthMonitorData {
totalRequests: tSuccess + tFailure, totalRequests: tSuccess + tFailure,
providerStats: providerStatsArr, providerStats: providerStatsArr,
}; };
}, [data?.authStatus, statsData]); }, [data?.authStatus, activeStatsData]);
const overallSuccessRate = const overallSuccessRate =
totalRequests > 0 ? Math.round((totalSuccess / totalRequests) * 100) : 100; totalRequests > 0 ? Math.round((totalSuccess / totalRequests) * 100) : 100;
@@ -143,7 +154,7 @@ export function useAuthMonitorData(): AuthMonitorData {
totalRequests, totalRequests,
providerStats, providerStats,
overallSuccessRate, overallSuccessRate,
isLoading: isLoading || statsLoading, isLoading: isLoading || (statsEnabled && statsLoading),
error: error ?? null, error: error ?? null,
timeSinceUpdate, timeSinceUpdate,
}; };
@@ -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);
});
});