Преглед изворни кода

TG-221 #closed - some file change due to refresh of md files.

Lange François пре 1 месец
родитељ
комит
5d8f37fa06

+ 10 - 10
.agents/workflows/taiga-commit.md

@@ -1,11 +1,11 @@
-# Taiga Commit Workflow
-Description: Garantit que chaque commit contient l'ID Taiga pour la mise à jour automatique.
-
-### Étapes :
-1. **Analyse** : Analyse les fichiers modifiés (`git status`).
-2. **Demande d'ID** : Demande à l'utilisateur : "Quel est l'ID de la tâche Taiga (ex: 123) et le nouveau statut (ex: closed) ?"
-3. **Génération** : Génère un message de commit qui inclut obligatoirement le tag `TG-<ID> #<STATUS>`.
-4. **Exécution** : 
-   - `git add .`
-   - `git commit -m "TG-<ID> #<STATUS> - [Description concise des changements]"`
+# Taiga Commit Workflow
+Description: Garantit que chaque commit contient l'ID Taiga pour la mise à jour automatique.
+
+### Étapes :
+1. **Analyse** : Analyse les fichiers modifiés (`git status`).
+2. **Demande d'ID** : Demande à l'utilisateur : "Quel est l'ID de la tâche Taiga (ex: 123) et le nouveau statut (ex: closed) ?"
+3. **Génération** : Génère un message de commit qui inclut obligatoirement le tag `TG-<ID> #<STATUS>`.
+4. **Exécution** : 
+   - `git add .`
+   - `git commit -m "TG-<ID> #<STATUS> - [Description concise des changements]"`
    - `git push`

+ 110 - 110
Final_Presentation.html

@@ -1,110 +1,110 @@
-
-<!DOCTYPE html>
-<html>
-<head>
-    <meta charset="utf-8">
-    <title>Customer Presentation</title>
-    <style>
-        body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; line-height: 1.6; color: #333; max-width: 900px; margin: 0 auto; padding: 2rem; }
-        h1 { color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 10px; }
-        h2 { color: #2980b9; margin-top: 2rem; }
-        h3 { color: #16a085; }
-        table { border-collapse: collapse; width: 100%; margin-bottom: 2rem; }
-        th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
-        th { background-color: #f2f2f2; color: #333; }
-        @media print {
-            body { padding: 0; max-width: 100%; }
-            hr { page-break-after: always; border: 0; }
-        }
-    </style>
-</head>
-<body>
-    <div style="text-align:center; margin-bottom: 3rem;">
-        <h1 style="border: none;">Clinical Food AI Platform</h1>
-        <p><strong>Master Deliverable Overview</strong></p>
-    </div>
-    <h1>🚀 Executive Project Update: Local Food AI Platform</h1>
-<p><strong>To Our Valued Client,</strong></p>
-<p>We are thrilled to present the monumental progress achieved in the <strong>Local Food AI Platform</strong>. Your investment has successfully funded the transition of a conceptual idea into a highly secure, enterprise-grade Artificial Intelligence ecosystem. </p>
-<p>Below is an executive summary of the value delivered during our most recent development cycles:</p>
-<h2>🏦 1. Total Data Sovereignty &amp; Security</h2>
-<p>We have engineered an architecture that guarantees <strong>100% Data Privacy</strong>. Unlike consumer AI tools that leak confidential queries to the cloud:
-* <strong>True Local Intelligence:</strong> The Mistral AI neural network and your massive MySQL databases run entirely on isolated, air-gapped internal servers. No recipe, no search query, and no user profile ever leaves your corporate firewall.
-* <strong>Encrypted Access:</strong> We deployed heavy <code>bcrypt</code> cryptographic hashing to secure every user account against breaches.</p>
-<h2>🧠 2. Autonomous Web Intelligence (SearXNG)</h2>
-<p>To ensure the AI is never outdated, we successfully deployed an anonymous Docker-based metasearch proxy. If a user asks the AI about a brand-new medical ingredient not present in your databases, the AI recognizes the gap autonomously, covertly scrapes the internet without tracking, and instantly incorporates the live data to answer the question!</p>
-<h2>🔬 3. The "Scientific Medical" User Interface</h2>
-<p>We completely overhauled the front-end user experience to reflect luxury and scientific precision. </p>
-<p><img alt="Premium UI Dashboard Visualization" src="file:///C:/Users/lanfr144/.gemini/antigravity/brain/fa60b8a2-c1d5-4b3d-8ff2-f6588c78798f/premium_nutrition_dashboard_ui_1776925129649.png" /></p>
-<ul>
-<li><strong>Dynamic 'My Plate' Architecture:</strong> Users can dynamically combine ingredients from a database of millions of entries. Our backend calculates compounding macro-totals (Protein, Fat, Carbs) in real-time, functioning as an enterprise diet tracker.</li>
-<li><strong>Granular Data Search:</strong> The platform boasts high-speed filtration algorithms, allowing practitioners to search exactly for criteria like <em>"Products with &gt; 20g Protein and &lt; 5g Sugar"</em>.</li>
-</ul>
-<h2>🤖 4. The Prompt-Engineered Dietitian</h2>
-<p>Most chatbots simply "talk". We implemented complex algorithmic <em>Prompt Engineering</em> to force the AI into acting as a highly structured Clinical Dietitian. The system now mathematically generates highly accurate, multi-day meal plans mapped directly to exact caloric and dietary constraints (Vegan, Keto, Omnivore) and outputs them strictly as professional Markdown data tables instead of loose text.</p>
-<hr />
-<p><strong>Return on Investment (ROI):</strong> 
-Your financing has birthed a fully-scalable, premium-designed, highly secure platform capable of replacing thousands of dollars in cloud API costs while protecting intellectual property. The system is ready to revolutionize local nutritional analysis pipelines.</p>
-<hr />
-<h1>🏆 Synthèse Agile &amp; Wiki SCRUM</h1>
-<p>Voici le compte-rendu officiel du projet <strong>Local Food AI</strong>, structuré pour répondre aux exigences des rituels Scrum (Daily, Review, Planning) et pour alimenter directement votre Wiki Taiga.</p>
-<hr />
-<h2>1. 🌅 Le Daily (Où en sommes-nous ?)</h2>
-<p><strong>Statut Actuel :</strong> 
-Le socle applicatif est à 90% terminé. L'infrastructure de base (MySQL, Ubuntu, Docker, Ollama) est parfaitement stable, le pipeline d'intégration Git/Taiga via Webhook est fonctionnel, et l'interface utilisateur (UI) vient de subir une refonte technologique massive. Il ne reste techniquement qu'une seule "Epic/User Story" majeure dans notre Backlog.</p>
-<hr />
-<h2>2. 🔍 La Sprint Review (Qu'avons-nous fait hier ?)</h2>
-<p>Lors du dernier Sprint de développement continu, nous avons validé les User Stories <strong>#5, #6, #7, et #8</strong>. </p>
-<p><strong>Réalisations Techniques et Démontrables :</strong>
-* <strong>Refonte "Scientific Medical" (Frontend) :</strong> Injection de CSS avancé dans <code>app.py</code> pour basculer Streamlit vers un design "Dark Mode" Premium, utilisant la police Inter, des dégradés bleus/cyan, et des effets "Glassmorphism".
-* <strong>Filtres Avancés (SQL/Backend) :</strong> Création de 4 sliders interactifs (Protéines, Lipides, Glucides, Sucres) modifiant dynamiquement la clause <code>WHERE ... AND protéines &gt;= X</code> de la base MySQL.
-* <strong>Architecture "My Plate" (Database) :</strong> Modification sécurisée de <code>setup_db.py</code> pour générer automatiquement deux nouvelles tables relationnelles (<code>plates</code> et <code>plate_items</code>). Ces tables utilisent des clefs étrangères (Foreign Keys) pour lier les aliments directement au <code>user_id</code> de la session.
-* <strong>Algorithme d'Agrégation (Logique Data) :</strong> Intégration d'une logique en Python/Pandas calculant et additionnant instantanément les macros (Protéines, Graisses, Carbs) de tous les aliments présents dans une assiette virtuelle.
-* <em>Toutes ces modifications ont été commitées sur Gogs avec succès, déclenchant le Webhook vers Taiga (Tasks #23, #24, #26, #27).</em></p>
-<hr />
-<h2>3. 🎯 Le Sprint Planning (Qu'allons-nous faire ?)</h2>
-<p><strong>Prochain Objectif :</strong> Construire la <strong>User Story #11 (AI Menu Proposals)</strong>.</p>
-<p><strong>Tâches prévues (Sprint Backlog) :</strong>
-1. Créer une nouvelle section/tab dans le code pour la génération de menus.
-2. Concevoir un algorithme de "Prompt Engineering" très spécifique qui imposera à <strong>Mistral</strong> des contraintes strictes.
-3. Câbler la demande de l'utilisateur (ex: "Je veux un menu à 2000 kcal riche en protéines") avec la base de données SQL locale pour fournir de vrais exemples au LLM, afin qu'il propose un menu concret et non inventé.
-4. Finaliser les play-tests finaux sur la VM Ubuntu.</p>
-<hr />
-<h2>4. 📚 Ce que tu dois mettre dans le Wiki SCRUM (Taiga)</h2>
-<p>Copiez-collez ces blocs dans votre Wiki Taiga pour prouver la maîtrise technique du projet :</p>
-<h3>🏛️ Architecture &amp; Technologies</h3>
-<ul>
-<li><strong>Frontend :</strong> Framework <strong>Streamlit</strong> (Python) surchargé par du CSS natif injecté via <code>st.markdown(unsafe_allow_html=True)</code> pour garantir une esthétique "Scientific Medical" (Focalisation UX/UI Premium).</li>
-<li><strong>Backend Intelligence :</strong> Intégration native de l'API <strong>Ollama (modèle Mistral)</strong> avec le concept de <em>Tool/Function Calling</em> pour scraper anonymement le Web via un conteneur local <strong>SearXNG</strong> sur le port <code>8080</code>.</li>
-<li><strong>Database Pipeline :</strong> Injection dynamique et asynchrone des données CSV ouvertes via Pandas vers MySQL. Abandon des schémas SQL rigides au profit de l'auto-génération des 200 colonnes via l'ORM.</li>
-<li><strong>Sécurité &amp; Accès :</strong> Mise en place d'un modèle <strong>PoLP</strong> (Principle of Least Privilege). L'application gère nativement le HMAC (via <code>bcrypt</code>) et le script <code>setup_db.py</code> octroie des droits granulaires (ex: <code>IDENTIFIED BY ... GRANT SELECT, INSERT... TO 'db_app_auth'</code>).</li>
-</ul>
-<h3>🔄 DevOps &amp; Déploiement</h3>
-<ul>
-<li>Le CI/CD rudimentaire repose sur une intégration <strong>Gogs -&gt; Taiga</strong>. Chaque commit (ex: <code>TG-23</code>) documente automatiquement la carte Agile via Webhook.</li>
-<li>Le système est déployable via le script unifié <code>deploy.sh</code> (qui gère l'environnement virtuel Python) et <code>setup_searxng.sh</code> (qui gère l'orchestration Docker).</li>
-</ul>
-<hr />
-<h1>Agile Sprint Retrospective</h1>
-<p><strong>Project:</strong> Local Food AI Platform
-<strong>Sprint Goal:</strong> Secure Data Ingestion, Medical Expansion, and UI/UX Overhaul</p>
-<h2>🏆 What Went Well</h2>
-<ul>
-<li><strong>Database Agility:</strong> Transitioning from rigid SQL arrays to dynamic pandas DataFrame ingestion (<code>ingest_csv.py</code>) allowed us to process massive OpenFoodFacts schemas instantly without crashing.</li>
-<li><strong>Privacy-First Architecture:</strong> Successfully establishing an air-gapped system where the AI scraper (SearXNG) and the Large Language Model (Mistral) operate entirely locally proves extreme Corporate Data Sovereignty.</li>
-<li><strong>Rapid Feature Integration:</strong> Expanding the platform from a simple calculator to a full-fledged Clinical Profiler (incorporating Diabetes, Hypertension, and Pregnancy monitoring) was achieved incredibly fast using Pandas styling logic.</li>
-</ul>
-<h2>🚧 What Went Wrong (Or Needed Improvement)</h2>
-<ul>
-<li><strong>Dataset Encoding Bugs:</strong> The OpenFoodFacts CSV files contain heavy French datasets. Early ingestion attempts on Windows corrupted characters (<code>'Artichaut' -&gt; 'Artichaut'</code>) due to OS-default rendering limitations over <code>utf-8</code>. This required an urgent hotfix in the data pipeline.</li>
-<li><strong>Schema Scalability:</strong> Constantly injecting new tables (<code>plates</code>, <code>user_profiles</code>) into <code>setup_db.py</code> without a formal migration tool (like Alembic) makes iterative DevOps slightly dangerous for live production data.</li>
-</ul>
-<h2>🎯 Action Items for Next Sprint</h2>
-<ul>
-<li>Implement a formal database schema migration tool (Flyway or Alembic) to prevent data loss during continuous integration.</li>
-<li>Optimize the SQL parsing speed by adding specific integer boundaries to the B-TREE indexes.</li>
-<li>Deploy an actual external SMTP server (e.g., Postfix/Sendgrid) to fully operationalize the mocked password-reset pipeline.</li>
-</ul>
-<hr />
-</body>
-</html>
+
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="utf-8">
+    <title>Customer Presentation</title>
+    <style>
+        body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; line-height: 1.6; color: #333; max-width: 900px; margin: 0 auto; padding: 2rem; }
+        h1 { color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 10px; }
+        h2 { color: #2980b9; margin-top: 2rem; }
+        h3 { color: #16a085; }
+        table { border-collapse: collapse; width: 100%; margin-bottom: 2rem; }
+        th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
+        th { background-color: #f2f2f2; color: #333; }
+        @media print {
+            body { padding: 0; max-width: 100%; }
+            hr { page-break-after: always; border: 0; }
+        }
+    </style>
+</head>
+<body>
+    <div style="text-align:center; margin-bottom: 3rem;">
+        <h1 style="border: none;">Clinical Food AI Platform</h1>
+        <p><strong>Master Deliverable Overview</strong></p>
+    </div>
+    <h1>🚀 Executive Project Update: Local Food AI Platform</h1>
+<p><strong>To Our Valued Client,</strong></p>
+<p>We are thrilled to present the monumental progress achieved in the <strong>Local Food AI Platform</strong>. Your investment has successfully funded the transition of a conceptual idea into a highly secure, enterprise-grade Artificial Intelligence ecosystem. </p>
+<p>Below is an executive summary of the value delivered during our most recent development cycles:</p>
+<h2>🏦 1. Total Data Sovereignty &amp; Security</h2>
+<p>We have engineered an architecture that guarantees <strong>100% Data Privacy</strong>. Unlike consumer AI tools that leak confidential queries to the cloud:
+* <strong>True Local Intelligence:</strong> The Mistral AI neural network and your massive MySQL databases run entirely on isolated, air-gapped internal servers. No recipe, no search query, and no user profile ever leaves your corporate firewall.
+* <strong>Encrypted Access:</strong> We deployed heavy <code>bcrypt</code> cryptographic hashing to secure every user account against breaches.</p>
+<h2>🧠 2. Autonomous Web Intelligence (SearXNG)</h2>
+<p>To ensure the AI is never outdated, we successfully deployed an anonymous Docker-based metasearch proxy. If a user asks the AI about a brand-new medical ingredient not present in your databases, the AI recognizes the gap autonomously, covertly scrapes the internet without tracking, and instantly incorporates the live data to answer the question!</p>
+<h2>🔬 3. The "Scientific Medical" User Interface</h2>
+<p>We completely overhauled the front-end user experience to reflect luxury and scientific precision. </p>
+<p><img alt="Premium UI Dashboard Visualization" src="file:///C:/Users/lanfr144/.gemini/antigravity/brain/fa60b8a2-c1d5-4b3d-8ff2-f6588c78798f/premium_nutrition_dashboard_ui_1776925129649.png" /></p>
+<ul>
+<li><strong>Dynamic 'My Plate' Architecture:</strong> Users can dynamically combine ingredients from a database of millions of entries. Our backend calculates compounding macro-totals (Protein, Fat, Carbs) in real-time, functioning as an enterprise diet tracker.</li>
+<li><strong>Granular Data Search:</strong> The platform boasts high-speed filtration algorithms, allowing practitioners to search exactly for criteria like <em>"Products with &gt; 20g Protein and &lt; 5g Sugar"</em>.</li>
+</ul>
+<h2>🤖 4. The Prompt-Engineered Dietitian</h2>
+<p>Most chatbots simply "talk". We implemented complex algorithmic <em>Prompt Engineering</em> to force the AI into acting as a highly structured Clinical Dietitian. The system now mathematically generates highly accurate, multi-day meal plans mapped directly to exact caloric and dietary constraints (Vegan, Keto, Omnivore) and outputs them strictly as professional Markdown data tables instead of loose text.</p>
+<hr />
+<p><strong>Return on Investment (ROI):</strong> 
+Your financing has birthed a fully-scalable, premium-designed, highly secure platform capable of replacing thousands of dollars in cloud API costs while protecting intellectual property. The system is ready to revolutionize local nutritional analysis pipelines.</p>
+<hr />
+<h1>🏆 Synthèse Agile &amp; Wiki SCRUM</h1>
+<p>Voici le compte-rendu officiel du projet <strong>Local Food AI</strong>, structuré pour répondre aux exigences des rituels Scrum (Daily, Review, Planning) et pour alimenter directement votre Wiki Taiga.</p>
+<hr />
+<h2>1. 🌅 Le Daily (Où en sommes-nous ?)</h2>
+<p><strong>Statut Actuel :</strong> 
+Le socle applicatif est à 90% terminé. L'infrastructure de base (MySQL, Ubuntu, Docker, Ollama) est parfaitement stable, le pipeline d'intégration Git/Taiga via Webhook est fonctionnel, et l'interface utilisateur (UI) vient de subir une refonte technologique massive. Il ne reste techniquement qu'une seule "Epic/User Story" majeure dans notre Backlog.</p>
+<hr />
+<h2>2. 🔍 La Sprint Review (Qu'avons-nous fait hier ?)</h2>
+<p>Lors du dernier Sprint de développement continu, nous avons validé les User Stories <strong>#5, #6, #7, et #8</strong>. </p>
+<p><strong>Réalisations Techniques et Démontrables :</strong>
+* <strong>Refonte "Scientific Medical" (Frontend) :</strong> Injection de CSS avancé dans <code>app.py</code> pour basculer Streamlit vers un design "Dark Mode" Premium, utilisant la police Inter, des dégradés bleus/cyan, et des effets "Glassmorphism".
+* <strong>Filtres Avancés (SQL/Backend) :</strong> Création de 4 sliders interactifs (Protéines, Lipides, Glucides, Sucres) modifiant dynamiquement la clause <code>WHERE ... AND protéines &gt;= X</code> de la base MySQL.
+* <strong>Architecture "My Plate" (Database) :</strong> Modification sécurisée de <code>setup_db.py</code> pour générer automatiquement deux nouvelles tables relationnelles (<code>plates</code> et <code>plate_items</code>). Ces tables utilisent des clefs étrangères (Foreign Keys) pour lier les aliments directement au <code>user_id</code> de la session.
+* <strong>Algorithme d'Agrégation (Logique Data) :</strong> Intégration d'une logique en Python/Pandas calculant et additionnant instantanément les macros (Protéines, Graisses, Carbs) de tous les aliments présents dans une assiette virtuelle.
+* <em>Toutes ces modifications ont été commitées sur Gogs avec succès, déclenchant le Webhook vers Taiga (Tasks #23, #24, #26, #27).</em></p>
+<hr />
+<h2>3. 🎯 Le Sprint Planning (Qu'allons-nous faire ?)</h2>
+<p><strong>Prochain Objectif :</strong> Construire la <strong>User Story #11 (AI Menu Proposals)</strong>.</p>
+<p><strong>Tâches prévues (Sprint Backlog) :</strong>
+1. Créer une nouvelle section/tab dans le code pour la génération de menus.
+2. Concevoir un algorithme de "Prompt Engineering" très spécifique qui imposera à <strong>Mistral</strong> des contraintes strictes.
+3. Câbler la demande de l'utilisateur (ex: "Je veux un menu à 2000 kcal riche en protéines") avec la base de données SQL locale pour fournir de vrais exemples au LLM, afin qu'il propose un menu concret et non inventé.
+4. Finaliser les play-tests finaux sur la VM Ubuntu.</p>
+<hr />
+<h2>4. 📚 Ce que tu dois mettre dans le Wiki SCRUM (Taiga)</h2>
+<p>Copiez-collez ces blocs dans votre Wiki Taiga pour prouver la maîtrise technique du projet :</p>
+<h3>🏛️ Architecture &amp; Technologies</h3>
+<ul>
+<li><strong>Frontend :</strong> Framework <strong>Streamlit</strong> (Python) surchargé par du CSS natif injecté via <code>st.markdown(unsafe_allow_html=True)</code> pour garantir une esthétique "Scientific Medical" (Focalisation UX/UI Premium).</li>
+<li><strong>Backend Intelligence :</strong> Intégration native de l'API <strong>Ollama (modèle Mistral)</strong> avec le concept de <em>Tool/Function Calling</em> pour scraper anonymement le Web via un conteneur local <strong>SearXNG</strong> sur le port <code>8080</code>.</li>
+<li><strong>Database Pipeline :</strong> Injection dynamique et asynchrone des données CSV ouvertes via Pandas vers MySQL. Abandon des schémas SQL rigides au profit de l'auto-génération des 200 colonnes via l'ORM.</li>
+<li><strong>Sécurité &amp; Accès :</strong> Mise en place d'un modèle <strong>PoLP</strong> (Principle of Least Privilege). L'application gère nativement le HMAC (via <code>bcrypt</code>) et le script <code>setup_db.py</code> octroie des droits granulaires (ex: <code>IDENTIFIED BY ... GRANT SELECT, INSERT... TO 'db_app_auth'</code>).</li>
+</ul>
+<h3>🔄 DevOps &amp; Déploiement</h3>
+<ul>
+<li>Le CI/CD rudimentaire repose sur une intégration <strong>Gogs -&gt; Taiga</strong>. Chaque commit (ex: <code>TG-23</code>) documente automatiquement la carte Agile via Webhook.</li>
+<li>Le système est déployable via le script unifié <code>deploy.sh</code> (qui gère l'environnement virtuel Python) et <code>setup_searxng.sh</code> (qui gère l'orchestration Docker).</li>
+</ul>
+<hr />
+<h1>Agile Sprint Retrospective</h1>
+<p><strong>Project:</strong> Local Food AI Platform
+<strong>Sprint Goal:</strong> Secure Data Ingestion, Medical Expansion, and UI/UX Overhaul</p>
+<h2>🏆 What Went Well</h2>
+<ul>
+<li><strong>Database Agility:</strong> Transitioning from rigid SQL arrays to dynamic pandas DataFrame ingestion (<code>ingest_csv.py</code>) allowed us to process massive OpenFoodFacts schemas instantly without crashing.</li>
+<li><strong>Privacy-First Architecture:</strong> Successfully establishing an air-gapped system where the AI scraper (SearXNG) and the Large Language Model (Mistral) operate entirely locally proves extreme Corporate Data Sovereignty.</li>
+<li><strong>Rapid Feature Integration:</strong> Expanding the platform from a simple calculator to a full-fledged Clinical Profiler (incorporating Diabetes, Hypertension, and Pregnancy monitoring) was achieved incredibly fast using Pandas styling logic.</li>
+</ul>
+<h2>🚧 What Went Wrong (Or Needed Improvement)</h2>
+<ul>
+<li><strong>Dataset Encoding Bugs:</strong> The OpenFoodFacts CSV files contain heavy French datasets. Early ingestion attempts on Windows corrupted characters (<code>'Artichaut' -&gt; 'Artichaut'</code>) due to OS-default rendering limitations over <code>utf-8</code>. This required an urgent hotfix in the data pipeline.</li>
+<li><strong>Schema Scalability:</strong> Constantly injecting new tables (<code>plates</code>, <code>user_profiles</code>) into <code>setup_db.py</code> without a formal migration tool (like Alembic) makes iterative DevOps slightly dangerous for live production data.</li>
+</ul>
+<h2>🎯 Action Items for Next Sprint</h2>
+<ul>
+<li>Implement a formal database schema migration tool (Flyway or Alembic) to prevent data loss during continuous integration.</li>
+<li>Optimize the SQL parsing speed by adding specific integer boundaries to the B-TREE indexes.</li>
+<li>Deploy an actual external SMTP server (e.g., Postfix/Sendgrid) to fully operationalize the mocked password-reset pipeline.</li>
+</ul>
+<hr />
+</body>
+</html>

+ 982 - 982
app.py

@@ -1,982 +1,982 @@
-# $Id$
-# $Author$
-# $log$
-#ident "@(#)LocalFoodAI:app.py:$Format:%D:%ci:%cN:%h$"
-import streamlit as st
-import pymysql
-import bcrypt
-import random
-import string
-import time
-import os
-import pandas as pd
-import html
-from snmp_notifier import notifier
-from unit_converter import UnitConverter
-from fpdf import FPDF
-import myloginpath
-import ollama
-import bcrypt
-import requests
-import string
-import random
-import smtplib
-from email.message import EmailMessage
-import pandas as pd
-from unit_converter import UnitConverter
-from typing import Optional, List, Dict, Any, Tuple
-from snmp_notifier import notifier
-import time
-
-import threading
-
-def strip_scratchpad(text: str) -> str:
-    import re
-    # Strip out the XML <scratchpad> tag and everything in between, non-greedily
-    clean_text = re.sub(r'<scratchpad>.*?</scratchpad>', '', text, flags=re.DOTALL)
-    return clean_text.strip()
-
-def filter_scratchpad_stream(stream):
-    buffer = ""
-    in_scratchpad = False
-    for chunk in stream:
-        content = chunk['message']['content']
-        buffer += content
-        
-        while True:
-            if not in_scratchpad:
-                start_idx = buffer.find("<scratchpad>")
-                if start_idx != -1:
-                    yield buffer[:start_idx]
-                    buffer = buffer[start_idx:]
-                    in_scratchpad = True
-                else:
-                    yield_len = max(0, len(buffer) - 11)
-                    if yield_len > 0:
-                        yield buffer[:yield_len]
-                        buffer = buffer[yield_len:]
-                    break
-            else:
-                end_idx = buffer.find("</scratchpad>")
-                if end_idx != -1:
-                    buffer = buffer[end_idx + 13:]
-                    in_scratchpad = False
-                else:
-                    keep_len = 12
-                    if len(buffer) > keep_len:
-                        buffer = buffer[-keep_len:]
-                    break
-    if not in_scratchpad and buffer:
-        yield buffer
-
-def pull_model_bg():
-    try: ollama.pull('qwen2.5:7b')
-    except: pass
-threading.Thread(target=pull_model_bg, daemon=True).start()
-
-def local_web_search(query: str) -> str:
-    try:
-        req = requests.get(f'http://127.0.0.1:8080/search', params={'q': query, 'format': 'json'})
-        if req.status_code == 200:
-            data = req.json()
-            results = data.get('results', [])
-            if not results: return f"No results found on the web for '{query}'."
-            snippets = [f"Source: {r.get('url')}\nContent: {r.get('content')}" for r in results[:3]]
-            return "\n\n".join(snippets)
-        return "Search engine returned an error."
-    except Exception as e: return f"Local search engine unreachable: {e}"
-
-search_tool_schema = {
-    'type': 'function',
-    'function': {
-        'name': 'local_web_search',
-        'description': 'Search the internet for info not in DB.',
-        'parameters': {'type': 'object', 'properties': {'query': {'type': 'string'}}, 'required': ['query']},
-    },
-}
-
-def search_nutrition_db(query: str, user_eav=None) -> str:
-    conn = get_db_connection('app_reader')
-    if not conn: return "Database connection failed."
-    try:
-        with conn.cursor() as cursor:
-            # Dynamically build strictly-enforced clinical SQL filters
-            clinical_filters = ""
-            if user_eav:
-                for p in user_eav:
-                    name = p['name'].lower()
-                    val = p['value'].lower()
-                    if name in ['condition', 'illness']:
-                        if val == 'diabetes': clinical_filters += " AND m.sugars_100g < 5.0"
-                        elif 'kidney' in val: clinical_filters += " AND m.proteins_100g < 15.0"
-                        elif 'hypertension' in val: clinical_filters += " AND m.sodium_100g < 0.2"
-                    elif name in ['diet', 'religious', 'preference']:
-                        if val == 'kosher': clinical_filters += " AND c.ingredients_text NOT LIKE '%pork%' AND c.ingredients_text NOT LIKE '%shellfish%'"
-                        elif val == 'halal': clinical_filters += " AND c.ingredients_text NOT LIKE '%pork%' AND c.ingredients_text NOT LIKE '%wine%' AND c.ingredients_text NOT LIKE '%alcohol%'"
-                        elif val in ['christian', 'good friday', 'ash wednesday']: clinical_filters += " AND c.ingredients_text NOT LIKE '%meat%' AND c.ingredients_text NOT LIKE '%beef%' AND c.ingredients_text NOT LIKE '%chicken%' AND c.ingredients_text NOT LIKE '%pork%'"
-
-            sql = f"""
-                SELECT c.code, c.product_name, m.proteins_100g, m.fat_100g, m.carbohydrates_100g, m.sugars_100g 
-                FROM food_db.products_core c
-                LEFT JOIN food_db.products_macros m ON c.code = m.code
-                WHERE MATCH(c.product_name, c.ingredients_text) AGAINST(%s IN BOOLEAN MODE)
-                AND c.product_name IS NOT NULL AND c.product_name != '' AND c.product_name != 'None'
-                {clinical_filters}
-            """
-            bool_query = " ".join([f"+{w}" for w in query.split()])
-            cursor.execute(sql, (bool_query,))
-            results = cursor.fetchall()
-            if not results: return f"No database records found for '{query}'."
-            
-            snippets = []
-            for r in results:
-                snippets.append(f"- {r['product_name']}: Protein {r['proteins_100g']}g, Fat {r['fat_100g']}g, Carbs {r['carbohydrates_100g']}g, Sugars {r['sugars_100g']}g (per 100g)")
-            return "\n".join(snippets)
-    except Exception as e:
-        return f"Database query failed: {e}"
-    finally:
-        conn.close()
-
-db_search_tool_schema = {
-    'type': 'function',
-    'function': {
-        'name': 'search_nutrition_db',
-        'description': 'Search the local medical nutrition database for product macros and ingredients. ALWAYS prioritize this over web search.',
-        'parameters': {'type': 'object', 'properties': {'query': {'type': 'string', 'description': 'The product or food name to search for (e.g. apple, chicken, bread)'}}, 'required': ['query']},
-    },
-}
-
-def get_db_connection(login_path):
-    try:
-        import os
-        db_host = os.environ.get('DB_HOST')
-        # Check if environment variables exist for this login path
-        db_user = os.environ.get(f'{login_path.upper()}_USER') or os.environ.get('DB_USER')
-        db_pass = os.environ.get(f'{login_path.upper()}_PASS') or os.environ.get('DB_PASS')
-
-        if db_host and db_user and db_pass:
-            return pymysql.connect(
-                host=db_host,
-                user=db_user,
-                password=db_pass,
-                database='food_db',
-                cursorclass=pymysql.cursors.DictCursor
-            )
-            
-        conf = myloginpath.parse(login_path)
-        if not conf or not conf.get('user'):
-            st.error(f"⚠️ MySQL configuration missing for `{login_path}`. If you are testing locally on Windows, this app must be run on the Ubuntu server where `mysql_config_editor` is properly configured.")
-            return None
-            
-        return pymysql.connect(
-            host=conf.get('host', '127.0.0.1'),
-            user=conf.get('user'),
-            password=conf.get('password'),
-            database='food_db',
-            cursorclass=pymysql.cursors.DictCursor
-        )
-    except Exception as e:
-        st.error(f"Connection Failed: {e}")
-        return None
-
-from contextlib import contextmanager
-
-@contextmanager
-def db_cursor(login_path: str):
-    conn = get_db_connection(login_path)
-    if not conn:
-        yield None
-        return
-    try:
-        with conn.cursor() as cursor:
-            yield cursor
-        conn.commit()
-    except Exception as e:
-        conn.rollback()
-        st.error(f"Database query error: {e}")
-        raise e
-    finally:
-        conn.close()
-
-def verify_login(username: str, password: str) -> bool:
-    with db_cursor('app_auth') as cursor:
-        if not cursor: return False
-        cursor.execute("SELECT password_hash FROM users WHERE username = %s", (username,))
-        result = cursor.fetchone()
-        if result: return bcrypt.checkpw(password.encode('utf-8'), result['password_hash'].encode('utf-8'))
-    return False
-
-def get_user_id(username: str) -> Optional[int]:
-    with db_cursor('app_auth') as cursor:
-        if not cursor: return None
-        cursor.execute("SELECT id FROM users WHERE username = %s", (username,))
-        result = cursor.fetchone()
-        return result['id'] if result else None
-
-def get_eav_profile(username: str) -> List[Dict[str, Any]]:
-    uid = get_user_id(username)
-    if not uid: return []
-    with db_cursor('app_auth') as cursor:
-        if not cursor: return []
-        cursor.execute("SELECT id, illness_health_condition_diet_dislikes_name as name, illness_health_condition_diet_dislikes_value as value FROM user_health_profiles WHERE user_id = %s", (uid,))
-        return cursor.fetchall()
-
-def get_user_limit(username: str) -> str:
-    with db_cursor('app_auth') as cursor:
-        if not cursor: return "50"
-        cursor.execute("SELECT search_limit FROM users WHERE username = %s", (username,))
-        result = cursor.fetchone()
-        return result['search_limit'] if (result and result['search_limit']) else "50"
-
-def register_user(username: str, password: str, email: str) -> bool:
-    hashed = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
-    try:
-        with db_cursor('app_auth') as cursor:
-            if not cursor: return False
-            cursor.execute("INSERT INTO users (username, password_hash, email) VALUES (%s, %s, %s)", (username, hashed, email))
-        send_email(email, "Welcome to Local Food AI", f"Hello {username}, your account was securely created!", to_name=username.title())
-        return True
-    except pymysql.err.IntegrityError:
-        return False
-
-def send_email(to_email: str, subject: str, body: str, to_name: str = "User") -> Any:
-    msg = EmailMessage()
-    msg.set_content(body)
-    msg['Subject'] = subject
-    msg['From'] = '"Clinical Food AI System" <security@localfoodai.com>'
-    msg['To'] = f'"{to_name}" <{to_email}>'
-    
-    for attempt in range(5):
-        try:
-            s = smtplib.SMTP('localhost', 25)
-            s.send_message(msg)
-            s.quit()
-            return True
-        except Exception as e:
-            if attempt == 4:
-                return f"SMTP Delivery Failed: {str(e)}"
-            time.sleep(2)
-    return "Unknown Error Occurred"
-
-def reset_password(username: str, email: str) -> Any:
-    with db_cursor('app_auth') as cursor:
-        if not cursor: return False
-        cursor.execute("SELECT id, email FROM users WHERE username = %s", (username,))
-        user = cursor.fetchone()
-        if user and user['email'] == email:
-            new_pass = ''.join(random.choices(string.ascii_letters + string.digits, k=10))
-            hashed = bcrypt.hashpw(new_pass.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
-            cursor.execute("UPDATE users SET password_hash = %s WHERE id = %s", (hashed, user['id']))
-            status = send_email(email, "Password Reset", f"Your new temporary password is: {new_pass}", to_name=username.title())
-            return True if status is True else status
-    return False
-
-# UI Theming
-def render_version():
-    st.markdown("---")
-    st.caption("🚀 Version: v1.3.0")
-    st.caption(f"📅 Git ID: $Id$")
-
-st.set_page_config(page_title="Food AI Explorer", page_icon="🍔", layout="wide")
-st.markdown("""
-<style>
-    @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&display=swap');
-    html, body, [class*="css"]  { font-family: 'Inter', sans-serif; background-color: #0b192c; color: #e2e8f0; }
-    h1, h2, h3 { color: #38bdf8 !important; font-weight: 600; letter-spacing: 0.5px; }
-    div[data-testid="stSidebar"] { background: rgba(11, 25, 44, 0.95) !important; backdrop-filter: blur(10px); border-right: 1px solid #1e293b; }
-    .stButton>button { background: linear-gradient(135deg, #0ea5e9, #0284c7); color: white; border: none; border-radius: 6px; }
-    .stButton>button:hover { transform: scale(1.02); }
-    .stTextInput>div>div>input, .stNumberInput>div>div>input, .stSelectbox>div>div>div { background-color: #0f172a; color: #f8fafc; border: 1px solid #38bdf8; }
-</style>
-""", unsafe_allow_html=True)
-
-if "authenticated_user" not in st.session_state:
-    st.session_state["authenticated_user"] = None
-
-with st.sidebar:
-    st.title("User Portal 🔐")
-    render_version()
-    
-    with st.expander("ℹ️ Welcome"):
-        st.info("Welcome to the secure Local Food AI environment.")
-            
-    if st.session_state["authenticated_user"]:
-        st.success(f"Logged in as: {st.session_state['authenticated_user']}")
-        if st.button("Logout"):
-            st.session_state["authenticated_user"] = None
-            st.rerun()
-            
-        eav_data = get_eav_profile(st.session_state["authenticated_user"])
-        uid = get_user_id(st.session_state["authenticated_user"])
-        user_lim = get_user_limit(st.session_state["authenticated_user"])
-        
-        with st.expander("⚙️ Account Preferences"):
-            opts = ["10", "20", "50", "100", "All"]
-            idx = opts.index(user_lim) if user_lim in opts else 2
-            new_lim = st.selectbox("Default Search Limit", opts, index=idx)
-            if new_lim != user_lim:
-                conn = get_db_connection('app_auth')
-                with conn.cursor() as c:
-                    c.execute("UPDATE users SET search_limit = %s WHERE id = %s", (new_lim, uid))
-                    conn.commit()
-                st.rerun()
-
-        with st.expander("➕ Add Condition / Diet"):
-            new_cat = st.selectbox("Category", ["Condition", "Illness", "Diet", "Dislike", "Allergy"])
-            
-            if new_cat == "Condition":
-                new_val = st.selectbox("Value", ["Pregnant", "Breastfeeding", "Low Fat"])
-            elif new_cat == "Illness":
-                new_val = st.selectbox("Value", ["Diabetes", "Hypertension", "Kidney Disease", "Osteoporosis", "Scurvy", "Anemia"])
-            elif new_cat == "Diet":
-                new_val = st.selectbox("Value", ["Vegan", "Vegetarian", "Kosher", "Halal", "Christian", "Good Friday", "Ash Wednesday", "Keto", "Paleo"])
-            else:
-                new_val = st.text_input("Value (e.g. 'peanuts', 'broccoli')").strip()
-                
-            new_val_clean = new_val.lower()
-            
-            if st.button("Add to Profile") and new_val_clean and uid:
-                conn = get_db_connection('app_auth')
-                with conn.cursor() as c:
-                    c.execute("INSERT INTO user_health_profiles (user_id, illness_health_condition_diet_dislikes_name, illness_health_condition_diet_dislikes_value) VALUES (%s, %s, %s)", (uid, new_cat.lower(), new_val_clean))
-                    conn.commit()
-                st.rerun()
-                
-        if eav_data:
-            st.markdown("#### Active Flags")
-            for e in eav_data:
-                col1, col2 = st.columns([4, 1])
-                col1.info(f"**{e['name']}:** {e['value'].title()}")
-                if col2.button("X", key=f"del_eav_{e['id']}"):
-                    conn = get_db_connection('app_auth')
-                    with conn.cursor() as c:
-                        c.execute("DELETE FROM user_health_profiles WHERE id = %s", (e['id'],))
-                        conn.commit()
-                    st.rerun()
-    else:
-        tab1, tab2, tab3 = st.tabs(["Login", "Register", "Reset"])
-        with tab1:
-            l_user = st.text_input("Username", key="l_user").strip()
-            l_pass = st.text_input("Password", type="password", key="l_pass")
-            if st.button("Login"):
-                if verify_login(l_user, l_pass):
-                    notifier.send_alert(f"User Login Success: {l_user}")
-                    st.session_state["authenticated_user"] = l_user
-                    st.rerun()
-                else:
-                    notifier.send_alert(f"User Login Failed: {l_user}")
-                    st.error("Invalid login.")
-        with tab2:
-            r_user = st.text_input("Username", key="r_user")
-            r_email = st.text_input("Email Address", key="r_email")
-            r_pass = st.text_input("Password", type="password", key="r_pass")
-            if st.button("Register"):
-                if len(r_pass) < 6: st.error("Password too short.")
-                elif register_user(r_user, r_pass, r_email): st.success("Registered safely!")
-                else: st.error("Username exists.")
-        with tab3:
-            f_user = st.text_input("Username", key="f_user")
-            f_email = st.text_input("Registered Email", key="f_email")
-            if st.button("Send Reset Link"):
-                status = reset_password(f_user, f_email)
-                if status is True: 
-                    st.success("Password reset emailed.")
-                else: 
-                    st.error(f"Failed: {status}")
-
-if not st.session_state["authenticated_user"]:
-    st.title("🍔 Food AI Medical Explorer")
-    st.info("Please login to interrogate the Clinical Data.")
-    st.stop()
-
-st.title("🍔 Food AI Clinical Explorer")
-conn_reader = get_db_connection('app_reader')
-
-tab_chat, tab_explore, tab_plate, tab_planner = st.tabs(["💬 AI Chat", "🔬 Clinical Search", "🍽️ My Plate Builder", "🤖 AI Meal Planner"])
-
-import re
-
-with tab_chat:
-    c1, c2 = st.columns([4, 1])
-    c1.subheader("Chat with the Context")
-    if c2.button("🧹 Clear Chat"):
-        st.session_state["messages"] = [{"role": "assistant", "content": "How can I help you analyze the food data today?"}]
-        st.rerun()
-    st.info("""
-    ℹ️ **How to use this feature (Examples)**
-    **Your active conditions (e.g. Pregnant, Diabetic) are automatically sent to the AI in the background. You do not need to type them out.**
-    
-    *Examples:*
-    1. "I am pregnant, diabetic, and have kidney problems. Can I eat sushi?"
-    2. "What is a safe snack to stabilize my blood sugar without hurting my kidneys?"
-    3. "Can I drink milk? I need calcium for the baby."
-    4. "Is it safe to eat a large steak for iron?"
-    5. "What foods are strictly forbidden for me?"
-    """)
-    if "messages" not in st.session_state:
-        st.session_state["messages"] = [{"role": "assistant", "content": "How can I help you analyze the food data today?"}]
-
-    # Display chat history, filtering out TOOL_CALLS
-    for msg in st.session_state.messages:
-        if msg["role"] == "tool": continue
-        display_text = re.sub(r'\[TOOL_CALLS\]\s*\[.*?\]', '', msg["content"]).strip()
-        if display_text:
-            st.chat_message(msg["role"]).write(display_text)
-
-    if prompt := st.chat_input("Ask a clinical question about your food..."):
-        st.session_state.messages.append({"role": "user", "content": prompt})
-        st.chat_message("user").write(prompt)
-        
-        user_eav = get_eav_profile(st.session_state["authenticated_user"])
-        profile_text = ", ".join([f"{p['name']}: {p['value']}" for p in user_eav]) if user_eav else "None"
-        
-        db_context = search_nutrition_db(prompt, user_eav)
-        searxng_context = ""
-        
-        if "No database records found" in db_context:
-            try:
-                searxng_url = os.environ.get("SEARXNG_HOST", "http://searxng:8080")
-                resp = requests.get(f"{searxng_url}/search", params={'q': prompt, 'format': 'json'}, timeout=5)
-                if resp.status_code == 200:
-                    results = resp.json().get('results', [])
-                    if results:
-                        snippets = [r.get('content', '') for r in results[:3]]
-                        searxng_context = "Web Search Context: " + " | ".join(snippets)
-            except Exception as e:
-                pass
-                
-        sys_prompt = f"""You are a helpful medical data analyst AI. 
-        Health profile: {profile_text}. 
-        Act as a specialized clinical dietitian. Provide a direct answer. Use Chain of Thought reasoning, and skip pleasantries.
-        Local Database Context: {db_context}
-        {searxng_context}
-        """
-        
-        try:
-            temp_messages = [{"role": "system", "content": sys_prompt}] + [m for m in st.session_state.messages if m["role"] != "tool"]
-            response_stream = ollama.chat(model='qwen2.5:7b', messages=temp_messages, stream=True)
-            
-            with st.chat_message("assistant"):
-                ai_reply = st.write_stream(chunk['message']['content'] for chunk in response_stream)
-            
-            st.session_state.messages.append({"role": "assistant", "content": ai_reply})
-        except Exception as e: 
-            ai_reply = f"Hold on! Engine execution fault: {e}"
-            st.session_state.messages.append({"role": "assistant", "content": ai_reply})
-            st.chat_message("assistant").write(ai_reply)
-
-def highlight_medical_warnings(row):
-    try:
-        val = str(row.get('Medical Warning', ''))
-        if '⚠️' in val: return ['background-color: rgba(255, 0, 0, 0.4); color: white;'] * len(row)
-        if '💚' in val: return ['background-color: rgba(0, 255, 0, 0.3); color: white;'] * len(row)
-    except: pass
-    return [''] * len(row)
-
-with tab_explore:
-    st.subheader("Clinical Data Search")
-    st.info("""
-    ℹ️ **How to use this feature (Examples)**
-    **Your active conditions are automatically flagged (⚠️ or 💚) in the search results.**
-    
-    *Example Searches:*
-    1. `Cereal` *(Checks for high sugar & hidden phosphorus)*
-    2. `Cheese` *(Checks for unpasteurized pregnancy risks & high sodium)*
-    3. `Fruit Juice` *(Checks for high sugar spikes)*
-    4. `Deli Meat` *(Checks for Listeria risk & extreme sodium)*
-    5. `White Rice` *(Safe for kidneys but flags high glycemic index)*
-    """)
-    sq = st.text_input("Search Product Name or Ingredient")
-    cols = st.columns(5)
-    min_pro = cols[0].number_input("Min Protein (g)", 0, 1000, 0)
-    min_fat = cols[1].number_input("Min Fat (g)", 0, 1000, 0)
-    min_carb = cols[2].number_input("Min Carbs (g)", 0, 1000, 0)
-    max_sug = cols[3].number_input("Max Sugar (g)", 0, 1000, 1000)
-    
-    # Load dynamically fetched limit to prevent Pandas Styler crash
-    pd.set_option("styler.render.max_elements", 5000000)
-    opts = [10, 50, 100, 500, 1000]
-    
-    user_lim_str = get_user_limit(st.session_state["authenticated_user"])
-    user_lim_val = 1000 if user_lim_str == "All" else int(user_lim_str)
-    if user_lim_val not in opts: user_lim_val = 50
-    idx = opts.index(user_lim_val)
-    limit_rc = cols[4].selectbox("Limit Results", opts, index=idx)
-    
-    if st.button("Search Database"):
-        st.session_state["trigger_search"] = True
-        
-    if st.session_state.get("trigger_search", False) and sq and conn_reader:
-        notifier.send_alert(f"Medical DB Search Executed: {sq}")
-        with st.spinner("Processing massive clinical query..."):
-            try:
-                with conn_reader.cursor() as cursor:
-                    l_str = "" if limit_rc == "All" else f"LIMIT {limit_rc}"
-                    query = f"""
-                        SELECT c.code, c.product_name, c.generic_name, c.brands, c.ingredients_text,
-                               a.allergens,
-                               m.`energy-kcal_100g`, m.proteins_100g, m.fat_100g, m.carbohydrates_100g, m.sugars_100g, m.fiber_100g, m.sodium_100g, m.salt_100g, m.cholesterol_100g,
-                               v.`vitamin-a_100g`, v.`vitamin-b1_100g`, v.`vitamin-b2_100g`, v.`vitamin-pp_100g`, v.`vitamin-b6_100g`, v.`vitamin-b9_100g`, v.`vitamin-b12_100g`, v.`vitamin-c_100g`, v.`vitamin-d_100g`, v.`vitamin-e_100g`, v.`vitamin-k_100g`,
-                               min.calcium_100g, min.iron_100g, min.magnesium_100g, min.potassium_100g, min.zinc_100g
-                        FROM (
-                            SELECT code, product_name, generic_name, brands, ingredients_text
-                            FROM food_db.products_core
-                            WHERE MATCH(product_name, ingredients_text) AGAINST(%s IN BOOLEAN MODE)
-                            AND product_name IS NOT NULL AND product_name != '' AND product_name != 'None'
-                            {l_str}
-                        ) c
-                        LEFT JOIN food_db.products_allergens a ON c.code = a.code
-                        LEFT JOIN food_db.products_macros m ON c.code = m.code
-                        LEFT JOIN food_db.products_vitamins v ON c.code = v.code
-                        LEFT JOIN food_db.products_minerals min ON c.code = min.code
-                        WHERE (m.proteins_100g >= %s OR m.proteins_100g IS NULL)
-                        AND (m.fat_100g >= %s OR m.fat_100g IS NULL)
-                        AND (m.carbohydrates_100g >= %s OR m.carbohydrates_100g IS NULL)
-                        AND (m.sugars_100g <= %s OR m.sugars_100g IS NULL)
-                    """
-                    sq_bool = " ".join([f"+{w}" for w in sq.split()])
-                    start_time = time.time()
-                    cursor.execute(query, (sq_bool, min_pro, min_fat, min_carb, max_sug))
-                    results = cursor.fetchall()
-                    elapsed = time.time() - start_time
-                    st.caption(f"⏱️ DB Query Executed in {elapsed:.3f} seconds")
-                    
-                    if results:
-                        # Fetch EAV Medical Profile
-                        eav_profile = get_eav_profile(st.session_state["authenticated_user"])
-                        df = pd.DataFrame(results)
-                        
-                        st.markdown("### 🛠️ Dynamic Column Display")
-                        default_columns = [
-                            'code', 'product_name', 'generic_name', 'brands', 'allergens', 'ingredients_text',
-                            'proteins_100g', 'fat_100g', 'carbohydrates_100g', 'sugars_100g', 'sodium_100g', 'energy-kcal_100g',
-                            'vitamin-c_100g', 'iron_100g', 'calcium_100g'
-                        ]
-                        all_fetched_cols = list(df.columns)
-                        valid_defaults = [c for c in default_columns if c in all_fetched_cols]
-                        
-                        if "selected_columns" not in st.session_state or st.button("Reset Default Columns"):
-                            st.session_state["selected_columns"] = valid_defaults
-                            st.rerun()
-                            
-                        chosen_cols = st.multiselect("Customize Dataset View", all_fetched_cols, default=st.session_state["selected_columns"], key="multi_cols")
-                        st.session_state["selected_columns"] = chosen_cols
-                        
-                        # Filter dataframe gracefully, but we retain a copy for background analytics
-                        df_display = df[chosen_cols].copy()
-                        warnings_col = []
-                        
-                        for idx, row in df.iterrows():
-                            warns = []
-                            ing_text = str(row['ingredients_text']).lower()
-                            all_text = str(row['allergens']).lower()
-                            
-                            for param in eav_profile:
-                                cat = param['name'].lower()
-                                val = param['value']
-                                
-                                # Disease Analytics
-                                if cat == 'illness':
-                                    if val == 'diabetes' and pd.notnull(row.get('sugars_100g')) and float(row['sugars_100g']) > 10.0:
-                                        warns.append("⚠️ High Sugar (Diabetes)")
-                                    if (val == 'hypertension' or val == 'high bp') and pd.notnull(row.get('sodium_100g')) and float(row['sodium_100g']) > 1.5:
-                                        warns.append("⚠️ High Salt (Hypertension)")
-                                    if val == 'scurvy' and pd.notnull(row.get('vitamin-c_100g')) and float(row['vitamin-c_100g']) > 0.005:
-                                        warns.append("💚 High Vitamin C (Scurvy Recommended)")
-                                    if val == 'anemia' and pd.notnull(row.get('iron_100g')) and float(row['iron_100g']) > 0.002:
-                                        warns.append("💚 High Iron (Anemia Recommended)")
-                                        
-                                # Condition Analytics
-                                if cat == 'condition':
-                                    if val == 'pregnant':
-                                        if ('cru' in ing_text or 'raw' in ing_text or 'viande crue' in ing_text):
-                                            warns.append("⚠️ Raw Foods (Pregnancy Toxoplasmosis)")
-                                        if pd.notnull(row.get('iron_100g')) and float(row['iron_100g']) > 0.002:
-                                            warns.append("💚 Med-High Iron (Pregnancy Health)")
-                                    if val == 'low fat' and pd.notnull(row.get('fat_100g')) and float(row['fat_100g']) > 20.0:
-                                        warns.append("⚠️ High Fat")
-                                    if val == 'osteoporosis' and pd.notnull(row.get('calcium_100g')) and float(row['calcium_100g']) > 0.1:
-                                        warns.append("💚 High Calcium (Bone Health)")
-                                        
-                            if eav_data:
-                                ing_text = str(row.get('ingredients_text', '')).lower()
-                                all_text = str(row.get('allergens', '')).lower()
-                                product_name_text = str(row.get('product_name', '')).lower()
-                                
-                                for e in eav_data:
-                                    cat = str(e['name']).lower()
-                                    val = str(e['value']).lower()
-                                    
-                                    # Clinical Trace Checks...
-                                    if cat == 'condition' and (val == 'pregnant' or val == 'pregnancy' or val == 'breastfeeding'):
-                                        # Forbidden / High Risk (Toxoplasmosis & Listeria)
-                                        if any(x in ing_text or x in product_name_text for x in ['cru', 'raw', 'viande crue', 'sushi', 'sashimi', 'poisson cru']):
-                                            warns.append("⚠️ Forbidden: Raw Meat/Fish (Toxoplasmosis/Parasite Risk)")
-                                        if any(x in ing_text or x in product_name_text for x in ['lait cru', 'unpasteurized', 'non pasteurisé']):
-                                            warns.append("⚠️ Forbidden: Unpasteurized Dairy (Listeria Risk)")
-                                        if any(x in ing_text or x in product_name_text for x in ['alcool', 'wine', 'alcohol', 'beer']):
-                                            warns.append("⚠️ Forbidden: Contains Alcohol")
-                                            
-                                        # Recommended (Iron & Calcium)
-                                        if float(row.get('iron_100g', 0) or 0) > 0.003:
-                                            warns.append("💚 Recommended: High Iron (Pregnancy Health)")
-                                        if float(row.get('calcium_100g', 0) or 0) > 0.120:
-                                            warns.append("💚 Recommended: High Calcium (Bone Health / Breastfeeding)")
-                                    
-                                    if cat == 'illness' and val == 'osteoporosis':
-                                        if float(row.get('calcium_100g', 0) or 0) < 0.120:
-                                            warns.append("⚠️ Low Calcium (Osteoporosis Risk)")
-                                        else:
-                                            warns.append("💚 Recommended (High Calcium)")
-                                            
-                                    if cat == 'illness' and val == 'scurvy':
-                                        if float(row.get('vitamin-c_100g', 0) or 0) < 0.010:
-                                            warns.append("⚠️ Low Vitamin C (Scurvy Risk)")
-                                        else:
-                                            warns.append("💚 Recommended (High Vitamin C)")
-                                            
-                                    if cat == 'diet' and val in ['vegan', 'vegetarian']:
-                                        if any(x in ing_text for x in ['meat', 'beef', 'chicken', 'fish', 'gelatin', 'whey', 'pork', 'porc', 'poulet']):
-                                            warns.append("⚠️ Contains Animal Products")
-                                    if cat == 'diet' and val == 'halal':
-                                        if any(x in ing_text for x in ['pork', 'pig', 'porc', 'wine', 'alcohol', 'beer', 'vin']):
-                                            warns.append("⚠️ Probable Haram Ingredients (e.g. Pork/Wine)")
-                                            
-                                    if cat in ['dislike', 'allergy']:
-                                        if val in ing_text or val in all_text or val in product_name_text:
-                                            warns.append(f"⚠️ Contains: {val.upper()}")
-                                            
-                            warnings_col.append(" | ".join(list(set(warns))) if warns else "✅ Safe for Profile")
-                            
-                        df_display.insert(0, 'Medical Warning', warnings_col)
-                        styled_df = df_display.style.apply(highlight_medical_warnings, axis=1)
-
-                        st.success(f"Analysed {len(results)} records utilizing dynamic Partitions!")
-                        st.dataframe(styled_df, use_container_width=True)
-                        
-                        if st.button("🤖 Ask AI to Evaluate This Table"):
-                            with st.spinner("AI is dynamically evaluating these records against your profile..."):
-                                user_eav = get_eav_profile(st.session_state["authenticated_user"])
-                                profile_text = ", ".join([f"{p['name']}: {p['value']}" for p in user_eav]) if user_eav else "None"
-                                minimal_records = df_display[['product_name', 'Medical Warning']].head(10).to_dict('records')
-                                eval_prompt = f"The user has this profile: {profile_text}. Evaluate these top foods and state which are highly recommended or strictly forbidden: {minimal_records}. Provide a direct, readable clinical summary. Do not output raw JSON."
-                                try:
-                                    response_stream = ollama.chat(model='qwen2.5:7b', messages=[{'role': 'user', 'content': eval_prompt}], stream=True)
-                                    st.write_stream(chunk['message']['content'] for chunk in response_stream)
-                                except Exception as e:
-                                    error_msg = str(e).lower()
-                                    if "404" in error_msg or "not found" in error_msg:
-                                        st.warning("⚠️ The AI engine is currently downloading its core models in the background. Please wait a minute and try again!")
-                                    else:
-                                        st.error(f"AI Evaluation Failed: {e}")
-                    else:
-                        st.warning("No products found matching those strict terms.")
-            except Exception as e: st.error(f"SQL/Pandas Error: {e}")
-
-with tab_plate:
-    st.subheader("🍽️ My Plate Builder")
-    st.info("""
-    ℹ️ **How to use this feature (Examples & Logic)**
-    **Plate Builder Logic:**
-    1. Create a New Plate.
-    2. Search for exact food words (e.g. 'chicken', 'egg').
-    3. Add the food with a specific portion (e.g. '150g').
-    4. The system calculates the combined macros.
-    5. Use the 🗑️ buttons to delete incorrect items or entire plates.
-    
-    *Example Plates:*
-    1. `150g White Rice` + `50g Chicken Breast` + `100g Green Beans`
-    2. `200g Potatoes` + `100g Tomatoes` + `100g Beef`
-    3. `100g Spinach Salad` + `50g Feta Cheese`
-    4. `200g Lentils` + `100g Quinoa`
-    5. `100g Apple` + `30g Almonds`
-    """)
-    uid = get_user_id(st.session_state["authenticated_user"])
-    conn = get_db_connection('app_auth')
-    if conn and uid:
-        with conn.cursor() as cursor:
-            cursor.execute("SELECT id, plate_name FROM plates WHERE user_id = %s", (uid,))
-            plates = cursor.fetchall()
-            
-            with st.expander("➕ Create a New Plate"):
-                new_plate_name = st.text_input("Plate Name")
-                if st.button("Create Plate"):
-                    cursor.execute("INSERT INTO plates (user_id, plate_name) VALUES (%s, %s)", (uid, new_plate_name))
-                    conn.commit()
-                    st.session_state["active_plate"] = new_plate_name
-                    st.rerun()
-
-            if plates:
-                colA, colB = st.columns([4, 1])
-                plate_names = [p['plate_name'] for p in plates]
-                default_idx = plate_names.index(st.session_state["active_plate"]) if "active_plate" in st.session_state and st.session_state["active_plate"] in plate_names else 0
-                selected_plate = colA.selectbox("Select Active Plate", plate_names, index=default_idx)
-                st.session_state["active_plate"] = selected_plate
-                active_p_id = next(p['id'] for p in plates if p['plate_name'] == selected_plate)
-                
-                if colB.button("🗑️ Delete Plate"):
-                    cursor.execute("DELETE FROM plates WHERE id = %s", (active_p_id,))
-                    conn.commit()
-                    if "active_plate" in st.session_state: del st.session_state["active_plate"]
-                    st.rerun()
-                
-                cursor.execute("""
-                    SELECT i.id, i.product_code, MAX(i.quantity_grams) as quantity_grams, MAX(p.product_name) as product_name, MAX(m.proteins_100g) as proteins_100g, MAX(m.fat_100g) as fat_100g, MAX(m.carbohydrates_100g) as carbohydrates_100g 
-                    FROM plate_items i LEFT JOIN products_core p ON i.product_code = p.code LEFT JOIN products_macros m ON i.product_code = m.code WHERE i.plate_id = %s
-                    GROUP BY i.id, i.product_code
-                """, (active_p_id,))
-                items = cursor.fetchall()
-                if items:
-                    for i in items:
-                        c1, c2 = st.columns([5, 1])
-                        safe_name = html.escape(str(i['product_name']))
-                        c1.markdown(f"<li><b>{i['quantity_grams']}g</b> of {safe_name} (Pro: {i['proteins_100g'] or 0}g)</li>", unsafe_allow_html=True)
-                        if c2.button("🗑️", key=f"del_item_{i['id']}"):
-                            cursor.execute("DELETE FROM plate_items WHERE id = %s", (i['id'],))
-                            conn.commit()
-                            st.rerun()
-                            
-                    total_pro = sum((float(i['proteins_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items)
-                    total_fat = sum((float(i['fat_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items)
-                    total_carb = sum((float(i['carbohydrates_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items)
-                    st.info(f"**Total Protein:** {total_pro:.1f}g | **Total Fat:** {total_fat:.1f}g | **Total Carbs:** {total_carb:.1f}g")
-                
-                st.markdown("---")
-                st.markdown("#### ➕ Add Food to Plate")
-                add_search = st.text_input("Search Exact Product Name (e.g. 'chicken', 'egg')")
-                
-                col_scope, col_comp = st.columns(2)
-                search_scope = col_scope.radio("Search Scope", ["Auto (Cascaded)", "Product Name Only", "Both (Product & Ingredients)", "Ingredients Only"], horizontal=True)
-                comp_reqs = col_comp.multiselect("Require Nutrients (Sorts by highest)", ["Iron", "Vitamin C", "Calcium", "Proteins", "Fiber"])
-                
-                if add_search:
-                    bool_search = " ".join([f"+{w}" for w in add_search.split()])
-                    start_time = time.time()
-                    
-                    def execute_search(match_col_override=None):
-                        m_col = "product_name"
-                        if match_col_override: m_col = match_col_override
-                        elif "Both" in search_scope: m_col = "product_name, ingredients_text"
-                        elif "Ingredients" in search_scope: m_col = "ingredients_text"
-                        
-                        join_min = "LEFT JOIN food_db.products_minerals min ON c.code = min.code" if any(n in comp_reqs for n in ["Iron", "Calcium"]) else ""
-                        join_vit = "LEFT JOIN food_db.products_vitamins v ON c.code = v.code" if "Vitamin C" in comp_reqs else ""
-                        
-                        r_clauses, o_clauses = [], []
-                        if "Iron" in comp_reqs: r_clauses.append("min.iron_100g > 0"); o_clauses.append("min.iron_100g DESC")
-                        if "Vitamin C" in comp_reqs: r_clauses.append("v.`vitamin-c_100g` > 0"); o_clauses.append("v.`vitamin-c_100g` DESC")
-                        if "Calcium" in comp_reqs: r_clauses.append("min.calcium_100g > 0"); o_clauses.append("min.calcium_100g DESC")
-                        if "Proteins" in comp_reqs: r_clauses.append("m.proteins_100g > 0"); o_clauses.append("m.proteins_100g DESC")
-                        if "Fiber" in comp_reqs: r_clauses.append("m.fiber_100g > 0"); o_clauses.append("m.fiber_100g DESC")
-                        
-                        wh_comp = " AND " + " AND ".join(r_clauses) if r_clauses else ""
-                        order_by = "ORDER BY " + ", ".join(o_clauses) if o_clauses else ""
-                        
-                        sql = f"""
-                            SELECT c.code, c.product_name
-                            FROM (
-                                SELECT code, product_name
-                                FROM food_db.products_core
-                                WHERE MATCH({m_col}) AGAINST(%s IN BOOLEAN MODE)
-                                AND product_name IS NOT NULL AND product_name != '' AND product_name != 'None'
-                                ORDER BY LENGTH(product_name) ASC
-                            ) c
-                            JOIN food_db.products_macros m ON c.code = m.code
-                            {join_min}
-                            {join_vit}
-                            WHERE m.proteins_100g IS NOT NULL AND m.fat_100g IS NOT NULL AND m.carbohydrates_100g IS NOT NULL
-                            {wh_comp}
-                            {order_by}
-                        """
-                        cursor.execute(sql, (bool_search,))
-                        return cursor.fetchall()
-
-                    search_res = execute_search()
-                    
-                    if not search_res and search_scope == "Auto (Cascaded)":
-                        st.warning("No product found in names, so I am looking into the ingredients...")
-                        search_res = execute_search("ingredients_text")
-                        
-                    elapsed = time.time() - start_time
-                    st.caption(f"⏱️ Plate Search Executed in {elapsed:.3f} seconds")
-                    if search_res:
-                        options = {f"{r['product_name']} ({r['code']})": r for r in search_res}
-                        selected_str = st.selectbox("Select Product", list(options.keys()))
-                        selected_product = options[selected_str]
-                        
-                        add_amount_str = st.text_input("Portion Quantity (e.g., '100g', '2 tbsp', '1.5 cups', '1 pinch')", value="100g")
-                        
-                        if st.button("Add Item to Plate"):
-                            # Use UnitConverter to parse
-                            grams = UnitConverter.parse_and_convert(add_amount_str, product_name=selected_product['product_name'])
-                            if grams is not None:
-                                cursor.execute("INSERT INTO plate_items (plate_id, product_code, quantity_grams) VALUES (%s, %s, %s)", 
-                                              (active_p_id, selected_product['code'], grams))
-                                conn.commit()
-                                st.success(f"Added {grams}g of {selected_product['product_name']}!")
-                                st.rerun()
-                            else:
-                                st.error("Could not parse unit. Please use format like '100g' or '1 cup'.")
-                    else:
-                        st.warning("No products found.")
-
-with tab_planner:
-    st.subheader("🤖 AI Meal Planner")
-    st.info("""
-    ℹ️ **How to use this feature (Examples)**
-    **Your active conditions are automatically applied to the generated menu.**
-    
-    *Example Prompts:*
-    1. "Generate a full day meal plan for me. I am pregnant, diabetic, and have kidney disease."
-    2. "Plan a pregnancy-safe dinner that won't spike my blood sugar."
-    3. "I need a high-iron lunch that is safe for my kidneys."
-    4. "Plan a breakfast without dairy that is kidney-friendly."
-    5. "Give me a 3-day meal prep plan ensuring no raw fish, controlled protein portions, and steady complex carbs."
-    """)
-    p_col1, p_col2, p_col3 = st.columns(3)
-    target_cal = p_col1.number_input("Target Daily Calories (kcal)", 1000, 5000, 2000, 50)
-    diet_pref = p_col2.selectbox("Dietary Preference", ["Omnivore", "Vegetarian", "Vegan", "Keto", "Paleo"])
-    meal_count = p_col3.slider("Number of Meals", 1, 6, 3)
-    extra_notes = st.text_input("Any additional allergies or goals?")
-    
-    if st.button("Generate Professional Menu"):
-        with st.spinner("Executing Lightning-Fast Context RAG..."):
-            user_eav = get_eav_profile(st.session_state["authenticated_user"])
-            profile_text = ", ".join([f"{p['name']}: {p['value']}" for p in user_eav]) if user_eav else "None"
-            
-            # Pre-fetch database context directly without using AI tools!
-            # Enforce the strict clinical constraints directly via SQL
-            db_context = search_nutrition_db(diet_pref, user_eav)
-            
-            meal_names = ["Breakfast", "Lunch", "Dinner", "Morning Snack", "Afternoon Snack", "Evening Snack"]
-            selected_meals = ", ".join(meal_names[:int(meal_count)])
-            
-            sys_prompt = f"""You are a professional clinical Dietitian planner. Target: {target_cal}kcal. 
-            You must generate a meal plan consisting of EXACTLY {meal_count} meals. Do NOT generate more than {meal_count} meals under any circumstance.
-            The allowed meal(s) are strictly: {selected_meals}.
-            Dietary constraint: {diet_pref}. Additional notes: {extra_notes}.
-            Health profile: {profile_text}. 
-            
-            COGNITIVE SCRATCHPAD INSTRUCTIONS:
-            - You MUST perform all your intermediate thinking, unit conversions (e.g. converting cups, tablespoons, or ounces to exact metric grams based on food density), and calorie/protein mathematical additions inside a `<scratchpad>` tag.
-            - Format:
-              <scratchpad>
-              Calculations:
-              - 1.5 cups of Cheese = X grams (density Y). Calories = A, Protein = B.
-              - 2 tbsp of Peanut Butter = Z grams (density C). Calories = D, Protein = E.
-              - Summation: Total Calories = A + D = Z kcal (vs target {target_cal}kcal). Total Protein = B + E = Fg.
-              </scratchpad>
-              | Meal Time | Exact Food | Portion Size | Calories | Protein |
-              | --- | --- | --- | --- | --- |
-              ...
-            
-            CRITICAL FORMATTING INSTRUCTIONS:
-            - After the </scratchpad> closing tag, you MUST strictly output the menu formatted as a Markdown Table.
-            - The table MUST contain exactly 5 columns separated by pipes (|): | Meal Time | Exact Food | Portion Size | Calories | Protein |
-            - The items in the table MUST be selected strictly from: {db_context}
-            - Do NOT output JSON. Do NOT use tool calls. Skip pleasantries.
-            """
-            
-            temp_messages = [{'role': 'system', 'content': sys_prompt}, {'role': 'user', 'content': 'Generate my meal plan as a markdown table.'}]
-            
-            # Stream the response instantly!
-            try:
-                response_stream = ollama.chat(model='qwen2.5:7b', messages=temp_messages, stream=True)
-                clean_stream = filter_scratchpad_stream(response_stream)
-                ai_reply = st.write_stream(clean_stream)
-                
-                # PDF Generation
-                def generate_pdf(text):
-                    import re
-                    # Aggressive sanitization: if a table row has 4 columns and the last contains a comma or space before 'g', split it
-                    sanitized_lines = []
-                    for line in text.split('\\n'):
-                        line = line.strip()
-                        if line.startswith('|') and line.endswith('|') and '---' not in line:
-                            cols = [c.strip() for c in line.strip('|').split('|')]
-                            # If exactly 4 columns and the last one contains calories and protein merged
-                            if len(cols) == 4 and any(char.isdigit() for char in cols[3]):
-                                # Attempt to split by comma or 'kcal'
-                                if ',' in cols[3]:
-                                    split_last = cols[3].split(',', 1)
-                                    cols = cols[:3] + [split_last[0].strip(), split_last[1].strip()]
-                                elif 'kcal' in cols[3].lower():
-                                    split_last = re.split(r'(?<=kcal)\s+', cols[3], flags=re.IGNORECASE, maxsplit=1)
-                                    if len(split_last) == 2:
-                                        cols = cols[:3] + [split_last[0].strip(), split_last[1].strip()]
-                            sanitized_lines.append('| ' + ' | '.join(cols) + ' |')
-                        else:
-                            sanitized_lines.append(line)
-                    text = '\\n'.join(sanitized_lines)
-
-                    pdf = FPDF()
-                    pdf.add_page()
-                    pdf.set_font("Helvetica", 'B', 16)
-                    pdf.cell(0, 10, "Strict Clinical Meal Plan", new_x="LMARGIN", new_y="NEXT", align='C')
-                    pdf.ln(h=5)
-                    in_table = False
-                    table_data = []
-                    
-                    def flush_table():
-                        if not table_data: return
-                        pdf.set_font("Helvetica", size=9)
-                        # Auto-calculate col_widths based on 5 columns if present
-                        cw = (20, 40, 15, 10, 15) if len(table_data[0]) == 5 else None
-                        try:
-                            with pdf.table(text_align="LEFT", col_widths=cw) as table:
-                                for row_data in table_data:
-                                    row = table.row()
-                                    for datum in row_data:
-                                        row.cell(str(datum).encode('latin-1', 'replace').decode('latin-1'))
-                        except Exception as e:
-                            pdf.multi_cell(0, 8, "Table Render Error: " + str(e))
-                        table_data.clear()
-                        pdf.ln(h=5)
-
-                    for line in text.split('\n'):
-                        line = line.strip()
-                        if not line:
-                            flush_table()
-                            pdf.ln(h=2)
-                            continue
-                        
-                        if line.startswith('|') and line.endswith('|'):
-                            if '---' in line: continue
-                            cols = [col.strip() for col in line.strip('|').split('|')]
-                            
-                            # Normalize column length to prevent FPDF table crashing
-                            if table_data:
-                                target_len = len(table_data[0])
-                                while len(cols) < target_len: cols.append("")
-                                cols = cols[:target_len]
-                                
-                            table_data.append(cols)
-                        else:
-                            flush_table()
-                            pdf.set_font("Helvetica", size=11)
-                            clean_line = str(line).encode('latin-1', 'replace').decode('latin-1')
-                            pdf.multi_cell(0, 8, clean_line)
-                            
-                    flush_table()
-                            
-                    pdf_path = "/tmp/meal_plan.pdf"
-                    pdf.output(pdf_path)
-                    with open(pdf_path, "rb") as f:
-                        return f.read()
-                
-                st.download_button(
-                    label="📄 Download PDF Export",
-                    data=generate_pdf(strip_scratchpad(ai_reply)),
-                    file_name="Clinical_Meal_Plan.pdf",
-                    mime="application/pdf",
-                    type="primary"
-                )
-                
-            except Exception as e:
-                error_msg = str(e).lower()
-                if "404" in error_msg or "not found" in error_msg:
-                    st.warning("⚠️ The AI engine is currently downloading its core models in the background. Please wait a minute and try again!")
-                else:
-                    st.error(f"AI Generation Failed: {e}")
-
-if conn_reader: conn_reader.close()
+# $Id$
+# $Author$
+# $log$
+#ident "@(#)LocalFoodAI:app.py:$Format:%D:%ci:%cN:%h$"
+import streamlit as st
+import pymysql
+import bcrypt
+import random
+import string
+import time
+import os
+import pandas as pd
+import html
+from snmp_notifier import notifier
+from unit_converter import UnitConverter
+from fpdf import FPDF
+import myloginpath
+import ollama
+import bcrypt
+import requests
+import string
+import random
+import smtplib
+from email.message import EmailMessage
+import pandas as pd
+from unit_converter import UnitConverter
+from typing import Optional, List, Dict, Any, Tuple
+from snmp_notifier import notifier
+import time
+
+import threading
+
+def strip_scratchpad(text: str) -> str:
+    import re
+    # Strip out the XML <scratchpad> tag and everything in between, non-greedily
+    clean_text = re.sub(r'<scratchpad>.*?</scratchpad>', '', text, flags=re.DOTALL)
+    return clean_text.strip()
+
+def filter_scratchpad_stream(stream):
+    buffer = ""
+    in_scratchpad = False
+    for chunk in stream:
+        content = chunk['message']['content']
+        buffer += content
+        
+        while True:
+            if not in_scratchpad:
+                start_idx = buffer.find("<scratchpad>")
+                if start_idx != -1:
+                    yield buffer[:start_idx]
+                    buffer = buffer[start_idx:]
+                    in_scratchpad = True
+                else:
+                    yield_len = max(0, len(buffer) - 11)
+                    if yield_len > 0:
+                        yield buffer[:yield_len]
+                        buffer = buffer[yield_len:]
+                    break
+            else:
+                end_idx = buffer.find("</scratchpad>")
+                if end_idx != -1:
+                    buffer = buffer[end_idx + 13:]
+                    in_scratchpad = False
+                else:
+                    keep_len = 12
+                    if len(buffer) > keep_len:
+                        buffer = buffer[-keep_len:]
+                    break
+    if not in_scratchpad and buffer:
+        yield buffer
+
+def pull_model_bg():
+    try: ollama.pull('qwen2.5:7b')
+    except: pass
+threading.Thread(target=pull_model_bg, daemon=True).start()
+
+def local_web_search(query: str) -> str:
+    try:
+        req = requests.get(f'http://127.0.0.1:8080/search', params={'q': query, 'format': 'json'})
+        if req.status_code == 200:
+            data = req.json()
+            results = data.get('results', [])
+            if not results: return f"No results found on the web for '{query}'."
+            snippets = [f"Source: {r.get('url')}\nContent: {r.get('content')}" for r in results[:3]]
+            return "\n\n".join(snippets)
+        return "Search engine returned an error."
+    except Exception as e: return f"Local search engine unreachable: {e}"
+
+search_tool_schema = {
+    'type': 'function',
+    'function': {
+        'name': 'local_web_search',
+        'description': 'Search the internet for info not in DB.',
+        'parameters': {'type': 'object', 'properties': {'query': {'type': 'string'}}, 'required': ['query']},
+    },
+}
+
+def search_nutrition_db(query: str, user_eav=None) -> str:
+    conn = get_db_connection('app_reader')
+    if not conn: return "Database connection failed."
+    try:
+        with conn.cursor() as cursor:
+            # Dynamically build strictly-enforced clinical SQL filters
+            clinical_filters = ""
+            if user_eav:
+                for p in user_eav:
+                    name = p['name'].lower()
+                    val = p['value'].lower()
+                    if name in ['condition', 'illness']:
+                        if val == 'diabetes': clinical_filters += " AND m.sugars_100g < 5.0"
+                        elif 'kidney' in val: clinical_filters += " AND m.proteins_100g < 15.0"
+                        elif 'hypertension' in val: clinical_filters += " AND m.sodium_100g < 0.2"
+                    elif name in ['diet', 'religious', 'preference']:
+                        if val == 'kosher': clinical_filters += " AND c.ingredients_text NOT LIKE '%pork%' AND c.ingredients_text NOT LIKE '%shellfish%'"
+                        elif val == 'halal': clinical_filters += " AND c.ingredients_text NOT LIKE '%pork%' AND c.ingredients_text NOT LIKE '%wine%' AND c.ingredients_text NOT LIKE '%alcohol%'"
+                        elif val in ['christian', 'good friday', 'ash wednesday']: clinical_filters += " AND c.ingredients_text NOT LIKE '%meat%' AND c.ingredients_text NOT LIKE '%beef%' AND c.ingredients_text NOT LIKE '%chicken%' AND c.ingredients_text NOT LIKE '%pork%'"
+
+            sql = f"""
+                SELECT c.code, c.product_name, m.proteins_100g, m.fat_100g, m.carbohydrates_100g, m.sugars_100g 
+                FROM food_db.products_core c
+                LEFT JOIN food_db.products_macros m ON c.code = m.code
+                WHERE MATCH(c.product_name, c.ingredients_text) AGAINST(%s IN BOOLEAN MODE)
+                AND c.product_name IS NOT NULL AND c.product_name != '' AND c.product_name != 'None'
+                {clinical_filters}
+            """
+            bool_query = " ".join([f"+{w}" for w in query.split()])
+            cursor.execute(sql, (bool_query,))
+            results = cursor.fetchall()
+            if not results: return f"No database records found for '{query}'."
+            
+            snippets = []
+            for r in results:
+                snippets.append(f"- {r['product_name']}: Protein {r['proteins_100g']}g, Fat {r['fat_100g']}g, Carbs {r['carbohydrates_100g']}g, Sugars {r['sugars_100g']}g (per 100g)")
+            return "\n".join(snippets)
+    except Exception as e:
+        return f"Database query failed: {e}"
+    finally:
+        conn.close()
+
+db_search_tool_schema = {
+    'type': 'function',
+    'function': {
+        'name': 'search_nutrition_db',
+        'description': 'Search the local medical nutrition database for product macros and ingredients. ALWAYS prioritize this over web search.',
+        'parameters': {'type': 'object', 'properties': {'query': {'type': 'string', 'description': 'The product or food name to search for (e.g. apple, chicken, bread)'}}, 'required': ['query']},
+    },
+}
+
+def get_db_connection(login_path):
+    try:
+        import os
+        db_host = os.environ.get('DB_HOST')
+        # Check if environment variables exist for this login path
+        db_user = os.environ.get(f'{login_path.upper()}_USER') or os.environ.get('DB_USER')
+        db_pass = os.environ.get(f'{login_path.upper()}_PASS') or os.environ.get('DB_PASS')
+
+        if db_host and db_user and db_pass:
+            return pymysql.connect(
+                host=db_host,
+                user=db_user,
+                password=db_pass,
+                database='food_db',
+                cursorclass=pymysql.cursors.DictCursor
+            )
+            
+        conf = myloginpath.parse(login_path)
+        if not conf or not conf.get('user'):
+            st.error(f"⚠️ MySQL configuration missing for `{login_path}`. If you are testing locally on Windows, this app must be run on the Ubuntu server where `mysql_config_editor` is properly configured.")
+            return None
+            
+        return pymysql.connect(
+            host=conf.get('host', '127.0.0.1'),
+            user=conf.get('user'),
+            password=conf.get('password'),
+            database='food_db',
+            cursorclass=pymysql.cursors.DictCursor
+        )
+    except Exception as e:
+        st.error(f"Connection Failed: {e}")
+        return None
+
+from contextlib import contextmanager
+
+@contextmanager
+def db_cursor(login_path: str):
+    conn = get_db_connection(login_path)
+    if not conn:
+        yield None
+        return
+    try:
+        with conn.cursor() as cursor:
+            yield cursor
+        conn.commit()
+    except Exception as e:
+        conn.rollback()
+        st.error(f"Database query error: {e}")
+        raise e
+    finally:
+        conn.close()
+
+def verify_login(username: str, password: str) -> bool:
+    with db_cursor('app_auth') as cursor:
+        if not cursor: return False
+        cursor.execute("SELECT password_hash FROM users WHERE username = %s", (username,))
+        result = cursor.fetchone()
+        if result: return bcrypt.checkpw(password.encode('utf-8'), result['password_hash'].encode('utf-8'))
+    return False
+
+def get_user_id(username: str) -> Optional[int]:
+    with db_cursor('app_auth') as cursor:
+        if not cursor: return None
+        cursor.execute("SELECT id FROM users WHERE username = %s", (username,))
+        result = cursor.fetchone()
+        return result['id'] if result else None
+
+def get_eav_profile(username: str) -> List[Dict[str, Any]]:
+    uid = get_user_id(username)
+    if not uid: return []
+    with db_cursor('app_auth') as cursor:
+        if not cursor: return []
+        cursor.execute("SELECT id, illness_health_condition_diet_dislikes_name as name, illness_health_condition_diet_dislikes_value as value FROM user_health_profiles WHERE user_id = %s", (uid,))
+        return cursor.fetchall()
+
+def get_user_limit(username: str) -> str:
+    with db_cursor('app_auth') as cursor:
+        if not cursor: return "50"
+        cursor.execute("SELECT search_limit FROM users WHERE username = %s", (username,))
+        result = cursor.fetchone()
+        return result['search_limit'] if (result and result['search_limit']) else "50"
+
+def register_user(username: str, password: str, email: str) -> bool:
+    hashed = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
+    try:
+        with db_cursor('app_auth') as cursor:
+            if not cursor: return False
+            cursor.execute("INSERT INTO users (username, password_hash, email) VALUES (%s, %s, %s)", (username, hashed, email))
+        send_email(email, "Welcome to Local Food AI", f"Hello {username}, your account was securely created!", to_name=username.title())
+        return True
+    except pymysql.err.IntegrityError:
+        return False
+
+def send_email(to_email: str, subject: str, body: str, to_name: str = "User") -> Any:
+    msg = EmailMessage()
+    msg.set_content(body)
+    msg['Subject'] = subject
+    msg['From'] = '"Clinical Food AI System" <security@localfoodai.com>'
+    msg['To'] = f'"{to_name}" <{to_email}>'
+    
+    for attempt in range(5):
+        try:
+            s = smtplib.SMTP('localhost', 25)
+            s.send_message(msg)
+            s.quit()
+            return True
+        except Exception as e:
+            if attempt == 4:
+                return f"SMTP Delivery Failed: {str(e)}"
+            time.sleep(2)
+    return "Unknown Error Occurred"
+
+def reset_password(username: str, email: str) -> Any:
+    with db_cursor('app_auth') as cursor:
+        if not cursor: return False
+        cursor.execute("SELECT id, email FROM users WHERE username = %s", (username,))
+        user = cursor.fetchone()
+        if user and user['email'] == email:
+            new_pass = ''.join(random.choices(string.ascii_letters + string.digits, k=10))
+            hashed = bcrypt.hashpw(new_pass.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
+            cursor.execute("UPDATE users SET password_hash = %s WHERE id = %s", (hashed, user['id']))
+            status = send_email(email, "Password Reset", f"Your new temporary password is: {new_pass}", to_name=username.title())
+            return True if status is True else status
+    return False
+
+# UI Theming
+def render_version():
+    st.markdown("---")
+    st.caption("🚀 Version: v1.3.0")
+    st.caption(f"📅 Git ID: $Id$")
+
+st.set_page_config(page_title="Food AI Explorer", page_icon="🍔", layout="wide")
+st.markdown("""
+<style>
+    @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&display=swap');
+    html, body, [class*="css"]  { font-family: 'Inter', sans-serif; background-color: #0b192c; color: #e2e8f0; }
+    h1, h2, h3 { color: #38bdf8 !important; font-weight: 600; letter-spacing: 0.5px; }
+    div[data-testid="stSidebar"] { background: rgba(11, 25, 44, 0.95) !important; backdrop-filter: blur(10px); border-right: 1px solid #1e293b; }
+    .stButton>button { background: linear-gradient(135deg, #0ea5e9, #0284c7); color: white; border: none; border-radius: 6px; }
+    .stButton>button:hover { transform: scale(1.02); }
+    .stTextInput>div>div>input, .stNumberInput>div>div>input, .stSelectbox>div>div>div { background-color: #0f172a; color: #f8fafc; border: 1px solid #38bdf8; }
+</style>
+""", unsafe_allow_html=True)
+
+if "authenticated_user" not in st.session_state:
+    st.session_state["authenticated_user"] = None
+
+with st.sidebar:
+    st.title("User Portal 🔐")
+    render_version()
+    
+    with st.expander("ℹ️ Welcome"):
+        st.info("Welcome to the secure Local Food AI environment.")
+            
+    if st.session_state["authenticated_user"]:
+        st.success(f"Logged in as: {st.session_state['authenticated_user']}")
+        if st.button("Logout"):
+            st.session_state["authenticated_user"] = None
+            st.rerun()
+            
+        eav_data = get_eav_profile(st.session_state["authenticated_user"])
+        uid = get_user_id(st.session_state["authenticated_user"])
+        user_lim = get_user_limit(st.session_state["authenticated_user"])
+        
+        with st.expander("⚙️ Account Preferences"):
+            opts = ["10", "20", "50", "100", "All"]
+            idx = opts.index(user_lim) if user_lim in opts else 2
+            new_lim = st.selectbox("Default Search Limit", opts, index=idx)
+            if new_lim != user_lim:
+                conn = get_db_connection('app_auth')
+                with conn.cursor() as c:
+                    c.execute("UPDATE users SET search_limit = %s WHERE id = %s", (new_lim, uid))
+                    conn.commit()
+                st.rerun()
+
+        with st.expander("➕ Add Condition / Diet"):
+            new_cat = st.selectbox("Category", ["Condition", "Illness", "Diet", "Dislike", "Allergy"])
+            
+            if new_cat == "Condition":
+                new_val = st.selectbox("Value", ["Pregnant", "Breastfeeding", "Low Fat"])
+            elif new_cat == "Illness":
+                new_val = st.selectbox("Value", ["Diabetes", "Hypertension", "Kidney Disease", "Osteoporosis", "Scurvy", "Anemia"])
+            elif new_cat == "Diet":
+                new_val = st.selectbox("Value", ["Vegan", "Vegetarian", "Kosher", "Halal", "Christian", "Good Friday", "Ash Wednesday", "Keto", "Paleo"])
+            else:
+                new_val = st.text_input("Value (e.g. 'peanuts', 'broccoli')").strip()
+                
+            new_val_clean = new_val.lower()
+            
+            if st.button("Add to Profile") and new_val_clean and uid:
+                conn = get_db_connection('app_auth')
+                with conn.cursor() as c:
+                    c.execute("INSERT INTO user_health_profiles (user_id, illness_health_condition_diet_dislikes_name, illness_health_condition_diet_dislikes_value) VALUES (%s, %s, %s)", (uid, new_cat.lower(), new_val_clean))
+                    conn.commit()
+                st.rerun()
+                
+        if eav_data:
+            st.markdown("#### Active Flags")
+            for e in eav_data:
+                col1, col2 = st.columns([4, 1])
+                col1.info(f"**{e['name']}:** {e['value'].title()}")
+                if col2.button("X", key=f"del_eav_{e['id']}"):
+                    conn = get_db_connection('app_auth')
+                    with conn.cursor() as c:
+                        c.execute("DELETE FROM user_health_profiles WHERE id = %s", (e['id'],))
+                        conn.commit()
+                    st.rerun()
+    else:
+        tab1, tab2, tab3 = st.tabs(["Login", "Register", "Reset"])
+        with tab1:
+            l_user = st.text_input("Username", key="l_user").strip()
+            l_pass = st.text_input("Password", type="password", key="l_pass")
+            if st.button("Login"):
+                if verify_login(l_user, l_pass):
+                    notifier.send_alert(f"User Login Success: {l_user}")
+                    st.session_state["authenticated_user"] = l_user
+                    st.rerun()
+                else:
+                    notifier.send_alert(f"User Login Failed: {l_user}")
+                    st.error("Invalid login.")
+        with tab2:
+            r_user = st.text_input("Username", key="r_user")
+            r_email = st.text_input("Email Address", key="r_email")
+            r_pass = st.text_input("Password", type="password", key="r_pass")
+            if st.button("Register"):
+                if len(r_pass) < 6: st.error("Password too short.")
+                elif register_user(r_user, r_pass, r_email): st.success("Registered safely!")
+                else: st.error("Username exists.")
+        with tab3:
+            f_user = st.text_input("Username", key="f_user")
+            f_email = st.text_input("Registered Email", key="f_email")
+            if st.button("Send Reset Link"):
+                status = reset_password(f_user, f_email)
+                if status is True: 
+                    st.success("Password reset emailed.")
+                else: 
+                    st.error(f"Failed: {status}")
+
+if not st.session_state["authenticated_user"]:
+    st.title("🍔 Food AI Medical Explorer")
+    st.info("Please login to interrogate the Clinical Data.")
+    st.stop()
+
+st.title("🍔 Food AI Clinical Explorer")
+conn_reader = get_db_connection('app_reader')
+
+tab_chat, tab_explore, tab_plate, tab_planner = st.tabs(["💬 AI Chat", "🔬 Clinical Search", "🍽️ My Plate Builder", "🤖 AI Meal Planner"])
+
+import re
+
+with tab_chat:
+    c1, c2 = st.columns([4, 1])
+    c1.subheader("Chat with the Context")
+    if c2.button("🧹 Clear Chat"):
+        st.session_state["messages"] = [{"role": "assistant", "content": "How can I help you analyze the food data today?"}]
+        st.rerun()
+    st.info("""
+    ℹ️ **How to use this feature (Examples)**
+    **Your active conditions (e.g. Pregnant, Diabetic) are automatically sent to the AI in the background. You do not need to type them out.**
+    
+    *Examples:*
+    1. "I am pregnant, diabetic, and have kidney problems. Can I eat sushi?"
+    2. "What is a safe snack to stabilize my blood sugar without hurting my kidneys?"
+    3. "Can I drink milk? I need calcium for the baby."
+    4. "Is it safe to eat a large steak for iron?"
+    5. "What foods are strictly forbidden for me?"
+    """)
+    if "messages" not in st.session_state:
+        st.session_state["messages"] = [{"role": "assistant", "content": "How can I help you analyze the food data today?"}]
+
+    # Display chat history, filtering out TOOL_CALLS
+    for msg in st.session_state.messages:
+        if msg["role"] == "tool": continue
+        display_text = re.sub(r'\[TOOL_CALLS\]\s*\[.*?\]', '', msg["content"]).strip()
+        if display_text:
+            st.chat_message(msg["role"]).write(display_text)
+
+    if prompt := st.chat_input("Ask a clinical question about your food..."):
+        st.session_state.messages.append({"role": "user", "content": prompt})
+        st.chat_message("user").write(prompt)
+        
+        user_eav = get_eav_profile(st.session_state["authenticated_user"])
+        profile_text = ", ".join([f"{p['name']}: {p['value']}" for p in user_eav]) if user_eav else "None"
+        
+        db_context = search_nutrition_db(prompt, user_eav)
+        searxng_context = ""
+        
+        if "No database records found" in db_context:
+            try:
+                searxng_url = os.environ.get("SEARXNG_HOST", "http://searxng:8080")
+                resp = requests.get(f"{searxng_url}/search", params={'q': prompt, 'format': 'json'}, timeout=5)
+                if resp.status_code == 200:
+                    results = resp.json().get('results', [])
+                    if results:
+                        snippets = [r.get('content', '') for r in results[:3]]
+                        searxng_context = "Web Search Context: " + " | ".join(snippets)
+            except Exception as e:
+                pass
+                
+        sys_prompt = f"""You are a helpful medical data analyst AI. 
+        Health profile: {profile_text}. 
+        Act as a specialized clinical dietitian. Provide a direct answer. Use Chain of Thought reasoning, and skip pleasantries.
+        Local Database Context: {db_context}
+        {searxng_context}
+        """
+        
+        try:
+            temp_messages = [{"role": "system", "content": sys_prompt}] + [m for m in st.session_state.messages if m["role"] != "tool"]
+            response_stream = ollama.chat(model='qwen2.5:7b', messages=temp_messages, stream=True)
+            
+            with st.chat_message("assistant"):
+                ai_reply = st.write_stream(chunk['message']['content'] for chunk in response_stream)
+            
+            st.session_state.messages.append({"role": "assistant", "content": ai_reply})
+        except Exception as e: 
+            ai_reply = f"Hold on! Engine execution fault: {e}"
+            st.session_state.messages.append({"role": "assistant", "content": ai_reply})
+            st.chat_message("assistant").write(ai_reply)
+
+def highlight_medical_warnings(row):
+    try:
+        val = str(row.get('Medical Warning', ''))
+        if '⚠️' in val: return ['background-color: rgba(255, 0, 0, 0.4); color: white;'] * len(row)
+        if '💚' in val: return ['background-color: rgba(0, 255, 0, 0.3); color: white;'] * len(row)
+    except: pass
+    return [''] * len(row)
+
+with tab_explore:
+    st.subheader("Clinical Data Search")
+    st.info("""
+    ℹ️ **How to use this feature (Examples)**
+    **Your active conditions are automatically flagged (⚠️ or 💚) in the search results.**
+    
+    *Example Searches:*
+    1. `Cereal` *(Checks for high sugar & hidden phosphorus)*
+    2. `Cheese` *(Checks for unpasteurized pregnancy risks & high sodium)*
+    3. `Fruit Juice` *(Checks for high sugar spikes)*
+    4. `Deli Meat` *(Checks for Listeria risk & extreme sodium)*
+    5. `White Rice` *(Safe for kidneys but flags high glycemic index)*
+    """)
+    sq = st.text_input("Search Product Name or Ingredient")
+    cols = st.columns(5)
+    min_pro = cols[0].number_input("Min Protein (g)", 0, 1000, 0)
+    min_fat = cols[1].number_input("Min Fat (g)", 0, 1000, 0)
+    min_carb = cols[2].number_input("Min Carbs (g)", 0, 1000, 0)
+    max_sug = cols[3].number_input("Max Sugar (g)", 0, 1000, 1000)
+    
+    # Load dynamically fetched limit to prevent Pandas Styler crash
+    pd.set_option("styler.render.max_elements", 5000000)
+    opts = [10, 50, 100, 500, 1000]
+    
+    user_lim_str = get_user_limit(st.session_state["authenticated_user"])
+    user_lim_val = 1000 if user_lim_str == "All" else int(user_lim_str)
+    if user_lim_val not in opts: user_lim_val = 50
+    idx = opts.index(user_lim_val)
+    limit_rc = cols[4].selectbox("Limit Results", opts, index=idx)
+    
+    if st.button("Search Database"):
+        st.session_state["trigger_search"] = True
+        
+    if st.session_state.get("trigger_search", False) and sq and conn_reader:
+        notifier.send_alert(f"Medical DB Search Executed: {sq}")
+        with st.spinner("Processing massive clinical query..."):
+            try:
+                with conn_reader.cursor() as cursor:
+                    l_str = "" if limit_rc == "All" else f"LIMIT {limit_rc}"
+                    query = f"""
+                        SELECT c.code, c.product_name, c.generic_name, c.brands, c.ingredients_text,
+                               a.allergens,
+                               m.`energy-kcal_100g`, m.proteins_100g, m.fat_100g, m.carbohydrates_100g, m.sugars_100g, m.fiber_100g, m.sodium_100g, m.salt_100g, m.cholesterol_100g,
+                               v.`vitamin-a_100g`, v.`vitamin-b1_100g`, v.`vitamin-b2_100g`, v.`vitamin-pp_100g`, v.`vitamin-b6_100g`, v.`vitamin-b9_100g`, v.`vitamin-b12_100g`, v.`vitamin-c_100g`, v.`vitamin-d_100g`, v.`vitamin-e_100g`, v.`vitamin-k_100g`,
+                               min.calcium_100g, min.iron_100g, min.magnesium_100g, min.potassium_100g, min.zinc_100g
+                        FROM (
+                            SELECT code, product_name, generic_name, brands, ingredients_text
+                            FROM food_db.products_core
+                            WHERE MATCH(product_name, ingredients_text) AGAINST(%s IN BOOLEAN MODE)
+                            AND product_name IS NOT NULL AND product_name != '' AND product_name != 'None'
+                            {l_str}
+                        ) c
+                        LEFT JOIN food_db.products_allergens a ON c.code = a.code
+                        LEFT JOIN food_db.products_macros m ON c.code = m.code
+                        LEFT JOIN food_db.products_vitamins v ON c.code = v.code
+                        LEFT JOIN food_db.products_minerals min ON c.code = min.code
+                        WHERE (m.proteins_100g >= %s OR m.proteins_100g IS NULL)
+                        AND (m.fat_100g >= %s OR m.fat_100g IS NULL)
+                        AND (m.carbohydrates_100g >= %s OR m.carbohydrates_100g IS NULL)
+                        AND (m.sugars_100g <= %s OR m.sugars_100g IS NULL)
+                    """
+                    sq_bool = " ".join([f"+{w}" for w in sq.split()])
+                    start_time = time.time()
+                    cursor.execute(query, (sq_bool, min_pro, min_fat, min_carb, max_sug))
+                    results = cursor.fetchall()
+                    elapsed = time.time() - start_time
+                    st.caption(f"⏱️ DB Query Executed in {elapsed:.3f} seconds")
+                    
+                    if results:
+                        # Fetch EAV Medical Profile
+                        eav_profile = get_eav_profile(st.session_state["authenticated_user"])
+                        df = pd.DataFrame(results)
+                        
+                        st.markdown("### 🛠️ Dynamic Column Display")
+                        default_columns = [
+                            'code', 'product_name', 'generic_name', 'brands', 'allergens', 'ingredients_text',
+                            'proteins_100g', 'fat_100g', 'carbohydrates_100g', 'sugars_100g', 'sodium_100g', 'energy-kcal_100g',
+                            'vitamin-c_100g', 'iron_100g', 'calcium_100g'
+                        ]
+                        all_fetched_cols = list(df.columns)
+                        valid_defaults = [c for c in default_columns if c in all_fetched_cols]
+                        
+                        if "selected_columns" not in st.session_state or st.button("Reset Default Columns"):
+                            st.session_state["selected_columns"] = valid_defaults
+                            st.rerun()
+                            
+                        chosen_cols = st.multiselect("Customize Dataset View", all_fetched_cols, default=st.session_state["selected_columns"], key="multi_cols")
+                        st.session_state["selected_columns"] = chosen_cols
+                        
+                        # Filter dataframe gracefully, but we retain a copy for background analytics
+                        df_display = df[chosen_cols].copy()
+                        warnings_col = []
+                        
+                        for idx, row in df.iterrows():
+                            warns = []
+                            ing_text = str(row['ingredients_text']).lower()
+                            all_text = str(row['allergens']).lower()
+                            
+                            for param in eav_profile:
+                                cat = param['name'].lower()
+                                val = param['value']
+                                
+                                # Disease Analytics
+                                if cat == 'illness':
+                                    if val == 'diabetes' and pd.notnull(row.get('sugars_100g')) and float(row['sugars_100g']) > 10.0:
+                                        warns.append("⚠️ High Sugar (Diabetes)")
+                                    if (val == 'hypertension' or val == 'high bp') and pd.notnull(row.get('sodium_100g')) and float(row['sodium_100g']) > 1.5:
+                                        warns.append("⚠️ High Salt (Hypertension)")
+                                    if val == 'scurvy' and pd.notnull(row.get('vitamin-c_100g')) and float(row['vitamin-c_100g']) > 0.005:
+                                        warns.append("💚 High Vitamin C (Scurvy Recommended)")
+                                    if val == 'anemia' and pd.notnull(row.get('iron_100g')) and float(row['iron_100g']) > 0.002:
+                                        warns.append("💚 High Iron (Anemia Recommended)")
+                                        
+                                # Condition Analytics
+                                if cat == 'condition':
+                                    if val == 'pregnant':
+                                        if ('cru' in ing_text or 'raw' in ing_text or 'viande crue' in ing_text):
+                                            warns.append("⚠️ Raw Foods (Pregnancy Toxoplasmosis)")
+                                        if pd.notnull(row.get('iron_100g')) and float(row['iron_100g']) > 0.002:
+                                            warns.append("💚 Med-High Iron (Pregnancy Health)")
+                                    if val == 'low fat' and pd.notnull(row.get('fat_100g')) and float(row['fat_100g']) > 20.0:
+                                        warns.append("⚠️ High Fat")
+                                    if val == 'osteoporosis' and pd.notnull(row.get('calcium_100g')) and float(row['calcium_100g']) > 0.1:
+                                        warns.append("💚 High Calcium (Bone Health)")
+                                        
+                            if eav_data:
+                                ing_text = str(row.get('ingredients_text', '')).lower()
+                                all_text = str(row.get('allergens', '')).lower()
+                                product_name_text = str(row.get('product_name', '')).lower()
+                                
+                                for e in eav_data:
+                                    cat = str(e['name']).lower()
+                                    val = str(e['value']).lower()
+                                    
+                                    # Clinical Trace Checks...
+                                    if cat == 'condition' and (val == 'pregnant' or val == 'pregnancy' or val == 'breastfeeding'):
+                                        # Forbidden / High Risk (Toxoplasmosis & Listeria)
+                                        if any(x in ing_text or x in product_name_text for x in ['cru', 'raw', 'viande crue', 'sushi', 'sashimi', 'poisson cru']):
+                                            warns.append("⚠️ Forbidden: Raw Meat/Fish (Toxoplasmosis/Parasite Risk)")
+                                        if any(x in ing_text or x in product_name_text for x in ['lait cru', 'unpasteurized', 'non pasteurisé']):
+                                            warns.append("⚠️ Forbidden: Unpasteurized Dairy (Listeria Risk)")
+                                        if any(x in ing_text or x in product_name_text for x in ['alcool', 'wine', 'alcohol', 'beer']):
+                                            warns.append("⚠️ Forbidden: Contains Alcohol")
+                                            
+                                        # Recommended (Iron & Calcium)
+                                        if float(row.get('iron_100g', 0) or 0) > 0.003:
+                                            warns.append("💚 Recommended: High Iron (Pregnancy Health)")
+                                        if float(row.get('calcium_100g', 0) or 0) > 0.120:
+                                            warns.append("💚 Recommended: High Calcium (Bone Health / Breastfeeding)")
+                                    
+                                    if cat == 'illness' and val == 'osteoporosis':
+                                        if float(row.get('calcium_100g', 0) or 0) < 0.120:
+                                            warns.append("⚠️ Low Calcium (Osteoporosis Risk)")
+                                        else:
+                                            warns.append("💚 Recommended (High Calcium)")
+                                            
+                                    if cat == 'illness' and val == 'scurvy':
+                                        if float(row.get('vitamin-c_100g', 0) or 0) < 0.010:
+                                            warns.append("⚠️ Low Vitamin C (Scurvy Risk)")
+                                        else:
+                                            warns.append("💚 Recommended (High Vitamin C)")
+                                            
+                                    if cat == 'diet' and val in ['vegan', 'vegetarian']:
+                                        if any(x in ing_text for x in ['meat', 'beef', 'chicken', 'fish', 'gelatin', 'whey', 'pork', 'porc', 'poulet']):
+                                            warns.append("⚠️ Contains Animal Products")
+                                    if cat == 'diet' and val == 'halal':
+                                        if any(x in ing_text for x in ['pork', 'pig', 'porc', 'wine', 'alcohol', 'beer', 'vin']):
+                                            warns.append("⚠️ Probable Haram Ingredients (e.g. Pork/Wine)")
+                                            
+                                    if cat in ['dislike', 'allergy']:
+                                        if val in ing_text or val in all_text or val in product_name_text:
+                                            warns.append(f"⚠️ Contains: {val.upper()}")
+                                            
+                            warnings_col.append(" | ".join(list(set(warns))) if warns else "✅ Safe for Profile")
+                            
+                        df_display.insert(0, 'Medical Warning', warnings_col)
+                        styled_df = df_display.style.apply(highlight_medical_warnings, axis=1)
+
+                        st.success(f"Analysed {len(results)} records utilizing dynamic Partitions!")
+                        st.dataframe(styled_df, use_container_width=True)
+                        
+                        if st.button("🤖 Ask AI to Evaluate This Table"):
+                            with st.spinner("AI is dynamically evaluating these records against your profile..."):
+                                user_eav = get_eav_profile(st.session_state["authenticated_user"])
+                                profile_text = ", ".join([f"{p['name']}: {p['value']}" for p in user_eav]) if user_eav else "None"
+                                minimal_records = df_display[['product_name', 'Medical Warning']].head(10).to_dict('records')
+                                eval_prompt = f"The user has this profile: {profile_text}. Evaluate these top foods and state which are highly recommended or strictly forbidden: {minimal_records}. Provide a direct, readable clinical summary. Do not output raw JSON."
+                                try:
+                                    response_stream = ollama.chat(model='qwen2.5:7b', messages=[{'role': 'user', 'content': eval_prompt}], stream=True)
+                                    st.write_stream(chunk['message']['content'] for chunk in response_stream)
+                                except Exception as e:
+                                    error_msg = str(e).lower()
+                                    if "404" in error_msg or "not found" in error_msg:
+                                        st.warning("⚠️ The AI engine is currently downloading its core models in the background. Please wait a minute and try again!")
+                                    else:
+                                        st.error(f"AI Evaluation Failed: {e}")
+                    else:
+                        st.warning("No products found matching those strict terms.")
+            except Exception as e: st.error(f"SQL/Pandas Error: {e}")
+
+with tab_plate:
+    st.subheader("🍽️ My Plate Builder")
+    st.info("""
+    ℹ️ **How to use this feature (Examples & Logic)**
+    **Plate Builder Logic:**
+    1. Create a New Plate.
+    2. Search for exact food words (e.g. 'chicken', 'egg').
+    3. Add the food with a specific portion (e.g. '150g').
+    4. The system calculates the combined macros.
+    5. Use the 🗑️ buttons to delete incorrect items or entire plates.
+    
+    *Example Plates:*
+    1. `150g White Rice` + `50g Chicken Breast` + `100g Green Beans`
+    2. `200g Potatoes` + `100g Tomatoes` + `100g Beef`
+    3. `100g Spinach Salad` + `50g Feta Cheese`
+    4. `200g Lentils` + `100g Quinoa`
+    5. `100g Apple` + `30g Almonds`
+    """)
+    uid = get_user_id(st.session_state["authenticated_user"])
+    conn = get_db_connection('app_auth')
+    if conn and uid:
+        with conn.cursor() as cursor:
+            cursor.execute("SELECT id, plate_name FROM plates WHERE user_id = %s", (uid,))
+            plates = cursor.fetchall()
+            
+            with st.expander("➕ Create a New Plate"):
+                new_plate_name = st.text_input("Plate Name")
+                if st.button("Create Plate"):
+                    cursor.execute("INSERT INTO plates (user_id, plate_name) VALUES (%s, %s)", (uid, new_plate_name))
+                    conn.commit()
+                    st.session_state["active_plate"] = new_plate_name
+                    st.rerun()
+
+            if plates:
+                colA, colB = st.columns([4, 1])
+                plate_names = [p['plate_name'] for p in plates]
+                default_idx = plate_names.index(st.session_state["active_plate"]) if "active_plate" in st.session_state and st.session_state["active_plate"] in plate_names else 0
+                selected_plate = colA.selectbox("Select Active Plate", plate_names, index=default_idx)
+                st.session_state["active_plate"] = selected_plate
+                active_p_id = next(p['id'] for p in plates if p['plate_name'] == selected_plate)
+                
+                if colB.button("🗑️ Delete Plate"):
+                    cursor.execute("DELETE FROM plates WHERE id = %s", (active_p_id,))
+                    conn.commit()
+                    if "active_plate" in st.session_state: del st.session_state["active_plate"]
+                    st.rerun()
+                
+                cursor.execute("""
+                    SELECT i.id, i.product_code, MAX(i.quantity_grams) as quantity_grams, MAX(p.product_name) as product_name, MAX(m.proteins_100g) as proteins_100g, MAX(m.fat_100g) as fat_100g, MAX(m.carbohydrates_100g) as carbohydrates_100g 
+                    FROM plate_items i LEFT JOIN products_core p ON i.product_code = p.code LEFT JOIN products_macros m ON i.product_code = m.code WHERE i.plate_id = %s
+                    GROUP BY i.id, i.product_code
+                """, (active_p_id,))
+                items = cursor.fetchall()
+                if items:
+                    for i in items:
+                        c1, c2 = st.columns([5, 1])
+                        safe_name = html.escape(str(i['product_name']))
+                        c1.markdown(f"<li><b>{i['quantity_grams']}g</b> of {safe_name} (Pro: {i['proteins_100g'] or 0}g)</li>", unsafe_allow_html=True)
+                        if c2.button("🗑️", key=f"del_item_{i['id']}"):
+                            cursor.execute("DELETE FROM plate_items WHERE id = %s", (i['id'],))
+                            conn.commit()
+                            st.rerun()
+                            
+                    total_pro = sum((float(i['proteins_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items)
+                    total_fat = sum((float(i['fat_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items)
+                    total_carb = sum((float(i['carbohydrates_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items)
+                    st.info(f"**Total Protein:** {total_pro:.1f}g | **Total Fat:** {total_fat:.1f}g | **Total Carbs:** {total_carb:.1f}g")
+                
+                st.markdown("---")
+                st.markdown("#### ➕ Add Food to Plate")
+                add_search = st.text_input("Search Exact Product Name (e.g. 'chicken', 'egg')")
+                
+                col_scope, col_comp = st.columns(2)
+                search_scope = col_scope.radio("Search Scope", ["Auto (Cascaded)", "Product Name Only", "Both (Product & Ingredients)", "Ingredients Only"], horizontal=True)
+                comp_reqs = col_comp.multiselect("Require Nutrients (Sorts by highest)", ["Iron", "Vitamin C", "Calcium", "Proteins", "Fiber"])
+                
+                if add_search:
+                    bool_search = " ".join([f"+{w}" for w in add_search.split()])
+                    start_time = time.time()
+                    
+                    def execute_search(match_col_override=None):
+                        m_col = "product_name"
+                        if match_col_override: m_col = match_col_override
+                        elif "Both" in search_scope: m_col = "product_name, ingredients_text"
+                        elif "Ingredients" in search_scope: m_col = "ingredients_text"
+                        
+                        join_min = "LEFT JOIN food_db.products_minerals min ON c.code = min.code" if any(n in comp_reqs for n in ["Iron", "Calcium"]) else ""
+                        join_vit = "LEFT JOIN food_db.products_vitamins v ON c.code = v.code" if "Vitamin C" in comp_reqs else ""
+                        
+                        r_clauses, o_clauses = [], []
+                        if "Iron" in comp_reqs: r_clauses.append("min.iron_100g > 0"); o_clauses.append("min.iron_100g DESC")
+                        if "Vitamin C" in comp_reqs: r_clauses.append("v.`vitamin-c_100g` > 0"); o_clauses.append("v.`vitamin-c_100g` DESC")
+                        if "Calcium" in comp_reqs: r_clauses.append("min.calcium_100g > 0"); o_clauses.append("min.calcium_100g DESC")
+                        if "Proteins" in comp_reqs: r_clauses.append("m.proteins_100g > 0"); o_clauses.append("m.proteins_100g DESC")
+                        if "Fiber" in comp_reqs: r_clauses.append("m.fiber_100g > 0"); o_clauses.append("m.fiber_100g DESC")
+                        
+                        wh_comp = " AND " + " AND ".join(r_clauses) if r_clauses else ""
+                        order_by = "ORDER BY " + ", ".join(o_clauses) if o_clauses else ""
+                        
+                        sql = f"""
+                            SELECT c.code, c.product_name
+                            FROM (
+                                SELECT code, product_name
+                                FROM food_db.products_core
+                                WHERE MATCH({m_col}) AGAINST(%s IN BOOLEAN MODE)
+                                AND product_name IS NOT NULL AND product_name != '' AND product_name != 'None'
+                                ORDER BY LENGTH(product_name) ASC
+                            ) c
+                            JOIN food_db.products_macros m ON c.code = m.code
+                            {join_min}
+                            {join_vit}
+                            WHERE m.proteins_100g IS NOT NULL AND m.fat_100g IS NOT NULL AND m.carbohydrates_100g IS NOT NULL
+                            {wh_comp}
+                            {order_by}
+                        """
+                        cursor.execute(sql, (bool_search,))
+                        return cursor.fetchall()
+
+                    search_res = execute_search()
+                    
+                    if not search_res and search_scope == "Auto (Cascaded)":
+                        st.warning("No product found in names, so I am looking into the ingredients...")
+                        search_res = execute_search("ingredients_text")
+                        
+                    elapsed = time.time() - start_time
+                    st.caption(f"⏱️ Plate Search Executed in {elapsed:.3f} seconds")
+                    if search_res:
+                        options = {f"{r['product_name']} ({r['code']})": r for r in search_res}
+                        selected_str = st.selectbox("Select Product", list(options.keys()))
+                        selected_product = options[selected_str]
+                        
+                        add_amount_str = st.text_input("Portion Quantity (e.g., '100g', '2 tbsp', '1.5 cups', '1 pinch')", value="100g")
+                        
+                        if st.button("Add Item to Plate"):
+                            # Use UnitConverter to parse
+                            grams = UnitConverter.parse_and_convert(add_amount_str, product_name=selected_product['product_name'])
+                            if grams is not None:
+                                cursor.execute("INSERT INTO plate_items (plate_id, product_code, quantity_grams) VALUES (%s, %s, %s)", 
+                                              (active_p_id, selected_product['code'], grams))
+                                conn.commit()
+                                st.success(f"Added {grams}g of {selected_product['product_name']}!")
+                                st.rerun()
+                            else:
+                                st.error("Could not parse unit. Please use format like '100g' or '1 cup'.")
+                    else:
+                        st.warning("No products found.")
+
+with tab_planner:
+    st.subheader("🤖 AI Meal Planner")
+    st.info("""
+    ℹ️ **How to use this feature (Examples)**
+    **Your active conditions are automatically applied to the generated menu.**
+    
+    *Example Prompts:*
+    1. "Generate a full day meal plan for me. I am pregnant, diabetic, and have kidney disease."
+    2. "Plan a pregnancy-safe dinner that won't spike my blood sugar."
+    3. "I need a high-iron lunch that is safe for my kidneys."
+    4. "Plan a breakfast without dairy that is kidney-friendly."
+    5. "Give me a 3-day meal prep plan ensuring no raw fish, controlled protein portions, and steady complex carbs."
+    """)
+    p_col1, p_col2, p_col3 = st.columns(3)
+    target_cal = p_col1.number_input("Target Daily Calories (kcal)", 1000, 5000, 2000, 50)
+    diet_pref = p_col2.selectbox("Dietary Preference", ["Omnivore", "Vegetarian", "Vegan", "Keto", "Paleo"])
+    meal_count = p_col3.slider("Number of Meals", 1, 6, 3)
+    extra_notes = st.text_input("Any additional allergies or goals?")
+    
+    if st.button("Generate Professional Menu"):
+        with st.spinner("Executing Lightning-Fast Context RAG..."):
+            user_eav = get_eav_profile(st.session_state["authenticated_user"])
+            profile_text = ", ".join([f"{p['name']}: {p['value']}" for p in user_eav]) if user_eav else "None"
+            
+            # Pre-fetch database context directly without using AI tools!
+            # Enforce the strict clinical constraints directly via SQL
+            db_context = search_nutrition_db(diet_pref, user_eav)
+            
+            meal_names = ["Breakfast", "Lunch", "Dinner", "Morning Snack", "Afternoon Snack", "Evening Snack"]
+            selected_meals = ", ".join(meal_names[:int(meal_count)])
+            
+            sys_prompt = f"""You are a professional clinical Dietitian planner. Target: {target_cal}kcal. 
+            You must generate a meal plan consisting of EXACTLY {meal_count} meals. Do NOT generate more than {meal_count} meals under any circumstance.
+            The allowed meal(s) are strictly: {selected_meals}.
+            Dietary constraint: {diet_pref}. Additional notes: {extra_notes}.
+            Health profile: {profile_text}. 
+            
+            COGNITIVE SCRATCHPAD INSTRUCTIONS:
+            - You MUST perform all your intermediate thinking, unit conversions (e.g. converting cups, tablespoons, or ounces to exact metric grams based on food density), and calorie/protein mathematical additions inside a `<scratchpad>` tag.
+            - Format:
+              <scratchpad>
+              Calculations:
+              - 1.5 cups of Cheese = X grams (density Y). Calories = A, Protein = B.
+              - 2 tbsp of Peanut Butter = Z grams (density C). Calories = D, Protein = E.
+              - Summation: Total Calories = A + D = Z kcal (vs target {target_cal}kcal). Total Protein = B + E = Fg.
+              </scratchpad>
+              | Meal Time | Exact Food | Portion Size | Calories | Protein |
+              | --- | --- | --- | --- | --- |
+              ...
+            
+            CRITICAL FORMATTING INSTRUCTIONS:
+            - After the </scratchpad> closing tag, you MUST strictly output the menu formatted as a Markdown Table.
+            - The table MUST contain exactly 5 columns separated by pipes (|): | Meal Time | Exact Food | Portion Size | Calories | Protein |
+            - The items in the table MUST be selected strictly from: {db_context}
+            - Do NOT output JSON. Do NOT use tool calls. Skip pleasantries.
+            """
+            
+            temp_messages = [{'role': 'system', 'content': sys_prompt}, {'role': 'user', 'content': 'Generate my meal plan as a markdown table.'}]
+            
+            # Stream the response instantly!
+            try:
+                response_stream = ollama.chat(model='qwen2.5:7b', messages=temp_messages, stream=True)
+                clean_stream = filter_scratchpad_stream(response_stream)
+                ai_reply = st.write_stream(clean_stream)
+                
+                # PDF Generation
+                def generate_pdf(text):
+                    import re
+                    # Aggressive sanitization: if a table row has 4 columns and the last contains a comma or space before 'g', split it
+                    sanitized_lines = []
+                    for line in text.split('\\n'):
+                        line = line.strip()
+                        if line.startswith('|') and line.endswith('|') and '---' not in line:
+                            cols = [c.strip() for c in line.strip('|').split('|')]
+                            # If exactly 4 columns and the last one contains calories and protein merged
+                            if len(cols) == 4 and any(char.isdigit() for char in cols[3]):
+                                # Attempt to split by comma or 'kcal'
+                                if ',' in cols[3]:
+                                    split_last = cols[3].split(',', 1)
+                                    cols = cols[:3] + [split_last[0].strip(), split_last[1].strip()]
+                                elif 'kcal' in cols[3].lower():
+                                    split_last = re.split(r'(?<=kcal)\s+', cols[3], flags=re.IGNORECASE, maxsplit=1)
+                                    if len(split_last) == 2:
+                                        cols = cols[:3] + [split_last[0].strip(), split_last[1].strip()]
+                            sanitized_lines.append('| ' + ' | '.join(cols) + ' |')
+                        else:
+                            sanitized_lines.append(line)
+                    text = '\\n'.join(sanitized_lines)
+
+                    pdf = FPDF()
+                    pdf.add_page()
+                    pdf.set_font("Helvetica", 'B', 16)
+                    pdf.cell(0, 10, "Strict Clinical Meal Plan", new_x="LMARGIN", new_y="NEXT", align='C')
+                    pdf.ln(h=5)
+                    in_table = False
+                    table_data = []
+                    
+                    def flush_table():
+                        if not table_data: return
+                        pdf.set_font("Helvetica", size=9)
+                        # Auto-calculate col_widths based on 5 columns if present
+                        cw = (20, 40, 15, 10, 15) if len(table_data[0]) == 5 else None
+                        try:
+                            with pdf.table(text_align="LEFT", col_widths=cw) as table:
+                                for row_data in table_data:
+                                    row = table.row()
+                                    for datum in row_data:
+                                        row.cell(str(datum).encode('latin-1', 'replace').decode('latin-1'))
+                        except Exception as e:
+                            pdf.multi_cell(0, 8, "Table Render Error: " + str(e))
+                        table_data.clear()
+                        pdf.ln(h=5)
+
+                    for line in text.split('\n'):
+                        line = line.strip()
+                        if not line:
+                            flush_table()
+                            pdf.ln(h=2)
+                            continue
+                        
+                        if line.startswith('|') and line.endswith('|'):
+                            if '---' in line: continue
+                            cols = [col.strip() for col in line.strip('|').split('|')]
+                            
+                            # Normalize column length to prevent FPDF table crashing
+                            if table_data:
+                                target_len = len(table_data[0])
+                                while len(cols) < target_len: cols.append("")
+                                cols = cols[:target_len]
+                                
+                            table_data.append(cols)
+                        else:
+                            flush_table()
+                            pdf.set_font("Helvetica", size=11)
+                            clean_line = str(line).encode('latin-1', 'replace').decode('latin-1')
+                            pdf.multi_cell(0, 8, clean_line)
+                            
+                    flush_table()
+                            
+                    pdf_path = "/tmp/meal_plan.pdf"
+                    pdf.output(pdf_path)
+                    with open(pdf_path, "rb") as f:
+                        return f.read()
+                
+                st.download_button(
+                    label="📄 Download PDF Export",
+                    data=generate_pdf(strip_scratchpad(ai_reply)),
+                    file_name="Clinical_Meal_Plan.pdf",
+                    mime="application/pdf",
+                    type="primary"
+                )
+                
+            except Exception as e:
+                error_msg = str(e).lower()
+                if "404" in error_msg or "not found" in error_msg:
+                    st.warning("⚠️ The AI engine is currently downloading its core models in the background. Please wait a minute and try again!")
+                else:
+                    st.error(f"AI Generation Failed: {e}")
+
+if conn_reader: conn_reader.close()

+ 191 - 191
docker-compose.yml

@@ -1,191 +1,191 @@
-services:
-  mysql:
-    build:
-      context: ./docker/mysql
-    ports:
-      - "3306:3306"
-    volumes:
-      - mysql_data:/var/lib/mysql
-      - ./my.cnf:/etc/mysql/conf.d/custom_ai_app.cnf
-      - ./init.sql:/docker-entrypoint-initdb.d/1-init.sql
-    environment:
-      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
-    healthcheck:
-      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
-      interval: 10s
-      timeout: 5s
-      retries: 20
-    restart: always
-    logging:
-      driver: "json-file"
-      options:
-        max-size: "50m"
-        max-file: "3"
-
-  ingest:
-    build:
-      context: .
-      dockerfile: docker/ingest/Dockerfile
-    environment:
-      - DB_HOST=mysql
-      - DB_USER=food_loader
-      - DB_PASS=${DB_LOADER_PASS}
-    volumes:
-      - ./:/app
-    profiles:
-      - manual
-
-  ollama:
-    image: ollama/ollama:latest
-    volumes:
-      - ollama_data:/root/.ollama
-    restart: always
-    logging:
-      driver: "json-file"
-      options:
-        max-size: "50m"
-        max-file: "3"
-
-  searxng:
-    image: searxng/searxng:latest
-    ports:
-      - "8085:8080"
-    volumes:
-      - ./searxng:/etc/searxng
-    environment:
-      - SEARXNG_BASE_URL=http://localhost:8080/
-    restart: always
-    logging:
-      driver: "json-file"
-      options:
-        max-size: "50m"
-        max-file: "3"
-
-  app:
-    build:
-      context: .
-      dockerfile: docker/app/Dockerfile
-    ports:
-      - "8502:8501"
-    environment:
-      - DB_HOST=mysql
-      - DB_USER=food_reader
-      - DB_PASS=${DB_READER_PASS}
-      - APP_AUTH_USER=food_app_auth
-      - APP_AUTH_PASS=${DB_APP_AUTH_PASS}
-      - OLLAMA_HOST=http://ollama:11434
-      - SEARXNG_HOST=http://searxng:8080
-    restart: always
-    logging:
-      driver: "json-file"
-      options:
-        max-size: "50m"
-        max-file: "3"
-
-  nginx:
-    image: nginx:latest
-    ports:
-      - "80:80"
-    volumes:
-      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
-    restart: always
-    logging:
-      driver: "json-file"
-      options:
-        max-size: "50m"
-        max-file: "3"
-
-  zabbix-server:
-    image: zabbix/zabbix-server-mysql:ubuntu-7.0-latest
-    environment:
-      - DB_SERVER_HOST=mysql
-      - MYSQL_USER=zabbix
-      - MYSQL_PASSWORD=${MYSQL_ZABBIX_PASSWORD}
-      - ZBX_SNMPTRAPPER=1
-    restart: always
-    logging:
-      driver: "json-file"
-      options:
-        max-size: "50m"
-        max-file: "3"
-    ports:
-      - "10051:10051"
-
-  zabbix-web:
-    image: zabbix/zabbix-web-nginx-mysql:ubuntu-7.0-latest
-    ports:
-      - "8081:8080"
-      - "8444:8443"
-    environment:
-      - DB_SERVER_HOST=mysql
-      - MYSQL_USER=zabbix
-      - MYSQL_PASSWORD=${MYSQL_ZABBIX_PASSWORD}
-      - ZBX_SERVER_HOST=zabbix-server
-      - PHP_TZ=Europe/Paris
-    restart: always
-    logging:
-      driver: "json-file"
-      options:
-        max-size: "50m"
-        max-file: "3"
-
-  zabbix-agent:
-    image: zabbix/zabbix-agent:ubuntu-7.0-latest
-    environment:
-      - ZBX_HOSTNAME=DistributedNode
-      - ZBX_SERVER_HOST=zabbix-server
-    privileged: true
-    pid: "host"
-    volumes:
-      - /var/run:/var/run
-    restart: always
-    logging:
-      driver: "json-file"
-      options:
-        max-size: "50m"
-        max-file: "3"
-
-
-  airflow-webserver:
-    image: apache/airflow:2.8.1
-    environment:
-      - AIRFLOW__CORE__EXECUTOR=SequentialExecutor
-      - AIRFLOW__DATABASE__SQL_ALCHEMY_CONN=sqlite:////opt/airflow/data/airflow.db
-      - AIRFLOW__CORE__LOAD_EXAMPLES=False
-    ports:
-      - "8082:8080"
-    volumes:
-      - ./dags:/opt/airflow/dags
-      - ./logs:/opt/airflow/logs
-      - ./data:/opt/airflow/data
-      - /var/run/docker.sock:/var/run/docker.sock
-    command: webserver
-    restart: always
-    logging:
-      driver: "json-file"
-      options:
-        max-size: "50m"
-        max-file: "3"
-
-  airflow-scheduler:
-    image: apache/airflow:2.8.1
-    environment:
-      - AIRFLOW__CORE__EXECUTOR=SequentialExecutor
-      - AIRFLOW__DATABASE__SQL_ALCHEMY_CONN=sqlite:////opt/airflow/data/airflow.db
-      - AIRFLOW__CORE__LOAD_EXAMPLES=False
-    volumes:
-      - ./dags:/opt/airflow/dags
-      - ./logs:/opt/airflow/logs
-      - ./data:/opt/airflow/data
-      - /var/run/docker.sock:/var/run/docker.sock
-    command: bash -c "airflow db migrate && airflow users create --role Admin --username admin --email admin --firstname admin --lastname admin --password admin && airflow scheduler"
-    restart: always
-    logging:
-      driver: "json-file"
-      options:
-        max-size: "50m"
-        max-file: "3"
-
-volumes:
-  mysql_data:
-  ollama_data:
+services:
+  mysql:
+    build:
+      context: ./docker/mysql
+    ports:
+      - "3306:3306"
+    volumes:
+      - mysql_data:/var/lib/mysql
+      - ./my.cnf:/etc/mysql/conf.d/custom_ai_app.cnf
+      - ./init.sql:/docker-entrypoint-initdb.d/1-init.sql
+    environment:
+      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
+    healthcheck:
+      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
+      interval: 10s
+      timeout: 5s
+      retries: 20
+    restart: always
+    logging:
+      driver: "json-file"
+      options:
+        max-size: "50m"
+        max-file: "3"
+
+  ingest:
+    build:
+      context: .
+      dockerfile: docker/ingest/Dockerfile
+    environment:
+      - DB_HOST=mysql
+      - DB_USER=food_loader
+      - DB_PASS=${DB_LOADER_PASS}
+    volumes:
+      - ./:/app
+    profiles:
+      - manual
+
+  ollama:
+    image: ollama/ollama:latest
+    volumes:
+      - ollama_data:/root/.ollama
+    restart: always
+    logging:
+      driver: "json-file"
+      options:
+        max-size: "50m"
+        max-file: "3"
+
+  searxng:
+    image: searxng/searxng:latest
+    ports:
+      - "8085:8080"
+    volumes:
+      - ./searxng:/etc/searxng
+    environment:
+      - SEARXNG_BASE_URL=http://localhost:8080/
+    restart: always
+    logging:
+      driver: "json-file"
+      options:
+        max-size: "50m"
+        max-file: "3"
+
+  app:
+    build:
+      context: .
+      dockerfile: docker/app/Dockerfile
+    ports:
+      - "8502:8501"
+    environment:
+      - DB_HOST=mysql
+      - DB_USER=food_reader
+      - DB_PASS=${DB_READER_PASS}
+      - APP_AUTH_USER=food_app_auth
+      - APP_AUTH_PASS=${DB_APP_AUTH_PASS}
+      - OLLAMA_HOST=http://ollama:11434
+      - SEARXNG_HOST=http://searxng:8080
+    restart: always
+    logging:
+      driver: "json-file"
+      options:
+        max-size: "50m"
+        max-file: "3"
+
+  nginx:
+    image: nginx:latest
+    ports:
+      - "80:80"
+    volumes:
+      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
+    restart: always
+    logging:
+      driver: "json-file"
+      options:
+        max-size: "50m"
+        max-file: "3"
+
+  zabbix-server:
+    image: zabbix/zabbix-server-mysql:ubuntu-7.0-latest
+    environment:
+      - DB_SERVER_HOST=mysql
+      - MYSQL_USER=zabbix
+      - MYSQL_PASSWORD=${MYSQL_ZABBIX_PASSWORD}
+      - ZBX_SNMPTRAPPER=1
+    restart: always
+    logging:
+      driver: "json-file"
+      options:
+        max-size: "50m"
+        max-file: "3"
+    ports:
+      - "10051:10051"
+
+  zabbix-web:
+    image: zabbix/zabbix-web-nginx-mysql:ubuntu-7.0-latest
+    ports:
+      - "8081:8080"
+      - "8444:8443"
+    environment:
+      - DB_SERVER_HOST=mysql
+      - MYSQL_USER=zabbix
+      - MYSQL_PASSWORD=${MYSQL_ZABBIX_PASSWORD}
+      - ZBX_SERVER_HOST=zabbix-server
+      - PHP_TZ=Europe/Paris
+    restart: always
+    logging:
+      driver: "json-file"
+      options:
+        max-size: "50m"
+        max-file: "3"
+
+  zabbix-agent:
+    image: zabbix/zabbix-agent:ubuntu-7.0-latest
+    environment:
+      - ZBX_HOSTNAME=DistributedNode
+      - ZBX_SERVER_HOST=zabbix-server
+    privileged: true
+    pid: "host"
+    volumes:
+      - /var/run:/var/run
+    restart: always
+    logging:
+      driver: "json-file"
+      options:
+        max-size: "50m"
+        max-file: "3"
+
+
+  airflow-webserver:
+    image: apache/airflow:2.8.1
+    environment:
+      - AIRFLOW__CORE__EXECUTOR=SequentialExecutor
+      - AIRFLOW__DATABASE__SQL_ALCHEMY_CONN=sqlite:////opt/airflow/data/airflow.db
+      - AIRFLOW__CORE__LOAD_EXAMPLES=False
+    ports:
+      - "8082:8080"
+    volumes:
+      - ./dags:/opt/airflow/dags
+      - ./logs:/opt/airflow/logs
+      - ./data:/opt/airflow/data
+      - /var/run/docker.sock:/var/run/docker.sock
+    command: webserver
+    restart: always
+    logging:
+      driver: "json-file"
+      options:
+        max-size: "50m"
+        max-file: "3"
+
+  airflow-scheduler:
+    image: apache/airflow:2.8.1
+    environment:
+      - AIRFLOW__CORE__EXECUTOR=SequentialExecutor
+      - AIRFLOW__DATABASE__SQL_ALCHEMY_CONN=sqlite:////opt/airflow/data/airflow.db
+      - AIRFLOW__CORE__LOAD_EXAMPLES=False
+    volumes:
+      - ./dags:/opt/airflow/dags
+      - ./logs:/opt/airflow/logs
+      - ./data:/opt/airflow/data
+      - /var/run/docker.sock:/var/run/docker.sock
+    command: bash -c "airflow db migrate && airflow users create --role Admin --username admin --email admin --firstname admin --lastname admin --password admin && airflow scheduler"
+    restart: always
+    logging:
+      driver: "json-file"
+      options:
+        max-size: "50m"
+        max-file: "3"
+
+volumes:
+  mysql_data:
+  ollama_data:

+ 111 - 111
docker-compose_skip.yml

@@ -1,111 +1,111 @@
-services:
-  mysql:
-    build:
-      context: ./docker/mysql
-    ports:
-      - "3307:3306"
-    volumes:
-      - mysql_data:/var/lib/mysql
-      - ./my.cnf:/etc/mysql/conf.d/custom_ai_app.cnf
-      - ./init.sql:/docker-entrypoint-initdb.d/1-init.sql
-    environment:
-      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
-    healthcheck:
-      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
-      interval: 10s
-      timeout: 5s
-      retries: 20
-    restart: always
-
-
-  ingest:
-    build:
-      context: .
-      dockerfile: docker/ingest/Dockerfile
-    environment:
-      - DB_HOST=mysql
-      - DB_USER=food_loader
-      - DB_PASS=${DB_LOADER_PASS}
-    volumes:
-      - ./:/app
-    profiles:
-      - manual
-
-  ollama:
-    image: ollama/ollama:latest
-    volumes:
-      - ollama_data:/root/.ollama
-    restart: always
-
-  searxng:
-    image: searxng/searxng:latest
-    ports:
-      - "8085:8080"
-    volumes:
-      - ./searxng:/etc/searxng
-    environment:
-      - SEARXNG_BASE_URL=http://localhost:8080/
-    restart: always
-
-  app:
-    build:
-      context: .
-      dockerfile: docker/app/Dockerfile
-    ports:
-      - "8502:8501"
-    environment:
-      - DB_HOST=mysql
-      - DB_USER=food_reader
-      - DB_PASS=${DB_READER_PASS}
-      - APP_AUTH_USER=food_app_auth
-      - APP_AUTH_PASS=${DB_APP_AUTH_PASS}
-      - OLLAMA_HOST=http://ollama:11434
-      - SEARXNG_HOST=http://searxng:8080
-    restart: always
-
-  nginx:
-    image: nginx:latest
-    ports:
-      - "80:80"
-    volumes:
-      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
-    restart: always
-
-  zabbix-server:
-    image: zabbix/zabbix-server-mysql:ubuntu-7.0-latest
-    environment:
-      - DB_SERVER_HOST=mysql
-      - MYSQL_USER=zabbix
-      - MYSQL_PASSWORD=${MYSQL_ZABBIX_PASSWORD}
-      - ZBX_SNMPTRAPPER=1
-    restart: always
-    ports:
-      - "10051:10051"
-
-  zabbix-web:
-    image: zabbix/zabbix-web-nginx-mysql:ubuntu-7.0-latest
-    ports:
-      - "8081:8080"
-      - "8444:8443"
-    environment:
-      - DB_SERVER_HOST=mysql
-      - MYSQL_USER=zabbix
-      - MYSQL_PASSWORD=${MYSQL_ZABBIX_PASSWORD}
-      - ZBX_SERVER_HOST=zabbix-server
-      - PHP_TZ=Europe/Paris
-    restart: always
-
-  zabbix-agent:
-    image: zabbix/zabbix-agent:ubuntu-7.0-latest
-    environment:
-      - ZBX_HOSTNAME=DistributedNode
-      - ZBX_SERVER_HOST=zabbix-server
-    privileged: true
-    pid: "host"
-    volumes:
-      - /var/run:/var/run
-    restart: always
-
-volumes:
-  mysql_data:
-  ollama_data:
+services:
+  mysql:
+    build:
+      context: ./docker/mysql
+    ports:
+      - "3307:3306"
+    volumes:
+      - mysql_data:/var/lib/mysql
+      - ./my.cnf:/etc/mysql/conf.d/custom_ai_app.cnf
+      - ./init.sql:/docker-entrypoint-initdb.d/1-init.sql
+    environment:
+      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
+    healthcheck:
+      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
+      interval: 10s
+      timeout: 5s
+      retries: 20
+    restart: always
+
+
+  ingest:
+    build:
+      context: .
+      dockerfile: docker/ingest/Dockerfile
+    environment:
+      - DB_HOST=mysql
+      - DB_USER=food_loader
+      - DB_PASS=${DB_LOADER_PASS}
+    volumes:
+      - ./:/app
+    profiles:
+      - manual
+
+  ollama:
+    image: ollama/ollama:latest
+    volumes:
+      - ollama_data:/root/.ollama
+    restart: always
+
+  searxng:
+    image: searxng/searxng:latest
+    ports:
+      - "8085:8080"
+    volumes:
+      - ./searxng:/etc/searxng
+    environment:
+      - SEARXNG_BASE_URL=http://localhost:8080/
+    restart: always
+
+  app:
+    build:
+      context: .
+      dockerfile: docker/app/Dockerfile
+    ports:
+      - "8502:8501"
+    environment:
+      - DB_HOST=mysql
+      - DB_USER=food_reader
+      - DB_PASS=${DB_READER_PASS}
+      - APP_AUTH_USER=food_app_auth
+      - APP_AUTH_PASS=${DB_APP_AUTH_PASS}
+      - OLLAMA_HOST=http://ollama:11434
+      - SEARXNG_HOST=http://searxng:8080
+    restart: always
+
+  nginx:
+    image: nginx:latest
+    ports:
+      - "80:80"
+    volumes:
+      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
+    restart: always
+
+  zabbix-server:
+    image: zabbix/zabbix-server-mysql:ubuntu-7.0-latest
+    environment:
+      - DB_SERVER_HOST=mysql
+      - MYSQL_USER=zabbix
+      - MYSQL_PASSWORD=${MYSQL_ZABBIX_PASSWORD}
+      - ZBX_SNMPTRAPPER=1
+    restart: always
+    ports:
+      - "10051:10051"
+
+  zabbix-web:
+    image: zabbix/zabbix-web-nginx-mysql:ubuntu-7.0-latest
+    ports:
+      - "8081:8080"
+      - "8444:8443"
+    environment:
+      - DB_SERVER_HOST=mysql
+      - MYSQL_USER=zabbix
+      - MYSQL_PASSWORD=${MYSQL_ZABBIX_PASSWORD}
+      - ZBX_SERVER_HOST=zabbix-server
+      - PHP_TZ=Europe/Paris
+    restart: always
+
+  zabbix-agent:
+    image: zabbix/zabbix-agent:ubuntu-7.0-latest
+    environment:
+      - ZBX_HOSTNAME=DistributedNode
+      - ZBX_SERVER_HOST=zabbix-server
+    privileged: true
+    pid: "host"
+    volumes:
+      - /var/run:/var/run
+    restart: always
+
+volumes:
+  mysql_data:
+  ollama_data:

+ 53 - 53
docker/zabbix/docker-compose.yml

@@ -1,53 +1,53 @@
-version: '3.5'
-services:
-  zabbix-server:
-    image: zabbix/zabbix-server-mysql:ubuntu-7.0-latest
-    ports:
-      - "10051:10051"
-    environment:
-      - DB_SERVER_HOST=192.168.130.170 # Use the unified MySQL DB
-      - MYSQL_USER=zabbix
-      - MYSQL_PASSWORD=${MYSQL_ZABBIX_PASSWORD}
-      - ZBX_SNMPTRAPPER=1
-    restart: always
-    logging:
-      driver: "json-file"
-      options:
-        max-size: "50m"
-        max-file: "3"
-
-  zabbix-web:
-    image: zabbix/zabbix-web-nginx-mysql:ubuntu-7.0-latest
-    ports:
-      - "8080:8080"
-      - "8443:8443"
-    environment:
-      - DB_SERVER_HOST=192.168.130.170
-      - MYSQL_USER=zabbix
-      - MYSQL_PASSWORD=${MYSQL_ZABBIX_PASSWORD}
-      - ZBX_SERVER_HOST=zabbix-server
-      - PHP_TZ=Europe/Paris
-    depends_on:
-      - zabbix-server
-    restart: always
-    logging:
-      driver: "json-file"
-      options:
-        max-size: "50m"
-        max-file: "3"
-
-  zabbix-agent:
-    image: zabbix/zabbix-agent:ubuntu-7.0-latest
-    environment:
-      - ZBX_HOSTNAME=Zabbix server
-      - ZBX_SERVER_HOST=zabbix-server
-    privileged: true
-    pid: "host"
-    volumes:
-      - /var/run:/var/run
-    restart: always
-    logging:
-      driver: "json-file"
-      options:
-        max-size: "50m"
-        max-file: "3"
+version: '3.5'
+services:
+  zabbix-server:
+    image: zabbix/zabbix-server-mysql:ubuntu-7.0-latest
+    ports:
+      - "10051:10051"
+    environment:
+      - DB_SERVER_HOST=192.168.130.170 # Use the unified MySQL DB
+      - MYSQL_USER=zabbix
+      - MYSQL_PASSWORD=${MYSQL_ZABBIX_PASSWORD}
+      - ZBX_SNMPTRAPPER=1
+    restart: always
+    logging:
+      driver: "json-file"
+      options:
+        max-size: "50m"
+        max-file: "3"
+
+  zabbix-web:
+    image: zabbix/zabbix-web-nginx-mysql:ubuntu-7.0-latest
+    ports:
+      - "8080:8080"
+      - "8443:8443"
+    environment:
+      - DB_SERVER_HOST=192.168.130.170
+      - MYSQL_USER=zabbix
+      - MYSQL_PASSWORD=${MYSQL_ZABBIX_PASSWORD}
+      - ZBX_SERVER_HOST=zabbix-server
+      - PHP_TZ=Europe/Paris
+    depends_on:
+      - zabbix-server
+    restart: always
+    logging:
+      driver: "json-file"
+      options:
+        max-size: "50m"
+        max-file: "3"
+
+  zabbix-agent:
+    image: zabbix/zabbix-agent:ubuntu-7.0-latest
+    environment:
+      - ZBX_HOSTNAME=Zabbix server
+      - ZBX_SERVER_HOST=zabbix-server
+    privileged: true
+    pid: "host"
+    volumes:
+      - /var/run:/var/run
+    restart: always
+    logging:
+      driver: "json-file"
+      options:
+        max-size: "50m"
+        max-file: "3"

+ 78 - 78
docs/Backup_Procedure.md

@@ -1,78 +1,78 @@
-# $Id$
-# Database Backup and Restore Procedure
-
-## 1. Overview & Policy
-To guarantee clinical records integrity and high availability, Local Food AI enforces a strict backup schedule.
-- **Scope**: Includes MySQL schemas (`food_db`), user profiles (`app_auth`), and configuration states.
-- **Retention Plan**: Automated daily backups with a strict 7-day rolling window purge.
-- **Storage Location**: Stored securely inside the persistent `/backups` directory on the host server.
-
----
-
-## 2. Automated Daily Backups
-The automated backup mechanism runs via a host cron job pointing to `backup_db.sh`.
-- The script dynamically detects the active MySQL container name (`food-mysql-1` or `food_project-mysql-1`).
-- It executes `mysqldump` directly inside the container without exposing root passwords to shell logs.
-- Outputs are compressed via `gzip` and timestamped: `food_db_YYYYMMDD_HHMM.sql.gz`.
-
-### Cron Configuration Example:
-To run the backup daily at 02:00 AM, add the following to `/etc/crontab`:
-```bash
-0 2 * * * root /bin/bash /c/Users/lanfr144/Documents/DOPRO1/Antigravity/Food/backup_db.sh >> /var/log/backup_db.log 2>&1
-```
-
----
-
-## 3. Manual Backup Execution
-If a system migration or major upgrade is scheduled, perform a manual dump using the following command:
-```bash
-# 1. Navigate to the project directory
-cd /c/Users/lanfr144/Documents/DOPRO1/Antigravity/Food
-
-# 2. Run the backup wrapper
-bash backup_db.sh
-```
-Verify the output exists inside the backups folder:
-```bash
-ls -lh backups/
-```
-
----
-
-## 4. Step-by-Step Restore Procedure
-In the event of database corruption or hardware failure, follow these exact steps to restore the database.
-
-### Step 4.1: Identify the Target Backup File
-List available files and pick the desired timestamp:
-```bash
-ls -la backups/
-# Example Target: backups/food_db_20260521_1100.sql.gz
-```
-
-### Step 4.2: Verify MySQL Container Health
-Ensure the MySQL service container is running and healthy:
-```bash
-docker ps --filter name=mysql
-```
-
-### Step 4.3: Execute Restore Stream
-Decompress the backup on-the-fly and pipe it directly into the running MySQL container:
-```bash
-# Adjust the container name ('food-mysql-1' or 'food_project-mysql-1') based on active deployment
-gunzip < backups/food_db_20260521_1100.sql.gz | docker exec -i food-mysql-1 mysql -u root -proot_pass food_db
-```
-
-### Step 4.4: Verify Restored Tables
-Log in to the database and query the core table to confirm the tables are intact and populated:
-```bash
-docker exec -it food-mysql-1 mysql -u food_reader -preader_pass food_db -e "SELECT COUNT(*) FROM products_core;"
-```
-Expected result: A count of OpenFoodFacts entries (typically > 10,000 records).
-
----
-
-## 5. Verification & Health Check Loops
-Operators must verify the backup archive integrity weekly:
-1. Copy the `.gz` backup to a local testing workspace.
-2. Run `gzip -t backups/filename.sql.gz` to ensure the archive is not corrupted.
-3. Test restoring to a local fallback container instance to verify data accessibility.
+# $Id$
+# Database Backup and Restore Procedure
+
+## 1. Overview & Policy
+To guarantee clinical records integrity and high availability, Local Food AI enforces a strict backup schedule.
+- **Scope**: Includes MySQL schemas (`food_db`), user profiles (`app_auth`), and configuration states.
+- **Retention Plan**: Automated daily backups with a strict 7-day rolling window purge.
+- **Storage Location**: Stored securely inside the persistent `/backups` directory on the host server.
+
+---
+
+## 2. Automated Daily Backups
+The automated backup mechanism runs via a host cron job pointing to `backup_db.sh`.
+- The script dynamically detects the active MySQL container name (`food-mysql-1` or `food_project-mysql-1`).
+- It executes `mysqldump` directly inside the container without exposing root passwords to shell logs.
+- Outputs are compressed via `gzip` and timestamped: `food_db_YYYYMMDD_HHMM.sql.gz`.
+
+### Cron Configuration Example:
+To run the backup daily at 02:00 AM, add the following to `/etc/crontab`:
+```bash
+0 2 * * * root /bin/bash /c/Users/lanfr144/Documents/DOPRO1/Antigravity/Food/backup_db.sh >> /var/log/backup_db.log 2>&1
+```
+
+---
+
+## 3. Manual Backup Execution
+If a system migration or major upgrade is scheduled, perform a manual dump using the following command:
+```bash
+# 1. Navigate to the project directory
+cd /c/Users/lanfr144/Documents/DOPRO1/Antigravity/Food
+
+# 2. Run the backup wrapper
+bash backup_db.sh
+```
+Verify the output exists inside the backups folder:
+```bash
+ls -lh backups/
+```
+
+---
+
+## 4. Step-by-Step Restore Procedure
+In the event of database corruption or hardware failure, follow these exact steps to restore the database.
+
+### Step 4.1: Identify the Target Backup File
+List available files and pick the desired timestamp:
+```bash
+ls -la backups/
+# Example Target: backups/food_db_20260521_1100.sql.gz
+```
+
+### Step 4.2: Verify MySQL Container Health
+Ensure the MySQL service container is running and healthy:
+```bash
+docker ps --filter name=mysql
+```
+
+### Step 4.3: Execute Restore Stream
+Decompress the backup on-the-fly and pipe it directly into the running MySQL container:
+```bash
+# Adjust the container name ('food-mysql-1' or 'food_project-mysql-1') based on active deployment
+gunzip < backups/food_db_20260521_1100.sql.gz | docker exec -i food-mysql-1 mysql -u root -proot_pass food_db
+```
+
+### Step 4.4: Verify Restored Tables
+Log in to the database and query the core table to confirm the tables are intact and populated:
+```bash
+docker exec -it food-mysql-1 mysql -u food_reader -preader_pass food_db -e "SELECT COUNT(*) FROM products_core;"
+```
+Expected result: A count of OpenFoodFacts entries (typically > 10,000 records).
+
+---
+
+## 5. Verification & Health Check Loops
+Operators must verify the backup archive integrity weekly:
+1. Copy the `.gz` backup to a local testing workspace.
+2. Run `gzip -t backups/filename.sql.gz` to ensure the archive is not corrupted.
+3. Test restoring to a local fallback container instance to verify data accessibility.

+ 11 - 11
docs/Data_Ingestion.md

@@ -1,11 +1,11 @@
-# $Id$
-# Data Ingestion Pipeline
-
-## Overview
-The application utilizes `data_sync.sh` to update the OpenFoodFacts dataset.
-
-## Online Mode
-Run `bash data_sync.sh --online`. The script will download the latest CSV directly from the official servers and trigger the ingestion pipeline.
-
-## Offline Mode
-Drop a `en.openfoodfacts.org.products.csv` file into the `/data` folder and run `bash data_sync.sh`. The script detects the file and triggers the Docker ingestion container.
+# $Id$
+# Data Ingestion Pipeline
+
+## Overview
+The application utilizes `data_sync.sh` to update the OpenFoodFacts dataset.
+
+## Online Mode
+Run `bash data_sync.sh --online`. The script will download the latest CSV directly from the official servers and trigger the ingestion pipeline.
+
+## Offline Mode
+Drop a `en.openfoodfacts.org.products.csv` file into the `/data` folder and run `bash data_sync.sh`. The script detects the file and triggers the Docker ingestion container.

+ 19 - 19
docs/Final_Report.md

@@ -1,19 +1,19 @@
-# $Id$
-# Final Project Report (Living Document)
-
-## What Has Been Done
-1. **Core Architecture**: Deployed a resilient 8-container local fallback Docker Compose stack (MySQL, Streamlit UI, local Ollama LLM, anonymous SearXNG search, secure Nginx proxy, and local Zabbix Server/Web/Agent observability suite).
-2. **Database Optimization**: Successfully loaded OpenFoodFacts records and utilized advanced vertical partitioning and FULLTEXT indices.
-3. **Clinical Subquery Strategy**: Refactored the core Pandas/SQL query pipeline to use subquery limiting, resolving Cartesian join explosions and reducing query latency to ~0.04s.
-4. **Monitoring & Security**: Nginx securely proxies traffic on Port 80. Zabbix actively monitors proxy and server health, dynamically handling SNMP/alert loops in local/offline fallback mode.
-5. **Git Versioning**: Implemented Git `.gitattributes` to push `$Id$` tracking directly into the Python Application UI.
-
-## What Needs To Be Done (Day 2 Operations)
-1. **SSL/TLS Certificates**: The Nginx proxy is functional on HTTP port 80. Port 443 (HTTPS) must be configured with a Let's Encrypt certificate for true production encryption.
-2. **User Acceptance Testing (UAT)**: Clinical dietitians should rigorously test the AI Chat constraints and Plate Builder to ensure edge cases are handled safely.
-3. **Advanced Rate Limiting**: Limit the number of AI requests per user using a sliding window algorithm in `app.py`.
-
-## What Is The Next Step
-- Execute the `data_sync.sh` cron job monthly.
-- Maintain the automated `backup_db.sh` 7-day retention cycle.
-- Begin the hand-off to the operational team for Phase 2 feature requests.
+# $Id$
+# Final Project Report (Living Document)
+
+## What Has Been Done
+1. **Core Architecture**: Deployed a resilient 8-container local fallback Docker Compose stack (MySQL, Streamlit UI, local Ollama LLM, anonymous SearXNG search, secure Nginx proxy, and local Zabbix Server/Web/Agent observability suite).
+2. **Database Optimization**: Successfully loaded OpenFoodFacts records and utilized advanced vertical partitioning and FULLTEXT indices.
+3. **Clinical Subquery Strategy**: Refactored the core Pandas/SQL query pipeline to use subquery limiting, resolving Cartesian join explosions and reducing query latency to ~0.04s.
+4. **Monitoring & Security**: Nginx securely proxies traffic on Port 80. Zabbix actively monitors proxy and server health, dynamically handling SNMP/alert loops in local/offline fallback mode.
+5. **Git Versioning**: Implemented Git `.gitattributes` to push `$Id$` tracking directly into the Python Application UI.
+
+## What Needs To Be Done (Day 2 Operations)
+1. **SSL/TLS Certificates**: The Nginx proxy is functional on HTTP port 80. Port 443 (HTTPS) must be configured with a Let's Encrypt certificate for true production encryption.
+2. **User Acceptance Testing (UAT)**: Clinical dietitians should rigorously test the AI Chat constraints and Plate Builder to ensure edge cases are handled safely.
+3. **Advanced Rate Limiting**: Limit the number of AI requests per user using a sliding window algorithm in `app.py`.
+
+## What Is The Next Step
+- Execute the `data_sync.sh` cron job monthly.
+- Maintain the automated `backup_db.sh` 7-day retention cycle.
+- Begin the hand-off to the operational team for Phase 2 feature requests.

+ 18 - 18
docs/Installation_Guide.md

@@ -1,18 +1,18 @@
-# $Id$
-# Installation Guide
-
-## Requirements
-- Ubuntu 24.04 LTS (or WSL2)
-- Docker & Docker Compose
-- 16GB RAM Minimum
-
-## Deployment Steps
-1. **Clone the Repository**:
-   - *Online Mode*: `git clone https://git.btshub.lu/lanfr/LocalFoodAI_lanfr144.git`
-   - *Offline/Disconnected Mode*: Copy the repository files directly to the target environment via SCP or USB storage.
-2. `cd LocalFoodAI_lanfr144`
-3. `chmod +x data_sync.sh backup_db.sh`
-4. **Deploy Stack**:
-   - For regular production: `docker compose up -d --build`
-   - For local/offline single-node fallback: `docker compose -f docker-compose_skip.yml up -d`
-5. Navigate to `http://localhost` (or `http://localhost:8502` for direct Streamlit port)
+# $Id$
+# Installation Guide
+
+## Requirements
+- Ubuntu 24.04 LTS (or WSL2)
+- Docker & Docker Compose
+- 16GB RAM Minimum
+
+## Deployment Steps
+1. **Clone the Repository**:
+   - *Online Mode*: `git clone https://git.btshub.lu/lanfr/LocalFoodAI_lanfr144.git`
+   - *Offline/Disconnected Mode*: Copy the repository files directly to the target environment via SCP or USB storage.
+2. `cd LocalFoodAI_lanfr144`
+3. `chmod +x data_sync.sh backup_db.sh`
+4. **Deploy Stack**:
+   - For regular production: `docker compose up -d --build`
+   - For local/offline single-node fallback: `docker compose -f docker-compose_skip.yml up -d`
+5. Navigate to `http://localhost` (or `http://localhost:8502` for direct Streamlit port)

+ 184 - 184
docs/Operator_Installation_Guide.md

@@ -1,184 +1,184 @@
-# $Id$
-# Local Food AI - Detailed Operator Installation Guide
-
-This document is a step-by-step installation, mapping, configuration, and verification manual for deploying the **Local Food AI** system in an enterprise environment. It covers hybrid hypervisor infrastructure (WSL2, Hyper-V, and VirtualBox), cross-node networking, SNMPv3 monitoring, alert channels, and acceptance testing.
-
----
-
-## 1. Pre-Deployment Operator Survey (Pre-requisites Gathering)
-Before running installation scripts, the operator **must** collect the following physical/virtual infrastructure parameters and store them in the deployment matrix:
-
-| REQUIRED PARAMETER | OPERATOR INPUT / DESCRIPTION |
-| :--- | :--- |
-| **Deployment Workstation IP** | e.g., 192.168.1.50 |
-| **Hyper-V Host VM IP** | e.g., 192.168.130.170 |
-| **VirtualBox Host VM IP** | e.g., 192.168.130.161 |
-| **SSH Key Location (Private)** | e.g., `~/.ssh/id_rsa` |
-| **SMTP Relay Password** | e.g., `********` (For Zabbix/App password reset email) |
-| **Teams/Discord Webhook URL** | e.g., `https://discord.com/api/webhooks/...` |
-
----
-
-## 2. Platform Mapping: Which Container Goes Where?
-
-To maximize CPU/GPU efficiency and secure database read/writes, services are distributed across three distinct environments:
-
-| COMPONENT CONTAINER | DEPLOYMENT ENVIRONMENT | WHY |
-| :--- | :--- | :--- |
-| **streamlit-app (app.py)** | Local WSL2 (Windows) | Low-latency rendering and direct client access |
-| **mysql (Database Node)** | Hyper-V VM (Server A) | Persistent enterprise-grade disk storage |
-| **ollama (NLP Qwen2.5:7b Engine)** | VirtualBox VM (Server B) | Dedicated CPU/GPU virtualization allocation |
-| **zabbix-server & web (Monitoring)** | Hyper-V VM (Server A) | Centralized SNMPv3 alert processing and logs |
-| **searxng (Meta-Search Gateway)** | Local WSL2 (Windows) | Dynamic browser-level loopbacks |
-
----
-
-## 3. Platform Provisioning Commands
-
-### 3.1: WSL2 Provisioning (Local Client Workstation)
-Enable WSL2 and install Ubuntu 24.04:
-```powershell
-# Run in Administrator PowerShell
-dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart
-dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart
-wsl --install -d Ubuntu-24.04
-```
-
-### 3.2: Hyper-V VM Provisioning (Server A - Database & Zabbix)
-Deploy a dedicated Ubuntu VM on Hyper-V using PowerShell:
-```powershell
-# Run in Administrator PowerShell on Server A
-New-VM -Name "FoodAI-Database-Node" -MemoryStartupBytes 8GB -Generation 2 -NewVHDPath "C:\VMs\FoodAI_DB.vhdx" -VHDSizeBytes 80GB -SwitchName "External Switch"
-Set-VMFirmware -VMName "FoodAI-Database-Node" -EnableSecureBoot Off
-Start-VM -Name "FoodAI-Database-Node"
-```
-
-### 3.3: VirtualBox VM Provisioning (Server B - Ollama AI Engine)
-Deploy a dedicated VM on VirtualBox using Command Line:
-```bash
-# Run in Command Prompt on Server B
-vboxmanage createvm --name "FoodAI-AI-Node" --ostype "Ubuntu_64" --register
-vboxmanage modifyvm "FoodAI-AI-Node" --memory 8192 --cpus 4 --vram 128 --nic1 bridged --bridgeadapter1 "Intel Ethernet Connection"
-vboxmanage createhd --filename "C:\VMs\FoodAI_AI.vdi" --size 60000
-vboxmanage storagectl "FoodAI-AI-Node" --name "SATA Controller" --add sata --controller IntelAHCI
-vboxmanage storageattach "FoodAI-AI-Node" --storagectl "SATA Controller" --port 0 --device 0 --type hdd --medium "C:\VMs\FoodAI_AI.vdi"
-vboxmanage startvm "FoodAI-AI-Node" --type headless
-```
-
----
-
-## 4. Secure Authentication & SSH Exchange
-Exchange SSH public keys to allow automated, passwordless container management across nodes:
-```bash
-# 1. Generate SSH Keys on WSL Client
-ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa_foodai -N ""
-
-# 2. Push Key to Database VM (Server A)
-ssh-copy-id -i ~/.ssh/id_rsa_foodai.pub operator@192.168.130.170
-
-# 3. Push Key to AI VM (Server B)
-ssh-copy-id -i ~/.ssh/id_rsa_foodai.pub operator@192.168.130.161
-```
-
----
-
-## 5. Multi-Node Docker Network & Configuration
-
-To allow WSL, Hyper-V, and VirtualBox nodes to communicate, update the `.env` variables and `docker-compose.yml` to use bridged network endpoints.
-
-### Step 5.1: Configure WSL Client `.env`
-Update `.env` in the Streamlit workspace:
-```ini
-DB_HOST=192.168.130.170
-DB_USER=food_reader
-DB_PASS=reader_pass
-APP_AUTH_USER=food_app_auth
-APP_AUTH_PASS=auth_pass
-OLLAMA_HOST=http://192.168.130.161:11434
-SEARXNG_HOST=http://localhost:8080
-ZBX_SERVER_HOST=192.168.130.170
-```
-
-### Step 5.2: Configure Ollama (VirtualBox Server B) Listening Port
-Ensure the Ollama daemon inside VirtualBox binds to `0.0.0.0` (all interfaces):
-```bash
-# SSH into Server B (192.168.130.161)
-sudo systemctl edit ollama.service
-
-# Add the environment variables:
-[Service]
-Environment="OLLAMA_HOST=0.0.0.0"
-
-# Reload and restart service
-sudo systemctl daemon-reload
-sudo systemctl restart ollama
-```
-
----
-
-## 6. Zabbix Reconfiguration for Multi-Node SNMPv3 Telemetry
-
-To monitor all distributed deployment environments securely:
-
-### Step 6.1: Deploy SNMPv3 Daemons
-Install and configure SNMPv3 daemons on WSL, Hyper-V Database VM, and VirtualBox AI VM:
-```bash
-sudo apt update && sudo apt install -y snmpd
-```
-Edit `/etc/snmp/snmpd.conf`:
-```
-# Listen on all interfaces
-agentAddress udp:161
-
-# Create secure SNMPv3 User
-createUser securityUser SHA "securityAuthPassword" AES "securityPrivPassword"
-rouser securityUser authpriv
-```
-Restart daemon:
-```bash
-sudo systemctl restart snmpd
-```
-
-### Step 6.2: Configure Zabbix Server Dashboard (Web UI)
-1. Open Zabbix in your browser at `http://192.168.130.170:8081`.
-2. Navigate to **Configuration > Hosts > Create Host**.
-3. Create three distinct hosts:
-   - **WSL-Workstation** (IP: `192.168.1.50`)
-   - **Database-Node** (IP: `192.168.130.170`)
-   - **AI-Node** (IP: `192.168.130.161`)
-4. Add the **SNMP Interface** pointing to Port 161 for each host.
-5. In the **Security Tab**, select SNMPv3, enter Username `securityUser`, select Auth Protocol `SHA` / `securityAuthPassword`, and Privacy Protocol `AES` / `securityPrivPassword`.
-6. Attach the pre-installed **Local Food AI Telemetry** Template.
-
----
-
-## 7. Verifying Alert Channels
-
-### 7.1: Microsoft Teams / Discord Alert Webhook
-To verify Zabbix is communicating with Discord / Teams:
-1. Trigger a test CPU threshold spike inside WSL:
-   ```bash
-   yes > /dev/null & sleep 10 ; killall yes
-   ```
-2. Verify Zabbix triggers the alert and transmits the notification.
-3. Check your designated channel for the incoming payload:
-   - Expected Output: `[PROBLEM] High CPU Utilization Detected on WSL-Workstation`.
-
-### 7.2: Password Reset Email (SMTP Gateway)
-1. In the Streamlit UI Sidebar, select **Reset Password**.
-2. Trigger a reset link for user `ClinicianA`.
-3. Check the inbox or SMTP system log (`tail -f /var/log/mail.log` on Server A) to verify outbound delivery.
-
----
-
-## 8. Operator Post-Installation Checklist
-
-Run these test cases to verify the installation:
-
-| TEST CASE ID | ACTIONS TO PERFORM | EXPECTED RESULTS | STATUS |
-| :--- | :--- | :--- | :---: |
-| **TC-OP-01** | Search 'Cheese' on Search Tab | 10+ records returned in <0.04s. Listeria warning flags on unpasteurized. | `[ ]` |
-| **TC-OP-02** | Enter '1.5 cups' in Plate Tab | Parsed and converted to metric grams based on density index. | `[ ]` |
-| **TC-OP-03** | Ask Chat: 'Can I eat sushi?' | Qwen2.5:7b retrieves database context and flags raw fish as forbidden for pregnancy. | `[ ]` |
-| **TC-OP-04** | Trigger manual db backup | Timestamped compressed .sql.gz created inside backups/ folder. | `[ ]` |
-| **TC-OP-05** | Terminate Ollama Container | Zabbix PROBLEM active alert generated on dashboard in < 30 seconds. | `[ ]` |
+# $Id$
+# Local Food AI - Detailed Operator Installation Guide
+
+This document is a step-by-step installation, mapping, configuration, and verification manual for deploying the **Local Food AI** system in an enterprise environment. It covers hybrid hypervisor infrastructure (WSL2, Hyper-V, and VirtualBox), cross-node networking, SNMPv3 monitoring, alert channels, and acceptance testing.
+
+---
+
+## 1. Pre-Deployment Operator Survey (Pre-requisites Gathering)
+Before running installation scripts, the operator **must** collect the following physical/virtual infrastructure parameters and store them in the deployment matrix:
+
+| REQUIRED PARAMETER | OPERATOR INPUT / DESCRIPTION |
+| :--- | :--- |
+| **Deployment Workstation IP** | e.g., 192.168.1.50 |
+| **Hyper-V Host VM IP** | e.g., 192.168.130.170 |
+| **VirtualBox Host VM IP** | e.g., 192.168.130.161 |
+| **SSH Key Location (Private)** | e.g., `~/.ssh/id_rsa` |
+| **SMTP Relay Password** | e.g., `********` (For Zabbix/App password reset email) |
+| **Teams/Discord Webhook URL** | e.g., `https://discord.com/api/webhooks/...` |
+
+---
+
+## 2. Platform Mapping: Which Container Goes Where?
+
+To maximize CPU/GPU efficiency and secure database read/writes, services are distributed across three distinct environments:
+
+| COMPONENT CONTAINER | DEPLOYMENT ENVIRONMENT | WHY |
+| :--- | :--- | :--- |
+| **streamlit-app (app.py)** | Local WSL2 (Windows) | Low-latency rendering and direct client access |
+| **mysql (Database Node)** | Hyper-V VM (Server A) | Persistent enterprise-grade disk storage |
+| **ollama (NLP Qwen2.5:7b Engine)** | VirtualBox VM (Server B) | Dedicated CPU/GPU virtualization allocation |
+| **zabbix-server & web (Monitoring)** | Hyper-V VM (Server A) | Centralized SNMPv3 alert processing and logs |
+| **searxng (Meta-Search Gateway)** | Local WSL2 (Windows) | Dynamic browser-level loopbacks |
+
+---
+
+## 3. Platform Provisioning Commands
+
+### 3.1: WSL2 Provisioning (Local Client Workstation)
+Enable WSL2 and install Ubuntu 24.04:
+```powershell
+# Run in Administrator PowerShell
+dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart
+dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart
+wsl --install -d Ubuntu-24.04
+```
+
+### 3.2: Hyper-V VM Provisioning (Server A - Database & Zabbix)
+Deploy a dedicated Ubuntu VM on Hyper-V using PowerShell:
+```powershell
+# Run in Administrator PowerShell on Server A
+New-VM -Name "FoodAI-Database-Node" -MemoryStartupBytes 8GB -Generation 2 -NewVHDPath "C:\VMs\FoodAI_DB.vhdx" -VHDSizeBytes 80GB -SwitchName "External Switch"
+Set-VMFirmware -VMName "FoodAI-Database-Node" -EnableSecureBoot Off
+Start-VM -Name "FoodAI-Database-Node"
+```
+
+### 3.3: VirtualBox VM Provisioning (Server B - Ollama AI Engine)
+Deploy a dedicated VM on VirtualBox using Command Line:
+```bash
+# Run in Command Prompt on Server B
+vboxmanage createvm --name "FoodAI-AI-Node" --ostype "Ubuntu_64" --register
+vboxmanage modifyvm "FoodAI-AI-Node" --memory 8192 --cpus 4 --vram 128 --nic1 bridged --bridgeadapter1 "Intel Ethernet Connection"
+vboxmanage createhd --filename "C:\VMs\FoodAI_AI.vdi" --size 60000
+vboxmanage storagectl "FoodAI-AI-Node" --name "SATA Controller" --add sata --controller IntelAHCI
+vboxmanage storageattach "FoodAI-AI-Node" --storagectl "SATA Controller" --port 0 --device 0 --type hdd --medium "C:\VMs\FoodAI_AI.vdi"
+vboxmanage startvm "FoodAI-AI-Node" --type headless
+```
+
+---
+
+## 4. Secure Authentication & SSH Exchange
+Exchange SSH public keys to allow automated, passwordless container management across nodes:
+```bash
+# 1. Generate SSH Keys on WSL Client
+ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa_foodai -N ""
+
+# 2. Push Key to Database VM (Server A)
+ssh-copy-id -i ~/.ssh/id_rsa_foodai.pub operator@192.168.130.170
+
+# 3. Push Key to AI VM (Server B)
+ssh-copy-id -i ~/.ssh/id_rsa_foodai.pub operator@192.168.130.161
+```
+
+---
+
+## 5. Multi-Node Docker Network & Configuration
+
+To allow WSL, Hyper-V, and VirtualBox nodes to communicate, update the `.env` variables and `docker-compose.yml` to use bridged network endpoints.
+
+### Step 5.1: Configure WSL Client `.env`
+Update `.env` in the Streamlit workspace:
+```ini
+DB_HOST=192.168.130.170
+DB_USER=food_reader
+DB_PASS=reader_pass
+APP_AUTH_USER=food_app_auth
+APP_AUTH_PASS=auth_pass
+OLLAMA_HOST=http://192.168.130.161:11434
+SEARXNG_HOST=http://localhost:8080
+ZBX_SERVER_HOST=192.168.130.170
+```
+
+### Step 5.2: Configure Ollama (VirtualBox Server B) Listening Port
+Ensure the Ollama daemon inside VirtualBox binds to `0.0.0.0` (all interfaces):
+```bash
+# SSH into Server B (192.168.130.161)
+sudo systemctl edit ollama.service
+
+# Add the environment variables:
+[Service]
+Environment="OLLAMA_HOST=0.0.0.0"
+
+# Reload and restart service
+sudo systemctl daemon-reload
+sudo systemctl restart ollama
+```
+
+---
+
+## 6. Zabbix Reconfiguration for Multi-Node SNMPv3 Telemetry
+
+To monitor all distributed deployment environments securely:
+
+### Step 6.1: Deploy SNMPv3 Daemons
+Install and configure SNMPv3 daemons on WSL, Hyper-V Database VM, and VirtualBox AI VM:
+```bash
+sudo apt update && sudo apt install -y snmpd
+```
+Edit `/etc/snmp/snmpd.conf`:
+```
+# Listen on all interfaces
+agentAddress udp:161
+
+# Create secure SNMPv3 User
+createUser securityUser SHA "securityAuthPassword" AES "securityPrivPassword"
+rouser securityUser authpriv
+```
+Restart daemon:
+```bash
+sudo systemctl restart snmpd
+```
+
+### Step 6.2: Configure Zabbix Server Dashboard (Web UI)
+1. Open Zabbix in your browser at `http://192.168.130.170:8081`.
+2. Navigate to **Configuration > Hosts > Create Host**.
+3. Create three distinct hosts:
+   - **WSL-Workstation** (IP: `192.168.1.50`)
+   - **Database-Node** (IP: `192.168.130.170`)
+   - **AI-Node** (IP: `192.168.130.161`)
+4. Add the **SNMP Interface** pointing to Port 161 for each host.
+5. In the **Security Tab**, select SNMPv3, enter Username `securityUser`, select Auth Protocol `SHA` / `securityAuthPassword`, and Privacy Protocol `AES` / `securityPrivPassword`.
+6. Attach the pre-installed **Local Food AI Telemetry** Template.
+
+---
+
+## 7. Verifying Alert Channels
+
+### 7.1: Microsoft Teams / Discord Alert Webhook
+To verify Zabbix is communicating with Discord / Teams:
+1. Trigger a test CPU threshold spike inside WSL:
+   ```bash
+   yes > /dev/null & sleep 10 ; killall yes
+   ```
+2. Verify Zabbix triggers the alert and transmits the notification.
+3. Check your designated channel for the incoming payload:
+   - Expected Output: `[PROBLEM] High CPU Utilization Detected on WSL-Workstation`.
+
+### 7.2: Password Reset Email (SMTP Gateway)
+1. In the Streamlit UI Sidebar, select **Reset Password**.
+2. Trigger a reset link for user `ClinicianA`.
+3. Check the inbox or SMTP system log (`tail -f /var/log/mail.log` on Server A) to verify outbound delivery.
+
+---
+
+## 8. Operator Post-Installation Checklist
+
+Run these test cases to verify the installation:
+
+| TEST CASE ID | ACTIONS TO PERFORM | EXPECTED RESULTS | STATUS |
+| :--- | :--- | :--- | :---: |
+| **TC-OP-01** | Search 'Cheese' on Search Tab | 10+ records returned in <0.04s. Listeria warning flags on unpasteurized. | `[ ]` |
+| **TC-OP-02** | Enter '1.5 cups' in Plate Tab | Parsed and converted to metric grams based on density index. | `[ ]` |
+| **TC-OP-03** | Ask Chat: 'Can I eat sushi?' | Qwen2.5:7b retrieves database context and flags raw fish as forbidden for pregnancy. | `[ ]` |
+| **TC-OP-04** | Trigger manual db backup | Timestamped compressed .sql.gz created inside backups/ folder. | `[ ]` |
+| **TC-OP-05** | Terminate Ollama Container | Zabbix PROBLEM active alert generated on dashboard in < 30 seconds. | `[ ]` |

+ 3 - 3
docs/Scrum_Artifacts.md

@@ -1,3 +1,3 @@
-# $Id$
-# Scrum Artifacts
-Contains User Stories, velocity tracking, and burndown charts from Taiga.
+# $Id$
+# Scrum Artifacts
+Contains User Stories, velocity tracking, and burndown charts from Taiga.

+ 3 - 3
docs/Scrum_Daily.md

@@ -1,3 +1,3 @@
-# $Id$
-# Daily Scrums
-- **26.05.07 DAILY**: Fixed time scope bug, added Nginx proxy, built sync scripts.
+# $Id$
+# Daily Scrums
+- **26.05.07 DAILY**: Fixed time scope bug, added Nginx proxy, built sync scripts.

+ 3 - 3
docs/Scrum_Plan.md

@@ -1,3 +1,3 @@
-# $Id$
-# Sprint Plans
-- **Sprint 10 PLAN**: Fix LLM Tool Calling, optimize Cartesian SQL explosion, build Teams webhooks.
+# $Id$
+# Sprint Plans
+- **Sprint 10 PLAN**: Fix LLM Tool Calling, optimize Cartesian SQL explosion, build Teams webhooks.

+ 3 - 3
docs/Scrum_Retro.md

@@ -1,3 +1,3 @@
-# $Id$
-# Sprint Retrospectives
-- **Sprint 10 RETROSPECTIVE**: Mitigated dirty data duplicates using SQL `GROUP BY`. Need to maintain strict Git commit tagging (`TG-XXX`).
+# $Id$
+# Sprint Retrospectives
+- **Sprint 10 RETROSPECTIVE**: Mitigated dirty data duplicates using SQL `GROUP BY`. Need to maintain strict Git commit tagging (`TG-XXX`).

+ 3 - 3
docs/Scrum_Review.md

@@ -1,3 +1,3 @@
-# $Id$
-# Sprint Reviews
-- **Sprint 10 REVIEW**: App executes sub-second searches. Nginx fully operational on Port 80.
+# $Id$
+# Sprint Reviews
+- **Sprint 10 REVIEW**: App executes sub-second searches. Nginx fully operational on Port 80.

+ 35 - 35
docs/Scrum_Wiki.md

@@ -1,35 +1,35 @@
-# $Id$
-# Scrum Wiki Master List & Index Portal
-
-Welcome to the static Scrum documentation portal. This master wiki aggregates and organizes all daily stand-up logs, planning reports, retrospectives, reviews, and velocity charts recorded during the agile development of the **Local Food AI** clinical dietetics engine.
-
----
-
-## 📅 Sprint Ceremonies & Logs
-
-### 1. [Sprint Plans (Scrum_Plan.md)](file:///c:/Users/lanfr144/Documents/DOPRO1/Antigravity/Food/docs/Scrum_Plan.md)
-*Contains Sprint Plan formulations, active user stories selection, scope statements, and team capacity bounds for each milestone loop.*
-
-### 2. [Daily Scrums (Scrum_Daily.md)](file:///c:/Users/lanfr144/Documents/DOPRO1/Antigravity/Food/docs/Scrum_Daily.md)
-*Continuous daily stand-up summaries tracking individual task completion, blocker mitigations, and immediate day-to-day coordination.*
-
-### 3. [Sprint Reviews (Scrum_Review.md)](file:///c:/Users/lanfr144/Documents/DOPRO1/Antigravity/Food/docs/Scrum_Review.md)
-*Contains sprint review logs, clinician demonstration summaries, feature validation checklists, and stakeholder feedback logs.*
-
-### 4. [Sprint Retrospectives (Scrum_Retro.md)](file:///c:/Users/lanfr144/Documents/DOPRO1/Antigravity/Food/docs/Scrum_Retro.md)
-*Reviews process improvements, continuous integration learnings, and action items aimed at optimizing team operations and environment tuning.*
-
----
-
-## 📊 Deliverables & Quality Assurance
-
-### 5. [Scrum Artifacts (Scrum_Artifacts.md)](file:///c:/Users/lanfr144/Documents/DOPRO1/Antigravity/Food/docs/Scrum_Artifacts.md)
-*Indexes sprint velocity metrics, completed story points distributions, burndown coordinates, and final Taiga delivery milestones.*
-
-### 6. [Sprint 8 Test Cases (Test_Cases_Sprint8.md)](file:///c:/Users/lanfr144/Documents/DOPRO1/Antigravity/Food/docs/Test_Cases_Sprint8.md)
-*Legacy acceptance test logs covering core NLP chat, portion converters, and initial search validations.*
-
----
-
-> [!NOTE]
-> **Operational Compliance**: All Scrum files above are synchronized with their respective Taiga milestone identifiers (`Sprint 13` and `Sprint 7`). All physical activities recorded in these markdown logs have corresponding closed tasks inside Taiga.
+# $Id$
+# Scrum Wiki Master List & Index Portal
+
+Welcome to the static Scrum documentation portal. This master wiki aggregates and organizes all daily stand-up logs, planning reports, retrospectives, reviews, and velocity charts recorded during the agile development of the **Local Food AI** clinical dietetics engine.
+
+---
+
+## 📅 Sprint Ceremonies & Logs
+
+### 1. [Sprint Plans (Scrum_Plan.md)](file:///c:/Users/lanfr144/Documents/DOPRO1/Antigravity/Food/docs/Scrum_Plan.md)
+*Contains Sprint Plan formulations, active user stories selection, scope statements, and team capacity bounds for each milestone loop.*
+
+### 2. [Daily Scrums (Scrum_Daily.md)](file:///c:/Users/lanfr144/Documents/DOPRO1/Antigravity/Food/docs/Scrum_Daily.md)
+*Continuous daily stand-up summaries tracking individual task completion, blocker mitigations, and immediate day-to-day coordination.*
+
+### 3. [Sprint Reviews (Scrum_Review.md)](file:///c:/Users/lanfr144/Documents/DOPRO1/Antigravity/Food/docs/Scrum_Review.md)
+*Contains sprint review logs, clinician demonstration summaries, feature validation checklists, and stakeholder feedback logs.*
+
+### 4. [Sprint Retrospectives (Scrum_Retro.md)](file:///c:/Users/lanfr144/Documents/DOPRO1/Antigravity/Food/docs/Scrum_Retro.md)
+*Reviews process improvements, continuous integration learnings, and action items aimed at optimizing team operations and environment tuning.*
+
+---
+
+## 📊 Deliverables & Quality Assurance
+
+### 5. [Scrum Artifacts (Scrum_Artifacts.md)](file:///c:/Users/lanfr144/Documents/DOPRO1/Antigravity/Food/docs/Scrum_Artifacts.md)
+*Indexes sprint velocity metrics, completed story points distributions, burndown coordinates, and final Taiga delivery milestones.*
+
+### 6. [Sprint 8 Test Cases (Test_Cases_Sprint8.md)](file:///c:/Users/lanfr144/Documents/DOPRO1/Antigravity/Food/docs/Test_Cases_Sprint8.md)
+*Legacy acceptance test logs covering core NLP chat, portion converters, and initial search validations.*
+
+---
+
+> [!NOTE]
+> **Operational Compliance**: All Scrum files above are synchronized with their respective Taiga milestone identifiers (`Sprint 13` and `Sprint 7`). All physical activities recorded in these markdown logs have corresponding closed tasks inside Taiga.

+ 90 - 90
docs/Start_Stop_Procedures.md

@@ -1,90 +1,90 @@
-# $Id$
-# Infrastructure Stop & Start Operational Procedures
-
-This runbook outlines the exact sequence and commands to start, stop, and verify each microservice in the Local Food AI environment.
-
----
-
-## 1. Sequence Priority Rules
-Due to database socket requirements and network bindings, services **must** be started and stopped in the following order:
-
-```mermaid
-graph TD
-    subgraph Startup Sequence
-        direction TB
-        A[1. MySQL Database] --> B[2. Ollama & SearXNG AI Services]
-        B --> C[3. Streamlit Application & Nginx Proxy]
-        C --> D[4. Zabbix Monitoring & Airflow Supervisor]
-    end
-```
-
----
-
-## 2. Startup Procedures
-
-### Step 2.1: Start the Core MySQL Database
-Verify that the database service is up and listening on port 3307:
-```bash
-docker compose up -d mysql
-# Verify database logs
-docker compose logs -f mysql
-```
-
-### Step 2.2: Start AI Engine & SearXNG Search
-Deploy the AI components:
-```bash
-docker compose up -d ollama searxng
-# Check that Ollama responds
-curl http://localhost:11434/api/tags
-```
-
-### Step 2.3: Start Streamlit App and Nginx Gateway
-Bring up the frontend web interface and reverse proxy:
-```bash
-docker compose up -d app nginx
-# Verify Web Interface status
-curl -I http://localhost
-```
-
-### Step 2.4: Start Zabbix Monitoring Suite
-Deploy the monitoring server and agents:
-```bash
-docker compose up -d zabbix-server zabbix-web zabbix-agent
-# Check dashboard availability
-curl -I http://localhost:8081
-```
-
----
-
-## 3. Shutdown Procedures
-
-To perform system maintenance or schema migration, stop services in reverse order to prevent lockups:
-
-```bash
-# 1. Stop Monitoring Components
-docker compose stop zabbix-agent zabbix-web zabbix-server
-
-# 2. Stop Web Frontend and Proxy Gateway
-docker compose stop nginx app
-
-# 3. Stop NLP and Search Services
-docker compose stop searxng ollama
-
-# 4. Stop Database Container gracefully
-docker compose stop mysql
-```
-
----
-
-## 4. Status Verification Commands
-Use these commands to verify container state and port bindings:
-```bash
-# List all running containers in the stack
-docker compose ps
-
-# Inspect raw container logs for error spikes
-docker compose logs --tail=100
-
-# Verify TCP socket listener binds
-netstat -tulpn | grep -E "80|3307|8081|11434"
-```
+# $Id$
+# Infrastructure Stop & Start Operational Procedures
+
+This runbook outlines the exact sequence and commands to start, stop, and verify each microservice in the Local Food AI environment.
+
+---
+
+## 1. Sequence Priority Rules
+Due to database socket requirements and network bindings, services **must** be started and stopped in the following order:
+
+```mermaid
+graph TD
+    subgraph Startup Sequence
+        direction TB
+        A[1. MySQL Database] --> B[2. Ollama & SearXNG AI Services]
+        B --> C[3. Streamlit Application & Nginx Proxy]
+        C --> D[4. Zabbix Monitoring & Airflow Supervisor]
+    end
+```
+
+---
+
+## 2. Startup Procedures
+
+### Step 2.1: Start the Core MySQL Database
+Verify that the database service is up and listening on port 3307:
+```bash
+docker compose up -d mysql
+# Verify database logs
+docker compose logs -f mysql
+```
+
+### Step 2.2: Start AI Engine & SearXNG Search
+Deploy the AI components:
+```bash
+docker compose up -d ollama searxng
+# Check that Ollama responds
+curl http://localhost:11434/api/tags
+```
+
+### Step 2.3: Start Streamlit App and Nginx Gateway
+Bring up the frontend web interface and reverse proxy:
+```bash
+docker compose up -d app nginx
+# Verify Web Interface status
+curl -I http://localhost
+```
+
+### Step 2.4: Start Zabbix Monitoring Suite
+Deploy the monitoring server and agents:
+```bash
+docker compose up -d zabbix-server zabbix-web zabbix-agent
+# Check dashboard availability
+curl -I http://localhost:8081
+```
+
+---
+
+## 3. Shutdown Procedures
+
+To perform system maintenance or schema migration, stop services in reverse order to prevent lockups:
+
+```bash
+# 1. Stop Monitoring Components
+docker compose stop zabbix-agent zabbix-web zabbix-server
+
+# 2. Stop Web Frontend and Proxy Gateway
+docker compose stop nginx app
+
+# 3. Stop NLP and Search Services
+docker compose stop searxng ollama
+
+# 4. Stop Database Container gracefully
+docker compose stop mysql
+```
+
+---
+
+## 4. Status Verification Commands
+Use these commands to verify container state and port bindings:
+```bash
+# List all running containers in the stack
+docker compose ps
+
+# Inspect raw container logs for error spikes
+docker compose logs --tail=100
+
+# Verify TCP socket listener binds
+netstat -tulpn | grep -E "80|3307|8081|11434"
+```

+ 4 - 4
docs/Test_Cases_Sprint8.md

@@ -1,4 +1,4 @@
-# $Id$
-# Sprint 8 Legacy Test Cases
-- Tested RAG AI tool integration.
-- Tested user authentication flows.
+# $Id$
+# Sprint 8 Legacy Test Cases
+- Tested RAG AI tool integration.
+- Tested user authentication flows.

+ 41 - 41
docs/User_Description.md

@@ -1,41 +1,41 @@
-# $Id$
-# Local Food AI - User Description & Functional Guide
-
-## 1. System Vision
-The **Local Food AI** system is a strictly local, privacy-first, professional-grade clinical dietetics assistant. Developed specifically for clinics and healthcare practitioners, it provides offline nutritional analysis, meal planning, and warning flags based on dynamic patient health profiles. 
-
-Since the system operates entirely locally on local hypervisors, **zero patient medical data or search queries ever leave the server boundary**, ensuring 100% HIPAA compliance and data sovereignty.
-
----
-
-## 2. Core Functional Pillars
-
-### 📊 tab 1: Clinical Data Search (🔬 Clinical Search)
-Allows practitioners to search the 24GB OpenFoodFacts dataset in real time (average query response time < 0.04 seconds).
-- **Dynamic Medical Warnings**: Based on the active patient profile, foods are immediately flagged in the search results:
-  - ⚠️ **Red Warning Flags**: Highlight high-risk ingredients (e.g. Unpasteurized dairy or raw fish for pregnant patients, high-sodium foods for hypertensive patients, or high-sugar foods for diabetic patients).
-  - 💚 **Green Recommendations**: Highlight recommended dietary components (e.g. High iron/calcium for pregnant or breastfeeding mothers, high Vitamin C for scurvy prevention, or high iron for anemia).
-- **Flexible Column Customization**: Multi-select column headers to inspect specific macro and micro-nutrients.
-
-### 💬 tab 2: AI Clinical Chat (💬 AI Chat)
-An interactive NLP dialogue interface powered by a local lightweight LLM (**Qwen2.5:7b**).
-- **RAG-Driven Precision**: The AI dietitian automatically retrieves and reviews local database records and private meta-search results before formulating an answer.
-- **Dynamic Medical Guardrails**: The user's active illnesses, diets, and conditions are injected into the AI's system prompt in the background, forcing the AI to strictly enforce clinical safety constraints.
-
-### 🍽️ tab 3: My Plate Builder (🍽️ My Plate Builder)
-A recipe formulation utility to calculate combined nutritional intake.
-- **Natural Language Parsing**: Enables entering quantities in natural units (e.g., "1.5 cups", "2 tablespoons", "150g").
-- **Exact Conversion**: The system translates these custom units into metric grams based on product density metrics.
-- **Macro Summaries**: Instantly calculates and displays the total combined Protein, Fat, and Carbohydrates.
-
-### 🤖 tab 4: AI Meal Planner (🤖 AI Meal Planner)
-An automated clinical diet planner.
-- Generates a multi-meal daily menu formatted strictly as a Markdown table.
-- Dynamically enforces user-defined calorie limits and active medical restrictions.
-
----
-
-## 3. Supported Health & Medical Profiles
-- **Conditions**: Pregnant, Breastfeeding, Low Fat, Osteoporosis.
-- **Illnesses**: Diabetes, Hypertension, Kidney Disease, Scurvy, Anemia.
-- **Diets**: Vegan, Vegetarian, Kosher, Halal, Keto, Paleo, Christian (Lent/Good Friday).
+# $Id$
+# Local Food AI - User Description & Functional Guide
+
+## 1. System Vision
+The **Local Food AI** system is a strictly local, privacy-first, professional-grade clinical dietetics assistant. Developed specifically for clinics and healthcare practitioners, it provides offline nutritional analysis, meal planning, and warning flags based on dynamic patient health profiles. 
+
+Since the system operates entirely locally on local hypervisors, **zero patient medical data or search queries ever leave the server boundary**, ensuring 100% HIPAA compliance and data sovereignty.
+
+---
+
+## 2. Core Functional Pillars
+
+### 📊 tab 1: Clinical Data Search (🔬 Clinical Search)
+Allows practitioners to search the 24GB OpenFoodFacts dataset in real time (average query response time < 0.04 seconds).
+- **Dynamic Medical Warnings**: Based on the active patient profile, foods are immediately flagged in the search results:
+  - ⚠️ **Red Warning Flags**: Highlight high-risk ingredients (e.g. Unpasteurized dairy or raw fish for pregnant patients, high-sodium foods for hypertensive patients, or high-sugar foods for diabetic patients).
+  - 💚 **Green Recommendations**: Highlight recommended dietary components (e.g. High iron/calcium for pregnant or breastfeeding mothers, high Vitamin C for scurvy prevention, or high iron for anemia).
+- **Flexible Column Customization**: Multi-select column headers to inspect specific macro and micro-nutrients.
+
+### 💬 tab 2: AI Clinical Chat (💬 AI Chat)
+An interactive NLP dialogue interface powered by a local lightweight LLM (**Qwen2.5:7b**).
+- **RAG-Driven Precision**: The AI dietitian automatically retrieves and reviews local database records and private meta-search results before formulating an answer.
+- **Dynamic Medical Guardrails**: The user's active illnesses, diets, and conditions are injected into the AI's system prompt in the background, forcing the AI to strictly enforce clinical safety constraints.
+
+### 🍽️ tab 3: My Plate Builder (🍽️ My Plate Builder)
+A recipe formulation utility to calculate combined nutritional intake.
+- **Natural Language Parsing**: Enables entering quantities in natural units (e.g., "1.5 cups", "2 tablespoons", "150g").
+- **Exact Conversion**: The system translates these custom units into metric grams based on product density metrics.
+- **Macro Summaries**: Instantly calculates and displays the total combined Protein, Fat, and Carbohydrates.
+
+### 🤖 tab 4: AI Meal Planner (🤖 AI Meal Planner)
+An automated clinical diet planner.
+- Generates a multi-meal daily menu formatted strictly as a Markdown table.
+- Dynamically enforces user-defined calorie limits and active medical restrictions.
+
+---
+
+## 3. Supported Health & Medical Profiles
+- **Conditions**: Pregnant, Breastfeeding, Low Fat, Osteoporosis.
+- **Illnesses**: Diabetes, Hypertension, Kidney Disease, Scurvy, Anemia.
+- **Diets**: Vegan, Vegetarian, Kosher, Halal, Keto, Paleo, Christian (Lent/Good Friday).

+ 11 - 11
docs/User_Guide.md

@@ -1,11 +1,11 @@
-# $Id$
-# User Guide
-
-## 1. Clinical Data Search
-Search for products using keywords. The system utilizes FULLTEXT matching to instantly return the top 10 relevant matches alongside macronutrient data.
-
-## 2. My Plate Builder
-Add portion sizes of different foods to calculate cumulative nutritional intake. Use the 🗑️ icon to remove items.
-
-## 3. Chat with AI
-Ask the `qwen2.5:7b` model complex dietary questions. It natively utilizes RAG Tool Calling to silently search the database and formulate clinical answers.
+# $Id$
+# User Guide
+
+## 1. Clinical Data Search
+Search for products using keywords. The system utilizes FULLTEXT matching to instantly return the top 10 relevant matches alongside macronutrient data.
+
+## 2. My Plate Builder
+Add portion sizes of different foods to calculate cumulative nutritional intake. Use the 🗑️ icon to remove items.
+
+## 3. Chat with AI
+Ask the `qwen2.5:7b` model complex dietary questions. It natively utilizes RAG Tool Calling to silently search the database and formulate clinical answers.

+ 5 - 5
docs/WSL_Deployment.md

@@ -1,5 +1,5 @@
-# $Id$
-# WSL Deployment Runbook
-To deploy on Windows Subsystem for Linux:
-1. Ensure WSL2 backend is enabled in Docker Desktop.
-2. Follow standard Installation Guide inside the WSL Ubuntu terminal.
+# $Id$
+# WSL Deployment Runbook
+To deploy on Windows Subsystem for Linux:
+1. Ensure WSL2 backend is enabled in Docker Desktop.
+2. Follow standard Installation Guide inside the WSL Ubuntu terminal.

+ 3 - 3
docs/Wiki_Home.md

@@ -1,3 +1,3 @@
-# $Id$
-# Documentation Home
-Welcome to the static documentation mirror. Please navigate the markdown files in this directory for architectural diagrams and guides.
+# $Id$
+# Documentation Home
+Welcome to the static documentation mirror. Please navigate the markdown files in this directory for architectural diagrams and guides.

+ 82 - 82
docs/project_report.md

@@ -1,82 +1,82 @@
-# Capstone Project Report & File Documentation
-
-> [!NOTE]
-> **Dynamic Version Control**: This document is versioned under the master Git ID: `$Id$`.
-> All file versions and commit histories below are extracted directly from the live Git metadata logs.
-
----
-
-## 1. Project Overview & Deliverables
-The **Local Food AI** capstone project has successfully completed all sprint iterations. The system stands fully verified, containerized, and documented. 
-
-### What Has Been Done
-1. **Model Upgraded to Ollama Latest**: Transitioned from the `llama3.2:3b` model to the much more robust, large reasoning-focused **`qwen2.5:7b`** model (4.7 GB) with structured XML Chain-of-Thought (CoT) calculations. Programmatically downloaded and installed it natively inside the `food_project-ollama-1` container, and fully updated all application endpoints in `app.py`.
-2. **Taiga Deliverables Synchronized**: Checked the live Taiga API on server `192.168.130.161`. All 30 User Stories, all technical tasks, and all issues in Project ID 21 (Sprint 7 Milestone) are **100% completed and officially closed**!
-3. **Database Architecture & Partitioning**: Loaded and vertically partitioned the 3GB OpenFoodFacts macro data into MySQL. Configured matching FULLTEXT engines to search records in less than **0.04s** (averaging 90% latency reduction).
-4. **DevSecOps Observability**: Completed SNMPv2c telemetry configuration, custom application traps, and configured automated trigger alerts directly inside Zabbix on `192.168.130.170`.
-5. **Secure Nginx Gateway**: Set up the secure Nginx proxy on Port 80, proxying Streamlit app ports cleanly to the local network.
-6. **Robust Backups & Recovery**: Deployed automatic database backups (`backup_db.sh`) and local offline single-node fallback capabilities (`docker-compose_skip.yml`).
-7. **Sequential Operations Manager**: Created `manage_services.sh` to allow developers to safely stop, start, and restart all microservices in the proper dependency order without triggering redundant online ingestion sequences.
-
----
-
-## 2. Project File Catalog & Documentation
-Below is an exhaustive catalog of every critical file in the repository, detailing its path, functional purpose, and active Git version tags. 
-
-*Note: This chapter is compiled in landscape layout inside Project.pdf to guarantee complete columns readability.*
-
-| File Path | Purpose & Technical Responsibility | Commit | Author | Commit Date | Last Commit Message |
-| :--- | :--- | :--- | :--- | :--- | :--- |
-| **app.py**<br>`./app.py` | Core Streamlit Web Application. Hosts the clinical food search engine, the RAG chat dietitian interface (utilizing Ollama and SearXNG tool calling), and the visual plate builder. | `3e5cddb` | Lange François | 2026/05/22 09:47:33 | *TG-221 #closed - Refactor Ask Chat system prompt to use Chain of Thought (CoT) reasoning for higher clinical accuracy* |
-| **ingest_csv.py**<br>`./ingest_csv.py` | High-performance background database loader. Stream-reads and batch-inserts the 3GB OpenFoodFacts dataset into MySQL using Pandas chunking and optimizes indices post-load. | `264d274` | lanfr144 | 2026/05/21 09:43:09 | *TG-442: Sync resilience configurations, resolve SearXNG crash, and update docs with dynamic custom Git log ID and tag* |
-| **unit_converter.py**<br>`./unit_converter.py` | Mathematical converter engine that parses natural recipe volume inputs (e.g. cups, spoons) and converts them to metric weights based on macro density mappings. | `ea04a85` | lanfr144 | 2026/05/08 08:57:06 | *TG-86: finalize system pre-initialization, auto-pull LLM, egg scales* |
-| **snmp_notifier.py**<br>`./snmp_notifier.py` | Observability SNMP utility. Formulates and transmits raw SNMP trap payloads to the central Zabbix monitoring server on critical application failures. | `264d274` | lanfr144 | 2026/05/21 09:43:09 | *TG-442: Sync resilience configurations, resolve SearXNG crash, and update docs with dynamic custom Git log ID and tag* |
-| **configure_zabbix_alerts.py**<br>`./configure_zabbix_alerts.py` | DevOps provisioning script. Uses the Zabbix API to automatically set up host groups, custom templates, items, triggers, actions, and media types for alerts. | `264d274` | lanfr144 | 2026/05/21 09:43:09 | *TG-442: Sync resilience configurations, resolve SearXNG crash, and update docs with dynamic custom Git log ID and tag* |
-| **configure_zabbix_email.py**<br>`./configure_zabbix_email.py` | Security & Monitoring. Configures email media types and SMTP server routes for Zabbix alert notifications on system downtime. | `ade82af` | lanfr144 | 2026/05/18 14:08:27 | *TG-196: Full security refactor, Taiga sync, and Data pipeline automation* |
-| **zabbix_telemetry.py**<br>`./zabbix_telemetry.py` | Monitoring agent daemon. Queries active application statistics, memory, and query timers to supply Zabbix telemetry indicators. | `ade82af` | lanfr144 | 2026/05/18 14:08:27 | *TG-196: Full security refactor, Taiga sync, and Data pipeline automation* |
-| **check_users.py**<br>`./check_users.py` | Security utility. Verifies user accounts inside the MySQL `users` table and checks password hashing complexity. | `7766898` | lanfr144 | 2026/04/29 14:39:55 | *Add check users script* |
-| **rotate_passwords.py**<br>`./rotate_passwords.py` | Administrative credential utility. Cycles and re-encrypts database passwords within the `.env` secret file. | `ade82af` | lanfr144 | 2026/05/18 14:08:27 | *TG-196: Full security refactor, Taiga sync, and Data pipeline automation* |
-| **myloginpath.py**<br>`./myloginpath.py` | MySQL credential companion helper that simplifies the generation of encrypted login path configuration profiles. | `4655c26` | lanfr144 | 2026/04/29 08:30:03 | *Add untracked project files and configs* |
-| **data_sync.sh**<br>`./data_sync.sh` | Master pipeline coordinator. Supports download fetching in --online mode and local file processing in offline fallback mode. | `264d274` | lanfr144 | 2026/05/21 09:43:09 | *TG-442: Sync resilience configurations, resolve SearXNG crash, and update docs with dynamic custom Git log ID and tag* |
-| **backup_db.sh**<br>`./backup_db.sh` | Resiliency backup automation. Runs mysqldump on user tables inside the active container and prunes backups older than 7 days. | `264d274` | lanfr144 | 2026/05/21 09:43:09 | *TG-442: Sync resilience configurations, resolve SearXNG crash, and update docs with dynamic custom Git log ID and tag* |
-| **reset.sh**<br>`./reset.sh` | Teardown script. Wipes local temporary containers and prunes volume locks during crashes. | `264d274` | lanfr144 | 2026/05/21 09:43:09 | *TG-442: Sync resilience configurations, resolve SearXNG crash, and update docs with dynamic custom Git log ID and tag* |
-| **proper_reset.sh**<br>`./proper_reset.sh` | High-level administrative wipe script that brings the entire network stack and repositories back to a pristine state. | `776d6a6` | lanfr144 | 2026/04/29 12:44:49 | *Add proper reset* |
-| **deploy.sh**<br>`./deploy.sh` | Naked OS installation guide. Installs necessary system packages, Python venv libraries, and native Ollama. | `a54dc25` | lanfr144 | 2026/04/22 15:01:17 | *TG-21: Update deploy.sh to include requests connectivity dependency.* |
-| **start_batch_ingest.sh**<br>`./start_batch_ingest.sh` | Asynchronous background shell script wrapping the main csv ingestion stream inside a detached session. | `00f1d63` | lanfr144 | 2026/04/24 07:50:40 | *Fix python virtual env paths* |
-| **download_csv.sh**<br>`./download_csv.sh` | Downloader helper script that fetches specific smaller subsets of OpenFoodFacts CSV files. | `1a3cdca` | lanfr144 | 2026/05/05 07:14:54 | *fix: resolve pip encoding issue and add exec permissions to download script* |
-| **master_trigger.sh**<br>`./master_trigger.sh` | Orchestrator script that wakes and verifies multiple secondary subservices in sequence. | `38a83a1` | lanfr144 | 2026/04/23 10:50:37 | *Deployment Finalization: Vitamin schemas, Green UI, and Taiga tools* |
-| **manage_services.sh**<br>`./manage_services.sh` | DevOps service manager script. Handles automated, sequential startup, shutdown, restart, and health checking of all container elements in the stack. | `78a1c2c` | Lange François | 2026/05/22 07:55:19 | *docs: Hardening, hybrid landscape, documentation index, and US-203 Taiga tasks alignment* |
-| **generate_docs.py**<br>`./generate_docs.py` | Dynamic doc generator. Generates and mirrors all markdown manuals under `/docs` with live Git log metadata injection. | `09c5304` | Lange François | 2026/05/22 09:19:09 | *TG-220 TG-221 TG-222 #closed - Upgrade Ollama to Qwen2.5-7B, refactor backend prompts for XML scratchpad reasoning, and implement response parsing* |
-| **docker-compose.yml**<br>`./docker-compose.yml` | Main 10-container Docker orchestration map defining MySQL, App UI, Ollama Engine, SearXNG, Nginx proxy, Airflow stack, and Zabbix server suites. | `264d274` | lanfr144 | 2026/05/21 09:43:09 | *TG-442: Sync resilience configurations, resolve SearXNG crash, and update docs with dynamic custom Git log ID and tag* |
-| **docker-compose_skip.yml**<br>`./docker-compose_skip.yml` | Resilient 8-container offline/local single-node orchestration manifest. | `264d274` | lanfr144 | 2026/05/21 09:43:09 | *TG-442: Sync resilience configurations, resolve SearXNG crash, and update docs with dynamic custom Git log ID and tag* |
-| **docker-compose-wsl.yml**<br>`./docker-compose-wsl.yml` | WSL2-specific Docker Compose configuration file. Configures services with a +20 port shift to guarantee zero port conflicts on developer workstations. | `c52c6a1` | Lange François | 2026/05/31 15:41:22 | *docs: add WSL installation runbook, WSL compose file with shifted ports, and Taiga JSON export* |
-| **alembic.ini**<br>`./alembic.ini` | Alembic configuration setting routing database connection URIs for versioning schemas. | `73f7a04` | lanfr144 | 2026/04/24 16:18:55 | *Optimize horizontal partitioning to slice into 8-column chunks bypassing InnoDB limits* |
-| **my.cnf**<br>`./my.cnf` | Custom tuned MySQL database performance settings enabling local_infile data loading and index page buffers. | `86c76e2` | lanfr144 | 2026/04/17 10:26:35 | *TG-1: Fix MySQL 8.0 startup crash by removing premature validate_password plugin config* |
-| **.env**<br>`./.env` | Secret storage container holding encrypted MySQL user passwords and active environment flags. | `ca3877d` | lanfr144 | 2026/05/13 11:15:42 | *Stop save the .env file* |
-| **.gitattributes**<br>`./.gitattributes` | Git clean/smudge layout mapping enabling automatic tracking of dynamic $Id$ metadata expansion within version files. | `0cfdf52` | lanfr144 | 2026/05/07 09:54:17 | *TG-85: enable export-subst for Format string git identification* |
-| **requirements.txt**<br>`./requirements.txt` | Python runtime dependency catalog storing strict library versioning constraints. | `bb2ac28` | lanfr144 | 2026/05/11 07:59:05 | *fix requirements.txt encoding for fpdf2* |
-| **INSTALL_WSL.md**<br>`./INSTALL_WSL.md` | WSL2 deployment guide. Provides step-by-step instructions for installing and deploying the application inside WSL2 with port shifts. | `c52c6a1` | Lange François | 2026/05/31 15:41:22 | *docs: add WSL installation runbook, WSL compose file with shifted ports, and Taiga JSON export* |
-| **taiga/local-food-ai-1-36f35ff9-da1b-4eb5-9309-058448c998ad.json**<br>`./taiga/local-food-ai-1-36f35ff9-da1b-4eb5-9309-058448c998ad.json` | Historical Taiga Agile export. Contains the complete project history, including all closed user stories, tasks, and sprint configurations. | `c52c6a1` | Lange François | 2026/05/31 15:41:22 | *docs: add WSL installation runbook, WSL compose file with shifted ports, and Taiga JSON export* |
-| **scripts/generate_pdfs.py**<br>`./scripts/generate_pdfs.py` | PDF document builder. Converts all markdown documentation manuals under `/docs` into high-fidelity PDF format with expanded Git version headers. | `78a1c2c` | Lange François | 2026/05/22 07:55:19 | *docs: Hardening, hybrid landscape, documentation index, and US-203 Taiga tasks alignment* |
-| **scripts/generate_project_report.py**<br>`./scripts/generate_project_report.py` | Technical project report generator. Automatically gathers codebase structure, Git commit metadata, and purpose records to construct the Project.pdf report. | `a7732e0` | Lange François | 2026/05/31 15:41:33 | *docs: add WSL runbook, WSL compose file, and Taiga JSON export to report catalog* |
-| **scripts/setup_deploy.py**<br>`./scripts/setup_deploy.py` | DevOps deployment script. Orchestrates local and VM container sets, verifying network connectivity and system parameters. | `0065125` | lanfr144 | 2026/05/20 08:52:08 | *TG-202: Add log rotation limits to prevent 100% disk usage* |
-| **scripts/taiga_sync_final.py**<br>`./scripts/taiga_sync_final.py` | Taiga automated synchronization helper. Pushes bug tickets, fills wiki pages, and assigns unassigned user stories. | `a4342d3` | lanfr144 | 2026/05/19 09:09:10 | *TG-198: Add Taiga consistency automation script for full Jira/Agile alignment* |
-
----
-
-## 3. Directory Structure Map
-An overview of the folder hierarchy organizing our microservice infrastructure:
-
-- [**`alembic/`**](file:///c:/Users/lanfr144/Documents/DOPRO1/Antigravity/Food/alembic): Contains automated schema database migration revision files.
-- [**`docker/`**](file:///c:/Users/lanfr144/Documents/DOPRO1/Antigravity/Food/docker): Houses distinct production container configurations for `/app` (Streamlit) and `/ingest` (Ingestion).
-- [**`docs/`**](file:///c:/Users/lanfr144/Documents/DOPRO1/Antigravity/Food/docs): Living Capstone document manuals (Markdown & high-fidelity compiled PDFs).
-- [**`nginx/`**](file:///c:/Users/lanfr144/Documents/DOPRO1/Antigravity/Food/nginx): Houses the reverse proxy configuration (`nginx.conf`) forwarding local port 80 traffic.
-- [**`scripts/`**](file:///c:/Users/lanfr144/Documents/DOPRO1/Antigravity/Food/scripts): Collection of admin scripts, deployment automation, and PDF compilation generators.
-- [**`searxng/`**](file:///c:/Users/lanfr144/Documents/DOPRO1/Antigravity/Food/searxng): Core configuration files (`settings.yml`) securing private, localized search operations.
-
----
-
-## 4. Operational Next Steps (Day 2 Procedures)
-1. **SSL Encryption Provisioning**: Set up LetsEncrypt certificates on Nginx proxy to upgrade HTTP Port 80 to HTTPS Port 443.
-2. **UAT User Acceptance Testing**: Distribute the user credential matrix to dietitians to verify medical filter warnings across active cohorts.
-3. **Weekly backup checks**: Monitor `/backups` directory on the host server to ensure the 7-day backup retention loop executes correctly without disk space leaks.
+# Capstone Project Report & File Documentation
+
+> [!NOTE]
+> **Dynamic Version Control**: This document is versioned under the master Git ID: `$Id$`.
+> All file versions and commit histories below are extracted directly from the live Git metadata logs.
+
+---
+
+## 1. Project Overview & Deliverables
+The **Local Food AI** capstone project has successfully completed all sprint iterations. The system stands fully verified, containerized, and documented. 
+
+### What Has Been Done
+1. **Model Upgraded to Ollama Latest**: Transitioned from the `llama3.2:3b` model to the much more robust, large reasoning-focused **`qwen2.5:7b`** model (4.7 GB) with structured XML Chain-of-Thought (CoT) calculations. Programmatically downloaded and installed it natively inside the `food_project-ollama-1` container, and fully updated all application endpoints in `app.py`.
+2. **Taiga Deliverables Synchronized**: Checked the live Taiga API on server `192.168.130.161`. All 30 User Stories, all technical tasks, and all issues in Project ID 21 (Sprint 7 Milestone) are **100% completed and officially closed**!
+3. **Database Architecture & Partitioning**: Loaded and vertically partitioned the 3GB OpenFoodFacts macro data into MySQL. Configured matching FULLTEXT engines to search records in less than **0.04s** (averaging 90% latency reduction).
+4. **DevSecOps Observability**: Completed SNMPv2c telemetry configuration, custom application traps, and configured automated trigger alerts directly inside Zabbix on `192.168.130.170`.
+5. **Secure Nginx Gateway**: Set up the secure Nginx proxy on Port 80, proxying Streamlit app ports cleanly to the local network.
+6. **Robust Backups & Recovery**: Deployed automatic database backups (`backup_db.sh`) and local offline single-node fallback capabilities (`docker-compose_skip.yml`).
+7. **Sequential Operations Manager**: Created `manage_services.sh` to allow developers to safely stop, start, and restart all microservices in the proper dependency order without triggering redundant online ingestion sequences.
+
+---
+
+## 2. Project File Catalog & Documentation
+Below is an exhaustive catalog of every critical file in the repository, detailing its path, functional purpose, and active Git version tags. 
+
+*Note: This chapter is compiled in landscape layout inside Project.pdf to guarantee complete columns readability.*
+
+| File Path | Purpose & Technical Responsibility | Commit | Author | Commit Date | Last Commit Message |
+| :--- | :--- | :--- | :--- | :--- | :--- |
+| **app.py**<br>`./app.py` | Core Streamlit Web Application. Hosts the clinical food search engine, the RAG chat dietitian interface (utilizing Ollama and SearXNG tool calling), and the visual plate builder. | `3e5cddb` | Lange François | 2026/05/22 09:47:33 | *TG-221 #closed - Refactor Ask Chat system prompt to use Chain of Thought (CoT) reasoning for higher clinical accuracy* |
+| **ingest_csv.py**<br>`./ingest_csv.py` | High-performance background database loader. Stream-reads and batch-inserts the 3GB OpenFoodFacts dataset into MySQL using Pandas chunking and optimizes indices post-load. | `264d274` | lanfr144 | 2026/05/21 09:43:09 | *TG-442: Sync resilience configurations, resolve SearXNG crash, and update docs with dynamic custom Git log ID and tag* |
+| **unit_converter.py**<br>`./unit_converter.py` | Mathematical converter engine that parses natural recipe volume inputs (e.g. cups, spoons) and converts them to metric weights based on macro density mappings. | `ea04a85` | lanfr144 | 2026/05/08 08:57:06 | *TG-86: finalize system pre-initialization, auto-pull LLM, egg scales* |
+| **snmp_notifier.py**<br>`./snmp_notifier.py` | Observability SNMP utility. Formulates and transmits raw SNMP trap payloads to the central Zabbix monitoring server on critical application failures. | `264d274` | lanfr144 | 2026/05/21 09:43:09 | *TG-442: Sync resilience configurations, resolve SearXNG crash, and update docs with dynamic custom Git log ID and tag* |
+| **configure_zabbix_alerts.py**<br>`./configure_zabbix_alerts.py` | DevOps provisioning script. Uses the Zabbix API to automatically set up host groups, custom templates, items, triggers, actions, and media types for alerts. | `264d274` | lanfr144 | 2026/05/21 09:43:09 | *TG-442: Sync resilience configurations, resolve SearXNG crash, and update docs with dynamic custom Git log ID and tag* |
+| **configure_zabbix_email.py**<br>`./configure_zabbix_email.py` | Security & Monitoring. Configures email media types and SMTP server routes for Zabbix alert notifications on system downtime. | `ade82af` | lanfr144 | 2026/05/18 14:08:27 | *TG-196: Full security refactor, Taiga sync, and Data pipeline automation* |
+| **zabbix_telemetry.py**<br>`./zabbix_telemetry.py` | Monitoring agent daemon. Queries active application statistics, memory, and query timers to supply Zabbix telemetry indicators. | `ade82af` | lanfr144 | 2026/05/18 14:08:27 | *TG-196: Full security refactor, Taiga sync, and Data pipeline automation* |
+| **check_users.py**<br>`./check_users.py` | Security utility. Verifies user accounts inside the MySQL `users` table and checks password hashing complexity. | `7766898` | lanfr144 | 2026/04/29 14:39:55 | *Add check users script* |
+| **rotate_passwords.py**<br>`./rotate_passwords.py` | Administrative credential utility. Cycles and re-encrypts database passwords within the `.env` secret file. | `ade82af` | lanfr144 | 2026/05/18 14:08:27 | *TG-196: Full security refactor, Taiga sync, and Data pipeline automation* |
+| **myloginpath.py**<br>`./myloginpath.py` | MySQL credential companion helper that simplifies the generation of encrypted login path configuration profiles. | `4655c26` | lanfr144 | 2026/04/29 08:30:03 | *Add untracked project files and configs* |
+| **data_sync.sh**<br>`./data_sync.sh` | Master pipeline coordinator. Supports download fetching in --online mode and local file processing in offline fallback mode. | `264d274` | lanfr144 | 2026/05/21 09:43:09 | *TG-442: Sync resilience configurations, resolve SearXNG crash, and update docs with dynamic custom Git log ID and tag* |
+| **backup_db.sh**<br>`./backup_db.sh` | Resiliency backup automation. Runs mysqldump on user tables inside the active container and prunes backups older than 7 days. | `264d274` | lanfr144 | 2026/05/21 09:43:09 | *TG-442: Sync resilience configurations, resolve SearXNG crash, and update docs with dynamic custom Git log ID and tag* |
+| **reset.sh**<br>`./reset.sh` | Teardown script. Wipes local temporary containers and prunes volume locks during crashes. | `264d274` | lanfr144 | 2026/05/21 09:43:09 | *TG-442: Sync resilience configurations, resolve SearXNG crash, and update docs with dynamic custom Git log ID and tag* |
+| **proper_reset.sh**<br>`./proper_reset.sh` | High-level administrative wipe script that brings the entire network stack and repositories back to a pristine state. | `776d6a6` | lanfr144 | 2026/04/29 12:44:49 | *Add proper reset* |
+| **deploy.sh**<br>`./deploy.sh` | Naked OS installation guide. Installs necessary system packages, Python venv libraries, and native Ollama. | `a54dc25` | lanfr144 | 2026/04/22 15:01:17 | *TG-21: Update deploy.sh to include requests connectivity dependency.* |
+| **start_batch_ingest.sh**<br>`./start_batch_ingest.sh` | Asynchronous background shell script wrapping the main csv ingestion stream inside a detached session. | `00f1d63` | lanfr144 | 2026/04/24 07:50:40 | *Fix python virtual env paths* |
+| **download_csv.sh**<br>`./download_csv.sh` | Downloader helper script that fetches specific smaller subsets of OpenFoodFacts CSV files. | `1a3cdca` | lanfr144 | 2026/05/05 07:14:54 | *fix: resolve pip encoding issue and add exec permissions to download script* |
+| **master_trigger.sh**<br>`./master_trigger.sh` | Orchestrator script that wakes and verifies multiple secondary subservices in sequence. | `38a83a1` | lanfr144 | 2026/04/23 10:50:37 | *Deployment Finalization: Vitamin schemas, Green UI, and Taiga tools* |
+| **manage_services.sh**<br>`./manage_services.sh` | DevOps service manager script. Handles automated, sequential startup, shutdown, restart, and health checking of all container elements in the stack. | `78a1c2c` | Lange François | 2026/05/22 07:55:19 | *docs: Hardening, hybrid landscape, documentation index, and US-203 Taiga tasks alignment* |
+| **generate_docs.py**<br>`./generate_docs.py` | Dynamic doc generator. Generates and mirrors all markdown manuals under `/docs` with live Git log metadata injection. | `09c5304` | Lange François | 2026/05/22 09:19:09 | *TG-220 TG-221 TG-222 #closed - Upgrade Ollama to Qwen2.5-7B, refactor backend prompts for XML scratchpad reasoning, and implement response parsing* |
+| **docker-compose.yml**<br>`./docker-compose.yml` | Main 10-container Docker orchestration map defining MySQL, App UI, Ollama Engine, SearXNG, Nginx proxy, Airflow stack, and Zabbix server suites. | `264d274` | lanfr144 | 2026/05/21 09:43:09 | *TG-442: Sync resilience configurations, resolve SearXNG crash, and update docs with dynamic custom Git log ID and tag* |
+| **docker-compose_skip.yml**<br>`./docker-compose_skip.yml` | Resilient 8-container offline/local single-node orchestration manifest. | `264d274` | lanfr144 | 2026/05/21 09:43:09 | *TG-442: Sync resilience configurations, resolve SearXNG crash, and update docs with dynamic custom Git log ID and tag* |
+| **docker-compose-wsl.yml**<br>`./docker-compose-wsl.yml` | WSL2-specific Docker Compose configuration file. Configures services with a +20 port shift to guarantee zero port conflicts on developer workstations. | `c52c6a1` | Lange François | 2026/05/31 15:41:22 | *docs: add WSL installation runbook, WSL compose file with shifted ports, and Taiga JSON export* |
+| **alembic.ini**<br>`./alembic.ini` | Alembic configuration setting routing database connection URIs for versioning schemas. | `73f7a04` | lanfr144 | 2026/04/24 16:18:55 | *Optimize horizontal partitioning to slice into 8-column chunks bypassing InnoDB limits* |
+| **my.cnf**<br>`./my.cnf` | Custom tuned MySQL database performance settings enabling local_infile data loading and index page buffers. | `86c76e2` | lanfr144 | 2026/04/17 10:26:35 | *TG-1: Fix MySQL 8.0 startup crash by removing premature validate_password plugin config* |
+| **.env**<br>`./.env` | Secret storage container holding encrypted MySQL user passwords and active environment flags. | `ca3877d` | lanfr144 | 2026/05/13 11:15:42 | *Stop save the .env file* |
+| **.gitattributes**<br>`./.gitattributes` | Git clean/smudge layout mapping enabling automatic tracking of dynamic $Id$ metadata expansion within version files. | `0cfdf52` | lanfr144 | 2026/05/07 09:54:17 | *TG-85: enable export-subst for Format string git identification* |
+| **requirements.txt**<br>`./requirements.txt` | Python runtime dependency catalog storing strict library versioning constraints. | `bb2ac28` | lanfr144 | 2026/05/11 07:59:05 | *fix requirements.txt encoding for fpdf2* |
+| **INSTALL_WSL.md**<br>`./INSTALL_WSL.md` | WSL2 deployment guide. Provides step-by-step instructions for installing and deploying the application inside WSL2 with port shifts. | `c52c6a1` | Lange François | 2026/05/31 15:41:22 | *docs: add WSL installation runbook, WSL compose file with shifted ports, and Taiga JSON export* |
+| **taiga/local-food-ai-1-36f35ff9-da1b-4eb5-9309-058448c998ad.json**<br>`./taiga/local-food-ai-1-36f35ff9-da1b-4eb5-9309-058448c998ad.json` | Historical Taiga Agile export. Contains the complete project history, including all closed user stories, tasks, and sprint configurations. | `c52c6a1` | Lange François | 2026/05/31 15:41:22 | *docs: add WSL installation runbook, WSL compose file with shifted ports, and Taiga JSON export* |
+| **scripts/generate_pdfs.py**<br>`./scripts/generate_pdfs.py` | PDF document builder. Converts all markdown documentation manuals under `/docs` into high-fidelity PDF format with expanded Git version headers. | `78a1c2c` | Lange François | 2026/05/22 07:55:19 | *docs: Hardening, hybrid landscape, documentation index, and US-203 Taiga tasks alignment* |
+| **scripts/generate_project_report.py**<br>`./scripts/generate_project_report.py` | Technical project report generator. Automatically gathers codebase structure, Git commit metadata, and purpose records to construct the Project.pdf report. | `a7732e0` | Lange François | 2026/05/31 15:41:33 | *docs: add WSL runbook, WSL compose file, and Taiga JSON export to report catalog* |
+| **scripts/setup_deploy.py**<br>`./scripts/setup_deploy.py` | DevOps deployment script. Orchestrates local and VM container sets, verifying network connectivity and system parameters. | `0065125` | lanfr144 | 2026/05/20 08:52:08 | *TG-202: Add log rotation limits to prevent 100% disk usage* |
+| **scripts/taiga_sync_final.py**<br>`./scripts/taiga_sync_final.py` | Taiga automated synchronization helper. Pushes bug tickets, fills wiki pages, and assigns unassigned user stories. | `a4342d3` | lanfr144 | 2026/05/19 09:09:10 | *TG-198: Add Taiga consistency automation script for full Jira/Agile alignment* |
+
+---
+
+## 3. Directory Structure Map
+An overview of the folder hierarchy organizing our microservice infrastructure:
+
+- [**`alembic/`**](file:///c:/Users/lanfr144/Documents/DOPRO1/Antigravity/Food/alembic): Contains automated schema database migration revision files.
+- [**`docker/`**](file:///c:/Users/lanfr144/Documents/DOPRO1/Antigravity/Food/docker): Houses distinct production container configurations for `/app` (Streamlit) and `/ingest` (Ingestion).
+- [**`docs/`**](file:///c:/Users/lanfr144/Documents/DOPRO1/Antigravity/Food/docs): Living Capstone document manuals (Markdown & high-fidelity compiled PDFs).
+- [**`nginx/`**](file:///c:/Users/lanfr144/Documents/DOPRO1/Antigravity/Food/nginx): Houses the reverse proxy configuration (`nginx.conf`) forwarding local port 80 traffic.
+- [**`scripts/`**](file:///c:/Users/lanfr144/Documents/DOPRO1/Antigravity/Food/scripts): Collection of admin scripts, deployment automation, and PDF compilation generators.
+- [**`searxng/`**](file:///c:/Users/lanfr144/Documents/DOPRO1/Antigravity/Food/searxng): Core configuration files (`settings.yml`) securing private, localized search operations.
+
+---
+
+## 4. Operational Next Steps (Day 2 Procedures)
+1. **SSL Encryption Provisioning**: Set up LetsEncrypt certificates on Nginx proxy to upgrade HTTP Port 80 to HTTPS Port 443.
+2. **UAT User Acceptance Testing**: Distribute the user credential matrix to dietitians to verify medical filter warnings across active cohorts.
+3. **Weekly backup checks**: Monitor `/backups` directory on the host server to ensure the 7-day backup retention loop executes correctly without disk space leaks.

+ 1 - 1
k8s/secret.yaml.example

@@ -15,4 +15,4 @@ stringData:
 
   EMAIL_PASS: "placeholder_email_pass"
   TAIGA_USER: base64_encoded_placeholder
-  TAIGA_PASS: base64_encoded_placeholder
+  TAIGA_PASS: base64_encoded_placeholder

+ 290 - 290
scripts/setup_deploy.py

@@ -1,290 +1,290 @@
-import os
-import sys
-import getpass
-
-import subprocess
-
-def clear_screen():
-    os.system('cls' if os.name == 'nt' else 'clear')
-
-print("="*60)
-print(" Local Food AI - Distributed Deployment Configuration Tool")
-print("="*60)
-
-# Check Docker availability
-try:
-    subprocess.run(["docker", "info"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
-    print("[+] Docker is correctly configured and accessible.")
-except (subprocess.CalledProcessError, FileNotFoundError):
-    print("[-] Warning: Docker is not running or not accessible. Please ensure Docker Desktop or Docker Engine is installed and running before deploying.")
-
-print("\nSelect the role for this specific node in the network:")
-print("1. All-in-One (Runs everything, default)")
-print("2. Application Frontend (Runs Streamlit, Nginx, AI Services)")
-print("3. Database Node (Runs MySQL & Ingestion)")
-print("4. Monitoring Node (Runs Zabbix Server & UI)")
-
-choice = input("\nEnter choice (1-4) [1]: ").strip() or "1"
-
-# Environment Variables
-env_vars = {}
-
-if choice != "1":
-    print("\n--- Network Configuration ---")
-    if choice != "3":
-        env_vars['DB_HOST'] = input("Enter the IP address of the Database Node: ").strip()
-    else:
-        env_vars['DB_HOST'] = "mysql"
-        
-    if choice != "4":
-        env_vars['ZBX_SERVER_HOST'] = input("Enter the IP address of the Monitoring Node: ").strip()
-    else:
-        env_vars['ZBX_SERVER_HOST'] = "zabbix-server"
-else:
-    env_vars['DB_HOST'] = "mysql"
-    env_vars['ZBX_SERVER_HOST'] = "zabbix-server"
-
-print("\n--- Security Configuration ---")
-env_vars['MYSQL_ROOT_PASSWORD'] = getpass.getpass("Enter MySQL Root Password (will not echo): ")
-env_vars['DB_READER_PASS'] = getpass.getpass("Enter DB Reader Password: ")
-env_vars['DB_LOADER_PASS'] = getpass.getpass("Enter DB Loader Password: ")
-env_vars['DB_APP_AUTH_PASS'] = getpass.getpass("Enter App Auth Password: ")
-env_vars['MYSQL_ZABBIX_PASSWORD'] = getpass.getpass("Enter Zabbix DB Password: ")
-
-# Generate .env
-print("\n[+] Generating .env file...")
-with open(".env", "w") as f:
-    for k, v in env_vars.items():
-        f.write(f"{k}={v}\n")
-
-# Base compose dictionaries
-compose_services = {}
-
-mysql_service = """
-  mysql:
-    build:
-      context: ./docker/mysql
-    ports:
-      - "3307:3306"
-    volumes:
-      - mysql_data:/var/lib/mysql
-      - ./my.cnf:/etc/mysql/conf.d/custom_ai_app.cnf
-      - ./init.sql:/docker-entrypoint-initdb.d/1-init.sql
-    environment:
-      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
-    healthcheck:
-      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
-      interval: 10s
-      timeout: 5s
-      retries: 20
-    restart: always
-    logging:
-      driver: "json-file"
-      options:
-        max-size: "50m"
-        max-file: "3"
-"""
-
-ingest_service = """
-  ingest:
-    build:
-      context: .
-      dockerfile: docker/ingest/Dockerfile
-    environment:
-      - DB_HOST=${DB_HOST}
-      - DB_USER=food_loader
-      - DB_PASS=${DB_LOADER_PASS}
-    volumes:
-      - ./:/app
-    profiles:
-      - manual
-"""
-
-ai_services = """
-  ollama:
-    image: ollama/ollama:latest
-    volumes:
-      - ollama_data:/root/.ollama
-    restart: always
-    logging:
-      driver: "json-file"
-      options:
-        max-size: "50m"
-        max-file: "3"
-
-  searxng:
-    image: searxng/searxng:latest
-    ports:
-      - "8080:8080"
-    volumes:
-      - ./searxng:/etc/searxng
-    environment:
-      - SEARXNG_BASE_URL=http://localhost:8080/
-    restart: always
-    logging:
-      driver: "json-file"
-      options:
-        max-size: "50m"
-        max-file: "3"
-"""
-
-app_service = """
-  app:
-    build:
-      context: .
-      dockerfile: docker/app/Dockerfile
-    ports:
-      - "8502:8501"
-    environment:
-      - DB_HOST=${DB_HOST}
-      - DB_USER=food_reader
-      - DB_PASS=${DB_READER_PASS}
-      - APP_AUTH_USER=food_app_auth
-      - APP_AUTH_PASS=${DB_APP_AUTH_PASS}
-      - OLLAMA_HOST=http://ollama:11434
-      - SEARXNG_HOST=http://searxng:8080
-    restart: always
-    logging:
-      driver: "json-file"
-      options:
-        max-size: "50m"
-        max-file: "3"
-
-  nginx:
-    image: nginx:latest
-    ports:
-      - "80:80"
-    volumes:
-      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
-    restart: always
-    logging:
-      driver: "json-file"
-      options:
-        max-size: "50m"
-        max-file: "3"
-"""
-
-monitoring_services = """
-  zabbix-server:
-    image: zabbix/zabbix-server-mysql:ubuntu-7.0-latest
-    environment:
-      - DB_SERVER_HOST=${DB_HOST}
-      - MYSQL_USER=zabbix
-      - MYSQL_PASSWORD=${MYSQL_ZABBIX_PASSWORD}
-      - ZBX_SNMPTRAPPER=1
-    restart: always
-    logging:
-      driver: "json-file"
-      options:
-        max-size: "50m"
-        max-file: "3"
-    ports:
-      - "10051:10051"
-
-  zabbix-web:
-    image: zabbix/zabbix-web-nginx-mysql:ubuntu-7.0-latest
-    ports:
-      - "8081:8080"
-      - "8444:8443"
-    environment:
-      - DB_SERVER_HOST=${DB_HOST}
-      - MYSQL_USER=zabbix
-      - MYSQL_PASSWORD=${MYSQL_ZABBIX_PASSWORD}
-      - ZBX_SERVER_HOST=zabbix-server
-      - PHP_TZ=Europe/Paris
-    restart: always
-    logging:
-      driver: "json-file"
-      options:
-        max-size: "50m"
-        max-file: "3"
-
-  zabbix-agent:
-    image: zabbix/zabbix-agent:ubuntu-7.0-latest
-    environment:
-      - ZBX_HOSTNAME=DistributedNode
-      - ZBX_SERVER_HOST=${ZBX_SERVER_HOST}
-    privileged: true
-    pid: "host"
-    volumes:
-      - /var/run:/var/run
-    restart: always
-    logging:
-      driver: "json-file"
-      options:
-        max-size: "50m"
-        max-file: "3"
-"""
-airflow_services = """
-  airflow-webserver:
-    image: apache/airflow:2.8.1
-    environment:
-      - AIRFLOW__CORE__EXECUTOR=SequentialExecutor
-      - AIRFLOW__DATABASE__SQL_ALCHEMY_CONN=sqlite:////opt/airflow/airflow.db
-      - AIRFLOW__CORE__LOAD_EXAMPLES=False
-    ports:
-      - "8082:8080"
-    volumes:
-      - ./dags:/opt/airflow/dags
-      - ./logs:/opt/airflow/logs
-      - ./data:/opt/airflow/data
-      - /var/run/docker.sock:/var/run/docker.sock
-    command: webserver
-    restart: always
-    logging:
-      driver: "json-file"
-      options:
-        max-size: "50m"
-        max-file: "3"
-
-  airflow-scheduler:
-    image: apache/airflow:2.8.1
-    environment:
-      - AIRFLOW__CORE__EXECUTOR=SequentialExecutor
-      - AIRFLOW__DATABASE__SQL_ALCHEMY_CONN=sqlite:////opt/airflow/airflow.db
-      - AIRFLOW__CORE__LOAD_EXAMPLES=False
-    volumes:
-      - ./dags:/opt/airflow/dags
-      - ./logs:/opt/airflow/logs
-      - ./data:/opt/airflow/data
-      - /var/run/docker.sock:/var/run/docker.sock
-    command: bash -c "airflow db migrate && airflow users create --role Admin --username admin --email admin --firstname admin --lastname admin --password admin && airflow scheduler"
-    restart: always
-    logging:
-      driver: "json-file"
-      options:
-        max-size: "50m"
-        max-file: "3"
-"""
-
-header = "services:\n"
-footer = """
-volumes:
-  mysql_data:
-  ollama_data:
-"""
-
-compose_content = header
-
-if choice == "1":
-    compose_content += mysql_service + ingest_service + ai_services + app_service + monitoring_services + airflow_services
-elif choice == "2":
-    compose_content += ai_services + app_service
-    footer = "volumes:\n  ollama_data:\n"
-elif choice == "3":
-    compose_content += mysql_service + ingest_service + airflow_services
-    footer = "volumes:\n  mysql_data:\n"
-elif choice == "4":
-    compose_content += monitoring_services
-    footer = ""
-
-print("\n[+] Generating docker-compose.yml for selected role...")
-with open("docker-compose.yml", "w") as f:
-    f.write(compose_content + footer)
-
-print("\n" + "="*60)
-print("⚠️ IMPORTANT HYPERVISOR NETWORKING REMINDER:")
-print("If this node is running inside VirtualBox or Hyper-V, you MUST configure the VM network adapter to use a 'Bridged Adapter' or 'External Virtual Switch' so it shares the host's subnet. Otherwise, cross-node communication will fail.")
-print("="*60)
-
-print("\nDone! You can now run `docker compose up -d`.")
+import os
+import sys
+import getpass
+
+import subprocess
+
+def clear_screen():
+    os.system('cls' if os.name == 'nt' else 'clear')
+
+print("="*60)
+print(" Local Food AI - Distributed Deployment Configuration Tool")
+print("="*60)
+
+# Check Docker availability
+try:
+    subprocess.run(["docker", "info"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
+    print("[+] Docker is correctly configured and accessible.")
+except (subprocess.CalledProcessError, FileNotFoundError):
+    print("[-] Warning: Docker is not running or not accessible. Please ensure Docker Desktop or Docker Engine is installed and running before deploying.")
+
+print("\nSelect the role for this specific node in the network:")
+print("1. All-in-One (Runs everything, default)")
+print("2. Application Frontend (Runs Streamlit, Nginx, AI Services)")
+print("3. Database Node (Runs MySQL & Ingestion)")
+print("4. Monitoring Node (Runs Zabbix Server & UI)")
+
+choice = input("\nEnter choice (1-4) [1]: ").strip() or "1"
+
+# Environment Variables
+env_vars = {}
+
+if choice != "1":
+    print("\n--- Network Configuration ---")
+    if choice != "3":
+        env_vars['DB_HOST'] = input("Enter the IP address of the Database Node: ").strip()
+    else:
+        env_vars['DB_HOST'] = "mysql"
+        
+    if choice != "4":
+        env_vars['ZBX_SERVER_HOST'] = input("Enter the IP address of the Monitoring Node: ").strip()
+    else:
+        env_vars['ZBX_SERVER_HOST'] = "zabbix-server"
+else:
+    env_vars['DB_HOST'] = "mysql"
+    env_vars['ZBX_SERVER_HOST'] = "zabbix-server"
+
+print("\n--- Security Configuration ---")
+env_vars['MYSQL_ROOT_PASSWORD'] = getpass.getpass("Enter MySQL Root Password (will not echo): ")
+env_vars['DB_READER_PASS'] = getpass.getpass("Enter DB Reader Password: ")
+env_vars['DB_LOADER_PASS'] = getpass.getpass("Enter DB Loader Password: ")
+env_vars['DB_APP_AUTH_PASS'] = getpass.getpass("Enter App Auth Password: ")
+env_vars['MYSQL_ZABBIX_PASSWORD'] = getpass.getpass("Enter Zabbix DB Password: ")
+
+# Generate .env
+print("\n[+] Generating .env file...")
+with open(".env", "w") as f:
+    for k, v in env_vars.items():
+        f.write(f"{k}={v}\n")
+
+# Base compose dictionaries
+compose_services = {}
+
+mysql_service = """
+  mysql:
+    build:
+      context: ./docker/mysql
+    ports:
+      - "3307:3306"
+    volumes:
+      - mysql_data:/var/lib/mysql
+      - ./my.cnf:/etc/mysql/conf.d/custom_ai_app.cnf
+      - ./init.sql:/docker-entrypoint-initdb.d/1-init.sql
+    environment:
+      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
+    healthcheck:
+      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
+      interval: 10s
+      timeout: 5s
+      retries: 20
+    restart: always
+    logging:
+      driver: "json-file"
+      options:
+        max-size: "50m"
+        max-file: "3"
+"""
+
+ingest_service = """
+  ingest:
+    build:
+      context: .
+      dockerfile: docker/ingest/Dockerfile
+    environment:
+      - DB_HOST=${DB_HOST}
+      - DB_USER=food_loader
+      - DB_PASS=${DB_LOADER_PASS}
+    volumes:
+      - ./:/app
+    profiles:
+      - manual
+"""
+
+ai_services = """
+  ollama:
+    image: ollama/ollama:latest
+    volumes:
+      - ollama_data:/root/.ollama
+    restart: always
+    logging:
+      driver: "json-file"
+      options:
+        max-size: "50m"
+        max-file: "3"
+
+  searxng:
+    image: searxng/searxng:latest
+    ports:
+      - "8080:8080"
+    volumes:
+      - ./searxng:/etc/searxng
+    environment:
+      - SEARXNG_BASE_URL=http://localhost:8080/
+    restart: always
+    logging:
+      driver: "json-file"
+      options:
+        max-size: "50m"
+        max-file: "3"
+"""
+
+app_service = """
+  app:
+    build:
+      context: .
+      dockerfile: docker/app/Dockerfile
+    ports:
+      - "8502:8501"
+    environment:
+      - DB_HOST=${DB_HOST}
+      - DB_USER=food_reader
+      - DB_PASS=${DB_READER_PASS}
+      - APP_AUTH_USER=food_app_auth
+      - APP_AUTH_PASS=${DB_APP_AUTH_PASS}
+      - OLLAMA_HOST=http://ollama:11434
+      - SEARXNG_HOST=http://searxng:8080
+    restart: always
+    logging:
+      driver: "json-file"
+      options:
+        max-size: "50m"
+        max-file: "3"
+
+  nginx:
+    image: nginx:latest
+    ports:
+      - "80:80"
+    volumes:
+      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
+    restart: always
+    logging:
+      driver: "json-file"
+      options:
+        max-size: "50m"
+        max-file: "3"
+"""
+
+monitoring_services = """
+  zabbix-server:
+    image: zabbix/zabbix-server-mysql:ubuntu-7.0-latest
+    environment:
+      - DB_SERVER_HOST=${DB_HOST}
+      - MYSQL_USER=zabbix
+      - MYSQL_PASSWORD=${MYSQL_ZABBIX_PASSWORD}
+      - ZBX_SNMPTRAPPER=1
+    restart: always
+    logging:
+      driver: "json-file"
+      options:
+        max-size: "50m"
+        max-file: "3"
+    ports:
+      - "10051:10051"
+
+  zabbix-web:
+    image: zabbix/zabbix-web-nginx-mysql:ubuntu-7.0-latest
+    ports:
+      - "8081:8080"
+      - "8444:8443"
+    environment:
+      - DB_SERVER_HOST=${DB_HOST}
+      - MYSQL_USER=zabbix
+      - MYSQL_PASSWORD=${MYSQL_ZABBIX_PASSWORD}
+      - ZBX_SERVER_HOST=zabbix-server
+      - PHP_TZ=Europe/Paris
+    restart: always
+    logging:
+      driver: "json-file"
+      options:
+        max-size: "50m"
+        max-file: "3"
+
+  zabbix-agent:
+    image: zabbix/zabbix-agent:ubuntu-7.0-latest
+    environment:
+      - ZBX_HOSTNAME=DistributedNode
+      - ZBX_SERVER_HOST=${ZBX_SERVER_HOST}
+    privileged: true
+    pid: "host"
+    volumes:
+      - /var/run:/var/run
+    restart: always
+    logging:
+      driver: "json-file"
+      options:
+        max-size: "50m"
+        max-file: "3"
+"""
+airflow_services = """
+  airflow-webserver:
+    image: apache/airflow:2.8.1
+    environment:
+      - AIRFLOW__CORE__EXECUTOR=SequentialExecutor
+      - AIRFLOW__DATABASE__SQL_ALCHEMY_CONN=sqlite:////opt/airflow/airflow.db
+      - AIRFLOW__CORE__LOAD_EXAMPLES=False
+    ports:
+      - "8082:8080"
+    volumes:
+      - ./dags:/opt/airflow/dags
+      - ./logs:/opt/airflow/logs
+      - ./data:/opt/airflow/data
+      - /var/run/docker.sock:/var/run/docker.sock
+    command: webserver
+    restart: always
+    logging:
+      driver: "json-file"
+      options:
+        max-size: "50m"
+        max-file: "3"
+
+  airflow-scheduler:
+    image: apache/airflow:2.8.1
+    environment:
+      - AIRFLOW__CORE__EXECUTOR=SequentialExecutor
+      - AIRFLOW__DATABASE__SQL_ALCHEMY_CONN=sqlite:////opt/airflow/airflow.db
+      - AIRFLOW__CORE__LOAD_EXAMPLES=False
+    volumes:
+      - ./dags:/opt/airflow/dags
+      - ./logs:/opt/airflow/logs
+      - ./data:/opt/airflow/data
+      - /var/run/docker.sock:/var/run/docker.sock
+    command: bash -c "airflow db migrate && airflow users create --role Admin --username admin --email admin --firstname admin --lastname admin --password admin && airflow scheduler"
+    restart: always
+    logging:
+      driver: "json-file"
+      options:
+        max-size: "50m"
+        max-file: "3"
+"""
+
+header = "services:\n"
+footer = """
+volumes:
+  mysql_data:
+  ollama_data:
+"""
+
+compose_content = header
+
+if choice == "1":
+    compose_content += mysql_service + ingest_service + ai_services + app_service + monitoring_services + airflow_services
+elif choice == "2":
+    compose_content += ai_services + app_service
+    footer = "volumes:\n  ollama_data:\n"
+elif choice == "3":
+    compose_content += mysql_service + ingest_service + airflow_services
+    footer = "volumes:\n  mysql_data:\n"
+elif choice == "4":
+    compose_content += monitoring_services
+    footer = ""
+
+print("\n[+] Generating docker-compose.yml for selected role...")
+with open("docker-compose.yml", "w") as f:
+    f.write(compose_content + footer)
+
+print("\n" + "="*60)
+print("⚠️ IMPORTANT HYPERVISOR NETWORKING REMINDER:")
+print("If this node is running inside VirtualBox or Hyper-V, you MUST configure the VM network adapter to use a 'Bridged Adapter' or 'External Virtual Switch' so it shares the host's subnet. Otherwise, cross-node communication will fail.")
+print("="*60)
+
+print("\nDone! You can now run `docker compose up -d`.")