Skip to content

Comments

app/insights#470

Merged
IAmKio merged 20 commits intostagingfrom
app/insights
Dec 4, 2025
Merged

app/insights#470
IAmKio merged 20 commits intostagingfrom
app/insights

Conversation

@IAmKio
Copy link
Collaborator

@IAmKio IAmKio commented Dec 2, 2025

Summary by CodeRabbit

  • New Features

    • Full Insights dashboard: signal cards, feed/events, KPIs, sparklines, interactive filters/tabs/leverage, detailed P&L modal, responsive mobile/desktop UI, carousel, OTP input, and sidebar.
    • Consent & Terms flow with knowledge checks, subscription gating, polling, and checkout flow.
    • New toast/notification system, charts, and many reusable UI controls (dialogs, modals, tooltips, popovers, menus, forms, tables, inputs).
  • Chores

    • New design tokens, styles, manifest and Tailwind config; expanded frontend dependencies; added insights ignore rule.

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

@IAmKio IAmKio self-assigned this Dec 2, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 2, 2025

Walkthrough

Adds a new Insights app: UI primitives, many React components (cards, charts, consent flow, modals), hooks, utils, types, styles and Tailwind config, an API client for Firebase Functions, a Supabase-compatible adapter, and a top-level App coordinating consent, subscription gating, polling, sparklines and PnL aggregation.

Changes

Cohort / File(s) Summary
Repo config
\.gitignore, package.json
Added ignored path /src/apps/insights/_lovable and a large set of new dependencies and devDependencies (Radix UI packages, cmdk, framer-motion, recharts, react-hook-form, vaul, sonner, tailwind-merge, tailwindcss-animate, etc.).
App entry & manifest
src/apps/insights/index.tsx, src/apps/insights/manifest.json
New top-level Insights App component (orchestrator) and static manifest.json.
API client
src/apps/insights/api/insightsApi.ts
New HTTP client for Firebase Functions with endpoints: sparkline-data, trading-signals, update-signal-prices, recalculate-historical-pnl, webhook-receiver, and subscription status.
Supabase adapter
src/apps/insights/utils/supabaseAdapter.ts
New adapter exposing a Supabase-like API surface routing to insights API functions and simple query helpers (from/select/order/eq, functions.invoke, channel stubs).
Components — Feature
src/apps/insights/components/*
src/apps/insights/components/FeedEventCard/FeedEventCard.tsx, .../Header/Header.tsx, .../KPICard/KPICard.tsx, .../SignalCard/SignalCard.tsx, .../SparklineChart.tsx, .../PnLDetailModal.tsx, .../Filters/Filters.tsx
Many new React components implementing insights UI: KPI cards, signal & feed cards, filters, sparkline charts, PnL modal, header, and related feature pieces with typed props, animations, and chart integrations.
Components — Consent
src/apps/insights/components/consent/*
Consent modal, layout, tab contents (Consent/Terms/Risk), constants, types, validation hook, and index re-exports modeling consent payload and UI flow.
UI primitives & toolkit
src/apps/insights/components/ui/*
Large set of UI primitives and wrappers (Radix/vaul/cmdk/others): accordion, alert, alert-dialog, avatar, badge, button, calendar, card, carousel, command palette, context menu, dialog, drawer, dropdown-menu, form primitives, input, label, menubar, navigation-menu, select, slider, sheet, sidebar, table, tabs, toast/toaster, and many others.
Toasts & runtime
src/apps/insights/components/ui/use-toast.ts, .../toaster.tsx, .../sonner.tsx, .../toast.tsx
In-memory toast manager hook, Toaster component, Sonner wrapper and styled toast primitives.
Hooks
src/apps/insights/hooks/*
New hooks: useIsMobile, useLogoMap, useSparklineData, useSubscriptionStatus, useTradingSignals.
Utilities & libs
src/apps/insights/lib/*, src/apps/insights/utils/*
Utility functions: cn (clsx + twMerge), format helpers, logo caching + Mobula fetch, stop-loss normalization, signal timeline/feed/PnL generators, and the Supabase adapter.
Types
src/apps/insights/types/index.ts, src/apps/insights/components/consent/types.ts
New TypeScript types/interfaces: TradingSignal, FeedEvent, SparklineDataPoint, subscription records, consent payloads, unions and related types.
Styling & Tailwind
src/apps/insights/styles/insights.css, src/apps/insights/tailwind.insights.config.js
New design-system CSS (tokens, utilities, dark theme, fonts, utility classes) and an app-specific Tailwind config (content, theme extensions, keyframes, animations, plugin).

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant App
    participant Cache as LocalStorage
    participant API as FirebaseFunctions
    participant Adapter as SupabaseAdapter

    User->>App: open Insights app
    App->>Cache: read stored consent, logo cache, cached sparklines
    App->>App: evaluate consent & subscription state
    alt consent missing
        App->>User: show ConsentModal
        User->>App: accept consent
        App->>Cache: persist consent
    end
    App->>API: GET /insights/trading-signals
    API-->>App: returns signals
    App->>Adapter: optional normalize/query via supabase-style adapter
    App->>App: compute timelines, feed events, PnL, identify sparklines needed
    loop per ticker needing sparkline
        App->>API: POST /insights/sparkline-data (ticker,start,end)
        API-->>App: sparkline data
        App->>Cache: save sparkline
    end
    loop every 30s when active
        App->>API: POST /insights/update-signal-prices
        API-->>App: updated prices
        App->>App: recompute PnL & refresh UI
    end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

  • Areas requiring close attention:
    • src/apps/insights/index.tsx — initialization, side effects, consent/subscription gating, polling, and external navigation handling.
    • src/apps/insights/lib/stopLossUtils.ts and src/apps/insights/utils/signalUtils.ts — trailing-stop normalization, timeline generation, and PnL calculations (legacy formats & edge cases).
    • src/apps/insights/api/insightsApi.ts and src/apps/insights/utils/supabaseAdapter.ts — HTTP contracts, error handling, and function routing.
    • UI primitives & form integrations: src/apps/insights/components/ui/form.tsx, carousel, toast/use-toast, sidebar — accessibility, ID wiring, event cleanup.
    • Charts and cards: SparklineChart, SignalCard, KPICard — SVG geometry, chart scaling, empty-data handling and performance.
    • package.json dependency additions — versions, peer compatibility, and potential bundle size impact.

Poem

🐇 I hopped through props and stitched each chart,
Sparkline dots and KPIs found their art.
Consent was signed, the feed now sings—
Signals leap and PnL springs.
A cheerful rabbit cheers: Insights take heart!

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Description check ⚠️ Warning The PR description is largely incomplete and does not follow the required template structure. Provide a detailed description section explaining what changes are included and why. Complete the 'How Has This Been Tested?' section with specific testing details and environment information. If applicable, add screenshots. Select the relevant checkbox(es) for 'Types of changes' (Bug fix, New feature, or Breaking change).
Title check ❓ Inconclusive The title 'app/insights' is vague and generic, using a directory path instead of describing the actual change. Replace with a concise, specific title summarizing the main feature (e.g., 'Add Insights App UI with trading signal dashboard and consent system').
✨ 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 app/insights

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.

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Dec 2, 2025

Deploying pillarx-debug with  Cloudflare Pages  Cloudflare Pages

Latest commit: cd08069
Status: ✅  Deploy successful!
Preview URL: https://39cd51d7.pillarx-debug.pages.dev
Branch Preview URL: https://app-insights.pillarx-debug.pages.dev

View logs

@github-actions github-actions bot requested a deployment to Preview (app/insights) December 2, 2025 13:59 Abandoned
@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Dec 2, 2025

Deploying x with  Cloudflare Pages  Cloudflare Pages

Latest commit: cd08069
Status: ✅  Deploy successful!
Preview URL: https://a1aedacb.x-e62.pages.dev
Branch Preview URL: https://app-insights.x-e62.pages.dev

View logs

@github-actions github-actions bot requested a deployment to Preview (app/insights) December 2, 2025 14:11 Abandoned
@github-actions github-actions bot requested a deployment to Preview (app/insights) December 2, 2025 14:20 Abandoned
@github-actions github-actions bot requested a deployment to Preview (app/insights) December 2, 2025 14:23 Abandoned
@github-actions github-actions bot requested a deployment to Preview (app/insights) December 2, 2025 17:06 Abandoned
@github-actions github-actions bot requested a deployment to Preview (app/insights) December 2, 2025 17:11 Abandoned
@github-actions github-actions bot requested a deployment to Preview (app/insights) December 2, 2025 17:23 Abandoned
@github-actions github-actions bot requested a deployment to Preview (app/insights) December 2, 2025 17:55 Abandoned
@github-actions github-actions bot temporarily deployed to Preview (app/insights) December 2, 2025 18:25 Inactive
@github-actions github-actions bot temporarily deployed to Preview (app/insights) December 3, 2025 11:26 Inactive
@github-actions github-actions bot temporarily deployed to Preview (app/insights) December 3, 2025 16:55 Inactive
@github-actions github-actions bot temporarily deployed to Preview (app/insights) December 3, 2025 17:54 Inactive
@github-actions github-actions bot temporarily deployed to Preview (app/insights) December 3, 2025 18:11 Inactive
@IAmKio IAmKio marked this pull request as ready for review December 3, 2025 22:28
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 13

Note

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

🟡 Minor comments (17)
src/apps/insights/components/ui/carousel.tsx-97-102 (1)

97-102: Missing cleanup for "reInit" event listener.

The reInit event listener is registered but not removed in the cleanup function, which can cause memory leaks if the carousel unmounts while the API still holds a reference.

Apply this diff to remove both listeners:

      return () => {
+       api?.off("reInit", onSelect);
        api?.off("select", onSelect);
      };
src/apps/insights/utils/formatUtils.ts-5-21 (1)

5-21: Align PEPE comment with actual behavior in formatPrice

normalizeSymbol and the general pricing rules look fine. One small mismatch:

  • The comment says: “except PEPEUSDT uses 2 decimals”.
  • The code applies toFixed(2) to any ticker whose symbol includes "PEPE" (e.g. PEPE, PEPEUSDT, PEPEUSD).

Either broaden the comment (e.g. “PEPE tickers use 2 decimals”) or narrow the condition to the exact symbol(s) you intend (for example by checking a normalized symbol). Using toFixed is consistent with the existing trade-off around float precision in this codebase. Based on learnings, this matches prior decisions.

src/apps/insights/components/ui/input-otp.tsx-27-29 (1)

27-29: Add defensive check for slot access.

Accessing inputOTPContext.slots[index] without validation could cause a runtime error if index is out of bounds or the context is not properly initialized.

 >(({ index, className, ...props }, ref) => {
   const inputOTPContext = React.useContext(OTPInputContext);
-  const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index];
+  const slot = inputOTPContext?.slots?.[index];
+  if (!slot) {
+    return null;
+  }
+  const { char, hasFakeCaret, isActive } = slot;
src/apps/insights/components/Header/Header.tsx-84-91 (1)

84-91: Redundant calculation: closedTotalPnL is already a prop.

calculateTotalPnL(closedSignals) is called on line 86, but the computed value closedTotalPnL is already passed as a prop. This creates inconsistency with how Open Positions P&L uses openTotalPnL directly.

        <KPICard
          title="Closed Trades P&L"
-          value={applyLeverage(calculateTotalPnL(closedSignals))}
+          value={applyLeverage(closedTotalPnL)}
          badge={closedSignals.length}
          badgeColor="slate"
          sparklineData={closedPnLSparklineData}
          onClick={() => onPnLViewClick('closed')}
        />

If calculateTotalPnL is intentionally needed here for a different calculation, consider removing closedTotalPnL from props to avoid confusion.

src/apps/insights/components/consent/useConsentValidation.ts-3-3 (1)

3-3: Unused import: CORRECT_Q2_ANSWERS.

This constant is imported but never used in the hook.

-import { CORRECT_Q2_ANSWERS } from "./constants";
src/apps/insights/hooks/useSparklineData.ts-37-40 (1)

37-40: Add type definition for the API response structure.

The fetchSparklineData function returns an untyped any response, and line 39's cast to SparklineDataPoint[] lacks validation. Define a proper response interface (e.g., SparklineApiResponse) with the candles property typed as SparklineDataPoint[] to ensure type safety and catch potential mismatches at compile time rather than runtime.

src/apps/insights/components/PnLDetailModal.tsx-481-481 (1)

481-481: Typo: Double colon :: will render as text.

There's an extraneous :: at the end of the className string that will be rendered as visible text in the DOM.

-      <DialogContent className="w-full sm:w-full max-w-[calc(100vw-2rem)] sm:max-w-4xl mx-4 sm:mx-0 h-[92vh] sm:h-auto max-h-[90vh] overflow-y-auto rounded-3xl sm:rounded-3xl p-5 sm:p-7 glass-card border-primary/20">::
+      <DialogContent className="w-full sm:w-full max-w-[calc(100vw-2rem)] sm:max-w-4xl mx-4 sm:mx-0 h-[92vh] sm:h-auto max-h-[90vh] overflow-y-auto rounded-3xl sm:rounded-3xl p-5 sm:p-7 glass-card border-primary/20">
src/apps/insights/components/PnLDetailModal.tsx-302-307 (1)

302-307: colSpan mismatch: Table has 7 columns but empty state spans 6.

The table header defines 7 columns (Ticker, Entry, Current/Exit, P&L, TPs Hit, Status, Date), but the empty state colSpan is set to 6, causing misalignment.

-                    <TableCell colSpan={6} className="text-center text-muted-foreground py-8">
+                    <TableCell colSpan={7} className="text-center text-muted-foreground py-8">
src/apps/insights/components/FeedEventCard/FeedEventCard.tsx-36-48 (1)

36-48: Missing default case in getEventColor - returns undefined for unknown event types.

If an unexpected event.type is passed, this function returns undefined, which could cause styling issues. Consider adding a default case.

   const getEventColor = () => {
     switch (event.type) {
       case 'signal_opened':
         return event.order_side === 'buy' ? 'border-green-500/30' : 'border-red-500/30';
       case 'tp_hit':
       case 'completed':
         return 'border-green-500/30';
       case 'stop_loss_hit':
         return 'border-red-500/30';
       case 'opposite_closed':
         return 'border-yellow-500/30';
+      default:
+        return 'border-slate-500/30';
     }
   };
src/apps/insights/components/consent/ConsentTabContent.tsx-86-105 (1)

86-105: Gate cooling‑off waiver error on needsCoolingOffWaiver

The error message:

{showErrors && !validation.isCoolingOffWaiverValid && (/* ... */)}

is not conditioned on needsCoolingOffWaiver. If needsCoolingOffWaiver becomes false while isCoolingOffWaiverValid is still false, users could see an error for a control that is no longer rendered.

Consider:

{needsCoolingOffWaiver &&
  showErrors &&
  !validation.isCoolingOffWaiverValid && (
    /* error */
)}

to keep UI and validation messaging aligned.

src/apps/insights/components/ui/breadcrumb.tsx-80-80 (1)

80-80: Typo in displayName: "BreadcrumbElipssis" should be "BreadcrumbEllipsis".

-BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
+BreadcrumbEllipsis.displayName = "BreadcrumbEllipsis";
src/apps/insights/components/ui/menubar.tsx-185-188 (1)

185-188: Typo: displayname should be displayName (capital N).

This will cause MenubarShortcut to not show its name correctly in React DevTools.

 const MenubarShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
   return <span className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)} {...props} />;
 };
-MenubarShortcut.displayname = "MenubarShortcut";
+MenubarShortcut.displayName = "MenubarShortcut";
src/apps/insights/components/ui/card.tsx-17-22 (1)

17-22: Type mismatch in CardTitle ref typing.

The forwardRef generic specifies HTMLParagraphElement for the ref, but the component renders an <h3> element (which is HTMLHeadingElement). This should be consistent.

-const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
+const CardTitle = React.forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>(
   ({ className, ...props }, ref) => (
     <h3 ref={ref} className={cn("text-2xl font-semibold leading-none tracking-tight", className)} {...props} />
   ),
 );
src/apps/insights/components/ui/use-toast.ts-166-177 (1)

166-177: Incorrect dependency in useEffect causes unnecessary re-subscriptions.

The effect depends on [state], but state is not used inside the effect body. This causes the listener to unsubscribe and re-subscribe on every state change, which is unnecessary and could lead to subtle bugs if the array order changes during rapid updates.

   React.useEffect(() => {
     listeners.push(setState);
     return () => {
       const index = listeners.indexOf(setState);
       if (index > -1) {
         listeners.splice(index, 1);
       }
     };
-  }, [state]);
+  }, []);
src/apps/insights/utils/signalUtils.ts-48-73 (1)

48-73: Wrap switch case declarations in blocks to prevent scope leakage.

The const declarations inside switch cases can be erroneously accessed by other cases. This is flagged by static analysis (Biome: noSwitchDeclarations).

Apply this diff to fix the scoping issue:

       case 'tp_hit':
+      {
         const tpPercent = ((event.tp_price || 0) - signal.entry_price) / signal.entry_price * 100;
         const lockedIn = tpPercent * 0.3333;
         
         timelineItems.push({
           icon: 'Target',
           iconColor: 'text-emerald-400',
           label: `${event.tp_level?.toUpperCase()} Hit`,
           price: event.tp_price || 0,
           timestamp: event.timestamp,
           pnl: lockedIn,
         });
         
         // Add stop loss move event if it moved
         if (event.moved && event.old_stop && event.new_stop) {
           timelineItems.push({
             icon: 'RefreshCw',
             iconColor: 'text-green-400',
             label: 'Stop Loss Moved',
             price: event.new_stop,
             timestamp: event.timestamp,
             pnl: null,
             detail: `$${formatPrice(event.old_stop, signal.ticker)} → $${formatPrice(event.new_stop, signal.ticker)}`
           });
         }
         break;
+      }
src/apps/insights/utils/signalUtils.ts-75-89 (1)

75-89: Wrap stop_loss_hit case declarations in a block.

Same issue as above - the const declarations need block scoping to prevent accidental access from other cases.

Apply this diff:

       case 'stop_loss_hit':
+      {
         // Calculate stop loss P&L based on position type
         const isShortSL = signal.order_side?.toLowerCase() === 'sell';
         const slPercent = isShortSL
           ? ((signal.entry_price - (event.stop_price || latestStop)) / signal.entry_price) * 100  // SHORT
           : (((event.stop_price || latestStop) - signal.entry_price) / signal.entry_price) * 100; // LONG
         timelineItems.push({
           icon: 'XCircle',
           iconColor: 'text-red-400',
           label: 'Stop Loss Hit',
           price: event.stop_price || latestStop,
           timestamp: event.timestamp,
           pnl: slPercent,
         });
         break;
+      }
src/apps/insights/components/ui/chart.tsx-210-214 (1)

210-214: Bug: Truthy check on item.value will skip rendering when value is 0.

The condition item.value && (...) evaluates to false when item.value is 0, which is a valid numeric value that should be displayed in charts.

Apply this diff to fix:

-                      {item.value && (
+                      {item.value !== undefined && item.value !== null && (
                         <span className="font-mono font-medium tabular-nums text-foreground">
                           {item.value.toLocaleString()}
                         </span>
                       )}
🧹 Nitpick comments (44)
.gitignore (1)

32-32: Reconsider the section placement for this entry.

The /src/apps/insights/_lovable entry is placed under the "Sentry Config File" section comment, but based on the directory name, this appears to be generated or third-party tooling output (likely from Lovable AI), not Sentry-specific configuration. Consider either:

  1. Creating a new section comment for generated/tooling artifacts (e.g., # Generated/tooling artifacts), or
  2. Moving this entry under a more appropriate existing section (e.g., # misc).

The ignore pattern itself is correct and appropriate to keep generated content out of version control.

src/apps/insights/components/ui/carousel.tsx (1)

70-81: Consider supporting ArrowUp/ArrowDown for vertical orientation.

Keyboard navigation only handles ArrowLeft/ArrowRight. For vertical carousels, users would intuitively expect ArrowUp/ArrowDown to navigate slides.

 const handleKeyDown = React.useCallback(
   (event: React.KeyboardEvent<HTMLDivElement>) => {
-    if (event.key === "ArrowLeft") {
+    const prevKey = orientation === "horizontal" ? "ArrowLeft" : "ArrowUp";
+    const nextKey = orientation === "horizontal" ? "ArrowRight" : "ArrowDown";
+
+    if (event.key === prevKey) {
       event.preventDefault();
       scrollPrev();
-    } else if (event.key === "ArrowRight") {
+    } else if (event.key === nextKey) {
       event.preventDefault();
       scrollNext();
     }
   },
-  [scrollPrev, scrollNext],
+  [orientation, scrollPrev, scrollNext],
 );
src/apps/insights/tailwind.insights.config.js (1)

4-6: Simplify redundant glob pattern.

The content path **/**/*.{...} is redundant. A single **/*.{...} will recursively match all nested directories.

   content: [
-    './src/apps/insights/**/**/*.{js,ts,jsx,tsx,html,mdx}',
+    './src/apps/insights/**/*.{js,ts,jsx,tsx,html,mdx}',
   ],
src/apps/insights/utils/supabaseAdapter.ts (1)

99-101: Reminder: Address the TODO comment.

The recalculate-historical-pnl function is not yet implemented.

Would you like me to help implement this function or open a tracking issue for it?

src/apps/insights/hooks/use-mobile.tsx (1)

5-16: Consider initializing state to avoid hydration mismatch.

The initial undefined state means useIsMobile() returns false before the effect runs. On SSR or first client render, this can cause a layout shift when the effect updates the state. Consider initializing with a computed value or handling the undefined case explicitly.

Apply this diff to initialize state immediately:

 export function useIsMobile() {
-  const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined);
+  const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
+    () => typeof window !== 'undefined' ? window.innerWidth < MOBILE_BREAKPOINT : undefined
+  );

   React.useEffect(() => {

Alternatively, return boolean | undefined and let consumers handle the undefined case during initial render.

src/apps/insights/components/ui/sonner.tsx (1)

6-23: Clarify toast system usage and consider theme configuration.

Two observations:

  1. The theme is hardcoded to "dark" (Line 9). Consider making this configurable or deriving it from a theme context to support potential light mode or user preferences.

  2. This Sonner-based toaster coexists with a custom toast system in toaster.tsx. Both export a Toaster component, which may cause confusion about which system to use where. Consider documenting the intended usage pattern or consolidating to a single toast system.

src/apps/insights/components/Filters/Filters.tsx (1)

55-59: Consider graduated risk indicators for different leverage levels.

The current implementation shows a "High Risk" badge for any leverage > 1, treating 3x and 10x equally. Since 10x leverage is significantly riskier than 3x, consider showing graduated risk levels (e.g., "Moderate Risk" for 3x, "High Risk" for 5x, "Extreme Risk" for 10x) to better inform users.

src/apps/insights/components/consent/RiskTabContent.tsx (1)

3-102: React.memo is unnecessary for components without props.

The component is wrapped in React.memo but accepts no props. Memoization only prevents re-renders when props change, so it provides no benefit here. The component will still re-render whenever its parent re-renders.

Apply this diff to simplify:

-export const RiskTabContent = React.memo(() => {
+export const RiskTabContent = () => {
   return (
     <div className="space-y-6">
       {/* ... content ... */}
     </div>
   );
-});
-
-RiskTabContent.displayName = "RiskTabContent";
+};
src/apps/insights/hooks/useLogoMap.ts (2)

54-81: Batch cache saves to avoid excessive localStorage writes.

saveLogoCache(cache) is called after every individual logo fetch, causing N localStorage writes for N missing symbols. This is inefficient and could cause performance issues on devices with slower storage.

Consider saving the cache once after all fetches complete:

          for (const chunk of chunks) {
            await Promise.all(
              chunk.map(async (symbol) => {
                try {
                  const logoUrl = await fetchLogoFromMobula(symbol);
                  
                  if (logoUrl) {
                    // Update logoMap progressively
                    setLogoMap(prev => ({ ...prev, [symbol]: logoUrl }));
                    
                    // Update cache
                    cache[symbol] = { url: logoUrl, updatedAt: Date.now() };
-                   saveLogoCache(cache);
                    console.log(`✓ ${symbol} logo cached:`, logoUrl);
                  } else {
                    console.log(`❌ No logo found for ${symbol}`);
                    // Mark as miss in cache
                    cache[symbol] = { miss: true, updatedAt: Date.now() };
-                   saveLogoCache(cache);
                  }
                } catch (err) {
                  console.error(`❌ Failed to fetch logo for ${symbol}:`, err);
                  cache[symbol] = { miss: true, updatedAt: Date.now() };
-                 saveLogoCache(cache);
                }
              })
            );
+           // Save cache after each chunk completes
+           saveLogoCache(cache);
          }

92-93: Effect may re-run excessively if signals reference changes.

Using signals array directly in the dependency array triggers re-runs whenever the array reference changes, even if the content is identical. Additionally, there's no cleanup to abort in-flight fetches if the component unmounts or signals change mid-fetch.

Consider stabilizing the dependency and adding cleanup:

+import { useRef } from 'react';
+
 export const useLogoMap = (signals: TradingSignal[]) => {
   const [logoMap, setLogoMap] = useState<Record<string, string>>({});
+  const abortControllerRef = useRef<AbortController | null>(null);

   useEffect(() => {
     if (signals.length === 0) return;
+    
+    // Abort previous fetch if still running
+    abortControllerRef.current?.abort();
+    abortControllerRef.current = new AbortController();

     const fetchLogos = async () => {
       // ... existing logic
     };

     fetchLogos();
+    
+    return () => {
+      abortControllerRef.current?.abort();
+    };
-  }, [signals]);
+  }, [signals.map(s => s.id).join(',')]); // Or use a memoized signals array from parent
src/apps/insights/hooks/useTradingSignals.ts (1)

31-53: Consider gating verbose debug logs.

These detailed console logs are helpful during development but will be noisy in production. Consider wrapping them in a development check or removing before release.

-      console.log('🔍 [useTradingSignals] API response:', result);
+      if (process.env.NODE_ENV === 'development') {
+        console.log('🔍 [useTradingSignals] API response:', result);
+      }
src/apps/insights/components/consent/ConsentModal.tsx (1)

18-18: isMobile is not reactive to window resize.

The mobile detection is computed once at render and won't update if the user resizes their browser window. Consider using a custom hook with useEffect and resize listener, or a library like usehooks-ts's useMediaQuery for reactive behavior.

+import { useState, useEffect } from 'react';
+
+function useIsMobile(breakpoint = 768) {
+  const [isMobile, setIsMobile] = useState(() => 
+    typeof window !== 'undefined' ? window.innerWidth < breakpoint : false
+  );
+  
+  useEffect(() => {
+    const handler = () => setIsMobile(window.innerWidth < breakpoint);
+    window.addEventListener('resize', handler);
+    return () => window.removeEventListener('resize', handler);
+  }, [breakpoint]);
+  
+  return isMobile;
+}

Then use const isMobile = useIsMobile(); instead of the direct window.innerWidth check. This also guards against SSR hydration issues.

src/apps/insights/components/consent/ConsentModalLayout.tsx (1)

4-4: Unused import: TabsContent.

TabsContent is imported but not used in this file. It's used by the children passed to this layout component.

-import { Tabs, TabsList, TabsTrigger, TabsContent } from "../ui/tabs";
+import { Tabs, TabsList, TabsTrigger } from "../ui/tabs";
src/apps/insights/components/KPICard/KPICard.tsx (2)

57-59: Hardcoded decimal separator may not match user's locale.

The .replace('.', ',') converts the decimal point to a comma, which is correct for some European locales but incorrect for US/UK users. Consider using Intl.NumberFormat for locale-aware formatting:

-          {value >= 0 ? '+' : ''}{value.toFixed(2).replace('.', ',')}%
+          {value >= 0 ? '+' : ''}{new Intl.NumberFormat(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(value)}%

Or if the comma format is intentional for a specific target audience, add a comment documenting this decision.


27-31: Consider aligning badge colors with Badge component variants.

The custom badgeColors map duplicates styling that could potentially be added as variants to the Badge component. This would centralize badge styling and improve consistency across the app.

If these colors are specific to KPICard, the current approach is acceptable. Otherwise, consider extending badgeVariants in badge.tsx with blue, violet, and slate variants.

src/apps/insights/components/Header/Header.tsx (1)

10-26: Consider grouping related props for maintainability.

The component has 16 props which works but could become unwieldy. For better maintainability, consider grouping related props:

interface PnLData {
  floating: number;
  open: number;
  closed: number;
  sparklines: {
    overall: Array<{ value: number }>;
    open: Array<{ value: number }>;
    closed: Array<{ value: number }>;
  };
}

interface HeaderProps {
  signals: { open: TradingSignal[]; closed: TradingSignal[] };
  pnlData: PnLData;
  controls: {
    activeTab: TabType;
    onTabChange: (tab: TabType) => void;
    leverage: LeverageType;
    onLeverageChange: (leverage: LeverageType) => void;
  };
  // ...
}

This is optional and can be deferred if the current structure is working well.

src/apps/insights/components/consent/useConsentValidation.ts (1)

5-8: Consider importing UseConsentValidationProps from types.ts.

The AI summary indicates this interface is already defined in types.ts. Importing it avoids duplication and keeps types centralized.

-import { ConsentState, ValidationState } from "./types";
-import { CORRECT_Q2_ANSWERS } from "./constants";
-
-interface UseConsentValidationProps {
-  userRegion?: string;
-  immediateAccess?: boolean;
-}
+import { ConsentState, ValidationState, UseConsentValidationProps } from "./types";
src/apps/insights/utils/logoUtils.ts (3)

65-68: Add timeout to prevent indefinite hangs on network issues.

The fetch call has no timeout, which could cause the request to hang indefinitely if the Mobula API is unresponsive.

-    const response = await fetch(
-      `https://explorer-api.mobula.io/api/1/search?mode=og&sortBy=volume_24h&input=${symbol}`
-    );
+    const controller = new AbortController();
+    const timeoutId = setTimeout(() => controller.abort(), 10000);
+    
+    try {
+      const response = await fetch(
+        `https://explorer-api.mobula.io/api/1/search?mode=og&sortBy=volume_24h&input=${symbol}`,
+        { signal: controller.signal }
+      );
+      clearTimeout(timeoutId);

77-98: Define a type for the Mobula API response token.

Using any reduces type safety and IDE support. Consider defining an interface for the token structure.

interface MobulaToken {
  symbol?: string;
  name?: string;
  logo?: string;
  market_cap?: number;
  liquidity?: number;
  volume?: number;
}

16-37: Pruned cache entries are not persisted.

loadLogoCache prunes expired entries but doesn't save the pruned cache back to localStorage. This means expired entries persist until the next saveLogoCache call.

     // Prune expired entries
     const pruned: Record<string, LogoCacheEntry> = {};
     Object.entries(cache).forEach(([symbol, entry]) => {
       const ttl = entry.miss ? MISS_TTL_MS : HIT_TTL_MS;
       if (now - entry.updatedAt < ttl) {
         pruned[symbol] = entry;
       }
     });
     
+    // Persist pruned cache if entries were removed
+    if (Object.keys(pruned).length < Object.keys(cache).length) {
+      saveLogoCache(pruned);
+    }
+    
     return pruned;
src/apps/insights/hooks/useSparklineData.ts (1)

50-57: Fire-and-forget async calls in forEach lose error context.

Using forEach with an async callback means errors thrown by individual fetchSparkline calls won't propagate to the caller. While errors are logged internally, the caller has no way to know if all fetches succeeded.

If the caller needs to track completion or handle errors:

-  const fetchSparklines = useCallback((signals: TradingSignal[]) => {
+  const fetchSparklines = useCallback(async (signals: TradingSignal[]) => {
     const activeSignals = signals.filter(s => s.status === 'active');
     console.log(`🔄 [Sparkline] Fetching sparklines for ${activeSignals.length} active signals`);
     
-    activeSignals.forEach(signal => {
-      fetchSparkline(signal);
-    });
+    await Promise.allSettled(activeSignals.map(signal => fetchSparkline(signal)));
   }, [fetchSparkline]);
src/apps/insights/components/PnLDetailModal.tsx (1)

57-64: Consider typing the isNow property instead of using as any.

The type assertion on line 63 bypasses TypeScript's type checking. Consider defining a proper interface for chart data points that includes the optional isNow property.

interface ChartDataPoint {
  ts: number;
  value: number;
  label: string;
  isNow?: boolean;
}
src/apps/insights/components/SignalCard/SignalCard.tsx (2)

140-156: Trailing stop heuristic may produce false positives.

The secondary check (lines 142-145) infers trailing stop based on whether stop_loss has moved past entry_price. While this is a reasonable heuristic when trailing_stop_history is unavailable, it could incorrectly flag signals as "trailing" if the initial stop loss was set beyond entry (e.g., a very tight stop on a short).

Consider adding a comment documenting this heuristic behavior for maintainability.


330-337: Exit price conditions can be consolidated.

The closed and completed cases produce identical output. Consider combining them:

                  {signal.status === 'stopped' || signal.stop_loss_hit
                    ? `$${formatPrice(getEffectiveStopLoss(signal), signal.ticker)}`
-                    : signal.status === 'closed'
-                      ? `$${formatPrice(signal.current_price || signal.entry_price, signal.ticker)}`
-                      : signal.status === 'completed'
-                        ? `$${formatPrice(signal.current_price || signal.entry_price, signal.ticker)}`
-                        : `$${formatPrice(signal.entry_price, signal.ticker)}`
+                    : ['closed', 'completed'].includes(signal.status || '')
+                      ? `$${formatPrice(signal.current_price || signal.entry_price, signal.ticker)}`
+                      : `$${formatPrice(signal.entry_price, signal.ticker)}`
                  }
src/apps/insights/components/FeedEventCard/FeedEventCard.tsx (1)

84-89: Inconsistent price formatting.

Other components (SignalCard, PnLDetailModal) use the shared formatPrice utility for consistent decimal handling, but here toFixed(2) is used directly. This could cause inconsistent formatting for low-value assets (e.g., PEPE).

+import { formatPrice } from '../../utils/formatUtils';
 // ...
 {event.details.entry_price && (
-  <p>Entry: ${event.details.entry_price.toFixed(2)}</p>
+  <p>Entry: ${formatPrice(event.details.entry_price, event.ticker)}</p>
 )}
 {event.details.exit_price && (
-  <p>Exit: ${event.details.exit_price.toFixed(2)}</p>
+  <p>Exit: ${formatPrice(event.details.exit_price, event.ticker)}</p>
 )}
src/apps/insights/hooks/useSubscriptionStatus.ts (1)

31-55: Guard against overlapping fetches and stale subscription updates

fetchStatus can be invoked both from the mount effect and the polling effect, and is async but scheduled via setInterval. With a slow network or a shorter pollIntervalMs, multiple in‑flight calls can overlap, and the last to resolve (possibly for an older eoaAddress) wins.

A lightweight pattern is to track an in‑flight request ID or an isMounted/latestRequestId ref and ignore results that don’t match, or to skip starting a new fetch while one is active.

This isn’t a blocker with the current 10s default but would make the hook more robust to future changes.

Also applies to: 68-86

src/apps/insights/components/SparklineChart.tsx (1)

59-71: Remove debug console.log from render path

The "Sparkline positions" console.log inside useMemo will fire on every data change and can be noisy in production.

Recommend removing it or guarding it behind an explicit debug flag.

src/apps/insights/components/ui/alert.tsx (1)

21-41: Align forwardRef generics with actual DOM elements

AlertTitle and AlertDescription use forwardRef<HTMLParagraphElement>(...) but render <h5> and <div> respectively. This makes the exposed ref type inconsistent with the underlying element:

const AlertTitle = React.forwardRef<
  HTMLParagraphElement, // but renders <h5>
  React.HTMLAttributes<HTMLHeadingElement>
>(...)

and

const AlertDescription = React.forwardRef<
  HTMLParagraphElement, // but renders <div>
  React.HTMLAttributes<HTMLParagraphElement>
>(...)

For stricter typing and clearer refs, consider changing these to match the actual elements, e.g. HTMLHeadingElement for AlertTitle and HTMLDivElement for AlertDescription.

src/apps/insights/types/index.ts (1)

72-119: Document timestamp units on subscription fields

SubscriptionRecord carries several time-like numeric fields (currentPeriodEnd, trialEnd, canceledAt, upcomingInvoiceDueDate, etc.) typed as number | null, but the unit (seconds vs milliseconds) isn’t specified.

Since other logic (e.g. useSubscriptionStatus) compares these directly to Date.now(), it would help future maintainers to document the intended unit in a comment or via a nominal type alias to avoid subtle expiry bugs.

src/apps/insights/styles/insights.css (1)

174-181: Hardcoded hex color breaks design system consistency.

The focus color #8A77FF should use the CSS variable for maintainability and theme consistency.

 /* Override default focus colors */
 [type='text']:focus,
 [type='number']:focus,
 [type='search']:focus,
 textarea:focus {
-  --tw-ring-color: #8A77FF;
-  border-color: #8A77FF;
+  --tw-ring-color: hsl(var(--ring));
+  border-color: hsl(var(--ring));
 }
src/apps/insights/index.tsx (3)

199-199: Remove debug console.log statements before production.

Multiple console.log statements (lines 199, 243-250, 258, 264) should be removed or replaced with a proper logging utility that can be disabled in production.


370-374: Replace any with proper type for consent payload.

The ConsentPayload type is imported but not used here.

+import type { TradingSignal, TabType, LeverageType, PnLViewType } from './types';
+import type { ConsentPayload } from './components/consent';

-  const handleConsentAccepted = (payload: any) => {
+  const handleConsentAccepted = (payload: ConsentPayload) => {

299-301: Empty catch blocks silently swallow errors.

The empty .catch(() => {}) hides potential issues. Consider logging or handling the error.

   const handleRefreshSubscription = useCallback(() => {
-    refetchSubscription().catch(() => {});
+    refetchSubscription().catch((err) => {
+      console.error('[insights] Failed to refresh subscription:', err);
+    });
   }, [refetchSubscription]);
src/apps/insights/lib/stopLossUtils.ts (2)

3-18: Duplicate TradingSignal interface with looser typing.

This interface duplicates the TradingSignal type already defined in src/apps/insights/types/index.ts. The local version uses Array<any> for trailing_stop_history (line 11), which loses type safety compared to the canonical type.

Consider importing and extending the existing type, or using a Pick/Partial utility type to avoid duplication and maintain type safety.

+import type { TradingSignal as BaseTradingSignal } from '../types';
+
+// Use a subset of TradingSignal fields needed for stop-loss utilities
+type TradingSignal = Pick<BaseTradingSignal, 
+  'stop_loss' | 'order_side' | 'created_at' | 'closed_at' | 'status' | 
+  'stop_loss_hit' | 'current_price' | 'trailing_stop_history' | 
+  'tp1' | 'tp1_hit' | 'tp2' | 'tp2_hit' | 'tp3' | 'tp3_hit'
+>;
-interface TradingSignal {
-  stop_loss: number;
-  order_side?: string | null;
-  // ... rest of local interface
-}

164-166: Synthetic timestamps in inferred timeline may confuse users.

When history is missing, the code generates evenly-spaced synthetic timestamps between created_at and closed_at. This could be misleading in the UI if users interpret these as actual event times.

Consider adding a flag (e.g., inferred: true) to these events so the UI can indicate they are approximate.

src/apps/insights/api/insightsApi.ts (3)

17-41: Inconsistent error handling patterns across API functions.

The API functions use two different patterns:

  • fetchSparklineData, getTradingSignals, webhookReceiver: throw errors
  • updateSignalPrices, recalculateHistoricalPnL: return { data, error } tuples

This inconsistency makes it harder for consumers to handle errors uniformly. Consider standardizing on one approach across all functions.

Also applies to: 46-65, 70-89


121-121: Avoid any type for signalData parameter.

Using any loses type safety. Consider defining a proper type for the webhook payload.

-export const webhookReceiver = async (signalData: any) => {
+export const webhookReceiver = async (signalData: Record<string, unknown>) => {

17-41: Consider adding request timeouts to prevent hanging requests.

The fetch calls have no timeout configuration. If the server is slow or unresponsive, requests will hang indefinitely. Consider using AbortController with a timeout.

Example pattern:

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 30000);

try {
  const response = await fetch(url, { signal: controller.signal, ... });
  // ...
} finally {
  clearTimeout(timeoutId);
}
src/apps/insights/utils/signalUtils.ts (2)

131-137: Redundant isShort declaration shadows outer scope.

isShort is already declared at line 115 with the same value. This inner declaration shadows it and can cause confusion.

Consider removing the redundant declaration:

       if (tpHit === true && tpPrice && Number(tpPrice) > 0) {
         // Calculate TP profit based on position type
-        const isShort = signal.order_side?.toLowerCase() === 'sell';
         const tpPercent = isShort
           ? ((signal.entry_price - Number(tpPrice)) / signal.entry_price) * 100  // SHORT
           : ((Number(tpPrice) - signal.entry_price) / signal.entry_price) * 100; // LONG

224-236: Null check may not catch undefined values.

The filter s.realized_pnl_percent !== null won't catch undefined values. Based on the TradingSignal interface, realized_pnl_percent is optional (realized_pnl_percent?: number), so it could be undefined.

Consider using a truthy check or explicit undefined handling:

   const closedWithPnL = closedSignals
-    .filter(s => s.realized_pnl_percent !== null && s.closed_at)
+    .filter(s => s.realized_pnl_percent != null && s.closed_at)
     .sort((a, b) => new Date(a.closed_at!).getTime() - new Date(b.closed_at!).getTime());

The same applies to generateOpenPnLSparkline (line 243) and generateClosedPnLSparkline (line 264).

src/apps/insights/components/ui/sidebar.tsx (1)

67-69: Consider SSR safety for cookie access.

Direct document.cookie access will throw in SSR environments. If this app may be server-rendered, wrap the cookie access in a check.

       // This sets the cookie to keep the sidebar state.
-      document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
+      if (typeof document !== 'undefined') {
+        document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
+      }
src/apps/insights/components/ui/pagination.tsx (1)

24-27: Empty className string is unnecessary.

The cn("", className) can be simplified since an empty string adds no value.

 const PaginationItem = React.forwardRef<HTMLLIElement, React.ComponentProps<"li">>(({ className, ...props }, ref) => (
-  <li ref={ref} className={cn("", className)} {...props} />
+  <li ref={ref} className={className} {...props} />
 ));
src/apps/insights/components/ui/command.tsx (1)

24-24: Empty interface could be a type alias.

The empty interface extending DialogProps could be simplified to a type alias, though keeping it as an interface allows for future extension.

-interface CommandDialogProps extends DialogProps {}
+type CommandDialogProps = DialogProps;

However, keeping it as an interface is also fine if you anticipate adding command-dialog-specific props later.

src/apps/insights/components/ui/chart.tsx (1)

61-88: Acknowledged: dangerouslySetInnerHTML usage for CSS injection.

The static analysis correctly flags this. However, in this case the CSS is generated purely from developer-defined ChartConfig keys and color strings—not user input—so XSS risk is minimal. Consider adding a comment to clarify this is intentional and safe because the config is controlled by developers.

+// ChartStyle injects CSS custom properties for chart theming.
+// Safe: config keys and colors are developer-defined, not user input.
 const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {

Comment on lines +76 to +83
const calculateDistance = (from: number, to: number): string => {
const percent = ((to - from) / from) * 100;
return `${percent >= 0 ? '+' : ''}${percent.toFixed(2)}%`;
};

const slDistance = calculateDistance(currentPrice, stopLoss);
const tpDistance = calculateDistance(currentPrice, nextTP);

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use orderSide to make risk/reward distances intuitive for sell orders

calculateDistance currently does:

const percent = ((to - from) / from) * 100;

For buys this yields negative SL distance and positive TP distance, which is intuitive. For sells (SL above, TP below), SL shows as +X% and TP as -Y%, effectively inverting risk/reward signs.

Since you already accept orderSide, consider folding it in, e.g.:

const calculateDistance = (from: number, to: number, side: "buy" | "sell"): string => {
  const raw = ((to - from) / from) * 100;
  const percent = side === "sell" ? -raw : raw;
  return `${percent >= 0 ? "+" : ""}${percent.toFixed(2)}%`;
};

const slDistance = calculateDistance(currentPrice, stopLoss, orderSide);
const tpDistance = calculateDistance(currentPrice, nextTP, orderSide);

so that “risk” remains negative and “reward” positive for both directions.

🤖 Prompt for AI Agents
In src/apps/insights/components/SparklineChart.tsx around lines 76 to 83,
calculateDistance currently computes percent = ((to - from)/from)*100 which
causes SL/TP signs to invert for sell orders; update the function to accept
orderSide ("buy" | "sell") and negate the raw percent when side === "sell" so
that risk is always negative and reward always positive, then call it with
currentPrice, stopLoss, orderSide and currentPrice, nextTP, orderSide
respectively and format the signed percent as before.

@github-actions github-actions bot temporarily deployed to Preview (app/insights) December 3, 2025 23:26 Inactive
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: 3

♻️ Duplicate comments (1)
src/apps/insights/components/PnLDetailModal.tsx (1)

21-43: Fix Rules of Hooks violation caused by early return before hooks.

The if (!view) return null; at Line 22 prevents useIsMobile and the useMemo hooks from running consistently on every render, which violates React’s Rules of Hooks and matches the Biome errors you’re seeing.

Move the early return after all hook calls (you already have a guard at Line 89), and rely on the if (!view) return []; checks inside the useMemos for safe handling.

A minimal fix is simply to remove the early return at Line 22:

-export const PnLDetailModal = ({ open, onOpenChange, view, signals }: PnLDetailModalProps) => {
-  if (!view) return null;
-
-  const isMobile = useIsMobile();
+export const PnLDetailModal = ({ open, onOpenChange, view, signals }: PnLDetailModalProps) => {
+  const isMobile = useIsMobile();

The later if (!view) return null; (Line 89) can stay.

🧹 Nitpick comments (4)
src/apps/insights/hooks/useTradingSignals.ts (2)

18-61: Clarify or remove isInitialLoad usage inside fetchSignals.

isInitialLoad is only used inside fetchSignals, but it is not part of the useCallback dependency array. As a result, the callback always sees isInitialLoad as true and calls setIsInitialLoad(false) on every successful fetch (React will short‑circuit the state update once it’s already false, but it’s misleading and unnecessary work).

If you don’t need this flag outside the hook, consider removing the isInitialLoad state entirely. If you do want a one‑time side effect, either:

  • Track it with a useRef that is local to fetchSignals, or
  • Move the “mark initial load complete” logic into the effect that triggers the first fetch, where you can safely derive it from signals.length / loading.

31-52: Consider gating verbose logging behind an env flag.

The detailed console.log calls for every fetch and first signal structure are useful during development but can be noisy in production and in tests.

You might want to wrap these logs in an environment check (e.g. only in development) or centralize them behind a debug flag to reduce console noise.

src/apps/insights/components/PnLDetailModal.tsx (1)

26-87: Verify 'floating' view filtering and chart semantics.

For view === 'floating':

  • filteredSignals returns only status === 'active' signals (same as 'open'), but
  • chartData falls into the non‑open branch, using realized_pnl_percent and timestamps based on closed_at || last_price_update || created_at.

This means the “Floating P&L Performance” chart is effectively plotting realized P&L over time for active positions only, excluding closed trades, which may not match how floatingPnL is computed in the main App or user expectations for “floating”.

If the intent is to reflect combined open + closed performance, you may want to:

  • Include closed signals in filteredSignals for 'floating', or
  • Document that 'floating' is “realized portion of active positions only” and adjust naming accordingly.
src/apps/insights/index.tsx (1)

256-268: Verify PnL formulas to avoid double-counting realized PnL for open positions.

Current logic:

  • openTotalPnL = sum of profit_loss_percent for open signals.
  • closedTotalPnL = sum of realized_pnl_percent for closed signals + realized_pnl_percent for open signals.
  • floatingPnL = openTotalPnL + closedTotalPnL.

If profit_loss_percent for an open signal already represents the total PnL for that position (realized + unrealized), then floatingPnL is effectively:

floating = (openRealized + openUnrealized) + (closedRealized + openRealized)
         = closedRealized + openUnrealized + 2 * openRealized

i.e., realized PnL from open signals is counted twice.

Please confirm the intended semantics:

  • If profit_loss_percent is total PnL, consider setting floatingPnL = openTotalPnL + closedPnL (without adding openRealizedPnL again), or
  • If profit_loss_percent is strictly unrealized, adjust names/comments to make that clear.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0435ec6 and bcc68a7.

⛔ Files ignored due to path filters (6)
  • src/assets/landing-fonts/Formular-Bold.woff is excluded by !**/*.woff
  • src/assets/landing-fonts/Formular-Bold.woff2 is excluded by !**/*.woff2
  • src/assets/landing-fonts/Formular-Medium.woff is excluded by !**/*.woff
  • src/assets/landing-fonts/Formular-Medium.woff2 is excluded by !**/*.woff2
  • src/assets/landing-fonts/Formular-Regular.woff is excluded by !**/*.woff
  • src/assets/landing-fonts/Formular-Regular.woff2 is excluded by !**/*.woff2
📒 Files selected for processing (11)
  • src/apps/insights/components/Filters/Filters.tsx (1 hunks)
  • src/apps/insights/components/PnLDetailModal.tsx (1 hunks)
  • src/apps/insights/components/SparklineChart.tsx (1 hunks)
  • src/apps/insights/components/ui/form.tsx (1 hunks)
  • src/apps/insights/components/ui/progress.tsx (1 hunks)
  • src/apps/insights/hooks/useSubscriptionStatus.ts (1 hunks)
  • src/apps/insights/hooks/useTradingSignals.ts (1 hunks)
  • src/apps/insights/index.tsx (1 hunks)
  • src/apps/insights/styles/insights.css (1 hunks)
  • src/apps/insights/tailwind.insights.config.js (1 hunks)
  • src/apps/insights/utils/supabaseAdapter.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/apps/insights/components/ui/progress.tsx
  • src/apps/insights/components/SparklineChart.tsx
  • src/apps/insights/tailwind.insights.config.js
  • src/apps/insights/hooks/useSubscriptionStatus.ts
  • src/apps/insights/components/ui/form.tsx
  • src/apps/insights/components/Filters/Filters.tsx
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-03-28T09:22:22.712Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 275
File: src/apps/the-exchange/components/DropdownTokensList/DropdownTokenList.tsx:180-195
Timestamp: 2025-03-28T09:22:22.712Z
Learning: In the Exchange app, `swapTokenList` and `receiveTokenList` are derived from `searchTokenResult` when search is active, so including `searchToken` in the useEffect dependency array that uses these lists would be redundant as the lists will update when search results change.

Applied to files:

  • src/apps/insights/hooks/useTradingSignals.ts
📚 Learning: 2025-08-20T09:14:16.888Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 374
File: src/apps/pillarx-app/index.tsx:12-12
Timestamp: 2025-08-20T09:14:16.888Z
Learning: In this codebase, Transaction Kit providers are set up at the container level (src/containers/Authorized.tsx), not at individual app component levels. App components like src/apps/pillarx-app/index.tsx are children that consume the context through the provider tree.

Applied to files:

  • src/apps/insights/index.tsx
📚 Learning: 2025-12-03T10:01:15.789Z
Learnt from: aldin4u
Repo: pillarwallet/x PR: 471
File: src/apps/pillarx-app/components/AlgoInsightsTile/AlgoInsightsTile.tsx:36-48
Timestamp: 2025-12-03T10:01:15.789Z
Learning: In the AlgoInsightsTile component (src/apps/pillarx-app/components/AlgoInsightsTile/AlgoInsightsTile.tsx), the data is always hardcoded and guaranteed to be present with complete history arrays for all timeframes, so edge case handling for empty or sparse history data is not required.

Applied to files:

  • src/apps/insights/index.tsx
🧬 Code graph analysis (3)
src/apps/insights/hooks/useTradingSignals.ts (2)
src/apps/insights/types/index.ts (1)
  • TradingSignal (5-35)
src/apps/insights/api/insightsApi.ts (1)
  • getTradingSignals (46-65)
src/apps/insights/components/PnLDetailModal.tsx (4)
src/apps/insights/types/index.ts (1)
  • TradingSignal (5-35)
src/apps/insights/hooks/use-mobile.tsx (1)
  • useIsMobile (5-19)
src/apps/insights/lib/stopLossUtils.ts (2)
  • normalizeTrailingHistory (84-229)
  • getEffectiveStopLoss (25-33)
src/apps/insights/utils/formatUtils.ts (1)
  • formatPrice (18-21)
src/apps/insights/utils/supabaseAdapter.ts (1)
src/apps/insights/api/insightsApi.ts (3)
  • getTradingSignals (46-65)
  • fetchSparklineData (17-41)
  • updateSignalPrices (70-89)
🪛 Biome (2.1.2)
src/apps/insights/components/PnLDetailModal.tsx

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

Hooks should not be called after an early return.

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

(lint/correctness/useHookAtTopLevel)


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

Hooks should not be called after an early return.

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

(lint/correctness/useHookAtTopLevel)


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

Hooks should not be called after an early return.

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

(lint/correctness/useHookAtTopLevel)

src/apps/insights/utils/supabaseAdapter.ts

[error] 62-76: Promise executor functions should not be async.

(lint/suspicious/noAsyncPromiseExecutor)

⏰ 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). (4)
  • GitHub Check: lint
  • GitHub Check: unit-tests
  • GitHub Check: build
  • GitHub Check: Cloudflare Pages: pillarx-debug
🔇 Additional comments (5)
src/apps/insights/styles/insights.css (3)

141-194: LGTM!

Utility layer is well-structured with clear separation of concerns: glass effects, gradients, glows, and backgrounds. Naming is descriptive and follows BEM-like conventions.


1-1: Paths are correctly configured and resolve properly.

The @config directive and @font-face declarations reference existing assets. All paths resolve correctly:

  • Line 1: ../tailwind.insights.config.js correctly points to src/apps/insights/tailwind.insights.config.js
  • Lines 6-7, 16-17, 26-27: Font files correctly reference src/assets/landing-fonts/ with all Formular font variants (Regular, Medium, Bold) in both .woff and .woff2 formats ✓

41-104: Ensure sidebar tokens are consistently integrated and consolidate profit/loss color definitions.

The sidebar-specific tokens (lines 96-103) are properly defined and actively consumed by the sidebar.tsx component and Tailwind config, confirming their usage. However, the --green-profit and --red-loss tokens defined in the CSS are not referenced anywhere in the codebase; components currently hardcode equivalent HSL values inline (e.g., text-[hsl(142,76%,58%)] for profit). To maintain design consistency and enable future theme updates, migrate KPICard and similar components to use the corresponding CSS variables instead of hardcoded values. The --destructive token is defined and used in UI components for alert/error states, which adequately covers error handling; no additional warning or success state tokens are required unless the product roadmap specifies distinct warning visuals.

src/apps/insights/components/PnLDetailModal.tsx (1)

486-486: Remove stray :: from DialogContent to fix JSX/TSX syntax.

The DialogContent opening tag currently ends with ">::", which will not compile:

<DialogContent ... glass-card border-primary/20">::

It should be a normal JSX tag without the trailing :::

-      <DialogContent className="w-full sm:w-full max-w-[calc(100vw-2rem)] sm:max-w-4xl mx-4 sm:mx-0 h-[92vh] sm:h-auto max-h-[90vh] overflow-y-auto rounded-3xl sm:rounded-3xl p-5 sm:p-7 glass-card border-primary/20">::
+      <DialogContent className="w-full sm:w-full max-w-[calc(100vw-2rem)] sm:max-w-4xl mx-4 sm:mx-0 h-[92vh] sm:h-auto max-h-[90vh] overflow-y-auto rounded-3xl sm:rounded-3xl p-5 sm:p-7 glass-card border-primary/20">
src/apps/insights/index.tsx (1)

353-369: Fix stale signals closure in 30s polling effect.

The polling effect at lines 353-369 depends only on [consentGiven, hasActiveSubscription] but uses signals and fetchSparklines inside the interval callback. Because signals is not in the dependency array, the closure captures whatever value signals held when the effect first ran (typically [] before initial data load completes). The interval then continues using that stale reference every 30 seconds, so fetchSparklines never receives updated signals after the initial load.

Add signals and fetchSparklines to the dependency array and remove the eslint override:

  }, [consentGiven, hasActiveSubscription, signals, fetchSparklines]);

This ensures the interval always operates on current signals data. Alternatively, store signals in a useRef to update it without recreating the effect.

@github-actions github-actions bot requested a deployment to Preview (app/insights) December 3, 2025 23:41 Abandoned
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: 3

🧹 Nitpick comments (2)
src/apps/insights/utils/supabaseAdapter.ts (2)

101-104: TODO: Implement recalculate-historical-pnl handler.

The recalculate-historical-pnl function is not yet implemented.

Would you like me to generate a placeholder implementation or open an issue to track this task?


112-120: Consider returning a proper mock subscription object.

The subscribe() method returns an empty object. If callers attempt to use subscription methods (e.g., unsubscribe()), this could cause runtime errors.

Consider returning a minimal subscription interface:

        subscribe: () => ({
-         // Return a mock subscription - realtime will be handled by polling
+         unsubscribe: () => {
+           // No-op: realtime handled by polling
+         },
        }),
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bcc68a7 and a7451fa.

📒 Files selected for processing (2)
  • src/apps/insights/components/ui/form.tsx (1 hunks)
  • src/apps/insights/utils/supabaseAdapter.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/apps/insights/components/ui/form.tsx (2)
src/apps/insights/lib/utils.ts (1)
  • cn (4-6)
src/apps/insights/components/ui/label.tsx (1)
  • Label (17-17)
src/apps/insights/utils/supabaseAdapter.ts (1)
src/apps/insights/api/insightsApi.ts (3)
  • getTradingSignals (46-65)
  • fetchSparklineData (17-41)
  • updateSignalPrices (70-89)
⏰ 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). (4)
  • GitHub Check: lint
  • GitHub Check: unit-tests
  • GitHub Check: build
  • GitHub Check: Cloudflare Pages: pillarx-debug
🔇 Additional comments (3)
src/apps/insights/utils/supabaseAdapter.ts (2)

59-75: LGTM! Async Promise executor anti-pattern resolved.

The refactored runQuery is now a plain async function, eliminating the previous async Promise executor issue.


77-81: LGTM! Clean query execution flow.

The eq and exec methods correctly return Promises from runQuery, avoiding the thenable anti-pattern flagged in previous reviews.

src/apps/insights/components/ui/form.tsx (1)

33-57: Previous issue resolved: guards now properly placed before usage.

The null checks for fieldContext?.name and itemContext?.id now occur before these values are accessed on lines 46-47, fixing the critical bug from the previous review.

@github-actions github-actions bot temporarily deployed to Preview (app/insights) December 3, 2025 23:56 Inactive
@github-actions github-actions bot temporarily deployed to Preview (app/insights) December 4, 2025 11:44 Inactive
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

🧹 Nitpick comments (3)
src/apps/insights/index.tsx (3)

45-86: Consider extracting utility functions to a dedicated utils file.

Functions like getStoredValue, getInitialDevicePlatform, shortenAddress, and formatDate are generic utilities that would benefit from extraction to src/apps/insights/utils/ for improved reusability, testability, and separation of concerns.


98-107: Consider extracting wallet resolution logic into a custom hook.

The EOA address resolution logic (Privy user wallet, Privy wallets array, localStorage) could be extracted into a reusable hook like useEoaAddress() for better testability and reusability across the app.

Example:

// src/apps/insights/hooks/useEoaAddress.ts
export const useEoaAddress = () => {
  const storedEoaAddress = useMemo(() => getStoredValue('EOA_ADDRESS'), []);
  const { user } = usePrivy();
  const { wallets } = useWallets();
  const privyWalletAddress = user?.wallet?.address;
  const fallbackWalletAddress = wallets.length > 0 ? wallets[0]?.address : undefined;

  return useMemo(() => {
    return storedEoaAddress || privyWalletAddress || fallbackWalletAddress || null;
  }, [storedEoaAddress, privyWalletAddress, fallbackWalletAddress]);
};

545-632: Consider wrapping main content in an Error Boundary.

The main content rendering doesn't have an error boundary. If any child component (Header, SignalCard, FeedEventCard, etc.) throws an error, the entire app will crash with no fallback UI.

Add an error boundary:

// src/apps/insights/components/ErrorBoundary.tsx
import { Component, ReactNode } from 'react';

interface Props {
  children: ReactNode;
}

interface State {
  hasError: boolean;
  error?: Error;
}

export class ErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error: Error) {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: any) {
    console.error('Insights app error:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return (
        <div className="min-h-screen bg-parallax-glow flex items-center justify-center px-4">
          <div className="max-w-md rounded-3xl border border-white/10 bg-white/5 p-8 text-center backdrop-blur">
            <h1 className="text-2xl font-semibold text-white mb-4">
              Something went wrong
            </h1>
            <p className="text-muted-foreground mb-4">
              We encountered an unexpected error. Please refresh the page to try again.
            </p>
            <button
              onClick={() => window.location.reload()}
              className="rounded-2xl bg-white py-2 px-6 font-semibold text-black hover:bg-white/90"
            >
              Refresh page
            </button>
          </div>
        </div>
      );
    }

    return this.props.children;
  }
}

Then wrap the main content:

 return (
   <>
     {consentModalElement}
+    <ErrorBoundary>
       <div className="min-h-screen bg-parallax-glow">
         {/* ... rest of content ... */}
       </div>
+    </ErrorBoundary>
   </>
 );
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a7451fa and 878e44a.

📒 Files selected for processing (1)
  • src/apps/insights/index.tsx (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-20T09:14:16.888Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 374
File: src/apps/pillarx-app/index.tsx:12-12
Timestamp: 2025-08-20T09:14:16.888Z
Learning: In this codebase, Transaction Kit providers are set up at the container level (src/containers/Authorized.tsx), not at individual app component levels. App components like src/apps/pillarx-app/index.tsx are children that consume the context through the provider tree.

Applied to files:

  • src/apps/insights/index.tsx
📚 Learning: 2025-12-03T10:01:15.801Z
Learnt from: aldin4u
Repo: pillarwallet/x PR: 471
File: src/apps/pillarx-app/components/AlgoInsightsTile/AlgoInsightsTile.tsx:36-48
Timestamp: 2025-12-03T10:01:15.801Z
Learning: In the AlgoInsightsTile component (src/apps/pillarx-app/components/AlgoInsightsTile/AlgoInsightsTile.tsx), the data is always hardcoded and guaranteed to be present with complete history arrays for all timeframes, so edge case handling for empty or sparse history data is not required.

Applied to files:

  • src/apps/insights/index.tsx
🧬 Code graph analysis (1)
src/apps/insights/index.tsx (12)
src/utils/blockchain.ts (1)
  • isTestnet (33-39)
src/apps/insights/types/index.ts (4)
  • TabType (57-57)
  • PnLViewType (58-58)
  • LeverageType (59-59)
  • TradingSignal (5-35)
src/apps/insights/hooks/useSubscriptionStatus.ts (1)
  • useSubscriptionStatus (12-146)
src/apps/insights/hooks/useTradingSignals.ts (1)
  • useTradingSignals (13-96)
src/apps/insights/hooks/useSparklineData.ts (1)
  • useSparklineData (9-65)
src/apps/insights/utils/signalUtils.ts (1)
  • generateFeedEvents (110-219)
src/apps/insights/api/insightsApi.ts (1)
  • updateSignalPrices (70-89)
src/apps/insights/components/consent/ConsentModal.tsx (1)
  • ConsentModal (9-81)
src/apps/insights/components/consent/index.ts (1)
  • ConsentModal (1-1)
src/apps/insights/components/Header/Header.tsx (1)
  • Header (28-103)
src/apps/insights/components/FeedEventCard/FeedEventCard.tsx (1)
  • FeedEventCard (16-100)
src/apps/insights/components/SignalCard/SignalCard.tsx (1)
  • SignalCard (34-365)
⏰ 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). (4)
  • GitHub Check: unit-tests
  • GitHub Check: lint
  • GitHub Check: build
  • GitHub Check: Cloudflare Pages: pillarx-debug
🔇 Additional comments (1)
src/apps/insights/index.tsx (1)

177-207: Disabled exhaustive-deps rule may cause stale closures.

The effect uses sparklineDataMap, sparklineLoading, and fetchSparklines but these are excluded from the dependency array via the disabled ESLint rule. While this may be intentional to avoid infinite loops, it creates a risk of stale closures if the hook implementations change.

Consider one of these safer alternatives:

Option 1: Use a callback ref pattern for the effect logic:

const fetchSparklineLogicRef = useRef<(signals: TradingSignal[]) => void>();

useEffect(() => {
  fetchSparklineLogicRef.current = (signals) => {
    if (!hasActiveSubscription || signals.length === 0 || loading) return;
    
    const openSignals = signals.filter(s => s.status === 'active');
    const signalsNeedingSparklines = openSignals.filter(signal => {
      // ... existing filter logic
    });
    
    if (signalsNeedingSparklines.length > 0) {
      fetchSparklines(signalsNeedingSparklines);
    }
  };
});

useEffect(() => {
  fetchSparklineLogicRef.current?.(signals);
}, [signals, loading, hasActiveSubscription]);

Option 2: Ensure fetchSparklines from useSparklineData is wrapped in useCallback with stable deps (verify in that hook file).

⛔ Skipped due to learnings
Learnt from: RanaBug
Repo: pillarwallet/x PR: 275
File: src/apps/the-exchange/components/DropdownTokensList/DropdownTokenList.tsx:180-195
Timestamp: 2025-03-28T09:22:22.712Z
Learning: In the Exchange app, `swapTokenList` and `receiveTokenList` are derived from `searchTokenResult` when search is active, so including `searchToken` in the useEffect dependency array that uses these lists would be redundant as the lists will update when search results change.

Comment on lines +37 to +39
const STRIPE_CHECKOUT_URL_MAINNET =
import.meta.env.VITE_STRIPE_CHECKOUT_URL_MAINNET ||
STRIPE_CHECKOUT_URL_TESTNET;
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

🏁 Script executed:

head -50 src/apps/insights/index.tsx

Repository: pillarwallet/x

Length of output: 1883


🏁 Script executed:

# Search for vite config files
fd -e "vite.config" -e ".env*"

Repository: pillarwallet/x

Length of output: 40


🏁 Script executed:

# Search for environment variable validation or checks
rg "VITE_STRIPE_CHECKOUT_URL" --type ts --type tsx -A 3 -B 3

Repository: pillarwallet/x

Length of output: 85


🏁 Script executed:

# Search for similar fallback patterns in the codebase
rg "import.meta.env" --type ts --type tsx | head -20

Repository: pillarwallet/x

Length of output: 85


🏁 Script executed:

# Find vite config and env files
find . -maxdepth 2 -name "vite.config*" -o -name ".env*" | grep -v node_modules

Repository: pillarwallet/x

Length of output: 89


🏁 Script executed:

# Search for STRIPE env vars more broadly
rg "VITE_STRIPE_CHECKOUT_URL" -A 3 -B 3

Repository: pillarwallet/x

Length of output: 758


🏁 Script executed:

# Look for build scripts and CI/CD configurations
find . -maxdepth 2 -type f \( -name "*.yml" -o -name "*.yaml" -o -name "package.json" \) | grep -v node_modules | head -10

Repository: pillarwallet/x

Length of output: 95


🏁 Script executed:

cat .env.example

Repository: pillarwallet/x

Length of output: 820


🏁 Script executed:

cat vite.config.js

Repository: pillarwallet/x

Length of output: 1121


🏁 Script executed:

cat .circleci/config.yml

Repository: pillarwallet/x

Length of output: 4971


🏁 Script executed:

cat package.json | head -50

Repository: pillarwallet/x

Length of output: 2136


🏁 Script executed:

# Search for GitHub workflows
find . -path "./.github/workflows" -type f 2>/dev/null | head -5

Repository: pillarwallet/x

Length of output: 40


🏁 Script executed:

# Search for any env validation in the codebase
rg "VITE_STRIPE_CHECKOUT_URL_MAINNET" -B 5 -A 5

Repository: pillarwallet/x

Length of output: 779


Mainnet Stripe checkout URL lacks required environment variable validation.

The VITE_STRIPE_CHECKOUT_URL_MAINNET environment variable is not documented in .env.example and has no build-time or CI/CD validation. If this variable is missing during a production build, mainnet users will be silently directed to the testnet Stripe checkout URL without any error notification.

This is a configuration gap that could cause production payment failures. Add the environment variable to .env.example and implement build-time validation to ensure it's set before deployment:

-const STRIPE_CHECKOUT_URL_MAINNET =
-  import.meta.env.VITE_STRIPE_CHECKOUT_URL_MAINNET ||
-  STRIPE_CHECKOUT_URL_TESTNET;
+const STRIPE_CHECKOUT_URL_MAINNET = (() => {
+  const url = import.meta.env.VITE_STRIPE_CHECKOUT_URL_MAINNET;
+  if (!url && !isTestnet) {
+    throw new Error('VITE_STRIPE_CHECKOUT_URL_MAINNET is required for mainnet builds');
+  }
+  return url || STRIPE_CHECKOUT_URL_TESTNET;
+})();

Additionally, document VITE_STRIPE_CHECKOUT_URL_MAINNET as a required environment variable in .env.example.

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

🤖 Prompt for AI Agents
In src/apps/insights/index.tsx around lines 37-39 the code falls back to the
testnet Stripe checkout URL when VITE_STRIPE_CHECKOUT_URL_MAINNET is missing,
causing silent misrouting in production; update .env.example to include
VITE_STRIPE_CHECKOUT_URL_MAINNET with a clear "required for production" note and
add build-time validation (e.g., during app startup or CI build script) that
checks process.env.VITE_STRIPE_CHECKOUT_URL_MAINNET when NODE_ENV ===
"production" and fails the build/exit with a clear error message if it's
undefined or empty so mainnet traffic cannot be silently redirected to testnet.

Comment on lines +150 to +170
useEffect(() => {
const storedConsent = localStorage.getItem('pillarx_consent_accepted');
if (storedConsent) {
try {
const consent = JSON.parse(storedConsent);
const consentDate = new Date(consent.timestamp);
const oneYearAgo = new Date();
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);

if (consentDate > oneYearAgo) {
setConsentGiven(true);
} else {
setShowConsentModal(true);
}
} catch {
setShowConsentModal(true);
}
} else {
setShowConsentModal(true);
}
}, []);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add structure validation after parsing consent from localStorage.

The code assumes the parsed consent object has a timestamp property without validating its structure. If the localStorage data is corrupted or manually edited, this could fail unexpectedly.

Apply this diff to add validation:

 const storedConsent = localStorage.getItem('pillarx_consent_accepted');
 if (storedConsent) {
   try {
     const consent = JSON.parse(storedConsent);
-    const consentDate = new Date(consent.timestamp);
+    if (!consent?.timestamp) {
+      setShowConsentModal(true);
+      return;
+    }
+    const consentDate = new Date(consent.timestamp);
     const oneYearAgo = new Date();
     oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
     
     if (consentDate > oneYearAgo) {
       setConsentGiven(true);
     } else {
       setShowConsentModal(true);
     }
   } catch {
     setShowConsentModal(true);
   }
 } else {
   setShowConsentModal(true);
 }
📝 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(() => {
const storedConsent = localStorage.getItem('pillarx_consent_accepted');
if (storedConsent) {
try {
const consent = JSON.parse(storedConsent);
const consentDate = new Date(consent.timestamp);
const oneYearAgo = new Date();
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
if (consentDate > oneYearAgo) {
setConsentGiven(true);
} else {
setShowConsentModal(true);
}
} catch {
setShowConsentModal(true);
}
} else {
setShowConsentModal(true);
}
}, []);
useEffect(() => {
const storedConsent = localStorage.getItem('pillarx_consent_accepted');
if (storedConsent) {
try {
const consent = JSON.parse(storedConsent);
if (!consent?.timestamp) {
setShowConsentModal(true);
return;
}
const consentDate = new Date(consent.timestamp);
const oneYearAgo = new Date();
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
if (consentDate > oneYearAgo) {
setConsentGiven(true);
} else {
setShowConsentModal(true);
}
} catch {
setShowConsentModal(true);
}
} else {
setShowConsentModal(true);
}
}, []);
🤖 Prompt for AI Agents
In src/apps/insights/index.tsx around lines 150 to 170, after parsing
storedConsent you must validate the parsed object has a timestamp property and
that it represents a valid date before using it; update the try block to check
that consent is an object, consent.timestamp exists and is a string/number,
create a Date and verify !isNaN(date.getTime()) (or similar) before comparing to
oneYearAgo, and if any check fails fall back to setShowConsentModal(true) so
corrupted or malformed localStorage entries don’t cause incorrect behavior.

Comment on lines +243 to +268
const openTotalPnL = useMemo(() => {
const value = calculateOpenPnL();
console.log(`💰 [PnL] openTotalPnL: ${value}% (${openSignals.length} signals)`);
if (openSignals.length > 0) {
console.log('📊 [PnL] Open signals breakdown:', openSignals.map(s => ({
ticker: s.ticker,
profit_loss_percent: s.profit_loss_percent,
realized_pnl_percent: s.realized_pnl_percent,
})));
}
return value;
}, [openSignals]);

const closedTotalPnL = useMemo(() => {
const closedPnL = calculateTotalPnL(closedSignals);
const openRealizedPnL = calculateOpenRealizedPnL();
const value = closedPnL + openRealizedPnL;
console.log(`💰 [PnL] closedTotalPnL: ${value}% (closed: ${closedPnL}%, open realized: ${openRealizedPnL}%)`);
return value;
}, [closedSignals, openSignals]);

const floatingPnL = useMemo(() => {
const value = openTotalPnL + closedTotalPnL;
console.log(`💰 [PnL] floatingPnL: ${value}% (open: ${openTotalPnL}%, closed: ${closedTotalPnL}%)`);
return value;
}, [openTotalPnL, closedTotalPnL]);
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

Remove or guard console.log statements for production.

The PnL calculation logging (lines 245-252, 260, 266) should be removed or wrapped in a debug flag to avoid cluttering the console in production and exposing calculation logic to end users.

Apply this diff to remove the logs:

 const openTotalPnL = useMemo(() => {
   const value = calculateOpenPnL();
-  console.log(`💰 [PnL] openTotalPnL: ${value}% (${openSignals.length} signals)`);
-  if (openSignals.length > 0) {
-    console.log('📊 [PnL] Open signals breakdown:', openSignals.map(s => ({
-      ticker: s.ticker,
-      profit_loss_percent: s.profit_loss_percent,
-      realized_pnl_percent: s.realized_pnl_percent,
-    })));
-  }
   return value;
 }, [openSignals]);
 
 const closedTotalPnL = useMemo(() => {
   const closedPnL = calculateTotalPnL(closedSignals);
   const openRealizedPnL = calculateOpenRealizedPnL();
   const value = closedPnL + openRealizedPnL;
-  console.log(`💰 [PnL] closedTotalPnL: ${value}% (closed: ${closedPnL}%, open realized: ${openRealizedPnL}%)`);
   return value;
 }, [closedSignals, openSignals]);
 
 const floatingPnL = useMemo(() => {
   const value = openTotalPnL + closedTotalPnL;
-  console.log(`💰 [PnL] floatingPnL: ${value}% (open: ${openTotalPnL}%, closed: ${closedTotalPnL}%)`);
   return value;
 }, [openTotalPnL, closedTotalPnL]);

Alternatively, wrap them in a debug flag:

const DEBUG = import.meta.env.DEV;
if (DEBUG) console.log(...);
🤖 Prompt for AI Agents
In src/apps/insights/index.tsx around lines 243 to 268, there are several
console.log statements used to debug PnL calculations; remove these logs for
production or guard them behind a debug flag (e.g. const DEBUG =
Boolean(import.meta.env.DEV) at module scope) and wrap each console.log with if
(DEBUG) { ... } so logging only occurs in dev; ensure the DEBUG variable is
defined once in the module and used consistently for all PnL logs.


const handleSubscribeClick = useCallback(() => {
if (!eoaAddress) {
alert('No wallet detected. Please re-open PillarX from the mobile app.');
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 alert() calls with toast notifications or modals.

Multiple alert() calls (lines 307, 323, 348, 381) provide a poor user experience and look unprofessional. Consider using a toast notification library (e.g., sonner, react-hot-toast) or modal dialogs for a more modern UX.

Example with a toast library:

import { toast } from 'sonner';

// Instead of:
alert('No wallet detected...');

// Use:
toast.error('No wallet detected. Please re-open PillarX from the mobile app.');

Also applies to: 323-323, 348-350, 381-381

🤖 Prompt for AI Agents
In src/apps/insights/index.tsx around lines 307, 323, 348-350 and 381, replace
the use of synchronous browser alert() calls with non-blocking toast
notifications or modal dialogs; remove alert(...) calls and instead import and
use a toast library (e.g., sonner or react-hot-toast) to show error/info toasts
(e.g., toast.error(...) for errors) or open a reusable modal component where
appropriate, ensuring the messages remain the same and that the toast/modal UI
is styled consistently with the app and triggered in the same control flow
contexts as the original alerts.

Comment on lines +315 to +317
const checkoutUrl = `${STRIPE_CHECKOUT_URL}?client_reference_id=${encodeURIComponent(
eoaAddress
)}`;
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

Privacy concern: wallet address exposed in URL query parameter.

Passing the wallet address as a URL query parameter means it will be logged by Stripe's servers, proxies, browser history, and potentially leaked via Referer headers. While wallet addresses are public on-chain, this creates an unnecessary privacy vector linking users to their subscription actions.

Consider alternatives:

  1. Pass an opaque session ID in the URL and resolve to wallet address server-side
  2. Use Stripe's Customer Portal with pre-authenticated sessions
  3. If client_reference_id must be in URL, document the privacy implications in your privacy policy
// Better approach: use session-based checkout
const session = await createCheckoutSession({ walletAddress: eoaAddress });
const checkoutUrl = `${STRIPE_CHECKOUT_URL}?session_id=${session.id}`;
🤖 Prompt for AI Agents
In src/apps/insights/index.tsx around lines 315-317, the code places the wallet
address into the checkout URL query (client_reference_id) which leaks the
address to Stripe, proxies, browser history and Referer headers; instead create
a server-side opaque checkout/session object that stores the wallet address and
return only the session id to the client (or use Stripe Customer Portal
pre-authenticated sessions), then build the checkout URL with that session_id;
if you must keep client_reference_id, document the privacy implications in the
privacy policy and avoid exposing the raw address in URLs or referer-prone
places.

Comment on lines +355 to +371
// Poll for price updates every 30 seconds
useEffect(() => {
if (!consentGiven || !hasActiveSubscription) return;

handleUpdatePrices();
const interval = setInterval(() => {
handleUpdatePrices();
// Refresh sparklines for open signals
const currentOpenSignals = signals.filter(s => s.status === 'active');
if (currentOpenSignals.length > 0) {
fetchSparklines(currentOpenSignals);
}
}, 30000);

return () => clearInterval(interval);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [consentGiven, hasActiveSubscription]);
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

Disabled exhaustive-deps may cause stale closures in polling effect.

The effect uses handleUpdatePrices, signals, and fetchSparklines but excludes them from dependencies. Since handleUpdatePrices is defined inline (lines 287-299), it's recreated on every render, and the effect may reference stale versions.

Apply this diff to fix:

+const handleUpdatePrices = useCallback(async () => {
+  setUpdating(true);
+  try {
+    const result = await updateSignalPrices();
+    if (result.error) {
+      console.error('Error updating prices:', result.error);
+    }
+  } catch (error) {
+    console.error('Error calling update function:', error);
+  } finally {
+    setUpdating(false);
+  }
+}, []);

 // Poll for price updates every 30 seconds
 useEffect(() => {
   if (!consentGiven || !hasActiveSubscription) return;

   handleUpdatePrices();
   const interval = setInterval(() => {
     handleUpdatePrices();
     // Refresh sparklines for open signals
     const currentOpenSignals = signals.filter(s => s.status === 'active');
     if (currentOpenSignals.length > 0) {
       fetchSparklines(currentOpenSignals);
     }
   }, 30000);

   return () => clearInterval(interval);
-  // eslint-disable-next-line react-hooks/exhaustive-deps
-}, [consentGiven, hasActiveSubscription]);
+}, [consentGiven, hasActiveSubscription, handleUpdatePrices, signals, fetchSparklines]);

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

🤖 Prompt for AI Agents
In src/apps/insights/index.tsx around lines 355-371, the polling effect closes
over inline handleUpdatePrices, fetchSparklines and signals which can become
stale; to fix, memoize handleUpdatePrices and fetchSparklines with useCallback
(listing their real dependencies) so they are stable, replace direct use of
signals inside the interval with a signalsRef (useRef) that you update whenever
signals change and read signalsRef.current inside the setInterval, and include
the stable callbacks and consentGiven/hasActiveSubscription in the effect
dependency array so the interval uses current logic while avoiding unnecessary
resets.

}, [consentGiven, hasActiveSubscription]);

// Consent handlers
const handleConsentAccepted = (payload: any) => {
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

Use proper TypeScript type instead of any for consent payload.

The payload parameter should be typed as ConsentPayload (imported from consent types) instead of any for better type safety.

-const handleConsentAccepted = (payload: any) => {
+const handleConsentAccepted = (payload: ConsentPayload) => {
   localStorage.setItem('pillarx_consent_accepted', JSON.stringify(payload));
   setConsentGiven(true);
   setShowConsentModal(false);
 };

Make sure to import the type:

 // Types
-import type { TradingSignal, TabType, LeverageType, PnLViewType } from './types';
+import type { TradingSignal, TabType, LeverageType, PnLViewType, ConsentPayload } from './types';

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

🤖 Prompt for AI Agents
In src/apps/insights/index.tsx around line 374, the handler signature uses
payload: any; change the parameter type to ConsentPayload and import that type
from the consent types module where ConsentPayload is exported (e.g., add an
import for ConsentPayload at the top of the file). Update the function signature
to use the ConsentPayload type and adjust any internal usages if needed to
satisfy the type (narrow properties or add guards) so the handler is fully
typed.

@github-actions github-actions bot temporarily deployed to Preview (app/insights) December 4, 2025 12:26 Inactive
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: 0

♻️ Duplicate comments (6)
src/apps/insights/index.tsx (6)

34-43: Environment variable validation concern already raised.

The fallback pattern for VITE_STRIPE_CHECKOUT_URL_MAINNET silently falling back to testnet was flagged in a previous review. Please address that feedback to prevent production payment routing issues.


149-170: Consent parsing validation concern already raised.

The missing validation of consent.timestamp after JSON parsing was flagged in a previous review. Please add a guard to check that consent?.timestamp exists before using it.


243-268: Console.log removal concern already raised.

The debug logging in PnL calculations was flagged in a previous review. Please remove these or wrap them in a DEBUG flag.


305-360: Previous concerns still apply.

The use of alert()/window.confirm() (lines 307, 323, 355-357) and the wallet address in URL query parameter (lines 315-317) were flagged in previous reviews. Please address those concerns.

The popup security handling at line 346 (popup.opener = null) is a good practice to prevent reverse tabnapping.


362-378: Stale closure concern already raised.

The disabled exhaustive-deps and potential stale closures were flagged in a previous review. Please memoize handleUpdatePrices with useCallback and properly include dependencies to avoid referencing stale function versions.


380-389: Previous typing and alert concerns still apply.

The any type for payload parameter (line 381) and the alert() usage (line 388) were flagged in previous reviews. Please type the payload properly and replace alerts with toast notifications.

🧹 Nitpick comments (2)
src/apps/insights/index.tsx (2)

200-202: Remove debug console.log for production.

This console.log statement should be removed or wrapped in a debug flag, consistent with the approach needed for the PnL calculation logs.

       if (signalsNeedingSparklines.length > 0) {
-        console.log(`📊 [Sparkline] Initial fetch for ${signalsNeedingSparklines.length} new signals`);
         fetchSparklines(signalsNeedingSparklines);
       }

128-128: Consider removing unused setSignals destructuring.

setSignals is destructured from useTradingSignals but not used in this component. If it's not needed, remove it to avoid confusion.

-  const { signals, loading, setSignals } = useTradingSignals({
+  const { signals, loading } = useTradingSignals({
     enabled: consentGiven && hasActiveSubscription,
   });
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 878e44a and 67da754.

📒 Files selected for processing (1)
  • src/apps/insights/index.tsx (1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-08-20T09:14:16.888Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 374
File: src/apps/pillarx-app/index.tsx:12-12
Timestamp: 2025-08-20T09:14:16.888Z
Learning: In this codebase, Transaction Kit providers are set up at the container level (src/containers/Authorized.tsx), not at individual app component levels. App components like src/apps/pillarx-app/index.tsx are children that consume the context through the provider tree.

Applied to files:

  • src/apps/insights/index.tsx
📚 Learning: 2025-12-03T10:01:15.801Z
Learnt from: aldin4u
Repo: pillarwallet/x PR: 471
File: src/apps/pillarx-app/components/AlgoInsightsTile/AlgoInsightsTile.tsx:36-48
Timestamp: 2025-12-03T10:01:15.801Z
Learning: In the AlgoInsightsTile component (src/apps/pillarx-app/components/AlgoInsightsTile/AlgoInsightsTile.tsx), the data is always hardcoded and guaranteed to be present with complete history arrays for all timeframes, so edge case handling for empty or sparse history data is not required.

Applied to files:

  • src/apps/insights/index.tsx
📚 Learning: 2025-03-28T09:22:22.712Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 275
File: src/apps/the-exchange/components/DropdownTokensList/DropdownTokenList.tsx:180-195
Timestamp: 2025-03-28T09:22:22.712Z
Learning: In the Exchange app, `swapTokenList` and `receiveTokenList` are derived from `searchTokenResult` when search is active, so including `searchToken` in the useEffect dependency array that uses these lists would be redundant as the lists will update when search results change.

Applied to files:

  • src/apps/insights/index.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). (4)
  • GitHub Check: unit-tests
  • GitHub Check: lint
  • GitHub Check: build
  • GitHub Check: Cloudflare Pages: pillarx-debug
🔇 Additional comments (3)
src/apps/insights/index.tsx (3)

45-86: Utility functions are well-implemented.

SSR-safe checks, proper null handling, and the timestamp normalization logic in formatDate (detecting seconds vs milliseconds) are correctly implemented.


88-132: State and hook initialization looks good.

The wallet address resolution priority (stored → Privy → fallback) aligns with existing patterns. Conditional enabling of useTradingSignals based on consent and subscription prevents unnecessary API calls.


552-639: Main render structure is well-organized.

The component properly handles different states with early returns, uses AnimatePresence correctly for animated list transitions, and provides appropriate keys for list items. The modal open state is cleanly derived from selectedPnLView.

@github-actions github-actions bot temporarily deployed to Preview (app/insights) December 4, 2025 14:01 Inactive
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: 0

🧹 Nitpick comments (3)
src/apps/insights/index.tsx (3)

423-431: Consider detecting actual user region instead of hardcoding "Other".

The userRegion prop is hardcoded to "Other", which may not accurately reflect the user's location. If consent requirements differ by region (e.g., GDPR for EU users), this could lead to compliance issues.

Consider implementing region detection (via IP geolocation or browser locale) if consent requirements vary by jurisdiction:

const [userRegion, setUserRegion] = useState<string>('Other');

useEffect(() => {
  // Detect user region via API or browser locale
  // e.g., fetch from a geolocation service
  // setUserRegion(detectedRegion);
}, []);

const consentModalElement = (
  <ConsentModal
    open={showConsentModal}
    onConsentAccepted={handleConsentAccepted}
    onConsentDeclined={handleConsentDeclined}
    userRegion={userRegion}
    immediateAccess={true}
  />
);

306-308: Empty catch block silently swallows subscription refresh errors.

The refetchSubscription().catch(() => {}) pattern suppresses all errors without logging or notifying the user. If the refresh fails, users won't know why their subscription status isn't updating.

Apply this diff to log errors:

 const handleRefreshSubscription = useCallback(() => {
-  refetchSubscription().catch(() => {});
+  refetchSubscription().catch((error) => {
+    console.error('Failed to refresh subscription:', error);
+  });
 }, [refetchSubscription]);

324-326: Empty catch block silently swallows subscription refresh errors.

Similar to the handleRefreshSubscription, this refetchSubscription().catch(() => {}) call suppresses errors without logging, making it difficult to debug subscription issues.

Apply this diff to log errors:

 setIsAwaitingSubscription(true);
 startPolling();
-refetchSubscription().catch(() => {});
+refetchSubscription().catch((error) => {
+  console.error('Failed to refetch subscription after subscribe click:', error);
+});
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 67da754 and a7c8ed3.

📒 Files selected for processing (1)
  • src/apps/insights/index.tsx (1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-08-20T09:14:16.888Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 374
File: src/apps/pillarx-app/index.tsx:12-12
Timestamp: 2025-08-20T09:14:16.888Z
Learning: In this codebase, Transaction Kit providers are set up at the container level (src/containers/Authorized.tsx), not at individual app component levels. App components like src/apps/pillarx-app/index.tsx are children that consume the context through the provider tree.

Applied to files:

  • src/apps/insights/index.tsx
📚 Learning: 2025-12-03T10:01:15.801Z
Learnt from: aldin4u
Repo: pillarwallet/x PR: 471
File: src/apps/pillarx-app/components/AlgoInsightsTile/AlgoInsightsTile.tsx:36-48
Timestamp: 2025-12-03T10:01:15.801Z
Learning: In the AlgoInsightsTile component (src/apps/pillarx-app/components/AlgoInsightsTile/AlgoInsightsTile.tsx), the data is always hardcoded and guaranteed to be present with complete history arrays for all timeframes, so edge case handling for empty or sparse history data is not required.

Applied to files:

  • src/apps/insights/index.tsx
📚 Learning: 2025-03-28T09:22:22.712Z
Learnt from: RanaBug
Repo: pillarwallet/x PR: 275
File: src/apps/the-exchange/components/DropdownTokensList/DropdownTokenList.tsx:180-195
Timestamp: 2025-03-28T09:22:22.712Z
Learning: In the Exchange app, `swapTokenList` and `receiveTokenList` are derived from `searchTokenResult` when search is active, so including `searchToken` in the useEffect dependency array that uses these lists would be redundant as the lists will update when search results change.

Applied to files:

  • src/apps/insights/index.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). (4)
  • GitHub Check: unit-tests
  • GitHub Check: lint
  • GitHub Check: build
  • GitHub Check: Cloudflare Pages: pillarx-debug
🔇 Additional comments (1)
src/apps/insights/index.tsx (1)

182-212: The fetchSparklines function is properly memoized—no issue here.

The useSparklineData hook wraps both fetchSparkline (line 16) and fetchSparklines (line 50) in useCallback, making them stable across renders. The disabled exhaustive-deps linting rule is justified and safe because the effect intentionally depends only on signals.length and loading to avoid unnecessary re-fetches when signal data changes.

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

🧹 Nitpick comments (2)
src/apps/insights/hooks/useSparklineData.ts (2)

19-19: Consider removing or conditionalizing console logs for production.

Multiple console.log statements are present throughout the hook. While useful for debugging, these should be removed or wrapped in a conditional flag for production builds to avoid console pollution.

Consider creating a debug utility:

const DEBUG = process.env.NODE_ENV === 'development';
const debugLog = (...args: any[]) => DEBUG && console.log(...args);

Then replace console.log calls with debugLog.

Also applies to: 30-30, 47-47, 49-49


56-63: Consider rate limiting or batching concurrent requests.

Fetching sparklines for all active signals simultaneously could trigger many concurrent API calls. Consider adding rate limiting or request batching to avoid overwhelming the backend or hitting rate limits.

Example approach using promise batching:

const fetchSparklines = useCallback(async (signals: TradingSignal[]) => {
  const activeSignals = signals.filter(s => s.status === 'active');
  console.log(`🔄 [Sparkline] Fetching sparklines for ${activeSignals.length} active signals`);
  
  // Batch requests in groups of 5
  const batchSize = 5;
  for (let i = 0; i < activeSignals.length; i += batchSize) {
    const batch = activeSignals.slice(i, i + batchSize);
    await Promise.all(batch.map(signal => fetchSparkline(signal)));
  }
}, [fetchSparkline]);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a7c8ed3 and cd08069.

📒 Files selected for processing (1)
  • src/apps/insights/hooks/useSparklineData.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/apps/insights/hooks/useSparklineData.ts (2)
src/apps/insights/types/index.ts (2)
  • SparklineDataPoint (52-55)
  • TradingSignal (5-35)
src/apps/insights/api/insightsApi.ts (1)
  • fetchSparklineData (17-41)
⏰ 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). (4)
  • GitHub Check: lint
  • GitHub Check: unit-tests
  • GitHub Check: build
  • GitHub Check: Cloudflare Pages: pillarx-debug
🔇 Additional comments (3)
src/apps/insights/hooks/useSparklineData.ts (3)

1-14: LGTM!

The hook initialization is well-structured. The use of loadingRef alongside state is a good pattern to prevent dependency loops while keeping the UI reactive.


16-21: LGTM!

The early-return pattern using the ref to check loading state is a good approach to prevent duplicate fetches without causing dependency issues.


65-71: LGTM!

The return interface is clean and provides the expected API for consumers of this hook.

Comment on lines +23 to +31
try {
loadingRef.current[signal.id] = true;
setLoading(prev => ({ ...prev, [signal.id]: true }));

const startTime = new Date(signal.created_at).getTime();
const endTime = Date.now();

console.log(`📊 [Sparkline] Fetching ${signal.ticker} (${signal.id})`);
const result = await fetchSparklineData(signal.ticker, startTime, endTime);
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

Validate signal.created_at before converting to timestamp.

If signal.created_at is malformed or missing, new Date(signal.created_at).getTime() will return NaN, leading to incorrect API calls or unexpected behavior.

Apply this diff to add validation:

     try {
       loadingRef.current[signal.id] = true;
       setLoading(prev => ({ ...prev, [signal.id]: true }));
       
-      const startTime = new Date(signal.created_at).getTime();
+      const startTime = new Date(signal.created_at).getTime();
+      if (isNaN(startTime)) {
+        throw new Error(`Invalid created_at date: ${signal.created_at}`);
+      }
       const endTime = Date.now();
       
       console.log(`📊 [Sparkline] Fetching ${signal.ticker} (${signal.id})`);
🤖 Prompt for AI Agents
In src/apps/insights/hooks/useSparklineData.ts around lines 23 to 31, the code
converts signal.created_at to a timestamp without validating it, so new
Date(signal.created_at).getTime() can produce NaN if created_at is missing or
malformed; validate that signal.created_at exists and is a valid date before
using it (e.g., attempt to parse new Date(signal.created_at), check
isFinite(date.getTime())/!isNaN, and if invalid fall back to a safe default or
abort the fetch with an error/log and clear the loading flags), and ensure any
early returns also reset loadingRef.current[signal.id] and setLoading to avoid
leaving the UI stuck in loading state.

Comment on lines +33 to +35
if (result.error) {
throw result.error;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for fetchSparklineData implementation
rg -A 20 "export const fetchSparklineData" --type ts

Repository: pillarwallet/x

Length of output: 1383


🏁 Script executed:

#!/bin/bash
# Get the complete fetchSparklineData function
rg -A 30 "export const fetchSparklineData" src/apps/insights/api/insightsApi.ts

Repository: pillarwallet/x

Length of output: 781


🏁 Script executed:

#!/bin/bash
# Get complete context around lines 33-35 in useSparklineData.ts
cat -n src/apps/insights/hooks/useSparklineData.ts | head -50

Repository: pillarwallet/x

Length of output: 2271


Remove the incorrect error check on lines 33-35.

The fetchSparklineData function throws errors directly (via throw error in its catch block) rather than returning an object with an error property. The check if (result.error) is dead code that will never be reached—if fetchSparklineData encounters an error, it throws immediately and the execution jumps to the catch block on line 48. This check should be removed as it's misleading and serves no purpose.

🤖 Prompt for AI Agents
In src/apps/insights/hooks/useSparklineData.ts around lines 33-35, remove the
dead error check "if (result.error) { throw result.error; }" because
fetchSparklineData throws errors directly and never returns an object with an
error property; update the function by deleting those lines so errors propagate
to the existing catch block instead and ensure no other code depends on
result.error.

Comment on lines +37 to +46
const candles = Array.isArray(result?.candles)
? result.candles
: Array.isArray(result?.data?.candles)
? result.data.candles
: [];

setSparklineDataMap(prev => ({
...prev,
[signal.id]: candles as SparklineDataPoint[],
}));
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add type validation before casting to SparklineDataPoint[].

The extracted candles array is cast to SparklineDataPoint[] without validating that each element has the required time and price properties. This could lead to runtime errors if the API returns unexpected data.

Apply this diff to add validation:

       const candles = Array.isArray(result?.candles)
         ? result.candles
         : Array.isArray(result?.data?.candles)
           ? result.data.candles
           : [];
+      
+      // Validate candles structure
+      const validCandles = candles.filter((c: any) => 
+        typeof c?.time === 'number' && typeof c?.price === 'number'
+      );
 
       setSparklineDataMap(prev => ({
         ...prev,
-        [signal.id]: candles as SparklineDataPoint[],
+        [signal.id]: validCandles as SparklineDataPoint[],
       }));
-      console.log(`✅ [Sparkline] Fetched ${signal.ticker} - ${candles.length} candles`);
+      console.log(`✅ [Sparkline] Fetched ${signal.ticker} - ${validCandles.length} candles`);

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

🤖 Prompt for AI Agents
In src/apps/insights/hooks/useSparklineData.ts around lines 37 to 46, the code
assigns result-derived candles to SparklineDataPoint[] by casting without
validation; add a runtime type guard that checks each item is an object with
numeric (or parseable) time and price properties, use Array.isArray on the
source then filter using that guard to produce a validated array (or [] if
none), and pass the filtered array to setSparklineDataMap instead of the direct
cast so only properly shaped SparklineDataPoint objects are stored.

@IAmKio IAmKio merged commit f7a22cb into staging Dec 4, 2025
7 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Jan 7, 2026
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants