Initial commit: JARVIS AI dashboard v2.3

- 4-tier chat: HA control → Ollama → Groq → Claude
- Push-based agent system with heartbeat/metrics
- Network monitoring, alerts, Proxmox, Home Assistant
- Windows + Linux agent installers
- Stats cache cron, facts collector, KB engine
This commit is contained in:
2026-05-25 13:22:57 +00:00
commit dc55e6c45b
27 changed files with 5835 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
Options -Indexes
RewriteEngine On
# Route all /api/* requests to api.php
RewriteCond %{REQUEST_URI} ^/api(/|$)
RewriteRule ^api(/.*)?$ api.php [QSA,L]
# Everything else serves static files or index.html
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
+122
View File
@@ -0,0 +1,122 @@
#!/bin/bash
# JARVIS Agent Installer — macOS
# Usage: bash install-mac.sh --jarvis-url https://jarvis.orbishosting.com --key YOUR_KEY
set -e
JARVIS_URL=""
REG_KEY=""
INSTALL_DIR="$HOME/.jarvis-agent"
CONFIG_DIR="$HOME/.jarvis-agent"
PLIST_PATH="$HOME/Library/LaunchAgents/com.jarvis.agent.plist"
SERVICE_LABEL="com.jarvis.agent"
while [[ $# -gt 0 ]]; do
case "$1" in
--jarvis-url) JARVIS_URL="$2"; shift 2 ;;
--key) REG_KEY="$2"; shift 2 ;;
*) echo "Unknown arg: $1"; exit 1 ;;
esac
done
if [[ -z "$JARVIS_URL" ]]; then
read -rp "JARVIS URL (e.g. https://jarvis.orbishosting.com): " JARVIS_URL
fi
if [[ -z "$REG_KEY" ]]; then
read -rp "Registration key: " REG_KEY
fi
JARVIS_URL="${JARVIS_URL%/}"
# Check for Python3
PYTHON3=$(command -v python3 2>/dev/null || command -v /usr/bin/python3 2>/dev/null || "")
if [[ -z "$PYTHON3" ]]; then
echo "Python 3 is required. Install it with:"
echo " brew install python3"
echo " or download from https://www.python.org/downloads/"
exit 1
fi
echo "Using Python: $PYTHON3 ($($PYTHON3 --version))"
mkdir -p "$INSTALL_DIR"
# Download or copy agent script
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [[ -f "$SCRIPT_DIR/jarvis-agent.py" ]]; then
cp "$SCRIPT_DIR/jarvis-agent.py" "$INSTALL_DIR/jarvis-agent.py"
else
echo "Downloading agent..."
curl -sSL "https://raw.githubusercontent.com/myronblair/jarvis/master/agent/jarvis-agent.py" \
-o "$INSTALL_DIR/jarvis-agent.py"
fi
chmod +x "$INSTALL_DIR/jarvis-agent.py"
HOSTNAME=$(hostname -f 2>/dev/null || hostname)
# Write config
if [[ ! -f "$CONFIG_DIR/config.json" ]]; then
cat > "$CONFIG_DIR/config.json" << JSONEOF
{
"jarvis_url": "$JARVIS_URL",
"registration_key": "$REG_KEY",
"hostname": "$HOSTNAME",
"agent_type": "linux",
"poll_interval": 30,
"heartbeat_every": 10,
"watch_services": []
}
JSONEOF
chmod 600 "$CONFIG_DIR/config.json"
fi
# Override state path in agent for macOS
STATE_PATH="$INSTALL_DIR/state.json"
# Write launchd plist
mkdir -p "$HOME/Library/LaunchAgents"
cat > "$PLIST_PATH" << PLISTEOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>$SERVICE_LABEL</string>
<key>ProgramArguments</key>
<array>
<string>$PYTHON3</string>
<string>$INSTALL_DIR/jarvis-agent.py</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>JARVIS_CONFIG</key>
<string>$CONFIG_DIR/config.json</string>
<key>JARVIS_STATE</key>
<string>$STATE_PATH</string>
</dict>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>$INSTALL_DIR/jarvis-agent.log</string>
<key>StandardErrorPath</key>
<string>$INSTALL_DIR/jarvis-agent.log</string>
</dict>
</plist>
PLISTEOF
# Load the service
launchctl unload "$PLIST_PATH" 2>/dev/null || true
launchctl load "$PLIST_PATH"
sleep 2
if launchctl list | grep -q "$SERVICE_LABEL"; then
echo ""
echo "✓ JARVIS Agent installed and running."
echo " View logs: tail -f $INSTALL_DIR/jarvis-agent.log"
echo " Config: $CONFIG_DIR/config.json"
echo " Stop: launchctl unload $PLIST_PATH"
else
echo "⚠ Agent installed but not running. Check logs:"
echo " tail -f $INSTALL_DIR/jarvis-agent.log"
fi
+152
View File
@@ -0,0 +1,152 @@
# JARVIS Agent Installer — Windows (PowerShell)
# Run as Administrator:
# Set-ExecutionPolicy Bypass -Scope Process
# .\install-windows.ps1 -JarvisUrl https://jarvis.orbishosting.com -Key YOUR_KEY
# Or one-liner (from PowerShell as Admin):
# irm https://jarvis.orbishosting.com/agent/install-windows.ps1 | iex
param(
[string]$JarvisUrl = "https://jarvis.orbishosting.com",
[string]$Key = "",
[string]$AgentName = $env:COMPUTERNAME.ToLower()
)
$ErrorActionPreference = "Stop"
$InstallDir = "C:\ProgramData\jarvis-agent"
$AgentScript = "$InstallDir\jarvis-agent.py"
$ConfigFile = "$InstallDir\config.json"
$TaskName = "JARVIS-Agent"
Write-Host ""
Write-Host " ====================================" -ForegroundColor Cyan
Write-Host " JARVIS Agent Installer v2.2 " -ForegroundColor Cyan
Write-Host " ====================================" -ForegroundColor Cyan
Write-Host ""
# ── Require admin ─────────────────────────────────────────────────────────────
if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
Write-Error "Run PowerShell as Administrator and try again."
}
# ── Prompt if not provided ────────────────────────────────────────────────────
$JarvisUrl = $JarvisUrl.TrimEnd("/")
if (-not $Key) { $Key = Read-Host "Enter registration key" }
# ── Find Python 3 ─────────────────────────────────────────────────────────────
Write-Host "Checking Python 3..." -NoNewline
$python = $null
$searchPaths = @(
"python", "python3", "py",
"$env:LOCALAPPDATA\Programs\Python\Python312\python.exe",
"$env:LOCALAPPDATA\Programs\Python\Python311\python.exe",
"$env:LOCALAPPDATA\Programs\Python\Python310\python.exe",
"C:\Python312\python.exe", "C:\Python311\python.exe"
)
foreach ($p in $searchPaths) {
try {
$ver = & $p --version 2>&1
if ("$ver" -match "Python 3") { $python = $p; break }
} catch {}
}
if (-not $python) {
Write-Host " not found." -ForegroundColor Yellow
Write-Host "Installing Python 3.12 via winget..." -ForegroundColor Yellow
try {
winget install Python.Python.3.12 --silent --accept-package-agreements --accept-source-agreements
$env:PATH = [System.Environment]::GetEnvironmentVariable("PATH","Machine") + ";" +
[System.Environment]::GetEnvironmentVariable("PATH","User")
foreach ($p in @("python","python3")) {
try { $ver = & $p --version 2>&1; if ("$ver" -match "Python 3") { $python = $p; break } } catch {}
}
} catch {}
}
if (-not $python) { Write-Error "Python 3 not found. Install from https://python.org then re-run." }
# Resolve full path for task scheduler (PS 5.1 compatible — no ?. operator)
$_pyCmd = Get-Command $python -ErrorAction SilentlyContinue
$pythonPath = if ($_pyCmd) { $_pyCmd.Source } else { $null }
if (-not $pythonPath -or -not (Test-Path $pythonPath)) {
foreach ($p in @("$env:LOCALAPPDATA\Programs\Python\Python312\python.exe",
"$env:LOCALAPPDATA\Programs\Python\Python311\python.exe",
"C:\Python312\python.exe")) {
if (Test-Path $p) { $pythonPath = $p; break }
}
}
if (-not $pythonPath) { $pythonPath = $python }
Write-Host " $pythonPath" -ForegroundColor Green
# ── Create install directory ──────────────────────────────────────────────────
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
Write-Host "Install dir: $InstallDir"
# ── Download Windows agent script ─────────────────────────────────────────────
Write-Host "Downloading jarvis-agent-windows.py..." -NoNewline
try {
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true }
$wc = New-Object System.Net.WebClient
$wc.Headers.Add("User-Agent", "JARVIS-Installer/1.0")
$wc.DownloadFile("$JarvisUrl/agent/jarvis-agent-windows.py", $AgentScript)
Write-Host " done." -ForegroundColor Green
} catch {
Write-Error "Download failed from $JarvisUrl/agent/jarvis-agent-windows.py`nError: $_"
}
# ── Write config ──────────────────────────────────────────────────────────────
$agentId = "${AgentName}_windows"
$config = [ordered]@{
jarvis_url = $JarvisUrl
host_header = ""
ssl_verify = $true
registration_key = $Key
agent_type = "windows"
hostname = $AgentName
agent_id = $agentId
poll_interval = 30
heartbeat_every = 10
watch_services = @("WinDefend", "Spooler")
} | ConvertTo-Json -Depth 3
[System.IO.File]::WriteAllText($ConfigFile, $config, [System.Text.UTF8Encoding]::new($false))
Write-Host "Config: $ConfigFile"
# ── Register scheduled task ───────────────────────────────────────────────────
try { Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue } catch {}
$action = New-ScheduledTaskAction -Execute "`"$pythonPath`"" `
-Argument "`"$AgentScript`"" -WorkingDirectory $InstallDir
$trigger = New-ScheduledTaskTrigger -AtStartup
$settings = New-ScheduledTaskSettingsSet `
-ExecutionTimeLimit (New-TimeSpan -Seconds 0) `
-RestartCount 10 -RestartInterval (New-TimeSpan -Minutes 1) `
-StartWhenAvailable -MultipleInstances IgnoreNew
# Run as current user (not SYSTEM) so per-user Python install is accessible
$currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
$principal = New-ScheduledTaskPrincipal -UserId $currentUser -LogonType Interactive -RunLevel Highest
Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger `
-Settings $settings -Principal $principal `
-Description "JARVIS AI System Monitoring Agent" -Force | Out-Null
Write-Host "Scheduled task '$TaskName' registered." -ForegroundColor Green
# ── Start now ─────────────────────────────────────────────────────────────────
Write-Host "Starting agent..." -NoNewline
Start-ScheduledTask -TaskName $TaskName
Start-Sleep -Seconds 3
$state = (Get-ScheduledTask -TaskName $TaskName).State
Write-Host " $state" -ForegroundColor $(if ($state -eq "Running") {"Green"} else {"Yellow"})
Write-Host ""
Write-Host " Installation complete!" -ForegroundColor Green
Write-Host " Machine : $AgentName ($agentId)" -ForegroundColor White
Write-Host " JARVIS : $JarvisUrl" -ForegroundColor White
Write-Host " Logs : $InstallDir\jarvis-agent.log" -ForegroundColor White
Write-Host ""
Write-Host " Useful commands:" -ForegroundColor Gray
Write-Host " Get-Content '$InstallDir\jarvis-agent.log' -Tail 20 -Wait" -ForegroundColor Gray
Write-Host " Stop-ScheduledTask -TaskName '$TaskName'" -ForegroundColor Gray
Write-Host " Start-ScheduledTask -TaskName '$TaskName'" -ForegroundColor Gray
Write-Host " Unregister-ScheduledTask -TaskName '$TaskName' -Confirm:`$false" -ForegroundColor Gray
Write-Host ""
+117
View File
@@ -0,0 +1,117 @@
#!/bin/bash
# JARVIS Agent Installer
# Usage: curl -sSL https://raw.githubusercontent.com/myronblair/jarvis/master/agent/install.sh | sudo bash
# Or: sudo bash install.sh --jarvis-url https://jarvis.orbishosting.com --key YOUR_REGISTRATION_KEY
set -e
JARVIS_URL=""
REG_KEY=""
AGENT_TYPE="linux"
INSTALL_DIR="/opt/jarvis-agent"
CONFIG_DIR="/etc/jarvis-agent"
STATE_DIR="/var/lib/jarvis-agent"
SERVICE_NAME="jarvis-agent"
# ── Parse args ────────────────────────────────────────────────────────────────
while [[ $# -gt 0 ]]; do
case "$1" in
--jarvis-url) JARVIS_URL="$2"; shift 2 ;;
--key) REG_KEY="$2"; shift 2 ;;
--type) AGENT_TYPE="$2"; shift 2 ;;
*) echo "Unknown arg: $1"; exit 1 ;;
esac
done
# ── Interactive prompts if not provided ──────────────────────────────────────
if [[ -z "$JARVIS_URL" ]]; then
read -rp "JARVIS URL (e.g. https://jarvis.orbishosting.com): " JARVIS_URL
fi
if [[ -z "$REG_KEY" ]]; then
read -rp "Registration key: " REG_KEY
fi
JARVIS_URL="${JARVIS_URL%/}"
echo ""
echo "Installing JARVIS Agent..."
echo " URL: $JARVIS_URL"
echo " Type: $AGENT_TYPE"
echo ""
# ── Install Python3 if needed ─────────────────────────────────────────────────
if ! command -v python3 &>/dev/null; then
echo "Installing python3..."
apt-get update -qq && apt-get install -y python3 || yum install -y python3 || dnf install -y python3
fi
# ── Create directories ────────────────────────────────────────────────────────
mkdir -p "$INSTALL_DIR" "$CONFIG_DIR" "$STATE_DIR"
# ── Copy agent script ─────────────────────────────────────────────────────────
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [[ -f "$SCRIPT_DIR/jarvis-agent.py" ]]; then
cp "$SCRIPT_DIR/jarvis-agent.py" "$INSTALL_DIR/jarvis-agent.py"
else
echo "Downloading agent script..."
curl -sSL "https://raw.githubusercontent.com/myronblair/jarvis/master/agent/jarvis-agent.py" \
-o "$INSTALL_DIR/jarvis-agent.py"
fi
chmod +x "$INSTALL_DIR/jarvis-agent.py"
# ── Write config ──────────────────────────────────────────────────────────────
HOSTNAME=$(hostname -f 2>/dev/null || hostname)
if [[ -f "$CONFIG_DIR/config.json" ]]; then
echo "Config already exists at $CONFIG_DIR/config.json — skipping (keeping existing settings)."
else
cat > "$CONFIG_DIR/config.json" << JSONEOF
{
"jarvis_url": "$JARVIS_URL",
"registration_key": "$REG_KEY",
"hostname": "$HOSTNAME",
"agent_type": "$AGENT_TYPE",
"poll_interval": 30,
"heartbeat_every": 10,
"watch_services": ["ollama", "homeassistant", "mysql", "mariadb", "nginx", "apache2", "docker"]
}
JSONEOF
chmod 600 "$CONFIG_DIR/config.json"
echo "Config written to $CONFIG_DIR/config.json"
fi
# ── Write systemd service ─────────────────────────────────────────────────────
cat > "/etc/systemd/system/${SERVICE_NAME}.service" << SVCEOF
[Unit]
Description=JARVIS Monitoring Agent
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/bin/python3 $INSTALL_DIR/jarvis-agent.py
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
SVCEOF
# ── Enable and start ──────────────────────────────────────────────────────────
systemctl daemon-reload
systemctl enable "$SERVICE_NAME"
systemctl restart "$SERVICE_NAME"
sleep 2
if systemctl is-active --quiet "$SERVICE_NAME"; then
echo ""
echo "✓ JARVIS Agent installed and running."
echo " View logs: journalctl -u $SERVICE_NAME -f"
echo " Config: $CONFIG_DIR/config.json"
else
echo ""
echo "⚠ Agent installed but not running. Check logs:"
echo " journalctl -u $SERVICE_NAME -n 30"
fi
+346
View File
@@ -0,0 +1,346 @@
#!/usr/bin/env python3
"""
JARVIS Agent for Windows — system monitor that reports metrics to JARVIS HUD.
Install via PowerShell: iwr https://jarvis.orbishosting.com/agent/install-windows.ps1 | iex
Config: C:\\ProgramData\\jarvis-agent\\config.json
Logs: C:\\ProgramData\\jarvis-agent\\jarvis-agent.log
"""
import json
import os
import platform
import socket
import subprocess
import sys
import time
import urllib.request
import urllib.error
import uuid
from datetime import datetime, timezone
from pathlib import Path
INSTALL_DIR = Path(r"C:\ProgramData\jarvis-agent")
CONFIG_PATH = INSTALL_DIR / "config.json"
STATE_PATH = INSTALL_DIR / "state.json"
LOG_PATH = INSTALL_DIR / "jarvis-agent.log"
AGENT_VERSION = "2.2"
# ── Logging ────────────────────────────────────────────────────────────────────
_log_file = None
def log(msg: str):
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
line = f"[{ts}] {msg}"
print(line, flush=True)
try:
with open(LOG_PATH, "a", encoding="utf-8") as f:
f.write(line + "\n")
except Exception:
pass
# ── Config ─────────────────────────────────────────────────────────────────────
def load_config() -> dict:
if not CONFIG_PATH.exists():
print(f"[ERROR] Config not found at {CONFIG_PATH}. Run the installer first.")
sys.exit(1)
with open(CONFIG_PATH, encoding="utf-8-sig") as f:
return json.load(f)
def load_state() -> dict:
if STATE_PATH.exists():
with open(STATE_PATH, encoding="utf-8") as f:
return json.load(f)
return {}
def save_state(state: dict):
INSTALL_DIR.mkdir(parents=True, exist_ok=True)
with open(STATE_PATH, "w", encoding="utf-8") as f:
json.dump(state, f, indent=2)
# ── HTTP ───────────────────────────────────────────────────────────────────────
import ssl as _ssl
def _make_ssl_ctx(verify: bool):
if not verify:
ctx = _ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = _ssl.CERT_NONE
return ctx
return None
_host_header: str = ""
def api_post(url: str, payload: dict, headers: dict = {}, timeout: int = 15,
ssl_verify: bool = True) -> dict:
body = json.dumps(payload).encode()
req = urllib.request.Request(url, data=body, method="POST")
req.add_header("Content-Type", "application/json")
req.add_header("User-Agent", "JARVIS-Agent-Windows/1.0")
if _host_header:
req.add_header("Host", _host_header)
for k, v in headers.items():
req.add_header(k, v)
try:
ctx = _make_ssl_ctx(ssl_verify)
with urllib.request.urlopen(req, timeout=timeout, context=ctx) as resp:
return json.loads(resp.read().decode())
except urllib.error.HTTPError as e:
return {"error": f"HTTP {e.code}: {e.read().decode()[:200]}"}
except Exception as e:
return {"error": str(e)}
def api_get(url: str, headers: dict = {}, timeout: int = 10,
ssl_verify: bool = True) -> dict:
req = urllib.request.Request(url)
req.add_header("User-Agent", "JARVIS-Agent-Windows/1.0")
if _host_header:
req.add_header("Host", _host_header)
for k, v in headers.items():
req.add_header(k, v)
try:
ctx = _make_ssl_ctx(ssl_verify)
with urllib.request.urlopen(req, timeout=timeout, context=ctx) as resp:
return json.loads(resp.read().decode())
except Exception as e:
return {"error": str(e)}
# ── Metrics ────────────────────────────────────────────────────────────────────
def _ps(script: str, timeout: int = 8) -> str:
"""Run a PowerShell one-liner and return stdout."""
try:
r = subprocess.run(
["powershell", "-NoProfile", "-NonInteractive", "-Command", script],
capture_output=True, text=True, timeout=timeout
)
return r.stdout.strip()
except Exception:
return ""
def get_local_ip() -> str:
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
ip = s.getsockname()[0]
s.close()
return ip
except Exception:
return "unknown"
_last_cpu_counters = None
def get_cpu_percent() -> float:
global _last_cpu_counters
try:
out = _ps("(Get-CimInstance Win32_Processor | Measure-Object -Property LoadPercentage -Average).Average")
return round(float(out), 1)
except Exception:
return 0.0
def get_memory() -> dict:
try:
out = _ps("$o=Get-CimInstance Win32_OperatingSystem; [PSCustomObject]@{total=$o.TotalVisibleMemorySize;free=$o.FreePhysicalMemory}|ConvertTo-Json")
d = json.loads(out)
total_kb = int(d.get("total", 0))
free_kb = int(d.get("free", 0))
used_kb = total_kb - free_kb
if total_kb == 0:
return {}
return {
"total_mb": round(total_kb / 1024, 1),
"used_mb": round(used_kb / 1024, 1),
"free_mb": round(free_kb / 1024, 1),
"percent": round(used_kb / total_kb * 100, 1),
}
except Exception:
return {}
def get_disk() -> list:
try:
out = _ps("Get-PSDrive -PSProvider FileSystem | Where-Object{$_.Used -ne $null} | Select-Object Name,@{n='used';e={[math]::Round($_.Used/1GB,2)}},@{n='free';e={[math]::Round($_.Free/1GB,2)}} | ConvertTo-Json")
if not out:
return []
items = json.loads(out)
if isinstance(items, dict):
items = [items]
disks = []
for d in items:
used = float(d.get("used", 0))
free = float(d.get("free", 0))
total = used + free
pct = round(used / total * 100, 1) if total else 0
disks.append({
"mount": d.get("Name", "?") + ":\\",
"size": f"{round(total, 1)}G",
"used": f"{used}G",
"avail": f"{free}G",
"percent": str(int(pct)),
})
return disks
except Exception:
return []
def get_uptime() -> dict:
try:
out = _ps("(Get-Date) - (gcim Win32_OperatingSystem).LastBootUpTime | Select-Object -ExpandProperty TotalSeconds")
secs = float(out)
days = int(secs // 86400)
hours = int((secs % 86400) // 3600)
minutes = int((secs % 3600) // 60)
return {"seconds": int(secs), "days": days, "hours": hours, "minutes": minutes,
"human": f"{days}d {hours}h {minutes}m"}
except Exception:
return {}
def get_services(cfg: dict) -> list:
watch = cfg.get("watch_services", ["WinDefend", "wuauserv", "Spooler"])
statuses = []
for svc in watch:
try:
out = _ps(f"(Get-Service -Name '{svc}' -ErrorAction SilentlyContinue).Status")
status = "active" if out.lower() == "running" else (out.lower() or "unknown")
statuses.append({"service": svc, "status": status})
except Exception:
statuses.append({"service": svc, "status": "unknown"})
return statuses
def detect_capabilities(cfg: dict) -> list:
caps = ["metrics", "commands"]
if Path(r"C:\Program Files\Docker\Docker\Docker Desktop.exe").exists():
caps.append("docker")
return caps
def collect_metrics(cfg: dict) -> dict:
return {
"hostname": cfg.get("hostname", socket.gethostname()),
"cpu_percent": get_cpu_percent(),
"memory": get_memory(),
"disk": get_disk(),
"uptime": get_uptime(),
"load": [0, 0, 0],
"services": get_services(cfg),
"platform": "Windows",
"timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
}
# ── Registration ───────────────────────────────────────────────────────────────
def register(cfg: dict, state: dict) -> str:
hostname = cfg.get("hostname", socket.gethostname().lower())
agent_type = cfg.get("agent_type", "linux")
ip = get_local_ip()
capabilities = detect_capabilities(cfg)
agent_id = cfg.get("agent_id", f"{hostname}_{hostname[:8]}")
ssl_verify = bool(cfg.get("ssl_verify", True))
log(f"[JARVIS] Registering as '{agent_id}' from {ip}...")
result = api_post(
f"{cfg['jarvis_url']}/api/agent/register",
{"hostname": hostname, "agent_type": agent_type, "ip_address": ip,
"capabilities": capabilities, "agent_id": agent_id},
headers={"X-Registration-Key": cfg["registration_key"]},
ssl_verify=ssl_verify,
)
if "error" in result:
log(f"[ERROR] Registration failed: {result['error']}")
return ""
api_key = result.get("api_key", "")
if api_key:
state["api_key"] = api_key
state["agent_id"] = result.get("agent_id", agent_id)
save_state(state)
log(f"[JARVIS] Registered. agent_id={state['agent_id']}")
return api_key
# ── Command execution ──────────────────────────────────────────────────────────
def execute_command(cmd: dict, cfg: dict) -> dict:
cmd_type = cmd.get("command_type", "")
cmd_data = cmd.get("command_data", {})
try:
if cmd_type == "ping":
host = cmd_data.get("host", "8.8.8.8")
r = subprocess.run(["ping", "-n", "3", host], capture_output=True, text=True, timeout=15)
return {"success": r.returncode == 0, "output": r.stdout}
elif cmd_type == "update":
log("[CMD] Self-update requested")
return {"success": True, "message": "Windows agent self-update not implemented"}
elif cmd_type == "shell":
if not cmd_data.get("allowed", False):
return {"success": False, "error": "Shell commands not enabled"}
cmd_str = cmd_data.get("command", "")
r = subprocess.run(["powershell", "-NoProfile", "-Command", cmd_str],
capture_output=True, text=True, timeout=30)
return {"success": True, "stdout": r.stdout[:2000], "stderr": r.stderr[:500]}
else:
return {"success": False, "error": f"Unknown command type: {cmd_type}"}
except subprocess.TimeoutExpired:
return {"success": False, "error": "Command timed out"}
except Exception as e:
return {"success": False, "error": str(e)}
# ── Main loop ──────────────────────────────────────────────────────────────────
def main():
global _host_header
cfg = load_config()
state = load_state()
jarvis_url = cfg["jarvis_url"].rstrip("/")
ssl_verify = bool(cfg.get("ssl_verify", True))
_host_header = cfg.get("host_header", "")
poll_interval = int(cfg.get("poll_interval", 30))
heartbeat_every = int(cfg.get("heartbeat_every", 10))
api_key = state.get("api_key", "")
if not api_key:
api_key = register(cfg, state)
if not api_key:
log("[ERROR] Could not register with JARVIS. Retrying in 60s...")
time.sleep(60)
main()
return
headers = {"X-Agent-Key": api_key}
last_metrics = 0
log(f"[JARVIS] Agent v{AGENT_VERSION} (Windows) running. Connecting to {jarvis_url} every {heartbeat_every}s.")
while True:
now = time.time()
try:
hb = api_post(f"{jarvis_url}/api/agent/heartbeat", {}, headers, ssl_verify=ssl_verify)
if "error" in hb:
log(f"[WARN] Heartbeat failed: {hb['error']}")
else:
for cmd in hb.get("commands", []):
log(f"[CMD] Executing: {cmd['command_type']}")
result = execute_command(cmd, cfg)
api_post(f"{jarvis_url}/api/agent/command_result",
{"command_id": cmd["id"], "success": result.get("success", False), "result": result},
headers, ssl_verify=ssl_verify)
if now - last_metrics >= poll_interval:
metrics = collect_metrics(cfg)
r = api_post(f"{jarvis_url}/api/agent/metrics",
{"system": metrics}, headers, ssl_verify=ssl_verify)
if "error" not in r:
last_metrics = now
except Exception as e:
log(f"[ERROR] Loop error: {e}")
time.sleep(heartbeat_every)
if __name__ == "__main__":
main()
+454
View File
@@ -0,0 +1,454 @@
#!/usr/bin/env python3
"""
JARVIS Agent — lightweight system monitor for Linux machines.
Registers with JARVIS, reports metrics, and executes commands.
Install: sudo bash /opt/jarvis-agent/install.sh
Config: /etc/jarvis-agent/config.json
Logs: journalctl -u jarvis-agent -f
"""
import json
import os
import platform
import socket
import subprocess
import sys
import time
import urllib.request
import urllib.error
import uuid
from datetime import datetime
from pathlib import Path
CONFIG_PATH = "/etc/jarvis-agent/config.json"
STATE_PATH = "/var/lib/jarvis-agent/state.json"
AGENT_VERSION = "2.3" # bumped on each release
# ── Config helpers ────────────────────────────────────────────────────────────
def load_config() -> dict:
if not os.path.exists(CONFIG_PATH):
print(f"[ERROR] Config not found at {CONFIG_PATH}. Run the installer first.", flush=True)
sys.exit(1)
with open(CONFIG_PATH) as f:
return json.load(f)
def load_state() -> dict:
if os.path.exists(STATE_PATH):
with open(STATE_PATH) as f:
return json.load(f)
return {}
def save_state(state: dict):
Path(STATE_PATH).parent.mkdir(parents=True, exist_ok=True)
with open(STATE_PATH, "w") as f:
json.dump(state, f, indent=2)
# ── HTTP helpers ──────────────────────────────────────────────────────────────
import ssl as _ssl
def _make_ssl_ctx(verify: bool) -> _ssl.SSLContext | None:
if not verify:
ctx = _ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = _ssl.CERT_NONE
return ctx
return None
_host_header: str = "" # set from config at startup
def api_post(url: str, payload: dict, headers: dict = {}, timeout: int = 15,
ssl_verify: bool = True) -> dict:
body = json.dumps(payload).encode()
req = urllib.request.Request(url, data=body, method="POST")
req.add_header("Content-Type", "application/json")
req.add_header("User-Agent", "JARVIS-Agent/1.0")
if _host_header:
req.add_header("Host", _host_header)
for k, v in headers.items():
req.add_header(k, v)
try:
ctx = _make_ssl_ctx(ssl_verify)
with urllib.request.urlopen(req, timeout=timeout, context=ctx) as resp:
return json.loads(resp.read().decode())
except urllib.error.HTTPError as e:
return {"error": f"HTTP {e.code}: {e.read().decode()[:200]}"}
except Exception as e:
return {"error": str(e)}
def api_get(url: str, headers: dict = {}, timeout: int = 10,
ssl_verify: bool = True) -> dict:
req = urllib.request.Request(url)
req.add_header("User-Agent", "JARVIS-Agent/1.0")
if _host_header:
req.add_header("Host", _host_header)
for k, v in headers.items():
req.add_header(k, v)
try:
ctx = _make_ssl_ctx(ssl_verify)
with urllib.request.urlopen(req, timeout=timeout, context=ctx) as resp:
return json.loads(resp.read().decode())
except Exception as e:
return {"error": str(e)}
# ── Registration ──────────────────────────────────────────────────────────────
def get_local_ip() -> str:
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
ip = s.getsockname()[0]
s.close()
return ip
except Exception:
return "unknown"
def detect_capabilities(cfg: dict) -> list:
caps = ["metrics", "commands"]
# Check for Proxmox
if os.path.exists("/usr/bin/pvesh") or os.path.exists("/usr/sbin/pveversion"):
caps.append("proxmox")
# Check for Docker
if os.path.exists("/usr/bin/docker") or os.path.exists("/usr/local/bin/docker"):
caps.append("docker")
# Check for Ollama
if os.path.exists("/usr/local/bin/ollama") or os.path.exists("/usr/bin/ollama"):
caps.append("ollama")
# Check for Home Assistant
if os.path.exists("/etc/homeassistant") or os.path.exists("/config/configuration.yaml"):
caps.append("homeassistant")
return caps
def register(cfg: dict, state: dict) -> str:
"""Register with JARVIS. Returns api_key."""
hostname = cfg.get("hostname", socket.gethostname())
agent_type = cfg.get("agent_type", "linux")
ip = get_local_ip()
capabilities = detect_capabilities(cfg)
agent_id = cfg.get("agent_id", f"{hostname}_{socket.gethostname()[:8]}")
ssl_verify = bool(cfg.get("ssl_verify", True))
print(f"[JARVIS] Registering as '{agent_id}' ({agent_type}) from {ip}...", flush=True)
result = api_post(
f"{cfg['jarvis_url']}/api/agent/register",
{
"hostname": hostname,
"agent_type": agent_type,
"ip_address": ip,
"capabilities": capabilities,
"agent_id": agent_id,
},
headers={"X-Registration-Key": cfg["registration_key"]},
ssl_verify=ssl_verify,
)
if "error" in result:
print(f"[ERROR] Registration failed: {result['error']}", flush=True)
return ""
api_key = result.get("api_key", "")
if api_key:
state["api_key"] = api_key
state["agent_id"] = result.get("agent_id", agent_id)
save_state(state)
print(f"[JARVIS] Registered. agent_id={state['agent_id']}", flush=True)
return api_key
# ── Metrics collection ────────────────────────────────────────────────────────
def read_cpu_percent() -> float:
try:
with open("/proc/stat") as f:
line = f.readline()
fields = list(map(int, line.split()[1:]))
idle = fields[3]
total = sum(fields)
return round((1 - idle / total) * 100, 1) if total else 0.0
except Exception:
return 0.0
_last_cpu = None
def get_cpu_percent() -> float:
global _last_cpu
try:
with open("/proc/stat") as f:
line = f.readline()
fields = list(map(int, line.split()[1:]))
idle = fields[3] + fields[4] # idle + iowait
total = sum(fields)
if _last_cpu:
d_idle = idle - _last_cpu[0]
d_total = total - _last_cpu[1]
result = round((1 - d_idle / d_total) * 100, 1) if d_total else 0.0
else:
result = 0.0
_last_cpu = (idle, total)
return result
except Exception:
return 0.0
def get_memory() -> dict:
mem = {}
try:
with open("/proc/meminfo") as f:
for line in f:
parts = line.split()
if parts[0] in ("MemTotal:", "MemAvailable:", "MemFree:", "Buffers:", "Cached:"):
mem[parts[0].rstrip(":")] = int(parts[1])
total = mem.get("MemTotal", 0)
available = mem.get("MemAvailable", 0)
used = total - available
return {
"total_mb": round(total / 1024, 1),
"used_mb": round(used / 1024, 1),
"free_mb": round(available / 1024, 1),
"percent": round(used / total * 100, 1) if total else 0,
}
except Exception:
return {}
def get_disk() -> list:
disks = []
try:
result = subprocess.run(["df", "-h", "--output=source,fstype,size,used,avail,pcent,target"],
capture_output=True, text=True, timeout=5)
lines = result.stdout.strip().split("\n")[1:]
for line in lines:
parts = line.split()
if len(parts) >= 7:
mount = parts[6]
if not any(mount.startswith(x) for x in ["/sys", "/proc", "/dev/pts", "/run", "/snap"]):
disks.append({
"mount": mount,
"size": parts[2],
"used": parts[3],
"avail": parts[4],
"percent": parts[5].rstrip("%"),
})
except Exception:
pass
return disks
def get_uptime() -> dict:
try:
with open("/proc/uptime") as f:
secs = float(f.read().split()[0])
days = int(secs // 86400)
hours = int((secs % 86400) // 3600)
minutes = int((secs % 3600) // 60)
return {"seconds": int(secs), "days": days, "hours": hours, "minutes": minutes,
"human": f"{days}d {hours}h {minutes}m"}
except Exception:
return {}
def get_services(cfg: dict) -> list:
watch = cfg.get("watch_services", ["ollama", "homeassistant", "mysql", "nginx", "apache2"])
statuses = []
for svc in watch:
try:
r = subprocess.run(["systemctl", "is-active", svc], capture_output=True, text=True, timeout=3)
statuses.append({"service": svc, "status": r.stdout.strip()})
except Exception:
statuses.append({"service": svc, "status": "unknown"})
return statuses
def get_load() -> list:
try:
with open("/proc/loadavg") as f:
parts = f.read().split()
return [float(parts[0]), float(parts[1]), float(parts[2])]
except Exception:
return [0, 0, 0]
def collect_metrics(cfg: dict) -> dict:
# First reading for CPU delta
get_cpu_percent()
time.sleep(1)
return {
"hostname": cfg.get("hostname", socket.gethostname()),
"cpu_percent": get_cpu_percent(),
"memory": get_memory(),
"disk": get_disk(),
"uptime": get_uptime(),
"load": get_load(),
"services": get_services(cfg),
"platform": platform.system(),
"timestamp": datetime.utcnow().isoformat() + "Z",
}
# ── Proxmox metrics ───────────────────────────────────────────────────────────
def collect_proxmox_metrics(cfg: dict) -> dict | None:
try:
result = subprocess.run(
["pvesh", "get", "/nodes/pve/status", "--output-format", "json"],
capture_output=True, text=True, timeout=10
)
node_status = json.loads(result.stdout)
vms_result = subprocess.run(
["pvesh", "get", "/nodes/pve/qemu", "--output-format", "json"],
capture_output=True, text=True, timeout=10
)
vms = json.loads(vms_result.stdout)
return {"node": node_status, "vms": vms}
except Exception as e:
return {"error": str(e)}
# ── Command execution ─────────────────────────────────────────────────────────
def execute_command(cmd: dict) -> dict:
cmd_type = cmd.get("command_type", "")
cmd_data = cmd.get("command_data", {})
try:
if cmd_type == "restart_service":
svc = cmd_data.get("service", "")
if not svc or "/" in svc:
return {"success": False, "error": "Invalid service name"}
r = subprocess.run(["systemctl", "restart", svc], capture_output=True, text=True, timeout=30)
return {"success": r.returncode == 0, "stdout": r.stdout, "stderr": r.stderr}
elif cmd_type == "get_logs":
svc = cmd_data.get("service", "")
lines = min(int(cmd_data.get("lines", 50)), 200)
if not svc or "/" in svc:
return {"success": False, "error": "Invalid service name"}
r = subprocess.run(["journalctl", "-u", svc, "-n", str(lines), "--no-pager"],
capture_output=True, text=True, timeout=15)
return {"success": True, "output": r.stdout}
elif cmd_type == "ping":
host = cmd_data.get("host", "8.8.8.8")
r = subprocess.run(["ping", "-c", "3", "-W", "2", host], capture_output=True, text=True, timeout=15)
return {"success": r.returncode == 0, "output": r.stdout}
elif cmd_type == "update":
updated = self_update(cfg)
return {"success": True, "updated": updated}
elif cmd_type == "shell":
# Only allow if explicitly enabled in config
if not cmd_data.get("allowed", False):
return {"success": False, "error": "Shell commands not enabled"}
cmd_str = cmd_data.get("command", "")
r = subprocess.run(cmd_str, shell=True, capture_output=True, text=True, timeout=30)
return {"success": True, "stdout": r.stdout[:2000], "stderr": r.stderr[:500]}
else:
return {"success": False, "error": f"Unknown command type: {cmd_type}"}
except subprocess.TimeoutExpired:
return {"success": False, "error": "Command timed out"}
except Exception as e:
return {"success": False, "error": str(e)}
# ── Main loop ─────────────────────────────────────────────────────────────────
def main():
global _host_header
cfg = load_config()
state = load_state()
jarvis_url = cfg["jarvis_url"].rstrip("/")
ssl_verify = bool(cfg.get("ssl_verify", True))
_host_header = cfg.get("host_header", "")
poll_interval = int(cfg.get("poll_interval", 30))
heartbeat_every = int(cfg.get("heartbeat_every", 10))
# Register if no API key yet
api_key = state.get("api_key", "")
if not api_key:
api_key = register(cfg, state)
if not api_key:
print("[ERROR] Could not register with JARVIS. Retrying in 60s...", flush=True)
time.sleep(60)
main()
return
headers = {"X-Agent-Key": api_key}
last_metrics = 0
last_update_chk = 0
update_interval = int(cfg.get("update_check_hours", 24)) * 3600
tick = 0
print(f"[JARVIS] Agent v{AGENT_VERSION} running. Polling {jarvis_url} every {heartbeat_every}s.", flush=True)
while True:
tick += 1
now = time.time()
try:
# Heartbeat + get commands
hb = api_post(f"{jarvis_url}/api/agent/heartbeat", {}, headers, ssl_verify=ssl_verify)
if "error" in hb:
print(f"[WARN] Heartbeat failed: {hb['error']}", flush=True)
else:
commands = hb.get("commands", [])
for cmd in commands:
print(f"[CMD] Executing: {cmd['command_type']}", flush=True)
result = execute_command(cmd)
api_post(f"{jarvis_url}/api/agent/command_result",
{"command_id": cmd["id"], "success": result.get("success", False), "result": result},
headers, ssl_verify=ssl_verify)
# Self-update check (every update_interval seconds, default 24h)
if now - last_update_chk >= update_interval:
last_update_chk = now
self_update(cfg) # restarts process if update found
# Push metrics every poll_interval seconds
if now - last_metrics >= poll_interval:
metrics = collect_metrics(cfg)
api_post(f"{jarvis_url}/api/agent/metrics",
{"type": "system", "data": metrics}, headers, ssl_verify=ssl_verify)
# Proxmox metrics if available
if "proxmox" in detect_capabilities(cfg):
px = collect_proxmox_metrics(cfg)
if px:
api_post(f"{jarvis_url}/api/agent/metrics",
{"type": "proxmox", "data": px}, headers, ssl_verify=ssl_verify)
last_metrics = now
except Exception as e:
print(f"[ERROR] Loop error: {e}", flush=True)
time.sleep(heartbeat_every)
# ── Self-update ────────────────────────────────────────────────────────────────
def self_update(cfg: dict) -> bool:
"""Check JARVIS server for a newer version of this script. If different, replace and restart."""
jarvis_url = cfg.get("jarvis_url", "").rstrip("/")
default_update_url = f"{jarvis_url}/agent/jarvis-agent.py" if jarvis_url else ""
update_url = cfg.get("update_url", default_update_url)
if not update_url:
return False
script_path = os.path.abspath(__file__)
try:
req = urllib.request.Request(update_url)
req.add_header("User-Agent", "JARVIS-Agent/1.0")
with urllib.request.urlopen(req, timeout=30) as resp:
new_content = resp.read()
with open(script_path, "rb") as f:
current = f.read()
if new_content != current:
print(f"[JARVIS] Update available — replacing {script_path} and restarting...", flush=True)
with open(script_path, "wb") as f:
f.write(new_content)
os.execv(sys.executable, [sys.executable] + sys.argv)
return True
return False
except Exception as e:
print(f"[JARVIS] Self-update check failed: {e}", flush=True)
return False
if __name__ == "__main__":
main()
+36
View File
@@ -0,0 +1,36 @@
# Kill any stale Task Scheduler approach
Unregister-ScheduledTask -TaskName 'JARVIS-Agent' -Confirm:$false -ErrorAction SilentlyContinue
# Create a VBScript launcher (runs Python silently, no console window)
$vbs = 'Set WShell = CreateObject("WScript.Shell")' + "`r`n" +
'WShell.Run """C:\Users\myron\AppData\Local\Programs\Python\Python312\pythonw.exe"" ""C:\ProgramData\jarvis-agent\jarvis-agent.py""", 0, False'
[System.IO.File]::WriteAllText('C:\ProgramData\jarvis-agent\start-agent.vbs', $vbs, [System.Text.ASCIIEncoding]::new())
# Add to user startup folder so it runs at every login
$startupDir = "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup"
Copy-Item 'C:\ProgramData\jarvis-agent\start-agent.vbs' "$startupDir\JARVIS-Agent.vbs" -Force
Write-Host "Startup entry created: $startupDir\JARVIS-Agent.vbs" -ForegroundColor Green
# Kill any existing python process running the agent
Get-Process python*, pythonw* -ErrorAction SilentlyContinue | Where-Object {$_.CommandLine -like '*jarvis-agent*'} | Stop-Process -Force -ErrorAction SilentlyContinue
# Launch now
Write-Host "Starting agent..." -ForegroundColor Cyan
Start-Process 'C:\Users\myron\AppData\Local\Programs\Python\Python312\pythonw.exe' -ArgumentList 'C:\ProgramData\jarvis-agent\jarvis-agent.py' -WorkingDirectory 'C:\ProgramData\jarvis-agent'
Start-Sleep -Seconds 4
# Confirm running
$proc = Get-Process pythonw -ErrorAction SilentlyContinue
if ($proc) {
Write-Host "Agent running — PID $($proc.Id)" -ForegroundColor Green
} else {
Write-Host "pythonw not found — check C:\ProgramData\jarvis-agent\jarvis-agent.log" -ForegroundColor Yellow
}
# Show log tail
Start-Sleep -Seconds 2
if (Test-Path 'C:\ProgramData\jarvis-agent\jarvis-agent.log') {
Write-Host "`nLog:" -ForegroundColor Cyan
Get-Content 'C:\ProgramData\jarvis-agent\jarvis-agent.log' -Tail 10
}
+90
View File
@@ -0,0 +1,90 @@
<?php
/**
* JARVIS API Router
*/
require_once __DIR__ . '/../api/config.php';
require_once __DIR__ . '/../api/lib/db.php';
require_once __DIR__ . '/../api/lib/kb_engine.php';
session_start();
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, X-Session-Token');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit; }
$uri = $_SERVER['REQUEST_URI'] ?? '/';
$method = $_SERVER['REQUEST_METHOD'];
$path = trim(parse_url($uri, PHP_URL_PATH), '/');
$parts = explode('/', $path);
// Remove 'api' prefix if present
if (($parts[0] ?? '') === 'api') array_shift($parts);
$endpoint = $parts[0] ?? '';
$action = $parts[1] ?? '';
// Auth check (except login and ping)
if ($endpoint !== 'auth' && $endpoint !== 'agent') {
$token = $_SESSION['jarvis_token'] ?? ($_SERVER['HTTP_X_SESSION_TOKEN'] ?? '');
if (empty($token) || $token !== ($_SESSION['jarvis_token'] ?? '')) {
$localIP = $_SERVER['REMOTE_ADDR'] ?? '';
$isLocal = in_array($localIP, ['127.0.0.1', '::1', JARVIS_IP]);
if (!$isLocal && $endpoint !== 'ping') {
http_response_code(401);
echo json_encode(['error' => 'Unauthorized', 'code' => 401]);
exit;
}
}
}
if ($endpoint !== 'auth') session_write_close(); // Skip for auth so login can write session token
$body = file_get_contents('php://input');
$data = json_decode($body, true) ?? [];
switch ($endpoint) {
case 'ping':
echo json_encode(['status' => 'online', 'time' => date('c'), 'codename' => JARVIS_CODENAME]);
break;
case 'auth':
require __DIR__ . '/../api/endpoints/auth.php';
break;
case 'chat':
require __DIR__ . '/../api/endpoints/chat.php';
break;
case 'system':
require __DIR__ . '/../api/endpoints/system.php';
break;
case 'network':
require __DIR__ . '/../api/endpoints/network.php';
break;
case 'proxmox':
require __DIR__ . '/../api/endpoints/proxmox.php';
break;
case 'ha':
require __DIR__ . '/../api/endpoints/ha.php';
break;
case 'do':
require __DIR__ . '/../api/endpoints/do_server.php';
break;
case 'alerts':
require __DIR__ . '/../api/endpoints/alerts.php';
break;
case 'facts':
require __DIR__ . '/../api/endpoints/facts_collector.php';
break;
case 'weather':
require __DIR__ . '/../api/endpoints/weather.php';
break;
case 'news':
require __DIR__ . '/../api/endpoints/news.php';
break;
case "agent":
require __DIR__ . '/../api/endpoints/agent.php';
break;
default:
http_response_code(404);
echo json_encode(['error' => 'Unknown endpoint: ' . $endpoint]);
}
File diff suppressed because it is too large Load Diff