Skip to content

Conversation

@ritesh301
Copy link
Contributor

@ritesh301 ritesh301 commented Sep 20, 2025

πŸš€ Pull Request

πŸ“‹ Description

Brief description of what this PR does.

🎯 Type of Change

  • πŸ› Bug fix (non-breaking change which fixes an issue)
  • ✨ New feature (non-breaking change which adds functionality)
  • πŸ’₯ Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • πŸ“š Documentation update
  • 🎨 Code style/formatting
  • ♻️ Code refactoring
  • ⚑ Performance improvements
  • πŸ§ͺ Tests

πŸ”— Related Issue

Fixes #(issue number)

πŸ§ͺ Testing

  • Tested locally
  • Added/updated tests
  • All tests pass
  • Manual testing completed

πŸ“Έ Screenshots/Videos

If applicable, add screenshots or videos demonstrating the changes.

πŸ“‹ Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

🌍 Environment Tested

  • OS: [e.g. macOS, Windows, Linux]
  • Browser: [e.g. Chrome, Firefox, Safari] (if applicable)
  • Node Version: [e.g. 18.x, 20.x]

πŸ“ Additional Notes

Any additional information that reviewers should know.

Summary by CodeRabbit

  • New Features

    • Redesigned Sign In / Sign Up with centered card UI, animated background, persistent header, clearer error messages, OAuth buttons, loading indicators, and post-sign-in routing to the dashboard.
    • New/updated app pages: Home, Features, Dashboard, Reporting, Billing Usage, Workspaces (list, new, IDE), AI Agents, Settings, Profile.
    • Large themed UI kit and toast system plus a theme provider and persistent sidebar for consistent app navigation.
  • Chores

    • Global CSS/token overhaul, Tailwind config updates, utility helpers, added UI dependencies, and initial auth DB migration.

@VAIBHAVSING
Copy link
Owner

please solve lint error

@VAIBHAVSING VAIBHAVSING requested a review from Copilot September 30, 2025 18:51
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR redesigns the application's user interface by implementing a comprehensive design system with shadcn/ui components and modern styling for the landing page, features page, sign-in, and sign-up pages.

  • Comprehensive UI component library integration with shadcn/ui
  • Complete landing page redesign with modern gradient backgrounds and feature sections
  • Modernized authentication pages with improved user experience

Reviewed Changes

Copilot reviewed 68 out of 74 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
apps/web/tailwind.config.js Added dark mode support and comprehensive color system with CSS variables
apps/web/package.json Added UI component dependencies (Radix UI, class-variance-authority, lucide-react)
apps/web/lib/utils.ts Added utility function for conditional class name merging
apps/web/components/ui/* Complete UI component library implementation with 40+ shadcn/ui components
apps/web/app/globals.css Replaced basic styles with comprehensive design system variables
apps/web/app/page.tsx Complete landing page redesign with modern sections and interactive elements
apps/web/app/features/page.tsx New comprehensive features showcase page
apps/web/app/(auth)/signin/page.tsx Redesigned sign-in page with modern card-based layout
apps/web/app/(auth)/signup/page.tsx Redesigned sign-up page with improved form validation
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Start using Dev8.dev today and transform your development workflow with cloud-based environments.
</p>
<div className="mt-8 flex items-center justify-center gap-x-6">
<Button size="lg" className="h-12 px-8" onClick={() => router.push("/register")}>
Copy link

Copilot AI Sep 30, 2025

Choose a reason for hiding this comment

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

Multiple buttons are routing to '/register' instead of '/signup'. This inconsistency will break navigation for users trying to sign up from the features page.

Suggested change
<Button size="lg" className="h-12 px-8" onClick={() => router.push("/register")}>
<Button size="lg" className="h-12 px-8" onClick={() => router.push("/signup")}>

Copilot uses AI. Check for mistakes.
</li>
<li>
<button
onClick={() => router.push("/register")}
Copy link

Copilot AI Sep 30, 2025

Choose a reason for hiding this comment

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

Multiple buttons are routing to '/register' instead of '/signup'. This inconsistency will break navigation for users trying to sign up from the features page.

Suggested change
onClick={() => router.push("/register")}
onClick={() => router.push("/signup")}

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +6
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
Copy link

Copilot AI Sep 30, 2025

Choose a reason for hiding this comment

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

This utility function is duplicated in both '/lib/utils.ts' and '/app/lib/utils.ts'. Having two identical files can lead to confusion and maintenance issues. Consider removing one of these duplicates and updating import paths accordingly.

Copilot uses AI. Check for mistakes.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 2, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Massive UI and infra addition: new comprehensive UI component library, theme and Tailwind refactor, many client pages (landing, dashboard, workspaces, reporting, billing, settings, AI agents), numerous App Router API routes and in-memory state, Prisma auth migration, utility helpers, and package updates.

Changes

Cohort / File(s) Summary
Auth pages
apps/web/app/(auth)/signin/page.tsx, apps/web/app/(auth)/signup/page.tsx
Renamed exported page components, replaced inline forms with Card-based UIs, switched inputs to shared UI components, added loading states, updated error text, OAuth buttons call signIn(..., { callbackUrl: '/dashboard' }), post-auth navigation to /dashboard, and default export wrapped with dynamic SSR:false.
Theme & global styles
apps/web/app/components/theme-provider.tsx, apps/web/app/globals.css, apps/web/tailwind.config.js
Added ThemeProvider (next-themes), replaced global CSS with CSS variables/utilities and visual effects, added dark-mode tokens and tailwindcss-animate plugin.
UI primitives (app-scoped)
apps/web/app/components/ui/* (many files)
Added a large set of Radix/third‑party based UI primitives and composed components (accordion, alert, alert-dialog, aspect-ratio, avatar, badge, breadcrumb, button, calendar, card, carousel, chart, checkbox, collapsible, command, context-menu, dialog, drawer, dropdown-menu, form, hover-card, input-otp, input, label, menubar, navigation-menu, pagination, popover, progress, radio-group, resizable, scroll-area, select, separator, sheet, sidebar provider/hooks, skeleton, slider, sonner wrapper, switch, table, tabs, textarea, toast, toaster, toggle-group, toggle, tooltip, use-mobile, use-toast).
Shared UI & components
apps/web/components/*, apps/web/components/ui/*
Added shared forwardRef components and helpers used across pages (Button, Card, Badge, Input, Label, GlowMenu, icons, Sidebar, Icon helpers, IDE components like CodeEditor & SandboxRunner).
Toasts & notifications
apps/web/app/components/ui/use-toast.ts, apps/web/app/components/ui/toast.tsx, apps/web/app/components/ui/toaster.tsx, apps/web/app/components/ui/sonner.tsx
Implemented in-memory toast reducer and API (useToast, toast), Radix-based Toast primitives, Toaster renderer and Sonner wrapper with theme mapping.
Pages β€” marketing, dashboards & features
apps/web/app/page.tsx, apps/web/app/features/page.tsx, apps/web/app/reporting/page.tsx, apps/web/app/billing-usage/page.tsx, apps/web/app/dashboard/page.tsx
Replaced minimal landing with rich client-driven pages (hero, features, animated background), added reporting/billing dashboards, reworked dashboard with Sidebar and polling for workspaces.
Workspaces & IDE pages
apps/web/app/workspaces/*, apps/web/app/workspaces/[id]/ide/page.tsx
New workspaces listing, new workspace creation page, workspace IDE page with CodeEditor, SandboxRunner, periodic metrics/terminal/snapshots polling and workspace actions.
Pages β€” settings, profile, ai agents
apps/web/app/settings/*, apps/web/app/profile/page.tsx, apps/web/app/ai-agents/page.tsx
Added settings UI (change password, connected accounts, delete flow), profile connected accounts polling, AI agents page with MCP config and agent connect/disconnect.
APIs & in-memory state
apps/web/app/api/**, apps/web/app/api/_state/workspaces.ts
Added many App Router routes: workspaces (list, options, estimate, subroutes: metrics, terminal, snapshots, details, action), billing endpoints (billing, invoice), reporting, templates, AI agents, account endpoints (connections, password, delete), and an in-memory workspace state simulator.
Prisma migration
apps/web/prisma/migrations/.../migration.sql, apps/web/prisma/migrations/migration_lock.toml
Added initial Prisma SQL migration for authentication tables and migration lock file.
Utilities & lib
apps/web/app/lib/utils.ts, apps/web/lib/utils.ts
Added cn(...inputs) utility (clsx + twMerge) in app and root lib locations.
Config & deps
apps/web/package.json, apps/web/components.json, apps/web/tsconfig.json
Added several dependencies (lucide-react, clsx, class-variance-authority, next-themes, tailwind-merge, @radix packages, tailwindcss-animate, etc.), components.json config, and updated tsconfig exclude to ignore demo UI dir.
Auth / middleware tweaks
apps/web/lib/auth-config.ts, apps/web/middleware.ts
Secret resolution now prefers AUTH_SECRET with NEXTAUTH_SECRET fallback; middleware token secret lookup similarly updated.

Sequence Diagram(s)

sequenceDiagram
  actor U as User
  participant SignIn as SignInPage
  participant NextAuth as Auth Provider
  participant Router as Next Router

  U->>SignIn: submits credentials
  SignIn->>NextAuth: signIn('credentials', { callbackUrl: '/dashboard' })
  alt success
    NextAuth-->>SignIn: success
    SignIn->>Router: push("/dashboard")
    SignIn->>Router: refresh()
  else failure
    NextAuth-->>SignIn: error
    SignIn-->>U: display error message
  end
Loading
sequenceDiagram
  actor U as User
  participant API as toast() API
  participant Store as In-memory Toast Store
  participant Toaster as Toaster component

  U->>API: toast({ title, description })
  API->>Store: dispatch ADD_TOAST (open: true)
  Store-->>Toaster: notify listeners (state update)
  Toaster-->>U: render toast
  U->>Toaster: click dismiss
  Toaster->>Store: dispatch DISMISS_TOAST(id)
  Store-->>Toaster: update (open=false) and schedule REMOVE_TOAST
  Store-->>Toaster: dispatch REMOVE_TOAST(id) after delay
Loading
sequenceDiagram
  actor U as User
  participant SidebarTrigger as UI
  participant SidebarProvider as Provider
  participant Sheet as MobileSheet

  U->>SidebarTrigger: click toggle
  SidebarTrigger->>SidebarProvider: toggleOpen()
  alt mobile viewport
    SidebarProvider->>Sheet: open mobile sheet UI
  else desktop
    SidebarProvider-->>SidebarTrigger: update open/collapsed state
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120+ minutes

Potential high-attention areas:

  • New UI primitives with many files: ensure consistent typing, SSR/client boundaries, and proper exports (radix wrappers, forwardRef usage).
  • Theme/globals.css and tailwind config: verify CSS variable names and dark-mode behavior.
  • App Router API routes and in-memory state: check concurrency, deterministic behavior, and security assumptions for demo endpoints.
  • Prisma migration SQL: verify schema correctness and FK/index constraints.
  • Pages that perform polling and AbortController usage (workspaces/IDE): ensure cleanup to avoid leaks.
  • Dynamic imports (Monaco editor) and SandboxRunner iframe execution: security and error handling.
  • Package.json changes: dependency additions and potential version conflicts.

Poem

I hop through props and Tailwind thread,
I nudge a card, then spin a head.
Sidebars bloom and toasts take flight,
Tokens glow in day and night.
A rabbit cheers β€” the UI feels right. πŸ‡βœ¨

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings, 1 inconclusive)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The PR description is entirely incomplete. It contains only the template structure with all placeholder text left unfilled, no checkboxes selected, and no actual information provided about the changes, their purpose, testing performed, or relevant context. Critical sections are missing: the description field contains only "Brief description of what this PR does," the type of change is not indicated, related issues are not referenced, testing details are unchecked, environment information is blank, and no screenshots or additional notes are provided. This leaves reviewers with no substantive information about the PR's intent, scope, or validation.
Docstring Coverage ⚠️ Warning Docstring coverage is 4.41% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title Check ❓ Inconclusive The PR title "design landing, features, signin, and signup pages" is partially related to the changeset but significantly undersells the scope of what this PR actually accomplishes. While the title accurately identifies that several pages are being designed (landing, features, signin, and signup pages), the true substance of this PR is the introduction of a comprehensive UI component library with 60+ new component files (accordion, alert, badge, button, card, dialog, etc.), extensive configuration changes, API infrastructure, and state management. The page redesigns are merely applications of this larger architectural change. The title captures real aspects of the PR but fails to convey the main architectural addition and infrastructure changes that dominate the changeset.
✨ Finishing touches
  • πŸ“ Generate docstrings
πŸ§ͺ Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

❀️ Share

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 36

♻️ Duplicate comments (5)
apps/web/lib/utils.ts (1)

1-6: Duplicate cn utility; consolidate to a single source

This file duplicates apps/web/app/lib/utils.ts. Keep one and update imports to prevent divergence.

Proposed actions:

  • Delete one copy (recommend apps/web/lib/utils.ts or apps/web/app/lib/utils.ts), pick a stable path.
  • Align tsconfig β€œpaths” so @/lib/utils resolves to the chosen file.
  • Run a project-wide replace to fix imports and avoid ambiguity.
#!/bin/bash
# Find all imports of the cn helper and their resolved paths
rg -nP --type=ts --type=tsx -C2 "from ['\"]@/lib/utils['\"]|from ['\"][./].*/lib/utils['\"]"

</blockquote></details>
<details>
<summary>apps/web/app/features/page.tsx (2)</summary><blockquote>

`325-325`: **Fix route to match signup page.**

This button routes to `/register` but the actual signup route is `/signup`. This will result in a 404 error when users click "Get Started Free".




Apply this diff:

```diff
-              <Button size="lg" className="h-12 px-8" onClick={() => router.push("/register")}>
+              <Button size="lg" className="h-12 px-8" onClick={() => router.push("/signup")}>

366-366: Fix route to match signup page.

This button routes to /register but the actual signup route is /signup. This will result in a 404 error when users click "Get Started" in the footer.

Apply this diff:

-                    onClick={() => router.push("/register")}
+                    onClick={() => router.push("/signup")}
apps/web/app/components/ui/accordion.tsx (1)

7-7: Verify import path resolution for the cn utility.

Same concern as in apps/web/components/ui/button.tsx: ensure the @/lib/utils path alias correctly resolves to the intended utils file.

apps/web/app/components/ui/select.tsx (1)

7-7: Verify import path resolution for the cn utility.

Same concern as previous files: ensure @/lib/utils resolves to the correct utils file.

🧹 Nitpick comments (39)
apps/web/app/components/ui/use-mobile.tsx (3)

1-1: Consider using named imports for React hooks.

Modern React codebases typically use named imports (import { useState, useEffect } from 'react') rather than namespace imports. This reduces bundle size slightly and aligns with current React conventions.

Apply this diff:

-import * as React from 'react'
+import { useState, useEffect } from 'react'

Then update the hook implementation:

-  const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
+  const [isMobile, setIsMobile] = useState<boolean | undefined>(undefined)
 
-  React.useEffect(() => {
+  useEffect(() => {

8-16: Use mql.matches for consistency and clarity.

The onChange handler reads window.innerWidth directly (line 11), but the MediaQueryList object already provides a .matches property that reflects the media query state. Using mql.matches is more idiomatic and ensures consistency with the media query definition.

Additionally, line 14 performs a redundant state update immediately after adding the listener. Consider initializing from mql.matches directly.

Apply this diff to use mql.matches:

   React.useEffect(() => {
     const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
     const onChange = () => {
-      setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
+      setIsMobile(mql.matches)
     }
     mql.addEventListener('change', onChange)
-    setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
+    setIsMobile(mql.matches)
     return () => mql.removeEventListener('change', onChange)
   }, [])

18-18: Consider nullish coalescing for clarity.

The double negation !!isMobile coerces undefined | boolean to boolean. While correct, using nullish coalescing (isMobile ?? false) is more explicit about the default value.

Apply this diff:

-  return !!isMobile
+  return isMobile ?? false
apps/web/app/components/ui/input-otp.tsx (3)

39-67: Consider adding ARIA attributes for better accessibility.

The InputOTPSlot component renders individual OTP input slots but lacks explicit ARIA roles or labels. While the parent OTPInput may handle some accessibility, each slot should ideally have:

  • A descriptive aria-label (e.g., "digit 1 of 6")
  • Potentially role="presentation" if the parent input handles the semantic role

This improves screen reader experience for users navigating OTP inputs.

Consider adding ARIA attributes:

 function InputOTPSlot({
   index,
   className,
   ...props
 }: React.ComponentProps<'div'> & {
   index: number
 }) {
   const inputOTPContext = React.useContext(OTPInputContext)
   const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {}
 
   return (
     <div
       data-slot="input-otp-slot"
       data-active={isActive}
+      aria-label={`Digit ${index + 1}`}
+      role="presentation"
       className={cn(
         'data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]',
         className,
       )}
       {...props}
     >
       {char}
       {hasFakeCaret && (
         <div className="pointer-events-none absolute inset-0 flex items-center justify-center">
           <div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
         </div>
       )}
     </div>
   )
 }

Alternatively, verify that the input-otp library handles accessibility at the parent level and document this in a comment.


53-56: Optional: Improve readability of complex className.

The className on line 54 contains many conditional classes in a single string. While functionally correct, consider splitting it across multiple lines or extracting common class groups into constants for better readability and maintainability.

Example refactor:

+const slotBaseClasses = 'relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none'
+const slotBorderClasses = 'border-input first:rounded-l-md first:border-l last:rounded-r-md'
+const slotActiveClasses = 'data-[active=true]:z-10 data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:ring-[3px]'
+const slotInvalidClasses = 'aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40'
+const slotDarkClasses = 'dark:bg-input/30'
+
 function InputOTPSlot({
   index,
   className,
   ...props
 }: React.ComponentProps<'div'> & {
   index: number
 }) {
   const inputOTPContext = React.useContext(OTPInputContext)
   const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {}
 
   return (
     <div
       data-slot="input-otp-slot"
       data-active={isActive}
       className={cn(
-        'data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]',
+        slotBaseClasses,
+        slotBorderClasses,
+        slotActiveClasses,
+        slotInvalidClasses,
+        slotDarkClasses,
         className,
       )}
       {...props}
     >

69-75: Optional: Allow customization of separator icon.

The InputOTPSeparator component renders a MinusIcon with no customization options. Consider accepting props for icon size, color, or allowing a custom icon to be passed for flexibility.

Example enhancement:

-function InputOTPSeparator({ ...props }: React.ComponentProps<'div'>) {
+function InputOTPSeparator({ 
+  icon = <MinusIcon />,
+  className,
+  ...props 
+}: React.ComponentProps<'div'> & {
+  icon?: React.ReactNode
+}) {
   return (
-    <div data-slot="input-otp-separator" role="separator" {...props}>
-      <MinusIcon />
+    <div 
+      data-slot="input-otp-separator" 
+      role="separator" 
+      className={cn('flex items-center', className)}
+      {...props}
+    >
+      {icon}
     </div>
   )
 }

This allows consumers to pass custom separators (e.g., <InputOTPSeparator icon={<DotIcon />} />) while maintaining backward compatibility with the default MinusIcon.

apps/web/app/components/ui/sonner.tsx (1)

6-26: Avoid duplicate Toaster naming (potential confusion).

There’s already a Toaster in apps/web/app/components/ui/toaster.tsx. Consider renaming export to SonnerToaster or consolidating to a single implementation.

apps/web/app/components/ui/chart.tsx (1)

58-60: Tailwind class syntax here assumes v4 (e.g., bg-(--color-bg), border-(--color-border)).

Ensure Tailwind is v4 or switch to v3-compatible arbitrary values (e.g., bg-[var(--color-bg)], border-[var(--color-border)]).

Based on learnings

Also applies to: 176-179, 205-219

apps/web/app/components/ui/table.tsx (1)

7-20: Consider forwarding refs for DOM access/composability

Wrap these primitives with React.forwardRef so consumers can attach refs (e.g., measure rows, focus cells).

Example pattern for one component:

-function Table({ className, ...props }: React.ComponentProps<'table'>) {
-  return (
+const Table = React.forwardRef<HTMLTableElement, React.ComponentProps<'table'>>(
+  ({ className, ...props }, ref) => (
     <div
       data-slot="table-container"
       className="relative w-full overflow-x-auto"
     >
       <table
         data-slot="table"
-        className={cn('w-full caption-bottom text-sm', className)}
+        ref={ref}
+        className={cn('w-full caption-bottom text-sm', className)}
         {...props}
       />
     </div>
-  )
-}
+  ),
+)
+Table.displayName = 'Table'
apps/web/app/components/ui/sheet.tsx (2)

31-45: Optional: expose Overlay and Portal as part of the API

Exporting SheetOverlay and SheetPortal allows consumers to customize/backdrop or mount point.

 export {
   Sheet,
   SheetTrigger,
   SheetClose,
+  SheetPortal,
+  SheetOverlay,
   SheetContent,
   SheetHeader,
   SheetFooter,
   SheetTitle,
   SheetDescription,
 }

75-78: Replace invalid Tailwind classes on Close button

  • rounded-xs β†’ rounded-sm
  • focus:outline-hidden β†’ focus:outline-none
apps/web/app/lib/utils.ts (1)

1-6: Prefer default import for clsx for wider compatibility

Some toolchains expect default import. This avoids ESM/CJS interop edge cases.

-import { clsx, type ClassValue } from 'clsx'
+import clsx, { type ClassValue } from 'clsx'
 import { twMerge } from 'tailwind-merge'
 
 export function cn(...inputs: ClassValue[]) {
   return twMerge(clsx(inputs))
 }

Confirm your current clsx version supports named import; if not, this change is required to build.

apps/web/lib/utils.ts (1)

1-6: Also prefer default import for clsx here

Mirror the import change to avoid interop issues.

-import { clsx, type ClassValue } from "clsx"
+import clsx, { type ClassValue } from "clsx"
 import { twMerge } from "tailwind-merge"
 
 export function cn(...inputs: ClassValue[]) {
   return twMerge(clsx(inputs))
 }
apps/web/components/ui/input.tsx (1)

10-13: Consider breaking the long className string for readability.

The className string spans multiple style concerns (sizing, borders, focus, file input, placeholder, disabled) in a single line, making it harder to read and maintain.

Apply this diff to improve readability:

         className={cn(
-          "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
+          [
+            "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors",
+            "file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground",
+            "placeholder:text-muted-foreground",
+            "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
+            "disabled:cursor-not-allowed disabled:opacity-50",
+            "md:text-sm",
+          ].join(" "),
           className
         )}
apps/web/app/components/theme-provider.tsx (1)

9-11: LGTM - Consider documenting the wrapper purpose.

The thin wrapper over NextThemesProvider is a valid abstraction pattern that enables future customization without breaking imports. However, consider adding a brief JSDoc comment explaining why this wrapper exists rather than directly using NextThemesProvider.

Optional improvement:

+/**
+ * Theme provider wrapper around next-themes.
+ * Abstraction layer for future theme customization without breaking consumer imports.
+ */
 export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
   return <NextThemesProvider {...props}>{children}</NextThemesProvider>
 }
apps/web/components.json (1)

1-22: LGTM - Configuration follows shadcn/ui conventions.

The configuration is well-structured and follows shadcn/ui standards. The aliases, Tailwind paths, and style settings are all appropriate.

Minor: The empty registries object (Line 21) could be removed unless you plan to add custom registries later. If keeping it for future use, consider adding a comment:

-  "registries": {}
+  "registries": {} // Reserved for custom component registries
apps/web/app/components/ui/toggle.tsx (1)

10-10: Consider clarifying focus styling approach.

Line 10 applies both outline-none and focus-visible:border-ring alongside focus-visible:ring-ring/50 focus-visible:ring-[3px]. The outline-none removes the default browser outline, which is then replaced by custom ring/border styles. While this works, ensure this pattern is consistent with your accessibility guidelines and that keyboard focus remains clearly visible across all themes.

apps/web/app/globals.css (2)

26-37: Consider dark mode strategy alignment.

Lines 26-37 use a prefers-color-scheme: dark media query for image toggling, while the new theming system (lines 76-105) uses a class-based .dark approach. These two strategies can conflictβ€”if a user's system preference is dark but the theme toggle is light, images may not display as intended.

Consider aligning the image display logic with the class-based approach or removing the media query if you're exclusively using the .dark class for theme switching.


111-114: Review universal border color application.

Lines 112-114 apply border-border to all elements using the universal selector (*). This means every element inherits the theme border color by default, which can:

  • Cause unexpected borders on elements that don't explicitly set border-width: 0
  • Have minor performance implications due to universal selector overhead
  • Override border colors you might want to remain transparent

Consider scoping this to specific element types (e.g., inputs, buttons) or ensure all components explicitly set borders when needed.

apps/web/app/page.tsx (1)

36-45: Prefer Link (anchor semantics) for nav items.

Use next/link for navigation instead of for better semantics/accessibility (keyboard, screen readers).

Example:

-import { useRouter } from "next/navigation"
+import Link from "next/link"

-<button onClick={() => handleNavigation("/features")} className="text-muted-foreground hover:text-foreground transition-colors">
-  Features
-</button>
+<Link href="/features" className="text-muted-foreground hover:text-foreground transition-colors">
+  Features
+</Link>

apps/web/app/components/ui/button.tsx (2)

12-23: Nonstandard shadow-xs utilities.

shadow-xs isn’t a default Tailwind utility. Replace with shadow-sm (or extend theme.boxShadow).

Apply:

-          'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
+          'bg-primary text-primary-foreground shadow-sm hover:bg-primary/90',
-          'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
+          'bg-destructive text-white shadow-sm hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
-          'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
+          'border bg-background shadow-sm hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
-          'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
+          'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',

Or add a custom xs shadow in tailwind.config.js (theme.extend.boxShadow.xs).


38-57: Forward ref + default type for safer composition.

Forward refs improve interoperability; default type="button" avoids accidental form submits.

Apply:

-function Button({
+const Button = React.forwardRef<HTMLButtonElement, React.ComponentProps<'button'> & VariantProps<typeof buttonVariants> & { asChild?: boolean }>(function Button(
+  {
   className,
   variant,
   size,
   asChild = false,
   ...props
-}: React.ComponentProps<'button'> &
-  VariantProps<typeof buttonVariants> & {
-    asChild?: boolean
-  }) {
-  const Comp = asChild ? Slot : 'button'
+  },
+  ref,
+) {
+  const Comp = asChild ? Slot : 'button' as any
   return (
     <Comp
       data-slot="button"
-      className={cn(buttonVariants({ variant, size, className }))}
-      {...props}
+      ref={ref}
+      className={cn(buttonVariants({ variant, size }), className)}
+      {...(!asChild && { type: (props as any)?.type ?? 'button' })}
+      {...props}
     />
   )
-}
+})
 
 export { Button, buttonVariants }
apps/web/components/ui/card.tsx (1)

32-41: Use semantic elements for headings/description

Prefer h3/p for better a11y/semantics (matches common Card patterns).

-const CardTitle = React.forwardRef<
-  HTMLDivElement,
-  React.HTMLAttributes<HTMLDivElement>
->(({ className, ...props }, ref) => (
-  <div
+const CardTitle = React.forwardRef<
+  HTMLHeadingElement,
+  React.HTMLAttributes<HTMLHeadingElement>
+>(({ className, ...props }, ref) => (
+  <h3
     ref={ref}
     className={cn("font-semibold leading-none tracking-tight", className)}
     {...props}
-  />
+  />
 ))
 CardTitle.displayName = "CardTitle"

-const CardDescription = React.forwardRef<
-  HTMLDivElement,
-  React.HTMLAttributes<HTMLDivElement>
->(({ className, ...props }, ref) => (
-  <div
+const CardDescription = React.forwardRef<
+  HTMLParagraphElement,
+  React.HTMLAttributes<HTMLParagraphElement>
+>(({ className, ...props }, ref) => (
+  <p
     ref={ref}
     className={cn("text-sm text-muted-foreground", className)}
     {...props}
-  />
+  />
 ))
 CardDescription.displayName = "CardDescription"

Also applies to: 44-55

apps/web/app/components/ui/avatar.tsx (1)

8-22: Forward refs through Avatar wrappers

Current functions drop refs; forward them to Radix primitives for focus/measurement.

-function Avatar({
-  className,
-  ...props
-}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
-  return (
-    <AvatarPrimitive.Root
+const Avatar = React.forwardRef<
+  React.ElementRef<typeof AvatarPrimitive.Root>,
+  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
+>(({ className, ...props }, ref) => (
+  <AvatarPrimitive.Root
+      ref={ref}
       data-slot="avatar"
       className={cn(
         'relative flex size-8 shrink-0 overflow-hidden rounded-full',
         className,
       )}
       {...props}
-    />
-  )
-}
+    />
+))
+Avatar.displayName = 'Avatar'
 
-function AvatarImage({
-  className,
-  ...props
-}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
-  return (
-    <AvatarPrimitive.Image
+const AvatarImage = React.forwardRef<
+  React.ElementRef<typeof AvatarPrimitive.Image>,
+  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
+>(({ className, ...props }, ref) => (
+  <AvatarPrimitive.Image
+      ref={ref}
       data-slot="avatar-image"
-      className={cn('aspect-square size-full', className)}
+      className={cn('aspect-square size-full', className)}
       {...props}
-    />
-  )
-}
+    />
+))
+AvatarImage.displayName = 'AvatarImage'
 
-function AvatarFallback({
-  className,
-  ...props
-}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
-  return (
-    <AvatarPrimitive.Fallback
+const AvatarFallback = React.forwardRef<
+  React.ElementRef<typeof AvatarPrimitive.Fallback>,
+  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
+>(({ className, ...props }, ref) => (
+  <AvatarPrimitive.Fallback
+      ref={ref}
       data-slot="avatar-fallback"
       className={cn(
         'bg-muted flex size-full items-center justify-center rounded-full',
         className,
       )}
       {...props}
-    />
-  )
-}
+    />
+))
+AvatarFallback.displayName = 'AvatarFallback'

Also applies to: 24-35, 37-51

apps/web/app/(auth)/signin/page.tsx (2)

87-91: Announce errors to screen readers

Mark the error container as an alert with polite live region.

-              {error && (
-                <div className="mb-4 rounded-md border border-destructive/50 bg-destructive/10 p-3">
+              {error && (
+                <div
+                  role="alert"
+                  aria-live="polite"
+                  className="mb-4 rounded-md border border-destructive/50 bg-destructive/10 p-3"
+                >
                   <p className="text-sm font-medium text-destructive">{error}</p>
                 </div>
               )}

65-68: Prefer Link for navigation (preserves a11y and browser behaviors)

Use Button asChild + Link instead of imperatively pushing.

Verify your Button supports asChild (Radix Slot). If not, skip.

-            <Button variant="ghost" size="sm" onClick={() => router.push("/")}>
-              <ArrowLeft className="mr-2 h-4 w-4" />
-              Back to Home
-            </Button>
+            <Button variant="ghost" size="sm" asChild>
+              <Link href="/">
+                <ArrowLeft className="mr-2 h-4 w-4" />
+                Back to Home
+              </Link>
+            </Button>
apps/web/app/components/ui/use-toast.ts (3)

174-182: Avoid resubscribing on every state change

useEffect should mount once; current dependency causes needless subscribe/unsubscribe churn.

-  React.useEffect(() => {
+  React.useEffect(() => {
     listeners.push(setState)
     return () => {
       const index = listeners.indexOf(setState)
       if (index > -1) {
         listeners.splice(index, 1)
       }
     }
-  }, [state])
+  }, [])

8-10: Toast removal delay is excessively long

1,000,000ms (~16m) keeps toasts around too long. Use a typical 3000–5000ms.

-const TOAST_LIMIT = 1
-const TOAST_REMOVE_DELAY = 1000000
+const TOAST_LIMIT = 1
+const TOAST_REMOVE_DELAY = 4000

145-150: Allow partial updates in update()

update should accept Partial to modify only changed fields.

-  const update = (props: ToasterToast) =>
+  const update = (props: Partial<ToasterToast>) =>
     dispatch({
       type: 'UPDATE_TOAST',
       toast: { ...props, id },
     })
apps/web/app/(auth)/signup/page.tsx (1)

192-222: Consider using lucide-react icons for OAuth providers.

The OAuth buttons use inline SVG icons. For consistency with the rest of the codebase (which uses lucide-react for icons like Code and ArrowLeft), consider replacing these with icon library equivalents if available.

apps/web/app/components/ui/select.tsx (1)

27-51: LGTM! Comprehensive trigger with size variants.

SelectTrigger correctly:

  • Supports sm and default size variants
  • Includes extensive state handling (focus, invalid, disabled, dark mode)
  • Properly wraps the chevron icon in SelectPrimitive.Icon
  • Uses aria-invalid for validation feedback

The long className string on line 40 is functional but consider extracting repeated patterns if this becomes unmaintainable.

apps/web/app/components/ui/resizable.tsx (3)

42-42: Likely invalid Tailwind class outline-hidden; use outline-none.

Tailwind provides outline-none, not outline-hidden. This can trigger lint errors.

-        'bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90',
+        'bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-none data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90',

48-50: Mark decorative grip as hidden from assistive tech.

The inner grip is purely visual; hide it from screen readers.

-        <div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
+        <div aria-hidden="true" className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">

9-23: Forward refs to underlying primitives.

Consumers often need refs for focus/measure. Forwarding preserves the base API.

-function ResizablePanelGroup({
-  className,
-  ...props
-}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) {
-  return (
-    <ResizablePrimitive.PanelGroup
+const ResizablePanelGroup = React.forwardRef<
+  React.ElementRef<typeof ResizablePrimitive.PanelGroup>,
+  React.ComponentPropsWithoutRef<typeof ResizablePrimitive.PanelGroup>
+>(({ className, ...props }, ref) => (
+  <ResizablePrimitive.PanelGroup
+      ref={ref}
       data-slot="resizable-panel-group"
       className={cn(
         'flex h-full w-full data-[panel-group-direction=vertical]:flex-col',
         className,
       )}
       {...props}
-    />
-  )
-}
+  />
+))
+ResizablePanelGroup.displayName = 'ResizablePanelGroup'
 
-function ResizablePanel({
-  ...props
-}: React.ComponentProps<typeof ResizablePrimitive.Panel>) {
-  return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />
-}
+const ResizablePanel = React.forwardRef<
+  React.ElementRef<typeof ResizablePrimitive.Panel>,
+  React.ComponentPropsWithoutRef<typeof ResizablePrimitive.Panel>
+>((props, ref) => (
+  <ResizablePrimitive.Panel ref={ref} data-slot="resizable-panel" {...props} />
+))
+ResizablePanel.displayName = 'ResizablePanel'
 
-function ResizableHandle({
-  withHandle,
-  className,
-  ...props
-}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
-  withHandle?: boolean
-}) {
-  return (
-    <ResizablePrimitive.PanelResizeHandle
+const ResizableHandle = React.forwardRef<
+  React.ElementRef<typeof ResizablePrimitive.PanelResizeHandle>,
+  React.ComponentPropsWithoutRef<typeof ResizablePrimitive.PanelResizeHandle> & {
+    withHandle?: boolean
+  }
+>(({ withHandle, className, ...props }, ref) => (
+  <ResizablePrimitive.PanelResizeHandle
+      ref={ref}
       data-slot="resizable-handle"
       className={cn(
         'bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-none data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90',
         className,
       )}
       {...props}
     >
       {withHandle && (
         <div aria-hidden="true" className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
           <GripVertical className="size-2.5" />
         </div>
       )}
-    </ResizablePrimitive.PanelResizeHandle>
-  )
-}
+    </ResizablePrimitive.PanelResizeHandle>
+))
+ResizableHandle.displayName = 'ResizableHandle'

Also applies to: 25-29, 31-54

apps/web/app/components/ui/card.tsx (1)

5-16: Forward ref for Card container.

Forwarding refs improves interoperability (focus, measurements, scrolling).

-function Card({ className, ...props }: React.ComponentProps<'div'>) {
-  return (
-    <div
+const Card = React.forwardRef<HTMLDivElement, React.ComponentProps<'div'>>(
+  ({ className, ...props }, ref) => (
+    <div
+      ref={ref}
       data-slot="card"
       className={cn(
         'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
         className,
       )}
       {...props}
     />
-  )
-}
+  ),
+)
+Card.displayName = 'Card'

Apply the same pattern to CardHeader, CardContent, etc., to keep the API consistent.

apps/web/app/components/ui/tabs.tsx (1)

8-19: Forward refs to Radix primitives for Tabs components.

Keeps the wrapper API aligned with Radix (focus management, measurements).

Example for TabsTrigger:

-function TabsTrigger({
-  className,
-  ...props
-}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
-  return (
-    <TabsPrimitive.Trigger
+const TabsTrigger = React.forwardRef<
+  React.ElementRef<typeof TabsPrimitive.Trigger>,
+  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
+>(({ className, ...props }, ref) => (
+  <TabsPrimitive.Trigger
+      ref={ref}
       data-slot="tabs-trigger"
       className={cn(
         "data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
         className,
       )}
       {...props}
-    />
-  )
-}
+    />
+))
+TabsTrigger.displayName = 'TabsTrigger'

Apply similar forwardRef patterns to Tabs, TabsList, and TabsContent.

Also applies to: 21-35, 37-51, 53-64

apps/web/app/components/ui/tooltip.tsx (2)

37-59: Forward ref from TooltipContent to Radix Content.

Allows consumers to control focus/measure and aligns with other UI primitives.

-function TooltipContent({
-  className,
-  sideOffset = 0,
-  children,
-  ...props
-}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
-  return (
-    <TooltipPrimitive.Portal>
-      <TooltipPrimitive.Content
+const TooltipContent = React.forwardRef<
+  React.ElementRef<typeof TooltipPrimitive.Content>,
+  React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
+>(({ className, sideOffset = 0, children, ...props }, ref) => (
+  <TooltipPrimitive.Portal>
+    <TooltipPrimitive.Content
+        ref={ref}
         data-slot="tooltip-content"
         sideOffset={sideOffset}
         className={cn(
           'bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance',
           className,
         )}
         {...props}
       >
         {children}
         <TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
-      </TooltipPrimitive.Content>
-    </TooltipPrimitive.Portal>
-  )
-}
+      </TooltipPrimitive.Content>
+  </TooltipPrimitive.Portal>
+))
+TooltipContent.displayName = 'TooltipContent'

21-29: Consider not nesting Provider inside Tooltip.

Wrapping Provider inside Tooltip can lead to multiple Providers when composing tooltips. Prefer exporting Tooltip as the Radix Root and let apps wrap a single TooltipProvider at app root.

apps/web/app/components/ui/toast.tsx (1)

27-41: Variant flag 'destructive' relies on plain class; confirm intended group selector

toastVariants adds class 'destructive' to Root for styling children via group-[.destructive]. This works, but it's easy to miss. Consider documenting or encoding it via data attributes for clarity.

Confirm that consumers pass variant="destructive" as intended; else the child selectors won’t apply.

πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 011896d and a91b602.

β›” Files ignored due to path filters (6)
  • apps/web/app/public/placeholder-logo.png is excluded by !**/*.png
  • apps/web/app/public/placeholder-logo.svg is excluded by !**/*.svg
  • apps/web/app/public/placeholder-user.jpg is excluded by !**/*.jpg
  • apps/web/app/public/placeholder.jpg is excluded by !**/*.jpg
  • apps/web/app/public/placeholder.svg is excluded by !**/*.svg
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
πŸ“’ Files selected for processing (68)
  • apps/web/app/(auth)/signin/page.tsx (2 hunks)
  • apps/web/app/(auth)/signup/page.tsx (2 hunks)
  • apps/web/app/components/theme-provider.tsx (1 hunks)
  • apps/web/app/components/ui/accordion.tsx (1 hunks)
  • apps/web/app/components/ui/alert-dialog.tsx (1 hunks)
  • apps/web/app/components/ui/alert.tsx (1 hunks)
  • apps/web/app/components/ui/aspect-ratio.tsx (1 hunks)
  • apps/web/app/components/ui/avatar.tsx (1 hunks)
  • apps/web/app/components/ui/badge.tsx (1 hunks)
  • apps/web/app/components/ui/breadcrumb.tsx (1 hunks)
  • apps/web/app/components/ui/button.tsx (1 hunks)
  • apps/web/app/components/ui/calendar.tsx (1 hunks)
  • apps/web/app/components/ui/card.tsx (1 hunks)
  • apps/web/app/components/ui/carousel.tsx (1 hunks)
  • apps/web/app/components/ui/chart.tsx (1 hunks)
  • apps/web/app/components/ui/checkbox.tsx (1 hunks)
  • apps/web/app/components/ui/collapsible.tsx (1 hunks)
  • apps/web/app/components/ui/command.tsx (1 hunks)
  • apps/web/app/components/ui/context-menu.tsx (1 hunks)
  • apps/web/app/components/ui/dialog.tsx (1 hunks)
  • apps/web/app/components/ui/drawer.tsx (1 hunks)
  • apps/web/app/components/ui/dropdown-menu.tsx (1 hunks)
  • apps/web/app/components/ui/form.tsx (1 hunks)
  • apps/web/app/components/ui/hover-card.tsx (1 hunks)
  • apps/web/app/components/ui/input-otp.tsx (1 hunks)
  • apps/web/app/components/ui/input.tsx (1 hunks)
  • apps/web/app/components/ui/label.tsx (1 hunks)
  • apps/web/app/components/ui/menubar.tsx (1 hunks)
  • apps/web/app/components/ui/navigation-menu.tsx (1 hunks)
  • apps/web/app/components/ui/pagination.tsx (1 hunks)
  • apps/web/app/components/ui/popover.tsx (1 hunks)
  • apps/web/app/components/ui/progress.tsx (1 hunks)
  • apps/web/app/components/ui/radio-group.tsx (1 hunks)
  • apps/web/app/components/ui/resizable.tsx (1 hunks)
  • apps/web/app/components/ui/scroll-area.tsx (1 hunks)
  • apps/web/app/components/ui/select.tsx (1 hunks)
  • apps/web/app/components/ui/separator.tsx (1 hunks)
  • apps/web/app/components/ui/sheet.tsx (1 hunks)
  • apps/web/app/components/ui/sidebar.tsx (1 hunks)
  • apps/web/app/components/ui/skeleton.tsx (1 hunks)
  • apps/web/app/components/ui/slider.tsx (1 hunks)
  • apps/web/app/components/ui/sonner.tsx (1 hunks)
  • apps/web/app/components/ui/switch.tsx (1 hunks)
  • apps/web/app/components/ui/table.tsx (1 hunks)
  • apps/web/app/components/ui/tabs.tsx (1 hunks)
  • apps/web/app/components/ui/textarea.tsx (1 hunks)
  • apps/web/app/components/ui/toast.tsx (1 hunks)
  • apps/web/app/components/ui/toaster.tsx (1 hunks)
  • apps/web/app/components/ui/toggle-group.tsx (1 hunks)
  • apps/web/app/components/ui/toggle.tsx (1 hunks)
  • apps/web/app/components/ui/tooltip.tsx (1 hunks)
  • apps/web/app/components/ui/use-mobile.tsx (1 hunks)
  • apps/web/app/components/ui/use-toast.ts (1 hunks)
  • apps/web/app/features/page.tsx (1 hunks)
  • apps/web/app/globals.css (1 hunks)
  • apps/web/app/lib/utils.ts (1 hunks)
  • apps/web/app/page.tsx (1 hunks)
  • apps/web/components.json (1 hunks)
  • apps/web/components/ui/badge.tsx (1 hunks)
  • apps/web/components/ui/button.tsx (1 hunks)
  • apps/web/components/ui/card.tsx (1 hunks)
  • apps/web/components/ui/input.tsx (1 hunks)
  • apps/web/components/ui/label.tsx (1 hunks)
  • apps/web/lib/utils.ts (1 hunks)
  • apps/web/package.json (1 hunks)
  • apps/web/prisma/migrations/20250911181640_init/migration.sql (1 hunks)
  • apps/web/prisma/migrations/migration_lock.toml (1 hunks)
  • apps/web/tailwind.config.js (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (55)
apps/web/components/ui/input.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/textarea.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/sonner.tsx (1)
apps/web/app/components/ui/toaster.tsx (1)
  • Toaster (13-35)
apps/web/app/components/ui/popover.tsx (1)
apps/web/app/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/label.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/toggle-group.tsx (2)
apps/web/app/components/ui/toggle.tsx (1)
  • toggleVariants (47-47)
apps/web/app/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/calendar.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/page.tsx (3)
packages/ui/src/code.tsx (1)
  • Code (3-11)
packages/ui/src/button.tsx (1)
  • Button (11-20)
packages/ui/src/card.tsx (1)
  • Card (3-27)
apps/web/app/components/ui/switch.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/skeleton.tsx (2)
apps/web/app/lib/utils.ts (1)
  • cn (4-6)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/breadcrumb.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/lib/utils.ts (1)
apps/web/app/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/toaster.tsx (3)
apps/web/app/components/ui/sonner.tsx (1)
  • Toaster (25-25)
apps/web/app/components/ui/use-toast.ts (1)
  • useToast (191-191)
apps/web/app/components/ui/toast.tsx (6)
  • ToastProvider (122-122)
  • Toast (124-124)
  • ToastTitle (125-125)
  • ToastDescription (126-126)
  • ToastClose (127-127)
  • ToastViewport (123-123)
apps/web/app/components/ui/input-otp.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/separator.tsx (1)
apps/web/app/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/tabs.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/components/ui/card.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/sheet.tsx (1)
apps/web/app/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/table.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/hover-card.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/command.tsx (2)
apps/web/app/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/dialog.tsx (5)
  • Dialog (133-133)
  • DialogHeader (138-138)
  • DialogTitle (141-141)
  • DialogDescription (136-136)
  • DialogContent (135-135)
apps/web/app/components/ui/card.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/avatar.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/carousel.tsx (1)
apps/web/app/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/dropdown-menu.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/resizable.tsx (1)
apps/web/app/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/checkbox.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/alert.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/radio-group.tsx (1)
apps/web/app/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/sidebar.tsx (2)
apps/web/app/components/ui/use-mobile.tsx (1)
  • useIsMobile (5-19)
apps/web/app/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/dialog.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/menubar.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/chart.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/badge.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/components/ui/badge.tsx (2)
apps/web/app/lib/utils.ts (1)
  • cn (4-6)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/alert-dialog.tsx (2)
apps/web/app/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/button.tsx (1)
  • buttonVariants (59-59)
apps/web/app/components/ui/input.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/toast.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/slider.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/navigation-menu.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/button.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/features/page.tsx (1)
packages/ui/src/code.tsx (1)
  • Code (3-11)
apps/web/components/ui/button.tsx (2)
apps/web/app/lib/utils.ts (1)
  • cn (4-6)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/pagination.tsx (3)
apps/web/app/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/button.tsx (2)
  • Button (59-59)
  • buttonVariants (59-59)
apps/web/components/ui/button.tsx (2)
  • Button (57-57)
  • buttonVariants (57-57)
apps/web/app/(auth)/signin/page.tsx (1)
apps/web/lib/auth-config.ts (1)
  • signIn (103-119)
apps/web/app/components/ui/accordion.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/tooltip.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/toggle.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/context-menu.tsx (1)
apps/web/app/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/select.tsx (1)
apps/web/app/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/form.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/progress.tsx (1)
apps/web/app/lib/utils.ts (1)
  • cn (4-6)
apps/web/components/ui/label.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/scroll-area.tsx (1)
apps/web/app/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/drawer.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
πŸͺ› ast-grep (0.39.5)
apps/web/app/components/ui/chart.tsx

[warning] 82-82: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

πŸͺ› Biome (2.1.2)
apps/web/app/components/ui/chart.tsx

[error] 83-83: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

Comment on lines +4 to +8
import {
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
} from 'lucide-react'
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | πŸ”΄ Critical

🧩 Analysis chain

Fix lucide-react icon names (remove Icon suffix).

lucide-react exports are ChevronLeft, ChevronRight, ChevronDown (no Icon suffix). Current imports will break compilation and linting.

Apply this diff:

-import {
-  ChevronDownIcon,
-  ChevronLeftIcon,
-  ChevronRightIcon,
-} from 'lucide-react'
+import { ChevronDown, ChevronLeft, ChevronRight } from 'lucide-react'
@@
-              <ChevronLeftIcon className={cn('size-4', className)} {...props} />
+              <ChevronLeft className={cn('size-4', className)} {...props} />
@@
-              <ChevronRightIcon
+              <ChevronRight
                 className={cn('size-4', className)}
                 {...props}
               />
@@
-            <ChevronDownIcon className={cn('size-4', className)} {...props} />
+            <ChevronDown className={cn('size-4', className)} {...props} />

Run this script to find any other bad imports across the repo:

Also applies to: 141-143, 147-151, 155-156


🏁 Script executed:

#!/bin/bash
# Find lucide-react imports that use the `Icon` suffix (likely invalid).
rg -nP "import\s*{\s*[^}]*Icon[^}]*}\s*from\s*['\"]lucide-react['\"]" -C0

Length of output: 1312


Fix lucide-react imports (remove Icon suffix)
lucide-react exports icons without the Icon suffixβ€”current imports and JSX usage will break compilation.

In apps/web/app/components/ui/calendar.tsx:

-import {
-  ChevronDownIcon,
-  ChevronLeftIcon,
-  ChevronRightIcon,
-} from 'lucide-react'
+import { ChevronDown, ChevronLeft, ChevronRight } from 'lucide-react'
@@
-              <ChevronLeftIcon className={cn('size-4', className)} {...props} />
+              <ChevronLeft className={cn('size-4', className)} {...props} />
@@
-              <ChevronRightIcon
+              <ChevronRight
                 className={cn('size-4', className)}
                 {...props}
               />
@@
-            <ChevronDownIcon className={cn('size-4', className)} {...props} />
+            <ChevronDown className={cn('size-4', className)} {...props} />

Also apply the same renaming to these files:

  • apps/web/app/components/ui/checkbox.tsx
  • apps/web/app/components/ui/resizable.tsx
  • apps/web/app/components/ui/input-otp.tsx
  • apps/web/app/components/ui/select.tsx
  • apps/web/app/components/ui/sidebar.tsx
  • apps/web/app/components/ui/radio-group.tsx
  • apps/web/app/components/ui/navigation-menu.tsx
  • apps/web/app/components/ui/sheet.tsx
  • apps/web/app/components/ui/dialog.tsx
  • apps/web/app/components/ui/menubar.tsx
  • apps/web/app/components/ui/context-menu.tsx
  • apps/web/app/components/ui/command.tsx
  • apps/web/app/components/ui/dropdown-menu.tsx
  • apps/web/app/components/ui/accordion.tsx
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import {
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
} from 'lucide-react'
// apps/web/app/components/ui/calendar.tsx
-import {
- ChevronDownIcon,
- ChevronLeftIcon,
- ChevronRightIcon,
import { ChevronDown, ChevronLeft, ChevronRight } from 'lucide-react'
// … later in your JSX …
<ChevronLeft className={cn('size-4', className)} {...props} />
<ChevronRight
className={cn('size-4', className)}
{...props}
/>
<ChevronDown className={cn('size-4', className)} {...props} />
πŸ€– Prompt for AI Agents
In apps/web/app/components/ui/calendar.tsx around lines 4-8, the lucide-react
icons were imported with an "Icon" suffix (e.g., ChevronDownIcon) but
lucide-react exports names without the "Icon" suffix; update the import names to
the actual exported names (e.g., ChevronDown) and update any JSX usage to match
those names. Apply the same rename (remove trailing "Icon" from imports and JSX
usages) across the listed files: apps/web/app/components/ui/checkbox.tsx,
resizable.tsx, input-otp.tsx, select.tsx, sidebar.tsx, radio-group.tsx,
navigation-menu.tsx, sheet.tsx, dialog.tsx, menubar.tsx, context-menu.tsx,
command.tsx, dropdown-menu.tsx, and accordion.tsx so all lucide-react imports
and component references match the library exports.

Comment on lines +78 to +89
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'ArrowLeft') {
event.preventDefault()
scrollPrev()
} else if (event.key === 'ArrowRight') {
event.preventDefault()
scrollNext()
}
},
[scrollPrev, scrollNext],
)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Handle vertical keyboard navigation.

Vertical carousels never respond to ArrowUp/ArrowDown, so keyboard users cannot move slides in that orientation. Extend handleKeyDown to map the up/down keys when orientation === 'vertical', and add orientation as a dependency.

   const handleKeyDown = React.useCallback(
     (event: React.KeyboardEvent<HTMLDivElement>) => {
-      if (event.key === 'ArrowLeft') {
-        event.preventDefault()
-        scrollPrev()
-      } else if (event.key === 'ArrowRight') {
-        event.preventDefault()
-        scrollNext()
-      }
+      const isHorizontal = orientation === 'horizontal'
+
+      if (
+        (isHorizontal && event.key === 'ArrowLeft') ||
+        (!isHorizontal && event.key === 'ArrowUp')
+      ) {
+        event.preventDefault()
+        scrollPrev()
+        return
+      }
+
+      if (
+        (isHorizontal && event.key === 'ArrowRight') ||
+        (!isHorizontal && event.key === 'ArrowDown')
+      ) {
+        event.preventDefault()
+        scrollNext()
+      }
     },
-    [scrollPrev, scrollNext],
+    [orientation, scrollPrev, scrollNext],
   )
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'ArrowLeft') {
event.preventDefault()
scrollPrev()
} else if (event.key === 'ArrowRight') {
event.preventDefault()
scrollNext()
}
},
[scrollPrev, scrollNext],
)
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
const isHorizontal = orientation === 'horizontal'
if (
(isHorizontal && event.key === 'ArrowLeft') ||
(!isHorizontal && event.key === 'ArrowUp')
) {
event.preventDefault()
scrollPrev()
return
}
if (
(isHorizontal && event.key === 'ArrowRight') ||
(!isHorizontal && event.key === 'ArrowDown')
) {
event.preventDefault()
scrollNext()
}
},
[orientation, scrollPrev, scrollNext],
)
πŸ€– Prompt for AI Agents
In apps/web/app/components/ui/carousel.tsx around lines 78 to 89, extend the
handleKeyDown callback to also map ArrowUp and ArrowDown to
scrollPrev/scrollNext when orientation === 'vertical', and add orientation to
the dependency array; specifically, check event.key for 'ArrowUp'/'ArrowDown'
and only call the vertical mapping when orientation === 'vertical', call
event.preventDefault() as done for left/right, and include orientation in the
React.useCallback dependency list.

Comment on lines +96 to +105
React.useEffect(() => {
if (!api) return
onSelect(api)
api.on('reInit', onSelect)
api.on('select', onSelect)

return () => {
api?.off('select', onSelect)
}
}, [api, onSelect])
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Remove both Embla event listeners on cleanup.

We register onSelect for reInit and select, but only detach the select listener. That leaks callbacks and fires onSelect multiple times after hot reloads/reinitializations. Clean up the reInit listener as well.

     return () => {
       api?.off('select', onSelect)
+      api?.off('reInit', onSelect)
     }
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
React.useEffect(() => {
if (!api) return
onSelect(api)
api.on('reInit', onSelect)
api.on('select', onSelect)
return () => {
api?.off('select', onSelect)
}
}, [api, onSelect])
React.useEffect(() => {
if (!api) return
onSelect(api)
api.on('reInit', onSelect)
api.on('select', onSelect)
return () => {
api?.off('select', onSelect)
api?.off('reInit', onSelect)
}
}, [api, onSelect])
πŸ€– Prompt for AI Agents
In apps/web/app/components/ui/carousel.tsx around lines 96 to 105, the effect
registers onSelect for both 'reInit' and 'select' but only removes 'select' on
cleanup; update the cleanup to unsubscribe both events (call api.off('reInit',
onSelect) and api.off('select', onSelect) β€” using optional chaining or a guard
for api as needed) so the reInit listener is removed and callbacks don't leak or
duplicate after hot reloads/reinitializations.

Comment on lines +82 to +101
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color
return color ? ` --color-${key}: ${color};` : null
})
.join('\n')}
}
`,
)
.join('\n'),
}}
/>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid dangerouslySetInnerHTML in <style> (lint/security).

Current approach trips lint and weakens XSS posture. Prefer setting CSS vars via inline style on the chart container based on the active theme, or at minimum sanitize and suppress the rule locally.

Option A (recommended): compute vars and set on container; remove ChartStyle:

 function ChartContainer({
   ...
 }: React.ComponentProps<'div'> & { ... }) {
   const uniqueId = React.useId()
   const chartId = `chart-${id || uniqueId.replace(/:/g, '')}`
+  // Build CSS vars from config (basic example; extend to handle dark/light)
+  const styleVars = React.useMemo(() => {
+    const vars: React.CSSProperties = {}
+    for (const [key, item] of Object.entries(config)) {
+      const color = 'theme' in item && item.theme ? item.theme.light : item.color
+      if (color) (vars as any)[`--color-${key}`] = color
+    }
+    return vars
+  }, [config])
 
   return (
     <ChartContext.Provider value={{ config }}>
       <div
         data-slot="chart"
         data-chart={chartId}
         className={cn( ..., className )}
+        style={styleVars}
         {...props}
       >
-        <ChartStyle id={chartId} config={config} />
         <RechartsPrimitive.ResponsiveContainer>
           {children}
         </RechartsPrimitive.ResponsiveContainer>
       </div>
     </ChartContext.Provider>
   )
 }
 
-const ChartStyle = ...
+// Remove ChartStyle

Option B (quick): sanitize and suppress lint locally:

-    <style
+    {/* biome-ignore lint/security/noDangerouslySetInnerHtml: chart-scoped CSS vars from controlled config */}
+    <style
       dangerouslySetInnerHTML={{
         __html: Object.entries(THEMES)
           .map(
             ([theme, prefix]) => `
 ${prefix} [data-chart=${id}] {
 ${colorConfig
   .map(([key, itemConfig]) => {
-    const color =
+    // basic color whitelist: CSS vars and hex/rgb/hsl
+    const raw =
       itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
       itemConfig.color
-    return color ? `  --color-${key}: ${color};` : null
+    const color = typeof raw === 'string' && /^(var\(--[a-zA-Z0-9-_]+\)|#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})|rgb[a]?\([^)]+\)|hsl[a]?\([^)]+\))$/.test(raw) ? raw : null
+    return color ? `  --color-${key}: ${color};` : null
   })
   .join('\n')}
 }
 `,
           )
           .join('\n'),
       }}
     />

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
πŸͺ› ast-grep (0.39.5)

[warning] 82-82: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

πŸͺ› Biome (2.1.2)

[error] 83-83: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

Comment on lines 19 to 38
"dependencies": {
"@auth/prisma-adapter": "^2.10.0",
"@prisma/client": "^6.14.0",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@repo/ui": "workspace:*",
"@types/bcryptjs": "^3.0.0",
"bcryptjs": "^3.0.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.544.0",
"next": "^15.5.0",
"next-auth": "^4.24.11",
"prisma": "^6.14.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"tailwind-merge": "^3.3.1",
"tailwindcss-animate": "^1.0.7",
"zod": "^4.1.1"
},
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | πŸ”΄ Critical

🧩 Analysis chain

Build will fail: dependencies used in code are missing.

Code imports these but they’re not listed here: @radix-ui/react-collapsible, @radix-ui/react-scroll-area, next-themes, sonner, recharts.

Run to confirm and list all missing:

Add them under dependencies (pick versions per your lockfile policy):

   "dependencies": {
+    "@radix-ui/react-collapsible": "^1.0.0",
+    "@radix-ui/react-scroll-area": "^1.0.0",
+    "next-themes": "^0.3.0",
+    "recharts": "^2.10.0",
+    "sonner": "^1.5.0",

🏁 Script executed:

#!/bin/bash
set -e
echo "Imports present in code:"
rg -nP "@radix-ui/react-(collapsible|scroll-area)|next-themes|sonner|recharts" -g '!**/dist/**'
echo
echo "Packages declared in package.json files:"
fd package.json | xargs -I{} sh -c 'echo "== {} =="; jq -r ".dependencies|keys[]" {} 2>/dev/null | rg -n "^(@radix-ui/react-collapsible|@radix-ui/react-scroll-area|next-themes|sonner|recharts)$" || true'

Length of output: 173


🏁 Script executed:

#!/bin/bash
set -e
echo "Checking imports in code under apps/web:"
rg -nP "import .* from ['\"](@radix-ui/react-(collapsible|scroll-area)|next-themes|sonner|recharts)['\"]" -g "apps/web/**/*.{ts,tsx,js,jsx}"
echo
echo "Declared dependencies in apps/web/package.json:"
jq -r '.dependencies|keys[]' apps/web/package.json

Length of output: 808


Add missing dependencies to apps/web/package.json

The code imports next-themes, sonner, @radix-ui/react-scroll-area, @radix-ui/react-collapsible, and recharts but they’re not declared:

   "dependencies": {
+    "@radix-ui/react-collapsible": "^1.0.0",
+    "@radix-ui/react-scroll-area": "^1.0.0",
+    "next-themes": "^0.3.0",
+    "recharts": "^2.10.0",
+    "sonner": "^1.5.0",
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"dependencies": {
"@auth/prisma-adapter": "^2.10.0",
"@prisma/client": "^6.14.0",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@repo/ui": "workspace:*",
"@types/bcryptjs": "^3.0.0",
"bcryptjs": "^3.0.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.544.0",
"next": "^15.5.0",
"next-auth": "^4.24.11",
"prisma": "^6.14.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"tailwind-merge": "^3.3.1",
"tailwindcss-animate": "^1.0.7",
"zod": "^4.1.1"
},
"dependencies": {
"@radix-ui/react-collapsible": "^1.0.0",
"@radix-ui/react-scroll-area": "^1.0.0",
"next-themes": "^0.3.0",
"recharts": "^2.10.0",
"sonner": "^1.5.0",
"@auth/prisma-adapter": "^2.10.0",
"@prisma/client": "^6.14.0",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@repo/ui": "workspace:*",
"@types/bcryptjs": "^3.0.0",
"bcryptjs": "^3.0.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.544.0",
"next": "^15.5.0",
"next-auth": "^4.24.11",
"prisma": "^6.14.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"tailwind-merge": "^3.3.1",
"tailwindcss-animate": "^1.0.7",
"zod": "^4.1.1"
},
πŸ€– Prompt for AI Agents
In apps/web/package.json around lines 19 to 38, the package.json is missing
runtime dependencies that the code imports (next-themes, sonner,
@radix-ui/react-scroll-area, @radix-ui/react-collapsible, recharts); add each of
these to the "dependencies" section (not devDependencies) with appropriate
versions that match the repo's React/Next stack (or use the latest compatible
semver), then run the package manager (npm/yarn/pnpm) to install and update the
lockfile; ensure import names match the installed package names and commit the
updated package.json and lockfile.

Comment on lines +35 to +36
"tailwind-merge": "^3.3.1",
"tailwindcss-animate": "^1.0.7",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Tailwind v3/v4 mismatch (classes will not compile).

Code uses Tailwind v4-only syntax (e.g., bg-(--var), border-(--var)) but tailwindcss is ^3.4.0 and tailwind-merge is v3 (targets v4). This causes broken styles.

Options:

  • Upgrade Tailwind to v4 and follow the migration (preferred).
  • Or keep Tailwind v3, change classes to v3 syntax (e.g., bg-[var(--color-bg)], border-[var(--color-border)]) and pin tailwind-merge to ^2.6.0.

Based on learnings

Also applies to: 48-48

πŸ€– Prompt for AI Agents
In apps/web/package.json around lines 35-36 the project has a Tailwind v3/v4
mismatch: code uses Tailwind v4-only syntax while tailwindcss is ^3.4.0 and
tailwind-merge is v3, causing broken styles. Preferred fix: upgrade tailwindcss
to v4 (bump package.json to the v4 release), run the official Tailwind v4
migration steps (update config, plugins and any class syntax), and upgrade
tailwind-merge to the v4-compatible release; Alternative fix: keep Tailwind v3
by changing all v4-style classes (e.g., bg-(--var), border-(--var)) to v3 syntax
like bg-[var(--color-bg)] and border-[var(--color-border)] and pin
tailwind-merge to ^2.6.0 in package.json; after either change run install,
rebuild CSS, and verify styles.

Comment on lines +35 to +41
CREATE TABLE "public"."Session" (
"sessionToken" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | πŸ”΄ Critical

Add primary key/id column to Session.

Session currently has no primary key, so inserts from the Prisma/NextAuth adapter that expect an id field will fail at runtime. Introduce an id column with a default generator and set it as the primary key.

 CREATE TABLE "public"."Session" (
+    "id" TEXT NOT NULL DEFAULT gen_random_uuid(),
     "sessionToken" TEXT NOT NULL,
     "userId" TEXT NOT NULL,
     "expires" TIMESTAMP(3) NOT NULL,
     "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "updatedAt" TIMESTAMP(3) NOT NULL
+    "updatedAt" TIMESTAMP(3) NOT NULL,
+
+    CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
 );
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
CREATE TABLE "public"."Session" (
"sessionToken" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL
);
CREATE TABLE "public"."Session" (
"id" TEXT NOT NULL DEFAULT gen_random_uuid(),
"sessionToken" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
);
πŸ€– Prompt for AI Agents
In apps/web/prisma/migrations/20250911181640_init/migration.sql around lines
35-41, the Session table lacks a primary key/id column; add an "id" column and
make it the primary key (e.g., id UUID NOT NULL DEFAULT gen_random_uuid() and
add PRIMARY KEY ("id")) so inserts from the Prisma/NextAuth adapter that expect
an id succeed; ensure the DB has the uuid generator available (enable the
appropriate extension) or use the project's chosen ID generator function.

Comment on lines +44 to +73
CREATE TABLE "public"."VerificationToken" (
"identifier" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL,

CONSTRAINT "VerificationToken_pkey" PRIMARY KEY ("identifier","token")
);

-- CreateTable
CREATE TABLE "public"."Authenticator" (
"credentialID" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"providerAccountId" TEXT NOT NULL,
"credentialPublicKey" TEXT NOT NULL,
"counter" INTEGER NOT NULL,
"credentialDeviceType" TEXT NOT NULL,
"credentialBackedUp" BOOLEAN NOT NULL,
"transports" TEXT,

CONSTRAINT "Authenticator_pkey" PRIMARY KEY ("userId","credentialID")
);

-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "public"."User"("email");

-- CreateIndex
CREATE UNIQUE INDEX "Session_sessionToken_key" ON "public"."Session"("sessionToken");

-- CreateIndex
CREATE UNIQUE INDEX "Authenticator_credentialID_key" ON "public"."Authenticator"("credentialID");
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | πŸ”΄ Critical

Restore the unique index on VerificationToken.token.

The Prisma/NextAuth adapter calls findUnique({ where: { token } }); without a unique constraint on token, those queries will error. Add back the unique index for token.

 -- CreateIndex
 CREATE UNIQUE INDEX "User_email_key" ON "public"."User"("email");

 -- CreateIndex
 CREATE UNIQUE INDEX "Session_sessionToken_key" ON "public"."Session"("sessionToken");

 -- CreateIndex
 CREATE UNIQUE INDEX "Authenticator_credentialID_key" ON "public"."Authenticator"("credentialID");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "VerificationToken_token_key" ON "public"."VerificationToken"("token");
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
CREATE TABLE "public"."VerificationToken" (
"identifier" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL,
CONSTRAINT "VerificationToken_pkey" PRIMARY KEY ("identifier","token")
);
-- CreateTable
CREATE TABLE "public"."Authenticator" (
"credentialID" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"providerAccountId" TEXT NOT NULL,
"credentialPublicKey" TEXT NOT NULL,
"counter" INTEGER NOT NULL,
"credentialDeviceType" TEXT NOT NULL,
"credentialBackedUp" BOOLEAN NOT NULL,
"transports" TEXT,
CONSTRAINT "Authenticator_pkey" PRIMARY KEY ("userId","credentialID")
);
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "public"."User"("email");
-- CreateIndex
CREATE UNIQUE INDEX "Session_sessionToken_key" ON "public"."Session"("sessionToken");
-- CreateIndex
CREATE UNIQUE INDEX "Authenticator_credentialID_key" ON "public"."Authenticator"("credentialID");
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "public"."User"("email");
-- CreateIndex
CREATE UNIQUE INDEX "Session_sessionToken_key" ON "public"."Session"("sessionToken");
-- CreateIndex
CREATE UNIQUE INDEX "Authenticator_credentialID_key" ON "public"."Authenticator"("credentialID");
-- CreateIndex
CREATE UNIQUE INDEX "VerificationToken_token_key" ON "public"."VerificationToken"("token");
πŸ€– Prompt for AI Agents
In apps/web/prisma/migrations/20250911181640_init/migration.sql around lines 44
to 73, the VerificationToken table lacks a unique constraint on the token column
which causes NextAuth/Prisma findUnique({ where: { token } }) calls to fail; add
a unique index on the VerificationToken.token column (matching naming
conventions used elsewhere, e.g., VerificationToken_token_key) by creating a
UNIQUE INDEX on "public"."VerificationToken"("token") and then regenerate or
apply the migration so the database has the unique constraint.

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 16

♻️ Duplicate comments (34)
apps/web/app/components/ui/collapsible.tsx (3)

1-4: Critical: Missing React import (previously flagged).

The code uses React.ComponentProps without importing React. This issue was already identified in a previous review and remains unresolved.


11-20: Critical: Wrong Radix export names (previously flagged).

CollapsiblePrimitive.CollapsibleTrigger does not exist in @radix-ui/react-collapsible. The correct export is CollapsiblePrimitive.Trigger. This runtime error was already flagged and must be fixed.


22-31: Critical: Wrong Radix export names (previously flagged).

CollapsiblePrimitive.CollapsibleContent does not exist. The correct export is CollapsiblePrimitive.Content. This issue was previously identified and remains unresolved.

apps/web/app/components/ui/calendar.tsx (1)

4-8: lucide-react icon import issue (already flagged in previous review).

The incorrect Icon suffix on lucide-react imports was comprehensively addressed in the previous review comment, which includes the fix, verification script, and list of all affected files.

apps/web/app/components/ui/form.tsx (3)

28-30: Previously flagged context initialization issue remains unresolved.

The FormFieldContext is still initialized with a non-nullable empty object ({} as FormFieldContextValue), which makes the guard in useFormField ineffective. This issue was previously identified and needs to be addressed.


45-66: Previously flagged guard order issue remains unresolved.

The useFormField hook still accesses fieldContext.name (lines 49-50) before validating that fieldContext exists (line 52). Additionally, itemContext.id is accessed (line 56) without checking if itemContext is valid. These issues were previously identified and need to be addressed.


72-74: Previously flagged context initialization issue remains unresolved.

The FormItemContext has the same initialization problem as FormFieldContext - using a non-nullable empty object. This issue was previously identified and needs to be addressed.

apps/web/app/components/ui/switch.tsx (1)

8-29: Add ref forwarding and displayName.

The Switch component should support ref forwarding for form library integration and imperative access. It should also set a displayName for better debugging in React DevTools.

apps/web/app/components/ui/slider.tsx (2)

8-61: Add ref forwarding and displayName.

Consistent with other form components, Slider should support ref forwarding for form library integration and debugging.


16-24: Review _values fallback logic.

The fallback [min, max] when neither value nor defaultValue is an array could create unexpected behavior. If the Slider is controlled with a single numeric value, this will incorrectly create a two-thumb slider instead of a single-thumb slider.

apps/web/prisma/migrations/20250911181640_init/migration.sql (2)

35-41: Add primary key/id column to Session.

Session currently has no primary key, so inserts from the Prisma/NextAuth adapter that expect an id field will fail at runtime. Introduce an id column with a default generator and set it as the primary key.

Apply this diff to add the primary key:

 CREATE TABLE "public"."Session" (
+    "id" TEXT NOT NULL DEFAULT gen_random_uuid(),
     "sessionToken" TEXT NOT NULL,
     "userId" TEXT NOT NULL,
     "expires" TIMESTAMP(3) NOT NULL,
     "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    "updatedAt" TIMESTAMP(3) NOT NULL
+    "updatedAt" TIMESTAMP(3) NOT NULL,
+
+    CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
 );

44-73: Restore the unique index on VerificationToken.token.

The Prisma/NextAuth adapter calls findUnique({ where: { token } }); without a unique constraint on token, those queries will error. Add back the unique index for token.

Apply this diff:

 -- CreateIndex
 CREATE UNIQUE INDEX "User_email_key" ON "public"."User"("email");

 -- CreateIndex
 CREATE UNIQUE INDEX "Session_sessionToken_key" ON "public"."Session"("sessionToken");

 -- CreateIndex
 CREATE UNIQUE INDEX "Authenticator_credentialID_key" ON "public"."Authenticator"("credentialID");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "VerificationToken_token_key" ON "public"."VerificationToken"("token");
apps/web/app/components/ui/hover-card.tsx (1)

35-37: Invalid Tailwind classes: origin-(...) and outline-hidden.

Use Tailwind's arbitrary values syntax and correct outline utility.

Apply this diff:

-          'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden',
+          'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-[var(--radix-hover-card-content-transform-origin)] rounded-md border p-4 shadow-md outline-none',
apps/web/app/components/ui/scroll-area.tsx (1)

31-55: Fix Radix ScrollArea identifiers: use Scrollbar/Thumb (not ScrollAreaScrollbar/ScrollAreaThumb).

@radix-ui/react-scroll-area exposes Scrollbar and Thumb, not ScrollAreaScrollbar and ScrollAreaThumb.

Apply this diff:

 function ScrollBar({
   className,
   orientation = 'vertical',
   ...props
-}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
+}: React.ComponentProps<typeof ScrollAreaPrimitive.Scrollbar>) {
   return (
-    <ScrollAreaPrimitive.ScrollAreaScrollbar
+    <ScrollAreaPrimitive.Scrollbar
       data-slot="scroll-area-scrollbar"
       orientation={orientation}
       className={cn(
         'flex touch-none p-px transition-colors select-none',
         orientation === 'vertical' &&
           'h-full w-2.5 border-l border-l-transparent',
         orientation === 'horizontal' &&
           'h-2.5 flex-col border-t border-t-transparent',
         className,
       )}
       {...props}
     >
-      <ScrollAreaPrimitive.ScrollAreaThumb
+      <ScrollAreaPrimitive.Thumb
         data-slot="scroll-area-thumb"
         className="bg-border relative flex-1 rounded-full"
       />
-    </ScrollAreaPrimitive.ScrollAreaScrollbar>
+    </ScrollAreaPrimitive.Scrollbar>
   )
 }
apps/web/app/components/ui/chart.tsx (1)

82-101: Remove dangerouslySetInnerHTML or suppress lint properly.

Lint is failing on this block (noDangerouslySetInnerHtml). Until you either move CSS vars into inline styles or at least sanitize and add a scoped lint waiver, CI will keep breaking. Please adopt one of the safer patterns (e.g., build the CSS vars object and pass it via style, or keep the <style> tag but sanitize and add /* biome-ignore lint/security/noDangerouslySetInnerHtml: reason */).

apps/web/app/components/ui/navigation-menu.tsx (1)

90-95: Replace invalid **: Tailwind selectors with arbitrary selectors.

**:data-[slot=…] isn’t a Tailwind variant, so your focus resets are ignored. Use an arbitrary selector variant targeting the descendants instead (e.g., [&_*[data-slot=navigation-menu-link]:focus]:ring-0 and matching outline rule).

-        '... group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none',
+        '... group-data-[viewport=false]/navigation-menu:duration-200 [&_*[data-slot=navigation-menu-link]:focus]:ring-0 [&_*[data-slot=navigation-menu-link]:focus]:outline-none',
apps/web/app/(auth)/signup/page.tsx (1)

191-223: Extract OAuth provider buttons to shared components.

The Google and GitHub SVG markup is duplicated from the signin page. As mentioned in the signin page review, extract these into reusable components to maintain consistency and reduce duplication.

apps/web/app/components/ui/sidebar.tsx (1)

97-110: Scope the Ctrl/Cmd +B shortcut to avoid hijacking native bold.

The global keydown listener always intercepts Ctrl/Cmd +B and prevents the default action, breaking the standard "bold" shortcut in any rich-text editor or contenteditable on the page. Limit the handler to non-editable contexts by checking if the event target is an input, textarea, or contenteditable element before calling toggleSidebar().

apps/web/app/components/ui/context-menu.tsx (2)

88-88: Correct Tailwind arbitrary value for Radix transform origin.

origin-(--radix-context-menu-content-transform-origin) uses incorrect parenthesis syntax and will not generate valid CSS. Switch to bracket syntax with var().

Apply this diff:

-          'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg',
+          'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-[var(--radix-context-menu-content-transform-origin)] overflow-hidden rounded-md border p-1 shadow-lg',

105-105: Correct Tailwind arbitrary values for Radix sizing and transform origin.

Both max-h-(--radix-context-menu-content-available-height) and origin-(--radix-context-menu-content-transform-origin) use incorrect parenthesis syntax. Tailwind requires bracket syntax with var() to generate valid CSS. Without this fix, large menus will ignore Radix's height cap and transform origin.

Apply this diff:

-          'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md',
+          'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-[var(--radix-context-menu-content-available-height)] min-w-[8rem] origin-[var(--radix-context-menu-content-transform-origin)] overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md',
apps/web/app/components/ui/label.tsx (1)

8-22: Add ref forwarding for form library compatibility.

The Label component doesn't support ref forwarding, preventing integration with form libraries (React Hook Form, Formik, etc.) that need to access the underlying DOM element. Radix Label supports refs, but this wrapper doesn't forward them.

Apply this diff to add ref forwarding:

-function Label({
+const Label = React.forwardRef<
+  React.ElementRef<typeof LabelPrimitive.Root>,
+  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
+>(({ 
   className,
   ...props
-}: React.ComponentProps<typeof LabelPrimitive.Root>) {
+}, ref) => {
   return (
     <LabelPrimitive.Root
       data-slot="label"
+      ref={ref}
       className={cn(
         'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
         className,
       )}
       {...props}
     />
   )
-}
+})
+Label.displayName = LabelPrimitive.Root.displayName
apps/web/lib/utils.ts (1)

1-6: Consolidate duplicate utility files.

This utility file is duplicated at both apps/web/lib/utils.ts and apps/web/app/lib/utils.ts. Having identical files can lead to confusion and maintenance issues. Consider removing one and updating import paths accordingly to maintain a single source of truth.

Run the following script to confirm the duplication and identify which files import from each location:

#!/bin/bash
# Check if both files exist and compare them
if [ -f "apps/web/lib/utils.ts" ] && [ -f "apps/web/app/lib/utils.ts" ]; then
  echo "=== Both files exist ==="
  diff apps/web/lib/utils.ts apps/web/app/lib/utils.ts
  echo ""
  echo "=== Files importing from /lib/utils ==="
  rg -n "@/lib/utils" --type=ts --type=tsx
  echo ""
  echo "=== Files importing from /app/lib/utils ==="
  rg -n "@/app/lib/utils" --type=ts --type=tsx
fi
apps/web/app/components/ui/toaster.tsx (1)

3-3: Fix incorrect useToast import path.

Line 3 imports from @/hooks/use-toast, but the hook is defined at apps/web/app/components/ui/use-toast.ts. This path mismatch will fail at compile time.

Apply this diff to correct the import:

-import { useToast } from '@/hooks/use-toast'
+import { useToast } from './use-toast'
apps/web/app/components/ui/sonner.tsx (2)

3-4: Add missing dependencies.

The imports from next-themes and sonner will fail unless those packages are declared in dependencies.

This concern was previously raised and remains unresolved. Ensure next-themes and sonner are added to apps/web/package.json dependencies.


14-18: Fix: Add React type import for CSSProperties.

You're using React.CSSProperties without importing React types, which will cause TypeScript errors under isolatedModules.

This concern was previously raised. Apply the suggested fix from the previous review:

 'use client'
 
+import type { CSSProperties } from 'react'
 import { useTheme } from 'next-themes'

Then update the cast:

-      } as React.CSSProperties
+      } as CSSProperties
apps/web/app/components/ui/checkbox.tsx (1)

9-30: Add ref forwarding and displayName.

Form libraries require ref access for validation and focus management. This concern was previously raised and remains unresolved.

Apply the diff from the previous review to add ref forwarding:

-function Checkbox({
+const Checkbox = React.forwardRef<
+  React.ElementRef<typeof CheckboxPrimitive.Root>,
+  React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
+>(({
   className,
   ...props
-}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
+}, ref) => {
   return (
     <CheckboxPrimitive.Root
       data-slot="checkbox"
+      ref={ref}
       className={cn(
         'peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
         className,
       )}
       {...props}
     >
       <CheckboxPrimitive.Indicator
         data-slot="checkbox-indicator"
         className="flex items-center justify-center text-current transition-none"
       >
         <CheckIcon className="size-3.5" />
       </CheckboxPrimitive.Indicator>
     </CheckboxPrimitive.Root>
   )
-}
+})
+Checkbox.displayName = CheckboxPrimitive.Root.displayName
apps/web/app/components/ui/popover.tsx (1)

32-35: Fix non-standard Tailwind utilities.

The className uses invalid Tailwind syntax that was previously flagged. This concern remains unresolved.

Apply the fix from the previous review:

  • Replace outline-hidden with outline-none
  • Replace origin-(--radix-popover-content-transform-origin) with origin-[var(--radix-popover-content-transform-origin)]
         className={cn(
-          'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden',
+          'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-[var(--radix-popover-content-transform-origin)] rounded-md border p-4 shadow-md outline-none',
           className,
         )}
apps/web/app/components/ui/carousel.tsx (2)

78-89: Still missing vertical arrow handling.

Vertical carousels still ignore ArrowUp/ArrowDown, so keyboard users cannot move slides. Please map the vertical keys and add orientation as a dependency.

   const handleKeyDown = React.useCallback(
     (event: React.KeyboardEvent<HTMLDivElement>) => {
-      if (event.key === 'ArrowLeft') {
+      const isHorizontal = orientation === 'horizontal'
+
+      if (
+        (isHorizontal && event.key === 'ArrowLeft') ||
+        (!isHorizontal && event.key === 'ArrowUp')
+      ) {
         event.preventDefault()
         scrollPrev()
-      } else if (event.key === 'ArrowRight') {
+        return
+      }
+
+      if (
+        (isHorizontal && event.key === 'ArrowRight') ||
+        (!isHorizontal && event.key === 'ArrowDown')
+      ) {
         event.preventDefault()
         scrollNext()
       }
     },
-    [scrollPrev, scrollNext],
+    [orientation, scrollPrev, scrollNext],
   )

96-105: Clean up both Embla listeners.

onSelect is still registered for reInit but never removed, so callbacks leak after reinitialization/hot reload.

     api.on('reInit', onSelect)
     api.on('select', onSelect)

     return () => {
+      api?.off('reInit', onSelect)
       api?.off('select', onSelect)
     }
apps/web/app/components/ui/command.tsx (2)

55-55: Replace the invalid **:data[...] selector

**:data-[slot=command-input-wrapper]:h-12 isn’t valid Tailwind syntax, so the lint error persists. Switch to the descendant arbitrary variant form Radix expects.

Use this diff:

-        <Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
+        <Command className="[&_[cmdk-group-heading]]:text-muted-foreground [&_[data-slot=command-input-wrapper]]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">

76-150: Swap outline-hidden for Tailwind’s supported focus styles

outline-hidden isn’t a Tailwind utility, so builds fail. Replace it with outline-none (and keep the existing focus rings).

-        className={cn(
-          'placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
+        className={cn(
+          'placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
@@
-      className={cn(
-        "data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+      className={cn(
+        "data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
apps/web/app/components/ui/dialog.tsx (1)

70-74: Replace non-existent Tailwind utilities on the close button

rounded-xs and focus:outline-hidden aren’t provided by Tailwind, so the build fails. Swap them for rounded-sm and focus:outline-none (focus ring already present).

-          <DialogPrimitive.Close
-            data-slot="dialog-close"
-            className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
+          <DialogPrimitive.Close
+            data-slot="dialog-close"
+            className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
apps/web/app/components/ui/menubar.tsx (1)

82-251: Fix invalid Tailwind utilities (origin/outline/rounded)

origin-(--radix-…), outline-hidden, and rounded-xs all break Tailwind builds. Use the bracket var() syntax and supported outline/border tokens.

-        className={cn(
-          'bg-popover text-popover-foreground ... origin-(--radix-menubar-content-transform-origin) ...',
+        className={cn(
+          'bg-popover text-popover-foreground ... origin-[var(--radix-menubar-content-transform-origin)] ...',
@@
-        " ... text-sm outline-hidden select-none ...",
+        " ... text-sm outline-none focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 select-none ...",
@@
-        " ... rounded-xs ... outline-hidden ...",
+        " ... rounded-sm ... outline-none focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 ...",
@@
-        '... origin-(--radix-menubar-content-transform-origin) ...',
+        '... origin-[var(--radix-menubar-content-transform-origin)] ...',

(Apply the same replacements in MenubarItem, CheckboxItem, RadioItem, and SubContent.)

apps/web/app/components/ui/dropdown-menu.tsx (1)

45-233: Correct Tailwind syntax for Radix vars and focus utilities

max-h-(--radix-…), origin-(--radix-…), and outline-hidden aren’t valid utilities, so lint/build fail. Use bracketed var() tokens and Tailwind-supported focus styles.

-        className={cn(
-          '... max-h-(--radix-dropdown-menu-content-available-height) ... origin-(--radix-dropdown-menu-content-transform-origin) ...',
+        className={cn(
+          '... max-h-[var(--radix-dropdown-menu-content-available-height)] ... origin-[var(--radix-dropdown-menu-content-transform-origin)] ...',
@@
-      className={cn(
-        " ... text-sm outline-hidden select-none ...",
+      className={cn(
+        " ... text-sm outline-none focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 select-none ...",
@@
-        className={cn(
-          '... origin-(--radix-dropdown-menu-content-transform-origin) ...',
+        className={cn(
+          '... origin-[var(--radix-dropdown-menu-content-transform-origin)] ...',

(Apply the outline fix across Item, CheckboxItem, RadioItem, and SubTrigger.)

🧹 Nitpick comments (13)
apps/web/app/components/ui/aspect-ratio.tsx (2)

1-1: Evaluate the necessity of 'use client' directive.

The Radix UI AspectRatio primitive is primarily CSS-based and doesn't require client-side JavaScript for its core functionality. Consider whether this component truly needs to be a client component, as removing the directive could enable server-side rendering and reduce the client bundle size.

If you determine that 'use client' is unnecessary, apply this diff:

-'use client'
-
 import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio'

5-9: Add ref forwarding for better component composability.

The component doesn't forward refs, which could limit its usage in scenarios where consumers need to attach refs to the underlying DOM element. Since the TypeScript typing includes ref support via ComponentProps, consumers might expect ref forwarding to work.

Apply this diff to add proper ref forwarding:

-function AspectRatio({
+const AspectRatio = React.forwardRef<
+  React.ElementRef<typeof AspectRatioPrimitive.Root>,
+  React.ComponentPropsWithoutRef<typeof AspectRatioPrimitive.Root>
+>(({
   ...props
-}: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {
-  return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} />
-}
+}, ref) => {
+  return <AspectRatioPrimitive.Root ref={ref} data-slot="aspect-ratio" {...props} />
+})
+AspectRatio.displayName = 'AspectRatio'
apps/web/app/components/ui/use-mobile.tsx (1)

9-14: Optional: Use mql.matches for consistency.

Currently, the code creates a media query string (max-width: 767px) but then uses window.innerWidth < MOBILE_BREAKPOINT to determine the mobile state. While these are functionally equivalent for integer pixel values, using mql.matches would be more consistent and rely on the browser's media query evaluation rather than manual window size checks.

 const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
 const onChange = () => {
-  setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
+  setIsMobile(mql.matches)
 }
 mql.addEventListener('change', onChange)
-setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
+setIsMobile(mql.matches)
apps/web/app/components/ui/drawer.tsx (1)

48-73: Consider breaking up long className strings for maintainability.

The component structure correctly composes Portal, Overlay, and Content. However, lines 60-63 contain very long Tailwind class strings that could benefit from formatting for readability.

Consider formatting the className for better readability:

       className={cn(
         'group/drawer-content bg-background fixed z-50 flex h-auto flex-col',
-        'data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b',
-        'data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t',
-        'data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm',
-        'data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm',
+        // Top drawer styles
+        'data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24',
+        'data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b',
+        // Bottom drawer styles
+        'data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24',
+        'data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t',
+        // Right drawer styles
+        'data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0',
+        'data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm',
+        // Left drawer styles
+        'data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0',
+        'data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm',
         className,
       )}
apps/web/components/ui/card.tsx (1)

32-42: Prefer semantic heading element for CardTitle.

CardTitle currently renders a div, which lacks semantic meaning for screen readers and document structure. Consider using a heading element (h2, h3, or accepting an as prop) to improve accessibility.

Apply this diff to add an as prop for flexibility:

-const CardTitle = React.forwardRef<
-  HTMLDivElement,
-  React.HTMLAttributes<HTMLDivElement>
->(({ className, ...props }, ref) => (
-  <div
+const CardTitle = React.forwardRef<
+  HTMLHeadingElement,
+  React.HTMLAttributes<HTMLHeadingElement> & { as?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' }
+>(({ className, as: Component = 'h3', ...props }, ref) => (
+  <Component
     ref={ref}
     className={cn("font-semibold leading-none tracking-tight", className)}
     {...props}
   />
 ))
apps/web/app/components/ui/sidebar.tsx (1)

85-87: Consider adding secure flag to sidebar state cookie.

The cookie is set without explicit secure or httpOnly flags. While this is UI state and not sensitive data, consider adding secure in production to prevent transmission over insecure connections.

Apply this diff to conditionally add the secure flag:

-      document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
+      const secure = process.env.NODE_ENV === 'production' ? '; secure' : ''
+      document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}${secure}`
apps/web/app/(auth)/signin/page.tsx (1)

135-167: Extract OAuth provider buttons to shared components.

The Google and GitHub SVG markup (lines 137-166) is duplicated between signin and signup pages. Extract these into reusable OAuthButton or GoogleButton/GitHubButton components to improve maintainability.

Create a shared component:

// components/ui/oauth-button.tsx
export function GoogleButton({ onClick, disabled }: { onClick: () => void; disabled?: boolean }) {
  return (
    <Button variant="outline" type="button" onClick={onClick} disabled={disabled}>
      <svg className="mr-2 h-4 w-4" viewBox="0 0 24 24">
        {/* SVG paths */}
      </svg>
      Google
    </Button>
  )
}

Then replace the inline buttons with <GoogleButton onClick={() => handleOAuthSignIn("google")} disabled={isLoading} />.

apps/web/app/(auth)/signup/page.tsx (1)

30-34: Add password strength validation.

The current validation only checks if passwords match. Consider adding minimum length, complexity requirements, or using a library like zxcvbn to ensure users create secure passwords.

Example validation:

if (password.length < 8) {
  setError("Password must be at least 8 characters long")
  setIsLoading(false)
  return
}
if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(password)) {
  setError("Password must contain uppercase, lowercase, and number")
  setIsLoading(false)
  return
}
if (password !== confirmPassword) {
  setError("Passwords do not match")
  setIsLoading(false)
  return
}
apps/web/app/components/ui/textarea.tsx (2)

5-16: Consider adding ref forwarding for form library integration.

Form libraries like react-hook-form often require ref access for validation and focus management.

Apply this diff to add ref forwarding:

-function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
+const Textarea = React.forwardRef<
+  HTMLTextAreaElement,
+  React.ComponentProps<'textarea'>
+>(({ className, ...props }, ref) => {
   return (
     <textarea
+      ref={ref}
       data-slot="textarea"
       className={cn(
         'border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
         className,
       )}
       {...props}
     />
   )
-}
+})
+Textarea.displayName = 'Textarea'

10-10: Provide fallback for experimental field-sizing: content
The CSS field-sizing: content property is experimental and only supported in Chromium-based browsers (Chrome 123+, Edge 123+, Opera, some Android builds); it’s not supported in Firefox or Safari. Add a standard sizing fallback or implement a JS polyfill to ensure consistent textarea resizing across all browsers.

apps/web/app/components/ui/input.tsx (2)

5-19: Add ref forwarding for form library integration.

Form libraries like react-hook-form require ref access for validation and focus management.

Apply this diff:

-function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
+const Input = React.forwardRef<
+  HTMLInputElement,
+  React.ComponentProps<'input'>
+>(({ className, type, ...props }, ref) => {
   return (
     <input
+      ref={ref}
       type={type}
       data-slot="input"
       className={cn(
         'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
         'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
         'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
         className,
       )}
       {...props}
     />
   )
-}
+})
+Input.displayName = 'Input'

5-5: Simplify: Remove unnecessary type destructuring.

The type prop is destructured separately but passed through without transformation, which adds no value.

-function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
+function Input({ className, ...props }: React.ComponentProps<'input'>) {
   return (
     <input
-      type={type}
       data-slot="input"
apps/web/components/ui/label.tsx (1)

9-11: Simplify: Remove unnecessary cva wrapper.

The labelVariants uses class-variance-authority but defines no actual variants (no variant options to switch between). This adds unnecessary complexity and bundle size.

Apply this diff to simplify:

-import { cva, type VariantProps } from "class-variance-authority"
-
-const labelVariants = cva(
-  "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
-)
-
 const Label = React.forwardRef<
   React.ElementRef<typeof LabelPrimitive.Root>,
-  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
-    VariantProps<typeof labelVariants>
+  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
 >(({ className, ...props }, ref) => (
   <LabelPrimitive.Root
     ref={ref}
-    className={cn(labelVariants(), className)}
+    className={cn("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", className)}
     {...props}
   />
 ))
πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 011896d and c578d06.

β›” Files ignored due to path filters (6)
  • apps/web/app/public/placeholder-logo.png is excluded by !**/*.png
  • apps/web/app/public/placeholder-logo.svg is excluded by !**/*.svg
  • apps/web/app/public/placeholder-user.jpg is excluded by !**/*.jpg
  • apps/web/app/public/placeholder.jpg is excluded by !**/*.jpg
  • apps/web/app/public/placeholder.svg is excluded by !**/*.svg
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
πŸ“’ Files selected for processing (68)
  • apps/web/app/(auth)/signin/page.tsx (2 hunks)
  • apps/web/app/(auth)/signup/page.tsx (2 hunks)
  • apps/web/app/components/theme-provider.tsx (1 hunks)
  • apps/web/app/components/ui/accordion.tsx (1 hunks)
  • apps/web/app/components/ui/alert-dialog.tsx (1 hunks)
  • apps/web/app/components/ui/alert.tsx (1 hunks)
  • apps/web/app/components/ui/aspect-ratio.tsx (1 hunks)
  • apps/web/app/components/ui/avatar.tsx (1 hunks)
  • apps/web/app/components/ui/badge.tsx (1 hunks)
  • apps/web/app/components/ui/breadcrumb.tsx (1 hunks)
  • apps/web/app/components/ui/button.tsx (1 hunks)
  • apps/web/app/components/ui/calendar.tsx (1 hunks)
  • apps/web/app/components/ui/card.tsx (1 hunks)
  • apps/web/app/components/ui/carousel.tsx (1 hunks)
  • apps/web/app/components/ui/chart.tsx (1 hunks)
  • apps/web/app/components/ui/checkbox.tsx (1 hunks)
  • apps/web/app/components/ui/collapsible.tsx (1 hunks)
  • apps/web/app/components/ui/command.tsx (1 hunks)
  • apps/web/app/components/ui/context-menu.tsx (1 hunks)
  • apps/web/app/components/ui/dialog.tsx (1 hunks)
  • apps/web/app/components/ui/drawer.tsx (1 hunks)
  • apps/web/app/components/ui/dropdown-menu.tsx (1 hunks)
  • apps/web/app/components/ui/form.tsx (1 hunks)
  • apps/web/app/components/ui/hover-card.tsx (1 hunks)
  • apps/web/app/components/ui/input-otp.tsx (1 hunks)
  • apps/web/app/components/ui/input.tsx (1 hunks)
  • apps/web/app/components/ui/label.tsx (1 hunks)
  • apps/web/app/components/ui/menubar.tsx (1 hunks)
  • apps/web/app/components/ui/navigation-menu.tsx (1 hunks)
  • apps/web/app/components/ui/pagination.tsx (1 hunks)
  • apps/web/app/components/ui/popover.tsx (1 hunks)
  • apps/web/app/components/ui/progress.tsx (1 hunks)
  • apps/web/app/components/ui/radio-group.tsx (1 hunks)
  • apps/web/app/components/ui/resizable.tsx (1 hunks)
  • apps/web/app/components/ui/scroll-area.tsx (1 hunks)
  • apps/web/app/components/ui/select.tsx (1 hunks)
  • apps/web/app/components/ui/separator.tsx (1 hunks)
  • apps/web/app/components/ui/sheet.tsx (1 hunks)
  • apps/web/app/components/ui/sidebar.tsx (1 hunks)
  • apps/web/app/components/ui/skeleton.tsx (1 hunks)
  • apps/web/app/components/ui/slider.tsx (1 hunks)
  • apps/web/app/components/ui/sonner.tsx (1 hunks)
  • apps/web/app/components/ui/switch.tsx (1 hunks)
  • apps/web/app/components/ui/table.tsx (1 hunks)
  • apps/web/app/components/ui/tabs.tsx (1 hunks)
  • apps/web/app/components/ui/textarea.tsx (1 hunks)
  • apps/web/app/components/ui/toast.tsx (1 hunks)
  • apps/web/app/components/ui/toaster.tsx (1 hunks)
  • apps/web/app/components/ui/toggle-group.tsx (1 hunks)
  • apps/web/app/components/ui/toggle.tsx (1 hunks)
  • apps/web/app/components/ui/tooltip.tsx (1 hunks)
  • apps/web/app/components/ui/use-mobile.tsx (1 hunks)
  • apps/web/app/components/ui/use-toast.ts (1 hunks)
  • apps/web/app/features/page.tsx (1 hunks)
  • apps/web/app/globals.css (1 hunks)
  • apps/web/app/lib/utils.ts (1 hunks)
  • apps/web/app/page.tsx (1 hunks)
  • apps/web/components.json (1 hunks)
  • apps/web/components/ui/badge.tsx (1 hunks)
  • apps/web/components/ui/button.tsx (1 hunks)
  • apps/web/components/ui/card.tsx (1 hunks)
  • apps/web/components/ui/input.tsx (1 hunks)
  • apps/web/components/ui/label.tsx (1 hunks)
  • apps/web/lib/utils.ts (1 hunks)
  • apps/web/package.json (1 hunks)
  • apps/web/prisma/migrations/20250911181640_init/migration.sql (1 hunks)
  • apps/web/prisma/migrations/migration_lock.toml (1 hunks)
  • apps/web/tailwind.config.js (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (56)
apps/web/app/components/ui/calendar.tsx (2)
apps/web/app/components/ui/button.tsx (2)
  • Button (59-59)
  • buttonVariants (59-59)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/context-menu.tsx (1)
apps/web/app/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/drawer.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/toggle.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/toaster.tsx (2)
apps/web/app/components/ui/use-toast.ts (1)
  • useToast (191-191)
apps/web/app/components/ui/toast.tsx (6)
  • ToastProvider (122-122)
  • Toast (124-124)
  • ToastTitle (125-125)
  • ToastDescription (126-126)
  • ToastClose (127-127)
  • ToastViewport (123-123)
apps/web/app/components/ui/label.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/input.tsx (2)
apps/web/app/lib/utils.ts (1)
  • cn (4-6)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/badge.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/components/ui/input.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/components/ui/badge.tsx (2)
apps/web/app/components/ui/badge.tsx (2)
  • badgeVariants (46-46)
  • Badge (46-46)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/components/ui/card.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/(auth)/signup/page.tsx (1)
apps/web/lib/auth-config.ts (1)
  • signIn (103-119)
apps/web/app/components/ui/textarea.tsx (1)
apps/web/app/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/radio-group.tsx (1)
apps/web/app/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/separator.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/pagination.tsx (2)
apps/web/app/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/button.tsx (2)
  • Button (59-59)
  • buttonVariants (59-59)
apps/web/app/components/ui/breadcrumb.tsx (1)
apps/web/app/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/toggle-group.tsx (2)
apps/web/app/components/ui/toggle.tsx (1)
  • toggleVariants (47-47)
apps/web/app/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/use-toast.ts (1)
apps/web/app/components/ui/toast.tsx (3)
  • ToastProps (120-120)
  • ToastActionElement (121-121)
  • Toast (124-124)
apps/web/components/ui/button.tsx (2)
apps/web/app/lib/utils.ts (1)
  • cn (4-6)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/page.tsx (3)
packages/ui/src/code.tsx (1)
  • Code (3-11)
packages/ui/src/button.tsx (1)
  • Button (11-20)
packages/ui/src/card.tsx (1)
  • Card (3-27)
apps/web/app/components/ui/hover-card.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/sonner.tsx (1)
apps/web/app/components/ui/toaster.tsx (1)
  • Toaster (13-35)
apps/web/app/(auth)/signin/page.tsx (2)
apps/web/lib/auth-config.ts (1)
  • signIn (103-119)
packages/ui/src/code.tsx (1)
  • Code (3-11)
apps/web/app/components/ui/avatar.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/scroll-area.tsx (1)
apps/web/app/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/alert.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/features/page.tsx (1)
packages/ui/src/code.tsx (1)
  • Code (3-11)
apps/web/app/components/ui/accordion.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/progress.tsx (1)
apps/web/app/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/tabs.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/table.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/popover.tsx (1)
apps/web/app/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/toast.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/dialog.tsx (1)
apps/web/app/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/switch.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/checkbox.tsx (1)
apps/web/app/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/navigation-menu.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/menubar.tsx (1)
apps/web/app/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/alert-dialog.tsx (3)
apps/web/app/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/button.tsx (1)
  • buttonVariants (59-59)
apps/web/components/ui/button.tsx (1)
  • buttonVariants (57-57)
apps/web/app/components/ui/sheet.tsx (1)
apps/web/app/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/skeleton.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/button.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/tooltip.tsx (1)
apps/web/app/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/select.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/resizable.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/dropdown-menu.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/slider.tsx (1)
apps/web/app/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/card.tsx (2)
apps/web/app/lib/utils.ts (1)
  • cn (4-6)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/chart.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/carousel.tsx (1)
apps/web/app/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/command.tsx (2)
apps/web/app/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/dialog.tsx (5)
  • Dialog (133-133)
  • DialogHeader (138-138)
  • DialogTitle (141-141)
  • DialogDescription (136-136)
  • DialogContent (135-135)
apps/web/app/components/ui/sidebar.tsx (2)
apps/web/app/components/ui/use-mobile.tsx (1)
  • useIsMobile (5-19)
apps/web/app/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/form.tsx (1)
apps/web/app/lib/utils.ts (1)
  • cn (4-6)
apps/web/app/components/ui/input-otp.tsx (1)
apps/web/app/lib/utils.ts (1)
  • cn (4-6)
apps/web/components/ui/label.tsx (1)
apps/web/lib/utils.ts (1)
  • cn (4-6)
πŸͺ› ast-grep (0.39.6)
apps/web/app/components/ui/chart.tsx

[warning] 82-82: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

πŸͺ› Biome (2.1.2)
apps/web/app/components/ui/chart.tsx

[error] 83-83: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

πŸ”‡ Additional comments (14)
apps/web/app/components/ui/form.tsx (1)

32-43: Form component implementations are well-structured.

The form components are thoughtfully designed with good accessibility practices:

  • FormField cleanly wraps Controller and provides field name via context
  • FormItem generates stable unique IDs using React.useId()
  • FormLabel properly connects labels to inputs with htmlFor and error state
  • FormControl correctly implements ARIA attributes (aria-describedby, aria-invalid)
  • FormDescription and FormMessage maintain stable IDs for ARIA relationships
  • Consistent use of data-slot attributes for styling hooks
  • Conditional rendering in FormMessage prevents empty elements

Once the context initialization issues (flagged above) are resolved, this will be a solid, accessible form system.

Also applies to: 76-156

apps/web/app/components/ui/drawer.tsx (3)

1-12: LGTM!

The import structure and root component wrapper are well-implemented. Using React.ComponentProps<typeof DrawerPrimitive.Root> provides proper type inference while maintaining flexibility.


14-46: LGTM!

The wrapper components follow a consistent pattern and properly forward props. The DrawerOverlay correctly implements fade animations using tailwindcss-animate utilities with data attribute selectors.


75-122: LGTM!

The header, footer, title, and description components are well-implemented with appropriate responsive behavior and accessibility support through the vaul primitives.

apps/web/prisma/migrations/migration_lock.toml (1)

1-3: LGTM!

Standard Prisma migration lock file correctly configured for PostgreSQL.

apps/web/app/lib/utils.ts (1)

4-6: LGTM!

The cn utility correctly combines clsx for conditional class composition with twMerge for Tailwind conflict resolution. This is the standard pattern for Tailwind projects and provides consistent class merging across the UI component suite.

apps/web/components/ui/input.tsx (1)

5-20: LGTM!

Excellent implementation. The Input component correctly uses forwardRef for form library integration, sets a displayName for debugging, and properly composes classNames. This serves as a good reference for the other UI primitives.

apps/web/app/components/ui/progress.tsx (1)

8-29: LGTM!

The Progress component implementation is clean and correct. The transform calculation properly translates the indicator based on the progress value, and the component follows the project's pattern of wrapping Radix primitives with consistent styling and data-slot attributes.

apps/web/app/components/ui/radio-group.tsx (1)

9-43: LGTM!

The RadioGroup components are well-implemented. The RadioGroupItem correctly centers the CircleIcon indicator using absolute positioning with transforms, and both components follow the project's consistent pattern of Radix primitive wrappers with data-slot attributes.

apps/web/tailwind.config.js (1)

3-60: LGTM!

The Tailwind configuration is well-structured:

  • Class-based dark mode is correctly configured
  • Theme tokens properly use CSS variables for runtime theming
  • The tailwindcss-animate plugin is correctly integrated for animation utilities

This aligns with the project-wide theming strategy and enables consistent styling across UI components.

Based on learnings.

apps/web/app/components/ui/pagination.tsx (1)

11-117: LGTM!

The pagination components are well-implemented with proper accessibility features:

  • Correct ARIA attributes (role, aria-label, aria-current)
  • Screen reader text for ellipsis
  • Semantic anchor tags for links
  • Consistent styling patterns with buttonVariants

The implementation follows best practices for accessible pagination navigation.

apps/web/app/components/theme-provider.tsx (1)

1-11: LGTM!

Clean wrapper around next-themes that properly forwards all props and children. The implementation follows Next.js best practices for client-side theming providers.

apps/web/app/components/ui/skeleton.tsx (1)

3-11: LGTM!

The Skeleton component follows the established pattern correctly, with appropriate styling for loading states and proper prop forwarding.

apps/web/app/components/ui/badge.tsx (1)

7-44: LGTM!

The Badge component is well-implemented with proper variant handling, the asChild pattern for composition flexibility, and consistent styling. The use of class-variance-authority is appropriate here given the multiple variants.

Comment on lines +235 to +239
{item.value && (
<span className="text-foreground font-mono font-medium tabular-nums">
{item.value.toLocaleString()}
</span>
)}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Render zero values in tooltips.

item.value && … skips legitimate 0 values, so zero-valued series never show up. Guard with a null/undefined check instead of relying on truthiness.

-                    {item.value && (
+                    {item.value !== undefined && item.value !== null && (
                       <span className="text-foreground font-mono font-medium tabular-nums">
                         {item.value.toLocaleString()}
                       </span>
                     )}
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{item.value && (
<span className="text-foreground font-mono font-medium tabular-nums">
{item.value.toLocaleString()}
</span>
)}
{item.value !== undefined && item.value !== null && (
<span className="text-foreground font-mono font-medium tabular-nums">
{item.value.toLocaleString()}
</span>
)}
πŸ€– Prompt for AI Agents
In apps/web/app/components/ui/chart.tsx around lines 235 to 239, the tooltip
condition uses truthiness (item.value && …) which hides legitimate zero values;
change the guard to explicitly check for null/undefined (e.g., item.value !=
null or item.value !== null && item.value !== undefined) so 0 renders, leaving
the inner span and toLocaleString() unchanged.

Comment on lines +124 to +135
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | πŸ”΄ Critical

🧩 Analysis chain

Verify lint configuration addresses the reported error.

The exports are correctly structured. However, the PR comment mentions "please solve lint error." Ensure your linter configuration is properly set up and run linting locally to catch any formatting or style violations.

Run the following to check for lint errors:


🏁 Script executed:

#!/bin/bash
# Description: Check for ESLint/formatting issues in the drawer component

# Check if ESLint is configured
if [ -f ".eslintrc.js" ] || [ -f ".eslintrc.json" ] || [ -f "eslint.config.js" ]; then
  echo "Found ESLint config"
  fd -e js -e ts -e tsx eslintrc
fi

# Look for lint scripts in package.json
if [ -f "package.json" ]; then
  echo "Checking for lint scripts..."
  cat package.json | jq '.scripts | to_entries[] | select(.key | contains("lint"))'
fi

# Check if Prettier is configured
if [ -f ".prettierrc" ] || [ -f ".prettierrc.js" ] || [ -f ".prettierrc.json" ] || [ -f "prettier.config.js" ]; then
  echo "Found Prettier config"
  fd -e json -e js prettierrc
fi

Length of output: 132


Add ESLint and Prettier configuration
No ESLint (.eslintrc.*) or Prettier (.prettierrc*, prettier.config.js) files found. Add or update your lint config to resolve the reported errors and rerun npm run lint.

πŸ€– Prompt for AI Agents
In apps/web/app/components/ui/drawer.tsx around lines 124 to 135: the repo is
missing ESLint and Prettier config files which is causing lint errors; add
project-level configuration files (e.g., .eslintrc.js or .eslintrc.json and
.prettierrc or prettier.config.js) that extend recommended presets
(eslint:recommended, plugin:react/recommended,
plugin:@typescript-eslint/recommended, and Prettier), set parser/options for
TypeScript and React (ecmaVersion, sourceType, ecmaFeatures.jsx), enable
relevant plugins (react, @typescript-eslint, import, prettier), and add or
update package.json scripts for lint and format (e.g., "lint": "eslint . --ext
.js,.jsx,.ts,.tsx" and "format": "prettier --write ."); commit these config
files and rerun npm run lint.

'use client'

import * as React from 'react'
import { GripVerticalIcon } from 'lucide-react'
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | πŸ”΄ Critical

Fix lucide icon name – GripVerticalIcon does not exist.

lucide-react exports GripVertical, not GripVerticalIcon. Keeping the Icon suffix breaks the import and any TypeScript/Bundler step. Swap both the import and JSX usage to GripVertical.

-import { GripVerticalIcon } from 'lucide-react'
+import { GripVertical } from 'lucide-react'
@@
-          <GripVerticalIcon className="size-2.5" />
+          <GripVertical className="size-2.5" />

Also applies to: 48-50

πŸ€– Prompt for AI Agents
In apps/web/app/components/ui/resizable.tsx around lines 4 and 48-50, the code
imports and uses a non-existent lucide-react export GripVerticalIcon; change the
import to import { GripVertical } from 'lucide-react' and update the JSX to use
<GripVertical /> instead of <GripVerticalIcon /> (apply both the import line and
the component usage changes).

Comment on lines +8 to +26
function Separator({
className,
orientation = 'horizontal',
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot="separator"
decorative={decorative}
orientation={orientation}
className={cn(
'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
className,
)}
{...props}
/>
)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

πŸ› οΈ Refactor suggestion | 🟠 Major

Add ref forwarding and displayName.

The Separator component should support ref forwarding for imperative access and proper React DevTools debugging.

Apply this diff:

-function Separator({
+const Separator = React.forwardRef<
+  React.ElementRef<typeof SeparatorPrimitive.Root>,
+  React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
+>(({
   className,
   orientation = 'horizontal',
   decorative = true,
   ...props
-}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
+}, ref) => {
   return (
     <SeparatorPrimitive.Root
       data-slot="separator"
+      ref={ref}
       decorative={decorative}
       orientation={orientation}
       className={cn(
         'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
         className,
       )}
       {...props}
     />
   )
-}
+})
+Separator.displayName = SeparatorPrimitive.Root.displayName
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function Separator({
className,
orientation = 'horizontal',
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot="separator"
decorative={decorative}
orientation={orientation}
className={cn(
'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
className,
)}
{...props}
/>
)
}
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(({
className,
orientation = 'horizontal',
decorative = true,
...props
}, ref) => {
return (
<SeparatorPrimitive.Root
data-slot="separator"
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
className,
)}
{...props}
/>
)
})
Separator.displayName = SeparatorPrimitive.Root.displayName
πŸ€– Prompt for AI Agents
In apps/web/app/components/ui/separator.tsx around lines 8 to 26, the Separator
component needs ref forwarding and a displayName for DevTools; convert the
function to a forwardRef component (use React.forwardRef) with an appropriate
generic prop/ref type (e.g. React.ComponentPropsWithRef<typeof
SeparatorPrimitive.Root> or forwardRef generics matching
SeparatorPrimitive.Root), accept (props, ref) and pass ref into
<SeparatorPrimitive.Root ref={ref} ... />, and then set Separator.displayName =
'Separator' after the forwardRef assignment.

Comment on lines +74 to +78
{children}
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
<XIcon className="size-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use supported Tailwind tokens for close button styles

rounded-xs and focus:outline-hidden aren’t defined, so Tailwind fails. Replace them with the nearest built-ins (rounded-sm, focus:outline-none) while keeping the focus ring.

-        <SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
+        <SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:pointer-events-none">
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{children}
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
<XIcon className="size-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
{children}
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:pointer-events-none">
<XIcon className="size-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
πŸ€– Prompt for AI Agents
In apps/web/app/components/ui/sheet.tsx around lines 74 to 78, the Close button
uses unsupported Tailwind tokens `rounded-xs` and `focus:outline-hidden`;
replace `rounded-xs` with the nearest supported `rounded-sm` and replace
`focus:outline-hidden` with `focus:outline-none` while preserving the existing
focus ring classes (e.g., keep `focus:ring-2 focus:ring-offset-2`) so visual
focus styling remains intact.

Comment on lines +5 to +19
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)

React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
}
mql.addEventListener('change', onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
return () => mql.removeEventListener('change', onChange)
}, [])

return !!isMobile
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Consider avoiding hydration mismatch.

The hook initializes isMobile to undefined but returns !!isMobile (coercing undefined to false). During SSR and the initial client render before the effect runs, this returns false, which may not match the actual mobile status once the effect executes. This can cause a hydration mismatch warning and a visible content shift if the user is actually on mobile.

A more SSR-friendly pattern would be to return undefined on the initial render and let consuming components handle the undefined state gracefully, or use a different approach that avoids the initial false value.

Consider one of these approaches:

Option 1: Return undefined initially and update the return type

-export function useIsMobile() {
+export function useIsMobile(): boolean | undefined {
   const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)

   React.useEffect(() => {
     const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
     const onChange = () => {
       setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
     }
     mql.addEventListener('change', onChange)
     setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
     return () => mql.removeEventListener('change', onChange)
   }, [])

-  return !!isMobile
+  return isMobile
 }

Then consuming components can check for undefined and render a loading state or default content.

Option 2: Use mql.matches for cleaner initial value detection

 export function useIsMobile() {
   const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)

   React.useEffect(() => {
     const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
     const onChange = () => {
-      setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
+      setIsMobile(mql.matches)
     }
     mql.addEventListener('change', onChange)
-    setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
+    setIsMobile(mql.matches)
     return () => mql.removeEventListener('change', onChange)
   }, [])

   return !!isMobile
 }

This makes the logic more consistent by using the media query's native matches property throughout.

πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
}
mql.addEventListener('change', onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
return () => mql.removeEventListener('change', onChange)
}, [])
return !!isMobile
}
export function useIsMobile(): boolean | undefined {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
}
mql.addEventListener('change', onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
return () => mql.removeEventListener('change', onChange)
}, [])
return isMobile
}
πŸ€– Prompt for AI Agents
In apps/web/app/components/ui/use-mobile.tsx around lines 5 to 19, the hook
currently initializes isMobile as undefined but returns !!isMobile causing
SSR/client hydration mismatch; change the hook to either (A) initialize and
return the state as boolean | undefined (do not coerce with !!) so initial
undefined is preserved and update the return type, letting consumers handle
undefined, or (B) use the media query object for the initial value by creating
the mql inside the effect (or before setting state) and initialize isMobile from
mql.matches so the first client render reflects the real match; also ensure
addEventListener/removeEventListener usage remains and that the hook returns the
state directly rather than coercing it.

Comment on lines +8 to +70
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000

type ToasterToast = ToastProps & {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}

const actionTypes = {
ADD_TOAST: 'ADD_TOAST',
UPDATE_TOAST: 'UPDATE_TOAST',
DISMISS_TOAST: 'DISMISS_TOAST',
REMOVE_TOAST: 'REMOVE_TOAST',
} as const

let count = 0

function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER
return count.toString()
}

type ActionType = typeof actionTypes

type Action =
| {
type: ActionType['ADD_TOAST']
toast: ToasterToast
}
| {
type: ActionType['UPDATE_TOAST']
toast: Partial<ToasterToast>
}
| {
type: ActionType['DISMISS_TOAST']
toastId?: ToasterToast['id']
}
| {
type: ActionType['REMOVE_TOAST']
toastId?: ToasterToast['id']
}

interface State {
toasts: ToasterToast[]
}

const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()

const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return
}

const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({
type: 'REMOVE_TOAST',
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Restore sane toast removal delay.

TOAST_REMOVE_DELAY is set to 1,000,000β€―ms (~16β€―min), so dismissed toasts linger in memory/DOM far longer than intended and block timely cleanup. This regresses UX and can leak timers. Drop it back to 1,000β€―ms (or similar short duration) so toasts are removed promptly after closing.

-const TOAST_REMOVE_DELAY = 1000000
+const TOAST_REMOVE_DELAY = 1000
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
type ToasterToast = ToastProps & {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}
const actionTypes = {
ADD_TOAST: 'ADD_TOAST',
UPDATE_TOAST: 'UPDATE_TOAST',
DISMISS_TOAST: 'DISMISS_TOAST',
REMOVE_TOAST: 'REMOVE_TOAST',
} as const
let count = 0
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER
return count.toString()
}
type ActionType = typeof actionTypes
type Action =
| {
type: ActionType['ADD_TOAST']
toast: ToasterToast
}
| {
type: ActionType['UPDATE_TOAST']
toast: Partial<ToasterToast>
}
| {
type: ActionType['DISMISS_TOAST']
toastId?: ToasterToast['id']
}
| {
type: ActionType['REMOVE_TOAST']
toastId?: ToasterToast['id']
}
interface State {
toasts: ToasterToast[]
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({
type: 'REMOVE_TOAST',
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000
πŸ€– Prompt for AI Agents
In apps/web/app/components/ui/use-toast.ts around lines 8 to 70,
TOAST_REMOVE_DELAY is incorrectly set to 1000000 (β‰ˆ16 minutes) causing dismissed
toasts to linger; change the constant to a short duration (e.g. 1000 ms) so the
setTimeout removes the toast promptly, preventing long-lived timers and
DOM/memory leaks, and ensure any related documentation/comments reflect the new
timeout.

Comment on lines +325 to +328
<Button size="lg" className="h-12 px-8" onClick={() => router.push("/register")}>
Get Started Free
<ArrowLeft className="ml-2 h-4 w-4 rotate-180" />
</Button>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | πŸ”΄ Critical

Fix broken sign‑up navigation

These CTAs push users to /register, but the rest of the app (and this page earlier) uses /signup. Unless you add a /register route, these buttons 404.

-              <Button size="lg" className="h-12 px-8" onClick={() => router.push("/register")}>
+              <Button size="lg" className="h-12 px-8" onClick={() => router.push("/signup")}>
@@
-                  <button
-                    onClick={() => router.push("/register")}
+                  <button
+                    onClick={() => router.push("/signup")}

Also applies to: 365-368

πŸ€– Prompt for AI Agents
In apps/web/app/features/page.tsx around lines 325-328 and 365-368 the CTA
buttons call router.push("/register") which 404s because the app uses "/signup";
update those onClick handlers to router.push("/signup") (and search for any
other "/register" CTA occurrences on this page to change to "/signup") so the
buttons navigate to the correct sign-up route.

Comment on lines +1 to +26
"use client"

import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/lib/utils"

const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)

const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName

export { Label }
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | πŸ”΄ Critical

🧩 Analysis chain

Fix: File path inconsistency.

This file is located at apps/web/components/ui/label.tsx while all other UI components in this PR are at apps/web/app/components/ui/. This inconsistency could break imports and the module resolution strategy.

Verify:

  1. Is this file in the correct location?
  2. Are imports elsewhere using the correct path?

If this file should be in apps/web/app/components/ui/, move it there and update any imports.


🏁 Script executed:

#!/bin/bash
# Check for imports of this Label component
rg -n "from ['\"]@/.*label['\"]" --type=ts --type=tsx -C2
rg -n "from ['\"].*components/ui/label['\"]" --type=ts --type=tsx -C2

Length of output: 55


🏁 Script executed:

#!/bin/bash
echo "UI components in apps/web/app/components/ui:"
find apps/web/app/components/ui -type f \( -name '*.ts' -o -name '*.tsx' \)
echo ""
echo "Import references to Label component:"
rg -n "from ['\"][^'\"]*components/ui/label['\"]" -g "*.ts" -g "*.tsx" -C2
rg -n "@/components/ui/label" -g "*.ts" -g "*.tsx" -C2

Length of output: 4428


Remove or relocate duplicate Label component file
apps/web/components/ui/label.tsx duplicates the component at apps/web/app/components/ui/label.tsx and is never imported; delete it or merge its contents into the correct path and update imports.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 13

🧹 Nitpick comments (12)
apps/web/app/(auth)/signin/page.tsx (1)

11-11: Consider aliasing the lucide-react Code icon for clarity.

The Code import from lucide-react is used correctly as an icon, but a Code component also exists in packages/ui/src/code.tsx for wrapping text. While not a functional issue, aliasing the icon (e.g., import { Code as CodeIcon }) could prevent future confusion when both components are needed in the same file.

apps/web/components/sidebar.tsx (2)

33-46: Consider making navigation items configurable via props.

The hardcoded navigation arrays reduce component reusability. Consider accepting navItems and bottomNavItems as optional props with these as defaults, allowing different navigation structures across different parts of the application.

Apply this diff to make the component more flexible:

 interface SidebarProps {
   className?: string;
+  navItems?: NavItem[];
+  bottomNavItems?: NavItem[];
 }

-const navItems: NavItem[] = [
+const defaultNavItems: NavItem[] = [
   { title: "Dashboard", href: "/dashboard", icon: Home },
   { title: "Projects", href: "/projects", icon: Layers },
   { title: "Code Editor", href: "/editor", icon: Code },
   { title: "Terminal", href: "/terminal", icon: Terminal },
   { title: "Files", href: "/files", icon: FileCode },
   { title: "Activity", href: "/activity", icon: Activity },
   { title: "Deploy", href: "/deploy", icon: Zap, badge: "New" },
 ];

-const bottomNavItems: NavItem[] = [
+const defaultBottomNavItems: NavItem[] = [
   { title: "Profile", href: "/profile", icon: User },
   { title: "Settings", href: "/settings", icon: Settings },
 ];

-export function Sidebar({ className }: SidebarProps) {
+export function Sidebar({ 
+  className,
+  navItems = defaultNavItems,
+  bottomNavItems = defaultBottomNavItems
+}: SidebarProps) {

131-156: Consider extracting navigation link rendering logic.

The bottom navigation duplicates the rendering logic from the main navigation (lines 92-126). Consider extracting a reusable NavLink component to reduce duplication and improve maintainability.

apps/web/components/icons.tsx (2)

3-21: Consider using a className utility function.

The string concatenation pattern for building className is functional but verbose. Using a utility like clsx or a custom cn function would improve readability and handle edge cases better (e.g., undefined values, conditional classes).

Example refactor:

+import { cn } from "@/lib/utils"; // or clsx
+
 export function IconWrapper({
   children,
   className,
   glow = false,
 }: {
   children: React.ReactNode;
   className?: string;
   glow?: boolean;
 }) {
   return (
     <div
-      className={`inline-flex items-center justify-center ${
-        glow ? "glow-primary" : ""
-      } ${className || ""}`}
+      className={cn(
+        "inline-flex items-center justify-center",
+        glow && "glow-primary",
+        className
+      )}
     >
       {children}
     </div>
   );
 }

48-62: Optional: Simplify animation class selection.

The nested ternary for selecting animation classes is functional but could be more maintainable using an object lookup pattern, making it easier to add new animation types in the future.

+const ANIMATION_CLASSES = {
+  pulse: "animate-pulse",
+  spin: "animate-spin",
+  bounce: "animate-bounce",
+} as const;
+
 export function AnimatedIcon({
   Icon,
   animation = "pulse",
   className,
   ...props
 }: AnimatedIconProps) {
-  const animationClass =
-    animation === "pulse"
-      ? "animate-pulse"
-      : animation === "spin"
-      ? "animate-spin"
-      : "animate-bounce";
+  const animationClass = ANIMATION_CLASSES[animation];

   return <Icon className={`${animationClass} ${className || ""}`} {...props} />;
 }
apps/web/app/(auth)/signup/page.tsx (3)

35-39: Consider strengthening password requirements.

The current validation only checks for a minimum length of 8 characters. While this meets basic requirements, modern security best practices recommend additional complexity rules such as requiring a mix of uppercase, lowercase, numbers, and special characters. You might also consider adding a password strength indicator to improve user experience.

Example enhancement:

     if (password.length < 8) {
       setError("Password must be at least 8 characters long");
       setIsLoading(false);
       return;
     }
+
+    // Check password complexity
+    const hasUpperCase = /[A-Z]/.test(password);
+    const hasLowerCase = /[a-z]/.test(password);
+    const hasNumber = /\d/.test(password);
+    const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password);
+    
+    if (!hasUpperCase || !hasLowerCase || !hasNumber || !hasSpecialChar) {
+      setError("Password must contain uppercase, lowercase, number, and special character");
+      setIsLoading(false);
+      return;
+    }

94-97: Prefer Link component over Button with onClick for navigation.

Using a Button with onClick={() => router.push("/")} works but sacrifices accessibility and SEO benefits. A Link component styled as a button provides better semantics, keyboard navigation, and allows users to right-click to open in a new tab.

-            <Button variant="ghost" size="sm" onClick={() => router.push("/")}>
+            <Link href="/">
+              <Button variant="ghost" size="sm" asChild>
               <ArrowLeft className="mr-2 h-4 w-4" />
               Back to Home
-            </Button>
+              </Button>
+            </Link>

Note: Check if your Button component supports the asChild pattern (from Radix UI). If not, you can use buttonVariants helper:

-            <Button variant="ghost" size="sm" onClick={() => router.push("/")}>
-              <ArrowLeft className="mr-2 h-4 w-4" />
-              Back to Home
-            </Button>
+            <Link 
+              href="/" 
+              className={buttonVariants({ variant: "ghost", size: "sm" })}
+            >
+              <ArrowLeft className="mr-2 h-4 w-4" />
+              Back to Home
+            </Link>

235-273: Consider extracting OAuth buttons into a reusable component.

The GitHub and Google sign-in buttons share identical structure and only differ in their icon SVGs and provider names. Additionally, the inline SVGs make the JSX harder to read. Extracting these into a reusable OAuthButton component would improve maintainability and reduce duplication, especially if these buttons are used on other auth pages (like sign-in).

Create a new component OAuthButton:

// components/auth/oauth-button.tsx
import { Button } from "@/components/ui/button";
import { signIn } from "next-auth/react";

interface OAuthButtonProps {
  provider: "github" | "google";
  disabled?: boolean;
  callbackUrl?: string;
}

const providerConfig = {
  github: {
    name: "GitHub",
    icon: (
      <svg className="mr-2 h-4 w-4" fill="currentColor" viewBox="0 0 24 24">
        <path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
      </svg>
    ),
  },
  google: {
    name: "Google",
    icon: (
      <svg className="mr-2 h-4 w-4" viewBox="0 0 24 24">
        <path fill="currentColor" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" />
        <path fill="currentColor" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" />
        <path fill="currentColor" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" />
        <path fill="currentColor" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" />
      </svg>
    ),
  },
};

export function OAuthButton({ provider, disabled, callbackUrl = "/dashboard" }: OAuthButtonProps) {
  const config = providerConfig[provider];
  
  return (
    <Button
      variant="outline"
      onClick={() => signIn(provider, { callbackUrl })}
      disabled={disabled}
      className="border-border hover:bg-muted hover:glow-secondary"
    >
      {config.icon}
      {config.name}
    </Button>
  );
}

Then simplify the signup page:

+              import { OAuthButton } from "@/components/auth/oauth-button";
+
               <div className="grid grid-cols-2 gap-4">
-                <Button
-                  variant="outline"
-                  onClick={() => signIn("github", { callbackUrl: "/dashboard" })}
-                  disabled={isLoading}
-                  className="border-border hover:bg-muted hover:glow-secondary"
-                >
-                  <svg className="mr-2 h-4 w-4" fill="currentColor" viewBox="0 0 24 24">
-                    <path d="..." />
-                  </svg>
-                  GitHub
-                </Button>
-                <Button
-                  variant="outline"
-                  onClick={() => signIn("google", { callbackUrl: "/dashboard" })}
-                  disabled={isLoading}
-                  className="border-border hover:bg-muted hover:glow-secondary"
-                >
-                  <svg className="mr-2 h-4 w-4" viewBox="0 0 24 24">
-                    <path fill="currentColor" d="..." />
-                    ...
-                  </svg>
-                  Google
-                </Button>
+                <OAuthButton provider="github" disabled={isLoading} />
+                <OAuthButton provider="google" disabled={isLoading} />
               </div>
apps/web/app/globals.css (1)

65-84: Consider refactoring repetitive glow utilities to reduce duplication.

The glow utilities repeat identical box-shadow/text-shadow patterns with only variable names changing. If your project uses SCSS or Tailwind plugins, this could be abstracted into a mixin or component directive.

If using vanilla CSS only, this duplication is acceptable but worth revisiting if you add more color variants in the future.

Example refactor with SCSS (if available):

@mixin glow-effect($color-var) {
  box-shadow: 
    0 0 20px hsla(var($color-var), var(--glow-intensity)),
    0 0 40px hsla(var($color-var), calc(var(--glow-intensity) * 0.6)),
    0 0 60px hsla(var($color-var), calc(var(--glow-intensity) * 0.3));
}

.glow-primary { @include glow-effect('--glow-primary'); }
.glow-secondary { @include glow-effect('--glow-secondary'); }
.glow-accent { @include glow-effect('--glow-accent'); }

Also applies to: 86-105

apps/web/app/dashboard/page.tsx (3)

39-44: Improve loader accessibility.

Expose progress to AT with role, aria-busy, and live-region.

Apply:

-      <div className="min-h-screen flex items-center justify-center bg-background">
-        <div className="flex flex-col items-center gap-4">
+      <div className="min-h-screen flex items-center justify-center bg-background" role="status" aria-busy="true" aria-live="polite">
+        <div className="flex flex-col items-center gap-4">
           <Loader2 className="h-8 w-8 animate-spin text-primary" />
           <div className="text-lg text-muted-foreground">Loading your workspace...</div>
         </div>
       </div>

52-103: Static data inside render; unused field.

Both arrays are recreated each render; also stats.color is unused. Memoize or hoist constants; remove or use color for styling.

Example:

-import { useEffect, useState } from "react";
+import { useEffect, useMemo, useState } from "react";
@@
-  const recentProjects = [
+  const recentProjects = useMemo(() => [
     { /* … */ },
     { /* … */ },
     { /* … */ },
-  ];
+  ], []);
@@
-  const stats = [
+  const stats = useMemo(() => [
     { title: "Total Projects", value: "12", icon: Code, trend: "+2 this week" },
     { title: "Active Sessions", value: "3", icon: Activity, trend: "2 running now" },
     { title: "Deployments", value: "24", icon: Zap, trend: "+6 this month" },
     { title: "Build Time", value: "2.3s", icon: Clock, trend: "25% faster" },
-  ];
+  ], []);

Or leverage color to set icon/badge classes if kept.


30-35: Client-only auth redirect can cause blank flash.

Prefer server-side protection (middleware or server component redirect with getServerSession) to avoid FOUC and double work.

Briefly: move redirect to a server layout/page using next-auth’s getServerSession and return redirect("/signin") when unauthenticated.

πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between c578d06 and 2db4631.

πŸ“’ Files selected for processing (8)
  • apps/web/app/(auth)/signin/page.tsx (2 hunks)
  • apps/web/app/(auth)/signup/page.tsx (3 hunks)
  • apps/web/app/dashboard/page.tsx (1 hunks)
  • apps/web/app/globals.css (1 hunks)
  • apps/web/app/page.tsx (1 hunks)
  • apps/web/components/glow-menu.tsx (1 hunks)
  • apps/web/components/icons.tsx (1 hunks)
  • apps/web/components/sidebar.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
apps/web/app/page.tsx (4)
packages/ui/src/code.tsx (1)
  • Code (3-11)
apps/web/components/glow-menu.tsx (1)
  • GlowMenu (19-83)
packages/ui/src/button.tsx (1)
  • Button (11-20)
packages/ui/src/card.tsx (1)
  • Card (3-27)
apps/web/app/(auth)/signup/page.tsx (2)
packages/ui/src/code.tsx (1)
  • Code (3-11)
apps/web/lib/auth-config.ts (1)
  • signIn (103-119)
apps/web/app/(auth)/signin/page.tsx (2)
packages/ui/src/code.tsx (1)
  • Code (3-11)
apps/web/lib/auth-config.ts (1)
  • signIn (103-119)
apps/web/app/dashboard/page.tsx (2)
packages/ui/src/code.tsx (1)
  • Code (3-11)
apps/web/components/sidebar.tsx (1)
  • Sidebar (48-177)
πŸ”‡ Additional comments (12)
apps/web/components/sidebar.tsx (1)

1-177: Now searching the codebase to verify if the mentioned features (SidebarContext, useSidebar, etc.) actually exist or are being used:

Now I need to search the actual codebase to determine if it's using the shadcn/ui sidebar library or a custom implementation:

Based on my verification, I need to make a determination with the available information:


The review comment conflates different implementation approaches without evidence from the codebase.

The review compares the actual code (a simple custom Sidebar component) to an unspecified "AI summary" that apparently describes shadcn/ui's SidebarProvider, useSidebar hook, and context-based architecture. However, the provided code is a functional custom implementation with no claims about these features.

The actual sidebar.tsx shows:

  • Local useState for collapse stateβ€”appropriate for this scope
  • No mobile responsiveness requirement declared in the code itself
  • Fixed positioning (common for admin dashboards)

Two issues need resolution:

  1. Verify the "AI summary" reference: Is this comparing against a separate project spec, auto-generated documentation, or a misattribution?
  2. Mobile responsiveness: While not implemented, confirm whether mobile support is an actual requirement by checking the design specs or layout architecture.

Without access to the referenced "AI summary" or project requirements, the comment cannot be validated as either correct or incorrect.

apps/web/app/(auth)/signup/page.tsx (1)

133-224: LGTM! Well-structured and accessible form.

The form implementation follows accessibility best practices:

  • Proper label associations (htmlFor/id matching)
  • Required field validation
  • Autocomplete attributes for better UX
  • Visual loading states
  • Icon placement for visual enhancement
  • Disabled states during submission

The form provides a good user experience with clear feedback and proper error handling.

apps/web/components/icons.tsx (1)

23-40: ****

The concern about Tailwind CSS purge/JIT not detecting dynamic classes does not apply here. The .glow-primary, .glow-secondary, and .glow-accent classes are custom CSS classes explicitly defined in apps/web/app/globals.css (lines 65-83), not Tailwind utilities. Custom CSS classes in global stylesheets are always available regardless of how they're referenced in code. The dynamic class name generation in GlowIcon using glow-${glowColor} is safe and consistent with how these classes are used throughout the codebase.

Likely an incorrect or invalid review comment.

apps/web/components/glow-menu.tsx (1)

90-126: LGTM!

The FloatingGlowMenu component is well-implemented with proper hover state management and tooltip positioning.

apps/web/app/page.tsx (5)

74-81: Both grid-background and pulse-glow classes are defined in apps/web/app/globals.css (lines 107 and 176 respectively). The background effects will render correctly as designed.

Likely an incorrect or invalid review comment.


78-80: pulse-glow class is properly defined.

The pulse-glow class is defined in apps/web/app/globals.css (lines 167-177) as a keyframe animation with the corresponding class rule. The elements will render with the pulse effect as intended.


126-149: All custom glow utility classes are properly defined.

Verification confirms that text-glow-primary, .glow-primary, and .glow-secondary are all implemented in apps/web/app/globals.css (lines 44–97). The hover variants will work correctly since the base classes exist. No issues found.


181-195: The hover-lift class is already properly defined.

The hover-lift class is defined in apps/web/app/globals.css (lines 159-163) and implements the intended lift effect correctly. No changes are needed.

Likely an incorrect or invalid review comment.


88-91: The glow utility classes are properly definedβ€”no changes needed for that concern.

The glow-primary and text-glow-primary classes exist in apps/web/app/globals.css (lines 65-69 and 86-90) and are used consistently throughout the codebase. They adapt via CSS variables.

The text-black on line 89 appears intentional for contrast on gradient backgrounds. If dark mode support requires adjustment, consider using a theme-aware class like text-primary-foreground, but this may not be necessary given the gradient context.

Likely an incorrect or invalid review comment.

apps/web/app/globals.css (2)

22-62: Token-based CSS variable structure is well-organized and enables flexible theming.

The @layer base approach with comprehensive color tokens and design variables is a solid foundation for a scalable UI system. The HSL variable format is also a good choice for runtime theme switching.


1-178: Verify lint errors mentioned in PR comment are resolved with these changes.

The PR includes a comment requesting lint error fixes. This CSS file may have triggered some of those errors (e.g., aggressive global selectors, animation structure, or property ordering). Ensure linting passes after addressing the accessibility and styling concerns above.

Please run your linter (e.g., stylelint) to confirm all warnings/errors are resolved:

apps/web/app/dashboard/page.tsx (1)

104-255: Solid overall structure and theming.

Consistent UI primitives, clear loading guard, and tidy composition with Sidebar.

Comment on lines 114 to 116
<main className="ml-64 min-h-screen transition-all duration-300">
<div className="container mx-auto px-8 py-8">
<div className="flex items-center justify-between mb-8">
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟑 Minor

Sidebar collapse de-sync: static ml-64.

When Sidebar collapses to w-20, content still reserves 16rem. Make margin track the actual sidebar width (CSS var or context).

Example approach:

-      <main className="ml-64 min-h-screen transition-all duration-300">
+      <main className="min-h-screen transition-all duration-300" style={{ marginLeft: "var(--sidebar-width, 16rem)" }}>

Then set --sidebar-width in <Sidebar …> via inline style on its root for both states (e.g., 16rem/5rem).

πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<main className="ml-64 min-h-screen transition-all duration-300">
<div className="container mx-auto px-8 py-8">
<div className="flex items-center justify-between mb-8">
<main className="min-h-screen transition-all duration-300" style={{ marginLeft: "var(--sidebar-width, 16rem)" }}>
<div className="container mx-auto px-8 py-8">
<div className="flex items-center justify-between mb-8">
πŸ€– Prompt for AI Agents
In apps/web/app/dashboard/page.tsx around lines 114 to 116 the layout uses a
static class ml-64 which keeps 16rem left margin even when the Sidebar collapses
to w-20; change the layout to use a CSS variable for the sidebar width and have
the Sidebar component set that variable inline on its root (e.g.,
--sidebar-width: 16rem or --sidebar-width: 5rem depending on collapsed state),
then replace ml-64 with a dynamic margin that reads var(--sidebar-width) (or
apply style={{ marginLeft: 'var(--sidebar-width)' }} to <main>), and keep the
existing transition classes so the content animates in sync with the sidebar.

Comment on lines 176 to 215
{recentProjects.map((project, index) => (
<div
key={index}
className="flex items-center justify-between p-4 rounded-lg border border-border bg-card hover:bg-muted/50 transition-all cursor-pointer hover-lift"
>
<div className="flex items-center gap-4">
<div className="p-3 rounded-lg bg-primary/10">
<Code className="h-5 w-5 text-primary" />
</div>
<div>
<h3 className="font-semibold text-foreground">{project.name}</h3>
<p className="text-sm text-muted-foreground">{project.language}</p>
</div>
</div>
<div className="flex items-center gap-4">
<div className="text-right">
<p className="text-sm text-muted-foreground">{project.lastModified}</p>
</div>
<Badge
variant={
project.status === "active"
? "default"
: project.status === "deployed"
? "secondary"
: "outline"
}
className={
project.status === "active"
? "bg-accent/20 text-accent border-accent/50"
: ""
}
>
{project.status}
</Badge>
<Button variant="ghost" size="sm">
<Terminal className="h-4 w-4" />
</Button>
</div>
</div>
))}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Make rows semantic links instead of pointer-styled divs.

Current rows look clickable but aren’t keyboard-navigable. Wrap with Next Link and use a stable key.

Apply:

+import Link from "next/link";
@@
-                {recentProjects.map((project, index) => (
-                  <div
-                    key={index}
-                    className="flex items-center justify-between p-4 rounded-lg border border-border bg-card hover:bg-muted/50 transition-all cursor-pointer hover-lift"
-                  >
+                {recentProjects.map((project) => (
+                  <Link
+                    key={project.name}
+                    href={`/projects/${encodeURIComponent(project.name)}`}
+                    className="flex items-center justify-between p-4 rounded-lg border border-border bg-card hover:bg-muted/50 transition-all hover-lift focus:outline-none focus-visible:ring-2 focus-visible:ring-primary"
+                    aria-label={`Open project ${project.name}`}
+                  >
@@
-                  </div>
+                  </Link>
                 ))}
πŸ€– Prompt for AI Agents
In apps/web/app/dashboard/page.tsx around lines 176 to 215, each recentProjects
row is a pointer-styled div that looks clickable but isn't keyboard-accessible
and uses an unstable key; wrap the row content in Next.js Link (href built from
project id/slug) so the entire row becomes a semantic, keyboard-navigable link,
move the className and interactive styles onto the Link (remove misleading
cursor-only styling on the inner div), and replace key={index} with a stable key
such as key={project.id} (or project.slug) to avoid React list key issues.

Comment on lines 210 to 212
<Button variant="ghost" size="sm">
<Terminal className="h-4 w-4" />
</Button>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Icon-only button needs an accessible name.

Add aria-label/title so screen readers can announce the action.

Apply:

-                      <Button variant="ghost" size="sm">
-                        <Terminal className="h-4 w-4" />
-                      </Button>
+                      <Button variant="ghost" size="sm" aria-label="Open terminal" title="Open terminal">
+                        <Terminal className="h-4 w-4" aria-hidden="true" />
+                      </Button>
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Button variant="ghost" size="sm">
<Terminal className="h-4 w-4" />
</Button>
<Button variant="ghost" size="sm" aria-label="Open terminal" title="Open terminal">
<Terminal className="h-4 w-4" aria-hidden="true" />
</Button>
πŸ€– Prompt for AI Agents
In apps/web/app/dashboard/page.tsx around lines 210 to 212, the icon-only Button
lacks an accessible name; add an aria-label (and optional title) to the Button
component (for example aria-label="Open terminal" or aria-label describing the
action) so screen readers can announce its purpose, ensuring the label is
concise and matches the button's behavior; keep the visual appearance unchanged
and avoid adding visible text.

Comment on lines +228 to +232
<div className="flex items-center space-x-2">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-primary to-secondary">
<Code className="h-5 w-5 text-black" />
</div>
<span className="text-lg font-bold">Dev8.dev</span>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟑 Minor

Hardcoded text-black won't work in dark mode.

Line 230 uses text-black, which will be invisible on dark backgrounds. Use a theme-aware color instead.

-                <Code className="h-5 w-5 text-black" />
+                <Code className="h-5 w-5 text-primary-foreground" />
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className="flex items-center space-x-2">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-primary to-secondary">
<Code className="h-5 w-5 text-black" />
</div>
<span className="text-lg font-bold">Dev8.dev</span>
<div className="flex items-center space-x-2">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-primary to-secondary">
<Code className="h-5 w-5 text-primary-foreground" />
</div>
<span className="text-lg font-bold">Dev8.dev</span>
πŸ€– Prompt for AI Agents
In apps/web/app/page.tsx around lines 228 to 232 the Code icon uses the
hardcoded class "text-black" which will be invisible in dark mode; replace this
with a theme-aware utility such as "text-neutral-900 dark:text-neutral-100" (or
"text-black dark:text-white") so the icon color adapts to light/dark themes,
updating the className on the Code component accordingly.

Comment on lines +19 to +83
export function GlowMenu({ items, className, orientation = "horizontal" }: GlowMenuProps) {
const [activeIndex, setActiveIndex] = useState(0);
const [hoverIndex, setHoverIndex] = useState<number | null>(null);

return (
<nav
className={cn(
"relative flex rounded-lg border border-border bg-card/50 backdrop-blur-sm p-1",
orientation === "vertical" ? "flex-col" : "flex-row",
className
)}
>
{items.map((item, index) => {
const isActive = activeIndex === index;
const isHovered = hoverIndex === index;

return (
<Link
key={item.href}
href={item.href}
onClick={() => setActiveIndex(index)}
onMouseEnter={() => setHoverIndex(index)}
onMouseLeave={() => setHoverIndex(null)}
className={cn(
"relative z-10 flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium transition-all duration-300",
orientation === "vertical" ? "w-full" : "flex-1 justify-center",
isActive
? "text-primary"
: isHovered
? "text-foreground"
: "text-muted-foreground"
)}
>
{/* Glow Effect */}
{(isActive || isHovered) && (
<div
className={cn(
"absolute inset-0 -z-10 rounded-md transition-all duration-300",
isActive
? "bg-primary/20 glow-primary"
: "bg-muted"
)}
/>
)}

{item.icon && <span className="flex-shrink-0">{item.icon}</span>}
<span>{item.label}</span>

{/* Active Indicator */}
{isActive && (
<div
className={cn(
"absolute bg-primary",
orientation === "vertical"
? "left-0 top-1/2 h-8 w-1 -translate-y-1/2 rounded-r-full"
: "bottom-0 left-1/2 h-1 w-8 -translate-x-1/2 rounded-t-full"
)}
/>
)}
</Link>
);
})}
</nav>
);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Active state doesn't sync with the current route.

The activeIndex state is updated only on click and defaults to 0, so the active indicator always highlights the first item after a page refresh or direct navigation. This creates a mismatch between the visual indicator and the actual route.

Use Next.js usePathname to sync the active state with the current route:

+"use client";
+
 import { useState } from "react";
 import Link from "next/link";
+import { usePathname } from "next/navigation";
 import { cn } from "@/lib/utils";

 export function GlowMenu({ items, className, orientation = "horizontal" }: GlowMenuProps) {
-  const [activeIndex, setActiveIndex] = useState(0);
+  const pathname = usePathname();
   const [hoverIndex, setHoverIndex] = useState<number | null>(null);

   return (
     <nav
       className={cn(
         "relative flex rounded-lg border border-border bg-card/50 backdrop-blur-sm p-1",
         orientation === "vertical" ? "flex-col" : "flex-row",
         className
       )}
     >
       {items.map((item, index) => {
-        const isActive = activeIndex === index;
+        const isActive = pathname === item.href || pathname.startsWith(item.href + '/');
         const isHovered = hoverIndex === index;

         return (
           <Link
             key={item.href}
             href={item.href}
-            onClick={() => setActiveIndex(index)}
             onMouseEnter={() => setHoverIndex(index)}
             onMouseLeave={() => setHoverIndex(null)}

Committable suggestion skipped: line range outside the PR's diff.

πŸ€– Prompt for AI Agents
In apps/web/components/glow-menu.tsx around lines 19 to 83, the activeIndex
state is only set on click and defaults to 0 so the highlighted item doesn’t
reflect the current route after refresh or direct navigation; instead, import
and call Next.js usePathname(), then derive the active index from items by
matching item.href to the pathname (or pathname startsWith item.href for nested
routes) and update activeIndex in a useEffect that runs when pathname or items
change; ensure you handle cases where no item matches (set to -1 or null) and
keep click behavior to update activeIndex for immediate feedback.

Comment on lines +79 to +88
<button
onClick={() => setCollapsed(!collapsed)}
className="absolute -right-3 top-20 z-50 flex h-6 w-6 items-center justify-center rounded-full border border-border bg-card text-muted-foreground hover:text-foreground hover:glow-primary transition-all"
>
{collapsed ? (
<ChevronRight className="h-4 w-4" />
) : (
<ChevronLeft className="h-4 w-4" />
)}
</button>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add accessibility attributes to the toggle button.

The toggle button is missing aria-label and aria-expanded attributes, which are essential for screen reader users to understand the button's purpose and current state.

Apply this diff to improve accessibility:

 <button
   onClick={() => setCollapsed(!collapsed)}
+  aria-label={collapsed ? "Expand sidebar" : "Collapse sidebar"}
+  aria-expanded={!collapsed}
   className="absolute -right-3 top-20 z-50 flex h-6 w-6 items-center justify-center rounded-full border border-border bg-card text-muted-foreground hover:text-foreground hover:glow-primary transition-all"
 >
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button
onClick={() => setCollapsed(!collapsed)}
className="absolute -right-3 top-20 z-50 flex h-6 w-6 items-center justify-center rounded-full border border-border bg-card text-muted-foreground hover:text-foreground hover:glow-primary transition-all"
>
{collapsed ? (
<ChevronRight className="h-4 w-4" />
) : (
<ChevronLeft className="h-4 w-4" />
)}
</button>
<button
onClick={() => setCollapsed(!collapsed)}
aria-label={collapsed ? "Expand sidebar" : "Collapse sidebar"}
aria-expanded={!collapsed}
className="absolute -right-3 top-20 z-50 flex h-6 w-6 items-center justify-center rounded-full border border-border bg-card text-muted-foreground hover:text-foreground hover:glow-primary transition-all"
>
{collapsed ? (
<ChevronRight className="h-4 w-4" />
) : (
<ChevronLeft className="h-4 w-4" />
)}
</button>
πŸ€– Prompt for AI Agents
In apps/web/components/sidebar.tsx around lines 79 to 88, the sidebar toggle
button lacks accessibility attributes; add an aria-label and aria-expanded to
the button element β€” use a dynamic label like "Expand sidebar" when collapsed is
true and "Collapse sidebar" when collapsed is false, and set aria-expanded to
the logical inverse of collapsed (aria-expanded={ !collapsed }) so assistive
tech knows the current state.

Comment on lines +119 to +123
{collapsed && (
<span className="absolute left-full ml-6 hidden w-auto min-w-max rounded-md bg-popover px-2 py-1 text-xs text-popover-foreground group-hover:block border border-border shadow-lg">
{item.title}
</span>
)}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Tooltip implementation is not keyboard-accessible.

The tooltip uses hidden and group-hover:block, which only shows on mouse hover and won't be accessible to keyboard users. Consider using a proper tooltip component or adding focus state handling.

Example fix to support keyboard navigation:

-{collapsed && (
-  <span className="absolute left-full ml-6 hidden w-auto min-w-max rounded-md bg-popover px-2 py-1 text-xs text-popover-foreground group-hover:block border border-border shadow-lg">
+{collapsed && (
+  <span className="absolute left-full ml-6 hidden w-auto min-w-max rounded-md bg-popover px-2 py-1 text-xs text-popover-foreground group-hover:block group-focus:block border border-border shadow-lg">
     {item.title}
   </span>
 )}

Note: This is a partial fix. For full accessibility, consider using a tooltip library that handles keyboard navigation, focus management, and ARIA attributes properly.

πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{collapsed && (
<span className="absolute left-full ml-6 hidden w-auto min-w-max rounded-md bg-popover px-2 py-1 text-xs text-popover-foreground group-hover:block border border-border shadow-lg">
{item.title}
</span>
)}
{collapsed && (
<span className="absolute left-full ml-6 hidden w-auto min-w-max rounded-md bg-popover px-2 py-1 text-xs text-popover-foreground group-hover:block group-focus:block border border-border shadow-lg">
{item.title}
</span>
)}
πŸ€– Prompt for AI Agents
In apps/web/components/sidebar.tsx around lines 119 to 123, the tooltip markup
only appears on mouse hover via "hidden" and "group-hover:block", which makes it
inaccessible to keyboard users; update the implementation so the tooltip is
reachable and visible on keyboard focus: ensure the trigger element is focusable
(add tabIndex or use a button/link), replace or augment "group-hover:block" with
"group-focus:block" (or "group-focus-within:block") so it shows on focus, keep
the tooltip element rendered but toggle visibility with CSS classes rather than
removing it from the DOM, add role="tooltip" and an id to the tooltip and set
aria-describedby on the trigger to reference that id, and ensure you manage
aria-hidden appropriately when hidden; for full accessibility consider swapping
to a dedicated tooltip component/library that handles focus management and ARIA
semantics.

Comment on lines 159 to 172
<button
className={cn(
"group relative flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium text-muted-foreground hover:bg-destructive/20 hover:text-destructive transition-all",
collapsed && "justify-center"
)}
>
<LogOut className="h-5 w-5 flex-shrink-0" />
{!collapsed && <span className="flex-1">Logout</span>}
{collapsed && (
<span className="absolute left-full ml-6 hidden w-auto min-w-max rounded-md bg-popover px-2 py-1 text-xs text-popover-foreground group-hover:block border border-border shadow-lg">
Logout
</span>
)}
</button>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | πŸ”΄ Critical

Missing onClick handler for Logout button.

The Logout button is missing an onClick handler, so clicking it currently does nothing. You need to implement the logout functionality.

Add an onClick handler. Example:

 <button
+  onClick={() => {
+    // Add your logout logic here
+    // e.g., signOut(), clear session, redirect to login
+  }}
   className={cn(
     "group relative flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium text-muted-foreground hover:bg-destructive/20 hover:text-destructive transition-all",
     collapsed && "justify-center"
   )}
 >

Committable suggestion skipped: line range outside the PR's diff.

πŸ€– Prompt for AI Agents
In apps/web/components/sidebar.tsx around lines 159 to 172, the Logout button
has no onClick so it does nothing; add an onClick handler that performs the
logout action (call your existing logout/signOut function or import signOut from
next-auth/react if using NextAuth), set the button type to "button" to avoid
accidental form submission, and include an accessible label/aria-label; ensure
the handler also handles any async cleanup (await signOut/logout) and handles
errors (e.g., try/catch or promise .catch).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 39

♻️ Duplicate comments (1)
apps/web/app/dashboard/page.tsx (1)

86-86: Sidebar collapse de-sync: static ml-64.

Use a CSS variable for margin so content tracks Sidebar width.

-      <main className="ml-64 min-h-screen transition-all duration-300">
+      <main
+        className="min-h-screen transition-all duration-300"
+        style={{ marginLeft: "var(--sidebar-width, 16rem)" }}
+      >
🧹 Nitpick comments (33)
apps/web/app/api/reporting/route.ts (2)

12-15: Code duplication with workspaces.ts.

This jitter function is nearly identical to the one in apps/web/app/api/_state/workspaces.ts (lines 29-33), which includes additional min and max parameters. Consider extracting to a shared utility module to maintain consistency.

Create a shared utility file (e.g., apps/web/lib/utils/math.ts) and import from both locations:

// apps/web/lib/utils/math.ts
export function jitter(
  n: number, 
  pct = 0.15, 
  rngState: { seed: number },
  min = 0, 
  max = Number.POSITIVE_INFINITY
) {
  const j = 1 + (rnd(rngState) * 2 - 1) * pct;
  const v = Math.max(min, Math.min(max, n * j));
  return Math.round(v * 100) / 100;
}

31-31: Use parentheses for clarity in ternary expression.

The nested ternary calculating points would benefit from explicit grouping to improve readability.

-  const points = range === "last_24h" ? 24 : range === "last_7d" ? 7 * 24 : 30 * 24;
+  const points = range === "last_24h" ? 24 : (range === "last_7d" ? 7 * 24 : 30 * 24);

Or use a more explicit approach:

const pointsMap = {
  last_24h: 24,
  last_7d: 7 * 24,
  last_30d: 30 * 24,
  this_month: 30 * 24, // approximate
};
const points = pointsMap[range];
apps/web/app/reporting/page.tsx (2)

203-209: Extract magic numbers to constants for spark bar height calculation.

The height calculation uses magic numbers (10, 30, 100) that obscure the scaling intent.

+const SPARK_MIN_HEIGHT = 10; // minimum bar height %
+const SPARK_SCALE_FACTOR = 30; // expected max value for scaling
+const SPARK_MAX_HEIGHT = 100; // maximum bar height %
+
 <div className="h-28 w-full flex items-end gap-1">
   {data?.timeseries.builds.slice(-60).map((p, i) => (
     <div
       key={i}
       className="flex-1 bg-primary/60"
-      style={{ height: `${Math.min(100, 10 + (p.v / 30) * 100)}%` }}
+      style={{ 
+        height: `${Math.min(
+          SPARK_MAX_HEIGHT, 
+          SPARK_MIN_HEIGHT + (p.v / SPARK_SCALE_FACTOR) * SPARK_MAX_HEIGHT
+        )}%` 
+      }}
     />
   ))}
 </div>

59-64: Consider adding user-facing error state.

Currently, fetch errors are only logged to the console (line 63). Users have no visual indication when data fails to load or refresh, which could lead to confusion when stale data is displayed.

Add an error state and display a banner or toast notification:

 const [data, setData] = useState<ReportingResponse | null>(null);
+const [error, setError] = useState<string | null>(null);

 async function load() {
   try {
     const res = await fetch(`/api/reporting?range=${range}`, { cache: "no-store" });
+    if (!res.ok) throw new Error(`HTTP ${res.status}`);
     const j = (await res.json()) as ReportingResponse;
     setData(j);
+    setError(null);
   } catch (e) {
     console.error(e);
+    setError("Failed to load reporting data");
   }
 }

Then render the error state in the UI:

{error && (
  <div className="mb-4 p-4 bg-destructive/10 border border-destructive rounded-lg">
    <p className="text-sm text-destructive">{error}</p>
  </div>
)}
apps/web/app/api/billing/invoice/route.ts (2)

6-6: Improve filename date formatting.

Using .slice(0, 7) on an ISO string to extract "YYYY-MM" is brittle and unclear. Use explicit date formatting instead.

+const date = new Date();
+const yearMonth = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
+
 return new Response(content, {
   headers: {
     "Content-Type": "text/plain",
-    "Content-Disposition": `attachment; filename=invoice-${new Date().toISOString().slice(0, 7)}.txt`,
+    "Content-Disposition": `attachment; filename=invoice-${yearMonth}.txt`,
   },
 });

2-2: Remove or track the TODO comment.

The comment "(This is a placeholder invoice. Replace with PDF generation.)" indicates incomplete functionality that shouldn't be in production code.

Would you like me to open an issue to track the PDF invoice generation feature, or should this be removed before merging?

apps/web/app/billing-usage/page.tsx (5)

56-56: Reduce polling frequency.

Polling every 10 seconds is aggressive and may:

  • Create unnecessary server load (especially with current in-memory state mutations)
  • Drain battery on mobile devices
  • Generate excessive network traffic

For billing data that changes infrequently, consider 60 seconds or user-triggered refresh.

-      timer = setInterval(load, 10000); // realtime-ish polling
+      timer = setInterval(load, 60000); // Poll every 60 seconds

Or add a manual refresh button and remove auto-polling:

const refreshData = () => {
  setLoading(true);
  load();
};
// In UI: <Button onClick={refreshData}>Refresh</Button>

48-49: Improve error handling UX.

Errors are logged to console but not displayed to users:

  1. Line 48-49: Fetch errors are silently swallowed
  2. Line 89: Browser alert() is jarring and blocks the UI

Add error state and display inline:

 const [data, setData] = useState<BillingData | null>(null);
 const [loading, setLoading] = useState(true);
+const [error, setError] = useState<string | null>(null);

 async function load() {
   try {
+    setError(null);
     const res = await fetch("/api/billing", { cache: "no-store" });
+    if (!res.ok) throw new Error("Failed to fetch billing data");
     const j = (await res.json()) as BillingData;
     setData(j);
   } catch (e) {
     console.error(e);
+    setError(e instanceof Error ? e.message : "Failed to load billing data");
   } finally {
     setLoading(false);
   }
 }

 async function downloadInvoice() {
   try {
     const res = await fetch("/api/billing/invoice", { method: "GET" });
+    if (!res.ok) throw new Error("Failed to download invoice");
     // ... existing download logic
   } catch (e) {
     console.error(e);
-    alert("Could not download invoice.");
+    setError("Could not download invoice. Please try again.");
   }
 }

Then display error in UI:

{error && (
  <div className="mb-4 p-4 bg-destructive/10 border border-destructive rounded-md text-sm text-destructive">
    {error}
  </div>
)}

Also applies to: 88-89


109-109: Replace raw emoji with icon components.

Raw emoji characters (πŸ””, πŸ“Š) can have inconsistent rendering across platforms and don't respect theme colors.

Use Lucide icons instead:

+import { Bell, BarChart3 } from "lucide-react";

-<button className="...">πŸ””</button>
+<button className="..."><Bell className="h-4 w-4" /></button>

-<span className="...">πŸ“Š</span>
+<BarChart3 className="h-4 w-4" />

Also applies to: 140-140


75-91: Add loading state to download button.

The download button doesn't show loading state, leaving users uncertain if their click registered during slow network conditions.

+const [downloading, setDownloading] = useState(false);
+
 async function downloadInvoice() {
   try {
+    setDownloading(true);
     const res = await fetch("/api/billing/invoice", { method: "GET" });
     // ... existing logic
   } catch (e) {
     console.error(e);
     alert("Could not download invoice.");
+  } finally {
+    setDownloading(false);
   }
 }

 // In render:
-<Button onClick={downloadInvoice} className="bg-primary">Download Invoice</Button>
+<Button onClick={downloadInvoice} disabled={downloading} className="bg-primary">
+  {downloading ? (
+    <><Loader2 className="h-4 w-4 animate-spin mr-2" /> Downloading...</>
+  ) : (
+    "Download Invoice"
+  )}
+</Button>

24-28: Consider making currency configurable.

The currency is hardcoded to INR (Indian Rupee), but a production billing system typically supports multiple currencies based on user location or preference.

This can be deferred if single-currency support is intentional for now, but consider adding currency to the user's account settings and billing data response in the future.

apps/web/app/ai-agents/page.tsx (2)

13-22: Consider sharing types between client and server.

The Agent and McpConfig interfaces are duplicated between this page and the API routes (apps/web/app/api/ai/agents/route.ts lines 3, and implied in apps/web/app/api/ai/mcp-config/route.ts). This creates a maintenance burden and potential type mismatches.

Consider extracting these types to a shared location:

Create a shared types file:

// apps/web/types/ai-agents.ts
export interface Agent {
  id: string;
  name: string;
  status: "connected" | "disconnected" | "warning";
}

export interface McpConfig {
  url: string;
  apiKey: string;
}

Then import in both locations:

 "use client";
 
 import { useEffect, useState } from "react";
 import { useRouter } from "next/navigation";
 import { useSession } from "next-auth/react";
+import type { Agent, McpConfig } from "@/types/ai-agents";
 import { Sidebar } from "@/components/sidebar";
 // ... other imports
-
-interface Agent {
-  id: string;
-  name: string;
-  status: "connected" | "disconnected" | "warning";
-}
-
-interface McpConfig {
-  url: string;
-  apiKey: string;
-}

149-149: Consider using a skeleton component instead of placeholder data.

The loading state pattern using [1,2,3].map(n => ({ id: String(n), name: "", status: "disconnected" })) is creative but could be clearer and more maintainable.

Consider using a dedicated skeleton component:

// Option 1: Separate loading state rendering
{loadingAgents ? (
  <div className="space-y-4">
    {[1, 2, 3].map((n) => (
      <div key={n} className="flex items-center justify-between rounded-md border border-border bg-card px-4 py-3">
        <div className="flex items-center gap-3">
          <div className="h-9 w-9 rounded-md bg-muted animate-pulse" />
          <div className="h-4 w-32 bg-muted animate-pulse rounded" />
        </div>
        <div className="h-9 w-24 bg-muted animate-pulse rounded" />
      </div>
    ))}
  </div>
) : (
  <div className="space-y-4">
    {agents.map((agent) => (
      // ... actual agent rendering
    ))}
  </div>
)}

This makes the loading vs. loaded states more explicit and easier to style differently.

apps/web/app/api/account/delete/route.ts (1)

4-5: TODO: Implement deletion logic.

The placeholder implementation is appropriate for initial development, but ensure real deletion logic is added before production deployment.

Do you want me to open an issue to track implementing the actual user deletion logic with proper database cleanup and audit logging?

apps/web/app/settings/change-password/page.tsx (1)

27-31: Replace alert() with better UI feedback.

Using alert() for notifications is dated and blocks the user. Consider using a toast notification library or inline error messages for better UX.

Example with inline state:

const [error, setError] = useState("");
const [success, setSuccess] = useState(false);

// In submit():
if (!res.ok) {
  setError("Unable to update password");
  return;
}
setSuccess(true);
setTimeout(() => router.push("/settings"), 1500);

// In JSX, before the buttons:
{error && <p className="text-sm text-red-600">{error}</p>}
{success && <p className="text-sm text-green-600">Password updated successfully!</p>}
apps/web/app/api/account/connections/route.ts (1)

20-23: Consider making the provider list configurable.

The provider list is hardcoded to Google and GitHub. If you add more OAuth providers in the future, you'll need to update this code. Consider deriving the list from your auth configuration or making it configurable.

Example with dynamic providers:

// Get unique providers from all accounts across users or from auth config
const allProviders = ["Google", "GitHub", "Discord", "GitLab"]; // from config
const providers: Conn[] = allProviders.map(provider => ({
  provider,
  connected: connected.has(provider.toLowerCase())
}));
apps/web/app/api/_state/workspaces.ts (2)

22-28: Consider using crypto.getRandomValues for better randomness.

The LCG PRNG is adequate for dev/demo, but using a more standard random source would be more maintainable. However, if deterministic sequences are needed for testing, this is acceptable.

If determinism isn't required, consider:

-let seed = Date.now() % 100000;
-function rnd() {
-  seed = (seed * 1664525 + 1013904223) % 4294967296;
-  return seed / 4294967296;
-}
+function rnd() {
+  return Math.random();
+}

44-74: Document the in-memory store lifecycle and concurrency model.

The ensureWorkspace function mutates a shared in-memory store without synchronization. While acceptable for dev/demo, document that:

  1. State is per-process and will be lost on restart
  2. Multiple concurrent requests could race during workspace creation
  3. This is not suitable for production or multi-instance deployments

Add a JSDoc comment:

/**
 * Ensures a workspace exists in the in-memory store, creating it lazily if needed.
 * 
 * @remarks
 * - State is per-process and ephemeral
 * - No synchronization; concurrent calls may create duplicate instances (check-then-act race)
 * - Not suitable for production use
 * 
 * @param id - Workspace identifier
 * @returns The workspace state
 */
export function ensureWorkspace(id: string): WorkspaceState {
  // ...
}
apps/web/app/workspaces/[id]/ide/page.tsx (1)

64-74: Consider adjusting polling interval based on workspace status.

A 5-second polling interval is reasonable, but you could optimize by polling less frequently when the workspace is stopped or more frequently during state transitions.

For example:

const interval = details?.status === "running" ? 5000 : 10000;
timer = setInterval(() => loadAll(controller.signal), interval);
apps/web/app/settings/page.tsx (4)

74-74: Sidebar/content desync: replace static ml-64 with CSS var.

Keep content margin in sync with Sidebar collapse width.

-      <main className="ml-64 min-h-screen transition-all duration-300">
+      <main
+        className="min-h-screen transition-all duration-300"
+        style={{ marginLeft: "var(--sidebar-width, 16rem)" }}
+      >

Set --sidebar-width on the Sidebar root for both states (e.g., 16rem/5rem).


23-28: Harden fetch: check res.ok before .json().

Prevents noisy JSON errors and enables better UX fallback.

-        const r = await fetch("/api/account/connections", { cache: "no-store" });
-        const j = await r.json();
+        const r = await fetch("/api/account/connections", { cache: "no-store" });
+        if (!r.ok) throw new Error(`Connections fetch failed: ${r.status}`);
+        const j = await r.json();

80-83: Prefer next-auth signIn for linking providers (avoid window.location).

Using signIn keeps auth flow consistent and easier to test.

-import { useSession, signOut } from "next-auth/react";
+import { useSession, signOut, signIn } from "next-auth/react";
@@
-                        <Button
-                          variant="outline"
-                          onClick={() => {
-                            const p = c.provider.toLowerCase();
-                            // Kick off OAuth sign-in to link account
-                            window.location.href = `/api/auth/signin/${p}`;
-                          }}
-                        >
+                        <Button
+                          variant="outline"
+                          onClick={() => signIn(c.provider.toLowerCase(), { callbackUrl: "/settings" })}
+                        >
                           Connect
                         </Button>

Also applies to: 136-145


80-83: Optional: Explicit callback on sign out.

Ensure consistent post-signout navigation.

-              <Button variant="destructive" onClick={() => signOut()}>
+              <Button variant="destructive" onClick={() => signOut({ callbackUrl: "/signin" })}>
apps/web/app/workspaces/page.tsx (3)

85-85: Sidebar/content desync: replace static ml-64 with CSS var.

Same issue as Dashboard; keep content margin synced to Sidebar.

-      <main className="ml-64 min-h-screen transition-all duration-300">
+      <main
+        className="min-h-screen transition-all duration-300"
+        style={{ marginLeft: "var(--sidebar-width, 16rem)" }}
+      >

100-102: Icon-only button needs an accessible name.

Add aria-label/title for screen readers.

-              <button className="h-9 w-9 rounded-md bg-card border border-border flex items-center justify-center text-muted-foreground">πŸ””</button>
+              <button
+                aria-label="Notifications"
+                title="Notifications"
+                className="h-9 w-9 rounded-md bg-card border border-border flex items-center justify-center text-muted-foreground"
+              >
+                πŸ””
+              </button>

199-203: Guard against empty submits.

Disable Save until required fields are populated.

-                  <Button onClick={handleSave} disabled={saving} className="bg-gradient-to-r from-primary to-secondary">
+                  <Button
+                    onClick={handleSave}
+                    disabled={
+                      saving ||
+                      !templateName ||
+                      (baseType === "image" && !dockerImage) ||
+                      (baseType === "dockerfile" && !dockerfile)
+                    }
+                    className="bg-gradient-to-r from-primary to-secondary"
+                  >
apps/web/app/dashboard/page.tsx (2)

104-107: Icon-only button needs an accessible name.

Add aria-label/title.

-              <button className="h-9 w-9 rounded-md bg-card border border-border flex items-center justify-center text-muted-foreground">
-                <span>πŸ””</span>
-              </button>
+              <button
+                aria-label="Notifications"
+                title="Notifications"
+                className="h-9 w-9 rounded-md bg-card border border-border flex items-center justify-center text-muted-foreground"
+              >
+                <span aria-hidden="true">πŸ””</span>
+              </button>

43-50: Check res.ok before parsing.

Avoids JSON parse errors and makes failures explicit.

-        const res = await fetch("/api/workspaces", { cache: "no-store" });
-        const j = await res.json();
+        const res = await fetch("/api/workspaces", { cache: "no-store" });
+        if (!res.ok) throw new Error(`Workspaces fetch failed: ${res.status}`);
+        const j = await res.json();
apps/web/app/(auth)/signin/page.tsx (2)

1-1: Redundant no-SSR wrapper with β€œuse client”.

Either keep β€œuse client” and export the component directly, or keep the dynamic({ ssr: false }) wrapperβ€”not both.

-"use client";
+// "use client"; // Optional if you keep the dynamic no-SSR export below.
@@
-export default dynamic(() => Promise.resolve(SignInPage), { ssr: false });
+export default dynamic(() => Promise.resolve(SignInPage), { ssr: false });
// Or:
// export default SignInPage;

Also applies to: 217-217


60-66: A11y polish: mark decorative icons as aria-hidden.

Keeps the label noise-free for screen readers.

-              <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-primary to-secondary glow-primary">
-                <Code className="h-5 w-5 text-black" />
+              <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-primary to-secondary glow-primary" aria-hidden="true">
+                <Code className="h-5 w-5 text-black" aria-hidden="true" />
               </div>
@@
-                    <Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-muted-foreground" />
+                    <Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-muted-foreground" aria-hidden="true" />
@@
-                    <Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-muted-foreground" />
+                    <Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-muted-foreground" aria-hidden="true" />
@@
-                      <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+                      <Loader2 className="mr-2 h-4 w-4 animate-spin" aria-hidden="true" />

Also applies to: 98-113, 117-133, 140-148

apps/web/app/workspaces/new/page.tsx (3)

108-109: Sidebar/content desync: replace static ml-64 with CSS var.

Keep content margin synced to Sidebar width.

-      <main className="ml-64 min-h-screen transition-all duration-300">
+      <main
+        className="min-h-screen transition-all duration-300"
+        style={{ marginLeft: "var(--sidebar-width, 16rem)" }}
+      >

46-51: Check res.ok for all fetches (options, estimate, create).

Improves error handling and avoids JSON parse failures.

-        const res = await fetch("/api/workspaces/options", { cache: "no-store" });
-        const j = (await res.json()) as Options;
+        const res = await fetch("/api/workspaces/options", { cache: "no-store" });
+        if (!res.ok) throw new Error(`Options fetch failed: ${res.status}`);
+        const j = (await res.json()) as Options;
@@
-        const res = await fetch("/api/workspaces/estimate", {
+        const res = await fetch("/api/workspaces/estimate", {
           method: "POST",
           headers: { "Content-Type": "application/json" },
           body: JSON.stringify({ provider, size, hoursPerDay }),
         });
-        const j = await res.json();
+        if (!res.ok) throw new Error(`Estimate failed: ${res.status}`);
+        const j = await res.json();
@@
-      await fetch("/api/workspaces", {
+      const res = await fetch("/api/workspaces", {
         method: "POST",
         headers: { "Content-Type": "application/json" },
         body: JSON.stringify({ action: "create", name, provider, image, size, region }),
       });
+      if (!res.ok) throw new Error(`Create workspace failed: ${res.status}`);

Also applies to: 60-67, 79-85


186-188: Clamp hours/day input to 0–24.

Prevents invalid state when users type out-of-range values.

-                    <Input id="hours" type="number" min={0} max={24} value={hoursPerDay} onChange={(e) => setHoursPerDay(Number(e.target.value))} />
+                    <Input
+                      id="hours"
+                      type="number"
+                      min={0}
+                      max={24}
+                      value={hoursPerDay}
+                      onChange={(e) => {
+                        const n = Number(e.target.value);
+                        const clamped = Number.isFinite(n) ? Math.max(0, Math.min(24, n)) : 0;
+                        setHoursPerDay(clamped);
+                      }}
+                    />
πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 2db4631 and a38bfcd.

β›” Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
πŸ“’ Files selected for processing (33)
  • apps/web/app/(auth)/signin/page.tsx (2 hunks)
  • apps/web/app/ai-agents/page.tsx (1 hunks)
  • apps/web/app/api/_state/workspaces.ts (1 hunks)
  • apps/web/app/api/account/connections/route.ts (1 hunks)
  • apps/web/app/api/account/delete/route.ts (1 hunks)
  • apps/web/app/api/account/password/route.ts (1 hunks)
  • apps/web/app/api/ai/agents/route.ts (1 hunks)
  • apps/web/app/api/ai/mcp-config/route.ts (1 hunks)
  • apps/web/app/api/billing/invoice/route.ts (1 hunks)
  • apps/web/app/api/billing/route.ts (1 hunks)
  • apps/web/app/api/reporting/route.ts (1 hunks)
  • apps/web/app/api/templates/route.ts (1 hunks)
  • apps/web/app/api/workspaces/[id]/action/route.ts (1 hunks)
  • apps/web/app/api/workspaces/[id]/details/route.ts (1 hunks)
  • apps/web/app/api/workspaces/[id]/metrics/route.ts (1 hunks)
  • apps/web/app/api/workspaces/[id]/snapshots/route.ts (1 hunks)
  • apps/web/app/api/workspaces/[id]/terminal/route.ts (1 hunks)
  • apps/web/app/api/workspaces/estimate/route.ts (1 hunks)
  • apps/web/app/api/workspaces/options/route.ts (1 hunks)
  • apps/web/app/api/workspaces/route.ts (1 hunks)
  • apps/web/app/billing-usage/page.tsx (1 hunks)
  • apps/web/app/components/theme-provider.tsx (1 hunks)
  • apps/web/app/dashboard/page.tsx (1 hunks)
  • apps/web/app/profile/page.tsx (3 hunks)
  • apps/web/app/reporting/page.tsx (1 hunks)
  • apps/web/app/settings/change-password/page.tsx (1 hunks)
  • apps/web/app/settings/page.tsx (1 hunks)
  • apps/web/app/workspaces/[id]/ide/page.tsx (1 hunks)
  • apps/web/app/workspaces/new/page.tsx (1 hunks)
  • apps/web/app/workspaces/page.tsx (1 hunks)
  • apps/web/components/sidebar.tsx (1 hunks)
  • apps/web/package.json (1 hunks)
  • apps/web/tsconfig.json (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/web/components/sidebar.tsx
  • apps/web/package.json
  • apps/web/app/components/theme-provider.tsx
🧰 Additional context used
🧬 Code graph analysis (23)
apps/web/app/api/account/password/route.ts (2)
apps/web/app/api/account/delete/route.ts (1)
  • POST (3-7)
apps/web/app/api/templates/route.ts (1)
  • POST (3-11)
apps/web/app/api/account/connections/route.ts (2)
apps/web/lib/auth-config.ts (1)
  • session (97-102)
apps/web/lib/prisma.ts (1)
  • prisma (5-5)
apps/web/app/api/workspaces/[id]/action/route.ts (2)
apps/web/app/api/workspaces/[id]/snapshots/route.ts (1)
  • POST (10-18)
apps/web/app/api/_state/workspaces.ts (1)
  • ensureWorkspace (44-74)
apps/web/app/ai-agents/page.tsx (2)
apps/web/lib/auth-config.ts (1)
  • session (97-102)
apps/web/components/sidebar.tsx (1)
  • Sidebar (42-157)
apps/web/app/api/workspaces/[id]/terminal/route.ts (1)
apps/web/app/api/_state/workspaces.ts (1)
  • ensureWorkspace (44-74)
apps/web/app/settings/change-password/page.tsx (1)
apps/web/components/sidebar.tsx (1)
  • Sidebar (42-157)
apps/web/app/api/reporting/route.ts (1)
apps/web/app/api/_state/workspaces.ts (1)
  • jitter (30-34)
apps/web/app/workspaces/page.tsx (1)
apps/web/components/sidebar.tsx (1)
  • Sidebar (42-157)
apps/web/app/reporting/page.tsx (2)
apps/web/lib/auth-config.ts (1)
  • session (97-102)
apps/web/components/sidebar.tsx (1)
  • Sidebar (42-157)
apps/web/app/api/workspaces/options/route.ts (2)
apps/web/app/api/workspaces/[id]/details/route.ts (1)
  • GET (4-17)
apps/web/app/api/workspaces/route.ts (1)
  • GET (11-19)
apps/web/app/billing-usage/page.tsx (2)
apps/web/lib/auth-config.ts (1)
  • session (97-102)
apps/web/components/sidebar.tsx (1)
  • Sidebar (42-157)
apps/web/app/(auth)/signin/page.tsx (2)
packages/ui/src/code.tsx (1)
  • Code (3-11)
apps/web/lib/auth-config.ts (1)
  • signIn (103-119)
apps/web/app/api/account/delete/route.ts (1)
apps/web/app/api/account/password/route.ts (1)
  • POST (3-10)
apps/web/app/api/workspaces/[id]/details/route.ts (1)
apps/web/app/api/_state/workspaces.ts (1)
  • ensureWorkspace (44-74)
apps/web/app/api/workspaces/[id]/snapshots/route.ts (1)
apps/web/app/api/_state/workspaces.ts (1)
  • ensureWorkspace (44-74)
apps/web/app/api/workspaces/[id]/metrics/route.ts (2)
apps/web/app/api/workspaces/[id]/details/route.ts (1)
  • GET (4-17)
apps/web/app/api/_state/workspaces.ts (2)
  • ensureWorkspace (44-74)
  • jitter (30-34)
apps/web/app/api/billing/invoice/route.ts (1)
apps/web/app/api/billing/route.ts (1)
  • GET (19-58)
apps/web/app/dashboard/page.tsx (1)
apps/web/components/sidebar.tsx (1)
  • Sidebar (42-157)
apps/web/app/workspaces/[id]/ide/page.tsx (1)
apps/web/components/sidebar.tsx (1)
  • Sidebar (42-157)
apps/web/app/workspaces/new/page.tsx (1)
apps/web/components/sidebar.tsx (1)
  • Sidebar (42-157)
apps/web/app/api/billing/route.ts (2)
apps/web/app/api/_state/workspaces.ts (1)
  • jitter (30-34)
apps/web/app/api/billing/invoice/route.ts (1)
  • GET (1-9)
apps/web/app/settings/page.tsx (1)
apps/web/components/sidebar.tsx (1)
  • Sidebar (42-157)
apps/web/app/api/ai/mcp-config/route.ts (1)
apps/web/app/api/ai/agents/route.ts (1)
  • GET (12-14)
πŸͺ› Biome (2.1.2)
apps/web/app/profile/page.tsx

[error] 33-33: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

Hooks should not be called after an early return.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

πŸ”‡ Additional comments (12)
apps/web/tsconfig.json (1)

24-24: Ignore this review comment. The exclusion is intentional and correct.

The assumption that UI components are "actively used" is contradicted by evidence: zero imports of app/components/ui/** exist anywhere in the codebase. The tsconfig.json comment explicitly documents these as "unused shadcn/radix demo components." The 50 components in this directory are library components (Accordion, Alert, Button, Card, etc.), and excluding demo/library UI components from TypeScript compilation is a standard practice. No fixes or changes are needed.

Likely an incorrect or invalid review comment.

apps/web/app/api/reporting/route.ts (2)

17-25: Verify makeTimeseries accounts for timezone and DST transitions.

The function generates hourly timestamps by subtracting i * 60 * 60 * 1000 from Date.now(). This assumes all hours are exactly 3600 seconds, which breaks during DST transitions in some timezones.

For synthetic test data this may be acceptable, but confirm whether clients expect accurate time alignment or if approximate hourly intervals suffice.


52-52: Verify error timeseries base value of 1.2.

Using makeTimeseries(points, 1.2, 0.8) for errors means the baseline error rate is 1.2 per hour with 80% volatility, which can produce fractional values or occasionally zero errors. Confirm this aligns with the intended synthetic data profile.

apps/web/app/reporting/page.tsx (1)

42-219: LGTM: Well-structured reporting dashboard with appropriate loading and auth handling.

The component correctly:

  • Uses the mounted flag to prevent hydration mismatches
  • Implements session-based authentication with redirect
  • Auto-refreshes data every 10 seconds
  • Provides a clean, card-based dashboard UI
  • Uses appropriate Next.js 15 and React 19 patterns
apps/web/app/ai-agents/page.tsx (1)

40-43: Client-side auth guard is necessary due to middleware limitations; improve UX to reduce flicker and potential race conditions.

The middleware in apps/web/middleware.ts has a logic issue: PROTECTED_ROUTES = ["/*"] matches only paths literally starting with "/*", so /ai-agents is not actually protected by middleware. The client-side useEffect redirect in page.tsx (lines 40-43) serves as a necessary fallback.

However, the original concerns remain valid:

  1. Flicker risk: Page renders content before redirect triggers in useEffect
  2. Race condition: Session becoming null mid-interaction causes unexpected redirects

Recommendations:

  • Add loading skeleton/blank state before useEffect redirect completes
  • Add dependency on session revalidation to prevent mid-interaction nulling
  • Or fix the middleware pattern to properly protect routes (change PROTECTED_ROUTES logic or use regex matching)
apps/web/app/settings/change-password/page.tsx (1)

56-59: LGTM: Button loading state handled correctly.

The disabled state during busy and the Loader2 spinner provide good user feedback during the async operation.

apps/web/app/api/workspaces/options/route.ts (1)

3-31: LGTM: Static configuration endpoint is well-structured.

The OPTIONS object provides a clean separation of configuration data for workspace creation. The endpoint appropriately returns read-only configuration without authentication, which is acceptable for non-sensitive data.

apps/web/app/profile/page.tsx (1)

156-166: LGTM: Connected Accounts UI is well-structured.

The new Connected Accounts section provides clear visual feedback with appropriate styling for connection status. The grid layout and conditional styling enhance readability.

apps/web/app/api/account/connections/route.ts (1)

9-12: LGTM: Proper authentication check implemented.

The authentication check correctly validates the user session before querying connections, preventing unauthorized access.

apps/web/app/api/workspaces/[id]/action/route.ts (1)

18-18: LGTM: Terminal history truncation prevents unbounded growth.

Limiting the terminal array to the last 120 entries is a good safeguard against memory issues in this in-memory state implementation.

apps/web/app/api/workspaces/[id]/snapshots/route.ts (1)

4-8: Remove this review comment - the suggested fix is incorrect and misrepresents the code.

The ensureWorkspace function never throws exceptions; it synchronously returns a WorkspaceState by either retrieving an existing workspace from the store or creating a new one. The suggested try-catch wrapper would never catch anything and is therefore not applicable.

Additionally, the lack of authentication is not unique to the snapshots routeβ€”it's consistent across all workspace API routes (action, details, metrics, terminal). The middleware explicitly excludes API routes from authentication (matcher config), and the workspace state file is explicitly marked as "dev/demo. Not for production use." This appears to be an intentional design choice for the demo application, not an oversight requiring the suggested fix.

Likely an incorrect or invalid review comment.

apps/web/app/dashboard/page.tsx (1)

8-19: Fix lints: remove unused imports (likely the β€œsolve lint error” comment).

Only Card and Loader2 are used; others trigger eslint/ts errors.

-import { Button } from "@/components/ui/button";
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
-import { Badge } from "@/components/ui/badge";
-import {
-  Activity,
-  Code,
-  GitBranch,
-  Terminal,
-  Zap,
-  TrendingUp,
-  Clock,
-  Loader2,
-} from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Card } from "@/components/ui/card";
+import { Loader2 } from "lucide-react";

Likely an incorrect or invalid review comment.

Comment on lines +46 to +64
useEffect(() => {
async function fetchData() {
try {
setLoadingAgents(true);
const [a, c] = await Promise.all([
fetch("/api/ai/agents").then((r) => r.json()),
fetch("/api/ai/mcp-config").then((r) => r.json()),
]);
setAgents(a.agents ?? []);
setConfig({ url: c.url ?? "", apiKey: c.apiKey ?? "" });
setRecent(c.recent ?? []);
} catch (e) {
console.error(e);
} finally {
setLoadingAgents(false);
}
}
if (mounted) fetchData();
}, [mounted]);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Improve error handling and user feedback for data fetching.

The data fetching logic has minimal error handling:

  1. Silent failures: Errors are only logged to console (line 58), leaving users without feedback
  2. No response validation: Missing check for response.ok before parsing JSON
  3. No retry logic: Transient network errors will fail permanently
 useEffect(() => {
   async function fetchData() {
     try {
       setLoadingAgents(true);
       const [a, c] = await Promise.all([
-        fetch("/api/ai/agents").then((r) => r.json()),
-        fetch("/api/ai/mcp-config").then((r) => r.json()),
+        fetch("/api/ai/agents").then(async (r) => {
+          if (!r.ok) throw new Error(`Failed to fetch agents: ${r.status}`);
+          return r.json();
+        }),
+        fetch("/api/ai/mcp-config").then(async (r) => {
+          if (!r.ok) throw new Error(`Failed to fetch config: ${r.status}`);
+          return r.json();
+        }),
       ]);
       setAgents(a.agents ?? []);
       setConfig({ url: c.url ?? "", apiKey: c.apiKey ?? "" });
       setRecent(c.recent ?? []);
     } catch (e) {
       console.error(e);
+      // Add user-visible error state
+      // setError("Failed to load AI agents. Please refresh the page.");
     } finally {
       setLoadingAgents(false);
     }
   }
   if (mounted) fetchData();
 }, [mounted]);

Consider adding an error state and displaying it to users in the UI.

πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
async function fetchData() {
try {
setLoadingAgents(true);
const [a, c] = await Promise.all([
fetch("/api/ai/agents").then((r) => r.json()),
fetch("/api/ai/mcp-config").then((r) => r.json()),
]);
setAgents(a.agents ?? []);
setConfig({ url: c.url ?? "", apiKey: c.apiKey ?? "" });
setRecent(c.recent ?? []);
} catch (e) {
console.error(e);
} finally {
setLoadingAgents(false);
}
}
if (mounted) fetchData();
}, [mounted]);
useEffect(() => {
async function fetchData() {
try {
setLoadingAgents(true);
const [a, c] = await Promise.all([
fetch("/api/ai/agents").then(async (r) => {
if (!r.ok) throw new Error(`Failed to fetch agents: ${r.status}`);
return r.json();
}),
fetch("/api/ai/mcp-config").then(async (r) => {
if (!r.ok) throw new Error(`Failed to fetch config: ${r.status}`);
return r.json();
}),
]);
setAgents(a.agents ?? []);
setConfig({ url: c.url ?? "", apiKey: c.apiKey ?? "" });
setRecent(c.recent ?? []);
} catch (e) {
console.error(e);
// Add user-visible error state
// setError("Failed to load AI agents. Please refresh the page.");
} finally {
setLoadingAgents(false);
}
}
if (mounted) fetchData();
}, [mounted]);

Comment on lines +66 to +98
async function toggleAgent(agent: Agent) {
setSavingAgentId(agent.id);
try {
const res = await fetch("/api/ai/agents", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id: agent.id, action: agent.status === "connected" ? "disconnect" : "connect" }),
});
const data = await res.json();
setAgents(data.agents);
} catch (e) {
console.error(e);
} finally {
setSavingAgentId(null);
}
}

async function saveConfig() {
setSavingConfig(true);
try {
const res = await fetch("/api/ai/mcp-config", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(config),
});
const data = await res.json();
setRecent(data.recent ?? []);
} catch (e) {
console.error(e);
} finally {
setSavingConfig(false);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add response validation, optimistic updates, and error feedback.

Both action handlers (toggleAgent and saveConfig) share similar issues:

  1. No response validation: Missing response.ok checks before parsing JSON
  2. Poor UX: No optimistic updates make interactions feel slow
  3. Silent failures: Errors only logged to console (lines 77, 94)
  4. No input validation: saveConfig doesn't validate URL format or API key presence

Suggested improvements for toggleAgent:

 async function toggleAgent(agent: Agent) {
   setSavingAgentId(agent.id);
+  
+  // Optimistic update
+  const previousAgents = agents;
+  const newStatus = agent.status === "connected" ? "disconnected" : "connected";
+  setAgents(agents.map(a => a.id === agent.id ? { ...a, status: newStatus } : a));
+  
   try {
     const res = await fetch("/api/ai/agents", {
       method: "POST",
       headers: { "Content-Type": "application/json" },
       body: JSON.stringify({ id: agent.id, action: agent.status === "connected" ? "disconnect" : "connect" }),
     });
+    
+    if (!res.ok) {
+      throw new Error(`Failed to toggle agent: ${res.status}`);
+    }
+    
     const data = await res.json();
     setAgents(data.agents);
   } catch (e) {
     console.error(e);
+    // Rollback optimistic update
+    setAgents(previousAgents);
+    // Show error to user
+    alert("Failed to toggle agent. Please try again.");
   } finally {
     setSavingAgentId(null);
   }
 }

Similar improvements should be applied to saveConfig with URL validation.

Comment on lines 14 to 23
const accounts = await prisma.account.findMany({
where: { userId: session.user.id },
select: { provider: true },
});

const connected = new Set(accounts.map((a) => a.provider.toLowerCase()));
const providers: Conn[] = [
{ provider: "Google", connected: connected.has("google") },
{ provider: "GitHub", connected: connected.has("github") },
];
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add error handling for database query.

The Prisma query lacks error handling. If the database connection fails or there's a query error, the endpoint will crash with an unhandled exception.

Apply this diff to add error handling:

 export async function GET() {
   // Derive connections for the signed-in user from the Accounts table
   const session = await auth();
   if (!session?.user?.id) {
     return NextResponse.json({ connections: [], updatedAt: new Date().toISOString() });
   }

-  const accounts = await prisma.account.findMany({
-    where: { userId: session.user.id },
-    select: { provider: true },
-  });
-
-  const connected = new Set(accounts.map((a) => a.provider.toLowerCase()));
+  try {
+    const accounts = await prisma.account.findMany({
+      where: { userId: session.user.id },
+      select: { provider: true },
+    });
+
+    const connected = new Set(accounts.map((a) => a.provider.toLowerCase()));
+  } catch (err) {
+    console.error("Failed to fetch connections:", err);
+    return NextResponse.json({ connections: [], updatedAt: new Date().toISOString() }, { status: 500 });
+  }
+
   const providers: Conn[] = [
     { provider: "Google", connected: connected.has("google") },
     { provider: "GitHub", connected: connected.has("github") },
   ];

   return NextResponse.json({ connections: providers, updatedAt: new Date().toISOString() });
 }
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const accounts = await prisma.account.findMany({
where: { userId: session.user.id },
select: { provider: true },
});
const connected = new Set(accounts.map((a) => a.provider.toLowerCase()));
const providers: Conn[] = [
{ provider: "Google", connected: connected.has("google") },
{ provider: "GitHub", connected: connected.has("github") },
];
export async function GET() {
// Derive connections for the signed-in user from the Accounts table
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ connections: [], updatedAt: new Date().toISOString() });
}
let connected: Set<string>;
try {
const accounts = await prisma.account.findMany({
where: { userId: session.user.id },
select: { provider: true },
});
connected = new Set(accounts.map((a) => a.provider.toLowerCase()));
} catch (err) {
console.error("Failed to fetch connections:", err);
return NextResponse.json({ connections: [], updatedAt: new Date().toISOString() }, { status: 500 });
}
const providers: Conn[] = [
{ provider: "Google", connected: connected.has("google") },
{ provider: "GitHub", connected: connected.has("github") },
];
return NextResponse.json({ connections: providers, updatedAt: new Date().toISOString() });
}
πŸ€– Prompt for AI Agents
In apps/web/app/api/account/connections/route.ts around lines 14 to 23, the
Prisma findMany call is unprotected and can throw, so wrap the database query in
a try/catch, log the error (or capture it with your existing logger), and return
an appropriate HTTP error response (e.g., 500 with a JSON error message) instead
of letting the exception crash the endpoint; ensure any resources or responses
are returned consistently after the catch.

Comment on lines +42 to +45
useEffect(() => {
if (status === "loading") return;
if (!session) router.push("/signin");
}, [status, session, router]);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Auth guard may execute late, allowing brief unauthorized render.

The useEffect redirect runs after the component renders. If status === "unauthenticated", the component may briefly render the IDE UI before redirecting.

Apply this diff to guard the main UI render:

   useEffect(() => {
     if (status === "loading") return;
     if (!session) router.push("/signin");
   }, [status, session, router]);

And update the loading condition on line 96:

-  if (status === "loading" || loading || !details || !metrics) {
+  if (status !== "authenticated" || loading || !details || !metrics) {
     return (

Committable suggestion skipped: line range outside the PR's diff.

πŸ€– Prompt for AI Agents
In apps/web/app/workspaces/[id]/ide/page.tsx around lines 42–45, the auth
redirect in useEffect runs after render which allows a brief unauthorized UI
flash; change the guard so the component does not render the main IDE when the
session is not authenticated by returning a loading placeholder (or null) while
status is "loading" or "unauthenticated", and perform router.push("/signin")
immediately when status === "unauthenticated" (inside an effect or guard) to
avoid rendering the UI; also update the loading condition at line 96 to treat
anything other than "authenticated" as loading (e.g., replace the existing check
with a status !== "authenticated" or include "unauthenticated") so the main UI
only renders when the user is authenticated.

Comment on lines +76 to +88
async function doAction(action: "start" | "stop" | "restart") {
setActing(action);
try {
await fetch(`/api/workspaces/${id}/action`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action }),
});
await loadAll();
} finally {
setActing(null);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add error handling for action requests.

The doAction function doesn't handle fetch errors, which could leave the UI in an "acting" state if the request fails.

Apply this diff:

   async function doAction(action: "start" | "stop" | "restart") {
     setActing(action);
     try {
       await fetch(`/api/workspaces/${id}/action`, {
         method: "POST",
         headers: { "Content-Type": "application/json" },
         body: JSON.stringify({ action }),
       });
       await loadAll();
+    } catch (error) {
+      console.error("Action failed:", error);
+      // Consider showing an error toast/message to the user
     } finally {
       setActing(null);
     }
   }
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function doAction(action: "start" | "stop" | "restart") {
setActing(action);
try {
await fetch(`/api/workspaces/${id}/action`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action }),
});
await loadAll();
} finally {
setActing(null);
}
}
async function doAction(action: "start" | "stop" | "restart") {
setActing(action);
try {
await fetch(`/api/workspaces/${id}/action`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action }),
});
await loadAll();
} catch (error) {
console.error("Action failed:", error);
// Consider showing an error toast/message to the user
} finally {
setActing(null);
}
}

Comment on lines +90 to +94
async function createSnapshot() {
await fetch(`/api/workspaces/${id}/snapshots`, { method: "POST" });
const s = await fetch(`/api/workspaces/${id}/snapshots`, { cache: "no-store" }).then((r) => r.json());
setSnapshots(s.snapshots);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add error handling for snapshot creation.

The createSnapshot function doesn't handle errors, which could fail silently.

Apply this diff:

   async function createSnapshot() {
+    try {
       await fetch(`/api/workspaces/${id}/snapshots`, { method: "POST" });
       const s = await fetch(`/api/workspaces/${id}/snapshots`, { cache: "no-store" }).then((r) => r.json());
       setSnapshots(s.snapshots);
+    } catch (error) {
+      console.error("Failed to create snapshot:", error);
+      // Consider showing an error toast/message to the user
+    }
   }
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function createSnapshot() {
await fetch(`/api/workspaces/${id}/snapshots`, { method: "POST" });
const s = await fetch(`/api/workspaces/${id}/snapshots`, { cache: "no-store" }).then((r) => r.json());
setSnapshots(s.snapshots);
}
async function createSnapshot() {
try {
await fetch(`/api/workspaces/${id}/snapshots`, { method: "POST" });
const s = await fetch(`/api/workspaces/${id}/snapshots`, { cache: "no-store" }).then((r) => r.json());
setSnapshots(s.snapshots);
} catch (error) {
console.error("Failed to create snapshot:", error);
// Consider showing an error toast/message to the user
}
}
πŸ€– Prompt for AI Agents
In apps/web/app/workspaces/[id]/ide/page.tsx around lines 90 to 94, the
createSnapshot function makes POST and GET fetches with no error handling so
failures can be silent; wrap the POST/GET sequence in a try/catch, check each
response.ok (and parse error body/text when not ok) before proceeding, only
perform the subsequent GET if the POST succeeded, and on error surface feedback
(e.g., call your existing toast/error handler or set an error state) and avoid
calling setSnapshots with bad data; ensure the catch logs the error and returns
early.

"exclude": [
"node_modules"
"node_modules",
// Exclude unused shadcn/radix demo components under app/components/ui
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | πŸ”΄ Critical

JSON syntax error: comments are not valid in JSON.

JavaScript-style comments (//) are not valid JSON syntax. While some tools tolerate this, strict JSON parsers will reject it, and this is likely causing the lint error mentioned in the PR comments. Remove the comment or move it outside the JSON structure.

  "exclude": [
    "node_modules",
-   // Exclude unused shadcn/radix demo components under app/components/ui
    "app/components/ui/**"
  ]

Alternatively, if you need to document this exclusion, consider adding a comment in your project's ADR or README file.

πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Exclude unused shadcn/radix demo components under app/components/ui
"exclude": [
"node_modules",
"app/components/ui/**"
]
πŸ€– Prompt for AI Agents
In apps/web/tsconfig.json around line 23, there is a JavaScript-style comment
("// Exclude unused shadcn/radix demo components under app/components/ui")
inside the JSON which is invalid and causes JSON parse/lint failures; remove
that inline comment (or move the explanatory text into a README/ADR or a
surrounding non-JSON file) so the file contains only valid JSON, ensuring the
exclusion entry remains but without any // comment text inside the JSON.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 13

♻️ Duplicate comments (18)
apps/web/app/settings/change-password/page.tsx (1)

18-35: Add comprehensive validation and improve error handling.

The missing client-side validation was previously flagged and remains unresolved. Additionally, the form lacks several security and UX best practices:

  1. No validation before API call (current/next empty, minimum length)
  2. No password confirmation field to prevent typos
  3. Generic error messages don't guide users (e.g., "wrong current password" vs "network error")
  4. Using alert() instead of inline validation messages
  5. Password fields should be cleared after successful update for security

Apply this diff to add basic validation and confirmation field support:

   const [current, setCurrent] = useState("");
   const [next, setNext] = useState("");
+  const [confirm, setConfirm] = useState("");
+  const [error, setError] = useState("");
   const [busy, setBusy] = useState(false);

   async function submit() {
+    setError("");
+    
+    // Validate inputs
+    if (!current || !next || !confirm) {
+      setError("All fields are required");
+      return;
+    }
+    if (next.length < 8) {
+      setError("New password must be at least 8 characters");
+      return;
+    }
+    if (next !== confirm) {
+      setError("New passwords do not match");
+      return;
+    }
+    if (current === next) {
+      setError("New password must be different from current password");
+      return;
+    }
+    
     setBusy(true);
     try {
       const res = await fetch("/api/account/password", {
         method: "POST",
         headers: { "Content-Type": "application/json" },
         body: JSON.stringify({ current, next }),
       });
-      if (!res.ok) throw new Error("Password update failed");
-      alert("Password updated (demo)");
+      if (!res.ok) {
+        const data = await res.json().catch(() => ({}));
+        throw new Error(data.error || "Password update failed");
+      }
+      
+      // Clear sensitive data
+      setCurrent("");
+      setNext("");
+      setConfirm("");
+      
+      // Show success and redirect
       router.push("/settings");
     } catch (e) {
       console.error(e);
-      alert("Unable to update password right now.");
+      setError(e instanceof Error ? e.message : "Unable to update password");
     } finally {
       setBusy(false);
     }
   }

Then add the confirmation field and error display in the UI (around line 52):

               <Label htmlFor="next">New password</Label>
               <Input id="next" type="password" value={next} onChange={(e) => setNext(e.target.value)} />
             </div>
+            <div className="space-y-2">
+              <Label htmlFor="confirm">Confirm new password</Label>
+              <Input id="confirm" type="password" value={confirm} onChange={(e) => setConfirm(e.target.value)} />
+            </div>
+            {error && (
+              <div className="text-sm text-destructive bg-destructive/10 border border-destructive/20 rounded-md p-3">
+                {error}
+              </div>
+            )}
             <div className="flex items-center gap-3">
apps/web/components/sidebar.tsx (2)

73-82: Add accessibility attributes to the toggle button.

The toggle button is missing aria-label and aria-expanded attributes, which are essential for screen reader users to understand the button's purpose and current state.

Apply this diff to improve accessibility:

 <button
   onClick={() => setCollapsed(!collapsed)}
+  aria-label={collapsed ? "Expand sidebar" : "Collapse sidebar"}
+  aria-expanded={!collapsed}
   className="absolute -right-3 top-20 z-50 flex h-6 w-6 items-center justify-center rounded-full border border-border bg-card text-muted-foreground hover:text-foreground hover:glow-primary transition-all"
 >

113-117: Tooltip implementation is not keyboard-accessible.

The tooltip uses hidden and group-hover:block, which only shows on mouse hover and won't be accessible to keyboard users. Consider using a proper tooltip component or adding focus state handling.

Example fix to support keyboard navigation:

 {collapsed && (
-  <span className="absolute left-full ml-6 hidden w-auto min-w-max rounded-md bg-popover px-2 py-1 text-xs text-popover-foreground group-hover:block border border-border shadow-lg">
+  <span className="absolute left-full ml-6 hidden w-auto min-w-max rounded-md bg-popover px-2 py-1 text-xs text-popover-foreground group-hover:block group-focus-within:block border border-border shadow-lg" role="tooltip">
     {item.title}
   </span>
 )}

Note: For full accessibility, consider using a dedicated tooltip library that handles keyboard navigation, focus management, and ARIA attributes properly.

apps/web/app/api/billing/invoice/route.ts (1)

1-9: Critical issues remain unaddressed from previous review.

The invoice endpoint still lacks authentication and uses hardcoded billing data inconsistent with the dynamic /api/billing response. The previous review comment comprehensively identifies these critical security and data integrity issues and provides a complete fix.

apps/web/app/api/reporting/route.ts (1)

5-10: Critical race condition still present.

This global mutable seed issue was already flagged in a previous review but remains unfixed. Multiple concurrent requests will interfere with each other's PRNG sequences, producing unpredictable results.

Please address the previous review comment by moving the seed into request scope as suggested.

apps/web/app/api/ai/agents/route.ts (2)

5-10: Replace in-memory store before production deployment.

This issue was already flagged in a previous review. The in-memory store will not persist across serverless function invocations in Next.js deployments. Replace with a database or persistent service before merging to production.


16-27: Add input validation, authentication, and improve error handling.

This issue was already flagged in a previous review. The POST handler needs:

  1. Input validation using Zod (as per learnings)
  2. Authentication checks
  3. Response validation for invalid agent IDs
  4. Better error handling

Based on learnings.

apps/web/app/ai-agents/page.tsx (3)

46-64: Improve error handling and response validation.

This issue was already flagged in a previous review. The data fetching needs response validation and user-visible error feedback.


66-81: Add response validation and optimistic updates.

This issue was already flagged in a previous review. The toggleAgent function needs proper error handling, response validation, and optimistic UI updates for better UX.


83-98: Add input validation and response validation.

Similar to the toggleAgent function flagged in previous review, this function needs URL format validation and proper error handling.

apps/web/app/profile/page.tsx (1)

33-52: Critical: Hook called after conditional return violates React Rules of Hooks.

The useEffect hook on line 33 is called after the early return on line 30, which violates React's Rules of Hooks. This will cause runtime errors and unpredictable behavior.

Move all hooks before any conditional returns:

   const router = useRouter();
   const [connections, setConnections] = useState<Array<{ provider: string; connected: boolean }>>([]);

   useEffect(() => {
     if (status === "loading") return;
     if (!session) {
       router.push("/signin");
     }
   }, [session, status, router]);

+  useEffect(() => {
+    let timer: ReturnType<typeof setInterval> | undefined;
+    async function load() {
+      try {
+        const r = await fetch("/api/account/connections", { cache: "no-store" });
+        if (!r.ok) return;
+        const text = await r.text();
+        if (!text) return;
+        const j = JSON.parse(text);
+        setConnections(j.connections ?? []);
+      } catch (e) {
+        console.error("profile: connections fetch error", e);
+      }
+    }
+    if (status === "authenticated") {
+      load();
+      timer = setInterval(load, 8000);
+    }
+    return () => { if (timer) clearInterval(timer); };
+  }, [status]);
+
   if (status === "loading") {
     return (
       <div className="min-h-screen flex items-center justify-center">
         <div className="text-lg">Loading...</div>
       </div>
     );
   }

   if (!session) {
     return null;
   }

-  useEffect(() => {
-    let timer: ReturnType<typeof setInterval> | undefined;
-    async function load() {
-      try {
-        const r = await fetch("/api/account/connections", { cache: "no-store" });
-        if (!r.ok) return;
-        const text = await r.text();
-        if (!text) return;
-        const j = JSON.parse(text);
-        setConnections(j.connections ?? []);
-      } catch (e) {
-        console.error("profile: connections fetch error", e);
-      }
-    }
-    if (status === "authenticated") {
-      load();
-      timer = setInterval(load, 8000);
-    }
-    return () => { if (timer) clearInterval(timer); };
-  }, [status]);
apps/web/app/settings/page.tsx (1)

17-17: Conflated loading and empty states for connections.

The component uses an empty array [] as the initial state for connections, which makes it impossible to distinguish between "still loading" and "no connections available." Line 129 renders "Loading connections..." whenever the array is empty, which is misleading if the user genuinely has no connections.

Apply this diff to track loading state separately:

   const [deleting, setDeleting] = useState(false);
-  const [connections, setConnections] = useState<Conn[]>([]);
+  const [connections, setConnections] = useState<Conn[] | null>(null);

   // fetch dynamic connection statuses
   useEffect(() => {
     let timer: ReturnType<typeof setInterval> | undefined;
     async function load() {
       try {
         const r = await fetch("/api/account/connections", { cache: "no-store" });
         if (!r.ok) return; // don't attempt to parse error pages
         const text = await r.text();
         if (!text) return; // guard against empty body
         const j = JSON.parse(text);
         setConnections(j.connections ?? []);
       } catch (e) {
         console.error("settings: connections fetch error", e);
       }
     }
     if (status === "authenticated") {
       load();
       timer = setInterval(load, 8000);
     }
     return () => { if (timer) clearInterval(timer); };
   }, [status]);

Then update the rendering logic:

-                {connections.length === 0 ? (
+                {connections === null ? (
                   <div className="text-xs text-muted-foreground">Loading connections...</div>
+                ) : connections.length === 0 ? (
+                  <div className="text-xs text-muted-foreground">No connected accounts yet.</div>
                 ) : (
                   connections.map((c) => (

Also applies to: 129-131

apps/web/app/api/workspaces/[id]/metrics/route.ts (1)

4-16: Missing error handling and authentication.

The GET handler lacks both error handling and authentication checks, which were explicitly requested in previous reviews. Without authentication, any user can access metrics for any workspace ID. Without error handling, exceptions from ensureWorkspace or metric updates will crash the endpoint.

Apply this diff to add authentication and error handling:

+import { getServerSession } from "next-auth";
+import { createAuthConfig } from "@/lib/auth-config";

 export async function GET(_: Request, { params }: { params: Promise<{ id: string }> }) {
-  const { id } = await params;
-  const ws = ensureWorkspace(id);
-  // update with gentle jitter
-  ws.metrics.cpu = Math.max(1, Math.min(100, Math.round(jitter(ws.metrics.cpu, 0.2))));
-  ws.metrics.memory.usedGb = Math.max(1, Math.min(ws.metrics.memory.totalGb, Math.round(jitter(ws.metrics.memory.usedGb, 0.15))));
-  ws.metrics.disk.usedGb = Math.max(5, Math.min(ws.metrics.disk.totalGb, Math.round(jitter(ws.metrics.disk.usedGb, 0.1))));
-  ws.metrics.network.inMb = Math.max(10, Math.round(jitter(ws.metrics.network.inMb, 0.3)));
-  ws.metrics.network.outMb = Math.max(10, Math.round(jitter(ws.metrics.network.outMb, 0.3)));
-  ws.lastUpdate = Date.now();
-
-  return NextResponse.json({ ...ws.metrics, updatedAt: ws.lastUpdate });
+  try {
+    const session = await getServerSession(createAuthConfig());
+    if (!session?.user) {
+      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+    }
+
+    const { id } = await params;
+    const ws = ensureWorkspace(id);
+    
+    // update with gentle jitter
+    ws.metrics.cpu = Math.max(1, Math.min(100, Math.round(jitter(ws.metrics.cpu, 0.2))));
+    ws.metrics.memory.usedGb = Math.max(1, Math.min(ws.metrics.memory.totalGb, Math.round(jitter(ws.metrics.memory.usedGb, 0.15))));
+    ws.metrics.disk.usedGb = Math.max(5, Math.min(ws.metrics.disk.totalGb, Math.round(jitter(ws.metrics.disk.usedGb, 0.1))));
+    ws.metrics.network.inMb = Math.max(10, Math.round(jitter(ws.metrics.network.inMb, 0.3)));
+    ws.metrics.network.outMb = Math.max(10, Math.round(jitter(ws.metrics.network.outMb, 0.3)));
+    ws.lastUpdate = Date.now();
+
+    return NextResponse.json({ ...ws.metrics, updatedAt: ws.lastUpdate });
+  } catch (error) {
+    console.error("Failed to fetch workspace metrics:", error);
+    return NextResponse.json({ error: "Failed to fetch metrics" }, { status: 500 });
+  }
 }
apps/web/app/dashboard/page.tsx (1)

86-87: Sidebar collapse de-sync: replace static ml-64 with a CSS var.

-      <main className="ml-64 min-h-screen transition-all duration-300">
+      <main
+        className="min-h-screen transition-all duration-300"
+        style={{ marginLeft: "var(--sidebar-width, 16rem)" }}
+      >

Have <Sidebar …> set style={{ ['--sidebar-width' as any]: collapsed ? '5rem' : '16rem' }} on its root. Keeps content aligned during collapse.

apps/web/app/workspaces/[id]/ide/page.tsx (3)

107-118: Add error handling in doAction to avoid stuck UI.

   async function doAction(action: "start" | "stop" | "restart") {
     setActing(action);
     try {
       await fetch(`/api/workspaces/${id}/action`, {
         method: "POST",
         headers: { "Content-Type": "application/json" },
         body: JSON.stringify({ action }),
       });
       await loadAll();
+    } catch (error) {
+      console.error("Action failed:", error);
     } finally {
       setActing(null);
     }
   }

121-125: Handle errors when creating snapshots.

   async function createSnapshot() {
-    await fetch(`/api/workspaces/${id}/snapshots`, { method: "POST" });
-    const s = await fetch(`/api/workspaces/${id}/snapshots`, { cache: "no-store" }).then((r) => r.json());
-    setSnapshots(s.snapshots);
+    try {
+      const r = await fetch(`/api/workspaces/${id}/snapshots`, { method: "POST" });
+      if (!r.ok) throw new Error(`snapshot POST ${r.status}`);
+      const s = await fetch(`/api/workspaces/${id}/snapshots`, { cache: "no-store" });
+      if (!s.ok) throw new Error(`snapshot GET ${s.status}`);
+      const j = await s.json();
+      setSnapshots(Array.isArray(j.snapshots) ? j.snapshots : []);
+    } catch (error) {
+      console.error("Failed to create snapshot:", error);
+    }
   }

56-60: Prevent unauthorized UI flash; guard until authenticated.

Treat any non-"authenticated" status as loading and block main UI.

   useEffect(() => {
     if (status === "loading") return;
     if (!session) router.push("/signin");
   }, [status, session, router]);

-  if (status === "loading" || loading || !details || !metrics) {
+  if (status !== "authenticated" || loading || !details || !metrics) {
     return (
       <div className="min-h-screen flex items-center justify-center bg-background">
         <div className="flex items-center gap-3 text-muted-foreground">
           <Loader2 className="h-5 w-5 animate-spin" /> Opening workspace IDE...
         </div>
       </div>
     );
   }

Also applies to: 147-155

apps/web/app/api/workspaces/[id]/details/route.ts (1)

4-17: Add auth + error handling; return 401/403/500 appropriately.

This route is unauthenticated and lacks try/catch. Gate with your auth helper (getServerSession/auth) and verify access to workspace; handle errors to avoid leaking internals.

Apply:

 import { NextResponse } from "next/server";
 import { ensureWorkspace } from "@/app/api/_state/workspaces";

-export async function GET(_: Request, { params }: { params: Promise<{ id: string }> }) {
-  const { id } = await params;
-  const ws = ensureWorkspace(id);
-  return NextResponse.json({
-    id: ws.id,
-    name: ws.name,
-    provider: ws.provider,
-    size: ws.size,
-    region: ws.region,
-    status: ws.status,
-    assistant: ws.assistant,
-    updatedAt: Date.now(),
-  });
-}
+export async function GET(_: Request, { params }: { params: { id: string } }) {
+  // TODO: import your actual auth helper and options
+  // e.g., const session = await auth(); or await getServerSession(authOptions);
+  try {
+    // const session = await auth();
+    // if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+    // Optional: verify session user can access this workspace (owner/member check)
+    const { id } = params;
+    const ws = ensureWorkspace(id);
+    return NextResponse.json({
+      id: ws.id,
+      name: ws.name,
+      provider: ws.provider,
+      size: ws.size,
+      region: ws.region,
+      status: ws.status,
+      assistant: ws.assistant,
+      updatedAt: ws.lastUpdate ?? Date.now(),
+    });
+  } catch (error) {
+    console.error("details route error:", error);
+    return NextResponse.json({ error: "Failed to fetch details" }, { status: 500 });
+  }
+}

Confirm the correct auth import (auth/getServerSession + options) for this app and wire it in.

🧹 Nitpick comments (20)
apps/web/app/settings/change-password/page.tsx (1)

46-52: Enhance password input UX.

Consider these UX improvements for better usability and security:

  1. Add autocomplete attributes for password managers
  2. Display password requirements before user types
  3. Add help text explaining requirements
  4. Consider a password strength indicator

Apply this diff to improve the password inputs:

             <div className="space-y-2">
               <Label htmlFor="cur">Current password</Label>
               <Input 
                 id="cur" 
                 type="password" 
                 value={current} 
                 onChange={(e) => setCurrent(e.target.value)}
+                autocomplete="current-password"
               />
             </div>
             <div className="space-y-2">
-              <Label htmlFor="next">New password</Label>
+              <Label htmlFor="next">
+                New password
+                <span className="ml-2 text-xs text-muted-foreground font-normal">
+                  (min. 8 characters)
+                </span>
+              </Label>
               <Input 
                 id="next" 
                 type="password" 
                 value={next} 
                 onChange={(e) => setNext(e.target.value)}
+                autocomplete="new-password"
               />
             </div>
apps/web/app/billing-usage/page.tsx (3)

36-39: Consider checking explicit auth status.

While the current logic works, checking status === "unauthenticated" is more explicit than inferring from !session.

   useEffect(() => {
     if (status === "loading") return;
-    if (!session) router.push("/signin");
+    if (status === "unauthenticated") router.push("/signin");
   }, [status, session, router]);

41-61: Refine polling interval and error handling.

The 10-second polling interval is aggressive for billing data that typically updates much less frequently (hourly or daily). Additionally, fetch errors are silently logged without user notification, leaving stale data displayed.

Consider:

  1. Increase the polling interval to 60 seconds or higher
  2. Add error state to inform users when data fetch fails
  3. Consider stopping polls after consecutive failures
  const [data, setData] = useState<BillingData | null>(null);
  const [loading, setLoading] = useState(true);
+ const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    let timer: ReturnType<typeof setInterval> | undefined;
    async function load() {
      try {
        const res = await fetch("/api/billing", { cache: "no-store" });
+       if (!res.ok) throw new Error("Failed to fetch billing data");
        const j = (await res.json()) as BillingData;
        setData(j);
+       setError(null);
      } catch (e) {
        console.error(e);
+       setError("Unable to load billing data. Retrying...");
      } finally {
        setLoading(false);
      }
    }
    if (status === "authenticated") {
      load();
-     timer = setInterval(load, 10000); // realtime-ish polling
+     timer = setInterval(load, 60000); // Poll every minute
    }
    return () => {
      if (timer) clearInterval(timer);
    };
  }, [status]);

Then display the error in your UI.


75-91: Check response status and improve error handling.

The function converts the response to a blob without verifying the HTTP status, potentially downloading error responses as invoice files. Additionally, alert() provides poor user experience.

  async function downloadInvoice() {
    try {
      const res = await fetch("/api/billing/invoice", { method: "GET" });
+     if (!res.ok) {
+       throw new Error(`Failed to download invoice: ${res.status}`);
+     }
      const blob = await res.blob();
      const url = URL.createObjectURL(blob);
      const a = document.createElement("a");
      a.href = url;
      a.download = `invoice-${new Date().toISOString().slice(0, 7)}.txt`;
      document.body.appendChild(a);
      a.click();
      a.remove();
      URL.revokeObjectURL(url);
    } catch (e) {
      console.error(e);
-     alert("Could not download invoice.");
+     setError("Could not download invoice. Please try again later.");
    }
  }

Note: This function also depends on the /api/billing/invoice endpoint, which currently lacks authentication (see review of that file).

apps/web/app/api/reporting/route.ts (3)

12-15: Consider consolidating with the shared jitter function.

A similar jitter function exists in apps/web/app/api/_state/workspaces.ts (lines 29-33) with additional min and max parameters. Consider extracting a shared utility to avoid code duplication and ensure consistent behavior across endpoints.


29-29: Consider validating the range parameter.

The range parameter is cast to Range type but not validated. Invalid values will default to "last_7d" on line 29, but the points calculation on line 31 will still evaluate the invalid value and default to 30 * 24, creating inconsistency.

Add validation:

const rangeParam = searchParams.get("range") || "last_7d";
const validRanges: Range[] = ["last_24h", "last_7d", "last_30d", "this_month"];
const range = validRanges.includes(rangeParam as Range) 
  ? (rangeParam as Range) 
  : "last_7d";

27-62: Consider documenting that this endpoint returns synthetic data.

The endpoint generates mock telemetry data but doesn't indicate this in the response or through logging. Consider adding a field like isMockData: true to the response or adding a comment in the code to clarify this is for demonstration purposes.

apps/web/app/workspaces/page.tsx (3)

90-94: Non-functional search input.

The search input has proper accessibility with aria-label but no actual search functionality (onChange, onSubmit handlers). Consider either implementing search or removing this placeholder if not needed yet.


115-131: Reduce duplicate button styling logic.

The two toggle buttons have identical conditional className logic. Consider extracting this into a helper function or using a map to reduce duplication.

Refactor example:

const baseOptions = [
  { value: "image" as const, label: "From a Base Image" },
  { value: "dockerfile" as const, label: "From a Dockerfile" },
];

// In render:
<div className="flex gap-3">
  {baseOptions.map(({ value, label }) => (
    <button
      key={value}
      onClick={() => setBaseType(value)}
      className={`rounded-md border px-3 py-2 text-sm ${
        baseType === value
          ? "border-primary text-primary bg-primary/10"
          : "border-border text-muted-foreground hover:text-foreground"
      }`}
    >
      {label}
    </button>
  ))}
</div>

196-198: Consider explicit navigation for Cancel.

Using router.back() may not always navigate to the expected location if the user arrived at this page directly (e.g., via URL, bookmark, or external link). Consider navigating to a specific route like /workspaces or /dashboard instead.

-                  <Button variant="outline" onClick={() => router.back()}>
+                  <Button variant="outline" onClick={() => router.push("/dashboard")}>
                     Cancel
                   </Button>
apps/web/app/ai-agents/page.tsx (1)

105-224: LGTM: Well-structured UI with proper loading states.

The render logic properly handles loading and authentication states. The layout is responsive and follows the design system established in the sidebar component.

Optional improvement: Consider adding proper accessibility labels to the notification button (line 134) and avatar (line 135):

-<button className="h-9 w-9 rounded-md bg-card border border-border flex items-center justify-center text-muted-foreground">πŸ””</button>
+<button 
+  aria-label="Notifications"
+  className="h-9 w-9 rounded-md bg-card border border-border flex items-center justify-center text-muted-foreground"
+>
+  πŸ””
+</button>
-<div className="h-9 w-9 rounded-full bg-card border border-border flex items-center justify-center text-muted-foreground">R</div>
+<div 
+  role="img"
+  aria-label="User avatar"
+  className="h-9 w-9 rounded-full bg-card border border-border flex items-center justify-center text-muted-foreground"
+>
+  R
+</div>
apps/web/app/(auth)/signin/page.tsx (3)

36-38: Avoid redundant router.refresh() after navigation.

After push("/dashboard"), refresh triggers extra network work. Remove unless you rely on it for a known race.

-        router.push("/dashboard");
-        router.refresh();
+        router.push("/dashboard");

167-171: Hide decorative OAuth SVGs from screen readers.

Mark inline SVG icons as aria-hidden to prevent noisy announcements.

-                  <svg className="mr-2 h-4 w-4" fill="currentColor" viewBox="0 0 24 24">
+                  <svg className="mr-2 h-4 w-4" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
@@
-                  <svg className="mr-2 h-4 w-4" viewBox="0 0 24 24">
+                  <svg className="mr-2 h-4 w-4" viewBox="0 0 24 24" aria-hidden="true" focusable="false">

Also applies to: 178-197


217-217: Consider exporting the component directly and enabling SSR unless truly needed.

The dynamic SSR-disabled wrapper can be dropped if there’s no SSR-only mismatch. Keeps routing simpler and improves SEO.

apps/web/app/dashboard/page.tsx (2)

39-59: Make polling abortable and resilient.

Abort in-flight fetches on unmount/session change; keep loading state accurate.

   useEffect(() => {
-    let timer: ReturnType<typeof setInterval> | undefined;
+    let timer: ReturnType<typeof setInterval> | undefined;
+    const controller = new AbortController();
     async function load() {
       try {
-        const res = await fetch("/api/workspaces", { cache: "no-store" });
+        const res = await fetch("/api/workspaces", { cache: "no-store", signal: controller.signal });
+        if (!res.ok) throw new Error(`GET /api/workspaces ${res.status}`);
         const j = await res.json();
         setWorkspaces(j.workspaces ?? []);
       } catch (e) {
         console.error(e);
       } finally {
         setLoadingWs(false);
       }
     }
     if (session) {
       load();
       timer = setInterval(load, 10000);
     }
     return () => {
+      controller.abort();
       if (timer) clearInterval(timer);
     };
   }, [session]);

130-152: Handle action errors and reflect state immediately.

Check res.ok and optimistically update UI or re-fetch on success; show minimal feedback on failure.

-                      <button onClick={() => router.push(`/workspaces/${ws.id}/ide`)} className="text-primary hover:underline">Open IDE</button>
+                      <button onClick={() => router.push(`/workspaces/${ws.id}/ide`)} className="text-primary hover:underline">Open IDE</button>
                       <button
                         onClick={async () => {
-                          try { await fetch('/api/workspaces', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: ws.id, action: 'toggle' }) }); } catch {}
+                          try {
+                            const r = await fetch('/api/workspaces', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: ws.id, action: 'toggle' }) });
+                            if (!r.ok) throw new Error(`toggle ${r.status}`);
+                            // Optionally: update local state instead of waiting for poll
+                            setWorkspaces(prev => prev.map(w => w.id === ws.id ? { ...w, status: w.status === 'running' ? 'stopped' : 'running' } : w));
+                          } catch (e) { console.error(e); }
                         }}
                         className="hover:underline"
                       >
                         {ws.status === 'running' ? 'Stop' : 'Start'}
                       </button>
                       <button
-                        onClick={async () => { try { await fetch('/api/workspaces', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: ws.id, action: 'clone' }) }); } catch {} }}
+                        onClick={async () => { try { const r = await fetch('/api/workspaces', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: ws.id, action: 'clone' }) }); if (!r.ok) throw new Error(`clone ${r.status}`); } catch (e) { console.error(e); } }}
                         className="hover:underline"
                       >
                         Clone
                       </button>
                       <button
-                        onClick={async () => { try { await fetch('/api/workspaces', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: ws.id, action: 'delete' }) }); } catch {} }}
+                        onClick={async () => { try { const r = await fetch('/api/workspaces', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: ws.id, action: 'delete' }) }); if (!r.ok) throw new Error(`delete ${r.status}`); setWorkspaces(prev => prev.filter(w => w.id !== ws.id)); } catch (e) { console.error(e); } }}
                         className="text-rose-500 hover:underline"
                       >
                         Delete
                       </button>
apps/web/app/workspaces/[id]/ide/page.tsx (2)

162-163: Sync content margin with collapsible Sidebar; avoid static ml-64.

Same issue as Dashboard: replace ml-64 with CSS var and set it from Sidebar.

-      <main className="ml-64 min-h-screen transition-all duration-300">
+      <main className="min-h-screen transition-all duration-300" style={{ marginLeft: "var(--sidebar-width, 16rem)" }}>

246-248: Avoid array index as key for dynamic lists (terminal lines, tips).

Use stable ids if available, or combine content+index to reduce reconciliation issues.

-                  {terminal.map((l, i) => (
-                    <div key={i}>{l}</div>
+                  {terminal.map((l, i) => (
+                    <div key={`${i}-${l.slice(0,12)}`}>{l}</div>
                   ))}

And:

-                    {details.assistant.tips.map((t, i) => (
-                      <li key={i}>{t}</li>
+                    {details.assistant.tips.map((t, i) => (
+                      <li key={`${i}-${t.slice(0,12)}`}>{t}</li>
                     ))}

Also applies to: 230-233

apps/web/components/ide/SandboxRunner.tsx (1)

23-27: Use a real TS transpiler in a worker for safety and correctness.

The regex-based type stripping is fragile. Consider sucrase or SWC in a Web Worker to avoid blocking and improve fidelity.

Happy to draft a worker-based runner wired to sucrase.

apps/web/app/components/theme-provider.tsx (1)

8-10: LGTM! Consider setting default theme props.

The wrapper implementation is correct. If you want to ensure consistent theme configuration across the app, you might consider adding default props here:

export function ThemeProvider({ children, ...props }: Props) {
  return (
    <NextThemesProvider
      attribute="class"
      defaultTheme="system"
      enableSystem
      {...props}
    >
      {children}
    </NextThemesProvider>
  )
}

This would set sensible defaults while still allowing overrides via props. However, the current implementation is fine if you prefer to configure these at the call site.

πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 2db4631 and 00305f7.

β›” Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
πŸ“’ Files selected for processing (37)
  • apps/web/app/(auth)/signin/page.tsx (2 hunks)
  • apps/web/app/ai-agents/page.tsx (1 hunks)
  • apps/web/app/api/_state/workspaces.ts (1 hunks)
  • apps/web/app/api/account/connections/route.ts (1 hunks)
  • apps/web/app/api/account/delete/route.ts (1 hunks)
  • apps/web/app/api/account/password/route.ts (1 hunks)
  • apps/web/app/api/ai/agents/route.ts (1 hunks)
  • apps/web/app/api/ai/mcp-config/route.ts (1 hunks)
  • apps/web/app/api/billing/invoice/route.ts (1 hunks)
  • apps/web/app/api/billing/route.ts (1 hunks)
  • apps/web/app/api/reporting/route.ts (1 hunks)
  • apps/web/app/api/templates/route.ts (1 hunks)
  • apps/web/app/api/workspaces/[id]/action/route.ts (1 hunks)
  • apps/web/app/api/workspaces/[id]/details/route.ts (1 hunks)
  • apps/web/app/api/workspaces/[id]/metrics/route.ts (1 hunks)
  • apps/web/app/api/workspaces/[id]/snapshots/route.ts (1 hunks)
  • apps/web/app/api/workspaces/[id]/terminal/route.ts (1 hunks)
  • apps/web/app/api/workspaces/estimate/route.ts (1 hunks)
  • apps/web/app/api/workspaces/options/route.ts (1 hunks)
  • apps/web/app/api/workspaces/route.ts (1 hunks)
  • apps/web/app/billing-usage/page.tsx (1 hunks)
  • apps/web/app/components/theme-provider.tsx (1 hunks)
  • apps/web/app/dashboard/page.tsx (1 hunks)
  • apps/web/app/profile/page.tsx (3 hunks)
  • apps/web/app/reporting/page.tsx (1 hunks)
  • apps/web/app/settings/change-password/page.tsx (1 hunks)
  • apps/web/app/settings/page.tsx (1 hunks)
  • apps/web/app/workspaces/[id]/ide/page.tsx (1 hunks)
  • apps/web/app/workspaces/new/page.tsx (1 hunks)
  • apps/web/app/workspaces/page.tsx (1 hunks)
  • apps/web/components/ide/CodeEditor.tsx (1 hunks)
  • apps/web/components/ide/SandboxRunner.tsx (1 hunks)
  • apps/web/components/sidebar.tsx (1 hunks)
  • apps/web/lib/auth-config.ts (1 hunks)
  • apps/web/middleware.ts (1 hunks)
  • apps/web/package.json (1 hunks)
  • apps/web/tsconfig.json (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (16)
  • apps/web/app/api/workspaces/options/route.ts
  • apps/web/app/api/billing/route.ts
  • apps/web/app/api/workspaces/[id]/snapshots/route.ts
  • apps/web/app/api/workspaces/[id]/terminal/route.ts
  • apps/web/app/reporting/page.tsx
  • apps/web/app/api/ai/mcp-config/route.ts
  • apps/web/app/workspaces/new/page.tsx
  • apps/web/app/api/_state/workspaces.ts
  • apps/web/tsconfig.json
  • apps/web/app/api/workspaces/route.ts
  • apps/web/app/api/templates/route.ts
  • apps/web/app/api/account/delete/route.ts
  • apps/web/package.json
  • apps/web/app/api/account/password/route.ts
  • apps/web/app/api/workspaces/[id]/action/route.ts
  • apps/web/app/api/workspaces/estimate/route.ts
🧰 Additional context used
🧠 Learnings (1)
πŸ“š Learning: 2025-10-16T09:52:24.041Z
Learnt from: CR
PR: VAIBHAVSING/Dev8.dev#0
File: agent/AGENT.md:0-0
Timestamp: 2025-10-16T09:52:24.041Z
Learning: Applies to agent/apps/web/app/api/**/route.ts : Use Zod for input validation in Next.js API route handlers

Applied to files:

  • apps/web/app/api/ai/agents/route.ts
🧬 Code graph analysis (16)
apps/web/app/workspaces/[id]/ide/page.tsx (2)
apps/web/components/ide/SandboxRunner.tsx (2)
  • SandboxRunnerHandle (5-8)
  • SandboxRunner (15-58)
apps/web/components/ide/CodeEditor.tsx (1)
  • CodeEditor (17-39)
apps/web/app/dashboard/page.tsx (1)
apps/web/components/sidebar.tsx (1)
  • Sidebar (42-157)
apps/web/app/ai-agents/page.tsx (1)
apps/web/components/sidebar.tsx (1)
  • Sidebar (42-157)
apps/web/app/profile/page.tsx (1)
apps/web/lib/auth-config.ts (1)
  • session (97-102)
apps/web/app/api/workspaces/[id]/details/route.ts (1)
apps/web/app/api/_state/workspaces.ts (1)
  • ensureWorkspace (44-74)
apps/web/app/billing-usage/page.tsx (1)
apps/web/components/sidebar.tsx (1)
  • Sidebar (42-157)
apps/web/app/api/workspaces/[id]/metrics/route.ts (2)
apps/web/app/api/workspaces/[id]/details/route.ts (1)
  • GET (4-17)
apps/web/app/api/_state/workspaces.ts (2)
  • ensureWorkspace (44-74)
  • jitter (30-34)
apps/web/app/settings/change-password/page.tsx (1)
apps/web/components/sidebar.tsx (1)
  • Sidebar (42-157)
apps/web/app/(auth)/signin/page.tsx (2)
packages/ui/src/code.tsx (1)
  • Code (3-11)
apps/web/lib/auth-config.ts (1)
  • signIn (103-119)
apps/web/components/sidebar.tsx (1)
apps/web/app/settings/page.tsx (1)
  • Settings (13-187)
apps/web/app/api/reporting/route.ts (1)
apps/web/app/api/_state/workspaces.ts (1)
  • jitter (30-34)
apps/web/app/api/ai/agents/route.ts (2)
apps/web/app/api/account/connections/route.ts (1)
  • GET (8-44)
apps/web/app/api/workspaces/route.ts (2)
  • GET (11-19)
  • POST (21-45)
apps/web/app/workspaces/page.tsx (2)
apps/web/lib/auth-config.ts (1)
  • session (97-102)
apps/web/components/sidebar.tsx (1)
  • Sidebar (42-157)
apps/web/app/api/account/connections/route.ts (2)
apps/web/lib/auth-config.ts (2)
  • session (97-102)
  • createAuthConfig (17-132)
apps/web/lib/prisma.ts (1)
  • prisma (5-5)
apps/web/app/settings/page.tsx (1)
apps/web/components/sidebar.tsx (1)
  • Sidebar (42-157)
apps/web/app/api/billing/invoice/route.ts (1)
apps/web/app/api/billing/route.ts (1)
  • GET (19-58)
πŸͺ› Biome (2.1.2)
apps/web/app/profile/page.tsx

[error] 33-33: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

Hooks should not be called after an early return.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

πŸ”‡ Additional comments (15)
apps/web/components/sidebar.tsx (2)

1-17: LGTM! Imports are appropriate.

The imports are well-organized and all necessary for the component's functionality.


144-147: Note: Same tooltip accessibility pattern in bottom nav.

While bottomNavItems is currently empty (so this code doesn't render), the tooltip implementation here has the same keyboard accessibility limitation as the main navigation. If bottom nav items are added in the future, apply the same accessibility fixes.

apps/web/app/workspaces/page.tsx (3)

62-71: LGTM!

The loading state properly handles both client-side mounting and session initialization with clear visual feedback.


159-167: LGTM!

The customization script editor provides clear context with the description text and a sensible pre-populated default.


97-99: Navigation logic is correctβ€”original concern is unfounded.

The page at /workspaces is for creating workspace templates (see title "Create a New Workspace Template" at line 105). The button correctly navigates to /workspaces/new, which is a distinct feature for creating actual workspaces. These are two separate flows, not duplicate functionality. No action required.

Likely an incorrect or invalid review comment.

apps/web/app/ai-agents/page.tsx (3)

1-23: LGTM: Clean imports and type definitions.

The imports are well-organized and the interface definitions correctly match the API contract.


24-44: LGTM: Proper authentication and state setup.

The authentication redirect logic and state initialization follow Next.js best practices. The mounted pattern prevents hydration mismatches.


100-103: LGTM: Clean status indicator component.

The StatusDot component provides clear visual feedback with appropriate color mapping.

apps/web/middleware.ts (1)

15-16: LGTM! Secret resolution is consistent across the codebase.

The fallback from AUTH_SECRET to NEXTAUTH_SECRET aligns with the same approach in apps/web/lib/auth-config.ts and supports smooth migration between secret environment variables.

apps/web/app/api/account/connections/route.ts (1)

8-44: LGTM! Error handling addresses previous review feedback.

The implementation correctly:

  • Determines provider availability from environment variables
  • Handles unauthenticated sessions gracefully (empty connected set)
  • Wraps the Prisma query in try-catch to prevent crashes (lines 19-27)
  • Provides a top-level error handler with consistent JSON responses (lines 36-43)
apps/web/app/settings/page.tsx (1)

20-39: LGTM! Hooks are correctly ordered and error handling is thorough.

The useEffect hooks are placed before any conditional returns, which complies with React's Rules of Hooks. The timer cleanup and error handling are implemented correctly.

apps/web/components/ide/CodeEditor.tsx (1)

1-39: LGTM! Monaco editor wrapper is well-structured.

The implementation correctly:

  • Disables SSR for the browser-only Monaco editor
  • Provides strong typing with specific language options
  • Handles nullish onChange values gracefully
  • Configures sensible editor options (disabled minimap, word wrap, etc.)
apps/web/app/components/theme-provider.tsx (3)

1-1: Correct usage of client directive.

The 'use client' directive is appropriate since next-themes requires client-side rendering for theme management.


3-4: Verify the lint error is resolved.

The PR comment mentions a lint error that needs to be fixed. Please confirm that this implementation resolves the linting issues by running your linter locally.

If the linter complains about the React import being unused, note that it's actually used for the React.ComponentProps type utility on line 6, so it's necessary.


6-6: LGTM!

Using React.ComponentProps to extract the props type is a solid pattern that ensures type safety and full compatibility with the underlying NextThemesProvider.

Comment on lines +90 to +94
{error && (
<div className="rounded-lg border border-destructive/50 bg-destructive/10 p-3 glow-primary">
<p className="text-sm font-medium text-destructive">{error}</p>
</div>
)}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟑 Minor

Announce form errors to assistive tech.

Add role="alert" and aria-live to the error region.

-              {error && (
-                <div className="rounded-lg border border-destructive/50 bg-destructive/10 p-3 glow-primary">
+              {error && (
+                <div role="alert" aria-live="polite" className="rounded-lg border border-destructive/50 bg-destructive/10 p-3 glow-primary">
                   <p className="text-sm font-medium text-destructive">{error}</p>
                 </div>
               )}
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{error && (
<div className="rounded-lg border border-destructive/50 bg-destructive/10 p-3 glow-primary">
<p className="text-sm font-medium text-destructive">{error}</p>
</div>
)}
{error && (
<div role="alert" aria-live="polite" className="rounded-lg border border-destructive/50 bg-destructive/10 p-3 glow-primary">
<p className="text-sm font-medium text-destructive">{error}</p>
</div>
)}
πŸ€– Prompt for AI Agents
In apps/web/app/(auth)/signin/page.tsx around lines 90 to 94, the error message
container isn't announced to assistive tech; add accessibility attributes to the
error region by adding role="alert" and aria-live="assertive" (and optionally
aria-atomic="true") to the wrapping div so screen readers immediately announce
the error when it appears.

Comment on lines +12 to +14
export async function GET() {
return NextResponse.json({ agents });
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add authentication to the GET endpoint.

The GET handler allows unauthenticated access to agent data. Consider adding session validation to ensure only authenticated users can view agents.

Apply this diff to add authentication:

+import { getServerSession } from "next-auth/next";
+import { createAuthConfig } from "@/lib/auth-config";
+
 export async function GET() {
+  const session = await getServerSession(createAuthConfig());
+  if (!session) {
+    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+  }
   return NextResponse.json({ agents });
 }

Committable suggestion skipped: line range outside the PR's diff.

πŸ€– Prompt for AI Agents
In apps/web/app/api/ai/agents/route.ts around lines 12 to 14, the GET handler
currently returns agents without authentication; add session validation so only
authenticated users can access agents. Retrieve the current user/session from
the incoming request using the app's auth/session helper (e.g., getServerSession
or your auth utility), check that a valid user id exists, and if not return a
401/unauthorized NextResponse; if the user is authenticated, proceed to return
NextResponse.json({ agents }). Ensure any new auth helper is imported at the top
of the file and keep error/response messages minimal and consistent with other
API handlers.

const { searchParams } = new URL(req.url);
const range = (searchParams.get("range") as Range) || "last_7d";

const points = range === "last_24h" ? 24 : range === "last_7d" ? 7 * 24 : 30 * 24;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Incomplete handling of "this_month" range.

The points calculation doesn't explicitly handle the "this_month" case defined in the Range type. It falls through to the default 30 * 24, which may not accurately represent the current month's length (28-31 days) or the elapsed portion of the month.

Consider adding explicit logic:

-  const points = range === "last_24h" ? 24 : range === "last_7d" ? 7 * 24 : 30 * 24;
+  const points = 
+    range === "last_24h" ? 24 
+    : range === "last_7d" ? 7 * 24 
+    : range === "last_30d" ? 30 * 24
+    : new Date().getDate() * 24; // this_month: elapsed days in current month

Comment on lines +4 to +6
export async function GET(_: Request, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const ws = ensureWorkspace(id);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Incorrect params typing; avoid awaiting a non-Promise.

Route context params is a plain object in App Router. Use { params: { id: string } } and drop await params.

πŸ€– Prompt for AI Agents
In apps/web/app/api/workspaces/[id]/details/route.ts around lines 4 to 6, the
handler types `params` as a Promise and awaits it, but in App Router the route
context `params` is a plain object; change the function signature to accept {
params: { id: string } } (no Promise) and remove the `await params` so you
extract id directly and call ensureWorkspace(id).

Comment on lines +104 to 106
<button className="h-9 w-9 rounded-md bg-card border border-border flex items-center justify-center text-muted-foreground">
<span>πŸ””</span>
</button>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟑 Minor

Add an accessible name to the bell button.

Screen readers need a label.

-              <button className="h-9 w-9 rounded-md bg-card border border-border flex items-center justify-center text-muted-foreground">
+              <button aria-label="Notifications" title="Notifications" className="h-9 w-9 rounded-md bg-card border border-border flex items-center justify-center text-muted-foreground">
                 <span>πŸ””</span>
               </button>
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button className="h-9 w-9 rounded-md bg-card border border-border flex items-center justify-center text-muted-foreground">
<span>πŸ””</span>
</button>
<button aria-label="Notifications" title="Notifications" className="h-9 w-9 rounded-md bg-card border border-border flex items-center justify-center text-muted-foreground">
<span>πŸ””</span>
</button>
πŸ€– Prompt for AI Agents
In apps/web/app/dashboard/page.tsx around lines 104 to 106, the bell button only
contains an emoji and lacks an accessible name for screen readers; add an
accessible name by giving the button an aria-label (e.g., aria-label="Open
notifications" or "Notifications") or include an offscreen text node (visually
hidden) that describes the button, and optionally add a title attribute for
tooltip consistency.

Comment on lines +100 to +101
<button className="h-9 w-9 rounded-md bg-card border border-border flex items-center justify-center text-muted-foreground">πŸ””</button>
<div className="h-9 w-9 rounded-full bg-card border border-border flex items-center justify-center text-muted-foreground">R</div>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟑 Minor

Add accessibility labels to icon buttons.

The notification bell button (line 100) and user avatar (line 101) lack aria-label attributes, making them inaccessible to screen readers.

Apply this diff:

-              <button className="h-9 w-9 rounded-md bg-card border border-border flex items-center justify-center text-muted-foreground">πŸ””</button>
+              <button aria-label="Notifications" className="h-9 w-9 rounded-md bg-card border border-border flex items-center justify-center text-muted-foreground">πŸ””</button>
-              <div className="h-9 w-9 rounded-full bg-card border border-border flex items-center justify-center text-muted-foreground">R</div>
+              <div role="button" aria-label="User menu" tabIndex={0} className="h-9 w-9 rounded-full bg-card border border-border flex items-center justify-center text-muted-foreground">R</div>
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button className="h-9 w-9 rounded-md bg-card border border-border flex items-center justify-center text-muted-foreground">πŸ””</button>
<div className="h-9 w-9 rounded-full bg-card border border-border flex items-center justify-center text-muted-foreground">R</div>
<button aria-label="Notifications" className="h-9 w-9 rounded-md bg-card border border-border flex items-center justify-center text-muted-foreground">πŸ””</button>
<div role="button" aria-label="User menu" tabIndex={0} className="h-9 w-9 rounded-full bg-card border border-border flex items-center justify-center text-muted-foreground">R</div>
πŸ€– Prompt for AI Agents
In apps/web/app/workspaces/page.tsx around lines 100-101, the notification bell
and avatar elements are missing accessibility labels; add
aria-label="Notifications" to the bell button and add an accessible label to the
avatar (for example aria-label="User profile" and role="img" on the avatar
element, or convert it to a button with aria-label="User profile" if it opens a
menu) so screen readers can describe them.

Comment on lines +26 to +36
const js = language === "typescript" ? code.replace(/:\s*[^=;,)]+/g, "") : code;
const html = `<!doctype html><html><head><meta charset=\"utf-8\" /><style>body{font:12px monospace;color:#e2e8f0;background:#0f172a;padding:8px}</style></head><body><script>
const send = (type, args) => parent.postMessage({ type, args }, '*');
const origLog = console.log, origErr = console.error;
console.log = (...a) => { send('log', a.map(x=>String(x))); origLog(...a); };
console.error = (...a) => { send('error', a.map(x=>String(x))); origErr(...a); };
try {
${js}
} catch (e) { console.error(e && e.stack ? e.stack : String(e)); }
<\/script></body></html>`;
iframe.srcdoc = html;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Escape closing </script> in user code to prevent premature script termination.

If user code contains "</script>", the script tag can be closed early. Escape it before injecting.

-        const js = language === "typescript" ? code.replace(/:\s*[^=;,)]+/g, "") : code;
+        const stripTypes = (src: string) => src.replace(/:\s*[^=;,)]+/g, "");
+        const raw = language === "typescript" ? stripTypes(code) : code;
+        // Prevent premature </script> termination
+        const js = raw.replace(/<\/script/gi, "<\\/script");
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const js = language === "typescript" ? code.replace(/:\s*[^=;,)]+/g, "") : code;
const html = `<!doctype html><html><head><meta charset=\"utf-8\" /><style>body{font:12px monospace;color:#e2e8f0;background:#0f172a;padding:8px}</style></head><body><script>
const send = (type, args) => parent.postMessage({ type, args }, '*');
const origLog = console.log, origErr = console.error;
console.log = (...a) => { send('log', a.map(x=>String(x))); origLog(...a); };
console.error = (...a) => { send('error', a.map(x=>String(x))); origErr(...a); };
try {
${js}
} catch (e) { console.error(e && e.stack ? e.stack : String(e)); }
<\/script></body></html>`;
iframe.srcdoc = html;
const stripTypes = (src: string) => src.replace(/:\s*[^=;,)]+/g, "");
const raw = language === "typescript" ? stripTypes(code) : code;
// Prevent premature </script> termination
const js = raw.replace(/<\/script/gi, "<\\/script");
const html = `<!doctype html><html><head><meta charset=\"utf-8\" /><style>body{font:12px monospace;color:#e2e8f0;background:#0f172a;padding:8px}</style></head><body><script>
const send = (type, args) => parent.postMessage({ type, args }, '*');
const origLog = console.log, origErr = console.error;
console.log = (...a) => { send('log', a.map(x=>String(x))); origLog(...a); };
console.error = (...a) => { send('error', a.map(x=>String(x))); origErr(...a); };
try {
${js}
} catch (e) { console.error(e && e.stack ? e.stack : String(e)); }
<\/script></body></html>`;
iframe.srcdoc = html;
πŸ€– Prompt for AI Agents
In apps/web/components/ide/SandboxRunner.tsx around lines 26 to 36, the injected
user JS can contain a literal "</script>" which will prematurely terminate the
enclosing script tag; before interpolating user code into the html string,
replace occurrences of "</script>" (case-insensitive) with an escaped form such
as "<\\/script>" or break the sequence (e.g. "<" + "/script>") so the browser
does not close the host script; perform this replacement on the js variable
after the TypeScript stripping step and before embedding into html, keeping
other sanitization intact.

Comment on lines +44 to +54
React.useEffect(() => {
const handler = (ev: MessageEvent) => {
if (ev?.data?.type === "log") {
onLog?.(ev.data.args.join(" "));
} else if (ev?.data?.type === "error") {
onLog?.(ev.data.args.join(" "));
}
};
window.addEventListener("message", handler);
return () => window.removeEventListener("message", handler);
}, [onLog]);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟑 Minor

Filter postMessage by the sandbox iframe source.

Guard against messages from other frames.

-    React.useEffect(() => {
-      const handler = (ev: MessageEvent) => {
+    React.useEffect(() => {
+      const handler = (ev: MessageEvent) => {
+        const fromSandbox = ev.source === iframeRef.current?.contentWindow;
+        if (!fromSandbox) return;
         if (ev?.data?.type === "log") {
           onLog?.(ev.data.args.join(" "));
         } else if (ev?.data?.type === "error") {
           onLog?.(ev.data.args.join(" "));
         }
       };
       window.addEventListener("message", handler);
       return () => window.removeEventListener("message", handler);
-    }, [onLog]);
+    }, [onLog]);
πŸ€– Prompt for AI Agents
In apps/web/components/ide/SandboxRunner.tsx around lines 44 to 54, the
postMessage handler does not verify the message source so it may process
messages from other frames; update the handler to ignore messages not coming
from the sandbox iframe by checking ev.source ===
iframeRef.current?.contentWindow (and optionally check ev.origin if you have a
fixed expected origin), returning early when the check fails, then proceed to
handle "log" and "error" as before; ensure the iframeRef is defined in this
component and include it in the effect dependency array.

const navItems: NavItem[] = [
{ title: "Dashboard", href: "/dashboard", icon: LayoutDashboard },
{ title: "Workspaces", href: "/workspaces", icon: Boxes },
{ title: "Ai Agents", href: "/ai-agents", icon: Bot },
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟑 Minor

Fix capitalization of "AI Agents".

"AI" should be capitalized as it's an acronym for Artificial Intelligence.

Apply this diff:

-  { title: "Ai Agents", href: "/ai-agents", icon: Bot },
+  { title: "AI Agents", href: "/ai-agents", icon: Bot },
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{ title: "Ai Agents", href: "/ai-agents", icon: Bot },
{ title: "AI Agents", href: "/ai-agents", icon: Bot },
πŸ€– Prompt for AI Agents
In apps/web/components/sidebar.tsx around line 33, the menu item label uses "Ai
Agents" which incorrectly capitalizes the acronym; change the title string to
"AI Agents" (i.e., replace "Ai Agents" with "AI Agents") so the acronym is
properly uppercased everywhere this label is used.

Comment on lines 124 to 130
secret: (() => {
if (process.env.NODE_ENV === "production" && !process.env.AUTH_SECRET) {
throw new Error("AUTH_SECRET must be set in production environments.");
const secret = process.env.AUTH_SECRET || process.env.NEXTAUTH_SECRET || "development-secret";
if (process.env.NODE_ENV === "production" && !secret) {
throw new Error("AUTH_SECRET or NEXTAUTH_SECRET must be set in production environments.");
}
return process.env.AUTH_SECRET || "development-secret";
return secret;
})(),
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | πŸ”΄ Critical

Critical: Production guard is ineffective due to fallback ordering.

The production environment check on line 126 will never throw because secret is assigned a fallback value ("development-secret") on line 125 before the check. This allows production to run with the insecure development secret.

Apply this diff to check for the presence of environment variables before applying the development fallback:

     secret: (() => {
-      const secret = process.env.AUTH_SECRET || process.env.NEXTAUTH_SECRET || "development-secret";
-      if (process.env.NODE_ENV === "production" && !secret) {
+      if (process.env.NODE_ENV === "production" && !process.env.AUTH_SECRET && !process.env.NEXTAUTH_SECRET) {
         throw new Error("AUTH_SECRET or NEXTAUTH_SECRET must be set in production environments.");
       }
+      const secret = process.env.AUTH_SECRET || process.env.NEXTAUTH_SECRET || "development-secret";
       return secret;
     })(),
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
secret: (() => {
if (process.env.NODE_ENV === "production" && !process.env.AUTH_SECRET) {
throw new Error("AUTH_SECRET must be set in production environments.");
const secret = process.env.AUTH_SECRET || process.env.NEXTAUTH_SECRET || "development-secret";
if (process.env.NODE_ENV === "production" && !secret) {
throw new Error("AUTH_SECRET or NEXTAUTH_SECRET must be set in production environments.");
}
return process.env.AUTH_SECRET || "development-secret";
return secret;
})(),
secret: (() => {
if (process.env.NODE_ENV === "production" && !process.env.AUTH_SECRET && !process.env.NEXTAUTH_SECRET) {
throw new Error("AUTH_SECRET or NEXTAUTH_SECRET must be set in production environments.");
}
const secret = process.env.AUTH_SECRET || process.env.NEXTAUTH_SECRET || "development-secret";
return secret;
})(),
πŸ€– Prompt for AI Agents
In apps/web/lib/auth-config.ts around lines 124 to 130, the current code assigns
a development fallback before checking production, so the production guard never
throws; change the logic to first read process.env.AUTH_SECRET and
process.env.NEXTAUTH_SECRET, if NODE_ENV === "production" and neither env var is
set throw the Error, otherwise set secret to the first available env var or the
"development-secret" fallback; return that secret.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants