feat: v0.15.0 - #469
Conversation
Fix GPU view modal close button
Add comment submit shortcut
…ection Lazy load language detection with franc-min
…ication-tags Remove unused SEO verification tags
Use pagination helper in SoCs router
…based game image validation and rendering
…outer Refactor hardware modules around feature-owned architecture
…tch lookup, and trpc caching
Server hardening
test: add handheld listing repository tests and fix search filters
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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 |
There was a problem hiding this comment.
40 issues found across 423 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/app/games/components/GameCard.tsx">
<violation number="1" location="src/app/games/components/GameCard.tsx:37">
P2: For below-the-fold cards (`eagerLoad` false) the `loading` value resolves to `undefined`, so external images render as plain `<img>` tags with no `loading` attribute and load eagerly. The previous `priority={false}` path made next/image lazy-load these images by default. Set `loading` to `'lazy'` when not eager to preserve lazy loading on the games grid.</violation>
<violation number="2" location="src/app/games/components/GameCard.tsx:37">
P2: For the four above-fold cards, replacing `priority` with `loading="eager"` removes Next's preload and high fetch-priority behavior, which can delay the first-row images. Pass the eager flag through `preload` so `ImageRenderer` preserves the previous loading priority for both Next and native images.</violation>
</file>
<file name="src/app/pc-listings/[id]/components/EditPcListingButton.tsx">
<violation number="1" location="src/app/pc-listings/[id]/components/EditPcListingButton.tsx:23">
P2: Removing the 60s refetchInterval makes the remaining-minutes countdown and the edit-time-expired transition stale while the page stays open. remainingMinutes and timeExpired are computed server-side per query, so with only `refetchOnWindowFocus: true` the button title keeps showing a stale 'N minutes remaining after approval' and the button stays enabled even after the 60-minute window passes, until the user refocuses or reloads. Restore a short polling interval (or decrement client-side) so the countdown/expiry stay accurate.</violation>
</file>
<file name="prisma/seeders/usersSeeder.ts">
<violation number="1" location="prisma/seeders/usersSeeder.ts:81">
P3: The newly added VerifiedDeveloper cleanup (handling the restricted `verifiedBy` foreign key) lives in `cleanupExistingUsers`, which is never reached: the only call site `seed.ts` invokes `usersSeeder(prisma)` with the default `shouldCleanup = false`. Wire a cleanup path (pass `true` where a reset seed is intended) or drop the unreachable branch so the FK-guard code is actually exercised.</violation>
<violation number="2" location="prisma/seeders/usersSeeder.ts:143">
P2: On every rerun of `usersSeeder`, existing developer/emulator verification rows have their audit verifier and notes replaced with the seed values. Preserve existing metadata in the upsert update so rerunning `pnpm db:seed` does not erase administrator provenance and notes.</violation>
<violation number="3" location="prisma/seeders/usersSeeder.ts:234">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**
This added persistent cleanup and emulator-assignment behavior has no meaningful tests for deletion, idempotent upserts, verifier fallback, or the empty-emulator path. Add focused seeder tests before shipping this behavior.</violation>
</file>
<file name="src/app/games/[id]/components/GameEditForm.tsx">
<violation number="1" location="src/app/games/[id]/components/GameEditForm.tsx:421">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**
This adds a moderator-gated IGDB provider without tests for either side of the boundary. Add `ImageSelectorSwitcher` or `GameEditForm` tests that verify moderators see IGDB and non-moderators do not.</violation>
</file>
<file name="src/features/hardware/gpu/client/admin/GpuFormModal.tsx">
<violation number="1" location="src/features/hardware/gpu/client/admin/GpuFormModal.tsx:50">
P2: When an admin adds the first GPU for a brand, this query hides that brand because `category: 'gpu'` requires an existing GPU. Query all brands here so newly created brands can receive their first GPU.</violation>
<violation number="2" location="src/features/hardware/gpu/client/admin/GpuFormModal.tsx:88">
P3: The label points to `brand`, but `Autocomplete` receives no `id`, so its input has no matching id and the label is not associated. Pass `id="brand"` to the autocomplete input.</violation>
</file>
<file name="src/app/admin/trust-logs/page.tsx">
<violation number="1" location="src/app/admin/trust-logs/page.tsx:156">
P2: The 'Trusted+ Users' stat counts every level with minScore >= 250, which includes the base 'Trusted' level (250-499), not just 'Trusted+' and above. This makes the displayed number higher than the label implies. Filter on minScore >= 500 for 'Trusted+' level and above, or rename the stat if 'Trusted and above' is intended.</violation>
<violation number="2" location="src/app/admin/trust-logs/page.tsx:157">
P2: When no users are Trusted or above, `trustedPlusUsers` is `0`, but this card displays `...` instead of `0`. Preserve zero values in `AdminStatsDisplay` so the statistic is accurate.</violation>
</file>
<file name="src/features/hardware/cpu/client/admin/CpuTable.tsx">
<violation number="1" location="src/features/hardware/cpu/client/admin/CpuTable.tsx:34">
P2: Keyboard-only users cannot sort the new CPU table because `SortableHeader` puts the click handler directly on a non-focusable `<th>`. Make the sortable header keyboard-accessible, such as by rendering a button inside the header or adding equivalent focus and keyboard handling.</violation>
</file>
<file name="config/image-hosts.ts">
<violation number="1" location="config/image-hosts.ts:14">
P2: When a nested Clerk subdomain reaches `ImageRenderer`, this pattern makes the shared matcher select `next/image`, but Next only allows one subdomain for `*.clerk.com`. Make the matcher and Next pattern semantics identical, or replace the wildcard with the exact Clerk image hosts, so these URLs use the correct rendering path.</violation>
</file>
<file name="src/components/ui/form/async-multi-select/AsyncMultiSelect.tsx">
<violation number="1" location="src/components/ui/form/async-multi-select/AsyncMultiSelect.tsx:167">
P2: When an existing selection is not on the current page, adding another option while the `getByIds` query refreshes emits incomplete `selectedOptions`. The listings and PC sidebar handlers record those names in analytics, so retain previous option metadata before invoking `onChange`.</violation>
</file>
<file name="src/features/hardware/gpu/shared/gpu.schemas.ts">
<violation number="1" location="src/features/hardware/gpu/shared/gpu.schemas.ts:35">
P2: The mobile GPU catalog accepts negative, zero, and fractional pagination values here. `listMobileCompatibility` passes them through to Prisma, so malformed requests reach invalid `take`/`skip` values and fail as server errors; add integer bounds while retaining the compatibility route's unbounded upper limit.</violation>
</file>
<file name="src/app/listings/[id]/components/EditListingButton.tsx">
<violation number="1" location="src/app/listings/[id]/components/EditListingButton.tsx:23">
P2: Removing `refetchInterval: 60000` stops the canEdit query from refreshing while the page is open, so the "X minutes remaining" text freezes and the Edit button stays enabled after the 60-minute window expires until the user refocuses the window. This directly undoes the interval's stated purpose ("update time remaining"). The 60-minute limit is still enforced server-side on update, so this is a UI staleness regression; either keep a polling interval or add a client-side countdown that updates remainingMinutes/timeExpired.</violation>
</file>
<file name="src/server/api/mobileContext.ts">
<violation number="1" location="src/server/api/mobileContext.ts:123">
P2: When `Authorization` contains the `ApiKey` scheme without a token, this guard returns no credential, so the request bypasses the new strict invalid-API-key path. Reject an empty `ApiKey` credential instead of falling through to anonymous access.</violation>
</file>
<file name="src/features/hardware/cpu/shared/cpu.schemas.ts">
<violation number="1" location="src/features/hardware/cpu/shared/cpu.schemas.ts:35">
P2: Malformed mobile catalog pagination reaches Prisma because this schema does not validate integer, positive, or nonnegative values. Validate `limit`, `offset`, and `page` while retaining the intentionally supported high limits such as 1000.</violation>
<violation number="2" location="src/features/hardware/cpu/shared/cpu.schemas.ts:46">
P2: The PC-listing CPU endpoint accepts fractional limits, but Prisma requires `take` to be an integer. Add `.int()` so malformed limits are rejected at the API boundary.</violation>
</file>
<file name="next.config.ts">
<violation number="1" location="next.config.ts:154">
P2: Uploads between 5,000,001 and 5,242,880 bytes pass `handleFileUpload` but fail when `next/image` optimizes them. Set this limit to the same 5 MiB value used by `MAX_FILE_SIZE`.</violation>
<violation number="2" location="next.config.ts:161">
P1: Removing `dangerouslyAllowSVG: true` while `getSafePlaceholderImageUrl` still returns `https://placehold.co/...` URLs breaks every placeholder/fallback image in production.</violation>
</file>
<file name="src/app/listings/components/ListingsFiltersSidebar.tsx">
<violation number="1" location="src/app/listings/components/ListingsFiltersSidebar.tsx:77">
P3: When a selected device/SoC's data hasn't loaded yet (getByIds still in flight, e.g. right after page load with filters restored from URL), `selectedOptions` omits it, so the analytics `names` array no longer matches `values`. The old code resolved names from the full preloaded lookup array instead. Track names from a source that includes all values, or accept the gap.</violation>
</file>
<file name="src/features/hardware/cpu/client/admin/CpuFormModal.tsx">
<violation number="1" location="src/features/hardware/cpu/client/admin/CpuFormModal.tsx:50">
P1: When the first CPU is added for an existing brand with no CPUs, this query omits that brand, so the form cannot create that first CPU. Load all brands here and reserve the category filter for the CPU table filter.</violation>
<violation number="2" location="src/features/hardware/cpu/client/admin/CpuFormModal.tsx:88">
P3: The Brand label is not associated with this autocomplete because the rendered input has no `id="brand"`. Pass `id="brand"` so screen readers announce the field and clicking the label focuses it.</violation>
</file>
<file name="src/components/ui/image-selectors/AdminImageSelectorSwitcher.tsx">
<violation number="1" location="src/components/ui/image-selectors/AdminImageSelectorSwitcher.tsx:42">
P3: This effect overwrites the user's in-progress `manualUrl` on every change of `selectedImageUrl`. If the prop updates for any reason while the user is typing a new manual URL (e.g. async re-sync or form update), their input is silently replaced with the applied value. Guard the reset so it only runs when the value actually differs, or track a dirty flag.</violation>
</file>
<file name="src/app/admin/performance/components/ReplacementSelectionModal.tsx">
<violation number="1" location="src/app/admin/performance/components/ReplacementSelectionModal.tsx:43">
P2: After a successful replacement or cancel, reopening this mounted modal reuses the previous `selectedReplacementId`. Reset the selection when `scaleToDelete` changes or the modal closes before sending it as the next replacement.</violation>
</file>
<file name="src/features/hardware/gpu/server/persistence/gpu.errors.ts">
<violation number="1" location="src/features/hardware/gpu/server/persistence/gpu.errors.ts:9">
P3: This GPU translator duplicates `translateCpuWriteError` line-for-line, so future Prisma error-mapping fixes can diverge between the CPU and GPU paths. Extract the shared translation logic and supply resource-specific errors as parameters.</violation>
</file>
<file name="src/app/home/components/HomeTrendingDevices.tsx">
<violation number="1" location="src/app/home/components/HomeTrendingDevices.tsx:21">
P2: After one minute, remounting the home page marks `trendingSummary` stale, and after five minutes it discards the result entirely, causing repeated network requests for a summary whose server cache lasts six hours. Restore the six-hour `staleTime` and twelve-hour `gcTime` (or add an endpoint-specific default) to avoid this unnecessary traffic.</violation>
</file>
<file name="src/app/profile/components/connections/SocialConnectionList.tsx">
<violation number="1" location="src/app/profile/components/connections/SocialConnectionList.tsx:41">
P3: Assistive technology does not receive a loading status here: a plain `div` has no semantic role, and every `Skeleton` child is `aria-hidden`. Give the wrapper `role="status"` and `aria-busy="true"` so loading is announced.</violation>
</file>
<file name="src/app/admin/games/[id]/form-schemas/updateGameSchema.ts">
<violation number="1" location="src/app/admin/games/[id]/form-schemas/updateGameSchema.ts:7">
P2: When a game still has a legacy `http://` image URL, submitting any unrelated edit now fails validation because this schema validates the defaulted image field. Preserve unchanged legacy values or migrate existing image URLs before enforcing HTTPS here.</violation>
</file>
<file name="src/app/admin/audit-logs/page.tsx">
<violation number="1" location="src/app/admin/audit-logs/page.tsx:184">
P2: Keyboard-only and assistive-technology users cannot select another action or entity because `Dropdown` exposes click-only, non-focusable options. Restore native selects here or make `Dropdown` keyboard-accessible with listbox semantics.</violation>
</file>
<file name="src/data/storageKeys.ts">
<violation number="1" location="src/data/storageKeys.ts:40">
P3: After upgrading, existing handheld processed-report column preferences are discarded because `useColumnVisibility` reads only this new localStorage key. Keep the old key or migrate its value before switching names.</violation>
</file>
<file name="src/app/api/trpc/[trpc]/route.ts">
<violation number="1" location="src/app/api/trpc/[trpc]/route.ts:21">
P2: When an anonymous cacheable lookup is stored, a shared cache can serve that object to an authenticated request before this handler runs because the response does not vary on auth headers. Add `Vary` for `Cookie`, `Authorization`, `x-api-key`, and `x-auth-token` so the private policy is enforceable at the cache boundary.</violation>
</file>
<file name="src/components/ui/ImageRenderer.tsx">
<violation number="1" location="src/components/ui/ImageRenderer.tsx:42">
P2: When an external image omits `preload` and `loading`, this native branch eagerly downloads it instead of preserving `next/image`'s default lazy loading. Pages with many external game images will fetch below-the-fold full-size images immediately; default `loading` to `lazy` unless `preload` is enabled.</violation>
</file>
<file name="src/data/constants.ts">
<violation number="1" location="src/data/constants.ts:7">
P2: When the notification menu stays closed, `NotificationCenter` now refreshes the unread badge only every 10 minutes, so new notifications can remain hidden substantially longer than before. Keep a dedicated notification interval (or use `SHORT`) rather than reusing `EXTRA_LONG` for this user-facing count.</violation>
</file>
<file name="src/lib/api.tsx">
<violation number="1" location="src/lib/api.tsx:31">
P2: After a performance scale changes, listing forms can show the old scale list for up to six hours because this procedure has no matching invalidation. Invalidate `listings.performanceScales` from performance-scale mutations or keep this query on the shorter default cache.</violation>
</file>
<file name="src/scripts/api/mobile-schema-registry.ts">
<violation number="1" location="src/scripts/api/mobile-schema-registry.ts:15">
P2: For mobile endpoints using schemas outside these six modules, `getMobileApiSchema` returns null, so the generated OpenAPI omits request parameters/bodies and response schema references. Register every schema module used by `src/server/api/routers/mobile` before generating the docs.</violation>
</file>
<file name="src/hooks/useTranslation.tsx">
<violation number="1" location="src/hooks/useTranslation.tsx:29">
P2: When content changes and then returns to a previously translated value, the translation auto-applies without the user toggling it. `translatedContentKey` and `translationState` are never reset on content change (only `translationOption` is), and `showTranslated` is derived purely from content-key equality, so switching A→B→A renders the translated text on the final A automatically. Clear `translatedContentKey` whenever `content` changes so cached `translationState` can still be used on demand but never auto-shows.</violation>
</file>
<file name="src/app/admin/dashboard/components/ActivityCard/ReportActivityItem.tsx">
<violation number="1" location="src/app/admin/dashboard/components/ActivityCard/ReportActivityItem.tsx:14">
P2: The admin dashboard 'Report Activity' item now deep-links '→ Review' to the public listing page (`/listings/:id` or `/pc-listings/:id`) instead of the admin reports review surface. Moderators clicking 'Review' land on a public page with no moderation context. Either route to the admin report review flow for the target report, or if this redirect is intended, update the CTA label so it no longer implies a moderation action.</violation>
</file>
<file name="src/app/listings/shared/components/GameImage.tsx">
<violation number="1" location="src/app/listings/shared/components/GameImage.tsx:76">
P2: When `preload` is false, external images get no `loading` attribute and load eagerly instead of lazily. The previous `priority={false}` default lazy-loaded these via next/image. Force lazy loading when not preloaded.</violation>
</file>
Note: This PR contains a large number of files. cubic selects up to 200 of the highest-priority eligible files for this review, so some files may not have been reviewed.
Re-trigger cubic
| const updateCpu = api.cpus.update.useMutation() | ||
| const deviceBrandsQuery = api.deviceBrands.get.useQuery({ | ||
| limit: PAGINATION.MAX_LIMIT, | ||
| category: 'cpu', |
There was a problem hiding this comment.
P1: When the first CPU is added for an existing brand with no CPUs, this query omits that brand, so the form cannot create that first CPU. Load all brands here and reserve the category filter for the CPU table filter.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/hardware/cpu/client/admin/CpuFormModal.tsx, line 50:
<comment>When the first CPU is added for an existing brand with no CPUs, this query omits that brand, so the form cannot create that first CPU. Load all brands here and reserve the category filter for the CPU table filter.</comment>
<file context>
@@ -0,0 +1,135 @@
+ const updateCpu = api.cpus.update.useMutation()
+ const deviceBrandsQuery = api.deviceBrands.get.useQuery({
+ limit: PAGINATION.MAX_LIMIT,
+ category: 'cpu',
+ })
+
</file context>
| { protocol: 'https', hostname: 'storage.ko-fi.com', pathname: '/**' }, | ||
| { protocol: 'https', hostname: 'ko-fi.com', pathname: '/**' }, | ||
| ], | ||
| remotePatterns: NEXT_IMAGE_REMOTE_PATTERNS, |
There was a problem hiding this comment.
P1: Removing dangerouslyAllowSVG: true while getSafePlaceholderImageUrl still returns https://placehold.co/... URLs breaks every placeholder/fallback image in production.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At next.config.ts, line 161:
<comment>Removing `dangerouslyAllowSVG: true` while `getSafePlaceholderImageUrl` still returns `https://placehold.co/...` URLs breaks every placeholder/fallback image in production.</comment>
<file context>
@@ -167,26 +149,16 @@ function createContentSecurityPolicy(): string {
- { protocol: 'https', hostname: 'storage.ko-fi.com', pathname: '/**' },
- { protocol: 'https', hostname: 'ko-fi.com', pathname: '/**' },
- ],
+ remotePatterns: NEXT_IMAGE_REMOTE_PATTERNS,
},
</file context>
There was a problem hiding this comment.
@coderabbitai create a new issue/ticket for this
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
repo="Producdevity/EmuReady"
body=$(cat <<'EOF'
## Summary
The Next.js image configuration removes `dangerouslyAllowSVG: true`. The `getSafePlaceholderImageUrl` helper still produces `https://placehold.co/...` placeholder URLs. This can prevent placeholder and fallback images from rendering in production when the remote response is SVG.
## Required changes
- Review `getSafePlaceholderImageUrl` and all placeholder image call sites.
- Make the placeholder format compatible with the current `next/image` SVG policy, or explicitly configure the required safe SVG handling.
- Keep the remote image allowlist aligned with the selected placeholder solution.
## Rationale
Placeholder and fallback images must render when a primary image is unavailable. The current configuration change can break this behavior for `placehold.co` SVG responses.
## Affected areas
- `next.config.ts`
- The module that defines `getSafePlaceholderImageUrl`
- Components that use placeholder or fallback image URLs
## Acceptance criteria
- Placeholder and fallback images render in production.
- The selected solution does not re-enable unsafe SVG processing without the required safeguards.
- The configured `remotePatterns` allow the selected placeholder host and path.
- Add or update focused coverage where practical.
## Backlinks
- Pull request: https://github.com/Producdevity/EmuReady/pull/469
- Review comment: https://github.com/Producdevity/EmuReady/pull/469#discussion_r3840188993
- Requested by: `@Producdevity`
EOF
)
gh issue create \
--repo "$repo" \
--title "Fix placeholder images after disabling Next.js SVG image support" \
--body "$body" \
--assignee "Producdevity"Length of output: 1900
@Producdevity, created follow-up issue #472: Fix placeholder images after disabling Next.js SVG image support.
You are interacting with an AI system.
| className="object-cover" | ||
| sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw" | ||
| priority={props.priority ?? false} | ||
| loading={props.eagerLoad ? 'eager' : undefined} |
There was a problem hiding this comment.
P2: For below-the-fold cards (eagerLoad false) the loading value resolves to undefined, so external images render as plain <img> tags with no loading attribute and load eagerly. The previous priority={false} path made next/image lazy-load these images by default. Set loading to 'lazy' when not eager to preserve lazy loading on the games grid.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/app/games/components/GameCard.tsx, line 37:
<comment>For below-the-fold cards (`eagerLoad` false) the `loading` value resolves to `undefined`, so external images render as plain `<img>` tags with no `loading` attribute and load eagerly. The previous `priority={false}` path made next/image lazy-load these images by default. Set `loading` to `'lazy'` when not eager to preserve lazy loading on the games grid.</comment>
<file context>
@@ -28,13 +28,13 @@ function GameCard(props: Props) {
className="object-cover"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
- priority={props.priority ?? false}
+ loading={props.eagerLoad ? 'eager' : undefined}
unoptimized
/>
</file context>
| loading={props.eagerLoad ? 'eager' : undefined} | |
| loading={props.eagerLoad ? 'eager' : 'lazy'} |
There was a problem hiding this comment.
@coderabbitai create a new issue/ticket for this
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
repo="Producdevity/EmuReady"
body=$(cat <<'EOF'
## Summary
Restore lazy loading for non-eager game-card images.
## Required change
In `src/app/games/components/GameCard.tsx`, set `ImageRenderer` to use `loading="lazy"` when `props.eagerLoad` is false. Keep `loading="eager"` when `props.eagerLoad` is true.
## Rationale
The current `undefined` value omits the `loading` attribute. For external images rendered as plain `<img>` elements, this can cause below-the-fold cards to load eagerly.
## Affected area
- `src/app/games/components/GameCard.tsx`
- `GameCard`
- `ImageRenderer` loading behavior
## Acceptance criteria
- Cards with `eagerLoad={true}` render with `loading="eager"`.
- Cards with `eagerLoad` false or omitted render with `loading="lazy"`.
- The games grid does not eagerly load below-the-fold external images.
## Backlinks
- Pull request: https://github.com/Producdevity/EmuReady/pull/469
- Review comment: https://github.com/Producdevity/EmuReady/pull/469#discussion_r3840188995
- Requested by: `@Producdevity`
EOF
)
gh issue create \
--repo "$repo" \
--title "Restore lazy loading for non-eager game cards" \
--body "$body" \
--assignee "Producdevity"Length of output: 1403
@Producdevity, created and assigned issue #473: Restore lazy loading for non-eager game cards.
You are interacting with an AI system.
| { | ||
| enabled: !!user?.id, | ||
| refetchInterval: 60000, // Refetch every minute to update time remaining | ||
| refetchOnWindowFocus: true, |
There was a problem hiding this comment.
P2: Removing the 60s refetchInterval makes the remaining-minutes countdown and the edit-time-expired transition stale while the page stays open. remainingMinutes and timeExpired are computed server-side per query, so with only refetchOnWindowFocus: true the button title keeps showing a stale 'N minutes remaining after approval' and the button stays enabled even after the 60-minute window passes, until the user refocuses or reloads. Restore a short polling interval (or decrement client-side) so the countdown/expiry stay accurate.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/app/pc-listings/[id]/components/EditPcListingButton.tsx, line 23:
<comment>Removing the 60s refetchInterval makes the remaining-minutes countdown and the edit-time-expired transition stale while the page stays open. remainingMinutes and timeExpired are computed server-side per query, so with only `refetchOnWindowFocus: true` the button title keeps showing a stale 'N minutes remaining after approval' and the button stays enabled even after the 60-minute window passes, until the user refocuses or reloads. Restore a short polling interval (or decrement client-side) so the countdown/expiry stay accurate.</comment>
<file context>
@@ -20,7 +20,7 @@ function EditPcListingButton(props: Props) {
{
enabled: !!user?.id,
- refetchInterval: 60000, // Refetch every minute to update time remaining
+ refetchOnWindowFocus: true,
},
)
</file context>
| refetchOnWindowFocus: true, | |
| refetchInterval: 60000, // Refetch every minute to update time remaining |
| // isn't implemented in the backend yet. This is marked as TODO. | ||
| await deletePerformanceScale.mutateAsync({ | ||
| id: props.scaleToDelete.id, | ||
| replacementId: selectedReplacementId, |
There was a problem hiding this comment.
P2: After a successful replacement or cancel, reopening this mounted modal reuses the previous selectedReplacementId. Reset the selection when scaleToDelete changes or the modal closes before sending it as the next replacement.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/app/admin/performance/components/ReplacementSelectionModal.tsx, line 43:
<comment>After a successful replacement or cancel, reopening this mounted modal reuses the previous `selectedReplacementId`. Reset the selection when `scaleToDelete` changes or the modal closes before sending it as the next replacement.</comment>
<file context>
@@ -33,15 +33,14 @@ function ReplacementSelectionModal(props: Props) {
- // isn't implemented in the backend yet. This is marked as TODO.
await deletePerformanceScale.mutateAsync({
id: props.scaleToDelete.id,
+ replacementId: selectedReplacementId,
} satisfies RouterInput['performanceScales']['delete'])
} catch (err) {
</file context>
There was a problem hiding this comment.
@coderabbitai verify claim and create a new issue/ticket for this
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '== ReplacementSelectionModal references =='
rg -n -C 5 '\bReplacementSelectionModal\b' src
echo '== Candidate modal components =='
fd -t f '^Modal\.(tsx|ts)$' src | while IFS= read -r file; do
echo "--- $file"
ast-grep outline "$file" --items all --type function --match 'Modal' || true
doneLength of output: 4205
🏁 Script executed:
#!/bin/bash
set -euo pipefail
repo="Producdevity/EmuReady"
title="Reset replacement selection when the performance scale modal closes"
query='repo:Producdevity/EmuReady is:issue is:open "replacement selection"'
existing_url=$(gh issue list \
--repo "$repo" \
--state open \
--search "$query" \
--json url,title \
--jq '.[] | select(.title == "'"$title"'") | .url' \
| head -n 1)
if [ -n "$existing_url" ]; then
echo "EXISTING_ISSUE=$existing_url"
exit 0
fi
body=$(cat <<'EOF'
## Summary
`ReplacementSelectionModal` retains `selectedReplacementId` after the modal closes.
## Required change
Reset the selected replacement when the modal closes and when the target performance scale changes. The delete action must require a replacement selected during the current modal session.
## Rationale
The parent keeps the modal component mounted. Closing it sets `scaleToDelete` to `null`, but the component state persists. When an administrator opens the modal again, the next delete request can submit the prior replacement ID.
## Affected area
- `src/app/admin/performance/components/ReplacementSelectionModal.tsx`
- `src/app/admin/performance/page.tsx`
## Acceptance criteria
- Canceling the modal clears `selectedReplacementId`.
- A successful replacement clears `selectedReplacementId`.
- Opening the modal for a different performance scale starts with no replacement selected.
- The Delete and Replace button remains disabled until the administrator selects a replacement in the current session.
## Backlinks
- Pull request: https://github.com/Producdevity/EmuReady/pull/469
- Review comment: https://github.com/Producdevity/EmuReady/pull/469#discussion_r3840189013
- Requested by: `@Producdevity`
EOF
)
issue_url=$(gh issue create \
--repo "$repo" \
--title "$title" \
--body "$body" \
--assignee "Producdevity")
echo "CREATED_ISSUE=$issue_url"Length of output: 2080
@Producdevity, I verified the claim and created follow-up issue #474.
The issue documents the stale selectedReplacementId state, the affected files, and the required reset behavior. It is assigned to you.
You are interacting with an AI system.
| <label htmlFor="brand" className="block mb-2 font-medium text-gray-700 dark:text-gray-300"> | ||
| Brand | ||
| </label> | ||
| <Autocomplete |
There was a problem hiding this comment.
P3: The Brand label is not associated with this autocomplete because the rendered input has no id="brand". Pass id="brand" so screen readers announce the field and clicking the label focuses it.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/hardware/cpu/client/admin/CpuFormModal.tsx, line 88:
<comment>The Brand label is not associated with this autocomplete because the rendered input has no `id="brand"`. Pass `id="brand"` so screen readers announce the field and clicking the label focuses it.</comment>
<file context>
@@ -0,0 +1,135 @@
+ <label htmlFor="brand" className="block mb-2 font-medium text-gray-700 dark:text-gray-300">
+ Brand
+ </label>
+ <Autocomplete
+ value={brandId}
+ onChange={(value) => setBrandId(value ?? '')}
</file context>
|
|
||
| if (seedUsers.length > 0) { | ||
| const seedUserIds = seedUsers.map((user) => user.id) | ||
| await prisma.verifiedDeveloper.deleteMany({ |
There was a problem hiding this comment.
P3: The newly added VerifiedDeveloper cleanup (handling the restricted verifiedBy foreign key) lives in cleanupExistingUsers, which is never reached: the only call site seed.ts invokes usersSeeder(prisma) with the default shouldCleanup = false. Wire a cleanup path (pass true where a reset seed is intended) or drop the unreachable branch so the FK-guard code is actually exercised.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At prisma/seeders/usersSeeder.ts, line 81:
<comment>The newly added VerifiedDeveloper cleanup (handling the restricted `verifiedBy` foreign key) lives in `cleanupExistingUsers`, which is never reached: the only call site `seed.ts` invokes `usersSeeder(prisma)` with the default `shouldCleanup = false`. Wire a cleanup path (pass `true` where a reset seed is intended) or drop the unreachable branch so the FK-guard code is actually exercised.</comment>
<file context>
@@ -64,11 +63,27 @@ const users: UserData[] = [
+
+ if (seedUsers.length > 0) {
+ const seedUserIds = seedUsers.map((user) => user.id)
+ await prisma.verifiedDeveloper.deleteMany({
+ where: {
+ OR: [{ userId: { in: seedUserIds } }, { verifiedBy: { in: seedUserIds } }],
</file context>
| const handleDeviceChange = (values: string[], selectedOptions: Option[]) => { | ||
| props.onDeviceChange(values) | ||
| const names = getDeviceNames(props.devices, values) | ||
| const names = selectedOptions.map((option) => option.name) |
There was a problem hiding this comment.
P3: When a selected device/SoC's data hasn't loaded yet (getByIds still in flight, e.g. right after page load with filters restored from URL), selectedOptions omits it, so the analytics names array no longer matches values. The old code resolved names from the full preloaded lookup array instead. Track names from a source that includes all values, or accept the gap.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/app/listings/components/ListingsFiltersSidebar.tsx, line 77:
<comment>When a selected device/SoC's data hasn't loaded yet (getByIds still in flight, e.g. right after page load with filters restored from URL), `selectedOptions` omits it, so the analytics `names` array no longer matches `values`. The old code resolved names from the full preloaded lookup array instead. Track names from a source that includes all values, or accept the gap.</comment>
<file context>
@@ -79,18 +72,28 @@ function ListingsFiltersSidebar(props: FiltersProps) {
+ const handleDeviceChange = (values: string[], selectedOptions: Option[]) => {
props.onDeviceChange(values)
- const names = getDeviceNames(props.devices, values)
+ const names = selectedOptions.map((option) => option.name)
filterAnalytics.devices(values, names)
}
</file context>
There was a problem hiding this comment.
@coderabbitai create a new issue/ticket for this
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
repo="Producdevity/EmuReady"
body_file="$(mktemp)"
trap 'rm -f "$body_file"' EXIT
cat > "$body_file" <<'EOF'
## Summary
Ensure filter analytics sends a name for every selected device and SoC ID when selections are restored from the URL.
## Rationale
`handleDeviceChange` and `handleSocChange` in `src/app/listings/components/ListingsFiltersSidebar.tsx` currently derive analytics names from `selectedOptions`. While `getByIds` is still loading, `selectedOptions` can omit IDs restored from the URL. This produces a `names` array that does not correspond to all entries in `values`.
## Affected area
- `src/app/listings/components/ListingsFiltersSidebar.tsx`
- Device and SoC filter analytics
## Required changes
Use an analytics-name source that covers each selected ID, including selections that have not loaded in the current async options page. If a complete name cannot be resolved, define and apply explicit behavior for that ID.
## Acceptance criteria
- Device analytics does not silently omit names for URL-restored device IDs while option data is loading.
- SoC analytics does not silently omit names for URL-restored SoC IDs while option data is loading.
- The relationship between selected IDs and analytics names is documented in code or enforced by the implementation.
- Existing analytics behavior remains correct for normally loaded selections.
## Backlinks
- Pull request: https://github.com/Producdevity/EmuReady/pull/469
- Review comment: https://github.com/Producdevity/EmuReady/pull/469#discussion_r3840189098
- Requested by: `@Producdevity`
EOF
gh issue create \
--repo "$repo" \
--title "Preserve device and SoC filter analytics names during async option loading" \
--body-file "$body_file" \
--assignee "Producdevity"Length of output: 1980
@Producdevity, created and assigned follow-up issue #470: #470
You are interacting with an AI system.
|
|
||
| useEffect(() => { | ||
| const selectedImageUrl = props.selectedImageUrl ?? '' | ||
| setManualUrl(selectedImageUrl) |
There was a problem hiding this comment.
P3: This effect overwrites the user's in-progress manualUrl on every change of selectedImageUrl. If the prop updates for any reason while the user is typing a new manual URL (e.g. async re-sync or form update), their input is silently replaced with the applied value. Guard the reset so it only runs when the value actually differs, or track a dirty flag.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/components/ui/image-selectors/AdminImageSelectorSwitcher.tsx, line 42:
<comment>This effect overwrites the user's in-progress `manualUrl` on every change of `selectedImageUrl`. If the prop updates for any reason while the user is typing a new manual URL (e.g. async re-sync or form update), their input is silently replaced with the applied value. Guard the reset so it only runs when the value actually differs, or track a dirty flag.</comment>
<file context>
@@ -42,10 +37,16 @@ export function AdminImageSelectorSwitcher(props: Props) {
+ useEffect(() => {
+ const selectedImageUrl = props.selectedImageUrl ?? ''
+ setManualUrl(selectedImageUrl)
+ setIsValidUrl(selectedImageUrl ? !getGameImageUrlValidationError(selectedImageUrl) : false)
+ }, [props.selectedImageUrl])
</file context>
There was a problem hiding this comment.
@coderabbitai create a new issue/ticket for this
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
repo="Producdevity/EmuReady"
title="Prevent selectedImageUrl sync from overwriting manual image URL input"
body=$(cat <<'EOF'
## Summary
Prevent `AdminImageSelectorSwitcher` from replacing a user's in-progress `manualUrl` when `selectedImageUrl` changes.
## Rationale
The synchronization effect in `src/components/ui/image-selectors/AdminImageSelectorSwitcher.tsx` currently resets `manualUrl` whenever `selectedImageUrl` changes. An asynchronous form update or re-sync can therefore discard a manual URL that the user is still entering.
## Affected area
- `src/components/ui/image-selectors/AdminImageSelectorSwitcher.tsx`
- `AdminImageSelectorSwitcher`
- The `useEffect` that synchronizes `props.selectedImageUrl` into `manualUrl` and `isValidUrl`
## Required changes
Guard synchronization so it does not overwrite in-progress manual input. Use a value comparison or explicit dirty-state tracking. Preserve validation behavior for externally applied image URLs.
## Acceptance criteria
- A user can type a new manual URL without an unrelated `selectedImageUrl` update replacing the typed value.
- An intentional external image URL update still updates the selector state when appropriate.
- `isValidUrl` remains consistent with the displayed manual URL.
## Backlinks
- Pull request: https://github.com/Producdevity/EmuReady/pull/469
- Review comment: https://github.com/Producdevity/EmuReady/pull/469#discussion_r3840189100
- Requested by: `@Producdevity`
EOF
)
gh issue create \
--repo "$repo" \
--title "$title" \
--body "$body" \
--assignee "Producdevity"Length of output: 1799
@Producdevity, created follow-up issue #471: Prevent selectedImageUrl sync from overwriting manual image URL input.
You are interacting with an AI system.
Description
Releases v0.15.0 from
stagingto production.Full user, moderator, and developer notes are included in
RELEASE_NOTES.md.Type of change
How Has This Been Tested?
Screenshots (if applicable)
N/A
Checklist
Notes for reviewers
The experimental
/v2/listingsroute has been removed.The permissive legacy
x-api-keyfallback remains temporary compatibility behavior for shipped mobile clients. Explicit invalidAuthorization: ApiKey ...credentials are rejected.Summary by cubic
Ships v0.15.0: aligns PC report moderation with handheld, hardens public/mobile tRPC caching and CORS, and centralizes remote image handling while removing unused analytics and realtime code. Notable behavior changes: handheld report search now combines text and role visibility correctly; game images must be HTTPS from approved hosts; the image proxy route is removed.
x-api-keyfor public endpoints; Bearer remains for protected endpoints.config/image-hosts.ts; introduces sharedImageRenderer.@vercel/analyticsand@vercel/speed-insightsintegrations and related envs/CSP entries./api/notifications/stream,/api/proxy-image, and the experimental/v2/listings.db:backup.Bolded sections are not requested per constraints.
Written for commit fdf80a7. Summary will update on new commits.