Skip to content

Feat/improved review process#353

Merged
Producdevity merged 31 commits into
stagingfrom
feat/improved-review-process
May 18, 2026
Merged

Feat/improved review process#353
Producdevity merged 31 commits into
stagingfrom
feat/improved-review-process

Conversation

@Producdevity

@Producdevity Producdevity commented May 14, 2026

Copy link
Copy Markdown
Owner

Description

Fixes # (issue)

Type of change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update
  • Refactor
  • Other (please describe):

How Has This Been Tested?

  • Local build
  • Lint
  • Typecheck
  • Unit tests
  • Manual testing

Screenshots (if applicable)

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my code
  • I have made corresponding changes to the documentation
  • I have checked that all checks (lint, typecheck, test) pass

Notes for reviewers

Summary by CodeRabbit

  • New Features

    • Admin “Risk only” filter, review-risk indicators, and warning banners; new Risk-only toggle UI.
    • New reusable Switch component for consistent toggles.
  • Bug Fixes

    • More reliable reCAPTCHA client/server handling and clearer verification errors.
    • Safer normalization of custom-field values to reduce validation/data issues.
  • Improvements

    • Improved admin error UI, selection/paging behavior, and dev/local image allowlist plus refined production vs non-production caching.
    • Better detection of placeholder emulator-version submission risk.
  • Documentation

    • Updated contribution guide.

Review Change Stack

@vercel

vercel Bot commented May 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
emuready Ready Ready Preview, Comment May 18, 2026 7:32pm

Request Review

@coderabbitai

coderabbitai Bot commented May 14, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4b9eea6c-11e2-43a9-8360-6a0b48f9dd1f

📥 Commits

Reviewing files that changed from the base of the PR and between 351a2aa and 8f0a10c.

📒 Files selected for processing (1)
  • CONTRIBUTING.md
✅ Files skipped from review due to trivial changes (1)
  • CONTRIBUTING.md

Walkthrough

Adds review-risk end-to-end (submission + author profiles), pending-list risk candidate paging, admin risk-only filter and UI, shared custom-field validation and normalization, CAPTCHA client/server split and verification, query-builder tweaks for shadow-ban/NSFW, infra gating (Sentry/init), and many tests.

Changes

Review Risk Filtering and Submission Risk Detection

Layer / File(s) Summary
Schemas and basic imports
src/schemas/submissionRisk.ts, src/schemas/listing.ts, src/schemas/pcListing.ts
Adds submission-risk and review-risk filter schemas and extends pending-list inputs.
Submission-risk service & tests
src/server/services/submission-risk.service.ts, src/server/services/submission-risk.service.test.ts
Detects placeholder-like emulator versions, batches author credibility, and computes submission risk profiles with severity rules.
Review-risk service & tests
src/server/services/review-risk.service.ts, src/server/services/review-risk.service.test.ts
Computes author+submission risk maps, extracts risky IDs, attaches profiles, and provides risky-only pagination helper.
Repositories: pending queries & risk candidates
src/server/repositories/listings.repository.ts, src/server/repositories/pc-listings.repository.ts, tests
Adds pending-list filters, risk-candidate selects, getPendingListings/getPendingListingRiskCandidates/getPendingListingsByIds, includes, order builders, and active-ban predicates.
Routers: admin pending + pcListings
src/server/api/routers/listings/admin.ts, src/server/api/routers/pcListings.ts, tests
Admin pending procedures accept riskFilter and choose risky-only (getRiskOnlyReviewPage) or standard (compute+attach) paths; creation flows verify CAPTCHA and normalize custom-field inputs; tests updated.
Admin UI: indicators, banners, filter hook
src/components/admin/review-risk/*, src/app/admin/hooks/useReviewRiskFilter.ts, src/components/admin/ReviewRiskFilterButton.tsx, src/components/ui/Switch.tsx, src/components/admin/AdminErrorState.tsx, pages/modals
New ReviewRiskIndicator/WarningBanner and display helpers; risk-only switch/button and hook; AdminErrorState component; pages and modals wired to show combined author+submission risk.

Custom Field Validation & Normalization

Layer / File(s) Summary
Shared custom-field validation
src/utils/custom-field-validation.ts, src/utils/custom-field-validation.test.ts
Central createCustomFieldValuesSchema with per-type validation and superRefine error pathing.
Empty-value utility
src/utils/custom-fields.ts, updates to imports
Adds isCustomFieldValueEmpty and re-uses it across renderers/helpers.
Form schema refactors
src/app/listings/new/form-schemas/createDynamicListingSchema.ts, src/app/pc-listings/new/form-schemas/createDynamicPcListingSchema.ts
Delegates customFieldValues validation to shared factory; removes inline validators and local exported types.
Normalization before persistence
src/server/utils/custom-field-values.ts, src/server/api/routers/listings/core.ts, src/server/api/routers/pcListings.ts
Adds normalizeCustomFieldValues and uses it for create/update repository inputs.

CAPTCHA & Provider Changes

Layer / File(s) Summary
src/lib/captcha/config.ts, src/lib/captcha/verify.ts, src/lib/captcha/hooks.ts, src/lib/captcha/verify.test.ts Splits client/server enablement, safe env fallbacks, optional token handling, explicit missing-token error, and tests for verification flow.
src/components/Providers.tsx Provider gating now uses isCaptchaClientEnabled.
Creation flows Routers call verifyRecaptcha and abort on failure; tests updated to assert recaptcha usage.

Query Builders & Utilities

Layer / File(s) Summary
src/server/utils/query-builders.ts, src/server/api/utils/pcListingHelpers.ts, tests buildShadowBanFilter now optionally includes user id to let users see their own record; buildNsfwFilter defaults to excluding NSFW. Used consistently by listing helpers and tested.

Infra, tooling, and misc

Layer / File(s) Summary
next.config.ts, src/instrumentation-client.ts, instrumentation.ts, src/server/init.ts CSP generation refactor, images.allowedDevOrigins adds 127.0.0.1, Sentry client init gated to production, idempotent server initialization, instrumentation uses initializeServerOnce.
CORS & Playwright src/lib/cors.ts, src/lib/cors.test.ts, playwright.config.ts — merges local test origins and loads .env.test for E2E.
Tests & E2E helpers Multiple test expansions and helpers for pending flows, recaptcha, dialogs, and device-driven filter selection.
Small edits package.json dev scripts and @radix-ui/react-switch addition, .gitignore /output, PR template link, AppError.captcha typing, GlobalError props typing, rejection preset text.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

"A rabbit hopped through code at night,
I stitched risks from author and post so bright,
I checked for placeholders and badges that gleam,
Now admin queues can filter by risky theme,
Hop in — the review queue's clearer in light!"

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/improved-review-process

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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/server/repositories/pc-listings.repository.ts (1)

855-882: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Inconsistent shadow-ban filtering between pending listings repositories.

The buildPendingListingsWhere method in this file applies shadow-ban filtering to pending PC listings when canSeeBannedUsers is false, but the equivalent method in listings.repository.ts does not apply any shadow-ban filtering.

This creates inconsistent behavior:

  • Pending PC listings from shadow-banned users are hidden by default
  • Pending handheld listings from shadow-banned users are shown

Both repositories should apply the same shadow-ban filtering logic, or neither should. Without alignment, moderators reviewing different listing types will have different visibility, which could lead to confusion and inconsistent workflows.

🤖 Prompt for 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.

In `@src/server/repositories/pc-listings.repository.ts` around lines 855 - 882,
The PC listings method buildPendingListingsWhere applies shadow-ban filtering
when canSeeBannedUsers is false but the handheld equivalent does not; align them
by applying the same logic: compute const shadowBanFilter = canSeeBannedUsers ?
undefined : buildShadowBanFilter(null) and merge it into the returned where
clause as ...(shadowBanFilter && { author: shadowBanFilter }) (or conversely
remove that merge from buildPendingListingsWhere if you intend no shadow-ban
filtering), ensuring both repositories use the same approach for
canSeeBannedUsers and buildShadowBanFilter so moderator visibility is
consistent.
🧹 Nitpick comments (6)
src/server/api/routers/listings/admin.test.ts (2)

21-43: 💤 Low value

Consider extracting the inline mock implementation to a shared test helper.

The inline implementation of createExistingAuthorBansMap duplicates business logic within the mock. If the real implementation changes, this could drift out of sync. Consider either importing the real helper or creating a shared test utility that both test files can use.

🤖 Prompt for 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.

In `@src/server/api/routers/listings/admin.test.ts` around lines 21 - 43, The
inline mock function createExistingAuthorBansMap duplicates production logic and
should be extracted: move the implementation into a shared test helper (e.g.,
tests/helpers or tests/utils) or import the real helper if appropriate, then
replace the inline definition in admin.test.ts with an import of
createExistingAuthorBansMap and update any other tests to reuse that single
exported function so logic remains centralized and in-sync with production code.

131-217: ⚡ Quick win

Add test coverage for the non-risky filter path.

The test suite only covers riskFilter: 'risky'. Consider adding test cases for:

  • riskFilter: 'all' to verify the standard pending listings path
  • Edge cases like no risky listings found, or pagination edge cases

This would ensure both code paths are exercised and prevent regressions.

🤖 Prompt for 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.

In `@src/server/api/routers/listings/admin.test.ts` around lines 131 - 217, Add
tests covering the non-risky and no-risky paths: in
src/server/api/routers/listings/admin.test.ts add a test calling
caller.getPending({ riskFilter: 'all', page: 1, limit: 20 }) that stubs
mockGetPendingListings to return expected rows and asserts
mockGetPendingListings was called (and mockGetPendingListingRiskCandidates was
not), verifies returned listings and pagination; also add a test for riskFilter:
'risky' when mockGetPendingListingRiskCandidates returns an empty array to
assert mockGetPendingListingsByIds is not called and result.listings.length and
pagination.total are 0. Ensure you reference and use the existing helpers/mocks
(createCaller, mockGetPendingListingRiskCandidates, mockGetPendingListingsByIds,
mockGetPendingListings) and mirror the style/assertions used in the existing
'filters risk-only listings' test.
src/server/api/routers/pcListings.test.ts (2)

81-103: ⚡ Quick win

Extract the inline mock implementation to a shared test utility.

This duplicates the same inline implementation pattern from admin.test.ts. Both test files would benefit from a shared test helper for createExistingAuthorBansMap to avoid duplication and ensure consistency.

🤖 Prompt for 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.

In `@src/server/api/routers/pcListings.test.ts` around lines 81 - 103, Extract the
inline createExistingAuthorBansMap implementation into a shared test utility
function and have both tests import and use it instead of duplicating the logic;
specifically, create and export a helper named createExistingAuthorBansMap that
accepts the same listings shape and returns a Map<string, {reason:string}[]>,
then replace the inline implementations in pcListings.test.ts and admin.test.ts
with imports of that helper so both tests call the single shared function and
retain identical behavior.

477-561: ⚡ Quick win

Add test coverage for the non-risky filter path.

Similar to admin.test.ts, this test suite only covers riskFilter: 'risky'. Add test cases for:

  • riskFilter: 'all' or undefined to verify the standard pending listings retrieval
  • Edge cases such as empty results or different user roles

This ensures both code paths are tested and prevents regressions.

🤖 Prompt for 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.

In `@src/server/api/routers/pcListings.test.ts` around lines 477 - 561, The suite
only tests the 'risky' riskFilter path; add tests that exercise the
non-risky/default path by calling caller.pending with riskFilter: 'all' and with
riskFilter omitted/undefined, verifying that mockRepositoryGetPendingListings is
used (not getPendingListingRiskCandidates/getPendingListingsByIds), that
returned pcListings match the mocked pending listings and pagination.total
reflects the count, and add an edge-case test for empty results and a test using
a non-moderator role (e.g., Role.USER) to validate permission-based branches;
reference mocks and helpers like createCaller, caller.pending,
mockRepositoryGetPendingListings, mockRepositoryGetPendingListingRiskCandidates,
mockRepositoryGetPendingListingsByIds, mockComputeAuthorRiskProfiles and
mockComputeSubmissionRiskProfiles to seed expected responses and assertions.
src/components/ui/ReviewRiskIndicator.test.tsx (1)

33-68: ⚡ Quick win

Add test coverage for the investigate interaction path.

The suite does not currently verify the clickable onInvestigate behavior (mouse + keyboard), which is core logic in the component.

🧪 Suggested test addition
-import { render, screen } from '@testing-library/react'
-import { describe, expect, it } from 'vitest'
+import { fireEvent, render, screen } from '@testing-library/react'
+import { describe, expect, it, vi } from 'vitest'
@@
   it('renders for submission risk without author risk', () => {
@@
   })
+
+  it('invokes investigate callback for clickable indicator', () => {
+    const onInvestigate = vi.fn()
+    render(
+      <ReviewRiskIndicator
+        authorRiskProfile={authorRiskProfile}
+        submissionRiskProfile={submissionRiskProfile}
+        onInvestigate={onInvestigate}
+      />,
+    )
+
+    const button = screen.getByRole('button')
+    fireEvent.click(button)
+    fireEvent.keyDown(button, { key: 'Enter' })
+    fireEvent.keyDown(button, { key: ' ' })
+
+    expect(onInvestigate).toHaveBeenCalledWith('author-1')
+    expect(onInvestigate).toHaveBeenCalledTimes(3)
+  })
 })
🤖 Prompt for 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.

In `@src/components/ui/ReviewRiskIndicator.test.tsx` around lines 33 - 68, Add
tests for the investigate interaction by rendering ReviewRiskIndicator with a
jest.fn() onInvestigate prop and asserting it is called on mouse click and
keyboard activation; locate the interactable element via
screen.getByRole('status') (or the same role used in the component), use
userEvent.click(status) to simulate mouse activation and
userEvent.keyboard('{Enter}') and userEvent.keyboard(' ') after status.focus()
to simulate Enter/Space keyboard activation, and assert onInvestigate was
invoked for each interaction; ensure the test covers both the case with
author/submission risk and the case with only submission risk so the click
target exists in both scenarios.
src/server/repositories/pc-listings.repository.ts (1)

890-894: 💤 Low value

Consider extracting active ban criteria to a helper method.

The inline active ban filtering (lines 891-893) duplicates the same logic used in forList include at lines 86-87. While correct, extracting this to a getActiveBanWhere() helper method (similar to listings.repository.ts line 572-577) would improve maintainability and ensure consistency across query builders.

♻️ Suggested refactor

Add a private helper method:

+  private getActiveBanWhere(): Prisma.UserBanWhereInput {
+    return {
+      isActive: true,
+      OR: [{ expiresAt: null }, { expiresAt: { gt: new Date() } }],
+    }
+  }
+
   private getPendingListingRiskCandidateSelect() {
     return {
       id: true,
       authorId: true,
       author: {
         select: {
           userBans: {
-            where: {
-              isActive: true,
-              OR: [{ expiresAt: null }, { expiresAt: { gt: new Date() } }],
-            },
+            where: this.getActiveBanWhere(),
             select: { reason: true },
           },
         },
       },

Then reuse in the forList include at line 85:

       author: {
         include: {
           userBans: {
-            where: {
-              isActive: true,
-              OR: [{ expiresAt: null }, { expiresAt: { gt: new Date() } }],
-            },
+            where: this.getActiveBanWhere(),
             select: { id: true, reason: true, bannedAt: true, expiresAt: true },
           },
         },
       },
🤖 Prompt for 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.

In `@src/server/repositories/pc-listings.repository.ts` around lines 890 - 894,
Extract the duplicated active-ban predicate into a private helper (e.g., private
getActiveBanWhere()) that returns the object { isActive: true, OR: [{ expiresAt:
null }, { expiresAt: { gt: new Date() } }] }, add it to the PCListingsRepository
class, and replace the inline userBans.where in the forList include and the
other occurrence (the block using userBans: { where: { ... } }) to call
getActiveBanWhere() so both queries reuse the same logic for consistency.
🤖 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 `@src/components/admin/ReviewRiskFilterButton.tsx`:
- Around line 12-19: This toggle button (Button in ReviewRiskFilterButton) needs
an accessible pressed state: add aria-pressed={props.isActive} to the Button
element so screen readers announce the on/off state; ensure props.isActive is a
boolean in the component's props type and keep onToggle as the click handler.

In `@src/server/api/routers/listings/admin.ts`:
- Around line 89-109: The final by-ID fetch (repository.getPendingListingsByIds)
can return listings that no longer meet the original scope (emulatorIds/pending)
between the two queries; update the fetch to re-apply the same scope checks used
to build riskCandidates: either modify repository.getPendingListingsByIds to
accept the same scope parameters (emulatorIds and pending status/search
constraints) and use them in the DB query, or after fetching pageListings filter
them against the original riskCandidates set (or a map keyed by id) so only
listings that still match the emulatorIds/pending scope are kept before calling
attachReviewRiskProfiles; reference getPendingListingRiskCandidates,
getRiskyReviewItemIds, getPendingListingsByIds, computeReviewRiskProfiles, and
attachReviewRiskProfiles.

In `@src/server/api/routers/pcListings.ts`:
- Around line 467-489: The refetch of listings via
repository.getPendingListingsByIds creates a TOCTOU gap because the original
candidate query used emulatorIds and other auth constraints; fix by keeping the
same authorization scope when loading the final page: either change
getPendingListingsByIds to accept the same filter arguments (emulatorIds,
canSeeBannedUsers, search/sort if needed) and call it with the same parameters
used for getPendingListingRiskCandidates, or avoid the re-query entirely by
selecting full listing fields in getPendingListingRiskCandidates and using those
results (so computeReviewRiskProfiles and attachReviewRiskProfiles still work).
Update calls around getPendingListingRiskCandidates, getPendingListingsByIds,
computeReviewRiskProfiles, and attachReviewRiskProfiles to ensure emulatorIds
and canSeeBannedUsers are preserved when producing paginatedPcListings.

---

Outside diff comments:
In `@src/server/repositories/pc-listings.repository.ts`:
- Around line 855-882: The PC listings method buildPendingListingsWhere applies
shadow-ban filtering when canSeeBannedUsers is false but the handheld equivalent
does not; align them by applying the same logic: compute const shadowBanFilter =
canSeeBannedUsers ? undefined : buildShadowBanFilter(null) and merge it into the
returned where clause as ...(shadowBanFilter && { author: shadowBanFilter }) (or
conversely remove that merge from buildPendingListingsWhere if you intend no
shadow-ban filtering), ensuring both repositories use the same approach for
canSeeBannedUsers and buildShadowBanFilter so moderator visibility is
consistent.

---

Nitpick comments:
In `@src/components/ui/ReviewRiskIndicator.test.tsx`:
- Around line 33-68: Add tests for the investigate interaction by rendering
ReviewRiskIndicator with a jest.fn() onInvestigate prop and asserting it is
called on mouse click and keyboard activation; locate the interactable element
via screen.getByRole('status') (or the same role used in the component), use
userEvent.click(status) to simulate mouse activation and
userEvent.keyboard('{Enter}') and userEvent.keyboard(' ') after status.focus()
to simulate Enter/Space keyboard activation, and assert onInvestigate was
invoked for each interaction; ensure the test covers both the case with
author/submission risk and the case with only submission risk so the click
target exists in both scenarios.

In `@src/server/api/routers/listings/admin.test.ts`:
- Around line 21-43: The inline mock function createExistingAuthorBansMap
duplicates production logic and should be extracted: move the implementation
into a shared test helper (e.g., tests/helpers or tests/utils) or import the
real helper if appropriate, then replace the inline definition in admin.test.ts
with an import of createExistingAuthorBansMap and update any other tests to
reuse that single exported function so logic remains centralized and in-sync
with production code.
- Around line 131-217: Add tests covering the non-risky and no-risky paths: in
src/server/api/routers/listings/admin.test.ts add a test calling
caller.getPending({ riskFilter: 'all', page: 1, limit: 20 }) that stubs
mockGetPendingListings to return expected rows and asserts
mockGetPendingListings was called (and mockGetPendingListingRiskCandidates was
not), verifies returned listings and pagination; also add a test for riskFilter:
'risky' when mockGetPendingListingRiskCandidates returns an empty array to
assert mockGetPendingListingsByIds is not called and result.listings.length and
pagination.total are 0. Ensure you reference and use the existing helpers/mocks
(createCaller, mockGetPendingListingRiskCandidates, mockGetPendingListingsByIds,
mockGetPendingListings) and mirror the style/assertions used in the existing
'filters risk-only listings' test.

In `@src/server/api/routers/pcListings.test.ts`:
- Around line 81-103: Extract the inline createExistingAuthorBansMap
implementation into a shared test utility function and have both tests import
and use it instead of duplicating the logic; specifically, create and export a
helper named createExistingAuthorBansMap that accepts the same listings shape
and returns a Map<string, {reason:string}[]>, then replace the inline
implementations in pcListings.test.ts and admin.test.ts with imports of that
helper so both tests call the single shared function and retain identical
behavior.
- Around line 477-561: The suite only tests the 'risky' riskFilter path; add
tests that exercise the non-risky/default path by calling caller.pending with
riskFilter: 'all' and with riskFilter omitted/undefined, verifying that
mockRepositoryGetPendingListings is used (not
getPendingListingRiskCandidates/getPendingListingsByIds), that returned
pcListings match the mocked pending listings and pagination.total reflects the
count, and add an edge-case test for empty results and a test using a
non-moderator role (e.g., Role.USER) to validate permission-based branches;
reference mocks and helpers like createCaller, caller.pending,
mockRepositoryGetPendingListings, mockRepositoryGetPendingListingRiskCandidates,
mockRepositoryGetPendingListingsByIds, mockComputeAuthorRiskProfiles and
mockComputeSubmissionRiskProfiles to seed expected responses and assertions.

In `@src/server/repositories/pc-listings.repository.ts`:
- Around line 890-894: Extract the duplicated active-ban predicate into a
private helper (e.g., private getActiveBanWhere()) that returns the object {
isActive: true, OR: [{ expiresAt: null }, { expiresAt: { gt: new Date() } }] },
add it to the PCListingsRepository class, and replace the inline userBans.where
in the forList include and the other occurrence (the block using userBans: {
where: { ... } }) to call getActiveBanWhere() so both queries reuse the same
logic for consistency.
🪄 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: e401bb9c-b655-46b0-ae0a-d291c6aea83a

📥 Commits

Reviewing files that changed from the base of the PR and between 5d32935 and 7eebded.

📒 Files selected for processing (32)
  • next.config.ts
  • package.json
  • src/app/admin/approvals/components/ApprovalModal.tsx
  • src/app/admin/approvals/page.tsx
  • src/app/admin/hooks/index.ts
  • src/app/admin/hooks/useReviewRiskFilter.ts
  • src/app/admin/pc-listing-approvals/components/ApprovalModal.tsx
  • src/app/admin/pc-listing-approvals/page.tsx
  • src/app/global-error.tsx
  • src/app/testing/page.tsx
  • src/components/admin/ReviewRiskFilterButton.tsx
  • src/components/admin/index.ts
  • src/components/ui/ReviewRiskIndicator.test.tsx
  • src/components/ui/ReviewRiskIndicator.tsx
  • src/components/ui/ReviewRiskWarningBanner.test.tsx
  • src/components/ui/ReviewRiskWarningBanner.tsx
  • src/components/ui/index.ts
  • src/schemas/listing.ts
  • src/schemas/pcListing.ts
  • src/schemas/submissionRisk.ts
  • src/server/api/routers/listings/admin.test.ts
  • src/server/api/routers/listings/admin.ts
  • src/server/api/routers/pcListings.test.ts
  • src/server/api/routers/pcListings.ts
  • src/server/repositories/listings.repository.ts
  • src/server/repositories/pc-listings.repository.ts
  • src/server/services/author-risk.service.ts
  • src/server/services/review-risk.service.test.ts
  • src/server/services/review-risk.service.ts
  • src/server/services/submission-risk.service.test.ts
  • src/server/services/submission-risk.service.ts
  • src/server/utils/query-builders.ts

Comment thread src/components/admin/ReviewRiskFilterButton.tsx Outdated
Comment thread src/server/api/routers/listings/admin.ts Outdated
Comment thread src/server/api/routers/pcListings.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/lib/cors.ts (1)

30-34: 💤 Low value

Optional: use a Set to dedupe instead of repeated includes scans.

addMissingOrigins does an O(n) includes per candidate, and getAllowedOrigins calls it 3–4 times. The list is small so this isn't a real performance issue, but a Set-based accumulator would be both cleaner and avoid the in-place mutation pattern.

♻️ Sketch
-function addMissingOrigins(origins: string[], additionalOrigins: string[]) {
-  for (const origin of additionalOrigins) {
-    if (!origins.includes(origin)) origins.push(origin)
-  }
-}
+function addMissingOrigins(origins: Set<string>, additionalOrigins: readonly string[]) {
+  for (const origin of additionalOrigins) origins.add(origin)
+}

Then build origins as a Set<string> in getAllowedOrigins and return [...origins] at the end.

🤖 Prompt for 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.

In `@src/lib/cors.ts` around lines 30 - 34, The current addMissingOrigins uses
repeated origins.includes scans and mutates an array; change the logic to use a
Set for deduplication: in getAllowedOrigins create a Set<string> (e.g., allowed
= new Set()), add the base origins and then add additional origins with Set.add
(or refactor addMissingOrigins to accept and add into a Set rather than an
array), and finally return Array.from(allowed) (or [...allowed]); update
references to addMissingOrigins/getAllowedOrigins accordingly to remove O(n)
includes and in-place array mutation.
🤖 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 `@src/lib/cors.test.ts`:
- Around line 21-27: The test titled "allows localhost during CI production-mode
E2E runs" does not stub NODE_ENV so it does not actually exercise
production-mode logic in getAllowedOrigins(); update the test to stub
NODE_ENV='production' (alongside CI='true', NEXT_PUBLIC_ALLOWED_ORIGINS='' and
ALLOWED_ORIGINS='') so getAllowedOrigins() hits the intended production branch
that adds LOCAL_TEST_ORIGINS, or alternatively rename the test to remove the
"production-mode" claim if you prefer not to change environment stubs.

In `@src/lib/cors.ts`:
- Around line 56-58: The current CI-only guard that calls
addMissingOrigins(origins, LOCAL_TEST_ORIGINS) should also verify NODE_ENV to
mirror defensive checks elsewhere; update the condition around addMissingOrigins
(the block that uses process.env.CI) to require process.env.NODE_ENV !==
'production' in addition to process.env.CI === 'true' so it only runs in
non-production CI/test environments and remains consistent with
threeDsGameRefreshService.ts and switchGameRefreshService.ts.

---

Nitpick comments:
In `@src/lib/cors.ts`:
- Around line 30-34: The current addMissingOrigins uses repeated
origins.includes scans and mutates an array; change the logic to use a Set for
deduplication: in getAllowedOrigins create a Set<string> (e.g., allowed = new
Set()), add the base origins and then add additional origins with Set.add (or
refactor addMissingOrigins to accept and add into a Set rather than an array),
and finally return Array.from(allowed) (or [...allowed]); update references to
addMissingOrigins/getAllowedOrigins accordingly to remove O(n) includes and
in-place array mutation.
🪄 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: 07cb3a01-0789-4a1e-a6d8-ad69b493edc9

📥 Commits

Reviewing files that changed from the base of the PR and between a02bd24 and df63733.

📒 Files selected for processing (2)
  • src/lib/cors.test.ts
  • src/lib/cors.ts

Comment thread src/lib/cors.test.ts Outdated
Comment thread src/lib/cors.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/server/utils/query-builders.test.ts (1)

146-148: ⚡ Quick win

Add explicit null coverage for buildNsfwFilter.

Current tests validate omitted args but not null. Add a case for buildNsfwFilter(null) to lock expected behavior and prevent regressions.

Suggested test addition
   it('should return filter by default', () => {
     expect(buildNsfwFilter()).toEqual({ isErotic: false })
   })
+
+  it('should return filter when showNsfw is null', () => {
+    expect(buildNsfwFilter(null)).toEqual({ isErotic: false })
+  })
🤖 Prompt for 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.

In `@src/server/utils/query-builders.test.ts` around lines 146 - 148, Add an
explicit unit test that calls buildNsfwFilter(null) and asserts it returns the
same default object as when no argument is provided
(expect(buildNsfwFilter(null)).toEqual({ isErotic: false })), so the behavior
for null input is locked and regressions are prevented; place this alongside the
existing 'should return filter by default' test in the query-builders tests and
reference the buildNsfwFilter function in the new test.
tests/helpers/data-factory.ts (1)

595-609: 💤 Low value

Consider moving the click outside the retry loop.

The current implementation clicks the button inside the toPass callback (line 602), which may result in multiple clicks if the dialog takes longer than 2 seconds to appear. While this likely works, it could be more efficient to click once and only retry the visibility assertion:

async function openUserDetailsDialog(page: Page, userRow: Locator): Promise<Locator> {
  const viewDetailsButton = userRow.getByRole('button', { name: 'View User Details' })
  await expect(viewDetailsButton).toBeVisible()
  
  const dialog = page.locator('[role="dialog"]')
  
  // Click once outside the retry loop
  if (!(await dialog.isVisible())) {
    await viewDetailsButton.click()
  }
  
  // Then retry only the visibility check
  await expect(dialog).toBeVisible({ timeout: 10000 })
  
  return dialog
}

However, the current implementation with the conditional click inside toPass does provide more resilience if the button needs to be clicked multiple times, so this is optional.

🤖 Prompt for 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.

In `@tests/helpers/data-factory.ts` around lines 595 - 609, The click on
viewDetailsButton is currently inside the retry callback in
openUserDetailsDialog which can cause multiple clicks; instead, perform a single
click before entering the retry/assertion and then only retry the visibility
check: check viewDetailsButton visibility, compute dialog =
page.locator('[role="dialog"]'), if dialog isn't visible await
viewDetailsButton.click(), then replace the toPass block with a single await
expect(dialog).toBeVisible({ timeout: 10000 }) and return dialog.
🤖 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 `@src/server/utils/query-builders.ts`:
- Around line 133-136: The current filter returns undefined unless showNsfw ===
false, which lets null bypass NSFW filtering; update the condition in the
function handling parameters showNsfw and fieldName so that only an explicit
true allows NSFW (e.g., treat null like false). Concretely, change the predicate
that decides to return { [fieldName]: false } versus undefined to require
showNsfw === true to allow NSFW (or use showNsfw !== true to apply the false
filter), referencing the showNsfw parameter and the fieldName return shape.

In `@src/utils/custom-field-validation.ts`:
- Around line 33-52: The centralized schema createCustomFieldValuesSchema
currently only enforces required/non-empty checks and drops type-specific
validation; update the superRefine callback inside createCustomFieldValuesSchema
to, for each CustomFieldDefinitionWithOptions passed, fetch the fieldValue via
getFieldValue and run type-specific validations (using field.type and
field.options): e.g., for SELECT ensure the value matches one of field.options,
for RANGE ensure the value is numeric and within min/max, for NUMBER ensure
numeric shape, for URL/text enforce string and pattern/format, and for any
option-based or multi-select enforce arrays of valid option ids — when a check
fails call ctx.addIssue with code z.ZodIssueCode.custom, a clear message and
path computed by getFieldErrorPath(values, field.id, index); reuse
isCustomFieldValueEmpty to skip absent values and keep
CustomFieldValueSchema/JsonValueSchema as the base.

---

Nitpick comments:
In `@src/server/utils/query-builders.test.ts`:
- Around line 146-148: Add an explicit unit test that calls
buildNsfwFilter(null) and asserts it returns the same default object as when no
argument is provided (expect(buildNsfwFilter(null)).toEqual({ isErotic: false
})), so the behavior for null input is locked and regressions are prevented;
place this alongside the existing 'should return filter by default' test in the
query-builders tests and reference the buildNsfwFilter function in the new test.

In `@tests/helpers/data-factory.ts`:
- Around line 595-609: The click on viewDetailsButton is currently inside the
retry callback in openUserDetailsDialog which can cause multiple clicks;
instead, perform a single click before entering the retry/assertion and then
only retry the visibility check: check viewDetailsButton visibility, compute
dialog = page.locator('[role="dialog"]'), if dialog isn't visible await
viewDetailsButton.click(), then replace the toPass block with a single await
expect(dialog).toBeVisible({ timeout: 10000 }) and return dialog.
🪄 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: 18c3fe7c-f662-4c00-bc6c-0dcd1db36cdb

📥 Commits

Reviewing files that changed from the base of the PR and between a02bd24 and cc15640.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (47)
  • .gitignore
  • duplication-audit.md
  • package.json
  • pnpm-workspace.yaml
  • src/app/listings/components/shared/approval/ApprovalModalSharedComponents.tsx
  • src/app/listings/components/shared/utils/customFieldHelpers.tsx
  • src/app/listings/new/NewListingPage.tsx
  • src/app/listings/new/form-schemas/createDynamicListingSchema.ts
  • src/app/pc-listings/new/NewPcListingPage.tsx
  • src/app/pc-listings/new/form-schemas/createDynamicPcListingSchema.ts
  • src/components/Providers.tsx
  • src/components/admin/AdminErrorState.test.tsx
  • src/components/admin/AdminErrorState.tsx
  • src/components/admin/ReviewRiskFilterButton.test.tsx
  • src/components/admin/ReviewRiskFilterButton.tsx
  • src/components/ui/Switch.tsx
  • src/components/ui/index.ts
  • src/instrumentation-client.ts
  • src/lib/captcha/config.ts
  • src/lib/captcha/hooks.ts
  • src/lib/captcha/verify.test.ts
  • src/lib/captcha/verify.ts
  • src/lib/cors.test.ts
  • src/lib/cors.ts
  • src/lib/errors.ts
  • src/schemas/pcListing.ts
  • src/server/api/routers/listings/admin.ts
  • src/server/api/routers/listings/core.test.ts
  • src/server/api/routers/listings/core.ts
  • src/server/api/routers/mobile/pcListings.ts
  • src/server/api/routers/pcListings.test.ts
  • src/server/api/routers/pcListings.ts
  • src/server/api/utils/pcListingHelpers.ts
  • src/server/repositories/listings.repository.ts
  • src/server/repositories/pc-listings.repository.test.ts
  • src/server/repositories/pc-listings.repository.ts
  • src/server/services/author-risk.service.ts
  • src/server/services/review-risk.service.ts
  • src/server/services/submission-risk.service.test.ts
  • src/server/utils/custom-field-values.ts
  • src/server/utils/query-builders.test.ts
  • src/server/utils/query-builders.ts
  • src/utils/custom-field-validation.ts
  • src/utils/custom-fields.ts
  • tests/filtering.spec.ts
  • tests/helpers/data-factory.ts
  • tests/pages/ListingsPage.ts
💤 Files with no reviewable changes (1)
  • duplication-audit.md
✅ Files skipped from review due to trivial changes (2)
  • src/server/utils/custom-field-values.ts
  • .gitignore
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/server/services/submission-risk.service.test.ts
  • src/server/services/author-risk.service.ts
  • src/lib/cors.test.ts
  • src/server/repositories/listings.repository.ts

Comment thread src/server/utils/query-builders.ts Outdated
Comment thread src/utils/custom-field-validation.ts
Comment thread src/app/listings/components/shared/utils/customFieldHelpers.tsx Outdated
Comment thread pnpm-workspace.yaml Outdated
Comment thread src/app/listings/new/form-schemas/createDynamicListingSchema.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/utils/custom-field-validation.ts (1)

57-71: 💤 Low value

Consider adding a default case for defensive coding.

The switch on field.type lacks a default case. If CustomFieldType is extended or runtime data has an unexpected type, the function returns undefined, causing line 94 to treat valid values as invalid. While TypeScript's exhaustiveness checking mitigates compile-time risk, a defensive default prevents silent failures.

🛡️ Suggested defensive default
     case CustomFieldType.TEXT:
     case CustomFieldType.TEXTAREA:
       return typeof value === 'string'
+    default:
+      return true
   }
 }
🤖 Prompt for 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.

In `@src/utils/custom-field-validation.ts` around lines 57 - 71, The switch in
isCustomFieldValueValid can fall through and return undefined for unknown
CustomFieldType values; add a defensive default branch in the switch on
field.type (inside isCustomFieldValueValid) that returns false (or logs/throws
if you prefer strict behavior) so the function always returns a boolean for any
runtime value of CustomFieldType and avoids silent failures when new types are
added or invalid data is passed.
🤖 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 `@src/server/init.ts`:
- Around line 231-233: The current initializeServerOnce sets
globalForServices.initializationPromise to
initializeServer().catch(logger.error), which swallows errors and leaves
initializationPromise fulfilled with undefined, preventing retries; change the
catch handler so that on error it clears globalForServices.initializationPromise
(set it back to undefined or delete it) and then re-throws the error (or returns
a rejected promise) so failures aren’t cached; update the initializeServerOnce
implementation (referencing initializeServerOnce,
globalForServices.initializationPromise, initializeServer, and logger.error) to
perform this reset-before-rethrow behavior.

---

Nitpick comments:
In `@src/utils/custom-field-validation.ts`:
- Around line 57-71: The switch in isCustomFieldValueValid can fall through and
return undefined for unknown CustomFieldType values; add a defensive default
branch in the switch on field.type (inside isCustomFieldValueValid) that returns
false (or logs/throws if you prefer strict behavior) so the function always
returns a boolean for any runtime value of CustomFieldType and avoids silent
failures when new types are added or invalid data is passed.
🪄 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: 3b28834d-7bee-4335-86ed-e5e1f76dbe78

📥 Commits

Reviewing files that changed from the base of the PR and between cc15640 and 914aa8b.

📒 Files selected for processing (33)
  • .github/pull_request_template.md
  • instrumentation.ts
  • src/app/admin/approvals/components/ApprovalModal.tsx
  • src/app/admin/approvals/page.tsx
  • src/app/admin/layout.tsx
  • src/app/admin/loading.tsx
  • src/app/admin/pc-listing-approvals/components/ApprovalModal.tsx
  • src/app/admin/pc-listing-approvals/page.tsx
  • src/app/listings/components/shared/CustomFieldRenderer.tsx
  • src/app/listings/components/shared/utils/customFieldHelpers.test.ts
  • src/app/listings/components/shared/utils/customFieldHelpers.tsx
  • src/app/listings/new/NewListingPage.tsx
  • src/app/listings/new/form-schemas/createDynamicListingSchema.test.ts
  • src/app/listings/new/form-schemas/createDynamicListingSchema.ts
  • src/app/listings/new/hooks/__tests__/useEmulatorConfigImporter.test.tsx
  • src/app/listings/new/hooks/useEmulatorConfigImporter.tsx
  • src/app/pc-listings/new/NewPcListingPage.tsx
  • src/app/pc-listings/new/form-schemas/createDynamicPcListingSchema.test.ts
  • src/app/pc-listings/new/form-schemas/createDynamicPcListingSchema.ts
  • src/app/terms/page.tsx
  • src/components/admin/index.ts
  • src/components/admin/review-risk/ReviewRiskIndicator.test.tsx
  • src/components/admin/review-risk/ReviewRiskIndicator.tsx
  • src/components/admin/review-risk/ReviewRiskWarningBanner.test.tsx
  • src/components/admin/review-risk/ReviewRiskWarningBanner.tsx
  • src/components/admin/review-risk/index.ts
  • src/components/admin/review-risk/reviewRiskDisplay.ts
  • src/components/ui/index.ts
  • src/server/init.ts
  • src/server/utils/query-builders.test.ts
  • src/server/utils/query-builders.ts
  • src/utils/custom-field-validation.test.ts
  • src/utils/custom-field-validation.ts
💤 Files with no reviewable changes (1)
  • src/app/listings/components/shared/utils/customFieldHelpers.tsx
✅ Files skipped from review due to trivial changes (10)
  • .github/pull_request_template.md
  • src/app/listings/new/hooks/useEmulatorConfigImporter.tsx
  • src/app/listings/new/form-schemas/createDynamicListingSchema.test.ts
  • src/app/listings/new/hooks/tests/useEmulatorConfigImporter.test.tsx
  • src/app/listings/components/shared/utils/customFieldHelpers.test.ts
  • src/app/admin/loading.tsx
  • src/app/admin/layout.tsx
  • src/app/terms/page.tsx
  • src/app/pc-listings/new/form-schemas/createDynamicPcListingSchema.test.ts
  • src/app/listings/components/shared/CustomFieldRenderer.tsx
🚧 Files skipped from review as they are similar to previous changes (8)
  • src/app/listings/new/NewListingPage.tsx
  • src/server/utils/query-builders.ts
  • src/server/utils/query-builders.test.ts
  • src/app/pc-listings/new/NewPcListingPage.tsx
  • src/app/admin/approvals/components/ApprovalModal.tsx
  • src/app/admin/pc-listing-approvals/components/ApprovalModal.tsx
  • src/app/admin/approvals/page.tsx
  • src/app/admin/pc-listing-approvals/page.tsx

Comment thread src/server/init.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant