Parcourir la source

TG-62: Implement backend endpoints, UI dynamic updates

FerRo988 il y a 1 mois
Parent
commit
5edcbd768f

BIN
Completion/Sprint4_Completion.zip


BIN
Completion/Sprint5_Task28_Completion.zip


BIN
Completion/Sprint5_Task31_Completion.zip


BIN
Completion/Sprint5_Task32_33_Completion.zip


BIN
Completion/Sprint5_Task35_Completion.zip


BIN
Completion/Sprint6_Task28_Completion.zip


+ 1 - 1
DOCUMENTATION_CHECKLIST.md

@@ -5,7 +5,7 @@ This file tracks information that Antigravity (the AI) must provide to the Tech
 ## Technical Document Requirements
 - [ ] **Installation & Configuration:** Step-by-step guide for a clean Ubuntu 24.04 VM.
 - [ ] **Tech Stack Rationale:** Why FastAPI, SQLite/PostgreSQL, etc.
-- [ ] **LLM Selection:** Explain which local model is used (e.g., Qwen 3.5:9B), why it was chosen, and its quantization level.
+- [ ] **LLM Selection:** Explain which local model is used (e.g., Qwen 3.5:4B), why it was chosen, and its quantization level.
 - [ ] **Agent Permissions:** Explain how and why Antigravity model permissions were configured.
 - [ ] **Infrastructure Diagram:** Description/code for a diagram showing how app components communicate locally.
 - [ ] **Privacy Proof:** Explanation of how we verified that no user data leaves the server.

BIN
__pycache__/database.cpython-313.pyc


BIN
__pycache__/main.cpython-313.pyc


+ 46 - 0
database.py

@@ -159,6 +159,52 @@ def save_user_meal(user_id: int, name: str, items: List[Dict[str, Any]]) -> Opti
     finally:
         if conn: conn.close()
 
+def update_user_meal(user_id: int, meal_id: int, name: str) -> bool:
+    """Update the name of a user's saved meal, verifying ownership."""
+    conn = None
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor()
+        cursor.execute(
+            "UPDATE saved_meals SET name = ? WHERE id = ? AND user_id = ?",
+            (name, meal_id, user_id)
+        )
+        success = cursor.rowcount > 0
+        conn.commit()
+        return success
+    except Exception as e:
+        logger.error(f"Error updating user meal: {e}")
+        if conn: conn.rollback()
+        return False
+    finally:
+        if conn: conn.close()
+
+def delete_user_meal(user_id: int, meal_id: int) -> bool:
+    """Delete a user's saved meal and its items, verifying ownership."""
+    conn = None
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor()
+        # Verify ownership and get the meal ID
+        cursor.execute("SELECT id FROM saved_meals WHERE id = ? AND user_id = ?", (meal_id, user_id))
+        if not cursor.fetchone():
+            return False
+            
+        # Manually delete items first to handle missing PRAGMA foreign_keys
+        cursor.execute("DELETE FROM meal_items WHERE meal_id = ?", (meal_id,))
+        cursor.execute("DELETE FROM saved_meals WHERE id = ? AND user_id = ?", (meal_id, user_id))
+        
+        success = cursor.rowcount > 0
+        conn.commit()
+        return success
+    except Exception as e:
+        logger.error(f"Error deleting user meal: {e}")
+        if conn: conn.rollback()
+        return False
+    finally:
+        if conn: conn.close()
+
+
 
 def get_user_by_username(username: str) -> Optional[Dict[str, Any]]:
     """Retrieve user dictionary if they exist"""

+ 469 - 0
database.py.final

@@ -0,0 +1,469 @@
+import sqlite3
+import os
+import logging
+import secrets
+from datetime import datetime, timedelta
+from typing import Optional, Dict, Any, List
+
+logger = logging.getLogger(__name__)
+
+# Locate db correctly in the same directory
+DB_PATH = os.path.join(os.path.dirname(__file__), "localfood.db")
+
+def get_db_connection():
+    # Enable higher timeout and disable thread checks for FastAPI async compatibility
+    conn = sqlite3.connect(DB_PATH, timeout=30.0, check_same_thread=False)
+    conn.row_factory = sqlite3.Row
+    # Enable Write-Ahead Log (WAL) mode for simultaneous read/write operations
+    conn.execute('PRAGMA journal_mode=WAL')
+    conn.execute('PRAGMA synchronous=NORMAL')
+    return conn
+
+def create_tables():
+    """Initialize the SQLite database with required tables"""
+    conn = None
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor()
+        
+        # Create users table securely locally
+        cursor.execute('''
+        CREATE TABLE IF NOT EXISTS users (
+            id INTEGER PRIMARY KEY AUTOINCREMENT,
+            username TEXT UNIQUE NOT NULL,
+            password_hash TEXT NOT NULL,
+            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+        )
+        ''')
+
+        # Create sessions table for database-backed tokens
+        cursor.execute('''
+        CREATE TABLE IF NOT EXISTS sessions (
+            token TEXT PRIMARY KEY,
+            user_id INTEGER NOT NULL,
+            expires_at TIMESTAMP NOT NULL,
+            FOREIGN KEY (user_id) REFERENCES users (id)
+        )
+        ''')
+
+        # Create localized foods table based on Sprint 5 architecture
+        cursor.execute('''
+        CREATE TABLE IF NOT EXISTS foods (
+            id INTEGER PRIMARY KEY AUTOINCREMENT,
+            name TEXT NOT NULL,
+            category TEXT,
+            base_weight_g REAL DEFAULT 100.0,
+            calories REAL DEFAULT 0.0,
+            protein_g REAL DEFAULT 0.0,
+            fat_g REAL DEFAULT 0.0,
+            carbs_g REAL DEFAULT 0.0,
+            fiber_g REAL DEFAULT 0.0,
+            sugar_g REAL DEFAULT 0.0,
+            sodium_mg REAL DEFAULT 0.0,
+            vitamin_a_iu REAL DEFAULT 0.0,
+            vitamin_c_mg REAL DEFAULT 0.0,
+            calcium_mg REAL DEFAULT 0.0,
+            iron_mg REAL DEFAULT 0.0,
+            potassium_mg REAL DEFAULT 0.0,
+            cholesterol_mg REAL DEFAULT 0.0,
+            source TEXT DEFAULT 'System'
+        )
+        ''')
+
+        # Create chat history table for Sprint 6 persistence
+        cursor.execute('''
+        CREATE TABLE IF NOT EXISTS chat_messages (
+            id INTEGER PRIMARY KEY AUTOINCREMENT,
+            user_id INTEGER NOT NULL,
+            role TEXT NOT NULL,
+            content TEXT NOT NULL,
+            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+            FOREIGN KEY (user_id) REFERENCES users (id)
+        )
+        ''')
+        
+        # Create minimal user_profiles table for macro targets (US-07)
+        cursor.execute('''
+        CREATE TABLE IF NOT EXISTS user_profiles (
+            user_id INTEGER PRIMARY KEY,
+            target_calories INTEGER DEFAULT 2000,
+            target_protein_g INTEGER DEFAULT 150,
+            target_carbs_g INTEGER DEFAULT 200,
+            target_fat_g INTEGER DEFAULT 65,
+            FOREIGN KEY (user_id) REFERENCES users (id)
+        )
+        ''')
+
+        # Create user-named meals table for Sprint 8
+        cursor.execute('''
+        CREATE TABLE IF NOT EXISTS saved_meals (
+            id INTEGER PRIMARY KEY AUTOINCREMENT,
+            user_id INTEGER NOT NULL,
+            name TEXT NOT NULL,
+            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+            FOREIGN KEY (user_id) REFERENCES users (id)
+        )
+        ''')
+        
+        # Create meal items table to link multiple foods to a single saved meal
+        cursor.execute('''
+        CREATE TABLE IF NOT EXISTS meal_items (
+            id INTEGER PRIMARY KEY AUTOINCREMENT,
+            meal_id INTEGER NOT NULL,
+            food_id INTEGER NOT NULL,
+            amount_g REAL NOT NULL,
+            FOREIGN KEY (meal_id) REFERENCES saved_meals (id) ON DELETE CASCADE,
+            FOREIGN KEY (food_id) REFERENCES foods (id)
+        )
+        ''')
+        
+        # Create index for rapid fuzzy search compatibility
+        cursor.execute('CREATE INDEX IF NOT EXISTS idx_food_name ON foods(name COLLATE NOCASE)')
+        cursor.execute('CREATE INDEX IF NOT EXISTS idx_saved_meals_user ON saved_meals(user_id)')
+        
+        conn.commit()
+        logger.info("Database and tables initialized successfully.")
+    except Exception as e:
+        logger.error(f"Error initializing database: {e}")
+        raise
+    finally:
+        if conn:
+            conn.close()
+
+def save_user_meal(user_id: int, name: str, items: List[Dict[str, Any]]) -> Optional[int]:
+    """Persist a collection of food items as a named meal list for a user"""
+    conn = None
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor()
+        
+        # 1. Create the meal header
+        cursor.execute(
+            "INSERT INTO saved_meals (user_id, name) VALUES (?, ?)",
+            (user_id, name)
+        )
+        meal_id = cursor.lastrowid
+        
+        # 2. Add each item linked to this meal
+        for item in items:
+            cursor.execute(
+                "INSERT INTO meal_items (meal_id, food_id, amount_g) VALUES (?, ?, ?)",
+                (meal_id, item['food_id'], item['amount_g'])
+            )
+        
+        conn.commit()
+        return meal_id
+    except Exception as e:
+        logger.error(f"Error saving user meal: {e}")
+        if conn: conn.rollback()
+        return None
+    finally:
+        if conn: conn.close()
+
+def get_user_meals(user_id: int) -> List[Dict[str, Any]]:
+    """Retrieve all saved meals for a user, including total macro calculations"""
+    conn = None
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor()
+        
+        # Fetch meal headers
+        cursor.execute(
+            "SELECT * FROM saved_meals WHERE user_id = ? ORDER BY created_at DESC",
+            (user_id,)
+        )
+        meals = [dict(row) for row in cursor.fetchall()]
+        
+        # For each meal, fetch items and calculate totals
+        for meal in meals:
+            cursor.execute('''
+                SELECT mi.amount_g, f.* 
+                FROM meal_items mi
+                JOIN foods f ON mi.food_id = f.id
+                WHERE mi.meal_id = ?
+            ''', (meal['id'],))
+            items = [dict(row) for row in cursor.fetchall()]
+            
+            # Calculate totals for the meal card summary
+            meal['items'] = items
+            meal['total_calories'] = sum((item['calories'] * item['amount_g'] / 100.0) for item in items)
+            meal['total_protein'] = sum((item['protein_g'] * item['amount_g'] / 100.0) for item in items)
+            meal['total_carbs'] = sum((item['carbs_g'] * item['amount_g'] / 100.0) for item in items)
+            meal['total_fat'] = sum((item['fat_g'] * item['amount_g'] / 100.0) for item in items)
+            
+        return meals
+    except Exception as e:
+        logger.error(f"Error fetching user meals: {e}")
+        return []
+    finally:
+        if conn: conn.close()
+
+def delete_user_meal(user_id: int, meal_id: int) -> bool:
+    """Securely delete a meal and its items, ensuring ownership"""
+    conn = None
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor()
+        
+        # Verify ownership first
+        cursor.execute("SELECT id FROM saved_meals WHERE id = ? AND user_id = ?", (meal_id, user_id))
+        if not cursor.fetchone():
+            return False
+            
+        # Delete items first (even if cascading is enabled, we stay transactional)
+        cursor.execute("DELETE FROM meal_items WHERE meal_id = ?", (meal_id,))
+        # Delete header
+        cursor.execute("DELETE FROM saved_meals WHERE id = ?", (meal_id,))
+        
+        conn.commit()
+        return True
+    except Exception as e:
+        logger.error(f"Error deleting meal: {e}")
+        if conn: conn.rollback()
+        return False
+    finally:
+        if conn: conn.close()
+
+def update_user_meal(user_id: int, meal_id: int, new_name: str) -> bool:
+    """Updates the name of a meal, ensuring ownership"""
+    conn = None
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor()
+        
+        cursor.execute(
+            "UPDATE saved_meals SET name = ? WHERE id = ? AND user_id = ?",
+            (new_name, meal_id, user_id)
+        )
+        conn.commit()
+        # rowcount will be 1 if updated, 0 if ID/ownership failed
+        return cursor.rowcount > 0
+    except Exception as e:
+        logger.error(f"Error updating meal: {e}")
+        if conn: conn.rollback()
+        return False
+    finally:
+        if conn: conn.close()
+
+def get_user_by_username(username: str) -> Optional[Dict[str, Any]]:
+    """Retrieve user dictionary if they exist"""
+    conn = None
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor()
+        cursor.execute("SELECT * FROM users WHERE username = ?", (username,))
+        row = cursor.fetchone()
+        return dict(row) if row else None
+    except Exception as e:
+        logger.error(f"Database error fetching user: {e}")
+        return None
+    finally:
+        if conn: conn.close()
+
+def create_user(username: str, password_hash: str) -> Optional[int]:
+    """Creates a user securely. Returns user_id if successful, None if username exists."""
+    conn = None
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor()
+        cursor.execute(
+            "INSERT INTO users (username, password_hash) VALUES (?, ?)",
+            (username, password_hash)
+        )
+        user_id = cursor.lastrowid
+        conn.commit()
+        return user_id
+    except sqlite3.IntegrityError:
+        return None
+    except Exception as e:
+        logger.error(f"Database error during user creation: {e}")
+        raise
+    finally:
+        if conn: conn.close()
+
+def create_session(user_id: int) -> str:
+    """Create a secure 32-character session token in the DB valid for 7 days"""
+    token = secrets.token_urlsafe(32)
+    expires_at = datetime.now() + timedelta(days=7)
+    
+    conn = None
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor()
+        cursor.execute(
+            "INSERT INTO sessions (token, user_id, expires_at) VALUES (?, ?, ?)",
+            (token, user_id, expires_at)
+        )
+        conn.commit()
+        return token
+    except Exception as e:
+        logger.error(f"Error creating session: {e}")
+        raise
+    finally:
+        if conn: conn.close()
+
+def get_user_from_token(token: str) -> Optional[Dict[str, Any]]:
+    """Verify a session token and return the associated user data if valid and not expired"""
+    conn = None
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor()
+        # Find user if token exists and hasn't expired
+        cursor.execute('''
+            SELECT users.* FROM users
+            JOIN sessions ON users.id = sessions.user_id
+            WHERE sessions.token = ? AND sessions.expires_at > ?
+        ''', (token, datetime.now()))
+        row = cursor.fetchone()
+        return dict(row) if row else None
+    except Exception as e:
+        logger.error(f"Database error verifying token: {e}")
+        return None
+    finally:
+        if conn: conn.close()
+
+def delete_session(token: str):
+    """Securely remove a session token when the user logs out"""
+    conn = None
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor()
+        cursor.execute("DELETE FROM sessions WHERE token = ?", (token,))
+        conn.commit()
+    except Exception as e:
+        logger.error(f"Error deleting session: {e}")
+    finally:
+        if conn: conn.close()
+
+def search_foods_by_name(query: str, limit: int = 15) -> list[Dict[str, Any]]:
+    """Securely search for foods matching a string query with relevance-based ordering"""
+    conn = None
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor()
+        
+        # SQL Injection safe query utilizing LIKE parameterization
+        # We prioritize: 
+        # 1. Items NOT in 'Baby Foods' 
+        # 2. Shorter names (usually more fundamental ingredients)
+        # 3. Alphabetical order as a tie-breaker
+        q = f"%{query}%"
+        prefix_match = f"{query}%"
+        
+        cursor.execute('''
+            SELECT * FROM foods 
+            WHERE name LIKE ? 
+            ORDER BY 
+                CASE WHEN category = 'Baby Foods' THEN 1 ELSE 0 END,
+                CASE WHEN name LIKE ? THEN 0 ELSE 1 END,
+                LENGTH(name) ASC,
+                name ASC
+            LIMIT ?
+        ''', (q, prefix_match, limit))
+        
+        rows = cursor.fetchall()
+        return [dict(row) for row in rows]
+    except Exception as e:
+        logger.error(f"Error searching foods: {e}")
+        return []
+    finally:
+        if conn: conn.close()
+
+def save_chat_message(user_id: int, role: str, content: str):
+    """Persist a chat message to the database"""
+    conn = None
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor()
+        cursor.execute(
+            "INSERT INTO chat_messages (user_id, role, content) VALUES (?, ?, ?)",
+            (user_id, role, content)
+        )
+        conn.commit()
+    except Exception as e:
+        logger.error(f"Error saving chat message: {e}")
+    finally:
+        if conn: conn.close()
+
+def get_user_chat_history(user_id: int, limit: int = 50) -> list[Dict[str, Any]]:
+    """Retrieve the most recent chat messages for a user"""
+    conn = None
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor()
+        # Order by created_at DESC to get recent ones, then reverse for display
+        cursor.execute('''
+            SELECT role, content FROM chat_messages 
+            WHERE user_id = ? 
+            ORDER BY created_at ASC 
+            LIMIT ?
+        ''', (user_id, limit))
+        rows = cursor.fetchall()
+        return [dict(row) for row in rows]
+    except Exception as e:
+        logger.error(f"Error fetching chat history: {e}")
+        return []
+    finally:
+        if conn: conn.close()
+def get_user_profile(user_id: int) -> Optional[Dict[str, Any]]:
+    """Fetch the user's profile containing macro targets. Inserts defaults if none exists."""
+    conn = None
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor()
+        cursor.execute("SELECT * FROM user_profiles WHERE user_id = ?", (user_id,))
+        row = cursor.fetchone()
+        
+        if not row:
+            # Create a default profile row if one does not exist
+            cursor.execute('''
+                INSERT INTO user_profiles (user_id) VALUES (?)
+            ''', (user_id,))
+            conn.commit()
+            cursor.execute("SELECT * FROM user_profiles WHERE user_id = ?", (user_id,))
+            row = cursor.fetchone()
+            
+        return dict(row) if row else None
+    except Exception as e:
+        logger.error(f"Error fetching user profile: {e}")
+        return None
+    finally:
+        if conn: conn.close()
+
+def get_food_by_id(food_id: int) -> Optional[Dict[str, Any]]:
+    """Retrieve a single food item by its unique ID"""
+    conn = None
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor()
+        cursor.execute("SELECT * FROM foods WHERE id = ?", (food_id,))
+        row = cursor.fetchone()
+        return dict(row) if row else None
+    except Exception as e:
+        logger.error(f"Error fetching food by ID {food_id}: {e}")
+        return None
+    finally:
+        if conn: conn.close()
+
+def get_foods_by_ids(food_ids: List[int]) -> List[Dict[str, Any]]:
+    """Retrieve multiple food items by their unique IDs in bulk"""
+    if not food_ids:
+        return []
+        
+    conn = None
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor()
+        
+        # Create placeholders for the IN clause
+        placeholders = ', '.join(['?'] * len(food_ids))
+        query = f"SELECT * FROM foods WHERE id IN ({placeholders})"
+        
+        cursor.execute(query, food_ids)
+        rows = cursor.fetchall()
+        return [dict(row) for row in rows]
+    except Exception as e:
+        logger.error(f"Error fetching foods by IDs {food_ids}: {e}")
+        return []
+    finally:
+        if conn: conn.close()

BIN
localfood.db


+ 26 - 2
main.py

@@ -4,7 +4,7 @@ import httpx
 import bcrypt
 from contextlib import asynccontextmanager
 from fastapi import FastAPI, HTTPException, Depends, Header
-from database import create_tables, create_user, get_user_by_username, create_session, get_user_from_token, delete_session, search_foods_by_name, save_chat_message, get_user_chat_history, get_user_profile, get_food_by_id, get_foods_by_ids
+from database import create_tables, create_user, get_user_by_username, create_session, get_user_from_token, delete_session, search_foods_by_name, save_chat_message, get_user_chat_history, get_user_profile, get_food_by_id, get_foods_by_ids, save_user_meal, update_user_meal, delete_user_meal
 from fastapi.responses import HTMLResponse, StreamingResponse
 from fastapi.staticfiles import StaticFiles
 from pydantic import BaseModel
@@ -146,6 +146,9 @@ class MealSaveRequest(BaseModel):
     name: str
     items: List[MealItemInput]
 
+class MealUpdateRequest(BaseModel):
+    name: str
+
 @app.get("/", response_class=HTMLResponse)
 async def read_root():
     """Serve the chat interface HTML"""
@@ -419,6 +422,27 @@ async def save_meal(request: MealSaveRequest, current_user: dict = Depends(get_c
         
     return {"status": "success", "meal_id": meal_id, "name": request.name.strip()}
 
+@app.put("/api/meals/{meal_id}")
+async def rename_meal(meal_id: int, request: MealUpdateRequest, current_user: dict = Depends(get_current_user)):
+    """Securely rename a saved meal for the authenticated user"""
+    if not request.name.strip():
+        raise HTTPException(status_code=400, detail="Meal name cannot be empty")
+        
+    success = update_user_meal(current_user['id'], meal_id, request.name.strip())
+    if not success:
+        raise HTTPException(status_code=404, detail="Meal not found or unauthorized")
+        
+    return {"status": "success", "message": "Meal renamed"}
+
+@app.delete("/api/meals/{meal_id}")
+async def delete_meal(meal_id: int, current_user: dict = Depends(get_current_user)):
+    """Securely delete a saved meal for the authenticated user"""
+    success = delete_user_meal(current_user['id'], meal_id)
+    if not success:
+        raise HTTPException(status_code=404, detail="Meal not found or unauthorized")
+        
+    return {"status": "success", "message": "Meal deleted"}
+
 if __name__ == "__main__":
     import uvicorn
-    uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=True)
+    uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)

+ 454 - 0
main.py.final

@@ -0,0 +1,454 @@
+import json
+import logging
+import httpx
+import bcrypt
+from contextlib import asynccontextmanager
+from fastapi import FastAPI, HTTPException, Depends, Header
+from database import create_tables, create_user, get_user_by_username, create_session, get_user_from_token, delete_session, search_foods_by_name, save_chat_message, get_user_chat_history, get_user_profile, get_food_by_id, get_foods_by_ids, save_user_meal, get_user_meals, delete_user_meal
+from fastapi.responses import HTMLResponse, StreamingResponse
+from fastapi.staticfiles import StaticFiles
+from pydantic import BaseModel
+from typing import List, Generator, Optional
+
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+    create_tables()
+    yield
+
+app = FastAPI(title="LocalFoodAI Chat", lifespan=lifespan)
+
+# Use direct bcrypt for better environment compatibility
+def get_password_hash(password: str):
+    # Hash requires bytes
+    pwd_bytes = password.encode('utf-8')
+    salt = bcrypt.gensalt()
+    hashed = bcrypt.hashpw(pwd_bytes, salt)
+    return hashed.decode('utf-8')
+
+def verify_password(plain_password: str, hashed_password: str):
+    # bcrypt.checkpw handles verification
+    return bcrypt.checkpw(plain_password.encode('utf-8'), hashed_password.encode('utf-8'))
+
+class UserCreate(BaseModel):
+    username: str
+    password: str
+
+class UserLogin(BaseModel):
+    username: str
+    password: str
+
+async def get_current_user(authorization: Optional[str] = Header(None)):
+    if not authorization or not authorization.startswith("Bearer "):
+        raise HTTPException(status_code=401, detail="Authentication required")
+    
+    token = authorization.split(" ")[1]
+    user = get_user_from_token(token)
+    if not user:
+        raise HTTPException(status_code=401, detail="Invalid or expired session")
+    return user
+
+OLLAMA_URL = "http://localhost:11434/api/chat"
+MODEL_NAME = "qwen3.5:4b"
+
+# Common stopwords to strip before searching the food database
+_STOPWORDS = {
+    'how', 'many', 'much', 'calories', 'does', 'have', 'has', 'is', 'are', 
+    'in', 'the', 'a', 'an', 'of', 'for', 'with', 'what', 'tell', 'me',
+    'about', 'nutritional', 'value', 'nutrition', 'macro', 'macros',
+    'protein', 'fat', 'carbs', 'fiber', 'can', 'you', 'i', 'want', 'need',
+    'eat', 'eating', 'food', 'meal', 'diet', 'healthy', 'make', 'cook',
+    'recipe', 'per', '100g', 'gram', 'grams', 'serving'
+}
+
+def extract_food_context(messages: list) -> str | None:
+    """Scan the last user message for food keywords and enrich with local DB data."""
+    # Find the last user message
+    last_user_msg = None
+    for msg in reversed(messages):
+        role = msg.get('role', '') if isinstance(msg, dict) else msg.role
+        content = msg.get('content', '') if isinstance(msg, dict) else msg.content
+        if role == 'user':
+            last_user_msg = content
+            break
+    
+    if not last_user_msg:
+        return None
+    
+    # Extract meaningful keywords by removing stopwords
+    words = last_user_msg.lower().replace('?', '').replace(',', '').split()
+    keywords = [w for w in words if w not in _STOPWORDS and len(w) > 2]
+    
+    if not keywords:
+        return None
+    
+    # Try each keyword against the local food database, collect unique results
+    found_items = {}
+    # Optimization: Only use the first 2 most relevant keywords to keep context small on CPU
+    for kw in keywords[:2]:
+        results = search_foods_by_name(kw, limit=2)
+        for item in results:
+            # Truncate extremely long USDA names for performance
+            short_name = item['name'][:100] + ("..." if len(item['name']) > 100 else "")
+            if short_name not in found_items:
+                found_items[short_name] = item
+        if len(found_items) >= 3:
+            break
+    
+    if not found_items:
+        return None
+    
+    # Build a structured context block for the system prompt
+    lines = [
+        "[SYSTEM: NUTRITIONAL ANALYST MODE]",
+        "You are the LocalFoodAI Analyst. Use ONLY verified local data for values.",
+        "CRITICAL: Provide direct, concise answers. Skip all internal monologues, <thought> tags, or reasoning steps.",
+        "For each food discussed, you MUST follow this structure:",
+        "1. Header: ### 🥗 [Name] (per 100g)",
+        "2. Macros: A markdown table for Cal, P, F, C, Fib, Sug, Chol.",
+        "3. Micros: A bulleted list for Na, Ca, Fe, K, VitA, VitC.",
+        "4. Insight: A 1-sentence analysis of the food's nutritional profile.",
+        "Always prioritize local data over training memory. If a nutrient is missing, say 'Data not available'.",
+        ""
+    ]
+    for name, item in found_items.items():
+        # Compact, token-efficient format for the LLM
+        line = (
+            f"- {name}: {item['calories']}kcal | P:{item['protein_g']}g | F:{item['fat_g']}g | C:{item['carbs_g']}g | "
+            f"Fib:{item['fiber_g']}g | Sug:{item['sugar_g']}g | Chol:{item['cholesterol_mg']}mg | "
+            f"Na:{item['sodium_mg']}mg | Ca:{item['calcium_mg']}mg | Fe:{item['iron_mg']}mg | "
+            f"K:{item['potassium_mg']}mg | VitA:{item['vitamin_a_iu']}IU | VitC:{item['vitamin_c_mg']}mg"
+        )
+        lines.append(line)
+    
+    return "\n".join(lines)
+
+# Mount static files to serve the frontend
+app.mount("/static", StaticFiles(directory="static"), name="static")
+
+class ChatMessage(BaseModel):
+    role: str
+    content: str
+
+class ChatRequest(BaseModel):
+    messages: List[ChatMessage]
+
+class MealItemInput(BaseModel):
+    food_id: int
+    amount_g: float
+
+class MealCalculateRequest(BaseModel):
+    items: List[MealItemInput]
+
+class MealSaveRequest(BaseModel):
+    name: str
+    items: List[MealItemInput]
+
+class MealUpdateRequest(BaseModel):
+    name: str
+
+@app.get("/", response_class=HTMLResponse)
+async def read_root():
+    """Serve the chat interface HTML"""
+    try:
+        with open("static/index.html", "r", encoding="utf-8") as f:
+            return HTMLResponse(content=f.read())
+    except FileNotFoundError:
+        return HTMLResponse(content="<h1>Welcome to LocalFoodAI</h1><p>static/index.html not found. Please create the frontend.</p>")
+
+@app.post("/api/register")
+async def register_user(user: UserCreate):
+    if len(user.username.strip()) < 3:
+        raise HTTPException(status_code=400, detail="Username must be at least 3 characters")
+    if len(user.password.strip()) < 6:
+        raise HTTPException(status_code=400, detail="Password must be at least 6 characters")
+    
+    hashed_password = get_password_hash(user.password)
+    user_id = create_user(user.username.strip(), hashed_password)
+    if not user_id:
+        raise HTTPException(status_code=400, detail="Username already exists")
+    
+    # Auto-login after registration
+    token = create_session(user_id)
+    return {"message": "User registered successfully", "token": token, "username": user.username.strip()}
+
+@app.post("/api/login")
+async def login_user(user: UserLogin):
+    db_user = get_user_by_username(user.username.strip())
+    if not db_user:
+        raise HTTPException(status_code=401, detail="Invalid username or password")
+    
+    if not verify_password(user.password, db_user["password_hash"]):
+        raise HTTPException(status_code=401, detail="Invalid username or password")
+    
+    token = create_session(db_user["id"])
+    return {"status": "success", "username": db_user["username"], "token": token}
+
+@app.post("/api/logout")
+async def logout(authorization: Optional[str] = Header(None)):
+    if authorization and authorization.startswith("Bearer "):
+        token = authorization.split(" ")[1]
+        delete_session(token)
+    return {"message": "Logged out successfully"}
+
+@app.get("/api/macros/targets")
+async def get_macro_targets(current_user: dict = Depends(get_current_user)):
+    """API endpoint to securely fetch the user's current macronutrient targets"""
+    profile = get_user_profile(current_user['id'])
+    
+    if not profile:
+        # Fallback to defaults in case database insertion failed
+        return {
+            "calories": 2000,
+            "protein_g": 150,
+            "carbs_g": 200,
+            "fat_g": 65
+        }
+        
+    return {
+        "calories": profile.get("target_calories", 2000),
+        "protein_g": profile.get("target_protein_g", 150),
+        "carbs_g": profile.get("target_carbs_g", 200),
+        "fat_g": profile.get("target_fat_g", 65)
+    }
+
+@app.post("/chat")
+async def chat_endpoint(request: ChatRequest, current_user: dict = Depends(get_current_user)):
+    """Proxy chat requests to the local Ollama instance with streaming support.
+    Automatically enriches prompts with verified local SQLite nutritional data.
+    """
+    # Keep only the last 6 messages for context window performance on CPU
+    all_messages = [msg.model_dump() for msg in request.messages]
+    messages = all_messages[-6:]
+    
+    # Save the latest user message to DB
+    if messages and messages[-1]['role'] == 'user':
+        save_chat_message(current_user['id'], 'user', messages[-1]['content'])
+    
+    # --- TG-35: Local SQL RAG Enrichment ---
+    db_context = extract_food_context(messages)
+    if db_context:
+        # Prepend as a system message so it acts as grounded knowledge
+        # We ensure it's a short, concise instruction to prevent context bloat
+        messages = [{"role": "system", "content": db_context}] + messages
+
+    logger.info(f"[Chat] User '{current_user['username']}' is chatting. Context items: {'Yes' if db_context else 'No'}. Message count: {len(messages)}")
+    
+    payload = {
+        "model": MODEL_NAME,
+        "messages": messages,
+        "stream": True,
+        "think": False  # Disable reasoning/thinking mode for faster responses
+    }
+    
+    async def generate_response():
+        try:
+            bot_full_response = ""
+            async with httpx.AsyncClient(timeout=300.0) as client:
+                # Use a combined timeout for the entire request
+                async with client.stream("POST", OLLAMA_URL, json=payload, timeout=300.0) as response:
+                    if response.status_code != 200:
+                        error_detail = await response.aread()
+                        logger.error(f"Ollama returned error {response.status_code}: {error_detail}")
+                        yield f"data: {json.dumps({'error': f'LLM Error ({response.status_code})'})}\n\n"
+                        return
+
+                    async for line in response.aiter_lines():
+                        if line:
+                            try:
+                                data = json.loads(line)
+                                if "message" in data and "content" in data["message"]:
+                                    content = data["message"]["content"]
+                                    bot_full_response += content
+                                    yield f"data: {json.dumps({'content': content})}\n\n"
+                                if data.get("done"):
+                                    break
+                            except json.JSONDecodeError:
+                                continue
+            
+            # Save final bot response to DB
+            if bot_full_response.strip():
+                save_chat_message(current_user['id'], 'assistant', bot_full_response)
+                
+        except Exception as e:
+            logger.exception(f"Unexpected error during chat stream: {e}")
+            yield f"data: {json.dumps({'error': 'A technical error occurred while generating the response.'})}\n\n"
+
+    return StreamingResponse(generate_response(), media_type="text/event-stream")
+
+@app.get("/api/chat/history")
+async def get_history(current_user: dict = Depends(get_current_user)):
+    """Fetch the chat history for the authenticated user"""
+    history = get_user_chat_history(current_user['id'])
+    return {"history": history}
+
+@app.get("/api/food/search")
+async def search_food(q: str, current_user: dict = Depends(get_current_user)):
+    """API endpoint to search for food items securely using token authentication"""
+    if not q or len(q.strip()) < 1:
+        return {"results": []}
+    
+    logger.info(f"User {current_user['username']} searched for [{q}]")
+    results = search_foods_by_name(q.strip(), limit=15)
+    return {"results": results}
+
+@app.get("/api/food/{food_id}")
+async def get_food_detail(food_id: int, current_user: dict = Depends(get_current_user)):
+    """API endpoint to fetch structured nutritional details for a specific food item"""
+    food = get_food_by_id(food_id)
+    if not food:
+        raise HTTPException(status_code=404, detail="Food item not found")
+    
+    # Structure the data as proposed in the implementation plan
+    structured_data = {
+        "id": food["id"],
+        "name": food["name"],
+        "category": food["category"],
+        "base_weight_g": food["base_weight_g"],
+        "macros": {
+            "calories": food["calories"],
+            "protein_g": food["protein_g"],
+            "fat_g": food["fat_g"],
+            "carbs_g": food["carbs_g"]
+        },
+        "extended": {
+            "fiber_g": food["fiber_g"],
+            "sugar_g": food["sugar_g"],
+            "cholesterol_mg": food["cholesterol_mg"]
+        },
+        "vitamins": {
+            "vitamin_a_iu": food["vitamin_a_iu"],
+            "vitamin_c_mg": food["vitamin_c_mg"]
+        },
+        "minerals": {
+            "calcium_mg": food["calcium_mg"],
+            "iron_mg": food["iron_mg"],
+            "potassium_mg": food["potassium_mg"],
+            "sodium_mg": food["sodium_mg"]
+        },
+        "source": food["source"]
+    }
+    
+    return structured_data
+
+@app.post("/api/meal/calculate")
+async def calculate_meal(request: MealCalculateRequest, current_user: dict = Depends(get_current_user)):
+    """Calculate the total nutritional value for a combined list of foods and their custom weights."""
+    if not request.items:
+        return {"error": "Meal is empty"}
+        
+    # Validation: Cast to floats and ensure > 0
+    items = []
+    for item in request.items:
+        try:
+            amount = float(item.amount_g)
+            if amount <= 0:
+                raise HTTPException(status_code=400, detail="Quantity must be greater than 0g for all items.")
+            items.append({"food_id": item.food_id, "amount_g": amount})
+        except ValueError:
+            raise HTTPException(status_code=400, detail=f"Invalid amount for food ID {item.food_id}")
+            
+    # Bulk fetch from DB
+    requested_ids = list(set(item["food_id"] for item in items))
+    foods_data = get_foods_by_ids(requested_ids)
+    
+    # Map for easy lookup
+    foods_map = {food["id"]: food for food in foods_data}
+    
+    # Fail-fast: Check if all requested IDs exist
+    found_ids = set(foods_map.keys())
+    missing_ids = [fid for fid in requested_ids if fid not in found_ids]
+    if missing_ids:
+        raise HTTPException(status_code=400, detail=f"Invalid food IDs provided: {missing_ids}")
+    
+    # Initialize aggregator
+    totals = {
+        "total_weight_g": 0.0,
+        "macros": {"calories": 0.0, "protein_g": 0.0, "fat_g": 0.0, "carbs_g": 0.0},
+        "extended": {"fiber_g": 0.0, "sugar_g": 0.0, "cholesterol_mg": 0.0},
+        "vitamins": {"vitamin_a_iu": 0.0, "vitamin_c_mg": 0.0},
+        "minerals": {"calcium_mg": 0.0, "iron_mg": 0.0, "potassium_mg": 0.0, "sodium_mg": 0.0}
+    }
+    
+    def safe_val(val):
+        return float(val) if val is not None else 0.0
+        
+    for item in items:
+        food = foods_map[item["food_id"]]
+        
+        ratio = item["amount_g"] / 100.0
+        totals["total_weight_g"] += item["amount_g"]
+        
+        totals["macros"]["calories"] += safe_val(food.get("calories")) * ratio
+        totals["macros"]["protein_g"] += safe_val(food.get("protein_g")) * ratio
+        totals["macros"]["fat_g"] += safe_val(food.get("fat_g")) * ratio
+        totals["macros"]["carbs_g"] += safe_val(food.get("carbs_g")) * ratio
+        
+        totals["extended"]["fiber_g"] += safe_val(food.get("fiber_g")) * ratio
+        totals["extended"]["sugar_g"] += safe_val(food.get("sugar_g")) * ratio
+        totals["extended"]["cholesterol_mg"] += safe_val(food.get("cholesterol_mg")) * ratio
+        
+        totals["vitamins"]["vitamin_a_iu"] += safe_val(food.get("vitamin_a_iu")) * ratio
+        totals["vitamins"]["vitamin_c_mg"] += safe_val(food.get("vitamin_c_mg")) * ratio
+        
+        totals["minerals"]["calcium_mg"] += safe_val(food.get("calcium_mg")) * ratio
+        totals["minerals"]["iron_mg"] += safe_val(food.get("iron_mg")) * ratio
+        totals["minerals"]["potassium_mg"] += safe_val(food.get("potassium_mg")) * ratio
+        totals["minerals"]["sodium_mg"] += safe_val(food.get("sodium_mg")) * ratio
+        
+    # Rounding to 2 decimal places
+    totals["total_weight_g"] = round(totals["total_weight_g"], 2)
+    for category in ["macros", "extended", "vitamins", "minerals"]:
+        for key in totals[category]:
+            totals[category][key] = round(totals[category][key], 2)
+            
+    return totals
+
+@app.post("/api/meals")
+async def save_meal(request: MealSaveRequest, current_user: dict = Depends(get_current_user)):
+    """Securely save a named meal list for the authenticated user"""
+    if not request.name.strip():
+        raise HTTPException(status_code=400, detail="Meal name cannot be empty")
+    if not request.items:
+        raise HTTPException(status_code=400, detail="Meal items cannot be empty")
+        
+    items_list = [item.model_dump() for item in request.items]
+    meal_id = save_user_meal(current_user['id'], request.name.strip(), items_list)
+    
+    if meal_id is None:
+        raise HTTPException(status_code=500, detail="Failed to save meal to database")
+        
+    return {"status": "success", "meal_id": meal_id, "name": request.name.strip()}
+
+@app.get("/api/meals")
+async def list_meals(current_user: dict = Depends(get_current_user)):
+    """Retrieve all saved meals for the authenticated user"""
+    meals = get_user_meals(current_user['id'])
+    return {"meals": meals}
+
+@app.put("/api/meals/{meal_id}")
+async def update_meal(meal_id: int, request: MealUpdateRequest, current_user: dict = Depends(get_current_user)):
+    """Securely rename a meal after verifying ownership"""
+    if not request.name.strip():
+        raise HTTPException(status_code=400, detail="Meal name cannot be empty")
+        
+    success = update_user_meal(current_user['id'], meal_id, request.name.strip())
+    if not success:
+        raise HTTPException(status_code=404, detail="Meal not found or not owned by user")
+        
+    return {"status": "success", "message": "Meal renamed successfully"}
+
+@app.delete("/api/meals/{meal_id}")
+async def delete_meal(meal_id: int, current_user: dict = Depends(get_current_user)):
+    """Securely delete a specific meal after verifying ownership"""
+    success = delete_user_meal(current_user['id'], meal_id)
+    if not success:
+        raise HTTPException(status_code=404, detail="Meal not found or not owned by user")
+        
+    return {"status": "success", "message": "Meal deleted successfully"}
+
+if __name__ == "__main__":
+    import uvicorn
+    uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=True)

+ 17 - 0
profile_db.py

@@ -0,0 +1,17 @@
+import database
+import time
+import sys
+
+def profile_search(query):
+    start = time.time()
+    results = database.search_foods_by_name(query)
+    end = time.time()
+    print(f"Query: '{query}'")
+    print(f"Results found: {len(results)}")
+    print(f"Time taken: {end - start:.4f} seconds")
+    if results:
+        print(f"First result: {results[0]['name']}")
+
+if __name__ == "__main__":
+    q = sys.argv[1] if len(sys.argv) > 1 else "chicken"
+    profile_search(q)

+ 33 - 0
scratch/check_db.py

@@ -0,0 +1,33 @@
+import sqlite3
+import os
+
+DB_PATH = "localfood.db"
+
+def check_foods():
+    if not os.path.exists(DB_PATH):
+        print(f"Database {DB_PATH} not found.")
+        return
+
+    conn = sqlite3.connect(DB_PATH)
+    cursor = conn.cursor()
+    
+    # Check total count
+    cursor.execute("SELECT COUNT(*) FROM foods")
+    count = cursor.fetchone()[0]
+    print(f"Total foods in DB: {count}")
+
+    # Check for eggs and bacon
+    print("\nSearching for 'egg':")
+    cursor.execute("SELECT name, category, source FROM foods WHERE name LIKE '%egg%' LIMIT 5")
+    for row in cursor.fetchall():
+        print(row)
+
+    print("\nSearching for 'bacon':")
+    cursor.execute("SELECT name, category, source FROM foods WHERE name LIKE '%bacon%' LIMIT 5")
+    for row in cursor.fetchall():
+        print(row)
+
+    conn.close()
+
+if __name__ == "__main__":
+    check_foods()

+ 16 - 0
scratch/force_db.py

@@ -0,0 +1,16 @@
+import sys
+import os
+sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
+from database import create_tables
+
+if __name__ == "__main__":
+    print("Attempting to create tables...")
+    create_tables()
+    if os.path.exists("localfood.db"):
+        print("SUCCESS: localfood.db created.")
+        print(f"Size: {os.path.getsize('localfood.db')} bytes")
+    else:
+        print("FAILURE: localfood.db not found in current directory.")
+        # Check absolute path
+        abs_path = os.path.abspath("localfood.db")
+        print(f"Looked at: {abs_path}")

+ 36 - 0
scratch/remote_ls.py

@@ -0,0 +1,36 @@
+import paramiko
+import sys
+
+def run_remote_command(hostname, username, password, command):
+    try:
+        client = paramiko.SSHClient()
+        client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+        client.connect(hostname, username=username, password=password)
+        
+        stdin, stdout, stderr = client.exec_command(command)
+        output = stdout.read().decode()
+        error = stderr.read().decode()
+        
+        client.close()
+        return output, error
+    except Exception as e:
+        return None, str(e)
+
+if __name__ == "__main__":
+    host = "192.168.130.171"
+    user = "roni"
+    pw = "BTSai123"
+    cmd = "ollama --version && echo '---' && curl -s http://localhost:11434/api/version"
+    
+    print(f"Connecting to {host}...")
+    out, err = run_remote_command(host, user, pw, cmd)
+    
+    if out is not None:
+        with open("scratch/remote_output.txt", "w", encoding="utf-8") as f:
+            f.write(out)
+        print("Output written to scratch/remote_output.txt")
+        if err:
+            print("--- Error ---")
+            print(err)
+    else:
+        print(f"SSH Error: {err}")

+ 3 - 0
scratch/remote_output.txt

@@ -0,0 +1,3 @@
+ollama version is 0.18.3
+---
+{"version":"0.18.3"}

+ 13 - 0
scratch/test_search.py

@@ -0,0 +1,13 @@
+import sys
+import os
+sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
+from database import search_foods_by_name
+
+if __name__ == "__main__":
+    for query in ["egg", "bacon", "salmon", "beef"]:
+        results = search_foods_by_name(query)
+        print(f"Results for '{query}':")
+        for r in results:
+            print(f" - {r['name']} (Category: {r['category']})")
+        if not results:
+            print(" - NO RESULTS")

+ 224 - 0
static/index.html.final

@@ -0,0 +1,224 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>LocalFoodAI Chat</title>
+    <meta name="description" content="LocalFoodAI Assistant for Nutritional Information and Menu Proposals">
+    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
+    <link rel="stylesheet" href="/static/style.css">
+</head>
+<body>
+    <div class="app-container" id="chat-app" style="display: none;">
+        <header class="chat-header">
+            <div class="brand">
+                <div class="logo">🍳</div>
+                <div>
+                    <h1>LocalFoodAI</h1>
+                    <span class="status-indicator" id="status-dot"></span><span class="status-text" id="status-text">Local LLM Ready</span>
+                </div>
+            </div>
+            <div class="actions">
+                <span id="user-greeting" style="margin-right:15px; font-size: 0.9rem; color: var(--text-muted);"></span>
+                <button id="nav-dashboard-btn" class="nav-btn">Dashboard</button>
+                <button id="nav-logout-btn" class="nav-btn">Logout</button>
+                <button id="clear-chat" title="Clear Chat">
+                    <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"></path></svg>
+                </button>
+            </div>
+        </header>
+        
+        <!-- New Visual Food Search Component -->
+        <div class="search-module" id="food-search-module">
+            <div class="search-input-wrapper">
+                <svg class="search-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
+                <input type="text" id="food-search-input" placeholder="Search for standard foods or raw ingredients..." autocomplete="off">
+                <button id="clear-search-btn" style="display: none;" title="Clear search">
+                    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
+                </button>
+            </div>
+            <div class="search-results-dropdown" id="search-results-dropdown" style="display: none;">
+                <!-- Dynamically populated by JS fetch -->
+            </div>
+        </div>
+        
+        <!-- Meal Builder Component (US-10 Task #46) -->
+        <div class="meal-builder-tray" id="meal-builder">
+            <div class="meal-builder-header">
+                <div class="tray-title">
+                    <span class="icon">🍽️</span>
+                    <h3>Meal Builder</h3>
+                    <span class="item-count" id="meal-item-count">0 items</span>
+                </div>
+                <button id="toggle-meal-btn" class="icon-btn" title="Toggle Meal Builder">
+                    <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 9l-7 7-7-7"></path></svg>
+                </button>
+            </div>
+            <div class="meal-content collapsed" id="meal-content">
+                <div class="meal-items-list" id="meal-items-list">
+                    <!-- Dynamic food rows added here -->
+                    <div class="empty-meal-msg" id="empty-meal-msg">No foods added yet. Use the search + button to build a meal.</div>
+                </div>
+                <div class="meal-builder-footer" id="meal-builder-footer" style="display: none;">
+                    <div class="meal-totals-banner" id="meal-totals-banner">
+                        <div class="totals-header">
+                            <span class="totals-title">Total Macros</span>
+                            <span class="totals-weight" id="meal-total-weight">0g</span>
+                        </div>
+                        <div class="totals-grid">
+                            <div class="total-card">
+                                <span class="total-val" id="meal-total-cal">0</span>
+                                <span class="total-lbl">kcal</span>
+                            </div>
+                            <div class="total-card protein-highlight">
+                                <span class="total-val" id="meal-total-pro">0</span>
+                                <span class="total-lbl">Protein</span>
+                            </div>
+                            <div class="total-card">
+                                <span class="total-val" id="meal-total-fat">0</span>
+                                <span class="total-lbl">Fat</span>
+                            </div>
+                            <div class="total-card">
+                                <span class="total-val" id="meal-total-carb">0</span>
+                                <span class="total-lbl">Carbs</span>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="meal-builder-actions" style="display: flex; gap: 10px; margin-top: 15px;">
+                        <button id="generate-recipe-btn" class="primary-btn-sm" style="flex: 1;">🍲 Generate Recipe Prompt</button>
+                        <button id="open-save-modal-btn" class="secondary-btn-sm" title="Save this meal list">💾 Save Meal</button>
+                    </div>
+                </div>
+            </div>
+        </div>
+        
+        <!-- Macro Dashboard UI (US-07 Task #38) -->
+        <div class="macro-dashboard" id="macro-dashboard" style="display: none;">
+            <div class="macro-card">
+                <div class="macro-label">Calories</div>
+                <div class="macro-value" id="macro-calories">-- kcal</div>
+            </div>
+            <div class="macro-card">
+                <div class="macro-label">Protein</div>
+                <div class="macro-value" id="macro-protein">-- g</div>
+            </div>
+            <div class="macro-card">
+                <div class="macro-label">Carbs</div>
+                <div class="macro-value" id="macro-carbs">-- g</div>
+            </div>
+            <div class="macro-card">
+                <div class="macro-label">Fat</div>
+                <div class="macro-value" id="macro-fat">-- g</div>
+            </div>
+        </div>
+        
+        <main class="chat-container" id="chat-container">
+            <div class="message system">
+                <div class="avatar">🤖</div>
+                <div class="message-content">
+                    <p>Hello! I am LocalFoodAI, your completely local nutrition and menu assistant. How can I help you today?</p>
+                </div>
+            </div>
+        </main>
+
+        <footer class="chat-input-area">
+            <form id="chat-form" class="input-form">
+                <textarea id="user-input" placeholder="Ask about recipes, nutrition, menus..." rows="1" required></textarea>
+                <button type="submit" id="send-btn" class="send-btn" aria-label="Send message">
+                    <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>
+                </button>
+            </form>
+            <div class="footer-note">Powered by Qwen 3.5:4B running locally on Ubuntu 24.04 via Ollama</div>
+        </footer>
+
+        <!-- Save Meal Modal (Sprint 8) -->
+        <div class="modal" id="save-meal-modal" style="display: none;">
+            <div class="modal-content">
+                <div class="modal-header">
+                    <h3>💾 Save Meal Combination</h3>
+                    <button id="close-save-modal-btn" class="close-btn">&times;</button>
+                </div>
+                <div class="modal-body">
+                    <p>Give your meal a name to save it to your personal dashboard.</p>
+                    <div class="input-group" style="margin-top: 15px;">
+                        <label for="meal-name-input">Meal Name</label>
+                        <input type="text" id="meal-name-input" placeholder="e.g. Healthy Salmon Dinner" maxlength="50">
+                    </div>
+                    <div id="save-meal-error" class="error-text"></div>
+                </div>
+                <div class="modal-footer">
+                    <button id="cancel-save-btn" class="secondary-btn">Cancel</button>
+                    <button id="confirm-save-btn" class="primary-btn">Save to Dashboard</button>
+                </div>
+            </div>
+        </div>
+        <!-- Saved Meals Dashboard Overlay (Sprint 8) -->
+        <div class="dashboard-overlay" id="dashboard-overlay" style="display: none;">
+            <div class="dashboard-content">
+                <div class="dashboard-header">
+                    <div class="dashboard-title-group">
+                        <span class="icon">📁</span>
+                        <h2>My Saved Meals</h2>
+                    </div>
+                    <button id="close-dashboard-btn" class="close-btn">&times;</button>
+                </div>
+                <div class="dashboard-body">
+                    <div class="dashboard-grid" id="dashboard-grid">
+                        <!-- Meal cards injected here -->
+                        <div class="search-loading">Loading your meals...</div>
+                    </div>
+                    <div id="dashboard-empty-msg" style="display: none;" class="empty-meal-msg">
+                        You haven't saved any meals yet. Build one in the chat and click "Save Meal"!
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+    
+    <!-- Authentication Gateway -->
+    <div class="auth-container" id="auth-screen">
+        <div class="auth-header">
+            <div class="logo" style="margin-bottom: 10px;">🍳</div>
+            <h2>Welcome to LocalFoodAI</h2>
+            <p>Please log in or create an account to continue.</p>
+        </div>
+
+        <!-- Login Form -->
+        <form id="login-form">
+            <div class="input-group">
+                <label for="login-username">Username</label>
+                <input type="text" id="login-username" required>
+            </div>
+            <div class="input-group">
+                <label for="login-password">Password</label>
+                <input type="password" id="login-password" required>
+            </div>
+            <div id="login-error" class="error-text"></div>
+            <button type="submit" class="primary-btn" id="login-submit-btn">Login</button>
+            <p class="auth-toggle">Don't have an account? <a href="#" id="show-register">Register here</a></p>
+        </form>
+
+        <!-- Registration Form (Hidden by default) -->
+        <form id="register-form" style="display: none;">
+            <div class="input-group">
+                <label for="reg-username">Username</label>
+                <input type="text" id="reg-username" required minlength="3">
+            </div>
+            <div class="input-group">
+                <label for="reg-password">Password</label>
+                <input type="password" id="reg-password" required minlength="6">
+            </div>
+            <div class="input-group">
+                <label for="reg-confirm">Confirm Password</label>
+                <input type="password" id="reg-confirm" required minlength="6">
+            </div>
+            <div id="reg-error" class="error-text"></div>
+            <div id="reg-success" class="success-text"></div>
+            <button type="submit" class="primary-btn" id="reg-submit-btn">Register</button>
+            <p class="auth-toggle">Already have an account? <a href="#" id="show-login">Login here</a></p>
+        </form>
+    </div>
+
+    <script src="/static/script.js"></script>
+</body>
+</html>

+ 19 - 2
static/script.js

@@ -1015,6 +1015,7 @@ document.addEventListener('DOMContentLoaded', () => {
         meals.forEach((meal, index) => {
             const card = document.createElement('div');
             card.className = 'meal-card';
+            card.setAttribute('data-meal-id', meal.id);
             card.style.animationDelay = `${index * 0.07}s`;
 
             const dateStr = new Date(meal.created_at).toLocaleDateString(undefined, {
@@ -1087,7 +1088,12 @@ document.addEventListener('DOMContentLoaded', () => {
             });
 
             if (response.ok) {
-                loadSavedMeals(); // Refresh
+                const card = document.querySelector(`.meal-card[data-meal-id="${id}"]`);
+                if (card) {
+                    card.querySelector('.meal-card-name').textContent = newName;
+                    const editBtn = card.querySelector('.edit-meal-btn');
+                    if (editBtn) editBtn.dataset.name = newName;
+                }
             } else {
                 alert('Failed to rename meal.');
             }
@@ -1105,7 +1111,18 @@ document.addEventListener('DOMContentLoaded', () => {
             });
 
             if (response.ok) {
-                loadSavedMeals(); // Refresh
+                const card = document.querySelector(`.meal-card[data-meal-id="${id}"]`);
+                if (card) {
+                    card.style.transition = 'all 0.3s ease';
+                    card.style.opacity = '0';
+                    card.style.transform = 'scale(0.9)';
+                    setTimeout(() => {
+                        card.remove();
+                        if (document.querySelectorAll('.meal-card').length === 0) {
+                            dashboardEmptyMsg.style.display = 'block';
+                        }
+                    }, 300);
+                }
             } else {
                 alert('Failed to delete meal.');
             }

+ 1045 - 0
static/script.js.final

@@ -0,0 +1,1045 @@
+document.addEventListener('DOMContentLoaded', () => {
+    const chatForm = document.getElementById('chat-form');
+    const userInput = document.getElementById('user-input');
+    const chatContainer = document.getElementById('chat-container');
+    const sendBtn = document.getElementById('send-btn');
+    const clearChatBtn = document.getElementById('clear-chat');
+
+    let chatHistory = []; // Store conversation history
+
+    // Auto-resize textarea
+    userInput.addEventListener('input', function() {
+        this.style.height = 'auto';
+        this.style.height = (this.scrollHeight > 150 ? 150 : this.scrollHeight) + 'px';
+        if (this.value.trim() === '') {
+            sendBtn.disabled = true;
+        } else {
+            sendBtn.disabled = false;
+        }
+    });
+
+    // Handle Enter key (Shift+Enter for new line)
+    userInput.addEventListener('keydown', function(e) {
+        if (e.key === 'Enter' && !e.shiftKey) {
+            e.preventDefault();
+            if (!sendBtn.disabled) {
+                chatForm.requestSubmit();
+            }
+        }
+    });
+
+    clearChatBtn.addEventListener('click', () => {
+        if (confirm('Are you sure you want to clear the chat history?')) {
+            chatHistory = [];
+            chatContainer.innerHTML = '';
+            addMessage('system', 'Hello! I am LocalFoodAI, your completely local nutrition and menu assistant. How can I help you today?');
+        }
+    });
+
+    chatForm.addEventListener('submit', async (e) => {
+        e.preventDefault();
+        const message = userInput.value.trim();
+        if (!message) return;
+
+        // Reset input
+        userInput.value = '';
+        userInput.style.height = 'auto';
+        sendBtn.disabled = true;
+
+        // Add user message to UI
+        addMessage('user', message);
+        chatHistory.push({ role: 'user', content: message });
+
+        // Add loading indicator
+        const loadingId = addTypingIndicator();
+
+        try {
+            const token = localStorage.getItem('localFoodToken');
+            // Fetch response from backend
+            const response = await fetch('/chat', {
+                method: 'POST',
+                headers: { 
+                    'Content-Type': 'application/json',
+                    'Authorization': `Bearer ${token}`
+                },
+                body: JSON.stringify({ messages: chatHistory })
+            });
+
+            if (response.status === 401) {
+                setLoggedOutState();
+                addMessage('system', 'Your session has expired. Please log in again.');
+                return;
+            }
+
+            if (!response.ok) {
+                throw new Error(`HTTP error! status: ${response.status}`);
+            }
+
+            // Remove loading indicator
+            removeElement(loadingId);
+
+            // Create new bot message container
+            const botMessageId = 'msg-' + Date.now();
+            const botContentEl = addMessage('system', '', botMessageId);
+
+            let botFullResponse = '';
+
+            // Handle Server-Sent Events (Streaming)
+            const reader = response.body.getReader();
+            const decoder = new TextDecoder('utf-8');
+            let done = false;
+
+            while (!done) {
+                const { value, done: readerDone } = await reader.read();
+                done = readerDone;
+                if (value) {
+                    const chunk = decoder.decode(value, { stream: true });
+                    // Split the chunk by double newline to get individual SSE messages
+                    const lines = chunk.split('\n\n');
+                    for (const line of lines) {
+                        if (line.startsWith('data: ')) {
+                            const dataStr = line.substring(6);
+                            if (dataStr.trim() === '') continue;
+                            try {
+                                const data = JSON.parse(dataStr);
+                                if (data.error) {
+                                    botContentEl.innerHTML += `<br><span style="color:#f85149">Error: ${data.error}</span>`;
+                                } else if (data.content !== undefined) {
+                                    botFullResponse += data.content;
+                                    // Basic text to HTML conversion
+                                    botContentEl.innerHTML = formatText(botFullResponse);
+                                    chatContainer.scrollTop = chatContainer.scrollHeight;
+                                }
+                            } catch (err) {
+                                console.error('Error parsing SSE data:', err, dataStr);
+                            }
+                        }
+                    }
+                }
+            }
+
+            // Save bot response to history once complete
+            chatHistory.push({ role: 'assistant', content: botFullResponse });
+
+        } catch (error) {
+            console.error('Chat error:', error);
+            removeElement(loadingId);
+            addMessage('system', 'Sorry, there was an error communicating with the local LLM. Make sure the server and Ollama are running.');
+        } finally {
+            sendBtn.disabled = false;
+            userInput.focus();
+        }
+    });
+
+    function addMessage(role, content, id = null) {
+        const msgDiv = document.createElement('div');
+        msgDiv.className = `message ${role}`;
+        if (id) msgDiv.id = id;
+
+        const avatarDiv = document.createElement('div');
+        avatarDiv.className = 'avatar';
+        avatarDiv.textContent = role === 'user' ? '👤' : '🤖';
+
+        const contentDiv = document.createElement('div');
+        contentDiv.className = 'message-content';
+        contentDiv.innerHTML = formatText(content);
+
+        msgDiv.appendChild(avatarDiv);
+        msgDiv.appendChild(contentDiv);
+        chatContainer.appendChild(msgDiv);
+        chatContainer.scrollTop = chatContainer.scrollHeight;
+
+        return contentDiv;
+    }
+
+    function addTypingIndicator() {
+        const id = 'typing-' + Date.now();
+        const msgDiv = document.createElement('div');
+        msgDiv.className = 'message system';
+        msgDiv.id = id;
+
+        const avatarDiv = document.createElement('div');
+        avatarDiv.className = 'avatar';
+        avatarDiv.textContent = '🤖';
+
+        const contentDiv = document.createElement('div');
+        contentDiv.className = 'message-content typing-indicator';
+        contentDiv.innerHTML = `
+            <div class="typing-dot"></div>
+            <div class="typing-dot"></div>
+            <div class="typing-dot"></div>
+        `;
+
+        msgDiv.appendChild(avatarDiv);
+        msgDiv.appendChild(contentDiv);
+        chatContainer.appendChild(msgDiv);
+        chatContainer.scrollTop = chatContainer.scrollHeight;
+
+        return id;
+    }
+
+    function removeElement(id) {
+        const el = document.getElementById(id);
+        if (el) el.remove();
+    }
+
+    function formatText(text) {
+        if (!text) return '';
+        // Very basic markdown parsing for bold, italics, code, and newlines
+        let formatted = text
+            .replace(/&/g, "&amp;")
+            .replace(/</g, "&lt;")
+            .replace(/>/g, "&gt;")
+            .replace(/\n/g, "<br>")
+            .replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>") // bold
+            .replace(/\*(.*?)\*/g, "<em>$1</em>") // italic
+            .replace(/`(.*?)`/g, "<code style='background:rgba(255,255,255,0.1);padding:2px 4px;border-radius:4px'>$1</code>"); // inline code
+        return formatted;
+    }
+
+    // Authentication & Session Logic
+    const authScreen = document.getElementById('auth-screen');
+    const chatApp = document.getElementById('chat-app');
+    
+    const loginForm = document.getElementById('login-form');
+    const registerForm = document.getElementById('register-form');
+    
+    const showRegisterLink = document.getElementById('show-register');
+    const showLoginLink = document.getElementById('show-login');
+    
+    const loginError = document.getElementById('login-error');
+    const regError = document.getElementById('reg-error');
+    const regSuccess = document.getElementById('reg-success');
+    
+    const logoutBtn = document.getElementById('nav-logout-btn');
+    const userGreeting = document.getElementById('user-greeting');
+
+    // Check session on load
+    const savedUser = localStorage.getItem('localFoodUser');
+    const savedToken = localStorage.getItem('localFoodToken');
+    if (savedUser && savedToken) {
+        setLoggedInState(savedUser, savedToken);
+    }
+
+    async function setLoggedInState(username, token) {
+        localStorage.setItem('localFoodUser', username);
+        localStorage.setItem('localFoodToken', token);
+        userGreeting.textContent = `Welcome, ${username}`;
+        
+        authScreen.classList.add('fade-out');
+        setTimeout(() => {
+            authScreen.style.display = 'none';
+            chatApp.style.display = 'flex';
+            chatApp.classList.add('fade-in');
+            userInput.focus();
+        }, 500);
+
+        // Load persisted chat history and macro targets from the server
+        await loadChatHistory();
+        await loadMacroTargets();
+    }
+
+    async function loadChatHistory() {
+        const token = localStorage.getItem('localFoodToken');
+        if (!token) return;
+
+        try {
+            const response = await fetch('/api/chat/history', {
+                headers: { 'Authorization': `Bearer ${token}` }
+            });
+
+            if (response.status === 401) {
+                setLoggedOutState();
+                return;
+            }
+
+            if (response.ok) {
+                const data = await response.json();
+                if (data.history && data.history.length > 0) {
+                    // Clear initial welcome message if we have real history
+                    chatContainer.innerHTML = '';
+                    chatHistory = []; // Reset local state
+                    
+                    data.history.forEach(msg => {
+                        addMessage(msg.role, msg.content);
+                        chatHistory.push({ role: msg.role, content: msg.content });
+                    });
+                }
+            }
+        } catch (err) {
+            console.error("Failed to load chat history:", err);
+        }
+    }
+
+    async function loadMacroTargets() {
+        const token = localStorage.getItem('localFoodToken');
+        if (!token) return;
+
+        const dashboard = document.getElementById('macro-dashboard');
+        try {
+            const response = await fetch('/api/macros/targets', {
+                headers: { 'Authorization': `Bearer ${token}` }
+            });
+
+            if (response.ok) {
+                const data = await response.json();
+                document.getElementById('macro-calories').textContent = `${data.calories} kcal`;
+                document.getElementById('macro-protein').textContent  = `${data.protein_g} g`;
+                document.getElementById('macro-carbs').textContent    = `${data.carbs_g} g`;
+                document.getElementById('macro-fat').textContent      = `${data.fat_g} g`;
+                if (dashboard) dashboard.style.display = 'flex';
+            }
+        } catch (err) {
+            console.error('Failed to load macro targets:', err);
+        }
+    }
+
+    async function setLoggedOutState() {
+        const token = localStorage.getItem('localFoodToken');
+        if (token) {
+            try {
+                await fetch('/api/logout', {
+                    method: 'POST',
+                    headers: { 'Authorization': `Bearer ${token}` }
+                });
+            } catch (err) {
+                console.error("Error during logout:", err);
+            }
+        }
+
+        // Clear chat memory and interface so the next user doesn't see old messages
+        chatHistory = [];
+        chatContainer.innerHTML = '';
+        addMessage('system', 'Hello! I am LocalFoodAI, your completely local nutrition and menu assistant. How can I help you today?');
+
+        localStorage.removeItem('localFoodUser');
+        localStorage.removeItem('localFoodToken');
+        chatApp.style.display = 'none';
+        chatApp.classList.remove('fade-in');
+        const dashboard = document.getElementById('macro-dashboard');
+        if (dashboard) dashboard.style.display = 'none';
+        
+        authScreen.style.display = 'block';
+        setTimeout(() => {
+            authScreen.classList.remove('fade-out');
+        }, 50);
+        loginForm.reset();
+        registerForm.reset();
+        loginError.textContent = '';
+        regError.textContent = '';
+    }
+
+    logoutBtn.addEventListener('click', () => {
+        setLoggedOutState();
+    });
+
+    // Toggles
+    showRegisterLink.addEventListener('click', (e) => {
+        e.preventDefault();
+        loginForm.style.display = 'none';
+        registerForm.style.display = 'block';
+    });
+    
+    showLoginLink.addEventListener('click', (e) => {
+        e.preventDefault();
+        registerForm.style.display = 'none';
+        loginForm.style.display = 'block';
+    });
+
+    // Login Submission
+    loginForm.addEventListener('submit', async (e) => {
+        e.preventDefault();
+        loginError.textContent = '';
+        const submitBtn = document.getElementById('login-submit-btn');
+        submitBtn.disabled = true;
+
+        const username = document.getElementById('login-username').value;
+        const password = document.getElementById('login-password').value;
+
+        try {
+            const response = await fetch('/api/login', {
+                method: 'POST',
+                headers: { 'Content-Type': 'application/json' },
+                body: JSON.stringify({ username, password })
+            });
+
+            const data = await response.json();
+
+            if (!response.ok) {
+                loginError.textContent = data.detail || 'Login failed.';
+            } else {
+                setLoggedInState(data.username, data.token);
+            }
+        } catch (err) {
+            loginError.textContent = 'Server error. Is the backend running?';
+        } finally {
+            submitBtn.disabled = false;
+        }
+    });
+
+    // Registration Submission
+    registerForm.addEventListener('submit', async (e) => {
+        e.preventDefault();
+        regError.textContent = '';
+        regSuccess.textContent = '';
+        const submitBtn = document.getElementById('reg-submit-btn');
+        submitBtn.disabled = true;
+
+        const username = document.getElementById('reg-username').value;
+        const password = document.getElementById('reg-password').value;
+        const confirmInfo = document.getElementById('reg-confirm').value;
+
+        if (password !== confirmInfo) {
+            regError.textContent = "Passwords do not match.";
+            submitBtn.disabled = false;
+            return;
+        }
+
+        try {
+            const response = await fetch('/api/register', {
+                method: 'POST',
+                headers: { 'Content-Type': 'application/json' },
+                body: JSON.stringify({ username, password })
+            });
+
+            const data = await response.json();
+
+            if (!response.ok) {
+                regError.textContent = data.detail || 'Registration failed.';
+            } else {
+                regSuccess.textContent = 'Account created! Logging in...';
+                setTimeout(() => {
+                    setLoggedInState(data.username, data.token);
+                }, 1000);
+            }
+        } catch (err) {
+            regError.textContent = 'Server error. Please try again later.';
+        } finally {
+            submitBtn.disabled = false;
+        }
+    });
+
+    // --- Food Search Module Logic ---
+    const searchInput = document.getElementById('food-search-input');
+    const searchDropdown = document.getElementById('search-results-dropdown');
+    const clearSearchBtn = document.getElementById('clear-search-btn');
+    
+    // --- Meal Builder State & UI (US-10 Task #46) ---
+    let currentMealItems = [];
+    const mealBuilder = document.getElementById('meal-builder');
+    const mealContent = document.getElementById('meal-content');
+    const mealItemsList = document.getElementById('meal-items-list');
+    const mealItemCount = document.getElementById('meal-item-count');
+    const toggleMealBtn = document.getElementById('toggle-meal-btn');
+    const emptyMealMsg = document.getElementById('empty-meal-msg');
+    const mealBuilderFooter = document.getElementById('meal-builder-footer');
+    const generateRecipeBtn = document.getElementById('generate-recipe-btn');
+
+    const toggleMealBuilder = () => {
+        const isCollapsed = mealContent.classList.contains('collapsed');
+        if (isCollapsed) {
+            mealContent.classList.remove('collapsed');
+            toggleMealBtn.style.transform = 'rotate(180deg)';
+        } else {
+            mealContent.classList.add('collapsed');
+            toggleMealBtn.style.transform = 'rotate(0deg)';
+        }
+    };
+
+    const addItemToMeal = (food) => {
+        // Duplicate Handling: Merge by updating grams
+        const existingItem = currentMealItems.find(item => item.id === food.id);
+        if (existingItem) {
+            existingItem.amount += 100;
+            // Directly update the DOM input for this item to avoid re-rendering the whole list
+            const inputEl = document.querySelector(`.meal-weight-input[data-id="${food.id}"]`);
+            if (inputEl) {
+                inputEl.value = existingItem.amount;
+                // Animate the row slightly to show it was updated
+                const rowEl = inputEl.closest('.meal-item-row');
+                if (rowEl) {
+                    rowEl.style.transform = 'scale(1.02)';
+                    rowEl.style.background = 'rgba(255, 255, 255, 0.1)';
+                    setTimeout(() => {
+                        rowEl.style.transform = '';
+                        rowEl.style.background = 'rgba(255, 255, 255, 0.05)';
+                    }, 200);
+                }
+            }
+        } else {
+            currentMealItems.push({
+                id: food.id,
+                name: food.name,
+                amount: 100,
+                base_macros: {
+                    calories: food.calories,
+                    protein_g: food.protein_g,
+                    fat_g: food.fat_g,
+                    carbs_g: food.carbs_g
+                }
+            });
+            renderMealItems(); // Only re-render if it's a new item
+        }
+        
+        // Ensure builder is expanded when adding an item
+        if (mealContent.classList.contains('collapsed')) {
+            toggleMealBuilder();
+        }
+
+        // Trigger calculation
+        calculateMealTotals();
+    };
+
+    const removeItemFromMeal = (id) => {
+        currentMealItems = currentMealItems.filter(item => item.id !== id);
+        renderMealItems();
+        calculateMealTotals();
+    };
+
+    let calculateAbortController = null;
+
+    const calculateMealTotals = async () => {
+        const activeItems = currentMealItems.filter(item => item.amount > 0);
+        
+        // DOM Elements
+        const totalsBanner = document.getElementById('meal-totals-banner');
+        const totalWeightEl = document.getElementById('meal-total-weight');
+        const totalCalEl = document.getElementById('meal-total-cal');
+        const totalProEl = document.getElementById('meal-total-pro');
+        const totalFatEl = document.getElementById('meal-total-fat');
+        const totalCarbEl = document.getElementById('meal-total-carb');
+
+        if (activeItems.length === 0) {
+            if (totalsBanner) totalsBanner.classList.remove('has-error');
+            if (totalWeightEl) totalWeightEl.textContent = '0g';
+            if (totalCalEl) totalCalEl.textContent = '0';
+            if (totalProEl) totalProEl.textContent = '0';
+            if (totalFatEl) totalFatEl.textContent = '0';
+            if (totalCarbEl) totalCarbEl.textContent = '0';
+            return;
+        }
+
+        // Race Condition Protection: Abort previous request
+        if (calculateAbortController) {
+            calculateAbortController.abort();
+        }
+        calculateAbortController = new AbortController();
+
+        try {
+            const token = localStorage.getItem('localFoodToken');
+            const payload = {
+                items: activeItems.map(item => ({
+                    food_id: item.id,
+                    amount_g: item.amount
+                }))
+            };
+
+            const response = await fetch('/api/meal/calculate', {
+                method: 'POST',
+                headers: {
+                    'Content-Type': 'application/json',
+                    'Authorization': `Bearer ${token}`
+                },
+                body: JSON.stringify(payload),
+                signal: calculateAbortController.signal
+            });
+
+            if (response.ok) {
+                const data = await response.json();
+                if (totalsBanner) totalsBanner.classList.remove('has-error');
+                
+                // Update UI with animation for "alive" feel
+                if (totalWeightEl) totalWeightEl.textContent = `${data.total_weight_g}g`;
+                if (totalCalEl) animateValue(totalCalEl, data.macros.calories);
+                if (totalProEl) animateValue(totalProEl, data.macros.protein_g);
+                if (totalFatEl) animateValue(totalFatEl, data.macros.fat_g);
+                if (totalCarbEl) animateValue(totalCarbEl, data.macros.carbs_g);
+            } else {
+                if (totalsBanner) totalsBanner.classList.add('has-error');
+            }
+        } catch (err) {
+            if (err.name === 'AbortError') return; // Ignore expected aborts
+            console.error('Failed to calculate meal totals:', err);
+            if (totalsBanner) totalsBanner.classList.add('has-error');
+        }
+    };
+
+    // Helper for smooth number transitions
+    const animateValue = (el, targetValue) => {
+        const startValue = parseFloat(el.textContent) || 0;
+        const duration = 400;
+        const startTime = performance.now();
+
+        const update = (currentTime) => {
+            const elapsed = currentTime - startTime;
+            const progress = Math.min(elapsed / duration, 1);
+            
+            // Ease out quad
+            const ease = progress * (2 - progress);
+            const currentValue = startValue + (targetValue - startValue) * ease;
+            
+            el.textContent = targetValue % 1 === 0 ? Math.round(currentValue) : currentValue.toFixed(1);
+
+            if (progress < 1) {
+                requestAnimationFrame(update);
+            } else {
+                el.textContent = targetValue;
+            }
+        };
+
+        requestAnimationFrame(update);
+    };
+
+    const debouncedCalculate = debounce(calculateMealTotals, 300);
+
+    const updateItemAmount = (id, newAmount) => {
+        const item = currentMealItems.find(item => item.id === id);
+        if (item) {
+            item.amount = parseInt(newAmount) || 0;
+            debouncedCalculate();
+        }
+    };
+
+    const renderMealItems = () => {
+        mealItemsList.innerHTML = '';
+        mealItemCount.textContent = `${currentMealItems.length} item${currentMealItems.length !== 1 ? 's' : ''}`;
+        
+        if (currentMealItems.length === 0) {
+            mealItemsList.appendChild(emptyMealMsg);
+            mealBuilderFooter.style.display = 'none';
+            return;
+        }
+
+        mealBuilderFooter.style.display = 'flex';
+
+        currentMealItems.forEach(item => {
+            const row = document.createElement('div');
+            row.className = 'meal-item-row';
+            row.innerHTML = `
+                <div class="meal-item-name" title="${item.name}">${item.name}</div>
+                <div class="meal-item-controls">
+                    <div class="weight-input-wrapper">
+                        <input type="number" value="${item.amount}" min="0" max="5000" data-id="${item.id}" class="meal-weight-input">
+                        <span class="weight-unit">g</span>
+                    </div>
+                    <button class="remove-item-btn" data-id="${item.id}" title="Remove item">
+                        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"></path></svg>
+                    </button>
+                </div>
+            `;
+
+            // Event Listeners for rows
+            row.querySelector('.meal-weight-input').addEventListener('input', (e) => {
+                updateItemAmount(item.id, e.target.value);
+            });
+
+            row.querySelector('.remove-item-btn').addEventListener('click', () => {
+                removeItemFromMeal(item.id);
+            });
+
+            mealItemsList.appendChild(row);
+        });
+    };
+
+    if (toggleMealBtn) {
+        toggleMealBtn.addEventListener('click', toggleMealBuilder);
+    }
+    
+    if (generateRecipeBtn) {
+        generateRecipeBtn.addEventListener('click', () => {
+            const activeItems = currentMealItems.filter(item => item.amount > 0);
+            if (activeItems.length === 0) return;
+            
+            const itemStrings = activeItems.map(item => `${item.name} (${item.amount}g)`);
+            const promptText = `Give me a recipe that contains: ${itemStrings.join(', ')}`;
+            userInput.value = promptText;
+            
+            // UI Feedback
+            userInput.focus();
+            userInput.style.height = 'auto';
+            userInput.style.height = userInput.scrollHeight + 'px';
+            sendBtn.disabled = false;
+            
+            // Smoothly scroll to the bottom to see the chat input
+            window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
+        });
+    }
+
+    // Also toggle when clicking the header
+    document.querySelector('.meal-builder-header').addEventListener('click', (e) => {
+        if (!e.target.closest('#toggle-meal-btn')) {
+            toggleMealBuilder();
+        }
+    });
+
+    function debounce(func, delay) {
+        let timeout;
+        return function(...args) {
+            clearTimeout(timeout);
+            timeout = setTimeout(() => func(...args), delay);
+        };
+    }
+
+    const performSearch = async (query) => {
+        if (!query) {
+            searchDropdown.style.display = 'none';
+            return;
+        }
+        
+        searchDropdown.style.display = 'block';
+        searchDropdown.innerHTML = '<div class="search-loading">Searching local database...</div>';
+        
+        try {
+            const token = localStorage.getItem('localFoodToken');
+            const response = await fetch(`/api/food/search?q=${encodeURIComponent(query)}`, {
+                headers: { 'Authorization': `Bearer ${token}` }
+            });
+            
+            if (response.status === 401) {
+                setLoggedOutState();
+                addMessage('system', 'Your session has expired. Please log in again to use the food search.');
+                searchDropdown.style.display = 'none';
+                return;
+            }
+
+            if (!response.ok) {
+                throw new Error(`HTTP error! status: ${response.status}`);
+            }
+
+            const data = await response.json();
+            
+            if (data.results && data.results.length > 0) {
+                searchDropdown.innerHTML = '';
+                data.results.forEach(item => {
+                    const foodEl = document.createElement('div');
+                    foodEl.className = 'food-item';
+                    
+                    const badgeStr = item.category === "Sourced Ingredient" ? "Raw Ingredient" : item.category;
+                    
+                    foodEl.innerHTML = `
+                        <div class="food-item-header">
+                            <span class="food-name">${item.name}</span>
+                            <span class="food-badge">${badgeStr}</span>
+                        </div>
+                        <div class="food-macros">
+                            <div class="macro-tag">Cal: <span>${item.calories}</span></div>
+                            <div class="macro-tag">Pro: <span>${item.protein_g}g</span></div>
+                            <div class="macro-tag">Fat: <span>${item.fat_g}g</span></div>
+                            <div class="macro-tag">Carb: <span>${item.carbs_g}g</span></div>
+                        </div>
+                        <div class="food-item-footer">
+                            <button class="add-meal-btn" data-id="${item.id}">➕ Add to Meal</button>
+                            <button class="details-btn" data-id="${item.id}">📊 Details</button>
+                        </div>
+                        <div class="details-panel" id="details-${item.id}"></div>
+                    `;
+                    
+                    // Click on the header/name area auto-fills the chat
+                    foodEl.querySelector('.food-item-header').addEventListener('click', (e) => {
+                        e.stopPropagation();
+                        userInput.value = `Can you build a meal around ${item.name} (${item.calories} cal, ${item.protein_g}g protein)?`;
+                        userInput.focus();
+                        userInput.style.height = 'auto'; // Reset
+                        sendBtn.disabled = false;
+                        searchDropdown.style.display = 'none';
+                        searchInput.value = '';
+                        clearSearchBtn.style.display = 'none';
+                    });
+
+                    // Click on the add to meal button
+                    foodEl.querySelector('.add-meal-btn').addEventListener('click', (e) => {
+                        e.stopPropagation();
+                        addItemToMeal(item);
+                        // Optional: provide visual feedback
+                        const btn = e.target;
+                        const originalText = btn.textContent;
+                        btn.textContent = '✅ Added';
+                        btn.style.background = '#22c55e';
+                        setTimeout(() => {
+                            btn.textContent = originalText;
+                            btn.style.background = '';
+                        }, 1000);
+                    });
+
+                    // Click on the details button toggles the panel
+                    foodEl.querySelector('.details-btn').addEventListener('click', (e) => {
+                        e.stopPropagation();
+                        const panel = document.getElementById(`details-${item.id}`);
+                        toggleFoodDetails(item.id, panel, e.target);
+                    });
+                    
+                    searchDropdown.appendChild(foodEl);
+                });
+            } else {
+                searchDropdown.innerHTML = '<div class="search-empty">No matching foods found.</div>';
+            }
+        } catch (error) {
+            console.error('Search error:', error);
+            searchDropdown.innerHTML = '<div class="search-empty">Service currently unavailable. Please try again.</div>';
+        }
+    };
+
+    const toggleFoodDetails = async (foodId, panel, btn) => {
+        const isExpanded = panel.classList.contains('expanded');
+        
+        if (isExpanded) {
+            panel.classList.remove('expanded');
+            btn.textContent = '📊 Details';
+            return;
+        }
+
+        // If not loaded yet, fetch from API
+        if (panel.innerHTML.trim() === '' || panel.innerHTML.includes('Loading')) {
+            console.log(`[UI] Fetching details for food ${foodId}...`);
+            panel.innerHTML = '<div class="search-loading">Loading details...</div>';
+            panel.classList.add('expanded'); // Show loading state
+            
+            try {
+                const token = localStorage.getItem('localFoodToken');
+                const response = await fetch(`/api/food/${foodId}`, {
+                    headers: { 'Authorization': `Bearer ${token}` }
+                });
+                
+                if (!response.ok) throw new Error("Failed to fetch details");
+                
+                const data = await response.json();
+                renderFoodDetails(panel, data);
+            } catch (err) {
+                console.error(err);
+                panel.innerHTML = '<div class="search-empty">Error loading details.</div>';
+            }
+        } else {
+            panel.classList.add('expanded');
+        }
+        
+        btn.textContent = '✖ Close';
+    };
+
+    const renderFoodDetails = (panel, data) => {
+        panel.innerHTML = `
+            <div class="nutrient-section">
+                <div class="nutrient-section-title">Extended Nutrition</div>
+                <div class="nutrient-grid">
+                    <div class="nutrient-item"><span class="nutrient-label">Fiber</span><span class="nutrient-value">${data.extended.fiber_g}g</span></div>
+                    <div class="nutrient-item"><span class="nutrient-label">Sugar</span><span class="nutrient-value">${data.extended.sugar_g}g</span></div>
+                    <div class="nutrient-item"><span class="nutrient-label">Cholesterol</span><span class="nutrient-value">${data.extended.cholesterol_mg}mg</span></div>
+                </div>
+            </div>
+            <div class="nutrient-section">
+                <div class="nutrient-section-title">Vitamins</div>
+                <div class="nutrient-grid">
+                    <div class="nutrient-item"><span class="nutrient-label">Vitamin A</span><span class="nutrient-value">${data.vitamins.vitamin_a_iu}IU</span></div>
+                    <div class="nutrient-item"><span class="nutrient-label">Vitamin C</span><span class="nutrient-value">${data.vitamins.vitamin_c_mg}mg</span></div>
+                </div>
+            </div>
+            <div class="nutrient-section">
+                <div class="nutrient-section-title">Minerals</div>
+                <div class="nutrient-grid">
+                    <div class="nutrient-item"><span class="nutrient-label">Calcium</span><span class="nutrient-value">${data.minerals.calcium_mg}mg</span></div>
+                    <div class="nutrient-item"><span class="nutrient-label">Iron</span><span class="nutrient-value">${data.minerals.iron_mg}mg</span></div>
+                    <div class="nutrient-item"><span class="nutrient-label">Potassium</span><span class="nutrient-value">${data.minerals.potassium_mg}mg</span></div>
+                    <div class="nutrient-item"><span class="nutrient-label">Sodium</span><span class="nutrient-value">${data.minerals.sodium_mg}mg</span></div>
+                </div>
+            </div>
+        `;
+    };
+
+    const handleSearchInput = debounce((e) => {
+        const query = e.target.value.trim();
+        if (query.length > 0) {
+            clearSearchBtn.style.display = 'block';
+            performSearch(query);
+        } else {
+            clearSearchBtn.style.display = 'none';
+            searchDropdown.style.display = 'none';
+        }
+    }, 300);
+
+    if (searchInput) {
+        searchInput.addEventListener('input', handleSearchInput);
+    }
+    
+    if (clearSearchBtn) {
+        clearSearchBtn.addEventListener('click', () => {
+            searchInput.value = '';
+            clearSearchBtn.style.display = 'none';
+            searchDropdown.style.display = 'none';
+            searchInput.focus();
+        });
+    }
+
+    document.addEventListener('click', (e) => {
+        if (!e.target.closest('#food-search-module')) {
+            if (searchDropdown) searchDropdown.style.display = 'none';
+        }
+    });
+
+    // Initialize state
+    sendBtn.disabled = true;
+
+    // --- Saved Meals Logic (Sprint 8) ---
+    const saveMealModal = document.getElementById('save-meal-modal');
+    const openSaveModalBtn = document.getElementById('open-save-modal-btn');
+    const closeSaveModalBtn = document.getElementById('close-save-modal-btn');
+    const cancelSaveBtn = document.getElementById('cancel-save-btn');
+    const confirmSaveBtn = document.getElementById('confirm-save-btn');
+    const mealNameInput = document.getElementById('meal-name-input');
+    const saveMealError = document.getElementById('save-meal-error');
+
+    if (openSaveModalBtn) {
+        openSaveModalBtn.addEventListener('click', (e) => {
+            e.stopPropagation();
+            if (currentMealItems.length === 0) return;
+            saveMealModal.style.display = 'flex';
+            mealNameInput.value = '';
+            saveMealError.textContent = '';
+            mealNameInput.focus();
+        });
+    }
+
+    const closeSaveModal = () => {
+        saveMealModal.style.display = 'none';
+    };
+
+    if (closeSaveModalBtn) closeSaveModalBtn.addEventListener('click', closeSaveModal);
+    if (cancelSaveBtn) cancelSaveBtn.addEventListener('click', closeSaveModal);
+
+    if (confirmSaveBtn) {
+        confirmSaveBtn.addEventListener('click', async () => {
+            const name = mealNameInput.value.trim();
+            if (!name) {
+                saveMealError.textContent = 'Please enter a name for your meal.';
+                return;
+            }
+
+            confirmSaveBtn.disabled = true;
+            confirmSaveBtn.textContent = 'Saving...';
+
+            try {
+                const token = localStorage.getItem('localFoodToken');
+                const response = await fetch('/api/meals', {
+                    method: 'POST',
+                    headers: {
+                        'Content-Type': 'application/json',
+                        'Authorization': `Bearer ${token}`
+                    },
+                    body: JSON.stringify({
+                        name: name,
+                        items: currentMealItems.map(item => ({
+                            food_id: item.id,
+                            amount_g: item.amount
+                        }))
+                    })
+                });
+
+                if (response.ok) {
+                    closeSaveModal();
+                    addMessage('system', `✅ Successfully saved "${name}" to your dashboard!`);
+                    
+                    // Trigger a refresh of the dashboard if it's implemented later
+                    if (window.refreshDashboard) window.refreshDashboard();
+                } else {
+                    const data = await response.json();
+                    saveMealError.textContent = data.detail || 'Failed to save meal.';
+                }
+            } catch (err) {
+                console.error('Save meal error:', err);
+                saveMealError.textContent = 'Server error. Please try again.';
+            } finally {
+                confirmSaveBtn.disabled = false;
+                confirmSaveBtn.textContent = 'Save to Dashboard';
+            }
+        });
+    }
+
+    // Close modal on outside click
+    window.addEventListener('click', (e) => {
+        if (e.target === saveMealModal) {
+            closeSaveModal();
+        }
+    });
+    // --- Dashboard Logic (Sprint 8) ---
+    const dashboardOverlay = document.getElementById('dashboard-overlay');
+    const openDashboardBtn = document.getElementById('nav-dashboard-btn');
+    const closeDashboardBtn = document.getElementById('close-dashboard-btn');
+    const dashboardGrid = document.getElementById('dashboard-grid');
+    const dashboardEmptyMsg = document.getElementById('dashboard-empty-msg');
+
+    const toggleDashboard = (show) => {
+        if (show) {
+            dashboardOverlay.style.display = 'flex';
+            loadSavedMeals();
+        } else {
+            dashboardOverlay.style.display = 'none';
+        }
+    };
+
+    if (openDashboardBtn) openDashboardBtn.addEventListener('click', () => toggleDashboard(true));
+    if (closeDashboardBtn) closeDashboardBtn.addEventListener('click', () => toggleDashboard(false));
+
+    async function loadSavedMeals() {
+        dashboardGrid.innerHTML = '<div class="search-loading">Loading your meals...</div>';
+        dashboardEmptyMsg.style.display = 'none';
+
+        try {
+            const token = localStorage.getItem('localFoodToken');
+            const response = await fetch('/api/meals', {
+                headers: { 'Authorization': `Bearer ${token}` }
+            });
+
+            if (response.ok) {
+                const data = await response.json();
+                renderDashboard(data.meals);
+            } else {
+                dashboardGrid.innerHTML = '<div class="error-text">Failed to load meals.</div>';
+            }
+        } catch (err) {
+            console.error('Dashboard load error:', err);
+            dashboardGrid.innerHTML = '<div class="error-text">Connection error.</div>';
+        }
+    }
+
+    function renderDashboard(meals) {
+        dashboardGrid.innerHTML = '';
+        if (!meals || meals.length === 0) {
+            dashboardEmptyMsg.style.display = 'block';
+            return;
+        }
+
+        meals.forEach((meal, index) => {
+            const card = document.createElement('div');
+            card.className = 'meal-card';
+            card.style.animationDelay = `${index * 0.07}s`;
+
+            const dateStr = new Date(meal.created_at).toLocaleDateString(undefined, {
+                month: 'short', day: 'numeric', year: 'numeric'
+            });
+
+            card.innerHTML = `
+                <div class="meal-card-header">
+                    <span class="meal-card-name">${meal.name}</span>
+                    <span class="meal-card-date">${dateStr}</span>
+                </div>
+                <div class="meal-card-macros">
+                    <div class="card-macro kcal">
+                        <span class="card-macro-val">${Math.round(meal.total_calories)}</span>
+                        <span class="card-macro-lbl">kcal</span>
+                    </div>
+                    <div class="card-macro protein">
+                        <span class="card-macro-val">${Math.round(meal.total_protein)}g</span>
+                        <span class="card-macro-lbl">Protein</span>
+                    </div>
+                    <div class="card-macro carbs">
+                        <span class="card-macro-val">${Math.round(meal.total_carbs)}g</span>
+                        <span class="card-macro-lbl">Carbs</span>
+                    </div>
+                    <div class="card-macro fat">
+                        <span class="card-macro-val">${Math.round(meal.total_fat)}g</span>
+                        <span class="card-macro-lbl">Fat</span>
+                    </div>
+                </div>
+            `;
+            dashboardGrid.appendChild(card);
+        });
+    }
+});

+ 1436 - 0
static/style.css.final

@@ -0,0 +1,1436 @@
+:root {
+    --bg-color: #0d1117;
+    --panel-bg: rgba(22, 27, 34, 0.7);
+    --border-color: rgba(48, 54, 61, 0.8);
+    --text-main: #c9d1d9;
+    --text-muted: #8b949e;
+    --primary-gradient: linear-gradient(135deg, #2ea043 0%, #238636 100%);
+    --primary-color: #2ea043;
+    --user-msg-bg: linear-gradient(135deg, #1f6feb 0%, #164e63 100%);
+    --bot-msg-bg: rgba(33, 38, 45, 0.8);
+    --glass-blur: blur(16px);
+}
+
+* {
+    box-sizing: border-box;
+    margin: 0;
+    padding: 0;
+}
+
+body {
+    font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
+    background-color: var(--bg-color);
+    background-image: radial-gradient(circle at top right, rgba(46, 160, 67, 0.15), transparent 400px),
+                      radial-gradient(circle at bottom left, rgba(31, 111, 235, 0.1), transparent 400px);
+    color: var(--text-main);
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    height: 100vh;
+    padding: 20px;
+    overflow: hidden;
+}
+
+.app-container {
+    width: 100%;
+    max-width: 900px;
+    height: 90vh;
+    background: var(--panel-bg);
+    backdrop-filter: var(--glass-blur);
+    -webkit-backdrop-filter: var(--glass-blur);
+    border: 1px solid var(--border-color);
+    border-radius: 20px;
+    display: flex;
+    flex-direction: column;
+    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
+    overflow: hidden;
+}
+
+.chat-header {
+    padding: 16px 24px;
+    border-bottom: 1px solid var(--border-color);
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    background: rgba(13, 17, 23, 0.6);
+}
+
+.brand {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+}
+
+.logo {
+    font-size: 28px;
+    background: var(--primary-gradient);
+    -webkit-background-clip: text;
+    background-clip: text;
+}
+
+h1 {
+    font-size: 1.1rem;
+    font-weight: 600;
+    color: #f0f6fc;
+}
+
+.status-indicator {
+    display: inline-block;
+    width: 8px;
+    height: 8px;
+    background-color: #2ea043;
+    border-radius: 50%;
+    margin-right: 6px;
+    box-shadow: 0 0 10px #2ea043;
+}
+
+.status-text {
+    font-size: 0.75rem;
+    color: var(--text-muted);
+}
+
+#clear-chat {
+    background: none;
+    border: none;
+    color: var(--text-muted);
+    cursor: pointer;
+    transition: color 0.2s ease;
+    padding: 8px;
+    border-radius: 8px;
+}
+
+#clear-chat:hover {
+    color: #f85149;
+    background: rgba(248, 81, 73, 0.1);
+}
+
+.chat-container {
+    flex: 1;
+    padding: 24px;
+    overflow-y: auto;
+    display: flex;
+    flex-direction: column;
+    gap: 20px;
+    scroll-behavior: smooth;
+}
+
+.chat-container::-webkit-scrollbar {
+    width: 6px;
+}
+.chat-container::-webkit-scrollbar-thumb {
+    background: var(--border-color);
+    border-radius: 10px;
+}
+
+.message {
+    display: flex;
+    gap: 16px;
+    max-width: 85%;
+    animation: fadeIn 0.3s ease forwards;
+    opacity: 0;
+    transform: translateY(10px);
+}
+
+@keyframes fadeIn {
+    to {
+        opacity: 1;
+        transform: translateY(0);
+    }
+}
+
+.message.user {
+    align-self: flex-end;
+    flex-direction: row-reverse;
+}
+
+.avatar {
+    width: 36px;
+    height: 36px;
+    border-radius: 10px;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    font-size: 20px;
+    flex-shrink: 0;
+    background: rgba(255, 255, 255, 0.1);
+}
+
+.message.user .avatar {
+    background: var(--user-msg-bg);
+}
+
+.message.system .avatar {
+    background: var(--bot-msg-bg);
+    border: 1px solid var(--border-color);
+}
+
+.message-content {
+    padding: 14px 18px;
+    border-radius: 18px;
+    line-height: 1.6;
+    font-size: 0.95rem;
+    white-space: pre-wrap;
+    word-break: break-word;
+}
+
+.message.user .message-content {
+    background: var(--user-msg-bg);
+    color: #fff;
+    border-top-right-radius: 4px;
+}
+
+.message.system .message-content {
+    background: var(--bot-msg-bg);
+    border: 1px solid var(--border-color);
+    border-top-left-radius: 4px;
+    box-shadow: 0 4px 12px rgba(0,0,0,0.1);
+}
+
+.chat-input-area {
+    padding: 20px 24px;
+    background: rgba(13, 17, 23, 0.8);
+    border-top: 1px solid var(--border-color);
+}
+
+.input-form {
+    display: flex;
+    gap: 12px;
+    align-items: flex-end;
+    background: var(--bg-color);
+    border: 1px solid var(--border-color);
+    border-radius: 16px;
+    padding: 8px 16px;
+    transition: border-color 0.2s, box-shadow 0.2s;
+}
+
+.input-form:focus-within {
+    border-color: rgba(46, 160, 67, 0.5);
+    box-shadow: 0 0 0 2px rgba(46, 160, 67, 0.1);
+}
+
+textarea {
+    flex: 1;
+    background: none;
+    border: none;
+    color: var(--text-main);
+    font-family: inherit;
+    font-size: 1rem;
+    resize: none;
+    max-height: 150px;
+    padding: 10px 0;
+    outline: none;
+}
+
+textarea::placeholder {
+    color: var(--text-muted);
+}
+
+.send-btn {
+    background: var(--primary-color);
+    border: none;
+    border-radius: 12px;
+    width: 40px;
+    height: 40px;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    color: #fff;
+    cursor: pointer;
+    transition: transform 0.2s, background 0.2s;
+    flex-shrink: 0;
+    margin-bottom: 2px;
+}
+
+.send-btn:hover {
+    background: #3fb950;
+    transform: scale(1.05);
+}
+
+.send-btn:active {
+    transform: scale(0.95);
+}
+
+.send-btn:disabled {
+    background: var(--border-color);
+    color: var(--text-muted);
+    cursor: not-allowed;
+    transform: none;
+}
+
+.footer-note {
+    text-align: center;
+    font-size: 0.7rem;
+    color: var(--text-muted);
+    margin-top: 12px;
+}
+
+.typing-indicator {
+    display: flex;
+    gap: 4px;
+    padding: 4px 8px;
+    align-items: center;
+}
+
+.typing-dot {
+    width: 6px;
+    height: 6px;
+    background: var(--text-muted);
+    border-radius: 50%;
+    animation: typing 1.4s infinite ease-in-out both;
+}
+
+.typing-dot:nth-child(1) { animation-delay: -0.32s; }
+.typing-dot:nth-child(2) { animation-delay: -0.16s; }
+.typing-dot:nth-child(3) { animation-delay: 0s; }
+
+@keyframes typing {
+    0%, 80%, 100% { transform: scale(0); }
+    40% { transform: scale(1); }
+}
+
+/* Modal Styles */
+.nav-btn {
+    background: rgba(255, 255, 255, 0.1);
+    border: 1px solid var(--border-color);
+    color: var(--text-main);
+    padding: 6px 12px;
+    border-radius: 8px;
+    cursor: pointer;
+    margin-right: 10px;
+    font-size: 0.85rem;
+    transition: background 0.2s;
+}
+
+.nav-btn:hover {
+    background: rgba(255, 255, 255, 0.2);
+}
+
+.modal-overlay {
+    position: fixed;
+    top: 0; left: 0; right: 0; bottom: 0;
+    background: rgba(0, 0, 0, 0.6);
+    backdrop-filter: blur(8px);
+    -webkit-backdrop-filter: blur(8px);
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    z-index: 1000;
+    opacity: 1;
+    transition: opacity 0.3s ease;
+}
+
+.modal-overlay.hidden {
+    opacity: 0;
+    pointer-events: none;
+}
+
+.modal-content {
+    background: var(--panel-bg);
+    border: 1px solid var(--border-color);
+    border-radius: 16px;
+    padding: 30px;
+    width: 350px;
+    position: relative;
+    box-shadow: 0 10px 30px rgba(0,0,0,0.5);
+    transform: translateY(0);
+    transition: transform 0.3s ease;
+}
+
+.modal-overlay.hidden .modal-content {
+    transform: translateY(-20px);
+}
+
+.close-modal {
+    position: absolute;
+    top: 15px;
+    right: 20px;
+    background: none;
+    border: none;
+    color: var(--text-muted);
+    font-size: 1.5rem;
+    cursor: pointer;
+}
+
+.close-modal:hover {
+    color: var(--text-main);
+}
+
+.modal-content h2 {
+    margin-top: 0;
+    margin-bottom: 20px;
+    font-size: 1.3rem;
+    text-align: center;
+}
+
+.input-group {
+    margin-bottom: 15px;
+}
+
+.input-group label {
+    display: block;
+    margin-bottom: 5px;
+    font-size: 0.85rem;
+    color: var(--text-muted);
+}
+
+.input-group input {
+    width: 100%;
+    padding: 10px;
+    background: rgba(0, 0, 0, 0.2);
+    border: 1px solid var(--border-color);
+    border-radius: 8px;
+    color: var(--text-main);
+    font-size: 0.95rem;
+    outline: none;
+    transition: border-color 0.2s;
+}
+
+.input-group input:focus {
+    border-color: var(--primary-color);
+}
+
+.error-text {
+    color: #f85149;
+    font-size: 0.85rem;
+    margin-bottom: 10px;
+    text-align: center;
+    min-height: 18px;
+}
+
+.success-text {
+    color: #3fb950;
+    font-size: 0.85rem;
+    margin-bottom: 10px;
+    text-align: center;
+    min-height: 18px;
+}
+
+.primary-btn {
+    width: 100%;
+    padding: 10px;
+    background: var(--primary-gradient);
+    border: none;
+    border-radius: 8px;
+    color: #fff;
+    font-size: 1rem;
+    cursor: pointer;
+    transition: opacity 0.2s;
+}
+
+.primary-btn:hover {
+    opacity: 0.9;
+}
+/* Authentication Screen Gateway specific styles */
+.auth-container {
+    width: 100%;
+    max-width: 400px;
+    background: var(--panel-bg);
+    backdrop-filter: var(--glass-blur);
+    -webkit-backdrop-filter: var(--glass-blur);
+    border: 1px solid var(--border-color);
+    border-radius: 20px;
+    padding: 40px;
+    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
+    transition: opacity 0.5s ease, transform 0.5s ease;
+}
+
+.auth-header {
+    text-align: center;
+    margin-bottom: 30px;
+}
+
+.auth-header h2 {
+    font-size: 1.5rem;
+    color: #f0f6fc;
+    margin-bottom: 8px;
+}
+
+.auth-header p {
+    font-size: 0.9rem;
+    color: var(--text-muted);
+}
+
+.auth-toggle {
+    text-align: center;
+    font-size: 0.85rem;
+    color: var(--text-muted);
+    margin-top: 20px;
+}
+
+.auth-toggle a {
+    color: var(--primary-color);
+    text-decoration: none;
+    font-weight: 500;
+    transition: color 0.2s;
+}
+
+.auth-toggle a:hover {
+    color: #3fb950;
+    text-decoration: underline;
+}
+
+/* Animations for transitioning out the auth screen */
+.fade-out {
+    opacity: 0;
+    transform: scale(0.95);
+    pointer-events: none;
+}
+.fade-in {
+    opacity: 1;
+    transform: scale(1);
+}
+
+/* --------------------------------- */
+/* Food Search Component Styles      */
+/* --------------------------------- */
+
+.search-module {
+    padding: 10px 24px;
+    background: rgba(22, 27, 34, 0.4);
+    border-bottom: 1px solid var(--border-color);
+    position: relative;
+    z-index: 10;
+}
+
+.search-input-wrapper {
+    position: relative;
+    display: flex;
+    align-items: center;
+    background: rgba(13, 17, 23, 0.8);
+    border: 1px solid var(--border-color);
+    border-radius: 12px;
+    padding: 8px 16px;
+    transition: all 0.3s ease;
+    box-shadow: inset 0 2px 4px rgba(0,0,0,0.1);
+}
+
+.search-input-wrapper:focus-within {
+    border-color: rgba(46, 160, 67, 0.6);
+    box-shadow: 0 0 0 3px rgba(46, 160, 67, 0.15), inset 0 2px 4px rgba(0,0,0,0.1);
+}
+
+.search-icon {
+    color: var(--text-muted);
+    margin-right: 12px;
+}
+
+#food-search-input {
+    flex: 1;
+    background: transparent;
+    border: none;
+    color: var(--text-main);
+    font-size: 0.95rem;
+    outline: none;
+    font-family: inherit;
+    width: 100%;
+}
+
+#clear-search-btn {
+    background: transparent;
+    border: none;
+    color: var(--text-muted);
+    cursor: pointer;
+    border-radius: 50%;
+    padding: 4px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    transition: all 0.2s;
+}
+
+#clear-search-btn:hover {
+    background: rgba(255, 255, 255, 0.1);
+    color: #f85149;
+}
+
+.search-results-dropdown {
+    position: absolute;
+    top: 100%;
+    left: 24px;
+    right: 24px;
+    margin-top: 8px;
+    background: rgba(22, 27, 34, 0.85);
+    backdrop-filter: blur(20px);
+    -webkit-backdrop-filter: blur(20px);
+    border: 1px solid var(--border-color);
+    border-radius: 12px;
+    box-shadow: 0 15px 35px rgba(0,0,0,0.4);
+    max-height: 350px;
+    overflow-y: auto;
+    z-index: 100;
+    opacity: 0;
+    transform: translateY(-10px);
+    animation: dropdownFadeIn 0.2s forwards;
+}
+
+@keyframes dropdownFadeIn {
+    to { opacity: 1; transform: translateY(0); }
+}
+
+.search-results-dropdown::-webkit-scrollbar {
+    width: 6px;
+}
+.search-results-dropdown::-webkit-scrollbar-thumb {
+    background: rgba(255,255,255,0.2);
+    border-radius: 10px;
+}
+
+.food-item {
+    padding: 14px 18px;
+    border-bottom: 1px solid rgba(255,255,255,0.05);
+    cursor: pointer;
+    transition: background 0.2s, padding-left 0.2s;
+}
+
+.food-item:last-child {
+    border-bottom: none;
+}
+
+.food-item:hover {
+    background: rgba(255,255,255,0.05);
+    padding-left: 22px;
+}
+
+.food-item-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 8px;
+}
+
+.food-name {
+    font-weight: 600;
+    color: #e6edf3;
+    font-size: 1rem;
+}
+
+.food-badge {
+    background: rgba(46, 160, 67, 0.15);
+    color: #3fb950;
+    border: 1px solid rgba(46, 160, 67, 0.3);
+    padding: 2px 8px;
+    border-radius: 12px;
+    font-size: 0.7rem;
+    font-weight: 600;
+    text-transform: uppercase;
+    letter-spacing: 0.5px;
+}
+
+.food-macros {
+    display: flex;
+    gap: 15px;
+    font-size: 0.8rem;
+    color: var(--text-muted);
+}
+
+.macro-tag span {
+    color: #c9d1d9;
+    font-weight: 500;
+}
+
+/* --- Expandable Details (Task #40) --- */
+.food-item-footer {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-top: 10px;
+}
+
+.details-btn {
+    background: none;
+    border: 1px solid var(--border);
+    border-radius: 4px;
+    color: var(--primary);
+    font-size: 0.75rem;
+    padding: 2px 8px;
+    cursor: pointer;
+    transition: all 0.2s;
+}
+
+.details-btn:hover {
+    background: rgba(255, 255, 255, 0.05);
+    border-color: var(--primary);
+}
+
+.details-panel {
+    max-height: 0;
+    overflow: hidden;
+    transition: max-height 0.3s ease-out, margin-top 0.3s;
+}
+
+.details-panel.expanded {
+    max-height: 500px; /* Large enough for content */
+    margin-top: 12px;
+    border-top: 1px solid rgba(255, 255, 255, 0.05);
+    padding-top: 12px;
+}
+
+.nutrient-section {
+    margin-bottom: 12px;
+}
+
+.nutrient-section-title {
+    font-size: 0.7rem;
+    text-transform: uppercase;
+    letter-spacing: 0.05em;
+    color: var(--text-muted);
+    margin-bottom: 6px;
+    font-weight: 700;
+}
+
+.nutrient-grid {
+    display: grid;
+    grid-template-columns: repeat(2, 1fr);
+    gap: 8px;
+}
+
+.nutrient-item {
+    display: flex;
+    justify-content: space-between;
+    font-size: 0.8rem;
+    padding: 4px 0;
+}
+
+.nutrient-label {
+    color: var(--text-muted);
+}
+
+.nutrient-value {
+    font-weight: 500;
+    color: var(--text);
+}
+
+.search-loading, .search-empty {
+    padding: 20px;
+    text-align: center;
+    color: var(--text-muted);
+    font-size: 0.9rem;
+    background: transparent;
+}
+
+/* Mobile Responsiveness */
+@media (max-width: 600px) {
+    body {
+        padding: 0;
+    }
+    .app-container {
+        height: 100vh;
+        border-radius: 0;
+        border: none;
+    }
+    .chat-header {
+        flex-direction: column;
+        align-items: flex-start;
+        gap: 10px;
+    }
+    .actions {
+        width: 100%;
+        justify-content: space-between;
+    }
+    .search-module {
+        padding: 10px 12px;
+    }
+    .search-results-dropdown {
+        left: 12px;
+        right: 12px;
+    }
+    .message {
+        max-width: 95%;
+    }
+}
+
+/* =============================================
+   Macro Dashboard (US-07 Task #38)
+   ============================================= */
+.macro-dashboard {
+    display: flex;
+    gap: 12px;
+    padding: 12px 20px;
+    background: rgba(255, 255, 255, 0.02);
+    border-bottom: 1px solid var(--border);
+    flex-wrap: wrap;
+    animation: fadeIn 0.4s ease;
+}
+
+.macro-card {
+    flex: 1;
+    min-width: 100px;
+    background: rgba(255, 255, 255, 0.05);
+    border: 1px solid var(--border);
+    border-radius: 12px;
+    padding: 12px 16px;
+    text-align: center;
+    backdrop-filter: blur(6px);
+    -webkit-backdrop-filter: blur(6px);
+    transition: transform 0.2s ease, border-color 0.2s ease;
+}
+
+.macro-card:hover {
+    transform: translateY(-2px);
+    border-color: rgba(255, 255, 255, 0.25);
+}
+
+.macro-label {
+    font-size: 0.75rem;
+    color: var(--text-muted);
+    text-transform: uppercase;
+    letter-spacing: 1.2px;
+    margin-bottom: 6px;
+    font-weight: 600;
+}
+
+.macro-value {
+    font-size: 1.4rem;
+    font-weight: 700;
+    transition: opacity 0.3s ease;
+}
+
+/* Color-coded per macro type */
+#macro-calories .macro-value, #macro-calories { --macro-color: #facc15; }
+#macro-protein  .macro-value, #macro-protein  { --macro-color: #ef4444; }
+#macro-carbs    .macro-value, #macro-carbs    { --macro-color: #3b82f6; }
+#macro-fat      .macro-value, #macro-fat      { --macro-color: #22c55e; }
+
+#macro-calories .macro-value { color: #facc15; }
+#macro-protein  .macro-value { color: #ef4444; }
+#macro-carbs    .macro-value { color: #3b82f6; }
+#macro-fat      .macro-value { color: #22c55e; }
+
+#macro-calories { border-color: rgba(250, 204, 21,  0.2); }
+#macro-protein  { border-color: rgba(239,  68, 68,  0.2); }
+#macro-carbs    { border-color: rgba(59,  130, 246,  0.2); }
+#macro-fat      { border-color: rgba(34,  197,  94,  0.2); }
+
+@media (max-width: 768px) {
+    .macro-dashboard {
+        gap: 8px;
+        padding: 10px 12px;
+    }
+    .macro-card {
+        min-width: 70px;
+        padding: 10px 10px;
+    }
+    .macro-value {
+        font-size: 1.1rem;
+    }
+}
+
+/* =============================================
+   Meal Builder (US-10 Task #46)
+   ============================================= */
+.meal-builder-tray {
+    margin: 0 20px;
+    background: rgba(255, 255, 255, 0.03);
+    border: 1px solid var(--border);
+    border-radius: 12px;
+    overflow: hidden;
+    backdrop-filter: blur(10px);
+    -webkit-backdrop-filter: blur(10px);
+    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+    margin-bottom: 12px;
+}
+
+.meal-builder-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 10px 16px;
+    background: rgba(255, 255, 255, 0.05);
+    cursor: pointer;
+}
+
+.tray-title {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+}
+
+.tray-title h3 {
+    font-size: 0.95rem;
+    font-weight: 600;
+    margin: 0;
+}
+
+.tray-title .item-count {
+    font-size: 0.75rem;
+    color: var(--text-muted);
+    background: rgba(255, 255, 255, 0.1);
+    padding: 2px 8px;
+    border-radius: 10px;
+}
+
+.meal-content {
+    max-height: 500px;
+    overflow: hidden;
+    transition: max-height 0.3s ease-out, opacity 0.3s ease;
+    opacity: 1;
+}
+
+.meal-content.collapsed {
+    max-height: 0;
+    opacity: 0;
+}
+
+.meal-items-list {
+    padding: 12px;
+    max-height: 250px;
+    overflow-y: auto;
+    display: flex;
+    flex-direction: column;
+    gap: 8px;
+}
+
+/* Custom Scrollbar for Meal List */
+.meal-items-list::-webkit-scrollbar {
+    width: 4px;
+}
+.meal-items-list::-webkit-scrollbar-thumb {
+    background: rgba(255, 255, 255, 0.1);
+    border-radius: 4px;
+}
+
+.meal-item-row {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    padding: 8px 12px;
+    background: rgba(255, 255, 255, 0.05);
+    border: 1px solid rgba(255, 255, 255, 0.05);
+    border-radius: 8px;
+    animation: slideInLeft 0.3s ease-out;
+}
+
+.meal-item-name {
+    flex: 1;
+    font-size: 0.9rem;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+}
+
+.meal-item-controls {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+}
+
+.weight-input-wrapper {
+    display: flex;
+    align-items: center;
+    background: rgba(0, 0, 0, 0.25);
+    border-radius: 8px;
+    padding: 4px 10px;
+    border: 1px solid var(--border);
+    transition: border-color 0.2s, box-shadow 0.2s;
+}
+
+.weight-input-wrapper:focus-within {
+    border-color: var(--primary);
+    box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
+}
+
+.weight-input-wrapper input {
+    background: transparent;
+    border: none;
+    color: var(--text);
+    width: 65px;
+    font-size: 0.95rem;
+    text-align: right;
+    padding: 2px;
+    outline: none;
+    font-weight: 500;
+}
+
+/* Hide Spinners (Arrows) for cleaner look */
+.weight-input-wrapper input::-webkit-outer-spin-button,
+.weight-input-wrapper input::-webkit-inner-spin-button {
+    -webkit-appearance: none;
+    margin: 0;
+}
+
+.weight-input-wrapper input[type=number] {
+    -moz-appearance: textfield;
+}
+
+.weight-unit {
+    font-size: 0.75rem;
+    color: var(--text-muted);
+    margin-left: 2px;
+}
+
+.remove-item-btn {
+    background: transparent;
+    border: none;
+    color: #ef4444;
+    cursor: pointer;
+    padding: 4px;
+    border-radius: 4px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    opacity: 0.7;
+    transition: opacity 0.2s, background 0.2s;
+}
+
+.remove-item-btn:hover {
+    opacity: 1;
+    background: rgba(239, 68, 68, 0.1);
+}
+
+.empty-meal-msg {
+    text-align: center;
+    padding: 20px;
+    font-size: 0.85rem;
+    color: var(--text-muted);
+    font-style: italic;
+}
+
+#toggle-meal-btn {
+    transition: transform 0.3s ease;
+}
+
+.meal-content:not(.collapsed) + .meal-builder-header #toggle-meal-btn {
+    transform: rotate(180deg);
+}
+
+/* Add-to-meal button in search results */
+.add-meal-btn {
+    background: var(--primary);
+    color: white;
+    border: none;
+    border-radius: 6px;
+    padding: 4px 10px;
+    font-size: 0.8rem;
+    cursor: pointer;
+    transition: background 0.2s, transform 0.1s;
+    font-weight: 600;
+}
+
+.add-meal-btn:hover {
+    background: var(--primary-hover);
+    transform: scale(1.05);
+}
+
+.add-meal-btn:active {
+    transform: scale(0.95);
+}
+
+.food-item-footer {
+    display: flex;
+    gap: 8px;
+    margin-top: 10px;
+}
+
+@keyframes slideInLeft {
+    from { opacity: 0; transform: translateX(-10px); }
+    to { opacity: 1; transform: translateX(0); }
+}
+
+@media (max-width: 600px) {
+    .meal-builder-tray {
+        margin: 0 12px 10px 12px;
+    }
+    .meal-item-name {
+        font-size: 0.8rem;
+    }
+}
+
+.meal-builder-footer {
+    padding: 12px;
+    border-top: 1px solid rgba(255, 255, 255, 0.05);
+    display: flex;
+    justify-content: center;
+    background: rgba(255, 255, 255, 0.02);
+}
+
+.primary-btn-sm {
+    background: var(--primary);
+    color: white;
+    border: none;
+    border-radius: 8px;
+    padding: 8px 16px;
+    font-size: 0.85rem;
+    font-weight: 600;
+    cursor: pointer;
+    transition: all 0.2s ease;
+    width: 100%;
+}
+
+.primary-btn-sm:hover {
+    background: var(--primary-hover);
+    transform: translateY(-1px);
+    box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
+}
+
+.primary-btn-sm:active {
+    transform: translateY(0);
+}
+
+/* --- Meal Totals Banner --- */
+.meal-totals-banner {
+    width: 100%;
+    margin-bottom: 12px;
+    background: rgba(15, 23, 42, 0.4);
+    border: 1px solid rgba(255, 255, 255, 0.1);
+    border-radius: 10px;
+    padding: 12px;
+}
+
+.totals-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 10px;
+    padding-bottom: 6px;
+    border-bottom: 1px solid rgba(255, 255, 255, 0.05);
+}
+
+.totals-title {
+    font-size: 0.85rem;
+    font-weight: 600;
+    color: var(--text-muted);
+    text-transform: uppercase;
+    letter-spacing: 0.5px;
+}
+
+.totals-weight {
+    font-size: 0.85rem;
+    font-weight: 500;
+    color: var(--text-muted);
+}
+
+.totals-grid {
+    display: grid;
+    grid-template-columns: repeat(4, 1fr);
+    gap: 8px;
+}
+
+.total-card {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    background: rgba(255, 255, 255, 0.03);
+    border-radius: 6px;
+    padding: 8px 4px;
+    transition: background 0.3s;
+}
+
+.total-val {
+    font-size: 1.05rem;
+    font-weight: 700;
+    color: var(--text-light);
+}
+
+.total-lbl {
+    font-size: 0.7rem;
+    color: var(--text-muted);
+    margin-top: 2px;
+}
+
+.protein-highlight .total-val {
+    color: var(--primary);
+}
+
+.protein-highlight {
+    background: rgba(59, 130, 246, 0.05);
+    border: 1px solid rgba(59, 130, 246, 0.1);
+}
+
+.meal-totals-banner.has-error {
+    border-color: rgba(239, 68, 68, 0.5);
+    background: rgba(127, 29, 29, 0.2);
+    animation: errorPulse 2s infinite;
+}
+
+@keyframes errorPulse {
+    0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4); }
+    70% { box-shadow: 0 0 0 10px rgba(239, 68, 68, 0); }
+    100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); }
+}
+
+/* =============================================
+   Sprint 8: Saved Meals & Dashboard UI
+   ============================================= */
+
+.secondary-btn-sm {
+    background: rgba(255, 255, 255, 0.05);
+    color: var(--text-main);
+    border: 1px solid var(--border-color);
+    border-radius: 8px;
+    padding: 8px 12px;
+    font-size: 0.85rem;
+    font-weight: 500;
+    cursor: pointer;
+    transition: all 0.2s ease;
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    white-space: nowrap;
+}
+
+.secondary-btn-sm:hover {
+    background: rgba(255, 255, 255, 0.1);
+    border-color: var(--text-muted);
+}
+
+.modal {
+    position: fixed;
+    top: 0; left: 0; width: 100%; height: 100%;
+    background: rgba(0, 0, 0, 0.75);
+    backdrop-filter: blur(10px);
+    -webkit-backdrop-filter: blur(10px);
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    z-index: 2000;
+}
+
+.modal-content {
+    background: #161b22;
+    border: 1px solid var(--border-color);
+    border-radius: 20px;
+    width: 90%;
+    max-width: 420px;
+    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.6);
+    animation: modalSlideUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
+    overflow: hidden;
+}
+
+@keyframes modalSlideUp {
+    from { transform: translateY(30px); opacity: 0; }
+    to { transform: translateY(0); opacity: 1; }
+}
+
+.modal-header {
+    padding: 20px 24px;
+    border-bottom: 1px solid var(--border-color);
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    background: rgba(255, 255, 255, 0.02);
+}
+
+.modal-header h3 {
+    font-size: 1.15rem;
+    font-weight: 600;
+    margin: 0;
+    color: #f0f6fc;
+}
+
+.close-btn {
+    background: none;
+    border: none;
+    color: var(--text-muted);
+    font-size: 1.8rem;
+    cursor: pointer;
+    line-height: 1;
+    padding: 0;
+    transition: color 0.2s;
+}
+
+.close-btn:hover {
+    color: #ef4444;
+}
+
+.modal-body {
+    padding: 24px;
+}
+
+.modal-body p {
+    font-size: 0.95rem;
+    color: var(--text-muted);
+    line-height: 1.5;
+    margin-bottom: 16px;
+}
+
+.modal-footer {
+    padding: 20px 24px;
+    border-top: 1px solid var(--border-color);
+    display: flex;
+    gap: 12px;
+    justify-content: flex-end;
+    background: rgba(255, 255, 255, 0.02);
+}
+
+.secondary-btn {
+    background: transparent;
+    border: 1px solid var(--border-color);
+    color: var(--text-main);
+    padding: 10px 20px;
+    border-radius: 10px;
+    font-size: 0.95rem;
+    font-weight: 500;
+    cursor: pointer;
+    transition: all 0.2s;
+}
+
+.secondary-btn:hover {
+    background: rgba(255, 255, 255, 0.05);
+    border-color: var(--text-muted);
+}
+
+/* --- Dashboard Overlay --- */
+.dashboard-overlay {
+    position: absolute;
+    top: 0; left: 0; right: 0; bottom: 0;
+    background: rgba(13, 17, 23, 0.95);
+    backdrop-filter: blur(20px);
+    -webkit-backdrop-filter: blur(20px);
+    z-index: 50;
+    display: flex;
+    flex-direction: column;
+    animation: fadeIn 0.3s ease;
+}
+
+.dashboard-content {
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+}
+
+.dashboard-header {
+    padding: 24px;
+    border-bottom: 1px solid var(--border-color);
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+}
+
+.dashboard-title-group {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+}
+
+.dashboard-title-group h2 {
+    font-size: 1.25rem;
+    color: #f0f6fc;
+}
+
+.dashboard-body {
+    flex: 1;
+    overflow-y: auto;
+    padding: 24px;
+}
+
+.dashboard-grid {
+    display: grid;
+    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+    gap: 20px;
+}
+
+.empty-meal-msg {
+    grid-column: 1 / -1;
+    text-align: center;
+    padding: 60px 20px;
+    color: var(--text-muted);
+    font-size: 1.1rem;
+    border: 2px dashed var(--border-color);
+    border-radius: 20px;
+}
+
+/* --- Meal Cards (#59) --- */
+@keyframes cardSlideIn {
+    from { opacity: 0; transform: translateY(20px); }
+    to   { opacity: 1; transform: translateY(0); }
+}
+
+.meal-card {
+    background: rgba(22, 27, 34, 0.6);
+    border: 1px solid rgba(48, 54, 61, 0.8);
+    border-radius: 18px;
+    padding: 20px;
+    display: flex;
+    flex-direction: column;
+    gap: 16px;
+    backdrop-filter: blur(12px);
+    -webkit-backdrop-filter: blur(12px);
+    transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
+                border-color 0.3s ease,
+                box-shadow 0.3s ease;
+    animation: cardSlideIn 0.4s ease both;
+    cursor: default;
+}
+
+.meal-card:hover {
+    transform: translateY(-6px);
+    border-color: rgba(88, 166, 255, 0.4);
+    box-shadow: 0 20px 40px -12px rgba(0, 0, 0, 0.5),
+                0 0 0 1px rgba(88, 166, 255, 0.1);
+}
+
+.meal-card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: flex-start;
+    gap: 10px;
+}
+
+.meal-card-name {
+    font-size: 1.05rem;
+    font-weight: 700;
+    color: #f0f6fc;
+    line-height: 1.3;
+    flex: 1;
+}
+
+.meal-card-date {
+    font-size: 0.72rem;
+    color: var(--text-muted);
+    white-space: nowrap;
+    padding-top: 3px;
+}
+
+.meal-card-macros {
+    display: grid;
+    grid-template-columns: repeat(2, 1fr);
+    gap: 8px;
+}
+
+.card-macro {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    padding: 10px 8px;
+    border-radius: 12px;
+    border: 1px solid transparent;
+    gap: 2px;
+}
+
+.card-macro-val {
+    font-size: 1.1rem;
+    font-weight: 700;
+    line-height: 1;
+}
+
+.card-macro-lbl {
+    font-size: 0.65rem;
+    font-weight: 500;
+    text-transform: uppercase;
+    letter-spacing: 0.05em;
+    opacity: 0.8;
+}
+
+/* Color-coded macro badges */
+.card-macro.kcal {
+    background: rgba(251, 146, 60, 0.12);
+    border-color: rgba(251, 146, 60, 0.25);
+    color: #fb923c;
+}
+.card-macro.protein {
+    background: rgba(59, 130, 246, 0.12);
+    border-color: rgba(59, 130, 246, 0.25);
+    color: #60a5fa;
+}
+.card-macro.carbs {
+    background: rgba(34, 197, 94, 0.12);
+    border-color: rgba(34, 197, 94, 0.25);
+    color: #4ade80;
+}
+.card-macro.fat {
+    background: rgba(168, 85, 247, 0.12);
+    border-color: rgba(168, 85, 247, 0.25);
+    color: #c084fc;
+}

+ 54 - 0
test_api_task62.py

@@ -0,0 +1,54 @@
+from fastapi.testclient import TestClient
+from main import app
+from database import get_db_connection
+import uuid
+
+client = TestClient(app)
+
+def run_tests():
+    u1 = f"testu1_{uuid.uuid4().hex[:6]}"
+    u2 = f"testu2_{uuid.uuid4().hex[:6]}"
+    
+    res1 = client.post("/api/register", json={"username": u1, "password": "password123"})
+    t1 = res1.json().get("token")
+    
+    res2 = client.post("/api/register", json={"username": u2, "password": "password123"})
+    t2 = res2.json().get("token")
+    
+    h1 = {"Authorization": f"Bearer {t1}"}
+    h2 = {"Authorization": f"Bearer {t2}"}
+    
+    res = client.post("/api/meals", headers=h1, json={
+        "name": "Test Meal",
+        "items": [{"food_id": 1, "amount_g": 100}]
+    })
+    
+    meal_id = res.json()["meal_id"]
+    print(f"Created meal {meal_id}")
+    
+    # User 2 tries to rename
+    res = client.put(f"/api/meals/{meal_id}", headers=h2, json={"name": "Hacked"})
+    print(f"User 2 rename status: {res.status_code} (Expected 404)")
+    
+    # User 1 renames
+    res = client.put(f"/api/meals/{meal_id}", headers=h1, json={"name": "Renamed"})
+    print(f"User 1 rename status: {res.status_code} (Expected 200)")
+    
+    # User 2 tries to delete
+    res = client.delete(f"/api/meals/{meal_id}", headers=h2)
+    print(f"User 2 delete status: {res.status_code} (Expected 404)")
+    
+    # Delete
+    res = client.delete(f"/api/meals/{meal_id}", headers=h1)
+    print(f"User 1 delete status: {res.status_code} (Expected 200)")
+    
+    # Check DB
+    conn = get_db_connection()
+    c = conn.cursor()
+    c.execute("SELECT COUNT(*) FROM meal_items WHERE meal_id = ?", (meal_id,))
+    count = c.fetchone()[0]
+    print(f"Remaining items in DB: {count} (Expected 0)")
+    conn.close()
+
+if __name__ == "__main__":
+    run_tests()

+ 24 - 0
test_debug.py

@@ -0,0 +1,24 @@
+import httpx
+import json
+
+def debug_chat():
+    url = "http://localhost:11434/api/chat"
+    messages = [
+        {"role": "system", "content": "[SYSTEM: NUTRITIONAL ANALYST MODE]\nYou are the LocalFoodAI Analyst. Use ONLY verified local data for values.\nCRITICAL: Provide direct, concise answers. Skip all internal monologues, <thought> tags, or reasoning steps.\nFor each food discussed, you MUST follow this structure:\n1. Header: ### 🥗 [Name] (per 100g)\n2. Macros: A markdown table for Cal, P, F, C, Fib, Sug, Chol.\n3. Micros: A bulleted list for Na, Ca, Fe, K, VitA, VitC.\n4. Insight: A 1-sentence analysis of the food's nutritional profile.\nAlways prioritize local data over training memory. If a nutrient is missing, say 'Data not available'."},
+        {"role": "user", "content": "How much protein in 100g of salmon?"}
+    ]
+    payload = {
+        "model": "qwen3.5:9b",
+        "messages": messages,
+        "stream": False
+    }
+    
+    print("Sending debug request...")
+    with httpx.Client(timeout=300.0) as client:
+        response = client.post(url, json=payload)
+        print(f"Status: {response.status_code}")
+        print("Response Body:")
+        print(response.text)
+
+if __name__ == "__main__":
+    debug_chat()

+ 105 - 0
test_meal_math.py

@@ -0,0 +1,105 @@
+import unittest
+import httpx
+import json
+
+class TestMealMath(unittest.TestCase):
+    BASE_URL = "http://192.168.130.171:8000/api"
+    TOKEN = None
+    FOOD_ID_1 = None # Will be populated
+    FOOD_1_DATA = None
+
+    @classmethod
+    def setUpClass(cls):
+        with httpx.Client(timeout=30.0) as client:
+            # 1. Login to get token
+            login_res = client.post(f"{cls.BASE_URL}/login", json={
+                "username": "ferro988",
+                "password": "password"
+            })
+            if login_res.status_code == 200:
+                cls.TOKEN = login_res.json()["token"]
+            
+            # 2. Search for a baseline food (e.g., Chicken)
+            search_res = client.get(f"{cls.BASE_URL}/food/search?q=Chicken", headers={
+                "Authorization": f"Bearer {cls.TOKEN}"
+            })
+            if search_res.status_code == 200:
+                foods = search_res.json()
+                if foods:
+                    cls.FOOD_ID_1 = foods[0]["id"]
+                    cls.FOOD_1_DATA = foods[0]
+
+    def test_baseline_100g(self):
+        """Verify that 100g returns the exact database values."""
+        payload = {"items": [{"food_id": self.FOOD_ID_1, "amount_g": 100}]}
+        with httpx.Client(timeout=30.0) as client:
+            res = client.post(f"{self.BASE_URL}/meal/calculate", json=payload, headers={
+                "Authorization": f"Bearer {self.TOKEN}"
+            })
+            self.assertEqual(res.status_code, 200)
+            data = res.json()
+            
+            # Check macros
+            self.assertEqual(data["macros"]["calories"], self.FOOD_1_DATA["calories"])
+            self.assertEqual(data["macros"]["protein_g"], self.FOOD_1_DATA["protein_g"])
+
+    def test_half_portion_50g(self):
+        """Verify that 50g returns exactly 50% of the database values."""
+        payload = {"items": [{"food_id": self.FOOD_ID_1, "amount_g": 50}]}
+        with httpx.Client(timeout=30.0) as client:
+            res = client.post(f"{self.BASE_URL}/meal/calculate", json=payload, headers={
+                "Authorization": f"Bearer {self.TOKEN}"
+            })
+            self.assertEqual(res.status_code, 200)
+            data = res.json()
+            
+            expected_cal = round(self.FOOD_1_DATA["calories"] * 0.5, 2)
+            expected_pro = round(self.FOOD_1_DATA["protein_g"] * 0.5, 2)
+            
+            self.assertEqual(data["macros"]["calories"], expected_cal)
+            self.assertEqual(data["macros"]["protein_g"], expected_pro)
+
+    def test_irregular_portion_237g(self):
+        """Verify that 237g correctly applies the ratio (2.37x)."""
+        payload = {"items": [{"food_id": self.FOOD_ID_1, "amount_g": 237}]}
+        with httpx.Client(timeout=30.0) as client:
+            res = client.post(f"{self.BASE_URL}/meal/calculate", json=payload, headers={
+                "Authorization": f"Bearer {self.TOKEN}"
+            })
+            self.assertEqual(res.status_code, 200)
+            data = res.json()
+            
+            ratio = 2.37
+            expected_cal = round(self.FOOD_1_DATA["calories"] * ratio, 2)
+            
+            self.assertEqual(data["macros"]["calories"], expected_cal)
+
+    def test_multi_item_aggregation(self):
+        """Verify that two items sum up correctly."""
+        with httpx.Client(timeout=30.0) as client:
+            # Get a second food
+            search_res = client.get(f"{self.BASE_URL}/food/search?q=Rice", headers={
+                "Authorization": f"Bearer {self.TOKEN}"
+            })
+            food2 = search_res.json()[0]
+            
+            payload = {
+                "items": [
+                    {"food_id": self.FOOD_ID_1, "amount_g": 100},
+                    {"food_id": food2["id"], "amount_g": 200}
+                ]
+            }
+            res = client.post(f"{self.BASE_URL}/meal/calculate", json=payload, headers={
+                "Authorization": f"Bearer {self.TOKEN}"
+            })
+            self.assertEqual(res.status_code, 200)
+            data = res.json()
+            
+            expected_cal = round(self.FOOD_1_DATA["calories"] + (food2["calories"] * 2.0), 2)
+            expected_weight = 100 + 200
+            
+            self.assertEqual(data["macros"]["calories"], expected_cal)
+            self.assertEqual(data["total_weight_g"], expected_weight)
+
+if __name__ == "__main__":
+    unittest.main()

+ 39 - 0
test_qwen_perf.py

@@ -0,0 +1,39 @@
+import httpx
+import time
+import json
+
+def test_performance():
+    url = "http://localhost:11434/api/chat"
+    payload = {
+        "model": "qwen3.5:9b",
+        "messages": [{"role": "user", "content": "Hi."}],
+        "stream": True
+    }
+    
+    print(f"Starting streaming test for qwen3.5:9b...")
+    start_time = time.time()
+    
+    try:
+        with httpx.Client(timeout=300.0) as client:
+            with client.stream("POST", url, json=payload) as response:
+                response.raise_for_status()
+                print("\n--- Response ---")
+                for line in response.iter_lines():
+                    if line:
+                        chunk = json.loads(line)
+                        content = chunk.get("message", {}).get("content", "")
+                        print(content, end="", flush=True)
+                        if chunk.get("done"):
+                            break
+                print("\n----------------")
+            
+            end_time = time.time()
+            duration = end_time - start_time
+            print(f"Duration: {duration:.2f} seconds")
+
+            
+    except Exception as e:
+        print(f"Error during test: {e}")
+
+if __name__ == "__main__":
+    test_performance()

+ 12 - 0
test_rag.py

@@ -0,0 +1,12 @@
+import sys
+import os
+sys.path.insert(0, '/home/roni/LocalFoodAI')
+from main import extract_food_context
+
+messages = [{"role": "user", "content": "how many calories in tuna?"}]
+context = extract_food_context(messages)
+if context:
+    print("SUCCESS: Context extracted")
+    print(context)
+else:
+    print("FAILURE: No context extracted")

+ 6 - 6
user_stories.md

@@ -61,7 +61,7 @@ _Note: Sprints 1–3 covered initial VM setup, Ollama framework installation, Go
   - **[Back] (2 pts):** Implement strict mathematical unit tests ensuring correct scaling (e.g., from base 100g to custom weights).
 
 ## 🏃 Sprint 8: Saved Lists & Combinations Management
-**Total Points: 10** | **Goal:** Implement the ability for users to save, edit, and organize their food combinations.
+**Total Points: 7** | **Goal:** Implement the ability for users to save, and organize their food combinations.
 
 - **[US-13]** As a user, I want to save a calculated food combination as a named list (e.g., "Post-Workout Smoothie").
   - **[Back] (2 pts):** Create `saved_lists` database tables and the corresponding POST API point.
@@ -69,13 +69,13 @@ _Note: Sprints 1–3 covered initial VM setup, Ollama framework installation, Go
 - **[US-14]** As a user, I want to view all my previously saved lists in a personal dashboard.
   - **[Front] (2 pts):** Implement a dashboard page executing fetches and displaying user's saved meals.
   - **[Design] (1 pt):** Design the aesthetic layout for the saved list cards.
-- **[US-15]** As a user, I want to edit or delete my saved lists to keep my preferences up to date.
-  - **[Back] (1 pt):** Implement PUT (edit) and DELETE API endpoints securely.
-  - **[Front] (2 pts):** Implement edit mode, remove buttons, and dynamic state updates across the UI.
 
-## 🏃 Sprint 9: Nutrient-Specific Sorting and Filtering
-**Total Points: 10** | **Goal:** Allow users to explore foods based on specific nutrient deficiencies or goals.
+## 🏃 Sprint 9: Nutrient-Specific Sorting and Filtering & Saved Lists Management
+**Total Points: 13** | **Goal:** Allow users to explore foods based on specific nutrient deficiencies or goals, and edit/delete saved food combinations.
 
+- **[US-15]** As a user, I want to edit or delete my saved lists to keep my preferences up to date. (Moved from Sprint 8)
+  - **[Back] (1 pt):** Implement PUT (edit) and DELETE API endpoints securely.
+  - **[Front] (2 pts):** Implement edit mode, remove buttons, and dynamic state updates across the UI.
 - **[US-16]** As a user, I want to search the database for foods high in a specific nutrient (e.g., "Foods high in Iron").
   - **[Back] (3 pts):** Build complex dynamic SQLite queries allowing deep sorting (e.g. `ORDER BY iron DESC`).
   - **[UX] (2 pts):** Design the advanced filtering UI, slider toggles, and dropdown menus.

+ 53 - 0
verify_meal_math.py

@@ -0,0 +1,53 @@
+import urllib.request
+import urllib.error
+import json
+
+def test_calculate_meal():
+    url = "http://127.0.0.1:8000/api/meal/calculate"
+    login_url = "http://127.0.0.1:8000/api/login"
+    
+    # Login
+    login_data = json.dumps({"username": "testuser", "password": "password"}).encode('utf-8')
+    req = urllib.request.Request(login_url, data=login_data, headers={'Content-Type': 'application/json'})
+    try:
+        with urllib.request.urlopen(req) as response:
+            login_resp = json.loads(response.read().decode())
+    except urllib.error.URLError as e:
+        print(f"Login failed: {e}")
+        return
+        
+    token = login_resp.get("token")
+    if not token:
+        print("No token received")
+        return
+        
+    headers = {
+        "Authorization": f"Bearer {token}",
+        "Content-Type": "application/json"
+    }
+    
+    payload = {
+        "items": [
+            {"food_id": 1, "amount_g": 200.5},
+            {"food_id": 2, "amount_g": 50}
+        ]
+    }
+    
+    data = json.dumps(payload).encode('utf-8')
+    req = urllib.request.Request(url, data=data, headers=headers)
+    
+    print("Sending payload:")
+    print(json.dumps(payload, indent=2))
+    
+    try:
+        with urllib.request.urlopen(req) as response:
+            print(f"\nResponse Code: {response.getcode()}")
+            resp_data = json.loads(response.read().decode())
+            print("Response JSON:")
+            print(json.dumps(resp_data, indent=2))
+    except urllib.error.HTTPError as e:
+        print(f"Response Error: {e.code}")
+        print(e.read().decode())
+
+if __name__ == "__main__":
+    test_calculate_meal()

+ 59 - 0
verify_validation.py

@@ -0,0 +1,59 @@
+import urllib.request
+import urllib.error
+import json
+
+def run_test(name, payload):
+    url = "http://127.0.0.1:8000/api/meal/calculate"
+    login_url = "http://127.0.0.1:8000/api/login"
+    register_url = "http://127.0.0.1:8000/api/register"
+    
+    print(f"\n--- Testing: {name} ---")
+    
+    auth_data = json.dumps({"username": "testuser", "password": "password"}).encode('utf-8')
+    
+    # Try to register first (ignore error if already exists)
+    req_reg = urllib.request.Request(register_url, data=auth_data, headers={'Content-Type': 'application/json'})
+    try:
+        urllib.request.urlopen(req_reg)
+    except Exception:
+        pass
+        
+    # Login
+    req = urllib.request.Request(login_url, data=auth_data, headers={'Content-Type': 'application/json'})
+    try:
+        with urllib.request.urlopen(req) as response:
+            token = json.loads(response.read().decode()).get("token")
+    except Exception as e:
+        print(f"Login failed: {e}")
+        return
+
+    headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
+    data = json.dumps(payload).encode('utf-8')
+    req = urllib.request.Request(url, data=data, headers=headers)
+    
+    try:
+        with urllib.request.urlopen(req) as response:
+            print(f"Status: {response.getcode()}")
+            print(f"Body: {response.read().decode()}")
+    except urllib.error.HTTPError as e:
+        print(f"Status: {e.code}")
+        print(f"Error Body: {e.read().decode()}")
+
+if __name__ == "__main__":
+    # Case 1: amount_g is 0
+    run_test("Zero Quantity", {"items": [{"food_id": 1, "amount_g": 0}]})
+    
+    # Case 2: amount_g is negative
+    run_test("Negative Quantity", {"items": [{"food_id": 1, "amount_g": -50}]})
+    
+    # Case 3: Invalid food_id
+    run_test("Invalid Food ID", {"items": [{"food_id": 999999, "amount_g": 100}]})
+    
+    # Case 4: Mixed valid and invalid IDs
+    run_test("Mixed Valid/Invalid IDs", {"items": [
+        {"food_id": 1, "amount_g": 100},
+        {"food_id": 888888, "amount_g": 200}
+    ]})
+
+    # Case 5: Valid request
+    run_test("Valid Request", {"items": [{"food_id": 1, "amount_g": 150}]})