feat: switch /skills and homepage to listPublicPageV4#955
feat: switch /skills and homepage to listPublicPageV4#955magicseth merged 2 commits intoopenclaw:mainfrom
Conversation
Root cause of V4 returning empty in production: `getPage()` ignores the `indexFields` property at runtime and always calls `getIndexFields(table, index, schema)`. Without `schema`, it threw "schema is required" silently. Fixes: - Pass `schema` instead of `indexFields` to `getPage()` - Use `absoluteMaxRows` instead of `targetMaxRows` (ignored when `endIndexKey` is provided) - Remove unused `DIGEST_INDEX_FIELDS` constant Staged release: - Add `/skillsv4` route (same UI as `/skills` but using V4 backend) - Add `/test-v4` debug page for raw V4 API testing - Add `useV4` flag to `useSkillsBrowseModel` hook - Keep `/skills` on V3 until V4 is verified in production Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@sethconvex is attempting to deploy a commit to the Amantus Machina Team on Vercel. A member of the Team first needs to authorize it. |
|
@codex review |
Greptile SummaryThis PR ships two correctness fixes to Key changes:
Confidence Score: 3/5
Prompt To Fix All With AIThis is a comment left during a code review.
Path: src/routes/skills/-useSkillsBrowseModel.ts
Line: 85-87
Comment:
**List reset when `hasMore=true` but `nextCursor=null`**
If the server returns `{ hasMore: true, nextCursor: null }` (an edge case possible when both fetch rounds return no accepted items — e.g., `highlightedOnly=true` with no matches and an unexpectedly empty `indexKeys` array from `getPage`), `listCursor` is set to `null` but `listStatus` is set to `'idle'`, which means `canLoadMore` is `true`.
The next "load more" call invokes `fetchPage(null, generation)`, where `cursor` is `null` (falsy). This triggers the **replace** branch of `setListResults`:
```ts
setListResults((prev) => (cursor ? [...prev, ...result.page] : result.page))
// ^ cursor is null → replaces, not appends
```
The accumulated results are silently reset to page 1, which is incorrect behavior for pagination. A defensive guard would prevent this:
```suggestion
if (generation !== fetchGeneration.current) return
setListResults((prev) => (cursor ? [...prev, ...result.page] : result.page))
const canAdvance = result.hasMore && result.nextCursor != null
setListCursor(canAdvance ? result.nextCursor : null)
setListStatus(canAdvance ? 'idle' : 'done')
```
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/routes/test-v4.tsx
Line: 7-13
Comment:
**Debug page is publicly accessible in production**
The `/test-v4` route has no `beforeLoad` auth guard. Any user who discovers the URL can visit it in production. The page:
- Exposes a user-editable Convex URL field, letting anyone point the debug UI at arbitrary Convex deployments.
- Logs verbose internal query details to the browser console (`[test-v4]` prefixed logs).
- Displays raw JSON API responses, revealing internal field names and data shape.
Since `listPublicPageV4` is a public query, this doesn't expose private data — but it does expose implementation details and is an unpolished surface for end users to stumble on. At minimum, consider redirecting non-admin users, consistent with how `/management` gates access via `isAdmin`:
```ts
beforeLoad: ({ context }) => {
if (!context.isAdmin) throw redirect({ to: '/' })
},
```
Or, add a note in the PR to remove this route once V4 is verified rather than leaving it in indefinitely.
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/routes/test-v4.tsx
Line: 50-51
Comment:
**New `ConvexHttpClient` instantiated on every fetch**
`new ConvexHttpClient(url)` is constructed inside `fetchPage`, so a fresh client is created on every query call. This bypasses any connection pooling or keep-alive the client may provide. For a debug tool this is fine, but if you expect heavy use during testing, consider memoizing the client with `useMemo` keyed on `url`:
```ts
const client = useMemo(() => new ConvexHttpClient(url), [url])
```
And remove it from the `fetchPage` closure.
How can I resolve this? If you propose a fix, please make it concise.Last reviewed commit: b3dd6c1 |
| setListResults((prev) => (cursor ? [...prev, ...result.page] : result.page)) | ||
| setListCursor(result.hasMore ? (result.nextCursor ?? null) : null) | ||
| setListStatus(result.hasMore ? 'idle' : 'done') |
There was a problem hiding this comment.
List reset when hasMore=true but nextCursor=null
If the server returns { hasMore: true, nextCursor: null } (an edge case possible when both fetch rounds return no accepted items — e.g., highlightedOnly=true with no matches and an unexpectedly empty indexKeys array from getPage), listCursor is set to null but listStatus is set to 'idle', which means canLoadMore is true.
The next "load more" call invokes fetchPage(null, generation), where cursor is null (falsy). This triggers the replace branch of setListResults:
setListResults((prev) => (cursor ? [...prev, ...result.page] : result.page))
// ^ cursor is null → replaces, not appendsThe accumulated results are silently reset to page 1, which is incorrect behavior for pagination. A defensive guard would prevent this:
| setListResults((prev) => (cursor ? [...prev, ...result.page] : result.page)) | |
| setListCursor(result.hasMore ? (result.nextCursor ?? null) : null) | |
| setListStatus(result.hasMore ? 'idle' : 'done') | |
| if (generation !== fetchGeneration.current) return | |
| setListResults((prev) => (cursor ? [...prev, ...result.page] : result.page)) | |
| const canAdvance = result.hasMore && result.nextCursor != null | |
| setListCursor(canAdvance ? result.nextCursor : null) | |
| setListStatus(canAdvance ? 'idle' : 'done') |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/routes/skills/-useSkillsBrowseModel.ts
Line: 85-87
Comment:
**List reset when `hasMore=true` but `nextCursor=null`**
If the server returns `{ hasMore: true, nextCursor: null }` (an edge case possible when both fetch rounds return no accepted items — e.g., `highlightedOnly=true` with no matches and an unexpectedly empty `indexKeys` array from `getPage`), `listCursor` is set to `null` but `listStatus` is set to `'idle'`, which means `canLoadMore` is `true`.
The next "load more" call invokes `fetchPage(null, generation)`, where `cursor` is `null` (falsy). This triggers the **replace** branch of `setListResults`:
```ts
setListResults((prev) => (cursor ? [...prev, ...result.page] : result.page))
// ^ cursor is null → replaces, not appends
```
The accumulated results are silently reset to page 1, which is incorrect behavior for pagination. A defensive guard would prevent this:
```suggestion
if (generation !== fetchGeneration.current) return
setListResults((prev) => (cursor ? [...prev, ...result.page] : result.page))
const canAdvance = result.hasMore && result.nextCursor != null
setListCursor(canAdvance ? result.nextCursor : null)
setListStatus(canAdvance ? 'idle' : 'done')
```
How can I resolve this? If you propose a fix, please make it concise.| export const Route = createFileRoute('/test-v4')({ | ||
| component: () => ( | ||
| <ClientOnly fallback={<div style={{ padding: 24 }}>Loading...</div>}> | ||
| <TestV4 /> | ||
| </ClientOnly> | ||
| ), | ||
| }) |
There was a problem hiding this comment.
Debug page is publicly accessible in production
The /test-v4 route has no beforeLoad auth guard. Any user who discovers the URL can visit it in production. The page:
- Exposes a user-editable Convex URL field, letting anyone point the debug UI at arbitrary Convex deployments.
- Logs verbose internal query details to the browser console (
[test-v4]prefixed logs). - Displays raw JSON API responses, revealing internal field names and data shape.
Since listPublicPageV4 is a public query, this doesn't expose private data — but it does expose implementation details and is an unpolished surface for end users to stumble on. At minimum, consider redirecting non-admin users, consistent with how /management gates access via isAdmin:
beforeLoad: ({ context }) => {
if (!context.isAdmin) throw redirect({ to: '/' })
},Or, add a note in the PR to remove this route once V4 is verified rather than leaving it in indefinitely.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/routes/test-v4.tsx
Line: 7-13
Comment:
**Debug page is publicly accessible in production**
The `/test-v4` route has no `beforeLoad` auth guard. Any user who discovers the URL can visit it in production. The page:
- Exposes a user-editable Convex URL field, letting anyone point the debug UI at arbitrary Convex deployments.
- Logs verbose internal query details to the browser console (`[test-v4]` prefixed logs).
- Displays raw JSON API responses, revealing internal field names and data shape.
Since `listPublicPageV4` is a public query, this doesn't expose private data — but it does expose implementation details and is an unpolished surface for end users to stumble on. At minimum, consider redirecting non-admin users, consistent with how `/management` gates access via `isAdmin`:
```ts
beforeLoad: ({ context }) => {
if (!context.isAdmin) throw redirect({ to: '/' })
},
```
Or, add a note in the PR to remove this route once V4 is verified rather than leaving it in indefinitely.
How can I resolve this? If you propose a fix, please make it concise.| const client = new ConvexHttpClient(url) | ||
| const result = await client.query(api.skills.listPublicPageV4, queryArgs) |
There was a problem hiding this comment.
New ConvexHttpClient instantiated on every fetch
new ConvexHttpClient(url) is constructed inside fetchPage, so a fresh client is created on every query call. This bypasses any connection pooling or keep-alive the client may provide. For a debug tool this is fine, but if you expect heavy use during testing, consider memoizing the client with useMemo keyed on url:
const client = useMemo(() => new ConvexHttpClient(url), [url])And remove it from the fetchPage closure.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/routes/test-v4.tsx
Line: 50-51
Comment:
**New `ConvexHttpClient` instantiated on every fetch**
`new ConvexHttpClient(url)` is constructed inside `fetchPage`, so a fresh client is created on every query call. This bypasses any connection pooling or keep-alive the client may provide. For a debug tool this is fine, but if you expect heavy use during testing, consider memoizing the client with `useMemo` keyed on `url`:
```ts
const client = useMemo(() => new ConvexHttpClient(url), [url])
```
And remove it from the `fetchPage` closure.
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
When V4 returns hasMore=true but nextCursor=null, the next load-more call would pass cursor=null, triggering the replace branch instead of append. Treat this edge case as 'done' to prevent silent list reset. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Codex Review: Didn't find any major issues. 🎉 ℹ️ About Codex in GitHubCodex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback". |
Summary
getPage()ignoresindexFieldsat runtime — always needsschema. Fixed by passingschemadirectly.targetMaxRowsignored whenendIndexKeyprovided — switched toabsoluteMaxRows./skillsbrowse page and homepage now uselistPublicPageV4(cacheable deterministic cursors)/skillsv4staging route verified in production, then removedhasMore=truewithnextCursor=nulledge caseTest plan
/skills— verify skills load, pagination, sort/filter/search all work🤖 Generated with Claude Code