feat: add Looney copyright check, animated background export, and UI polish - #76
Conversation
- 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
|
@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. |
|
Warning Review limit reached
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 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 configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (8)
📝 WalkthroughWalkthroughThe 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. ChangesLooney Checks and Background Generation
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
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
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 winThe copyright callback has no effect on the Minecraft music list.
ResourceCardrenders the copyright button only whenresource.category === "music"(src/components/resources/ResourceCard.tsxLine 182). This list suppliesminecraft-musicresources, so the button never appears. Either remove the prop here, or extend the condition inResourceCardto includeminecraft-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 winLimit 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 withtoast.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 winThe video preview goes stale after a control change.
invalidateGenerationruns only for image selection changes. The sliders for spacing, opacity, scale, rotation, size, and the pattern selector do not clearvideoUrl. 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. ClearvideoUrlin 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 winThe tab label promises GIF output, but the tool exports WebM only.
handleCreateVideorecords a WebM blob andhandleVideoDownloadsaves a.webmfile. Rename the tab to "Video" or "Animated (WebM)". Consider renaming theOutputModevalue"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 winThe seed changes nothing for four of the five pattern styles.
randomis consumed only by the Fisher-Yates shuffle at Lines 284-287, and that shuffle is read only in the"random"branch. Forgrid,staggered,diagonal, andscattered, the "Randomize" button at Lines 746-758 incrementsrandomSeed, clears the preview, and regenerates an identical image. Apply seeded jitter (position, scale, ortileRotation) in those branches, or disable the button when the selected pattern ignores the seed.Note that
drawTileacceptstileRotationat 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 winGive the animation sliders an accessible name.
The
labelelements at Lines 809, 817, and 825 are not associated with the Radix sliders, and these sliders have noaria-label. A screen reader announces an unnamed slider. The rotation slider at Line 788 already setsaria-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 winKeep
IconArrowRightabove the hover layers.When
tool.backgroundImageis set, the image and gradient are absolutely positioned. The icon container and text usez-10, butIconArrowRightdoes not. On hover, the gradient can cover or reduce the contrast of the arrow.Add
relative z-10toIconArrowRightor 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 winThe limit banner has no reset contract in either parent.
LooneyCheckFormreports the concurrency limit throughonLimitReached, 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: passonCheckStart={() => setLimitMessage(false)}toLooneyCheckFormso the banner clears when a check starts.src/components/LooneyCheckDialog.tsx#L19-L19: also callsetLimitReached(false)inonOpenChangewhen 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 winKeep the previous status when the job payload omits one.
updateLooneyHistoryFromJobassignsjob.statusunconditionally.getLooneyJobreturns the parsed body without validating it, so a response that omitsstatussetsrecord.statustoundefined. The record then leaves the "Running checks" list and renders with the failure icon inLooneyHistorySection.🛡️ 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 winGuard the date formatting against unparseable values.
Any key that contains "date" is passed to
new Date(raw). If the API returns a value thatDatecannot parse,toLocaleDateStringrenders "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 winReset
eventNamebefore the early return.
processEventreturns at Line 150 wheneventDatais empty, soeventNamekeeps its previous value. An SSE frame that carriesevent: completewith nodata:line leaveseventNameascomplete. 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 winMemoize
filteredTextures.The fuzzy filter runs on every render. Each slider drag or recording-progress update re-scores the whole texture list, and
fuzzyTextureScorecallseditDistanceper word pair. Wrap the computation inuseMemokeyed ontexturesandtextureSearch.♻️ 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
useMemoto 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 winReuse
loadSelectedImagesinhandleGenerate.Lines 342-348 duplicate
loadSelectedImagesat Lines 383-389 exactly. MoveloadSelectedImagesabovehandleGenerateand 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 winKeep successfully read images when one file fails.
Promise.allrejects on the firstFileReadererror. All other images in the same batch are then discarded. UsePromise.allSettledand 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 winKeep
ToggleGroupItem.valuerequired.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 winConfirm whether single selection is an invariant.
Because
{...props}followstype="single", a caller can still provide another toggle-group type. If this wrapper must always be single-select, placetype="single"after the spread or removetypefrom 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 winPin or self-host the Minecraft font.
The
@font-facesource uses the mutablemainbranch. 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 valueNote the coupling between the hash salt and the Supabase key.
hashIdentifierderives the salt fromSUPABASE_SECRET_KEY. A key rotation changes every bucket hash and resets all daily counters for that day. Use a dedicated secret, for exampleLOONEY_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 winReplace the substring-based status mapping with explicit error typing.
The mapping tests
message.includes('valid'). The database function raisesInvalid rate-limit bucket, which containsvalid. 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
ValidationErrorinbuildUpstreamRequestandreadBodyfor 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 winConsider consuming the rate limit only after the upstream job is created.
consumeRateLimitincrements 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 winConsider sharing one CORS origin allowlist.
The same origins are now declared here and in
api/looney-check.jsLines 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 winWrap
onCheckCopyrightinuseCallback.
ResourcesListis exported asReact.memo(ResourcesList). A new function identity on every render ofResourcesHubdefeats that memoization for both lists.♻️ Proposed refactor
- const onCheckCopyright = (resource: Resource) => { - setCopyrightResource(resource); - }; + const onCheckCopyright = useCallback((resource: Resource) => { + setCopyrightResource(resource); + }, []);Add
useCallbackto 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 winAdd 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_cronjob that deletes rows wherewindow_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 winMake the daily limit a parameter instead of a literal.
The value
5is duplicated here and asDAILY_CHECK_LIMITinapi/looney-check.jsLine 6. The two values can drift. Add ap_limit integer default 5argument, 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/grantstatements 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 valueCombine the read and the increment into a single pass.
The function iterates
p_bucketstwice. The first loop holdsFOR UPDATElocks, so the transaction is serialized correctly, but the second loop repeats the same lookups. A singleupdate ... returningper bucket, or a set-based statement, reduces the work and removes the duplicated matching logic. Duplicate entries inp_bucketsalso 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 winNarrow the effect dependency and skip fonts that are already registered.
The dependency list now contains the whole
resourceobject. If a parent recreates resource objects between renders, the identity changes and the effect refetches the font file. Each run also callsdocument.fonts.addagain for the same family, sodocument.fontsaccumulates duplicate entries.Depend on the specific fields and check
document.fonts.checkbefore 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 winRegenerate the Supabase types instead of using
anyplaceholders.Every table now resolves to
Row: any,Insert: any,Update: any, andFunctionsaccepts any name withanyarguments. All compile-time checks on Supabase queries across the application are lost. Column renames and the newconsume_looney_check_rate_limitsignature will not be validated.Run
supabase gen types typescriptand 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 valueReset
limitReachedwhen the dialog closes.
limitReachedstaystrueafteronClose.onCheckStartis 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 valueConsider redirecting one of the duplicate routes.
/music-copyrightand/gapparender the sameMusicCopyrightpage.GlobalComponentspasseslocation.pathnametoSeo, 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 valueCollapse 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 valueGuard the state write that runs after
await.
setRecordsat Line 63 runs after all streams settle. IfactiveJobIdchanges, the cleanup aborts the controllers, but the pendingstreamRunningJobscall 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 winReuse
isFreshRunningRecordinfindRunningLooneyCheck.
countRunningLooneyChecksignores running records older thanMAX_RUNNING_LOONEY_AGE_MS, butfindRunningLooneyCheckdoes not. A stalequeuedrecord therefore still blocks a new check for the same source and opens the "Check already running" dialog.refreshRunningLooneyChecksexpires 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 valueRename the shared image asset.
public/assets/image copy.pngexists 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
⛔ Files ignored due to path filters (2)
public/assets/image copy.pngis excluded by!**/*.pngpublic/assets/minecraft-pattern-background-1920x1080.pngis excluded by!**/*.png
📒 Files selected for processing (42)
api/looney-check.jsserver.jssrc/App.tsxsrc/components/Carousel.tsxsrc/components/ErrorBoundary.tsxsrc/components/Footer.tsxsrc/components/Hero.tsxsrc/components/LooneyCheckDialog.tsxsrc/components/LooneyCheckForm.tsxsrc/components/LooneyHistorySection.tsxsrc/components/LooneyResultDisplay.tsxsrc/components/LooneyRunningCheckDialog.tsxsrc/components/Navbar.tsxsrc/components/PopularTools.tsxsrc/components/ResultsDisplay.tsxsrc/components/RollingGallery.tsxsrc/components/UploadThingClient.tsxsrc/components/ValueProps.tsxsrc/components/generators/MinecraftNametagGenerator.tsxsrc/components/resources/ResourceCard.tsxsrc/components/resources/ResourcesList.tsxsrc/components/ui/toggle-group.tsxsrc/hooks/useMinecraftMusic.tssrc/index.csssrc/integrations/supabase/types.tssrc/pages/BackgroundGenerator.tsxsrc/pages/BlogView.tsxsrc/pages/Community.tsxsrc/pages/Contact.tsxsrc/pages/Index.tsxsrc/pages/LooneyResultPage.tsxsrc/pages/MusicCopyright.tsxsrc/pages/NotFound.tsxsrc/pages/ResourcesHub.tsxsrc/types/copyright.tssrc/types/looney.tssrc/utils/copyrightChecker.tssrc/utils/looneyChecker.tssrc/utils/looneyHistory.tssrc/utils/migrateResources.tssupabase/migrations/20260810000000_looney_check_rate_limits.sqlvite.config.ts
💤 Files with no reviewable changes (3)
- src/components/ResultsDisplay.tsx
- src/utils/copyrightChecker.ts
- src/types/copyright.ts
Greptile SummaryThis 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/5No blocking failure remains. No accepted blocking finding remains after exercising header rotation, anonymous visitor isolation, and concurrent quota reservation behavior.
What T-Rex did
Reviews (5): Last reviewed commit: "fix: use pixel corners across looney ui" | Re-trigger Greptile |
| function getClientIp(request) { | ||
| const forwarded = getHeader(request, 'x-forwarded-for') || getHeader(request, 'x-real-ip') || 'unknown'; | ||
| return String(forwarded).split(',')[0].trim() || 'unknown'; |
There was a problem hiding this comment.
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.
| 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]); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
public/assets/looney-icon.pngis excluded by!**/*.png
📒 Files selected for processing (22)
api/cors.jsapi/looney-check.jsserver.jssrc/App.tsxsrc/components/LooneyCheckDialog.tsxsrc/components/LooneyCheckForm.tsxsrc/components/LooneyHistorySection.tsxsrc/components/LooneyResultDisplay.tsxsrc/components/PopularTools.tsxsrc/components/resources/ResourceCard.tsxsrc/components/ui/toggle-group.tsxsrc/index.csssrc/integrations/supabase/types.d.tssrc/integrations/supabase/types.tssrc/pages/BackgroundGenerator.tsxsrc/pages/LooneyResultPage.tsxsrc/pages/MusicCopyright.tsxsrc/pages/ResourcesHub.tsxsrc/utils/looneyChecker.tssrc/utils/looneyHistory.tssrc/utils/migrateResources.tssupabase/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
| 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>; |
There was a problem hiding this comment.
🎯 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
}));
}
JSRepository: 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.
| const localRecord = loadLooneyHistory().find((item) => item.jobId === jobId); | ||
| if (localRecord) { | ||
| setRecord(localRecord); | ||
| setLoading(false); | ||
| setNotFound(false); | ||
| } |
There was a problem hiding this comment.
🗄️ 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.
| 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; |
There was a problem hiding this comment.
🗄️ 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 supabaseRepository: 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; |
There was a problem hiding this comment.
🩺 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 || trueRepository: 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:
- 1: https://supabase.com/docs/guides/cron/install
- 2: pg_cron SQL activation instructions are incorrect supabase/supabase#28261
- 3:
ERROR: extension "pg_cron" must be installed in schema "pg_catalog"After1.169.8Upgrade supabase/cli#2342 - 4: https://deepwiki.com/supabase/postgres/4-postgresql-extensions
- 5: https://www.postgresql.org/docs/19/extend-extensions.html
- 6: cron schema not available in seed.sql (pg_cron) supabase/supabase#28966
- 7: https://stackoverflow.com/questions/68132081/schema-for-pg-cron-when-initializing-the-extension
- 8: https://github.com/citusdata/pg_cron
- 9: https://www.postgresql.org/message-id/CAKFQuwa1cb9y6OW9rSmXFGH69J3ZEQOm3%2BOd4Ang4qi74YHcTw%40mail.gmail.com
- 10: Managed pg_net is non-relocatable and remains in public (extrelocatable=false) supabase/supabase#46919
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.
| } else { | ||
| // Anonymous callers must share a server-controlled bucket; client headers are not identities. | ||
| buckets.push({ type: 'ip', hash: hashIdentifier('anonymous') }); |
There was a problem hiding this comment.
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.
| 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); |
There was a problem hiding this comment.
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.
- 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.
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
public/assets/looney-icon.pngis excluded by!**/*.png
📒 Files selected for processing (7)
api/cors.jsapi/looney-check.jsserver.jssrc/integrations/supabase/types.tssrc/utils/looneyChecker.tssupabase/migrations/20260810000000_looney_check_rate_limits.sqlsupabase/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
| 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); | ||
| }; |
There was a problem hiding this comment.
🎯 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.
| const createAdapter = (handler) => (req, res) => { | ||
| const vercelReq = { | ||
| method: req.method, | ||
| headers: req.headers, | ||
| headers: { | ||
| ...req.headers, | ||
| 'x-vercel-ip': req.ip || req.socket.remoteAddress || '', | ||
| }, |
There was a problem hiding this comment.
🔒 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.jsRepository: 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"; }
doneRepository: 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
fiRepository: 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)
PYRepository: 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.
|
Want your agent to iterate on Greptile's feedback? Try greploops. |
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
Replaces the old external "Osmium" redirects with an in-app music copyright checker powered by the Looney service:
/api/looney-checkserver route that proxies the Looney API with CORS, input validation, and a 50 MB upload capbrowser,ip, andaccountbuckets via aconsume_looney_check_rate_limit()Postgres function and a newlooney_check_rate_limitstable (RLS +service_roleonly)LooneyCheckForm,LooneyResultDisplay,LooneyHistorySection,LooneyCheckDialog,LooneyRunningCheckDialog, and a shareable/gappa/check/:jobIdresult pagecopyrightChecker.tsutil,ResultsDisplaycomponent, and oldMusicCopyrightimplementation2. Refined page interactions and fonts
index.cssutilities (Minecraft Five font, tool planks hover texture)3. UI fonts, texture search, and tool card interactions
font-geistfamily consistently acrossErrorBoundary,BlogView,Community,Contact,NotFoundFontFaceAPI; GitHub link removed from the footer4. Randomized multi-image background patterns
5. Animated background export with pattern controls
MediaRecorder+canvas.captureStreamTests / verification
Notes
20260810000000_looney_check_rate_limits.sqlthat must be applied.LOONEY_API_KEYenv var (optional).Summary by CodeRabbit