Skip to content

feat: v0.15.0 - #469

Open
Producdevity wants to merge 111 commits into
masterfrom
staging
Open

feat: v0.15.0#469
Producdevity wants to merge 111 commits into
masterfrom
staging

Conversation

@Producdevity

@Producdevity Producdevity commented Aug 23, 2026

Copy link
Copy Markdown
Owner

Description

Releases v0.15.0 from staging to production.

  • adds the processed PC reports workflow and keeps moderation behavior aligned with handheld reports
  • notifies moderators about new handheld and PC reports submitted through the web or mobile API
  • fixes handheld report search so text filtering and role-based visibility are applied together
  • hardens public tRPC caching, mobile CORS/origin handling, and API-key authentication
  • moves CPU and GPU code into feature-owned modules and updates the mobile API contracts
  • reduces notification traffic and removes unused realtime, image proxy, analytics, popup, and v2 listings code

Full user, moderator, and developer notes are included in RELEASE_NOTES.md.

Type of change

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

How Has This Been Tested?

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

Screenshots (if applicable)

N/A

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

The experimental /v2/listings route has been removed.

The permissive legacy x-api-key fallback remains temporary compatibility behavior for shipped mobile clients. Explicit invalid Authorization: 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.

  • Adds a unified processed reports workflow:
    • New admin pages for handheld and PC processed reports with status overrides and parity across both report types.
    • Sends moderator notifications on new handheld and PC reports from web and mobile APIs.
  • Hardens APIs and caching:
    • Sets cache headers on web and mobile tRPC responses; refines mobile CORS/Vary handling.
    • Documents the Public Integration API; supports API keys via x-api-key for public endpoints; Bearer remains for protected endpoints.
  • Centralizes image handling and tightens validation:
    • Replaces proxying with Next Image remote patterns from config/image-hosts.ts; introduces shared ImageRenderer.
    • Requires HTTPS image URLs from approved providers; updates game edit validation and tests.
  • Cleans up and refactors:
    • Removes @vercel/analytics and @vercel/speed-insights integrations and related envs/CSP entries.
    • Drops /api/notifications/stream, /api/proxy-image, and the experimental /v2/listings.
    • Consolidates CPU/GPU into feature-owned modules; standardizes admin layouts and polling/cache intervals.
    • Improves retrocatalog device lookup handling and caching; updates tests.
    • Bumps service worker cache to v0.15.0; replaces Supabase-specific backup script with a single db:backup.

Bolded sections are not requested per constraints.

Written for commit fdf80a7. Summary will update on new commits.

Review in cubic

…ection

Lazy load language detection with franc-min
…ication-tags

Remove unused SEO verification tags
…outer

Refactor hardware modules around feature-owned architecture
test: add handheld listing repository tests and fix search filters
@vercel

vercel Bot commented Aug 23, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
emuready Building Building Aug 23, 2026 8:18pm
emuready (staging) Ready Ready Preview Aug 23, 2026 8:18pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 23, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f2da3381-7622-43fd-af1b-8bf5e487b67e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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

❤️ Share

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

@Producdevity
Producdevity marked this pull request as ready for review August 24, 2026 01:26

@cubic-dev-ai cubic-dev-ai 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.

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="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/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/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/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/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/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/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="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/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/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/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="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="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/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/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/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/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/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>

<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/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>

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',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>

Comment thread next.config.ts
{ protocol: 'https', hostname: 'storage.ko-fi.com', pathname: '/**' },
{ protocol: 'https', hostname: 'ko-fi.com', pathname: '/**' },
],
remotePatterns: NEXT_IMAGE_REMOTE_PATTERNS,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>

className="object-cover"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
priority={props.priority ?? false}
loading={props.eagerLoad ? 'eager' : undefined}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>
Suggested change
loading={props.eagerLoad ? 'eager' : undefined}
loading={props.eagerLoad ? 'eager' : 'lazy'}

{
enabled: !!user?.id,
refetchInterval: 60000, // Refetch every minute to update time remaining
refetchOnWindowFocus: true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>
Suggested change
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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>

Comment thread src/data/storageKeys.ts
adminListings: `${PREFIX}admin_listings_column_visibility`,
adminPerformance: `${PREFIX}admin_performance_column_visibility`,
adminProcessedListings: `${PREFIX}admin_processed_listings_column_visibility`,
adminProcessedListings: `${PREFIX}admin_processed_reports_column_visibility`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/data/storageKeys.ts, line 40:

<comment>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.</comment>

<file context>
@@ -39,7 +37,8 @@ const storageKeys = {
     adminListings: `${PREFIX}admin_listings_column_visibility`,
     adminPerformance: `${PREFIX}admin_performance_column_visibility`,
-    adminProcessedListings: `${PREFIX}admin_processed_listings_column_visibility`,
+    adminProcessedListings: `${PREFIX}admin_processed_reports_column_visibility`,
+    adminPcProcessedListings: `${PREFIX}admin_pc_processed_listings_column_visibility`,
     adminSoCs: `${PREFIX}admin_socs_column_visibility`,
</file context>

<label htmlFor="brand" className="block mb-2 font-medium text-gray-700 dark:text-gray-300">
Brand
</label>
<Autocomplete

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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({

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>


useEffect(() => {
const selectedImageUrl = props.selectedImageUrl ?? ''
setManualUrl(selectedImageUrl)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant