Compare commits

12 Commits

Author SHA1 Message Date
myron af03a2f2d8 Fix reactor startup: auto-create venv, requirements.txt, SETUP button, self-install
- deploy/requirements.txt: explicit pip deps (fastapi, uvicorn, aiomysql, aiohttp, anthropic, trafilatura)
- jarvis-deploy.sh: self-installs to /usr/local/bin/ on every run so updates propagate;
  auto-creates venv and installs packages if missing before restart attempt
- admin/index.php: add SETUP button next to RESTART — runs full setup+start in one click
  (creates venv, installs deps, copies reactor.py, starts it)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X8tDRrQqgLjqXebMCBNcP3
2026-07-01 04:11:12 -05:00
myron dfc92a6791 Merge branch 'master' 2026-07-01 04:09:28 -05:00
myron 05522edb1d Add llava vision: use Ollama llava:7b for all image analysis, Claude as fallback
- Add _ollama_vision_call() using Ollama /api/generate with images array
- handle_screenshot: try llava first (free, on-LAN), fall back to Claude on error;
  text-only snapshots now use ollama instead of groq
- handle_vision: same llava-first/Claude-fallback pattern; caller can force
  provider='ollama' or 'claude' explicitly; stored provider_used reflects actual
  provider that ran

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X8tDRrQqgLjqXebMCBNcP3
2026-07-01 04:05:37 -05:00
myron 435af8ccc9 Wire Ollama properly: fix IP, upgrade to llama3.1:8b, use for guardian/sitrep
- reactor.py: fix OLLAMA_HOST .95 → .210 (actual Ollama VM IP)
- reactor.py: upgrade OLLAMA_MODEL 1b → 8b (llama3.1:8b has tool-calling, 131K ctx)
- reactor.py: guardian alerts and sitrep now use ollama instead of groq (free, on-LAN, no quota)
- config.example.php: same IP/model fixes + fix GROQ_MODEL_SEARCH to compound-beta-mini (groq/ prefix causes 404)
- deploy script: patch live config.php on every deploy to correct Ollama IP/model and Groq model name

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X8tDRrQqgLjqXebMCBNcP3
2026-07-01 04:02:30 -05:00
myron a9ea75db98 Fix Arc Reactor restart: use bash for source, add systemd service, fix deploy
The admin panel restart command used shell_exec which runs /bin/sh (dash on
Ubuntu) — dash doesn't support 'source', so the venv never activated and the
reactor silently never started.

- admin/index.php: wrap restart in bash -c so 'source' works; try systemctl
  first then fall back to nohup
- deploy/jarvis-arc.service: add proper systemd unit so reactor auto-starts
  on boot and auto-restarts on crash
- deploy/jarvis-deploy.sh: install+enable service file when it changes; mkdir
  log dir before restart; fall back to nohup if systemctl not set up yet

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X8tDRrQqgLjqXebMCBNcP3
2026-07-01 03:56:11 -05:00
myron 651455cb47 Fix guardian mode: create missing tables on startup, fix conversations column name
- Add CREATE TABLE IF NOT EXISTS for guardian_config and guardian_events in
  reactor lifespan so tables are created automatically on next restart
- Fix conversations column bug: reactor used 'message' but schema has 'content';
  fix INSERT and SELECT queries to use content (SELECT aliases it as 'message'
  so the JSON response key stays the same)
- Add guardian tables to schema.sql for documentation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X8tDRrQqgLjqXebMCBNcP3
2026-07-01 03:49:10 -05:00
myron 90e4ded7c9 Fix 8 issues from code review
- ha-poller: replace recursive main() retry with while loop (stack overflow fix)
- ha-poller: advance last_push on empty HA response (log spam fix)
- ha-poller: use datetime.now(timezone.utc) instead of deprecated utcnow()
- ping-probe: always call update_status() unconditionally so offline devices register as offline
- agent.php: heartbeat reads status from payload instead of hardcoding 'online'
- phone-probe: delegate JSON building to python3 (bash concatenation injection fix)
- netscan + phone-probe: read registration key from /etc/jarvis-agent/reg-key
- admin/index.php: sync ha_list skipDomains with ha.php (14 missing domains added)
- facts_collector: self-check JARVIS via 127.0.0.1 instead of Cloudflare hairpin
2026-06-29 20:58:22 -05:00
myron c1275d47a6 Add PVE1 probe scripts to repo (netscan, ping-probe, phone-probe)
Scripts were running on PVE1 but not tracked in git. Pulling current
versions that push to http://10.48.200.211 (was old DO server IP).
2026-06-29 19:44:39 -05:00
myron 08fbfaa3e4 Seed kb_intents/preferences, fix usage_patterns column, update schema, fix site URL
- db/seed_kb.sql: 25 intent patterns + user prefs (Myron / Mr. Blair)
- usage_patterns: renamed last_used→last_seen to match chat.php
- facts_collector: JARVIS self-check URL was port 1972 (DO), now correct URL
- db/schema.sql: reflects current live DB schema

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 18:15:53 -05:00
myron 1f25b5d04d Fix facts_collector JARVIS site URL (was :1972 DO port, now correct URL)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 18:13:12 -05:00
myron 874f6e8c5c fix: rename parkersling site key to parkerslingshotrentals in facts_collector 2026-06-23 18:09:30 +00:00
myron 42a82c40cb fix: remove dead __NOVACPX_VM__ branch, dedupe date() call in webhook 2026-06-23 18:03:22 +00:00
15 changed files with 561 additions and 96 deletions
+3 -4
View File
@@ -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)
+67
View File
@@ -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"
+74
View File
@@ -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
+99
View File
@@ -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()
+4 -4
View File
@@ -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));
+2 -1
View File
@@ -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(
+2 -2
View File
@@ -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
View File
@@ -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
+70
View File
@@ -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;
+17
View File
@@ -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
View File
@@ -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
+77 -21
View File
@@ -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,10 +668,16 @@ 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:
analysis = await _ollama_vision_call(image_b64, analyze_prompt)
provider_used = "ollama:llava"
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: try:
import anthropic import anthropic
client = anthropic.AsyncAnthropic(api_key=CLAUDE_API_KEY) client = anthropic.AsyncAnthropic(api_key=CLAUDE_API_KEY)
@@ -679,17 +695,17 @@ async def handle_screenshot(payload: dict) -> dict:
) )
analysis = msg.content[0].text if msg.content else "" analysis = msg.content[0].text if msg.content else ""
provider_used = "claude" provider_used = "claude"
log.info(f"[VISION] Claude analysis complete ({len(analysis)} chars)") log.info(f"[VISION] Claude fallback analysis complete ({len(analysis)} chars)")
except Exception as e: except Exception as e2:
log.warning(f"[VISION] Claude vision failed: {e}") log.warning(f"[VISION] Claude fallback also failed: {e2}")
analysis = f"Vision analysis unavailable: {e}" 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,8 +758,22 @@ 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}")
analysis = ""
provider_used = provider
if provider == "ollama" or provider == "llava":
analysis = await _ollama_vision_call(image_b64, prompt)
provider_used = "ollama:llava"
else:
# Default: llava first, Claude fallback
try:
analysis = await _ollama_vision_call(image_b64, prompt)
provider_used = "ollama:llava"
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: try:
import anthropic import anthropic
client = anthropic.AsyncAnthropic(api_key=CLAUDE_API_KEY) client = anthropic.AsyncAnthropic(api_key=CLAUDE_API_KEY)
@@ -760,14 +790,15 @@ async def handle_vision(payload: dict) -> dict:
}], }],
) )
analysis = msg.content[0].text if msg.content else "" analysis = msg.content[0].text if msg.content else ""
except Exception as e: provider_used = "claude"
raise RuntimeError(f"Vision analysis failed: {e}") 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 []
+6
View File
@@ -0,0 +1,6 @@
fastapi
uvicorn[standard]
aiomysql
aiohttp
anthropic
trafilatura
+22 -3
View File
@@ -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')}">&#8635; RESTART</button></td> <td style="display:flex;gap:4px">
<button onclick="workerAction('daemon','arc_reactor','restart')" style="${wBtn('red')}">&#8635; RESTART</button>
<button onclick="workerAction('daemon','arc_reactor','setup')" style="${wBtn('orange')}">&#9881; SETUP</button>
</td>
</tr>`; </tr>`;
} }
async function loadDashboard() { async function loadDashboard() {
+2 -13
View File
@@ -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]);