feat(admin): pagination, search debounce, reset-stuck UI, error boundaries#2386
Conversation
… boundaries
- Add 50-item pagination (URL ?page=N) to catalog, packs, users pages
- Debounce search input 300ms; resets page param on new search
- POST /api/admin/analytics/catalog/etl/reset-stuck endpoint replaces raw SQL script
- Reset Stuck Jobs button in ETL card with spinner + result feedback
- Root + dashboard error boundaries
- Delete packages/api/scripts/reset-stuck-etl-jobs.sql
- Fix queryKeys.admin.* to accept { q, page, limit } params
|
Warning Rate limit exceeded
You’ve run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
Warning
|
| Layer / File(s) | Summary |
|---|---|
Query Key Architecture apps/admin/lib/queryKeys.ts |
queryKeys.admin and queryKeys.catalogAnalytics.etl refactored into factory objects with all() anchors and .list(params?) builders to include { q, page, limit } in keys. |
API Response & Client Types packages/api/src/routes/admin/index.ts, apps/admin/lib/api.ts |
Admin list responses extended (users: avatarUrl, updatedAt; packs: isAIGenerated,tags,image,updatedAt; catalog items: many new attributes) and client types updated; resetStuckEtlJobs() client added. |
ETL Reset API packages/api/src/routes/admin/analytics/catalog.ts |
Add POST /catalog/etl/reset-stuck route that updates running ETL jobs older than 3 hours to failed, sets completedAt, and returns reset count and IDs. |
nuqs Integration & Providers apps/admin/package.json, apps/admin/app/layout.tsx |
Add nuqs dependency and wrap app with NuqsAdapter outside QueryProvider to enable nuqs query-state. |
Paginated Search Hook apps/admin/hooks/use-paginated-search.ts |
New usePaginatedSearch() hook reading/writing q and page via nuqs, normalizing page and resetting page when search changes. |
SearchInput apps/admin/components/search-input.tsx |
Refactor SearchInput to use useQueryState(parseAsString) with optional onSearch, removing Suspense/router transition logic. |
Users Page Pagination apps/admin/app/dashboard/users/page.tsx |
Switch to usePaginatedSearch, compute offset = page * PAGE_SIZE, use queryKeys.admin.users.list({ q, page }) with getUsers({ q, limit, offset }), add Prev/Next controls and range footer, include RawObjectDialog in actions, invalidate queryKeys.admin.users.all() on delete. |
Packs Page Pagination apps/admin/app/dashboard/packs/page.tsx |
Switch to usePaginatedSearch, PAGE_SIZE=50, use paginated query key and getPacks({ q, limit, offset }), add Prev/Next and range footer, include RawObjectDialog, invalidate queryKeys.admin.packs.all() on delete. |
Catalog Page Pagination apps/admin/app/dashboard/catalog/page.tsx |
Switch to usePaginatedSearch, PAGE_SIZE=50, use queryKeys.admin.catalog.list({ q, page }), pass limit/offset to getCatalogItems, extend row UI (price/currency formatting, status/rating, productUrl), include RawObjectDialog, and add bottom range bar with Prev/Next; invalidate queryKeys.admin.catalog.all() on delete. |
Catalog Analytics: ETL Reset (frontend) apps/admin/lib/api.ts, apps/admin/hooks/use-catalog-analytics.ts, apps/admin/components/analytics/catalog-analytics.tsx |
Add resetStuckEtlJobs() client, update catalog analytics query keys to callable forms, wire useMutation with "Reset Stuck" button and result/error feedback, and invalidate ETL analytics query on success; ETL table gains Invalid/started/completed columns and RawObjectDialog actions. |
Row Actions & Utilities apps/admin/components/raw-object-dialog.tsx, apps/admin/components/edit-catalog-dialog.tsx |
Add RawObjectDialog for JSON inspection and update edit-catalog dialog to invalidate queryKeys.admin.catalog.all() on update. |
Error Boundaries & Global Error UIs apps/admin/app/dashboard/layout.tsx, apps/admin/components/error-fallback.tsx, apps/admin/app/error.tsx, apps/admin/app/global-error.tsx, apps/admin/app/dashboard/error.tsx |
Add client-side error fallback components, wrap dashboard in ErrorBoundary using ErrorFallback, and add global/dashboard error pages that log errors and provide retry. |
Cleanup / Misc packages/api/scripts/reset-stuck-etl-jobs.sql, .gitignore, apps/admin/package.json |
Remove obsolete SQL script (logic moved to API), add .worktrees ignore entry, and add nuqs dependency. |
Estimated code review effort
🎯 4 (Complex) | ⏱️ ~45 minutes
Possibly related PRs
- PackRat-AI/PackRat#2316: Related refactor of admin catalog React Query key usage and invalidation.
- PackRat-AI/PackRat#2268: Related API/admin route surface changes.
- PackRat-AI/PackRat#2307: Related catalog listing query/invalidation adjustments.
Suggested labels
web
Suggested reviewers
- mikib0
Poem
🐰 I hop through queries, nuqs in paw,
Pages flip neatly without a flaw.
ETL wakes from its long, stuck night,
Error fences catch the fright—
The admin dashboard hums just right!
🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
| Check name | Status | Explanation | Resolution |
|---|---|---|---|
| Docstring Coverage | Docstring coverage is 3.03% which is insufficient. The required threshold is 80.00%. | Write docstrings for the functions missing them to satisfy the coverage threshold. |
✅ Passed checks (4 passed)
| Check name | Status | Explanation |
|---|---|---|
| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. |
| Title check | ✅ Passed | The title directly reflects the main changes: pagination support across multiple pages, search debouncing functionality, a reset-stuck UI feature, and error boundary implementations. |
| Linked Issues check | ✅ Passed | Check skipped because no linked issues were found for this pull request. |
| Out of Scope Changes check | ✅ Passed | Check skipped because no linked issues were found for this pull request. |
✏️ Tip: You can configure your own custom pre-merge checks in the settings.
✨ Finishing Touches
🧪 Generate unit tests (beta)
- Create PR with unit tests
- Commit unit tests in branch
fix/etl-memory-and-weight-validation-v2
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.
Comment @coderabbitai help to get the list of available commands and usage tips.
Coverage Report for Expo Unit Tests Coverage (./apps/expo)
File CoverageNo changed files found. |
Coverage Report for API Unit Tests Coverage (./packages/api)
File CoverageNo changed files found. |
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
packrat-admin | 123e4ce | Commit Preview URL Branch Preview URL |
May 07 2026, 08:47 PM |
There was a problem hiding this comment.
Pull request overview
Adds UX and operational improvements to the admin app by introducing URL-synced pagination, debounced search, a UI control + API endpoint to reset “stuck” ETL jobs, and Next.js error boundaries to avoid blank screens on runtime errors.
Changes:
- Add pagination (50/page) to Users/Packs/Catalog pages and sync
?page=in the URL. - Debounce search input updates (300ms) and reset pagination when search changes.
- Add admin-only
POST /api/admin/analytics/catalog/etl/reset-stuckendpoint + “Reset Stuck” button in the ETL card; remove the legacy SQL script. - Add root and dashboard error boundary pages for the admin Next.js app.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/api/src/routes/admin/analytics/catalog.ts | Adds the reset-stuck ETL admin endpoint. |
| packages/api/scripts/reset-stuck-etl-jobs.sql | Removes manual SQL script (replaced by API endpoint). |
| apps/admin/lib/queryKeys.ts | Updates query keys to support pagination/search params. |
| apps/admin/lib/api.ts | Adds resetStuckEtlJobs() client wrapper. |
| apps/admin/components/search-input.tsx | Implements debounced, URL-synced search input. |
| apps/admin/components/analytics/catalog-analytics.tsx | Adds “Reset Stuck” ETL button + mutation wiring. |
| apps/admin/app/error.tsx | Adds global error boundary UI. |
| apps/admin/app/dashboard/error.tsx | Adds dashboard-segment error boundary UI. |
| apps/admin/app/dashboard/users/page.tsx | Adds pagination controls + URL syncing for users list. |
| apps/admin/app/dashboard/page.tsx | Updates dashboard queries to new query key shape. |
| apps/admin/app/dashboard/packs/page.tsx | Adds pagination controls + URL syncing for packs list. |
| apps/admin/app/dashboard/catalog/page.tsx | Adds pagination controls + URL syncing for catalog list. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ['admin', 'users', params] as const, | ||
| packs: (params?: { q?: string; page?: number; limit?: number }) => | ||
| ['admin', 'packs', params] as const, | ||
| catalog: (params?: { q?: string; page?: number; limit?: number }) => | ||
| ['admin', 'catalog', params] as const, |
| } = useMutation({ | ||
| mutationFn: resetStuckEtlJobs, | ||
| onSuccess: () => { | ||
| queryClient.invalidateQueries({ queryKey: queryKeys.catalogAnalytics.etl() }); |
| const urlValue = searchParams?.get(paramKey) ?? ''; | ||
| const [inputValue, setInputValue] = useState(urlValue); | ||
| const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null); | ||
|
|
||
| // Keep input in sync if URL changes externally (e.g. browser back) | ||
| useEffect(() => { | ||
| setInputValue(urlValue); | ||
| }, [urlValue]); | ||
|
|
||
| const handleChange = useCallback( | ||
| (e: React.ChangeEvent<HTMLInputElement>) => { | ||
| const next = new URLSearchParams(searchParams?.toString() ?? ''); | ||
| if (e.target.value) { | ||
| next.set(paramKey, e.target.value); | ||
| } else { | ||
| next.delete(paramKey); | ||
| } | ||
| startTransition(() => { | ||
| router.replace(`?${next.toString()}`, { scroll: false }); | ||
| }); | ||
| const next = e.target.value; | ||
| setInputValue(next); | ||
|
|
||
| if (debounceRef.current) clearTimeout(debounceRef.current); | ||
| debounceRef.current = setTimeout(() => { | ||
| const params = new URLSearchParams(searchParams?.toString() ?? ''); | ||
| if (next) { | ||
| params.set(paramKey, next); | ||
| } else { | ||
| params.delete(paramKey); | ||
| } | ||
| // Also reset pagination when search changes | ||
| params.delete('page'); | ||
| startTransition(() => { | ||
| router.replace(`?${params.toString()}`, { scroll: false }); | ||
| }); | ||
| }, 300); | ||
| }, |
| const [, startTransition] = useTransition(); | ||
|
|
||
| const q = searchParams?.get('q') ?? undefined; | ||
| const page = Math.max(0, Number(searchParams?.get('page') ?? '0')); |
| const [, startTransition] = useTransition(); | ||
|
|
||
| const q = searchParams?.get('q') ?? undefined; | ||
| const page = Math.max(0, Number(searchParams?.get('page') ?? '0')); |
| const [, startTransition] = useTransition(); | ||
|
|
||
| const q = searchParams?.get('q') ?? undefined; | ||
| const page = Math.max(0, Number(searchParams?.get('page') ?? '0')); |
| .update(etlJobs) | ||
| .set({ status: 'failed', completedAt: new Date() }) | ||
| .where(and(eq(etlJobs.status, 'running'), lt(etlJobs.startedAt, threeHoursAgo))) | ||
| .returning(); |
…ey invalidation
- Add nuqs for type-safe URL query state (search, pagination)
- SearchInput simplified to ~10 lines: useQueryState handles debounce (300ms
throttle), URL sync, and unmount cleanup automatically
- Pagination pages use parseAsInteger — invalid ?page=foo safely defaults to 0
- queryKeys.admin.{users,packs,catalog} restructured to .all (prefix, for
invalidation) and .list(params) (for useQuery) — fixes post-delete not
refreshing the list
- catalogAnalytics.etl same pattern: .all for invalidation, .list(limit) for
queries — fixes ETL table not updating after reset-stuck
- Wire NuqsAdapter into app/layout.tsx
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/admin/app/dashboard/users/page.tsx`:
- Around line 100-114: The page state isn't reset when the search query changes
and negative pages are allowed; replace the separate useQueryState('q'...) and
useQueryState('page'...) usage with a small shared hook (e.g.
usePaginatedSearch) that exposes { q, setSearch, page, setPage } where
setSearch(next) sets q and also sets page to 0, and ensure page is clamped via
page = Math.max(0, rawPage) (to avoid negative offsets); update the users page
to use that hook and call setSearch from the SearchInput on change (and apply
the same change in dashboard/packs/page.tsx and dashboard/catalog/page.tsx) so
queryKeys.admin.users.list and getUsers receive a non-negative page and new
searches reset to page 0.
In `@apps/admin/app/error.tsx`:
- Around line 6-27: The current app/error.tsx defines a component named
GlobalError but lives inside the root layout and therefore cannot catch errors
thrown by app/layout.tsx (so the root providers NuqsAdapter, QueryProvider,
ThemeProvider remain unguarded); rename the component GlobalError to RootError
to reflect its actual scope and update any imports/usages, then add a new
top-level app/global-error.tsx that provides a true global boundary (it must
render <html> and <body>) with a minimal inline fallback UI (avoid importing
components that depend on the crashed providers like Button/@packrat/web-ui) and
expose the reset handler to allow retry; ensure the signature uses the same
props (error, reset) so Next can call reset to recover.
In `@apps/admin/components/analytics/catalog-analytics.tsx`:
- Around line 64-73: The resetStuck mutation (`useMutation` using
`resetStuckEtlJobs`) only handles onSuccess and only shows inline UI when
`resetResult.reset > 0`, so failures and zero-result successes are invisible;
add an `onError` handler to `useMutation` to surface network/500/401 errors (via
a toast or inline error state) and add explicit success feedback when
`resetResult.reset === 0` (e.g., show a “no stuck jobs found” message or toast)
while still keeping `isResetting` to control the spinner; update UI rendering
logic that checks `isResetting`/`resetResult` to display success, zero-result,
and error states and keep `queryClient.invalidateQueries({ queryKey:
queryKeys.catalogAnalytics.etl.all })` on success as-is.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: 1925b99d-3851-46b4-b1d2-9d89c4a391f7
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (16)
apps/admin/app/dashboard/catalog/page.tsxapps/admin/app/dashboard/error.tsxapps/admin/app/dashboard/packs/page.tsxapps/admin/app/dashboard/page.tsxapps/admin/app/dashboard/users/page.tsxapps/admin/app/error.tsxapps/admin/app/layout.tsxapps/admin/components/analytics/catalog-analytics.tsxapps/admin/components/edit-catalog-dialog.tsxapps/admin/components/search-input.tsxapps/admin/hooks/use-catalog-analytics.tsapps/admin/lib/api.tsapps/admin/lib/queryKeys.tsapps/admin/package.jsonpackages/api/scripts/reset-stuck-etl-jobs.sqlpackages/api/src/routes/admin/analytics/catalog.ts
💤 Files with no reviewable changes (1)
- packages/api/scripts/reset-stuck-etl-jobs.sql
…root-based invalidation
- Add global-error.tsx as true root error boundary (with inline html/body)
- Rename RootError in error.tsx (was incorrectly named GlobalError)
- Add usePaginatedSearch hook so search atomically resets page via nuqs
- Wire onSearch={setSearch} on catalog/packs/users pages
- Show "no stuck jobs found" and error state on Reset Stuck ETL button
API: expand all three list endpoints to return full field sets - users: +avatarUrl, +updatedAt - packs: +isAIGenerated, +tags, +image, +updatedAt - catalog: +description, +productUrl, +sku, +model, +ratingValue, +reviewCount, +availability, +color, +size, +material, +seller, +currency, +images, +variants, +techs, +links UI: - Replace hand-rolled error.tsx files with react-error-boundary ErrorBoundary in layouts; add shared ErrorFallback component - Add RawObjectDialog (Braces icon) on every row: users, packs, catalog items, ETL jobs — shows full JSON in a scrollable dialog - Users: avatar thumbnail / initial fallback in User column - Packs: AI badge, inline tags (up to 3) - Catalog: Status column with availability + star rating, product URL link icon, description/model sub-lines - ETL table: +Invalid column (red when >0), +Completed column
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/admin/app/dashboard/layout.tsx`:
- Line 19: The ErrorBoundary around ErrorFallback is missing resetKeys so its
fallback can get stuck across route changes; retrieve the current pathname (e.g.
via usePathname from next/navigation) in the layout component and pass it as
resetKeys={[pathname]} to the <ErrorBoundary> (keep ErrorFallback and children
unchanged), and add the import for usePathname if not already present so the
boundary resets on navigation.
In `@apps/admin/app/global-error.tsx`:
- Around line 31-34: Replace the user-visible use of error.message in the
fallback UI: stop rendering {error.message} inside the <p> and always show a
generic message like "An unexpected error occurred." instead; move the detailed
information to logs/telemetry by calling console.error(error) or your
telemetry/reporting function from the error boundary where the error object is
available (i.e., the code that supplies the error used in the <p>), ensuring
only the generic string is rendered to users while error.message and stack are
recorded internally.
In `@apps/admin/components/error-fallback.tsx`:
- Around line 10-12: In the ErrorFallback component (error-fallback.tsx) do not
render raw error.message into the UI; replace the conditional JSX expression
that outputs error.message ({error instanceof Error ? error.message : ...}) with
a generic user-safe string like "Something went wrong" for the paragraph text,
and move the detailed diagnostic (error object or error.message/stack) to a
non-UI channel such as console.error or your telemetry/error-reporting hook
(e.g., call a logError/reportError helper) so internals are not exposed in the
shared fallback UI.
In `@apps/admin/components/raw-object-dialog.tsx`:
- Around line 31-40: Add a Radix DialogDescription inside the DialogContent
(e.g., directly under DialogTitle) to satisfy the aria-describedby requirement:
use the DialogDescription component (alongside existing DialogTitle in
raw-object-dialog.tsx) with a visually hidden text string such as "Raw object
content" or include the dynamic label to describe the dialog, so the warning
goes away while the visible UI remains unchanged.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: 84d54899-40d4-44ca-ab77-08760ad94bfc
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (21)
.gitignoreapps/admin/app/dashboard/catalog/page.tsxapps/admin/app/dashboard/layout.tsxapps/admin/app/dashboard/packs/page.tsxapps/admin/app/dashboard/page.tsxapps/admin/app/dashboard/users/page.tsxapps/admin/app/error.tsxapps/admin/app/global-error.tsxapps/admin/components/analytics/catalog-analytics.tsxapps/admin/components/edit-catalog-dialog.tsxapps/admin/components/error-fallback.tsxapps/admin/components/raw-object-dialog.tsxapps/admin/components/search-input.tsxapps/admin/hooks/use-catalog-analytics.tsapps/admin/hooks/use-paginated-search.tsapps/admin/hooks/use-platform-analytics.tsapps/admin/lib/api.tsapps/admin/lib/cfAccess.tsapps/admin/lib/queryKeys.tsapps/admin/package.jsonpackages/api/src/routes/admin/index.ts
✅ Files skipped from review due to trivial changes (3)
- .gitignore
- apps/admin/package.json
- apps/admin/app/error.tsx
🚧 Files skipped from review as they are similar to previous changes (9)
- apps/admin/components/edit-catalog-dialog.tsx
- apps/admin/components/search-input.tsx
- apps/admin/app/dashboard/page.tsx
- apps/admin/app/dashboard/packs/page.tsx
- apps/admin/components/analytics/catalog-analytics.tsx
- apps/admin/lib/api.ts
- apps/admin/app/dashboard/catalog/page.tsx
- apps/admin/app/dashboard/users/page.tsx
- apps/admin/lib/queryKeys.ts
- ErrorBoundary: add resetKeys={[pathname]} so boundary auto-resets
on route changes instead of keeping fallback across navigation
- error-fallback.tsx + global-error.tsx: stop surfacing raw error.message
to users; log to console instead
- RawObjectDialog: add visually-hidden DialogDescription to silence
Radix UI aria-describedby warning
- Note: .returning({ id }) on reset-stuck blocked by Drizzle TS types
in this version; full .returning() retained
- Add global-error.tsx as true root error boundary (with inline html/body)
- Rename RootError in error.tsx (was incorrectly named GlobalError)
- Add usePaginatedSearch hook so search atomically resets page via nuqs
- Wire onSearch={setSearch} on catalog/packs/users pages
- Show "no stuck jobs found" and error state on Reset Stuck ETL button
…lidation-v2 feat(admin): pagination, search debounce, reset-stuck UI, error boundaries
Summary
?page=N)POST /api/admin/analytics/catalog/etl/reset-stuckendpoint, shows spinner + how many jobs were reset; removes the manualreset-stuck-etl-jobs.sqlscriptapp/error.tsx(root) andapp/dashboard/error.tsxstop 500s from blanking the whole screenAPI change
POST /api/admin/analytics/catalog/etl/reset-stuck— admin-only endpoint that marks jobs stuck inrunningfor >3 hours asfailed. Replaces the raw SQL script.Post-Deploy Monitoring & Validation
{ reset: N, ids: [...] }🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Improvements