Преглед на файлове

[#1] Add sanitized version of scripts without history

Lange François преди 2 седмици
родител
ревизия
ba10b6be6f

+ 42 - 0
scratch/add_columns.py

@@ -0,0 +1,42 @@
+import os
+import paramiko
+import dotenv
+
+repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+dotenv.load_dotenv(dotenv_path=os.path.join(repo_root, ".env"))
+
+host = os.environ.get('SERVER_HOST')
+user = os.environ.get('SERVER_USER')
+password = os.environ.get('SERVER_PASS')
+if password == "your_db_password_here" or not password:
+    password = None
+
+ssh = paramiko.SSHClient()
+ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+
+try:
+    ssh.connect(host, username=user, password=password, timeout=10)
+    print("SSH Connected!")
+    
+    queries = [
+        "ALTER TABLE food_db.products_core ADD COLUMN url LONGTEXT NULL;",
+        "ALTER TABLE food_db.products_core ADD COLUMN image_url LONGTEXT NULL;",
+        "ALTER TABLE food_db.products_core ADD COLUMN image_small_url LONGTEXT NULL;",
+        "ALTER TABLE food_db.products_core ADD COLUMN image_ingredients_url LONGTEXT NULL;",
+        "ALTER TABLE food_db.products_core ADD COLUMN image_ingredients_small_url LONGTEXT NULL;",
+        "ALTER TABLE food_db.products_core ADD COLUMN image_nutrition_url LONGTEXT NULL;",
+        "ALTER TABLE food_db.products_core ADD COLUMN image_nutrition_small_url LONGTEXT NULL;"
+    ]
+    
+    db_pass = os.environ.get('DB_LOADER_PASS') or 'your_db_loader_pass'
+    for q in queries:
+        print(f"Running: {q}")
+        cmd = f"docker exec food_project-mysql-1 mysql -u food_loader -p{db_pass} food_db -e \"{q}\""
+        stdin, stdout, stderr = ssh.exec_command(cmd)
+        print("STDOUT:\n", stdout.read().decode('utf-8'))
+        print("STDERR:\n", stderr.read().decode('utf-8'))
+        
+except Exception as e:
+    print(f"Error: {e}")
+finally:
+    ssh.close()

+ 42 - 0
scratch/create_remote_indexes.py

@@ -0,0 +1,42 @@
+import os
+import sys
+import paramiko
+from dotenv import load_dotenv
+
+sys.stdout.reconfigure(encoding='utf-8')
+sys.stderr.reconfigure(encoding='utf-8')
+
+repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+load_dotenv(dotenv_path=os.path.join(repo_root, ".env"))
+
+host = os.environ.get('SERVER_HOST')
+user = os.environ.get('SERVER_USER')
+password = os.environ.get('SERVER_PASS')
+if password == "your_db_password_here" or not password:
+    password = None
+
+ssh = paramiko.SSHClient()
+ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+
+try:
+    ssh.connect(host, username=user, password=password, timeout=10)
+    print("SSH Connected!")
+    
+    queries = [
+        "ALTER TABLE food_db.products_core ADD FULLTEXT INDEX idx_prod_ing (product_name, ingredients_text);",
+        "ALTER TABLE food_db.products_core ADD FULLTEXT INDEX idx_prod_name (product_name);",
+        "ALTER TABLE food_db.products_core ADD FULLTEXT INDEX idx_ing_text (ingredients_text);"
+    ]
+    
+    db_pass = os.environ.get('MYSQL_ROOT_PASSWORD') or 'your_mysql_root_pass'
+    for q in queries:
+        print(f"Executing: {q}")
+        cmd = f"docker exec food_project-mysql-1 mysql -uroot -p{db_pass} -e '{q}'"
+        stdin, stdout, stderr = ssh.exec_command(cmd)
+        print("STDOUT:\n", stdout.read().decode('utf-8'))
+        print("STDERR:\n", stderr.read().decode('utf-8'))
+        
+except Exception as e:
+    print(f"Error: {e}")
+finally:
+    ssh.close()

+ 30 - 0
scratch/describe_db.py

@@ -0,0 +1,30 @@
+import os
+import paramiko
+import dotenv
+
+repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+dotenv.load_dotenv(dotenv_path=os.path.join(repo_root, ".env"))
+
+host = os.environ.get('SERVER_HOST')
+user = os.environ.get('SERVER_USER')
+password = os.environ.get('SERVER_PASS')
+if password == "your_db_password_here" or not password:
+    password = None
+
+ssh = paramiko.SSHClient()
+ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+
+try:
+    ssh.connect(host, username=user, password=password, timeout=10)
+    print("SSH Connected!")
+    
+    db_pass = os.environ.get('MYSQL_ROOT_PASSWORD') or 'your_mysql_root_pass'
+    cmd = f"docker exec food_project-mysql-1 mysql -u root -p{db_pass} food_db -e 'SHOW PROCESSLIST;'"
+    stdin, stdout, stderr = ssh.exec_command(cmd)
+    print("STDOUT:\n", stdout.read().decode('utf-8'))
+    print("STDERR:\n", stderr.read().decode('utf-8'))
+    
+except Exception as e:
+    print(f"Error: {e}")
+finally:
+    ssh.close()

+ 30 - 0
scratch/find_image_columns.py

@@ -0,0 +1,30 @@
+import os
+import paramiko
+import dotenv
+
+repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+dotenv.load_dotenv(dotenv_path=os.path.join(repo_root, ".env"))
+
+host = os.environ.get('SERVER_HOST')
+user = os.environ.get('SERVER_USER')
+password = os.environ.get('SERVER_PASS')
+if password == "your_db_password_here" or not password:
+    password = None
+
+ssh = paramiko.SSHClient()
+ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+
+try:
+    ssh.connect(host, username=user, password=password, timeout=10)
+    print("SSH Connected!")
+    
+    db_pass = os.environ.get('DB_READER_PASS') or 'your_db_reader_pass'
+    cmd = f"docker exec food_project-mysql-1 mysql -u food_reader -p{db_pass} food_db -e 'SELECT TABLE_NAME, COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = \"food_db\" AND COLUMN_NAME LIKE \"%image%\";'"
+    stdin, stdout, stderr = ssh.exec_command(cmd)
+    print("STDOUT:\n", stdout.read().decode('utf-8'))
+    print("STDERR:\n", stderr.read().decode('utf-8'))
+    
+except Exception as e:
+    print(f"Error: {e}")
+finally:
+    ssh.close()

+ 243 - 0
scratch/fix_generate_docs.py

@@ -0,0 +1,243 @@
+import re
+import os
+
+file_path = "generate_docs.py"
+with open(file_path, "r", encoding="utf-8", errors="replace") as f:
+    content = f.read()
+
+# Replace encoding issues in generate_docs.py header as well
+content = content.replace("Lange Franois", "Francois Lange")
+content = content.replace("Lange FranA ois", "Francois Lange")
+
+# 1. Find and replace Technical_Document.md .env configuration block
+old_tech_env = """    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
+    SERVER_HOST=192.168.130.170
+    SERVER_USER=your_ssh_user
+    SERVER_PASS=your_db_password_here"""
+
+new_tech_env = """    MYSQL_ROOT_PASSWORD=your_secure_root_password
+    DB_READER_PASS=your_secure_reader_password
+    DB_LOADER_PASS=your_secure_loader_password
+    DB_APP_AUTH_PASS=your_secure_auth_password
+    MYSQL_ZABBIX_PASSWORD=your_secure_zabbix_password
+    SERVER_HOST=192.168.130.170
+    SERVER_USER=your_ssh_user
+    SERVER_PASS=your_secure_server_password
+    
+    # ZABBIX & SNMP CREDENTIALS
+    ZABBIX_USER=Admin
+    ZABBIX_PASS=zabbix
+    ZABBIX_SNMP_USER=your_snmp_user
+    ZABBIX_SNMP_AUTHKEY=your_snmp_authkey
+    ZABBIX_SNMP_PRIVKEY=your_snmp_privkey
+    DISCORD_WEBHOOK=https://discord.com/api/webhooks/your_webhook_id
+
+    # EMAIL ALERTS CONFIGURATION
+    EMAIL_USER=your_email@gmail.com
+    EMAIL_PASS=your_email_app_password
+
+    # TAIGA CREDENTIALS
+    TAIGA_URL=https://192.168.130.161/taiga
+    TAIGA_USER=your_taiga_user
+    TAIGA_PASS=your_taiga_password"""
+
+content = content.replace(old_tech_env, new_tech_env)
+
+# 2. Overwrite Installation_Guide.md content block inside generate_docs.py
+old_install_guide_content = """    "Installation_Guide.md": \"\"\"# $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)
+\"\"\","""
+
+new_install_guide_content = """    "Installation_Guide.md": \"\"\"# $Id$
+# Local Food AI - Detailed Installation and Deployment Guide
+
+This guide describes how to provision the host hypervisor, install Docker on Ubuntu, clone the repository, check out the correct branch, and launch the application.
+
+## 1. WSL2 Ubuntu Instance Setup
+
+To create a dedicated WSL2 environment for the application, execute the following command in an Administrator PowerShell window:
+```powershell
+wsl --install -d Ubuntu-22.04 --name Dopro1
+```
+
+During initialization, configure the default Unix user and password as prompted:
+```
+Create a default Unix user account: lanfr144
+New password:
+Retype new password:
+passwd: password updated successfully
+```
+
+> [!WARNING]
+> **WSL Filesystem Mounts**: By default, launching WSL may place you in a Windows filesystem mount (e.g. `/mnt/d/...`). To prevent performance degradation and permission bugs, navigate to your WSL home directory immediately:
+```bash
+cd ~
+```
+
+---
+
+## 2. Docker & Docker Compose Installation inside WSL Ubuntu
+
+To install Docker directly inside your WSL Ubuntu instance (without Docker Desktop):
+
+### Step 2.1: Clean Existing Docker Versions
+```bash
+sudo apt remove -y docker.io docker-compose docker-compose-v2 docker-doc podman-docker containerd runc
+```
+
+### Step 2.2: Add Docker's Official GPG Key & Repository
+```bash
+sudo apt update
+sudo apt install -y ca-certificates curl
+sudo install -m 0755 -d /etc/apt/keyrings
+sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
+sudo chmod a+r /etc/apt/keyrings/docker.asc
+
+sudo tee /etc/apt/sources.list.d/docker.sources <<EOF
+Types: deb
+URIs: https://download.docker.com/linux/ubuntu
+Suites: \\$(. /etc/os-release && echo "\\${UBUNTU_CODENAME:-\\$VERSION_CODENAME}")
+Components: stable
+Architectures: \\$(dpkg --print-architecture)
+Signed-By: /etc/apt/keyrings/docker.asc
+EOF
+```
+
+### Step 2.3: Install Docker Components
+```bash
+sudo apt update
+sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
+```
+
+### Step 2.4: Start and Enable Docker Daemon
+```bash
+sudo systemctl start docker
+sudo systemctl enable docker
+```
+
+### Step 2.5: Add User to the Docker Group
+Ensure you can execute Docker commands without `sudo`:
+```bash
+grep "^docker:" /etc/group || sudo addgroup docker
+sudo usermod -aG docker \\$USER
+```
+
+### Step 2.6: Reboot the WSL Instance
+Execute the command below inside WSL to gracefully reboot the instance:
+```bash
+cd /mnt/c/ && cmd.exe /c start "rebooting WSL" cmd /c "timeout 5 && wsl -d \\$WSL_DISTRO_NAME" && wsl.exe --terminate \\$WSL_DISTRO_NAME
+```
+
+Upon reconnecting, verify Docker is running by starting the hello-world container:
+```bash
+docker run hello-world
+```
+
+---
+
+## 3. Network Configuration & Performance Tuning
+
+### Step 3.1: Switch to Legacy IPTables
+Ubuntu 22.04 uses `nftables` by default. Switch to legacy iptables to ensure Docker network NAT rules match correctly:
+```bash
+sudo update-alternatives --config iptables
+# Select option 1 (iptables-legacy)
+```
+
+### Step 3.2: Configure DNS Settings
+To ensure reliable package downloads and LLM registry calls:
+```bash
+echo "1,\\$ s/^/#/
+\\$ a
+nameserver 1.1.1.1
+.
+w
+q" | sudo ed /etc/resolv.conf
+
+echo "\\$ a
+# Added these 2 lines:
+[network]
+generateResolvConf = false
+.
+w
+q" | sudo ed /etc/wsl.conf
+```
+
+---
+
+## 4. Repository Clones & Branch Governance
+
+There are two repositories configured for this project:
+- Production Repository: `https://git.btshub.lu/lanfr/LocalFoodAI_lanfr144.git`
+- GitHub Mirror (Clone): `https://github.com/lanfr144/LocalFoodAI_lanfr144`
+
+Clone the primary repository inside your home directory:
+```bash
+git clone https://git.btshub.lu/lanfr/LocalFoodAI_lanfr144.git
+cd LocalFoodAI_lanfr144
+```
+
+### Step 4.1: List Available Branches
+Inspect both local and remote branches on the server:
+```bash
+git branch -a
+```
+*(Shows available branches like `remotes/origin/main` or `remotes/origin/dev`)*
+
+### Step 4.2: Track and Check Out the Right Branch
+Select the main production branch and extract it:
+```bash
+git checkout main
+```
+*(If the repository uses a master branch, replace 'main' with 'master')*
+
+### Step 4.3: Set Default Branch (Optional)
+To set the default tracking branch for your local copy:
+```bash
+git remote set-head origin main
+```
+
+---
+
+## 5. Launching the App
+
+Ensure the runbooks and sync scripts have executable permissions:
+```bash
+chmod +x data_sync.sh backup_db.sh manage_services.sh scripts/manage_models.sh
+```
+
+Follow the standard runbook to initialize credentials and launch services:
+```bash
+# 1. Create a local .env file based on step 3 guidelines
+# 2. Run the service manager to spin up containers
+./manage_services.sh start
+```
+\"\"\",
+"""
+
+content = content.replace(old_install_guide_content, new_install_guide_content)
+
+with open(file_path, "w", encoding="utf-8") as f:
+    f.write(content)
+
+print("generate_docs.py successfully updated and encoding sanitized.")

+ 67 - 0
scratch/fix_install_wsl.py

@@ -0,0 +1,67 @@
+import re
+import os
+
+file_path = "INSTALL_WSL.md"
+with open(file_path, "r", encoding="utf-8", errors="replace") as f:
+    content = f.read()
+
+# Replace the encoding artifact in the first line if present
+content = content.replace("Franois", "Francois")
+content = content.replace("FranA ois", "Francois")
+content = content.replace("Françoise", "Francois")
+
+# 1. Update Step 3 (.env template)
+old_step3 = """cat <<EOF > .env
+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"""
+
+new_step3 = """Configure your database credentials, active network mode, and the target model name in a `.env` file at the root of the repository. A generic template is provided below:
+
+```ini
+# NETWORK_MODE: local (offline) or server (online)
+NETWORK_MODE=local
+LLM_MODEL=llama3.2:3b
+
+# DATABASE CREDENTIALS (MySQL)
+MYSQL_ROOT_PASSWORD=your_secure_root_password
+DB_READER_PASS=your_secure_reader_password
+DB_LOADER_PASS=your_secure_loader_password
+DB_APP_AUTH_PASS=your_secure_auth_password
+MYSQL_ZABBIX_PASSWORD=your_secure_zabbix_password
+
+# ZABBIX & SNMP CREDENTIALS
+ZABBIX_USER=Admin
+ZABBIX_PASS=zabbix
+ZABBIX_SNMP_USER=your_snmp_user
+ZABBIX_SNMP_AUTHKEY=your_snmp_authkey
+ZABBIX_SNMP_PRIVKEY=your_snmp_privkey
+DISCORD_WEBHOOK=https://discord.com/api/webhooks/your_webhook_id
+
+# EMAIL ALERTS CONFIGURATION
+EMAIL_USER=your_email@gmail.com
+EMAIL_PASS=your_email_app_password
+
+# TAIGA CREDENTIALS
+TAIGA_URL=https://192.168.130.161/taiga
+TAIGA_USER=your_taiga_user
+TAIGA_PASS=your_taiga_password
+```"""
+
+# Remove the extra code fence that would follow the old cat block
+content = content.replace(f"```bash\n{old_step3}\n```", new_step3)
+
+# 2. Update Step 5 (model pulling command)
+old_step5 = "docker exec -it $(docker ps -q -f name=ollama) ollama pull qwen2.5:7b"
+new_step5 = "docker exec -it $(docker ps -q -f name=ollama) ollama pull $( grep '^[ \\t]*LLM_MODEL[ \t]*=' .env | sed 's/^.*=//' )"
+
+content = content.replace(old_step5, new_step5)
+content = content.replace("model **`qwen2.5:7b`**", "model")
+
+with open(file_path, "w", encoding="utf-8") as f:
+    f.write(content)
+
+print("INSTALL_WSL.md successfully updated and encoding sanitized.")

+ 36 - 0
scratch/remote_ingest_sample.py

@@ -0,0 +1,36 @@
+import os
+import paramiko
+import dotenv
+
+repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+dotenv.load_dotenv(dotenv_path=os.path.join(repo_root, ".env"))
+
+host = os.environ.get('SERVER_HOST')
+user = os.environ.get('SERVER_USER')
+password = os.environ.get('SERVER_PASS')
+if password == "your_db_password_here" or not password:
+    password = None
+
+ssh = paramiko.SSHClient()
+ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+
+try:
+    ssh.connect(host, username=user, password=password, timeout=10)
+    print("SSH Connected!")
+    
+    db_pass = os.environ.get('DB_LOADER_PASS') or 'your_db_loader_pass'
+    cmd_ingest = f"docker exec -e DB_HOST=mysql -e DB_USER=food_loader -e DB_PASS={db_pass} -e PYTHONIOENCODING=utf-8 food_project-app-1 python -X utf8 /app/ingest_csv.py"
+    stdin, stdout, stderr = ssh.exec_command(cmd_ingest)
+    
+    # Capture outputs
+    out_ing = stdout.read().decode('utf-8', errors='ignore')
+    err_ing = stderr.read().decode('utf-8', errors='ignore')
+    
+    # Print safely
+    print("INGEST STDOUT:\n", out_ing.encode('ascii', 'replace').decode('ascii'))
+    print("INGEST STDERR:\n", err_ing.encode('ascii', 'replace').decode('ascii'))
+    
+except Exception as e:
+    print(f"Error: {e}")
+finally:
+    ssh.close()

+ 64 - 0
scratch/remote_lock_free_indexes.py

@@ -0,0 +1,64 @@
+import os
+import sys
+import paramiko
+import dotenv
+import time
+
+sys.stdout.reconfigure(encoding='utf-8')
+sys.stderr.reconfigure(encoding='utf-8')
+
+repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+dotenv.load_dotenv(dotenv_path=os.path.join(repo_root, ".env"))
+
+host = os.environ.get('SERVER_HOST')
+user = os.environ.get('SERVER_USER')
+password = os.environ.get('SERVER_PASS')
+if password == "your_db_password_here" or not password:
+    password = None
+
+ssh = paramiko.SSHClient()
+ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+
+try:
+    ssh.connect(host, username=user, password=password, timeout=10)
+    print("SSH Connected!")
+    
+    # 1. Stop app container to release all locks
+    print("Stopping app container...")
+    stdin, stdout, stderr = ssh.exec_command("cd food_project && docker-compose stop app")
+    stdout.read()
+    
+    # 2. Restart MySQL to drop any blocked index creation queries
+    print("Restarting MySQL...")
+    stdin, stdout, stderr = ssh.exec_command("cd food_project && docker-compose restart mysql")
+    stdout.read()
+    
+    print("Waiting 10 seconds for MySQL to be healthy...")
+    time.sleep(10)
+    
+    # 3. Create full-text indexes
+    queries = [
+        "ALTER TABLE food_db.products_core ADD FULLTEXT INDEX idx_prod_ing (product_name, ingredients_text);",
+        "ALTER TABLE food_db.products_core ADD FULLTEXT INDEX idx_prod_name (product_name);",
+        "ALTER TABLE food_db.products_core ADD FULLTEXT INDEX idx_ing_text (ingredients_text);"
+    ]
+    
+    db_pass = os.environ.get('MYSQL_ROOT_PASSWORD') or 'your_mysql_root_pass'
+    for q in queries:
+        print(f"Executing: {q}")
+        cmd = f"docker exec food_project-mysql-1 mysql -uroot -p{db_pass} -e '{q}'"
+        stdin, stdout, stderr = ssh.exec_command(cmd)
+        print("STDOUT:\n", stdout.read().decode('utf-8'))
+        print("STDERR:\n", stderr.read().decode('utf-8'))
+        
+    # 4. Start app container back up
+    print("Starting app container...")
+    stdin, stdout, stderr = ssh.exec_command("cd food_project && docker-compose start app")
+    stdout.read()
+    
+    print("Index creation completed successfully!")
+    
+except Exception as e:
+    print(f"Error: {e}")
+finally:
+    ssh.close()

+ 95 - 0
zabbix_telemetry.py

@@ -0,0 +1,95 @@
+#!/usr/bin/env python3
+#ident "@(#)$Format:LocalFoodAI_lanfr144:zabbix_telemetry.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$""
+import os
+import sys
+import pymysql
+from dotenv import load_dotenv
+
+def get_db_connection():
+    current_dir = os.path.dirname(os.path.abspath(__file__))
+    env_path = os.path.join(current_dir, '.env')
+    load_dotenv(dotenv_path=env_path)
+    
+    # Try environment variables from compose or env
+    db_host = os.environ.get('DB_HOST', '127.0.0.1')
+    db_user = os.environ.get('DB_USER') or os.environ.get('DB_LOADER_USER') or 'food_loader'
+    db_pass = os.environ.get('DB_PASS') or os.environ.get('DB_LOADER_PASS') or 'your_db_loader_pass'
+    
+    # Check if .env config parameters exist
+    if os.path.exists(env_path):
+        with open(env_path, 'r') as f:
+            for line in f:
+                if line.strip().startswith('MYSQL_ROOT_PASSWORD'):
+                    db_pass = line.split('=')[1].strip()
+                    db_user = 'root'
+                elif line.strip().startswith('DB_LOADER_PASS') and db_user != 'root':
+                    db_pass = line.split('=')[1].strip()
+                    db_user = 'food_loader'
+                    
+    # Shifted MySQL Port check (if WSL or local offset)
+    db_port = 3306
+    if os.environ.get('NETWORK_MODE') == 'local' or os.path.exists(os.path.join(current_dir, 'docker-compose-wsl.yml')):
+        db_port = 3326
+        
+    return pymysql.connect(
+        host=db_host,
+        user=db_user,
+        password=db_pass,
+        database='food_db',
+        port=db_port,
+        cursorclass=pymysql.cursors.DictCursor
+    )
+
+def main():
+    if len(sys.argv) < 2:
+        print("Usage: python zabbix_telemetry.py [status|rows|start|end]")
+        sys.exit(1)
+        
+    metric = sys.argv[1].lower()
+    
+    try:
+        conn = get_db_connection()
+    except Exception as e:
+        if metric == 'status':
+            print("OFFLINE")
+        elif metric == 'rows':
+            print("0")
+        else:
+            print("N/A")
+        sys.exit(0)
+        
+    try:
+        with conn.cursor() as cursor:
+            # Query the last ingestion run details
+            cursor.execute("""
+                SELECT status, start_time, end_time, rows_loaded 
+                FROM ingestion_status 
+                ORDER BY id DESC LIMIT 1
+            """)
+            row = cursor.fetchone()
+            
+            if not row:
+                if metric == 'status': print("NO_DATA")
+                elif metric == 'rows': print("0")
+                else: print("N/A")
+                sys.exit(0)
+                
+            if metric == 'status':
+                print(row['status'])
+            elif metric == 'rows':
+                print(row['rows_loaded'])
+            elif metric == 'start':
+                print(row['start_time'].strftime("%Y-%m-%d %H:%M:%S") if row['start_time'] else "N/A")
+            elif metric == 'end':
+                print(row['end_time'].strftime("%Y-%m-%d %H:%M:%S") if row['end_time'] else "N/A")
+            else:
+                print("INVALID_METRIC")
+    except Exception as e:
+        if metric == 'status': print("ERROR")
+        elif metric == 'rows': print("-1")
+        else: print(f"Error: {e}")
+    finally:
+        conn.close()
+
+if __name__ == "__main__":
+    main()