Skip to content

feat: v0.15.0 - #469

Merged
Producdevity merged 111 commits into
masterfrom
staging
Aug 31, 2026
Merged

feat: v0.15.0#469
Producdevity merged 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="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',

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>

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

@coderabbitai create a new issue/ticket for this

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 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}

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

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

@coderabbitai create a new issue/ticket for this

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

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

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

no

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

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

@coderabbitai verify claim and create a new issue/ticket for this

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 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
done

Length 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.

Comment thread src/data/storageKeys.ts
<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>

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

@coderabbitai create a new issue/ticket for this

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 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)

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>

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

@coderabbitai create a new issue/ticket for this

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 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.

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