소스 검색

Restore dashboard functionality

FerRo988 3 주 전
부모
커밋
c8cde2f5c0
15개의 변경된 파일209개의 추가작업 그리고 3698개의 파일을 삭제
  1. BIN
      .gitignore
  2. BIN
      __pycache__/database.cpython-313.pyc
  3. BIN
      __pycache__/main.cpython-313.pyc
  4. 47 0
      database.py
  5. 0 469
      database.py.final
  6. 76 30
      main.py
  7. 0 454
      main.py.final
  8. 43 0
      scratch/deploy.py
  9. 0 36
      scratch/remote_ls.py
  10. 0 3
      scratch/remote_output.txt
  11. 25 0
      start_daemon.py
  12. 18 1
      static/index.html
  13. 0 224
      static/index.html.final
  14. 0 1045
      static/script.js.final
  15. 0 1436
      static/style.css.final

BIN
.gitignore


BIN
__pycache__/database.cpython-313.pyc


BIN
__pycache__/main.cpython-313.pyc


+ 47 - 0
database.py

@@ -205,6 +205,53 @@ def delete_user_meal(user_id: int, meal_id: int) -> bool:
         if conn: conn.close()
 
 
+def get_user_meals(user_id: int) -> List[Dict[str, Any]]:
+    """Retrieve all saved meals for a user, including total nutritional calculations"""
+    conn = None
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor()
+        
+        # 1. Get all meals for this user
+        cursor.execute("SELECT id, name, created_at FROM saved_meals WHERE user_id = ? ORDER BY created_at DESC", (user_id,))
+        meals = [dict(row) for row in cursor.fetchall()]
+        
+        # 2. For each meal, get the items and calculate totals
+        for meal in meals:
+            cursor.execute('''
+                SELECT mi.amount_g, f.calories, f.protein_g, f.fat_g, f.carbs_g 
+                FROM meal_items mi
+                JOIN foods f ON mi.food_id = f.id
+                WHERE mi.meal_id = ?
+            ''', (meal['id'],))
+            
+            items = cursor.fetchall()
+            
+            # Calculate totals based on weight (database stores per 100g)
+            total_cals = 0.0
+            total_protein = 0.0
+            total_fat = 0.0
+            total_carbs = 0.0
+            
+            for item in items:
+                ratio = item['amount_g'] / 100.0
+                total_cals += item['calories'] * ratio
+                total_protein += item['protein_g'] * ratio
+                total_fat += item['fat_g'] * ratio
+                total_carbs += item['carbs_g'] * ratio
+                
+            meal['total_calories'] = round(total_cals, 1)
+            meal['total_protein_g'] = round(total_protein, 1)
+            meal['total_fat_g'] = round(total_fat, 1)
+            meal['total_carbs_g'] = round(total_carbs, 1)
+            
+        return meals
+    except Exception as e:
+        logger.error(f"Error fetching user meals: {e}")
+        return []
+    finally:
+        if conn: conn.close()
+
 
 def get_user_by_username(username: str) -> Optional[Dict[str, Any]]:
     """Retrieve user dictionary if they exist"""

+ 0 - 469
database.py.final

@@ -1,469 +0,0 @@
-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()

+ 76 - 30
main.py

@@ -2,9 +2,11 @@ import json
 import logging
 import httpx
 import bcrypt
+import asyncio
 from contextlib import asynccontextmanager
+from concurrent.futures import ThreadPoolExecutor
 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, update_user_meal, delete_user_meal
+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, get_user_meals
 from fastapi.responses import HTMLResponse, StreamingResponse
 from fastapi.staticfiles import StaticFiles
 from pydantic import BaseModel
@@ -13,9 +15,19 @@ from typing import List, Generator, Optional
 logging.basicConfig(level=logging.INFO)
 logger = logging.getLogger(__name__)
 
+executor = ThreadPoolExecutor(max_workers=4)
+
 @asynccontextmanager
 async def lifespan(app: FastAPI):
     create_tables()
+    try:
+        async with httpx.AsyncClient() as client:
+            await client.post("http://localhost:11434/api/generate", 
+                json={"model": MODEL_NAME, "prompt": "", "keep_alive": "10m"},
+                timeout=60.0)
+        logger.info(f"Modèle {MODEL_NAME} pré-chargé en mémoire")
+    except Exception as e:
+        logger.warning(f"Impossible de pré-charger le modèle: {e}")
     yield
 
 app = FastAPI(title="LocalFoodAI Chat", lifespan=lifespan)
@@ -83,45 +95,66 @@ def extract_food_context(messages: list) -> str | None:
     
     if not keywords:
         return None
-    
+
+    recipe_keywords = ["recipe", "recette", "cook", "cuisiner", "plat", "dish", "make", "prepare"]
+    is_recipe_request = any(kw in last_user_msg.lower() for kw in recipe_keywords)
+
     # 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]:
+    # For recipes, scan more keywords to cover all ingredients
+    keyword_limit = 10 if is_recipe_request else 2
+    for kw in keywords[:keyword_limit]:
         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:
+        if not is_recipe_request and 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.",
+        "If a food item is not found in the local data below, use your training knowledge but clearly mark those values as '(approx.)'.",
+    ]
+
+    if is_recipe_request:
+        lines += [
+            "The user wants a recipe. You MUST follow this exact structure:",
+            "1. ### 🍽️ [Suggested dish name]",
+            "2. **Total nutritional values of the dish** : a markdown table with Cal, Protein, Fat, Carbs, Fiber.",
+            "3. **Recipe** : simple and clear steps.",
+            "4. **Suggestions** : 0 to 2 tips such as 'if you add X, you will get more Y' or 'you can also cook X this way'.",
+            "Use ONLY the nutritional data provided below to calculate the final values.",
+        ]
+    else:
+        lines += [
+            "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.",
+        ]
+
+    lines += [
         "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)
+
+    if found_items:
+        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)
 
@@ -223,12 +256,15 @@ async def chat_endpoint(request: ChatRequest, current_user: dict = Depends(get_c
     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'])
-    
+        asyncio.get_event_loop().run_in_executor(
+            executor, save_chat_message, current_user['id'], 'user', messages[-1]['content']
+        )
+
     # --- TG-35: Local SQL RAG Enrichment ---
-    db_context = extract_food_context(messages)
+    loop = asyncio.get_event_loop()
+    db_context = await loop.run_in_executor(executor, 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
@@ -240,14 +276,17 @@ async def chat_endpoint(request: ChatRequest, current_user: dict = Depends(get_c
         "model": MODEL_NAME,
         "messages": messages,
         "stream": True,
-        "think": False  # Disable reasoning/thinking mode for faster responses
+        "think": False,
+        "options": {
+            "num_predict": 512,
+            "temperature": 0.7,
+        }
     }
     
     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()
@@ -268,9 +307,10 @@ async def chat_endpoint(request: ChatRequest, current_user: dict = Depends(get_c
                             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)
+                asyncio.get_event_loop().run_in_executor(
+                    executor, save_chat_message, current_user['id'], 'assistant', bot_full_response
+                )
                 
         except Exception as e:
             logger.exception(f"Unexpected error during chat stream: {e}")
@@ -406,6 +446,12 @@ async def calculate_meal(request: MealCalculateRequest, current_user: dict = Dep
             
     return totals
 
+@app.get("/api/meals")
+async def get_meals(current_user: dict = Depends(get_current_user)):
+    """Retrieve all saved meals for the authenticated user"""
+    meals = get_user_meals(current_user['id'])
+    return {"status": "success", "meals": meals}
+
 @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"""
@@ -445,4 +491,4 @@ async def delete_meal(meal_id: int, current_user: dict = Depends(get_current_use
 
 if __name__ == "__main__":
     import uvicorn
-    uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
+    uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)

+ 0 - 454
main.py.final

@@ -1,454 +0,0 @@
-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)

+ 43 - 0
scratch/deploy.py

@@ -0,0 +1,43 @@
+import paramiko
+import os
+import textwrap
+
+def deploy():
+    host = "192.168.130.171"
+    user = "roni"
+    pw = "BTSai123"
+    try:
+        client = paramiko.SSHClient()
+        client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+        client.connect(host, username=user, password=pw)
+        
+        print("Uploading files via SFTP...")
+        sftp = client.open_sftp()
+        sftp.put("main.py", "/home/roni/LocalFoodAI/main.py")
+        sftp.put("database.py", "/home/roni/LocalFoodAI/database.py")
+        sftp.put("start_daemon.py", "/home/roni/LocalFoodAI/start_daemon.py")
+        sftp.put("static/script.js", "/home/roni/LocalFoodAI/static/script.js")
+        sftp.put("static/index.html", "/home/roni/LocalFoodAI/static/index.html")
+        sftp.close()
+        print("Upload complete.")
+        
+        cmd = """cd ~/LocalFoodAI
+pkill -f "main.py" || true
+/home/roni/LocalFoodAI/venv/bin/python3 start_daemon.py
+"""
+        
+        cmd = cmd.replace('\r\n', '\n')
+        print(f"Running:\n{cmd}")
+        stdin, stdout, stderr = client.exec_command(cmd)
+        out = stdout.read().decode()
+        err = stderr.read().decode()
+        
+        if out: print(f"OUT:\n{out}")
+        if err: print(f"ERR:\n{err}")
+        
+        client.close()
+    except Exception as e:
+        print(f"Error: {e}")
+
+if __name__ == "__main__":
+    deploy()

+ 0 - 36
scratch/remote_ls.py

@@ -1,36 +0,0 @@
-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}")

+ 0 - 3
scratch/remote_output.txt

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

+ 25 - 0
start_daemon.py

@@ -0,0 +1,25 @@
+import subprocess
+import sys
+import os
+
+def main():
+    venv_python = "/home/roni/LocalFoodAI/venv/bin/python3"
+    log_file = "/home/roni/LocalFoodAI/server.log"
+    main_py = "/home/roni/LocalFoodAI/main.py"
+    
+    # Check if files exist
+    if not os.path.exists(venv_python):
+        print(f"Error: Python venv not found at {venv_python}")
+        sys.exit(1)
+    if not os.path.exists(main_py):
+        print(f"Error: main.py not found at {main_py}")
+        sys.exit(1)
+        
+    print(f"Starting main.py with daemon wrapper...")
+    with open(log_file, "a") as f:
+        # start_new_session=True detaches the child process from the SSH session
+        subprocess.Popen([venv_python, main_py], stdout=f, stderr=f, start_new_session=True, cwd="/home/roni/LocalFoodAI")
+    print("Daemon successfully spawned main.py process.")
+
+if __name__ == "__main__":
+    main()

+ 18 - 1
static/index.html

@@ -20,6 +20,7 @@
             </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>
@@ -83,7 +84,10 @@
                             </div>
                         </div>
                     </div>
-                    <button id="generate-recipe-btn" class="primary-btn-sm">🍲 Generate Recipe Prompt</button>
+                    <div style="display: flex; gap: 10px;">
+                        <button id="save-meal-btn" class="secondary-btn-sm">💾 Save Meal</button>
+                        <button id="generate-recipe-btn" class="primary-btn-sm">🍲 Generate Recipe Prompt</button>
+                    </div>
                 </div>
             </div>
         </div>
@@ -192,6 +196,19 @@
         </form>
     </div>
 
+    <!-- Dashboard Overlay -->
+    <div class="dashboard-overlay" id="dashboard-overlay" style="display: none;">
+        <div class="dashboard-content">
+            <div class="dashboard-header">
+                <h2>My Saved Meals</h2>
+                <button class="close-btn" id="close-dashboard-btn">&times;</button>
+            </div>
+            <div class="dashboard-body" id="dashboard-body">
+                <!-- Meals populated by JS -->
+            </div>
+        </div>
+    </div>
+
     <script src="/static/script.js"></script>
 </body>
 </html>

+ 0 - 224
static/index.html.final

@@ -1,224 +0,0 @@
-<!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>

+ 0 - 1045
static/script.js.final

@@ -1,1045 +0,0 @@
-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);
-        });
-    }
-});

+ 0 - 1436
static/style.css.final

@@ -1,1436 +0,0 @@
-: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;
-}