mirror of
https://github.com/myronblair/jarvis
synced 2026-07-29 09:12:36 -05:00
Compare commits
12 Commits
84cd2ded50
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| af03a2f2d8 | |||
| dfc92a6791 | |||
| 05522edb1d | |||
| 435af8ccc9 | |||
| a9ea75db98 | |||
| 651455cb47 | |||
| 90e4ded7c9 | |||
| c1275d47a6 | |||
| 08fbfaa3e4 | |||
| 1f25b5d04d | |||
| 874f6e8c5c | |||
| 42a82c40cb |
@@ -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 --send-ip "$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));
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -181,10 +181,10 @@ function collect_all(): array {
|
|||||||
$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',
|
||||||
|
|||||||
+48
-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,41 @@ 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
|
||||||
|
|||||||
@@ -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,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
|
||||||
+35
-4
@@ -80,11 +80,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
|
||||||
|
|
||||||
|
|||||||
+108
-52
@@ -49,8 +49,8 @@ CLAUDE_API_KEY = "sk-ant-api03-JL6vjFeyEfajQmaTOmsT6AfLLPs2icrIAvvJ0hdi4DuMi0155
|
|||||||
CLAUDE_MODEL = "claude-sonnet-4-6"
|
CLAUDE_MODEL = "claude-sonnet-4-6"
|
||||||
GROQ_API_KEY = "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"
|
GROQ_API_KEY = "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"
|
||||||
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"
|
||||||
|
|
||||||
GMAIL_USER = "myronblair@gmail.com"
|
GMAIL_USER = "myronblair@gmail.com"
|
||||||
GMAIL_PASS = "demsvdylwweacbcx"
|
GMAIL_PASS = "demsvdylwweacbcx"
|
||||||
@@ -175,6 +175,16 @@ async def _ollama_call(messages: list, system: str = "") -> str:
|
|||||||
data = await resp.json()
|
data = await resp.json()
|
||||||
return data.get("response", "")
|
return data.get("response", "")
|
||||||
|
|
||||||
|
async def _ollama_vision_call(image_b64: str, prompt: str) -> str:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.post(
|
||||||
|
f"{OLLAMA_HOST}/api/generate",
|
||||||
|
json={"model": "llava:7b", "prompt": prompt, "images": [image_b64], "stream": False},
|
||||||
|
timeout=aiohttp.ClientTimeout(total=120),
|
||||||
|
) as resp:
|
||||||
|
data = await resp.json()
|
||||||
|
return data.get("response", "").strip()
|
||||||
|
|
||||||
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.")
|
||||||
@@ -658,38 +668,44 @@ async def handle_screenshot(payload: dict) -> dict:
|
|||||||
height = result.get("height", 0)
|
height = result.get("height", 0)
|
||||||
file_size = result.get("file_size", 0)
|
file_size = result.get("file_size", 0)
|
||||||
|
|
||||||
# Run Claude vision analysis if we have an image
|
# Run vision analysis if we have an image — llava first, Claude fallback
|
||||||
analysis = ""
|
analysis = ""
|
||||||
provider_used = ""
|
provider_used = ""
|
||||||
if do_analyze and image_b64:
|
if do_analyze and image_b64:
|
||||||
try:
|
try:
|
||||||
import anthropic
|
analysis = await _ollama_vision_call(image_b64, analyze_prompt)
|
||||||
client = anthropic.AsyncAnthropic(api_key=CLAUDE_API_KEY)
|
provider_used = "ollama:llava"
|
||||||
msg = await client.messages.create(
|
log.info(f"[VISION] llava analysis complete ({len(analysis)} chars)")
|
||||||
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:
|
except Exception as e:
|
||||||
log.warning(f"[VISION] Claude vision failed: {e}")
|
log.warning(f"[VISION] llava failed: {e} — falling back to Claude")
|
||||||
analysis = f"Vision analysis unavailable: {e}"
|
try:
|
||||||
|
import anthropic
|
||||||
|
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 fallback analysis complete ({len(analysis)} chars)")
|
||||||
|
except Exception as e2:
|
||||||
|
log.warning(f"[VISION] Claude fallback also failed: {e2}")
|
||||||
|
analysis = f"Vision analysis unavailable: {e2}"
|
||||||
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 Ollama
|
||||||
try:
|
try:
|
||||||
snap_text = json.dumps(result, indent=2)[:3000]
|
snap_text = json.dumps(result, indent=2)[:3000]
|
||||||
prompt = f"Summarize this server system snapshot for JARVIS. Highlight any concerns:\n\n{snap_text}"
|
prompt = f"Summarize this server system snapshot for JARVIS. Highlight any concerns:\n\n{snap_text}"
|
||||||
analysis = await llm_call([{"role": "user", "content": prompt}], "groq")
|
analysis = await llm_call([{"role": "user", "content": prompt}], "ollama")
|
||||||
provider_used = "groq"
|
provider_used = "ollama"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
analysis = f"Analysis unavailable: {e}"
|
analysis = f"Analysis unavailable: {e}"
|
||||||
|
|
||||||
@@ -742,32 +758,47 @@ async def handle_vision(payload: dict) -> dict:
|
|||||||
if not image_b64:
|
if not image_b64:
|
||||||
raise ValueError("No image data provided")
|
raise ValueError("No image data provided")
|
||||||
|
|
||||||
log.info(f"[VISION] Analysis: screenshot_id={screenshot_id} agent={hostname}")
|
log.info(f"[VISION] Analysis: screenshot_id={screenshot_id} agent={hostname} provider={provider}")
|
||||||
|
|
||||||
try:
|
analysis = ""
|
||||||
import anthropic
|
provider_used = provider
|
||||||
client = anthropic.AsyncAnthropic(api_key=CLAUDE_API_KEY)
|
|
||||||
msg = await client.messages.create(
|
if provider == "ollama" or provider == "llava":
|
||||||
model="claude-opus-4-8-20251101",
|
analysis = await _ollama_vision_call(image_b64, prompt)
|
||||||
max_tokens=2048,
|
provider_used = "ollama:llava"
|
||||||
messages=[{
|
else:
|
||||||
"role": "user",
|
# Default: llava first, Claude fallback
|
||||||
"content": [
|
try:
|
||||||
{"type": "image", "source": {"type": "base64",
|
analysis = await _ollama_vision_call(image_b64, prompt)
|
||||||
"media_type": "image/png", "data": image_b64}},
|
provider_used = "ollama:llava"
|
||||||
{"type": "text", "text": prompt},
|
log.info(f"[VISION] llava analysis complete ({len(analysis)} chars)")
|
||||||
],
|
except Exception as e:
|
||||||
}],
|
log.warning(f"[VISION] llava failed: {e} — falling back to Claude")
|
||||||
)
|
try:
|
||||||
analysis = msg.content[0].text if msg.content else ""
|
import anthropic
|
||||||
except Exception as e:
|
client = anthropic.AsyncAnthropic(api_key=CLAUDE_API_KEY)
|
||||||
raise RuntimeError(f"Vision analysis failed: {e}")
|
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 ""
|
||||||
|
provider_used = "claude"
|
||||||
|
except Exception as e2:
|
||||||
|
raise RuntimeError(f"Vision analysis failed (llava: {e}, claude: {e2})")
|
||||||
|
|
||||||
# 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 +806,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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1022,7 +1053,7 @@ async def guardian_loop() -> None:
|
|||||||
"for Myron. Be direct about severity and what action to take. "
|
"for Myron. Be direct about severity and what action to take. "
|
||||||
"No markdown, no headers."
|
"No markdown, no headers."
|
||||||
)
|
)
|
||||||
ai_msg = await llm_call([{"role": "user", "content": ai_prompt}], "groq")
|
ai_msg = await llm_call([{"role": "user", "content": ai_prompt}], "ollama")
|
||||||
# Update the most recent guardian event with AI analysis
|
# Update the most recent guardian event with AI analysis
|
||||||
await db_execute(
|
await db_execute(
|
||||||
"""UPDATE guardian_events SET ai_analysis=%s
|
"""UPDATE guardian_events SET ai_analysis=%s
|
||||||
@@ -1051,7 +1082,7 @@ async def _guardian_inject_chat(message: str) -> None:
|
|||||||
"""Write a proactive JARVIS message into the conversations table so the HUD picks it up."""
|
"""Write a proactive JARVIS message into the conversations table so the HUD picks it up."""
|
||||||
try:
|
try:
|
||||||
await db_execute(
|
await db_execute(
|
||||||
"""INSERT INTO conversations (session_id, role, message, created_at)
|
"""INSERT INTO conversations (session_id, role, content, created_at)
|
||||||
VALUES ('guardian', 'assistant', %s, NOW())""",
|
VALUES ('guardian', 'assistant', %s, NOW())""",
|
||||||
(message,)
|
(message,)
|
||||||
)
|
)
|
||||||
@@ -1065,7 +1096,7 @@ async def handle_sitrep(payload: dict) -> dict:
|
|||||||
payload: { detail: brief|full, provider: groq }
|
payload: { detail: brief|full, provider: groq }
|
||||||
"""
|
"""
|
||||||
detail = payload.get("detail", "full")
|
detail = payload.get("detail", "full")
|
||||||
provider = payload.get("provider", "groq")
|
provider = payload.get("provider", "ollama")
|
||||||
|
|
||||||
log.info(f"[GUARDIAN] SITREP requested (detail={detail})")
|
log.info(f"[GUARDIAN] SITREP requested (detail={detail})")
|
||||||
|
|
||||||
@@ -1806,7 +1837,7 @@ Keep the tone confident and action-oriented. Format with clear sections. Under 4
|
|||||||
|
|
||||||
# Store in conversations so HUD can surface it
|
# Store in conversations so HUD can surface it
|
||||||
await db_execute(
|
await db_execute(
|
||||||
"INSERT INTO conversations (session_id, role, message, created_at) VALUES ('guardian','assistant',%s,NOW())",
|
"INSERT INTO conversations (session_id, role, content, created_at) VALUES ('guardian','assistant',%s,NOW())",
|
||||||
(review_text,)
|
(review_text,)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2209,6 +2240,31 @@ async def lifespan(app: FastAPI):
|
|||||||
log.info(f"◈ JARVIS Arc Reactor v{VERSION} starting on {HOST}:{PORT}")
|
log.info(f"◈ JARVIS Arc Reactor v{VERSION} starting on {HOST}:{PORT}")
|
||||||
await get_pool()
|
await get_pool()
|
||||||
await db_execute("UPDATE arc_status SET started_at=NOW(), last_heartbeat=NOW(), version=%s WHERE id=1", (VERSION,))
|
await db_execute("UPDATE arc_status SET started_at=NOW(), last_heartbeat=NOW(), version=%s WHERE id=1", (VERSION,))
|
||||||
|
await db_execute("""CREATE TABLE IF NOT EXISTS guardian_config (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
key_name VARCHAR(64) NOT NULL,
|
||||||
|
value VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE KEY uk_key (key_name)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci""")
|
||||||
|
await db_execute("""CREATE TABLE IF NOT EXISTS guardian_events (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
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,
|
||||||
|
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""")
|
||||||
asyncio.create_task(job_poller())
|
asyncio.create_task(job_poller())
|
||||||
asyncio.create_task(heartbeat_loop())
|
asyncio.create_task(heartbeat_loop())
|
||||||
asyncio.create_task(guardian_loop())
|
asyncio.create_task(guardian_loop())
|
||||||
@@ -2728,13 +2784,13 @@ async def guardian_chat_events(since: str = ""):
|
|||||||
"""Return proactive guardian messages injected into conversations."""
|
"""Return proactive guardian messages injected into conversations."""
|
||||||
if since:
|
if since:
|
||||||
rows = await db_fetchall(
|
rows = await db_fetchall(
|
||||||
"SELECT id, message, created_at FROM conversations "
|
"SELECT id, content AS message, created_at FROM conversations "
|
||||||
"WHERE session_id='guardian' AND created_at > %s ORDER BY created_at ASC",
|
"WHERE session_id='guardian' AND created_at > %s ORDER BY created_at ASC",
|
||||||
(since,)
|
(since,)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
rows = await db_fetchall(
|
rows = await db_fetchall(
|
||||||
"SELECT id, message, created_at FROM conversations "
|
"SELECT id, content AS message, created_at FROM conversations "
|
||||||
"WHERE session_id='guardian' ORDER BY created_at DESC LIMIT 10"
|
"WHERE session_id='guardian' ORDER BY created_at DESC LIMIT 10"
|
||||||
)
|
)
|
||||||
return rows or []
|
return rows or []
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
fastapi
|
||||||
|
uvicorn[standard]
|
||||||
|
aiomysql
|
||||||
|
aiohttp
|
||||||
|
anthropic
|
||||||
|
trafilatura
|
||||||
@@ -242,7 +242,9 @@ if ($action) {
|
|||||||
$search = strtolower(trim($_GET['search'] ?? ''));
|
$search = strtolower(trim($_GET['search'] ?? ''));
|
||||||
$skipDomains = ['sensor','binary_sensor','button','update','select','number',
|
$skipDomains = ['sensor','binary_sensor','button','update','select','number',
|
||||||
'device_tracker','event','image','person','zone','tts','conversation',
|
'device_tracker','event','image','person','zone','tts','conversation',
|
||||||
'assist_satellite','input_button'];
|
'assist_satellite','input_button','media_player','scene','water_heater',
|
||||||
|
'alarm_control_panel','automation','script','calendar','notify',
|
||||||
|
'weather','camera','siren','remote','todo','lawn_mower'];
|
||||||
$skipKeywords = ['pre_release','_record','_ftp_','_push_','_hub_ringtone',
|
$skipKeywords = ['pre_release','_record','_ftp_','_push_','_hub_ringtone',
|
||||||
'_siren_on','_email_on','_manual_record','_infrared_',
|
'_siren_on','_email_on','_manual_record','_infrared_',
|
||||||
'do_not_disturb','matter_server','zerotier','mariadb',
|
'do_not_disturb','matter_server','zerotier','mariadb',
|
||||||
@@ -548,8 +550,22 @@ if ($action) {
|
|||||||
j(['ok'=>true,'msg'=>ucwords(str_replace('_',' ',$wId)).' triggered']);
|
j(['ok'=>true,'msg'=>ucwords(str_replace('_',' ',$wId)).' triggered']);
|
||||||
} else { bad('Unknown cron worker'); }
|
} else { bad('Unknown cron worker'); }
|
||||||
} elseif ($wType === 'daemon' && $wId === 'arc_reactor' && $wAction === 'restart') {
|
} elseif ($wType === 'daemon' && $wId === 'arc_reactor' && $wAction === 'restart') {
|
||||||
shell_exec('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 &');
|
shell_exec('bash -c "mkdir -p /home/jarvis.orbishosting.com/logs; systemctl restart jarvis-arc 2>/dev/null || (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 &)"');
|
||||||
j(['ok'=>true,'msg'=>'Arc Reactor restarting']);
|
j(['ok'=>true,'msg'=>'Arc Reactor restarting']);
|
||||||
|
} elseif ($wType === 'daemon' && $wId === 'arc_reactor' && $wAction === 'setup') {
|
||||||
|
$setupLog = '/home/jarvis.orbishosting.com/logs/arc_reactor.log';
|
||||||
|
$cmd = 'bash -c "'.
|
||||||
|
'mkdir -p /opt/jarvis-arc /home/jarvis.orbishosting.com/logs && '.
|
||||||
|
'cp /var/www/jarvis/deploy/reactor.py /opt/jarvis-arc/reactor.py && '.
|
||||||
|
'cp /var/www/jarvis/deploy/requirements.txt /opt/jarvis-arc/requirements.txt && '.
|
||||||
|
'if [ ! -d /opt/jarvis-arc/venv ]; then python3 -m venv /opt/jarvis-arc/venv; fi && '.
|
||||||
|
'/opt/jarvis-arc/venv/bin/pip install -q -r /opt/jarvis-arc/requirements.txt && '.
|
||||||
|
'pkill -f reactor.py 2>/dev/null; sleep 1 && '.
|
||||||
|
'cd /opt/jarvis-arc && source venv/bin/activate && '.
|
||||||
|
'nohup python3 reactor.py >> '.$setupLog.' 2>&1 &'.
|
||||||
|
'" >> '.$setupLog.' 2>&1';
|
||||||
|
shell_exec($cmd);
|
||||||
|
j(['ok'=>true,'msg'=>'Arc Reactor setup started — check log in ~30s then restart']);
|
||||||
} else { bad('Invalid worker action'); }
|
} else { bad('Invalid worker action'); }
|
||||||
break;
|
break;
|
||||||
case 'arc_status':
|
case 'arc_status':
|
||||||
@@ -2330,7 +2346,10 @@ async function loadWorkers() {
|
|||||||
<td class="ts">jarvis-do :7474</td>
|
<td class="ts">jarvis-do :7474</td>
|
||||||
<td>${rdot}${ron?'<span style="color:var(--green)">ONLINE</span>':'<span style="color:var(--red)">OFFLINE</span>'}</td>
|
<td>${rdot}${ron?'<span style="color:var(--green)">ONLINE</span>':'<span style="color:var(--red)">OFFLINE</span>'}</td>
|
||||||
<td class="ts">${rinfo}</td>
|
<td class="ts">${rinfo}</td>
|
||||||
<td><button onclick="workerAction('daemon','arc_reactor','restart')" style="${wBtn('red')}">↻ RESTART</button></td>
|
<td style="display:flex;gap:4px">
|
||||||
|
<button onclick="workerAction('daemon','arc_reactor','restart')" style="${wBtn('red')}">↻ RESTART</button>
|
||||||
|
<button onclick="workerAction('daemon','arc_reactor','setup')" style="${wBtn('orange')}">⚙ SETUP</button>
|
||||||
|
</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
}
|
}
|
||||||
async function loadDashboard() {
|
async function loadDashboard() {
|
||||||
|
|||||||
+4
-15
@@ -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/www/jarvis/logs/deploy.log');
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
@@ -50,20 +50,9 @@ 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]);
|
||||||
|
|||||||
Reference in New Issue
Block a user