diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/mergeDailyActivity.ts b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/mergeDailyActivity.ts index cb216516559..15ac770f1ab 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/mergeDailyActivity.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/mergeDailyActivity.ts @@ -74,14 +74,6 @@ const mergeDay = (a: DailyData, b: DailyData): DailyData => ({ breakdown: mergeBreakdown(a.breakdown, b.breakdown), }); -/** - * Combine daily activity pages into one series with a single entry per date. - * - * The backend paginates over raw spend rows, so a date whose rows straddle a - * page boundary comes back once per page, each entry holding only that page's - * share of the day. Concatenating those entries leaves duplicate dates that - * under-report every per-day figure in the charts and the CSV export. - */ export const mergeDailyResults = (existing: readonly DailyData[], incoming: readonly DailyData[]): DailyData[] => incoming.reduce( (acc, day) => { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/usePaginatedDailyActivity.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/usePaginatedDailyActivity.test.ts index aae34fa6364..6dd52bcf2a1 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/usePaginatedDailyActivity.test.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/usePaginatedDailyActivity.test.ts @@ -119,6 +119,24 @@ describe("usePaginatedDailyActivity", () => { expect(result.current.incomplete).toBe(false); }); + it("stays incomplete while the first page is still in flight", async () => { + let resolveFirstPage: (value: ReturnType) => void = () => {}; + const fetchFn = vi.fn().mockReturnValueOnce( + new Promise>((resolve) => { + resolveFirstPage = resolve; + }), + ); + + const { result } = renderHook(() => usePaginatedDailyActivity({ fetchFn, args, enabled: true })); + + await waitFor(() => expect(result.current.loading).toBe(true), { timeout: 3000 }); + expect(result.current.incomplete).toBe(true); + + resolveFirstPage(page("2026-06-25", 22.38, 1, 1)); + + await waitFor(() => expect(result.current.incomplete).toBe(false), { timeout: 3000 }); + }); + it("flags the range as incomplete when a page fetch fails instead of looking complete", async () => { const fetchFn = vi .fn() diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/usePaginatedDailyActivity.ts b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/usePaginatedDailyActivity.ts index 2fdfe1cf459..885a918b484 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/usePaginatedDailyActivity.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/usePaginatedDailyActivity.ts @@ -49,9 +49,7 @@ interface UsePaginatedDailyActivityReturn { isFetchingMore: boolean; progress: PaginationProgress; cancelled: boolean; - /** True when a page fetch failed, so the data on screen covers only part of the range. */ failed: boolean; - /** True whenever the data on screen is known not to cover the whole requested range. */ incomplete: boolean; cancel: () => void; } @@ -250,7 +248,7 @@ export function usePaginatedDailyActivity({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [enabled, fetchFn, argsKey]); - const incomplete = isFetchingMore || cancelled || failed; + const incomplete = loading || isFetchingMore || cancelled || failed; return { data, loading, isFetchingMore, progress, cancelled, failed, incomplete, cancel }; } diff --git a/ui/litellm-dashboard/src/components/EntityUsageExport/UsageExportHeader.tsx b/ui/litellm-dashboard/src/components/EntityUsageExport/UsageExportHeader.tsx index a0c8b31d877..1bb5f525b17 100644 --- a/ui/litellm-dashboard/src/components/EntityUsageExport/UsageExportHeader.tsx +++ b/ui/litellm-dashboard/src/components/EntityUsageExport/UsageExportHeader.tsx @@ -34,7 +34,6 @@ interface UsageExportHeaderProps { customTitle?: string; compactLayout?: boolean; teams?: Team[]; - /** Blocks the export while the data on screen does not cover the whole requested range. */ exportDisabled?: boolean; exportDisabledReason?: string; }