Explorar el Código

TG-10: Implement chat UI and local user registration with auth gateway

FerRo988 hace 4 días
padre
commit
b0d53236af
Se han modificado 8 ficheros con 631 adiciones y 4 borrados
  1. 1 1
      PROJECT_CONTEXT.md
  2. 69 0
      database.py
  3. 50 1
      main.py
  4. 2 0
      requirements.txt
  5. 48 2
      static/index.html
  6. 141 0
      static/script.js
  7. 193 0
      static/style.css
  8. 127 0
      user_stories.md

+ 1 - 1
PROJECT_CONTEXT.md

@@ -7,7 +7,7 @@ LocalFoodAI is a local food AI that provides complete nutritional information on
 - **Operating System:** Ubuntu 24.04 (VM)
 - **Backend:** Python 3.11+
 - **Database:** SQLite (local, no cloud)
-- **Local LLM:** Llama 3.1 8B (quantized via Ollama, Q4_K_M or equivalent)
+- **Local LLM:** Qwen 3.5 9B (quantized via Ollama, Q4_K_M or equivalent)
   - CPU-only compatible
   - Fits in 30 GB RAM with quantization
   - Instruction-following tuned

+ 69 - 0
database.py

@@ -0,0 +1,69 @@
+import sqlite3
+import os
+import logging
+from typing import Optional, Dict, Any
+
+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():
+    conn = sqlite3.connect(DB_PATH)
+    conn.row_factory = sqlite3.Row
+    return conn
+
+def create_tables():
+    """Initialize the SQLite database with required tables"""
+    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
+        )
+        ''')
+        
+        conn.commit()
+        conn.close()
+        logger.info("Database and tables initialized successfully.")
+    except Exception as e:
+        logger.error(f"Error initializing database: {e}")
+        raise
+
+def get_user_by_username(username: str) -> Optional[Dict[str, Any]]:
+    """Retrieve user dictionary if they exist"""
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor()
+        cursor.execute("SELECT * FROM users WHERE username = ?", (username,))
+        row = cursor.fetchone()
+        conn.close()
+        return dict(row) if row else None
+    except Exception as e:
+        logger.error(f"Database error fetching user: {e}")
+        return None
+
+def create_user(username: str, password_hash: str) -> bool:
+    """Creates a user securely. Returns True if successful, False if username exists."""
+    try:
+        conn = get_db_connection()
+        cursor = conn.cursor()
+        cursor.execute(
+            "INSERT INTO users (username, password_hash) VALUES (?, ?)",
+            (username, password_hash)
+        )
+        conn.commit()
+        conn.close()
+        return True
+    except sqlite3.IntegrityError:
+        # Prevent duplicate usernames explicitly
+        return False
+    except Exception as e:
+        logger.error(f"Database error during user creation: {e}")
+        raise

+ 50 - 1
main.py

@@ -1,7 +1,10 @@
 import json
 import logging
 import httpx
+from contextlib import asynccontextmanager
 from fastapi import FastAPI, HTTPException
+from database import create_tables, create_user, get_user_by_username
+from passlib.context import CryptContext
 from fastapi.responses import HTMLResponse, StreamingResponse
 from fastapi.staticfiles import StaticFiles
 from pydantic import BaseModel
@@ -10,7 +13,28 @@ from typing import List, Generator
 logging.basicConfig(level=logging.INFO)
 logger = logging.getLogger(__name__)
 
-app = FastAPI(title="LocalFoodAI Chat")
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+    create_tables()
+    yield
+
+app = FastAPI(title="LocalFoodAI Chat", lifespan=lifespan)
+
+pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
+
+def get_password_hash(password):
+    return pwd_context.hash(password)
+
+def verify_password(plain_password, hashed_password):
+    return pwd_context.verify(plain_password, hashed_password)
+
+class UserCreate(BaseModel):
+    username: str
+    password: str
+
+class UserLogin(BaseModel):
+    username: str
+    password: str
 
 OLLAMA_URL = "http://localhost:11434/api/chat"
 MODEL_NAME = "llama3.1:8b"
@@ -34,6 +58,31 @@ async def read_root():
     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)
+    success = create_user(user.username.strip(), hashed_password)
+    if not success:
+        raise HTTPException(status_code=400, detail="Username already exists")
+    
+    return {"message": "User registered successfully"}
+
+@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")
+    
+    return {"status": "success", "username": db_user["username"]}
+
 @app.post("/chat")
 async def chat_endpoint(request: ChatRequest):
     """Proxy chat requests to the local Ollama instance with streaming support"""

+ 2 - 0
requirements.txt

@@ -1,3 +1,5 @@
 fastapi>=0.100.0
 uvicorn>=0.23.0
 httpx>=0.24.0
+passlib[bcrypt]>=1.7.4
+bcrypt>=4.0.1

+ 48 - 2
static/index.html

@@ -9,16 +9,18 @@
     <link rel="stylesheet" href="/static/style.css">
 </head>
 <body>
-    <div class="app-container">
+    <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"></span><span class="status-text">Local LLM Ready</span>
+                    <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-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>
@@ -45,6 +47,50 @@
         </footer>
     </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>

+ 141 - 0
static/script.js

@@ -187,6 +187,147 @@ document.addEventListener('DOMContentLoaded', () => {
         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');
+    if (savedUser) {
+        setLoggedInState(savedUser);
+    }
+
+    function setLoggedInState(username) {
+        localStorage.setItem('localFoodUser', username);
+        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);
+    }
+
+    function setLoggedOutState() {
+        localStorage.removeItem('localFoodUser');
+        chatApp.style.display = 'none';
+        chatApp.classList.remove('fade-in');
+        
+        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);
+            }
+        } 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(username);
+                }, 1000);
+            }
+        } catch (err) {
+            regError.textContent = 'Server error. Please try again later.';
+        } finally {
+            submitBtn.disabled = false;
+        }
+    });
+
     // Initialize state
     sendBtn.disabled = true;
 });

+ 193 - 0
static/style.css

@@ -287,3 +287,196 @@ textarea::placeholder {
     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);
+}
+

+ 127 - 0
user_stories.md

@@ -0,0 +1,127 @@
+# User Stories & Taiga Sprint Plan (Sprints 4 - 13)
+
+**Vision Statement:**
+A local food AI that provides full nutritional value information on any food and can generate complete menu proposals based on the user's specification.
+
+_Note: Sprints 1–3 covered initial VM setup, Ollama framework installation, Gogs repository creation, and the first AI chat interface prototype. Each Sprint below totals exactly 10 points._
+
+---
+
+## 🏃 Sprint 4: User Accounts & Core Authentication
+**Total Points: 10** | **Goal:** Establish a secure, fully local user authentication system so user data can be saved locally.
+
+- **[US-01]** As a new user, I want to create an account with a username and password so that I can personalize my experience.
+  - **[Back] (3 pts):** Create SQLite users table, password hashing logic, and token API.
+  - **[Front] (2 pts):** Implement register and login forms routing to the API.
+  - **[UX] (2 pts):** Design the login/register workflow and error state alerts.
+- **[US-02]** As a returning user, I want to log in securely so that I can access my saved food data.
+  - **[Back] (1 pt):** Implement session management and JWT token validation.
+  - **[Front] (1 pt):** Handle session persistence in the browser via localStorage/cookies.
+- **[US-03]** As an administrator, I want to ensure all credentials are encrypted.
+  - **[Back] (1 pt):** Implement local-only security configurations preventing external access.
+
+## 🏃 Sprint 5: Local Food Database Setup & Basic Search
+**Total Points: 10** | **Goal:** Integrate a local database of food and nutritional info fitting CPU/RAM constraints.
+
+- **[US-04]** As a DB admin, I want to seed a local SQLite database with core food items.
+  - **[Back] (4 pts):** Parse an open dataset (USDA/OpenFoodFacts subset), design the SQLite schema, and import data cleanly.
+- **[US-05]** As a user, I want to search for a specific food item by name.
+  - **[Back] (2 pts):** Create a fast fuzzy-matching API endpoint `GET /api/food/search`.
+  - **[Front] (2 pts):** Implement the search bar component with real-time fetch autocomplete.
+- **[US-06]** As a developer, I want to connect the AI logic to the local database securely.
+  - **[Back] (2 pts):** Equip the Llama 3.1 8B agent with local SQL lookup tools so it can query the DB.
+
+## 🏃 Sprint 6: Comprehensive Nutritional Information
+**Total Points: 10** | **Goal:** Expose deep nutritional data (macros, minerals, vitamins, amino acids) to the user.
+
+- **[US-07]** As a user, I want to view complete macronutrient information for any specified food.
+  - **[Front] (2 pts):** Build the macro data UI components rendering calories, carbs, protein, and fat.
+  - **[Design] (2 pts):** Design beautiful, responsive nutrient breakdown cards/charts.
+- **[US-08]** As a user, I want to expand detailed nutritional info to see micronutrients.
+  - **[Front] (2 pts):** Build an expandable, detailed view table for vitamins, minerals, and amino acids.
+  - **[Back] (2 pts):** Ensure the API properly aggregates and serves all extended JSON fields.
+- **[US-09]** As the AI, I want to extract and summarize this complete nutritional profile accurately.
+  - **[Back] (2 pts):** Build a specific prompt template that forces the AI to summarize DB outputs without hallucinating data.
+
+## 🏃 Sprint 7: Food Combinations & Aggregations
+**Total Points: 10** | **Goal:** Allow users to build a "meal" by entering multiple foods and getting aggregate nutritional totals.
+
+- **[US-10]** As a user, I want to enter quantities of several foods at once to calculate an entire meal.
+  - **[Front] (2 pts):** Build a dynamic form field enabling adding/removing multiple food items and gram inputs.
+  - **[UX] (1 pt):** Layout the intuitive "meal builder" interface seamlessly.
+- **[US-11]** As a user, I want the system to calculate and display the total combined nutritional value.
+  - **[Back] (3 pts):** Create POST endpoint multiplying weights and summing macro/micro totals.
+  - **[Front] (2 pts):** Dynamically render the totals component with the derived data on-the-fly.
+- **[US-12]** As a developer, I want to ensure the mathematical aggregations account for varying portion sizes uniformly.
+  - **[Back] (2 pts):** Implement strict mathematical unit tests ensuring correct scaling (e.g., from base 100g to custom weights).
+
+## 🏃 Sprint 8: Saved Lists & Combinations Management
+**Total Points: 10** | **Goal:** Implement the ability for users to save, edit, and organize their food combinations.
+
+- **[US-13]** As a user, I want to save a calculated food combination as a named list (e.g., "Post-Workout Smoothie").
+  - **[Back] (2 pts):** Create `saved_lists` database tables and the corresponding POST API point.
+  - **[Front] (2 pts):** Add "Save Meal" button and a name entry modal component.
+- **[US-14]** As a user, I want to view all my previously saved lists in a personal dashboard.
+  - **[Front] (2 pts):** Implement a dashboard page executing fetches and displaying user's saved meals.
+  - **[Design] (1 pt):** Design the aesthetic layout for the saved list cards.
+- **[US-15]** As a user, I want to edit or delete my saved lists to keep my preferences up to date.
+  - **[Back] (1 pt):** Implement PUT (edit) and DELETE API endpoints securely.
+  - **[Front] (2 pts):** Implement edit mode, remove buttons, and dynamic state updates across the UI.
+
+## 🏃 Sprint 9: Nutrient-Specific Sorting and Filtering
+**Total Points: 10** | **Goal:** Allow users to explore foods based on specific nutrient deficiencies or goals.
+
+- **[US-16]** As a user, I want to search the database for foods high in a specific nutrient (e.g., "Foods high in Iron").
+  - **[Back] (3 pts):** Build complex dynamic SQLite queries allowing deep sorting (e.g. `ORDER BY iron DESC`).
+  - **[UX] (2 pts):** Design the advanced filtering UI, slider toggles, and dropdown menus.
+- **[US-17]** As a user, I want the resulting list of foods to be sortable interactively.
+  - **[Front] (2 pts):** Implement an interactive data table structure allowing instant column sorting.
+- **[US-18]** As the AI, I want to be able to generate natural language explanations of these sortable lists.
+  - **[Back] (3 pts):** Pass the top sorted database vectors to the LLM context to formulate explanations for "why these foods fit the goal."
+
+## 🏃 Sprint 10: Local Web Search Integration (SearXNG)
+**Total Points: 10** | **Goal:** Give the AI the ability to securely and anonymously search the broader web when local data is missing.
+
+- **[US-19]** As a system administrator, I want to deploy the SearXNG local web search tool on the VM.
+  - **[Back] (3 pts):** Install and configure the SearXNG instance on the Ubuntu 24.04 VM locally.
+- **[US-20]** As the AI, I want to autonomously query SearXNG if the local DB lacks information on a food.
+  - **[Back] (4 pts):** Implement advanced LangChain custom Tool logic giving parsing ability to Llama 3.1.
+- **[US-21]** As a user, I want the AI to fetch and summarize nutritional info on niche foods from the web anonymously.
+  - **[Back] (2 pts):** Scrape and sanitize the SearXNG website text inputs before feeding them to the LLM to prevent injection.
+  - **[Front] (1 pt):** Display an elegant "Web Search Active" UI indicator so the user understands minor delays.
+
+## 🏃 Sprint 11: AI Menu Proposals & Constraint Logic
+**Total Points: 10** | **Goal:** Leverage the LLM to creatively generate menus that strictly respect nutritional limits and allergies.
+
+- **[US-22]** As a user, I want to ask the AI for a full daily menu proposal that hits specific nutritional targets.
+  - **[Back] (3 pts):** Engineer complex multi-prompt logic uniting daily kcal targets with database selections.
+  - **[Design] (2 pts):** Design a visually distinct "Menu Plan" view (e.g., split into breakfast, lunch, dinner cards).
+- **[US-23]** As a user, I want to specify constraints such as food allergies (e.g., structurally no peanuts).
+  - **[Front] (2 pts):** Add an intuitive "Allergies/Diet Preferences" settings page.
+  - **[Back] (2 pts):** Modify database query architecture to absolutely EXCLUDE flagged ingredients via SQL safety before the LLM step.
+- **[US-24]** As a developer, I want to guarantee unsafe ingredients are filtered out globally.
+  - **[Back] (1 pt):** Write automated unit testing verifying that constraints heavily reject prohibited allergens.
+
+## 🏃 Sprint 12: General Nutrition Chat & AI Refinement
+**Total Points: 10** | **Goal:** Polish the conversational capabilities, lock down topics, and refine the interface.
+
+- **[US-25]** As a user, I want to chat freely about any nutrition-related topic and get competent, science-backed answers.
+  - **[Front] (2 pts):** Enhance HTML/JS chat UI to reliably handle deeply nested long-form threads and scrolling.
+  - **[Back] (2 pts):** Implement rolling conversational memory tracking via database records for session context.
+- **[US-26]** As a developer, I want to refine system prompting so the LLM strictly refuses non-nutrition topics.
+  - **[Back] (3 pts):** Build system Guardrails and robust boundary prompts locking behavior to nutrition.
+- **[US-27]** As a user, I want the AI's responses beautifully formatted in markdown.
+  - **[Front] (2 pts):** Integrate a safe Markdown rendering library (like marked.js) handling bolding, lists, and tables seamlessly.
+  - **[Design] (1 pt):** Style specific markdown elements cleanly inside the message chat bubbles.
+
+## 🏃 Sprint 13: Deployment Polish & Open Source Handover
+**Total Points: 10** | **Goal:** Finalize the codebase for public access, clear confidential keys, and organize the Git repo on Gogs.
+
+- **[US-28]** As a developer, I want to review the entire codebase for confidential data.
+  - **[Back] (3 pts):** Implement strict `python-dotenv` `.env` logic and build comprehensive `.gitignore` rules cleaning the repo.
+- **[US-29]** As an admin, I want to add my teacher (id: evegi144) to the Gogs repo and freeze visual assets.
+  - **[Back] (1 pt):** Process Git permission tasks natively on `git.btshub.lu`.
+  - **[UX] (1 pt):** Do a full-application UX pass checking spacing, margins, and fixing responsive mobile bugs.
+- **[US-30]** As an open-source user, I want a clear setup script and guide so I can clone and run LocalFoodAI securely.
+  - **[Back] (3 pts):** Write an automated bash `setup.sh` that checks system bounds (8 vCPU / 30GB limit), initiates a Python environment, and downloads the local LLM.
+  - **[Back] (2 pts):** Author an exhaustive `README.md` containing full instructions and architecture blueprints.