mirror of
https://github.com/myronblair/jarvis
synced 2026-07-29 17:22:35 -05:00
Compare commits
69 Commits
84cd2ded50
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| ee0e116963 | |||
| adbd1a7a24 | |||
| 73aac8ab01 | |||
| 646da21b86 | |||
| d97a345672 | |||
| 141191bcdd | |||
| 8399048252 | |||
| 652e44f7b3 | |||
| 43be3e1105 | |||
| 3d16061709 | |||
| 80588efa7a | |||
| f7309a15fc | |||
| 18783dc137 | |||
| 8f6be645ed | |||
| 185d2c889f | |||
| b5c47d898b | |||
| ffc01c3d4a | |||
| 156b210184 | |||
| 09f73edb0b | |||
| a4336ebdd6 | |||
| e7f555fff1 | |||
| 8034fc67e9 | |||
| 48b2a8523a | |||
| 5ff4f3e311 | |||
| 24b96e809c | |||
| 8567018cc3 | |||
| f8a095f783 | |||
| e060ff0c63 | |||
| 588cfe3f10 | |||
| 3db329e925 | |||
| c01805316a | |||
| 20b510186a | |||
| 9b5e16660a | |||
| 7020d445b3 | |||
| 80b0dfc583 | |||
| 30e2bc093c | |||
| a29670e93d | |||
| 10350cbcd8 | |||
| 24bc876c1d | |||
| cfb5b2a3f9 | |||
| 0b1a19d9de | |||
| 66a5443f22 | |||
| e5536b077c | |||
| a292411d52 | |||
| 26b501b600 | |||
| 7327104612 | |||
| a13f750846 | |||
| 3c8dd8e206 | |||
| 554488aefa | |||
| e99c3aa171 | |||
| 2528b37ea1 | |||
| 46dabe3c31 | |||
| d6a1fc9456 | |||
| 2f74b98bbc | |||
| ed15ff12dd | |||
| 3f18cec739 | |||
| 8911645c20 | |||
| af03a2f2d8 | |||
| dfc92a6791 | |||
| 05522edb1d | |||
| 435af8ccc9 | |||
| a9ea75db98 | |||
| 651455cb47 | |||
| 90e4ded7c9 | |||
| c1275d47a6 | |||
| 08fbfaa3e4 | |||
| 1f25b5d04d | |||
| 874f6e8c5c | |||
| 42a82c40cb |
@@ -1,4 +1,5 @@
|
|||||||
# Credentials - never commit
|
# Credentials - never commit
|
||||||
|
public_html/admin/downloads/INFRASTRUCTURE-REFERENCE.md
|
||||||
api/config.php
|
api/config.php
|
||||||
backup/
|
backup/
|
||||||
|
|
||||||
|
|||||||
+17
-38
@@ -6,20 +6,26 @@
|
|||||||
.DESCRIPTION
|
.DESCRIPTION
|
||||||
Installs JARVIS Agent as a Windows Service that auto-starts at boot.
|
Installs JARVIS Agent as a Windows Service that auto-starts at boot.
|
||||||
Requires: PowerShell 5.1+, internet access, and Administrator rights.
|
Requires: PowerShell 5.1+, internet access, and Administrator rights.
|
||||||
|
No Python installation needed — this installs the standalone .exe build.
|
||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
# Interactive install (prompts for registration key):
|
# Interactive install (prompts for registration key):
|
||||||
irm https://jarvis.orbishosting.com/agent/install-windows.ps1 | iex
|
irm https://jarvis.orbishosting.com:1972/agent/install-windows.ps1 | iex
|
||||||
|
|
||||||
# Silent install with key:
|
# Silent install with key:
|
||||||
$env:JARVIS_REG_KEY='your_key_here'; irm https://jarvis.orbishosting.com/agent/install-windows.ps1 | iex
|
$env:JARVIS_REG_KEY='your_key_here'; irm https://jarvis.orbishosting.com:1972/agent/install-windows.ps1 | iex
|
||||||
#>
|
#>
|
||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
$JARVIS_URL = 'https://jarvis.orbishosting.com'
|
# Fixed 2026-07-07: jarvis.orbishosting.com on the default port (80/443) is not
|
||||||
|
# reachable from outside the LAN at all (no FortiGate VIP forwards it) — every
|
||||||
|
# external install using the old default URL would have failed outright. Port
|
||||||
|
# 1972 is the confirmed-working external path (same fix applied to the GitHub
|
||||||
|
# webhook the same day).
|
||||||
|
$JARVIS_URL = 'http://jarvis.orbishosting.com:1972'
|
||||||
$INSTALL_DIR = 'C:\ProgramData\jarvis-agent'
|
$INSTALL_DIR = 'C:\ProgramData\jarvis-agent'
|
||||||
$SERVICE_NAME = 'JARVISAgent'
|
$SERVICE_NAME = 'JARVISAgent'
|
||||||
$AGENT_SCRIPT = "$INSTALL_DIR\jarvis-agent-windows.py"
|
$AGENT_EXE = "$INSTALL_DIR\jarvis-agent-windows.exe"
|
||||||
$CONFIG_FILE = "$INSTALL_DIR\config.json"
|
$CONFIG_FILE = "$INSTALL_DIR\config.json"
|
||||||
|
|
||||||
function Write-Step { param($msg) Write-Host "`n[JARVIS] $msg" -ForegroundColor Cyan }
|
function Write-Step { param($msg) Write-Host "`n[JARVIS] $msg" -ForegroundColor Cyan }
|
||||||
@@ -39,49 +45,23 @@ if ($existing) {
|
|||||||
Start-Sleep 2
|
Start-Sleep 2
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
& python "$INSTALL_DIR\jarvis-agent-windows.py" remove 2>$null
|
if (Test-Path $AGENT_EXE) { & $AGENT_EXE remove 2>$null }
|
||||||
} catch {}
|
} catch {}
|
||||||
Write-OK "Existing service removed."
|
Write-OK "Existing service removed."
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Check / install Python ────────────────────────────────────────────────────
|
|
||||||
Write-Step "Checking Python..."
|
|
||||||
$py = Get-Command python -ErrorAction SilentlyContinue
|
|
||||||
if (-not $py) {
|
|
||||||
Write-Host " Python not found. Installing via winget..." -ForegroundColor Yellow
|
|
||||||
if (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
|
|
||||||
Write-Fail "winget not available. Please install Python 3.11+ from https://python.org and re-run."
|
|
||||||
}
|
|
||||||
winget install -e --id Python.Python.3.11 --silent --accept-package-agreements --accept-source-agreements
|
|
||||||
$env:PATH = [System.Environment]::GetEnvironmentVariable("PATH","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("PATH","User")
|
|
||||||
$py = Get-Command python -ErrorAction SilentlyContinue
|
|
||||||
if (-not $py) { Write-Fail "Python install failed. Please install manually from https://python.org" }
|
|
||||||
}
|
|
||||||
$pyVersion = & python --version 2>&1
|
|
||||||
Write-OK $pyVersion
|
|
||||||
|
|
||||||
# ── Install pywin32 ───────────────────────────────────────────────────────────
|
|
||||||
Write-Step "Checking pywin32..."
|
|
||||||
$checkWin32 = & python -c "import win32service; print('ok')" 2>&1
|
|
||||||
if ($checkWin32 -ne 'ok') {
|
|
||||||
Write-Host " Installing pywin32..." -ForegroundColor Yellow
|
|
||||||
& python -m pip install --quiet pywin32
|
|
||||||
& python -m pywin32_postinstall -install 2>$null
|
|
||||||
Write-OK "pywin32 installed."
|
|
||||||
} else {
|
|
||||||
Write-OK "pywin32 already installed."
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Create install dir ────────────────────────────────────────────────────────
|
# ── Create install dir ────────────────────────────────────────────────────────
|
||||||
Write-Step "Creating install directory..."
|
Write-Step "Creating install directory..."
|
||||||
New-Item -ItemType Directory -Path $INSTALL_DIR -Force | Out-Null
|
New-Item -ItemType Directory -Path $INSTALL_DIR -Force | Out-Null
|
||||||
Write-OK $INSTALL_DIR
|
Write-OK $INSTALL_DIR
|
||||||
|
|
||||||
# ── Download agent script ─────────────────────────────────────────────────────
|
# ── Download agent exe ─────────────────────────────────────────────────────────
|
||||||
|
# No Python/pywin32 dependency anymore — this is a self-contained PyInstaller
|
||||||
|
# build with everything it needs bundled in.
|
||||||
Write-Step "Downloading JARVIS agent..."
|
Write-Step "Downloading JARVIS agent..."
|
||||||
try {
|
try {
|
||||||
Invoke-WebRequest -Uri "$JARVIS_URL/agent/jarvis-agent-windows.py" -OutFile $AGENT_SCRIPT -UseBasicParsing
|
Invoke-WebRequest -Uri "$JARVIS_URL/agent/jarvis-agent-windows.exe" -OutFile $AGENT_EXE -UseBasicParsing
|
||||||
Write-OK "Agent downloaded to $AGENT_SCRIPT"
|
Write-OK "Agent downloaded to $AGENT_EXE"
|
||||||
} catch {
|
} catch {
|
||||||
Write-Fail "Failed to download agent: $_"
|
Write-Fail "Failed to download agent: $_"
|
||||||
}
|
}
|
||||||
@@ -121,8 +101,7 @@ Write-OK "Config written to $CONFIG_FILE"
|
|||||||
|
|
||||||
# ── Install Windows Service ───────────────────────────────────────────────────
|
# ── Install Windows Service ───────────────────────────────────────────────────
|
||||||
Write-Step "Installing Windows service..."
|
Write-Step "Installing Windows service..."
|
||||||
$pyPath = (Get-Command python).Source
|
& $AGENT_EXE --startup auto install
|
||||||
& $pyPath "$AGENT_SCRIPT" --startup auto install
|
|
||||||
if ($LASTEXITCODE -ne 0) { Write-Fail "Service install failed." }
|
if ($LASTEXITCODE -ne 0) { Write-Fail "Service install failed." }
|
||||||
Write-OK "Service '$SERVICE_NAME' installed."
|
Write-OK "Service '$SERVICE_NAME' installed."
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -35,7 +35,7 @@ INSTALL_DIR = Path(r"C:\ProgramData\jarvis-agent")
|
|||||||
CONFIG_PATH = INSTALL_DIR / "config.json"
|
CONFIG_PATH = INSTALL_DIR / "config.json"
|
||||||
STATE_PATH = INSTALL_DIR / "state.json"
|
STATE_PATH = INSTALL_DIR / "state.json"
|
||||||
LOG_PATH = INSTALL_DIR / "jarvis-agent.log"
|
LOG_PATH = INSTALL_DIR / "jarvis-agent.log"
|
||||||
AGENT_VERSION = "3.1"
|
AGENT_VERSION = "3.2"
|
||||||
|
|
||||||
# Set by the service wrapper so self_update knows to stop instead of exec
|
# Set by the service wrapper so self_update knows to stop instead of exec
|
||||||
_is_service = False
|
_is_service = False
|
||||||
@@ -92,7 +92,7 @@ def api_post(url: str, payload: dict, headers: dict = {}, timeout: int = 15,
|
|||||||
body = json.dumps(payload).encode()
|
body = json.dumps(payload).encode()
|
||||||
req = urllib.request.Request(url, data=body, method="POST")
|
req = urllib.request.Request(url, data=body, method="POST")
|
||||||
req.add_header("Content-Type", "application/json")
|
req.add_header("Content-Type", "application/json")
|
||||||
req.add_header("User-Agent", "JARVIS-Agent-Windows/3.0")
|
req.add_header("User-Agent", "JARVIS-Agent-Windows/3.2")
|
||||||
if _host_header:
|
if _host_header:
|
||||||
req.add_header("Host", _host_header)
|
req.add_header("Host", _host_header)
|
||||||
for k, v in headers.items():
|
for k, v in headers.items():
|
||||||
@@ -109,7 +109,7 @@ def api_post(url: str, payload: dict, headers: dict = {}, timeout: int = 15,
|
|||||||
def api_get(url: str, headers: dict = {}, timeout: int = 10,
|
def api_get(url: str, headers: dict = {}, timeout: int = 10,
|
||||||
ssl_verify: bool = True) -> dict:
|
ssl_verify: bool = True) -> dict:
|
||||||
req = urllib.request.Request(url)
|
req = urllib.request.Request(url)
|
||||||
req.add_header("User-Agent", "JARVIS-Agent-Windows/3.0")
|
req.add_header("User-Agent", "JARVIS-Agent-Windows/3.2")
|
||||||
if _host_header:
|
if _host_header:
|
||||||
req.add_header("Host", _host_header)
|
req.add_header("Host", _host_header)
|
||||||
for k, v in headers.items():
|
for k, v in headers.items():
|
||||||
@@ -375,18 +375,28 @@ def _sysinfo_snapshot() -> dict:
|
|||||||
# ── Self-update ────────────────────────────────────────────────────────────────
|
# ── Self-update ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def self_update(cfg: dict) -> bool:
|
def self_update(cfg: dict) -> bool:
|
||||||
|
# Added: supports both script-mode (plain .py, run via a system Python) and
|
||||||
|
# frozen-mode (standalone PyInstaller .exe — sys.frozen is set, __file__ isn't
|
||||||
|
# meaningful/writable the way it is for a real .py file on disk). A running
|
||||||
|
# .exe can't be overwritten in place on Windows, but CAN be renamed while
|
||||||
|
# running, so frozen mode uses a download-new/rename-old/rename-new swap
|
||||||
|
# instead of the direct overwrite the script-mode path uses.
|
||||||
jarvis_url = cfg.get("jarvis_url", "").rstrip("/")
|
jarvis_url = cfg.get("jarvis_url", "").rstrip("/")
|
||||||
default_update_url = f"{jarvis_url}/agent/jarvis-agent-windows.py" if jarvis_url else ""
|
is_frozen = bool(getattr(sys, "frozen", False))
|
||||||
|
if is_frozen:
|
||||||
|
default_update_url = f"{jarvis_url}/agent/jarvis-agent-windows.exe" if jarvis_url else ""
|
||||||
|
else:
|
||||||
|
default_update_url = f"{jarvis_url}/agent/jarvis-agent-windows.py" if jarvis_url else ""
|
||||||
update_url = cfg.get("update_url", default_update_url)
|
update_url = cfg.get("update_url", default_update_url)
|
||||||
if not update_url:
|
if not update_url:
|
||||||
return False
|
return False
|
||||||
script_path = os.path.abspath(__file__)
|
target_path = os.path.abspath(sys.executable) if is_frozen else os.path.abspath(__file__)
|
||||||
ssl_verify = bool(cfg.get("ssl_verify", True))
|
ssl_verify = bool(cfg.get("ssl_verify", True))
|
||||||
try:
|
try:
|
||||||
# Download expected hash
|
# Download expected hash
|
||||||
hash_url = update_url + ".sha256"
|
hash_url = update_url + ".sha256"
|
||||||
req_hash = urllib.request.Request(hash_url)
|
req_hash = urllib.request.Request(hash_url)
|
||||||
req_hash.add_header("User-Agent", "JARVIS-Agent-Windows/3.0")
|
req_hash.add_header("User-Agent", "JARVIS-Agent-Windows/3.2")
|
||||||
if _host_header:
|
if _host_header:
|
||||||
req_hash.add_header("Host", _host_header)
|
req_hash.add_header("Host", _host_header)
|
||||||
expected_hash = None
|
expected_hash = None
|
||||||
@@ -397,13 +407,13 @@ def self_update(cfg: dict) -> bool:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Download new script
|
# Download new script/exe
|
||||||
req = urllib.request.Request(update_url)
|
req = urllib.request.Request(update_url)
|
||||||
req.add_header("User-Agent", "JARVIS-Agent-Windows/3.0")
|
req.add_header("User-Agent", "JARVIS-Agent-Windows/3.2")
|
||||||
if _host_header:
|
if _host_header:
|
||||||
req.add_header("Host", _host_header)
|
req.add_header("Host", _host_header)
|
||||||
ctx = _make_ssl_ctx(ssl_verify)
|
ctx = _make_ssl_ctx(ssl_verify)
|
||||||
with urllib.request.urlopen(req, timeout=30, context=ctx) as resp:
|
with urllib.request.urlopen(req, timeout=60, context=ctx) as resp:
|
||||||
new_content = resp.read()
|
new_content = resp.read()
|
||||||
|
|
||||||
# Verify hash
|
# Verify hash
|
||||||
@@ -413,20 +423,42 @@ def self_update(cfg: dict) -> bool:
|
|||||||
log(f"Update hash mismatch (expected {expected_hash[:16]}… got {actual_hash[:16]}…) — aborting")
|
log(f"Update hash mismatch (expected {expected_hash[:16]}… got {actual_hash[:16]}…) — aborting")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
with open(script_path, "rb") as f:
|
with open(target_path, "rb") as f:
|
||||||
current = f.read()
|
current = f.read()
|
||||||
if new_content != current:
|
if new_content == current:
|
||||||
log(f"Update verified — replacing {script_path} and restarting...")
|
return False
|
||||||
with open(script_path, "wb") as f:
|
|
||||||
|
log(f"Update verified — replacing {target_path} and restarting...")
|
||||||
|
if is_frozen:
|
||||||
|
# Can't overwrite a running exe, but can rename it and drop the new
|
||||||
|
# one in its place; the old copy is cleaned up on the next update.
|
||||||
|
old_path = target_path + ".old"
|
||||||
|
new_path = target_path + ".new"
|
||||||
|
with open(new_path, "wb") as f:
|
||||||
f.write(new_content)
|
f.write(new_content)
|
||||||
if _is_service:
|
try:
|
||||||
# Signal the main loop to exit; SCM failure-recovery will restart us
|
if os.path.exists(old_path):
|
||||||
log("Running as service — stopping for SCM-managed restart after update.")
|
os.remove(old_path)
|
||||||
_stop_event.set()
|
except Exception:
|
||||||
else:
|
pass
|
||||||
os.execv(sys.executable, [sys.executable] + sys.argv)
|
os.rename(target_path, old_path)
|
||||||
return True
|
os.rename(new_path, target_path)
|
||||||
return False
|
else:
|
||||||
|
with open(target_path, "wb") as f:
|
||||||
|
f.write(new_content)
|
||||||
|
|
||||||
|
if _is_service:
|
||||||
|
# Signal the main loop to exit; SCM failure-recovery will restart us
|
||||||
|
log("Running as service — stopping for SCM-managed restart after update.")
|
||||||
|
_stop_event.set()
|
||||||
|
elif is_frozen:
|
||||||
|
# sys.argv[0] is already the exe's own path for a frozen app — don't
|
||||||
|
# prepend sys.executable again or the new process misreads its own
|
||||||
|
# path as a command-line argument.
|
||||||
|
os.execv(sys.executable, sys.argv)
|
||||||
|
else:
|
||||||
|
os.execv(sys.executable, [sys.executable] + sys.argv)
|
||||||
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log(f"Self-update check failed: {e}")
|
log(f"Self-update check failed: {e}")
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -281,7 +281,7 @@ def get_uptime() -> dict:
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
def get_services(cfg: dict) -> list:
|
def get_services(cfg: dict) -> list:
|
||||||
watch = cfg.get("watch_services", ["ollama", "homeassistant", "mysql", "nginx", "apache2"])
|
watch = cfg.get("watch_services", [])
|
||||||
statuses = []
|
statuses = []
|
||||||
for svc in watch:
|
for svc in watch:
|
||||||
try:
|
try:
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -175,7 +175,7 @@ def fetch_ha_states(cfg: dict) -> list:
|
|||||||
dt = datetime.fromisoformat(lc.replace("Z", "+00:00"))
|
dt = datetime.fromisoformat(lc.replace("Z", "+00:00"))
|
||||||
lc = dt.astimezone(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
lc = dt.astimezone(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
except Exception:
|
except Exception:
|
||||||
lc = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
|
lc = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
entities.append({
|
entities.append({
|
||||||
"entity_id": entity_id,
|
"entity_id": entity_id,
|
||||||
@@ -194,13 +194,11 @@ def main():
|
|||||||
heartbeat_every = int(cfg.get("heartbeat_every", 10))
|
heartbeat_every = int(cfg.get("heartbeat_every", 10))
|
||||||
|
|
||||||
api_key = state.get("api_key", "")
|
api_key = state.get("api_key", "")
|
||||||
if not api_key:
|
while not api_key:
|
||||||
api_key = register(cfg, state)
|
api_key = register(cfg, state)
|
||||||
if not api_key:
|
if not api_key:
|
||||||
log("Could not register. Retrying in 60s...")
|
log("Could not register. Retrying in 60s...")
|
||||||
time.sleep(60)
|
time.sleep(60)
|
||||||
main()
|
|
||||||
return
|
|
||||||
|
|
||||||
headers = {"X-Agent-Key": api_key}
|
headers = {"X-Agent-Key": api_key}
|
||||||
last_push = 0
|
last_push = 0
|
||||||
@@ -229,6 +227,7 @@ def main():
|
|||||||
last_push = now
|
last_push = now
|
||||||
else:
|
else:
|
||||||
log("No HA entities fetched (HA down or token invalid?)")
|
log("No HA entities fetched (HA down or token invalid?)")
|
||||||
|
last_push = now
|
||||||
|
|
||||||
time.sleep(heartbeat_every)
|
time.sleep(heartbeat_every)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# JARVIS Network Scanner — runs on PVE1, pushes nmap results to JARVIS
|
||||||
|
# Cron: */3 * * * * /usr/local/bin/jarvis-netscan.sh >/dev/null 2>&1
|
||||||
|
|
||||||
|
JARVIS_URL="http://10.48.200.211"
|
||||||
|
JARVIS_HOST="jarvis.orbishosting.com"
|
||||||
|
REG_KEY=$(cat /etc/jarvis-agent/reg-key 2>/dev/null)
|
||||||
|
if [ -z "$REG_KEY" ]; then
|
||||||
|
echo "$(date): ERROR: /etc/jarvis-agent/reg-key not found" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
SUBNET="10.48.200.0/24"
|
||||||
|
|
||||||
|
TMPFILE=$(mktemp)
|
||||||
|
nmap -sn "$SUBNET" 2>/dev/null > "$TMPFILE"
|
||||||
|
|
||||||
|
if [ ! -s "$TMPFILE" ]; then
|
||||||
|
echo "$(date): nmap produced no output" >&2
|
||||||
|
rm -f "$TMPFILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
JSON=$(python3 - "$TMPFILE" <<'PYEOF'
|
||||||
|
import sys, re, json
|
||||||
|
|
||||||
|
with open(sys.argv[1]) as f:
|
||||||
|
data = f.read()
|
||||||
|
|
||||||
|
devices = []
|
||||||
|
cur = None
|
||||||
|
|
||||||
|
for line in data.splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
m = re.match(r'Nmap scan report for (?:(\S+) \()?(\d+\.\d+\.\d+\.\d+)\)?', line)
|
||||||
|
if m:
|
||||||
|
if cur:
|
||||||
|
devices.append(cur)
|
||||||
|
hn = m.group(1) if m.group(1) and m.group(1) != m.group(2) else ''
|
||||||
|
cur = {'ip': m.group(2), 'hostname': hn, 'mac': '', 'vendor': ''}
|
||||||
|
elif cur:
|
||||||
|
m2 = re.match(r'MAC Address: ([0-9A-Fa-f:]{17}) \(([^)]+)\)', line)
|
||||||
|
if m2:
|
||||||
|
cur['mac'] = m2.group(1).lower()
|
||||||
|
cur['vendor'] = '' if m2.group(2) == 'Unknown' else m2.group(2)
|
||||||
|
|
||||||
|
if cur:
|
||||||
|
devices.append(cur)
|
||||||
|
|
||||||
|
print(json.dumps({'devices': devices}))
|
||||||
|
PYEOF
|
||||||
|
)
|
||||||
|
|
||||||
|
rm -f "$TMPFILE"
|
||||||
|
|
||||||
|
if [ -z "$JSON" ]; then
|
||||||
|
echo "$(date): JSON parse failed" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
RESPONSE=$(curl -sk --max-time 15 \
|
||||||
|
-X POST "$JARVIS_URL/api/netscan" \
|
||||||
|
-H "Host: $JARVIS_HOST" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-Registration-Key: $REG_KEY" \
|
||||||
|
-d "$JSON" 2>/dev/null)
|
||||||
|
|
||||||
|
echo "$(date): $RESPONSE"
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# JARVIS VoIP Phone Probe — runs every minute on PVE1
|
||||||
|
# Pings all Yealink phones + checks FusionPBX SIP registration (read-only)
|
||||||
|
# 200.3 is on an external FusionPBX — ping only, no SIP check
|
||||||
|
|
||||||
|
JARVIS_URL="http://10.48.200.211"
|
||||||
|
JARVIS_HOST="jarvis.orbishosting.com"
|
||||||
|
REG_KEY=$(cat /etc/jarvis-agent/reg-key 2>/dev/null)
|
||||||
|
if [ -z "$REG_KEY" ]; then
|
||||||
|
echo "$(date): ERROR: /etc/jarvis-agent/reg-key not found" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
FUSION_HOST="134.209.72.226"
|
||||||
|
|
||||||
|
# IP|alias|extension(none=skip SIP check)|mac
|
||||||
|
PHONES=(
|
||||||
|
"10.48.200.2|Yealink — Myron Main (Ext 1000)|1000|80:5e:c0:35:04:77"
|
||||||
|
"10.48.200.3|Yealink — United Mirror & Glass (External SIP)|none|c4:fc:22:28:63:71"
|
||||||
|
"10.48.200.43|Yealink T48S — Tommy Main (Ext 1001)|1001|80:5e:0c:15:0c:4f"
|
||||||
|
"10.48.200.86|Yealink — Myron Vanguard WiFi (Offline During Work Hrs)|none|"
|
||||||
|
"10.48.200.65|Yealink — Myron Vanguard Work (Ext 1003)|1003|c4:fc:22:13:e1:89"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get SIP registrations from FusionPBX (read-only)
|
||||||
|
REG_OUTPUT=$(ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 -o BatchMode=yes \
|
||||||
|
root@$FUSION_HOST "fs_cli -x 'show registrations'" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
# Collect results as TSV, delegate JSON building to python3 to avoid injection
|
||||||
|
RESULTS=""
|
||||||
|
for PHONE in "${PHONES[@]}"; do
|
||||||
|
IFS='|' read -r IP ALIAS EXT MAC <<< "$PHONE"
|
||||||
|
|
||||||
|
if ping -c 1 -W 2 "$IP" > /dev/null 2>&1; then
|
||||||
|
STATUS="online"
|
||||||
|
else
|
||||||
|
STATUS="offline"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$EXT" = "none" ]; then
|
||||||
|
SIP="external"
|
||||||
|
elif [ -n "$REG_OUTPUT" ] && echo "$REG_OUTPUT" | grep -q "^${EXT},"; then
|
||||||
|
SIP="registered"
|
||||||
|
else
|
||||||
|
SIP="unregistered"
|
||||||
|
fi
|
||||||
|
|
||||||
|
RESULTS="${RESULTS}${IP}\t${ALIAS}\t${MAC}\t${STATUS}\t${SIP}\t${EXT}\n"
|
||||||
|
done
|
||||||
|
|
||||||
|
JSON=$(printf "%b" "$RESULTS" | python3 -c "
|
||||||
|
import sys, json
|
||||||
|
devices = []
|
||||||
|
for line in sys.stdin:
|
||||||
|
line = line.rstrip('\n')
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
parts = line.split('\t')
|
||||||
|
if len(parts) < 6:
|
||||||
|
continue
|
||||||
|
ip, alias, mac, status, sip, ext = parts[:6]
|
||||||
|
devices.append({
|
||||||
|
'ip': ip, 'alias': alias, 'mac': mac,
|
||||||
|
'vendor': 'Yealink', 'status': status,
|
||||||
|
'sip_status': sip, 'extension': ext,
|
||||||
|
})
|
||||||
|
print(json.dumps({'devices': devices}))
|
||||||
|
")
|
||||||
|
|
||||||
|
curl -sk --max-time 10 \
|
||||||
|
-X POST "$JARVIS_URL/api/netscan" \
|
||||||
|
-H "Host: $JARVIS_HOST" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-Registration-Key: $REG_KEY" \
|
||||||
|
-d "$JSON" > /dev/null 2>&1
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
JARVIS Ping Probe — runs on PVE1 (10.48.200.90), which is on the LAN.
|
||||||
|
Pings devices that can't run the full agent, then calls JARVIS heartbeat
|
||||||
|
on their behalf so the dashboard shows live status.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import urllib.request
|
||||||
|
import urllib.error
|
||||||
|
import ssl
|
||||||
|
|
||||||
|
JARVIS_URL = "http://10.48.200.211"
|
||||||
|
HOST_HEADER = "jarvis.orbishosting.com"
|
||||||
|
|
||||||
|
# Devices to probe: agent_id → api_key
|
||||||
|
DEVICES = {
|
||||||
|
"fortigate_gw": "00103aea6fcbf837bc55e11b445a3620",
|
||||||
|
"yealink_t48s": "2bf8bd7ca8dd31c28fd16aa956e15f88",
|
||||||
|
"homeassistant_ha": "6f8077dee7a7b4af202bc80886f1223d",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Map agent_id → IP (for ping)
|
||||||
|
IPS = {
|
||||||
|
"fortigate_gw": "10.48.200.1",
|
||||||
|
"yealink_t48s": "10.48.200.43",
|
||||||
|
"homeassistant_ha": "10.48.200.97",
|
||||||
|
}
|
||||||
|
|
||||||
|
def ping(ip: str) -> bool:
|
||||||
|
result = subprocess.run(
|
||||||
|
["ping", "-c", "1", "-W", "2", ip],
|
||||||
|
capture_output=True, timeout=5
|
||||||
|
)
|
||||||
|
return result.returncode == 0
|
||||||
|
|
||||||
|
def heartbeat(agent_id: str, api_key: str, alive: bool):
|
||||||
|
# If device is down we still send heartbeat so JARVIS updates last_seen
|
||||||
|
# and sets status based on the alive flag via the metric payload
|
||||||
|
ctx = ssl.create_default_context()
|
||||||
|
ctx.check_hostname = False
|
||||||
|
ctx.verify_mode = ssl.CERT_NONE
|
||||||
|
|
||||||
|
payload = json.dumps({}).encode()
|
||||||
|
req = urllib.request.Request(
|
||||||
|
f"{JARVIS_URL}/api/agent/heartbeat",
|
||||||
|
data=payload, method="POST"
|
||||||
|
)
|
||||||
|
req.add_header("Content-Type", "application/json")
|
||||||
|
req.add_header("X-Agent-Key", api_key)
|
||||||
|
req.add_header("Host", HOST_HEADER)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=10, context=ctx):
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def update_status(agent_id: str, api_key: str, status: str):
|
||||||
|
"""Push a minimal metric so JARVIS knows if device is up or down."""
|
||||||
|
ctx = ssl.create_default_context()
|
||||||
|
ctx.check_hostname = False
|
||||||
|
ctx.verify_mode = ssl.CERT_NONE
|
||||||
|
|
||||||
|
payload = json.dumps({
|
||||||
|
"type": "system",
|
||||||
|
"data": {
|
||||||
|
"hostname": agent_id,
|
||||||
|
"cpu_percent": 0,
|
||||||
|
"ping_only": True,
|
||||||
|
"ping_status": status,
|
||||||
|
}
|
||||||
|
}).encode()
|
||||||
|
req = urllib.request.Request(
|
||||||
|
f"{JARVIS_URL}/api/agent/metrics",
|
||||||
|
data=payload, method="POST"
|
||||||
|
)
|
||||||
|
req.add_header("Content-Type", "application/json")
|
||||||
|
req.add_header("X-Agent-Key", api_key)
|
||||||
|
req.add_header("Host", HOST_HEADER)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=10, context=ctx):
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def main():
|
||||||
|
for agent_id, api_key in DEVICES.items():
|
||||||
|
ip = IPS.get(agent_id, "")
|
||||||
|
alive = ping(ip) if ip else False
|
||||||
|
status = "online" if alive else "offline"
|
||||||
|
print(f"{agent_id} ({ip}): {status}", flush=True)
|
||||||
|
heartbeat(agent_id, api_key, alive)
|
||||||
|
update_status(agent_id, api_key, status)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -18,13 +18,13 @@ define(chr(39)+'CLAUDE_MODEL'.chr(39), chr(39)+'claude-sonnet-4-6'.chr(39))
|
|||||||
define(chr(39)+'CLAUDE_MAX_TOKENS'.chr(39), 1024);
|
define(chr(39)+'CLAUDE_MAX_TOKENS'.chr(39), 1024);
|
||||||
|
|
||||||
define(chr(39)+'GROQ_API_KEY'.chr(39), chr(39)+'gsk_...'.chr(39));
|
define(chr(39)+'GROQ_API_KEY'.chr(39), chr(39)+'gsk_...'.chr(39));
|
||||||
define(chr(39)+'GROQ_MODEL_SEARCH'.chr(39), chr(39)+'groq/compound-mini'.chr(39));
|
define(chr(39)+'GROQ_MODEL_SEARCH'.chr(39), chr(39)+'compound-beta-mini'.chr(39));
|
||||||
define(chr(39)+'GROQ_MODEL_GENERAL'.chr(39), chr(39)+'llama-3.3-70b-versatile'.chr(39));
|
define(chr(39)+'GROQ_MODEL_GENERAL'.chr(39), chr(39)+'llama-3.3-70b-versatile'.chr(39));
|
||||||
define(chr(39)+'GROQ_TIMEOUT'.chr(39), 30);
|
define(chr(39)+'GROQ_TIMEOUT'.chr(39), 30);
|
||||||
|
|
||||||
define(chr(39)+'OLLAMA_HOST'.chr(39), chr(39)+'http://10.48.200.95:11434'.chr(39));
|
define(chr(39)+'OLLAMA_HOST'.chr(39), chr(39)+'http://10.48.200.210:11434'.chr(39));
|
||||||
define(chr(39)+'OLLAMA_MODEL_PRIMARY'.chr(39), chr(39)+'llama3.2:1b'.chr(39));
|
define(chr(39)+'OLLAMA_MODEL_PRIMARY'.chr(39), chr(39)+'llama3.1:8b'.chr(39));
|
||||||
define(chr(39)+'OLLAMA_MODEL_HEAVY'.chr(39), chr(39)+'llama3.1:70b'.chr(39));
|
define(chr(39)+'OLLAMA_MODEL_HEAVY'.chr(39), chr(39)+'llama3.1:8b'.chr(39));
|
||||||
define(chr(39)+'OLLAMA_TIMEOUT'.chr(39), 90);
|
define(chr(39)+'OLLAMA_TIMEOUT'.chr(39), 90);
|
||||||
|
|
||||||
define(chr(39)+'LOCAL_SUBNET'.chr(39), chr(39)+'10.48.200'.chr(39));
|
define(chr(39)+'LOCAL_SUBNET'.chr(39), chr(39)+'10.48.200'.chr(39));
|
||||||
@@ -47,8 +47,11 @@ define(chr(39)+'HA_TOKEN'.chr(39), chr(39)+'YOUR_HA_LONG_LIVED_TOKEN'.chr(39));
|
|||||||
define(chr(39)+'SESSION_LIFETIME'.chr(39), 86400 * 7);
|
define(chr(39)+'SESSION_LIFETIME'.chr(39), 86400 * 7);
|
||||||
define(chr(39)+'SITE_URL'.chr(39), chr(39)+'https://jarvis.orbishosting.com'.chr(39));
|
define(chr(39)+'SITE_URL'.chr(39), chr(39)+'https://jarvis.orbishosting.com'.chr(39));
|
||||||
|
|
||||||
error_reporting(0);
|
error_reporting(E_ALL);
|
||||||
ini_set(chr(39)+'display_errors'.chr(39), 0);
|
ini_set(chr(39)+'display_errors'.chr(39), 0);
|
||||||
ini_set(chr(39)+'log_errors'.chr(39), 1);
|
ini_set(chr(39)+'log_errors'.chr(39), 1);
|
||||||
ini_set(chr(39)+'error_log'.chr(39), chr(39)+'/var/log/apache2/jarvis_errors.log'.chr(39));
|
ini_set(chr(39)+'error_log'.chr(39), chr(39)+'/var/log/apache2/jarvis_errors.log'.chr(39));
|
||||||
date_default_timezone_set(chr(39)+'America/Chicago'.chr(39));
|
date_default_timezone_set(chr(39)+'America/Chicago'.chr(39));
|
||||||
|
|
||||||
|
define('JELLYFIN_URL', 'http://10.48.200.33:8096');
|
||||||
|
define('JELLYFIN_API_KEY', 'your-jellyfin-api-key');
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ function update_agent_seen(string $agentId, string $status = 'online', ?string $
|
|||||||
// ── Auth (all actions except register) ───────────────────────────────────────
|
// ── Auth (all actions except register) ───────────────────────────────────────
|
||||||
|
|
||||||
$agentKey = $_SERVER['HTTP_X_AGENT_KEY'] ?? '';
|
$agentKey = $_SERVER['HTTP_X_AGENT_KEY'] ?? '';
|
||||||
$browserActions = ['list', 'status', 'myip'];
|
$browserActions = ['list', 'status', 'myip', 'regkey'];
|
||||||
|
|
||||||
if ($agentAction !== 'register') {
|
if ($agentAction !== 'register') {
|
||||||
if (in_array($agentAction, $browserActions)) {
|
if (in_array($agentAction, $browserActions)) {
|
||||||
@@ -111,7 +111,8 @@ switch ($agentAction) {
|
|||||||
|
|
||||||
// ── HEARTBEAT ────────────────────────────────────────────────────────────
|
// ── HEARTBEAT ────────────────────────────────────────────────────────────
|
||||||
case 'heartbeat':
|
case 'heartbeat':
|
||||||
update_agent_seen($agent['agent_id'], 'online', trim($data['version'] ?? '') ?: null);
|
$hbStatus = in_array($data['status'] ?? '', ['online','offline']) ? $data['status'] : 'online';
|
||||||
|
update_agent_seen($agent['agent_id'], $hbStatus, trim($data['version'] ?? '') ?: null);
|
||||||
|
|
||||||
// Return any pending commands for this agent
|
// Return any pending commands for this agent
|
||||||
$commands = JarvisDB::query(
|
$commands = JarvisDB::query(
|
||||||
@@ -211,6 +212,10 @@ switch ($agentAction) {
|
|||||||
);
|
);
|
||||||
agent_ok();
|
agent_ok();
|
||||||
|
|
||||||
|
// ── REGKEY (browser: session-authed fetch of registration key) ───────────
|
||||||
|
case 'regkey':
|
||||||
|
agent_ok(['registration_key' => AGENT_REGISTRATION_KEY]);
|
||||||
|
|
||||||
// ── LIST (admin: get all agents status) ──────────────────────────────────
|
// ── LIST (admin: get all agents status) ──────────────────────────────────
|
||||||
case 'list':
|
case 'list':
|
||||||
// Mark agents offline if last_seen > 2 minutes ago
|
// Mark agents offline if last_seen > 2 minutes ago
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ foreach ($svcNames as $s) {
|
|||||||
|
|
||||||
// Site health from kb_facts
|
// Site health from kb_facts
|
||||||
$siteLabels = [
|
$siteLabels = [
|
||||||
"jarvis" => "jarvis.orbishosting.com:1972",
|
"jarvis" => "jarvis.orbishosting.com",
|
||||||
"tomsjavajive" => "tomsjavajive.com",
|
"tomsjavajive" => "tomsjavajive.com",
|
||||||
"epictravelexp"=> "epictravelexpeditions.com",
|
"epictravelexp"=> "epictravelexpeditions.com",
|
||||||
"parkersling" => "parkerslingshotrentals.com",
|
"parkersling" => "parkerslingshotrentals.com",
|
||||||
|
|||||||
@@ -21,13 +21,18 @@ function collect_all(): array {
|
|||||||
|
|
||||||
// Returns true if a fact category has been updated within $secs seconds.
|
// Returns true if a fact category has been updated within $secs seconds.
|
||||||
// Prevents expensive external calls when data is still fresh.
|
// Prevents expensive external calls when data is still fresh.
|
||||||
|
// Comparison is done entirely in SQL (via NOW()) rather than PHP's time()/strtotime()
|
||||||
|
// — this file's config.php sets date_default_timezone_set('America/Chicago'), which
|
||||||
|
// makes strtotime() misinterpret MySQL's naive (UTC) datetime strings as being in
|
||||||
|
// Chicago time, throwing every freshness check off by the UTC offset (previously
|
||||||
|
// caused "sites" to always look artificially fresh and never actually refresh).
|
||||||
$fresh = function(string $cat, int $secs): bool {
|
$fresh = function(string $cat, int $secs): bool {
|
||||||
$row = JarvisDB::query(
|
$row = JarvisDB::query(
|
||||||
'SELECT updated_at FROM kb_facts WHERE category=? ORDER BY updated_at DESC LIMIT 1',
|
'SELECT (updated_at > DATE_SUB(NOW(), INTERVAL ? SECOND)) AS is_fresh FROM kb_facts WHERE category=? ORDER BY updated_at DESC LIMIT 1',
|
||||||
[$cat]
|
[$secs, $cat]
|
||||||
);
|
);
|
||||||
if (empty($row[0]['updated_at'])) return false;
|
if (empty($row)) return false;
|
||||||
return (time() - strtotime($row[0]['updated_at'])) < $secs;
|
return (bool) $row[0]['is_fresh'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -177,18 +182,26 @@ function collect_all(): array {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Site Health (TTL 5 min) ───────────────────────────────────────────
|
// ── Site Health (TTL 5 min) ───────────────────────────────────────────
|
||||||
if ($fresh('sites', 300)) {
|
// Fixed 2026-07-07: this guard was 300s but cron only runs every 180s, so sites
|
||||||
|
// were effectively only re-checked every OTHER run (~6 min gaps, felt "far apart").
|
||||||
|
// 170s keeps it just under the cron cadence so it re-checks on every run.
|
||||||
|
if ($fresh('sites', 170)) {
|
||||||
$results['sites'] = 'skipped (fresh)';
|
$results['sites'] = 'skipped (fresh)';
|
||||||
} else try {
|
} else try {
|
||||||
$sites = [
|
$sites = [
|
||||||
"jarvis" => "http://jarvis.orbishosting.com:1972",
|
"jarvis" => "http://127.0.0.1",
|
||||||
'tomsjavajive' => 'https://tomsjavajive.com',
|
'tomsjavajive' => 'https://tomsjavajive.com',
|
||||||
'epictravelexp'=> 'https://epictravelexpeditions.com',
|
'epictravelexp'=> 'https://epictravelexpeditions.com',
|
||||||
'parkersling' => 'https://parkerslingshotrentals.com',
|
'parkerslingshotrentals' => 'https://parkerslingshotrentals.com',
|
||||||
'orbishosting' => 'https://orbishosting.com',
|
'orbishosting' => 'https://orbishosting.com',
|
||||||
'orbisportal' => 'https://orbis.orbishosting.com',
|
'orbisportal' => 'https://orbis.orbishosting.com',
|
||||||
'tomtomgames' => 'https://tomtomgames.com',
|
'tomtomgames' => 'https://tomtomgames.com',
|
||||||
];
|
];
|
||||||
|
// Sites intentionally gated behind HTTP Basic Auth (e.g. a password-protected
|
||||||
|
// "Coming Soon" page during a rebuild) — treat these status codes as up, not down.
|
||||||
|
$expectedCodes = [
|
||||||
|
'parkerslingshotrentals' => [401],
|
||||||
|
];
|
||||||
$down = [];
|
$down = [];
|
||||||
foreach ($sites as $key => $url) {
|
foreach ($sites as $key => $url) {
|
||||||
$parsed = parse_url($url);
|
$parsed = parse_url($url);
|
||||||
@@ -208,7 +221,8 @@ function collect_all(): array {
|
|||||||
curl_exec($ch);
|
curl_exec($ch);
|
||||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
curl_close($ch);
|
curl_close($ch);
|
||||||
$status = ($code >= 200 && $code < 400) ? 'up' : "down-$code";
|
$ok = ($code >= 200 && $code < 400) || in_array($code, $expectedCodes[$key] ?? [], true);
|
||||||
|
$status = $ok ? 'up' : "down-$code";
|
||||||
KBEngine::storeFact('sites', $key, $status, $url, 180);
|
KBEngine::storeFact('sites', $key, $status, $url, 180);
|
||||||
if ($status !== 'up') $down[] = "$key($code)";
|
if ($status !== 'up') $down[] = "$key($code)";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
// Chat history search endpoint
|
// Chat history search endpoint
|
||||||
require_once __DIR__ . '/../config.php';
|
require_once __DIR__ . '/../config.php';
|
||||||
require_once __DIR__ . '/../../includes/auth.php';
|
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
AuthMiddleware::requireAuth();
|
|
||||||
|
|
||||||
$q = trim($_GET['q'] ?? '');
|
$q = trim($_GET['q'] ?? '');
|
||||||
if (strlen($q) < 2) {
|
if (strlen($q) < 2) {
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
require_once __DIR__ . '/../config.php';
|
require_once __DIR__ . '/../config.php';
|
||||||
require_once __DIR__ . '/../../includes/auth.php';
|
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
AuthMiddleware::requireAuth();
|
|
||||||
|
|
||||||
$action = $_GET['action'] ?? 'sessions';
|
$action = $_GET['action'] ?? 'sessions';
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,329 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* JARVIS KB Intent Generator
|
||||||
|
* Generates 1,000+ educational KB intents via Groq LLM and imports to kb_intents table.
|
||||||
|
* Also runs a cleanup pass: deduplication, short-response pruning, pattern normalisation.
|
||||||
|
*
|
||||||
|
* CLI / cron: /usr/bin/php8.3 /var/www/jarvis/api/endpoints/kb_intent_generator.php
|
||||||
|
* Schedule: Daily – 3 am
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../config.php';
|
||||||
|
require_once __DIR__ . '/../lib/db.php';
|
||||||
|
|
||||||
|
/* ── helpers ── */
|
||||||
|
function ts(): string { return '[' . date('Y-m-d H:i:s') . ']'; }
|
||||||
|
function log_line(string $msg): void { echo ts() . ' KB Intent Generator: ' . $msg . "\n"; flush(); }
|
||||||
|
|
||||||
|
function groq(string $system, string $user, int $max = 3000, int $retries = 2): ?string {
|
||||||
|
for ($attempt = 0; $attempt <= $retries; $attempt++) {
|
||||||
|
if ($attempt > 0) {
|
||||||
|
log_line(" Retry {$attempt}/{$retries} after rate-limit pause...");
|
||||||
|
sleep(25);
|
||||||
|
}
|
||||||
|
$ch = curl_init('https://api.groq.com/openai/v1/chat/completions');
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_POST => true,
|
||||||
|
CURLOPT_TIMEOUT => 60,
|
||||||
|
CURLOPT_HTTPHEADER => [
|
||||||
|
'Authorization: Bearer ' . GROQ_API_KEY,
|
||||||
|
'Content-Type: application/json',
|
||||||
|
],
|
||||||
|
CURLOPT_POSTFIELDS => json_encode([
|
||||||
|
'model' => 'llama-3.3-70b-versatile',
|
||||||
|
'max_tokens' => $max,
|
||||||
|
'temperature' => 0.7,
|
||||||
|
'messages' => [
|
||||||
|
['role' => 'system', 'content' => $system],
|
||||||
|
['role' => 'user', 'content' => $user],
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
$raw = curl_exec($ch);
|
||||||
|
$err = curl_error($ch);
|
||||||
|
$info = curl_getinfo($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($err || !$raw) continue;
|
||||||
|
|
||||||
|
// Check for rate limit response (429)
|
||||||
|
if (($info['http_code'] ?? 0) === 429) continue;
|
||||||
|
|
||||||
|
$d = json_decode($raw, true);
|
||||||
|
$content = $d['choices'][0]['message']['content'] ?? null;
|
||||||
|
if ($content !== null) return $content;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── normalize a pattern to valid PHP PCRE ── */
|
||||||
|
function normalize_pattern(string $pat): string {
|
||||||
|
$pat = trim($pat);
|
||||||
|
// If pattern already has PCRE delimiters, leave it alone
|
||||||
|
if (preg_match('/^[\/|~#!@%]/', $pat)) return $pat;
|
||||||
|
// Strip any (?i) inline flag — we'll add /i at the delimiter level
|
||||||
|
$pat = preg_replace('/^\(\?i\)/', '', $pat);
|
||||||
|
// Escape forward slashes inside the pattern
|
||||||
|
$pat = str_replace('/', '\\/', $pat);
|
||||||
|
return '/' . $pat . '/i';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── run guard: skip if ran within last 4 hours ── */
|
||||||
|
/* Set JARVIS_FORCE_RUN=1 (env) or pass --force (argv) to bypass */
|
||||||
|
/* Comparison done in SQL (NOW()) rather than PHP time()/strtotime() — this process's
|
||||||
|
date_default_timezone_set('America/Chicago') makes strtotime() misread MySQL's naive
|
||||||
|
(UTC) timestamps as Chicago time, throwing elapsed-time checks off by the UTC offset. */
|
||||||
|
$recentRun = JarvisDB::single(
|
||||||
|
"SELECT (updated_at > DATE_SUB(NOW(), INTERVAL 14400 SECOND)) AS is_recent FROM kb_facts WHERE category='kb_generator' AND fact_key='last_run'"
|
||||||
|
);
|
||||||
|
$forceRun = !empty(getenv('JARVIS_FORCE_RUN')) || (isset($argv[1]) && $argv[1] === '--force');
|
||||||
|
if (!$forceRun && $recentRun && $recentRun['is_recent']) {
|
||||||
|
log_line('Skipping – ran within last 4 hours. Use --force to override.');
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
if ($forceRun) log_line('Force-run flag set — bypassing 4-hour guard.');
|
||||||
|
|
||||||
|
log_line('Starting daily KB intent generation run.');
|
||||||
|
|
||||||
|
/* ── load active topics from database ── */
|
||||||
|
$BATCHES = JarvisDB::query(
|
||||||
|
"SELECT t.topic_id AS id, t.category, t.topic_name AS topic, t.description AS `desc`
|
||||||
|
FROM kb_generator_topics t WHERE t.active=1 ORDER BY t.id ASC"
|
||||||
|
);
|
||||||
|
if (empty($BATCHES)) {
|
||||||
|
log_line('ERROR: No active topics in kb_generator_topics table. Add topics via the JARVIS admin panel.');
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
log_line('Loaded ' . count($BATCHES) . ' active topics from database.');
|
||||||
|
|
||||||
|
/* ── ROTATION ENGINE: process BATCH_SIZE topics per run, cycling through all ── */
|
||||||
|
define('BATCH_SIZE', 25);
|
||||||
|
$totalTopics = count($BATCHES);
|
||||||
|
$offsetRow = JarvisDB::single(
|
||||||
|
"SELECT CAST(fact_value AS SIGNED) AS v FROM kb_facts WHERE category='kb_generator' AND fact_key='batch_offset'"
|
||||||
|
);
|
||||||
|
$batchOffset = max(0, (int)($offsetRow['v'] ?? 0));
|
||||||
|
if ($batchOffset >= $totalTopics) $batchOffset = 0;
|
||||||
|
$nextOffset = ($batchOffset + BATCH_SIZE) % $totalTopics;
|
||||||
|
$cycleComplete = ($batchOffset + BATCH_SIZE) >= $totalTopics;
|
||||||
|
$cycleLen = (int)ceil($totalTopics / BATCH_SIZE);
|
||||||
|
|
||||||
|
$runBatches = [];
|
||||||
|
for ($i = 0; $i < BATCH_SIZE; $i++) {
|
||||||
|
$runBatches[] = $BATCHES[($batchOffset + $i) % $totalTopics];
|
||||||
|
}
|
||||||
|
$endIdx = ($batchOffset + BATCH_SIZE - 1) % $totalTopics;
|
||||||
|
log_line("Rotation: topics " . ($batchOffset + 1) . "–" . ($endIdx + 1) . " of {$totalTopics} | cycle = {$cycleLen} runs × 6h = " . ($cycleLen * 6) . "h full cycle.");
|
||||||
|
if ($cycleComplete) log_line(" ↻ Full cycle complete — restarting from topic 1 next run.");
|
||||||
|
|
||||||
|
/* ── system prompt ── */
|
||||||
|
/* RESTORED 2026-07-07: this and safe_insert() below were lost during the 2026-07-05 rotation-engine
|
||||||
|
refactor (moving from a hardcoded $BATCHES array to the kb_generator_topics table). Their absence
|
||||||
|
caused an uncaught TypeError on every run since (undefined $SYSTEM passed to groq()'s non-nullable
|
||||||
|
string param), silently swallowed by config.php's error_reporting(0) — the cron looked like it was
|
||||||
|
running fine but died instantly on batch 1 every single time. Restored from kb_intent_generator.php.bak2,
|
||||||
|
with the intent count adjusted from 40 to 20 to match this version's actual per-topic request below. */
|
||||||
|
$SYSTEM = <<<'SYS'
|
||||||
|
You are an expert educator generating KB (knowledge-base) intents for an AI assistant called JARVIS.
|
||||||
|
Each intent is a question/phrase a student might ask, paired with a clear educational answer.
|
||||||
|
|
||||||
|
Respond ONLY with a valid JSON array (no markdown, no backticks, no commentary).
|
||||||
|
Each element must have exactly these keys:
|
||||||
|
"n" – intent_name: unique snake_case identifier ≤ 60 chars, prefixed with the batch id given
|
||||||
|
"p" – pattern: a PHP PCRE regex (use (?i) for case-insensitive) that matches the question
|
||||||
|
"r" – response: a thorough but concise educational answer (2–5 sentences or a short structured list)
|
||||||
|
"c" – category: the category string provided
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Patterns must use \\b word boundaries; escape backslashes for JSON (\\b not \b)
|
||||||
|
- Patterns should NOT start with ^ or end with $ (they are substring matches)
|
||||||
|
- Responses must be factually accurate
|
||||||
|
- Do not duplicate intent names; every "n" must be unique within this batch
|
||||||
|
- Return exactly 20 intents
|
||||||
|
SYS;
|
||||||
|
|
||||||
|
/* ── insert helper ── */
|
||||||
|
$inserted = 0;
|
||||||
|
$skipped = 0;
|
||||||
|
$errors = 0;
|
||||||
|
|
||||||
|
function safe_insert(array $intent, string $batchCategory): void {
|
||||||
|
global $inserted, $skipped, $errors;
|
||||||
|
$name = trim($intent['n'] ?? '');
|
||||||
|
$pattern = trim($intent['p'] ?? '');
|
||||||
|
$response = trim($intent['r'] ?? '');
|
||||||
|
$category = trim($intent['c'] ?? $batchCategory);
|
||||||
|
|
||||||
|
if (!$name || !$pattern || !$response) { $errors++; return; }
|
||||||
|
if (strlen($name) > 64) $name = substr($name, 0, 64);
|
||||||
|
if (strlen($pattern) > 512) $pattern = substr($pattern, 0, 512);
|
||||||
|
if (strlen($response) < 30) { $skipped++; return; } // too short
|
||||||
|
|
||||||
|
try {
|
||||||
|
JarvisDB::execute(
|
||||||
|
'INSERT INTO kb_intents (intent_name, pattern, response_template, fact_category, action_type, priority, active)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
pattern=VALUES(pattern),
|
||||||
|
response_template=VALUES(response_template),
|
||||||
|
fact_category=VALUES(fact_category)',
|
||||||
|
[$name, $pattern, $response, $category, 'response', 5, 1]
|
||||||
|
);
|
||||||
|
$inserted++;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$errors++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── main generation loop ── */
|
||||||
|
$totalBatches = count($runBatches);
|
||||||
|
foreach ($runBatches as $idx => $batch) {
|
||||||
|
$num = $idx + 1;
|
||||||
|
log_line("Batch {$num}/{$totalBatches}: {$batch['topic']}");
|
||||||
|
|
||||||
|
$user = "Generate 20 KB intents for the topic: {$batch['topic']}.\n"
|
||||||
|
. "Subtopics to cover: {$batch['desc']}.\n"
|
||||||
|
. "Prefix every intent_name with \"{$batch['id']}_\".\n"
|
||||||
|
. "Category string to use: \"{$batch['category']}\".";
|
||||||
|
|
||||||
|
$raw = groq($SYSTEM, $user, 5000);
|
||||||
|
if ($raw === null) {
|
||||||
|
log_line(" ✗ API call failed after retries – skipping batch.");
|
||||||
|
$errors += 20;
|
||||||
|
sleep(8);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip markdown code fences if model added them
|
||||||
|
$raw = preg_replace('/^```(?:json)?\s*/m', '', $raw);
|
||||||
|
$raw = preg_replace('/^```\s*/m', '', $raw);
|
||||||
|
$raw = trim($raw);
|
||||||
|
|
||||||
|
// Extract JSON array; fall back to partial recovery for truncated responses
|
||||||
|
$items = null;
|
||||||
|
if (preg_match('/\[\s*\{.*\}\s*\]/s', $raw, $m)) {
|
||||||
|
$items = json_decode($m[0], true);
|
||||||
|
if (!is_array($items)) {
|
||||||
|
log_line(" ✗ JSON parse failed – skipping batch.");
|
||||||
|
$errors += 20; sleep(8); continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$start = strpos($raw, '[');
|
||||||
|
if ($start !== false) {
|
||||||
|
$partial = substr($raw, $start);
|
||||||
|
if (preg_match_all('/\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/s', $partial, $objs) && !empty($objs[0])) {
|
||||||
|
$recovered = '[' . implode(',', $objs[0]) . ']';
|
||||||
|
$items = json_decode($recovered, true);
|
||||||
|
if (is_array($items) && count($items) > 0)
|
||||||
|
log_line(" ⚠ Truncated — recovered " . count($items) . " items.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!is_array($items) || count($items) === 0) {
|
||||||
|
log_line(" ✗ No JSON array found — raw[0:120]: " . substr(str_replace("\n", ' ', $raw), 0, 120));
|
||||||
|
$errors += 20; sleep(8); continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$batchInserted = 0;
|
||||||
|
foreach ($items as $item) {
|
||||||
|
if (!is_array($item)) continue;
|
||||||
|
safe_insert($item, $batch['category']);
|
||||||
|
$batchInserted++;
|
||||||
|
}
|
||||||
|
log_line(" ✓ Parsed {$batchInserted} intents (running total inserted: {$inserted}).");
|
||||||
|
|
||||||
|
// Polite delay between API calls — Groq TPM limit needs ~8s between batches
|
||||||
|
if ($num < $totalBatches) sleep(8);
|
||||||
|
}
|
||||||
|
|
||||||
|
log_line("Generation complete. Inserted/updated: {$inserted} | Short/invalid skipped: {$skipped} | Errors: {$errors}");
|
||||||
|
|
||||||
|
/* ── cleanup phase ── */
|
||||||
|
log_line('Starting cleanup phase...');
|
||||||
|
|
||||||
|
// 1. Remove exact duplicate intent_names (keep the one with the longer response)
|
||||||
|
$dups = JarvisDB::query(
|
||||||
|
'SELECT intent_name, COUNT(*) AS cnt FROM kb_intents GROUP BY intent_name HAVING cnt > 1'
|
||||||
|
);
|
||||||
|
$dupsPruned = 0;
|
||||||
|
foreach ($dups as $dup) {
|
||||||
|
$rows = JarvisDB::query(
|
||||||
|
'SELECT id, LENGTH(response_template) AS rlen FROM kb_intents WHERE intent_name=? ORDER BY rlen DESC',
|
||||||
|
[$dup['intent_name']]
|
||||||
|
);
|
||||||
|
array_shift($rows);
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
JarvisDB::execute('DELETE FROM kb_intents WHERE id=?', [$row['id']]);
|
||||||
|
$dupsPruned++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log_line(" Duplicate intent_names pruned: {$dupsPruned}");
|
||||||
|
|
||||||
|
// 2. Remove intents with very short responses (< 40 chars)
|
||||||
|
$shortPruned = JarvisDB::execute(
|
||||||
|
"DELETE FROM kb_intents WHERE LENGTH(response_template) < 40 AND priority <= 5"
|
||||||
|
);
|
||||||
|
log_line(" Short-response rows pruned: {$shortPruned}");
|
||||||
|
|
||||||
|
// 3. Trim whitespace on all generated intents
|
||||||
|
JarvisDB::execute(
|
||||||
|
"UPDATE kb_intents SET
|
||||||
|
intent_name = TRIM(intent_name),
|
||||||
|
pattern = TRIM(pattern),
|
||||||
|
response_template = TRIM(response_template),
|
||||||
|
fact_category = TRIM(fact_category)
|
||||||
|
WHERE priority = 5"
|
||||||
|
);
|
||||||
|
log_line(' Whitespace trimmed on all generated intents.');
|
||||||
|
|
||||||
|
// 4. Fix and validate PCRE patterns — normalize then deactivate only truly broken ones
|
||||||
|
$all = JarvisDB::query('SELECT id, pattern FROM kb_intents WHERE priority=5');
|
||||||
|
$badPattern = 0;
|
||||||
|
$fixedPattern = 0;
|
||||||
|
foreach ($all as $row) {
|
||||||
|
$pat = normalize_pattern($row['pattern']);
|
||||||
|
if ($pat !== $row['pattern']) {
|
||||||
|
// Update to normalized form
|
||||||
|
JarvisDB::execute('UPDATE kb_intents SET pattern=?, active=1 WHERE id=?', [$pat, $row['id']]);
|
||||||
|
$fixedPattern++;
|
||||||
|
} elseif (@preg_match($pat, '') === false) {
|
||||||
|
JarvisDB::execute('UPDATE kb_intents SET active=0 WHERE id=?', [$row['id']]);
|
||||||
|
$badPattern++;
|
||||||
|
} else {
|
||||||
|
// Valid pattern — make sure it's active
|
||||||
|
JarvisDB::execute('UPDATE kb_intents SET active=1 WHERE id=?', [$row['id']]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log_line(" Patterns normalized: {$fixedPattern} | Bad PCRE deactivated: {$badPattern}");
|
||||||
|
|
||||||
|
// 5. Enable ALL remaining inactive intents (including pre-existing ones)
|
||||||
|
$reactivated = JarvisDB::execute('UPDATE kb_intents SET active=1 WHERE active=0 AND priority <= 5');
|
||||||
|
log_line(" Re-activated previously inactive intents: {$reactivated}");
|
||||||
|
|
||||||
|
// 6. Final stats
|
||||||
|
$stats = JarvisDB::single('SELECT COUNT(*) AS total, SUM(active) AS active FROM kb_intents');
|
||||||
|
log_line("Final KB Intents table: {$stats['total']} total, {$stats['active']} active.");
|
||||||
|
|
||||||
|
/* ── record last-run timestamp ── */
|
||||||
|
JarvisDB::execute(
|
||||||
|
"INSERT INTO kb_facts (category, fact_key, fact_value, host)
|
||||||
|
VALUES ('kb_generator', 'last_run', NOW(), 'local')
|
||||||
|
ON DUPLICATE KEY UPDATE fact_value=NOW(), updated_at=NOW()",
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
JarvisDB::execute(
|
||||||
|
"INSERT INTO kb_facts (category, fact_key, fact_value, host)
|
||||||
|
VALUES ('kb_generator', 'batch_offset', ?, 'local')
|
||||||
|
ON DUPLICATE KEY UPDATE fact_value=VALUES(fact_value), updated_at=NOW()",
|
||||||
|
[$nextOffset]
|
||||||
|
);
|
||||||
|
log_line("Next run will start at topic offset {$nextOffset}/{$totalTopics}.");
|
||||||
|
JarvisDB::execute(
|
||||||
|
"INSERT INTO kb_facts (category, fact_key, fact_value, host)
|
||||||
|
VALUES ('kb_generator', 'last_inserted', ?, 'local')
|
||||||
|
ON DUPLICATE KEY UPDATE fact_value=VALUES(fact_value), updated_at=NOW()",
|
||||||
|
[$inserted]
|
||||||
|
);
|
||||||
|
|
||||||
|
log_line('Done.');
|
||||||
@@ -2,14 +2,14 @@
|
|||||||
// Network scan push endpoint — called by PVE1 cron with nmap results
|
// Network scan push endpoint — called by PVE1 cron with nmap results
|
||||||
// Authenticates via X-Registration-Key header (same key as agent installer)
|
// Authenticates via X-Registration-Key header (same key as agent installer)
|
||||||
|
|
||||||
define('NETSCAN_KEY', 'f846a9aaf7ce9a61742c63c87c4186052a71d2a580c65518');
|
define('NETSCAN_KEY', AGENT_REGISTRATION_KEY);
|
||||||
|
|
||||||
if ($method !== 'POST') {
|
if ($method !== 'POST') {
|
||||||
echo json_encode(['error' => 'POST only']); exit;
|
echo json_encode(['error' => 'POST only']); exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$reqKey = $_SERVER['HTTP_X_REGISTRATION_KEY'] ?? '';
|
$reqKey = $_SERVER['HTTP_X_REGISTRATION_KEY'] ?? '';
|
||||||
if ($reqKey !== NETSCAN_KEY) {
|
if (!hash_equals(NETSCAN_KEY, $reqKey)) {
|
||||||
http_response_code(401);
|
http_response_code(401);
|
||||||
echo json_encode(['error' => 'Unauthorized']); exit;
|
echo json_encode(['error' => 'Unauthorized']); exit;
|
||||||
}
|
}
|
||||||
@@ -35,6 +35,11 @@ foreach ($devices as $d) {
|
|||||||
if (!$ip) continue;
|
if (!$ip) continue;
|
||||||
|
|
||||||
$discoveredIPs[] = $ip;
|
$discoveredIPs[] = $ip;
|
||||||
|
if ($mac) {
|
||||||
|
// Device likely moved to a new IP (DHCP) — drop the stale row so it
|
||||||
|
// doesn't linger as an orphaned duplicate under the old address.
|
||||||
|
JarvisDB::execute('DELETE FROM network_devices WHERE mac=? AND ip<>?', [$mac, $ip]);
|
||||||
|
}
|
||||||
JarvisDB::execute(
|
JarvisDB::execute(
|
||||||
'INSERT INTO network_devices (ip, mac, hostname, status, last_seen)
|
'INSERT INTO network_devices (ip, mac, hostname, status, last_seen)
|
||||||
VALUES (?,?,?,?,NOW())
|
VALUES (?,?,?,?,NOW())
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ if ($weatherAge > 1800) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
$weatherRaw = curlGet(
|
$weatherRaw = curlGet(
|
||||||
'https://wttr.in/FortWorth,TX?format=j1',
|
'https://wttr.in/76088?format=j1',
|
||||||
['User-Agent: curl/7.88 Jarvis/1.0'],
|
['User-Agent: curl/7.88 Jarvis/1.0'],
|
||||||
15
|
15
|
||||||
);
|
);
|
||||||
@@ -245,7 +245,7 @@ if ($weatherAge > 1800) {
|
|||||||
|
|
||||||
cacheStore('weather', [
|
cacheStore('weather', [
|
||||||
'source' => 'wttr.in',
|
'source' => 'wttr.in',
|
||||||
'location' => 'Fort Worth, TX',
|
'location' => 'Weatherford, TX',
|
||||||
'current' => [
|
'current' => [
|
||||||
'temp' => (int)($cu['temp_F'] ?? 0),
|
'temp' => (int)($cu['temp_F'] ?? 0),
|
||||||
'feels' => (int)($cu['FeelsLikeF'] ?? 0),
|
'feels' => (int)($cu['FeelsLikeF'] ?? 0),
|
||||||
|
|||||||
+103
-11
@@ -55,7 +55,7 @@ CREATE TABLE `agent_metrics` (
|
|||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
KEY `idx_agent_time` (`agent_id`,`recorded_at`),
|
KEY `idx_agent_time` (`agent_id`,`recorded_at`),
|
||||||
KEY `idx_recorded` (`recorded_at`)
|
KEY `idx_recorded` (`recorded_at`)
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=29445 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB AUTO_INCREMENT=31422 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
|
||||||
--
|
--
|
||||||
@@ -182,7 +182,7 @@ CREATE TABLE `conversations` (
|
|||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
KEY `idx_session` (`session_id`),
|
KEY `idx_session` (`session_id`),
|
||||||
KEY `idx_created` (`created_at`)
|
KEY `idx_created` (`created_at`)
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=325 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB AUTO_INCREMENT=335 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
|
||||||
--
|
--
|
||||||
@@ -206,7 +206,7 @@ CREATE TABLE `ha_entities` (
|
|||||||
UNIQUE KEY `uk_agent_entity` (`agent_id`,`entity_id`),
|
UNIQUE KEY `uk_agent_entity` (`agent_id`,`entity_id`),
|
||||||
KEY `idx_domain` (`domain`),
|
KEY `idx_domain` (`domain`),
|
||||||
KEY `idx_updated` (`updated_at`)
|
KEY `idx_updated` (`updated_at`)
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=8436 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB AUTO_INCREMENT=77909 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
|
||||||
--
|
--
|
||||||
@@ -226,7 +226,7 @@ CREATE TABLE `kb_facts` (
|
|||||||
`updated_at` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
`updated_at` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
UNIQUE KEY `unique_fact` (`category`,`fact_key`,`host`)
|
UNIQUE KEY `unique_fact` (`category`,`fact_key`,`host`)
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=39129 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB AUTO_INCREMENT=41478 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
|
||||||
--
|
--
|
||||||
@@ -247,7 +247,7 @@ CREATE TABLE `kb_intents` (
|
|||||||
`active` tinyint(1) DEFAULT 1,
|
`active` tinyint(1) DEFAULT 1,
|
||||||
`created_at` timestamp NULL DEFAULT current_timestamp(),
|
`created_at` timestamp NULL DEFAULT current_timestamp(),
|
||||||
PRIMARY KEY (`id`)
|
PRIMARY KEY (`id`)
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=22 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB AUTO_INCREMENT=47 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
|
||||||
--
|
--
|
||||||
@@ -266,7 +266,7 @@ CREATE TABLE `kb_ollama_models` (
|
|||||||
`pulled_at` timestamp NULL DEFAULT current_timestamp(),
|
`pulled_at` timestamp NULL DEFAULT current_timestamp(),
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
UNIQUE KEY `model_name` (`model_name`)
|
UNIQUE KEY `model_name` (`model_name`)
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
|
||||||
--
|
--
|
||||||
@@ -283,7 +283,7 @@ CREATE TABLE `kb_preferences` (
|
|||||||
`updated_at` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
`updated_at` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
UNIQUE KEY `pref_key` (`pref_key`)
|
UNIQUE KEY `pref_key` (`pref_key`)
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB AUTO_INCREMENT=19 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
|
||||||
--
|
--
|
||||||
@@ -340,7 +340,7 @@ CREATE TABLE `network_devices` (
|
|||||||
`created_at` timestamp NULL DEFAULT current_timestamp(),
|
`created_at` timestamp NULL DEFAULT current_timestamp(),
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
UNIQUE KEY `uk_ip` (`ip`)
|
UNIQUE KEY `uk_ip` (`ip`)
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=409 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB AUTO_INCREMENT=5556 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
|
||||||
--
|
--
|
||||||
@@ -406,10 +406,10 @@ CREATE TABLE `usage_patterns` (
|
|||||||
`hour` tinyint(2) NOT NULL,
|
`hour` tinyint(2) NOT NULL,
|
||||||
`dow` tinyint(1) NOT NULL,
|
`dow` tinyint(1) NOT NULL,
|
||||||
`hit_count` int(11) DEFAULT 1,
|
`hit_count` int(11) DEFAULT 1,
|
||||||
`last_used` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
`last_seen` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
UNIQUE KEY `uk_intent_time` (`intent_name`,`hour`,`dow`)
|
UNIQUE KEY `uk_intent_time` (`intent_name`,`hour`,`dow`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
|
||||||
--
|
--
|
||||||
@@ -441,4 +441,96 @@ CREATE TABLE `users` (
|
|||||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||||
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
|
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
|
||||||
|
|
||||||
-- Dump completed on 2026-06-29 20:43:24
|
--
|
||||||
|
-- Table structure for table `guardian_config`
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `guardian_config` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`key_name` varchar(64) NOT NULL,
|
||||||
|
`value` varchar(255) NOT NULL DEFAULT '',
|
||||||
|
`updated_at` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_key` (`key_name`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Table structure for table `guardian_events`
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `guardian_events` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`event_type` varchar(64) NOT NULL,
|
||||||
|
`severity` enum('info','warning','critical') NOT NULL DEFAULT 'info',
|
||||||
|
`agent_id` varchar(64) NOT NULL DEFAULT '',
|
||||||
|
`hostname` varchar(128) NOT NULL DEFAULT '',
|
||||||
|
`metric` varchar(64) NOT NULL DEFAULT '',
|
||||||
|
`value` float NOT NULL DEFAULT 0,
|
||||||
|
`threshold` float NOT NULL DEFAULT 0,
|
||||||
|
`message` text NOT NULL,
|
||||||
|
`ai_analysis` text NOT NULL DEFAULT '',
|
||||||
|
`acknowledged` tinyint(1) NOT NULL DEFAULT 0,
|
||||||
|
`created_at` datetime NOT NULL DEFAULT current_timestamp(),
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_severity` (`severity`),
|
||||||
|
KEY `idx_ack` (`acknowledged`),
|
||||||
|
KEY `idx_created` (`created_at`),
|
||||||
|
KEY `idx_agent` (`agent_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Dump completed on 2026-06-29 23:15:44
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `email_triage` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`msg_id` varchar(255) NOT NULL,
|
||||||
|
`account` varchar(64) NOT NULL DEFAULT 'gmail',
|
||||||
|
`from_name` varchar(255) DEFAULT NULL,
|
||||||
|
`from_email` varchar(255) DEFAULT NULL,
|
||||||
|
`subject` varchar(500) DEFAULT NULL,
|
||||||
|
`date_received` datetime DEFAULT NULL,
|
||||||
|
`category` varchar(32) NOT NULL DEFAULT 'info',
|
||||||
|
`priority` int(11) NOT NULL DEFAULT 3,
|
||||||
|
`summary` text DEFAULT NULL,
|
||||||
|
`draft_reply` text DEFAULT NULL,
|
||||||
|
`action_taken` varchar(32) NOT NULL DEFAULT 'none',
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uq_msg` (`msg_id`),
|
||||||
|
KEY `idx_category` (`category`),
|
||||||
|
KEY `idx_action` (`action_taken`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `email_actions` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`msg_id` varchar(255) DEFAULT NULL,
|
||||||
|
`from_name` varchar(255) DEFAULT NULL,
|
||||||
|
`from_email` varchar(255) DEFAULT NULL,
|
||||||
|
`subject` varchar(500) DEFAULT NULL,
|
||||||
|
`received_at` datetime DEFAULT NULL,
|
||||||
|
`suggested_title` varchar(255) DEFAULT NULL,
|
||||||
|
`suggested_date` date DEFAULT NULL,
|
||||||
|
`task_id` int(11) DEFAULT NULL,
|
||||||
|
`appointment_id` int(11) DEFAULT NULL,
|
||||||
|
`dismissed` tinyint(1) NOT NULL DEFAULT 0,
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_dismissed` (`dismissed`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `email_sent` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`account` varchar(64) NOT NULL DEFAULT 'gmail',
|
||||||
|
`to_email` varchar(255) NOT NULL,
|
||||||
|
`to_name` varchar(255) DEFAULT NULL,
|
||||||
|
`subject` varchar(500) DEFAULT NULL,
|
||||||
|
`body` text DEFAULT NULL,
|
||||||
|
`triage_id` int(11) DEFAULT NULL,
|
||||||
|
`status` varchar(32) NOT NULL DEFAULT 'sent',
|
||||||
|
`sent_at` timestamp NULL DEFAULT current_timestamp(),
|
||||||
|
`error` text DEFAULT NULL,
|
||||||
|
`message_id` varchar(255) DEFAULT NULL,
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_account` (`account`),
|
||||||
|
KEY `idx_status` (`status`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
-- JARVIS KB Seed Data
|
||||||
|
-- Preferences
|
||||||
|
INSERT INTO kb_preferences (pref_key, pref_value) VALUES
|
||||||
|
('user_name', 'Myron'),
|
||||||
|
('user_title', 'Mr. Blair'),
|
||||||
|
('ai_model', 'llama3.1:8b'),
|
||||||
|
('timezone', 'America/Chicago')
|
||||||
|
ON DUPLICATE KEY UPDATE pref_value = VALUES(pref_value);
|
||||||
|
|
||||||
|
-- Intents: greeting, time, system, network, proxmox, ollama, tasks, HA
|
||||||
|
INSERT INTO kb_intents (intent_name, pattern, response_template, fact_category, action_type, priority, active) VALUES
|
||||||
|
|
||||||
|
-- Greetings
|
||||||
|
('greeting', '(?i)^(hello|hi|hey|good (morning|afternoon|evening)|what.?s up|howdy)\\b', 'Good {current_time}, {user_title}. All systems are online. How can I assist you?', 'system', 'response', 10, 1),
|
||||||
|
|
||||||
|
-- Time / date
|
||||||
|
('current_time', '(?i)\\b(what.?s the (time|current time)|what time is it|tell me the time)\\b', 'It is currently {current_time}, {user_title}.', NULL, 'response', 9, 1),
|
||||||
|
('current_date', '(?i)\\b(what.?s (today.?s date|the date)|what day is it|today.?s date)\\b', 'Today is {current_date}, {user_title}.', NULL, 'response', 9, 1),
|
||||||
|
|
||||||
|
-- System status
|
||||||
|
('system_status', '(?i)\\b(system (status|health)|how.?s (the system|everything)|jarvis status|all systems)\\b', 'JARVIS is fully operational, {user_title}. CPU: {cpu_usage}%, Memory: {mem_percent}% used ({mem_used_gb}GB / {mem_total_gb}GB). Disk: {disk_used} used of {disk_total}. Uptime: {uptime}. Network agents: {online_count}/{total_count} online.', 'system', 'response', 8, 1),
|
||||||
|
('cpu_status', '(?i)\\b(cpu|processor) (usage|load|status|percent|utilization)\\b', 'Current CPU usage is {cpu_usage}%, {user_title}. Load averages: {load_1m} (1m), {load_5m} (5m), {load_15m} (15m).', 'system', 'response', 8, 1),
|
||||||
|
('memory_status', '(?i)\\b(memory|ram|mem) (usage|status|free|used|available)\\b', 'Memory: {mem_used_gb}GB used of {mem_total_gb}GB ({mem_percent}% utilized), {user_title}. Free: {mem_free_gb}GB.', 'system', 'response', 8, 1),
|
||||||
|
('disk_status', '(?i)\\b(disk|storage|drive) (usage|space|status|free|used|available)\\b', 'Disk status: {disk_used} used of {disk_total} total, {disk_free} free, {user_title}.', 'system', 'response', 8, 1),
|
||||||
|
('uptime', '(?i)\\b(uptime|how long.*running|how long.*up|server uptime)\\b', 'JARVIS has been running for {uptime}, {user_title}.', 'system', 'response', 7, 1),
|
||||||
|
|
||||||
|
-- Network status
|
||||||
|
('network_status', '(?i)\\b(network (status|health|agents)|agents (online|status)|how many (agents|devices) (online|running))\\b', 'Network status: {online_count} of {total_count} agents are online, {user_title}.', 'network', 'response', 8, 1),
|
||||||
|
('network_scan', '(?i)\\b(run (a )?network scan|scan (the )?network|nmap scan|network devices)\\b', 'Initiating network scan, {user_title}.', NULL, 'action', 7, 1),
|
||||||
|
|
||||||
|
-- Proxmox
|
||||||
|
('proxmox_status', '(?i)\\b(proxmox (status|health)|vm (status|count|summary)|virtual machines|how many vms)\\b', 'Proxmox: {vm_running} of {vm_total} VMs/containers running, {user_title}. Host CPU: {pve_cpu_percent}%, Memory: {pve_mem_used_gb}GB / {pve_mem_total_gb}GB ({pve_mem_percent}%).', 'proxmox', 'response', 8, 1),
|
||||||
|
('vm_suggestions', '(?i)\\b(vm (resources|performance|usage)|check vms|resource usage)\\b', 'Checking VM resource usage, {user_title}.', 'proxmox', 'action', 7, 1),
|
||||||
|
|
||||||
|
-- Ollama / AI
|
||||||
|
('ollama_status', '(?i)\\b(ollama (status|health|models)|ai models|llm status|local (ai|models))\\b', 'Ollama is {status} with {model_count} model(s) available: {available_models}, {user_title}.', 'ollama', 'response', 7, 1),
|
||||||
|
|
||||||
|
-- Site health
|
||||||
|
('site_status', '(?i)\\b(site(s)? (status|health|up|down)|website status|are (the )?sites (up|down))\\b', 'Site health — jarvis: {jarvis}, orbishosting: {orbishosting}, tomtomgames: {tomtomgames}, tomsjavajive: {tomsjavajive}, parkerslingshotrentals: {parkersling}, epictravelexpeditions: {epictravelexp}, {user_title}.', 'sites', 'response', 7, 1),
|
||||||
|
|
||||||
|
-- Tasks / planner
|
||||||
|
('task_count', '(?i)\\b(how many tasks|pending tasks|task (count|summary)|my tasks)\\b', 'You have {pending_count} pending tasks and {overdue_count} overdue, {user_title}.', NULL, 'response', 7, 1),
|
||||||
|
('planner_briefing', '(?i)\\b((daily )?briefing|what.?s (on|happening) today|today.?s schedule|morning briefing)\\b', 'Fetching your daily briefing, {user_title}.', NULL, 'action', 8, 1),
|
||||||
|
|
||||||
|
-- Home Assistant
|
||||||
|
('ha_lights_on', '(?i)\\b(turn (on|off) (the |all )?lights?|lights? (on|off)|switch (on|off) (the )?lights?)\\b', 'Sending light command, {user_title}.', NULL, 'action', 8, 1),
|
||||||
|
('ha_scene', '(?i)\\b(activate (a |the )?scene|set (a |the )?scene|home scene)\\b', 'Activating home scene, {user_title}.', NULL, 'action', 7, 1),
|
||||||
|
|
||||||
|
-- Jellyfin
|
||||||
|
('jellyfin_now_playing', '(?i)\\b(what.?s (playing|on)|now playing|jellyfin.*playing|playing.*jellyfin)\\b', 'Checking Jellyfin now playing, {user_title}.', NULL, 'action', 7, 1),
|
||||||
|
('jellyfin_library', '(?i)\\b(jellyfin (library|media|shows?|movies?)|media library|show.*library)\\b', 'Fetching Jellyfin library, {user_title}.', NULL, 'action', 6, 1),
|
||||||
|
('jellyfin_pause', '(?i)\\b(pause (jellyfin|playback|media)|stop (playing|jellyfin))\\b', 'Pausing Jellyfin, {user_title}.', NULL, 'action', 7, 1),
|
||||||
|
|
||||||
|
-- DO server
|
||||||
|
('do_status', '(?i)\\b(do (server|status)|digital ocean (status|server)|vps status)\\b', 'Digital Ocean server is {do_status}, {user_title}.', 'do_server', 'response', 7, 1),
|
||||||
|
|
||||||
|
-- Focus / panels
|
||||||
|
('focus_mode', '(?i)\\b(focus (mode|on)|enable focus|concentration mode)\\b', 'Enabling focus mode, {user_title}.', NULL, 'action', 6, 1),
|
||||||
|
('show_panels', '(?i)\\b(show (all )?panels|expand (all|everything)|full view)\\b', 'Expanding all panels, {user_title}.', NULL, 'action', 6, 1),
|
||||||
|
|
||||||
|
-- Help
|
||||||
|
('help', '(?i)^(help|what can you do|commands|capabilities|what do you know)\\s*\\??$', 'I can help you with: system status, network status, VM/Proxmox status, Ollama AI models, site health, tasks and planner briefings, Jellyfin media, Home Assistant lights and devices, and general questions via Ollama. What would you like to know, {user_title}?', NULL, 'response', 5, 1)
|
||||||
|
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
pattern = VALUES(pattern),
|
||||||
|
response_template = VALUES(response_template),
|
||||||
|
active = 1;
|
||||||
|
|
||||||
|
SELECT COUNT(*) AS intents_seeded FROM kb_intents;
|
||||||
|
SELECT COUNT(*) AS prefs_seeded FROM kb_preferences;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Copy to /etc/jarvis/db.env (root:root 0600). Sourced by the root cron scripts
|
||||||
|
# (jarvis-backup.sh, jarvis-deploy.sh, jarvis-watchdog.sh).
|
||||||
|
JARVIS_DB_PASS=your-db-password
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=JARVIS Arc Reactor
|
||||||
|
After=network-online.target mysql.service
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
WorkingDirectory=/opt/jarvis-arc
|
||||||
|
ExecStart=/opt/jarvis-arc/venv/bin/python3 /opt/jarvis-arc/reactor.py
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
User=root
|
||||||
|
StandardOutput=append:/home/jarvis.orbishosting.com/logs/arc_reactor.log
|
||||||
|
StandardError=append:/home/jarvis.orbishosting.com/logs/arc_reactor.log
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
Executable
+51
@@ -0,0 +1,51 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
[ -r /etc/jarvis/db.env ] && . /etc/jarvis/db.env
|
||||||
|
# JARVIS backup — DB dump + all files needed to actually restore JARVIS, as tar.gz
|
||||||
|
# Fixed 2026-07-07: this only ever backed up the MySQL database. If this VM were
|
||||||
|
# lost, the DB alone is useless without the application code, the reactor daemon,
|
||||||
|
# its systemd unit, and the nginx site config — none of which were captured. Also
|
||||||
|
# fixed a typo ($SIYE -> $SIZE) that silently broke the size line in the log.
|
||||||
|
BACKUP_DIR="/var/backups/jarvis"
|
||||||
|
LOG="$BACKUP_DIR/backup.log"
|
||||||
|
LOCK="$BACKUP_DIR/backup.lock"
|
||||||
|
DB_NAME="jarvis_db"
|
||||||
|
DB_USER="jarvis_user"
|
||||||
|
DB_PASS="${JARVIS_DB_PASS:?DB pass unset - see /etc/jarvis/db.env}"
|
||||||
|
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
|
||||||
|
OUTFILE="$BACKUP_DIR/jarvis_backup_${TIMESTAMP}.tar.gz"
|
||||||
|
TMPDIR=$(mktemp -d)
|
||||||
|
|
||||||
|
mkdir -p "$BACKUP_DIR"
|
||||||
|
touch "$LOCK"
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Starting backup..." >> "$LOG"
|
||||||
|
|
||||||
|
cleanup() { rm -rf "$TMPDIR"; rm -f "$LOCK"; }
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
if mysqldump -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" > "$TMPDIR/jarvis_db.sql" 2>>"$LOG"; then
|
||||||
|
mkdir -p "$TMPDIR/files/etc"
|
||||||
|
cp -a /var/www/jarvis "$TMPDIR/files/var-www-jarvis"
|
||||||
|
cp -a /opt/jarvis-arc "$TMPDIR/files/opt-jarvis-arc"
|
||||||
|
cp -a /etc/nginx/sites-enabled/jarvis "$TMPDIR/files/etc/nginx-site-jarvis" 2>>"$LOG"
|
||||||
|
cp -a /etc/systemd/system/jarvis-arc.service "$TMPDIR/files/etc/jarvis-arc.service" 2>>"$LOG"
|
||||||
|
crontab -l > "$TMPDIR/files/etc/root-crontab.txt" 2>>"$LOG"
|
||||||
|
# Phase 1/2 additions: secrets + systemd drop-ins + operational scripts
|
||||||
|
# (these live outside /var/www/jarvis and /opt/jarvis-arc, so must be
|
||||||
|
# captured explicitly or a restore comes back with no keys/DB pass).
|
||||||
|
cp -a /etc/jarvis-arc "$TMPDIR/files/etc/jarvis-arc-etc" 2>>"$LOG" # reactor.env
|
||||||
|
cp -a /etc/jarvis "$TMPDIR/files/etc/jarvis-etc" 2>>"$LOG" # db.env
|
||||||
|
cp -a /etc/systemd/system/jarvis-arc.service.d "$TMPDIR/files/etc/jarvis-arc.service.d" 2>>"$LOG"
|
||||||
|
mkdir -p "$TMPDIR/files/usr-local-bin"
|
||||||
|
cp -a /usr/local/bin/jarvis-*.sh "$TMPDIR/files/usr-local-bin/" 2>>"$LOG" # health/deploy/watchdog/netscan
|
||||||
|
|
||||||
|
tar -czf "$OUTFILE" -C "$TMPDIR" jarvis_db.sql files
|
||||||
|
SIZE=$(du -sh "$OUTFILE" | cut -f1)
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Backup OK: $(basename "$OUTFILE") ($SIZE)" >> "$LOG"
|
||||||
|
else
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: mysqldump failed" >> "$LOG"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
find "$BACKUP_DIR" -name "jarvis_backup_*.tar.gz" -mtime +7 -delete
|
||||||
|
COUNT=$(ls "$BACKUP_DIR"/jarvis_backup_*.tar.gz 2>/dev/null | wc -l)
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Done. Files retained: $COUNT" >> "$LOG"
|
||||||
+37
-5
@@ -1,4 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
[ -r /etc/jarvis/db.env ] && . /etc/jarvis/db.env
|
||||||
# JARVIS Auto-Deploy Runner — processes GitHub webhook queue every minute.
|
# JARVIS Auto-Deploy Runner — processes GitHub webhook queue every minute.
|
||||||
# Validates PHP syntax before deploying; auto-reverts on bad code.
|
# Validates PHP syntax before deploying; auto-reverts on bad code.
|
||||||
# Restarts OLS after JARVIS deploys to pick up PHP changes.
|
# Restarts OLS after JARVIS deploys to pick up PHP changes.
|
||||||
@@ -64,7 +65,7 @@ while IFS= read -r path; do
|
|||||||
fi
|
fi
|
||||||
# Insert alert into JARVIS DB
|
# Insert alert into JARVIS DB
|
||||||
BAD_ESCAPED=$(printf '%s' "$BAD_FILE" | sed "s/'/\\\\\\'/g")
|
BAD_ESCAPED=$(printf '%s' "$BAD_FILE" | sed "s/'/\\\\\\'/g")
|
||||||
mysql -u jarvis_user -pJ4rv1s_Pr0t0c0l_2026! jarvis_db -se \
|
mysql -u jarvis_user -p"$JARVIS_DB_PASS" jarvis_db -se \
|
||||||
"INSERT INTO alerts (alert_type,title,message,severity)
|
"INSERT INTO alerts (alert_type,title,message,severity)
|
||||||
VALUES ('deploy_fail','Deploy reverted: syntax error',
|
VALUES ('deploy_fail','Deploy reverted: syntax error',
|
||||||
'PHP syntax error in $BAD_ESCAPED. Commit $AFTER was reverted and force-pushed to GitHub.','critical');" 2>/dev/null
|
'PHP syntax error in $BAD_ESCAPED. Commit $AFTER was reverted and force-pushed to GitHub.','critical');" 2>/dev/null
|
||||||
@@ -80,11 +81,42 @@ while IFS= read -r path; do
|
|||||||
systemctl reload lsws 2>/dev/null || systemctl restart lsws 2>/dev/null
|
systemctl reload lsws 2>/dev/null || systemctl restart lsws 2>/dev/null
|
||||||
log "OLS reloaded for JARVIS deploy"
|
log "OLS reloaded for JARVIS deploy"
|
||||||
|
|
||||||
# Sync reactor.py to runtime location if it changed
|
# Patch live config.php with correct Ollama host/model and Groq search model
|
||||||
if echo "$CHANGED" | grep -q 'deploy/reactor.py'; then
|
CONFIG="$path/api/config.php"
|
||||||
|
if [ -f "$CONFIG" ]; then
|
||||||
|
sed -i "s|http://10\.48\.200\.95:11434|http://10.48.200.210:11434|g" "$CONFIG"
|
||||||
|
sed -i "s|'llama3\.2:1b'|'llama3.1:8b'|g" "$CONFIG"
|
||||||
|
sed -i "s|\"llama3\.2:1b\"|\"llama3.1:8b\"|g" "$CONFIG"
|
||||||
|
sed -i "s|'llama3\.1:70b'|'llama3.1:8b'|g" "$CONFIG"
|
||||||
|
sed -i "s|\"llama3\.1:70b\"|\"llama3.1:8b\"|g" "$CONFIG"
|
||||||
|
sed -i "s|'groq/compound-mini'|'compound-beta-mini'|g;s|\"groq/compound-mini\"|\"compound-beta-mini\"|g" "$CONFIG"
|
||||||
|
log "Patched config.php: Ollama IP→.210, model→llama3.1:8b, Groq search→compound-beta-mini"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Self-install the deploy script on every run
|
||||||
|
cp "$path/deploy/jarvis-deploy.sh" /usr/local/bin/jarvis-deploy.sh 2>/dev/null && chmod +x /usr/local/bin/jarvis-deploy.sh
|
||||||
|
|
||||||
|
# Sync reactor.py + service file to runtime location if they changed
|
||||||
|
if echo "$CHANGED" | grep -q 'deploy/reactor.py\|deploy/jarvis-arc.service\|deploy/requirements.txt'; then
|
||||||
|
mkdir -p /opt/jarvis-arc /home/jarvis.orbishosting.com/logs
|
||||||
cp "$path/deploy/reactor.py" /opt/jarvis-arc/reactor.py
|
cp "$path/deploy/reactor.py" /opt/jarvis-arc/reactor.py
|
||||||
systemctl restart jarvis-arc
|
cp "$path/deploy/requirements.txt" /opt/jarvis-arc/requirements.txt 2>/dev/null
|
||||||
log "Arc Reactor updated and restarted (reactor.py changed)"
|
# Bootstrap venv if it doesn't exist
|
||||||
|
if [ ! -f /opt/jarvis-arc/venv/bin/activate ]; then
|
||||||
|
log "Arc Reactor venv missing — creating and installing packages"
|
||||||
|
python3 -m venv /opt/jarvis-arc/venv
|
||||||
|
/opt/jarvis-arc/venv/bin/pip install -q -r /opt/jarvis-arc/requirements.txt
|
||||||
|
log "Arc Reactor venv ready"
|
||||||
|
fi
|
||||||
|
if echo "$CHANGED" | grep -q 'deploy/jarvis-arc.service'; then
|
||||||
|
cp "$path/deploy/jarvis-arc.service" /etc/systemd/system/jarvis-arc.service
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable jarvis-arc
|
||||||
|
log "Arc Reactor service file installed and enabled"
|
||||||
|
fi
|
||||||
|
systemctl restart jarvis-arc 2>/dev/null || \
|
||||||
|
bash -c "pkill -f reactor.py 2>/dev/null; sleep 1; cd /opt/jarvis-arc && source venv/bin/activate && nohup python3 reactor.py >> /home/jarvis.orbishosting.com/logs/arc_reactor.log 2>&1 &"
|
||||||
|
log "Arc Reactor updated and restarted"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# JARVIS Health Self-Check — runs every 5 min via root cron (separate from the
|
||||||
|
# service watchdog). Detects silent failures the watchdog can't: stalled crons,
|
||||||
|
# stuck Arc jobs, low disk, and services the watchdog had to restart. Writes
|
||||||
|
# findings to the `alerts` table (auto-resolving when clear) and emails on any
|
||||||
|
# NEW finding. Added 2026-07-07 (Phase 2 reliability).
|
||||||
|
set -u
|
||||||
|
[ -r /etc/jarvis/db.env ] && . /etc/jarvis/db.env
|
||||||
|
DB_USER="jarvis_user"; DB_NAME="jarvis_db"
|
||||||
|
MYSQL=(mysql -u "$DB_USER" -p"${JARVIS_DB_PASS:-}" "$DB_NAME" -N -B -e)
|
||||||
|
CRONLOG=/var/log/jarvis/cron.log
|
||||||
|
WDLOG=/var/log/jarvis/watchdog.log
|
||||||
|
ALERT_TO="myronblair@gmail.com"
|
||||||
|
REACTOR_ENV=/etc/jarvis-arc/reactor.env
|
||||||
|
NEW_FINDINGS=""
|
||||||
|
|
||||||
|
sql() { "${MYSQL[@]}" "$1" 2>/dev/null; }
|
||||||
|
esc() { printf '%s' "$1" | sed "s/'/''/g"; }
|
||||||
|
|
||||||
|
# Raise (or keep) an alert for a condition. Emails only when it is newly raised.
|
||||||
|
# $1=source_key $2=severity $3=title $4=message
|
||||||
|
raise() {
|
||||||
|
local key sev title msg exists
|
||||||
|
key=$(esc "$1"); sev=$(esc "$2"); title=$(esc "$3"); msg=$(esc "$4")
|
||||||
|
exists=$(sql "SELECT COUNT(*) FROM alerts WHERE source_key='$key' AND resolved=0")
|
||||||
|
if [ "${exists:-0}" = "0" ]; then
|
||||||
|
sql "INSERT INTO alerts (alert_type,title,message,severity,source_key,auto_resolve,created_at)
|
||||||
|
VALUES ('health','$title','$msg','$sev','$key',1,NOW())"
|
||||||
|
NEW_FINDINGS="${NEW_FINDINGS}- [$2] $3: $4"$'\n'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
# Clear a condition's alert when it's no longer true.
|
||||||
|
clear_cond() {
|
||||||
|
local key; key=$(esc "$1")
|
||||||
|
sql "UPDATE alerts SET resolved=1, resolved_at=NOW()
|
||||||
|
WHERE source_key='$key' AND resolved=0 AND auto_resolve=1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 1) Disk usage on / > 85%
|
||||||
|
DISK=$(df / | tail -1 | awk '{print $5}' | tr -d '%')
|
||||||
|
if [ "${DISK:-0}" -gt 85 ]; then
|
||||||
|
raise "health:disk" "critical" "Disk usage high" "Root filesystem at ${DISK}% (threshold 85%)."
|
||||||
|
else clear_cond "health:disk"; fi
|
||||||
|
|
||||||
|
# 2) Arc jobs stuck in 'running' > 30 min
|
||||||
|
STUCK=$(sql "SELECT COUNT(*) FROM arc_jobs WHERE status='running'
|
||||||
|
AND COALESCE(started_at, created_at) < NOW() - INTERVAL 30 MINUTE")
|
||||||
|
if [ "${STUCK:-0}" -gt 0 ]; then
|
||||||
|
raise "health:arc_stuck" "critical" "Arc jobs stuck" "$STUCK Arc job(s) have been 'running' for over 30 minutes."
|
||||||
|
else clear_cond "health:arc_stuck"; fi
|
||||||
|
|
||||||
|
# 3) Cron stalled — cron.log is written by facts_collector every 3 min. If it
|
||||||
|
# hasn't changed in 10 min (>2 consecutive missed runs), cron work has stopped.
|
||||||
|
if [ -f "$CRONLOG" ]; then
|
||||||
|
AGE=$(( $(date +%s) - $(stat -c %Y "$CRONLOG") ))
|
||||||
|
if [ "$AGE" -gt 600 ]; then
|
||||||
|
raise "health:cron" "critical" "Cron jobs stalled" "No cron activity in $((AGE/60)) min (facts_collector runs every 3 min) — crons appear stopped."
|
||||||
|
else clear_cond "health:cron"; fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4) Watched service restarted by the watchdog in the last 6 min
|
||||||
|
if [ -f "$WDLOG" ]; then
|
||||||
|
RECENT=$(awk -v cutoff="$(date -d '6 minutes ago' '+%Y-%m-%d %H:%M:%S')" \
|
||||||
|
'match($0,/^\[([0-9-]+ [0-9:]+)\]/,m){ if(m[1]>=cutoff && /restarted successfully/) print }' "$WDLOG")
|
||||||
|
if [ -n "$RECENT" ]; then
|
||||||
|
SVC=$(printf '%s' "$RECENT" | grep -oE '(nginx|php8.3-fpm|mariadb|redis-server)' | sort -u | tr '\n' ' ')
|
||||||
|
raise "health:wd_restart" "warning" "Service auto-restarted" "Watchdog restarted: ${SVC:-a service}. Investigate why it died."
|
||||||
|
else
|
||||||
|
clear_cond "health:wd_restart"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Email any NEW findings via the reactor's Gmail SMTP creds.
|
||||||
|
if [ -n "$NEW_FINDINGS" ]; then
|
||||||
|
GPASS=""
|
||||||
|
[ -r "$REACTOR_ENV" ] && GPASS=$(grep -E '^GMAIL_PASS=' "$REACTOR_ENV" | cut -d= -f2-)
|
||||||
|
if [ -n "$GPASS" ]; then
|
||||||
|
GMAIL_PASS="$GPASS" ALERT_TO="$ALERT_TO" FINDINGS="$NEW_FINDINGS" python3 - <<'PY'
|
||||||
|
import os, smtplib, ssl, socket
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
user = "myronblair@gmail.com"
|
||||||
|
msg = MIMEText("JARVIS health self-check raised new alerts on %s:\n\n%s" % (socket.gethostname(), os.environ["FINDINGS"]))
|
||||||
|
msg["Subject"] = "JARVIS health alert"
|
||||||
|
msg["From"] = user; msg["To"] = os.environ["ALERT_TO"]
|
||||||
|
try:
|
||||||
|
with smtplib.SMTP("smtp.gmail.com", 587, timeout=20) as s:
|
||||||
|
s.starttls(context=ssl.create_default_context())
|
||||||
|
s.login(user, os.environ["GMAIL_PASS"])
|
||||||
|
s.send_message(msg)
|
||||||
|
print("health-email: sent")
|
||||||
|
except Exception as e:
|
||||||
|
print("health-email: FAILED", e)
|
||||||
|
PY
|
||||||
|
else
|
||||||
|
echo "health-email: no GMAIL_PASS available, skipped"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
[ -r /etc/jarvis/db.env ] && . /etc/jarvis/db.env
|
||||||
# JARVIS Self-Healing Watchdog — runs every 5 min via root cron
|
# JARVIS Self-Healing Watchdog — runs every 5 min via root cron
|
||||||
# Checks: lsws, mysql, redis, JARVIS HTTP, disk, memory
|
# Checks: lsws, mysql, redis, JARVIS HTTP, disk, memory
|
||||||
# Auto-heals: restarts failed services, restarts offline Proxmox VM agents
|
# Auto-heals: restarts failed services, restarts offline Proxmox VM agents
|
||||||
# Logs to: /home/jarvis.orbishosting.com/logs/watchdog.log
|
# Logs to: /home/jarvis.orbishosting.com/logs/watchdog.log
|
||||||
|
|
||||||
LOG=/home/jarvis.orbishosting.com/logs/watchdog.log
|
LOG=/home/jarvis.orbishosting.com/logs/watchdog.log
|
||||||
MYSQL="mysql -u jarvis_user -pJ4rv1s_Pr0t0c0l_2026! jarvis_db -se"
|
MYSQL="mysql -u jarvis_user -p$JARVIS_DB_PASS jarvis_db -se"
|
||||||
TS() { date '+%Y-%m-%d %H:%M:%S'; }
|
TS() { date '+%Y-%m-%d %H:%M:%S'; }
|
||||||
|
|
||||||
log() { echo "[$(TS)] $1" >> "$LOG"; }
|
log() { echo "[$(TS)] $1" >> "$LOG"; }
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# JARVIS Arc Reactor — required secrets. Copy to /etc/jarvis-arc/reactor.env
|
||||||
|
# (root:www-data 0640), loaded by systemd EnvironmentFile. Not committed.
|
||||||
|
JARVIS_DB_PASS=your-db-password
|
||||||
|
CLAUDE_API_KEY=sk-ant-...
|
||||||
|
GROQ_API_KEY=gsk_...
|
||||||
|
GMAIL_PASS=your-gmail-app-password
|
||||||
|
ICLOUD_PASS=your-icloud-app-password
|
||||||
+157
-67
@@ -39,23 +39,24 @@ VERSION = "9.0.0"
|
|||||||
DB_HOST = "localhost"
|
DB_HOST = "localhost"
|
||||||
DB_PORT = 3306
|
DB_PORT = 3306
|
||||||
DB_USER = "jarvis_user"
|
DB_USER = "jarvis_user"
|
||||||
DB_PASS = "J4rv1s_Pr0t0c0l_2026!"
|
DB_PASS = os.environ.get("JARVIS_DB_PASS", "")
|
||||||
DB_NAME = "jarvis_db"
|
DB_NAME = "jarvis_db"
|
||||||
LOG_FILE = "/home/jarvis.orbishosting.com/logs/arc_reactor.log"
|
LOG_FILE = "/var/log/jarvis/arc_reactor.log"
|
||||||
POLL_INTERVAL = 3
|
POLL_INTERVAL = 3
|
||||||
HEARTBEAT_INTERVAL = 30
|
HEARTBEAT_INTERVAL = 30
|
||||||
|
|
||||||
CLAUDE_API_KEY = "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"
|
CLAUDE_API_KEY = os.environ.get("CLAUDE_API_KEY", "")
|
||||||
CLAUDE_MODEL = "claude-sonnet-4-6"
|
CLAUDE_MODEL = "claude-sonnet-4-6"
|
||||||
GROQ_API_KEY = "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"
|
GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "")
|
||||||
GROQ_MODEL = "llama-3.3-70b-versatile"
|
GROQ_MODEL = "llama-3.3-70b-versatile"
|
||||||
OLLAMA_HOST = "http://10.48.200.95:11434"
|
OLLAMA_HOST = "http://10.48.200.210:11434"
|
||||||
OLLAMA_MODEL = "llama3.2:1b"
|
OLLAMA_MODEL = "llama3.1:8b"
|
||||||
|
OLLAMA_VISION_MODEL = os.environ.get("OLLAMA_VISION_MODEL", "") # e.g. "llava" or "moondream" -- empty = disabled
|
||||||
|
|
||||||
GMAIL_USER = "myronblair@gmail.com"
|
GMAIL_USER = "myronblair@gmail.com"
|
||||||
GMAIL_PASS = "demsvdylwweacbcx"
|
GMAIL_PASS = os.environ.get("GMAIL_PASS", "")
|
||||||
ICLOUD_USER = "myronblair@icloud.com"
|
ICLOUD_USER = "myronblair@icloud.com"
|
||||||
ICLOUD_PASS = "yxfi-yvzu-geqk-japr"
|
ICLOUD_PASS = os.environ.get("ICLOUD_PASS", "")
|
||||||
|
|
||||||
# ── LOGGING ───────────────────────────────────────────────────────────────────
|
# ── LOGGING ───────────────────────────────────────────────────────────────────
|
||||||
os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)
|
os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)
|
||||||
@@ -129,18 +130,35 @@ async def handle_shell(payload: dict) -> dict:
|
|||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
async def llm_call(messages: list, provider: str = "claude", system: str = "") -> str:
|
async def llm_call(messages: list, provider: str = "claude", system: str = "") -> str:
|
||||||
if provider == "claude" and CLAUDE_API_KEY:
|
# Fixed 2026-07-07: previously, an explicitly-requested provider (e.g. "claude")
|
||||||
return await _claude_call(messages, system)
|
# called its API directly with no exception handling, so any failure (rate limit,
|
||||||
elif provider == "groq" and GROQ_API_KEY:
|
# depleted credits, outage) raised straight up instead of falling back to the other
|
||||||
return await _groq_call(messages, system)
|
# providers below. The fallback loop only ever ran for an unrecognized provider
|
||||||
elif provider == "ollama":
|
# string, which never happens in practice — so callers requesting "claude" got zero
|
||||||
return await _ollama_call(messages, system)
|
# real resilience. Now every explicit request tries its provider first, then falls
|
||||||
for p in ["groq", "ollama"]:
|
# through the remaining ones in order before giving up.
|
||||||
|
order = {"claude": ["claude", "groq", "ollama"],
|
||||||
|
"groq": ["groq", "ollama"],
|
||||||
|
"ollama": ["ollama"]}.get(provider, ["claude", "groq", "ollama"])
|
||||||
|
last_err = None
|
||||||
|
for p in order:
|
||||||
try:
|
try:
|
||||||
return await llm_call(messages, p, system)
|
if p == "claude" and CLAUDE_API_KEY:
|
||||||
except Exception:
|
result = await _claude_call(messages, system)
|
||||||
|
elif p == "groq" and GROQ_API_KEY:
|
||||||
|
result = await _groq_call(messages, system)
|
||||||
|
elif p == "ollama":
|
||||||
|
result = await _ollama_call(messages, system)
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
if not result or not result.strip():
|
||||||
|
raise RuntimeError(f"{p} returned empty content")
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"[LLM] Provider {p} failed: {type(e).__name__}: {e}")
|
||||||
|
last_err = e
|
||||||
continue
|
continue
|
||||||
raise RuntimeError("All LLM providers failed")
|
raise RuntimeError(f"All LLM providers failed (last error: {last_err})")
|
||||||
|
|
||||||
async def _claude_call(messages: list, system: str = "") -> str:
|
async def _claude_call(messages: list, system: str = "") -> str:
|
||||||
payload = {"model": CLAUDE_MODEL, "max_tokens": 4096, "messages": messages}
|
payload = {"model": CLAUDE_MODEL, "max_tokens": 4096, "messages": messages}
|
||||||
@@ -155,26 +173,106 @@ async def _claude_call(messages: list, system: str = "") -> str:
|
|||||||
raise RuntimeError(f"Claude API error {resp.status}: {data.get('error',{}).get('message','')}")
|
raise RuntimeError(f"Claude API error {resp.status}: {data.get('error',{}).get('message','')}")
|
||||||
return data["content"][0]["text"]
|
return data["content"][0]["text"]
|
||||||
|
|
||||||
|
def _parse_groq_reset(val: str) -> float:
|
||||||
|
"""Parse Groq's Go-style duration strings ('229ms', '2.5s', '2m52.8s') into seconds."""
|
||||||
|
import re as _re
|
||||||
|
if not val:
|
||||||
|
return 0.0
|
||||||
|
total = 0.0
|
||||||
|
for num, unit in _re.findall(r'([\d.]+)(ms|s|m|h)', val):
|
||||||
|
n = float(num)
|
||||||
|
total += n/1000 if unit == 'ms' else n*60 if unit == 'm' else n*3600 if unit == 'h' else n
|
||||||
|
return total
|
||||||
|
|
||||||
async def _groq_call(messages: list, system: str = "") -> str:
|
async def _groq_call(messages: list, system: str = "") -> str:
|
||||||
|
# Added 2026-07-07: retry once on 429 (rate limit) using Groq's own reset-time
|
||||||
|
# header, capped short — this account's tier has a low 12k-tokens/min ceiling that
|
||||||
|
# gmail_triage alone can exhaust, so transient contention here is common and often
|
||||||
|
# clears in well under a second. Capped so a genuinely long reset just falls
|
||||||
|
# through to Ollama instead of blocking the whole compose flow.
|
||||||
all_msgs = ([{"role": "system", "content": system}] if system else []) + messages
|
all_msgs = ([{"role": "system", "content": system}] if system else []) + messages
|
||||||
payload = {"model": GROQ_MODEL, "messages": all_msgs, "max_tokens": 4096}
|
payload = {"model": GROQ_MODEL, "messages": all_msgs, "max_tokens": 4096}
|
||||||
headers = {"Authorization": f"Bearer {GROQ_API_KEY}", "Content-Type": "application/json"}
|
headers = {"Authorization": f"Bearer {GROQ_API_KEY}", "Content-Type": "application/json"}
|
||||||
async with aiohttp.ClientSession() as session:
|
for attempt in range(2):
|
||||||
async with session.post("https://api.groq.com/openai/v1/chat/completions", json=payload,
|
async with aiohttp.ClientSession() as session:
|
||||||
headers=headers, timeout=aiohttp.ClientTimeout(total=45)) as resp:
|
async with session.post("https://api.groq.com/openai/v1/chat/completions", json=payload,
|
||||||
data = await resp.json()
|
headers=headers, timeout=aiohttp.ClientTimeout(total=45)) as resp:
|
||||||
if resp.status != 200:
|
if resp.status == 429 and attempt == 0:
|
||||||
raise RuntimeError(f"Groq error {resp.status}")
|
wait = min(_parse_groq_reset(resp.headers.get("x-ratelimit-reset-tokens", "")) or
|
||||||
return data["choices"][0]["message"]["content"]
|
_parse_groq_reset(resp.headers.get("x-ratelimit-reset-requests", "")) or 2.0, 8.0)
|
||||||
|
log.info(f"[LLM] Groq 429 — retrying in {wait:.1f}s")
|
||||||
|
await asyncio.sleep(wait)
|
||||||
|
continue
|
||||||
|
data = await resp.json()
|
||||||
|
if resp.status != 200:
|
||||||
|
raise RuntimeError(f"Groq error {resp.status}")
|
||||||
|
return data["choices"][0]["message"]["content"]
|
||||||
|
|
||||||
async def _ollama_call(messages: list, system: str = "") -> str:
|
async def _ollama_call(messages: list, system: str = "") -> str:
|
||||||
prompt = (system + "\n\n" if system else "") + "\n".join(f"{m['role'].upper()}: {m['content']}" for m in messages)
|
prompt = (system + "\n\n" if system else "") + "\n".join(f"{m['role'].upper()}: {m['content']}" for m in messages)
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.post(f"{OLLAMA_HOST}/api/generate", json={"model": OLLAMA_MODEL, "prompt": prompt, "stream": False},
|
async with session.post(f"{OLLAMA_HOST}/api/generate", json={"model": OLLAMA_MODEL, "prompt": prompt, "stream": False},
|
||||||
timeout=aiohttp.ClientTimeout(total=30)) as resp:
|
timeout=aiohttp.ClientTimeout(total=90)) as resp: # bumped from 30s 2026-07-07: cold model loads on this CPU-only host alone took 15s+ in testing, leaving no room for actual generation
|
||||||
data = await resp.json()
|
data = await resp.json()
|
||||||
|
# Fixed 2026-07-07: this had no error checking at all — if the model wasn't
|
||||||
|
# pulled (or any other Ollama-side error), the API returns {"error": "..."}
|
||||||
|
# with no "response" key, and this silently returned "" instead of raising,
|
||||||
|
# which fooled llm_call()'s fallback into treating it as a real success.
|
||||||
|
if "error" in data:
|
||||||
|
raise RuntimeError(f"Ollama error: {data['error']}")
|
||||||
return data.get("response", "")
|
return data.get("response", "")
|
||||||
|
|
||||||
|
|
||||||
|
# -- VISION CALL ----------------------------------------------------------
|
||||||
|
async def _vision_call(image_b64: str, prompt: str) -> tuple:
|
||||||
|
"""
|
||||||
|
Vision provider cascade: Claude -> Ollama vision model -> graceful fallback.
|
||||||
|
Returns (analysis: str, provider: str).
|
||||||
|
To enable a local vision model: set OLLAMA_VISION_MODEL env var (e.g. "llava").
|
||||||
|
"""
|
||||||
|
# 1. Claude (primary)
|
||||||
|
if CLAUDE_API_KEY:
|
||||||
|
try:
|
||||||
|
import anthropic as _anthropic
|
||||||
|
_client = _anthropic.AsyncAnthropic(api_key=CLAUDE_API_KEY)
|
||||||
|
_msg = await _client.messages.create(
|
||||||
|
model="claude-opus-4-8-20251101",
|
||||||
|
max_tokens=2048,
|
||||||
|
messages=[{"role": "user", "content": [
|
||||||
|
{"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": image_b64}},
|
||||||
|
{"type": "text", "text": prompt},
|
||||||
|
]}],
|
||||||
|
)
|
||||||
|
text = _msg.content[0].text if _msg.content else ""
|
||||||
|
log.info("[VISION] Claude vision OK")
|
||||||
|
return text, "claude"
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"[VISION] Claude failed ({e}), trying next provider")
|
||||||
|
|
||||||
|
# 2. Ollama vision model (if configured via OLLAMA_VISION_MODEL env var)
|
||||||
|
if OLLAMA_VISION_MODEL:
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession() as _sess:
|
||||||
|
_payload = {"model": OLLAMA_VISION_MODEL, "prompt": prompt,
|
||||||
|
"images": [image_b64], "stream": False}
|
||||||
|
async with _sess.post(f"{OLLAMA_HOST}/api/generate", json=_payload,
|
||||||
|
timeout=aiohttp.ClientTimeout(total=120)) as _resp:
|
||||||
|
if _resp.status == 200:
|
||||||
|
_data = await _resp.json()
|
||||||
|
text = _data.get("response", "")
|
||||||
|
log.info(f"[VISION] Ollama vision OK ({OLLAMA_VISION_MODEL})")
|
||||||
|
return text, f"ollama/{OLLAMA_VISION_MODEL}"
|
||||||
|
raise RuntimeError(f"Ollama HTTP {_resp.status}")
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"[VISION] Ollama vision failed ({e})")
|
||||||
|
|
||||||
|
# 3. No vision provider available
|
||||||
|
msg = ("[Vision unavailable] Claude credits depleted and no local vision model configured. "
|
||||||
|
"To enable: pull a vision model on Ollama (e.g. 'ollama pull llava') then set "
|
||||||
|
"OLLAMA_VISION_MODEL=llava in the jarvis-arc systemd environment.")
|
||||||
|
log.warning("[VISION] All vision providers unavailable")
|
||||||
|
return msg, "none"
|
||||||
|
|
||||||
async def handle_llm(payload: dict) -> dict:
|
async def handle_llm(payload: dict) -> dict:
|
||||||
message = payload.get("message", "")
|
message = payload.get("message", "")
|
||||||
system = payload.get("system", "You are JARVIS, an Iron Man-style AI assistant.")
|
system = payload.get("system", "You are JARVIS, an Iron Man-style AI assistant.")
|
||||||
@@ -662,27 +760,8 @@ async def handle_screenshot(payload: dict) -> dict:
|
|||||||
analysis = ""
|
analysis = ""
|
||||||
provider_used = ""
|
provider_used = ""
|
||||||
if do_analyze and image_b64:
|
if do_analyze and image_b64:
|
||||||
try:
|
analysis, provider_used = await _vision_call(image_b64, analyze_prompt)
|
||||||
import anthropic
|
log.info(f"[VISION] Analysis complete via {provider_used} ({len(analysis)} chars)")
|
||||||
client = anthropic.AsyncAnthropic(api_key=CLAUDE_API_KEY)
|
|
||||||
msg = await client.messages.create(
|
|
||||||
model="claude-opus-4-8-20251101",
|
|
||||||
max_tokens=1024,
|
|
||||||
messages=[{
|
|
||||||
"role": "user",
|
|
||||||
"content": [
|
|
||||||
{"type": "image", "source": {"type": "base64",
|
|
||||||
"media_type": "image/png", "data": image_b64}},
|
|
||||||
{"type": "text", "text": analyze_prompt},
|
|
||||||
],
|
|
||||||
}],
|
|
||||||
)
|
|
||||||
analysis = msg.content[0].text if msg.content else ""
|
|
||||||
provider_used = "claude"
|
|
||||||
log.info(f"[VISION] Claude analysis complete ({len(analysis)} chars)")
|
|
||||||
except Exception as e:
|
|
||||||
log.warning(f"[VISION] Claude vision failed: {e}")
|
|
||||||
analysis = f"Vision analysis unavailable: {e}"
|
|
||||||
elif do_analyze and not image_b64 and result.get("snapshot_type") == "text":
|
elif do_analyze and not image_b64 and result.get("snapshot_type") == "text":
|
||||||
# Text-only sysinfo snapshot — summarize with LLM
|
# Text-only sysinfo snapshot — summarize with LLM
|
||||||
try:
|
try:
|
||||||
@@ -744,30 +823,13 @@ async def handle_vision(payload: dict) -> dict:
|
|||||||
|
|
||||||
log.info(f"[VISION] Analysis: screenshot_id={screenshot_id} agent={hostname}")
|
log.info(f"[VISION] Analysis: screenshot_id={screenshot_id} agent={hostname}")
|
||||||
|
|
||||||
try:
|
analysis, provider_used = await _vision_call(image_b64, prompt)
|
||||||
import anthropic
|
|
||||||
client = anthropic.AsyncAnthropic(api_key=CLAUDE_API_KEY)
|
|
||||||
msg = await client.messages.create(
|
|
||||||
model="claude-opus-4-8-20251101",
|
|
||||||
max_tokens=2048,
|
|
||||||
messages=[{
|
|
||||||
"role": "user",
|
|
||||||
"content": [
|
|
||||||
{"type": "image", "source": {"type": "base64",
|
|
||||||
"media_type": "image/png", "data": image_b64}},
|
|
||||||
{"type": "text", "text": prompt},
|
|
||||||
],
|
|
||||||
}],
|
|
||||||
)
|
|
||||||
analysis = msg.content[0].text if msg.content else ""
|
|
||||||
except Exception as e:
|
|
||||||
raise RuntimeError(f"Vision analysis failed: {e}")
|
|
||||||
|
|
||||||
# Update stored screenshot if we have an ID
|
# Update stored screenshot if we have an ID
|
||||||
if screenshot_id:
|
if screenshot_id:
|
||||||
await db_execute(
|
await db_execute(
|
||||||
"UPDATE agent_screenshots SET vision_analysis=%s, vision_provider=%s WHERE id=%s",
|
"UPDATE agent_screenshots SET vision_analysis=%s, vision_provider=%s WHERE id=%s",
|
||||||
(analysis, "claude", int(screenshot_id))
|
(analysis, provider_used, int(screenshot_id))
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -775,7 +837,7 @@ async def handle_vision(payload: dict) -> dict:
|
|||||||
"screenshot_id": screenshot_id,
|
"screenshot_id": screenshot_id,
|
||||||
"prompt": prompt,
|
"prompt": prompt,
|
||||||
"analysis": analysis,
|
"analysis": analysis,
|
||||||
"provider": "claude",
|
"provider": provider_used,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -2769,5 +2831,33 @@ async def comms_sent_delete(sent_id: int):
|
|||||||
await db_execute("DELETE FROM email_sent WHERE id=%s", (sent_id,))
|
await db_execute("DELETE FROM email_sent WHERE id=%s", (sent_id,))
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
||||||
|
@app.post("/comms/sent/{sent_id}/send")
|
||||||
|
async def comms_sent_send(sent_id: int):
|
||||||
|
"""
|
||||||
|
Added 2026-07-07: sends a previously-composed queued draft. Compose always
|
||||||
|
created a draft (status='queued') for review, but there was no action anywhere
|
||||||
|
to actually send one — this closes that gap. handle_send_email() records its own
|
||||||
|
fresh row in email_sent (sent/failed), so the original queued draft row is removed
|
||||||
|
here once we've handed its content off, rather than leaving a stale duplicate.
|
||||||
|
"""
|
||||||
|
row = await db_fetchone(
|
||||||
|
"SELECT account, to_email, to_name, subject, body, triage_id FROM email_sent WHERE id=%s AND status='queued'",
|
||||||
|
(sent_id,)
|
||||||
|
)
|
||||||
|
if not row:
|
||||||
|
raise HTTPException(status_code=404, detail="Queued draft not found")
|
||||||
|
|
||||||
|
result = await handle_send_email({
|
||||||
|
"account": row["account"],
|
||||||
|
"to_email": row["to_email"],
|
||||||
|
"to_name": row["to_name"],
|
||||||
|
"subject": row["subject"],
|
||||||
|
"body": row["body"],
|
||||||
|
"triage_id": row["triage_id"],
|
||||||
|
})
|
||||||
|
|
||||||
|
await db_execute("DELETE FROM email_sent WHERE id=%s", (sent_id,))
|
||||||
|
return result
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
uvicorn.run("reactor:app", host=HOST, port=PORT, log_level="info", access_log=False)
|
uvicorn.run("reactor:app", host=HOST, port=PORT, log_level="info", access_log=False)
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
fastapi
|
||||||
|
uvicorn[standard]
|
||||||
|
aiomysql
|
||||||
|
aiohttp
|
||||||
|
anthropic
|
||||||
|
trafilatura
|
||||||
@@ -1,738 +0,0 @@
|
|||||||
# INFRASTRUCTURE REFERENCE — COMPLETE SYSTEM MAP
|
|
||||||
**Last Updated:** 2026-06-18
|
|
||||||
**Owner:** Myron Blair — myronblair@outlook.com
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## TABLE OF CONTENTS
|
|
||||||
1. [Network Overview](#1-network-overview)
|
|
||||||
2. [Cloud Servers](#2-cloud-servers)
|
|
||||||
3. [On-Premise — Proxmox Hypervisors](#3-on-premise--proxmox-hypervisors)
|
|
||||||
4. [On-Premise — Virtual Machines](#4-on-premise--virtual-machines)
|
|
||||||
5. [NAS Storage](#5-nas-storage)
|
|
||||||
6. [Websites (all on DO)](#6-websites--all-on-do)
|
|
||||||
7. [JARVIS AI System](#7-jarvis-ai-system)
|
|
||||||
8. [Phone System (FusionPBX)](#8-phone-system-fusionpbx)
|
|
||||||
9. [Networking & VPN](#9-networking--vpn)
|
|
||||||
10. [Backup Systems](#10-backup-systems)
|
|
||||||
11. [SSH Quick Reference](#11-ssh-quick-reference)
|
|
||||||
12. [Critical Credentials Master List](#12-critical-credentials-master-list)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. NETWORK OVERVIEW
|
|
||||||
|
|
||||||
```
|
|
||||||
INTERNET
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
[Cloudflare CDN] ──────────────────────────────────────────────────────────────
|
|
||||||
│ (proxied DNS for public sites)
|
|
||||||
│
|
|
||||||
├─► [DigitalOcean 165.22.1.228] — CyberPanel/OLS — All websites (6 sites)
|
|
||||||
│
|
|
||||||
└─► [FusionPBX 134.209.72.226] — FreeSWITCH PBX (SSH via DO relay)
|
|
||||||
|
|
||||||
HOME NETWORK (FortiGate router at 10.48.200.1)
|
|
||||||
WAN: 97.154.109.245 (dynamic, DDNS: orbisne.fortiddns.com)
|
|
||||||
│
|
|
||||||
├─► PVE1 Proxmox 10.48.200.90 (primary hypervisor)
|
|
||||||
│ ├── VM 101 10.48.200.97 Home Assistant
|
|
||||||
│ ├── VM 112 10.48.200.33 Jellyfin
|
|
||||||
│ ├── VM 113 10.48.200.35 MediaStack (Sonarr/Radarr/qBT/Prowlarr)
|
|
||||||
│ ├── VM 118 10.48.200.18 Homebridge
|
|
||||||
│ ├── VM 120 10.48.200.110 NovaCPX hosting panel
|
|
||||||
│ ├── VM 210 10.48.200.210 Ollama (local LLM) (local LLM)
|
|
||||||
│ └── CT110 10.48.200.19 WireGuard exit container
|
|
||||||
│
|
|
||||||
├─► PVE2 Proxmox 10.48.200.91 (secondary hypervisor)
|
|
||||||
│ └── VM 302 10.48.200.99 NetworkBackup
|
|
||||||
│
|
|
||||||
├─► Synology NAS 10.48.200.249 — Media & backup storage
|
|
||||||
├─► Yealink T48S 10.48.200.2 — Ext 1000 (Myron Blair, Desk)
|
|
||||||
├─► Yealink T48S 10.48.200.43 — Ext 1001 (Tommy Ivy, Desk)
|
|
||||||
├─► Yealink AX86R 10.48.200.65 — Ext 1002 (Myron Blair, WiFi Work)
|
|
||||||
├─► Yealink T57W 10.48.200.3 — External SIP (United Mirror & Glass)
|
|
||||||
├─► Yealink T57W 10.48.200.83 — Ext 1003 (Kitchen)
|
|
||||||
└─► Yealink T57W 10.48.200.85 — Ext 1004 (Master Bedroom)
|
|
||||||
|
|
||||||
FortiGate Port Forwards:
|
|
||||||
orbisne.fortiddns.com:8006 → PVE1:8006 (Proxmox web UI)
|
|
||||||
orbisne.fortiddns.com:8123 → HA:8123 (Home Assistant)
|
|
||||||
orbisne.fortiddns.com:22 → HA VM:22 (SSH — key only, unreliable)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. CLOUD SERVERS
|
|
||||||
|
|
||||||
### 2A. DigitalOcean — Main Server
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **IP** | 165.22.1.228 |
|
|
||||||
| **OS** | Ubuntu 22.04 LTS |
|
|
||||||
| **Panel** | CyberPanel (OpenLiteSpeed) |
|
|
||||||
| **SSH** | `ssh root@165.22.1.228` — password: `Gonewalk1974!@#` |
|
|
||||||
| **Purpose** | All public websites (6 sites) — webhook deploy for websites |
|
|
||||||
|
|
||||||
**Key Paths:**
|
|
||||||
- All sites: `/home/<domain>/public_html/`
|
|
||||||
|
|
||||||
- Deploy log: per-site (website deploys only)
|
|
||||||
- Watchdog log: `/usr/local/lsws/logs/watchdog.log`
|
|
||||||
- Infra repo: `/opt/infra`
|
|
||||||
|
|
||||||
**Services running:**
|
|
||||||
- OpenLiteSpeed web server (`lsws`) — serves all 7 sites
|
|
||||||
- MySQL 8 — all site databases on localhost
|
|
||||||
- Redis — session/cache
|
|
||||||
- PHP 8.5 (`lsphp85`) — runtime for all sites
|
|
||||||
- Cron jobs: website deploy runner (every 1 min), watchdog (every 5 min)
|
|
||||||
|
|
||||||
**CyberPanel Web UI:** `https://165.22.1.228:8090`
|
|
||||||
Login: `myron / Joker1974!!!`
|
|
||||||
|
|
||||||
**phpMyAdmin:** `https://165.22.1.228/phpmyadmin`
|
|
||||||
Login: `myron / Joker1974!!!`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2B. FusionPBX / FreeSWITCH — PBX Server
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **IP** | 134.209.72.226 |
|
|
||||||
| **OS** | Debian (DigitalOcean droplet) |
|
|
||||||
| **SSH** | Direct via Tailscale: `ssh root@100.74.46.120` — password: `Joker1974!@#` |
|
|
||||||
| **Direct SSH** | Only from: 107.178.2.130 / 97.154.109.245 |
|
|
||||||
| **Purpose** | VoIP phone system — handles all inbound/outbound calls |
|
|
||||||
|
|
||||||
**Web UI:** `https://fusion.orbishosting.com`
|
|
||||||
Login: `admin / fY7XP5swgtpbzrYLhkeVYkA4744`
|
|
||||||
|
|
||||||
**Database:** PostgreSQL
|
|
||||||
User: `fusionpbx` / Password: `pSJaF9mUJqPr4Sj5mwJyRqvCCpc` / Host: 127.0.0.1
|
|
||||||
|
|
||||||
**SIP Trunk:** SignalWire
|
|
||||||
DID: +1 (817) 764-5007
|
|
||||||
Gateway: `signalwire` on external profile (port 5080, UDP)
|
|
||||||
|
|
||||||
**How calls flow:**
|
|
||||||
```
|
|
||||||
Caller → SignalWire SIP → FusionPBX:5080 → IVR (ext 900) → Ring extensions
|
|
||||||
Outbound: Phone → FusionPBX:5080 → SignalWire → PSTN
|
|
||||||
```
|
|
||||||
|
|
||||||
**SSH Relay Command:**
|
|
||||||
```bash
|
|
||||||
sshpass -p 'Gonewalk1974!@#' ssh -o StrictHostKeyChecking=no root@165.22.1.228 \
|
|
||||||
'sshpass -p "Joker1974!@#" ssh -o StrictHostKeyChecking=no root@134.209.72.226 "COMMAND"'
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. ON-PREMISE — PROXMOX HYPERVISORS
|
|
||||||
|
|
||||||
### PVE1 — Primary Hypervisor
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **Local IP** | 10.48.200.90 |
|
|
||||||
| **External** | orbisne.fortiddns.com (FortiGate DDNS — auto-updates on WAN IP change) |
|
|
||||||
| **OS** | Proxmox VE 8.x |
|
|
||||||
| **SSH** | `ssh root@orbisne.fortiddns.com` OR `ssh root@10.48.200.90` — password: `Joker1974!!!` |
|
|
||||||
| **Web UI** | `https://orbisne.fortiddns.com:8006` — `root / Joker1974!!!` |
|
|
||||||
| **Purpose** | Runs VMs 101, 112, 113, 118, 120, 210, CT110 |
|
|
||||||
|
|
||||||
**Useful commands:**
|
|
||||||
```bash
|
|
||||||
qm list # list all VMs
|
|
||||||
qm start/stop/restart <VMID> # control VMs
|
|
||||||
qm guest exec <VMID> -- bash -c "cmd" # run command inside VM (requires QEMU agent)
|
|
||||||
```
|
|
||||||
|
|
||||||
**JARVIS API Token:** `root@pam!jarvis=c45b5feb-f9a9-445d-a626-14fbb959f78b`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### PVE2 — Secondary Hypervisor
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **Local IP** | 10.48.200.91 |
|
|
||||||
| **OS** | Proxmox VE 8.x |
|
|
||||||
| **SSH** | `ssh root@10.48.200.91` — password: `Joker1974!!!` |
|
|
||||||
| **Web UI** | `https://10.48.200.91:8006` — `root / Joker1974!!!` |
|
|
||||||
| **Purpose** | Runs VM 302 (NetworkBackup); part of shared Proxmox cluster with PVE1 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. ON-PREMISE — VIRTUAL MACHINES
|
|
||||||
|
|
||||||
### VM 101 — Home Assistant (PVE1)
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **IP** | 10.48.200.97 |
|
|
||||||
| **OS** | Ubuntu + Home Assistant OS/Supervised |
|
|
||||||
| **Web UI** | `http://orbisne.fortiddns.com:8123` — `myron / [HA password]` |
|
|
||||||
| **SSH** | Via HA web terminal only (Settings → Add-ons → Advanced SSH & Web Terminal) |
|
|
||||||
| **Purpose** | Smart home automation — 212 entities (lights, switches, scenes, sensors) |
|
|
||||||
| **JARVIS Agent** | ID: `homeassistant_ha` — pushes entity states to JARVIS every 10s |
|
|
||||||
|
|
||||||
**JARVIS ↔ HA Integration:**
|
|
||||||
- HA custom component at `/config/custom_components/jarvis_agent/`
|
|
||||||
- Pushes all entity state changes to JARVIS `/api/agent/ha_state` (debounced 2s)
|
|
||||||
- JARVIS admin toggles → queued in `agent_commands` table → HA executes natively
|
|
||||||
- HA Long-lived Token (Jarvis2): `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIzNmI0N2I1Njk5ZGQ0MTQ2ODMwZWFmYjZiYTQ1MjJkMSIsImlhdCI6MTc4MDIwMzU5NCwiZXhwIjoyMDk1NTYzNTk0fQ.sYRok-jRDlA4lFgWxLQELcEjkJNGQdprk6ZziLwLtXE`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### VM 112 — Jellyfin Media Server (PVE1)
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **IP** | 10.48.200.33 |
|
|
||||||
| **OS** | Ubuntu 22.04 LTS |
|
|
||||||
| **SSH** | `ssh root@10.48.200.33` — password: `Joker1974!!!` (enabled 2026-06-14) |
|
|
||||||
| **Web UI** | `http://10.48.200.33:8096` |
|
|
||||||
| **Purpose** | Media streaming server — Movies and TV shows |
|
|
||||||
| **JARVIS Agent** | Not yet installed |
|
|
||||||
|
|
||||||
**Media Libraries:**
|
|
||||||
- Movies: `/mnt/mediastack/movies` — NFS from MediaStack (10.48.200.35:/media/movies)
|
|
||||||
- TV: `/mnt/mediastack/tv` — NFS from MediaStack (10.48.200.35:/media/tv)
|
|
||||||
|
|
||||||
**NFS chain:** Jellyfin → MediaStack → Synology NAS (`/volume1/video/movies` and `/volume1/video/tv`)
|
|
||||||
|
|
||||||
**Admin token:** `7c0ccf78b91d4b5bafa607f585f24f2d`
|
|
||||||
|
|
||||||
**If library scan needed:**
|
|
||||||
```bash
|
|
||||||
curl -X POST "http://10.48.200.33:8096/Library/Refresh" \
|
|
||||||
-H "X-Emby-Token: 7c0ccf78b91d4b5bafa607f585f24f2d"
|
|
||||||
```
|
|
||||||
|
|
||||||
**If NFS stale after MediaStack changes:**
|
|
||||||
```bash
|
|
||||||
umount -l /mnt/mediastack/movies && umount -l /mnt/mediastack/tv
|
|
||||||
mount /mnt/mediastack/movies && mount /mnt/mediastack/tv
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### VM 113 — MediaStack (PVE1)
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **IP** | 10.48.200.35 |
|
|
||||||
| **OS** | Ubuntu 24.04 LTS |
|
|
||||||
| **SSH** | Via PVE1: `ssh -i /root/.ssh/id_rsa root@10.48.200.35` (no direct access from DO) |
|
|
||||||
| **Purpose** | Automated media download pipeline + NFS server to Jellyfin |
|
|
||||||
| **JARVIS Agent** | ID: `MediaStack_2c00b1b8` |
|
|
||||||
|
|
||||||
**Services:**
|
|
||||||
| Service | Port | Login | API Key |
|
|
||||||
|---------|------|-------|---------|
|
|
||||||
| qBittorrent | :8080 | `admin / Joker1974!!!` | — |
|
|
||||||
| Sonarr | :8989 | `admin / Joker1974!!!` | `b43e04350a594846b4ee95261c29e9e0` |
|
|
||||||
| Radarr | :7878 | `admin / Joker1974!!!` | `53c4268360444feeae5f98c0cc24e0e3` |
|
|
||||||
| Prowlarr | :9696 | `admin / Joker1974!!!` | `9d0ce6c5660743b5bf1c7951efc62252` |
|
|
||||||
|
|
||||||
**All services run as root** — required by Synology NFS ACL (only root can write).
|
|
||||||
|
|
||||||
**VPN:** NordVPN — `nordlynx` WireGuard interface — exit IP 181.214.226.188 (US Dallas)
|
|
||||||
All download traffic exits via NordVPN. If downloads stall, check: `ip rule show` for rules 32764/32765.
|
|
||||||
|
|
||||||
**Media Flow:**
|
|
||||||
```
|
|
||||||
IPTorrents (Prowlarr) → Sonarr/Radarr search → qBittorrent download
|
|
||||||
→ /mnt/nas/video/downloads (NAS)
|
|
||||||
→ Sonarr/Radarr import → /mnt/nas/video/tv or /mnt/nas/video/movies (NAS)
|
|
||||||
→ NFS → Jellyfin /mnt/mediastack/movies or /mnt/mediastack/tv
|
|
||||||
```
|
|
||||||
|
|
||||||
**Indexer:** IPTorrents via Prowlarr cookie auth
|
|
||||||
Cookie: `uid=2237410; pass=JzLP2niTWxBJAZIU3yvtLbJzD55kdLeB`
|
|
||||||
(Expires — if search fails, log into iptorrents.com, copy uid+pass cookies)
|
|
||||||
|
|
||||||
**If Radarr/Sonarr shows "0 active indexers":**
|
|
||||||
```bash
|
|
||||||
systemctl stop radarr
|
|
||||||
sqlite3 /var/lib/radarr/radarr.db "DELETE FROM IndexerStatus WHERE ProviderId=1;"
|
|
||||||
systemctl start radarr
|
|
||||||
```
|
|
||||||
|
|
||||||
**SSH from DO:**
|
|
||||||
```bash
|
|
||||||
sshpass -p 'Joker1974!!!' ssh -o StrictHostKeyChecking=no root@10.48.200.90 \
|
|
||||||
'ssh -o StrictHostKeyChecking=no -i /root/.ssh/id_rsa root@10.48.200.35 "COMMAND"'
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### VM 118 — Homebridge (PVE1)
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **IP** | 10.48.200.18 |
|
|
||||||
| **OS** | Linux |
|
|
||||||
| **SSH** | `ssh myron@10.48.200.18` — password: `Joker1974!` |
|
|
||||||
| **Purpose** | Apple HomeKit bridge — exposes non-HomeKit devices to Apple Home app |
|
|
||||||
| **JARVIS Agent** | ID: `homebridge_b57cbaea` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### VM 120 — NovaCPX Hosting Panel (PVE1)
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **IP** | 10.48.200.110 |
|
|
||||||
| **OS** | Ubuntu 24.04 LTS |
|
|
||||||
| **SSH** | `ssh root@10.48.200.110` — password: `Joker1974!!!` (direct, no PVE hop) |
|
|
||||||
| **Purpose** | Custom web hosting control panel (cPanel alternative), v1.0.27 |
|
|
||||||
| **JARVIS Agent** | ID: `novacpx_e3b07264` |
|
|
||||||
|
|
||||||
**Ports:**
|
|
||||||
| Port | Panel |
|
|
||||||
|------|-------|
|
|
||||||
| :8880 | User panel |
|
|
||||||
| :8881 | Reseller panel |
|
|
||||||
| :8882 | Admin panel |
|
|
||||||
| :8883 | Roundcube webmail |
|
|
||||||
|
|
||||||
**Admin:** `https://10.48.200.110:8882` — `admin / Admin2026!`
|
|
||||||
**phpMyAdmin:** `http://10.48.200.110/phpmyadmin`
|
|
||||||
|
|
||||||
**File Paths:**
|
|
||||||
- Web root: `/srv/novacpx/public/`
|
|
||||||
- DB (SQLite): `/var/lib/novacpx/panel.db`
|
|
||||||
- Config: `/etc/novacpx/config.ini`
|
|
||||||
- Git repo: `/opt/novacpx-src/`
|
|
||||||
- GitHub: `myronblair/novacpx` (auto-deploy on push to `main`)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### VM 210 — Ollama Local LLM (PVE1)
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **IP** | 10.48.200.210 |
|
|
||||||
| **OS** | Ubuntu (cloud image) |
|
|
||||||
| **SSH** | `ssh myron@10.48.200.95` — password: `Joker1974!` (then `sudo`) |
|
|
||||||
| **Purpose** | Local AI inference — runs llama3.2 model for JARVIS Tier 1 chat |
|
|
||||||
| **API** | `http://10.48.200.210:11434` (Ollama REST API) |
|
|
||||||
| **JARVIS Agent** | ID: `ollama-ai_ubuntu` |
|
|
||||||
|
|
||||||
**JARVIS uses this as Tier 1 AI** — if Ollama is down, falls back to Groq (cloud).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### VM 302 — NetworkBackup (PVE2)
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **IP** | 10.48.200.99 |
|
|
||||||
| **OS** | Ubuntu/Linux |
|
|
||||||
| **SSH** | `ssh myron@10.48.200.99` — password: `Joker1974!` (then `sudo`) |
|
|
||||||
| **Purpose** | Network backup storage / backup operations |
|
|
||||||
| **JARVIS Agent** | ID: `networkbackup_NetworkB` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### CT110 — WireGuard Exit Container (PVE1)
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **IP** | 10.48.200.19 / 10.48.200.67 |
|
|
||||||
| **Purpose** | Legacy WireGuard exit tunnel to DO (10.200.0.4 via wg-exit) — currently NOT used by MediaStack/Jellyfin |
|
|
||||||
| **Note** | MediaStack uses NordVPN directly; Jellyfin uses wg1 peer on MediaStack for NFS only |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. NAS STORAGE
|
|
||||||
|
|
||||||
### Synology NAS
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **IP** | 10.48.200.249 |
|
|
||||||
| **Login** | `nas / Joker1974!!!` |
|
|
||||||
| **DSM Web UI** | `http://10.48.200.249:5000` |
|
|
||||||
| **Purpose** | Primary media and download storage |
|
|
||||||
|
|
||||||
**NFS Share:** `/volume1/video` (exported to MediaStack only)
|
|
||||||
|
|
||||||
**Directory structure:**
|
|
||||||
```
|
|
||||||
/volume1/video/
|
|
||||||
movies/ ← Radarr imports here; NFS-exported to Jellyfin via MediaStack
|
|
||||||
tv/ ← Sonarr imports here; NFS-exported to Jellyfin via MediaStack
|
|
||||||
downloads/ ← qBittorrent downloads here (temp)
|
|
||||||
incomplete/ ← in-progress torrents
|
|
||||||
```
|
|
||||||
|
|
||||||
**Important:** Synology NFS ACL only allows root to write. All services on MediaStack run as root.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. WEBSITES (ALL ON DO)
|
|
||||||
|
|
||||||
All sites are at `/home/<domain>/public_html/` on DO (165.22.1.228).
|
|
||||||
**Auto-deploy:** Push to `main` on GitHub → webhook → server pulls in ~1 min.
|
|
||||||
**GitHub PAT:** `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` (expires ~2026-08-20)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### jarvis.orbishosting.com — JARVIS AI Dashboard (MOVED TO PVE1 VM 211)
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **URL** | http://jarvis.orbishosting.com:1972 |
|
|
||||||
| **Path** | `/var/www/jarvis/ (on JARVIS VM 10.48.200.211)` |
|
|
||||||
| **GitHub** | `myronblair/jarvis` |
|
|
||||||
| **Login** | `myron / Joker1974!!!` |
|
|
||||||
| **Purpose** | Iron Man-style AI home dashboard with voice control, smart home, media, planner |
|
|
||||||
|
|
||||||
See Section 7 for full JARVIS details.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### tomsjavajive.com — Tom's Java Jive
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **URL** | https://tomsjavajive.com |
|
|
||||||
| **Path** | `/home/tomsjavajive.com/public_html/` |
|
|
||||||
| **GitHub** | `myronblair/tomsjavajive` |
|
|
||||||
| **Purpose** | Coffee shop e-commerce — products, orders, loyalty, wallet, reviews |
|
|
||||||
| **Admin URL** | `https://tomsjavajive.com/admin/` |
|
|
||||||
| **Admin Login** | `admin@tomsjavajive.com / Joker1974!!!` OR `myronblair@outlook.com / Joker1974!!!` |
|
|
||||||
| **DB** | `toms_tjj_db / toms_tjj_user / +60wlPc+55e@gFq4` |
|
|
||||||
| **Email** | CyberMail API key: `sk_live_7f9b0f9a29f6de31a0d229d4af75d56b094ad724fc58a57d` |
|
|
||||||
| **Email From** | `noreply@tomsjavajive.com` / `Toms Java Jive` (set in DB settings table) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### epictravelexpeditions.com — Epic Travel Expeditions
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **URL** | https://epictravelexpeditions.com |
|
|
||||||
| **Path** | `/home/epictravelexpeditions.com/public_html/` |
|
|
||||||
| **GitHub** | `myronblair/epictravelexpeditions` |
|
|
||||||
| **Purpose** | Travel booking / expeditions website |
|
|
||||||
| **DB** | `epic_travel_db` (see `api/config.php`) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### parkerslingshot.epictravelexpeditions.com — Parker Slingshot (OLD)
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **URL** | https://parkerslingshot.epictravelexpeditions.com |
|
|
||||||
| **Path** | `/home/epictravelexpeditions.com/parkerslingshot/` |
|
|
||||||
| **GitHub** | `myronblair/parkerslingshot` |
|
|
||||||
| **Purpose** | Old slingshot rental site (superseded by parkerslingshotrentals.com) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### parkerslingshotrentals.com — Parker Slingshot Rentals (LIVE)
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **URL** | https://www.parkerslingshotrentals.com |
|
|
||||||
| **Path** | `/home/parkerslingshotrentals.com/public_html/` |
|
|
||||||
| **GitHub** | `myronblair/parkerslingshotrentals` |
|
|
||||||
| **Purpose** | Polaris Slingshot rental — bookings, e-signature waiver, admin management |
|
|
||||||
| **Admin** | `/admin/index.php` — `admin / Parker2026!` |
|
|
||||||
| **DB** | `park_slingshot / park_slingshotuser / 4@rxg*8kovxCr7w6` |
|
|
||||||
| **Square** | Production token: `EAAAl3FsAu_2ri8kZE_ENEyi2T_C8HXXm5XQFY6Lbnd8SX6FqYp8J_upUeXNYh7v` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### orbishosting.com — Orbis Hosting (Landing Page)
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **URL** | https://orbishosting.com |
|
|
||||||
| **Path** | `/home/orbishosting.com/public_html/` |
|
|
||||||
| **GitHub** | `myronblair/orbishosting` |
|
|
||||||
| **Purpose** | Public landing page for Orbis Hosting brand |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### orbis.orbishosting.com — Orbis Hosting Portal
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **URL** | https://orbis.orbishosting.com |
|
|
||||||
| **Path** | `/home/orbis.orbishosting.com/public_html/` |
|
|
||||||
| **GitHub** | `myronblair/orbis-hosting-portal` |
|
|
||||||
| **Purpose** | Customer-facing hosting portal |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### tomtomgames.com — TomTom Games
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **URL** | https://tomtomgames.com |
|
|
||||||
| **Path** | `/home/tomtomgames.com/public_html/` |
|
|
||||||
| **GitHub** | `myronblair/tomtomgames` |
|
|
||||||
| **Purpose** | Gaming website |
|
|
||||||
| **DB** | `tomtom_games_db` (see config) |
|
|
||||||
| **Email** | CyberMail API key: `sk_live_7f9b...` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. JARVIS AI SYSTEM
|
|
||||||
|
|
||||||
**URL:** http://jarvis.orbishosting.com:1972
|
|
||||||
**Files:** `/var/www/jarvis/` on JARVIS VM (PVE1 VM 211 — 10.48.200.211, 8 cores, 16GB RAM)
|
|
||||||
**DB:** `jarvis_db` — `jarvis_user / J4rv1s_Pr0t0c0l_2026!`
|
|
||||||
**Login:** `myron / Joker1974!!!`
|
|
||||||
**Admin portal:** http://jarvis.orbishosting.com:1972/admin
|
|
||||||
|
|
||||||
### Architecture (end-to-end)
|
|
||||||
|
|
||||||
```
|
|
||||||
Voice (browser mic)
|
|
||||||
→ SpeechRecognition API
|
|
||||||
→ Wake phrase: "wake up JARVIS" / "daddy's home"
|
|
||||||
→ "JARVIS [command]" triggers action
|
|
||||||
→ /api/chat.php (4-tier AI)
|
|
||||||
Tier 0.7: KB intents / planner (tasks, appointments)
|
|
||||||
Tier 1: Knowledge Base (MySQL)
|
|
||||||
Tier 1.5: Ollama (10.48.200.210:11434, llama3.2) — local LLM
|
|
||||||
Tier 2: Groq (cloud, model: compound-beta-mini)
|
|
||||||
Tier 3: Claude API (Anthropic, fallback)
|
|
||||||
→ ElevenLabs TTS → browser speaker
|
|
||||||
```
|
|
||||||
|
|
||||||
### Deploy Pipeline
|
|
||||||
```
|
|
||||||
Code edit → git push → GitHub webhook → /webhook.php (HMAC verified)
|
|
||||||
→ /tmp/jarvis-deploy-queue.txt → /usr/local/bin/jarvis-deploy.sh (cron 1min)
|
|
||||||
→ git pull + PHP syntax check → deploy or auto-revert
|
|
||||||
```
|
|
||||||
Webhook secret: `4c8805f0285214ff0a0602b5880270b935f36a896946c7f1`
|
|
||||||
|
|
||||||
### Agent System
|
|
||||||
Agents installed on all servers — phone home every 10s (heartbeat) / 30s (metrics).
|
|
||||||
Registration key: `f846a9aaf7ce9a61742c63c87c4186052a71d2a580c65518`
|
|
||||||
Install command: `curl -sk http://10.48.200.211/install-agent.sh | bash -s <hostname> <linux|proxmox>`
|
|
||||||
|
|
||||||
### Self-Healing Watchdog
|
|
||||||
`/usr/local/bin/jarvis-watchdog.sh` — runs every 5 min (root cron on DO)
|
|
||||||
Restarts: lsws, mysql, redis if down
|
|
||||||
Restarts offline Proxmox VM agents via `qm guest exec`
|
|
||||||
|
|
||||||
### Cron Jobs (DO server)
|
|
||||||
| Schedule | Script | Purpose |
|
|
||||||
|----------|--------|---------|
|
|
||||||
| Every 1 min | `jarvis-deploy.sh` | Process GitHub deploy queue |
|
|
||||||
| Every 3 min | `facts_collector.php` | Collect agent metrics, KB facts, site health |
|
|
||||||
| Every 5 min | `stats_cache.php` | Weather, news, Proxmox stats refresh |
|
|
||||||
| Every 5 min | `jarvis-watchdog.sh` | Self-healing: restart dead services |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. PHONE SYSTEM (FUSIONPBX)
|
|
||||||
|
|
||||||
### Extensions
|
|
||||||
| Ext | Name | Phone | IP | SIP Password |
|
|
||||||
|-----|------|-------|----|-------------|
|
|
||||||
| 1000 | Myron Blair — Desk | Yealink T48S | 10.48.200.2 | `Xk9mPw3nQv7rLs2t` |
|
|
||||||
| 1001 | Tommy Ivy — Desk | Yealink T48S | 10.48.200.43 | `Tv8xNm4pWq6rZs3k` |
|
|
||||||
| 1002 | Myron Blair — WiFi Work | Yealink AX86R | 10.48.200.65 | `yXHaJTwa8rj?$GkrVFQB` |
|
|
||||||
| 1003 | Kitchen | Yealink T57W | 10.48.200.83 | — |
|
|
||||||
| 1004 | Master Bedroom | Yealink T57W | 10.48.200.85 | — |
|
|
||||||
| 1010 | Parker County Slingshot | Virtual (voicemail only) | — | — |
|
|
||||||
| 1011 | Epic Travel Expeditions | Virtual (voicemail only) | — | — |
|
|
||||||
| 1012 | Tom's Java Jive | Virtual (voicemail only) | — | — |
|
|
||||||
| 900 | IVR | — | — | (auto-attendant) |
|
|
||||||
|
|
||||||
**Phone SIP Settings (all phones):**
|
|
||||||
- Server: `134.209.72.226`
|
|
||||||
- Port: `5080`
|
|
||||||
- Transport: UDP
|
|
||||||
|
|
||||||
**Provisioning URL:** `https://fusion.orbishosting.com/app/provision/`
|
|
||||||
(Username: `provision-master`, Password: `Joker1974!!!`)
|
|
||||||
|
|
||||||
### Call Flow
|
|
||||||
```
|
|
||||||
Inbound (+18177645007)
|
|
||||||
→ SignalWire → FusionPBX:5080 (UDP)
|
|
||||||
→ signalwire-inbound dialplan (catch-all ^.*$)
|
|
||||||
→ IVR ext 900 (ivr_menu_16k.wav)
|
|
||||||
→ Routes to extensions 1000/1001/1002/1003/1004
|
|
||||||
|
|
||||||
Outbound
|
|
||||||
→ Phone → FusionPBX:5080
|
|
||||||
→ signalwire gateway → SignalWire → PSTN
|
|
||||||
```
|
|
||||||
|
|
||||||
### FreeSWITCH CLI Commands
|
|
||||||
```bash
|
|
||||||
fs_cli -x "sofia status profile external reg" # check registrations
|
|
||||||
fs_cli -x "sofia xmlstatus gateway" # check SignalWire gateway
|
|
||||||
fs_cli -x "reloadxml" # reload config (safe)
|
|
||||||
fs_cli -x "reloadacl" # reload ACL (safe)
|
|
||||||
# AVOID: sofia profile external restart (drops all phone registrations)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. NETWORKING & VPN
|
|
||||||
|
|
||||||
### FortiGate Firewall
|
|
||||||
- WAN IP: 97.154.109.245 (dynamic)
|
|
||||||
- DDNS: `orbisne.fortiddns.com` (FortiGate auto-updates on IP change)
|
|
||||||
- Blocks: outbound port 53 (DNS) — MediaStack uses PVE1 dnsmasq (10.48.200.90) as resolver → 100.100.100.100
|
|
||||||
|
|
||||||
**Port Forwards:**
|
|
||||||
| External Port | Internal Destination | Purpose |
|
|
||||||
|--------------|---------------------|---------|
|
|
||||||
| :8006 | PVE1:8006 | Proxmox web UI |
|
|
||||||
| :8123 | HA VM:8123 | Home Assistant |
|
|
||||||
| :22 | HA VM:22 | HA SSH (unreliable) |
|
|
||||||
|
|
||||||
### WireGuard — Jellyfin ↔ MediaStack
|
|
||||||
- MediaStack runs WireGuard server on `wg1` (port 51820, subnet 10.200.0.1/24)
|
|
||||||
- Jellyfin peer: 10.200.0.3 (active handshake)
|
|
||||||
- Used for NFS media file access ONLY — not internet VPN
|
|
||||||
|
|
||||||
### NordVPN — MediaStack Internet Traffic
|
|
||||||
- Interface: `nordlynx` on MediaStack
|
|
||||||
- Exit IP: 181.214.226.188 (US Dallas)
|
|
||||||
- Policy routing: table 205 (all traffic via nordlynx), managed by `nordvpn-routing.service`
|
|
||||||
- Required for IPTorrents access (blocks non-VPN IPs)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. BACKUP SYSTEMS
|
|
||||||
|
|
||||||
### DO Server Backup
|
|
||||||
- **Repo:** `myronblair/do-server-config`
|
|
||||||
- **Schedule:** Weekly, Sunday 4am
|
|
||||||
- **Launcher:** `/usr/local/bin/do-server-backup` on DO
|
|
||||||
- **Covers:** Scripts, systemd units, WireGuard, OLS vhosts, cron, MySQL credentials
|
|
||||||
- **Restore:** 8-phase wizard in `restore.sh`
|
|
||||||
- **DB backups:** `jarvis-backup.sh` runs daily (separate)
|
|
||||||
|
|
||||||
### Proxmox Config Backup
|
|
||||||
- **Repo:** `myronblair/proxmox-config`
|
|
||||||
- **Schedule:** Weekly, Sunday 3am (both PVE1 and PVE2)
|
|
||||||
- **Launcher:** `/usr/local/bin/proxmox-backup` on each node
|
|
||||||
- **Covers:** VM .conf files, network, cron, systemd, scripts
|
|
||||||
- **VM disks:** Covered by Proxmox Backup Server (PBS)
|
|
||||||
|
|
||||||
### FusionPBX Backup
|
|
||||||
- **Repo:** `myronblair/fusionpbx-config`
|
|
||||||
- **Schedule:** Weekly, Sunday 5am
|
|
||||||
- **Launcher:** `/usr/local/bin/fusionpbx-backup`
|
|
||||||
- **Covers:** PostgreSQL dump (gzip, ~29MB) + FreeSWITCH configs
|
|
||||||
- **Restore:** 10-phase wizard in `restore.sh`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 11. SSH QUICK REFERENCE
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# DO (main web server)
|
|
||||||
sshpass -p 'Gonewalk1974!@#' ssh -o StrictHostKeyChecking=no root@165.22.1.228
|
|
||||||
|
|
||||||
# FusionPBX (must relay via DO)
|
|
||||||
sshpass -p 'Gonewalk1974!@#' ssh root@165.22.1.228 \
|
|
||||||
'sshpass -p "Joker1974!@#" ssh root@134.209.72.226 "CMD"'
|
|
||||||
|
|
||||||
# PVE1 (direct or via DDNS)
|
|
||||||
sshpass -p 'Joker1974!!!' ssh -o StrictHostKeyChecking=no root@orbisne.fortiddns.com
|
|
||||||
sshpass -p 'Joker1974!!!' ssh -o StrictHostKeyChecking=no root@10.48.200.90
|
|
||||||
|
|
||||||
# PVE2
|
|
||||||
sshpass -p 'Joker1974!!!' ssh -o StrictHostKeyChecking=no root@10.48.200.91
|
|
||||||
|
|
||||||
# MediaStack (via PVE1)
|
|
||||||
sshpass -p 'Joker1974!!!' ssh root@10.48.200.90 \
|
|
||||||
'ssh -i /root/.ssh/id_rsa root@10.48.200.35 "CMD"'
|
|
||||||
|
|
||||||
# Jellyfin (direct, password enabled 2026-06-14)
|
|
||||||
sshpass -p 'Joker1974!!!' ssh -o StrictHostKeyChecking=no root@10.48.200.33
|
|
||||||
|
|
||||||
# NovaCPX (direct)
|
|
||||||
sshpass -p 'Joker1974!!!' ssh -o StrictHostKeyChecking=no root@10.48.200.110
|
|
||||||
|
|
||||||
# Ollama / Homebridge / NetworkBackup (myron user, then sudo)
|
|
||||||
sshpass -p 'Joker1974!' ssh myron@10.48.200.95 # Ollama
|
|
||||||
sshpass -p 'Joker1974!' ssh myron@10.48.200.18 # Homebridge
|
|
||||||
sshpass -p 'Joker1974!' ssh myron@10.48.200.99 # NetworkBackup
|
|
||||||
|
|
||||||
# Run command inside VM via Proxmox (requires QEMU agent installed)
|
|
||||||
sshpass -p 'Joker1974!!!' ssh root@10.48.200.90 \
|
|
||||||
'qm guest exec 210 -- bash -c "CMD"'
|
|
||||||
```
|
|
||||||
|
|
||||||
**Password fallback order:** `Joker1974!@#` → `Joker1974!!!` → `Joker1974!`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 12. CRITICAL CREDENTIALS MASTER LIST
|
|
||||||
|
|
||||||
### SSH / Root Access
|
|
||||||
| System | User | Password | Notes |
|
|
||||||
|--------|------|----------|-------|
|
|
||||||
| DO (165.22.1.228) | root | `Gonewalk1974!@#` | Main web server |
|
|
||||||
| FusionPBX (134.209.72.226) | root | `Joker1974!@#` | Via DO relay |
|
|
||||||
| PVE1 (10.48.200.90) | root | `Joker1974!!!` | Also via DDNS |
|
|
||||||
| PVE2 (10.48.200.91) | root | `Joker1974!!!` | |
|
|
||||||
| MediaStack (10.48.200.35) | root | key only | Via PVE1 (`/root/.ssh/id_rsa`) |
|
|
||||||
| Jellyfin (10.48.200.33) | root | `Joker1974!!!` | Enabled 2026-06-14 |
|
|
||||||
| NovaCPX (10.48.200.110) | root | `Joker1974!!!` | Direct SSH works |
|
|
||||||
| Ollama / Homebridge / Backup VMs | myron | `Joker1974!` | Then sudo |
|
|
||||||
|
|
||||||
### Web Panels & Admin
|
|
||||||
| System | URL | User | Password |
|
|
||||||
|--------|-----|------|----------|
|
|
||||||
| CyberPanel | https://165.22.1.228:8090 | myron | `Joker1974!!!` |
|
|
||||||
| phpMyAdmin (DO) | https://165.22.1.228/phpmyadmin | myron | `Joker1974!!!` |
|
|
||||||
| Proxmox PVE1 | https://orbisne.fortiddns.com:8006 | root | `Joker1974!!!` |
|
|
||||||
| Proxmox PVE2 | https://10.48.200.91:8006 | root | `Joker1974!!!` |
|
|
||||||
| JARVIS | http://jarvis.orbishosting.com:1972 | myron | `Joker1974!!!` |
|
|
||||||
| JARVIS Admin | http://jarvis.orbishosting.com:1972/admin | myron | `Joker1974!!!` |
|
|
||||||
| FusionPBX | https://fusion.orbishosting.com | admin | `fY7XP5swgtpbzrYLhkeVYkA4744` |
|
|
||||||
| Home Assistant | http://orbisne.fortiddns.com:8123 | myron | (HA password) |
|
|
||||||
| NovaCPX Admin | https://10.48.200.110:8882 | admin | `Admin2026!` |
|
|
||||||
| Jellyfin | http://10.48.200.33:8096 | — | token: `7c0ccf78b91d4b5bafa607f585f24f2d` |
|
|
||||||
| qBittorrent | http://10.48.200.35:8080 | admin | `Joker1974!!!` |
|
|
||||||
| Sonarr | http://10.48.200.35:8989 | admin | `Joker1974!!!` |
|
|
||||||
| Radarr | http://10.48.200.35:7878 | admin | `Joker1974!!!` |
|
|
||||||
| Prowlarr | http://10.48.200.35:9696 | admin | `Joker1974!!!` |
|
|
||||||
| Synology NAS | http://10.48.200.249:5000 | nas | `Joker1974!!!` |
|
|
||||||
| Parker Slingshot Admin | https://parkerslingshotrentals.com/admin | admin | `Parker2026!` |
|
|
||||||
| TJJ Admin | https://tomsjavajive.com/admin | `admin@tomsjavajive.com` OR `myronblair@outlook.com` | `Joker1974!!!` |
|
|
||||||
|
|
||||||
### Databases
|
|
||||||
| Site | DB Name | DB User | DB Password |
|
|
||||||
|------|---------|---------|-------------|
|
|
||||||
| JARVIS | `jarvis_db` | `jarvis_user` | `J4rv1s_Pr0t0c0l_2026!` |
|
|
||||||
| Tom's Java Jive | `toms_tjj_db` | `toms_tjj_user` | `+60wlPc+55e@gFq4` |
|
|
||||||
| Parker Slingshot Rentals | `park_slingshot` | `park_slingshotuser` | `4@rxg*8kovxCr7w6` |
|
|
||||||
| Epic Travel | `epic_travel_db` | (see config.php) | (see config.php) |
|
|
||||||
| Epic/Parker Slingshot | `epic_parkersling` | `epic_parkersling` | `Joker1974!!!` |
|
|
||||||
| NovaCPX | SQLite: `/var/lib/novacpx/panel.db` | — | — |
|
|
||||||
| FusionPBX | PostgreSQL | `fusionpbx` | `pSJaF9mUJqPr4Sj5mwJyRqvCCpc` |
|
|
||||||
| MySQL root (DO) | — | root | `b71e5c1a8c7457541b9c1db822de37adfa271926a38b6c20` |
|
|
||||||
|
|
||||||
### API Keys
|
|
||||||
| Service | Key |
|
|
||||||
|---------|-----|
|
|
||||||
| GitHub PAT | `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` (exp ~2026-08-20) |
|
|
||||||
| JARVIS Agent Registration | `f846a9aaf7ce9a61742c63c87c4186052a71d2a580c65518` |
|
|
||||||
| Proxmox API Token | `root@pam!jarvis=c45b5feb-f9a9-445d-a626-14fbb959f78b` |
|
|
||||||
| HA Long-lived Token | `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIzNmI0N2I1Njk5ZGQ0MTQ2ODMwZWFmYjZiYTQ1MjJkMSIsImlhdCI6MTc4MDIwMzU5NCwiZXhwIjoyMDk1NTYzNTk0fQ.sYRok-jRDlA4lFgWxLQELcEjkJNGQdprk6ZziLwLtXE` |
|
|
||||||
| Sonarr API | `b43e04350a594846b4ee95261c29e9e0` |
|
|
||||||
| Radarr API | `53c4268360444feeae5f98c0cc24e0e3` |
|
|
||||||
| Prowlarr API | `9d0ce6c5660743b5bf1c7951efc62252` |
|
|
||||||
| Jellyfin Admin Token | `7c0ccf78b91d4b5bafa607f585f24f2d` |
|
|
||||||
| Square (Parker) Production | `EAAAl3FsAu_2ri8kZE_ENEyi2T_C8HXXm5XQFY6Lbnd8SX6FqYp8J_upUeXNYh7v` |
|
|
||||||
| Square App ID (Parker) | `sq0idp-YSM7BU9IVyOWSzpeP-0nzQ` |
|
|
||||||
| Webhook HMAC Secret | `4c8805f0285214ff0a0602b5880270b935f36a896946c7f1` |
|
|
||||||
|
|
||||||
### SIP / Phone
|
|
||||||
| Extension | Name | SIP Password |
|
|
||||||
|-----------|------|-------------|
|
|
||||||
| 1000 | Myron Blair — Desk (10.48.200.2) | `Xk9mPw3nQv7rLs2t` |
|
|
||||||
| 1001 | Tommy Ivy — Desk (10.48.200.43) | `Tv8xNm4pWq6rZs3k` |
|
|
||||||
| 1002 | Myron Blair — WiFi Work (10.48.200.65) | `yXHaJTwa8rj?$GkrVFQB` |
|
|
||||||
| 1003 | Kitchen (10.48.200.83) | — |
|
|
||||||
| 1004 | Master Bedroom (10.48.200.85) | — |
|
|
||||||
| 1010 | Parker County Slingshot (voicemail only) | — |
|
|
||||||
| 1011 | Epic Travel Expeditions (voicemail only) | — |
|
|
||||||
| 1012 | Tom's Java Jive (voicemail only) | — |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*This document contains sensitive credentials. Store securely and do not share.*
|
|
||||||
+880
-60
File diff suppressed because it is too large
Load Diff
@@ -6,20 +6,26 @@
|
|||||||
.DESCRIPTION
|
.DESCRIPTION
|
||||||
Installs JARVIS Agent as a Windows Service that auto-starts at boot.
|
Installs JARVIS Agent as a Windows Service that auto-starts at boot.
|
||||||
Requires: PowerShell 5.1+, internet access, and Administrator rights.
|
Requires: PowerShell 5.1+, internet access, and Administrator rights.
|
||||||
|
No Python installation needed — this installs the standalone .exe build.
|
||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
# Interactive install (prompts for registration key):
|
# Interactive install (prompts for registration key):
|
||||||
irm https://jarvis.orbishosting.com/agent/install-windows.ps1 | iex
|
irm https://jarvis.orbishosting.com:1972/agent/install-windows.ps1 | iex
|
||||||
|
|
||||||
# Silent install with key:
|
# Silent install with key:
|
||||||
$env:JARVIS_REG_KEY='your_key_here'; irm https://jarvis.orbishosting.com/agent/install-windows.ps1 | iex
|
$env:JARVIS_REG_KEY='your_key_here'; irm https://jarvis.orbishosting.com:1972/agent/install-windows.ps1 | iex
|
||||||
#>
|
#>
|
||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
$JARVIS_URL = 'https://jarvis.orbishosting.com'
|
# Fixed 2026-07-07: jarvis.orbishosting.com on the default port (80/443) is not
|
||||||
|
# reachable from outside the LAN at all (no FortiGate VIP forwards it) — every
|
||||||
|
# external install using the old default URL would have failed outright. Port
|
||||||
|
# 1972 is the confirmed-working external path (same fix applied to the GitHub
|
||||||
|
# webhook the same day).
|
||||||
|
$JARVIS_URL = 'http://jarvis.orbishosting.com:1972'
|
||||||
$INSTALL_DIR = 'C:\ProgramData\jarvis-agent'
|
$INSTALL_DIR = 'C:\ProgramData\jarvis-agent'
|
||||||
$SERVICE_NAME = 'JARVISAgent'
|
$SERVICE_NAME = 'JARVISAgent'
|
||||||
$AGENT_SCRIPT = "$INSTALL_DIR\jarvis-agent-windows.py"
|
$AGENT_EXE = "$INSTALL_DIR\jarvis-agent-windows.exe"
|
||||||
$CONFIG_FILE = "$INSTALL_DIR\config.json"
|
$CONFIG_FILE = "$INSTALL_DIR\config.json"
|
||||||
|
|
||||||
function Write-Step { param($msg) Write-Host "`n[JARVIS] $msg" -ForegroundColor Cyan }
|
function Write-Step { param($msg) Write-Host "`n[JARVIS] $msg" -ForegroundColor Cyan }
|
||||||
@@ -39,49 +45,23 @@ if ($existing) {
|
|||||||
Start-Sleep 2
|
Start-Sleep 2
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
& python "$INSTALL_DIR\jarvis-agent-windows.py" remove 2>$null
|
if (Test-Path $AGENT_EXE) { & $AGENT_EXE remove 2>$null }
|
||||||
} catch {}
|
} catch {}
|
||||||
Write-OK "Existing service removed."
|
Write-OK "Existing service removed."
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Check / install Python ────────────────────────────────────────────────────
|
|
||||||
Write-Step "Checking Python..."
|
|
||||||
$py = Get-Command python -ErrorAction SilentlyContinue
|
|
||||||
if (-not $py) {
|
|
||||||
Write-Host " Python not found. Installing via winget..." -ForegroundColor Yellow
|
|
||||||
if (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
|
|
||||||
Write-Fail "winget not available. Please install Python 3.11+ from https://python.org and re-run."
|
|
||||||
}
|
|
||||||
winget install -e --id Python.Python.3.11 --silent --accept-package-agreements --accept-source-agreements
|
|
||||||
$env:PATH = [System.Environment]::GetEnvironmentVariable("PATH","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("PATH","User")
|
|
||||||
$py = Get-Command python -ErrorAction SilentlyContinue
|
|
||||||
if (-not $py) { Write-Fail "Python install failed. Please install manually from https://python.org" }
|
|
||||||
}
|
|
||||||
$pyVersion = & python --version 2>&1
|
|
||||||
Write-OK $pyVersion
|
|
||||||
|
|
||||||
# ── Install pywin32 ───────────────────────────────────────────────────────────
|
|
||||||
Write-Step "Checking pywin32..."
|
|
||||||
$checkWin32 = & python -c "import win32service; print('ok')" 2>&1
|
|
||||||
if ($checkWin32 -ne 'ok') {
|
|
||||||
Write-Host " Installing pywin32..." -ForegroundColor Yellow
|
|
||||||
& python -m pip install --quiet pywin32
|
|
||||||
& python -m pywin32_postinstall -install 2>$null
|
|
||||||
Write-OK "pywin32 installed."
|
|
||||||
} else {
|
|
||||||
Write-OK "pywin32 already installed."
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Create install dir ────────────────────────────────────────────────────────
|
# ── Create install dir ────────────────────────────────────────────────────────
|
||||||
Write-Step "Creating install directory..."
|
Write-Step "Creating install directory..."
|
||||||
New-Item -ItemType Directory -Path $INSTALL_DIR -Force | Out-Null
|
New-Item -ItemType Directory -Path $INSTALL_DIR -Force | Out-Null
|
||||||
Write-OK $INSTALL_DIR
|
Write-OK $INSTALL_DIR
|
||||||
|
|
||||||
# ── Download agent script ─────────────────────────────────────────────────────
|
# ── Download agent exe ─────────────────────────────────────────────────────────
|
||||||
|
# No Python/pywin32 dependency anymore — this is a self-contained PyInstaller
|
||||||
|
# build with everything it needs bundled in.
|
||||||
Write-Step "Downloading JARVIS agent..."
|
Write-Step "Downloading JARVIS agent..."
|
||||||
try {
|
try {
|
||||||
Invoke-WebRequest -Uri "$JARVIS_URL/agent/jarvis-agent-windows.py" -OutFile $AGENT_SCRIPT -UseBasicParsing
|
Invoke-WebRequest -Uri "$JARVIS_URL/agent/jarvis-agent-windows.exe" -OutFile $AGENT_EXE -UseBasicParsing
|
||||||
Write-OK "Agent downloaded to $AGENT_SCRIPT"
|
Write-OK "Agent downloaded to $AGENT_EXE"
|
||||||
} catch {
|
} catch {
|
||||||
Write-Fail "Failed to download agent: $_"
|
Write-Fail "Failed to download agent: $_"
|
||||||
}
|
}
|
||||||
@@ -121,8 +101,7 @@ Write-OK "Config written to $CONFIG_FILE"
|
|||||||
|
|
||||||
# ── Install Windows Service ───────────────────────────────────────────────────
|
# ── Install Windows Service ───────────────────────────────────────────────────
|
||||||
Write-Step "Installing Windows service..."
|
Write-Step "Installing Windows service..."
|
||||||
$pyPath = (Get-Command python).Source
|
& $AGENT_EXE --startup auto install
|
||||||
& $pyPath "$AGENT_SCRIPT" --startup auto install
|
|
||||||
if ($LASTEXITCODE -ne 0) { Write-Fail "Service install failed." }
|
if ($LASTEXITCODE -ne 0) { Write-Fail "Service install failed." }
|
||||||
Write-OK "Service '$SERVICE_NAME' installed."
|
Write-OK "Service '$SERVICE_NAME' installed."
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,34 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# JARVIS Agent Installer — one-liner for any Linux host:
|
# JARVIS Agent Installer — one-liner for any Linux host:
|
||||||
# curl -sk https://jarvis.orbishosting.com/install-agent.sh | bash -s <hostname> <agent_type>
|
# curl -sk http://jarvis.orbishosting.com:1972/agent/install.sh | bash -s <hostname> <agent_type>
|
||||||
#
|
#
|
||||||
# agent_type: linux | proxmox | homeassistant
|
# agent_type: linux | proxmox | homeassistant
|
||||||
# Example: curl -sk https://jarvis.orbishosting.com/install-agent.sh | bash -s myserver linux
|
# Example: curl -sk http://jarvis.orbishosting.com:1972/agent/install.sh | bash -s myserver linux
|
||||||
|
#
|
||||||
|
# On the LAN, set JARVIS_URL to the direct internal address instead (faster,
|
||||||
|
# doesn't hairpin through Cloudflare): JARVIS_URL=http://10.48.200.211 curl ... | bash -s ...
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
HOSTNAME_ARG="${1:-$(hostname -s)}"
|
HOSTNAME_ARG="${1:-$(hostname -s)}"
|
||||||
AGENT_TYPE="${2:-linux}"
|
AGENT_TYPE="${2:-linux}"
|
||||||
JARVIS_URL="${JARVIS_URL:-https://jarvis.orbishosting.com}"
|
# Fixed 2026-07-07: jarvis.orbishosting.com on the default port isn't reachable
|
||||||
|
# from outside the LAN at all (no FortiGate VIP forwards it) — :1972 is the
|
||||||
|
# confirmed-working external path (same fix as the GitHub webhook and the
|
||||||
|
# Windows agent installer).
|
||||||
|
JARVIS_URL="${JARVIS_URL:-http://jarvis.orbishosting.com:1972}"
|
||||||
JARVIS_HOST=""
|
JARVIS_HOST=""
|
||||||
INSTALL_DIR="/opt/jarvis-agent"
|
INSTALL_DIR="/opt/jarvis-agent"
|
||||||
CONFIG_DIR="/etc/jarvis-agent"
|
CONFIG_DIR="/etc/jarvis-agent"
|
||||||
STATE_DIR="/var/lib/jarvis-agent"
|
STATE_DIR="/var/lib/jarvis-agent"
|
||||||
REG_KEY="f846a9aaf7ce9a61742c63c87c4186052a71d2a580c65518"
|
REG_KEY="${JARVIS_REG_KEY:-}"
|
||||||
|
if [ -z "$REG_KEY" ] && [ -r /dev/tty ]; then
|
||||||
|
read -rp "Enter JARVIS registration key: " REG_KEY </dev/tty
|
||||||
|
fi
|
||||||
|
if [ -z "$REG_KEY" ]; then
|
||||||
|
echo "ERROR: registration key required (set JARVIS_REG_KEY env var or enter at prompt)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
SERVICE_FILE="/etc/systemd/system/jarvis-agent.service"
|
SERVICE_FILE="/etc/systemd/system/jarvis-agent.service"
|
||||||
|
|
||||||
echo "=== JARVIS Agent Installer v3.0 ==="
|
echo "=== JARVIS Agent Installer v3.0 ==="
|
||||||
@@ -57,7 +71,7 @@ else
|
|||||||
"poll_interval": 30,
|
"poll_interval": 30,
|
||||||
"heartbeat_every": 10,
|
"heartbeat_every": 10,
|
||||||
"update_check_hours": 24,
|
"update_check_hours": 24,
|
||||||
"watch_services": ["ollama", "homeassistant", "mysql", "mariadb", "nginx", "apache2", "docker"]
|
"watch_services": []
|
||||||
}
|
}
|
||||||
JSONEOF
|
JSONEOF
|
||||||
chmod 600 "$CONFIG_DIR/config.json"
|
chmod 600 "$CONFIG_DIR/config.json"
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
ad9b59c09e5862c5abc35f73999aa2666f8401817b053c385505fa420ca473e7
|
||||||
@@ -35,12 +35,11 @@ INSTALL_DIR = Path(r"C:\ProgramData\jarvis-agent")
|
|||||||
CONFIG_PATH = INSTALL_DIR / "config.json"
|
CONFIG_PATH = INSTALL_DIR / "config.json"
|
||||||
STATE_PATH = INSTALL_DIR / "state.json"
|
STATE_PATH = INSTALL_DIR / "state.json"
|
||||||
LOG_PATH = INSTALL_DIR / "jarvis-agent.log"
|
LOG_PATH = INSTALL_DIR / "jarvis-agent.log"
|
||||||
AGENT_VERSION = "3.1"
|
AGENT_VERSION = "3.2"
|
||||||
|
|
||||||
# Set by the service wrapper so self_update knows to stop instead of exec
|
# Set by the service wrapper so self_update knows to stop instead of exec
|
||||||
_is_service = False
|
_is_service = False
|
||||||
_stop_event = threading.Event()
|
_stop_event = threading.Event()
|
||||||
_update_restart = False # True when stopping for self-update; triggers SCM restart
|
|
||||||
|
|
||||||
# ── Logging ────────────────────────────────────────────────────────────────────
|
# ── Logging ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -93,7 +92,7 @@ def api_post(url: str, payload: dict, headers: dict = {}, timeout: int = 15,
|
|||||||
body = json.dumps(payload).encode()
|
body = json.dumps(payload).encode()
|
||||||
req = urllib.request.Request(url, data=body, method="POST")
|
req = urllib.request.Request(url, data=body, method="POST")
|
||||||
req.add_header("Content-Type", "application/json")
|
req.add_header("Content-Type", "application/json")
|
||||||
req.add_header("User-Agent", "JARVIS-Agent-Windows/3.0")
|
req.add_header("User-Agent", "JARVIS-Agent-Windows/3.2")
|
||||||
if _host_header:
|
if _host_header:
|
||||||
req.add_header("Host", _host_header)
|
req.add_header("Host", _host_header)
|
||||||
for k, v in headers.items():
|
for k, v in headers.items():
|
||||||
@@ -110,7 +109,7 @@ def api_post(url: str, payload: dict, headers: dict = {}, timeout: int = 15,
|
|||||||
def api_get(url: str, headers: dict = {}, timeout: int = 10,
|
def api_get(url: str, headers: dict = {}, timeout: int = 10,
|
||||||
ssl_verify: bool = True) -> dict:
|
ssl_verify: bool = True) -> dict:
|
||||||
req = urllib.request.Request(url)
|
req = urllib.request.Request(url)
|
||||||
req.add_header("User-Agent", "JARVIS-Agent-Windows/3.0")
|
req.add_header("User-Agent", "JARVIS-Agent-Windows/3.2")
|
||||||
if _host_header:
|
if _host_header:
|
||||||
req.add_header("Host", _host_header)
|
req.add_header("Host", _host_header)
|
||||||
for k, v in headers.items():
|
for k, v in headers.items():
|
||||||
@@ -376,18 +375,28 @@ def _sysinfo_snapshot() -> dict:
|
|||||||
# ── Self-update ────────────────────────────────────────────────────────────────
|
# ── Self-update ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def self_update(cfg: dict) -> bool:
|
def self_update(cfg: dict) -> bool:
|
||||||
|
# Added: supports both script-mode (plain .py, run via a system Python) and
|
||||||
|
# frozen-mode (standalone PyInstaller .exe — sys.frozen is set, __file__ isn't
|
||||||
|
# meaningful/writable the way it is for a real .py file on disk). A running
|
||||||
|
# .exe can't be overwritten in place on Windows, but CAN be renamed while
|
||||||
|
# running, so frozen mode uses a download-new/rename-old/rename-new swap
|
||||||
|
# instead of the direct overwrite the script-mode path uses.
|
||||||
jarvis_url = cfg.get("jarvis_url", "").rstrip("/")
|
jarvis_url = cfg.get("jarvis_url", "").rstrip("/")
|
||||||
default_update_url = f"{jarvis_url}/agent/jarvis-agent-windows.py" if jarvis_url else ""
|
is_frozen = bool(getattr(sys, "frozen", False))
|
||||||
|
if is_frozen:
|
||||||
|
default_update_url = f"{jarvis_url}/agent/jarvis-agent-windows.exe" if jarvis_url else ""
|
||||||
|
else:
|
||||||
|
default_update_url = f"{jarvis_url}/agent/jarvis-agent-windows.py" if jarvis_url else ""
|
||||||
update_url = cfg.get("update_url", default_update_url)
|
update_url = cfg.get("update_url", default_update_url)
|
||||||
if not update_url:
|
if not update_url:
|
||||||
return False
|
return False
|
||||||
script_path = os.path.abspath(__file__)
|
target_path = os.path.abspath(sys.executable) if is_frozen else os.path.abspath(__file__)
|
||||||
ssl_verify = bool(cfg.get("ssl_verify", True))
|
ssl_verify = bool(cfg.get("ssl_verify", True))
|
||||||
try:
|
try:
|
||||||
# Download expected hash
|
# Download expected hash
|
||||||
hash_url = update_url + ".sha256"
|
hash_url = update_url + ".sha256"
|
||||||
req_hash = urllib.request.Request(hash_url)
|
req_hash = urllib.request.Request(hash_url)
|
||||||
req_hash.add_header("User-Agent", "JARVIS-Agent-Windows/3.0")
|
req_hash.add_header("User-Agent", "JARVIS-Agent-Windows/3.2")
|
||||||
if _host_header:
|
if _host_header:
|
||||||
req_hash.add_header("Host", _host_header)
|
req_hash.add_header("Host", _host_header)
|
||||||
expected_hash = None
|
expected_hash = None
|
||||||
@@ -398,13 +407,13 @@ def self_update(cfg: dict) -> bool:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Download new script
|
# Download new script/exe
|
||||||
req = urllib.request.Request(update_url)
|
req = urllib.request.Request(update_url)
|
||||||
req.add_header("User-Agent", "JARVIS-Agent-Windows/3.0")
|
req.add_header("User-Agent", "JARVIS-Agent-Windows/3.2")
|
||||||
if _host_header:
|
if _host_header:
|
||||||
req.add_header("Host", _host_header)
|
req.add_header("Host", _host_header)
|
||||||
ctx = _make_ssl_ctx(ssl_verify)
|
ctx = _make_ssl_ctx(ssl_verify)
|
||||||
with urllib.request.urlopen(req, timeout=30, context=ctx) as resp:
|
with urllib.request.urlopen(req, timeout=60, context=ctx) as resp:
|
||||||
new_content = resp.read()
|
new_content = resp.read()
|
||||||
|
|
||||||
# Verify hash
|
# Verify hash
|
||||||
@@ -414,21 +423,42 @@ def self_update(cfg: dict) -> bool:
|
|||||||
log(f"Update hash mismatch (expected {expected_hash[:16]}… got {actual_hash[:16]}…) — aborting")
|
log(f"Update hash mismatch (expected {expected_hash[:16]}… got {actual_hash[:16]}…) — aborting")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
with open(script_path, "rb") as f:
|
with open(target_path, "rb") as f:
|
||||||
current = f.read()
|
current = f.read()
|
||||||
if new_content != current:
|
if new_content == current:
|
||||||
log(f"Update verified — replacing {script_path} and restarting...")
|
return False
|
||||||
with open(script_path, "wb") as f:
|
|
||||||
|
log(f"Update verified — replacing {target_path} and restarting...")
|
||||||
|
if is_frozen:
|
||||||
|
# Can't overwrite a running exe, but can rename it and drop the new
|
||||||
|
# one in its place; the old copy is cleaned up on the next update.
|
||||||
|
old_path = target_path + ".old"
|
||||||
|
new_path = target_path + ".new"
|
||||||
|
with open(new_path, "wb") as f:
|
||||||
f.write(new_content)
|
f.write(new_content)
|
||||||
if _is_service:
|
try:
|
||||||
global _update_restart
|
if os.path.exists(old_path):
|
||||||
_update_restart = True
|
os.remove(old_path)
|
||||||
log("Running as service — stopping for SCM-managed restart after update.")
|
except Exception:
|
||||||
_stop_event.set()
|
pass
|
||||||
else:
|
os.rename(target_path, old_path)
|
||||||
os.execv(sys.executable, [sys.executable] + sys.argv)
|
os.rename(new_path, target_path)
|
||||||
return True
|
else:
|
||||||
return False
|
with open(target_path, "wb") as f:
|
||||||
|
f.write(new_content)
|
||||||
|
|
||||||
|
if _is_service:
|
||||||
|
# Signal the main loop to exit; SCM failure-recovery will restart us
|
||||||
|
log("Running as service — stopping for SCM-managed restart after update.")
|
||||||
|
_stop_event.set()
|
||||||
|
elif is_frozen:
|
||||||
|
# sys.argv[0] is already the exe's own path for a frozen app — don't
|
||||||
|
# prepend sys.executable again or the new process misreads its own
|
||||||
|
# path as a command-line argument.
|
||||||
|
os.execv(sys.executable, sys.argv)
|
||||||
|
else:
|
||||||
|
os.execv(sys.executable, [sys.executable] + sys.argv)
|
||||||
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log(f"Self-update check failed: {e}")
|
log(f"Self-update check failed: {e}")
|
||||||
return False
|
return False
|
||||||
@@ -592,9 +622,6 @@ if _HAS_WIN32:
|
|||||||
(self._svc_name_, ""),
|
(self._svc_name_, ""),
|
||||||
)
|
)
|
||||||
main()
|
main()
|
||||||
if _update_restart:
|
|
||||||
# Non-zero exit triggers SCM failure recovery → automatic restart
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
224a634375b5d49ccc0a012e0e122ade5f8a1302615450dffbf9a03eac6b7a19
|
fff217657488830084780115665d0c772af1f7fe31f2084d61a7560424a5a91a
|
||||||
|
|||||||
@@ -281,7 +281,7 @@ def get_uptime() -> dict:
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
def get_services(cfg: dict) -> list:
|
def get_services(cfg: dict) -> list:
|
||||||
watch = cfg.get("watch_services", ["ollama", "homeassistant", "mysql", "nginx", "apache2"])
|
watch = cfg.get("watch_services", [])
|
||||||
statuses = []
|
statuses = []
|
||||||
for svc in watch:
|
for svc in watch:
|
||||||
try:
|
try:
|
||||||
|
|||||||
+7
-2
@@ -19,7 +19,7 @@ $_e1 = $_earlyParts[1] ?? '';
|
|||||||
$_skipSession = match(true) {
|
$_skipSession = match(true) {
|
||||||
$_e0 === 'ping' => true,
|
$_e0 === 'ping' => true,
|
||||||
$_e0 === 'netscan' => true,
|
$_e0 === 'netscan' => true,
|
||||||
$_e0 === 'agent' && !in_array($_e1, ['list','status','myip'], true) => true,
|
$_e0 === 'agent' && !in_array($_e1, ['list','status','myip','regkey'], true) => true,
|
||||||
default => false,
|
default => false,
|
||||||
};
|
};
|
||||||
if (!$_skipSession) {
|
if (!$_skipSession) {
|
||||||
@@ -27,7 +27,12 @@ if (!$_skipSession) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
header('Access-Control-Allow-Origin: *');
|
$_allowedOrigins = ['https://jarvis.orbishosting.com', 'http://jarvis.orbishosting.com'];
|
||||||
|
$_origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
||||||
|
if (in_array($_origin, $_allowedOrigins, true)) {
|
||||||
|
header('Access-Control-Allow-Origin: ' . $_origin);
|
||||||
|
header('Access-Control-Allow-Credentials: true');
|
||||||
|
}
|
||||||
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
|
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
|
||||||
header('Access-Control-Allow-Headers: Content-Type, X-Session-Token');
|
header('Access-Control-Allow-Headers: Content-Type, X-Session-Token');
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,14 @@
|
|||||||
// ── GLOBALS ──────────────────────────────────────────────────────────
|
// ── GLOBALS ──────────────────────────────────────────────────────────
|
||||||
|
function escHtml(s) {
|
||||||
|
return String(s == null ? '' : s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||||
|
}
|
||||||
|
// For values embedded inside a single-quoted JS string literal within an HTML attribute
|
||||||
|
// (e.g. onclick="fn('${escJs(x)}')"). escHtml() alone isn't enough there: the browser
|
||||||
|
// HTML-decodes the attribute before parsing it as JS, so an encoded quote would just
|
||||||
|
// turn back into a literal ' before the JS parser ever sees it.
|
||||||
|
function escJs(s) {
|
||||||
|
return escHtml(String(s == null ? '' : s).replace(/\\/g,'\\\\').replace(/'/g,"\\'").replace(/\n/g,'\\n').replace(/\r/g,'\\r'));
|
||||||
|
}
|
||||||
let sessionToken = '';
|
let sessionToken = '';
|
||||||
let sessionUser = '';
|
let sessionUser = '';
|
||||||
let sessionId = 'session_' + Date.now();
|
let sessionId = 'session_' + Date.now();
|
||||||
@@ -596,15 +606,15 @@ function renderNetworkStatus(n) {
|
|||||||
agent_id: d.agent_id, hostname: d.name};
|
agent_id: d.agent_id, hostname: d.name};
|
||||||
const lat = d.latency_ms ? ' · ' + d.latency_ms + 'ms' : '';
|
const lat = d.latency_ms ? ' · ' + d.latency_ms + 'ms' : '';
|
||||||
const badge = d.source === 'agent'
|
const badge = d.source === 'agent'
|
||||||
? `<span style="font-size:0.53rem;color:var(--cyan);letter-spacing:1px;margin-left:4px">${(d.agent_type||'AGENT').toUpperCase()}</span>` : '';
|
? `<span style="font-size:0.53rem;color:var(--cyan);letter-spacing:1px;margin-left:4px">${escHtml((d.agent_type||'AGENT').toUpperCase())}</span>` : '';
|
||||||
const del = d.deletable
|
const del = d.deletable
|
||||||
? `<button onclick="deleteNetworkDevice('${d.ip}',event)" style="background:none;border:none;color:var(--red);cursor:pointer;font-size:0.9rem;padding:0 2px;opacity:0.5;flex-shrink:0" title="Remove">×</button>` : '';
|
? `<button onclick="deleteNetworkDevice('${escJs(d.ip)}',event)" style="background:none;border:none;color:var(--red);cursor:pointer;font-size:0.9rem;padding:0 2px;opacity:0.5;flex-shrink:0" title="Remove">×</button>` : '';
|
||||||
const bl = d.source === 'agent' ? 'border-left:2px solid ' + (alive ? 'var(--green)' : 'var(--red)') + ';' : '';
|
const bl = d.source === 'agent' ? 'border-left:2px solid ' + (alive ? 'var(--green)' : 'var(--red)') + ';' : '';
|
||||||
return `<div class="device-item" data-ctx-key="${ctxKey}" onclick="selectContext('${ctxKey}')" style="${bl}display:flex;align-items:center">
|
return `<div class="device-item" data-ctx-key="${ctxKey}" onclick="selectContext('${escJs(ctxKey)}')" style="${bl}display:flex;align-items:center">
|
||||||
<div class="device-status ${alive?'on':'off'}" style="flex-shrink:0"></div>
|
<div class="device-status ${alive?'on':'off'}" style="flex-shrink:0"></div>
|
||||||
<div class="device-info" style="flex:1;min-width:0">
|
<div class="device-info" style="flex:1;min-width:0">
|
||||||
<div class="device-name" style="display:flex;align-items:center">${d.name||d.ip}${badge}</div>
|
<div class="device-name" style="display:flex;align-items:center">${escHtml(d.name||d.ip)}${badge}</div>
|
||||||
<div class="device-ip">${d.ip||''}${lat}</div>
|
<div class="device-ip">${escHtml(d.ip||'')}${lat}</div>
|
||||||
</div>${del}
|
</div>${del}
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
@@ -684,7 +694,7 @@ async function loadProxmox() {
|
|||||||
type_label:vm.type||'qemu', uptime:vm.uptime||0};
|
type_label:vm.type||'qemu', uptime:vm.uptime||0};
|
||||||
return `<div class="vm-card" data-ctx-key="${ctxKey}" onclick="selectContext('${ctxKey}')" title="Click to ask Jarvis about this VM">
|
return `<div class="vm-card" data-ctx-key="${ctxKey}" onclick="selectContext('${ctxKey}')" title="Click to ask Jarvis about this VM">
|
||||||
<div class="vm-header">
|
<div class="vm-header">
|
||||||
<span class="vm-name">${vm.name}</span>
|
<span class="vm-name">${escHtml(vm.name)}</span>
|
||||||
<span style="color:${statusColor};font-size:0.65rem">● ${(vm.status||'').toUpperCase()}</span>
|
<span style="color:${statusColor};font-size:0.65rem">● ${(vm.status||'').toUpperCase()}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="vm-metrics">
|
<div class="vm-metrics">
|
||||||
@@ -972,6 +982,7 @@ async function loadWeather() {
|
|||||||
const d = await api('weather');
|
const d = await api('weather');
|
||||||
if (!d || !d.current) return;
|
if (!d || !d.current) return;
|
||||||
const c = d.current;
|
const c = d.current;
|
||||||
|
if (d.location) document.getElementById('weather-loc').textContent = d.location.toUpperCase();
|
||||||
document.getElementById('weather-temp').textContent = c.temp;
|
document.getElementById('weather-temp').textContent = c.temp;
|
||||||
document.getElementById('weather-desc').textContent = (c.desc || '').toUpperCase();
|
document.getElementById('weather-desc').textContent = (c.desc || '').toUpperCase();
|
||||||
document.getElementById('weather-feels').textContent = c.feels + '°F';
|
document.getElementById('weather-feels').textContent = c.feels + '°F';
|
||||||
@@ -1031,10 +1042,11 @@ async function loadNews() {
|
|||||||
const ctxKey = 'news_' + (cat + '_' + a.title).replace(/[^a-z0-9]/gi,'').slice(0,30);
|
const ctxKey = 'news_' + (cat + '_' + a.title).replace(/[^a-z0-9]/gi,'').slice(0,30);
|
||||||
_panelCtx[ctxKey] = {type:'news', label:a.title,
|
_panelCtx[ctxKey] = {type:'news', label:a.title,
|
||||||
title:a.title, source:a.source, pub:a.pub||'', category:cat};
|
title:a.title, source:a.source, pub:a.pub||'', category:cat};
|
||||||
|
const titleDisplay = a.title.length > 90 ? a.title.slice(0,87)+'…' : a.title;
|
||||||
html += `<div class="news-item" data-ctx-key="${ctxKey}" onclick="selectContext('${ctxKey}')" title="Click to ask Jarvis about this story">
|
html += `<div class="news-item" data-ctx-key="${ctxKey}" onclick="selectContext('${ctxKey}')" title="Click to ask Jarvis about this story">
|
||||||
<div class="news-source">${a.source}</div>
|
<div class="news-source">${escHtml(a.source)}</div>
|
||||||
<div class="news-title">${a.title.length > 90 ? a.title.slice(0,87)+'…' : a.title}</div>
|
<div class="news-title">${escHtml(titleDisplay)}</div>
|
||||||
${a.pub ? '<div class="news-time">' + a.pub + '</div>' : ''}
|
${a.pub ? '<div class="news-time">' + escHtml(a.pub) + '</div>' : ''}
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1620,11 +1632,18 @@ async function checkAgentStatus() {
|
|||||||
sta.textContent = online.length > 0 ? online.length + ' ONLINE' : 'NONE';
|
sta.textContent = online.length > 0 ? online.length + ' ONLINE' : 'NONE';
|
||||||
const cnt = document.getElementById('net-agent-count');
|
const cnt = document.getElementById('net-agent-count');
|
||||||
if (cnt) cnt.textContent = online.length + ' AGENT' + (online.length !== 1 ? 'S' : '') + ' ONLINE';
|
if (cnt) cnt.textContent = online.length + ' AGENT' + (online.length !== 1 ? 'S' : '') + ' ONLINE';
|
||||||
|
// Fixed 2026-07-07: this used to fall back to "any agent on the same /24
|
||||||
|
// subnet" when the exact IP didn't match, meant to handle NAT — but when
|
||||||
|
// a LAN client hits the dashboard via its external hostname (hairpin NAT,
|
||||||
|
// or through Cloudflare), the server sees the SAME single reflected/proxy
|
||||||
|
// IP for every visitor on the LAN, not each machine's real one. The exact
|
||||||
|
// match then always fails, and the subnet fallback just grabbed whichever
|
||||||
|
// agent happened to be first in the list — showing a *different real
|
||||||
|
// machine* as "yours", not a harmless guess. Exact match only now; if it
|
||||||
|
// fails, the button correctly falls through to "not detected" instead of
|
||||||
|
// reporting someone else's agent as this one.
|
||||||
const myIp = data.my_ip || '';
|
const myIp = data.my_ip || '';
|
||||||
// Match by exact IP first, then by same /24 subnet (handles NAT behind same router)
|
_myAgent = online.find(a => a.ip_address === myIp) || null;
|
||||||
const mySubnet = myIp.split('.').slice(0,3).join('.');
|
|
||||||
_myAgent = online.find(a => a.ip_address === myIp)
|
|
||||||
|| online.find(a => a.ip_address && a.ip_address.startsWith(mySubnet + '.'));
|
|
||||||
_agentOnline = !!_myAgent;
|
_agentOnline = !!_myAgent;
|
||||||
if (btn) {
|
if (btn) {
|
||||||
const isTablet = detectOS() === 'tablet';
|
const isTablet = detectOS() === 'tablet';
|
||||||
|
|||||||
@@ -401,8 +401,8 @@ function renderAgentsTab(agents, metrics) {
|
|||||||
style="flex-direction:column;align-items:stretch;border-left:3px solid ${alive ? 'var(--green)' : 'var(--red)'}">
|
style="flex-direction:column;align-items:stretch;border-left:3px solid ${alive ? 'var(--green)' : 'var(--red)'}">
|
||||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
|
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
|
||||||
<div style="width:8px;height:8px;border-radius:50%;background:${alive ? 'var(--green)' : 'var(--red)'};box-shadow:${alive ? '0 0 6px var(--green)' : 'none'};flex-shrink:0"></div>
|
<div style="width:8px;height:8px;border-radius:50%;background:${alive ? 'var(--green)' : 'var(--red)'};box-shadow:${alive ? '0 0 6px var(--green)' : 'none'};flex-shrink:0"></div>
|
||||||
<span style="font-family:var(--font-mono);font-size:0.72rem;color:var(--text);flex:1">${ag.hostname}</span>
|
<span style="font-family:var(--font-mono);font-size:0.72rem;color:var(--text);flex:1">${escHtml(ag.hostname)}</span>
|
||||||
<span style="font-size:0.58rem;color:var(--text-dim)">${ag.agent_type.toUpperCase()} · ${ag.ip_address}</span>
|
<span style="font-size:0.58rem;color:var(--text-dim)">${escHtml(ag.agent_type.toUpperCase())} · ${escHtml(ag.ip_address)}</span>
|
||||||
<span style="font-size:0.58rem;color:${alive ? 'var(--green)' : 'var(--red)'};">${alive ? 'ONLINE' : 'OFFLINE'}</span>
|
<span style="font-size:0.58rem;color:${alive ? 'var(--green)' : 'var(--red)'};">${alive ? 'ONLINE' : 'OFFLINE'}</span>
|
||||||
</div>
|
</div>
|
||||||
${alive ? `<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;margin-bottom:4px">
|
${alive ? `<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;margin-bottom:4px">
|
||||||
@@ -425,21 +425,30 @@ function renderAgentsTab(agents, metrics) {
|
|||||||
${svcs ? `<div style="font-size:0.58rem">${svcs}</div>` : ''}
|
${svcs ? `<div style="font-size:0.58rem">${svcs}</div>` : ''}
|
||||||
</div>
|
</div>
|
||||||
${alive ? `<div style="display:flex;gap:5px;margin-top:6px">
|
${alive ? `<div style="display:flex;gap:5px;margin-top:6px">
|
||||||
<button onclick="event.stopPropagation();agentScreenshot('${ag.hostname}')" style="flex:1;background:rgba(0,212,255,0.06);border:1px solid var(--panel-border);border-radius:3px;padding:3px 6px;color:var(--cyan);font-family:var(--font-display);font-size:0.48rem;letter-spacing:1px;cursor:pointer">◈ SCREENSHOT</button>
|
<button onclick="event.stopPropagation();agentScreenshot('${escJs(ag.hostname)}')" style="flex:1;background:rgba(0,212,255,0.06);border:1px solid var(--panel-border);border-radius:3px;padding:3px 6px;color:var(--cyan);font-family:var(--font-display);font-size:0.48rem;letter-spacing:1px;cursor:pointer">◈ SCREENSHOT</button>
|
||||||
<button onclick="event.stopPropagation();agentSysinfo('${ag.hostname}')" style="flex:1;background:rgba(0,212,255,0.06);border:1px solid var(--panel-border);border-radius:3px;padding:3px 6px;color:var(--text-dim);font-family:var(--font-display);font-size:0.48rem;letter-spacing:1px;cursor:pointer">⚡ SYSINFO</button>
|
<button onclick="event.stopPropagation();agentSysinfo('${escJs(ag.hostname)}')" style="flex:1;background:rgba(0,212,255,0.06);border:1px solid var(--panel-border);border-radius:3px;padding:3px 6px;color:var(--text-dim);font-family:var(--font-display);font-size:0.48rem;letter-spacing:1px;cursor:pointer">⚡ SYSINFO</button>
|
||||||
</div>` : ''}
|
</div>` : ''}
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
function openAgentModal() {
|
async function openAgentModal() {
|
||||||
const os = detectOS();
|
const os = detectOS();
|
||||||
const title = document.getElementById('agentModalTitle');
|
const title = document.getElementById('agentModalTitle');
|
||||||
const content = document.getElementById('agentModalContent');
|
const content = document.getElementById('agentModalContent');
|
||||||
const modal = document.getElementById('agentModal');
|
const modal = document.getElementById('agentModal');
|
||||||
const regKey = 'f846a9aaf7ce9a61742c63c87c4186052a71d2a580c65518';
|
let regKey = '<YOUR-REGISTRATION-KEY>';
|
||||||
const baseUrl = 'https://jarvis.orbishosting.com/agent';
|
try {
|
||||||
|
const rkResp = await fetch('/api/agent/regkey');
|
||||||
|
if (rkResp.ok) { const rk = await rkResp.json(); if (rk.registration_key) regKey = rk.registration_key; }
|
||||||
|
} catch (e) { /* not logged in — placeholder stays */ }
|
||||||
const jUrl = window.location.origin;
|
const jUrl = window.location.origin;
|
||||||
|
// Fixed 2026-07-07: this used to be hardcoded to https://jarvis.orbishosting.com/agent,
|
||||||
|
// which isn't reachable from outside the LAN at all (no FortiGate VIP forwards the
|
||||||
|
// default port there — confirmed HTTP:000). Using the current page's own origin
|
||||||
|
// instead means the download link always matches wherever the visitor actually
|
||||||
|
// reached this dashboard from, LAN or external.
|
||||||
|
const baseUrl = jUrl + '/agent';
|
||||||
|
|
||||||
if (os === 'tablet') {
|
if (os === 'tablet') {
|
||||||
title.textContent = '● JARVIS — TABLET / MOBILE';
|
title.textContent = '● JARVIS — TABLET / MOBILE';
|
||||||
@@ -460,9 +469,12 @@ function openAgentModal() {
|
|||||||
const inst = {
|
const inst = {
|
||||||
windows: {
|
windows: {
|
||||||
label:'Windows',
|
label:'Windows',
|
||||||
cmd:'# Run PowerShell as Administrator:\nSet-ExecutionPolicy Bypass -Scope Process -Force\nInvoke-WebRequest -Uri "'+baseUrl+'/install-windows.ps1" -OutFile "$env:TEMP\\install.ps1"\n& "$env:TEMP\\install.ps1" -JarvisUrl '+jUrl+' -Key '+regKey,
|
// install-windows.ps1 takes no parameters — it reads the reg key from
|
||||||
dl: baseUrl+'/install-windows.ps1',
|
// $env:JARVIS_REG_KEY and downloads the standalone exe itself (no
|
||||||
note:'Run PowerShell as Administrator. Installs as a Windows Task Scheduler service.'
|
// Python/pywin32 needed on the target machine as of the 2026-07-07 rebuild).
|
||||||
|
cmd:'# Run PowerShell as Administrator:\n$env:JARVIS_REG_KEY=\''+regKey+'\'\nirm '+baseUrl+'/install-windows.ps1 | iex',
|
||||||
|
dl: baseUrl+'/install-windows.exe',
|
||||||
|
note:'Run PowerShell as Administrator. Installs as a real Windows Service (auto-starts at boot, no window to keep open).'
|
||||||
},
|
},
|
||||||
mac: {
|
mac: {
|
||||||
label:'macOS',
|
label:'macOS',
|
||||||
@@ -472,15 +484,17 @@ function openAgentModal() {
|
|||||||
},
|
},
|
||||||
linux: {
|
linux: {
|
||||||
label:'Linux',
|
label:'Linux',
|
||||||
cmd:'curl -sSL '+baseUrl+'/install.sh | sudo bash -s -- \\\n --jarvis-url '+jUrl+' \\\n --key '+regKey,
|
// install.sh takes positional args (hostname, agent_type); JARVIS URL and
|
||||||
|
// registration key come from env vars (key is no longer baked into the script).
|
||||||
|
cmd:'curl -sSL '+baseUrl+'/install.sh | JARVIS_URL='+jUrl+' JARVIS_REG_KEY=\''+regKey+'\' bash -s -- $(hostname) linux',
|
||||||
dl: baseUrl+'/install.sh',
|
dl: baseUrl+'/install.sh',
|
||||||
note:'Run in terminal. Installs as a systemd service.'
|
note:'Run in terminal (sudo). Installs as a systemd service.'
|
||||||
},
|
},
|
||||||
unknown: {
|
unknown: {
|
||||||
label:'Your System',
|
label:'Your System',
|
||||||
cmd:'# Browse installers:\nhttps://jarvis.orbishosting.com/agent/',
|
cmd:'# Couldn\'t detect your OS automatically. Installers are at:\n'+baseUrl+'/install.sh (Linux)\n'+baseUrl+'/install-mac.sh (macOS)\n'+baseUrl+'/install-windows.ps1 (Windows)',
|
||||||
dl: 'https://jarvis.orbishosting.com/agent/',
|
dl: baseUrl+'/install.sh',
|
||||||
note:'Choose your platform installer from the JARVIS agent directory.'
|
note:'Auto-detection didn\'t recognize this browser/OS — pick the matching installer above.'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const i = inst[os] || inst.unknown;
|
const i = inst[os] || inst.unknown;
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
<div id="leftPanel">
|
<div id="leftPanel">
|
||||||
<!-- Weather Widget -->
|
<!-- Weather Widget -->
|
||||||
<div class="panel" style="flex:0 0 auto">
|
<div class="panel" style="flex:0 0 auto">
|
||||||
<div class="panel-title">WEATHER <span id="weather-loc" style="font-size:0.55rem;color:var(--text-dim)">FORT WORTH, TX</span></div>
|
<div class="panel-title">WEATHER <span id="weather-loc" style="font-size:0.55rem;color:var(--text-dim)">WEATHERFORD, TX</span></div>
|
||||||
<div style="display:flex;align-items:flex-start;gap:12px;margin-bottom:8px">
|
<div style="display:flex;align-items:flex-start;gap:12px;margin-bottom:8px">
|
||||||
<div style="flex:1">
|
<div style="flex:1">
|
||||||
<div style="display:flex;align-items:baseline;gap:8px">
|
<div style="display:flex;align-items:baseline;gap:8px">
|
||||||
|
|||||||
@@ -1,20 +1,34 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# JARVIS Agent Installer — one-liner for any Linux host:
|
# JARVIS Agent Installer — one-liner for any Linux host:
|
||||||
# curl -sk https://jarvis.orbishosting.com/install-agent.sh | bash -s <hostname> <agent_type>
|
# curl -sk http://jarvis.orbishosting.com:1972/agent/install.sh | bash -s <hostname> <agent_type>
|
||||||
#
|
#
|
||||||
# agent_type: linux | proxmox | homeassistant
|
# agent_type: linux | proxmox | homeassistant
|
||||||
# Example: curl -sk https://jarvis.orbishosting.com/install-agent.sh | bash -s myserver linux
|
# Example: curl -sk http://jarvis.orbishosting.com:1972/agent/install.sh | bash -s myserver linux
|
||||||
|
#
|
||||||
|
# On the LAN, set JARVIS_URL to the direct internal address instead (faster,
|
||||||
|
# doesn't hairpin through Cloudflare): JARVIS_URL=http://10.48.200.211 curl ... | bash -s ...
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
HOSTNAME_ARG="${1:-$(hostname -s)}"
|
HOSTNAME_ARG="${1:-$(hostname -s)}"
|
||||||
AGENT_TYPE="${2:-linux}"
|
AGENT_TYPE="${2:-linux}"
|
||||||
JARVIS_URL="https://165.22.1.228"
|
# Fixed 2026-07-07: jarvis.orbishosting.com on the default port isn't reachable
|
||||||
JARVIS_HOST="jarvis.orbishosting.com"
|
# from outside the LAN at all (no FortiGate VIP forwards it) — :1972 is the
|
||||||
|
# confirmed-working external path (same fix as the GitHub webhook and the
|
||||||
|
# Windows agent installer).
|
||||||
|
JARVIS_URL="${JARVIS_URL:-http://jarvis.orbishosting.com:1972}"
|
||||||
|
JARVIS_HOST=""
|
||||||
INSTALL_DIR="/opt/jarvis-agent"
|
INSTALL_DIR="/opt/jarvis-agent"
|
||||||
CONFIG_DIR="/etc/jarvis-agent"
|
CONFIG_DIR="/etc/jarvis-agent"
|
||||||
STATE_DIR="/var/lib/jarvis-agent"
|
STATE_DIR="/var/lib/jarvis-agent"
|
||||||
REG_KEY="f846a9aaf7ce9a61742c63c87c4186052a71d2a580c65518"
|
REG_KEY="${JARVIS_REG_KEY:-}"
|
||||||
|
if [ -z "$REG_KEY" ] && [ -r /dev/tty ]; then
|
||||||
|
read -rp "Enter JARVIS registration key: " REG_KEY </dev/tty
|
||||||
|
fi
|
||||||
|
if [ -z "$REG_KEY" ]; then
|
||||||
|
echo "ERROR: registration key required (set JARVIS_REG_KEY env var or enter at prompt)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
SERVICE_FILE="/etc/systemd/system/jarvis-agent.service"
|
SERVICE_FILE="/etc/systemd/system/jarvis-agent.service"
|
||||||
|
|
||||||
echo "=== JARVIS Agent Installer v3.0 ==="
|
echo "=== JARVIS Agent Installer v3.0 ==="
|
||||||
@@ -38,7 +52,7 @@ mkdir -p "$INSTALL_DIR" "$CONFIG_DIR" "$STATE_DIR"
|
|||||||
|
|
||||||
# ── Download agent ─────────────────────────────────────────────────────────────
|
# ── Download agent ─────────────────────────────────────────────────────────────
|
||||||
echo "Downloading agent..."
|
echo "Downloading agent..."
|
||||||
curl -sk -H "Host: $JARVIS_HOST" "$JARVIS_URL/agent/jarvis-agent.py" -o "$INSTALL_DIR/jarvis-agent.py"
|
curl -sk "$JARVIS_URL/agent/jarvis-agent.py" -o "$INSTALL_DIR/jarvis-agent.py"
|
||||||
cp "$INSTALL_DIR/jarvis-agent.py" /usr/local/bin/jarvis-agent.py
|
cp "$INSTALL_DIR/jarvis-agent.py" /usr/local/bin/jarvis-agent.py
|
||||||
chmod +x "$INSTALL_DIR/jarvis-agent.py" /usr/local/bin/jarvis-agent.py
|
chmod +x "$INSTALL_DIR/jarvis-agent.py" /usr/local/bin/jarvis-agent.py
|
||||||
|
|
||||||
@@ -50,14 +64,14 @@ else
|
|||||||
{
|
{
|
||||||
"jarvis_url": "$JARVIS_URL",
|
"jarvis_url": "$JARVIS_URL",
|
||||||
"host_header": "$JARVIS_HOST",
|
"host_header": "$JARVIS_HOST",
|
||||||
"ssl_verify": false,
|
"ssl_verify": true,
|
||||||
"registration_key": "$REG_KEY",
|
"registration_key": "$REG_KEY",
|
||||||
"hostname": "$HOSTNAME_ARG",
|
"hostname": "$HOSTNAME_ARG",
|
||||||
"agent_type": "$AGENT_TYPE",
|
"agent_type": "$AGENT_TYPE",
|
||||||
"poll_interval": 30,
|
"poll_interval": 30,
|
||||||
"heartbeat_every": 10,
|
"heartbeat_every": 10,
|
||||||
"update_check_hours": 24,
|
"update_check_hours": 24,
|
||||||
"watch_services": ["ollama", "homeassistant", "mysql", "mariadb", "nginx", "apache2", "docker"]
|
"watch_services": []
|
||||||
}
|
}
|
||||||
JSONEOF
|
JSONEOF
|
||||||
chmod 600 "$CONFIG_DIR/config.json"
|
chmod 600 "$CONFIG_DIR/config.json"
|
||||||
|
|||||||
+41
-20
@@ -1,30 +1,51 @@
|
|||||||
<?php
|
<?php
|
||||||
ini_set('session.cache_limiter', '');
|
ini_set('session.cache_limiter', '');
|
||||||
header('Cache-Control: no-store, no-cache, must-revalidate, no-transform');
|
header('Cache-Control: no-store, no-cache, must-revalidate, no-transform');
|
||||||
|
require_once __DIR__ . '/../api/config.php';
|
||||||
session_start();
|
session_start();
|
||||||
if (!empty($_SESSION['jarvis_token'])) { header('Location: /'); exit; }
|
if (!empty($_SESSION['jarvis_token'])) { header('Location: /'); exit; }
|
||||||
$error = '';
|
$error = '';
|
||||||
|
// ── Login rate limiting (Redis, per client IP) ────────────────────────────────
|
||||||
|
// Blocks brute force: 10 failed attempts within 15 min -> locked out for 15 min.
|
||||||
|
$clientIp = $_SERVER['HTTP_CF_CONNECTING_IP'] ?? $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||||
|
$clientIp = trim(explode(',', $clientIp)[0]);
|
||||||
|
$rl = null;
|
||||||
|
try {
|
||||||
|
$rl = new Redis();
|
||||||
|
$rl->connect('127.0.0.1', 6379, 1.5);
|
||||||
|
} catch (Throwable $e) { $rl = null; } // fail open if Redis is down
|
||||||
|
$rlKey = 'login_fail:' . $clientIp;
|
||||||
|
$RL_MAX = 10; $RL_WINDOW = 900;
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
$u = trim($_POST['username'] ?? '');
|
$fails = ($rl && $rl->exists($rlKey)) ? (int)$rl->get($rlKey) : 0;
|
||||||
$p = $_POST['password'] ?? '';
|
if ($fails >= $RL_MAX) {
|
||||||
if ($u && $p) {
|
$error = 'TOO MANY ATTEMPTS — LOCKED';
|
||||||
$pdo = new PDO('mysql:host=localhost;dbname=jarvis_db;charset=utf8mb4',
|
} else {
|
||||||
'jarvis_user', 'J4rv1s_Pr0t0c0l_2026!',
|
$u = trim($_POST['username'] ?? '');
|
||||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
$p = $_POST['password'] ?? '';
|
||||||
$row = $pdo->prepare('SELECT * FROM users WHERE username=? LIMIT 1');
|
if ($u && $p) {
|
||||||
$row->execute([$u]);
|
$pdo = new PDO('mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';charset=utf8mb4',
|
||||||
$user = $row->fetch(PDO::FETCH_ASSOC);
|
DB_USER, DB_PASS,
|
||||||
if ($user && password_verify($p, $user['password_hash'])) {
|
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
||||||
$token = bin2hex(random_bytes(32));
|
$row = $pdo->prepare('SELECT * FROM users WHERE username=? LIMIT 1');
|
||||||
$_SESSION['jarvis_token'] = $token;
|
$row->execute([$u]);
|
||||||
$_SESSION['jarvis_user_id'] = $user['id'];
|
$user = $row->fetch(PDO::FETCH_ASSOC);
|
||||||
$_SESSION['jarvis_name'] = $user['display_name'];
|
if ($user && password_verify($p, $user['password_hash'])) {
|
||||||
$pdo->prepare('UPDATE users SET last_seen=NOW() WHERE id=?')->execute([$user['id']]);
|
if ($rl) $rl->del($rlKey);
|
||||||
header('Location: /');
|
session_regenerate_id(true);
|
||||||
exit;
|
$token = bin2hex(random_bytes(32));
|
||||||
}
|
$_SESSION['jarvis_token'] = $token;
|
||||||
$error = 'ACCESS DENIED';
|
$_SESSION['jarvis_user_id'] = $user['id'];
|
||||||
} else { $error = 'ENTER CREDENTIALS'; }
|
$_SESSION['jarvis_name'] = $user['display_name'];
|
||||||
|
$pdo->prepare('UPDATE users SET last_seen=NOW() WHERE id=?')->execute([$user['id']]);
|
||||||
|
header('Location: /');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if ($rl) { $rl->incr($rlKey); $rl->expire($rlKey, $RL_WINDOW); }
|
||||||
|
$error = 'ACCESS DENIED';
|
||||||
|
} else { $error = 'ENTER CREDENTIALS'; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
?><!DOCTYPE html>
|
?><!DOCTYPE html>
|
||||||
<html lang="en"><head>
|
<html lang="en"><head>
|
||||||
|
|||||||
+9
-18
@@ -13,8 +13,8 @@ if (!defined('WEBHOOK_SECRET')) {
|
|||||||
echo json_encode(['error' => 'Webhook not configured']);
|
echo json_encode(['error' => 'Webhook not configured']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
define('DEPLOY_QUEUE', '/tmp/jarvis-deploy-queue.txt');
|
define('DEPLOY_QUEUE', '/tmp/jarvis-deploy-queue.txt');
|
||||||
define('DEPLOY_LOG', '/var/www/jarvis/logs/deploy.log');
|
define('DEPLOY_LOG', '/var/log/jarvis/deploy.log');
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
@@ -33,9 +33,10 @@ $repo = $data['repository']['name'] ?? '';
|
|||||||
$ref = $data['ref'] ?? '';
|
$ref = $data['ref'] ?? '';
|
||||||
$pusher = $data['pusher']['name'] ?? 'unknown';
|
$pusher = $data['pusher']['name'] ?? 'unknown';
|
||||||
|
|
||||||
// Only deploy on pushes to main
|
// Only deploy on pushes to the repo's actual default branch (master, not main —
|
||||||
if ($ref !== 'refs/heads/main') {
|
// this was checking 'main' for a while even though the jarvis repo has always used 'master')
|
||||||
echo json_encode(['ok' => true, 'skipped' => "ref $ref is not main"]);
|
if ($ref !== 'refs/heads/master') {
|
||||||
|
echo json_encode(['ok' => true, 'skipped' => "ref $ref is not master"]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,20 +51,10 @@ if (!isset($repoMap[$repo])) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$path = $repoMap[$repo];
|
$path = $repoMap[$repo];
|
||||||
|
$ts = date('Y-m-d H:i:s');
|
||||||
// NovaCPX lives on a private VM — the VM polls GitHub every minute via cron
|
|
||||||
// This webhook receipt confirms GitHub delivered the push notification
|
|
||||||
if ($path === '__NOVACPX_VM__') {
|
|
||||||
$commit = $data['after'] ?? 'HEAD';
|
|
||||||
$msg = "[" . date('Y-m-d H:i:s') . "] NovaCPX push by $pusher (commit: $commit) — VM will deploy within 1 min";
|
|
||||||
file_put_contents(DEPLOY_LOG, $msg . "\n", FILE_APPEND | LOCK_EX);
|
|
||||||
echo json_encode(['ok' => true, 'queued' => 'novacpx', 'commit' => $commit]);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
file_put_contents(DEPLOY_QUEUE, $path . "\n", FILE_APPEND | LOCK_EX);
|
file_put_contents(DEPLOY_QUEUE, $path . "\n", FILE_APPEND | LOCK_EX);
|
||||||
|
file_put_contents(DEPLOY_LOG, "[$ts] Queued deploy: $repo by $pusher -> $path\n", FILE_APPEND | LOCK_EX);
|
||||||
$msg = "[" . date('Y-m-d H:i:s') . "] Queued deploy: $repo by $pusher -> $path";
|
|
||||||
file_put_contents(DEPLOY_LOG, $msg . "\n", FILE_APPEND | LOCK_EX);
|
|
||||||
|
|
||||||
echo json_encode(['ok' => true, 'queued' => $repo, 'path' => $path]);
|
echo json_encode(['ok' => true, 'queued' => $repo, 'path' => $path]);
|
||||||
|
// deploy pipeline verified working 2026-07-07
|
||||||
|
|||||||
Reference in New Issue
Block a user