Skip to content

feat: add Looney copyright check, animated background export, and UI polish - #76

Merged
creatorcluster merged 14 commits into
creatorcluster:mainfrom
Coder-soft:feat/looney-check-and-background-generator-updates
Aug 11, 2026
Merged

feat: add Looney copyright check, animated background export, and UI polish#76
creatorcluster merged 14 commits into
creatorcluster:mainfrom
Coder-soft:feat/looney-check-and-background-generator-updates

Conversation

@Coder-soft

@Coder-soft Coder-soft commented Aug 11, 2026

Copy link
Copy Markdown

Summary

Five commits of new feature work that have been developed on the fork and are ready to be merged upstream.

What's included

1. Looney copyright check with rate limiting and history

feat: add Looney copyright check with rate limiting and history

Replaces the old external "Osmium" redirects with an in-app music copyright checker powered by the Looney service:

  • New /api/looney-check server route that proxies the Looney API with CORS, input validation, and a 50 MB upload cap
  • Supabase rate limiting (5 checks/day) across browser, ip, and account buckets via a consume_looney_check_rate_limit() Postgres function and a new looney_check_rate_limits table (RLS + service_role only)
  • Real-time SSE job-status streaming from the upstream Looney service with job recovery/polling on page refresh
  • New UI: LooneyCheckForm, LooneyResultDisplay, LooneyHistorySection, LooneyCheckDialog, LooneyRunningCheckDialog, and a shareable /gappa/check/:jobId result page
  • Copyright check button on music resource cards in the Resources Hub
  • Replaces the copyrightChecker.ts util, ResultsDisplay component, and old MusicCopyright implementation

2. Refined page interactions and fonts

fix: refine small page interactions and fonts

  • PopularTools cards get a looney-styled hover panel and reworked hover states
  • Minecraft nametag generator polish, plus new index.css utilities (Minecraft Five font, tool planks hover texture)

3. UI fonts, texture search, and tool card interactions

polish UI fonts, add texture search, and refine tool card interactions

  • Applies the font-geist family consistently across ErrorBoundary, BlogView, Community, Contact, NotFound
  • Fuzzy texture search with edit-distance scoring in the Background Generator
  • New 4K DCI, 5K, and 8K resolution options
  • Hover background images on PopularTools cards
  • Font preview loading simplified with the FontFace API; GitHub link removed from the footer

4. Randomized multi-image background patterns

feat: add randomized multi-image background patterns

  • Background Generator now supports multiple selected textures rendered as randomized patterns (grid / staggered / diagonal / scattered / random) using a seeded PRNG

5. Animated background export with pattern controls

feat: add animated background export with pattern controls

  • New Image vs GIF/Video output mode that records an animated WebM loop via MediaRecorder + canvas.captureStream
  • New controls: texture rotation, animation duration, FPS, and scroll distance
  • Copyright check button on resource cards switched to a hover-expanding image icon

Tests / verification

  • No automated test suite configured for this repo; verified via manual local build and preview.

Notes

  • Includes a new Supabase migration 20260810000000_looney_check_rate_limits.sql that must be applied.
  • Looney API key is read from LOONEY_API_KEY env var (optional).

Summary by CodeRabbit

  • New Features
    • Added Looney Checks for uploaded audio files and Spotify links.
    • Added live progress, check history, recovery for active checks, detailed result pages, and concurrency safeguards.
    • Added copyright-check actions to music resource cards.
    • Expanded the background generator with multi-image patterns, seeded randomization, animation previews, and WebM downloads.
    • Added selectable Minecraft nametag fonts.
  • UI Improvements
    • Updated navigation, labels, tool visuals, typography, icons, and hover effects.
  • Bug Fixes
    • Improved resource font loading and migration validation.

- Proxy Looney API behind /api/looney-check with CORS and input validation
- Add Supabase rate limiting (5 checks/day, IP+browser+account buckets)
- Implement streaming job status via SSE
- Add LooneyCheckForm, LooneyResultDisplay, and history components
- Replace /music-copyright and /gappa external redirects with in-app pages
- apply font-geist consistently across ErrorBoundary, BlogView, Community, Contact, NotFound
- add fuzzy texture search with edit-distance scoring to BackgroundGenerator
- add 4K DCI, 5K, and 8K resolution options
- add hover background images to PopularTools cards
- simplify font preview loading in ResourceCard with FontFace API
- remove GitHub link from Footer
- responsive tweaks to LooneyCheckForm tabs and MusicCopyright header
- add Image vs GIF/Video mode with WebM recording via MediaRecorder
- add texture rotation and animation duration, FPS, distance controls
- swap copyright check button to image icon with hover-expand
@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown

@Coder-soft is attempting to deploy a commit to the yamura3's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@Coder-soft, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 17 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8ca399d4-7db2-4cb0-aac0-a6e0c00ee1d1

📥 Commits

Reviewing files that changed from the base of the PR and between 01a4e08 and 2d48b5b.

📒 Files selected for processing (8)
  • src/components/LooneyCheckDialog.tsx
  • src/components/LooneyCheckForm.tsx
  • src/components/LooneyHistorySection.tsx
  • src/components/LooneyResultDisplay.tsx
  • src/components/LooneyRunningCheckDialog.tsx
  • src/pages/BackgroundGenerator.tsx
  • src/pages/LooneyResultPage.tsx
  • src/pages/MusicCopyright.tsx
📝 Walkthrough

Walkthrough

The change replaces the legacy copyright checker with a Looney job workflow, adds API proxying and Supabase rate limits, integrates checks with resources and history, expands background generation, and applies supporting UI and tooling updates.

Changes

Looney Checks and Background Generation

Layer / File(s) Summary
Looney API and rate-limit enforcement
api/looney-check.js, server.js, api/cors.js, supabase/migrations/*, src/integrations/supabase/*
The application adds validated Looney requests, multipart and URL inputs, polling, SSE forwarding, CORS handling, and browser/IP/account rate limits backed by Supabase.
Looney client workflow
src/types/looney.ts, src/utils/looneyChecker.ts, src/utils/looneyHistory.ts, src/pages/LooneyResultPage.tsx
The client adds Looney job types, job creation, streaming, recovery, timeout handling, result routes, and bounded local history persistence.
Looney UI and resource integration
src/App.tsx, src/pages/MusicCopyright.tsx, src/pages/ResourcesHub.tsx, src/components/Looney*.tsx, src/components/resources/*
/gappa now hosts Looney Checks. Forms, dialogs, progress history, structured results, and music-resource check actions are included.
Multi-image background and WebM generation
src/pages/BackgroundGenerator.tsx
The generator supports multiple image sources, searchable selections, seeded patterns, rotation, larger output sizes, animated WebM creation, progress, preview, download, and cleanup.
Supporting UI and tooling updates
src/components/*, src/pages/{BlogView,Community,Contact,NotFound}.tsx, src/index.css, src/utils/migrateResources.ts, src/hooks/useMinecraftMusic.ts, vite.config.ts
The change updates labels, icons, fonts, card visuals, animation typings, toggle behavior, migration validation, playlist typing, upload helpers, and Vite environment configuration.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant MusicCopyright
  participant LooneyCheckForm
  participant LooneyAPI
  participant LooneyService
  participant LooneyHistory
  User->>MusicCopyright: Select source and start check
  MusicCopyright->>LooneyCheckForm: Submit file or Spotify URL
  LooneyCheckForm->>LooneyAPI: Create Looney job
  LooneyAPI->>LooneyService: Forward validated request
  LooneyService-->>LooneyAPI: Return job events
  LooneyAPI-->>LooneyCheckForm: Stream progress and result
  LooneyCheckForm->>LooneyHistory: Save job state and result
  LooneyHistory-->>MusicCopyright: Publish history update
Loading

Poem

A rabbit checks the tunes with care,
Streams little job clouds through the air.
History keeps each result bright,
While patterns dance in video light.
New fonts hop onto every sign—
Looney checks are running fine!

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main Looney checker, animated background export, and UI refinement changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 12

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/pages/ResourcesHub.tsx (1)

343-354: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The copyright callback has no effect on the Minecraft music list.

ResourceCard renders the copyright button only when resource.category === "music" (src/components/resources/ResourceCard.tsx Line 182). This list supplies minecraft-music resources, so the button never appears. Either remove the prop here, or extend the condition in ResourceCard to include minecraft-music.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/ResourcesHub.tsx` around lines 343 - 354, Update the Minecraft
music ResourcesList usage and related ResourceCard behavior so onCheckCopyright
is effective for minecraft-music resources: either remove the unused callback
prop from this list or extend ResourceCard’s copyright-button condition to
include the minecraft-music category, preserving existing music behavior.
🟡 Minor comments (10)
src/pages/BackgroundGenerator.tsx-677-681 (1)

677-681: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Limit the size and count of uploaded images.

The input now accepts multiple files, and each file is stored in React state as a base64 data URL. Base64 adds about 33 % overhead. A user can select many large photos and exhaust tab memory before any pattern is drawn. Add a per-file size check and a maximum image count in handleImageUpload, and report rejected files with toast.error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/BackgroundGenerator.tsx` around lines 677 - 681, Update
handleImageUpload to enforce both a per-file size limit and a maximum number of
uploaded images before storing files in React state. Reject oversized files and
selections exceeding the count limit, reporting each rejection with toast.error,
while preserving valid uploads and the existing state update flow.
src/pages/BackgroundGenerator.tsx-965-976 (1)

965-976: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The video preview goes stale after a control change.

invalidateGeneration runs only for image selection changes. The sliders for spacing, opacity, scale, rotation, size, and the pattern selector do not clear videoUrl. After a user records a video and then changes any of those controls, the image preview regenerates through the debounced effect while this element still plays the old video, and "Download WebM" still saves the old file. Clear videoUrl in the debounced generation effect, or mark the video as outdated in the UI.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/BackgroundGenerator.tsx` around lines 965 - 976, Update the
debounced generation effect in BackgroundGenerator so changes to spacing,
opacity, scale, rotation, size, or pattern invalidate the existing video by
clearing videoUrl. Ensure the preview and Download WebM action no longer use the
stale recording while preserving the existing image regeneration flow.
src/pages/BackgroundGenerator.tsx-571-574 (1)

571-574: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

The tab label promises GIF output, but the tool exports WebM only.

handleCreateVideo records a WebM blob and handleVideoDownload saves a .webm file. Rename the tab to "Video" or "Animated (WebM)". Consider renaming the OutputMode value "gif" to "video" at Line 35 for the same reason.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/BackgroundGenerator.tsx` around lines 571 - 574, Update the tab
label in the TabsTrigger using value "gif" to accurately state that the tool
exports WebM, such as "Video" or "Animated (WebM)". Also rename the related
OutputMode value from "gif" to "video" and update all references, including
handleCreateVideo and handleVideoDownload, while preserving the existing WebM
export behavior.
src/pages/BackgroundGenerator.tsx-283-288 (1)

283-288: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The seed changes nothing for four of the five pattern styles.

random is consumed only by the Fisher-Yates shuffle at Lines 284-287, and that shuffle is read only in the "random" branch. For grid, staggered, diagonal, and scattered, the "Randomize" button at Lines 746-758 increments randomSeed, clears the preview, and regenerates an identical image. Apply seeded jitter (position, scale, or tileRotation) in those branches, or disable the button when the selected pattern ignores the seed.

Note that drawTile accepts tileRotation at Line 270 but no caller passes a non-zero value.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/BackgroundGenerator.tsx` around lines 283 - 288, Update the
non-random pattern branches in the rendering logic around drawTile so randomSeed
affects grid, staggered, diagonal, and scattered output, using the seeded random
source for deterministic position, scale, or tileRotation jitter. Ensure
drawTile receives the resulting non-zero rotation or equivalent seeded
variation, while preserving the existing random branch behavior.
src/pages/BackgroundGenerator.tsx-807-828 (1)

807-828: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Give the animation sliders an accessible name.

The label elements at Lines 809, 817, and 825 are not associated with the Radix sliders, and these sliders have no aria-label. A screen reader announces an unnamed slider. The rotation slider at Line 788 already sets aria-label. Apply the same attribute here.

🔧 Proposed fix
-            <Slider value={animationDuration} onValueChange={setAnimationDuration} min={2} max={12} step={1} className="pixel-corners" />
+            <Slider value={animationDuration} onValueChange={setAnimationDuration} min={2} max={12} step={1} aria-label="Animation duration in seconds" className="pixel-corners" />

Apply the same change to the frame rate and movement distance sliders.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/BackgroundGenerator.tsx` around lines 807 - 828, Add an aria-label
to the Duration, Frame rate, and Movement distance Slider components in the
animation controls, matching the accessible naming approach used by the rotation
slider. Use distinct labels that identify each slider’s setting.
src/components/PopularTools.tsx-99-109 (1)

99-109: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep IconArrowRight above the hover layers.

When tool.backgroundImage is set, the image and gradient are absolutely positioned. The icon container and text use z-10, but IconArrowRight does not. On hover, the gradient can cover or reduce the contrast of the arrow.

Add relative z-10 to IconArrowRight or to its containing row.

Proposed fix
-                  <IconArrowRight className="w-5 h-5 text-cow-purple opacity-0 group-hover:opacity-100 group-hover:translate-x-1 transition-all" />
+                  <IconArrowRight className="relative z-10 w-5 h-5 text-cow-purple opacity-0 group-hover:opacity-100 group-hover:translate-x-1 transition-all" />
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/PopularTools.tsx` around lines 99 - 109, Update the
IconArrowRight element in the tool header row to use relative z-10 positioning,
or apply the same stacking context to its containing row, so it remains above
the background image and gradient hover layers.
src/pages/MusicCopyright.tsx-30-35 (1)

30-35: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The limit banner has no reset contract in either parent. LooneyCheckForm reports the concurrency limit through onLimitReached, but each parent owns the resulting banner state and clears it differently. The page never clears it, and the dialog clears it only when a new check starts.

  • src/pages/MusicCopyright.tsx#L30-L35: pass onCheckStart={() => setLimitMessage(false)} to LooneyCheckForm so the banner clears when a check starts.
  • src/components/LooneyCheckDialog.tsx#L19-L19: also call setLimitReached(false) in onOpenChange when the dialog closes, so a reopened dialog does not show the previous limit message.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/MusicCopyright.tsx` around lines 30 - 35, The limit banner state is
not consistently reset when a check begins or the dialog closes. In
src/pages/MusicCopyright.tsx lines 30-35, pass onCheckStart to LooneyCheckForm
to call setLimitMessage(false); in src/components/LooneyCheckDialog.tsx line 19,
update onOpenChange to call setLimitReached(false) when the dialog closes.
src/utils/looneyHistory.ts-57-67 (1)

57-67: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep the previous status when the job payload omits one.

updateLooneyHistoryFromJob assigns job.status unconditionally. getLooneyJob returns the parsed body without validating it, so a response that omits status sets record.status to undefined. The record then leaves the "Running checks" list and renders with the failure icon in LooneyHistorySection.

🛡️ Proposed fix
-    status: job.status,
+    status: job.status || current.status,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/looneyHistory.ts` around lines 57 - 67, Update
updateLooneyHistoryFromJob so status falls back to current.status when
job.status is omitted, while preserving the provided job.status when present.
Leave the existing result and error fallback behavior unchanged.
src/components/LooneyResultDisplay.tsx-42-42 (1)

42-42: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Guard the date formatting against unparseable values.

Any key that contains "date" is passed to new Date(raw). If the API returns a value that Date cannot parse, toLocaleDateString renders "Invalid Date" in the result card. Fall back to the raw text.

🐛 Proposed fix
-const display = key.toLowerCase().includes('duration') && Number.isFinite(Number(raw)) ? `${Math.floor(Number(raw) / 60000)} min ${Math.round((Number(raw) % 60000) / 1000)} sec` : key.toLowerCase().includes('date') ? new Date(raw).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }) : raw;
+const parsedDate = key.toLowerCase().includes('date') ? new Date(raw) : null;
+const display = key.toLowerCase().includes('duration') && Number.isFinite(Number(raw)) ? `${Math.floor(Number(raw) / 60000)} min ${Math.round((Number(raw) % 60000) / 1000)} sec` : parsedDate && !Number.isNaN(parsedDate.getTime()) ? parsedDate.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }) : raw;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/LooneyResultDisplay.tsx` at line 42, Update the
date-formatting branch in the entries mapping within LooneyResultDisplay so it
validates the parsed Date before calling toLocaleDateString. For keys containing
“date,” display the localized date only when the value is parseable; otherwise
fall back to raw, while preserving the existing duration and non-date formatting
behavior.
src/utils/looneyChecker.ts-149-157 (1)

149-157: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reset eventName before the early return.

processEvent returns at Line 150 when eventData is empty, so eventName keeps its previous value. An SSE frame that carries event: complete with no data: line leaves eventName as complete. The next frame is then treated as a completion event and terminates the stream with a payload that has no result.

🐛 Proposed fix
   const processEvent = () => {
-    if (eventData.length === 0) return;
+    if (eventData.length === 0) {
+      eventName = 'message';
+      return;
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/looneyChecker.ts` around lines 149 - 157, Update processEvent to
reset eventName before the eventData.length === 0 early return, ensuring empty
SSE frames cannot carry the previous event name into the next frame while
preserving the existing non-empty processing.
🧹 Nitpick comments (22)
src/pages/BackgroundGenerator.tsx (3)

146-152: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Memoize filteredTextures.

The fuzzy filter runs on every render. Each slider drag or recording-progress update re-scores the whole texture list, and fuzzyTextureScore calls editDistance per word pair. Wrap the computation in useMemo keyed on textures and textureSearch.

♻️ Proposed refactor
-  const filteredTextures = textureSearch.trim()
-    ? textures
-      .map((texture) => ({ texture, score: fuzzyTextureScore(texture, textureSearch) }))
-      .filter(({ score }) => score !== Infinity)
-      .sort((left, right) => left.score - right.score || left.texture.title.localeCompare(right.texture.title))
-      .map(({ texture }) => texture)
-    : textures;
+  const filteredTextures = useMemo(() => (
+    textureSearch.trim()
+      ? textures
+        .map((texture) => ({ texture, score: fuzzyTextureScore(texture, textureSearch) }))
+        .filter(({ score }) => score !== Infinity)
+        .sort((left, right) => left.score - right.score || left.texture.title.localeCompare(right.texture.title))
+        .map(({ texture }) => texture)
+      : textures
+  ), [textures, textureSearch]);

Add useMemo to the React import.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/BackgroundGenerator.tsx` around lines 146 - 152, Memoize the
filteredTextures computation with React useMemo, adding useMemo to the existing
React import and using textures and textureSearch as dependencies. Keep the
current fuzzy scoring, filtering, sorting, and unfiltered fallback behavior
unchanged.

334-348: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse loadSelectedImages in handleGenerate.

Lines 342-348 duplicate loadSelectedImages at Lines 383-389 exactly. Move loadSelectedImages above handleGenerate and call it.

♻️ Proposed refactor
-      const loadedImages = await Promise.all(selectedImages.map((source) => new Promise<PatternImage & { image: HTMLImageElement }>((resolve, reject) => {
-        const image = new Image();
-        image.crossOrigin = "Anonymous";
-        image.onload = () => resolve({ ...source, image });
-        image.onerror = () => reject(new Error(`Failed to load ${source.title}`));
-        image.src = source.url;
-      })));
+      const loadedImages = await loadSelectedImages();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/BackgroundGenerator.tsx` around lines 334 - 348, Move the shared
image-loading logic into the existing loadSelectedImages helper before
handleGenerate, then replace the duplicated Promise.all image-loading block
inside handleGenerate with a call to that helper. Preserve the current loading,
crossOrigin, error, and result behavior.

205-210: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Keep successfully read images when one file fails.

Promise.all rejects on the first FileReader error. All other images in the same batch are then discarded. Use Promise.allSettled and add the fulfilled images.

♻️ Proposed refactor
-    Promise.all(newImages)
-      .then((images) => {
-        setUploadedImages((current) => [...current, ...images]);
-        setSelectedImages((current) => [...current, ...images]);
-      })
-      .catch(() => toast.error("One or more images could not be read"));
+    Promise.allSettled(newImages).then((results) => {
+      const images = results
+        .filter((result): result is PromiseFulfilledResult<PatternImage> => result.status === "fulfilled")
+        .map((result) => result.value);
+      if (images.length) {
+        setUploadedImages((current) => [...current, ...images]);
+        setSelectedImages((current) => [...current, ...images]);
+      }
+      if (images.length !== results.length) {
+        toast.error("One or more images could not be read");
+      }
+    });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/BackgroundGenerator.tsx` around lines 205 - 210, Update the
image-loading promise flow around Promise.all(newImages) in BackgroundGenerator
so one rejected FileReader does not discard successful results. Use
Promise.allSettled, collect only fulfilled image values for setUploadedImages
and setSelectedImages, and preserve the toast error notification when any read
fails.
src/components/ui/toggle-group.tsx (2)

51-52: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Keep ToggleGroupItem.value required.

The empty-string fallback hides a missing item value and gives every missing-value item the same identity. Pass the required value directly and let the type checker catch invalid callers. Verify this behavior against @radix-ui/react-toggle-group@1.1.11. (radix-ui.com)

Proposed fix
-      value={props.value || ""}
       {...props}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ui/toggle-group.tsx` around lines 51 - 52, Update the
ToggleGroupItem props spread to pass props.value directly instead of applying an
empty-string fallback, preserving ToggleGroupItem.value as required so invalid
callers are caught by the type checker and matching
`@radix-ui/react-toggle-group`@1.1.11 behavior.

Source: MCP tools


23-24: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Confirm whether single selection is an invariant.

Because {...props} follows type="single", a caller can still provide another toggle-group type. If this wrapper must always be single-select, place type="single" after the spread or remove type from the public props. Radix supports both single and multiple modes, so verify the intended contract against all call sites. (radix-ui.com)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ui/toggle-group.tsx` around lines 23 - 24, Verify the intended
selection contract for the toggle-group wrapper against its call sites. If it
must always be single-select, update the component around the type="single" and
{...props} declarations so callers cannot override the type, either by applying
type after the spread or excluding type from the public props.

Source: MCP tools

src/index.css (1)

17-21: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Pin or self-host the Minecraft font.

The @font-face source uses the mutable main branch. The downloaded font can change or disappear without a reviewed application change. Pin the source to a commit or package the font with the application.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/index.css` around lines 17 - 21, Update the `@font-face` declaration for
“Minecraft Five Bold” to use a commit-pinned font URL or a locally packaged
application asset instead of the mutable main branch. Preserve the existing
TrueType format and font-display behavior.
api/looney-check.js (3)

29-31: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Note the coupling between the hash salt and the Supabase key.

hashIdentifier derives the salt from SUPABASE_SECRET_KEY. A key rotation changes every bucket hash and resets all daily counters for that day. Use a dedicated secret, for example LOONEY_RATE_LIMIT_SALT, with the current value as the fallback.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/looney-check.js` around lines 29 - 31, Update hashIdentifier so its hash
salt comes from the dedicated LOONEY_RATE_LIMIT_SALT environment variable,
falling back to the current 'looney-rate-limit' value, and remove its dependency
on SUPABASE_SECRET_KEY.

282-285: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Replace the substring-based status mapping with explicit error typing.

The mapping tests message.includes('valid'). The database function raises Invalid rate-limit bucket, which contains valid. A server-side failure is then reported as HTTP 400. The mapping is also sensitive to future wording changes in error strings.

Use a typed error instead.

♻️ Proposed refactor
+class ValidationError extends Error {}
+
 export default async function handler(request) {
@@
   } catch (error) {
     const message = error instanceof Error ? error.message : 'Unable to check this track';
-    const status = message.includes('50 MB') || message.includes('valid') || message.includes('Provide') || message.includes('Invalid JSON') ? 400 : 502;
+    const status = error instanceof ValidationError ? 400 : 502;
     return jsonResponse(request, { error: message }, status);
   }

Throw ValidationError in buildUpstreamRequest and readBody for the user-input failures.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/looney-check.js` around lines 282 - 285, Replace the message-substring
status mapping in the catch block with explicit error typing: ensure user-input
failures thrown by buildUpstreamRequest and readBody use ValidationError, then
return HTTP 400 only for ValidationError instances and HTTP 502 for other errors
while preserving the existing error response message.

268-281: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Consider consuming the rate limit only after the upstream job is created.

consumeRateLimit increments the daily counters before the upstream POST runs. If the upstream request times out or returns 5xx, the user loses one of five daily checks without receiving a job. Move the consumption after a successful upstream response, or add a compensating decrement when the upstream call fails.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/looney-check.js` around lines 268 - 281, Move consumeRateLimit in the
job-creation flow so it runs only after the upstream POST succeeds, preserving
the existing 429 response when the limit is exceeded. Ensure upstream timeouts
and 5xx responses do not consume a daily check, using the surrounding fetch and
proxyUpstreamResponse logic without changing unrelated behavior.
server.js (1)

19-25: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider sharing one CORS origin allowlist.

The same origins are now declared here and in api/looney-check.js Lines 8-17. The two lists can drift, which produces inconsistent CORS behavior between the local server and the deployed function. Export a single list from a shared module and import it in both places.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server.js` around lines 19 - 25, Move the duplicated CORS origin list from
server.js and api/looney-check.js into one shared exported allowlist, then
import and reuse that list in both locations. Preserve all currently allowed
origins and ensure both the local server and deployed function reference the
same symbol.
src/pages/ResourcesHub.tsx (1)

212-215: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Wrap onCheckCopyright in useCallback.

ResourcesList is exported as React.memo(ResourcesList). A new function identity on every render of ResourcesHub defeats that memoization for both lists.

♻️ Proposed refactor
-  const onCheckCopyright = (resource: Resource) => {
-    setCopyrightResource(resource);
-  };
+  const onCheckCopyright = useCallback((resource: Resource) => {
+    setCopyrightResource(resource);
+  }, []);

Add useCallback to the React import on Line 1.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/ResourcesHub.tsx` around lines 212 - 215, Wrap the onCheckCopyright
handler in ResourcesHub with useCallback, adding the React import and using an
appropriate dependency array for setCopyrightResource. Preserve its existing
behavior of setting the selected copyright resource so ResourcesList receives a
stable callback identity.
supabase/migrations/20260810000000_looney_check_rate_limits.sql (3)

1-13: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Add a retention policy for the rate-limit rows.

The table stores one row per bucket per UTC day and nothing deletes old windows. The row count grows with unique browsers and IP addresses. Hashed IP addresses are personal data under GDPR, so indefinite retention is also a compliance concern.

Add a scheduled cleanup, for example a pg_cron job that deletes rows where window_started_at < now() - interval '7 days'.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/migrations/20260810000000_looney_check_rate_limits.sql` around lines
1 - 13, Add scheduled retention cleanup for public.looney_check_rate_limits,
using the existing pg_cron mechanism to periodically delete rows whose
window_started_at is older than seven days. Keep the cleanup scoped to this
table and ensure the migration creates the job safely without duplicating an
existing schedule.

68-68: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make the daily limit a parameter instead of a literal.

The value 5 is duplicated here and as DAILY_CHECK_LIMIT in api/looney-check.js Line 6. The two values can drift. Add a p_limit integer default 5 argument, or read the limit from a settings table, and pass it from the API.

Note that adding a parameter changes the function signature, so the revoke/grant statements at Lines 91-92 must be updated as well.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/migrations/20260810000000_looney_check_rate_limits.sql` at line 68,
Update the rate-limit function containing the bucket_count check to accept a
p_limit integer parameter defaulting to 5, compare bucket_count against p_limit,
and update its corresponding revoke/grant statements to use the new function
signature. Ensure the API’s DAILY_CHECK_LIMIT is passed through when invoking
the function.

71-83: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Combine the read and the increment into a single pass.

The function iterates p_buckets twice. The first loop holds FOR UPDATE locks, so the transaction is serialized correctly, but the second loop repeats the same lookups. A single update ... returning per bucket, or a set-based statement, reduces the work and removes the duplicated matching logic. Duplicate entries in p_buckets also increment the same row twice with the current structure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/migrations/20260810000000_looney_check_rate_limits.sql` around lines
71 - 83, Update the blocked=false processing in the rate-limit function to
combine bucket matching, counter increments, and
browser_total/ip_total/account_total accumulation into one pass over p_buckets,
preferably using a set-based UPDATE or UPDATE ... RETURNING. Remove the
duplicated bucket lookup logic and ensure duplicate entries in p_buckets do not
increment the same rate-limit row more than once.
src/components/resources/ResourceCard.tsx (1)

70-72: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Narrow the effect dependency and skip fonts that are already registered.

The dependency list now contains the whole resource object. If a parent recreates resource objects between renders, the identity changes and the effect refetches the font file. Each run also calls document.fonts.add again for the same family, so document.fonts accumulates duplicate entries.

Depend on the specific fields and check document.fonts.check before loading.

♻️ Proposed refactor
-  }, [resource, isInView]);
+  }, [resource.category, resource.title, resource.download_url, resource.credit, resource.filetype, isInView]);

Add an early exit in maybeLoadFont:

if (document.fonts.check(`12px "${fontName}"`)) {
  if (active) setIsFontLoaded(true);
  return;
}

Also applies to: 103-103

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/resources/ResourceCard.tsx` around lines 70 - 72, Update the
ResourceCard effect and its maybeLoadFont logic: depend only on the specific
resource fields used to derive the font, rather than the whole resource object,
and check document.fonts.check for the family before loading. When the font is
already registered, mark it loaded if active and return without adding or
fetching it again.
src/integrations/supabase/types.ts (1)

1-31: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Regenerate the Supabase types instead of using any placeholders.

Every table now resolves to Row: any, Insert: any, Update: any, and Functions accepts any name with any arguments. All compile-time checks on Supabase queries across the application are lost. Column renames and the new consume_looney_check_rate_limit signature will not be validated.

Run supabase gen types typescript and commit the generated file. If the generated file is temporarily unavailable, add a comment in this file that records why the placeholder exists and when it will be replaced.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/integrations/supabase/types.ts` around lines 1 - 31, Replace the
placeholder Database definition in the types file with output generated by
`supabase gen types typescript`, including concrete table Row/Insert/Update
shapes and function argument/return signatures such as
consume_looney_check_rate_limit. Do not retain AnyTable or wildcard Functions
typing; if generation cannot be completed, document the reason and replacement
timing in a comment.
src/components/LooneyCheckDialog.tsx (1)

19-19: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reset limitReached when the dialog closes.

limitReached stays true after onClose. onCheckStart is the only reset, and it does not run on the limit path. If the user reopens the dialog for another resource, the amber message renders before the new check reports its own state.

♻️ Proposed change
-  return <><Dialog open={!!resource} onOpenChange={(open) => !open && onClose()}>
+  return <><Dialog open={!!resource} onOpenChange={(open) => { if (!open) { setLimitReached(false); onClose(); } }}>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/LooneyCheckDialog.tsx` at line 19, Update the dialog close
handler in LooneyCheckDialog so it resets limitReached before invoking onClose
when the dialog transitions to closed. Preserve the existing close behavior and
ensure reopening for a new resource starts without the previous limit state.
src/App.tsx (1)

120-128: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider redirecting one of the duplicate routes.

/music-copyright and /gappa render the same MusicCopyright page. GlobalComponents passes location.pathname to Seo, so both paths publish the same content under two canonical URLs. A <Navigate to="/gappa" replace /> on the old path keeps one canonical URL and preserves old links.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/App.tsx` around lines 120 - 128, Update the route definitions in App so
/gappa is the canonical MusicCopyright path: replace the /music-copyright
route’s MusicCopyright element with a Navigate redirect to /gappa using replace,
while leaving the /gappa and /gappa/check/:jobId routes unchanged.
src/utils/looneyChecker.ts (1)

70-78: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Collapse the duplicated header setup.

Both branches call getLooneyRequestHeaders('application/json'). Only the body differs.

♻️ Proposed simplification
-  const options: RequestInit = { method: 'POST' };
-  options.signal = signal;
-  if (fileUrl) {
-    options.headers = await getLooneyRequestHeaders('application/json');
-    options.body = JSON.stringify({ file_url: fileUrl });
-  } else {
-    options.headers = await getLooneyRequestHeaders('application/json');
-    options.body = JSON.stringify({ spotify_url: spotifyUrl });
-  }
+  const options: RequestInit = {
+    method: 'POST',
+    signal,
+    headers: await getLooneyRequestHeaders('application/json'),
+    body: JSON.stringify(fileUrl ? { file_url: fileUrl } : { spotify_url: spotifyUrl }),
+  };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/looneyChecker.ts` around lines 70 - 78, In the request options
setup around getLooneyRequestHeaders, move the shared application/json headers
assignment outside the fileUrl conditional. Keep the conditional focused only on
selecting the appropriate file_url or spotify_url request body, preserving the
existing POST options and payload behavior.
src/components/LooneyHistorySection.tsx (1)

63-68: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Guard the state write that runs after await.

setRecords at Line 63 runs after all streams settle. If activeJobId changes, the cleanup aborts the controllers, but the pending streamRunningJobs call still reaches Line 63. Add a cancellation flag.

♻️ Proposed change
   useEffect(() => {
     const controllers = new Set<AbortController>();
+    let cancelled = false;
     const streamRunningJobs = async () => {
@@
-      setRecords(loadLooneyHistory());
+      if (!cancelled) setRecords(loadLooneyHistory());
     };
 
     void streamRunningJobs();
-    return () => controllers.forEach((controller) => controller.abort());
+    return () => { cancelled = true; controllers.forEach((controller) => controller.abort()); };
   }, [activeJobId]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/LooneyHistorySection.tsx` around lines 63 - 68, In the effect
containing streamRunningJobs, add a cancellation flag that cleanup sets before
aborting the controllers, and guard the post-await
setRecords(loadLooneyHistory()) call so it runs only while the effect is still
active. Keep the existing activeJobId dependency and controller-abort behavior
unchanged.

Source: Linters/SAST tools

src/utils/looneyHistory.ts (1)

16-22: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Reuse isFreshRunningRecord in findRunningLooneyCheck.

countRunningLooneyChecks ignores running records older than MAX_RUNNING_LOONEY_AGE_MS, but findRunningLooneyCheck does not. A stale queued record therefore still blocks a new check for the same source and opens the "Check already running" dialog. refreshRunningLooneyChecks expires such records first in the current call path, so this only surfaces if a caller skips that refresh.

♻️ Proposed change
 export function findRunningLooneyCheck(sourceKey: string): LooneyHistoryRecord | undefined {
   return loadLooneyHistory().find((record) => {
-    if (record.status !== 'queued' && record.status !== 'running') return false;
+    if (!isFreshRunningRecord(record)) return false;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/looneyHistory.ts` around lines 16 - 22, Update
findRunningLooneyCheck to reuse isFreshRunningRecord when filtering queued or
running records, matching the freshness behavior of countRunningLooneyChecks and
excluding stale records before source matching. Preserve the existing source-key
and legacy Spotify matching logic for fresh records.
src/pages/MusicCopyright.tsx (1)

29-29: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Rename the shared image asset.

public/assets/image copy.png exists and is used by three components. Rename it to a descriptive filename and update all references. The encoded space is valid.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/MusicCopyright.tsx` at line 29, Rename the shared public asset
currently referenced as “/assets/image%20copy.png” to a descriptive filename,
then update every reference across the three consuming components, including the
image in MusicCopyright, to use the new asset path while preserving the existing
image behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@api/looney-check.js`:
- Around line 71-79: Update corsHeaders to include x-looney-browser-id and
authorization in Access-Control-Allow-Headers, while retaining Content-Type. Add
an appropriate Access-Control-Max-Age response header so successful CORS
preflight results are cached.

In `@src/components/LooneyCheckForm.tsx`:
- Around line 84-89: Update the limit check in LooneyCheckForm’s request flow to
avoid counting the current source twice: use only pending keys without a
corresponding running history record, or remove each key from pendingSourceKeys
when onJobCreated creates that record. Preserve the existing finally cleanup and
allow the second concurrent check until the actual running-check limit is
reached.
- Around line 218-231: Make the file upload drop zone keyboard-accessible by
giving the clickable element in the !autoStart && sourceTab === 'file' branch an
appropriate button role, a tabIndex, and keyboard handling that triggers
fileInputRef.current?.click() for Enter and Space while preventing default
behavior. Preserve the existing pointer click and drag/drop behavior.

In `@src/components/LooneyHistorySection.tsx`:
- Around line 42-58: Update the resume flow in the effect around streamLooneyJob
so each saveLooneyHistoryRecord call re-reads the latest matching record via
loadLooneyHistory().find(...) instead of spreading the captured record. Merge
progress, completion, or failure fields onto that current record while
preserving the existing fallback behavior when no record is found.

In `@src/components/resources/ResourceCard.tsx`:
- Around line 84-94: Update maybeLoadFont to safely construct FontFace by
escaping fontName and fontUrl for CSS and wrapping the constructor and load flow
in synchronous error handling. Ensure invalid resource title or download URL is
caught and follows the existing active-state logging behavior, including when
invoked from the deferred setTimeout branch.

In `@src/pages/BackgroundGenerator.tsx`:
- Around line 438-445: Update the recording flow around recordingPromise and
recorder.start() to reject when MediaRecorder emits an error or start() throws,
ensuring the await settles and cleanup runs. In the failure path, clear the
pending frame timer before propagating the rejection; preserve the existing
onstop resolution behavior and final state cleanup.
- Around line 265-292: Update the tile-generation flow in the rendering function
around motionPadding so static image output uses no extra padding, while
animation frame rendering receives 400 from handleCreateVideo. Also enforce a
minimum horizontalStep and verticalStep that scales with canvas area to bound
drawTile/drawImage calls for large outputs without changing normal-sized
rendering behavior.
- Around line 371-381: Update the debounced regeneration useEffect and its
handleGenerate callback in BackgroundGenerator so they do nothing while
isRecording is true, including preventing pending timers from invoking
generation after recording starts. Preserve normal 500 ms regeneration behavior
when recording is inactive and continue using the shared canvas otherwise.
- Around line 938-946: Restrict the 5120x2880 and 7680x4320 SelectItem options
when the shared size selector is in "gif" mode, while keeping them available for
still-image exports. Update the large-image export flow to use canvas.toBlob and
an object URL instead of synchronous toDataURL encoding, ensuring the object URL
is released after use.

In `@src/pages/LooneyResultPage.tsx`:
- Around line 11-18: Update LooneyResultPage to fall back to fetching the job
via the existing getLooneyJob API when loadLooneyHistory finds no matching
record, and use streamLooneyJob to refresh records while status is queued or
running. Preserve local-history behavior when available, update state from
fetched/streamed data, and render a distinct not-found state when the job cannot
be retrieved instead of the current failure message.

In `@src/utils/looneyChecker.ts`:
- Around line 142-203: Ensure the SSE reader created in the stream-processing
function is always released by cancelling reader in a finally block surrounding
the read loop and related processing. Preserve existing abort handling, fallback
polling, and terminal-job behavior while guaranteeing cleanup when the loop
exits after streamJob is set or due to an error.

In `@src/utils/migrateResources.ts`:
- Around line 41-45: Validate the payload in the migration flow around
sourceData and normalizedData, rejecting null or malformed JSON instead of
treating it as an empty object; the type cast must not serve as validation.
Before the destructive delete near supabaseResources, abort when normalized
resources or supabaseResources has zero entries, preserving existing data rather
than reporting a successful empty migration.

---

Outside diff comments:
In `@src/pages/ResourcesHub.tsx`:
- Around line 343-354: Update the Minecraft music ResourcesList usage and
related ResourceCard behavior so onCheckCopyright is effective for
minecraft-music resources: either remove the unused callback prop from this list
or extend ResourceCard’s copyright-button condition to include the
minecraft-music category, preserving existing music behavior.

---

Minor comments:
In `@src/components/LooneyResultDisplay.tsx`:
- Line 42: Update the date-formatting branch in the entries mapping within
LooneyResultDisplay so it validates the parsed Date before calling
toLocaleDateString. For keys containing “date,” display the localized date only
when the value is parseable; otherwise fall back to raw, while preserving the
existing duration and non-date formatting behavior.

In `@src/components/PopularTools.tsx`:
- Around line 99-109: Update the IconArrowRight element in the tool header row
to use relative z-10 positioning, or apply the same stacking context to its
containing row, so it remains above the background image and gradient hover
layers.

In `@src/pages/BackgroundGenerator.tsx`:
- Around line 677-681: Update handleImageUpload to enforce both a per-file size
limit and a maximum number of uploaded images before storing files in React
state. Reject oversized files and selections exceeding the count limit,
reporting each rejection with toast.error, while preserving valid uploads and
the existing state update flow.
- Around line 965-976: Update the debounced generation effect in
BackgroundGenerator so changes to spacing, opacity, scale, rotation, size, or
pattern invalidate the existing video by clearing videoUrl. Ensure the preview
and Download WebM action no longer use the stale recording while preserving the
existing image regeneration flow.
- Around line 571-574: Update the tab label in the TabsTrigger using value "gif"
to accurately state that the tool exports WebM, such as "Video" or "Animated
(WebM)". Also rename the related OutputMode value from "gif" to "video" and
update all references, including handleCreateVideo and handleVideoDownload,
while preserving the existing WebM export behavior.
- Around line 283-288: Update the non-random pattern branches in the rendering
logic around drawTile so randomSeed affects grid, staggered, diagonal, and
scattered output, using the seeded random source for deterministic position,
scale, or tileRotation jitter. Ensure drawTile receives the resulting non-zero
rotation or equivalent seeded variation, while preserving the existing random
branch behavior.
- Around line 807-828: Add an aria-label to the Duration, Frame rate, and
Movement distance Slider components in the animation controls, matching the
accessible naming approach used by the rotation slider. Use distinct labels that
identify each slider’s setting.

In `@src/pages/MusicCopyright.tsx`:
- Around line 30-35: The limit banner state is not consistently reset when a
check begins or the dialog closes. In src/pages/MusicCopyright.tsx lines 30-35,
pass onCheckStart to LooneyCheckForm to call setLimitMessage(false); in
src/components/LooneyCheckDialog.tsx line 19, update onOpenChange to call
setLimitReached(false) when the dialog closes.

In `@src/utils/looneyChecker.ts`:
- Around line 149-157: Update processEvent to reset eventName before the
eventData.length === 0 early return, ensuring empty SSE frames cannot carry the
previous event name into the next frame while preserving the existing non-empty
processing.

In `@src/utils/looneyHistory.ts`:
- Around line 57-67: Update updateLooneyHistoryFromJob so status falls back to
current.status when job.status is omitted, while preserving the provided
job.status when present. Leave the existing result and error fallback behavior
unchanged.

---

Nitpick comments:
In `@api/looney-check.js`:
- Around line 29-31: Update hashIdentifier so its hash salt comes from the
dedicated LOONEY_RATE_LIMIT_SALT environment variable, falling back to the
current 'looney-rate-limit' value, and remove its dependency on
SUPABASE_SECRET_KEY.
- Around line 282-285: Replace the message-substring status mapping in the catch
block with explicit error typing: ensure user-input failures thrown by
buildUpstreamRequest and readBody use ValidationError, then return HTTP 400 only
for ValidationError instances and HTTP 502 for other errors while preserving the
existing error response message.
- Around line 268-281: Move consumeRateLimit in the job-creation flow so it runs
only after the upstream POST succeeds, preserving the existing 429 response when
the limit is exceeded. Ensure upstream timeouts and 5xx responses do not consume
a daily check, using the surrounding fetch and proxyUpstreamResponse logic
without changing unrelated behavior.

In `@server.js`:
- Around line 19-25: Move the duplicated CORS origin list from server.js and
api/looney-check.js into one shared exported allowlist, then import and reuse
that list in both locations. Preserve all currently allowed origins and ensure
both the local server and deployed function reference the same symbol.

In `@src/App.tsx`:
- Around line 120-128: Update the route definitions in App so /gappa is the
canonical MusicCopyright path: replace the /music-copyright route’s
MusicCopyright element with a Navigate redirect to /gappa using replace, while
leaving the /gappa and /gappa/check/:jobId routes unchanged.

In `@src/components/LooneyCheckDialog.tsx`:
- Line 19: Update the dialog close handler in LooneyCheckDialog so it resets
limitReached before invoking onClose when the dialog transitions to closed.
Preserve the existing close behavior and ensure reopening for a new resource
starts without the previous limit state.

In `@src/components/LooneyHistorySection.tsx`:
- Around line 63-68: In the effect containing streamRunningJobs, add a
cancellation flag that cleanup sets before aborting the controllers, and guard
the post-await setRecords(loadLooneyHistory()) call so it runs only while the
effect is still active. Keep the existing activeJobId dependency and
controller-abort behavior unchanged.

In `@src/components/resources/ResourceCard.tsx`:
- Around line 70-72: Update the ResourceCard effect and its maybeLoadFont logic:
depend only on the specific resource fields used to derive the font, rather than
the whole resource object, and check document.fonts.check for the family before
loading. When the font is already registered, mark it loaded if active and
return without adding or fetching it again.

In `@src/components/ui/toggle-group.tsx`:
- Around line 51-52: Update the ToggleGroupItem props spread to pass props.value
directly instead of applying an empty-string fallback, preserving
ToggleGroupItem.value as required so invalid callers are caught by the type
checker and matching `@radix-ui/react-toggle-group`@1.1.11 behavior.
- Around line 23-24: Verify the intended selection contract for the toggle-group
wrapper against its call sites. If it must always be single-select, update the
component around the type="single" and {...props} declarations so callers cannot
override the type, either by applying type after the spread or excluding type
from the public props.

In `@src/index.css`:
- Around line 17-21: Update the `@font-face` declaration for “Minecraft Five Bold”
to use a commit-pinned font URL or a locally packaged application asset instead
of the mutable main branch. Preserve the existing TrueType format and
font-display behavior.

In `@src/integrations/supabase/types.ts`:
- Around line 1-31: Replace the placeholder Database definition in the types
file with output generated by `supabase gen types typescript`, including
concrete table Row/Insert/Update shapes and function argument/return signatures
such as consume_looney_check_rate_limit. Do not retain AnyTable or wildcard
Functions typing; if generation cannot be completed, document the reason and
replacement timing in a comment.

In `@src/pages/BackgroundGenerator.tsx`:
- Around line 146-152: Memoize the filteredTextures computation with React
useMemo, adding useMemo to the existing React import and using textures and
textureSearch as dependencies. Keep the current fuzzy scoring, filtering,
sorting, and unfiltered fallback behavior unchanged.
- Around line 334-348: Move the shared image-loading logic into the existing
loadSelectedImages helper before handleGenerate, then replace the duplicated
Promise.all image-loading block inside handleGenerate with a call to that
helper. Preserve the current loading, crossOrigin, error, and result behavior.
- Around line 205-210: Update the image-loading promise flow around
Promise.all(newImages) in BackgroundGenerator so one rejected FileReader does
not discard successful results. Use Promise.allSettled, collect only fulfilled
image values for setUploadedImages and setSelectedImages, and preserve the toast
error notification when any read fails.

In `@src/pages/MusicCopyright.tsx`:
- Line 29: Rename the shared public asset currently referenced as
“/assets/image%20copy.png” to a descriptive filename, then update every
reference across the three consuming components, including the image in
MusicCopyright, to use the new asset path while preserving the existing image
behavior.

In `@src/pages/ResourcesHub.tsx`:
- Around line 212-215: Wrap the onCheckCopyright handler in ResourcesHub with
useCallback, adding the React import and using an appropriate dependency array
for setCopyrightResource. Preserve its existing behavior of setting the selected
copyright resource so ResourcesList receives a stable callback identity.

In `@src/utils/looneyChecker.ts`:
- Around line 70-78: In the request options setup around
getLooneyRequestHeaders, move the shared application/json headers assignment
outside the fileUrl conditional. Keep the conditional focused only on selecting
the appropriate file_url or spotify_url request body, preserving the existing
POST options and payload behavior.

In `@src/utils/looneyHistory.ts`:
- Around line 16-22: Update findRunningLooneyCheck to reuse isFreshRunningRecord
when filtering queued or running records, matching the freshness behavior of
countRunningLooneyChecks and excluding stale records before source matching.
Preserve the existing source-key and legacy Spotify matching logic for fresh
records.

In `@supabase/migrations/20260810000000_looney_check_rate_limits.sql`:
- Around line 1-13: Add scheduled retention cleanup for
public.looney_check_rate_limits, using the existing pg_cron mechanism to
periodically delete rows whose window_started_at is older than seven days. Keep
the cleanup scoped to this table and ensure the migration creates the job safely
without duplicating an existing schedule.
- Line 68: Update the rate-limit function containing the bucket_count check to
accept a p_limit integer parameter defaulting to 5, compare bucket_count against
p_limit, and update its corresponding revoke/grant statements to use the new
function signature. Ensure the API’s DAILY_CHECK_LIMIT is passed through when
invoking the function.
- Around line 71-83: Update the blocked=false processing in the rate-limit
function to combine bucket matching, counter increments, and
browser_total/ip_total/account_total accumulation into one pass over p_buckets,
preferably using a set-based UPDATE or UPDATE ... RETURNING. Remove the
duplicated bucket lookup logic and ensure duplicate entries in p_buckets do not
increment the same rate-limit row more than once.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b9e1773e-320a-4d7c-ad58-8e821ba818d5

📥 Commits

Reviewing files that changed from the base of the PR and between 623ed13 and 4889c81.

⛔ Files ignored due to path filters (2)
  • public/assets/image copy.png is excluded by !**/*.png
  • public/assets/minecraft-pattern-background-1920x1080.png is excluded by !**/*.png
📒 Files selected for processing (42)
  • api/looney-check.js
  • server.js
  • src/App.tsx
  • src/components/Carousel.tsx
  • src/components/ErrorBoundary.tsx
  • src/components/Footer.tsx
  • src/components/Hero.tsx
  • src/components/LooneyCheckDialog.tsx
  • src/components/LooneyCheckForm.tsx
  • src/components/LooneyHistorySection.tsx
  • src/components/LooneyResultDisplay.tsx
  • src/components/LooneyRunningCheckDialog.tsx
  • src/components/Navbar.tsx
  • src/components/PopularTools.tsx
  • src/components/ResultsDisplay.tsx
  • src/components/RollingGallery.tsx
  • src/components/UploadThingClient.tsx
  • src/components/ValueProps.tsx
  • src/components/generators/MinecraftNametagGenerator.tsx
  • src/components/resources/ResourceCard.tsx
  • src/components/resources/ResourcesList.tsx
  • src/components/ui/toggle-group.tsx
  • src/hooks/useMinecraftMusic.ts
  • src/index.css
  • src/integrations/supabase/types.ts
  • src/pages/BackgroundGenerator.tsx
  • src/pages/BlogView.tsx
  • src/pages/Community.tsx
  • src/pages/Contact.tsx
  • src/pages/Index.tsx
  • src/pages/LooneyResultPage.tsx
  • src/pages/MusicCopyright.tsx
  • src/pages/NotFound.tsx
  • src/pages/ResourcesHub.tsx
  • src/types/copyright.ts
  • src/types/looney.ts
  • src/utils/copyrightChecker.ts
  • src/utils/looneyChecker.ts
  • src/utils/looneyHistory.ts
  • src/utils/migrateResources.ts
  • supabase/migrations/20260810000000_looney_check_rate_limits.sql
  • vite.config.ts
💤 Files with no reviewable changes (3)
  • src/components/ResultsDisplay.tsx
  • src/utils/copyrightChecker.ts
  • src/types/copyright.ts

Comment thread api/looney-check.js
Comment thread src/components/LooneyCheckForm.tsx
Comment thread src/components/LooneyCheckForm.tsx
Comment thread src/components/LooneyHistorySection.tsx
Comment thread src/components/resources/ResourceCard.tsx
Comment thread src/pages/BackgroundGenerator.tsx Outdated
Comment thread src/pages/BackgroundGenerator.tsx Outdated
Comment thread src/pages/LooneyResultPage.tsx
Comment thread src/utils/looneyChecker.ts
Comment thread src/utils/migrateResources.ts Outdated
@greptile-apps

greptile-apps Bot commented Aug 11, 2026

Copy link
Copy Markdown

Greptile Summary

This update adds the Looney copyright-check flow, including job creation, polling, streaming, daily limits, and result history, alongside expanded background-generator pattern and animation exports.

The previously reported rate-limit issues are resolved: changing client-provided forwarding and browser headers no longer bypasses the daily limit, browser-ID-free visitors no longer share one global quota bucket, and concurrent submissions do not create more upstream jobs than the available quota.

Confidence Score: 5/5

No blocking failure remains.

No accepted blocking finding remains after exercising header rotation, anonymous visitor isolation, and concurrent quota reservation behavior.

T-Rex T-Rex Logs

What T-Rex did

  • We tested header-rotation against the rate limit by sending six unauthenticated Looney requests while rotating X-Forwarded-For and X-Looney-Browser-ID; the endpoint accepted five and returned HTTP 429 on the sixth.
  • We exercised per-IP quota isolation by issuing six browser-ID-free requests from distinct IPs; the current behavior allocated unique buckets so all six requests succeeded.
  • We ran a concurrency harness that issued six simultaneous requests against a five-reservation quota; five reservations were persisted and five upstream jobs were created, while the sixth request was rejected before upstream.
  • We opened a direct-route Chromium check against the completed-job route; the app did not mount, so the direct route behavior could not be observed from this run.
  • We summarize the before/after: before, all six requests created upstream effects; after, five reservations persisted and one request was rejected with 429 before reaching upstream; no application source files were changed, only additional validation artifacts.

View all artifacts

T-Rex Ran code and verified through T-Rex

Reviews (5): Last reviewed commit: "fix: use pixel corners across looney ui" | Re-trigger Greptile

Comment thread api/looney-check.js Outdated
Comment on lines +24 to +26
function getClientIp(request) {
const forwarded = getHeader(request, 'x-forwarded-for') || getHeader(request, 'x-real-ip') || 'unknown';
return String(forwarded).split(',')[0].trim() || 'unknown';

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 security Caller-controlled rate-limit identities

Unauthenticated clients can exceed the five-check daily limit by changing both X-Forwarded-For and X-Looney-Browser-ID. These client-supplied values are the only rate-limit buckets for unauthenticated traffic, so rotating them creates fresh buckets and permits more upstream jobs after the original identities have reached the limit. Derive the source identity from trusted infrastructure and add a server-controlled abuse-control bucket for anonymous requests.

Artifacts

Focused Looney rate-limit validation script

  • This authored Node script invokes the actual API handler against local Supabase-RPC and upstream HTTP stubs, showing the handler accepts a rotated identity after its original bucket is exhausted.

Looney rate limit before identity rotation

  • This captured command output shows five HTTP 201 upstream-job responses followed by HTTP 429 with the five-per-day limit message for the unchanged identities.

Looney rate limit after rotating browser and forwarded IP headers

  • This captured command output shows that rotating both client-controlled headers changes both bucket hashes and yields HTTP 201 with a sixth upstream job after the prior HTTP 429.

View artifacts

T-Rex Ran code and verified through T-Rex

Comment thread src/pages/LooneyResultPage.tsx Outdated
Comment on lines +14 to +15
const [record, setRecord] = useState<LooneyHistoryRecord | null>(() => loadLooneyHistory().find((item) => item.jobId === jobId) || null);
useEffect(() => { const refresh = () => setRecord(loadLooneyHistory().find((item) => item.jobId === jobId) || null); window.addEventListener(looneyHistoryUpdateEvent, refresh); return () => window.removeEventListener(looneyHistoryUpdateEvent, refresh); }, [jobId]);

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 Shared result links depend on browser-local history

Opening /gappa/check/:jobId outside the originating browser session renders Check failed even when the job ID is valid. The page only loads the local Looney history and does not request the job from the server when that record is absent, so shared links, private windows, and cleared sessions cannot display completed reports. Fetch the job by ID as a fallback and persist the recovered result locally.

- Extract shared CORS origins to api/cors.js and reuse across server and worker
- Use platform IP header for anonymous rate limiting and skip spoofable forwarded headers
- Validate upstream response before consuming rate-limit quota
- Add keyboard/accessibility to file drop zone
- Fix Supabase table types and narrow preview URL construction
- Limit background-generator uploads to 20 files at 10 MB each
- Redirect /music-copyright to /gappa

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/LooneyResultDisplay.tsx`:
- Line 42: Update the date formatting logic in the entries mapping of
LooneyResultDisplay so date-only YYYY-MM-DD values preserve their calendar day
in every timezone. Parse those values as local dates or format them using UTC,
while keeping existing handling for other date values unchanged.

In `@src/pages/LooneyResultPage.tsx`:
- Around line 49-54: Update the jobId-loading effect and currentRecord flow in
LooneyResultPage so navigation cannot retain or use a record from a different
job: clear record and set the not-found/loading state when loadLooneyHistory
finds no matching item, and derive rendering and running status through an
activeRecord whose jobId equals the current route jobId. Ensure currentRecord()
falls back only to that validated active record, preventing stream updates for
one job from being saved into another.

In `@src/utils/migrateResources.ts`:
- Around line 43-68: Validate every legacy resource category against the allowed
database categories before constructing normalizedData, covering both
object-entry keys and resource.category values in the array path; reject missing
or unsupported categories instead of mapping them to "uncategorized". Perform
this validation before any destructive migration begins, while preserving the
existing resource-shape validation and normalization for valid categories.

In `@supabase/migrations/20260810000000_looney_check_rate_limits.sql`:
- Line 15: Update the pg_cron extension declaration to install it in the
pg_catalog schema instead of extensions, ensuring the subsequent cron.job and
cron.schedule objects can be created on fresh databases.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7194c051-b734-4872-a929-359cf605cfeb

📥 Commits

Reviewing files that changed from the base of the PR and between 4889c81 and 3006a73.

⛔ Files ignored due to path filters (1)
  • public/assets/looney-icon.png is excluded by !**/*.png
📒 Files selected for processing (22)
  • api/cors.js
  • api/looney-check.js
  • server.js
  • src/App.tsx
  • src/components/LooneyCheckDialog.tsx
  • src/components/LooneyCheckForm.tsx
  • src/components/LooneyHistorySection.tsx
  • src/components/LooneyResultDisplay.tsx
  • src/components/PopularTools.tsx
  • src/components/resources/ResourceCard.tsx
  • src/components/ui/toggle-group.tsx
  • src/index.css
  • src/integrations/supabase/types.d.ts
  • src/integrations/supabase/types.ts
  • src/pages/BackgroundGenerator.tsx
  • src/pages/LooneyResultPage.tsx
  • src/pages/MusicCopyright.tsx
  • src/pages/ResourcesHub.tsx
  • src/utils/looneyChecker.ts
  • src/utils/looneyHistory.ts
  • src/utils/migrateResources.ts
  • supabase/migrations/20260810000000_looney_check_rate_limits.sql
🚧 Files skipped from review as they are similar to previous changes (14)
  • server.js
  • src/components/LooneyCheckDialog.tsx
  • src/components/PopularTools.tsx
  • src/components/LooneyHistorySection.tsx
  • src/components/ui/toggle-group.tsx
  • src/App.tsx
  • src/pages/ResourcesHub.tsx
  • src/pages/MusicCopyright.tsx
  • api/looney-check.js
  • src/utils/looneyChecker.ts
  • src/index.css
  • src/utils/looneyHistory.ts
  • src/pages/BackgroundGenerator.tsx
  • src/components/resources/ResourceCard.tsx

Comment thread src/components/LooneyResultDisplay.tsx Outdated
if (!entries.length) return null;
const trackLabel = entries.find(([key]) => sourceLabelKeys.includes(key.toLowerCase()))?.[1];
const trackLinkLabel = trackLabel === undefined ? undefined : primitive(trackLabel) || undefined;
return <div className="grid min-w-0 gap-3 sm:grid-cols-2">{entries.map(([key, value]) => { const raw = primitive(value) || ''; const parsedDate = key.toLowerCase().includes('date') ? new Date(raw) : null; const display = key.toLowerCase().includes('duration') && Number.isFinite(Number(raw)) ? `${Math.floor(Number(raw) / 60000)} min ${Math.round((Number(raw) % 60000) / 1000)} sec` : parsedDate && !Number.isNaN(parsedDate.getTime()) ? parsedDate.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }) : raw; return <div key={key} className="min-w-0 rounded-md border border-border/70 bg-muted/20 p-3"><p className="text-xs uppercase tracking-wide text-muted-foreground">{labelize(key)}</p><div className="mt-1 break-words font-medium">{isUrl(raw) ? <SourceLink value={raw} label={trackLinkLabel} spotify={/open\.spotify\.com/i.test(raw)} /> : display}</div></div>; })}</div>;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

node -e 'const d = new Date("2026-08-11"); console.log(d.toLocaleDateString("en-US", { timeZone: "America/New_York" }))'

Repository: creatorcluster/renderdragon.org

Length of output: 179


🏁 Script executed:

#!/bin/bash
set -eu

file="$(git ls-files | rg '^src/components/LooneyResultDisplay\.tsx$')"
printf '%s\n' "$file"
sed -n '1,90p' "$file"

node <<'JS'
const raw = '2026-08-11';
const dateOnly = /^\d{4}-\d{2}-\d{2}$/.test(raw);
for (const timeZone of ['America/New_York', 'UTC', 'Pacific/Honolulu']) {
  const localDate = new Date(`${raw}T00:00:00`);
  const utcDate = new Date(`${raw}T00:00:00Z`);
  console.log(JSON.stringify({
    timeZone,
    nativeDateOnly: new Date(raw).toLocaleDateString('en-US', { timeZone }),
    localMidnight: localDate.toLocaleDateString('en-US', { timeZone }),
    utcMidnight: utcDate.toLocaleDateString('en-US', { timeZone }),
    dateOnly
  }));
}
JS

Repository: creatorcluster/renderdragon.org

Length of output: 9409


Preserve date-only values as local calendar dates.

Line 42 parses YYYY-MM-DD values as UTC midnight. In US time zones, 2026-08-11 can display as August 10, 2026. Parse date-only values as local dates, or format those values with timeZone: 'UTC'.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/LooneyResultDisplay.tsx` at line 42, Update the date
formatting logic in the entries mapping of LooneyResultDisplay so date-only
YYYY-MM-DD values preserve their calendar day in every timezone. Parse those
values as local dates or format them using UTC, while keeping existing handling
for other date values unchanged.

Comment on lines +49 to +54
const localRecord = loadLooneyHistory().find((item) => item.jobId === jobId);
if (localRecord) {
setRecord(localRecord);
setLoading(false);
setNotFound(false);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reset and validate the active record when jobId changes.

If navigation changes from job A to job B, record can still contain job A. Line 91 then treats job A as job B. The stream callback can save job B progress into job A because currentRecord() falls back to recordRef.current.

Clear the record for a missing local job. Derive rendering and running from a record whose jobId equals the route jobId.

Proposed fix
-  const running = record?.status === 'queued' || record?.status === 'running';
+  const activeRecord = record?.jobId === jobId ? record : null;
+  const running = activeRecord?.status === 'queued' || activeRecord?.status === 'running';

Also set record to null before loading a route job that has no local record, and use activeRecord in the render branches.

Also applies to: 91-104

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/LooneyResultPage.tsx` around lines 49 - 54, Update the
jobId-loading effect and currentRecord flow in LooneyResultPage so navigation
cannot retain or use a record from a different job: clear record and set the
not-found/loading state when loadLooneyHistory finds no matching item, and
derive rendering and running status through an activeRecord whose jobId equals
the current route jobId. Ensure currentRecord() falls back only to that
validated active record, preventing stream updates for one job from being saved
into another.

Comment on lines +43 to +68
const normalizedData: Record<string, JsonResource[]> = Array.isArray(sourceData)
? sourceData.reduce(
(acc, resource) => {
const resourceId = (resource as Resource).id;
if (typeof resourceId !== "number") return acc;
const category = (resource as Resource).category || "uncategorized";
if (!isJsonResource(resource)) throw new Error("Resource JSON contains a malformed resource");
const typedResource = resource as Resource;
const category = typedResource.category || "uncategorized";
if (!acc[category]) acc[category] = [];
acc[category].push({
id: resourceId,
title: (resource as Resource).title,
credit: (resource as Resource).credit || undefined,
filetype: (resource as Resource).filetype || undefined,
software: (resource as Resource).software || undefined,
description: (resource as Resource).description || undefined,
preview_url: (resource as Resource).preview_url || undefined,
id: typedResource.id as number,
title: typedResource.title,
credit: typedResource.credit || undefined,
filetype: typedResource.filetype || undefined,
software: typedResource.software || undefined,
description: typedResource.description || undefined,
preview_url: typedResource.preview_url || undefined,
});
return acc;
},
{} as Record<string, JsonResource[]>,
)
: (jsonData as JsonResourcesData);
: Object.entries(sourceData).reduce<Record<string, JsonResource[]>>((acc, [category, resources]) => {
if (!Array.isArray(resources) || !resources.every(isJsonResource)) {
throw new Error(`Resource JSON category "${category}" is malformed`);
}
acc[category] = resources;
return acc;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the declared resource category contract and database constraints.
rg -n -C 5 'ResourceCategory|category.*CHECK|CHECK.*category|CREATE TABLE.*resources' src supabase

# Inspect source resource files for category values.
fd -a 'resources*.json' . -x sh -c '
  echo "=== $1 ==="
  jq -r "
    if type == \"array\" then .[] | .category // \"<missing>\"
    elif type == \"object\" then keys[]
    else empty
    end
  " "$1" | sort -u
' sh {}

Repository: creatorcluster/renderdragon.org

Length of output: 3343


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,170p' src/utils/migrateResources.ts
printf '\n=== category usage and validators ===\n'
rg -n -C 4 'isJsonResource|normalizedData|typedResource.category|from\\("resources"|resource_category' src supabase

Repository: creatorcluster/renderdragon.org

Length of output: 6957


Validate category names before the destructive migration.

The database accepts only music, sfx, images, animations, fonts, and presets. Validate both object keys and legacy resource categories before building normalizedData; isJsonResource does not validate category, and missing legacy categories become "uncategorized".

Invalid categories currently trigger the delete before inserts fail, which can leave the table empty or partially migrated.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/migrateResources.ts` around lines 43 - 68, Validate every legacy
resource category against the allowed database categories before constructing
normalizedData, covering both object-entry keys and resource.category values in
the array path; reject missing or unsupported categories instead of mapping them
to "uncategorized". Perform this validation before any destructive migration
begins, while preserving the existing resource-shape validation and
normalization for valid categories.

create index if not exists looney_check_rate_limits_user_idx
on public.looney_check_rate_limits (user_id, window_started_at);

create extension if not exists pg_cron with schema extensions;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

: "${SUPABASE_DB_URL:?Set SUPABASE_DB_URL to a non-production Supabase database URL}"

psql "$SUPABASE_DB_URL" -v ON_ERROR_STOP=1 -c \
  "select name, default_version, installed_version
   from pg_available_extensions
   where name = 'pg_cron';"

psql "$SUPABASE_DB_URL" -v ON_ERROR_STOP=1 -c \
  "select extname, extnamespace::regnamespace as schema, extrelocatable
   from pg_extension
   where extname = 'pg_cron';"

Repository: creatorcluster/renderdragon.org

Length of output: 262


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="$(git ls-files 'supabase/migrations/20260810000000_looney_check_rate_limits.sql')"
printf '%s\n' "$file"
[ -n "$file" ]

cat -n "$file"
printf '\nOther pg_cron references:\n'
rg -n -C 3 'pg_cron|cron\.schedule|cron\.job' supabase README.md .github 2>/dev/null || true

Repository: creatorcluster/renderdragon.org

Length of output: 7492


🌐 Web query:

Supabase pg_cron CREATE EXTENSION WITH SCHEMA extensions fixed cron schema relocatable

💡 Result:

On Supabase, you must install the pg_cron extension into the pg_catalog schema [1][2][3]. Attempting to install it into the extensions schema will typically result in an error, such as: ERROR: extension "pg_cron" must be installed in schema "pg_catalog" [2][3]. The pg_cron extension is non-relocatable, meaning its internal design requires it to reside in a fixed schema [4][5]. Specifically, pg_cron is configured to operate within the pg_catalog schema, and it automatically creates and manages its own separate cron schema for scheduling jobs [6][7]. For proper setup on Supabase, use the following SQL command [1]: CREATE EXTENSION pg_cron WITH SCHEMA pg_catalog; After creating the extension, you may need to grant access to the cron schema to the appropriate database roles [1][8]: GRANT USAGE ON SCHEMA cron TO postgres; GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA cron TO postgres; Because pg_cron is non-relocatable, it cannot be moved using ALTER EXTENSION... SET SCHEMA, as this would violate the extension's internal constraints [5][9][10]. If you have incorrectly installed it or need to change its configuration, you must drop the extension (which will delete all existing cron jobs) and recreate it correctly [1][10]. Always exercise caution when dropping the extension, as it will cascade-drop any dependent objects [1][10].

Citations:


Install pg_cron in pg_catalog.

Supabase requires pg_cron in the pg_catalog schema. Change line 15 to create extension if not exists pg_cron with schema pg_catalog;. The migration otherwise cannot create the later cron.job and cron.schedule objects on a fresh database.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/migrations/20260810000000_looney_check_rate_limits.sql` at line 15,
Update the pg_cron extension declaration to install it in the pg_catalog schema
instead of extensions, ensuring the subsequent cron.job and cron.schedule
objects can be created on fresh databases.

Comment thread api/looney-check.js Outdated
Comment on lines +51 to +53
} else {
// Anonymous callers must share a server-controlled bucket; client headers are not identities.
buckets.push({ type: 'ip', hash: hashIdentifier('anonymous') });

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 security Anonymous quota is shared globally

Unauthenticated requests that omit X-Looney-Browser-Id all consume the same hashIdentifier('anonymous') bucket. After any five such checks succeed, unrelated visitors without that header receive HTTP 429 until the daily reset. Use a requester-specific trusted infrastructure identity for this bucket, with an explicit fallback policy when it is unavailable.

Artifacts

Validation script for six anonymous Looney checks

  • A local mock Supabase RPC and upstream-job harness invokes the current handler six times with no authorization or browser identifier, showing the exercised condition.

Six anonymous Looney checks share the quota and the sixth is rejected

  • Captured execution output shows five 201 job responses followed by a 429, with the same IP bucket hash consumed on every successful request, confirming the shared anonymous quota.

View artifacts

T-Rex Ran code and verified through T-Rex

Comment thread api/looney-check.js Outdated
Comment on lines +267 to +280
const upstream = await buildUpstreamRequest(request);
const preflightRateLimit = await consumeRateLimit(request, false);
if (!preflightRateLimit.allowed) {
const retryAfter = String(preflightRateLimit.retryAfter || 86400);
return jsonResponse(request, { error: `Daily limit reached. You can run up to ${DAILY_CHECK_LIMIT} checks per day.`, retry_after_seconds: Number(retryAfter) }, 429);
}
const response = await fetch(`${getLooneyBaseUrl()}/jobs`, {
method: 'POST',
headers: upstreamHeaders(upstream.contentType),
body: upstream.body,
signal: AbortSignal.timeout(30000),
});
if (!response.ok) return proxyUpstreamResponse(request, response, true);
const rateLimit = await consumeRateLimit(request, 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.

P1 security Quota is consumed after job creation

Concurrent requests can all pass the non-consuming rate-limit preflight and create upstream Looney jobs before quota is consumed. With one remaining slot, the executed check created six jobs; only one response succeeded and the other five returned HTTP 429 after their jobs already existed. Reserve quota atomically before the upstream side effect, releasing the reservation only if job creation fails.

Artifacts

Concurrent quota-race validation harness source

  • Authored harness directly invokes the current handler with mocked Supabase RPC and Looney boundaries, demonstrating the quota ordering under concurrent requests; it is the executable proof source.

Exhausted quota control run

  • Executed control run starts at 5 of 5 quota and shows six preflights returning 429 with zero upstream jobs; an already exhausted bucket is blocked before job creation.

Concurrent quota-race execution output

  • Executed race run starts at 4 of 5 quota and shows six upstream jobs created before consumption, followed by one 201 and five 429 responses; the claimed failure path is reproduced.

Captured concurrent quota-race harness source

  • Captured output of the command that read the authored harness source, including command, working directory, and exit code; it records the exact executed test implementation.

View artifacts

T-Rex Ran code and verified through T-Rex

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@api/cors.js`:
- Around line 13-22: Update isLocalNetworkHostname to accept IPv6 Unique Local
Addresses in the fc00::/7 range, including bracketed hostnames such as
[fd00::1], while preserving the existing loopback and IPv4 private-range checks.
Ensure the hostname is validated through new URL() before applying the IPv6 ULA
check.

In `@server.js`:
- Around line 34-40: Configure Express proxy trust in the server initialization
to trust only the known proxy hops, and ensure the reverse proxy overwrites
X-Forwarded-For rather than appending to it. Preserve the existing req.ip usage
in createAdapter so rate limiting receives the originating client address.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 24477b6a-9258-4ded-9aa9-500d76e81b12

📥 Commits

Reviewing files that changed from the base of the PR and between 3006a73 and 01a4e08.

⛔ Files ignored due to path filters (1)
  • public/assets/looney-icon.png is excluded by !**/*.png
📒 Files selected for processing (7)
  • api/cors.js
  • api/looney-check.js
  • server.js
  • src/integrations/supabase/types.ts
  • src/utils/looneyChecker.ts
  • supabase/migrations/20260810000000_looney_check_rate_limits.sql
  • supabase/migrations/20260811000000_looney_check_rate_limit_release.sql
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/integrations/supabase/types.ts
  • api/looney-check.js
  • src/utils/looneyChecker.ts

Comment thread api/cors.js
Comment on lines +13 to +22
const isLocalNetworkHostname = (hostname) => {
if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]') return true;
const octets = hostname.split('.').map(Number);
if (octets.length !== 4 || octets.some((octet) => !Number.isInteger(octet) || octet < 0 || octet > 255)) return false;
const [first, second] = octets;
return first === 10
|| (first === 172 && second >= 16 && second <= 31)
|| (first === 192 && second === 168)
|| (first === 100 && second >= 64 && second <= 127);
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Allow IPv6 local-network origins.

Line 14 only allows the IPv6 loopback address. An IPv6-only LAN origin such as http://[fd00::1]:5173 fails CORS validation.

Accept IPv6 ULA addresses in fc00::/7 after new URL() validates the hostname.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/cors.js` around lines 13 - 22, Update isLocalNetworkHostname to accept
IPv6 Unique Local Addresses in the fc00::/7 range, including bracketed hostnames
such as [fd00::1], while preserving the existing loopback and IPv4 private-range
checks. Ensure the hostname is validated through new URL() before applying the
IPv6 ULA check.

Comment thread server.js
Comment on lines 34 to +40
const createAdapter = (handler) => (req, res) => {
const vercelReq = {
method: req.method,
headers: req.headers,
headers: {
...req.headers,
'x-vercel-ip': req.ip || req.socket.remoteAddress || '',
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect Express proxy trust configuration.
rg -n -C 4 "trust proxy|app\.set\s*\(\s*['\"]trust proxy" server.js .

# Trace the forwarded address into the Looney rate-limit implementation.
rg -n -C 5 "x-vercel-ip|X-Forwarded-For|remoteAddress|req\.ip" api/looney-check.js server.js

Repository: creatorcluster/renderdragon.org

Length of output: 169


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- candidate files ---'
git ls-files | rg '(^|/)(server\.js|package\.json|.*looney.*|.*app.*|.*config.*)$' || true

printf '%s\n' '--- server.js outline and relevant source ---'
if [ -f server.js ]; then
  wc -l server.js
  ast-grep outline server.js || true
  cat -n server.js
fi

printf '%s\n' '--- proxy trust and IP consumers across tracked source/config ---'
rg -n -C 6 --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' \
  "trust proxy|x-vercel-ip|X-Forwarded-For|remoteAddress|req\.ip|rate.?limit|rateLimit|Looney|looney" . || true

printf '%s\n' '--- package metadata ---'
for f in package.json package-lock.json yarn.lock pnpm-lock.yaml; do
  [ -f "$f" ] && { echo "### $f"; cat -n "$f"; }
done

Repository: creatorcluster/renderdragon.org

Length of output: 50387


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- tracked server and Looney files ---'
git ls-files | rg '(^|/)(server\.js|.*looney.*)$' || true

printf '%s\n' '--- server.js ---'
if [ -f server.js ]; then
  cat -n server.js
else
  echo 'server.js not found at repository root'
fi

printf '%s\n' '--- focused source/config matches, excluding lockfiles and generated/vendor paths ---'
rg -n -C 8 \
  --glob '!*.lock' \
  --glob '!**/node_modules/**' \
  --glob '!**/dist/**' \
  --glob '!**/build/**' \
  --glob '!**/.next/**' \
  "trust proxy|x-vercel-ip|X-Forwarded-For|remoteAddress|req\.ip|rate.?limit|rateLimit|Looney|looney" . || true

printf '%s\n' '--- relevant package scripts and dependencies ---'
if [ -f package.json ]; then
  node - <<'JS'
const fs = require('fs');
const p = JSON.parse(fs.readFileSync('package.json', 'utf8'));
console.log(JSON.stringify({
  scripts: p.scripts,
  dependencies: p.dependencies,
  devDependencies: p.devDependencies
}, null, 2));
JS
fi

Repository: creatorcluster/renderdragon.org

Length of output: 50388


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- deployment and runtime configuration files ---'
git ls-files | rg '(^|/)(render\.yaml|render\.yml|Dockerfile|Procfile|fly\.toml|railway\.json|vercel\.json|\.env\.example|README(\..*)?)$' || true

printf '%s\n' '--- deployment references ---'
rg -n -C 4 \
  --glob '!*.lock' \
  --glob '!**/node_modules/**' \
  --glob '!**/.next/**' \
  "render\.com|Render|reverse proxy|proxy|PORT|server\.js|node server|trust proxy" \
  README* .github . 2>/dev/null | head -n 300 || true

printf '%s\n' '--- read-only static call-path verifier ---'
python3 - <<'PY'
from pathlib import Path
import re

server = Path("server.js").read_text()
looney = Path("api/looney-check.js").read_text()

trust_proxy = re.findall(r"""(?:app|express)\.set\s*\(\s*['"]trust proxy['"]\s*,\s*([^)]+)\)""", server)
adapter_ip = re.findall(r"""['"]x-vercel-ip['"]\s*:\s*([^,\n]+)""", server)
looney_ip = re.findall(r"""function getClientIp\(request\).*?return platformIp.*?""", looney, re.S)
ip_bucket = re.findall(r"""buckets\.push\(\{ type: ['"]ip['"],\s*hash:\s*hashIdentifier\(`ip:\$\{([^}]+)\}""", looney)

print("trust_proxy_configuration:", trust_proxy or "none")
print("adapter_x_vercel_ip_expression:", adapter_ip or "not found")
print("looney_get_client_ip_uses_platform_header:", bool(looney_ip))
print("looney_ip_bucket_inputs:", ip_bucket or "not found")
print("adapter_routes_looney_handler:", "app.all('/api/looney-check', createAdapter(looneyCheckHandler))" in server)
PY

Repository: creatorcluster/renderdragon.org

Length of output: 16894


Configure Express proxy trust for the Looney IP bucket. If this server runs behind a reverse proxy, server.js does not configure trust proxy, so req.ip resolves to the proxy address and clients share one rate-limit bucket. Configure only the known proxy hops, and ensure the proxy overwrites X-Forwarded-For.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server.js` around lines 34 - 40, Configure Express proxy trust in the server
initialization to trust only the known proxy hops, and ensure the reverse proxy
overwrites X-Forwarded-For rather than appending to it. Preserve the existing
req.ip usage in createAdapter so rate limiting receives the originating client
address.

@greptile-apps

greptile-apps Bot commented Aug 11, 2026

Copy link
Copy Markdown

Want your agent to iterate on Greptile's feedback? Try greploops.

@creatorcluster
creatorcluster merged commit fa1d009 into creatorcluster:main Aug 11, 2026
2 of 4 checks passed
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.

2 participants