From 9417d6df35219f4a75e8fbbca63e5868a4bc31a5 Mon Sep 17 00:00:00 2001 From: Liyan Zhao Date: Mon, 25 May 2026 06:13:10 +0800 Subject: [PATCH 01/12] Refactor usage polling to paged APIs with legacy fallback --- .../monitoring/accountOverviewState.test.ts | 5 + .../hooks/useMonitoringData.test.ts | 5 + .../monitoring/hooks/useMonitoringData.ts | 230 ++++++-- src/features/monitoring/hooks/useUsageData.ts | 108 +++- src/pages/MonitoringCenterPage.tsx | 157 ++++- src/services/api/usageService.test.ts | 122 ++++ src/services/api/usageService.ts | 97 ++- src/utils/usage.ts | 15 + usage-service/internal/httpapi/server.go | 108 ++++ usage-service/internal/httpapi/server_test.go | 168 +++++- usage-service/internal/store/store.go | 550 +++++++++++++++++- usage-service/internal/store/store_test.go | 263 +++++++++ usage-service/internal/usage/event.go | 6 + 13 files changed, 1730 insertions(+), 104 deletions(-) create mode 100644 src/services/api/usageService.test.ts diff --git a/src/features/monitoring/accountOverviewState.test.ts b/src/features/monitoring/accountOverviewState.test.ts index 03f5413ca..0f9e3e663 100644 --- a/src/features/monitoring/accountOverviewState.test.ts +++ b/src/features/monitoring/accountOverviewState.test.ts @@ -69,8 +69,13 @@ const createEventRow = (overrides: Partial = {}): Monitoring channelHost: overrides.channelHost ?? 'localhost', channelDisabled: overrides.channelDisabled ?? false, failed: overrides.failed ?? false, + requestCount: overrides.requestCount ?? 1, + successCalls: overrides.successCalls ?? (overrides.failed ? 0 : 1), + failureCalls: overrides.failureCalls ?? (overrides.failed ? 1 : 0), statsIncluded: overrides.statsIncluded ?? true, latencyMs: overrides.latencyMs ?? 120, + latencySumMs: overrides.latencySumMs ?? (overrides.latencyMs ?? 120), + latencyCount: overrides.latencyCount ?? 1, inputTokens: overrides.inputTokens ?? 10, outputTokens: overrides.outputTokens ?? 5, reasoningTokens: overrides.reasoningTokens ?? 0, diff --git a/src/features/monitoring/hooks/useMonitoringData.test.ts b/src/features/monitoring/hooks/useMonitoringData.test.ts index f6748405e..968fb8608 100644 --- a/src/features/monitoring/hooks/useMonitoringData.test.ts +++ b/src/features/monitoring/hooks/useMonitoringData.test.ts @@ -40,8 +40,13 @@ const createMonitoringEventRow = ( channelHost: overrides.channelHost ?? 'example.com', channelDisabled: overrides.channelDisabled ?? false, failed: overrides.failed ?? false, + requestCount: overrides.requestCount ?? 1, + successCalls: overrides.successCalls ?? (overrides.failed ? 0 : 1), + failureCalls: overrides.failureCalls ?? (overrides.failed ? 1 : 0), statsIncluded: overrides.statsIncluded ?? true, latencyMs: overrides.latencyMs ?? 1200, + latencySumMs: overrides.latencySumMs ?? (overrides.latencyMs ?? 1200), + latencyCount: overrides.latencyCount ?? 1, inputTokens: overrides.inputTokens ?? 10, outputTokens: overrides.outputTokens ?? 5, reasoningTokens: overrides.reasoningTokens ?? 0, diff --git a/src/features/monitoring/hooks/useMonitoringData.ts b/src/features/monitoring/hooks/useMonitoringData.ts index e418dc0e6..a38af665e 100644 --- a/src/features/monitoring/hooks/useMonitoringData.ts +++ b/src/features/monitoring/hooks/useMonitoringData.ts @@ -381,8 +381,13 @@ export type MonitoringEventRow = { channelHost: string; channelDisabled: boolean; failed: boolean; + requestCount: number; + successCalls: number; + failureCalls: number; statsIncluded: boolean; latencyMs: number | null; + latencySumMs: number; + latencyCount: number; inputTokens: number; outputTokens: number; reasoningTokens: number; @@ -517,6 +522,11 @@ export type MonitoringMetadata = { export interface UseMonitoringDataParams { usage: unknown; + usagePages?: { + accounts?: { usage?: unknown } | null; + apiKeys?: { usage?: unknown } | null; + realtime?: { usage?: unknown } | null; + } | null; config: Config | null | undefined; modelPrices: Record; apiKeyAliases?: ApiKeyAlias[]; @@ -544,6 +554,9 @@ export interface UseMonitoringDataReturn { taskBuckets: MonitoringTaskBucketRow[]; recentFailures: MonitoringFailureRow[]; filteredRows: MonitoringEventRow[]; + accountPageRows: MonitoringAccountRow[] | null; + apiKeyPageRows: MonitoringApiKeyRow[] | null; + realtimePageRows: MonitoringEventRow[] | null; refreshMeta: (showLoading?: boolean) => Promise; } @@ -698,7 +711,7 @@ const buildTimeline = ( rows.forEach((row) => { const bucket = map.get(row.hourLabel); if (!bucket) return; - bucket.requests += 1; + bucket.requests += row.requestCount; bucket.tokens += row.totalTokens; bucket.cost += row.totalCost; }); @@ -715,7 +728,7 @@ const buildTimeline = ( tokens: 0, cost: 0, }; - existing.requests += 1; + existing.requests += row.requestCount; existing.tokens += row.totalTokens; existing.cost += row.totalCost; grouped.set(row.dayKey, existing); @@ -743,7 +756,7 @@ const buildHourlyDistribution = (rows: MonitoringEventRow[]) => { const hour = Number(row.hourLabel.slice(0, 2)); const bucket = Number.isFinite(hour) ? buckets[hour] : null; if (!bucket) return; - bucket.requests += 1; + bucket.requests += row.requestCount; bucket.tokens += row.totalTokens; bucket.cost += row.totalCost; }); @@ -760,8 +773,8 @@ const buildRecentPattern = (rows: MonitoringEventRow[], limit = 10) => .map((row) => !row.failed); export const buildMonitoringSummary = (rows: MonitoringEventRow[]): MonitoringSummary => { - const totalCalls = rows.length; - const failureCalls = rows.filter((row) => row.failed).length; + const totalCalls = rows.reduce((sum, row) => sum + row.requestCount, 0); + const failureCalls = rows.reduce((sum, row) => sum + row.failureCalls, 0); const successCalls = Math.max(totalCalls - failureCalls, 0); const inputTokens = rows.reduce((sum, row) => sum + row.inputTokens, 0); const outputTokens = rows.reduce((sum, row) => sum + row.outputTokens, 0); @@ -773,9 +786,8 @@ export const buildMonitoringSummary = (rows: MonitoringEventRow[]): MonitoringSu let latencySum = 0; let latencyCount = 0; rows.forEach((row) => { - if (row.latencyMs === null) return; - latencySum += row.latencyMs; - latencyCount += 1; + latencySum += row.latencySumMs; + latencyCount += row.latencyCount; }); const taskMap = new Map(); @@ -795,6 +807,7 @@ export const buildMonitoringSummary = (rows: MonitoringEventRow[]): MonitoringSu const recentRows = rows.filter( (row) => row.timestampMs >= windowStart && row.timestampMs <= nowMs ); + const recentCalls = recentRows.reduce((sum, row) => sum + row.requestCount, 0); const recentTokens = recentRows.reduce((sum, row) => sum + row.totalTokens, 0); return { @@ -809,7 +822,7 @@ export const buildMonitoringSummary = (rows: MonitoringEventRow[]): MonitoringSu totalTokens, totalCost, averageLatencyMs: latencyCount > 0 ? latencySum / latencyCount : null, - rpm30m: recentRows.length / 30, + rpm30m: recentCalls / 30, tpm30m: recentTokens / 30, avgDailyRequests: totalCalls / activeDayCount, avgDailyTokens: totalTokens / activeDayCount, @@ -817,7 +830,7 @@ export const buildMonitoringSummary = (rows: MonitoringEventRow[]): MonitoringSu approxTaskFailures, approxTaskSuccessRate: approxTasks > 0 ? Math.max(approxTasks - approxTaskFailures, 0) / approxTasks : 1, - zeroTokenCalls: zeroTokenRows.length, + zeroTokenCalls: zeroTokenRows.reduce((sum, row) => sum + row.requestCount, 0), zeroTokenModels: Array.from(new Set(zeroTokenRows.map((row) => row.model))).sort(), }; }; @@ -890,9 +903,9 @@ export const buildAccountRows = (rows: MonitoringEventRow[]): MonitoringAccountR existing.authLabels.add(row.authLabel); existing.authIndices.add(row.authIndex); existing.channels.add(row.channel); - existing.totalCalls += 1; - existing.successCalls += row.failed ? 0 : 1; - existing.failureCalls += row.failed ? 1 : 0; + existing.totalCalls += row.requestCount; + existing.successCalls += row.successCalls; + existing.failureCalls += row.failureCalls; existing.inputTokens += row.inputTokens; existing.outputTokens += row.outputTokens; existing.cachedTokens += row.cachedTokens; @@ -901,8 +914,8 @@ export const buildAccountRows = (rows: MonitoringEventRow[]): MonitoringAccountR existing.lastSeenAt = Math.max(existing.lastSeenAt, row.timestampMs); if (row.latencyMs !== null) { - existing.latencySum += row.latencyMs; - existing.latencyCount += 1; + existing.latencySum += row.latencySumMs; + existing.latencyCount += row.latencyCount; } const modelEntry = existing.modelMap.get(row.model) ?? { @@ -918,9 +931,9 @@ export const buildAccountRows = (rows: MonitoringEventRow[]): MonitoringAccountR lastSeenAt: 0, }; - modelEntry.totalCalls += 1; - modelEntry.successCalls += row.failed ? 0 : 1; - modelEntry.failureCalls += row.failed ? 1 : 0; + modelEntry.totalCalls += row.requestCount; + modelEntry.successCalls += row.successCalls; + modelEntry.failureCalls += row.failureCalls; modelEntry.inputTokens += row.inputTokens; modelEntry.outputTokens += row.outputTokens; modelEntry.cachedTokens += row.cachedTokens; @@ -1061,9 +1074,9 @@ export const buildApiKeyRows = (rows: MonitoringEventRow[]): MonitoringApiKeyRow existing.sourceLabels.add(row.sourceMasked || row.source); existing.channels.add(row.channel); - existing.totalCalls += 1; - existing.successCalls += row.failed ? 0 : 1; - existing.failureCalls += row.failed ? 1 : 0; + existing.totalCalls += row.requestCount; + existing.successCalls += row.successCalls; + existing.failureCalls += row.failureCalls; existing.inputTokens += row.inputTokens; existing.outputTokens += row.outputTokens; existing.cachedTokens += row.cachedTokens; @@ -1072,8 +1085,8 @@ export const buildApiKeyRows = (rows: MonitoringEventRow[]): MonitoringApiKeyRow existing.lastSeenAt = Math.max(existing.lastSeenAt, row.timestampMs); if (row.latencyMs !== null) { - existing.latencySum += row.latencyMs; - existing.latencyCount += 1; + existing.latencySum += row.latencySumMs; + existing.latencyCount += row.latencyCount; } const modelEntry = existing.modelMap.get(row.model) ?? { @@ -1089,9 +1102,9 @@ export const buildApiKeyRows = (rows: MonitoringEventRow[]): MonitoringApiKeyRow lastSeenAt: 0, }; - modelEntry.totalCalls += 1; - modelEntry.successCalls += row.failed ? 0 : 1; - modelEntry.failureCalls += row.failed ? 1 : 0; + modelEntry.totalCalls += row.requestCount; + modelEntry.successCalls += row.successCalls; + modelEntry.failureCalls += row.failureCalls; modelEntry.inputTokens += row.inputTokens; modelEntry.outputTokens += row.outputTokens; modelEntry.cachedTokens += row.cachedTokens; @@ -1207,8 +1220,8 @@ export const buildRealtimeMonitorRows = (rows: MonitoringEventRow[]): Monitoring }; existing.rows.push(row); - existing.successCalls += row.failed ? 0 : 1; - existing.failureCalls += row.failed ? 1 : 0; + existing.successCalls += row.successCalls; + existing.failureCalls += row.failureCalls; existing.inputTokens += row.inputTokens; existing.outputTokens += row.outputTokens; existing.cachedTokens += row.cachedTokens; @@ -1222,8 +1235,8 @@ export const buildRealtimeMonitorRows = (rows: MonitoringEventRow[]): Monitoring } if (row.latencyMs !== null) { - existing.latencySum += row.latencyMs; - existing.latencyCount += 1; + existing.latencySum += row.latencySumMs; + existing.latencyCount += row.latencyCount; } grouped.set(key, existing); @@ -1310,8 +1323,8 @@ const buildModelShareRows = (rows: MonitoringEventRow[]) => { totalTokens: 0, totalCost: 0, }; - existing.requests += 1; - existing.failures += row.failed ? 1 : 0; + existing.requests += row.requestCount; + existing.failures += row.failureCalls; existing.totalTokens += row.totalTokens; existing.totalCost += row.totalCost; grouped.set(row.model, existing); @@ -1373,13 +1386,13 @@ const buildChannelRows = (rows: MonitoringEventRow[]) => { existing.planTypes.add(row.planType); } existing.models.add(row.model); - existing.requests += 1; - existing.failures += row.failed ? 1 : 0; + existing.requests += row.requestCount; + existing.failures += row.failureCalls; existing.totalTokens += row.totalTokens; existing.totalCost += row.totalCost; if (row.latencyMs !== null) { - existing.latencySum += row.latencyMs; - existing.latencyCount += 1; + existing.latencySum += row.latencySumMs; + existing.latencyCount += row.latencyCount; } grouped.set(key, existing); }); @@ -1434,15 +1447,15 @@ const buildModelRows = (rows: MonitoringEventRow[]) => { channels: new Set(), }; - existing.requests += 1; - existing.failures += row.failed ? 1 : 0; + existing.requests += row.requestCount; + existing.failures += row.failureCalls; existing.totalTokens += row.totalTokens; existing.totalCost += row.totalCost; existing.sources.add(row.source); existing.channels.add(row.channel); if (row.latencyMs !== null) { - existing.latencySum += row.latencyMs; - existing.latencyCount += 1; + existing.latencySum += row.latencySumMs; + existing.latencyCount += row.latencyCount; } grouped.set(row.model, existing); @@ -1491,13 +1504,13 @@ const buildFailureSourceRows = (rows: MonitoringEventRow[]) => { latencyCount: 0, }; - existing.totalRequests += 1; + existing.totalRequests += row.requestCount; existing.lastSeenAt = Math.max(existing.lastSeenAt, row.timestampMs); if (row.failed) { - existing.failures += 1; + existing.failures += row.failureCalls; if (row.latencyMs !== null) { - existing.latencySum += row.latencyMs; - existing.latencyCount += 1; + existing.latencySum += row.latencySumMs; + existing.latencyCount += row.latencyCount; } } @@ -1564,15 +1577,15 @@ const buildTaskBuckets = (rows: MonitoringEventRow[]) => { maxLatencyMs: null, }; - existing.calls += 1; - existing.failedCalls += row.failed ? 1 : 0; + existing.calls += row.requestCount; + existing.failedCalls += row.failureCalls; existing.models.add(row.model); existing.endpoints.add(row.endpointPath || row.endpoint); existing.totalTokens += row.totalTokens; existing.totalCost += row.totalCost; if (row.latencyMs !== null) { - existing.latencySum += row.latencyMs; - existing.latencyCount += 1; + existing.latencySum += row.latencySumMs; + existing.latencyCount += row.latencyCount; existing.maxLatencyMs = Math.max(existing.maxLatencyMs ?? 0, row.latencyMs); } @@ -1692,6 +1705,22 @@ const buildEventRows = ( Number(detail.tokens?.total_tokens) || 0, extractTotalTokens(detail) ); + const requestCount = Math.max(Number(detail.request_count) || 1, 1); + const successCalls = Math.max( + Number(detail.success_count) || (detail.failed === true ? 0 : requestCount), + 0 + ); + const failureCalls = Math.max( + Number(detail.failure_count) || (detail.failed === true ? requestCount : 0), + 0 + ); + const latencyCount = Math.max(Number(detail.latency_count) || 0, 0); + const latencySumMs = + latencyCount > 0 + ? Math.max(Number(detail.latency_sum_ms) || 0, 0) + : typeof detail.latency_ms === 'number' + ? detail.latency_ms + : 0; const totalCost = calculateCost(detail, modelPriceIndex); const statsIncluded = detail.failed === true || inputTokens > 0 || outputTokens > 0; const dayKey = buildLocalDayKey(timestampMs); @@ -1728,8 +1757,14 @@ const buildEventRows = ( channelHost: channelMeta?.host || '-', channelDisabled: channelMeta?.disabled || false, failed: detail.failed === true, + requestCount, + successCalls, + failureCalls, statsIncluded, latencyMs: typeof detail.latency_ms === 'number' ? detail.latency_ms : null, + latencySumMs, + latencyCount: + latencyCount > 0 ? latencyCount : typeof detail.latency_ms === 'number' ? 1 : 0, inputTokens, outputTokens, reasoningTokens, @@ -1804,6 +1839,7 @@ const loadMonitoringMetaPayload = async ( export function useMonitoringData({ usage, + usagePages, config, modelPrices, apiKeyAliases, @@ -1905,25 +1941,90 @@ export function useMonitoringData({ const modelPriceIndex = useMemo(() => buildModelPriceIndex(modelPrices), [modelPrices]); - const allRows = useMemo(() => { - const details = collectUsageDetailsWithEndpoint(usage); - return buildEventRows( - details, - authMetaMap, + const buildRowsForUsage = useCallback( + (payload: unknown) => { + const details = collectUsageDetailsWithEndpoint(payload); + return buildEventRows( + details, + authMetaMap, + authFileMap, + sourceInfoMap, + channelByAuthIndex, + modelPriceIndex, + apiKeyDisplayMap + ).sort((left, right) => right.timestampMs - left.timestampMs); + }, + [ + apiKeyDisplayMap, authFileMap, - sourceInfoMap, + authMetaMap, channelByAuthIndex, modelPriceIndex, - apiKeyDisplayMap - ).sort((left, right) => right.timestampMs - left.timestampMs); + sourceInfoMap, + ] + ); + + const allRows = useMemo(() => { + return buildRowsForUsage(usage); + }, [buildRowsForUsage, usage]); + + const accountPageRows = useMemo(() => { + const pageUsage = usagePages?.accounts?.usage; + if (!pageUsage) return null; + const rows = buildRangeFilteredRows( + buildRowsForUsage(pageUsage), + timeRange, + customTimeRange, + searchQuery, + searchApiKeyHash + ); + return buildAccountRows(rows); + }, [ + buildRowsForUsage, + customTimeRange, + searchApiKeyHash, + searchQuery, + timeRange, + usagePages?.accounts?.usage, + ]); + + const apiKeyPageRows = useMemo(() => { + const pageUsage = usagePages?.apiKeys?.usage; + if (!pageUsage) return null; + const rows = buildRangeFilteredRows( + buildRowsForUsage(pageUsage), + timeRange, + customTimeRange, + searchQuery, + searchApiKeyHash + ); + return buildApiKeyRows(rows); + }, [ + buildRowsForUsage, + customTimeRange, + searchApiKeyHash, + searchQuery, + timeRange, + usagePages?.apiKeys?.usage, + ]); + + const realtimePageRows = useMemo(() => { + const pageUsage = usagePages?.realtime?.usage; + if (!pageUsage) return null; + return buildRangeFilteredRows( + buildRowsForUsage(pageUsage), + timeRange, + customTimeRange, + searchQuery, + searchApiKeyHash + ); }, [ - apiKeyDisplayMap, - authFileMap, - authMetaMap, - channelByAuthIndex, - modelPriceIndex, - sourceInfoMap, - usage, + buildRowsForUsage, + customTimeRange, + searchApiKeyHash, + searchQuery, + timeRange, + usagePages?.realtime?.usage, ]); const filteredRows = useMemo( @@ -1985,6 +2086,9 @@ export function useMonitoringData({ taskBuckets, recentFailures, filteredRows, + accountPageRows, + apiKeyPageRows, + realtimePageRows, refreshMeta, }; } diff --git a/src/features/monitoring/hooks/useUsageData.ts b/src/features/monitoring/hooks/useUsageData.ts index ab2b16bf2..30d0662f8 100644 --- a/src/features/monitoring/hooks/useUsageData.ts +++ b/src/features/monitoring/hooks/useUsageData.ts @@ -7,6 +7,9 @@ import { type ApiKeyAliasesResponse, type ModelPricesResponse, type ModelPriceSyncResponse, + type UsagePageQuery, + type UsagePageResponse, + type UsageQuery, type UsageExportResponse, type UsageImportResponse, } from '@/services/api/usageService'; @@ -23,8 +26,21 @@ export interface UsagePayload { [key: string]: unknown; } +export type UsagePageQueries = { + accounts?: UsagePageQuery; + apiKeys?: UsagePageQuery; + realtime?: UsagePageQuery; +}; + +export type UsagePages = { + accounts?: UsagePageResponse; + apiKeys?: UsagePageResponse; + realtime?: UsagePageResponse; +}; + export interface UseUsageDataReturn { usage: UsagePayload | null; + usagePages: UsagePages | null; loading: boolean; error: string; lastRefreshedAt: Date | null; @@ -39,12 +55,23 @@ export interface UseUsageDataReturn { loadUsage: () => Promise; } -export function useUsageData(): UseUsageDataReturn { +const isUsagePageFallbackError = (error: unknown) => { + if (!(error instanceof Error)) return false; + const status = (error as { status?: number }).status; + const code = (error as { code?: string }).code; + return status === 404 || status === 405 || code === 'method_not_allowed'; +}; + +export function useUsageData( + usageQuery?: UsageQuery, + usagePageQueries?: UsagePageQueries +): UseUsageDataReturn { const apiBase = useAuthStore((state) => state.apiBase); const managementKey = useAuthStore((state) => state.managementKey); const usageServiceEnabled = useUsageServiceStore((state) => state.enabled); const usageServiceBase = useUsageServiceStore((state) => state.serviceBase); const [usage, setUsage] = useState(null); + const [usagePages, setUsagePages] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [lastRefreshedAt, setLastRefreshedAt] = useState(null); @@ -138,6 +165,61 @@ export function useUsageData(): UseUsageDataReturn { [managementKey, resolveUsageServiceBase] ); + const loadUsagePages = useCallback( + async (serviceBase: string): Promise => { + if (!usagePageQueries) return null; + try { + const [accounts, apiKeys, realtime] = await Promise.all([ + usagePageQueries.accounts + ? usageServiceApi.getUsagePage( + serviceBase, + managementKey, + 'accounts', + usageQuery, + usagePageQueries.accounts + ) + : Promise.resolve(undefined), + usagePageQueries.apiKeys + ? usageServiceApi.getUsagePage( + serviceBase, + managementKey, + 'api-keys', + usageQuery, + usagePageQueries.apiKeys + ) + : Promise.resolve(undefined), + usagePageQueries.realtime + ? usageServiceApi.getUsagePage( + serviceBase, + managementKey, + 'realtime', + usageQuery, + usagePageQueries.realtime + ) + : Promise.resolve(undefined), + ]); + return { accounts, apiKeys, realtime }; + } catch (error) { + if (isUsagePageFallbackError(error)) { + return null; + } + throw error; + } + }, + [ + managementKey, + usagePageQueries?.accounts?.page, + usagePageQueries?.accounts?.pageSize, + usagePageQueries?.accounts?.sortDirection, + usagePageQueries?.accounts?.sortKey, + usagePageQueries?.apiKeys?.page, + usagePageQueries?.apiKeys?.pageSize, + usagePageQueries?.realtime?.page, + usagePageQueries?.realtime?.pageSize, + usageQuery, + ] + ); + const loadModelPricesFromStorage = useCallback(async () => { const fallbackPrices = loadModelPrices(); try { @@ -184,13 +266,18 @@ export function useUsageData(): UseUsageDataReturn { if (!serviceBase) { setUsageServiceAvailable(false); setUsage(null); + setUsagePages(null); setLastRefreshedAt(null); return; } setUsageServiceAvailable(true); - const payload = await usageServiceApi.getUsage(serviceBase, managementKey); + const [payload, pages] = await Promise.all([ + usageServiceApi.getUsage(serviceBase, managementKey, usageQuery), + loadUsagePages(serviceBase), + ]); if (requestIdRef.current !== requestId) return; setUsage(payload ?? null); + setUsagePages(pages); setLastRefreshedAt(new Date()); } catch (err) { if (requestIdRef.current !== requestId) return; @@ -200,7 +287,21 @@ export function useUsageData(): UseUsageDataReturn { setLoading(false); } } - }, [managementKey, resolveUsageServiceBase]); + }, [ + loadUsagePages, + managementKey, + resolveUsageServiceBase, + usageQuery?.account, + usageQuery?.apiKeyHash, + usageQuery?.channel, + usageQuery?.endMs, + usageQuery?.model, + usageQuery?.provider, + usageQuery?.search, + usageQuery?.searchApiKeyHash, + usageQuery?.startMs, + usageQuery?.status, + ]); useEffect(() => { void loadModelPricesFromStorage(); @@ -234,6 +335,7 @@ export function useUsageData(): UseUsageDataReturn { return { usage, + usagePages, loading, error, lastRefreshedAt, diff --git a/src/pages/MonitoringCenterPage.tsx b/src/pages/MonitoringCenterPage.tsx index b019d7aeb..826c0797d 100644 --- a/src/pages/MonitoringCenterPage.tsx +++ b/src/pages/MonitoringCenterPage.tsx @@ -269,6 +269,25 @@ const buildPaginationState = ( }; }; +const buildRemotePaginationState = ( + items: readonly T[], + page: number, + pageSize: number, + count: number +): PaginationState => { + const safePageSize = Math.max(1, pageSize); + const totalPages = Math.max(1, Math.ceil(Math.max(0, count) / safePageSize)); + const currentPage = Math.min(Math.max(1, page), totalPages); + const startItem = count > 0 ? (currentPage - 1) * safePageSize + 1 : 0; + return { + currentPage, + totalPages, + pageItems: [...items], + startItem, + endItem: count > 0 ? Math.min(startItem + items.length - 1, count) : 0, + }; +}; + const parsePageSize = (value: string, fallback: number) => { const parsed = Number.parseInt(value, 10); return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; @@ -1956,8 +1975,66 @@ export function MonitoringCenterPage() { return ''; }, [customDraftEndMs, customDraftStartMs, t]); + const usageQuery = useMemo(() => { + const bounds = getRangeBounds(timeRange, Date.now(), customTimeRange); + if (!bounds) return undefined; + return { + startMs: Number.isFinite(bounds.startMs) ? bounds.startMs : undefined, + endMs: Number.isFinite(bounds.endMs) ? bounds.endMs : undefined, + account: selectedAccount !== 'all' ? selectedAccount : undefined, + provider: selectedProvider !== 'all' ? selectedProvider : undefined, + model: selectedModel !== 'all' ? selectedModel : undefined, + channel: selectedChannel !== 'all' ? selectedChannel : undefined, + apiKeyHash: selectedApiKeyHash !== 'all' ? selectedApiKeyHash : undefined, + status: selectedStatus !== 'all' ? selectedStatus : undefined, + search: deferredSearch.trim() || undefined, + searchApiKeyHash: deferredSearch.trim() ? deferredSearchApiKeyHash : undefined, + }; + }, [ + customTimeRange, + deferredSearch, + deferredSearchApiKeyHash, + selectedAccount, + selectedApiKeyHash, + selectedChannel, + selectedModel, + selectedProvider, + selectedStatus, + timeRange, + ]); + + const usagePageQueries = useMemo( + () => ({ + accounts: { + page: accountPage, + pageSize: accountPageSize, + sortKey: accountSort.key, + sortDirection: accountSort.direction, + }, + apiKeys: { + page: apiKeyPage, + pageSize: apiKeyPageSize, + }, + realtime: { + page: realtimePage, + pageSize: realtimePageSize, + }, + }), + [ + accountPage, + accountPageSize, + accountSort.direction, + accountSort.key, + apiKeyPage, + apiKeyPageSize, + realtimePage, + realtimePageSize, + ] + ); + const { usage, + usagePages, loading: usageLoading, error: usageError, lastRefreshedAt, @@ -1970,16 +2047,20 @@ export function MonitoringCenterPage() { exportUsage, importUsage, loadUsage, - } = useUsageData(); + } = useUsageData(usageQuery, usagePageQueries); const { loading: monitoringLoading, error: monitoringError, authFiles, filteredRows, + accountPageRows, + apiKeyPageRows, + realtimePageRows, refreshMeta, } = useMonitoringData({ usage, + usagePages, config, modelPrices, apiKeyAliases, @@ -2256,22 +2337,64 @@ export function MonitoringCenterPage() { () => sortAccountRows(accountRows, accountSort), [accountRows, accountSort] ); + const displayedAccountRows = useMemo( + () => (accountPageRows ? sortAccountRows(accountPageRows, accountSort) : sortedAccountRows), + [accountPageRows, accountSort, sortedAccountRows] + ); + const accountTotalCount = + accountPageRows && usagePages?.accounts + ? Math.max(0, usagePages.accounts.total_items) + : sortedAccountRows.length; const groupedRealtimeRows = useMemo( () => buildRealtimeMonitorRows(scopedStatsRows), [scopedStatsRows] ); - const realtimeLogRows = useMemo(() => buildRealtimeLogRows(scopedRows), [scopedRows]); + const displayedApiKeyRows = apiKeyPageRows ?? apiKeyRows; + const apiKeyTotalCount = + apiKeyPageRows && usagePages?.apiKeys ? Math.max(0, usagePages.apiKeys.total_items) : apiKeyRows.length; + const realtimeLogRows = useMemo( + () => buildRealtimeLogRows(realtimePageRows ?? scopedRows), + [realtimePageRows, scopedRows] + ); + const realtimeTotalCount = + realtimePageRows && usagePages?.realtime + ? Math.max(0, usagePages.realtime.total_items) + : realtimeLogRows.length; const accountPagination = useMemo( - () => buildPaginationState(sortedAccountRows, accountPage, accountPageSize), - [accountPage, accountPageSize, sortedAccountRows] + () => + accountPageRows && usagePages?.accounts + ? buildRemotePaginationState( + displayedAccountRows, + usagePages.accounts.page, + usagePages.accounts.page_size, + usagePages.accounts.total_items + ) + : buildPaginationState(sortedAccountRows, accountPage, accountPageSize), + [accountPage, accountPageRows, accountPageSize, displayedAccountRows, sortedAccountRows, usagePages?.accounts] ); const apiKeyPagination = useMemo( - () => buildPaginationState(apiKeyRows, apiKeyPage, apiKeyPageSize), - [apiKeyPage, apiKeyPageSize, apiKeyRows] + () => + apiKeyPageRows && usagePages?.apiKeys + ? buildRemotePaginationState( + displayedApiKeyRows, + usagePages.apiKeys.page, + usagePages.apiKeys.page_size, + usagePages.apiKeys.total_items + ) + : buildPaginationState(apiKeyRows, apiKeyPage, apiKeyPageSize), + [apiKeyPage, apiKeyPageRows, apiKeyPageSize, apiKeyRows, displayedApiKeyRows, usagePages?.apiKeys] ); const realtimePagination = useMemo( - () => buildPaginationState(realtimeLogRows, realtimePage, realtimePageSize), - [realtimeLogRows, realtimePage, realtimePageSize] + () => + realtimePageRows && usagePages?.realtime + ? buildRemotePaginationState( + realtimeLogRows, + usagePages.realtime.page, + usagePages.realtime.page_size, + usagePages.realtime.total_items + ) + : buildPaginationState(realtimeLogRows, realtimePage, realtimePageSize), + [realtimeLogRows, realtimePage, realtimePageRows, realtimePageSize, usagePages?.realtime] ); const accountPageResetState = useMemo( () => ({ @@ -3511,7 +3634,7 @@ export function MonitoringCenterPage() { ); })} - {sortedAccountRows.length === 0 ? ( + {accountTotalCount === 0 ? ( {renderMonitoringEmptyState()} @@ -3519,7 +3642,7 @@ export function MonitoringCenterPage() { - ) : sortedAccountRows.length > 0 ? ( + ) : accountPagination.pageItems.length > 0 ? (
{accountPagination.pageItems.map((row) => { const authState = accountAuthStateByRowId.get(row.id) ?? EMPTY_ACCOUNT_AUTH_STATE; @@ -3550,7 +3673,7 @@ export function MonitoringCenterPage() { renderMonitoringEmptyState() )} - {t('monitoring.api_key_summary_keys_count', { count: apiKeyRows.length })} + {t('monitoring.api_key_summary_keys_count', { count: apiKeyTotalCount })}
} > @@ -3647,7 +3770,7 @@ export function MonitoringCenterPage() { ); })} - {apiKeyRows.length === 0 ? ( + {apiKeyTotalCount === 0 ? ( {renderMonitoringEmptyState()} @@ -3656,7 +3779,7 @@ export function MonitoringCenterPage() { - {`${t('monitoring.log_rows')}: ${realtimeLogRows.length}`} + {`${t('monitoring.log_rows')}: ${realtimeTotalCount}`} {`${t('monitoring.recent_failures')}: ${scopedFailureCount}`}