Skip to content

Sidebar redesign (#1012)#1019

Closed
Developing-Gamer wants to merge 51 commits intodevfrom
dashboard-redesign
Closed

Sidebar redesign (#1012)#1019
Developing-Gamer wants to merge 51 commits intodevfrom
dashboard-redesign

Conversation

@Developing-Gamer
Copy link
Contributor

@Developing-Gamer Developing-Gamer commented Nov 18, 2025

https://www.loom.com/share/c725fbfe9dd84e649d6d55d7a148348b

Summary by CodeRabbit

  • Documentation

    • Added a comprehensive dashboard design guide covering patterns, colors, typography, components, dark mode, and accessibility.
  • New Features

    • Collapsible, animated sidebar with improved navigation and breadcrumbs.
    • Configurable dashboard widgets, time-range controls, richer charts, and interactive activity grids.
    • Enhanced app store pages, enablement flow with stage warnings, copyable text UI, and app card/list variants.
  • Improvements

    • Simplified globe visualization and loading, responsive layout updates, accessibility and performance refinements.

✏️ Tip: You can customize this high-level summary in your review settings.


Note

Redesigns the sidebar and dashboard with widgetized metrics and improved charts, adds a guided Vercel setup flow, enhances the apps store/enablement and project settings, and introduces a design guide.

  • Documentation
    • Add apps/dashboard/DESIGN_GUIDE.md for patterns, dark mode, accessibility, and component conventions.
  • Navigation/Sidebar
    • Collapsible/expandable sidebar with compact mode, tooltips, improved styles, and refined breadcrumb resolution.
    • Fixed companion panel and responsive header updates.
  • Dashboard Overview
    • Widget framework with time-range toggle and modular cards (apps, DAU, sign-ups, globe).
    • Refactor charts: TabbedMetricsCard, LineChartDisplay, DonutChartDisplay, custom tooltips, weekend styling, lists with user actions.
    • Simplify 3D globe rendering, controls, tooltip, and sizing.
  • Apps
    • App grid with search, category tabs, installed-first sorting; redesigned AppStoreEntry.
    • AppSquare enable/disable with alpha/beta warning modal; new AppWarningModal component.
  • Integrations
    • Vercel setup: step-based cards with progress, auto-expand, action buttons, and completion confetti.
  • Settings
    • Copyable JWKS/Project ID via SettingCopyableText and CopyableText; form/UX improvements; transfer flow tweaks.
  • Components/Infra
    • New CopyableText; Link onClick signature; apps-frontend typing/asset tweaks; assorted UI polish and accessibility.

Written by Cursor Bugbot for commit 197c365. This will update automatically on new commits. Configure here.

@Developing-Gamer Developing-Gamer self-assigned this Nov 18, 2025
@vercel
Copy link

vercel bot commented Nov 18, 2025

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

Project Deployment Preview Comments Updated (UTC)
stack-backend Ready Ready Preview Comment Nov 25, 2025 11:41am
stack-dashboard Error Error Nov 25, 2025 11:41am
stack-demo Ready Ready Preview Comment Nov 25, 2025 11:41am
stack-docs Ready Ready Preview Comment Nov 25, 2025 11:41am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 18, 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

Adds a new DESIGN_GUIDE and extensive UI refactors across the dashboard: collapsible animated sidebar and nav refactor, chart and widget abstractions (time-range, donut/line), simplified globe, apps store/app tile redesign, new helper components (copyable text, warning modal), and major settings/form primitives overhaul.

Changes

Cohort / File(s) Summary
Design guide
apps/dashboard/DESIGN_GUIDE.md
New design guide documenting page structure, color system (light/dark), typography, components (Card, Badge, Tabs, Progress), spacing/layout patterns, interactive elements, accessibility (WCAG AA) and a design-consistency checklist.
Sidebar & Layout
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sidebar-layout.tsx
Replaced static left sidebar with a collapsible, width-animated sidebar; added isCollapsed state and toggle; introduced AppNavItem, refactored NavItem/NavSubItem, improved active/highlight detection using pathname parsing, memoized selections, and updated breadcrumb/header behavior; mobile Sheet retained.
Overview — charts, widgets & metrics
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/charts-section-with-data.tsx, .../line-chart.tsx, .../metrics-page.tsx
Introduced theme-aware chart configs, ChartCard, TimeRange type and toggle, TabbedMetricsCard/donut charts, replaced tables with interactive card grids, and added a widget-driven MetricsPage with persisted DashboardConfig, new widgets, and responsive layouts.
Overview — globe
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe.tsx
Simplified GlobeSection layout and state, consolidated sizing and tooltip position, streamlined hover/selection logic, simplified polygon rendering, updated globe controls and loading UI, preserved error refresh.
Vercel integration UI
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/vercel/page-client.tsx
Replaced static checklist with structured VercelStep/VercelSubStep model, dynamic StepCard/ChecklistRow, computed progress with animation, auto-expand next step, and confetti on completion.
Apps listing & app store
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/apps/page-client.tsx, apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/apps/[appId]/page-client.tsx, apps/dashboard/src/components/app-store-entry.tsx
Apps page rewired to searchable, categorized tabs with filtering, counts and environment gating; app client page adds stronger type checks and enable flow error handling; AppStoreEntry upgraded to hero, features grid, screenshots preview and stage warnings.
AppSquare / AppIcon / AppListItem
apps/dashboard/src/components/app-square.tsx
Reworked app tile UI to card-like tile with hover actions, enable/disable flow, warning modal for alpha/beta, added AppIcon signature changes (size, variant, showBadge), AppSquareVariant type and AppListItem export; added processing guards and toggle callbacks.
Modals & small components
apps/dashboard/src/components/app-warning-modal.tsx, apps/dashboard/src/components/copyable-text.tsx, apps/dashboard/src/components/link.tsx
Added AppWarningModal (alpha/beta enable warning), CopyableText (inline copy button), and updated LinkProps to accept mouse event and title.
Settings & form primitives
apps/dashboard/src/components/settings.tsx
Major refactor: memoized setting components (SettingCard, SettingSwitch, SettingInput, SettingSelect, SettingCopyableText), new generic FormSettingCard with enhanced submission lifecycle, async handling wrappers, and updated UI/hint/copyable behaviors.
Lib / types
apps/dashboard/src/lib/apps-frontend.tsx
Added public type alias export type AppId = keyof typeof ALL_APPS; and adjusted app logo rendering container.
Project pages & misc
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/project-settings/page-client.tsx, apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/apps/[appId]/page-client.tsx
Project settings: memoized lookups, copyable URLs, transfer flow with runAsynchronouslyWithAlert, toast feedback, and team member item UI; app page: stricter frontend resolution and error handling.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Header as HeaderToggle
  participant Layout as SidebarLayout
  participant Sidebar as SidebarContent
  participant Main as MainContent

  Header->>Layout: click collapse toggle
  Layout->>Layout: set isCollapsed state
  Layout->>Sidebar: pass isCollapsed, toggleCollapsed
  Sidebar-->>Layout: render collapsed/expanded UI
  Layout->>Main: reflow main content (adjust margin/width)
  Note over Sidebar,Main: animated width transition, tooltips/chevrons update
Loading
sequenceDiagram
  autonumber
  participant Page as MetricsPage
  participant Storage as localStorage
  participant Conductor as MetricsContent
  participant Widget as WidgetComponent
  participant Time as TimeRangeToggle

  Page->>Storage: loadConfig()
  Storage-->>Page: dashboardConfig
  Page->>Conductor: render with dashboardConfig, timeRange
  Conductor->>Widget: render enabled widgets in widgetOrder
  Time->>Conductor: user toggles timeRange
  Conductor->>Widget: re-render with new timeRange
  Page->>Storage: saveConfig(updatedConfig)
Loading

Estimated code review effort

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

Focus review on:

  • Sidebar navigation URL matching, memoization dependencies, and mobile Sheet interactions.
  • New/changed public component signatures: AppIcon/AppSquare/AppListItem, TimeRange and chart component props, LinkProps change.
  • MetricsPage widget persistence (load/save localStorage) and widget rendering order.
  • FormSettingCard generic lifecycle, validation, reset behavior, and async error handling.
  • Concurrency/state guards in app enable/disable flows and AppWarningModal integration.
  • Chart and globe rendering edge cases: tooltip positioning, camera controls, and performance.

Possibly related PRs

Poem

🐰 I hopped through code with eager paws,

New tiles, toggles, charts and laws.
Collapsed the side, made widgets sing,
Copyable keys and modal spring.
A tiny rabbit cheers—deploy, hooray, let's bring!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.50% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Sidebar redesign (#1012)' directly reflects the primary change in the pull request—a comprehensive redesign of the sidebar component with collapsible states, improved styling, and navigation updates.
Description check ✅ Passed The PR description provides a comprehensive summary with a Loom video link and detailed bullet-point breakdown covering documentation, navigation, dashboard, apps, integrations, settings, and infrastructure changes, though it lacks explicit structured sections matching a template.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dashboard-redesign

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.

@Developing-Gamer Developing-Gamer marked this pull request as ready for review November 23, 2025 18:39
Copilot AI review requested due to automatic review settings November 23, 2025 18:39
@claude
Copy link
Contributor

claude bot commented Nov 23, 2025

Claude finished @Developing-Gamer's task —— View job


PR Review: Sidebar Redesign

Review Summary

This is a comprehensive sidebar redesign PR that includes both a new design guide and significant improvements to the sidebar layout component. The changes are well-structured and demonstrate attention to both functionality and design consistency.

Review Checklist:

  • Read and analyze changed files
  • Review code quality and best practices
  • Check for potential bugs or issues
  • Evaluate performance considerations
  • Assess security concerns
  • Review test coverage needs
  • Provide constructive feedback

🎯 Overall Assessment

Strengths:

  • Excellent documentation: The 814-line DESIGN_GUIDE.md is comprehensive and provides clear patterns for consistent UI development
  • Performance optimizations: Good use of useMemo, useCallback, and proper dependency management
  • Accessibility: Proper ARIA attributes and semantic HTML structure
  • Dark mode support: Comprehensive theming with proper contrast ratios
  • Mobile responsiveness: Well-implemented responsive behavior with proper touch interactions

📋 Detailed Code Review

apps/dashboard/DESIGN_GUIDE.md

Excellent Addition

  • Comprehensive design system documentation covering colors, typography, spacing, and components
  • Strong emphasis on dark mode compatibility and accessibility (WCAG AA compliance)
  • Well-structured with clear examples and best practices
  • The color contrast requirements and dark mode patterns are particularly well thought out

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sidebar-layout.tsx

Code Quality: Very Good 📈

Positive Changes:

  1. Performance Improvements (lines 396-428):

    const enabledApps = useMemo(() => 
      typedEntries(config.apps.installed)
        .filter(([appId, appConfig]) => appConfig?.enabled && appId in ALL_APPS)
        .map(([appId]) => appId as AppId),
      [config.apps.installed]
    );
    • Proper memoization prevents unnecessary recalculations
    • Good use of useCallback for event handlers
  2. Improved State Management (lines 405-428):

    • Smart default expansion logic based on current URL
    • Clean toggle implementation with Set operations
  3. Better Visual Design (lines 252-280):

    • Consistent styling with proper hover states
    • Good use of CSS-in-JS with conditional classes
    • Proper transition animations

🐛 Potential Issues & Recommendations

Minor Issues:

  1. Type Safety (line 456):

    // Current - could be more type-safe
    const navItemData = {
      name: app.displayName,
      appId,
      items: appFrontend.navigationItems.map((navItem) => ({

    Recommendation: Consider extracting this to a typed helper function or using a more explicit type annotation.

  2. Error Boundary (lines 223-229):

    const currentUrl = useMemo(() => {
      try {
        return new URL(pathname, DUMMY_ORIGIN);
      } catch {
        return null;
      }
    }, [pathname]);

    Good: Error handling is present, but consider logging errors for debugging in development.

  3. Accessibility Enhancement (line 621):

    <SheetContent
      aria-describedby={undefined}
      side='left' className="w-[248px] bg-background p-0 backdrop-blur-sm" hasCloseButton={false}>

    Recommendation: Consider providing a proper aria-describedby instead of undefined.

Performance Considerations:

  1. Bundle Size: The component imports many utilities. Consider code-splitting if this becomes a performance bottleneck.

  2. Re-renders: The current memoization strategy is good. Consider adding React.memo to NavItem and NavSubItem if performance testing shows frequent re-renders.

🔒 Security Assessment

Status: Good

  • No direct security vulnerabilities identified
  • Proper URL handling with error boundaries
  • Safe use of external links with appropriate attributes
  • No XSS vectors in the dynamic content rendering

🧪 Test Coverage Recommendations

The PR lacks test coverage. Consider adding:

  1. Unit Tests:

    • NavItem component behavior (expansion/collapse)
    • Breadcrumb resolution logic
    • URL matching functions
  2. Integration Tests:

    • Mobile sidebar functionality
    • Theme switching behavior
    • Navigation state persistence
  3. Accessibility Tests:

    • Keyboard navigation
    • Screen reader compatibility
    • Focus management

📊 Performance Metrics

Positive Impact:

  • Memoization reduces unnecessary re-renders
  • Efficient state management with Sets
  • Proper cleanup in useEffect

Areas for Monitoring:

  • Bundle size impact of comprehensive design guide
  • Runtime performance with many navigation items

🎨 Design System Compliance

The new DESIGN_GUIDE.md establishes excellent patterns:

  • ✅ Consistent spacing system
  • ✅ Proper color tokens for light/dark mode
  • ✅ Typography hierarchy
  • ✅ Component composition patterns
  • ✅ Accessibility guidelines

🏆 Recommendations for Merge

Ready to Merge with minor considerations:

  1. Add basic unit tests for the navigation logic
  2. Consider logging errors in development for the URL parsing
  3. Document the performance optimizations in code comments
  4. Add JSDoc comments for the complex helper functions

📈 Long-term Suggestions

  1. Component Library: The design guide could evolve into a proper component library
  2. Storybook Integration: Consider adding Storybook stories for the navigation components
  3. Performance Monitoring: Track Core Web Vitals impact of the new sidebar
  4. User Testing: Validate the new navigation patterns with user feedback

🎯 Final Score

Code Quality: 8.5/10
Documentation: 10/10
Performance: 8/10
Accessibility: 8/10
Maintainability: 9/10

Overall: Excellent work! This PR significantly improves both the codebase quality and establishes important design patterns for future development.

⚠️ Build Note

The hook feedback indicates pnpm is not available in the environment, which prevented running type checking and linting. Based on the code review, the TypeScript looks well-typed and should pass type checking. Consider running the build locally to verify before merging.


@Developing-Gamer Developing-Gamer review requested due to automatic review settings November 23, 2025 18:39
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Nov 23, 2025

Greptile Overview

Greptile Summary

This PR implements a comprehensive sidebar redesign that significantly improves the visual hierarchy and user experience of the dashboard navigation.

Key Changes:

  • Completely redesigned NavItem and NavSubItem components with modern styling including rounded borders, subtle backgrounds, and smooth transitions
  • Added proper dark mode support with theme-aware colors (text-foreground, bg-background, etc.) and explicit dark mode variants (dark:text-blue-400, dark:bg-blue-500/20)
  • Improved active state indicators: sections and items now have blue highlight styling with increased opacity backgrounds and colored borders
  • Added visual breadcrumb-style dots for sub-items instead of plain text links
  • Enhanced performance with proper memoization using useCallback and useMemo to prevent unnecessary re-renders
  • Created comprehensive DESIGN_GUIDE.md documenting color systems, typography, dark mode requirements, and component patterns for future consistency

Design Improvements:

  • Active items now have border-blue-500/40 bg-blue-500/10 styling with shadow for clear visual feedback
  • Inactive items have subtle hover states with hover:border-blue-500/20 hover:bg-blue-500/5
  • Sub-items feature small circular indicators that change color based on active state
  • Section expansion now uses smooth height transitions with proper overflow handling

Confidence Score: 5/5

  • This PR is safe to merge with no issues found
  • The changes are purely visual/UI improvements with proper dark mode support, no logic bugs, no security concerns, and good performance optimizations. The code follows React best practices with proper memoization and the design guide ensures future consistency.
  • No files require special attention

Important Files Changed

File Analysis

Filename Score Overview
apps/dashboard/DESIGN_GUIDE.md 5/5 New comprehensive design guide documenting UI patterns, color system, dark mode support, and component conventions for dashboard consistency
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sidebar-layout.tsx 5/5 Complete sidebar redesign with improved visual hierarchy, dark mode support, better active state indicators, and performance optimizations using memoization

Sequence Diagram

sequenceDiagram
    participant User
    participant SidebarLayout
    participant NavItem
    participant NavSubItem
    participant Router

    User->>SidebarLayout: Navigate to page
    SidebarLayout->>SidebarContent: Render sidebar
    SidebarContent->>SidebarContent: Get enabled apps from config
    SidebarContent->>SidebarContent: getDefaultExpandedSections()
    SidebarContent->>NavItem: Render overview item
    
    loop For each enabled app
        SidebarContent->>NavItem: Render app section
        NavItem->>NavItem: Check if section is active
        NavItem->>NavItem: Apply highlight styles if active
        alt Section is expanded
            NavItem->>NavSubItem: Render navigation items
            NavSubItem->>NavSubItem: Check if item is active
            NavSubItem->>NavSubItem: Apply highlight styles if active
        end
    end
    
    SidebarContent->>NavItem: Render bottom items
    
    User->>NavItem: Click navigation item
    NavItem->>Router: Navigate to href
    Router->>SidebarLayout: Update pathname
    SidebarLayout->>NavItem: Re-render with new active state
    NavItem->>NavItem: Update highlight based on new pathname
    
    User->>NavItem: Click section toggle
    NavItem->>SidebarContent: toggleSection(appId)
    SidebarContent->>SidebarContent: Update expandedSections state
    SidebarContent->>NavItem: Re-render with new expanded state
    NavItem->>NavItem: Animate expansion/collapse
Loading

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

2 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

Copy link

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 is a comprehensive sidebar redesign PR that modernizes the dashboard UI with improved navigation, performance optimizations, and enhanced user experience features.

Key Changes

  • Complete sidebar redesign with collapsible functionality and improved visual hierarchy
  • New app management UI with filtering, search, and warning modals for alpha/beta apps
  • Performance optimizations using React.memo, useCallback, and useMemo throughout components
  • Enhanced metrics dashboard with configurable widgets, time range filtering, and improved chart visualizations
  • Comprehensive design guide documentation for maintaining consistency across the dashboard

Reviewed changes

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

Show a summary per file
File Description
apps-frontend.tsx Updated logo styling and added AppId type export
settings.tsx Added React.memo optimizations and new SettingCopyableText component
link.tsx Enhanced onClick handler to receive event parameter
copyable-text.tsx New reusable component for copyable text with visual feedback
app-warning-modal.tsx New modal component for alpha/beta app warnings
app-store-entry.tsx Redesigned app store entry with enhanced visual hierarchy
app-square.tsx Major refactoring with new hover interactions and enable/disable functionality
vercel/page-client.tsx Redesigned with confetti celebrations and improved step tracking
sidebar-layout.tsx Implemented collapsible sidebar with tooltips and performance optimizations
project-settings/page-client.tsx Added memoization and improved team member rendering
apps/page-client.tsx New filtering and search functionality with category tabs
apps/[appId]/page-client.tsx Added error handling for missing app frontends
metrics-page.tsx Complete dashboard redesign with configurable widgets
line-chart.tsx Enhanced chart components with tabbed metrics and improved tooltips
globe.tsx Simplified globe visualization with better interaction patterns
charts-section-with-data.tsx Updated layout with improved user activity displays
DESIGN_GUIDE.md Comprehensive design system documentation with dark mode guidelines

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 81 to 95
const performToggle = async () => {
setIsProcessing(true);

try {
await project.updateConfig({
[`apps.installed.${appId}.enabled`]: !isEnabled,
});
onToggleEnabled?.(!isEnabled);
} catch (error) {
console.error(`Failed to ${isEnabled ? 'disable' : 'enable'} app:`, error);
alert(`Failed to ${isEnabled ? 'disable' : 'enable'} ${app.displayName}. Please try again.`);
} finally {
setIsProcessing(false);
}
};
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

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

The performToggle function is missing error handling. If project.updateConfig fails, the processing state will be set to false in the finally block, but the user won't receive proper feedback about the failure. Consider wrapping the entire operation in a try-catch block before the alert call on line 91.

Copilot uses AI. Check for mistakes.
Comment on lines 75 to 94
const handleGenerateKeys = async () => {
if (isGenerating) return;
setIsGenerating(true);
setError(null);

try {
const newKey = await adminApp.createInternalApiKey({
hasPublishableClientKey: true,
hasSecretServerKey: true,
hasSuperSecretAdminKey: false,
expiresAt: new Date(Date.now() + TWO_HUNDRED_YEARS_IN_MS),
description: "Vercel Integration",
});

setKeys({
projectId: adminApp.projectId,
publishableClientKey: newKey.publishableClientKey!,
secretServerKey: newKey.secretServerKey!,
});
} catch (err) {
console.error(err);
setError(err instanceof Error ? err.message : "Failed to generate API keys. Please try again.");
} finally {
setIsGenerating(false);
}
const newKey = await adminApp.createInternalApiKey({
hasPublishableClientKey: true,
hasSecretServerKey: true,
hasSuperSecretAdminKey: false,
expiresAt: new Date(Date.now() + TWO_HUNDRED_YEARS_IN_MS),
description: "Vercel Integration",
});

setKeys({
projectId: adminApp.projectId,
publishableClientKey: newKey.publishableClientKey!,
secretServerKey: newKey.secretServerKey!,
});
setIsGenerating(false);
};
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

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

Missing error handling in handleGenerateKeys. The try-catch block was removed, so if createInternalApiKey fails, the error will propagate unhandled. The isGenerating state will remain true, leaving the UI in a broken state. Consider restoring the try-catch block or using runAsynchronouslyWithAlert to handle errors properly.

Copilot uses AI. Check for mistakes.
defaultValuesRef.current = props.defaultValues;
form.reset(props.defaultValues as any);
}
}, [props.defaultValues, form]);
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

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

The dependency array includes form which is an object reference that changes on every render. This will cause unnecessary re-renders and potential infinite loops. Consider using useCallback dependencies more carefully or exclude form from the dependency array since it should be stable from useForm.

Suggested change
}, [props.defaultValues, form]);
}, [props.defaultValues]);

Copilot uses AI. Check for mistakes.
Comment on lines 79 to 87
// TODO: This function will be used when widget configuration UI is implemented
function saveConfig(config: DashboardConfig) {
if (typeof window === 'undefined') return;
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(config));
} catch (e) {
console.error('Failed to save dashboard config:', e);
}
}
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

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

[nitpick] The TODO comment indicates saveConfig function will be used in future implementation, but it's currently unused. Consider either implementing the feature now or removing the function to avoid dead code. If keeping it for future use, add an ESLint disable comment to prevent linting warnings.

Copilot uses AI. Check for mistakes.
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: 7

Caution

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

⚠️ Outside diff range comments (2)
apps/dashboard/src/components/app-square.tsx (1)

16-40: Unused props: size, showBadge, and variant are not utilized.

The AppIcon component declares size, showBadge, and variant props but doesn't use them in the component body. Either implement the functionality or remove the unused props to avoid confusion.

 export function AppIcon({
   appId,
   className,
-  size = "medium",
-  showBadge = false,
-  variant = "default"
 }: {
   appId: AppId,
   className?: string,
-  size?: "small" | "medium" | "large",
-  showBadge?: boolean,
-  variant?: AppSquareVariant,
 }) {

Or pass these props to SharedAppIcon if it supports them.

apps/dashboard/src/components/link.tsx (1)

26-37: onClick is invoked twice when needConfirm is true

Because onClick?.(e) is called inside the if (needConfirm) block and again unconditionally afterward, any onClick handler will run twice when needConfirm is true, which can cause duplicate side effects (analytics, state changes, etc.).

You can fix this by returning early from the needConfirm branch:

-    onClick={(e: React.MouseEvent<HTMLAnchorElement>) => {
-      if (needConfirm) {
-        e.preventDefault();
-        onClick?.(e);
-        router.push(href.toString());
-      }
-      onClick?.(e);
-    }}
+    onClick={(e: React.MouseEvent<HTMLAnchorElement>) => {
+      if (needConfirm) {
+        e.preventDefault();
+        onClick?.(e);
+        router.push(href.toString());
+        return;
+      }
+      onClick?.(e);
+    }}
🧹 Nitpick comments (20)
apps/dashboard/src/components/app-warning-modal.tsx (2)

182-186: Consider using <Button> with async handler for loading state.

Per coding guidelines, button components like <Button> support async callbacks with built-in loading state. If onConfirm is async (e.g., enabling an app), the current implementation won't show loading feedback and allows double-clicks.

Consider importing and using the Button component from @stackframe/stack-ui with async onClick to leverage built-in loading state handling, similar to the Cancel button pattern.


87-92: Add aria-label to close button for accessibility.

The close button only has an X icon without accessible text for screen readers.

             <button
               onClick={onClose}
               className="p-1.5 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
+              aria-label="Close modal"
             >
               <X className="w-4 h-4 text-gray-500" />
             </button>
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/charts-section-with-data.tsx (1)

93-93: Consider typing user data instead of any.

Both user mappings use any type annotation, losing type safety. If a user type exists in the codebase, applying it would catch potential property access errors at compile time.

Also applies to: 157-157

apps/dashboard/src/components/app-store-entry.tsx (1)

23-28: Consider making features configurable per app.

The comment suggests future customization, but consider defining this in apps-config.ts or apps-frontend.tsx now to avoid hardcoded marketing claims that may not apply to all apps (e.g., "Production Ready" for alpha apps).

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/apps/page-client.tsx (2)

56-57: Use installedAppsSet for O(1) lookup instead of includes().

Line 57 uses installedApps.includes(appId) while installedAppsSet was created specifically for O(1) lookups. This inconsistency also appears in getCategoryCount at line 90.

     // Apply category filter
     if (selectedCategory === "installed") {
-      apps = apps.filter(appId => installedApps.includes(appId));
+      apps = apps.filter(appId => installedAppsSet.has(appId));
     } else if (selectedCategory !== "all") {

Also update line 90:

   const getCategoryCount = (categoryId: string) => {
-    if (categoryId === "installed") return installedApps.length;
+    if (categoryId === "installed") return installedAppsSet.size;

88-102: Consider memoizing getCategoryCount with useCallback.

getCategoryCount is called for each category tab during every render. While the computation is lightweight, wrapping it in useCallback would be consistent with the memoization strategy used elsewhere in this component.

apps/dashboard/src/components/app-square.tsx (1)

201-277: AppListItem looks good but consider consistent error handling.

The onEnable callback at line 267 is fire-and-forget. If the caller passes an async function, errors won't be handled. Consider documenting this expectation or wrapping in runAsynchronously.

apps/dashboard/src/components/settings.tsx (1)

66-69: Wrap async callback with runAsynchronouslyWithAlert.

If propOnCheckedChange throws, the error will propagate but may not be handled by the Switch component's event system. Consider wrapping with error handling.

   const onCheckedChange = useCallback(async (checked: boolean) => {
     setCheckedState(checked);
-    await propOnCheckedChange(checked);
+    await propOnCheckedChange(checked); 
   }, [propOnCheckedChange]);

Since this is called directly (not via runAsynchronously), ensure callers handle errors, or wrap the call site:

           <Switch
             id={id}
             checked={checked}
-            onCheckedChange={onCheckedChange}
+            onCheckedChange={(checked) => runAsynchronouslyWithAlert(onCheckedChange(checked))}
             disabled={props.disabled}
           />
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx (4)

50-77: Avoid try-catch with console.error for error handling.

The loadConfig function uses try-catch with console.error, which violates the coding guidelines. Per project conventions, use runAsynchronously or runAsynchronouslyWithAlert for error handling instead of try-catch blocks.

However, since this is a synchronous localStorage read during initialization, consider either:

  1. Letting the error propagate (localStorage errors are rare)
  2. Using a silent fallback without logging
 function loadConfig(): DashboardConfig {
   if (typeof window === 'undefined') return DEFAULT_CONFIG;
-  try {
-    const stored = localStorage.getItem(STORAGE_KEY);
-    if (stored) {
-      const parsed = JSON.parse(stored);
-      // Validate and merge with defaults for any new widgets
-      const validWidgetIds = new Set(AVAILABLE_WIDGETS.map(w => w.id));
-      const enabledWidgets = (parsed.enabledWidgets || []).filter((id: string) => validWidgetIds.has(id as WidgetId));
-      const widgetOrder = (parsed.widgetOrder || []).filter((id: string) => validWidgetIds.has(id as WidgetId));
-      
-      // Add any new widgets that aren't in the stored config
-      for (const widget of AVAILABLE_WIDGETS) {
-        if (!widgetOrder.includes(widget.id)) {
-          widgetOrder.push(widget.id);
-          if (widget.defaultEnabled) {
-            enabledWidgets.push(widget.id);
-          }
+  const stored = localStorage.getItem(STORAGE_KEY);
+  if (stored) {
+    const parsed = JSON.parse(stored);
+    // Validate and merge with defaults for any new widgets
+    const validWidgetIds = new Set(AVAILABLE_WIDGETS.map(w => w.id));
+    const enabledWidgets = (parsed.enabledWidgets || []).filter((id: string) => validWidgetIds.has(id as WidgetId));
+    const widgetOrder = (parsed.widgetOrder || []).filter((id: string) => validWidgetIds.has(id as WidgetId));
+    
+    // Add any new widgets that aren't in the stored config
+    for (const widget of AVAILABLE_WIDGETS) {
+      if (!widgetOrder.includes(widget.id)) {
+        widgetOrder.push(widget.id);
+        if (widget.defaultEnabled) {
+          enabledWidgets.push(widget.id);
         }
       }
-      
-      return { enabledWidgets, widgetOrder };
     }
-  } catch (e) {
-    console.error('Failed to load dashboard config:', e);
+    
+    return { enabledWidgets, widgetOrder };
   }
   return DEFAULT_CONFIG;
 }

79-87: Remove unused saveConfig function.

The saveConfig function is defined but never called. The TODO comment indicates it will be used when widget configuration UI is implemented, but dead code should not be committed. Consider removing it and adding it back when the feature is implemented.


121-128: Replace any types with proper typing.

The reduce callback uses any types which reduces type safety. Since DataPoint has a defined structure, use proper types.

-    const filteredData = timeRange === '7d' ? dailyUsers.slice(-7) : dailyUsers.slice(-30);
-    return filteredData.reduce((sum: any, point: { activity: any; }) => sum + point.activity, 0);
+    const filteredData = timeRange === '7d' ? dailyUsers.slice(-7) : dailyUsers.slice(-30);
+    return filteredData.reduce((sum: number, point: { activity: number }) => sum + point.activity, 0);

194-204: Replace any type for data prop with a proper interface.

Both DailyActiveUsersWidget and DailySignUpsWidget use data: any which loses type safety. Define an interface for the metrics data structure.

+interface MetricsData {
+  daily_active_users?: DataPoint[];
+  recently_active?: UserListItem[];
+  daily_users?: DataPoint[];
+  recently_registered?: UserListItem[];
+  total_users?: number;
+}

 function DailyActiveUsersWidget({ 
   data, 
   projectId, 
   router, 
   timeRange 
 }: { 
-  data: any, 
+  data: MetricsData, 
   projectId: string, 
   router: ReturnType<typeof useRouter>, 
   timeRange: TimeRange 
 }) {

Also applies to: 220-230

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/vercel/page-client.tsx (1)

309-312: Unused itemId parameter in handleItemClick.

The itemId parameter is received but never used. Either remove the parameter or use it for more granular item-level completion tracking if intended.

-const handleItemClick = (stepId: StepId, itemId: string) => {
+const handleItemClick = (stepId: StepId, _itemId: string) => {
   // Toggle the step completion when clicking on an item
   toggleStepCompletion(stepId);
 };

Or if item-level tracking is intended:

 const handleItemClick = (stepId: StepId, itemId: string) => {
-  // Toggle the step completion when clicking on an item
-  toggleStepCompletion(stepId);
+  // TODO: Implement item-level completion tracking using itemId
+  toggleStepCompletion(stepId);
 };
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/project-settings/page-client.tsx (3)

21-45: Replace any type with proper interface for TeamMemberItem.

The member prop uses any type which loses type safety. Define a proper interface based on the expected shape.

-function TeamMemberItem({ member }: { member: any }) {
+interface TeamMember {
+  id: string;
+  teamProfile: {
+    displayName?: string | null;
+    profileImageUrl?: string | null;
+  };
+}
+
+function TeamMemberItem({ member }: { member: TeamMember }) {
   const displayName = member.teamProfile.displayName?.trim() || "Name not set";

139-141: Replace any type with proper team type.

The handleTeamSwitcherChange callback uses any for the team parameter. Use the proper team type from the SDK.

-const handleTeamSwitcherChange = useCallback(async (team: any) => {
+const handleTeamSwitcherChange = useCallback(async (team: { id: string }) => {
   setSelectedTeamId(team.id);
 }, []);

230-245: Inconsistent memoization for logo handlers.

handleLogoChange and handleFullLogoChange are memoized (lines 125-131), but the dark mode logo handlers (lines 230-241) are inline async functions. This inconsistency could cause unnecessary re-renders.

Consider memoizing all logo handlers for consistency:

+const handleLogoDarkModeChange = useCallback(async (logoDarkModeUrl: string | null) => {
+  await project.update({ logoDarkModeUrl });
+}, [project]);
+
+const handleLogoFullDarkModeChange = useCallback(async (logoFullDarkModeUrl: string | null) => {
+  await project.update({ logoFullDarkModeUrl });
+}, [project]);

 <LogoUpload
   label="Logo (Dark Mode)"
   value={project.logoDarkModeUrl}
-  onValueChange={async (logoDarkModeUrl) => {
-    await project.update({ logoDarkModeUrl });
-  }}
+  onValueChange={handleLogoDarkModeChange}
   ...
 />
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe.tsx (2)

77-78: Remove unused displayName variable.

The displayName variable is defined but never used in the component. This appears to be leftover from the previous implementation.

 const user = useUser({ or: "redirect" });
-const displayName = user.displayName ?? user.primaryEmail;

39-40: Remove unused refs and size hooks.

sectionContainerRef and sectionContainerSize are defined but never used meaningfully. The ref is attached to an element (line 138) but the size is never consumed.

 const globeContainerRef = useRef<HTMLDivElement>(null);
 const globeContainerSize = useSize(globeContainerRef);
-const sectionContainerRef = useRef<HTMLDivElement>(null);
-const sectionContainerSize = useSize(sectionContainerRef);

And update the JSX:

 <div
-  ref={sectionContainerRef}
   className='relative flex items-center justify-center'
 >
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/line-chart.tsx (2)

34-39: Import isWeekend from shared utilities instead of duplicating.

This isWeekend function duplicates the one in packages/stack-shared/src/utils/dates.tsx. Import from the shared package to avoid duplication.

+import { isWeekend as isWeekendDate } from '@stackframe/stack-shared/dist/utils/dates';

-const isWeekend = (dateString: string): boolean => {
-  const date = new Date(dateString);
-  if (isNaN(date.getTime())) return false;
-  const dayOfWeek = date.getDay();
-  return dayOfWeek === 0 || dayOfWeek === 6; // Sunday (0) or Saturday (6)
-}
+const isWeekend = (dateString: string): boolean => {
+  const date = new Date(dateString);
+  if (isNaN(date.getTime())) return false;
+  return isWeekendDate(date);
+};

548-550: Inconsistent access patterns for BRAND_CONFIG.

The code uses both direct object access (BRAND_CONFIG[key as keyof typeof BRAND_CONFIG]) and Map access (BRAND_CONFIG_MAP.get(item.method)). Use the Map consistently since it was created for efficient lookups.

-const brandConfig = key ? BRAND_CONFIG[key as keyof typeof BRAND_CONFIG] : undefined;
+const brandConfig = key ? BRAND_CONFIG_MAP.get(key) : undefined;

-style={{ backgroundColor: BRAND_CONFIG[item.method as keyof typeof BRAND_CONFIG]?.color ?? "var(--color-other)" }}
+style={{ backgroundColor: BRAND_CONFIG_MAP.get(item.method)?.color ?? "var(--color-other)" }}

Also applies to: 602-605

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9bf049d and fdf3093.

📒 Files selected for processing (16)
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/charts-section-with-data.tsx (3 hunks)
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe.tsx (3 hunks)
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/line-chart.tsx (2 hunks)
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx (1 hunks)
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/apps/[appId]/page-client.tsx (1 hunks)
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/apps/page-client.tsx (1 hunks)
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/project-settings/page-client.tsx (10 hunks)
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sidebar-layout.tsx (11 hunks)
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/vercel/page-client.tsx (5 hunks)
  • apps/dashboard/src/components/app-square.tsx (2 hunks)
  • apps/dashboard/src/components/app-store-entry.tsx (2 hunks)
  • apps/dashboard/src/components/app-warning-modal.tsx (1 hunks)
  • apps/dashboard/src/components/copyable-text.tsx (1 hunks)
  • apps/dashboard/src/components/link.tsx (2 hunks)
  • apps/dashboard/src/components/settings.tsx (5 hunks)
  • apps/dashboard/src/lib/apps-frontend.tsx (3 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Ensure code passes pnpm typecheck for TypeScript type validation
Never use toast for blocking alerts and errors; use alerts instead as they are more visible to users
Keep hover/click transitions snappy and fast; apply transitions after the action (e.g., smooth fade-out on hover end) rather than delaying actions with pre-transitions to avoid sluggish UI
Never use try-catch-all, never void a promise, never use .catch(console.error) or similar; use runAsynchronously or runAsynchronouslyWithAlert for error handling instead of try-catch blocks
Use loading indicators instead of try-catch blocks for asynchronous UI operations; button components like support async callbacks with built-in loading state
Use ES6 maps instead of records wherever possible

Files:

  • apps/dashboard/src/components/app-warning-modal.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/apps/[appId]/page-client.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/apps/page-client.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/charts-section-with-data.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/vercel/page-client.tsx
  • apps/dashboard/src/components/app-square.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sidebar-layout.tsx
  • apps/dashboard/src/components/copyable-text.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/line-chart.tsx
  • apps/dashboard/src/components/link.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/project-settings/page-client.tsx
  • apps/dashboard/src/components/app-store-entry.tsx
  • apps/dashboard/src/lib/apps-frontend.tsx
  • apps/dashboard/src/components/settings.tsx
apps/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Prefer client components and dynamic functions like usePathname over Next.js dynamic functions to keep pages static; avoid using await params or similar dynamic patterns

Files:

  • apps/dashboard/src/components/app-warning-modal.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/apps/[appId]/page-client.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/apps/page-client.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/charts-section-with-data.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/vercel/page-client.tsx
  • apps/dashboard/src/components/app-square.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sidebar-layout.tsx
  • apps/dashboard/src/components/copyable-text.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/line-chart.tsx
  • apps/dashboard/src/components/link.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/project-settings/page-client.tsx
  • apps/dashboard/src/components/app-store-entry.tsx
  • apps/dashboard/src/lib/apps-frontend.tsx
  • apps/dashboard/src/components/settings.tsx
**/apps-{frontend,config}.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

To update available apps, edit apps-frontend.tsx and apps-config.ts

Files:

  • apps/dashboard/src/lib/apps-frontend.tsx
🧠 Learnings (7)
📚 Learning: 2025-11-25T02:09:03.086Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T02:09:03.086Z
Learning: Applies to **/apps-{frontend,config}.{ts,tsx} : To update available apps, edit `apps-frontend.tsx` and `apps-config.ts`

Applied to files:

  • apps/dashboard/src/components/app-warning-modal.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/apps/[appId]/page-client.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/apps/page-client.tsx
  • apps/dashboard/src/components/app-square.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sidebar-layout.tsx
  • apps/dashboard/src/components/app-store-entry.tsx
  • apps/dashboard/src/lib/apps-frontend.tsx
  • apps/dashboard/src/components/settings.tsx
📚 Learning: 2025-10-11T04:13:19.308Z
Learnt from: N2D4
Repo: stack-auth/stack-auth PR: 943
File: examples/convex/app/action/page.tsx:23-28
Timestamp: 2025-10-11T04:13:19.308Z
Learning: In the stack-auth codebase, use `runAsynchronouslyWithAlert` from `stackframe/stack-shared/dist/utils/promises` for async button click handlers and form submissions instead of manual try/catch blocks. This utility automatically handles errors and shows alerts to users.

Applied to files:

  • apps/dashboard/src/components/app-warning-modal.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/vercel/page-client.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/project-settings/page-client.tsx
  • apps/dashboard/src/components/settings.tsx
📚 Learning: 2025-11-25T02:09:03.086Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T02:09:03.086Z
Learning: Applies to **/*.{ts,tsx} : Never use `toast` for blocking alerts and errors; use alerts instead as they are more visible to users

Applied to files:

  • apps/dashboard/src/components/app-warning-modal.tsx
📚 Learning: 2025-11-25T02:09:03.086Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T02:09:03.086Z
Learning: Applies to apps/**/*.{ts,tsx} : Prefer client components and dynamic functions like `usePathname` over Next.js dynamic functions to keep pages static; avoid using `await params` or similar dynamic patterns

Applied to files:

  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/apps/[appId]/page-client.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/apps/page-client.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sidebar-layout.tsx
  • apps/dashboard/src/components/app-store-entry.tsx
📚 Learning: 2025-11-25T02:09:03.086Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T02:09:03.086Z
Learning: Applies to apps/backend/src/app/api/latest/**/*.{ts,tsx} : In the Stack Auth monorepo backend API, organize routes by resource type (auth, user management, team management, OAuth providers) in `/apps/backend/src/app/api/latest/*`

Applied to files:

  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/apps/page-client.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sidebar-layout.tsx
📚 Learning: 2025-11-25T02:09:03.086Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T02:09:03.086Z
Learning: When making dashboard changes, provide deep links to the dashboard page in the format `http://localhost:<$NEXT_PUBLIC_STACK_PORT_PREFIX>01/projects/-selector-/...` (or use a.localhost, b.localhost, c.localhost for port prefixes 91, 92, 93)

Applied to files:

  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx
📚 Learning: 2025-11-25T02:09:03.086Z
Learnt from: CR
Repo: stack-auth/stack-auth PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T02:09:03.086Z
Learning: Applies to **/*.{ts,tsx} : Never use try-catch-all, never void a promise, never use .catch(console.error) or similar; use `runAsynchronously` or `runAsynchronouslyWithAlert` for error handling instead of try-catch blocks

Applied to files:

  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/vercel/page-client.tsx
🧬 Code graph analysis (6)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/apps/[appId]/page-client.tsx (5)
apps/dashboard/src/lib/apps-frontend.tsx (3)
  • AppId (13-13)
  • ALL_APPS_FRONTEND (67-228)
  • getAppPath (40-43)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/use-admin-app.tsx (1)
  • useAdminApp (29-44)
packages/stack-shared/src/utils/errors.tsx (1)
  • StackAssertionError (69-85)
apps/dashboard/src/components/app-store-entry.tsx (1)
  • AppStoreEntry (11-197)
packages/stack-shared/src/utils/promises.tsx (1)
  • runAsynchronouslyWithAlert (312-328)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/apps/page-client.tsx (4)
apps/dashboard/src/lib/apps-frontend.tsx (1)
  • AppId (13-13)
packages/stack-shared/src/apps/apps-config.ts (2)
  • AppId (48-48)
  • ALL_APPS (50-141)
packages/stack-shared/src/utils/strings.tsx (1)
  • stringCompare (61-65)
docs/src/components/icons.tsx (1)
  • Search (93-96)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx (3)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/line-chart.tsx (5)
  • LineChartDisplayConfig (14-18)
  • TimeRange (12-12)
  • ChartCard (177-208)
  • TabbedMetricsCard (244-405)
  • TimeRangeToggle (210-242)
apps/dashboard/src/components/app-square.tsx (1)
  • AppIcon (16-40)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe-section-with-data.tsx (1)
  • GlobeSectionWithData (9-18)
apps/dashboard/src/components/copyable-text.tsx (1)
docs/src/components/icons.tsx (2)
  • Check (163-165)
  • Copy (185-205)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/line-chart.tsx (2)
packages/stack-shared/src/utils/dates.tsx (2)
  • isWeekend (5-7)
  • fromNow (28-30)
apps/dashboard/src/components/router.tsx (1)
  • useRouter (15-33)
apps/dashboard/src/components/app-store-entry.tsx (3)
docs/src/components/icons.tsx (2)
  • Zap (345-347)
  • Check (163-165)
apps/dashboard/src/components/app-square.tsx (1)
  • AppIcon (16-40)
packages/stack-shared/src/apps/apps-config.ts (1)
  • ALL_APP_TAGS (5-39)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: CodeQL analysis (javascript-typescript)
  • GitHub Check: Vercel Agent Review
  • GitHub Check: all-good
🔇 Additional comments (20)
apps/dashboard/src/lib/apps-frontend.tsx (2)

3-3: LGTM! Good type-safety improvement.

Deriving AppId from ALL_APPS ensures the type stays in sync with the configuration source of truth, eliminating potential drift between frontend and config definitions.

Also applies to: 13-14


218-220: LGTM! Improved logo centering and styling.

The flex container properly centers the Vercel logo, and the object-contain with padding ensures consistent rendering across different viewport sizes.

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/apps/[appId]/page-client.tsx (1)

14-14: Non-null assertion on useAdminApp() may mask errors.

Based on the useAdminApp implementation, it can return undefined when projectId is not provided and no AdminInterfaceProvider exists. The non-null assertion (!) suppresses TypeScript's null check but won't prevent runtime errors if the hook returns undefined. Consider using optional chaining or an early return/redirect pattern if the app could legitimately be undefined.

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/charts-section-with-data.tsx (2)

25-28: LGTM! Good theme-aware chart configuration.

Using theme objects with explicit light/dark mode colors ensures proper visibility and contrast in both modes.

Also applies to: 39-42


54-65: LGTM! Well-structured responsive grid layout.

The layout effectively uses CSS Grid with responsive breakpoints (lg:grid-cols-2, lg:col-span-2, etc.) and the button-based user items provide consistent interaction patterns with proper hover states.

Also applies to: 68-131, 133-189

apps/dashboard/src/components/app-store-entry.tsx (1)

11-196: LGTM! Clean UI refactor with good structure.

The component is well-organized with clear sections (hero, features, screenshots, description). Good use of conditional rendering for stage warnings and empty states.

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/apps/page-client.tsx (1)

28-219: Good use of client-side state and memoization.

The component correctly uses usePathname-style client patterns per the coding guidelines. The filtering/sorting logic with useMemo is well-structured.

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sidebar-layout.tsx (2)

516-525: getDefaultExpandedSections recreates URL on every call.

The function is memoized with useCallback, but pathname in the dependency array means it gets recreated on every pathname change, which then could trigger state updates. This is likely fine, but the initial state function passed to useState at line 527 will only be called once.

Verify that changing routes doesn't cause unexpected collapse/expand behavior. The expandedSections state is initialized once and won't update when pathname changes (which is likely the intended behavior).


722-820: Clean implementation of collapsible sidebar layout.

Good use of TooltipProvider at the root, proper fixed positioning for the sidebar, and smooth transitions with transition-[width] and transition-[margin]. The collapsed state management with useCallback is well done.

apps/dashboard/src/components/settings.tsx (2)

302-309: Good pattern for detecting defaultValues changes with ref.

Using a ref to track previous defaultValues and only resetting when they actually change is a good pattern to avoid unnecessary form resets. This correctly handles the case where form object reference changes but values don't.


12-49: Good memoization of settings components.

The use of React.memo for these components and useCallback for handlers is appropriate for preventing unnecessary re-renders in forms with many settings.

Also applies to: 51-100, 114-148, 150-171, 173-192

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx (1)

322-350: Widget 'total-users' is in AVAILABLE_WIDGETS but not handled in renderWidget.

The renderWidget function handles 'apps', 'daily-active-users', and 'daily-sign-ups', but 'total-users' (defined in AVAILABLE_WIDGETS at line 32) is not handled—it returns null. While TotalUsersDisplay is rendered separately in the globe section, this could cause confusion if someone enables only 'total-users' without the globe.

Verify this is intentional—if users disable the globe but enable 'total-users', they won't see any total users display since renderWidget returns null for it.

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/vercel/page-client.tsx (1)

518-528: LGTM! Good keyboard accessibility implementation.

The CardHeader correctly implements keyboard accessibility with role="button", tabIndex={0}, and proper onKeyDown handling for Enter and Space keys. This aligns with the PR's accessibility improvements.

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/project-settings/page-client.tsx (1)

103-122: Guard clause for isTransferring should come before async operation.

The isTransferring check on line 105 is good for preventing double-clicks, but the function is wrapped with runAsynchronouslyWithAlert at the call site (line 328). This is the correct pattern per coding guidelines.

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe.tsx (2)

270-302: LGTM! Clean tooltip implementation.

The tooltip now uses fixed positioning with cursor tracking via onMouseMove, which provides smooth tooltip behavior. The transform calculation properly centers and offsets the tooltip above the cursor.


310-324: LGTM! Simplified loader.

The PlanetLoader has been simplified from a complex animation to a clean, minimal spinner. This aligns with the PR's design improvements and reduces complexity.

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/line-chart.tsx (3)

489-490: Good use of ES6 Map for efficient lookups.

Using Map for BRAND_CONFIG_MAP follows the coding guidelines preference for ES6 maps over records for lookups.


89-175: LGTM! Well-structured shared chart component.

The ActivityBarChart component is a good refactoring that extracts common chart logic for reuse across LineChartDisplay and TabbedMetricsCard. The component properly handles weekend styling, animations, and responsive tick sizing.


210-242: LGTM! Clean TimeRangeToggle implementation.

The toggle component is well-structured with proper button semantics, clear visual states, and good accessibility (using type="button" to prevent form submission).

apps/dashboard/src/components/link.tsx (1)

10-20: LinkProps surface changes (eventful onClick, title) look consistent

The updated onClick signature and added optional title prop align with how the component uses them (passing the event into onClick and forwarding ...rest to NextLink). No issues from this change in this file.

<AppStoreEntry
appId={appId}
onEnable={handleEnable}
onEnable={async () => runAsynchronouslyWithAlert(handleEnable())}
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

Incorrect runAsynchronouslyWithAlert usage — pass the function, not the invocation.

handleEnable() is already invoked, returning a Promise. runAsynchronouslyWithAlert expects a function that returns a Promise, not a Promise itself. This will cause the async to execute immediately rather than being managed by the wrapper.

-        onEnable={async () => runAsynchronouslyWithAlert(handleEnable())}
+        onEnable={() => runAsynchronouslyWithAlert(handleEnable)}
📝 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
onEnable={async () => runAsynchronouslyWithAlert(handleEnable())}
onEnable={() => runAsynchronouslyWithAlert(handleEnable)}
🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/apps/[appId]/page-client.tsx
around line 33, the code invokes handleEnable() immediately and passes its
Promise into runAsynchronouslyWithAlert; instead pass the function itself so the
wrapper can call it when needed. Change the prop to pass a function reference
(e.g., wrap runAsynchronouslyWithAlert with an arrow that supplies handleEnable
without calling it) so runAsynchronouslyWithAlert receives a () => Promise and
not an already-started Promise.

Comment on lines 282 to 288
const caretClasses = cn(
"h-4 w-4 flex-shrink-0 transition-colors transition-transform",
isHighlighted
? "text-blue-600 dark:text-blue-400"
: "text-muted-foreground group-hover:text-foreground",
isSection && isExpanded && "rotate-180"
);
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

Duplicate transition- class in caretClasses.

Line 283 has both transition-colors and transition-transform which may conflict. Consider using a single transition-all or specifying transition with multiple properties.

   const caretClasses = cn(
-    "h-4 w-4 flex-shrink-0 transition-colors transition-transform",
+    "h-4 w-4 flex-shrink-0 transition-all",
     isHighlighted
       ? "text-blue-600 dark:text-blue-400"
       : "text-muted-foreground group-hover:text-foreground",
     isSection && isExpanded && "rotate-180"
   );
📝 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 caretClasses = cn(
"h-4 w-4 flex-shrink-0 transition-colors transition-transform",
isHighlighted
? "text-blue-600 dark:text-blue-400"
: "text-muted-foreground group-hover:text-foreground",
isSection && isExpanded && "rotate-180"
);
const caretClasses = cn(
"h-4 w-4 flex-shrink-0 transition-all",
isHighlighted
? "text-blue-600 dark:text-blue-400"
: "text-muted-foreground group-hover:text-foreground",
isSection && isExpanded && "rotate-180"
);
🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sidebar-layout.tsx
around lines 282 to 288, the caretClasses include both "transition-colors" and
"transition-transform" which the reviewer flagged as redundant; replace those
two classes with a single "transition-all" (or a single explicit transition
class you prefer) so the caret uses one transition utility, e.g., remove
"transition-colors transition-transform" and add "transition-all" to the cn
call.

Comment on lines 75 to 94
const handleGenerateKeys = async () => {
if (isGenerating) return;
setIsGenerating(true);
setError(null);

try {
const newKey = await adminApp.createInternalApiKey({
hasPublishableClientKey: true,
hasSecretServerKey: true,
hasSuperSecretAdminKey: false,
expiresAt: new Date(Date.now() + TWO_HUNDRED_YEARS_IN_MS),
description: "Vercel Integration",
});

setKeys({
projectId: adminApp.projectId,
publishableClientKey: newKey.publishableClientKey!,
secretServerKey: newKey.secretServerKey!,
});
} catch (err) {
console.error(err);
setError(err instanceof Error ? err.message : "Failed to generate API keys. Please try again.");
} finally {
setIsGenerating(false);
}
const newKey = await adminApp.createInternalApiKey({
hasPublishableClientKey: true,
hasSecretServerKey: true,
hasSuperSecretAdminKey: false,
expiresAt: new Date(Date.now() + TWO_HUNDRED_YEARS_IN_MS),
description: "Vercel Integration",
});

setKeys({
projectId: adminApp.projectId,
publishableClientKey: newKey.publishableClientKey!,
secretServerKey: newKey.secretServerKey!,
});
setIsGenerating(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

Wrap async key generation with error handling.

handleGenerateKeys is an async function that can throw errors from adminApp.createInternalApiKey, but it's not wrapped with runAsynchronouslyWithAlert. Per coding guidelines, async operations should use proper error handling utilities.

+import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises";

 const handleGenerateKeys = async () => {
   if (isGenerating) return;
   setIsGenerating(true);
   setError(null);

-  const newKey = await adminApp.createInternalApiKey({
-    hasPublishableClientKey: true,
-    hasSecretServerKey: true,
-    hasSuperSecretAdminKey: false,
-    expiresAt: new Date(Date.now() + TWO_HUNDRED_YEARS_IN_MS),
-    description: "Vercel Integration",
-  });
-
-  setKeys({
-    projectId: adminApp.projectId,
-    publishableClientKey: newKey.publishableClientKey!,
-    secretServerKey: newKey.secretServerKey!,
-  });
-  setIsGenerating(false);
+  await runAsynchronouslyWithAlert(async () => {
+    const newKey = await adminApp.createInternalApiKey({
+      hasPublishableClientKey: true,
+      hasSecretServerKey: true,
+      hasSuperSecretAdminKey: false,
+      expiresAt: new Date(Date.now() + TWO_HUNDRED_YEARS_IN_MS),
+      description: "Vercel Integration",
+    });
+
+    setKeys({
+      projectId: adminApp.projectId,
+      publishableClientKey: newKey.publishableClientKey!,
+      secretServerKey: newKey.secretServerKey!,
+    });
+  });
+  setIsGenerating(false);
 };

Note: runAsynchronouslyWithAlert is already imported but not used here.

🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/vercel/page-client.tsx
around lines 75 to 94, the async handler handleGenerateKeys calls
adminApp.createInternalApiKey directly and can throw — wrap the operation with
the existing runAsynchronouslyWithAlert helper: call runAsynchronouslyWithAlert
with an async callback that performs the createInternalApiKey call, sets keys on
success, and sets setError on failure; ensure you still short-circuit when
isGenerating is true, setIsGenerating(true) before the wrapped call and
setIsGenerating(false) in a finally-equivalent cleanup so loading state is
cleared even if an error occurs.

Comment on lines 81 to 95
const performToggle = async () => {
setIsProcessing(true);

try {
await project.updateConfig({
[`apps.installed.${appId}.enabled`]: !isEnabled,
});
onToggleEnabled?.(!isEnabled);
} catch (error) {
console.error(`Failed to ${isEnabled ? 'disable' : 'enable'} app:`, error);
alert(`Failed to ${isEnabled ? 'disable' : 'enable'} ${app.displayName}. Please try again.`);
} finally {
setIsProcessing(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

Replace try-catch with runAsynchronouslyWithAlert per coding guidelines.

The coding guidelines state: "never use try-catch-all" and recommend using runAsynchronouslyWithAlert for error handling. The current implementation uses a try-catch-all pattern with console.error and alert().

+import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises";

-  const performToggle = async () => {
-    setIsProcessing(true);
-
-    try {
-      await project.updateConfig({
-        [`apps.installed.${appId}.enabled`]: !isEnabled,
-      });
-      onToggleEnabled?.(!isEnabled);
-    } catch (error) {
-      console.error(`Failed to ${isEnabled ? 'disable' : 'enable'} app:`, error);
-      alert(`Failed to ${isEnabled ? 'disable' : 'enable'} ${app.displayName}. Please try again.`);
-    } finally {
-      setIsProcessing(false);
-    }
+  const performToggle = async () => {
+    setIsProcessing(true);
+    await project.updateConfig({
+      [`apps.installed.${appId}.enabled`]: !isEnabled,
+    });
+    onToggleEnabled?.(!isEnabled);
+    setIsProcessing(false);
   };

Then wrap the call sites with runAsynchronouslyWithAlert:

-  runAsynchronously(performToggle());
+  runAsynchronouslyWithAlert(performToggle());

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

🤖 Prompt for AI Agents
In apps/dashboard/src/components/app-square.tsx around lines 81–95, replace the
try-catch-all in performToggle with runAsynchronouslyWithAlert:
setIsProcessing(true) before invoking runAsynchronouslyWithAlert, call
runAsynchronouslyWithAlert with an async callback that performs
project.updateConfig({[`apps.installed.${appId}.enabled`]: !isEnabled}) and then
calls onToggleEnabled?.(!isEnabled) on success, provide a user-facing error
message that includes app.displayName (the same text currently used in alert),
and ensure setIsProcessing(false) runs in the finally/onComplete handler
provided to runAsynchronouslyWithAlert so processing state is cleared whether it
succeeds or fails.

Comment on lines 24 to 30
useEffect(() => {
if (isOpen) {
setTimeout(() => setIsAnimating(true), 10);
} else {
setIsAnimating(false);
}
}, [isOpen]);
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

Missing cleanup for setTimeout — potential memory leak.

When isOpen changes to true, a timeout is scheduled but never cleared if the component unmounts before the timeout fires. This can cause a state update on an unmounted component.

   useEffect(() => {
     if (isOpen) {
-      setTimeout(() => setIsAnimating(true), 10);
+      const timer = setTimeout(() => setIsAnimating(true), 10);
+      return () => clearTimeout(timer);
     } else {
       setIsAnimating(false);
     }
   }, [isOpen]);
📝 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(() => {
if (isOpen) {
setTimeout(() => setIsAnimating(true), 10);
} else {
setIsAnimating(false);
}
}, [isOpen]);
useEffect(() => {
if (isOpen) {
const timer = setTimeout(() => setIsAnimating(true), 10);
return () => clearTimeout(timer);
} else {
setIsAnimating(false);
}
}, [isOpen]);
🤖 Prompt for AI Agents
In apps/dashboard/src/components/app-warning-modal.tsx around lines 24 to 30,
the useEffect schedules a setTimeout when isOpen becomes true but never clears
it, risking state updates after unmount; fix by storing the timeout id (use a
ref or local variable compatible with the environment) and return a cleanup
function from the effect that calls clearTimeout on that id, and also clear the
timeout whenever isOpen changes before scheduling a new one so no stray timers
remain.

Comment on lines 22 to 29
const handleCopy = useCallback(async () => {
try {
await navigator.clipboard.writeText(props.value);
setCopied(true);
} catch (err) {
console.error("Failed to copy:", err);
}
}, [props.value]);
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

Replace try-catch with runAsynchronouslyWithAlert per coding guidelines.

The coding guidelines prohibit try-catch-all with console.error. For clipboard operations that can fail (e.g., permissions denied), use runAsynchronouslyWithAlert to properly alert the user.

+import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises";
+
-  const handleCopy = useCallback(async () => {
-    try {
-      await navigator.clipboard.writeText(props.value);
-      setCopied(true);
-    } catch (err) {
-      console.error("Failed to copy:", err);
-    }
-  }, [props.value]);
+  const handleCopy = useCallback(() => {
+    runAsynchronouslyWithAlert(async () => {
+      await navigator.clipboard.writeText(props.value);
+      setCopied(true);
+    });
+  }, [props.value]);
📝 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 handleCopy = useCallback(async () => {
try {
await navigator.clipboard.writeText(props.value);
setCopied(true);
} catch (err) {
console.error("Failed to copy:", err);
}
}, [props.value]);
import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises";
const handleCopy = useCallback(() => {
runAsynchronouslyWithAlert(async () => {
await navigator.clipboard.writeText(props.value);
setCopied(true);
});
}, [props.value]);
🤖 Prompt for AI Agents
In apps/dashboard/src/components/copyable-text.tsx around lines 22 to 29,
replace the manual try-catch that calls navigator.clipboard.writeText and
console.error with a call to the existing runAsynchronouslyWithAlert helper:
wrap the clipboard write in runAsynchronouslyWithAlert so any rejection shows a
user alert, and on success setCopied(true); keep the useCallback and dependency
on props.value unchanged and ensure the helper receives an async function that
awaits navigator.clipboard.writeText(props.value).

Comment on lines 281 to 296
const handleFormSubmit = useCallback((e: React.FormEvent<HTMLFormElement>) => {
runAsynchronously(async () => {
setSubmitting(true);
try {
await form.handleSubmit(onSubmit)(e);
} catch (error) {
toast({
title: 'Failed to save changes',
description: error instanceof Error ? error.message : 'An error occurred',
variant: 'destructive'
});
} finally {
setSubmitting(false);
}
});
}, [form, onSubmit, toast]);
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

Replace try-catch with runAsynchronouslyWithAlert per coding guidelines.

The coding guidelines prohibit try-catch-all patterns. Use runAsynchronouslyWithAlert which handles errors automatically.

   const handleFormSubmit = useCallback((e: React.FormEvent<HTMLFormElement>) => {
-    runAsynchronously(async () => {
-      setSubmitting(true);
-      try {
-        await form.handleSubmit(onSubmit)(e);
-      } catch (error) {
-        toast({
-          title: 'Failed to save changes',
-          description: error instanceof Error ? error.message : 'An error occurred',
-          variant: 'destructive'
-        });
-      } finally {
-        setSubmitting(false);
-      }
-    });
+    e.preventDefault();
+    setSubmitting(true);
+    runAsynchronouslyWithAlert(
+      form.handleSubmit(onSubmit)(e).finally(() => setSubmitting(false))
+    );
   }, [form, onSubmit, toast]);

Note: The guidelines state to "Never use toast for blocking alerts and errors; use alerts instead as they are more visible to users." runAsynchronouslyWithAlert uses alerts for errors.

📝 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 handleFormSubmit = useCallback((e: React.FormEvent<HTMLFormElement>) => {
runAsynchronously(async () => {
setSubmitting(true);
try {
await form.handleSubmit(onSubmit)(e);
} catch (error) {
toast({
title: 'Failed to save changes',
description: error instanceof Error ? error.message : 'An error occurred',
variant: 'destructive'
});
} finally {
setSubmitting(false);
}
});
}, [form, onSubmit, toast]);
const handleFormSubmit = useCallback((e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setSubmitting(true);
runAsynchronouslyWithAlert(
form.handleSubmit(onSubmit)(e).finally(() => setSubmitting(false))
);
}, [form, onSubmit]);
🤖 Prompt for AI Agents
In apps/dashboard/src/components/settings.tsx around lines 281 to 296, the block
uses runAsynchronously with an explicit try-catch and toast error handling which
violates guidelines; replace this with runAsynchronouslyWithAlert so errors are
handled via alert automatically and remove the toast usage, while preserving the
submitting state semantics (setSubmitting(true) before the awaited call and
setSubmitting(false) in a finally-equivalent path after the call). Call
runAsynchronouslyWithAlert with an async function that awaits
form.handleSubmit(onSubmit)(e) and ensure any error path does not use toast but
relies on the wrapper to show alerts.

}, [project]);

const handleFullLogoChange = useCallback(async (fullLogoUrl: string | null) => {
await project.update({ fullLogoUrl });
Copy link

@vercel vercel bot Nov 25, 2025

Choose a reason for hiding this comment

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

Property name mismatch: the code uses fullLogoUrl in the update call, but the correct property name is logoFullUrl according to the API and type definitions throughout the codebase.

View Details
📝 Patch Details
diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/project-settings/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/project-settings/page-client.tsx
index c0e8f56b..0f462b5e 100644
--- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/project-settings/page-client.tsx
+++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/project-settings/page-client.tsx
@@ -127,7 +127,7 @@ export default function PageClient() {
   }, [project]);
 
   const handleFullLogoChange = useCallback(async (fullLogoUrl: string | null) => {
-    await project.update({ fullLogoUrl });
+    await project.update({ logoFullUrl: fullLogoUrl });
   }, [project]);
 
   // Memoize production mode change callback
@@ -218,7 +218,7 @@ export default function PageClient() {
 
         <LogoUpload
           label="Full Logo"
-          value={project.fullLogoUrl}
+          value={project.logoFullUrl}
           onValueChange={handleFullLogoChange}
           description="Upload a full logo with text. Recommended size: At least 100px tall, landscape format"
           type="full-logo"

Analysis

Property name mismatch: fullLogoUrl vs logoFullUrl in project settings

What fails: Full logo upload fails silently. The handleFullLogoChange() callback and logo value reading use the wrong property name fullLogoUrl instead of the correct logoFullUrl defined in the type system.

How to reproduce:

  1. Navigate to project settings page
  2. Upload a full logo using the "Full Logo" upload field
  3. The upload callback is called but fails to update the project because it attempts to set non-existent property fullLogoUrl instead of logoFullUrl

Result: Full logo is uploaded but not saved to the project. Property access project.fullLogoUrl is undefined because the actual property on the AdminProject type is logoFullUrl.

Expected: Should use logoFullUrl throughout, consistent with:

  • Type definition in packages/template/src/lib/stack-app/projects/index.ts (line 58)
  • API mapping in packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts (line 130)
  • Backend schema in packages/stack-shared/src/interface/crud/projects.ts
  • Related properties pattern: logoUrl, logoDarkModeUrl, logoFullDarkModeUrl

Files affected:

  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/project-settings/page-client.tsx - line 130 and line 221

Comment on lines 34 to 36
}
onClick?.();
onClick?.(e);
Copy link

@vercel vercel bot Nov 25, 2025

Choose a reason for hiding this comment

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

Suggested change
}
onClick?.();
onClick?.(e);
} else {
onClick?.(e);
}

The onClick callback is being called twice when needConfirm is true, causing unintended duplicate invocations of click handlers.

View Details

Analysis

Duplicate onClick callback invocation in Link component with confirmation dialog

What fails: The Link component in apps/dashboard/src/components/link.tsx invokes the onClick callback twice when needConfirm is true, causing duplicate execution of click handlers.

How to reproduce:

  1. Render the Link component with needConfirm=true and an onClick handler
  2. Click the link
  3. The onClick handler executes twice - once on line 33 (inside the if block) and again on line 36 (unconditionally after the if block)

Expected behavior: The onClick callback should execute exactly once per click:

  • When needConfirm is true: execute inside the if block (before routing)
  • When needConfirm is false: execute in the else block (during normal link handling)

Root cause: Line 36 had an unconditional onClick?.(e) call outside the if-else structure, causing it to execute regardless of the needConfirm condition. This resulted in duplicate invocations when needConfirm was true.

Impact: Any code using the Link component with an onClick handler experiences duplicate callback execution when needConfirm is true, potentially causing unintended side effects or duplicate operations.

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: 2

🧹 Nitpick comments (2)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe.tsx (2)

133-133: Consider throttling tooltip position updates for better performance.

Updating tooltipPos on every mousemove event (line 157) triggers a re-render for each movement, which can be expensive. Consider throttling or using a ref-based approach for smoother performance.

Example with throttling:

const throttledSetTooltipPos = useRef(
  throttle((x: number, y: number) => setTooltipPos({ x, y }), 16) // ~60fps
).current;

Then in the onMouseMove handler:

  onMouseMove={(e) => {
    resumeRender();
-   setTooltipPos({ x: e.clientX, y: e.clientY });
+   throttledSetTooltipPos(e.clientX, e.clientY);
  }}

185-185: Clarify the purpose of the 100ms delay.

The setTimeout delay before setting isGlobeReady appears arbitrary. Document why this delay is necessary or consider using the globe's built-in ready state if available.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fdf3093 and 197c365.

📒 Files selected for processing (1)
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe.tsx (3 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Ensure code passes pnpm typecheck for TypeScript type validation
Never use toast for blocking alerts and errors; use alerts instead as they are more visible to users
Keep hover/click transitions snappy and fast; apply transitions after the action (e.g., smooth fade-out on hover end) rather than delaying actions with pre-transitions to avoid sluggish UI
Never use try-catch-all, never void a promise, never use .catch(console.error) or similar; use runAsynchronously or runAsynchronouslyWithAlert for error handling instead of try-catch blocks
Use loading indicators instead of try-catch blocks for asynchronous UI operations; button components like support async callbacks with built-in loading state
Use ES6 maps instead of records wherever possible

Files:

  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe.tsx
apps/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Prefer client components and dynamic functions like usePathname over Next.js dynamic functions to keep pages static; avoid using await params or similar dynamic patterns

Files:

  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (12)
  • GitHub Check: Vercel Agent Review
  • GitHub Check: Cursor Bugbot
  • GitHub Check: restart-dev-and-test-with-custom-base-port
  • GitHub Check: check_prisma_migrations (22.x)
  • GitHub Check: setup-tests
  • GitHub Check: build (22.x)
  • GitHub Check: restart-dev-and-test
  • GitHub Check: all-good
  • GitHub Check: build (22.x)
  • GitHub Check: docker
  • GitHub Check: build (22.x)
  • GitHub Check: lint_and_build (latest)
🔇 Additional comments (3)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe.tsx (3)

42-46: Good simplification of sizing logic.

The streamlined approach using container size with a sensible fallback improves readability.


310-325: Nice simplification of the loading indicator.

The new loader is cleaner, more maintainable, and uses standard Tailwind animations effectively.


272-272: The z-[100] value is appropriate and justified; no changes needed.

Verification shows the codebase maintains an intentional z-index hierarchy: z-[200] for dropdowns, z-[100] for the globe tooltip, and z-[9999] for critical overlays in stack-companion.tsx. The z-[100] value is neither "extremely high" nor causing stacking conflicts. As a non-interactive tooltip (pointer-events-none), it appropriately sits above standard UI elements while below critical system overlays. This follows standard tooltip positioning practices.

Comment on lines 184 to 199
onGlobeReady={() => {
setTimeout(() => setIsGlobeReady(true), 100);
const current = globeRef.current;
if (!current) return;

const controls = current.controls();
controls.autoRotate = true;
controls.autoRotateSpeed = 0.5;
controls.maxDistance = 1000;
controls.minDistance = 200;
controls.dampingFactor = 0.15;
controls.enableZoom = false;
controls.enableRotate = true;
current.camera().position.z = 420;
resumeRender();
}}
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

🧩 Analysis chain

Verify controls.dampingFactor is a valid property.

The code sets controls.dampingFactor = 0.15 on line 194, but this property may not exist on the OrbitControls type. Verify this doesn't cause issues or silent failures.


🌐 Web query:

Does three.js OrbitControls support a dampingFactor property, or should it be enableDamping and dampingFactor used together?

💡 Result:

Yes — OrbitControls has a dampingFactor property, but it only takes effect when enableDamping is true. Set controls.enableDamping = true and then adjust controls.dampingFactor (and call controls.update() each frame). [1][2]

Sources:
[1] three.js OrbitControls docs.
[2] three.js examples / docs page (OrbitControls properties).



---

🏁 Script executed:

```shell
#!/bin/bash
# Check the globe.tsx file for enableDamping usage
rg -n "enableDamping" apps/dashboard/src/app/\(main\)/\(protected\)/projects/\[projectId\]/\(overview\)/globe.tsx

Length of output: 0



---

🏁 Script executed:

```shell
#!/bin/bash
# Find the globe.tsx file and check its contents around line 194
fd -e tsx globe.tsx | head -5

Length of output: 83



---

🏁 Script executed:

```shell
#!/bin/bash
# View the context around lines 184-199 in globe.tsx
sed -n '180,205p' 'apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe.tsx'

Length of output: 1115


Add controls.enableDamping = true for dampingFactor to take effect.

The dampingFactor property only takes effect when enableDamping is true. Line 194 sets controls.dampingFactor = 0.15, but controls.enableDamping is never enabled, causing the damping effect to be silently inactive. Add controls.enableDamping = true; before setting dampingFactor.

🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe.tsx
around lines 184 to 199, the code sets controls.dampingFactor but never enables
damping; add controls.enableDamping = true before setting controls.dampingFactor
so the dampingFactor takes effect, leaving the rest of the control configuration
unchanged.

Comment on lines 207 to 214
onPolygonHover={(d: any) => {
resumeRender();
if (d) {
setPolygonSelectedCountry({ code: d.properties.ISO_A2_EH, name: d.properties.NAME });
} else {
if (value === null && maxColorValue < 0.0001) {
// if there are no users at all, in dark mode, show the globe in a slightly lighter color
return `hsl(240, 84%, ${highlight ? '30%' : '20%'})`;
} else {
return `hsl(240, 84%, ${highlight ? '25%' : '15%'})`;
}
setPolygonSelectedCountry(null);
}
}
const scaled = value / maxColorValue;
if (theme === 'light') {
return `hsl(${175 * (1 - scaled)}, 100%, ${20 + 40 * scaled + (highlight ? 10 : 0)}%)`;
} else {
return `hsl(240, 84%, ${24 + 60 * scaled + (highlight ? 10 : 0)}%)`;
}
};
const color = createColor(colorValues.get(country.properties.ISO_A2_EH) ?? null);
return color;
}}
onHexPolygonHover={(d: any) => {
resumeRender();
if (d) {
setHexSelectedCountry({ code: d.properties.ISO_A2_EH, name: d.properties.NAME });
} else {
setHexSelectedCountry(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

🧩 Analysis chain

Replace any types with proper TypeScript types.

Multiple callback handlers use any for polygon/country data (lines 207, 219, 223, 240, 251), which bypasses type checking and could lead to runtime errors.

Check if react-globe.gl exports proper types for these parameters:

If types are available, update the callbacks:

- onPolygonHover={(d: any) => {
+ onPolygonHover={(d: GeoJsonFeature | null) => {

Also applies to: 219-247, 251-257


🏁 Script executed:

#!/bin/bash
# Description: Check for type definitions in react-globe.gl

# Search for type exports in node_modules
fd -t f -e d.ts . node_modules/react-globe.gl --exec cat {} \; | rg -A 5 "onPolygonHover|onHexPolygonHover|onHexPolygonClick"

Length of output: 114


🏁 Script executed:

#!/bin/bash
# Description: Check repository structure and package.json for react-globe.gl

# Check if we're in the right directory and look for package.json
echo "=== Checking for package.json files ==="
find . -maxdepth 3 -name "package.json" -type f | head -10

echo -e "\n=== Checking repository root structure ==="
ls -la | head -20

echo -e "\n=== Checking if node_modules exists ==="
if [ -d "node_modules" ]; then
  echo "node_modules exists"
  echo "react-globe.gl location:"
  ls -la node_modules/ | grep -i globe || echo "Not found in direct listing"
else
  echo "node_modules does not exist"
fi

echo -e "\n=== Checking the actual globe.tsx file ==="
if [ -f "apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe.tsx" ]; then
  echo "File found. Showing lines 200-220:"
  sed -n '200,220p' "apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe.tsx"
else
  echo "File not found at expected path"
fi

echo -e "\n=== Checking package.json for react-globe.gl ==="
rg -A 2 -B 2 "react-globe" package.json --no-heading 2>/dev/null || echo "Not found in root package.json"

Length of output: 2701



---

🏁 Script executed:

```shell
#!/bin/bash
# Description: Check dashboard package.json and examine globe.tsx for type info

echo "=== Dashboard package.json react-globe.gl entry ==="
rg -A 2 -B 2 "react-globe" apps/dashboard/package.json || echo "Check failed"

echo -e "\n=== Checking for import statements and types in globe.tsx ==="
head -50 "apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe.tsx" | rg -A 3 "import|type |interface"

echo -e "\n=== Checking full callback signatures in globe.tsx ==="
sed -n '1,50p' "apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe.tsx"

Length of output: 3966


🌐 Web query:

react-globe.gl TypeScript type definitions onPolygonHover callback

💡 Result:

Signature (JS): onPolygonHover(polygon, prevPolygon)

TypeScript: type OnPolygonHover = (polygon: T | null, prevPolygon: T | null) => void

Usage example (assuming your polygon item type is MyPolygon or a GeoJSON Feature):

  • interface MyPolygon { id: string; geometry: any; name?: string }
  • const handlePolygonHover: OnPolygonHover = (polygon, prev) => { /* ... */ }

References: react-globe.gl docs (onPolygonHover signature). [1][2]

Sources
[1] react-globe.gl README (onPolygonHover description). [2] globe.gl / react-globe.gl props docs (polygons layer).



🏁 Script executed:

#!/bin/bash
# Description: Extract and verify all callback signatures with `any` types

echo "=== Extracting callback lines mentioned in review ==="
echo "Line 207 (onPolygonHover):"
sed -n '207p' "apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe.tsx"

echo -e "\nLine 219 area (hexPolygonAltitude - context 216-222):"
sed -n '216,222p' "apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe.tsx"

echo -e "\nLine 223 area (likely onHexPolygonHover - context 223-230):"
sed -n '223,230p' "apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe.tsx"

echo -e "\nLine 240 area (context 238-245):"
sed -n '238,245p' "apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe.tsx"

echo -e "\nLine 251 area (context 249-257):"
sed -n '249,257p' "apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe.tsx"

echo -e "\n=== Summary: All lines with 'any' type in callbacks ==="
rg -n "(: any)" "apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe.tsx"

Length of output: 2467


Replace any types with proper TypeScript types in callback handlers.

The library exports generic type definitions for these callbacks. Five instances use any which bypasses type safety:

  • Line 207: onPolygonHover
  • Line 219: hexPolygonAltitude
  • Line 223: hexPolygonColor
  • Line 240: onHexPolygonHover
  • Line 251: onHexPolygonClick

All callbacks access GeoJSON Feature properties (.properties.ISO_A2_EH, .properties.NAME), so import and use proper types from react-globe.gl or define a GeoJSON Feature interface. This aligns with the coding guideline requiring TypeScript type validation via pnpm typecheck.

🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/globe.tsx
around lines 207–214 and the other mentioned callbacks (lines ~219, ~223, ~240,
~251), replace the use of `any` in the callback parameter types with a proper
GeoJSON/feature type: import the appropriate types from react-globe.gl if
available (e.g., the library's Feature/Polygon/HexPolygon generic types) or
define a local GeoJSON Feature<T> interface that exposes properties.ISO_A2_EH
and properties.NAME, then annotate each handler parameter with that type and
adjust any null checks accordingly so TypeScript can validate access to
.properties.ISO_A2_EH and .properties.NAME (ensure corresponding imports/exports
are added at the top of the file).

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

This PR is being reviewed by Cursor Bugbot

Details

You are on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle.

To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

await project.update({ logoFullUrl });
}}
value={project.fullLogoUrl}
onValueChange={handleFullLogoChange}
Copy link

Choose a reason for hiding this comment

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

Bug: Incorrect property name for full logo URL

The property name was changed from logoFullUrl to fullLogoUrl, but this doesn't match the actual project property naming convention. Other logo properties in this file follow the pattern logoUrl, logoDarkModeUrl, logoFullDarkModeUrl (all starting with "logo"). The code-editor.tsx type definition also shows the correct name is logoFullUrl. Reading project.fullLogoUrl will return undefined and updating with { fullLogoUrl } won't affect the correct field, breaking the Full Logo upload functionality.

Additional Locations (1)

Fix in Cursor Fix in Web

publishableClientKey: newKey.publishableClientKey!,
secretServerKey: newKey.secretServerKey!,
});
setIsGenerating(false);
Copy link

Choose a reason for hiding this comment

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

Bug: Missing error handling leaves button permanently disabled

The handleGenerateKeys function had its try/catch/finally block removed. If createInternalApiKey throws an error, setIsGenerating(false) won't be called, leaving the "Generate keys" button permanently disabled. Additionally, setError is never called so the error alert UI (which still exists in StepCard) will never display feedback to the user. The old code properly handled errors and always reset the loading state in a finally block.

Fix in Cursor Fix in Web

} finally {
setIsGenerating(false);
}
const newKey = await adminApp.createInternalApiKey({
Copy link

@vercel vercel bot Nov 25, 2025

Choose a reason for hiding this comment

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

Missing error handling: The try-catch-finally block was removed from handleGenerateKeys, causing the isGenerating flag to never be reset if an error occurs during API key generation.

View Details
📝 Patch Details
diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/vercel/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/vercel/page-client.tsx
index ad7e5bfc..08c6122d 100644
--- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/vercel/page-client.tsx
+++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/vercel/page-client.tsx
@@ -77,20 +77,26 @@ export default function PageClient() {
     setIsGenerating(true);
     setError(null);
 
-    const newKey = await adminApp.createInternalApiKey({
-      hasPublishableClientKey: true,
-      hasSecretServerKey: true,
-      hasSuperSecretAdminKey: false,
-      expiresAt: new Date(Date.now() + TWO_HUNDRED_YEARS_IN_MS),
-      description: "Vercel Integration",
-    });
-
-    setKeys({
-      projectId: adminApp.projectId,
-      publishableClientKey: newKey.publishableClientKey!,
-      secretServerKey: newKey.secretServerKey!,
-    });
-    setIsGenerating(false);
+    try {
+      const newKey = await adminApp.createInternalApiKey({
+        hasPublishableClientKey: true,
+        hasSecretServerKey: true,
+        hasSuperSecretAdminKey: false,
+        expiresAt: new Date(Date.now() + TWO_HUNDRED_YEARS_IN_MS),
+        description: "Vercel Integration",
+      });
+
+      setKeys({
+        projectId: adminApp.projectId,
+        publishableClientKey: newKey.publishableClientKey!,
+        secretServerKey: newKey.secretServerKey!,
+      });
+    } catch (err) {
+      console.error(err);
+      setError(err instanceof Error ? err.message : "Failed to generate API keys. Please try again.");
+    } finally {
+      setIsGenerating(false);
+    }
   };
 
   const toggleStepCompletion = (id: StepId) => {

Analysis

Missing error handling in handleGenerateKeys leaves UI in loading state on API failure

What fails: The handleGenerateKeys function in apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/vercel/page-client.tsx lacks a try-catch block. When adminApp.createInternalApiKey() throws an error, the promise rejection is unhandled, causing setIsGenerating(false) to never execute.

How to reproduce:

  1. Navigate to the Vercel integration setup page
  2. Click "Generate keys" button
  3. If the API call fails (network error, server error, permissions issue, etc.), the button stays in the "Generating..." disabled state permanently
  4. The error is never displayed to the user
  5. User cannot retry the operation

Result: The "Generate keys" button becomes permanently disabled and displays "Generating..." indefinitely when an error occurs. The component is stuck and unusable.

Expected: When the API call fails, the error should be caught, displayed to the user via the error Alert component (line 590), and isGenerating should be reset to false to re-enable the button for retry.

Why it's a bug: The original implementation (commit 0540f39) included try-catch-finally error handling. This was removed in a recent change, breaking error recovery. Without the try-catch, execution stops at the failed await, and the finally block (which resets setIsGenerating(false)) never runs, leaving the UI in an incomplete state.

Fix applied: Restored the try-catch-finally block to properly handle errors and ensure setIsGenerating(false) always runs via the finally clause.

@BilalG1 BilalG1 closed this Nov 25, 2025
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.

3 participants