Răsfoiți Sursa

[TG-131] Purge database passwords from tracked files and format application versioning

Lange François 3 săptămâni în urmă
părinte
comite
45bdb66384
95 a modificat fișierele cu 2822 adăugiri și 2681 ștergeri
  1. 3 1
      .agents/workflows/pre_flight_check.md
  2. 12 10
      .agents/workflows/taiga-commit.md
  3. 2 1
      .dockerignore
  4. 2 1
      .gitattributes
  5. 1 0
      .gitignore
  6. 111 110
      Final_Presentation.html
  7. 8 6
      INSTALL_WSL.md
  8. 3 1
      PROJECT_CONTEXT.md
  9. 3 1
      README.md
  10. 2 1
      add_logging.py
  11. 3 2
      alembic.ini
  12. 1 0
      alembic/README
  13. 2 1
      alembic/env.py
  14. 2 1
      alembic/script.py.mako
  15. 2 1
      alembic/versions/701a919f4025_initial_schema.py
  16. 1247 1221
      app.py
  17. 2 1
      backup_db.sh
  18. 3 2
      check_users.py
  19. 2 1
      configure_zabbix_alerts.py
  20. 1 0
      configure_zabbix_email.py
  21. 3 2
      dags/openfoodfacts_ingestion.py
  22. 2 1
      data_sync.sh
  23. 2 1
      deploy.sh
  24. 2 1
      docker-compose-wsl.yml
  25. 192 191
      docker-compose.yml
  26. 112 111
      docker-compose_skip.yml
  27. 2 1
      docker/app/Dockerfile
  28. 2 1
      docker/ingest/Dockerfile
  29. 2 1
      docker/mysql/Dockerfile
  30. 54 53
      docker/zabbix/docker-compose.yml
  31. 80 78
      docs/Backup_Procedure.md
  32. 13 11
      docs/Data_Ingestion.md
  33. 21 19
      docs/Final_Report.md
  34. 20 18
      docs/Installation_Guide.md
  35. 186 184
      docs/Operator_Installation_Guide.md
  36. 5 3
      docs/Scrum_Artifacts.md
  37. 5 3
      docs/Scrum_Daily.md
  38. 5 3
      docs/Scrum_Plan.md
  39. 5 3
      docs/Scrum_Retro.md
  40. 5 3
      docs/Scrum_Review.md
  41. 37 35
      docs/Scrum_Wiki.md
  42. 92 90
      docs/Start_Stop_Procedures.md
  43. 6 4
      docs/Test_Cases_Sprint8.md
  44. 43 41
      docs/User_Description.md
  45. 13 11
      docs/User_Guide.md
  46. 7 5
      docs/WSL_Deployment.md
  47. 5 3
      docs/Wiki_Home.md
  48. 3 1
      docs/architecture.md
  49. 3 1
      docs/disaster_recovery_plan.md
  50. 3 1
      docs/distributed_deployment.md
  51. 2 2
      docs/docker_connection.md
  52. 84 82
      docs/project_report.md
  53. 3 1
      docs/retro_planning.md
  54. 3 1
      docs/taiga_audit_report.md
  55. 3 1
      docs/zabbix_monitoring.md
  56. 2 1
      download_csv.sh
  57. 2 1
      generate_docs.py
  58. 2 1
      git_id.txt
  59. 2 1
      git_version.txt
  60. 2 1
      ingest_csv.py
  61. 12 11
      init.sql
  62. 2 1
      k8s/alembic-migrate-job.yaml
  63. 2 1
      k8s/app-deployment.yaml
  64. 2 1
      k8s/app-service.yaml
  65. 2 1
      k8s/configmap.yaml
  66. 2 1
      k8s/ingest-job.yaml
  67. 2 1
      k8s/mysql-deployment.yaml
  68. 2 1
      k8s/namespace.yaml
  69. 2 1
      k8s/pvc.yaml
  70. 2 1
      k8s/secret.yaml.example
  71. 2 1
      k8s/taiga-sync-config.yaml
  72. 2 1
      k8s/taiga-sync-job.yaml
  73. 3 2
      manage_services.sh
  74. 2 1
      master_trigger.sh
  75. 2 1
      my.cnf
  76. 2 1
      myloginpath.py
  77. 2 1
      nginx/nginx.conf
  78. 2 1
      proper_reset.sh
  79. 1 0
      requirements.txt
  80. 12 11
      reset.sh
  81. 1 0
      rotate_passwords.py
  82. 2 1
      scratch/test_dr.sh
  83. 2 2
      scripts/create_delivery_zip.py
  84. 3 3
      scripts/deploy_to_server.py
  85. 2 1
      scripts/generate_pdfs.py
  86. 2 1
      scripts/generate_project_report.py
  87. 2 2
      scripts/manage_models.sh
  88. 291 290
      scripts/setup_deploy.py
  89. 2 1
      scripts/taiga_sync_final.py
  90. 2 2
      scripts/zip_project.py
  91. 1 1
      searxng/settings.yml
  92. 1 1
      snmp_notifier.py
  93. 2 1
      start_batch_ingest.sh
  94. 0 0
      taiga/local-food-ai-1-eab691c0-9c19-4dce-ac66-3b8fade77ef7.json
  95. 2 1
      unit_converter.py

+ 3 - 1
.agents/workflows/pre_flight_check.md

@@ -1,3 +1,5 @@
+The current version is #ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
+
 ---
 description: Hardware limits pre-flight check before builds
 ---
@@ -41,4 +43,4 @@ If **STATUS: FAIL** is determined (resources fall below safe operational thresho
 ### ✅ Success Protocol
 If **STATUS: PASS** is determined (resources are healthy):
 1. Note the current timestamp and a brief summary of the resource levels to the conversation log.
-2. Proceed safely with executing the planned build/install action.
+2. Proceed safely with executing the planned build/install action.

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

@@ -1,11 +1,13 @@
-# 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]"`
+The current version is #ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
+
+# 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`

+ 2 - 1
.dockerignore

@@ -1,5 +1,6 @@
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 *.csv
 *.tar
 venv/
 .git/
-__pycache__/
+__pycache__/

+ 2 - 1
.gitattributes

@@ -1,4 +1,5 @@
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 *.py ident export-subst
 *.sh ident export-subst
 *.sql ident export-subst
-*.md ident export-subst
+*.md ident export-subst

+ 1 - 0
.gitignore

@@ -1,3 +1,4 @@
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 venv/
 .venv/
 __pycache__/

+ 111 - 110
Final_Presentation.html

@@ -1,110 +1,111 @@
-
-<!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>
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
+
+<!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>

+ 8 - 6
INSTALL_WSL.md

@@ -1,3 +1,5 @@
+The current version is #ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
+
 # 🚀 WSL2 Port-Shifted Installation Guide (Local Food AI)
 
 This guide provides step-by-step instructions to install and run the **Local Food AI** system on Windows Subsystem for Linux (WSL2).
@@ -36,11 +38,11 @@ cd LocalFoodAI_lanfr144
 Create the required `.env` file at the root of the repository to feed standard local credentials to the containers:
 ```bash
 cat <<EOF > .env
-MYSQL_ROOT_PASSWORD=BTSai123
-DB_READER_PASS=BTSai123
-DB_LOADER_PASS=BTSai123
-DB_APP_AUTH_PASS=BTSai123
-MYSQL_ZABBIX_PASSWORD=BTSai123
+MYSQL_ROOT_PASSWORD=your_db_password_here
+DB_READER_PASS=your_db_password_here
+DB_LOADER_PASS=your_db_password_here
+DB_APP_AUTH_PASS=your_db_password_here
+MYSQL_ZABBIX_PASSWORD=your_db_password_here
 EOF
 ```
 
@@ -75,4 +77,4 @@ Once the stack is fully running, you can connect to all system components in you
 
 ---
 
-*Prepared by Francois Lange for the Local Food AI Delivery.*
+*Prepared by Francois Lange for the Local Food AI Delivery.*

+ 3 - 1
PROJECT_CONTEXT.md

@@ -1,3 +1,5 @@
+The current version is #ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
+
 # Project Context: Local Food AI
 
 ## 🎯 Vision Statement
@@ -39,4 +41,4 @@ The Ollama `mistral` model is fully integrated with Streamlit using **Tool Calli
 - **Historical Sprint Tracking**: A standalone historical Agile export file has been saved at [local-food-ai-1-5947063a-612b-454f-b3f1-6b5858445510.json](file:///c:/Users/lanfr144/Documents/DOPRO1/Antigravity/Food/taiga/local-food-ai-1-5947063a-612b-454f-b3f1-6b5858445510.json) inside the `taiga/` directory for historical reference and project tracking validation.
 
 ---
-*Generated by Antigravity.*
+*Generated by Antigravity.*

+ 3 - 1
README.md

@@ -1,3 +1,5 @@
+The current version is #ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
+
 # Local Food AI 🍔
 
 A strictly local, privacy-first AI Medical Dietitian and Food Explorer. This project leverages the OpenFoodFacts dataset and local LLMs (Ollama) to provide medically sound dietary advice, recipe parsing, and menu planning without sending any user data to the cloud.
@@ -36,4 +38,4 @@ This project leverages specialized AI skills to maintain code quality, documenta
 - **Git Commit**: Enforces strict Git governance, Taiga tracking (`TG-123`), and a single `main` branch workflow. For every commit, a task in Taiga must be associated. If the task does not exist, it must be created and added to a user story and a sprint.
 - **Refactor Coach**: Refactors code to improve readability, performance, and modularity without changing external behavior.
 - **SQL Optimizer**: Enforces DBA standards for MySQL, Oracle, and PostgreSQL, ensuring proper indexing, transaction management, and secure access.
-- **Test Generator**: Generates comprehensive unit and integration tests focusing on boundary conditions and logical coverage.
+- **Test Generator**: Generates comprehensive unit and integration tests focusing on boundary conditions and logical coverage.

+ 2 - 1
add_logging.py

@@ -1,3 +1,4 @@
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 import os
 
 files_to_update = ['scripts/setup_deploy.py', 'docker-compose.yml', 'docker/zabbix/docker-compose.yml']
@@ -17,4 +18,4 @@ for file_path in files_to_update:
     if 'logging:' not in content:
         content = content.replace('restart: always', log_config)
         with open(file_path, 'w', encoding='utf-8') as f:
-            f.write(content)
+            f.write(content)

+ 3 - 2
alembic.ini

@@ -1,3 +1,4 @@
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 # A generic, single database configuration.
 
 [alembic]
@@ -84,7 +85,7 @@ path_separator = os
 # database URL.  This is consumed by the user-maintained env.py script only.
 # other means of configuring database URLs may be customized within the env.py
 # file.
-sqlalchemy.url = mysql+pymysql://db_owner:BTSai123@192.168.130.170/food_db
+sqlalchemy.url = mysql+pymysql://db_owner:your_db_password_here@192.168.130.170/food_db
 
 
 [post_write_hooks]
@@ -144,4 +145,4 @@ formatter = generic
 
 [formatter_generic]
 format = %(levelname)-5.5s [%(name)s] %(message)s
-datefmt = %H:%M:%S
+datefmt = %H:%M:%S

+ 1 - 0
alembic/README

@@ -1 +1,2 @@
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 Generic single-database configuration.

+ 2 - 1
alembic/env.py

@@ -1,3 +1,4 @@
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 from logging.config import fileConfig
 
 from sqlalchemy import engine_from_config
@@ -86,4 +87,4 @@ def run_migrations_online() -> None:
 if context.is_offline_mode():
     run_migrations_offline()
 else:
-    run_migrations_online()
+    run_migrations_online()

+ 2 - 1
alembic/script.py.mako

@@ -1,3 +1,4 @@
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 """${message}
 
 Revision ID: ${up_revision}
@@ -25,4 +26,4 @@ def upgrade() -> None:
 
 def downgrade() -> None:
     """Downgrade schema."""
-    ${downgrades if downgrades else "pass"}
+    ${downgrades if downgrades else "pass"}

+ 2 - 1
alembic/versions/701a919f4025_initial_schema.py

@@ -1,3 +1,4 @@
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 """initial_schema
 
 Revision ID: 701a919f4025
@@ -65,4 +66,4 @@ def downgrade() -> None:
     op.drop_table('plate_items')
     op.drop_table('plates')
     op.drop_table('user_health_profiles')
-    op.drop_table('users')
+    op.drop_table('users')

+ 1247 - 1221
app.py

@@ -1,1221 +1,1247 @@
-# $Id$
-# $Author$
-# $log$
-#ident "@(#)LocalFoodAI:app.py:$Format:%D:%ci:%cN:%h$"
-#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
-import streamlit as st
-import extra_streamlit_components as stx
-import subprocess
-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 requests
-import smtplib
-from email.message import EmailMessage
-from typing import Optional, List, Dict, Any, Tuple
-import threading
-import os
-
-ACTIVE_MODEL = os.environ.get('LLM_MODEL', 'llama3.2-vision:11b')
-
-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 detect_allergens_from_text(name: str, ingredients: str) -> set:
-    import re
-    detected = set()
-    text = (name + " " + ingredients).lower()
-    mappings = {
-        "peanut": "Peanuts",
-        "cacahuète": "Peanuts",
-        "cacahuete": "Peanuts",
-        "egg": "Eggs",
-        "oeuf": "Eggs",
-        "œuf": "Eggs",
-        "milk": "Milk",
-        "lait": "Milk",
-        "butter": "Milk",
-        "beurre": "Milk",
-        "cheese": "Milk",
-        "fromage": "Milk",
-        "cream": "Milk",
-        "crème": "Milk",
-        "creme": "Milk",
-        "wheat": "Wheat",
-        "blé": "Wheat",
-        "ble": "Wheat",
-        "gluten": "Gluten",
-        "soy": "Soy",
-        "soja": "Soy",
-        "almond": "Tree Nuts",
-        "amande": "Tree Nuts",
-        "cashew": "Tree Nuts",
-        "walnut": "Tree Nuts",
-        "noix": "Tree Nuts",
-        "hazelnut": "Tree Nuts",
-        "noisette": "Tree Nuts",
-        "pecan": "Tree Nuts",
-        "pistachio": "Tree Nuts",
-        "pistache": "Tree Nuts",
-        "fish": "Fish",
-        "poisson": "Fish",
-        "salmon": "Fish",
-        "saumon": "Fish",
-        "tuna": "Fish",
-        "thon": "Fish",
-        "shrimp": "Shellfish",
-        "crevette": "Shellfish",
-        "crab": "Shellfish",
-        "crabe": "Shellfish",
-        "lobster": "Shellfish",
-        "homard": "Shellfish",
-        "mussel": "Shellfish",
-        "moule": "Shellfish",
-        "oyster": "Shellfish",
-        "huître": "Shellfish",
-        "huitre": "Shellfish",
-        "sesame": "Sesame",
-        "sésame": "Sesame",
-        "mustard": "Mustard",
-        "moutarde": "Mustard",
-        "celery": "Celery",
-        "céleri": "Celery",
-        "celeri": "Celery",
-        "lupin": "Lupin",
-        "mollusc": "Molluscs",
-        "mollusque": "Molluscs",
-        "sulphite": "Sulphites",
-        "sulfite": "Sulphites"
-    }
-    for keyword, allergen in mappings.items():
-        pattern = r'\b' + re.escape(keyword) + r's?\b'
-        if re.search(pattern, text):
-            detected.add(allergen)
-    return detected
-
-def filter_scratchpad_stream(stream, raw_accumulator=None):
-    buffer = ""
-    in_scratchpad = False
-    for chunk in stream:
-        content = chunk['message']['content']
-        if raw_accumulator is not None:
-            raw_accumulator.append(content)
-        buffer += content
-        
-        while True:
-            if not in_scratchpad:
-                start_idx = buffer.find("<scratchpad>")
-                if start_idx != -1:
-                    if start_idx > 0:
-                        yield buffer[:start_idx]
-                    yield "\n\n> 💭 **AI Thinking Process:**\n> "
-                    buffer = buffer[start_idx + 12:]
-                    in_scratchpad = True
-                else:
-                    yield_len = 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:
-                    scratch_content = buffer[:end_idx]
-                    scratch_content_formatted = scratch_content.replace("\n", "\n> ")
-                    yield scratch_content_formatted
-                    yield "\n\n"
-                    buffer = buffer[end_idx + 13:]
-                    in_scratchpad = False
-                else:
-                    yield_len = len(buffer) - 12
-                    if yield_len > 0:
-                        scratch_content = buffer[:yield_len]
-                        scratch_content_formatted = scratch_content.replace("\n", "\n> ")
-                        yield scratch_content_formatted
-                        buffer = buffer[yield_len:]
-                    break
-    if buffer:
-        if in_scratchpad:
-            yield buffer.replace("\n", "\n> ")
-        else:
-            yield buffer
-
-def pull_model_bg():
-    try: ollama.pull(ACTIVE_MODEL)
-    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:
-                pro = float(r['proteins_100g'] or 0)
-                fat = float(r['fat_100g'] or 0)
-                carb = float(r['carbohydrates_100g'] or 0)
-                sug = float(r['sugars_100g'] or 0)
-                snippets.append(f"- {r['product_name']}: Protein {pro:.2f}g, Fat {fat:.2f}g, Carbs {carb:.2f}g, Sugars {sug:.2f}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("---")
-    try:
-        if os.path.exists('git_version.txt'):
-            with open('git_version.txt', 'r') as f: git_version = f.read().strip()
-        else:
-            git_version = subprocess.check_output(['git', 'describe', '--tags']).decode('utf-8').strip()
-    except Exception:
-        git_version = "v1.3.0"
-    st.caption(f"🚀 Version: {git_version}")
-    
-    try:
-        if os.path.exists('git_id.txt'):
-            with open('git_id.txt', 'r') as f: git_id = f.read().strip()
-        else:
-            git_id = subprocess.check_output(['git', 'log', '-1', '--format=%cd %h', 'app.py']).decode('utf-8').strip()
-    except Exception:
-        git_id = "Unknown"
-    st.caption(f"📅 Git ID: {git_id}")
-
-st.set_page_config(page_title="Food AI Explorer", page_icon="🍔", layout="wide")
-
-cookie_manager = stx.CookieManager(key="cookie_manager")
-
-# Wait for cookies to load
-cookies = cookie_manager.get_all()
-if cookies is None:
-    st.stop()
-
-# If the cookie has auth_user, set/restore session state
-cookie_user = cookie_manager.get(cookie="auth_user")
-if cookie_user:
-    st.session_state["authenticated_user"] = cookie_user
-elif "authenticated_user" not in st.session_state:
-    st.session_state["authenticated_user"] = None
-
-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; caret-color: #f8fafc !important; }
-</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
-            cookie_manager.delete("auth_user")
-            time.sleep(0.5)
-            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
-                    import datetime
-                    # Set cookie with 30 days expiration
-                    cookie_manager.set(
-                        "auth_user",
-                        l_user,
-                        expires_at=datetime.datetime.now() + datetime.timedelta(days=30)
-                    )
-                    time.sleep(0.2)
-                    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"]
-            start_llm = time.time()
-            response_stream = ollama.chat(model=ACTIVE_MODEL, messages=temp_messages, stream=True)
-            
-            with st.chat_message("assistant"):
-                ai_reply = st.write_stream(chunk['message']['content'] for chunk in response_stream)
-                st.caption(f"⏱️ AI response generated in {time.time() - start_llm:.2f} seconds")
-            
-            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)*
-    """)
-    with st.form("explore_search_form"):
-        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)
-        
-        submit_search = st.form_submit_button("Search Database")
-        if submit_search:
-            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) OR product_name LIKE %s)
-                            AND product_name IS NOT NULL AND product_name != '' AND product_name != 'None'
-                            ORDER BY MATCH(product_name) AGAINST(%s IN BOOLEAN MODE) DESC, MATCH(ingredients_text) AGAINST(%s IN BOOLEAN MODE) DESC
-                            {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()])
-                    sq_like = f"%{sq}%"
-                    start_time = time.time()
-                    cursor.execute(query, (sq_bool, sq_like, sq_bool, 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)
-                        df.replace(r'^\s*$', None, regex=True, inplace=True)
-                        for col in df.columns:
-                            if col.endswith('_100g'):
-                                df[col] = pd.to_numeric(df[col], errors='coerce')
-                        
-                        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)
-                        # Only fillna with empty string on object columns to avoid Arrow float64 conversion errors
-                        for col in df_display.columns:
-                            if df_display[col].dtype == 'object':
-                                df_display[col] = df_display[col].fillna("")
-                        df_display.index = range(1, len(df_display) + 1)
-                        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, hide_index=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 = ollama.chat(model=ACTIVE_MODEL, messages=[{'role': 'user', 'content': eval_prompt}], stream=True)
-                                    st.write_stream(chunk['message']['content'] for chunk in response)
-                                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. `add White Rice use 150g then add Chicken Breast use 50g add Green Beans use 100g`
-    2. `add Potatoes use 200g then add Tomatoes use 100g add Beef use 100g`
-    3. `add Spinach Salad use 100g then add Feta Cheese use 50g`
-    4. `add Lentils use 200g then add Quinoa use 100g`
-    5. `add Apple use 100g then add Almonds use 30g`
-    """)
-    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()
-            
-            st.markdown("#### ➕ Create a New Plate")
-            col_p1, col_p2 = st.columns([3, 1])
-            new_plate_name = col_p1.text_input("Plate Name (e.g., 'Spaghetti Bolognese')", key="new_plate")
-            if col_p2.button("Create Plate", use_container_width=True) and new_plate_name:
-                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()
-            
-            st.markdown("---")
-
-            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 to Edit Ingredients", 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(p.ingredients_text) as ingredients_text,
-                           MAX(m.proteins_100g) as proteins_100g, MAX(m.fat_100g) as fat_100g, MAX(m.carbohydrates_100g) as carbohydrates_100g, 
-                           MAX(m.sodium_100g) as sodium_100g, MAX(m.sugars_100g) as sugars_100g, MAX(m.fiber_100g) as fiber_100g,
-                           MAX(v.`vitamin-a_100g`) as vitamin_a_100g, MAX(v.`vitamin-b1_100g`) as vitamin_b1_100g, 
-                           MAX(v.`vitamin-b2_100g`) as vitamin_b2_100g, MAX(v.`vitamin-pp_100g`) as vitamin_pp_100g, 
-                           MAX(v.`vitamin-b6_100g`) as vitamin_b6_100g, MAX(v.`vitamin-b9_100g`) as vitamin_b9_100g, 
-                           MAX(v.`vitamin-b12_100g`) as vitamin_b12_100g, MAX(v.`vitamin-c_100g`) as vitamin_c_100g, 
-                           MAX(v.`vitamin-d_100g`) as vitamin_d_100g, MAX(v.`vitamin-e_100g`) as vitamin_e_100g, 
-                           MAX(v.`vitamin-k_100g`) as vitamin_k_100g,
-                           MAX(min.calcium_100g) as calcium_100g, MAX(min.iron_100g) as iron_100g, 
-                           MAX(min.magnesium_100g) as magnesium_100g, MAX(min.potassium_100g) as potassium_100g, 
-                           MAX(min.zinc_100g) as zinc_100g,
-                           GROUP_CONCAT(DISTINCT a.allergens SEPARATOR ', ') as allergens
-                    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 
-                    LEFT JOIN products_vitamins v ON i.product_code = v.code
-                    LEFT JOIN products_minerals min ON i.product_code = min.code
-                    LEFT JOIN products_allergens a ON i.product_code = a.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])
-                        pro = float(i['proteins_100g'] or 0) * (float(i['quantity_grams'])/100.0)
-                        fat = float(i['fat_100g'] or 0) * (float(i['quantity_grams'])/100.0)
-                        carb = float(i['carbohydrates_100g'] or 0) * (float(i['quantity_grams'])/100.0)
-                        c1.markdown(f"<li><b>{i['quantity_grams']}g</b> of {i['product_name']} (Pro: {pro:.2f}g | Fat: {fat:.2f}g | Carb: {carb:.2f}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()
-                            
-                    totals = {
-                        "Total Protein (g)": sum((float(i['proteins_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items),
-                        "Total Fat (g)": sum((float(i['fat_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items),
-                        "Total Carbs (g)": sum((float(i['carbohydrates_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items),
-                        "Sodium (g)": sum((float(i['sodium_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items),
-                        "Sugars (g)": sum((float(i['sugars_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items),
-                        "Fiber (g)": sum((float(i['fiber_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items),
-                        "Vitamin A (g)": sum((float(i['vitamin_a_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items),
-                        "Vitamin B1 (g)": sum((float(i['vitamin_b1_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items),
-                        "Vitamin B2 (g)": sum((float(i['vitamin_b2_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items),
-                        "Vitamin B3/PP (g)": sum((float(i['vitamin_pp_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items),
-                        "Vitamin B6 (g)": sum((float(i['vitamin_b6_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items),
-                        "Vitamin B9 (g)": sum((float(i['vitamin_b9_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items),
-                        "Vitamin B12 (g)": sum((float(i['vitamin_b12_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items),
-                        "Vitamin C (g)": sum((float(i['vitamin_c_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items),
-                        "Vitamin D (g)": sum((float(i['vitamin_d_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items),
-                        "Vitamin E (g)": sum((float(i['vitamin_e_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items),
-                        "Vitamin K (g)": sum((float(i['vitamin_k_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items),
-                        "Calcium (g)": sum((float(i['calcium_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items),
-                        "Iron (g)": sum((float(i['iron_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items),
-                        "Magnesium (g)": sum((float(i['magnesium_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items),
-                        "Potassium (g)": sum((float(i['potassium_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items),
-                        "Zinc (g)": sum((float(i['zinc_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items),
-                    }
-                    
-                    st.markdown("---")
-                    st.markdown("### Plate Totals")
-                    metrics = list(totals.items())
-                    for idx in range(0, len(metrics), 3):
-                        cols = st.columns(3)
-                        for j in range(3):
-                            if idx + j < len(metrics):
-                                name, val = metrics[idx + j]
-                                cols[j].metric(name, f"{val:.5f}" if val < 0.1 else f"{val:.2f}")
-
-                    all_allergens = set()
-                    for i in items:
-                        # 1. Parse database allergens if available
-                        if i.get('allergens'):
-                            for alg in str(i['allergens']).split(','):
-                                alg_clean = alg.strip().lower()
-                                if alg_clean.startswith('en:'):
-                                    alg_clean = alg_clean[3:]
-                                if alg_clean and alg_clean != 'none':
-                                    all_allergens.add(alg_clean.replace('-', ' ').title())
-                        
-                        # 2. Text heuristics fallback for common allergens
-                        prod_name = str(i.get('product_name') or '')
-                        ing_text = str(i.get('ingredients_text') or '')
-                        heuristics = detect_allergens_from_text(prod_name, ing_text)
-                        all_allergens.update(heuristics)
-                        
-                    st.markdown("---")
-                    if all_allergens:
-                        st.warning(f"⚠️ **Plate Allergens Detected:** {', '.join(all_allergens)}")
-                    else:
-                        st.success("✅ **No Allergens Detected**")
-                
-                st.markdown("---")
-                st.markdown("#### ➕ Add Food to Plate")
-                with st.form("plate_add_form"):
-                    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"])
-                    
-                    submit_add_search = st.form_submit_button("Search Food")
-                
-                if add_search and submit_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")
-                    st.session_state['plate_search_res'] = search_res
-
-                if st.session_state.get('plate_search_res'):
-                    search_res = st.session_state['plate_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.session_state.pop('plate_search_res', None)
-                            st.rerun()
-                        else:
-                            st.error("Could not parse unit. Please use format like '100g' or '1 cup'.")
-                elif add_search and submit_add_search:
-                    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 EXACTLY {meal_count} meals and NO MORE. Failure to respect the meal count is a critical clinical error.
-            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, Carbs = C, Fat = D.
-              - 2 tbsp of Peanut Butter = Z grams (density C). Calories = D, Protein = E, Carbs = F, Fat = G.
-              - 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 | Carbs | Fat |
-              | --- | --- | --- | --- | --- | --- | --- |
-              ...
-              | Global Total | All Meals | | Total Calories | Total Protein | Total Carbs | Total Fat |
-            
-            CRITICAL FORMATTING INSTRUCTIONS:
-            - After the </scratchpad> closing tag, you MUST strictly output the menu formatted as a Markdown Table.
-            - The table MUST contain exactly 7 columns separated by pipes (|): | Meal Time | Exact Food | Portion Size | Calories | Protein | Carbs | Fat |
-            - The Portion Size MUST be reported in exactly metric grams (e.g. 200g) and NEVER in cups or oz.
-            - The items in the table MUST be selected strictly from: {db_context}
-            - Do NOT output JSON. Do NOT use tool calls. Skip pleasantries.
-            """
-            
-            st.info("🧠 AI is analyzing nutritional synergies and generating your plan...")
-            
-            # Stream the response instantly!
-            try:
-                start_llm = time.time()
-                response = ollama.chat(model=ACTIVE_MODEL, messages=[
-                    {'role': 'system', 'content': sys_prompt},
-                    {'role': 'user', 'content': 'Generate my meal plan as a markdown table.'}
-                ], stream=True)
-                raw_chunks = []
-                clean_stream = filter_scratchpad_stream(response, raw_chunks)
-                ai_reply = st.write_stream(clean_stream)
-                raw_reply = "".join(raw_chunks)
-                st.caption(f"⏱️ AI Meal Plan generated in {time.time() - start_llm:.2f} seconds")
-                
-                # 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 (20, 30, 15, 10, 10, 10, 10) if len(table_data[0]) >= 7 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('|') or ('|' in line and 'Total' in line):
-                            if not line.endswith('|'): line += ' |'
-                            if not line.startswith('|'): line = '| ' + line
-                            
-                            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(raw_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()
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
+# $Id$
+# $Author$
+# $log$
+#ident "@(#)LocalFoodAI:app.py:$Format:%D:%ci:%cN:%h$"
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
+import streamlit as st
+import extra_streamlit_components as stx
+import subprocess
+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 requests
+import smtplib
+from email.message import EmailMessage
+from typing import Optional, List, Dict, Any, Tuple
+import threading
+import os
+
+ACTIVE_MODEL = os.environ.get('LLM_MODEL', 'llama3.2-vision:11b')
+
+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 detect_allergens_from_text(name: str, ingredients: str) -> set:
+    import re
+    detected = set()
+    text = (name + " " + ingredients).lower()
+    mappings = {
+        "peanut": "Peanuts",
+        "cacahuète": "Peanuts",
+        "cacahuete": "Peanuts",
+        "egg": "Eggs",
+        "oeuf": "Eggs",
+        "œuf": "Eggs",
+        "milk": "Milk",
+        "lait": "Milk",
+        "butter": "Milk",
+        "beurre": "Milk",
+        "cheese": "Milk",
+        "fromage": "Milk",
+        "cream": "Milk",
+        "crème": "Milk",
+        "creme": "Milk",
+        "wheat": "Wheat",
+        "blé": "Wheat",
+        "ble": "Wheat",
+        "gluten": "Gluten",
+        "soy": "Soy",
+        "soja": "Soy",
+        "almond": "Tree Nuts",
+        "amande": "Tree Nuts",
+        "cashew": "Tree Nuts",
+        "walnut": "Tree Nuts",
+        "noix": "Tree Nuts",
+        "hazelnut": "Tree Nuts",
+        "noisette": "Tree Nuts",
+        "pecan": "Tree Nuts",
+        "pistachio": "Tree Nuts",
+        "pistache": "Tree Nuts",
+        "fish": "Fish",
+        "poisson": "Fish",
+        "salmon": "Fish",
+        "saumon": "Fish",
+        "tuna": "Fish",
+        "thon": "Fish",
+        "shrimp": "Shellfish",
+        "crevette": "Shellfish",
+        "crab": "Shellfish",
+        "crabe": "Shellfish",
+        "lobster": "Shellfish",
+        "homard": "Shellfish",
+        "mussel": "Shellfish",
+        "moule": "Shellfish",
+        "oyster": "Shellfish",
+        "huître": "Shellfish",
+        "huitre": "Shellfish",
+        "sesame": "Sesame",
+        "sésame": "Sesame",
+        "mustard": "Mustard",
+        "moutarde": "Mustard",
+        "celery": "Celery",
+        "céleri": "Celery",
+        "celeri": "Celery",
+        "lupin": "Lupin",
+        "mollusc": "Molluscs",
+        "mollusque": "Molluscs",
+        "sulphite": "Sulphites",
+        "sulfite": "Sulphites"
+    }
+    for keyword, allergen in mappings.items():
+        pattern = r'\b' + re.escape(keyword) + r's?\b'
+        if re.search(pattern, text):
+            detected.add(allergen)
+    return detected
+
+def filter_scratchpad_stream(stream, raw_accumulator=None):
+    buffer = ""
+    in_scratchpad = False
+    for chunk in stream:
+        content = chunk['message']['content']
+        if raw_accumulator is not None:
+            raw_accumulator.append(content)
+        buffer += content
+        
+        while True:
+            if not in_scratchpad:
+                start_idx = buffer.find("<scratchpad>")
+                if start_idx != -1:
+                    if start_idx > 0:
+                        yield buffer[:start_idx]
+                    yield "\n\n> 💭 **AI Thinking Process:**\n> "
+                    buffer = buffer[start_idx + 12:]
+                    in_scratchpad = True
+                else:
+                    yield_len = 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:
+                    scratch_content = buffer[:end_idx]
+                    scratch_content_formatted = scratch_content.replace("\n", "\n> ")
+                    yield scratch_content_formatted
+                    yield "\n\n"
+                    buffer = buffer[end_idx + 13:]
+                    in_scratchpad = False
+                else:
+                    yield_len = len(buffer) - 12
+                    if yield_len > 0:
+                        scratch_content = buffer[:yield_len]
+                        scratch_content_formatted = scratch_content.replace("\n", "\n> ")
+                        yield scratch_content_formatted
+                        buffer = buffer[yield_len:]
+                    break
+    if buffer:
+        if in_scratchpad:
+            yield buffer.replace("\n", "\n> ")
+        else:
+            yield buffer
+
+def pull_model_bg():
+    try: ollama.pull(ACTIVE_MODEL)
+    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:
+                pro = float(r['proteins_100g'] or 0)
+                fat = float(r['fat_100g'] or 0)
+                carb = float(r['carbohydrates_100g'] or 0)
+                sug = float(r['sugars_100g'] or 0)
+                snippets.append(f"- {r['product_name']}: Protein {pro:.2f}g, Fat {fat:.2f}g, Carbs {carb:.2f}g, Sugars {sug:.2f}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 reformat_git_date(date_str):
+    from datetime import datetime
+    try:
+        import email.utils
+        dt = email.utils.parsedate_to_datetime(date_str)
+        return dt.strftime("%Y/%m/%d %H:%M:%S")
+    except Exception:
+        try:
+            dt = datetime.strptime(date_str.strip(), "%a %b %d %H:%M:%S %Y %z")
+            return dt.strftime("%Y/%m/%d %H:%M:%S")
+        except Exception:
+            return date_str
+
+def render_version():
+    st.markdown("---")
+    try:
+        if os.path.exists('git_version.txt'):
+            with open('git_version.txt', 'r') as f: git_version = f.read().strip()
+        else:
+            git_version = subprocess.check_output(['git', 'describe', '--tags']).decode('utf-8').strip()
+    except Exception:
+        git_version = "v1.3.0"
+        
+    formatted_version = reformat_git_date(git_version)
+    st.caption(f"🚀 Version: {formatted_version}")
+    
+    try:
+        if os.path.exists('git_id.txt'):
+            with open('git_id.txt', 'r') as f: git_id = f.read().strip()
+        else:
+            git_id = subprocess.check_output(['git', 'log', '-1', '--format=%cd %h', 'app.py']).decode('utf-8').strip()
+    except Exception:
+        git_id = "Unknown"
+        
+    parts = git_id.strip().split()
+    if len(parts) >= 6:
+        date_part = " ".join(parts[:6])
+        hash_part = parts[6] if len(parts) > 6 else ""
+        formatted_date = reformat_git_date(date_part)
+        formatted_id = f"{formatted_date} {hash_part}".strip()
+    else:
+        formatted_id = git_id
+        
+    st.caption(f"📅 Git ID: {formatted_id}")
+
+st.set_page_config(page_title="Food AI Explorer", page_icon="🍔", layout="wide")
+
+cookie_manager = stx.CookieManager(key="cookie_manager")
+
+# Wait for cookies to load
+cookies = cookie_manager.get_all()
+if cookies is None:
+    st.stop()
+
+# If the cookie has auth_user, set/restore session state
+cookie_user = cookie_manager.get(cookie="auth_user")
+if cookie_user:
+    st.session_state["authenticated_user"] = cookie_user
+elif "authenticated_user" not in st.session_state:
+    st.session_state["authenticated_user"] = None
+
+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; caret-color: #f8fafc !important; }
+</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
+            cookie_manager.delete("auth_user")
+            time.sleep(0.5)
+            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
+                    import datetime
+                    # Set cookie with 30 days expiration
+                    cookie_manager.set(
+                        "auth_user",
+                        l_user,
+                        expires_at=datetime.datetime.now() + datetime.timedelta(days=30)
+                    )
+                    time.sleep(0.2)
+                    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"]
+            start_llm = time.time()
+            response_stream = ollama.chat(model=ACTIVE_MODEL, messages=temp_messages, stream=True)
+            
+            with st.chat_message("assistant"):
+                ai_reply = st.write_stream(chunk['message']['content'] for chunk in response_stream)
+                st.caption(f"⏱️ AI response generated in {time.time() - start_llm:.2f} seconds")
+            
+            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)*
+    """)
+    with st.form("explore_search_form"):
+        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)
+        
+        submit_search = st.form_submit_button("Search Database")
+        if submit_search:
+            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) OR product_name LIKE %s)
+                            AND product_name IS NOT NULL AND product_name != '' AND product_name != 'None'
+                            ORDER BY MATCH(product_name) AGAINST(%s IN BOOLEAN MODE) DESC, MATCH(ingredients_text) AGAINST(%s IN BOOLEAN MODE) DESC
+                            {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()])
+                    sq_like = f"%{sq}%"
+                    start_time = time.time()
+                    cursor.execute(query, (sq_bool, sq_like, sq_bool, 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)
+                        df.replace(r'^\s*$', None, regex=True, inplace=True)
+                        for col in df.columns:
+                            if col.endswith('_100g'):
+                                df[col] = pd.to_numeric(df[col], errors='coerce')
+                        
+                        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)
+                        # Only fillna with empty string on object columns to avoid Arrow float64 conversion errors
+                        for col in df_display.columns:
+                            if df_display[col].dtype == 'object':
+                                df_display[col] = df_display[col].fillna("")
+                        df_display.index = range(1, len(df_display) + 1)
+                        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, hide_index=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 = ollama.chat(model=ACTIVE_MODEL, messages=[{'role': 'user', 'content': eval_prompt}], stream=True)
+                                    st.write_stream(chunk['message']['content'] for chunk in response)
+                                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. `add White Rice use 150g then add Chicken Breast use 50g add Green Beans use 100g`
+    2. `add Potatoes use 200g then add Tomatoes use 100g add Beef use 100g`
+    3. `add Spinach Salad use 100g then add Feta Cheese use 50g`
+    4. `add Lentils use 200g then add Quinoa use 100g`
+    5. `add Apple use 100g then add Almonds use 30g`
+    """)
+    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()
+            
+            st.markdown("#### ➕ Create a New Plate")
+            col_p1, col_p2 = st.columns([3, 1])
+            new_plate_name = col_p1.text_input("Plate Name (e.g., 'Spaghetti Bolognese')", key="new_plate")
+            if col_p2.button("Create Plate", use_container_width=True) and new_plate_name:
+                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()
+            
+            st.markdown("---")
+
+            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 to Edit Ingredients", 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(p.ingredients_text) as ingredients_text,
+                           MAX(m.proteins_100g) as proteins_100g, MAX(m.fat_100g) as fat_100g, MAX(m.carbohydrates_100g) as carbohydrates_100g, 
+                           MAX(m.sodium_100g) as sodium_100g, MAX(m.sugars_100g) as sugars_100g, MAX(m.fiber_100g) as fiber_100g,
+                           MAX(v.`vitamin-a_100g`) as vitamin_a_100g, MAX(v.`vitamin-b1_100g`) as vitamin_b1_100g, 
+                           MAX(v.`vitamin-b2_100g`) as vitamin_b2_100g, MAX(v.`vitamin-pp_100g`) as vitamin_pp_100g, 
+                           MAX(v.`vitamin-b6_100g`) as vitamin_b6_100g, MAX(v.`vitamin-b9_100g`) as vitamin_b9_100g, 
+                           MAX(v.`vitamin-b12_100g`) as vitamin_b12_100g, MAX(v.`vitamin-c_100g`) as vitamin_c_100g, 
+                           MAX(v.`vitamin-d_100g`) as vitamin_d_100g, MAX(v.`vitamin-e_100g`) as vitamin_e_100g, 
+                           MAX(v.`vitamin-k_100g`) as vitamin_k_100g,
+                           MAX(min.calcium_100g) as calcium_100g, MAX(min.iron_100g) as iron_100g, 
+                           MAX(min.magnesium_100g) as magnesium_100g, MAX(min.potassium_100g) as potassium_100g, 
+                           MAX(min.zinc_100g) as zinc_100g,
+                           GROUP_CONCAT(DISTINCT a.allergens SEPARATOR ', ') as allergens
+                    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 
+                    LEFT JOIN products_vitamins v ON i.product_code = v.code
+                    LEFT JOIN products_minerals min ON i.product_code = min.code
+                    LEFT JOIN products_allergens a ON i.product_code = a.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])
+                        pro = float(i['proteins_100g'] or 0) * (float(i['quantity_grams'])/100.0)
+                        fat = float(i['fat_100g'] or 0) * (float(i['quantity_grams'])/100.0)
+                        carb = float(i['carbohydrates_100g'] or 0) * (float(i['quantity_grams'])/100.0)
+                        c1.markdown(f"<li><b>{i['quantity_grams']}g</b> of {i['product_name']} (Pro: {pro:.2f}g | Fat: {fat:.2f}g | Carb: {carb:.2f}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()
+                            
+                    totals = {
+                        "Total Protein (g)": sum((float(i['proteins_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items),
+                        "Total Fat (g)": sum((float(i['fat_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items),
+                        "Total Carbs (g)": sum((float(i['carbohydrates_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items),
+                        "Sodium (g)": sum((float(i['sodium_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items),
+                        "Sugars (g)": sum((float(i['sugars_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items),
+                        "Fiber (g)": sum((float(i['fiber_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items),
+                        "Vitamin A (g)": sum((float(i['vitamin_a_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items),
+                        "Vitamin B1 (g)": sum((float(i['vitamin_b1_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items),
+                        "Vitamin B2 (g)": sum((float(i['vitamin_b2_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items),
+                        "Vitamin B3/PP (g)": sum((float(i['vitamin_pp_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items),
+                        "Vitamin B6 (g)": sum((float(i['vitamin_b6_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items),
+                        "Vitamin B9 (g)": sum((float(i['vitamin_b9_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items),
+                        "Vitamin B12 (g)": sum((float(i['vitamin_b12_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items),
+                        "Vitamin C (g)": sum((float(i['vitamin_c_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items),
+                        "Vitamin D (g)": sum((float(i['vitamin_d_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items),
+                        "Vitamin E (g)": sum((float(i['vitamin_e_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items),
+                        "Vitamin K (g)": sum((float(i['vitamin_k_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items),
+                        "Calcium (g)": sum((float(i['calcium_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items),
+                        "Iron (g)": sum((float(i['iron_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items),
+                        "Magnesium (g)": sum((float(i['magnesium_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items),
+                        "Potassium (g)": sum((float(i['potassium_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items),
+                        "Zinc (g)": sum((float(i['zinc_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items),
+                    }
+                    
+                    st.markdown("---")
+                    st.markdown("### Plate Totals")
+                    metrics = list(totals.items())
+                    for idx in range(0, len(metrics), 3):
+                        cols = st.columns(3)
+                        for j in range(3):
+                            if idx + j < len(metrics):
+                                name, val = metrics[idx + j]
+                                cols[j].metric(name, f"{val:.5f}" if val < 0.1 else f"{val:.2f}")
+
+                    all_allergens = set()
+                    for i in items:
+                        # 1. Parse database allergens if available
+                        if i.get('allergens'):
+                            for alg in str(i['allergens']).split(','):
+                                alg_clean = alg.strip().lower()
+                                if alg_clean.startswith('en:'):
+                                    alg_clean = alg_clean[3:]
+                                if alg_clean and alg_clean != 'none':
+                                    all_allergens.add(alg_clean.replace('-', ' ').title())
+                        
+                        # 2. Text heuristics fallback for common allergens
+                        prod_name = str(i.get('product_name') or '')
+                        ing_text = str(i.get('ingredients_text') or '')
+                        heuristics = detect_allergens_from_text(prod_name, ing_text)
+                        all_allergens.update(heuristics)
+                        
+                    st.markdown("---")
+                    if all_allergens:
+                        st.warning(f"⚠️ **Plate Allergens Detected:** {', '.join(all_allergens)}")
+                    else:
+                        st.success("✅ **No Allergens Detected**")
+                
+                st.markdown("---")
+                st.markdown("#### ➕ Add Food to Plate")
+                with st.form("plate_add_form"):
+                    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"])
+                    
+                    submit_add_search = st.form_submit_button("Search Food")
+                
+                if add_search and submit_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")
+                    st.session_state['plate_search_res'] = search_res
+
+                if st.session_state.get('plate_search_res'):
+                    search_res = st.session_state['plate_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.session_state.pop('plate_search_res', None)
+                            st.rerun()
+                        else:
+                            st.error("Could not parse unit. Please use format like '100g' or '1 cup'.")
+                elif add_search and submit_add_search:
+                    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 EXACTLY {meal_count} meals and NO MORE. Failure to respect the meal count is a critical clinical error.
+            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, Carbs = C, Fat = D.
+              - 2 tbsp of Peanut Butter = Z grams (density C). Calories = D, Protein = E, Carbs = F, Fat = G.
+              - 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 | Carbs | Fat |
+              | --- | --- | --- | --- | --- | --- | --- |
+              ...
+              | Global Total | All Meals | | Total Calories | Total Protein | Total Carbs | Total Fat |
+            
+            CRITICAL FORMATTING INSTRUCTIONS:
+            - After the </scratchpad> closing tag, you MUST strictly output the menu formatted as a Markdown Table.
+            - The table MUST contain exactly 7 columns separated by pipes (|): | Meal Time | Exact Food | Portion Size | Calories | Protein | Carbs | Fat |
+            - The Portion Size MUST be reported in exactly metric grams (e.g. 200g) and NEVER in cups or oz.
+            - The items in the table MUST be selected strictly from: {db_context}
+            - Do NOT output JSON. Do NOT use tool calls. Skip pleasantries.
+            """
+            
+            st.info("🧠 AI is analyzing nutritional synergies and generating your plan...")
+            
+            # Stream the response instantly!
+            try:
+                start_llm = time.time()
+                response = ollama.chat(model=ACTIVE_MODEL, messages=[
+                    {'role': 'system', 'content': sys_prompt},
+                    {'role': 'user', 'content': 'Generate my meal plan as a markdown table.'}
+                ], stream=True)
+                raw_chunks = []
+                clean_stream = filter_scratchpad_stream(response, raw_chunks)
+                ai_reply = st.write_stream(clean_stream)
+                raw_reply = "".join(raw_chunks)
+                st.caption(f"⏱️ AI Meal Plan generated in {time.time() - start_llm:.2f} seconds")
+                
+                # 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 (20, 30, 15, 10, 10, 10, 10) if len(table_data[0]) >= 7 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('|') or ('|' in line and 'Total' in line):
+                            if not line.endswith('|'): line += ' |'
+                            if not line.startswith('|'): line = '| ' + line
+                            
+                            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(raw_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()

+ 2 - 1
backup_db.sh

@@ -1,4 +1,5 @@
 #!/bin/bash
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 # $Id$
 # $Author$
 # $log$
@@ -47,4 +48,4 @@ fi
 echo "Applying retention policy: keeping backups for $RETENTION_DAYS days..."
 find "$BACKUP_DIR" -name "food_db_*.sql.gz" -type f -mtime +$RETENTION_DAYS -exec rm {} \;
 
-echo "Backup process completed."
+echo "Backup process completed."

+ 3 - 2
check_users.py

@@ -1,3 +1,4 @@
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 import myloginpath
 import pymysql
 import bcrypt
@@ -23,6 +24,6 @@ with conn.cursor() as c:
     c.execute("SELECT * FROM users WHERE username='Admin'")
     admin = c.fetchone()
     if admin:
-        print("Admin check BTSai123:", bcrypt.checkpw(b'BTSai123', admin['password_hash'].encode('utf-8')))
+        print("Admin check your_db_password_here:", bcrypt.checkpw(b'your_db_password_here', admin['password_hash'].encode('utf-8')))
 
-conn.close()
+conn.close()

+ 2 - 1
configure_zabbix_alerts.py

@@ -1,3 +1,4 @@
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 import json
 #ident "@(#)$Format:LocalFoodAI:configure_zabbix_alerts.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 import urllib.request
@@ -187,4 +188,4 @@ def main():
     print("Zabbix Configuration Complete.")
 
 if __name__ == '__main__':
-    main()
+    main()

+ 1 - 0
configure_zabbix_email.py

@@ -0,0 +1 @@
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"

+ 3 - 2
dags/openfoodfacts_ingestion.py

@@ -1,3 +1,4 @@
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 from airflow import DAG
 from airflow.operators.python import PythonOperator
 from airflow.providers.docker.operators.docker import DockerOperator
@@ -101,7 +102,7 @@ t2_ingest = DockerOperator(
     environment={
         'DB_HOST': 'mysql',
         'DB_USER': 'food_loader',
-        'DB_PASS': 'BTSai123'
+        'DB_PASS': 'your_db_password_here'
     },
     mount_tmp_dir=False,
     dag=dag,
@@ -123,4 +124,4 @@ t3_save_checksum = PythonOperator(
     dag=dag,
 )
 
-t1_validate >> t2_ingest >> t3_save_checksum
+t1_validate >> t2_ingest >> t3_save_checksum

+ 2 - 1
data_sync.sh

@@ -1,4 +1,5 @@
 #!/bin/bash
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 # data_sync.sh - Automated Data Freshness Pipeline
 
 LOG_DIR="./logs"
@@ -118,4 +119,4 @@ if [ -f "$DATA_DIR/$INGEST_FILE" ]; then
     fi
 else
     echo "No dataset found in $DATA_DIR. Nothing to ingest."
-fi
+fi

+ 2 - 1
deploy.sh

@@ -1,4 +1,5 @@
 #!/bin/bash
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 # -----------------------------------------------------------------------------
 # Naked Environment Deployment Script for Ubuntu 24.04
 # Run this script to seamlessly fully provision the server for the AI Web App.
@@ -56,4 +57,4 @@ echo ""
 echo "Next steps:"
 echo "1. Activate your virtual environment manually:  source venv/bin/activate"
 echo "2. Check your config.ini file details."
-echo "3. Run your setup script to configure database users:  python setup_db.py"
+echo "3. Run your setup script to configure database users:  python setup_db.py"

+ 2 - 1
docker-compose-wsl.yml

@@ -1,3 +1,4 @@
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 services:
   mysql:
     build:
@@ -187,4 +188,4 @@ services:
 
 volumes:
   mysql_data:
-  ollama_data:
+  ollama_data:

+ 192 - 191
docker-compose.yml

@@ -1,191 +1,192 @@
-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:
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
+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:

+ 112 - 111
docker-compose_skip.yml

@@ -1,111 +1,112 @@
-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:
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
+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:

+ 2 - 1
docker/app/Dockerfile

@@ -1,3 +1,4 @@
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 # Dockerfile for Streamlit UI
 FROM python:3.11-slim
 
@@ -18,4 +19,4 @@ COPY *.py *.txt ./
 
 EXPOSE 8501
 
-CMD ["streamlit", "run", "app.py", "--server.port", "8501", "--server.headless", "true"]
+CMD ["streamlit", "run", "app.py", "--server.port", "8501", "--server.headless", "true"]

+ 2 - 1
docker/ingest/Dockerfile

@@ -1,3 +1,4 @@
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 # Dockerfile for ingestion service
 FROM python:3.11-slim
 
@@ -20,4 +21,4 @@ COPY ingest_csv.py ./
 COPY myloginpath.py ./
 
 # Entry point (will be overridden by K8s job)
-CMD ["python", "ingest_csv.py"]
+CMD ["python", "ingest_csv.py"]

+ 2 - 1
docker/mysql/Dockerfile

@@ -1,4 +1,5 @@
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 FROM mysql:8.0
 
 # Minimal MySQL Dockerfile – no custom config or init scripts
-EXPOSE 3306
+EXPOSE 3306

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

@@ -1,53 +1,54 @@
-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"
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
+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"

+ 80 - 78
docs/Backup_Procedure.md

@@ -1,78 +1,80 @@
-# $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.
+The current version is #ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
+
+# $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.

+ 13 - 11
docs/Data_Ingestion.md

@@ -1,11 +1,13 @@
-# $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.
+The current version is #ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
+
+# $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.

+ 21 - 19
docs/Final_Report.md

@@ -1,19 +1,21 @@
-# $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.
+The current version is #ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
+
+# $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.

+ 20 - 18
docs/Installation_Guide.md

@@ -1,18 +1,20 @@
-# $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)
+The current version is #ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
+
+# $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)

+ 186 - 184
docs/Operator_Installation_Guide.md

@@ -1,184 +1,186 @@
-# $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:1.5b 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. | `[ ]` |
+The current version is #ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
+
+# $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:1.5b 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. | `[ ]` |

+ 5 - 3
docs/Scrum_Artifacts.md

@@ -1,3 +1,5 @@
-# $Id$
-# Scrum Artifacts
-Contains User Stories, velocity tracking, and burndown charts from Taiga.
+The current version is #ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
+
+# $Id$
+# Scrum Artifacts
+Contains User Stories, velocity tracking, and burndown charts from Taiga.

+ 5 - 3
docs/Scrum_Daily.md

@@ -1,3 +1,5 @@
-# $Id$
-# Daily Scrums
-- **26.05.07 DAILY**: Fixed time scope bug, added Nginx proxy, built sync scripts.
+The current version is #ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
+
+# $Id$
+# Daily Scrums
+- **26.05.07 DAILY**: Fixed time scope bug, added Nginx proxy, built sync scripts.

+ 5 - 3
docs/Scrum_Plan.md

@@ -1,3 +1,5 @@
-# $Id$
-# Sprint Plans
-- **Sprint 10 PLAN**: Fix LLM Tool Calling, optimize Cartesian SQL explosion, build Teams webhooks.
+The current version is #ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
+
+# $Id$
+# Sprint Plans
+- **Sprint 10 PLAN**: Fix LLM Tool Calling, optimize Cartesian SQL explosion, build Teams webhooks.

+ 5 - 3
docs/Scrum_Retro.md

@@ -1,3 +1,5 @@
-# $Id$
-# Sprint Retrospectives
-- **Sprint 10 RETROSPECTIVE**: Mitigated dirty data duplicates using SQL `GROUP BY`. Need to maintain strict Git commit tagging (`TG-XXX`).
+The current version is #ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
+
+# $Id$
+# Sprint Retrospectives
+- **Sprint 10 RETROSPECTIVE**: Mitigated dirty data duplicates using SQL `GROUP BY`. Need to maintain strict Git commit tagging (`TG-XXX`).

+ 5 - 3
docs/Scrum_Review.md

@@ -1,3 +1,5 @@
-# $Id$
-# Sprint Reviews
-- **Sprint 10 REVIEW**: App executes sub-second searches. Nginx fully operational on Port 80.
+The current version is #ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
+
+# $Id$
+# Sprint Reviews
+- **Sprint 10 REVIEW**: App executes sub-second searches. Nginx fully operational on Port 80.

+ 37 - 35
docs/Scrum_Wiki.md

@@ -1,35 +1,37 @@
-# $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.
+The current version is #ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
+
+# $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.

+ 92 - 90
docs/Start_Stop_Procedures.md

@@ -1,90 +1,92 @@
-# $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"
-```
+The current version is #ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
+
+# $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"
+```

+ 6 - 4
docs/Test_Cases_Sprint8.md

@@ -1,4 +1,6 @@
-# $Id$
-# Sprint 8 Legacy Test Cases
-- Tested RAG AI tool integration.
-- Tested user authentication flows.
+The current version is #ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
+
+# $Id$
+# Sprint 8 Legacy Test Cases
+- Tested RAG AI tool integration.
+- Tested user authentication flows.

+ 43 - 41
docs/User_Description.md

@@ -1,41 +1,43 @@
-# $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).
+The current version is #ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
+
+# $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).

+ 13 - 11
docs/User_Guide.md

@@ -1,11 +1,13 @@
-# $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.
+The current version is #ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
+
+# $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.

+ 7 - 5
docs/WSL_Deployment.md

@@ -1,5 +1,7 @@
-# $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.
+The current version is #ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
+
+# $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.

+ 5 - 3
docs/Wiki_Home.md

@@ -1,3 +1,5 @@
-# $Id$
-# Documentation Home
-Welcome to the static documentation mirror. Please navigate the markdown files in this directory for architectural diagrams and guides.
+The current version is #ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
+
+# $Id$
+# Documentation Home
+Welcome to the static documentation mirror. Please navigate the markdown files in this directory for architectural diagrams and guides.

+ 3 - 1
docs/architecture.md

@@ -1,3 +1,5 @@
+The current version is #ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
+
 # $Id$
 # Local Food AI - Architecture Map
 
@@ -96,4 +98,4 @@ When the remote VM host network or Taiga server is completely unreachable:
 - **Dynamic Task Tracking**: Agile development logs are dynamically synced into the workspace [task.md](file:///C:/Users/lanfr144/Documents/DOPRO1/Antigravity/Food/task.md) and [walkthrough.md](file:///C:/Users/lanfr144/Documents/DOPRO1/Antigravity/Food/walkthrough.md) artifacts to track progress until connectivity is restored.
 
 ---
-*Documented by Antigravity.*
+*Documented by Antigravity.*

+ 3 - 1
docs/disaster_recovery_plan.md

@@ -1,3 +1,5 @@
+The current version is #ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
+
 # Disaster Recovery & Backup Plan
 
 This document outlines the backup and restore procedures, as well as the Disaster Recovery (DR) plan for the Local Food AI stack.
@@ -47,4 +49,4 @@ If deploying in the distributed Multi-Hypervisor PoC environment (Hyper-V / Virt
 When the network to the remote host VM (`192.168.130.170`) or the Taiga server (`192.168.130.161`) is unavailable:
 - **Resilient Single-Node Local Fallback**: The entire stack is completely containerized. By running `docker compose up -d` at the root of the workspace, MySQL, Ollama (with Llama 3.2:3B), SearXNG, Zabbix (Server, Web, Agent), Nginx, and the Streamlit App run entirely locally on the same host, avoiding external network dependencies.
 - **Taiga Sync Bypass**: The `taiga_sync_final.py` execution is decoupled from core runtime. In disconnected mode, commits and user stories are managed through local Git branch state and manual tasks/walkthroughs, and sync scripts can be run once the connection is restored.
-- **Dynamic Configuration Resolvers**: The application components, database migrations (`alembic/env.py`), and SNMP notification wrappers (`snmp_notifier.py`, `configure_zabbix_alerts.py`) automatically detect the local environment or fall back to container names (e.g. `mysql`, `zabbix-server`) rather than crashing on unreachable remote IPs.
+- **Dynamic Configuration Resolvers**: The application components, database migrations (`alembic/env.py`), and SNMP notification wrappers (`snmp_notifier.py`, `configure_zabbix_alerts.py`) automatically detect the local environment or fall back to container names (e.g. `mysql`, `zabbix-server`) rather than crashing on unreachable remote IPs.

+ 3 - 1
docs/distributed_deployment.md

@@ -1,3 +1,5 @@
+The current version is #ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
+
 # Distributed Deployment Guide
 
 This document outlines the procedure to deploy the Local Food AI stack across a mixed topology of Windows 11 subsystems and hypervisors on the same local network.
@@ -61,4 +63,4 @@ docker save -o local_food_monitoring.tar zabbix/zabbix-server-mysql:ubuntu-7.0-l
 Copy the `.tar` files via USB or SCP, then run:
 ```bash
 docker load -i local_food_app.tar
-```
+```

+ 2 - 2
docs/docker_connection.md

@@ -1,5 +1,5 @@
 # Docker Connection & Health Check Guide
-#ident "@(#)$Format:LocalFoodAI:docker_connection.md:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
+The current version is #ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 
 This document explains how to interact with the various Docker containers that power the Local Food AI system.
 
@@ -48,4 +48,4 @@ You can verify that all application components are working using:
 ```bash
 docker ps
 ```
-Look for `Up (healthy)` in the STATUS column for the `mysql` service, and ensure `food-app-1` (Streamlit) is running without restarting.
+Look for `Up (healthy)` in the STATUS column for the `mysql` service, and ensure `food-app-1` (Streamlit) is running without restarting.

+ 84 - 82
docs/project_report.md

@@ -1,82 +1,84 @@
-# 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.
+The current version is #ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
+
+# 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.

+ 3 - 1
docs/retro_planning.md

@@ -1,3 +1,5 @@
+The current version is #ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
+
 # Local Food AI: Retro Planning
 
 *Document compiled in accordance with BTS-AI DOPRO Guidelines on Backward/Reverse Planning.*
@@ -38,4 +40,4 @@ gantt
 
 ## 3. Resource & Buffer Analysis
 - **Milestone Buffers**: By utilizing a reverse plan, we identified that the massive 3GB OpenFoodFacts dataset required a 6-day window for background ingestion without blocking the frontend development. 
-- **Leeway Analysis**: The final 2 days (May 13 - 15) are strictly reserved for Disaster Recovery (DR) drills and Multi-VM Proof of Concept (PoC) validation, ensuring the presentation runs flawlessly regardless of infrastructure hiccups.
+- **Leeway Analysis**: The final 2 days (May 13 - 15) are strictly reserved for Disaster Recovery (DR) drills and Multi-VM Proof of Concept (PoC) validation, ensuring the presentation runs flawlessly regardless of infrastructure hiccups.

+ 3 - 1
docs/taiga_audit_report.md

@@ -1,3 +1,5 @@
+The current version is #ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
+
 # Taiga Agile Audit Report
 
 > [!NOTE]
@@ -114,4 +116,4 @@
   - `[x]` Task #22: Rework app.py LLM inference loop to support native Mistral Tool/Function calling integrations. (Closed)
   - `[x]` Task #188: Inject SearXNG container into docker-compose.yml (Closed)
   - `[x]` Task #190: Integrate SearXNG API payload parsing with Ollama (Closed)
-  - `[x]` Task #210: Integrate Local SearXNG Private Search Fallback (Closed)
+  - `[x]` Task #210: Integrate Local SearXNG Private Search Fallback (Closed)

+ 3 - 1
docs/zabbix_monitoring.md

@@ -1,3 +1,5 @@
+The current version is #ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
+
 # Zabbix Telemetry & Monitoring Guide
 
 ## Overview
@@ -21,4 +23,4 @@ The dashboard automatically queries the SNMP daemons running inside the Docker c
 ## Verifying Alerts
 1. Click **Monitoring > Problems**.
 2. If `snmpd` inside a container crashes or is unreachable, Zabbix will trigger an `Agent Unreachable` High-Severity Alert.
-3. If the Database Server container crashes, Zabbix will trigger an alert via the Application Python `snmp_notifier.py` wrapper which sends asynchronous trap payloads indicating critical RAG failures.
+3. If the Database Server container crashes, Zabbix will trigger an alert via the Application Python `snmp_notifier.py` wrapper which sends asynchronous trap payloads indicating critical RAG failures.

+ 2 - 1
download_csv.sh

@@ -1,4 +1,5 @@
 #!/bin/bash
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 # download latest OpenFoodFacts CSVs if not present or if newer version exists
 DATA_DIR="$(dirname "$0")/data"
 mkdir -p "$DATA_DIR"
@@ -25,4 +26,4 @@ download "$EN_URL" "$EN_FILE"
 
 download "$FR_URL" "$FR_FILE"
 
-echo "CSV download completed."
+echo "CSV download completed."

+ 2 - 1
generate_docs.py

@@ -1,3 +1,4 @@
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 # $Id$
 # $Author$
 # $log$
@@ -563,4 +564,4 @@ for filename, content in docs.items():
         f.write(content.replace('$Id$', git_id))
     print(f"Generated {filepath}")
 
-print("\nDocs directory perfectly mirrored with operator level runbooks.")
+print("\nDocs directory perfectly mirrored with operator level runbooks.")

+ 2 - 1
git_id.txt

@@ -1 +1,2 @@
-Unknown
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
+Unknown

+ 2 - 1
git_version.txt

@@ -1 +1,2 @@
-v1.3.0
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
+v1.3.0

+ 2 - 1
ingest_csv.py

@@ -1,4 +1,5 @@
 #!/usr/bin/env python3
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 import pandas as pd
 import myloginpath
 import urllib.parse
@@ -131,4 +132,4 @@ if __name__ == "__main__":
     if not processed_en and not processed_fr:
         print("\n❌ Could not find CSVs.")
     else:
-        print("\n🎉 Full database reload complete! Ready for AI RAG.")
+        print("\n🎉 Full database reload complete! Ready for AI RAG.")

+ 12 - 11
init.sql

@@ -1,3 +1,4 @@
+--ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 -- Create databases
 CREATE DATABASE IF NOT EXISTS food_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
 CREATE DATABASE IF NOT EXISTS zabbix CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;
@@ -6,21 +7,21 @@ CREATE DATABASE IF NOT EXISTS zabbix CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;
 SET GLOBAL log_bin_trust_function_creators = 1;
 
 -- Create/update root user for remote connections
-CREATE USER IF NOT EXISTS 'root'@'%' IDENTIFIED BY 'BTSai123';
-ALTER USER 'root'@'%' IDENTIFIED BY 'BTSai123';
+CREATE USER IF NOT EXISTS 'root'@'%' IDENTIFIED BY 'your_db_password_here';
+ALTER USER 'root'@'%' IDENTIFIED BY 'your_db_password_here';
 
 -- Create app users
-CREATE USER IF NOT EXISTS 'food_reader'@'%' IDENTIFIED BY 'BTSai123';
-ALTER USER 'food_reader'@'%' IDENTIFIED BY 'BTSai123';
+CREATE USER IF NOT EXISTS 'food_reader'@'%' IDENTIFIED BY 'your_db_password_here';
+ALTER USER 'food_reader'@'%' IDENTIFIED BY 'your_db_password_here';
 
-CREATE USER IF NOT EXISTS 'food_loader'@'%' IDENTIFIED BY 'BTSai123';
-ALTER USER 'food_loader'@'%' IDENTIFIED BY 'BTSai123';
+CREATE USER IF NOT EXISTS 'food_loader'@'%' IDENTIFIED BY 'your_db_password_here';
+ALTER USER 'food_loader'@'%' IDENTIFIED BY 'your_db_password_here';
 
-CREATE USER IF NOT EXISTS 'food_app_auth'@'%' IDENTIFIED BY 'BTSai123';
-ALTER USER 'food_app_auth'@'%' IDENTIFIED BY 'BTSai123';
+CREATE USER IF NOT EXISTS 'food_app_auth'@'%' IDENTIFIED BY 'your_db_password_here';
+ALTER USER 'food_app_auth'@'%' IDENTIFIED BY 'your_db_password_here';
 
-CREATE USER IF NOT EXISTS 'zabbix'@'%' IDENTIFIED BY 'BTSai123';
-ALTER USER 'zabbix'@'%' IDENTIFIED BY 'BTSai123';
+CREATE USER IF NOT EXISTS 'zabbix'@'%' IDENTIFIED BY 'your_db_password_here';
+ALTER USER 'zabbix'@'%' IDENTIFIED BY 'your_db_password_here';
 
 -- Grant privileges
 GRANT ALL PRIVILEGES ON food_db.* TO 'food_loader'@'%';
@@ -28,4 +29,4 @@ GRANT SELECT ON food_db.* TO 'food_reader'@'%';
 GRANT SELECT, INSERT, UPDATE, DELETE ON food_db.* TO 'food_app_auth'@'%';
 GRANT ALL PRIVILEGES ON zabbix.* TO 'zabbix'@'%';
 
-FLUSH PRIVILEGES;
+FLUSH PRIVILEGES;

+ 2 - 1
k8s/alembic-migrate-job.yaml

@@ -1,3 +1,4 @@
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 apiVersion: batch/v1
 kind: Job
 metadata:
@@ -30,4 +31,4 @@ spec:
       volumes:
       - name: logs
         persistentVolumeClaim:
-          claimName: logs-pvc
+          claimName: logs-pvc

+ 2 - 1
k8s/app-deployment.yaml

@@ -1,3 +1,4 @@
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 apiVersion: apps/v1
 kind: Deployment
 metadata:
@@ -44,4 +45,4 @@ spec:
       volumes:
       - name: logs
         persistentVolumeClaim:
-          claimName: logs-pvc
+          claimName: logs-pvc

+ 2 - 1
k8s/app-service.yaml

@@ -1,3 +1,4 @@
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 apiVersion: v1
 kind: Service
 metadata:
@@ -11,4 +12,4 @@ spec:
   - protocol: TCP
     port: 8501
     targetPort: 8501
-    nodePort: 30080
+    nodePort: 30080

+ 2 - 1
k8s/configmap.yaml

@@ -1,3 +1,4 @@
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 apiVersion: v1
 kind: ConfigMap
 metadata:
@@ -11,4 +12,4 @@ data:
     max_allowed_packet=256M
     innodb_log_file_size=256M
   APP_ENV: "production"
-  LOG_PATH: "/logs"
+  LOG_PATH: "/logs"

+ 2 - 1
k8s/ingest-job.yaml

@@ -1,3 +1,4 @@
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 apiVersion: batch/v1
 kind: Job
 metadata:
@@ -33,4 +34,4 @@ spec:
           claimName: csv-data-pvc
       - name: logs
         persistentVolumeClaim:
-          claimName: logs-pvc
+          claimName: logs-pvc

+ 2 - 1
k8s/mysql-deployment.yaml

@@ -1,3 +1,4 @@
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 apiVersion: apps/v1
 kind: Deployment
 metadata:
@@ -52,4 +53,4 @@ spec:
             path: my.cnf
       - name: logs
         persistentVolumeClaim:
-          claimName: logs-pvc
+          claimName: logs-pvc

+ 2 - 1
k8s/namespace.yaml

@@ -1,5 +1,6 @@
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 # Namespace for Food AI
 apiVersion: v1
 kind: Namespace
 metadata:
-  name: food-ai
+  name: food-ai

+ 2 - 1
k8s/pvc.yaml

@@ -1,3 +1,4 @@
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 apiVersion: v1
 kind: PersistentVolumeClaim
 metadata:
@@ -32,4 +33,4 @@ spec:
     - ReadWriteOnce
   resources:
     requests:
-      storage: 5Gi
+      storage: 5Gi

+ 2 - 1
k8s/secret.yaml.example

@@ -1,3 +1,4 @@
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 apiVersion: v1
 kind: Secret
 metadata:
@@ -15,4 +16,4 @@ stringData:
 
   EMAIL_PASS: "placeholder_email_pass"
   TAIGA_USER: base64_encoded_placeholder
-  TAIGA_PASS: base64_encoded_placeholder
+  TAIGA_PASS: base64_encoded_placeholder

+ 2 - 1
k8s/taiga-sync-config.yaml

@@ -1,3 +1,4 @@
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 apiVersion: v1
 kind: ConfigMap
 metadata:
@@ -23,4 +24,4 @@ data:
               * MySQL DB
               * Service d’ingestion
               * UI Streamlit
-              * Synchronisation Taiga
+              * Synchronisation Taiga

+ 2 - 1
k8s/taiga-sync-job.yaml

@@ -1,3 +1,4 @@
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 apiVersion: batch/v1
 kind: Job
 metadata:
@@ -29,4 +30,4 @@ spec:
             name: taiga-sync-config
             items:
               - key: sync.yaml
-                path: sync.yaml
+                path: sync.yaml

+ 3 - 2
manage_services.sh

@@ -1,4 +1,5 @@
 #!/bin/bash
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 # ==============================================================================
 # $Id$
 # File: manage_services.sh
@@ -55,7 +56,7 @@ start_services() {
     
     # Wait for MySQL to become fully ready and accept connections
     log_info "Waiting for MySQL database socket to be available..."
-    until docker compose -f "$COMPOSE_FILE" exec mysql mysqladmin ping -h"localhost" -u"root" -p"BTSai123" --silent; do
+    until docker compose -f "$COMPOSE_FILE" exec mysql mysqladmin ping -h"localhost" -u"root" -p"your_db_password_here" --silent; do
         sleep 2
         echo -n "."
     done
@@ -182,4 +183,4 @@ case "$1" in
     *)
         show_help
         ;;
-esac
+esac

+ 2 - 1
master_trigger.sh

@@ -1,8 +1,9 @@
 #!/bin/bash
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 # Natively reload all database logic without interactive blocks
 echo "Executing autonomous WSL reload..."
 pip3 install --break-system-packages pymysql pandas sqlalchemy sqlalchemy-utils cryptography openpyxl
 python3 setup_db.py
 echo "Spawning Batch Ingestion into background..."
 nohup bash start_batch_ingest.sh > ingest_log.txt 2>&1 &
-echo "Master pipeline triggered successfully."
+echo "Master pipeline triggered successfully."

+ 2 - 1
my.cnf

@@ -1,3 +1,4 @@
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 [mysqld]
 # ---------------------------------------------------------
 # MySQL Configuration Settings
@@ -15,4 +16,4 @@ local_infile = 1
 
 [mysql]
 # Enable local infile on the client side
-local_infile = 1
+local_infile = 1

+ 2 - 1
myloginpath.py

@@ -1,3 +1,4 @@
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 import os
 
 def parse(login_path: str):
@@ -11,4 +12,4 @@ def parse(login_path: str):
     host = os.getenv(f"{prefix}_HOST", "127.0.0.1")
     user = os.getenv(f"{prefix}_USER", "root")
     password = os.getenv(f"{prefix}_PASSWORD", "")
-    return {"host": host, "user": user, "password": password}
+    return {"host": host, "user": user, "password": password}

+ 2 - 1
nginx/nginx.conf

@@ -1,3 +1,4 @@
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 events {
     worker_connections 1024;
 }
@@ -21,4 +22,4 @@ http {
             proxy_read_timeout 86400;
         }
     }
-}
+}

+ 2 - 1
proper_reset.sh

@@ -1 +1,2 @@
-mysql -e "SET GLOBAL log_bin_trust_function_creators = 1; DROP DATABASE IF EXISTS zabbix; CREATE DATABASE zabbix character set utf8mb4 collate utf8mb4_bin; GRANT ALL PRIVILEGES ON zabbix.* TO 'zabbix'@'%'; FLUSH PRIVILEGES;"
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
+mysql -e "SET GLOBAL log_bin_trust_function_creators = 1; DROP DATABASE IF EXISTS zabbix; CREATE DATABASE zabbix character set utf8mb4 collate utf8mb4_bin; GRANT ALL PRIVILEGES ON zabbix.* TO 'zabbix'@'%'; FLUSH PRIVILEGES;"

+ 1 - 0
requirements.txt

@@ -1,3 +1,4 @@
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 pandas
 pymysql
 myloginpath

+ 12 - 11
reset.sh

@@ -1,20 +1,21 @@
 #!/bin/bash
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 cd /home/francois/food_project
 docker-compose stop mysql
 docker run -d --name mysql_temp_reset -v food_project_mysql_data:/var/lib/mysql mysql:8.0 --skip-grant-tables
 sleep 7
 docker exec mysql_temp_reset mysql -e "
   FLUSH PRIVILEGES;
-  ALTER USER 'root'@'localhost' IDENTIFIED BY 'BTSai123';
-  ALTER USER 'root'@'%' IDENTIFIED BY 'BTSai123';
-  CREATE USER IF NOT EXISTS 'food_reader'@'%' IDENTIFIED BY 'BTSai123';
-  ALTER USER 'food_reader'@'%' IDENTIFIED BY 'BTSai123';
-  CREATE USER IF NOT EXISTS 'food_loader'@'%' IDENTIFIED BY 'BTSai123';
-  ALTER USER 'food_loader'@'%' IDENTIFIED BY 'BTSai123';
-  CREATE USER IF NOT EXISTS 'food_app_auth'@'%' IDENTIFIED BY 'BTSai123';
-  ALTER USER 'food_app_auth'@'%' IDENTIFIED BY 'BTSai123';
-  CREATE USER IF NOT EXISTS 'zabbix'@'%' IDENTIFIED BY 'BTSai123';
-  ALTER USER 'zabbix'@'%' IDENTIFIED BY 'BTSai123';
+  ALTER USER 'root'@'localhost' IDENTIFIED BY 'your_db_password_here';
+  ALTER USER 'root'@'%' IDENTIFIED BY 'your_db_password_here';
+  CREATE USER IF NOT EXISTS 'food_reader'@'%' IDENTIFIED BY 'your_db_password_here';
+  ALTER USER 'food_reader'@'%' IDENTIFIED BY 'your_db_password_here';
+  CREATE USER IF NOT EXISTS 'food_loader'@'%' IDENTIFIED BY 'your_db_password_here';
+  ALTER USER 'food_loader'@'%' IDENTIFIED BY 'your_db_password_here';
+  CREATE USER IF NOT EXISTS 'food_app_auth'@'%' IDENTIFIED BY 'your_db_password_here';
+  ALTER USER 'food_app_auth'@'%' IDENTIFIED BY 'your_db_password_here';
+  CREATE USER IF NOT EXISTS 'zabbix'@'%' IDENTIFIED BY 'your_db_password_here';
+  ALTER USER 'zabbix'@'%' IDENTIFIED BY 'your_db_password_here';
   GRANT ALL PRIVILEGES ON food_db.* TO 'food_loader'@'%';
   GRANT SELECT ON food_db.* TO 'food_reader'@'%';
   GRANT SELECT, INSERT, UPDATE, DELETE ON food_db.* TO 'food_app_auth'@'%';
@@ -25,4 +26,4 @@ docker stop mysql_temp_reset
 docker rm mysql_temp_reset
 docker-compose start mysql
 sleep 5
-docker-compose restart app ingest
+docker-compose restart app ingest

+ 1 - 0
rotate_passwords.py

@@ -0,0 +1 @@
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"

+ 2 - 1
scratch/test_dr.sh

@@ -1,4 +1,5 @@
 #!/bin/bash
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 # test_dr.sh - Automated Disaster Recovery validation script
 
 # 1. Find the latest backup
@@ -66,4 +67,4 @@ fi
 # 6. Clean up
 echo "🧹 Destroying sandbox container..."
 docker rm -f "$CONTAINER_NAME" >/dev/null
-echo "✅ DR Test Sequence Complete."
+echo "✅ DR Test Sequence Complete."

+ 2 - 2
scripts/create_delivery_zip.py

@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-#ident "@(#)$Format:LocalFoodAI:create_delivery_zip.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 import os
 import zipfile
 import pathspec
@@ -99,4 +99,4 @@ SERVER_PASS=your_server_pass
     print(f"Successfully created: delivery.zip")
 
 if __name__ == "__main__":
-    main()
+    main()

+ 3 - 3
scripts/deploy_to_server.py

@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-#ident "@(#)$Format:LocalFoodAI:deploy_to_server.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 import os
 import sys
 import paramiko
@@ -31,7 +31,7 @@ def deploy():
         ssh.connect(host, username=user, password=password, timeout=10)
         print("Connected successfully!")
         
-        command = "cd food_project && rm -f git_version.txt git_id.txt && git pull && git log -1 --format='%cd' > git_version.txt && git log -1 --format='%cd %h' app.py > git_id.txt && docker-compose up -d --build"
+        command = "cd food_project && rm -f git_version.txt git_id.txt && git pull && git log -1 --date='format:%Y/%m/%d %H:%M:%S' --format='%cd' > git_version.txt && git log -1 --date='format:%Y/%m/%d %H:%M:%S' --format='%cd %h' app.py > git_id.txt && docker-compose up -d --build"
         print(f"Executing: {command}")
         
         stdin, stdout, stderr = ssh.exec_command(command)
@@ -49,4 +49,4 @@ def deploy():
         print("Connection closed.")
 
 if __name__ == "__main__":
-    deploy()
+    deploy()

+ 2 - 1
scripts/generate_pdfs.py

@@ -1,3 +1,4 @@
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 import os
 import glob
 from markdown_pdf import MarkdownPdf
@@ -91,4 +92,4 @@ def main():
             print(f"WARNING: Could not save {os.path.basename(pdf_file)}. File might be locked or open in a viewer. Error: {e}")
 
 if __name__ == "__main__":
-    main()
+    main()

+ 2 - 1
scripts/generate_project_report.py

@@ -1,3 +1,4 @@
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 # $Id$
 import os
 import subprocess
@@ -251,4 +252,4 @@ An overview of the folder hierarchy organizing our microservice infrastructure:
     print(f"Project report generated at: {report_path}")
 
 if __name__ == '__main__':
-    main()
+    main()

+ 2 - 2
scripts/manage_models.sh

@@ -1,5 +1,5 @@
 #!/bin/bash
-#ident "@(#)$Format:LocalFoodAI:manage_models.sh:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 
 echo "Pulling the new efficient billion-parameter model (llama3.2-vision:11b)..."
 docker exec food-ollama-1 ollama pull llama3.2-vision:11b
@@ -11,4 +11,4 @@ docker exec food-ollama-1 ollama rm llama3.2:3b
 echo "Currently installed models:"
 docker exec food-ollama-1 ollama list
 
-echo "Model management complete!"
+echo "Model management complete!"

+ 291 - 290
scripts/setup_deploy.py

@@ -1,290 +1,291 @@
-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`.")
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
+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`.")

+ 2 - 1
scripts/taiga_sync_final.py

@@ -1,3 +1,4 @@
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 import requests
 #ident "@(#)$Format:LocalFoodAI:taiga_sync_final.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 import urllib3
@@ -88,4 +89,4 @@ def run_sync():
     print("\n--- Great Taiga Cleanup Complete ---")
 
 if __name__ == "__main__":
-    run_sync()
+    run_sync()

+ 2 - 2
scripts/zip_project.py

@@ -1,9 +1,9 @@
 #!/usr/bin/env python3
-#ident "@(#)$Format:LocalFoodAI:zip_project.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 # Alias for create_delivery_zip.py
 import os
 import sys
 
 if __name__ == "__main__":
     script_dir = os.path.dirname(os.path.abspath(__file__))
-    os.system(f"python {os.path.join(script_dir, 'create_delivery_zip.py')}")
+    os.system(f"python {os.path.join(script_dir, 'create_delivery_zip.py')}")

+ 1 - 1
searxng/settings.yml

@@ -1,3 +1,4 @@
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 use_default_settings: true
 
 search:
@@ -8,4 +9,3 @@ server:
   port: 8080
   bind_address: "0.0.0.0"
   secret_key: "local_food_ai_secret"
-

+ 1 - 1
snmp_notifier.py

@@ -1,3 +1,4 @@
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 import os
 import socket
 #ident "@(#)$Format:LocalFoodAI:snmp_notifier.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
@@ -26,4 +27,3 @@ class SNMPNotifier:
 
 # Singleton instance
 notifier = SNMPNotifier()
-

+ 2 - 1
start_batch_ingest.sh

@@ -1,4 +1,5 @@
 #!/bin/bash
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 # Local Food AI - Disconnected Ingestion Wrapper
 # This script uses nohup to run the python ingestion script in the background.
 # You can exit your SSH session safely after starting this script.
@@ -27,4 +28,4 @@ BG_PID=$!
 echo "✅ Process started in the background (PID: $BG_PID)"
 echo "You can now safely close your terminal or turn off your computer."
 echo "To monitor progress from the server later, run:"
-echo "   tail -f ingestion_process.log"
+echo "   tail -f ingestion_process.log"

Fișier diff suprimat deoarece este prea mare
+ 0 - 0
taiga/local-food-ai-1-eab691c0-9c19-4dce-ac66-3b8fade77ef7.json


+ 2 - 1
unit_converter.py

@@ -1,3 +1,4 @@
+#ident "@(#)$Format:LocalFoodAI:app.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 import re
 #ident "@(#)$Format:LocalFoodAI:unit_converter.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
 
@@ -177,4 +178,4 @@ if __name__ == '__main__':
     print("1 pound of generic food:", UnitConverter.parse_and_convert("1 pound", "unknown"), "g")
     print("1 pinch of salt:", UnitConverter.parse_and_convert("1 pinch", "salt"), "g")
     print("1 xl egg:", UnitConverter.parse_and_convert("1 xl", "egg"), "g")
-    print("2 large eggs:", UnitConverter.parse_and_convert("2 large", "egg"), "g")
+    print("2 large eggs:", UnitConverter.parse_and_convert("2 large", "egg"), "g")

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff