Skip to content

Implement phase 3#41

Open
averyjennings wants to merge 1 commit intocoderabbit-demo:mainfrom
averyjennings:phaseThreeChanges
Open

Implement phase 3#41
averyjennings wants to merge 1 commit intocoderabbit-demo:mainfrom
averyjennings:phaseThreeChanges

Conversation

@averyjennings
Copy link

@averyjennings averyjennings commented Aug 11, 2025

Summary by CodeRabbit

  • New Features
    • Introduced a calorie-tracking dashboard with daily summary, goal progress, weekly trends, recent meals, and analytics.
    • Added tabs for Dashboard, Add Meal (photo upload), Trends, and History.
    • Enabled setting and updating personal nutrition goals.
    • Added editable meal history with search, filters, inline edits, and deletions.
    • Displayed AI meal analysis with confidence, details, and retry options.
  • Improvements
    • Updated Calorie Tracker page with streamlined layout, loading states, and error handling.
  • Chores
    • Added dependencies for progress, tabs, and date utilities.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 11, 2025

Walkthrough

Adds new nutrition dashboard feature: API routes for dashboard data and goals, React components for dashboard UI (summary, trends, recent meals, history, AI analysis), page update to render the dashboard, UI primitives (tabs, progress), and Supabase tables/triggers/migrations for daily summaries and user goals. Also adds Radix and date-fns dependencies.

Changes

Cohort / File(s) Summary
Dependencies
package.json
Adds dependencies: @radix-ui/react-progress, @radix-ui/react-tabs, date-fns.
API: Nutrition Dashboard
src/app/api/nutrition/dashboard/route.ts
New GET route fetching daily summaries, goals, recent meals; computes analytics and goal progress; requires auth; supports days param.
API: Nutrition Goals
src/app/api/nutrition/goals/route.ts
New GET/POST routes to read/upsert user goals with validation, auth, and structured error handling.
Page Wiring
src/app/calorie-tracker/page.tsx
Replaces prior UI with CalorieTrackerDashboard; updates loading, layout, and success alert placement; preserves auth redirect.
Dashboard Orchestration
src/components/calorie-tracker/CalorieTrackerDashboard.tsx
New tabbed dashboard (dashboard/add-meal/trends/history); fetches data from dashboard API; refresh, loading, error states; integrates subcomponents.
Daily Summary Component
src/components/calorie-tracker/DailyCalorieSummary.tsx
New component showing today’s calories, macro progress, status badge, progress bars; supports loading state and actions.
Weekly Trends
src/components/calorie-tracker/WeeklyTrendsChart.tsx
New component fetching last 7 days and rendering selectable metric line chart; includes stats and states.
AI Analysis Display
src/components/calorie-tracker/AIAnalysisDisplay.tsx
New component rendering AI analysis states, confidence, items, totals; supports retry/correct callbacks and compact mode.
Food Log Management
src/components/calorie-tracker/FoodLogManager.tsx
New component listing, filtering, editing, and deleting nutrition_logs; recalculates totals; notifies on updates.
UI Primitives
src/components/ui/progress.tsx, src/components/ui/tabs.tsx
New Radix-based Progress and Tabs wrappers with styling and ref forwarding.
DB: Daily Summaries
supabase/migrations/007_create_daily_nutrition_summaries.sql
Creates daily_nutrition_summaries table, indexes, RLS policies; trigger functions and triggers to aggregate from nutrition_logs.
DB: User Goals
supabase/migrations/008_create_user_nutrition_goals.sql
Creates user_nutrition_goals table, index, RLS policies; updated_at trigger; inserts defaults for existing users.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant NextAPI as /api/nutrition/dashboard
  participant Supabase
  Client->>NextAPI: GET ?days=7 (cookies)
  NextAPI->>Supabase: auth.getUser()
  Supabase-->>NextAPI: user or null
  alt unauthorized
    NextAPI-->>Client: 401
  else authorized
    NextAPI->>Supabase: select daily_nutrition_summaries (date range)
    NextAPI->>Supabase: select user_nutrition_goals (single)
    NextAPI->>Supabase: select nutrition_logs (recent completed, limit 10)
    Supabase-->>NextAPI: results
    NextAPI-->>Client: 200 JSON {summaries, goals, recentMeals, analytics}
  end
Loading
sequenceDiagram
  participant Client
  participant GoalsAPI as /api/nutrition/goals
  participant Supabase
  Client->>GoalsAPI: GET (cookies)
  GoalsAPI->>Supabase: auth.getUser()
  Supabase-->>GoalsAPI: user
  GoalsAPI->>Supabase: select user_nutrition_goals (single)
  Supabase-->>GoalsAPI: goals or none
  GoalsAPI-->>Client: 200 {goals or defaults}

  Client->>GoalsAPI: POST {goals payload}
  GoalsAPI->>GoalsAPI: validate payload
  GoalsAPI->>Supabase: upsert user_nutrition_goals
  Supabase-->>GoalsAPI: upsert result
  GoalsAPI-->>Client: 200 {success, goals}
Loading
sequenceDiagram
  participant App
  participant DB as Supabase DB
  participant Trigger as update_daily_nutrition_summary()
  App->>DB: INSERT/UPDATE/DELETE nutrition_logs
  DB->>Trigger: AFTER row change
  Trigger->>DB: Aggregate completed logs by (user_id, date)
  Trigger->>DB: UPSERT daily_nutrition_summaries
  DB-->>App: Row change committed
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

In tabs I hop, a dashboard bright,
With trends that twinkle, goals in sight.
Progress bars like carrot beams,
Summaries stitched from supper dreams.
Triggers nibble logs to sums—so neat!
I thump approval with rhythmic feet.
Time for lunch; metrics taste sweet. 🥕✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

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

🧹 Nitpick comments (7)
src/components/calorie-tracker/FoodLogManager.tsx (1)

384-398: Constrain numeric inputs and guard parsing

Prevent negatives and reduce NaN coercion with HTML attributes and safer parsing.

-  <Input
-    type="number"
-    value={item.calories}
-    onChange={(e) => updateFoodItem(log.id, index, 'calories', parseInt(e.target.value) || 0)}
-    placeholder="Calories"
-    className="text-sm"
-  />
+  <Input
+    type="number"
+    inputMode="numeric"
+    min={0}
+    value={item.calories}
+    onChange={(e) => {
+      const n = Number(e.target.value)
+      updateFoodItem(log.id, index, 'calories', Number.isFinite(n) ? n : 0)
+    }}
+    placeholder="Calories"
+    className="text-sm"
+  />

Apply similar min={0} and parsing to protein.

src/app/calorie-tracker/page.tsx (2)

34-41: Consider extracting success alert styling to a variant

The success alert uses inline color classes. Consider adding a success variant to the Alert component for better reusability and consistency.

You could extend the Alert component to support a success variant:

-        <Alert className="mb-6 border-green-200 bg-green-50">
-          <CheckCircle className="h-4 w-4 text-green-600" />
-          <AlertDescription className="text-green-800">
+        <Alert variant="success" className="mb-6">
+          <CheckCircle className="h-4 w-4" />
+          <AlertDescription>

This would require updating the Alert component to handle the success variant with appropriate styling.


43-50: Consider extracting the loading spinner to a reusable component

The inline spinner animation could be extracted to a reusable LoadingSpinner component for consistency across the app.

Consider creating a reusable loading component:

// components/ui/loading-spinner.tsx
export function LoadingSpinner({ message }: { message?: string }) {
  return (
    <div className="flex items-center justify-center py-12">
      <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500" />
      {message && <span className="ml-2 text-gray-600">{message}</span>}
    </div>
  );
}

Then use it here:

-      <Suspense fallback={
-        <div className="flex items-center justify-center py-12">
-          <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
-          <span className="ml-2 text-gray-600">Loading your nutrition dashboard...</span>
-        </div>
-      }>
+      <Suspense fallback={<LoadingSpinner message="Loading your nutrition dashboard..." />}>
src/components/calorie-tracker/WeeklyTrendsChart.tsx (1)

42-44: Move fetchWeeklyData inside useEffect or wrap with useCallback

The fetchWeeklyData function is recreated on every render. While not causing issues due to the dependency array, it's better to follow React best practices.

Move the function inside useEffect:

  useEffect(() => {
+    const fetchWeeklyData = async () => {
+      try {
+        setLoading(true);
+        // ... rest of the function
+      } catch (err) {
+        // ... error handling
+      }
+    };
    fetchWeeklyData();
  }, [userId]);
-
-  const fetchWeeklyData = async () => {
-    // ... function body
-  };
src/components/calorie-tracker/AIAnalysisDisplay.tsx (2)

72-88: Extract magic numbers as constants and add score validation

The confidence thresholds are hardcoded and the score parameter lacks validation.

+const CONFIDENCE_THRESHOLDS = {
+  HIGH: 0.8,
+  MEDIUM: 0.6
+} as const;
+
 const getConfidenceColor = (score: number) => {
-  if (score >= 0.8) return 'bg-green-500';
-  if (score >= 0.6) return 'bg-yellow-500';
+  const validScore = Math.max(0, Math.min(1, score)); // Clamp to [0, 1]
+  if (validScore >= CONFIDENCE_THRESHOLDS.HIGH) return 'bg-green-500';
+  if (validScore >= CONFIDENCE_THRESHOLDS.MEDIUM) return 'bg-yellow-500';
   return 'bg-red-500';
 };

 const getConfidenceText = (score: number) => {
-  if (score >= 0.8) return 'High Confidence';
-  if (score >= 0.6) return 'Medium Confidence';
+  const validScore = Math.max(0, Math.min(1, score));
+  if (validScore >= CONFIDENCE_THRESHOLDS.HIGH) return 'High Confidence';
+  if (validScore >= CONFIDENCE_THRESHOLDS.MEDIUM) return 'Medium Confidence';
   return 'Low Confidence';
 };

116-129: Align tip threshold with confidence thresholds

The tips threshold (0.7) doesn't align with the confidence thresholds (0.6, 0.8), creating inconsistency.

 const getTips = (score: number, hasError: boolean) => {
   if (hasError) return [];
   
-  if (score < 0.7) {
+  if (score < CONFIDENCE_THRESHOLDS.MEDIUM) {
     return [
       'Ensure good lighting when taking photos',
       'Include the entire meal in the frame',
       'Avoid heavily processed or mixed foods',
       'Take photos from directly above the food',
       'Remove packaging or plates that obscure the food'
     ];
   }
   return [];
 };
supabase/migrations/008_create_user_nutrition_goals.sql (1)

49-52: Consider using NOT EXISTS for better performance

The current approach with NOT IN subquery might be less efficient than NOT EXISTS for large user bases.

-INSERT INTO public.user_nutrition_goals (user_id)
-SELECT id FROM auth.users
-WHERE id NOT IN (SELECT user_id FROM public.user_nutrition_goals)
-ON CONFLICT (user_id) DO NOTHING;
+INSERT INTO public.user_nutrition_goals (user_id)
+SELECT u.id FROM auth.users u
+WHERE NOT EXISTS (
+  SELECT 1 FROM public.user_nutrition_goals g 
+  WHERE g.user_id = u.id
+)
+ON CONFLICT (user_id) DO NOTHING;
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8316dd1 and 0de757e.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (13)
  • package.json (1 hunks)
  • src/app/api/nutrition/dashboard/route.ts (1 hunks)
  • src/app/api/nutrition/goals/route.ts (1 hunks)
  • src/app/calorie-tracker/page.tsx (3 hunks)
  • src/components/calorie-tracker/AIAnalysisDisplay.tsx (1 hunks)
  • src/components/calorie-tracker/CalorieTrackerDashboard.tsx (1 hunks)
  • src/components/calorie-tracker/DailyCalorieSummary.tsx (1 hunks)
  • src/components/calorie-tracker/FoodLogManager.tsx (1 hunks)
  • src/components/calorie-tracker/WeeklyTrendsChart.tsx (1 hunks)
  • src/components/ui/progress.tsx (1 hunks)
  • src/components/ui/tabs.tsx (1 hunks)
  • supabase/migrations/007_create_daily_nutrition_summaries.sql (1 hunks)
  • supabase/migrations/008_create_user_nutrition_goals.sql (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (9)
src/app/api/nutrition/goals/route.ts (1)
src/app/api/nutrition/dashboard/route.ts (1)
  • GET (16-140)
src/components/calorie-tracker/AIAnalysisDisplay.tsx (6)
src/lib/nutrition-types.ts (1)
  • FoodItem (1-11)
src/components/ui/badge.tsx (1)
  • Badge (46-46)
src/components/ui/card.tsx (4)
  • Card (85-85)
  • CardHeader (86-86)
  • CardTitle (88-88)
  • CardContent (91-91)
src/components/ui/alert.tsx (2)
  • Alert (81-81)
  • AlertDescription (81-81)
src/components/ui/button.tsx (1)
  • Button (59-59)
src/components/ui/progress.tsx (1)
  • Progress (28-28)
src/components/ui/tabs.tsx (1)
src/lib/utils.ts (1)
  • cn (4-6)
src/app/api/nutrition/dashboard/route.ts (1)
src/app/api/nutrition/goals/route.ts (1)
  • GET (14-59)
src/components/calorie-tracker/CalorieTrackerDashboard.tsx (5)
src/components/calorie-tracker/DailyCalorieSummary.tsx (1)
  • DailyCalorieSummary (58-245)
src/components/calorie-tracker/WeeklyTrendsChart.tsx (1)
  • WeeklyTrendsChart (36-297)
src/components/calorie-tracker/AIAnalysisDisplay.tsx (1)
  • AIAnalysisDisplay (63-363)
src/components/calorie-tracker/PhotoUpload.tsx (1)
  • PhotoUpload (33-264)
src/components/calorie-tracker/FoodLogManager.tsx (1)
  • FoodLogManager (67-484)
src/app/calorie-tracker/page.tsx (2)
src/components/ui/alert.tsx (2)
  • Alert (81-81)
  • AlertDescription (81-81)
src/components/calorie-tracker/CalorieTrackerDashboard.tsx (1)
  • CalorieTrackerDashboard (44-354)
src/components/ui/progress.tsx (1)
src/lib/utils.ts (1)
  • cn (4-6)
src/components/calorie-tracker/FoodLogManager.tsx (6)
src/lib/nutrition-types.ts (1)
  • FoodItem (1-11)
src/components/ui/badge.tsx (1)
  • Badge (46-46)
src/components/ui/card.tsx (4)
  • Card (85-85)
  • CardHeader (86-86)
  • CardTitle (88-88)
  • CardContent (91-91)
src/components/ui/button.tsx (1)
  • Button (59-59)
src/components/ui/input.tsx (1)
  • Input (28-28)
src/components/ui/textarea.tsx (1)
  • Textarea (24-24)
src/components/calorie-tracker/DailyCalorieSummary.tsx (4)
src/components/ui/card.tsx (4)
  • Card (85-85)
  • CardHeader (86-86)
  • CardTitle (88-88)
  • CardContent (91-91)
src/components/ui/button.tsx (1)
  • Button (59-59)
src/components/ui/badge.tsx (1)
  • Badge (46-46)
src/components/ui/progress.tsx (1)
  • Progress (28-28)
🔇 Additional comments (16)
package.json (1)

20-27: Verify peer-dependency and ESM compatibility for new UI/date libraries

Additions look consistent with the new components, but please verify:

  • @radix-ui/react-tabs and @radix-ui/react-progress peer-deps align with React 19/Next 15.
  • date-fns v4 is ESM-only; ensure Jest/ts-jest/transforms are configured if tests import it.
src/components/ui/tabs.tsx (1)

8-55: LGTM: Tabs wrappers are well-typed and consistent with shadcn patterns

ForwardRef typings, class composition, and exports look solid. No issues found.

src/components/calorie-tracker/FoodLogManager.tsx (1)

130-134: Timezone edge case in date filter

Formatting created_at with local timezone can shift dates around midnight if the column is UTC. If users expect local dates, this is fine; otherwise, consider comparing against UTC or using toISOString().slice(0,10) consistently.

supabase/migrations/007_create_daily_nutrition_summaries.sql (1)

15-27: Ensure gen_random_uuid() is available

gen_random_uuid() requires the pgcrypto extension. Some Supabase projects have it enabled by default, but if not, the migration will fail.

Add before table creation if needed:

CREATE EXTENSION IF NOT EXISTS pgcrypto;
src/app/calorie-tracker/page.tsx (1)

13-19: Documentation accurately reflects the new dashboard functionality

The updated JSDoc properly describes the comprehensive dashboard features including daily summaries, trends, and meal logging.

src/app/api/nutrition/goals/route.ts (2)

14-59: GET handler implementation looks good

Proper authentication, error handling, and fallback to default values when no goals exist. The PGRST116 code handling for missing data is correct.


82-102: Input validation is comprehensive and well-implemented

Good validation for calorie goal range and enum values for activity level and weight goal with clear error messages.

src/components/calorie-tracker/WeeklyTrendsChart.tsx (2)

208-255: Chart implementation is well-structured

Good use of Recharts with responsive design, proper styling, and interactive elements. The tooltip configuration and line styling enhance user experience.


286-292: Good UX with the no-data state

Helpful messaging and clear call-to-action when no data is available for the selected metric.

src/components/calorie-tracker/DailyCalorieSummary.tsx (3)

66-84: Well-implemented loading skeleton

Good use of skeleton UI with animated pulse effect that maintains layout consistency during loading.


91-124: Verify fiber progress calculation inconsistency

Fiber progress is calculated inline (line 122) while other macros use goalProgress. This suggests the API doesn't include fiber in goalProgress.

Consider either:

  1. Adding fiber to the API's goalProgress calculation for consistency
  2. Document why fiber is handled differently

If this is intentional, consider adding a comment explaining the difference.


209-227: Clean and well-formatted macro display

Good formatting choices with toFixed(1) for amounts and toFixed(0) for percentages. The grid layout provides clear visual organization.

src/app/api/nutrition/dashboard/route.ts (1)

81-102: Goal progress calculation is well-implemented

Good null checking and fallback values. Note that fiber is intentionally excluded from goalProgress, which aligns with the frontend component's inline calculation for fiber.

src/components/calorie-tracker/CalorieTrackerDashboard.tsx (2)

57-66: Authentication handled server-side – no client auth header needed

The dashboard API route in src/app/api/nutrition/dashboard/route.ts calls supabase.auth.getUser() and returns a 401 if the user isn’t signed in. Browser fetch('/api/nutrition/dashboard') defaults to "same-origin" credentials, so session cookies (and Supabase auth) are sent automatically. No changes are needed to the client‐side fetch call.


232-239: Fix off-by-one error in food items display logic

The condition index < meal.food_items.length - 1 && index < 1 has an off-by-one error. When showing 2 items, the comma won't appear between them.

 {meal.food_items.slice(0, 2).map((item: any, index: number) => (
   <span key={index}>
     {item.name}
-    {index < meal.food_items.length - 1 && index < 1 ? ', ' : ''}
+    {index === 0 && meal.food_items.length > 1 ? ', ' : ''}
   </span>
 ))}

Likely an incorrect or invalid review comment.

supabase/migrations/008_create_user_nutrition_goals.sql (1)

43-45: Trigger function verified
The trigger in supabase/migrations/008_create_user_nutrition_goals.sql correctly references public.update_updated_at_column(), which is defined in

  • supabase/migrations/007_create_daily_nutrition_summaries.sql

No further changes required.

Comment on lines +31 to +69
// Fetch daily summaries
const { data: dailySummaries, error: summariesError } = await supabase
.from('daily_nutrition_summaries')
.select('*')
.eq('user_id', user.id)
.gte('date', format(startDate, 'yyyy-MM-dd'))
.lte('date', format(endDate, 'yyyy-MM-dd'))
.order('date', { ascending: true });

if (summariesError) {
logger.error('Failed to fetch daily summaries', { error: summariesError, userId: user.id });
throw summariesError;
}

// Fetch user goals
const { data: userGoals, error: goalsError } = await supabase
.from('user_nutrition_goals')
.select('*')
.eq('user_id', user.id)
.single();

if (goalsError && goalsError.code !== 'PGRST116') {
logger.error('Failed to fetch user goals', { error: goalsError, userId: user.id });
throw goalsError;
}

// Fetch recent meal logs
const { data: recentMeals, error: mealsError } = await supabase
.from('nutrition_logs')
.select('*')
.eq('user_id', user.id)
.eq('processing_status', 'completed')
.order('created_at', { ascending: false })
.limit(10);

if (mealsError) {
logger.error('Failed to fetch recent meals', { error: mealsError, userId: user.id });
throw mealsError;
}
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

Parallelize independent database queries for better performance

The three database queries (dailySummaries, userGoals, recentMeals) are executed sequentially but are independent. Parallelize them for better performance.

Use Promise.all to run queries in parallel:

-    // Fetch daily summaries
-    const { data: dailySummaries, error: summariesError } = await supabase
-      .from('daily_nutrition_summaries')
-      .select('*')
-      .eq('user_id', user.id)
-      .gte('date', format(startDate, 'yyyy-MM-dd'))
-      .lte('date', format(endDate, 'yyyy-MM-dd'))
-      .order('date', { ascending: true });
-
-    if (summariesError) {
-      logger.error('Failed to fetch daily summaries', { error: summariesError, userId: user.id });
-      throw summariesError;
-    }
-
-    // Fetch user goals
-    const { data: userGoals, error: goalsError } = await supabase
-      .from('user_nutrition_goals')
-      .select('*')
-      .eq('user_id', user.id)
-      .single();
-
-    if (goalsError && goalsError.code !== 'PGRST116') {
-      logger.error('Failed to fetch user goals', { error: goalsError, userId: user.id });
-      throw goalsError;
-    }
-
-    // Fetch recent meal logs
-    const { data: recentMeals, error: mealsError } = await supabase
-      .from('nutrition_logs')
-      .select('*')
-      .eq('user_id', user.id)
-      .eq('processing_status', 'completed')
-      .order('created_at', { ascending: false })
-      .limit(10);
-
-    if (mealsError) {
-      logger.error('Failed to fetch recent meals', { error: mealsError, userId: user.id });
-      throw mealsError;
-    }
+    // Fetch all data in parallel
+    const [summariesResult, goalsResult, mealsResult] = await Promise.all([
+      supabase
+        .from('daily_nutrition_summaries')
+        .select('*')
+        .eq('user_id', user.id)
+        .gte('date', format(startDate, 'yyyy-MM-dd'))
+        .lte('date', format(endDate, 'yyyy-MM-dd'))
+        .order('date', { ascending: true }),
+      
+      supabase
+        .from('user_nutrition_goals')
+        .select('*')
+        .eq('user_id', user.id)
+        .single(),
+      
+      supabase
+        .from('nutrition_logs')
+        .select('*')
+        .eq('user_id', user.id)
+        .eq('processing_status', 'completed')
+        .order('created_at', { ascending: false })
+        .limit(10)
+    ]);
+
+    const { data: dailySummaries, error: summariesError } = summariesResult;
+    const { data: userGoals, error: goalsError } = goalsResult;
+    const { data: recentMeals, error: mealsError } = mealsResult;
+
+    if (summariesError) {
+      logger.error('Failed to fetch daily summaries', { error: summariesError, userId: user.id });
+      throw summariesError;
+    }
+
+    if (goalsError && goalsError.code !== 'PGRST116') {
+      logger.error('Failed to fetch user goals', { error: goalsError, userId: user.id });
+      throw goalsError;
+    }
+
+    if (mealsError) {
+      logger.error('Failed to fetch recent meals', { error: mealsError, userId: user.id });
+      throw mealsError;
+    }

This reduces the total query time from sum(t1 + t2 + t3) to max(t1, t2, t3).

📝 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
// Fetch daily summaries
const { data: dailySummaries, error: summariesError } = await supabase
.from('daily_nutrition_summaries')
.select('*')
.eq('user_id', user.id)
.gte('date', format(startDate, 'yyyy-MM-dd'))
.lte('date', format(endDate, 'yyyy-MM-dd'))
.order('date', { ascending: true });
if (summariesError) {
logger.error('Failed to fetch daily summaries', { error: summariesError, userId: user.id });
throw summariesError;
}
// Fetch user goals
const { data: userGoals, error: goalsError } = await supabase
.from('user_nutrition_goals')
.select('*')
.eq('user_id', user.id)
.single();
if (goalsError && goalsError.code !== 'PGRST116') {
logger.error('Failed to fetch user goals', { error: goalsError, userId: user.id });
throw goalsError;
}
// Fetch recent meal logs
const { data: recentMeals, error: mealsError } = await supabase
.from('nutrition_logs')
.select('*')
.eq('user_id', user.id)
.eq('processing_status', 'completed')
.order('created_at', { ascending: false })
.limit(10);
if (mealsError) {
logger.error('Failed to fetch recent meals', { error: mealsError, userId: user.id });
throw mealsError;
}
// Fetch all data in parallel
const [summariesResult, goalsResult, mealsResult] = await Promise.all([
supabase
.from('daily_nutrition_summaries')
.select('*')
.eq('user_id', user.id)
.gte('date', format(startDate, 'yyyy-MM-dd'))
.lte('date', format(endDate, 'yyyy-MM-dd'))
.order('date', { ascending: true }),
supabase
.from('user_nutrition_goals')
.select('*')
.eq('user_id', user.id)
.single(),
supabase
.from('nutrition_logs')
.select('*')
.eq('user_id', user.id)
.eq('processing_status', 'completed')
.order('created_at', { ascending: false })
.limit(10)
]);
const { data: dailySummaries, error: summariesError } = summariesResult;
const { data: userGoals, error: goalsError } = goalsResult;
const { data: recentMeals, error: mealsError } = mealsResult;
if (summariesError) {
logger.error('Failed to fetch daily summaries', { error: summariesError, userId: user.id });
throw summariesError;
}
if (goalsError && goalsError.code !== 'PGRST116') {
logger.error('Failed to fetch user goals', { error: goalsError, userId: user.id });
throw goalsError;
}
if (mealsError) {
logger.error('Failed to fetch recent meals', { error: mealsError, userId: user.id });
throw mealsError;
}
🤖 Prompt for AI Agents
In src/app/api/nutrition/dashboard/route.ts around lines 31 to 69, the three
independent Supabase queries (dailySummaries, userGoals, recentMeals) are
executed sequentially; change them to run in parallel by constructing the three
query promises first (call
.from(...).select(...).eq(...).gte(...).lte(...).order... etc. but do not await
them immediately), then await Promise.all on the three promises and destructure
their results into the existing variables; after awaiting, keep the existing
per-query error handling logic (log and throw summariesError and mealsError as
before, and for userGoals keep the special-case check for PGRST116 before
logging/throwing). Ensure date formatting is done before creating the promises
so .gte/.lte receive strings, and preserve ordering/limit clauses in each query.

Comment on lines +104 to +119
const { data: goals, error } = await supabase
.from('user_nutrition_goals')
.upsert({
user_id: user.id,
daily_calorie_goal: parseInt(daily_calorie_goal),
daily_protein_goal_g: parseFloat(daily_protein_goal_g) || 150,
daily_carbs_goal_g: parseFloat(daily_carbs_goal_g) || 200,
daily_fat_goal_g: parseFloat(daily_fat_goal_g) || 70,
daily_fiber_goal_g: parseFloat(daily_fiber_goal_g) || 25,
activity_level: activity_level || 'moderate',
weight_goal: weight_goal || 'maintain',
updated_at: new Date().toISOString(),
})
.select()
.single();

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

Fix potential issue with zero values and improve number parsing

The current implementation has issues with handling zero values and number parsing:

  1. The || operator will incorrectly replace legitimate 0 values with defaults
  2. parseInt should specify radix for clarity
  3. Consider using nullish coalescing (??) instead

Apply this fix:

      .upsert({
        user_id: user.id,
-        daily_calorie_goal: parseInt(daily_calorie_goal),
-        daily_protein_goal_g: parseFloat(daily_protein_goal_g) || 150,
-        daily_carbs_goal_g: parseFloat(daily_carbs_goal_g) || 200,
-        daily_fat_goal_g: parseFloat(daily_fat_goal_g) || 70,
-        daily_fiber_goal_g: parseFloat(daily_fiber_goal_g) || 25,
+        daily_calorie_goal: parseInt(daily_calorie_goal, 10),
+        daily_protein_goal_g: daily_protein_goal_g !== undefined ? parseFloat(daily_protein_goal_g) : 150,
+        daily_carbs_goal_g: daily_carbs_goal_g !== undefined ? parseFloat(daily_carbs_goal_g) : 200,
+        daily_fat_goal_g: daily_fat_goal_g !== undefined ? parseFloat(daily_fat_goal_g) : 70,
+        daily_fiber_goal_g: daily_fiber_goal_g !== undefined ? parseFloat(daily_fiber_goal_g) : 25,
        activity_level: activity_level || 'moderate',
        weight_goal: weight_goal || 'maintain',
        updated_at: new Date().toISOString(),
      })

Also, since the migration includes an update trigger for updated_at, you might not need to set it manually on line 115.

📝 Committable suggestion

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

Suggested change
const { data: goals, error } = await supabase
.from('user_nutrition_goals')
.upsert({
user_id: user.id,
daily_calorie_goal: parseInt(daily_calorie_goal),
daily_protein_goal_g: parseFloat(daily_protein_goal_g) || 150,
daily_carbs_goal_g: parseFloat(daily_carbs_goal_g) || 200,
daily_fat_goal_g: parseFloat(daily_fat_goal_g) || 70,
daily_fiber_goal_g: parseFloat(daily_fiber_goal_g) || 25,
activity_level: activity_level || 'moderate',
weight_goal: weight_goal || 'maintain',
updated_at: new Date().toISOString(),
})
.select()
.single();
const { data: goals, error } = await supabase
.from('user_nutrition_goals')
.upsert({
user_id: user.id,
daily_calorie_goal: parseInt(daily_calorie_goal, 10),
daily_protein_goal_g: daily_protein_goal_g !== undefined
? parseFloat(daily_protein_goal_g)
: 150,
daily_carbs_goal_g: daily_carbs_goal_g !== undefined
? parseFloat(daily_carbs_goal_g)
: 200,
daily_fat_goal_g: daily_fat_goal_g !== undefined
? parseFloat(daily_fat_goal_g)
: 70,
daily_fiber_goal_g: daily_fiber_goal_g !== undefined
? parseFloat(daily_fiber_goal_g)
: 25,
activity_level: activity_level || 'moderate',
weight_goal: weight_goal || 'maintain',
updated_at: new Date().toISOString(),
})
.select()
.single();

Comment on lines +21 to +28
interface FoodItem {
name: string;
quantity: string;
calories: number;
protein_g: number;
carbs_g: number;
fat_g: number;
}
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

Consider using the existing FoodItem interface from nutrition-types

The local FoodItem interface duplicates a subset of fields from the existing FoodItem interface in src/lib/nutrition-types.ts. This could lead to type inconsistency and maintenance issues.

Replace the local interface with an import:

+import { FoodItem } from '@/lib/nutrition-types';
+
-interface FoodItem {
-  name: string;
-  quantity: string;
-  calories: number;
-  protein_g: number;
-  carbs_g: number;
-  fat_g: number;
-}

If quantity is needed but not in the global type, consider extending it:

interface AIFoodItem extends FoodItem {
  quantity: string;
}
🤖 Prompt for AI Agents
In src/components/calorie-tracker/AIAnalysisDisplay.tsx around lines 21 to 28
the file declares a local FoodItem interface that duplicates fields from the
global FoodItem in src/lib/nutrition-types.ts; remove the local declaration and
import the shared FoodItem from src/lib/nutrition-types.ts instead, and if the
component needs the quantity field that isn’t present on the shared type create
an extended interface (e.g., AIFoodItem extends FoodItem { quantity: string })
and use that extended type throughout the file to replace the local duplicate to
keep types consistent.

Comment on lines +103 to +114
const handleReprocess = async () => {
if (!onReprocess) return;

setIsReprocessing(true);
try {
await onReprocess(log.id);
} catch (error) {
console.error('Reprocess failed:', error);
} finally {
setIsReprocessing(false);
}
};
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

Surface reprocessing errors to the user

Errors are only logged to console without user feedback, leading to a poor UX when reprocessing fails.

Add error state and display:

 const [isReprocessing, setIsReprocessing] = useState(false);
+const [reprocessError, setReprocessError] = useState<string | null>(null);

 const handleReprocess = async () => {
   if (!onReprocess) return;
   
   setIsReprocessing(true);
+  setReprocessError(null);
   try {
     await onReprocess(log.id);
   } catch (error) {
     console.error('Reprocess failed:', error);
+    setReprocessError('Failed to reprocess. Please try again.');
   } finally {
     setIsReprocessing(false);
   }
 };

Then display the error in the UI where the reprocess button is shown.

📝 Committable suggestion

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

Suggested change
const handleReprocess = async () => {
if (!onReprocess) return;
setIsReprocessing(true);
try {
await onReprocess(log.id);
} catch (error) {
console.error('Reprocess failed:', error);
} finally {
setIsReprocessing(false);
}
};
// Add this alongside your other state hooks
const [isReprocessing, setIsReprocessing] = useState(false);
const [reprocessError, setReprocessError] = useState<string | null>(null);
const handleReprocess = async () => {
if (!onReprocess) return;
setIsReprocessing(true);
setReprocessError(null);
try {
await onReprocess(log.id);
} catch (error) {
console.error('Reprocess failed:', error);
setReprocessError('Failed to reprocess. Please try again.');
} finally {
setIsReprocessing(false);
}
};
🤖 Prompt for AI Agents
In src/components/calorie-tracker/AIAnalysisDisplay.tsx around lines 103 to 114,
the reprocess handler only logs errors to the console so failures aren’t visible
to users; add a state variable (e.g., reprocessError: string | null) and set it
to null at the start of handleReprocess, set it to a user-friendly message
derived from the caught error in the catch block (convert error to string
safely), and keep setIsReprocessing behavior in finally; then render that error
message in the component UI adjacent to the reprocess button (with minimal
styling/role for accessibility) so users see the failure and can retry.

Comment on lines +254 to +271
{log.food_items.map((item, index) => (
<div key={index} className="p-3 bg-gray-50 rounded-lg">
<div className="flex justify-between items-start">
<div>
<span className="font-medium text-gray-900">{item.name}</span>
<span className="text-gray-500 ml-2">({item.quantity})</span>
</div>
<div className="text-lg font-bold text-orange-600">
{item.calories} cal
</div>
</div>
<div className="mt-2 text-sm text-gray-600 grid grid-cols-3 gap-4">
<div>Protein: {item.protein_g}g</div>
<div>Carbs: {item.carbs_g}g</div>
<div>Fat: {item.fat_g}g</div>
</div>
</div>
))}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add null check for item.quantity display

The quantity field is displayed without checking if it exists, potentially showing "(undefined)".

 <div>
   <span className="font-medium text-gray-900">{item.name}</span>
-  <span className="text-gray-500 ml-2">({item.quantity})</span>
+  {item.quantity && (
+    <span className="text-gray-500 ml-2">({item.quantity})</span>
+  )}
 </div>
📝 Committable suggestion

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

Suggested change
{log.food_items.map((item, index) => (
<div key={index} className="p-3 bg-gray-50 rounded-lg">
<div className="flex justify-between items-start">
<div>
<span className="font-medium text-gray-900">{item.name}</span>
<span className="text-gray-500 ml-2">({item.quantity})</span>
</div>
<div className="text-lg font-bold text-orange-600">
{item.calories} cal
</div>
</div>
<div className="mt-2 text-sm text-gray-600 grid grid-cols-3 gap-4">
<div>Protein: {item.protein_g}g</div>
<div>Carbs: {item.carbs_g}g</div>
<div>Fat: {item.fat_g}g</div>
</div>
</div>
))}
{log.food_items.map((item, index) => (
<div key={index} className="p-3 bg-gray-50 rounded-lg">
<div className="flex justify-between items-start">
<div>
<span className="font-medium text-gray-900">{item.name}</span>
- <span className="text-gray-500 ml-2">({item.quantity})</span>
+ {item.quantity && (
+ <span className="text-gray-500 ml-2">({item.quantity})</span>
+ )}
</div>
<div className="text-lg font-bold text-orange-600">
{item.calories} cal
</div>
</div>
<div className="mt-2 text-sm text-gray-600 grid grid-cols-3 gap-4">
<div>Protein: {item.protein_g}g</div>
<div>Carbs: {item.carbs_g}g</div>
<div>Fat: {item.fat_g}g</div>
</div>
</div>
))}
🤖 Prompt for AI Agents
In src/components/calorie-tracker/AIAnalysisDisplay.tsx around lines 254 to 271,
the JSX renders item.quantity directly which can produce "(undefined)"; update
the rendering to check for null/undefined and only render the quantity segment
when it exists (or render a safe fallback like "1" or omit the parentheses
entirely). Ensure the conditional is applied inline so the span showing quantity
is only output when item.quantity !== null && item.quantity !== undefined (or
replaced with the chosen fallback).

Comment on lines +463 to +471
<div className="mt-3 flex items-center gap-2">
<ImageIcon className="h-4 w-4 text-gray-500" />
<Button
variant="ghost"
size="sm"
onClick={() => window.open(log.image_url, '_blank')}
className="text-blue-600 hover:text-blue-800"
>
View Image
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

Prevent reverse tabnabbing when opening images

Use noopener/noreferrer when opening a new tab.

- onClick={() => window.open(log.image_url, '_blank')}
+ onClick={() => window.open(log.image_url, '_blank', 'noopener,noreferrer')}

Alternatively, render an anchor:

<a href={log.image_url} target="_blank" rel="noopener noreferrer">
  <Button variant="ghost" size="sm" className="text-blue-600 hover:text-blue-800">
    View Image
  </Button>
</a>
🤖 Prompt for AI Agents
In src/components/calorie-tracker/FoodLogManager.tsx around lines 463 to 471,
the "View Image" button opens a new tab using window.open without
noopener/noreferrer which allows reverse tabnabbing; fix by preventing opener
access: either render the Button inside an anchor <a href={log.image_url}
target="_blank" rel="noopener noreferrer">...</a>, or change the onClick to open
the window and immediately null out the opener (const w =
window.open(log.image_url, '_blank'); if (w) w.opener = null;). Ensure
rel="noopener noreferrer" is present if using an anchor, or set newWindow.opener
= null when using window.open.

Comment on lines +194 to +204
{Object.entries(metricConfig).map(([key, config]) => (
<Button
key={key}
onClick={() => setActiveMetric(key as any)}
size="sm"
variant={activeMetric === key ? "default" : "outline"}
className="text-xs"
>
{config.label}
</Button>
))}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Avoid using 'any' type - use proper type casting

The any type in the onClick handler defeats TypeScript's type safety.

Replace with proper type casting:

-                onClick={() => setActiveMetric(key as any)}
+                onClick={() => setActiveMetric(key as 'calories' | 'protein' | 'carbs' | 'fat' | 'fiber')}

Or better yet, extract the type:

type MetricType = 'calories' | 'protein' | 'carbs' | 'fat' | 'fiber';
// Then use: key as MetricType
🤖 Prompt for AI Agents
In src/components/calorie-tracker/WeeklyTrendsChart.tsx around lines 194 to 204,
the onClick uses key as any which removes TypeScript safety; introduce a
concrete MetricType union (e.g. 'calories' | 'protein' | 'carbs' | 'fat' |
'fiber'), type metricConfig as Record<MetricType, ...> (or narrow Object.entries
with typed keys), and replace key as any with key as MetricType (or have the map
strongly-typed) so the handler and activeMetric use the proper MetricType
instead of any.

Comment on lines +11 to +23
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
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

Pass value to Root and clamp to [0,100] to avoid invalid CSS and improve a11y

  • The destructured value is not forwarded to ProgressPrimitive.Root. That removes ARIA semantics.
  • translateX(-${100 - (value || 0)}%) can produce --20% when value > 100, which is invalid CSS.
  • Guard against NaN and clamp to [0, 100].

Apply this diff:

>(({ className, value, ...props }, ref) => (
   <ProgressPrimitive.Root
     ref={ref}
     className={cn(
       "relative h-4 w-full overflow-hidden rounded-full bg-secondary",
       className
     )}
-    {...props}
+    value={typeof value === "number" ? value : 0}
+    {...props}
   >
     <ProgressPrimitive.Indicator
       className="h-full w-full flex-1 bg-primary transition-all"
-      style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
+      style={{
+        transform: `translateX(-${
+          100 - Math.min(100, Math.max(0, Number.isFinite(value as number) ? (value as number) : 0))
+        }%)`,
+      }}
     />
   </ProgressPrimitive.Root>
📝 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
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
className
)}
value={typeof value === "number" ? value : 0}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{
transform: `translateX(-${
100 -
Math.min(
100,
Math.max(0, Number.isFinite(value as number) ? (value as number) : 0)
)
}%)`,
}}
/>
🤖 Prompt for AI Agents
In src/components/ui/progress.tsx around lines 11 to 23, the destructured value
is not forwarded to ProgressPrimitive.Root and the inline transform can generate
invalid CSS for values outside [0,100]; convert value to a number, guard against
NaN, clamp it to the range 0–100 (e.g. const safeValue = Math.min(100,
Math.max(0, Number(value) || 0))), pass that safeValue into
ProgressPrimitive.Root as the value prop to preserve ARIA semantics, and use
safeValue when computing the translateX style (translateX(-${100 -
safeValue}%)).

Comment on lines +49 to +90
-- Function to update daily summaries
CREATE OR REPLACE FUNCTION public.update_daily_nutrition_summary()
RETURNS TRIGGER AS $$
BEGIN
-- Update or insert daily summary
INSERT INTO public.daily_nutrition_summaries (
user_id,
date,
total_calories,
total_protein_g,
total_carbs_g,
total_fat_g,
total_fiber_g,
meal_count
)
SELECT
user_id,
DATE(created_at) as date,
COALESCE(SUM(total_calories), 0) as total_calories,
COALESCE(SUM(total_protein_g), 0) as total_protein_g,
COALESCE(SUM(total_carbs_g), 0) as total_carbs_g,
COALESCE(SUM(total_fat_g), 0) as total_fat_g,
COALESCE(SUM(total_fiber_g), 0) as total_fiber_g,
COUNT(*) as meal_count
FROM public.nutrition_logs
WHERE user_id = COALESCE(NEW.user_id, OLD.user_id)
AND DATE(created_at) = DATE(COALESCE(NEW.created_at, OLD.created_at))
AND processing_status = 'completed'
GROUP BY user_id, DATE(created_at)
ON CONFLICT (user_id, date)
DO UPDATE SET
total_calories = EXCLUDED.total_calories,
total_protein_g = EXCLUDED.total_protein_g,
total_carbs_g = EXCLUDED.total_carbs_g,
total_fat_g = EXCLUDED.total_fat_g,
total_fiber_g = EXCLUDED.total_fiber_g,
meal_count = EXCLUDED.meal_count,
updated_at = NOW();

RETURN COALESCE(NEW, OLD);
END;
$$ LANGUAGE plpgsql;
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

Trigger function leaves stale rows on last-delete and misses old/new day recounts on UPDATE

Current logic only upserts when the SELECT returns rows. If the last completed log for a day is deleted or status changes away from 'completed', the SELECT yields 0 rows and the existing summary remains stale. Also, when an UPDATE changes user_id or created_at date, only the NEW pair is recalculated, leaving the OLD day/user incorrect.

Patch to:

  • Recompute for both OLD and NEW (when applicable).
  • Upsert when there are rows; otherwise delete the summary for that (user_id, date).
 CREATE OR REPLACE FUNCTION public.update_daily_nutrition_summary()
 RETURNS TRIGGER AS $$
-BEGIN
-    -- Update or insert daily summary
-    INSERT INTO public.daily_nutrition_summaries (
-        user_id, 
-        date, 
-        total_calories, 
-        total_protein_g, 
-        total_carbs_g, 
-        total_fat_g, 
-        total_fiber_g, 
-        meal_count
-    )
-    SELECT 
-        user_id,
-        DATE(created_at) as date,
-        COALESCE(SUM(total_calories), 0) as total_calories,
-        COALESCE(SUM(total_protein_g), 0) as total_protein_g,
-        COALESCE(SUM(total_carbs_g), 0) as total_carbs_g,
-        COALESCE(SUM(total_fat_g), 0) as total_fat_g,
-        COALESCE(SUM(total_fiber_g), 0) as total_fiber_g,
-        COUNT(*) as meal_count
-    FROM public.nutrition_logs 
-    WHERE user_id = COALESCE(NEW.user_id, OLD.user_id)
-      AND DATE(created_at) = DATE(COALESCE(NEW.created_at, OLD.created_at))
-      AND processing_status = 'completed'
-    GROUP BY user_id, DATE(created_at)
-    ON CONFLICT (user_id, date) 
-    DO UPDATE SET
-        total_calories = EXCLUDED.total_calories,
-        total_protein_g = EXCLUDED.total_protein_g,
-        total_carbs_g = EXCLUDED.total_carbs_g,
-        total_fat_g = EXCLUDED.total_fat_g,
-        total_fiber_g = EXCLUDED.total_fiber_g,
-        meal_count = EXCLUDED.meal_count,
-        updated_at = NOW();
-    
-    RETURN COALESCE(NEW, OLD);
-END;
+DECLARE
+    v_pairs TABLE (user_id uuid, day date);
+BEGIN
+    -- Collect affected (user, day) pairs for this mutation
+    IF TG_OP = 'UPDATE' THEN
+        INSERT INTO v_pairs VALUES (OLD.user_id, DATE(OLD.created_at));
+        IF DATE(NEW.created_at) <> DATE(OLD.created_at) OR NEW.user_id <> OLD.user_id THEN
+            INSERT INTO v_pairs VALUES (NEW.user_id, DATE(NEW.created_at));
+        END IF;
+    ELSE
+        INSERT INTO v_pairs VALUES (COALESCE(NEW.user_id, OLD.user_id), DATE(COALESCE(NEW.created_at, OLD.created_at)));
+    END IF;
+
+    -- Recompute each affected pair
+    FOR SELECT DISTINCT user_id, day FROM v_pairs LOOP
+        WITH agg AS (
+            SELECT
+                COALESCE(SUM(total_calories), 0) AS total_calories,
+                COALESCE(SUM(total_protein_g), 0) AS total_protein_g,
+                COALESCE(SUM(total_carbs_g), 0) AS total_carbs_g,
+                COALESCE(SUM(total_fat_g), 0) AS total_fat_g,
+                COALESCE(SUM(total_fiber_g), 0) AS total_fiber_g,
+                COUNT(*) AS meal_count
+            FROM public.nutrition_logs
+            WHERE user_id = v_pairs.user_id
+              AND DATE(created_at) = v_pairs.day
+              AND processing_status = 'completed'
+        )
+        INSERT INTO public.daily_nutrition_summaries (
+            user_id, date, total_calories, total_protein_g, total_carbs_g, total_fat_g, total_fiber_g, meal_count
+        )
+        SELECT v_pairs.user_id, v_pairs.day, a.total_calories, a.total_protein_g, a.total_carbs_g, a.total_fat_g, a.total_fiber_g, a.meal_count
+        FROM agg a
+        WHERE a.meal_count > 0
+        ON CONFLICT (user_id, date)
+        DO UPDATE SET
+            total_calories = EXCLUDED.total_calories,
+            total_protein_g = EXCLUDED.total_protein_g,
+            total_carbs_g = EXCLUDED.total_carbs_g,
+            total_fat_g = EXCLUDED.total_fat_g,
+            total_fiber_g = EXCLUDED.total_fiber_g,
+            meal_count = EXCLUDED.meal_count,
+            updated_at = NOW();
+
+        -- If no completed meals remain, remove the summary
+        IF NOT EXISTS (
+            SELECT 1 FROM public.nutrition_logs
+            WHERE user_id = v_pairs.user_id
+              AND DATE(created_at) = v_pairs.day
+              AND processing_status = 'completed'
+        ) THEN
+            DELETE FROM public.daily_nutrition_summaries
+            WHERE user_id = v_pairs.user_id AND date = v_pairs.day;
+        END IF;
+    END LOOP;
+
+    RETURN COALESCE(NEW, OLD);
+END;
 $$ LANGUAGE plpgsql;

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

🤖 Prompt for AI Agents
In supabase/migrations/007_create_daily_nutrition_summaries.sql around lines 49
to 90, update the trigger function so it recomputes summaries for both the OLD
and NEW (when present and possibly different) (user_id, date) pairs and deletes
the summary row when no completed logs remain. Implement by building a small
loop over the distinct pairs (COALESCE(NEW.user_id, OLD.user_id) /
COALESCE(NEW.created_at, OLD.created_at) and, when NEW and OLD differ, also the
OLD pair), for each pair run an aggregate query against public.nutrition_logs
filtering processing_status='completed' and DATE(created_at)=that date; if the
aggregate returns count>0 then INSERT ... ON CONFLICT ... DO UPDATE with the
computed totals and updated_at=NOW(), otherwise DELETE FROM
public.daily_nutrition_summaries WHERE user_id=... AND date=.... Ensure
duplicate pairs are deduplicated so you don’t run twice for the same
(user_id,date).

Comment on lines +7 to +11
daily_calorie_goal INTEGER DEFAULT 2000,
daily_protein_goal_g DECIMAL(6,2) DEFAULT 150,
daily_carbs_goal_g DECIMAL(6,2) DEFAULT 200,
daily_fat_goal_g DECIMAL(6,2) DEFAULT 70,
daily_fiber_goal_g DECIMAL(6,2) DEFAULT 25,
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

Consider adding CHECK constraints for positive values

The nutrition goal columns should validate that values are non-negative to prevent invalid data.

 daily_calorie_goal INTEGER DEFAULT 2000,
-daily_protein_goal_g DECIMAL(6,2) DEFAULT 150,
-daily_carbs_goal_g DECIMAL(6,2) DEFAULT 200,
-daily_fat_goal_g DECIMAL(6,2) DEFAULT 70,
-daily_fiber_goal_g DECIMAL(6,2) DEFAULT 25,
+daily_protein_goal_g DECIMAL(6,2) DEFAULT 150 CHECK (daily_protein_goal_g >= 0),
+daily_carbs_goal_g DECIMAL(6,2) DEFAULT 200 CHECK (daily_carbs_goal_g >= 0),
+daily_fat_goal_g DECIMAL(6,2) DEFAULT 70 CHECK (daily_fat_goal_g >= 0),
+daily_fiber_goal_g DECIMAL(6,2) DEFAULT 25 CHECK (daily_fiber_goal_g >= 0),

Also add for calories:

-daily_calorie_goal INTEGER DEFAULT 2000,
+daily_calorie_goal INTEGER DEFAULT 2000 CHECK (daily_calorie_goal > 0),
📝 Committable suggestion

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

Suggested change
daily_calorie_goal INTEGER DEFAULT 2000,
daily_protein_goal_g DECIMAL(6,2) DEFAULT 150,
daily_carbs_goal_g DECIMAL(6,2) DEFAULT 200,
daily_fat_goal_g DECIMAL(6,2) DEFAULT 70,
daily_fiber_goal_g DECIMAL(6,2) DEFAULT 25,
daily_calorie_goal INTEGER DEFAULT 2000 CHECK (daily_calorie_goal > 0),
daily_protein_goal_g DECIMAL(6,2) DEFAULT 150 CHECK (daily_protein_goal_g >= 0),
daily_carbs_goal_g DECIMAL(6,2) DEFAULT 200 CHECK (daily_carbs_goal_g >= 0),
daily_fat_goal_g DECIMAL(6,2) DEFAULT 70 CHECK (daily_fat_goal_g >= 0),
daily_fiber_goal_g DECIMAL(6,2) DEFAULT 25 CHECK (daily_fiber_goal_g >= 0),
🤖 Prompt for AI Agents
In supabase/migrations/008_create_user_nutrition_goals.sql around lines 7 to 11,
the numeric nutrition goal columns lack validation; add CHECK constraints so
daily_calorie_goal must be > 0 and the other goals (daily_protein_goal_g,
daily_carbs_goal_g, daily_fat_goal_g, daily_fiber_goal_g) must be >= 0 to
prevent negative values. Update the CREATE TABLE column definitions (or add
separate ALTER TABLE ... ADD CONSTRAINT statements) to include these checks and
ensure DECIMAL columns compare to 0.0 where applicable; keep default values
intact.

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.

1 participant