Skip to content

fix: stabilize usage log virtual scrolling#959

Merged
ding113 merged 5 commits intodevfrom
feat/stable-usage-log-virtual-scroll
Mar 21, 2026
Merged

fix: stabilize usage log virtual scrolling#959
ding113 merged 5 commits intodevfrom
feat/stable-usage-log-virtual-scroll

Conversation

@ding113
Copy link
Copy Markdown
Owner

@ding113 ding113 commented Mar 21, 2026

Summary

Fixes usage log virtual scrolling truncation and stability issues by removing the maxPages cap from dashboard logs and migrating my-usage logs from page-based pagination to cursor-based TanStack Query + TanStack Virtual infinite scrolling.

Problem

  • Dashboard logs page experienced truncation due to limited infinite-query page retention (maxPages: 5 was capping cached pages, causing deep scroll to lose access to the latest rows)
  • My-usage logs used page-based pagination which does not work well with virtual scrolling and could lead to data inconsistency when scrolling deep into history

Related Work:

Solution

  1. Remove the maxPages limit from the infinite query in the dashboard logs table to allow full page retention
  2. Create a reusable useVirtualizedInfiniteList hook that consolidates virtual list logic for both dashboard and my-usage tables
  3. Migrate my-usage logs from page-based (page, pageSize) to cursor-based pagination (cursor, limit) with proper nextCursor/hasMore response structure
  4. Replace traditional table with TanStack Virtual for my-usage logs to match the dashboard implementation

Changes

Core Changes

  • src/repository/usage-logs.ts: Replace findUsageLogsForKeySlim with findUsageLogsForKeyBatch using cursor-based pagination
  • src/actions/my-usage.ts: Replace getMyUsageLogs with getMyUsageLogsBatch action (cursor-based)
  • src/hooks/use-virtualized-infinite-list.ts: New reusable hook for virtualized infinite scrolling logic

UI Changes

  • src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx: Remove maxPages limit, use new hook
  • src/app/[locale]/my-usage/_components/usage-logs-section.tsx: Migrate to TanStack Query infinite query with cursor pagination
  • src/app/[locale]/my-usage/_components/usage-logs-table.tsx: Convert from Table component to virtual scrolling
  • src/app/[locale]/my-usage/page.tsx: Simplify by removing server-side logs loading (now client-side)

API Changes

  • src/app/api/actions/[...route]/route.ts: Replace getMyUsageLogs route with getMyUsageLogsBatch (cursor-based)

Testing

Automated Tests

  • Unit tests added for my-usage logs section (usage-logs-section.test.tsx)
  • Regression test added to verify maxPages is not capped in dashboard logs
  • Updated existing tests for cursor-based pagination (my-usage-date-range-dst.test.ts)
  • Updated API integrity tests for new action route name

Manual Testing

  1. Open dashboard logs page and scroll deep into history - verify no truncation
  2. Open my-usage page and test infinite scrolling
  3. Verify auto-refresh pauses when browsing history (scrolled down)
  4. Verify "scroll to top" button appears when scrolled

Checklist

  • Code follows project conventions
  • Self-review completed
  • Tests pass locally
  • No breaking changes (old action removed, new batch action added)

Description enhanced by Claude AI

Greptile Summary

This PR fixes virtual-scrolling reliability in both the dashboard logs table and the my-usage logs section by (1) removing the maxPages: 5 cap that caused deep-scroll truncation on the dashboard, and (2) migrating my-usage logs from page-based pagination to cursor-based TanStack Query infinite scroll backed by a new findUsageLogsForKeyBatch repository function.

Key changes:

  • src/repository/usage-logs.ts: Replaces findUsageLogsForKeySlim (OFFSET pagination + COUNT(*)) with findUsageLogsForKeyBatch (keyset/cursor pagination). Adds buildNextCursorOrThrow helper — addressing the prior review concern that hasMore and nextCursor could silently drift, now enforced with a hard throw.
  • src/hooks/use-virtualized-infinite-list.ts: New shared hook that consolidates scroll tracking, fetchNextPage triggering, scroll-to-top state, and scroll-position reset for both the dashboard and my-usage virtual tables.
  • src/app/[locale]/my-usage/_components/usage-logs-section.tsx: Replaces manual setInterval polling + page state with TanStack Query useInfiniteQuery; auto-refresh suppression wired through isBrowsingHistory state from the child table.
  • src/app/[locale]/my-usage/_components/usage-logs-table.tsx: Converts from a <Table> with prev/next buttons to a TanStack Virtual infinite scroll list matching the dashboard implementation.
  • Test coverage is solid: regression tests for maxPages absence, cursor integer schema, DST date-range correctness, buildNextCursorOrThrow throw path, and new component-level tests for the my-usage section and table.

Confidence Score: 4/5

  • Safe to merge; prior critical concern (hasMore/nextCursor drift) is fully resolved with buildNextCursorOrThrow, and remaining items are non-blocking style suggestions.
  • Both P0 issues flagged in the prior review thread have been addressed: the silent infinite-scroll truncation risk is eliminated by buildNextCursorOrThrow, and fetchNextPage is wrapped in useCallback at both call sites. The two remaining comments (duplicate errorMessage display and a dead-code refetch fallback) are cosmetic P2s that do not affect correctness or reliability. Score reflects strong convergence from prior concerns with only minor polish left.
  • usage-logs-table.tsx (minor duplicate error banner), usage-logs-section.tsx (dead-code refetch fallback)

Important Files Changed

Filename Overview
src/hooks/use-virtualized-infinite-list.ts New shared hook that extracts virtual-scroll + infinite-fetch logic. Clean design: stable scroll-element ref, rowVirtualizerRef sync pattern for resetScrollPosition, configurable thresholds. No issues found.
src/repository/usage-logs.ts Replaces findUsageLogsForKeySlim (page-based, with expensive COUNT(*)) with findUsageLogsForKeyBatch (cursor-based). Adds buildNextCursorOrThrow helper that ensures hasMore and nextCursor are always consistent — addressing the prior thread concern about silent pagination truncation. Both batch paths (message-request and ledger fallback) correctly thread the cursor condition.
src/app/[locale]/my-usage/_components/usage-logs-table.tsx Migrates from a paginated <Table> to a TanStack Virtual infinite-scroll list. handleLoadMore is correctly wrapped in useCallback. Minor: errorMessage is shown redundantly in both a top banner and the status row; the plain banner adds no actionable affordance.
src/app/[locale]/my-usage/_components/usage-logs-section.tsx Migrated from manual polling + page state to TanStack Query infinite query. isBrowsingHistory wired through onHistoryBrowsingChange correctly suppresses auto-refresh when scrolled down. query.refetch fallback is unnecessary dead code.
src/actions/my-usage.ts Replaces getMyUsageLogs (page+total) with getMyUsageLogsBatch (cursor+hasMore). Extracts mapMyUsageLogEntries to deduplicate mapping logic. Clean migration with no regressions visible.
src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx Removes maxPages: 5 cap (core fix), delegates virtual-list plumbing to useVirtualizedInfiniteList, and switches from JSON-stringify comparison for filter resets to the useEffectEvent pattern. The isHistoryBrowsing flag now controls auto-refresh suppression cleanly.
src/app/[locale]/my-usage/page.tsx Simplified by removing the server-side logs preload — logs are now fetched entirely client-side via TanStack Query. Clean removal of isLogsLoading / logsData state and the corresponding getMyUsageLogs call.
src/app/api/actions/[...route]/route.ts Route updated from getMyUsageLogs to getMyUsageLogsBatch, schema migrated from page/pageSize to cursor/limit. cursor.id and response nextCursor.id both declared as z.number().int(), verified by new integrity test.

Sequence Diagram

sequenceDiagram
    participant UI as UsageLogsSection
    participant Hook as useVirtualizedInfiniteList
    participant TQ as TanStack Query
    participant Action as getMyUsageLogsBatch
    participant DB as findUsageLogsForKeyBatch

    UI->>TQ: useInfiniteQuery(queryKey, queryFn, getNextPageParam)
    TQ->>Action: queryFn({ pageParam: undefined })
    Action->>DB: cursor=undefined, limit=20
    DB-->>Action: { logs, nextCursor, hasMore }
    Action-->>TQ: page[0]

    UI->>Hook: useVirtualizedInfiniteList({ itemCount, hasNextPage, fetchNextPage })
    Note over Hook: User scrolls near bottom
    Hook->>TQ: fetchNextPage()
    TQ->>Action: queryFn({ pageParam: nextCursor })
    Action->>DB: cursor={createdAt, id}, limit=20
    DB-->>Action: { logs, nextCursor, hasMore }
    Action-->>TQ: page[1]
    TQ-->>UI: data.pages = [page0, page1, ...]

    Note over UI: Filter change
    UI->>TQ: setAppliedFilters (new queryKey)
    TQ->>Action: queryFn({ pageParam: undefined })
    Hook->>Hook: resetScrollPosition()
Loading

Comments Outside Diff (4)

  1. src/app/[locale]/my-usage/_components/usage-logs-section.tsx, line 596 (link)

    P2 Misleading variable name latestPage

    data?.pages[0] is the first page fetched (the most-recent log batch in the DESC-ordered query). As the user scrolls, TanStack Query appends older data as pages[1], pages[2], etc. So pages[0] is actually the first/initial page, not the "latest" one in the sense of "last appended". The name may confuse future readers who equate "latest" with "most recently fetched". A name like firstPage or initialPage would be clearer.

    …and update the currencyCode prop reference accordingly:

    currencyCode={firstPage?.currencyCode}
    
    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/app/[locale]/my-usage/_components/usage-logs-section.tsx
    Line: 596
    
    Comment:
    **Misleading variable name `latestPage`**
    
    `data?.pages[0]` is the _first_ page fetched (the most-recent log batch in the DESC-ordered query). As the user scrolls, TanStack Query appends older data as `pages[1]`, `pages[2]`, etc. So `pages[0]` is actually the _first/initial_ page, not the "latest" one in the sense of "last appended". The name may confuse future readers who equate "latest" with "most recently fetched". A name like `firstPage` or `initialPage` would be clearer.
    
    
    
    …and update the `currencyCode` prop reference accordingly:
    
    ```
    currencyCode={firstPage?.currencyCode}
    ```
    
    How can I resolve this? If you propose a fix, please make it concise.
  2. src/app/[locale]/my-usage/_components/usage-logs-section.tsx, line 593 (link)

    P2 Dead-code fallback for refetch

    query.refetch is always a function in TanStack Query — it is part of the stable UseInfiniteQueryResult contract and will never be undefined. The ?? (async (): Promise<unknown> => undefined) fallback is unreachable and adds noise. Consider simplifying to:

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/app/[locale]/my-usage/_components/usage-logs-section.tsx
    Line: 593
    
    Comment:
    **Dead-code fallback for `refetch`**
    
    `query.refetch` is always a function in TanStack Query — it is part of the stable `UseInfiniteQueryResult` contract and will never be `undefined`. The `?? (async (): Promise<unknown> => undefined)` fallback is unreachable and adds noise. Consider simplifying to:
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.
  3. src/app/[locale]/my-usage/_components/usage-logs-table.tsx, line 1277-1296 (link)

    P2 errorMessage rendered twice when stale logs are present

    When errorMessage is set and onLoadMore is provided, the message appears in two adjacent places: a plain red banner (line 1277) and again in the status row with a retry button (lines 1281-1295). The banner has no actionable affordance, so the only useful version is the status-row entry with the retry button.

    The top banner should either be removed (let the status row own the error state) or changed to only show when !onLoadMore (i.e. truly unrecoverable), so the user doesn't see the same string twice:

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/app/[locale]/my-usage/_components/usage-logs-table.tsx
    Line: 1277-1296
    
    Comment:
    **`errorMessage` rendered twice when stale logs are present**
    
    When `errorMessage` is set and `onLoadMore` is provided, the message appears in two adjacent places: a plain red banner (line 1277) and again in the status row with a retry button (lines 1281-1295). The banner has no actionable affordance, so the only useful version is the status-row entry with the retry button.
    
    The top banner should either be removed (let the status row own the error state) or changed to only show when `!onLoadMore` (i.e. truly unrecoverable), so the user doesn't see the same string twice:
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.
  4. src/app/[locale]/my-usage/_components/usage-logs-section.tsx, line 593 (link)

    P2 Unnecessary refetch fallback

    query.refetch is always present on a TanStack Query result — it is part of the return type and is never undefined. The ?? (async (): Promise<unknown> => undefined) fallback is dead code and just adds noise.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/app/[locale]/my-usage/_components/usage-logs-section.tsx
    Line: 593
    
    Comment:
    **Unnecessary `refetch` fallback**
    
    `query.refetch` is always present on a TanStack Query result — it is part of the return type and is never `undefined`. The `?? (async (): Promise<unknown> => undefined)` fallback is dead code and just adds noise.
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/app/[locale]/my-usage/_components/usage-logs-table.tsx
Line: 1277-1296

Comment:
**`errorMessage` rendered twice when stale logs are present**

When `errorMessage` is set and `onLoadMore` is provided, the message appears in two adjacent places: a plain red banner (line 1277) and again in the status row with a retry button (lines 1281-1295). The banner has no actionable affordance, so the only useful version is the status-row entry with the retry button.

The top banner should either be removed (let the status row own the error state) or changed to only show when `!onLoadMore` (i.e. truly unrecoverable), so the user doesn't see the same string twice:

```suggestion
      {errorMessage && !onLoadMore ? (
        <div className="px-1 text-xs text-destructive">{errorMessage}</div>
      ) : null}
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/app/[locale]/my-usage/_components/usage-logs-section.tsx
Line: 593

Comment:
**Unnecessary `refetch` fallback**

`query.refetch` is always present on a TanStack Query result — it is part of the return type and is never `undefined`. The `?? (async (): Promise<unknown> => undefined)` fallback is dead code and just adds noise.

```suggestion
  const refetch = query.refetch;
```

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: "fix: stabilize my-us..."

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 21, 2026

📝 Walkthrough

Walkthrough

将使用日志检索从基于页码的分页切换为基于游标的批量(cursor/limit)分页,端到端替换后端仓储、动作、API 路由、前端列表与虚拟化钩子,并新增/调整多处测试以覆盖新行为与错误路径。

Changes

Cohort / File(s) Summary
API 路由
src/app/api/actions/[...route]/route.ts
路由与 OpenAPI 注册由 my-usage/getMyUsageLogsmy-usage/getMyUsageLogsBatch,请求由 page/pageSize → cursor/limit(新增可选 sessionId),响应由 total/page/pageSize → nextCursor/hasMore。
仓储实现
src/repository/usage-logs.ts
移除 slim page/total 实现与缓存,新增 findUsageLogsForKeyBatchUsageLogSlimBatchFilters/Result、键集分页逻辑((createdAt,id) < cursor、fetch limit+1)、buildNextCursorOrThrow 验证。
动作/服务层
src/actions/my-usage.ts
getMyUsageLogsgetMyUsageLogsBatch,类型与过滤器改为 Batch/Cursor,改为调用 findUsageLogsForKeyBatch,抽出 mapMyUsageLogEntries 并以 settings.billingModelSource 提供映射来源。
前端:使用量页面与容器
src/app/[locale]/my-usage/page.tsx, src/app/[locale]/my-usage/_components/usage-logs-section.tsx
移除客户端 page 初始数据/loading,改用 TanStack useInfiniteQuery + getMyUsageLogsBatch(cursor/limit);filters 去除 page 字段,pages 平铺合并并由 nextCursor 驱动加载更多与自动刷新。
前端:使用日志表格与测试
src/app/[locale]/my-usage/_components/usage-logs-table.tsx, src/app/[locale]/my-usage/_components/usage-logs-section.test.tsx, src/app/[locale]/my-usage/_components/usage-logs-table.test.tsx
表格从分页 Table → 虚拟化无限列表,移除 total/page/pageSize/onPageChange,新增 hasNextPage/isFetchingNextPage/onLoadMore/resetScrollKey/onHistoryBrowsingChange、固定行高、加载占位行、滚动到顶与错误/空状态分支;相关测试新增/调整为断言新行为且不调用旧 API。
虚拟化通用钩子
src/hooks/use-virtualized-infinite-list.ts
新增导出钩子 useVirtualizedInfiniteList:封装 useVirtualizer、动态项计数(含加载行)、自动触发 fetchNextPage、滚动顶部检测、scrollToTopresetScrollPosition,并导出类型。
仪表盘日志虚拟化及测试
src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx, ...virtualized-logs-table.test.tsx
useVirtualizedInfiniteList 替代本地 useVirtualizer + 手动 near-bottom effect,移除 maxPages 限制;测试改为通过 spy 断言 useInfiniteQuery 的 options。
后端/集成测试更新
tests/api/api-actions-integrity.test.ts, tests/api/my-usage-readonly.test.ts, tests/unit/actions/my-usage-date-range-dst.test.ts, tests/unit/repository/usage-logs-sessionid-filter.test.ts, tests/unit/...
更新/新增测试以反映新端点路径与参数(getMyUsageLogsBatchlimit、cursor 字段)、仓储 batch mocks,并新增异常用例:当 hasMore 为 true 且最后行缺失 createdAtRaw 时应抛错。

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed 标题清晰准确地概括了主要改动,即稳定虚拟滚动功能的修复,与变更集内容高度相关。
Description check ✅ Passed PR description clearly relates to the changeset, detailing the migration from page-based to cursor-based pagination and the virtual scrolling improvements across multiple components.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/stable-usage-log-virtual-scroll

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly improves the user experience and performance of the 'My Usage' logs section by transitioning from a paginated approach to a cursor-based infinite scrolling system. This change addresses previous data truncation issues and leverages virtualization to handle large log datasets efficiently. The update involves comprehensive refactoring across both frontend components and backend API actions to support the new data fetching and rendering paradigm.

Highlights

  • Cursor-Based Infinite Scrolling: Migrated the 'My Usage' logs section from traditional page-based pagination to a more efficient cursor-based infinite scrolling mechanism, enhancing performance and user experience for large datasets.
  • Virtualized List Implementation: Introduced a new useVirtualizedInfiniteList React hook to manage the virtualization of log entries, ensuring smooth rendering and reduced memory consumption even with thousands of items.
  • Backend API Refactoring: Refactored the backend getMyUsageLogs action to getMyUsageLogsBatch, adapting it to support cursor-based pagination with createdAt and id for efficient data retrieval.
  • Removed Page Truncation: Eliminated the previous limitation of dashboard logs page truncation caused by finite infinite-query page retention, allowing users to scroll through all available logs without artificial limits.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@github-actions github-actions bot added bug Something isn't working area:UI size/XL Extra Large PR (> 1000 lines) labels Mar 21, 2026
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request is a significant and well-executed refactoring to implement infinite virtual scrolling for usage logs, which addresses page truncation and improves performance. The changes span from the database layer to the UI, introducing cursor-based pagination and a reusable useVirtualizedInfiniteList hook. The code is cleaner and more efficient. I have a couple of minor suggestions to improve the React useEffect dependency management for better performance and adherence to best practices.

@github-actions
Copy link
Copy Markdown
Contributor

🧪 测试结果

测试类型 状态
代码质量
单元测试
集成测试
API 测试

总体结果: ✅ 所有测试通过

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/actions/my-usage.ts (1)

521-548: ⚠️ Potential issue | 🟠 Major

先校验 cursor,再下推到仓储层。

这里目前只裁剪了 limit,但 cursor.createdAt / cursor.id 完全信任调用方。只要传入非法日期字符串或非整数 id,后面的 SQL 元组比较和 ::timestamptz 强转就会把一个坏请求升级成 500。建议在 action 边界做运行时校验,并对无效 cursor 直接返回可预期的业务错误。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/actions/my-usage.ts` around lines 521 - 548, In getMyUsageLogsBatch the
incoming filters.cursor is implicitly trusted before calling
findUsageLogsForKeyBatch which can cause server errors when cursor.createdAt is
an invalid date string or cursor.id is not an integer; validate the cursor at
the action boundary: if filters.cursor is present ensure cursor.createdAt parses
to a valid Date/ISO timestamptz and cursor.id is a positive integer (or
null/undefined if not provided), and if validation fails return a controlled
ActionResult error (e.g. { ok: false, error: "Invalid cursor" }) instead of
calling findUsageLogsForKeyBatch; keep existing limit trimming but do not pass
malformed cursor values through to findUsageLogsForKeyBatch.
src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx (1)

115-139: ⚠️ Potential issue | 🟠 Major

每次自动刷新都会重新拉取所有已缓存的分页数据。

当前已移除了 maxPages 限制,但 refetchInterval 仍然启用。TanStack Query 在无限查询上触发周期性刷新时,会自动重新拉取所有当前缓存的页面(以维护数据一致性,避免光标陈旧导致重复或缺失)。随着用户向下滚动加载更多页面,每次自动刷新的成本会线性增长,最终导致性能问题。建议限制轮询仅作用于首页,或在启用轮询时使用 refetchPage 选项仅刷新第一页,或显式设置 maxPages 来限制缓存大小。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/dashboard/logs/_components/virtualized-logs-table.tsx
around lines 115 - 139, The infinite query currently re-fetches all cached pages
on each auto-refresh, causing linear cost as more pages load; update the
useInfiniteQuery call to limit polling to only the first page when shouldPoll is
true (or set a max cached pages). Concretely, keep the existing
queryFn/getNextPageParam but add the refetchPage option like refetchPage: (page,
index) => index === 0 when shouldPoll is enabled (or alternatively implement
logic to stop polling after N pages by enforcing a maxPages policy), ensuring
autoRefreshIntervalMs/refetchInterval still controls timing but only triggers a
refetch of the first page instead of all pages; reference useInfiniteQuery,
refetchInterval, refetchPage, shouldPoll, and autoRefreshIntervalMs in your
change.
🧹 Nitpick comments (2)
src/app/[locale]/my-usage/_components/usage-logs-table.tsx (1)

89-97: 没有依赖数组的 useEffect 可能导致不必要的执行

Line 93-97 的 useEffect 没有依赖数组,会在每次渲染时执行。虽然通过 ref 比较 (previousResetKeyRef.current === resolvedResetKey) 进行了门控,但这种模式可能在高频渲染场景下产生额外开销。

建议添加 resolvedResetKey 作为依赖项,使 effect 仅在 key 变化时触发:

♻️ 建议的优化
   useEffect(() => {
     if (previousResetKeyRef.current === resolvedResetKey) return;
     previousResetKeyRef.current = resolvedResetKey;
     resetScrollPosition();
-  });
+  }, [resolvedResetKey, resetScrollPosition]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/my-usage/_components/usage-logs-table.tsx around lines 89 -
97, The useEffect that checks previousResetKeyRef.current against
resolvedResetKey and calls resetScrollPosition runs on every render because it
lacks a dependency array; update the effect in usage-logs-table.tsx to include
resolvedResetKey (and any stable functions like resetScrollPosition if not
stable) in its dependency array so it only runs when resolvedResetKey changes,
e.g. change the effect that references previousResetKeyRef, resolvedResetKey,
and resetScrollPosition to useEffect(..., [resolvedResetKey]) and preserve the
existing ref-based guard.
src/app/[locale]/my-usage/_components/usage-logs-section.test.tsx (1)

75-108: 测试覆盖率可能不足

当前测试仅验证了 useInfiniteQuery 被调用且 getMyUsageLogs 未被调用。根据编码指南,新功能需要至少 80% 的单元测试覆盖率。

建议考虑补充以下测试场景:

  • 筛选条件变化时的行为(日期范围、模型、端点筛选)
  • 错误状态处理
  • 加载更多数据的交互
  • resetScrollKey 变化时的滚动重置行为
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/my-usage/_components/usage-logs-section.test.tsx around
lines 75 - 108, Add more unit tests to raise coverage for UsageLogsSection:
extend the existing test suite that mocks
useInfiniteQuery/getMyUsageLogs/getMyAvailableModels/getMyAvailableEndpoints and
add tests that (1) simulate changing filter props/controls (date range, model
and endpoint filters) and assert the component calls useInfiniteQuery with
updated params and renders filtered state, (2) mock useInfiniteQuery returning
isError/error and assert error UI is shown, (3) mock pages with more data and
simulate the "load more" interaction to ensure fetchNextPage is called and new
logs render, and (4) change the resetScrollKey prop (or trigger the
resetScrollKey behavior) and assert the scroll/visible list resets; reuse the
same mock setup and the UsageLogsSection render/unmount pattern to keep tests
focused and deterministic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/`[locale]/my-usage/_components/usage-logs-section.tsx:
- Around line 85-109: The infinite query is accumulating unlimited pages while
auto-refresh is enabled, causing memory/performance issues; update the
useInfiniteQuery setup (queryKey ["my-usage-logs-batch", appliedFilters],
useInfiniteQuery call, getMyUsageLogsBatch, BATCH_SIZE, getNextPageParam) to (1)
enforce a maxPages constant and trim cached pages in an onSuccess handler (use
queryClient.setQueryData to slice pages to the newest maxPages) and (2) stop
global refetching of every cached page by replacing the current refetchInterval
logic with a targeted refresh: when autoRefreshSeconds is set and
!isBrowsingHistory, run a periodic task (useEffect + setInterval) that calls
queryClient.fetchQuery for the same queryKey supplying cursor undefined (to
reload only the first page) and clear the interval on cleanup; this combination
limits cached pages and only refreshes the first-screen data during polling.

In `@src/app/api/actions/`[...route]/route.ts:
- Around line 1096-1097: The OpenAPI fields summary and description in the route
registration are hardcoded; replace them with i18n lookups (use the project's
i18n helper) instead of literal strings so the text is localized for
zh-CN/zh-TW/en/ja/ru. Locate the summary and description assignments in
src/app/api/actions/[...route]/route.ts and swap the literals for calls to the
i18n API (e.g., i18n.t or the project's translation function) using descriptive
keys like "api.actions.batchGetMyUsage.summary" and
"api.actions.batchGetMyUsage.description", and add corresponding entries to the
i18n resource files for all supported languages. Ensure the keys are
exported/used consistently where OpenAPI metadata is built (summary and
description fields) so Swagger/Scalar shows translated text.
- Around line 1043-1046: You removed the public action name
my-usage/getMyUsageLogs when adding my-usage/getMyUsageLogsBatch; restore
backwards compatibility by registering an alias route named
"my-usage/getMyUsageLogs" that delegates to the existing handler
(getMyUsageLogsBatchHandler) or a thin wrapper that calls
myUsageActions.getMyUsageLogsBatch and emits a deprecation warning; update the
createActionRoute usages (the const tuple with getMyUsageLogsBatchRoute and
getMyUsageLogsBatchHandler) to also create or export the legacy route/handler
alias so existing clients continue to work while you run a deprecation window.

---

Outside diff comments:
In `@src/actions/my-usage.ts`:
- Around line 521-548: In getMyUsageLogsBatch the incoming filters.cursor is
implicitly trusted before calling findUsageLogsForKeyBatch which can cause
server errors when cursor.createdAt is an invalid date string or cursor.id is
not an integer; validate the cursor at the action boundary: if filters.cursor is
present ensure cursor.createdAt parses to a valid Date/ISO timestamptz and
cursor.id is a positive integer (or null/undefined if not provided), and if
validation fails return a controlled ActionResult error (e.g. { ok: false,
error: "Invalid cursor" }) instead of calling findUsageLogsForKeyBatch; keep
existing limit trimming but do not pass malformed cursor values through to
findUsageLogsForKeyBatch.

In `@src/app/`[locale]/dashboard/logs/_components/virtualized-logs-table.tsx:
- Around line 115-139: The infinite query currently re-fetches all cached pages
on each auto-refresh, causing linear cost as more pages load; update the
useInfiniteQuery call to limit polling to only the first page when shouldPoll is
true (or set a max cached pages). Concretely, keep the existing
queryFn/getNextPageParam but add the refetchPage option like refetchPage: (page,
index) => index === 0 when shouldPoll is enabled (or alternatively implement
logic to stop polling after N pages by enforcing a maxPages policy), ensuring
autoRefreshIntervalMs/refetchInterval still controls timing but only triggers a
refetch of the first page instead of all pages; reference useInfiniteQuery,
refetchInterval, refetchPage, shouldPoll, and autoRefreshIntervalMs in your
change.

---

Nitpick comments:
In `@src/app/`[locale]/my-usage/_components/usage-logs-section.test.tsx:
- Around line 75-108: Add more unit tests to raise coverage for
UsageLogsSection: extend the existing test suite that mocks
useInfiniteQuery/getMyUsageLogs/getMyAvailableModels/getMyAvailableEndpoints and
add tests that (1) simulate changing filter props/controls (date range, model
and endpoint filters) and assert the component calls useInfiniteQuery with
updated params and renders filtered state, (2) mock useInfiniteQuery returning
isError/error and assert error UI is shown, (3) mock pages with more data and
simulate the "load more" interaction to ensure fetchNextPage is called and new
logs render, and (4) change the resetScrollKey prop (or trigger the
resetScrollKey behavior) and assert the scroll/visible list resets; reuse the
same mock setup and the UsageLogsSection render/unmount pattern to keep tests
focused and deterministic.

In `@src/app/`[locale]/my-usage/_components/usage-logs-table.tsx:
- Around line 89-97: The useEffect that checks previousResetKeyRef.current
against resolvedResetKey and calls resetScrollPosition runs on every render
because it lacks a dependency array; update the effect in usage-logs-table.tsx
to include resolvedResetKey (and any stable functions like resetScrollPosition
if not stable) in its dependency array so it only runs when resolvedResetKey
changes, e.g. change the effect that references previousResetKeyRef,
resolvedResetKey, and resetScrollPosition to useEffect(..., [resolvedResetKey])
and preserve the existing ref-based guard.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 72e09b51-a088-463c-a8d0-4c7bb9b2efd8

📥 Commits

Reviewing files that changed from the base of the PR and between b8b0f9d and e2bedea.

📒 Files selected for processing (13)
  • src/actions/my-usage.ts
  • src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.test.tsx
  • src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx
  • src/app/[locale]/my-usage/_components/usage-logs-section.test.tsx
  • src/app/[locale]/my-usage/_components/usage-logs-section.tsx
  • src/app/[locale]/my-usage/_components/usage-logs-table.tsx
  • src/app/[locale]/my-usage/page.tsx
  • src/app/api/actions/[...route]/route.ts
  • src/hooks/use-virtualized-infinite-list.ts
  • src/repository/usage-logs.ts
  • tests/api/api-actions-integrity.test.ts
  • tests/api/my-usage-readonly.test.ts
  • tests/unit/actions/my-usage-date-range-dst.test.ts

Copy link
Copy Markdown
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

This PR migrates my-usage logs from page-based to cursor-based pagination with TanStack Virtual infinite scrolling, and extracts a reusable useVirtualizedInfiniteList hook. The implementation is solid with proper error handling on the main data fetching path.

PR Size: XL

  • Lines changed: 1457 (828 additions, 629 deletions)
  • Files changed: 13

Issues Found

Category Critical High Medium Low
Logic/Bugs 0 0 0 0
Security 0 0 0 0
Error Handling 0 0 0 0
Types 0 0 0 0
Comments/Docs 0 0 0 0
Tests 0 0 0 0
Simplification 0 0 0 0

Review Coverage

  • Logic and correctness - Clean
  • Security (OWASP Top 10) - Clean (cursor values parameterized via Drizzle sql template tag)
  • Error handling - Clean (TanStack Query handles errors with isError/error state)
  • Type safety - Clean (proper TypeScript interfaces for batch results and cursors)
  • Documentation accuracy - Clean
  • Test coverage - Good (new tests for hook usage, regression test for maxPages, updated DST tests)
  • Code clarity - Good (extracted shared hook reduces duplication)

Notes

  • The cursor-based pagination SQL uses Drizzle's sql template tag for proper parameterization: sql\(...createdAt, ...id) < (${cursor.createdAt}::timestamptz, ${cursor.id})``
  • The maxPages removal in dashboard logs is validated by a regression test
  • Helper queries (getMyAvailableModels/getMyAvailableEndpoints) silently fail on error, but this is pre-existing behavior and non-critical (dropdowns just show loading state)

Automated review by Claude AI

Copy link
Copy Markdown
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

This PR replaces page-based my-usage logs with cursor-based batching + virtualized infinite scrolling and removes cached-page capping on dashboard logs.
Two issues need attention: errors can become invisible after initial data loads, and the new cursor schema allows non-integer ids.

PR Size: XL

  • Lines changed: 1457
  • Files changed: 13
  • For XL PRs: consider splitting into (1) backend cursor pagination + action route changes, (2) UI virtualization + UX changes, (3) test/regression updates.

Issues Found

Category Critical High Medium Low
Logic/Bugs 0 0 0 0
Security 0 0 0 0
Error Handling 0 1 0 0
Types 0 0 1 0
Comments/Docs 0 0 0 0
Tests 0 0 0 0
Simplification 0 0 0 0

Critical Issues (Must Fix)

  • None

High Priority Issues (Should Fix)

  • [HIGH] [ERROR-NO-USER-FEEDBACK] UsageLogsTable only renders errorMessage when logs.length === 0, so failures after initial load can be silent. (Confidence: 88)
  • [MEDIUM] [TYPE-MISSING-VALIDATION] Cursor id in getMyUsageLogsBatch schema uses z.number() instead of z.number().int(). (Confidence: 85)

Review Coverage

  • Logic and correctness
  • Security (OWASP Top 10)
  • Error handling
  • Type safety
  • Documentation accuracy
  • Test coverage
  • Code clarity

Automated review by Codex AI

Copy link
Copy Markdown
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Identified open PR #959 (“fix: stabilize usage log virtual scrolling”) and labeled it size/XL (13 files, 1457 lines changed).
  • Posted 2 inline review comments on diff lines:
    • src/app/[locale]/my-usage/_components/usage-logs-table.tsx:124[HIGH] [ERROR-NO-USER-FEEDBACK] errorMessage is only rendered when logs.length === 0, so refetch/fetch-more failures can become invisible once any data is loaded.
    • src/app/api/actions/[...route]/route.ts:1060[MEDIUM] [TYPE-MISSING-VALIDATION] cursor id is z.number() (allows floats); suggested z.number().int() for cursor/nextCursor ids.
  • Submitted the required “Code Review Summary” via gh pr review --comment on PR #959.

@github-actions
Copy link
Copy Markdown
Contributor

🧪 测试结果

测试类型 状态
代码质量
单元测试
集成测试
API 测试

总体结果: ✅ 所有测试通过

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx (1)

123-147: ⚠️ Potential issue | 🟠 Major

无界 infinite query 上的定时轮询会导致恢复时重抓成本爆炸

虽然代码已通过 !isHistoryBrowsing 防止在用户浏览历史时轮询,但当用户从历史页面回到最新时(showScrollToTop 变为 false),shouldPoll 恢复为 truerefetchInterval 会启动。此时 TanStack Query 会顺序重抓所有已缓存页面

由于移除了 maxPages 限制,已加载页数可无界增长。用户深滚加载大量历史页后再回到最新,Line 142-146 会按累积的页数进行全量重抓,网络和渲染成本会线性放大。

建议采用以下方案之一:

  • 设置 maxPages: N(如 3-5)限制缓存页数
  • 拆分为独立查询来处理最新数据刷新,保持历史页单独管理
  • 在轮询恢复前使用 queryClient.setQueryData 手动裁剪为仅保留第一页
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/dashboard/logs/_components/virtualized-logs-table.tsx
around lines 123 - 147, The infinite query can re-fetch every cached page when
polling resumes; fix it by limiting cached pages before polling restarts: use
useQueryClient() and in the refetchInterval callback (inside the
useInfiniteQuery for queryKey ["usage-logs-batch", filters]) detect when
shouldPoll would return an interval and call queryClient.setQueryData for that
queryKey to trim data.pages to the most recent N (e.g., 3–5) and adjust
data.pageParams accordingly, then return autoRefreshIntervalMs; alternatively
implement a separate queryKey for "latest" vs historical pages, but the
immediate fix is to trim the cached pages in refetchInterval to bound re-fetch
cost.
🧹 Nitpick comments (2)
tests/unit/repository/usage-logs-sessionid-filter.test.ts (1)

115-186: 建议抽取测试行构造器,减少重复字段带来的维护成本。

两段 case 的 mock 行结构高度重复;后续字段变更时需要多处同步,容易漏改。建议抽一个最小工厂函数。

可选重构示例(去重 mock row)
+function makeBatchRow(id: number, createdAt: string) {
+  return {
+    id,
+    createdAt: new Date(createdAt),
+    createdAtRaw: null,
+    sessionId: null,
+    requestSequence: null,
+    userName: "u",
+    keyName: "k",
+    providerName: "p",
+    model: "m",
+    originalModel: "m",
+    endpoint: "/v1/messages",
+    statusCode: 200,
+    inputTokens: 1,
+    outputTokens: 1,
+    cacheCreationInputTokens: 0,
+    cacheReadInputTokens: 0,
+    cacheCreation5mInputTokens: 0,
+    cacheCreation1hInputTokens: 0,
+    cacheTtlApplied: null,
+    costUsd: "0.01",
+    costMultiplier: null,
+    durationMs: 10,
+    ttfbMs: 5,
+    errorMessage: null,
+    providerChain: null,
+    blockedBy: null,
+    blockedReason: null,
+    userAgent: null,
+    messagesCount: null,
+    context1mApplied: null,
+    swapCacheTtlApplied: null,
+    specialSettings: null,
+  };
+}
+
-      createThenableQuery([
-        { ...大量重复字段..., id: 101, createdAt: new Date("2026-03-21T00:00:00Z"), createdAtRaw: null },
-        { ...大量重复字段..., id: 100, createdAt: new Date("2026-03-20T00:00:00Z"), createdAtRaw: null },
-      ])
+      createThenableQuery([
+        makeBatchRow(101, "2026-03-21T00:00:00Z"),
+        makeBatchRow(100, "2026-03-20T00:00:00Z"),
+      ])

Also applies to: 207-248

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/repository/usage-logs-sessionid-filter.test.ts` around lines 115 -
186, The two mock rows in the selectMock for createThenableQuery are nearly
identical and should be generated by a small factory to reduce duplication; add
a helper function (e.g., makeUsageLogRow or usageLogRowFactory) adjacent to the
test that returns a base row object (populating id, createdAt, sessionId,
userName, keyName, providerName, model, endpoint, statusCode, tokens, costUsd,
durationMs, etc.), then call that factory and override only the differing fields
when building the array passed to createThenableQuery in the selectMock; update
other similar mocks (the block referenced by selectMock and createThenableQuery
and the repeated block pointed out in the comment) to use the same factory.
src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx (1)

152-190: 修正滚动重置键的哈希语义,避免属性顺序导致的误触发

当前代码用 JSON.stringify(filters) 生成滚动重置键,但 TanStack Query 的对象 queryKey 使用的是自定义哈希(对键排序后序列化),与属性顺序无关。如果 filters 内容相同但属性插入顺序不同,标准 JSON.stringify 会产生不同的字符串,从而误触发 resetScrollPosition(),导致用户突然被拉回页面顶部。建议用 TanStack Query 的哈希函数生成 reset key,或对 filters 对象进行规范化处理后再序列化。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/dashboard/logs/_components/virtualized-logs-table.tsx
around lines 152 - 190, The reset-key should use a stable, order-insensitive
hash instead of JSON.stringify to avoid spurious resets; replace the
JSON.stringify(filters) used for filtersResetKey with a canonical hash (e.g.,
TanStack Query's stableHash/stableStringify) or by normalizing the filters (sort
object keys recursively) before serializing, keeping the rest of the flow
(previousFiltersResetKeyRef, handleFiltersReset, resetScrollPosition, and
useVirtualizedInfiniteList) unchanged so identical filter contents produce the
same key regardless of property order.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/`[locale]/my-usage/_components/usage-logs-table.tsx:
- Around line 218-223: The clickable <span> used to copy model names is not
focusable or keyboard-accessible; update the usage in usage-logs-table.tsx to
make it accessible: either replace the <span> with a native <button> (preserving
className and calling handleCopyModel(log.model!)) or keep the element but add
role="button", tabIndex={0}, an onKeyDown handler that triggers handleCopyModel
on Enter/Space, and an appropriate aria-label. Ensure you reference and update
the interactive element that calls handleCopyModel and keep visual styling
identical.
- Around line 128-129: The current UI only shows errorMessage when logs.length
=== 0, which hides subsequent-page load errors; update the rendering logic in
usage-logs-table.tsx (affecting the earlier error-check and the loader row
handling at the other occurrence) so that when errorMessage is set and
logs.length > 0 you render the error inline in the table status/loader row
(instead of only showing the spinner) and expose a retry affordance that calls
the existing onLoadMore handler; specifically, detect errorMessage alongside
logs in the loader/StatusRow code paths and replace/augment the spinner with a
small error text and a retry button that invokes onLoadMore.
- Around line 150-165: The issue is that the inner scroll container (the div
with ref={parentRef} and onScroll={handleScroll}) uses overflow-auto which
allows independent horizontal scrolling and causes header/body desync; change
that container to only scroll vertically (e.g., overflow-y-auto and
overflow-x-hidden or overflow-x-visible) so the outer wrapper (the element with
overflow-x-auto and min-w) remains responsible for horizontal scrolling; update
the class on the element that currently reads "h-[520px] overflow-auto" to use
vertical-only scrolling and keep the ref and handleScroll as-is to preserve
scroll handling and sticky header alignment.

---

Outside diff comments:
In `@src/app/`[locale]/dashboard/logs/_components/virtualized-logs-table.tsx:
- Around line 123-147: The infinite query can re-fetch every cached page when
polling resumes; fix it by limiting cached pages before polling restarts: use
useQueryClient() and in the refetchInterval callback (inside the
useInfiniteQuery for queryKey ["usage-logs-batch", filters]) detect when
shouldPoll would return an interval and call queryClient.setQueryData for that
queryKey to trim data.pages to the most recent N (e.g., 3–5) and adjust
data.pageParams accordingly, then return autoRefreshIntervalMs; alternatively
implement a separate queryKey for "latest" vs historical pages, but the
immediate fix is to trim the cached pages in refetchInterval to bound re-fetch
cost.

---

Nitpick comments:
In `@src/app/`[locale]/dashboard/logs/_components/virtualized-logs-table.tsx:
- Around line 152-190: The reset-key should use a stable, order-insensitive hash
instead of JSON.stringify to avoid spurious resets; replace the
JSON.stringify(filters) used for filtersResetKey with a canonical hash (e.g.,
TanStack Query's stableHash/stableStringify) or by normalizing the filters (sort
object keys recursively) before serializing, keeping the rest of the flow
(previousFiltersResetKeyRef, handleFiltersReset, resetScrollPosition, and
useVirtualizedInfiniteList) unchanged so identical filter contents produce the
same key regardless of property order.

In `@tests/unit/repository/usage-logs-sessionid-filter.test.ts`:
- Around line 115-186: The two mock rows in the selectMock for
createThenableQuery are nearly identical and should be generated by a small
factory to reduce duplication; add a helper function (e.g., makeUsageLogRow or
usageLogRowFactory) adjacent to the test that returns a base row object
(populating id, createdAt, sessionId, userName, keyName, providerName, model,
endpoint, statusCode, tokens, costUsd, durationMs, etc.), then call that factory
and override only the differing fields when building the array passed to
createThenableQuery in the selectMock; update other similar mocks (the block
referenced by selectMock and createThenableQuery and the repeated block pointed
out in the comment) to use the same factory.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5e2db829-162d-4f50-a2b1-3b4a063de1a3

📥 Commits

Reviewing files that changed from the base of the PR and between e2bedea and 9034302.

📒 Files selected for processing (4)
  • src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx
  • src/app/[locale]/my-usage/_components/usage-logs-table.tsx
  • src/repository/usage-logs.ts
  • tests/unit/repository/usage-logs-sessionid-filter.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/repository/usage-logs.ts

@github-actions
Copy link
Copy Markdown
Contributor

🧪 测试结果

测试类型 状态
代码质量
单元测试
集成测试
API 测试

总体结果: ✅ 所有测试通过

@github-actions
Copy link
Copy Markdown
Contributor

🧪 测试结果

测试类型 状态
代码质量
单元测试
集成测试
API 测试

总体结果: ✅ 所有测试通过

1 similar comment
@github-actions
Copy link
Copy Markdown
Contributor

🧪 测试结果

测试类型 状态
代码质量
单元测试
集成测试
API 测试

总体结果: ✅ 所有测试通过

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
src/app/[locale]/my-usage/_components/usage-logs-table.tsx (2)

217-225: ⚠️ Potential issue | 🟡 Minor

把模型名复制控件改成原生按钮。

Line 220-225 现在只有鼠标点击路径,键盘和辅助技术用户没法触发复制。这个交互本身就是按钮语义,用 button type="button" 最稳妥。

可行修复
-                          {log.model ? (
-                            <span
-                              className="cursor-pointer hover:underline truncate"
-                              onClick={() => handleCopyModel(log.model!)}
-                            >
-                              {log.model}
-                            </span>
+                          {log.model ? (
+                            <button
+                              type="button"
+                              className="max-w-full cursor-pointer truncate border-0 bg-transparent p-0 text-left hover:underline"
+                              onClick={() => handleCopyModel(log.model!)}
+                            >
+                              {log.model}
+                            </button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/my-usage/_components/usage-logs-table.tsx around lines 217
- 225, The model name copy control is currently a clickable span which is not
keyboard/AT accessible; replace the span used to display the model (the element
rendering {log.model} with onClick={() => handleCopyModel(log.model!)}) with a
semantic <button type="button"> that preserves the same className, onClick
handler (handleCopyModel), and visual styles (cursor, hover:underline, truncate)
and add an appropriate aria-label (e.g., `Copy model name`) so keyboard and
assistive-technology users can trigger the copy action.

152-166: ⚠️ Potential issue | 🟠 Major

让外层容器独占横向滚动。

Line 166 这里继续使用 overflow-auto,会让表体拿到独立横向滚动条;窄屏时表头还留在外层容器里,列会失去同步。内层容器应只负责纵向滚动。

可行修复
-          <div ref={parentRef} className="h-[520px] overflow-auto" onScroll={handleScroll}>
+          <div
+            ref={parentRef}
+            className="h-[520px] overflow-y-auto overflow-x-hidden"
+            onScroll={handleScroll}
+          >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/my-usage/_components/usage-logs-table.tsx around lines 152
- 166, The inner scroll container (the element with ref={parentRef} and
onScroll={handleScroll}) is using "overflow-auto", which allows it to create its
own horizontal scrollbar and desync the header; change its class to only allow
vertical scrolling (e.g., replace "overflow-auto" with "overflow-y-auto" and
ensure no "overflow-x-*" is present) so the outer wrapper (the div with
"overflow-x-auto") remains the single horizontal scroller and the header stays
aligned with the body.
🧹 Nitpick comments (1)
src/app/[locale]/my-usage/_components/usage-logs-table.test.tsx (1)

64-64: 这里不要再用相对路径导入 src 内模块。

Line 64 的 ./usage-logs-table 绕开了仓库统一的 @/ 别名约定,后续移动目录时也更容易漏改。直接走别名即可。

可行修复
-import { UsageLogsTable } from "./usage-logs-table";
+import { UsageLogsTable } from "@/app/[locale]/my-usage/_components/usage-logs-table";

As per coding guidelines, "Use path alias @/ to reference files in ./src/ directory".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/my-usage/_components/usage-logs-table.test.tsx at line 64,
当前文件使用相对路径导入 UsageLogsTable ("./usage-logs-table"),绕开了仓库约定的 "@/“ 别名;请将该 import
改为使用项目别名(使用 "@/..." 形式指向同一模块),确保导入语句仍引用同一符号 UsageLogsTable,并同时检查该测试文件中其它相对 src
内的导入也统一替换为 "@/..." 别名以避免未来移动或重构出错。
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/app/`[locale]/my-usage/_components/usage-logs-table.tsx:
- Around line 217-225: The model name copy control is currently a clickable span
which is not keyboard/AT accessible; replace the span used to display the model
(the element rendering {log.model} with onClick={() =>
handleCopyModel(log.model!)}) with a semantic <button type="button"> that
preserves the same className, onClick handler (handleCopyModel), and visual
styles (cursor, hover:underline, truncate) and add an appropriate aria-label
(e.g., `Copy model name`) so keyboard and assistive-technology users can trigger
the copy action.
- Around line 152-166: The inner scroll container (the element with
ref={parentRef} and onScroll={handleScroll}) is using "overflow-auto", which
allows it to create its own horizontal scrollbar and desync the header; change
its class to only allow vertical scrolling (e.g., replace "overflow-auto" with
"overflow-y-auto" and ensure no "overflow-x-*" is present) so the outer wrapper
(the div with "overflow-x-auto") remains the single horizontal scroller and the
header stays aligned with the body.

---

Nitpick comments:
In `@src/app/`[locale]/my-usage/_components/usage-logs-table.test.tsx:
- Line 64: 当前文件使用相对路径导入 UsageLogsTable ("./usage-logs-table"),绕开了仓库约定的 "@/“
别名;请将该 import 改为使用项目别名(使用 "@/..." 形式指向同一模块),确保导入语句仍引用同一符号
UsageLogsTable,并同时检查该测试文件中其它相对 src 内的导入也统一替换为 "@/..." 别名以避免未来移动或重构出错。

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 439f4dca-7f51-4b72-88f2-d64979d97dfc

📥 Commits

Reviewing files that changed from the base of the PR and between 9034302 and e3746bc.

📒 Files selected for processing (4)
  • src/app/[locale]/my-usage/_components/usage-logs-table.test.tsx
  • src/app/[locale]/my-usage/_components/usage-logs-table.tsx
  • src/app/api/actions/[...route]/route.ts
  • tests/api/api-actions-integrity.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/api/api-actions-integrity.test.ts
  • src/app/api/actions/[...route]/route.ts

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/app/[locale]/my-usage/_components/usage-logs-table.tsx (1)

104-108: 清理 effect 的依赖可能导致意外行为。

onHistoryBrowsingChange 回调引用变化时,cleanup 函数会执行并调用 onHistoryBrowsingChange?.(false),这可能导致在组件仍挂载时误报"不在浏览历史"状态。

建议使用 ref 存储回调,或移除依赖项(如果回调始终稳定):

建议修复
+ const onHistoryBrowsingChangeRef = useRef(onHistoryBrowsingChange);
+ onHistoryBrowsingChangeRef.current = onHistoryBrowsingChange;

  useEffect(() => {
    return () => {
-     onHistoryBrowsingChange?.(false);
+     onHistoryBrowsingChangeRef.current?.(false);
    };
- }, [onHistoryBrowsingChange]);
+ }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/my-usage/_components/usage-logs-table.tsx around lines 104
- 108, The effect's cleanup currently depends on onHistoryBrowsingChange so when
that callback's identity changes the cleanup will run and call
onHistoryBrowsingChange?.(false) unexpectedly; fix by keeping a stable ref to
the callback instead of listing it in the dependency array: create a ref (e.g.,
historyChangeRef) updated whenever onHistoryBrowsingChange changes, leave the
effect's dependency array empty (or only include truly stable values), and in
the cleanup call historyChangeRef.current?.(false); alternatively, if
onHistoryBrowsingChange is truly stable, remove it from the deps and document
that assumption.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/app/`[locale]/my-usage/_components/usage-logs-table.tsx:
- Around line 104-108: The effect's cleanup currently depends on
onHistoryBrowsingChange so when that callback's identity changes the cleanup
will run and call onHistoryBrowsingChange?.(false) unexpectedly; fix by keeping
a stable ref to the callback instead of listing it in the dependency array:
create a ref (e.g., historyChangeRef) updated whenever onHistoryBrowsingChange
changes, leave the effect's dependency array empty (or only include truly stable
values), and in the cleanup call historyChangeRef.current?.(false);
alternatively, if onHistoryBrowsingChange is truly stable, remove it from the
deps and document that assumption.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6d71f5cf-627e-4779-82cd-c755ad2b229e

📥 Commits

Reviewing files that changed from the base of the PR and between e3746bc and 79d70b9.

📒 Files selected for processing (2)
  • src/app/[locale]/my-usage/_components/usage-logs-table.test.tsx
  • src/app/[locale]/my-usage/_components/usage-logs-table.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/app/[locale]/my-usage/_components/usage-logs-table.test.tsx

@ding113 ding113 merged commit 650dbc6 into dev Mar 21, 2026
15 checks passed
@github-project-automation github-project-automation bot moved this from Backlog to Done in Claude Code Hub Roadmap Mar 21, 2026
@coderabbitai coderabbitai bot mentioned this pull request Mar 22, 2026
5 tasks
@github-actions github-actions bot mentioned this pull request Mar 22, 2026
@coderabbitai coderabbitai bot mentioned this pull request Mar 23, 2026
10 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:UI bug Something isn't working size/XL Extra Large PR (> 1000 lines)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant