mirror of
https://github.com/myronblair/jarvis
synced 2026-07-28 08:43:00 -05:00
Compare commits
31 Commits
main
...
f8a095f783
| Author | SHA1 | Date | |
|---|---|---|---|
| f8a095f783 | |||
| e060ff0c63 | |||
| 588cfe3f10 | |||
| 3db329e925 | |||
| c01805316a | |||
| 20b510186a | |||
| 9b5e16660a | |||
| 7020d445b3 | |||
| 80b0dfc583 | |||
| 30e2bc093c | |||
| a29670e93d | |||
| 10350cbcd8 | |||
| 24bc876c1d | |||
| cfb5b2a3f9 | |||
| 0b1a19d9de | |||
| 66a5443f22 | |||
| e5536b077c | |||
| a292411d52 | |||
| 26b501b600 | |||
| 7327104612 | |||
| a13f750846 | |||
| 3c8dd8e206 | |||
| 554488aefa | |||
| e99c3aa171 | |||
| 2528b37ea1 | |||
| 46dabe3c31 | |||
| d6a1fc9456 | |||
| 2f74b98bbc | |||
| ed15ff12dd | |||
| 3f18cec739 | |||
| 8911645c20 |
@@ -1,29 +1,29 @@
|
|||||||
# JARVIS
|
# JARVIS
|
||||||
|
|
||||||
Iron Man-style AI assistant for home and network management.
|
Iron Man-style AI assistant for home and network management.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- Home Assistant control (lights, climate, scenes, switches)
|
- Home Assistant control (lights, climate, scenes, switches)
|
||||||
- Proxmox VM management (start/stop/status)
|
- Proxmox VM management (start/stop/status)
|
||||||
- 4-tier chat: KB intents > Groq cloud > Ollama local > Claude API
|
- 4-tier chat: KB intents > Groq cloud > Ollama local > Claude API
|
||||||
- Real-time status bar (HA, Proxmox, DigitalOcean)
|
- Real-time status bar (HA, Proxmox, DigitalOcean)
|
||||||
- Iron Man HUD at jarvis.orbishosting.com
|
- Iron Man HUD at jarvis.orbishosting.com
|
||||||
|
|
||||||
## Stack
|
## Stack
|
||||||
- PHP 8.x / Apache / MySQL on Ubuntu 24.04
|
- PHP 8.x / Apache / MySQL on Ubuntu 24.04
|
||||||
- Ollama VM at 10.48.200.95 (llama3.2:1b)
|
- Ollama VM at 10.48.200.95 (llama3.2:1b)
|
||||||
- Groq API (llama-3.3-70b / compound-mini with web search)
|
- Groq API (llama-3.3-70b / compound-mini with web search)
|
||||||
- Claude API (Anthropic) final fallback
|
- Claude API (Anthropic) final fallback
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
cp api/config.example.php api/config.php
|
cp api/config.example.php api/config.php
|
||||||
Fill in all credentials in config.php before running.
|
Fill in all credentials in config.php before running.
|
||||||
|
|
||||||
## Key Files
|
## Key Files
|
||||||
- public/index.html Iron Man HUD frontend
|
- public/index.html Iron Man HUD frontend
|
||||||
- public/api.php API router
|
- public/api.php API router
|
||||||
- api/config.example.php Config template
|
- api/config.example.php Config template
|
||||||
- api/endpoints/chat.php 4-tier chat handler
|
- api/endpoints/chat.php 4-tier chat handler
|
||||||
- api/endpoints/facts_collector.php HA entity sync cron
|
- api/endpoints/facts_collector.php HA entity sync cron
|
||||||
- api/lib/kb_engine.php KB intent engine
|
- api/lib/kb_engine.php KB intent engine
|
||||||
- api/lib/db.php PDO database wrapper
|
- api/lib/db.php PDO database wrapper
|
||||||
|
|||||||
@@ -281,7 +281,7 @@ def get_uptime() -> dict:
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
def get_services(cfg: dict) -> list:
|
def get_services(cfg: dict) -> list:
|
||||||
watch = cfg.get("watch_services", ["ollama", "homeassistant", "mysql", "nginx", "apache2"])
|
watch = cfg.get("watch_services", [])
|
||||||
statuses = []
|
statuses = []
|
||||||
for svc in watch:
|
for svc in watch:
|
||||||
try:
|
try:
|
||||||
|
|||||||
+94
-94
@@ -1,94 +1,94 @@
|
|||||||
<?php
|
<?php
|
||||||
// Digital Ocean server monitoring — read local /proc directly (no SSH loopback)
|
// Digital Ocean server monitoring — read local /proc directly (no SSH loopback)
|
||||||
|
|
||||||
// CPU usage (sample over 200ms)
|
// CPU usage (sample over 200ms)
|
||||||
function getCpuPct(): float {
|
function getCpuPct(): float {
|
||||||
$s1 = file_get_contents("/proc/stat");
|
$s1 = file_get_contents("/proc/stat");
|
||||||
usleep(200000);
|
usleep(200000);
|
||||||
$s2 = file_get_contents("/proc/stat");
|
$s2 = file_get_contents("/proc/stat");
|
||||||
preg_match("/^cpu\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/m", $s1, $m1);
|
preg_match("/^cpu\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/m", $s1, $m1);
|
||||||
preg_match("/^cpu\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/m", $s2, $m2);
|
preg_match("/^cpu\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/m", $s2, $m2);
|
||||||
$idle1 = $m1[4]; $total1 = $m1[1]+$m1[2]+$m1[3]+$m1[4];
|
$idle1 = $m1[4]; $total1 = $m1[1]+$m1[2]+$m1[3]+$m1[4];
|
||||||
$idle2 = $m2[4]; $total2 = $m2[1]+$m2[2]+$m2[3]+$m2[4];
|
$idle2 = $m2[4]; $total2 = $m2[1]+$m2[2]+$m2[3]+$m2[4];
|
||||||
$dt = $total2 - $total1;
|
$dt = $total2 - $total1;
|
||||||
return $dt > 0 ? round((1 - ($idle2 - $idle1) / $dt) * 100, 1) : 0;
|
return $dt > 0 ? round((1 - ($idle2 - $idle1) / $dt) * 100, 1) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
$memLines = [];
|
$memLines = [];
|
||||||
foreach (file("/proc/meminfo") as $l) {
|
foreach (file("/proc/meminfo") as $l) {
|
||||||
[$k, $v] = explode(":", $l, 2) + [null, null];
|
[$k, $v] = explode(":", $l, 2) + [null, null];
|
||||||
if ($k) $memLines[trim($k)] = (int)trim($v);
|
if ($k) $memLines[trim($k)] = (int)trim($v);
|
||||||
}
|
}
|
||||||
$memTotal = $memLines["MemTotal"] ?? 0;
|
$memTotal = $memLines["MemTotal"] ?? 0;
|
||||||
$memFree = $memLines["MemAvailable"] ?? 0;
|
$memFree = $memLines["MemAvailable"] ?? 0;
|
||||||
$memUsed = $memTotal - $memFree;
|
$memUsed = $memTotal - $memFree;
|
||||||
|
|
||||||
$uptime = (int)explode(" ", file_get_contents("/proc/uptime"))[0];
|
$uptime = (int)explode(" ", file_get_contents("/proc/uptime"))[0];
|
||||||
$load = (float)explode(" ", file_get_contents("/proc/loadavg"))[0];
|
$load = (float)explode(" ", file_get_contents("/proc/loadavg"))[0];
|
||||||
|
|
||||||
$dfOut = shell_exec("df / | tail -1 | awk {print }") ?? "";
|
$dfOut = shell_exec("df / | tail -1 | awk {print }") ?? "";
|
||||||
$diskPct = trim($dfOut);
|
$diskPct = trim($dfOut);
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
$svcNames = ["nginx", "php8.3-fpm", "mariadb", "redis-server", "jarvis-arc", "jarvis-agent"];
|
$svcNames = ["nginx", "php8.3-fpm", "mariadb", "redis-server", "jarvis-arc", "jarvis-agent"];
|
||||||
$svcMap = [];
|
$svcMap = [];
|
||||||
foreach ($svcNames as $s) {
|
foreach ($svcNames as $s) {
|
||||||
$status = trim(shell_exec("systemctl is-active " . escapeshellarg($s) . " 2>/dev/null") ?? "");
|
$status = trim(shell_exec("systemctl is-active " . escapeshellarg($s) . " 2>/dev/null") ?? "");
|
||||||
if ($status === "active") $svcMap[$s] = true;
|
if ($status === "active") $svcMap[$s] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Site health from kb_facts
|
// Site health from kb_facts
|
||||||
$siteLabels = [
|
$siteLabels = [
|
||||||
"jarvis" => "jarvis.orbishosting.com:1972",
|
"jarvis" => "jarvis.orbishosting.com",
|
||||||
"tomsjavajive" => "tomsjavajive.com",
|
"tomsjavajive" => "tomsjavajive.com",
|
||||||
"epictravelexp"=> "epictravelexpeditions.com",
|
"epictravelexp"=> "epictravelexpeditions.com",
|
||||||
"parkersling" => "parkerslingshotrentals.com",
|
"parkersling" => "parkerslingshotrentals.com",
|
||||||
"orbishosting" => "orbishosting.com",
|
"orbishosting" => "orbishosting.com",
|
||||||
"orbisportal" => "orbis.orbishosting.com",
|
"orbisportal" => "orbis.orbishosting.com",
|
||||||
"tomtomgames" => "tomtomgames.com",
|
"tomtomgames" => "tomtomgames.com",
|
||||||
];
|
];
|
||||||
$sites = [];
|
$sites = [];
|
||||||
$rows = JarvisDB::query(
|
$rows = JarvisDB::query(
|
||||||
"SELECT fact_key, fact_value FROM kb_facts WHERE category='sites' AND updated_at > DATE_SUB(NOW(), INTERVAL 15 MINUTE) ORDER BY fact_key"
|
"SELECT fact_key, fact_value FROM kb_facts WHERE category='sites' AND updated_at > DATE_SUB(NOW(), INTERVAL 15 MINUTE) ORDER BY fact_key"
|
||||||
);
|
);
|
||||||
foreach ($rows as $r) {
|
foreach ($rows as $r) {
|
||||||
$label = $siteLabels[$r["fact_key"]] ?? $r["fact_key"];
|
$label = $siteLabels[$r["fact_key"]] ?? $r["fact_key"];
|
||||||
$sites[$label] = $r["fact_value"];
|
$sites[$label] = $r["fact_value"];
|
||||||
}
|
}
|
||||||
|
|
||||||
$uptimeDays = intdiv($uptime, 86400);
|
$uptimeDays = intdiv($uptime, 86400);
|
||||||
$uptimeHrs = intdiv($uptime % 86400, 3600);
|
$uptimeHrs = intdiv($uptime % 86400, 3600);
|
||||||
|
|
||||||
|
|
||||||
// DO server agent metrics (jarvis-do agent reporting via Tailscale)
|
// DO server agent metrics (jarvis-do agent reporting via Tailscale)
|
||||||
$doAgent = JarvisDB::query(
|
$doAgent = JarvisDB::query(
|
||||||
"SELECT metric_data FROM agent_metrics WHERE agent_id='jarvis-do_orbis' AND metric_type='system' ORDER BY recorded_at DESC LIMIT 1"
|
"SELECT metric_data FROM agent_metrics WHERE agent_id='jarvis-do_orbis' AND metric_type='system' ORDER BY recorded_at DESC LIMIT 1"
|
||||||
);
|
);
|
||||||
$doMet = [];
|
$doMet = [];
|
||||||
if (!empty($doAgent[0]['metric_data'])) {
|
if (!empty($doAgent[0]['metric_data'])) {
|
||||||
$dm = json_decode($doAgent[0]['metric_data'], true) ?? [];
|
$dm = json_decode($doAgent[0]['metric_data'], true) ?? [];
|
||||||
$doMet = [
|
$doMet = [
|
||||||
"cpu" => $dm['cpu_percent'] ?? 0,
|
"cpu" => $dm['cpu_percent'] ?? 0,
|
||||||
"mem" => $dm['memory']['percent'] ?? 0,
|
"mem" => $dm['memory']['percent'] ?? 0,
|
||||||
"disk" => (int)($dm['disk'][0]['percent'] ?? 0),
|
"disk" => (int)($dm['disk'][0]['percent'] ?? 0),
|
||||||
"uptime" => $dm['uptime']['human'] ?? "--",
|
"uptime" => $dm['uptime']['human'] ?? "--",
|
||||||
"online" => true,
|
"online" => true,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
"ip" => "10.48.200.211", // JARVIS VM (PVE1)
|
"ip" => "10.48.200.211", // JARVIS VM (PVE1)
|
||||||
"reachable" => true,
|
"reachable" => true,
|
||||||
"cpu_pct" => getCpuPct(),
|
"cpu_pct" => getCpuPct(),
|
||||||
"memory" => [
|
"memory" => [
|
||||||
"total_mb" => round($memTotal / 1024),
|
"total_mb" => round($memTotal / 1024),
|
||||||
"used_mb" => round($memUsed / 1024),
|
"used_mb" => round($memUsed / 1024),
|
||||||
"percent" => $memTotal > 0 ? round(($memUsed / $memTotal) * 100, 1) : 0,
|
"percent" => $memTotal > 0 ? round(($memUsed / $memTotal) * 100, 1) : 0,
|
||||||
],
|
],
|
||||||
"disk_used_pct" => $diskPct,
|
"disk_used_pct" => $diskPct,
|
||||||
"load_1m" => $load,
|
"load_1m" => $load,
|
||||||
"uptime" => "{$uptimeDays}d {$uptimeHrs}h",
|
"uptime" => "{$uptimeDays}d {$uptimeHrs}h",
|
||||||
"services" => $svcMap,
|
"services" => $svcMap,
|
||||||
"sites" => $sites,
|
"sites" => $sites,
|
||||||
"do_server" => $doMet,
|
"do_server" => $doMet,
|
||||||
"timestamp" => date("c"),
|
"timestamp" => date("c"),
|
||||||
]);
|
]);
|
||||||
|
|||||||
+256
-251
@@ -1,251 +1,256 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* JARVIS Facts Collector
|
* JARVIS Facts Collector
|
||||||
* HTTP endpoint: /api/facts/collect (POST or GET)
|
* HTTP endpoint: /api/facts/collect (POST or GET)
|
||||||
* CLI/cron: php facts_collector.php
|
* CLI/cron: php facts_collector.php
|
||||||
* Gathers live system, network, Proxmox, HA, and Ollama facts → kb_facts table.
|
* Gathers live system, network, Proxmox, HA, and Ollama facts → kb_facts table.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
$isCLI = (php_sapi_name() === 'cli' || php_sapi_name() === 'litespeed');
|
$isCLI = (php_sapi_name() === 'cli' || php_sapi_name() === 'litespeed');
|
||||||
|
|
||||||
// Bootstrap: load if not already available (HTTP via api.php loads these; CLI/lsphp/cron must load manually)
|
// Bootstrap: load if not already available (HTTP via api.php loads these; CLI/lsphp/cron must load manually)
|
||||||
if (!class_exists('KBEngine')) {
|
if (!class_exists('KBEngine')) {
|
||||||
require_once __DIR__ . '/../config.php';
|
require_once __DIR__ . '/../config.php';
|
||||||
require_once __DIR__ . '/../lib/db.php';
|
require_once __DIR__ . '/../lib/db.php';
|
||||||
require_once __DIR__ . '/../lib/kb_engine.php';
|
require_once __DIR__ . '/../lib/kb_engine.php';
|
||||||
}
|
}
|
||||||
|
|
||||||
function collect_all(): array {
|
function collect_all(): array {
|
||||||
$results = [];
|
$results = [];
|
||||||
$ttl = 300; // 5-minute TTL on live facts
|
$ttl = 300; // 5-minute TTL on live facts
|
||||||
|
|
||||||
// Returns true if a fact category has been updated within $secs seconds.
|
// Returns true if a fact category has been updated within $secs seconds.
|
||||||
// Prevents expensive external calls when data is still fresh.
|
// Prevents expensive external calls when data is still fresh.
|
||||||
$fresh = function(string $cat, int $secs): bool {
|
// Comparison is done entirely in SQL (via NOW()) rather than PHP's time()/strtotime()
|
||||||
$row = JarvisDB::query(
|
// — this file's config.php sets date_default_timezone_set('America/Chicago'), which
|
||||||
'SELECT updated_at FROM kb_facts WHERE category=? ORDER BY updated_at DESC LIMIT 1',
|
// makes strtotime() misinterpret MySQL's naive (UTC) datetime strings as being in
|
||||||
[$cat]
|
// Chicago time, throwing every freshness check off by the UTC offset (previously
|
||||||
);
|
// caused "sites" to always look artificially fresh and never actually refresh).
|
||||||
if (empty($row[0]['updated_at'])) return false;
|
$fresh = function(string $cat, int $secs): bool {
|
||||||
return (time() - strtotime($row[0]['updated_at'])) < $secs;
|
$row = JarvisDB::query(
|
||||||
};
|
'SELECT (updated_at > DATE_SUB(NOW(), INTERVAL ? SECOND)) AS is_fresh FROM kb_facts WHERE category=? ORDER BY updated_at DESC LIMIT 1',
|
||||||
|
[$secs, $cat]
|
||||||
|
);
|
||||||
// ── System ────────────────────────────────────────────────────────────
|
if (empty($row)) return false;
|
||||||
try {
|
return (bool) $row[0]['is_fresh'];
|
||||||
$stat1 = file_get_contents('/proc/stat');
|
};
|
||||||
usleep(200000);
|
|
||||||
$stat2 = file_get_contents('/proc/stat');
|
|
||||||
|
// ── System ────────────────────────────────────────────────────────────
|
||||||
$cpu1 = sscanf(explode("\n", $stat1)[0], "cpu %d %d %d %d %d %d %d");
|
try {
|
||||||
$cpu2 = sscanf(explode("\n", $stat2)[0], "cpu %d %d %d %d %d %d %d");
|
$stat1 = file_get_contents('/proc/stat');
|
||||||
$dIdle = $cpu2[3] - $cpu1[3];
|
usleep(200000);
|
||||||
$dTotal = array_sum($cpu2) - array_sum($cpu1);
|
$stat2 = file_get_contents('/proc/stat');
|
||||||
$cpuPct = $dTotal > 0 ? round(($dTotal - $dIdle) / $dTotal * 100, 1) : 0;
|
|
||||||
KBEngine::storeFact('system', 'cpu_usage', $cpuPct, 'local', $ttl);
|
$cpu1 = sscanf(explode("\n", $stat1)[0], "cpu %d %d %d %d %d %d %d");
|
||||||
|
$cpu2 = sscanf(explode("\n", $stat2)[0], "cpu %d %d %d %d %d %d %d");
|
||||||
$memLines = file('/proc/meminfo');
|
$dIdle = $cpu2[3] - $cpu1[3];
|
||||||
$mem = [];
|
$dTotal = array_sum($cpu2) - array_sum($cpu1);
|
||||||
foreach ($memLines as $l) {
|
$cpuPct = $dTotal > 0 ? round(($dTotal - $dIdle) / $dTotal * 100, 1) : 0;
|
||||||
if (preg_match('/^(\w+):\s+(\d+)/', $l, $m)) $mem[$m[1]] = (int)$m[2];
|
KBEngine::storeFact('system', 'cpu_usage', $cpuPct, 'local', $ttl);
|
||||||
}
|
|
||||||
$total = round($mem['MemTotal'] / 1048576, 1);
|
$memLines = file('/proc/meminfo');
|
||||||
$avail = round($mem['MemAvailable'] / 1048576, 1);
|
$mem = [];
|
||||||
$used = round($total - $avail, 1);
|
foreach ($memLines as $l) {
|
||||||
$free = round($mem['MemFree'] / 1048576, 1);
|
if (preg_match('/^(\w+):\s+(\d+)/', $l, $m)) $mem[$m[1]] = (int)$m[2];
|
||||||
$memPct = $total > 0 ? round($used / $total * 100) : 0;
|
}
|
||||||
KBEngine::storeFact('system', 'mem_total_gb', $total, 'local', $ttl);
|
$total = round($mem['MemTotal'] / 1048576, 1);
|
||||||
KBEngine::storeFact('system', 'mem_used_gb', $used, 'local', $ttl);
|
$avail = round($mem['MemAvailable'] / 1048576, 1);
|
||||||
KBEngine::storeFact('system', 'mem_free_gb', $free, 'local', $ttl);
|
$used = round($total - $avail, 1);
|
||||||
KBEngine::storeFact('system', 'mem_percent', $memPct, 'local', $ttl);
|
$free = round($mem['MemFree'] / 1048576, 1);
|
||||||
|
$memPct = $total > 0 ? round($used / $total * 100) : 0;
|
||||||
$la = explode(' ', file_get_contents('/proc/loadavg'));
|
KBEngine::storeFact('system', 'mem_total_gb', $total, 'local', $ttl);
|
||||||
KBEngine::storeFact('system', 'load_1m', $la[0], 'local', $ttl);
|
KBEngine::storeFact('system', 'mem_used_gb', $used, 'local', $ttl);
|
||||||
KBEngine::storeFact('system', 'load_5m', $la[1], 'local', $ttl);
|
KBEngine::storeFact('system', 'mem_free_gb', $free, 'local', $ttl);
|
||||||
KBEngine::storeFact('system', 'load_15m', $la[2], 'local', $ttl);
|
KBEngine::storeFact('system', 'mem_percent', $memPct, 'local', $ttl);
|
||||||
|
|
||||||
$sec = (int) file_get_contents('/proc/uptime');
|
$la = explode(' ', file_get_contents('/proc/loadavg'));
|
||||||
KBEngine::storeFact('system', 'uptime',
|
KBEngine::storeFact('system', 'load_1m', $la[0], 'local', $ttl);
|
||||||
intdiv($sec, 86400) . ' days, ' . intdiv($sec % 86400, 3600) . ' hours',
|
KBEngine::storeFact('system', 'load_5m', $la[1], 'local', $ttl);
|
||||||
'local', $ttl);
|
KBEngine::storeFact('system', 'load_15m', $la[2], 'local', $ttl);
|
||||||
|
|
||||||
$df = disk_free_space('/');
|
$sec = (int) file_get_contents('/proc/uptime');
|
||||||
$dt = disk_total_space('/');
|
KBEngine::storeFact('system', 'uptime',
|
||||||
KBEngine::storeFact('system', 'disk_total', round($dt / 1073741824, 1) . 'GB', 'local', $ttl);
|
intdiv($sec, 86400) . ' days, ' . intdiv($sec % 86400, 3600) . ' hours',
|
||||||
KBEngine::storeFact('system', 'disk_used', round(($dt - $df) / 1073741824, 1) . 'GB', 'local', $ttl);
|
'local', $ttl);
|
||||||
KBEngine::storeFact('system', 'disk_free', round($df / 1073741824, 1) . 'GB', 'local', $ttl);
|
|
||||||
|
$df = disk_free_space('/');
|
||||||
$results['system'] = "ok (CPU {$cpuPct}%, MEM {$memPct}%)";
|
$dt = disk_total_space('/');
|
||||||
} catch (Exception $e) {
|
KBEngine::storeFact('system', 'disk_total', round($dt / 1073741824, 1) . 'GB', 'local', $ttl);
|
||||||
$results['system'] = 'error: ' . $e->getMessage();
|
KBEngine::storeFact('system', 'disk_used', round(($dt - $df) / 1073741824, 1) . 'GB', 'local', $ttl);
|
||||||
}
|
KBEngine::storeFact('system', 'disk_free', round($df / 1073741824, 1) . 'GB', 'local', $ttl);
|
||||||
|
|
||||||
// ── Network — read from agent DB (agents push status, DO can't ping LAN IPs) ──
|
$results['system'] = "ok (CPU {$cpuPct}%, MEM {$memPct}%)";
|
||||||
try {
|
} catch (Exception $e) {
|
||||||
$rows = JarvisDB::query(
|
$results['system'] = 'error: ' . $e->getMessage();
|
||||||
"SELECT status FROM registered_agents WHERE last_seen > DATE_SUB(NOW(), INTERVAL 5 MINUTE)"
|
}
|
||||||
);
|
|
||||||
$online = count(array_filter($rows, fn($r) => $r['status'] === 'online'));
|
// ── Network — read from agent DB (agents push status, DO can't ping LAN IPs) ──
|
||||||
$total = count($rows);
|
try {
|
||||||
KBEngine::storeFact('network', 'online_count', $online, 'local', $ttl);
|
$rows = JarvisDB::query(
|
||||||
KBEngine::storeFact('network', 'total_count', $total, 'local', $ttl);
|
"SELECT status FROM registered_agents WHERE last_seen > DATE_SUB(NOW(), INTERVAL 5 MINUTE)"
|
||||||
KBEngine::storeFact('network', 'gateway_status', $online > 0 ? 'online' : 'offline', 'local', $ttl);
|
);
|
||||||
$results['network'] = "ok ({$online}/{$total} online)";
|
$online = count(array_filter($rows, fn($r) => $r['status'] === 'online'));
|
||||||
} catch (Exception $e) {
|
$total = count($rows);
|
||||||
$results['network'] = 'error: ' . $e->getMessage();
|
KBEngine::storeFact('network', 'online_count', $online, 'local', $ttl);
|
||||||
}
|
KBEngine::storeFact('network', 'total_count', $total, 'local', $ttl);
|
||||||
|
KBEngine::storeFact('network', 'gateway_status', $online > 0 ? 'online' : 'offline', 'local', $ttl);
|
||||||
// ── Proxmox (TTL 10 min) ─────────────────────────────────────────────
|
$results['network'] = "ok ({$online}/{$total} online)";
|
||||||
if ($fresh('proxmox', 600)) {
|
} catch (Exception $e) {
|
||||||
$results['proxmox'] = 'skipped (fresh)';
|
$results['network'] = 'error: ' . $e->getMessage();
|
||||||
} else try {
|
}
|
||||||
if (defined('PROXMOX_TOKEN_ID') && PROXMOX_TOKEN_ID) {
|
|
||||||
$base = 'https://10.48.200.90:' . PROXMOX_PORT . '/api2/json';
|
// ── Proxmox (TTL 10 min) ─────────────────────────────────────────────
|
||||||
$auth = 'Authorization: PVEAPIToken=' . PROXMOX_USER . '!' . PROXMOX_TOKEN_ID . '=' . PROXMOX_TOKEN_VAL;
|
if ($fresh('proxmox', 600)) {
|
||||||
|
$results['proxmox'] = 'skipped (fresh)';
|
||||||
$nd = pve_api_get("{$base}/nodes/" . PROXMOX_NODE . "/status", $auth);
|
} else try {
|
||||||
$vms = pve_api_get("{$base}/nodes/" . PROXMOX_NODE . "/qemu", $auth);
|
if (defined('PROXMOX_TOKEN_ID') && PROXMOX_TOKEN_ID) {
|
||||||
$cts = pve_api_get("{$base}/nodes/" . PROXMOX_NODE . "/lxc", $auth);
|
$base = 'https://10.48.200.90:' . PROXMOX_PORT . '/api2/json';
|
||||||
|
$auth = 'Authorization: PVEAPIToken=' . PROXMOX_USER . '!' . PROXMOX_TOKEN_ID . '=' . PROXMOX_TOKEN_VAL;
|
||||||
if (isset($nd['data'])) {
|
|
||||||
$cpuPct = round(($nd['data']['cpu'] ?? 0) * 100, 1);
|
$nd = pve_api_get("{$base}/nodes/" . PROXMOX_NODE . "/status", $auth);
|
||||||
$memU = round(($nd['data']['memory']['used'] ?? 0) / 1073741824, 1);
|
$vms = pve_api_get("{$base}/nodes/" . PROXMOX_NODE . "/qemu", $auth);
|
||||||
$memT = round(($nd['data']['memory']['total'] ?? 0) / 1073741824, 1);
|
$cts = pve_api_get("{$base}/nodes/" . PROXMOX_NODE . "/lxc", $auth);
|
||||||
$memPct = $memT > 0 ? round($memU / $memT * 100) : 0;
|
|
||||||
KBEngine::storeFact('proxmox', 'pve_cpu_percent', $cpuPct, PROXMOX_HOST, $ttl);
|
if (isset($nd['data'])) {
|
||||||
KBEngine::storeFact('proxmox', 'pve_mem_used_gb', $memU, PROXMOX_HOST, $ttl);
|
$cpuPct = round(($nd['data']['cpu'] ?? 0) * 100, 1);
|
||||||
KBEngine::storeFact('proxmox', 'pve_mem_total_gb', $memT, PROXMOX_HOST, $ttl);
|
$memU = round(($nd['data']['memory']['used'] ?? 0) / 1073741824, 1);
|
||||||
KBEngine::storeFact('proxmox', 'pve_mem_percent', $memPct, PROXMOX_HOST, $ttl);
|
$memT = round(($nd['data']['memory']['total'] ?? 0) / 1073741824, 1);
|
||||||
}
|
$memPct = $memT > 0 ? round($memU / $memT * 100) : 0;
|
||||||
$all = array_merge($vms['data'] ?? [], $cts['data'] ?? []);
|
KBEngine::storeFact('proxmox', 'pve_cpu_percent', $cpuPct, PROXMOX_HOST, $ttl);
|
||||||
$running = count(array_filter($all, fn($v) => ($v['status'] ?? '') === 'running'));
|
KBEngine::storeFact('proxmox', 'pve_mem_used_gb', $memU, PROXMOX_HOST, $ttl);
|
||||||
KBEngine::storeFact('proxmox', 'vm_total', count($all), PROXMOX_HOST, $ttl);
|
KBEngine::storeFact('proxmox', 'pve_mem_total_gb', $memT, PROXMOX_HOST, $ttl);
|
||||||
KBEngine::storeFact('proxmox', 'vm_running', $running, PROXMOX_HOST, $ttl);
|
KBEngine::storeFact('proxmox', 'pve_mem_percent', $memPct, PROXMOX_HOST, $ttl);
|
||||||
$results['proxmox'] = "ok ({$running}/" . count($all) . " running)";
|
}
|
||||||
} else {
|
$all = array_merge($vms['data'] ?? [], $cts['data'] ?? []);
|
||||||
$results['proxmox'] = 'skipped (no token)';
|
$running = count(array_filter($all, fn($v) => ($v['status'] ?? '') === 'running'));
|
||||||
}
|
KBEngine::storeFact('proxmox', 'vm_total', count($all), PROXMOX_HOST, $ttl);
|
||||||
} catch (Exception $e) {
|
KBEngine::storeFact('proxmox', 'vm_running', $running, PROXMOX_HOST, $ttl);
|
||||||
$results['proxmox'] = 'error: ' . $e->getMessage();
|
$results['proxmox'] = "ok ({$running}/" . count($all) . " running)";
|
||||||
}
|
} else {
|
||||||
|
$results['proxmox'] = 'skipped (no token)';
|
||||||
// ── Home Assistant — skipped (HA agent pushes entities every 30s) ────
|
}
|
||||||
$results['ha'] = 'skipped (agent push active)';
|
} catch (Exception $e) {
|
||||||
|
$results['proxmox'] = 'error: ' . $e->getMessage();
|
||||||
|
}
|
||||||
// ── Digital Ocean ─────────────────────────────────────────────────────
|
|
||||||
try {
|
// ── Home Assistant — skipped (HA agent pushes entities every 30s) ────
|
||||||
exec("ping -c1 -W1 165.22.1.228 > /dev/null 2>&1", $o2, $doCode);;
|
$results['ha'] = 'skipped (agent push active)';
|
||||||
$doStatus = ($doCode === 0) ? 'online' : 'unreachable';
|
|
||||||
KBEngine::storeFact('do_server', 'do_status', $doStatus, '165.22.1.228', $ttl);
|
|
||||||
$results['do_server'] = "ok ({$doStatus})";
|
// ── Digital Ocean ─────────────────────────────────────────────────────
|
||||||
} catch (Exception $e) {
|
try {
|
||||||
$results['do_server'] = 'error: ' . $e->getMessage();
|
exec("ping -c1 -W1 165.22.1.228 > /dev/null 2>&1", $o2, $doCode);;
|
||||||
}
|
$doStatus = ($doCode === 0) ? 'online' : 'unreachable';
|
||||||
|
KBEngine::storeFact('do_server', 'do_status', $doStatus, '165.22.1.228', $ttl);
|
||||||
// ── Ollama (TTL 15 min) ───────────────────────────────────────────────
|
$results['do_server'] = "ok ({$doStatus})";
|
||||||
if ($fresh('ollama', 900)) {
|
} catch (Exception $e) {
|
||||||
$results['ollama'] = 'skipped (fresh)';
|
$results['do_server'] = 'error: ' . $e->getMessage();
|
||||||
} else try {
|
}
|
||||||
$ollamaHost = defined('OLLAMA_HOST') ? OLLAMA_HOST : 'http://10.48.200.95:11434';
|
|
||||||
$ch = curl_init($ollamaHost . '/api/tags');
|
// ── Ollama (TTL 15 min) ───────────────────────────────────────────────
|
||||||
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_CONNECTTIMEOUT => 2, CURLOPT_TIMEOUT => 3]);
|
if ($fresh('ollama', 900)) {
|
||||||
$resp = curl_exec($ch);
|
$results['ollama'] = 'skipped (fresh)';
|
||||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
} else try {
|
||||||
curl_close($ch);
|
$ollamaHost = defined('OLLAMA_HOST') ? OLLAMA_HOST : 'http://10.48.200.95:11434';
|
||||||
|
$ch = curl_init($ollamaHost . '/api/tags');
|
||||||
if ($code === 200) {
|
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_CONNECTTIMEOUT => 2, CURLOPT_TIMEOUT => 3]);
|
||||||
$models = json_decode($resp, true)['models'] ?? [];
|
$resp = curl_exec($ch);
|
||||||
$names = array_column($models, 'name');
|
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
KBEngine::storeFact('ollama', 'available_models', implode(', ', $names) ?: 'none', 'proxmox', null);
|
curl_close($ch);
|
||||||
KBEngine::storeFact('ollama', 'model_count', count($names), 'proxmox', $ttl);
|
|
||||||
KBEngine::storeFact('ollama', 'status', 'online', 'proxmox', $ttl);
|
if ($code === 200) {
|
||||||
foreach ($models as $m) {
|
$models = json_decode($resp, true)['models'] ?? [];
|
||||||
JarvisDB::execute(
|
$names = array_column($models, 'name');
|
||||||
'INSERT INTO kb_ollama_models (model_name, size_gb) VALUES (?,?)
|
KBEngine::storeFact('ollama', 'available_models', implode(', ', $names) ?: 'none', 'proxmox', null);
|
||||||
ON DUPLICATE KEY UPDATE size_gb=VALUES(size_gb), pulled_at=NOW()',
|
KBEngine::storeFact('ollama', 'model_count', count($names), 'proxmox', $ttl);
|
||||||
[$m['name'], round(($m['size'] ?? 0) / 1073741824, 1)]
|
KBEngine::storeFact('ollama', 'status', 'online', 'proxmox', $ttl);
|
||||||
);
|
foreach ($models as $m) {
|
||||||
}
|
JarvisDB::execute(
|
||||||
$results['ollama'] = 'ok (' . (implode(', ', $names) ?: 'no models yet') . ')';
|
'INSERT INTO kb_ollama_models (model_name, size_gb) VALUES (?,?)
|
||||||
} else {
|
ON DUPLICATE KEY UPDATE size_gb=VALUES(size_gb), pulled_at=NOW()',
|
||||||
KBEngine::storeFact('ollama', 'status', 'offline', 'proxmox', $ttl);
|
[$m['name'], round(($m['size'] ?? 0) / 1073741824, 1)]
|
||||||
$results['ollama'] = 'unreachable (VM may be booting)';
|
);
|
||||||
}
|
}
|
||||||
} catch (Exception $e) {
|
$results['ollama'] = 'ok (' . (implode(', ', $names) ?: 'no models yet') . ')';
|
||||||
$results['ollama'] = 'error: ' . $e->getMessage();
|
} else {
|
||||||
}
|
KBEngine::storeFact('ollama', 'status', 'offline', 'proxmox', $ttl);
|
||||||
|
$results['ollama'] = 'unreachable (VM may be booting)';
|
||||||
// ── Site Health (TTL 5 min) ───────────────────────────────────────────
|
}
|
||||||
if ($fresh('sites', 300)) {
|
} catch (Exception $e) {
|
||||||
$results['sites'] = 'skipped (fresh)';
|
$results['ollama'] = 'error: ' . $e->getMessage();
|
||||||
} else try {
|
}
|
||||||
$sites = [
|
|
||||||
"jarvis" => "http://127.0.0.1",
|
// ── Site Health (TTL 5 min) ───────────────────────────────────────────
|
||||||
'tomsjavajive' => 'https://tomsjavajive.com',
|
if ($fresh('sites', 300)) {
|
||||||
'epictravelexp'=> 'https://epictravelexpeditions.com',
|
$results['sites'] = 'skipped (fresh)';
|
||||||
'parkerslingshotrentals' => 'https://parkerslingshotrentals.com',
|
} else try {
|
||||||
'orbishosting' => 'https://orbishosting.com',
|
$sites = [
|
||||||
'orbisportal' => 'https://orbis.orbishosting.com',
|
"jarvis" => "http://127.0.0.1",
|
||||||
'tomtomgames' => 'https://tomtomgames.com',
|
'tomsjavajive' => 'https://tomsjavajive.com',
|
||||||
];
|
'epictravelexp'=> 'https://epictravelexpeditions.com',
|
||||||
$down = [];
|
'parkerslingshotrentals' => 'https://parkerslingshotrentals.com',
|
||||||
foreach ($sites as $key => $url) {
|
'orbishosting' => 'https://orbishosting.com',
|
||||||
$parsed = parse_url($url);
|
'orbisportal' => 'https://orbis.orbishosting.com',
|
||||||
$host = $parsed['host'] ?? $url;
|
'tomtomgames' => 'https://tomtomgames.com',
|
||||||
// Check sites on the local server directly to avoid Cloudflare CDN timeouts.
|
];
|
||||||
// All JARVIS-hosted sites are served from this same OLS instance.
|
$down = [];
|
||||||
$localUrl = $url; // external check
|
foreach ($sites as $key => $url) {
|
||||||
$ch = curl_init($localUrl);
|
$parsed = parse_url($url);
|
||||||
curl_setopt_array($ch, [
|
$host = $parsed['host'] ?? $url;
|
||||||
CURLOPT_RETURNTRANSFER => true,
|
// Check sites on the local server directly to avoid Cloudflare CDN timeouts.
|
||||||
CURLOPT_CONNECTTIMEOUT => 3,
|
// All JARVIS-hosted sites are served from this same OLS instance.
|
||||||
CURLOPT_FOLLOWLOCATION => true,
|
$localUrl = $url; // external check
|
||||||
CURLOPT_TIMEOUT => 10,
|
$ch = curl_init($localUrl);
|
||||||
CURLOPT_CONNECTTIMEOUT => 5,
|
curl_setopt_array($ch, [
|
||||||
CURLOPT_NOBODY => true,
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
]);
|
CURLOPT_CONNECTTIMEOUT => 3,
|
||||||
curl_exec($ch);
|
CURLOPT_FOLLOWLOCATION => true,
|
||||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
CURLOPT_TIMEOUT => 10,
|
||||||
curl_close($ch);
|
CURLOPT_CONNECTTIMEOUT => 5,
|
||||||
$status = ($code >= 200 && $code < 400) ? 'up' : "down-$code";
|
CURLOPT_NOBODY => true,
|
||||||
KBEngine::storeFact('sites', $key, $status, $url, 180);
|
]);
|
||||||
if ($status !== 'up') $down[] = "$key($code)";
|
curl_exec($ch);
|
||||||
}
|
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
$results['sites'] = empty($down) ? 'all up' : 'DOWN: ' . implode(', ', $down);
|
curl_close($ch);
|
||||||
} catch (Exception $e) {
|
$status = ($code >= 200 && $code < 400) ? 'up' : "down-$code";
|
||||||
$results['sites'] = 'error: ' . $e->getMessage();
|
KBEngine::storeFact('sites', $key, $status, $url, 180);
|
||||||
}
|
if ($status !== 'up') $down[] = "$key($code)";
|
||||||
|
}
|
||||||
|
$results['sites'] = empty($down) ? 'all up' : 'DOWN: ' . implode(', ', $down);
|
||||||
// Network device scan is handled by PVE1 cron (/usr/local/bin/jarvis-netscan.sh)
|
} catch (Exception $e) {
|
||||||
// which POSTs nmap results to /api/netscan every 3 minutes.
|
$results['sites'] = 'error: ' . $e->getMessage();
|
||||||
$results['nmap_scan'] = 'handled by PVE1 push (jarvis-netscan.sh)';
|
}
|
||||||
|
|
||||||
return $results;
|
|
||||||
}
|
// Network device scan is handled by PVE1 cron (/usr/local/bin/jarvis-netscan.sh)
|
||||||
|
// which POSTs nmap results to /api/netscan every 3 minutes.
|
||||||
function pve_api_get(string $url, string $authHeader): array {
|
$results['nmap_scan'] = 'handled by PVE1 push (jarvis-netscan.sh)';
|
||||||
$ch = curl_init($url);
|
|
||||||
curl_setopt_array($ch, [
|
return $results;
|
||||||
CURLOPT_RETURNTRANSFER => true,
|
}
|
||||||
CURLOPT_CONNECTTIMEOUT => 3,
|
|
||||||
CURLOPT_HTTPHEADER => [$authHeader],
|
function pve_api_get(string $url, string $authHeader): array {
|
||||||
CURLOPT_SSL_VERIFYPEER => false,
|
$ch = curl_init($url);
|
||||||
CURLOPT_TIMEOUT => 5,
|
curl_setopt_array($ch, [
|
||||||
]);
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
$resp = curl_exec($ch);
|
CURLOPT_CONNECTTIMEOUT => 3,
|
||||||
curl_close($ch);
|
CURLOPT_HTTPHEADER => [$authHeader],
|
||||||
return $resp ? (json_decode($resp, true) ?? []) : [];
|
CURLOPT_SSL_VERIFYPEER => false,
|
||||||
}
|
CURLOPT_TIMEOUT => 5,
|
||||||
|
]);
|
||||||
// ── Entry point ───────────────────────────────────────────────────────────
|
$resp = curl_exec($ch);
|
||||||
$results = collect_all();
|
curl_close($ch);
|
||||||
if ($isCLI) {
|
return $resp ? (json_decode($resp, true) ?? []) : [];
|
||||||
echo date('Y-m-d H:i:s') . " JARVIS facts collected:\n";
|
}
|
||||||
foreach ($results as $k => $v) {
|
|
||||||
echo " {$k}: {$v}\n";
|
// ── Entry point ───────────────────────────────────────────────────────────
|
||||||
}
|
$results = collect_all();
|
||||||
} else {
|
if ($isCLI) {
|
||||||
echo json_encode(['status' => 'ok', 'results' => $results, 'timestamp' => date('c')]);
|
echo date('Y-m-d H:i:s') . " JARVIS facts collected:\n";
|
||||||
}
|
foreach ($results as $k => $v) {
|
||||||
|
echo " {$k}: {$v}\n";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo json_encode(['status' => 'ok', 'results' => $results, 'timestamp' => date('c')]);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,329 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* JARVIS KB Intent Generator
|
||||||
|
* Generates 1,000+ educational KB intents via Groq LLM and imports to kb_intents table.
|
||||||
|
* Also runs a cleanup pass: deduplication, short-response pruning, pattern normalisation.
|
||||||
|
*
|
||||||
|
* CLI / cron: /usr/bin/php8.3 /var/www/jarvis/api/endpoints/kb_intent_generator.php
|
||||||
|
* Schedule: Daily – 3 am
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../config.php';
|
||||||
|
require_once __DIR__ . '/../lib/db.php';
|
||||||
|
|
||||||
|
/* ── helpers ── */
|
||||||
|
function ts(): string { return '[' . date('Y-m-d H:i:s') . ']'; }
|
||||||
|
function log_line(string $msg): void { echo ts() . ' KB Intent Generator: ' . $msg . "\n"; flush(); }
|
||||||
|
|
||||||
|
function groq(string $system, string $user, int $max = 3000, int $retries = 2): ?string {
|
||||||
|
for ($attempt = 0; $attempt <= $retries; $attempt++) {
|
||||||
|
if ($attempt > 0) {
|
||||||
|
log_line(" Retry {$attempt}/{$retries} after rate-limit pause...");
|
||||||
|
sleep(25);
|
||||||
|
}
|
||||||
|
$ch = curl_init('https://api.groq.com/openai/v1/chat/completions');
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_POST => true,
|
||||||
|
CURLOPT_TIMEOUT => 60,
|
||||||
|
CURLOPT_HTTPHEADER => [
|
||||||
|
'Authorization: Bearer ' . GROQ_API_KEY,
|
||||||
|
'Content-Type: application/json',
|
||||||
|
],
|
||||||
|
CURLOPT_POSTFIELDS => json_encode([
|
||||||
|
'model' => 'llama-3.3-70b-versatile',
|
||||||
|
'max_tokens' => $max,
|
||||||
|
'temperature' => 0.7,
|
||||||
|
'messages' => [
|
||||||
|
['role' => 'system', 'content' => $system],
|
||||||
|
['role' => 'user', 'content' => $user],
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
$raw = curl_exec($ch);
|
||||||
|
$err = curl_error($ch);
|
||||||
|
$info = curl_getinfo($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($err || !$raw) continue;
|
||||||
|
|
||||||
|
// Check for rate limit response (429)
|
||||||
|
if (($info['http_code'] ?? 0) === 429) continue;
|
||||||
|
|
||||||
|
$d = json_decode($raw, true);
|
||||||
|
$content = $d['choices'][0]['message']['content'] ?? null;
|
||||||
|
if ($content !== null) return $content;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── normalize a pattern to valid PHP PCRE ── */
|
||||||
|
function normalize_pattern(string $pat): string {
|
||||||
|
$pat = trim($pat);
|
||||||
|
// If pattern already has PCRE delimiters, leave it alone
|
||||||
|
if (preg_match('/^[\/|~#!@%]/', $pat)) return $pat;
|
||||||
|
// Strip any (?i) inline flag — we'll add /i at the delimiter level
|
||||||
|
$pat = preg_replace('/^\(\?i\)/', '', $pat);
|
||||||
|
// Escape forward slashes inside the pattern
|
||||||
|
$pat = str_replace('/', '\\/', $pat);
|
||||||
|
return '/' . $pat . '/i';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── run guard: skip if ran within last 4 hours ── */
|
||||||
|
/* Set JARVIS_FORCE_RUN=1 (env) or pass --force (argv) to bypass */
|
||||||
|
/* Comparison done in SQL (NOW()) rather than PHP time()/strtotime() — this process's
|
||||||
|
date_default_timezone_set('America/Chicago') makes strtotime() misread MySQL's naive
|
||||||
|
(UTC) timestamps as Chicago time, throwing elapsed-time checks off by the UTC offset. */
|
||||||
|
$recentRun = JarvisDB::single(
|
||||||
|
"SELECT (updated_at > DATE_SUB(NOW(), INTERVAL 14400 SECOND)) AS is_recent FROM kb_facts WHERE category='kb_generator' AND fact_key='last_run'"
|
||||||
|
);
|
||||||
|
$forceRun = !empty(getenv('JARVIS_FORCE_RUN')) || (isset($argv[1]) && $argv[1] === '--force');
|
||||||
|
if (!$forceRun && $recentRun && $recentRun['is_recent']) {
|
||||||
|
log_line('Skipping – ran within last 4 hours. Use --force to override.');
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
if ($forceRun) log_line('Force-run flag set — bypassing 4-hour guard.');
|
||||||
|
|
||||||
|
log_line('Starting daily KB intent generation run.');
|
||||||
|
|
||||||
|
/* ── load active topics from database ── */
|
||||||
|
$BATCHES = JarvisDB::query(
|
||||||
|
"SELECT t.topic_id AS id, t.category, t.topic_name AS topic, t.description AS `desc`
|
||||||
|
FROM kb_generator_topics t WHERE t.active=1 ORDER BY t.id ASC"
|
||||||
|
);
|
||||||
|
if (empty($BATCHES)) {
|
||||||
|
log_line('ERROR: No active topics in kb_generator_topics table. Add topics via the JARVIS admin panel.');
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
log_line('Loaded ' . count($BATCHES) . ' active topics from database.');
|
||||||
|
|
||||||
|
/* ── ROTATION ENGINE: process BATCH_SIZE topics per run, cycling through all ── */
|
||||||
|
define('BATCH_SIZE', 25);
|
||||||
|
$totalTopics = count($BATCHES);
|
||||||
|
$offsetRow = JarvisDB::single(
|
||||||
|
"SELECT CAST(fact_value AS SIGNED) AS v FROM kb_facts WHERE category='kb_generator' AND fact_key='batch_offset'"
|
||||||
|
);
|
||||||
|
$batchOffset = max(0, (int)($offsetRow['v'] ?? 0));
|
||||||
|
if ($batchOffset >= $totalTopics) $batchOffset = 0;
|
||||||
|
$nextOffset = ($batchOffset + BATCH_SIZE) % $totalTopics;
|
||||||
|
$cycleComplete = ($batchOffset + BATCH_SIZE) >= $totalTopics;
|
||||||
|
$cycleLen = (int)ceil($totalTopics / BATCH_SIZE);
|
||||||
|
|
||||||
|
$runBatches = [];
|
||||||
|
for ($i = 0; $i < BATCH_SIZE; $i++) {
|
||||||
|
$runBatches[] = $BATCHES[($batchOffset + $i) % $totalTopics];
|
||||||
|
}
|
||||||
|
$endIdx = ($batchOffset + BATCH_SIZE - 1) % $totalTopics;
|
||||||
|
log_line("Rotation: topics " . ($batchOffset + 1) . "–" . ($endIdx + 1) . " of {$totalTopics} | cycle = {$cycleLen} runs × 6h = " . ($cycleLen * 6) . "h full cycle.");
|
||||||
|
if ($cycleComplete) log_line(" ↻ Full cycle complete — restarting from topic 1 next run.");
|
||||||
|
|
||||||
|
/* ── system prompt ── */
|
||||||
|
/* RESTORED 2026-07-07: this and safe_insert() below were lost during the 2026-07-05 rotation-engine
|
||||||
|
refactor (moving from a hardcoded $BATCHES array to the kb_generator_topics table). Their absence
|
||||||
|
caused an uncaught TypeError on every run since (undefined $SYSTEM passed to groq()'s non-nullable
|
||||||
|
string param), silently swallowed by config.php's error_reporting(0) — the cron looked like it was
|
||||||
|
running fine but died instantly on batch 1 every single time. Restored from kb_intent_generator.php.bak2,
|
||||||
|
with the intent count adjusted from 40 to 20 to match this version's actual per-topic request below. */
|
||||||
|
$SYSTEM = <<<'SYS'
|
||||||
|
You are an expert educator generating KB (knowledge-base) intents for an AI assistant called JARVIS.
|
||||||
|
Each intent is a question/phrase a student might ask, paired with a clear educational answer.
|
||||||
|
|
||||||
|
Respond ONLY with a valid JSON array (no markdown, no backticks, no commentary).
|
||||||
|
Each element must have exactly these keys:
|
||||||
|
"n" – intent_name: unique snake_case identifier ≤ 60 chars, prefixed with the batch id given
|
||||||
|
"p" – pattern: a PHP PCRE regex (use (?i) for case-insensitive) that matches the question
|
||||||
|
"r" – response: a thorough but concise educational answer (2–5 sentences or a short structured list)
|
||||||
|
"c" – category: the category string provided
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Patterns must use \\b word boundaries; escape backslashes for JSON (\\b not \b)
|
||||||
|
- Patterns should NOT start with ^ or end with $ (they are substring matches)
|
||||||
|
- Responses must be factually accurate
|
||||||
|
- Do not duplicate intent names; every "n" must be unique within this batch
|
||||||
|
- Return exactly 20 intents
|
||||||
|
SYS;
|
||||||
|
|
||||||
|
/* ── insert helper ── */
|
||||||
|
$inserted = 0;
|
||||||
|
$skipped = 0;
|
||||||
|
$errors = 0;
|
||||||
|
|
||||||
|
function safe_insert(array $intent, string $batchCategory): void {
|
||||||
|
global $inserted, $skipped, $errors;
|
||||||
|
$name = trim($intent['n'] ?? '');
|
||||||
|
$pattern = trim($intent['p'] ?? '');
|
||||||
|
$response = trim($intent['r'] ?? '');
|
||||||
|
$category = trim($intent['c'] ?? $batchCategory);
|
||||||
|
|
||||||
|
if (!$name || !$pattern || !$response) { $errors++; return; }
|
||||||
|
if (strlen($name) > 64) $name = substr($name, 0, 64);
|
||||||
|
if (strlen($pattern) > 512) $pattern = substr($pattern, 0, 512);
|
||||||
|
if (strlen($response) < 30) { $skipped++; return; } // too short
|
||||||
|
|
||||||
|
try {
|
||||||
|
JarvisDB::execute(
|
||||||
|
'INSERT INTO kb_intents (intent_name, pattern, response_template, fact_category, action_type, priority, active)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
pattern=VALUES(pattern),
|
||||||
|
response_template=VALUES(response_template),
|
||||||
|
fact_category=VALUES(fact_category)',
|
||||||
|
[$name, $pattern, $response, $category, 'response', 5, 1]
|
||||||
|
);
|
||||||
|
$inserted++;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$errors++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── main generation loop ── */
|
||||||
|
$totalBatches = count($runBatches);
|
||||||
|
foreach ($runBatches as $idx => $batch) {
|
||||||
|
$num = $idx + 1;
|
||||||
|
log_line("Batch {$num}/{$totalBatches}: {$batch['topic']}");
|
||||||
|
|
||||||
|
$user = "Generate 20 KB intents for the topic: {$batch['topic']}.\n"
|
||||||
|
. "Subtopics to cover: {$batch['desc']}.\n"
|
||||||
|
. "Prefix every intent_name with \"{$batch['id']}_\".\n"
|
||||||
|
. "Category string to use: \"{$batch['category']}\".";
|
||||||
|
|
||||||
|
$raw = groq($SYSTEM, $user, 5000);
|
||||||
|
if ($raw === null) {
|
||||||
|
log_line(" ✗ API call failed after retries – skipping batch.");
|
||||||
|
$errors += 20;
|
||||||
|
sleep(8);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip markdown code fences if model added them
|
||||||
|
$raw = preg_replace('/^```(?:json)?\s*/m', '', $raw);
|
||||||
|
$raw = preg_replace('/^```\s*/m', '', $raw);
|
||||||
|
$raw = trim($raw);
|
||||||
|
|
||||||
|
// Extract JSON array; fall back to partial recovery for truncated responses
|
||||||
|
$items = null;
|
||||||
|
if (preg_match('/\[\s*\{.*\}\s*\]/s', $raw, $m)) {
|
||||||
|
$items = json_decode($m[0], true);
|
||||||
|
if (!is_array($items)) {
|
||||||
|
log_line(" ✗ JSON parse failed – skipping batch.");
|
||||||
|
$errors += 20; sleep(8); continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$start = strpos($raw, '[');
|
||||||
|
if ($start !== false) {
|
||||||
|
$partial = substr($raw, $start);
|
||||||
|
if (preg_match_all('/\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/s', $partial, $objs) && !empty($objs[0])) {
|
||||||
|
$recovered = '[' . implode(',', $objs[0]) . ']';
|
||||||
|
$items = json_decode($recovered, true);
|
||||||
|
if (is_array($items) && count($items) > 0)
|
||||||
|
log_line(" ⚠ Truncated — recovered " . count($items) . " items.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!is_array($items) || count($items) === 0) {
|
||||||
|
log_line(" ✗ No JSON array found — raw[0:120]: " . substr(str_replace("\n", ' ', $raw), 0, 120));
|
||||||
|
$errors += 20; sleep(8); continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$batchInserted = 0;
|
||||||
|
foreach ($items as $item) {
|
||||||
|
if (!is_array($item)) continue;
|
||||||
|
safe_insert($item, $batch['category']);
|
||||||
|
$batchInserted++;
|
||||||
|
}
|
||||||
|
log_line(" ✓ Parsed {$batchInserted} intents (running total inserted: {$inserted}).");
|
||||||
|
|
||||||
|
// Polite delay between API calls — Groq TPM limit needs ~8s between batches
|
||||||
|
if ($num < $totalBatches) sleep(8);
|
||||||
|
}
|
||||||
|
|
||||||
|
log_line("Generation complete. Inserted/updated: {$inserted} | Short/invalid skipped: {$skipped} | Errors: {$errors}");
|
||||||
|
|
||||||
|
/* ── cleanup phase ── */
|
||||||
|
log_line('Starting cleanup phase...');
|
||||||
|
|
||||||
|
// 1. Remove exact duplicate intent_names (keep the one with the longer response)
|
||||||
|
$dups = JarvisDB::query(
|
||||||
|
'SELECT intent_name, COUNT(*) AS cnt FROM kb_intents GROUP BY intent_name HAVING cnt > 1'
|
||||||
|
);
|
||||||
|
$dupsPruned = 0;
|
||||||
|
foreach ($dups as $dup) {
|
||||||
|
$rows = JarvisDB::query(
|
||||||
|
'SELECT id, LENGTH(response_template) AS rlen FROM kb_intents WHERE intent_name=? ORDER BY rlen DESC',
|
||||||
|
[$dup['intent_name']]
|
||||||
|
);
|
||||||
|
array_shift($rows);
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
JarvisDB::execute('DELETE FROM kb_intents WHERE id=?', [$row['id']]);
|
||||||
|
$dupsPruned++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log_line(" Duplicate intent_names pruned: {$dupsPruned}");
|
||||||
|
|
||||||
|
// 2. Remove intents with very short responses (< 40 chars)
|
||||||
|
$shortPruned = JarvisDB::execute(
|
||||||
|
"DELETE FROM kb_intents WHERE LENGTH(response_template) < 40 AND priority <= 5"
|
||||||
|
);
|
||||||
|
log_line(" Short-response rows pruned: {$shortPruned}");
|
||||||
|
|
||||||
|
// 3. Trim whitespace on all generated intents
|
||||||
|
JarvisDB::execute(
|
||||||
|
"UPDATE kb_intents SET
|
||||||
|
intent_name = TRIM(intent_name),
|
||||||
|
pattern = TRIM(pattern),
|
||||||
|
response_template = TRIM(response_template),
|
||||||
|
fact_category = TRIM(fact_category)
|
||||||
|
WHERE priority = 5"
|
||||||
|
);
|
||||||
|
log_line(' Whitespace trimmed on all generated intents.');
|
||||||
|
|
||||||
|
// 4. Fix and validate PCRE patterns — normalize then deactivate only truly broken ones
|
||||||
|
$all = JarvisDB::query('SELECT id, pattern FROM kb_intents WHERE priority=5');
|
||||||
|
$badPattern = 0;
|
||||||
|
$fixedPattern = 0;
|
||||||
|
foreach ($all as $row) {
|
||||||
|
$pat = normalize_pattern($row['pattern']);
|
||||||
|
if ($pat !== $row['pattern']) {
|
||||||
|
// Update to normalized form
|
||||||
|
JarvisDB::execute('UPDATE kb_intents SET pattern=?, active=1 WHERE id=?', [$pat, $row['id']]);
|
||||||
|
$fixedPattern++;
|
||||||
|
} elseif (@preg_match($pat, '') === false) {
|
||||||
|
JarvisDB::execute('UPDATE kb_intents SET active=0 WHERE id=?', [$row['id']]);
|
||||||
|
$badPattern++;
|
||||||
|
} else {
|
||||||
|
// Valid pattern — make sure it's active
|
||||||
|
JarvisDB::execute('UPDATE kb_intents SET active=1 WHERE id=?', [$row['id']]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log_line(" Patterns normalized: {$fixedPattern} | Bad PCRE deactivated: {$badPattern}");
|
||||||
|
|
||||||
|
// 5. Enable ALL remaining inactive intents (including pre-existing ones)
|
||||||
|
$reactivated = JarvisDB::execute('UPDATE kb_intents SET active=1 WHERE active=0 AND priority <= 5');
|
||||||
|
log_line(" Re-activated previously inactive intents: {$reactivated}");
|
||||||
|
|
||||||
|
// 6. Final stats
|
||||||
|
$stats = JarvisDB::single('SELECT COUNT(*) AS total, SUM(active) AS active FROM kb_intents');
|
||||||
|
log_line("Final KB Intents table: {$stats['total']} total, {$stats['active']} active.");
|
||||||
|
|
||||||
|
/* ── record last-run timestamp ── */
|
||||||
|
JarvisDB::execute(
|
||||||
|
"INSERT INTO kb_facts (category, fact_key, fact_value, host)
|
||||||
|
VALUES ('kb_generator', 'last_run', NOW(), 'local')
|
||||||
|
ON DUPLICATE KEY UPDATE fact_value=NOW(), updated_at=NOW()",
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
JarvisDB::execute(
|
||||||
|
"INSERT INTO kb_facts (category, fact_key, fact_value, host)
|
||||||
|
VALUES ('kb_generator', 'batch_offset', ?, 'local')
|
||||||
|
ON DUPLICATE KEY UPDATE fact_value=VALUES(fact_value), updated_at=NOW()",
|
||||||
|
[$nextOffset]
|
||||||
|
);
|
||||||
|
log_line("Next run will start at topic offset {$nextOffset}/{$totalTopics}.");
|
||||||
|
JarvisDB::execute(
|
||||||
|
"INSERT INTO kb_facts (category, fact_key, fact_value, host)
|
||||||
|
VALUES ('kb_generator', 'last_inserted', ?, 'local')
|
||||||
|
ON DUPLICATE KEY UPDATE fact_value=VALUES(fact_value), updated_at=NOW()",
|
||||||
|
[$inserted]
|
||||||
|
);
|
||||||
|
|
||||||
|
log_line('Done.');
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* JARVIS KB Intent Generator
|
||||||
|
* Generates 1,000+ educational KB intents via Groq LLM and imports to kb_intents table.
|
||||||
|
* Also runs a cleanup pass: deduplication, short-response pruning, pattern normalisation.
|
||||||
|
*
|
||||||
|
* CLI / cron: /usr/bin/php8.3 /var/www/jarvis/api/endpoints/kb_intent_generator.php
|
||||||
|
* Schedule: Weekly – Sunday 3 am
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../config.php';
|
||||||
|
require_once __DIR__ . '/../lib/db.php';
|
||||||
|
|
||||||
|
/* ── helpers ── */
|
||||||
|
function ts(): string { return '[' . date('Y-m-d H:i:s') . ']'; }
|
||||||
|
function log_line(string $msg): void { echo ts() . ' KB Intent Generator: ' . $msg . "\n"; flush(); }
|
||||||
|
function groq(string $system, string $user, int $max = 3000): ?string {
|
||||||
|
$ch = curl_init('https://api.groq.com/openai/v1/chat/completions');
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_POST => true,
|
||||||
|
CURLOPT_TIMEOUT => 60,
|
||||||
|
CURLOPT_HTTPHEADER => [
|
||||||
|
'Authorization: Bearer ' . GROQ_API_KEY,
|
||||||
|
'Content-Type: application/json',
|
||||||
|
],
|
||||||
|
CURLOPT_POSTFIELDS => json_encode([
|
||||||
|
'model' => 'llama-3.3-70b-versatile',
|
||||||
|
'max_tokens' => $max,
|
||||||
|
'temperature' => 0.7,
|
||||||
|
'messages' => [
|
||||||
|
['role' => 'system', 'content' => $system],
|
||||||
|
['role' => 'user', 'content' => $user],
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
$raw = curl_exec($ch);
|
||||||
|
$err = curl_error($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
if ($err || !$raw) return null;
|
||||||
|
$d = json_decode($raw, true);
|
||||||
|
return $d['choices'][0]['message']['content'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── daily guard: skip if already ran within last 20 hours ── */
|
||||||
|
$lastRun = JarvisDB::single(
|
||||||
|
"SELECT updated_at FROM kb_facts WHERE category='kb_generator' AND fact_key='last_run'"
|
||||||
|
);
|
||||||
|
if ($lastRun && (time() - strtotime($lastRun['updated_at'])) < 72000) {
|
||||||
|
log_line('Skipping – ran within last 6 days (next run Sunday 3am).');
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
log_line('Starting weekly KB intent generation run.');
|
||||||
|
|
||||||
|
/* ── topic batches (25 topics × ~40 intents = 1,000+) ── */
|
||||||
|
$BATCHES = [
|
||||||
|
['id' => 'math_elem', 'category' => 'math_elementary', 'topic' => 'Elementary school mathematics',
|
||||||
|
'desc' => 'counting, basic arithmetic, place value, simple fractions, 2D/3D shapes, measurement, telling time, money, patterns'],
|
||||||
|
['id' => 'math_mid', 'category' => 'math_middle', 'topic' => 'Middle school mathematics',
|
||||||
|
'desc' => 'ratios, percentages, integers, exponents, pre-algebra equations, coordinate planes, probability, statistics (mean/median/mode), geometry area/volume'],
|
||||||
|
['id' => 'math_high', 'category' => 'math_high', 'topic' => 'High school mathematics',
|
||||||
|
'desc' => 'quadratic equations, polynomials, functions, logarithms, trigonometry (SOH-CAH-TOA, unit circle), sequences, permutations and combinations, matrices'],
|
||||||
|
['id' => 'math_college', 'category' => 'math_college', 'topic' => 'College-level mathematics',
|
||||||
|
'desc' => 'limits and continuity, derivatives, integrals, chain rule, L\'Hopital, differential equations, vectors, eigenvalues, hypothesis testing, normal distribution'],
|
||||||
|
['id' => 'bio_cell', 'category' => 'biology', 'topic' => 'Cell biology and genetics',
|
||||||
|
'desc' => 'organelles, mitosis vs meiosis, DNA structure, protein synthesis (transcription/translation), Mendelian genetics, dominant/recessive, mutations, genetic disorders'],
|
||||||
|
['id' => 'bio_body', 'category' => 'biology', 'topic' => 'Human body systems',
|
||||||
|
'desc' => 'skeletal, muscular, cardiovascular, respiratory, digestive, nervous, endocrine, immune, reproductive, and excretory systems'],
|
||||||
|
['id' => 'bio_ecology', 'category' => 'biology', 'topic' => 'Ecology and evolution',
|
||||||
|
'desc' => 'ecosystems, biomes, food webs, trophic levels, symbiosis (mutualism/commensalism/parasitism), natural selection, adaptation, speciation, biodiversity'],
|
||||||
|
['id' => 'chem_basics', 'category' => 'chemistry', 'topic' => 'Chemistry fundamentals',
|
||||||
|
'desc' => 'atomic structure, periodic table trends, ionic vs covalent bonds, Lewis structures, polarity, molecular geometry (VSEPR), intermolecular forces'],
|
||||||
|
['id' => 'chem_rxns', 'category' => 'chemistry', 'topic' => 'Chemical reactions and thermochemistry',
|
||||||
|
'desc' => 'balancing equations, stoichiometry, limiting reagents, reaction types, enthalpy, entropy, Gibbs free energy, Le Chatelier\'s principle, equilibrium constants'],
|
||||||
|
['id' => 'phys_mech', 'category' => 'physics', 'topic' => 'Physics – mechanics and energy',
|
||||||
|
'desc' => 'kinematics, Newton\'s three laws, friction, momentum, conservation of energy, work and power, circular motion, gravitation, projectile motion'],
|
||||||
|
['id' => 'phys_em', 'category' => 'physics', 'topic' => 'Physics – waves, electricity and magnetism',
|
||||||
|
'desc' => 'wave properties (amplitude, frequency, wavelength), sound, light spectrum, reflection/refraction, Ohm\'s law, series vs parallel circuits, magnetic fields, electromagnetic induction'],
|
||||||
|
['id' => 'earth_sci', 'category' => 'earth_science', 'topic' => 'Earth and environmental science',
|
||||||
|
'desc' => 'rock cycle, plate tectonics, earthquakes, volcanoes, atmosphere layers, weather vs climate, greenhouse effect, water cycle, ocean currents, soil formation'],
|
||||||
|
['id' => 'astronomy', 'category' => 'astronomy', 'topic' => 'Astronomy and space science',
|
||||||
|
'desc' => 'solar system planets, star life cycles, galaxies, Big Bang theory, light-years, telescopes, space exploration milestones, dark matter/energy, black holes'],
|
||||||
|
['id' => 'hist_us', 'category' => 'history_us', 'topic' => 'United States history',
|
||||||
|
'desc' => 'colonial era, American Revolution, Constitution, westward expansion, Civil War, Reconstruction, Gilded Age, WWI, Great Depression, WWII, Civil Rights, Cold War, modern era'],
|
||||||
|
['id' => 'hist_world', 'category' => 'history_world', 'topic' => 'World history',
|
||||||
|
'desc' => 'ancient civilisations (Egypt, Greece, Rome, Mesopotamia, China, India), Medieval period, Renaissance, Reformation, age of exploration, colonialism, Industrial Revolution, WWI, WWII, decolonisation, Cold War'],
|
||||||
|
['id' => 'geo_world', 'category' => 'geography', 'topic' => 'World geography',
|
||||||
|
'desc' => 'continents and oceans, countries and capitals, physical features (mountains, rivers, deserts), climate zones, latitude/longitude, map projections, time zones, population distribution'],
|
||||||
|
['id' => 'civics', 'category' => 'civics', 'topic' => 'Civics and US government',
|
||||||
|
'desc' => 'three branches of government, checks and balances, Bill of Rights, Constitutional amendments, federalism, Electoral College, legislative process, Supreme Court landmark cases, political parties'],
|
||||||
|
['id' => 'econ', 'category' => 'economics', 'topic' => 'Economics',
|
||||||
|
'desc' => 'supply and demand, market equilibrium, elasticity, GDP, inflation, unemployment, fiscal vs monetary policy, comparative advantage, market structures (monopoly, oligopoly, perfect competition), opportunity cost'],
|
||||||
|
['id' => 'lit_writing', 'category' => 'literature', 'topic' => 'Literature and writing',
|
||||||
|
'desc' => 'figurative language (simile, metaphor, personification, irony), plot structure, literary themes, point of view, genre types, essay structure, thesis statements, grammar rules, poetry forms, citing sources'],
|
||||||
|
['id' => 'cs_prog', 'category' => 'computer_science', 'topic' => 'Computer science and programming',
|
||||||
|
'desc' => 'variables, data types, loops, conditionals, functions, OOP concepts, arrays/lists, sorting and searching algorithms, time/space complexity, binary, hexadecimal, recursion, debugging'],
|
||||||
|
['id' => 'cs_systems', 'category' => 'computer_science', 'topic' => 'Computer systems and networking',
|
||||||
|
'desc' => 'CPU/RAM/storage, operating systems, file systems, TCP/IP, DNS, HTTP/HTTPS, databases (SQL vs NoSQL), cybersecurity threats (phishing, SQL injection, XSS, malware), encryption basics, cloud computing'],
|
||||||
|
['id' => 'psych', 'category' => 'psychology', 'topic' => 'Psychology',
|
||||||
|
'desc' => 'classical and operant conditioning, Maslow\'s hierarchy, Piaget\'s stages, Erikson\'s stages, memory types, sleep stages, cognitive biases, Freud\'s theory, social influence, abnormal psychology basics'],
|
||||||
|
['id' => 'phil', 'category' => 'philosophy', 'topic' => 'Philosophy and logic',
|
||||||
|
'desc' => 'Socrates/Plato/Aristotle, ethical theories (utilitarianism, deontology, virtue ethics), epistemology, deductive vs inductive reasoning, logical fallacies, existentialism, free will vs determinism, political philosophy'],
|
||||||
|
['id' => 'health_sci', 'category' => 'health', 'topic' => 'Health and life skills',
|
||||||
|
'desc' => 'nutrition (macronutrients, vitamins, minerals), exercise physiology, mental health (anxiety, depression, stress response), reproductive health, substance abuse, first aid, disease prevention, sleep hygiene'],
|
||||||
|
['id' => 'arts_music', 'category' => 'arts', 'topic' => 'Arts and music',
|
||||||
|
'desc' => 'elements of art (line, shape, color, texture), colour theory, major art movements, famous artists and their works, music notes and scales, rhythm and meter, instrument families, major composers and genres'],
|
||||||
|
];
|
||||||
|
|
||||||
|
/* ── system prompt ── */
|
||||||
|
$SYSTEM = <<<'SYS'
|
||||||
|
You are an expert educator generating KB (knowledge-base) intents for an AI assistant called JARVIS.
|
||||||
|
Each intent is a question/phrase a student might ask, paired with a clear educational answer.
|
||||||
|
|
||||||
|
Respond ONLY with a valid JSON array (no markdown, no backticks, no commentary).
|
||||||
|
Each element must have exactly these keys:
|
||||||
|
"n" – intent_name: unique snake_case identifier ≤ 60 chars, prefixed with the batch id given
|
||||||
|
"p" – pattern: a PHP PCRE regex (use (?i) for case-insensitive) that matches the question
|
||||||
|
"r" – response: a thorough but concise educational answer (2–5 sentences or a short structured list)
|
||||||
|
"c" – category: the category string provided
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Patterns must use \\b word boundaries; escape backslashes for JSON (\\b not \b)
|
||||||
|
- Patterns should NOT start with ^ or end with $ (they are substring matches)
|
||||||
|
- Responses must be factually accurate
|
||||||
|
- Do not duplicate intent names; every "n" must be unique within this batch
|
||||||
|
- Return exactly 40 intents
|
||||||
|
SYS;
|
||||||
|
|
||||||
|
/* ── insert helper ── */
|
||||||
|
$inserted = 0;
|
||||||
|
$skipped = 0;
|
||||||
|
$errors = 0;
|
||||||
|
|
||||||
|
function safe_insert(array $intent, string $batchCategory): void {
|
||||||
|
global $inserted, $skipped, $errors;
|
||||||
|
$name = trim($intent['n'] ?? '');
|
||||||
|
$pattern = trim($intent['p'] ?? '');
|
||||||
|
$response = trim($intent['r'] ?? '');
|
||||||
|
$category = trim($intent['c'] ?? $batchCategory);
|
||||||
|
|
||||||
|
if (!$name || !$pattern || !$response) { $errors++; return; }
|
||||||
|
if (strlen($name) > 64) $name = substr($name, 0, 64);
|
||||||
|
if (strlen($pattern) > 512) $pattern = substr($pattern, 0, 512);
|
||||||
|
if (strlen($response) < 30) { $skipped++; return; } // too short
|
||||||
|
|
||||||
|
try {
|
||||||
|
JarvisDB::execute(
|
||||||
|
'INSERT INTO kb_intents (intent_name, pattern, response_template, fact_category, action_type, priority, active)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
pattern=VALUES(pattern),
|
||||||
|
response_template=VALUES(response_template),
|
||||||
|
fact_category=VALUES(fact_category)',
|
||||||
|
[$name, $pattern, $response, $category, 'response', 5, 1]
|
||||||
|
);
|
||||||
|
$inserted++;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$errors++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── main generation loop ── */
|
||||||
|
$totalBatches = count($BATCHES);
|
||||||
|
foreach ($BATCHES as $idx => $batch) {
|
||||||
|
$num = $idx + 1;
|
||||||
|
log_line("Batch {$num}/{$totalBatches}: {$batch['topic']}");
|
||||||
|
|
||||||
|
$user = "Generate 40 KB intents for the topic: {$batch['topic']}.\n"
|
||||||
|
. "Subtopics to cover: {$batch['desc']}.\n"
|
||||||
|
. "Prefix every intent_name with \"{$batch['id']}_\".\n"
|
||||||
|
. "Category string to use: \"{$batch['category']}\".";
|
||||||
|
|
||||||
|
$raw = groq($SYSTEM, $user, 3500);
|
||||||
|
if ($raw === null) {
|
||||||
|
log_line(" ✗ API call failed – skipping batch.");
|
||||||
|
$errors += 40;
|
||||||
|
sleep(3);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip markdown code fences if model added them
|
||||||
|
$raw = preg_replace('/^```(?:json)?\s*/m', '', $raw);
|
||||||
|
$raw = preg_replace('/^```\s*/m', '', $raw);
|
||||||
|
$raw = trim($raw);
|
||||||
|
|
||||||
|
// Extract JSON array (even if the model added preamble text)
|
||||||
|
if (!preg_match('/\[\s*\{.*\}\s*\]/s', $raw, $m)) {
|
||||||
|
log_line(" ✗ No JSON array found in response – skipping batch.");
|
||||||
|
$errors += 40;
|
||||||
|
sleep(2);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$items = json_decode($m[0], true);
|
||||||
|
if (!is_array($items)) {
|
||||||
|
log_line(" ✗ JSON parse failed – skipping batch.");
|
||||||
|
$errors += 40;
|
||||||
|
sleep(2);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$batchInserted = 0;
|
||||||
|
foreach ($items as $item) {
|
||||||
|
if (!is_array($item)) continue;
|
||||||
|
safe_insert($item, $batch['category']);
|
||||||
|
$batchInserted++;
|
||||||
|
}
|
||||||
|
log_line(" ✓ Parsed {$batchInserted} intents (running total inserted: {$inserted}).");
|
||||||
|
|
||||||
|
// Polite delay between API calls to avoid rate limiting
|
||||||
|
if ($num < $totalBatches) sleep(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
log_line("Generation complete. Inserted/updated: {$inserted} | Short/invalid skipped: {$skipped} | Errors: {$errors}");
|
||||||
|
|
||||||
|
/* ── cleanup phase ── */
|
||||||
|
log_line('Starting cleanup phase...');
|
||||||
|
|
||||||
|
// 1. Remove exact duplicate intent_names (keep the one with the longer response)
|
||||||
|
$dups = JarvisDB::query(
|
||||||
|
'SELECT intent_name, COUNT(*) AS cnt FROM kb_intents GROUP BY intent_name HAVING cnt > 1'
|
||||||
|
);
|
||||||
|
$dupsPruned = 0;
|
||||||
|
foreach ($dups as $dup) {
|
||||||
|
$rows = JarvisDB::query(
|
||||||
|
'SELECT id, LENGTH(response_template) AS rlen FROM kb_intents WHERE intent_name=? ORDER BY rlen DESC',
|
||||||
|
[$dup['intent_name']]
|
||||||
|
);
|
||||||
|
// Delete all but the first (longest response)
|
||||||
|
array_shift($rows);
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
JarvisDB::execute('DELETE FROM kb_intents WHERE id=?', [$row['id']]);
|
||||||
|
$dupsPruned++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log_line(" Duplicate intent_names pruned: {$dupsPruned}");
|
||||||
|
|
||||||
|
// 2. Remove intents with very short responses (< 40 chars) — likely garbage
|
||||||
|
$shortPruned = JarvisDB::execute(
|
||||||
|
"DELETE FROM kb_intents WHERE LENGTH(response_template) < 40 AND priority <= 5"
|
||||||
|
);
|
||||||
|
log_line(" Short-response rows pruned: {$shortPruned}");
|
||||||
|
|
||||||
|
// 3. Trim whitespace on all text columns for the auto-generated ones
|
||||||
|
JarvisDB::execute(
|
||||||
|
"UPDATE kb_intents SET
|
||||||
|
intent_name = TRIM(intent_name),
|
||||||
|
pattern = TRIM(pattern),
|
||||||
|
response_template = TRIM(response_template),
|
||||||
|
fact_category = TRIM(fact_category)
|
||||||
|
WHERE priority = 5"
|
||||||
|
);
|
||||||
|
log_line(' Whitespace trimmed on all generated intents.');
|
||||||
|
|
||||||
|
// 4. Deactivate intents whose pattern is invalid PCRE
|
||||||
|
$all = JarvisDB::query('SELECT id, pattern FROM kb_intents WHERE active=1 AND priority=5');
|
||||||
|
$badPattern = 0;
|
||||||
|
foreach ($all as $row) {
|
||||||
|
if (@preg_match($row['pattern'], '') === false) {
|
||||||
|
JarvisDB::execute('UPDATE kb_intents SET active=0 WHERE id=?', [$row['id']]);
|
||||||
|
$badPattern++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log_line(" Bad PCRE patterns deactivated: {$badPattern}");
|
||||||
|
|
||||||
|
// 5. Stats
|
||||||
|
$stats = JarvisDB::single('SELECT COUNT(*) AS total, SUM(active) AS active FROM kb_intents');
|
||||||
|
log_line("Final KB Intents table: {$stats['total']} total, {$stats['active']} active.");
|
||||||
|
|
||||||
|
/* ── record last-run timestamp in kb_facts ── */
|
||||||
|
JarvisDB::execute(
|
||||||
|
"INSERT INTO kb_facts (category, fact_key, fact_value, host)
|
||||||
|
VALUES ('kb_generator', 'last_run', NOW(), 'local')
|
||||||
|
ON DUPLICATE KEY UPDATE fact_value=NOW(), updated_at=NOW()",
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
JarvisDB::execute(
|
||||||
|
"INSERT INTO kb_facts (category, fact_key, fact_value, host)
|
||||||
|
VALUES ('kb_generator', 'last_inserted', ?, 'local')
|
||||||
|
ON DUPLICATE KEY UPDATE fact_value=VALUES(fact_value), updated_at=NOW()",
|
||||||
|
[$inserted]
|
||||||
|
);
|
||||||
|
|
||||||
|
log_line('Done.');
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* JARVIS KB Intent Generator
|
||||||
|
* Generates 1,000+ educational KB intents via Groq LLM and imports to kb_intents table.
|
||||||
|
* Also runs a cleanup pass: deduplication, short-response pruning, pattern normalisation.
|
||||||
|
*
|
||||||
|
* CLI / cron: /usr/bin/php8.3 /var/www/jarvis/api/endpoints/kb_intent_generator.php
|
||||||
|
* Schedule: Daily – 3 am
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../config.php';
|
||||||
|
require_once __DIR__ . '/../lib/db.php';
|
||||||
|
|
||||||
|
/* ── helpers ── */
|
||||||
|
function ts(): string { return '[' . date('Y-m-d H:i:s') . ']'; }
|
||||||
|
function log_line(string $msg): void { echo ts() . ' KB Intent Generator: ' . $msg . "\n"; flush(); }
|
||||||
|
|
||||||
|
function groq(string $system, string $user, int $max = 3000, int $retries = 2): ?string {
|
||||||
|
for ($attempt = 0; $attempt <= $retries; $attempt++) {
|
||||||
|
if ($attempt > 0) {
|
||||||
|
log_line(" Retry {$attempt}/{$retries} after rate-limit pause...");
|
||||||
|
sleep(25);
|
||||||
|
}
|
||||||
|
$ch = curl_init('https://api.groq.com/openai/v1/chat/completions');
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_POST => true,
|
||||||
|
CURLOPT_TIMEOUT => 60,
|
||||||
|
CURLOPT_HTTPHEADER => [
|
||||||
|
'Authorization: Bearer ' . GROQ_API_KEY,
|
||||||
|
'Content-Type: application/json',
|
||||||
|
],
|
||||||
|
CURLOPT_POSTFIELDS => json_encode([
|
||||||
|
'model' => 'llama-3.3-70b-versatile',
|
||||||
|
'max_tokens' => $max,
|
||||||
|
'temperature' => 0.7,
|
||||||
|
'messages' => [
|
||||||
|
['role' => 'system', 'content' => $system],
|
||||||
|
['role' => 'user', 'content' => $user],
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
$raw = curl_exec($ch);
|
||||||
|
$err = curl_error($ch);
|
||||||
|
$info = curl_getinfo($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($err || !$raw) continue;
|
||||||
|
|
||||||
|
// Check for rate limit response (429)
|
||||||
|
if (($info['http_code'] ?? 0) === 429) continue;
|
||||||
|
|
||||||
|
$d = json_decode($raw, true);
|
||||||
|
$content = $d['choices'][0]['message']['content'] ?? null;
|
||||||
|
if ($content !== null) return $content;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── normalize a pattern to valid PHP PCRE ── */
|
||||||
|
function normalize_pattern(string $pat): string {
|
||||||
|
$pat = trim($pat);
|
||||||
|
// If pattern already has PCRE delimiters, leave it alone
|
||||||
|
if (preg_match('/^[\/|~#!@%]/', $pat)) return $pat;
|
||||||
|
// Strip any (?i) inline flag — we'll add /i at the delimiter level
|
||||||
|
$pat = preg_replace('/^\(\?i\)/', '', $pat);
|
||||||
|
// Escape forward slashes inside the pattern
|
||||||
|
$pat = str_replace('/', '\\/', $pat);
|
||||||
|
return '/' . $pat . '/i';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── run guard: skip if ran within last 4 hours ── */
|
||||||
|
/* Set JARVIS_FORCE_RUN=1 (env) or pass --force (argv) to bypass */
|
||||||
|
/* Comparison done in SQL (NOW()) rather than PHP time()/strtotime() — this process's
|
||||||
|
date_default_timezone_set('America/Chicago') makes strtotime() misread MySQL's naive
|
||||||
|
(UTC) timestamps as Chicago time, throwing elapsed-time checks off by the UTC offset. */
|
||||||
|
$recentRun = JarvisDB::single(
|
||||||
|
"SELECT (updated_at > DATE_SUB(NOW(), INTERVAL 14400 SECOND)) AS is_recent FROM kb_facts WHERE category='kb_generator' AND fact_key='last_run'"
|
||||||
|
);
|
||||||
|
$forceRun = !empty(getenv('JARVIS_FORCE_RUN')) || (isset($argv[1]) && $argv[1] === '--force');
|
||||||
|
if (!$forceRun && $recentRun && $recentRun['is_recent']) {
|
||||||
|
log_line('Skipping – ran within last 4 hours. Use --force to override.');
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
if ($forceRun) log_line('Force-run flag set — bypassing 4-hour guard.');
|
||||||
|
|
||||||
|
log_line('Starting daily KB intent generation run.');
|
||||||
|
|
||||||
|
/* ── load active topics from database ── */
|
||||||
|
$BATCHES = JarvisDB::query(
|
||||||
|
"SELECT t.topic_id AS id, t.category, t.topic_name AS topic, t.description AS `desc`
|
||||||
|
FROM kb_generator_topics t WHERE t.active=1 ORDER BY t.id ASC"
|
||||||
|
);
|
||||||
|
if (empty($BATCHES)) {
|
||||||
|
log_line('ERROR: No active topics in kb_generator_topics table. Add topics via the JARVIS admin panel.');
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
log_line('Loaded ' . count($BATCHES) . ' active topics from database.');
|
||||||
|
|
||||||
|
/* ── ROTATION ENGINE: process BATCH_SIZE topics per run, cycling through all ── */
|
||||||
|
define('BATCH_SIZE', 25);
|
||||||
|
$totalTopics = count($BATCHES);
|
||||||
|
$offsetRow = JarvisDB::single(
|
||||||
|
"SELECT CAST(fact_value AS SIGNED) AS v FROM kb_facts WHERE category='kb_generator' AND fact_key='batch_offset'"
|
||||||
|
);
|
||||||
|
$batchOffset = max(0, (int)($offsetRow['v'] ?? 0));
|
||||||
|
if ($batchOffset >= $totalTopics) $batchOffset = 0;
|
||||||
|
$nextOffset = ($batchOffset + BATCH_SIZE) % $totalTopics;
|
||||||
|
$cycleComplete = ($batchOffset + BATCH_SIZE) >= $totalTopics;
|
||||||
|
$cycleLen = (int)ceil($totalTopics / BATCH_SIZE);
|
||||||
|
|
||||||
|
$runBatches = [];
|
||||||
|
for ($i = 0; $i < BATCH_SIZE; $i++) {
|
||||||
|
$runBatches[] = $BATCHES[($batchOffset + $i) % $totalTopics];
|
||||||
|
}
|
||||||
|
$endIdx = ($batchOffset + BATCH_SIZE - 1) % $totalTopics;
|
||||||
|
log_line("Rotation: topics " . ($batchOffset + 1) . "–" . ($endIdx + 1) . " of {$totalTopics} | cycle = {$cycleLen} runs × 6h = " . ($cycleLen * 6) . "h full cycle.");
|
||||||
|
if ($cycleComplete) log_line(" ↻ Full cycle complete — restarting from topic 1 next run.");
|
||||||
|
|
||||||
|
/* ── main generation loop ── */
|
||||||
|
$totalBatches = count($runBatches);
|
||||||
|
foreach ($runBatches as $idx => $batch) {
|
||||||
|
$num = $idx + 1;
|
||||||
|
log_line("Batch {$num}/{$totalBatches}: {$batch['topic']}");
|
||||||
|
|
||||||
|
$user = "Generate 20 KB intents for the topic: {$batch['topic']}.\n"
|
||||||
|
. "Subtopics to cover: {$batch['desc']}.\n"
|
||||||
|
. "Prefix every intent_name with \"{$batch['id']}_\".\n"
|
||||||
|
. "Category string to use: \"{$batch['category']}\".";
|
||||||
|
|
||||||
|
$raw = groq($SYSTEM, $user, 5000);
|
||||||
|
if ($raw === null) {
|
||||||
|
log_line(" ✗ API call failed after retries – skipping batch.");
|
||||||
|
$errors += 20;
|
||||||
|
sleep(8);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip markdown code fences if model added them
|
||||||
|
$raw = preg_replace('/^```(?:json)?\s*/m', '', $raw);
|
||||||
|
$raw = preg_replace('/^```\s*/m', '', $raw);
|
||||||
|
$raw = trim($raw);
|
||||||
|
|
||||||
|
// Extract JSON array; fall back to partial recovery for truncated responses
|
||||||
|
$items = null;
|
||||||
|
if (preg_match('/\[\s*\{.*\}\s*\]/s', $raw, $m)) {
|
||||||
|
$items = json_decode($m[0], true);
|
||||||
|
if (!is_array($items)) {
|
||||||
|
log_line(" ✗ JSON parse failed – skipping batch.");
|
||||||
|
$errors += 20; sleep(8); continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$start = strpos($raw, '[');
|
||||||
|
if ($start !== false) {
|
||||||
|
$partial = substr($raw, $start);
|
||||||
|
if (preg_match_all('/\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/s', $partial, $objs) && !empty($objs[0])) {
|
||||||
|
$recovered = '[' . implode(',', $objs[0]) . ']';
|
||||||
|
$items = json_decode($recovered, true);
|
||||||
|
if (is_array($items) && count($items) > 0)
|
||||||
|
log_line(" ⚠ Truncated — recovered " . count($items) . " items.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!is_array($items) || count($items) === 0) {
|
||||||
|
log_line(" ✗ No JSON array found — raw[0:120]: " . substr(str_replace("\n", ' ', $raw), 0, 120));
|
||||||
|
$errors += 20; sleep(8); continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$batchInserted = 0;
|
||||||
|
foreach ($items as $item) {
|
||||||
|
if (!is_array($item)) continue;
|
||||||
|
safe_insert($item, $batch['category']);
|
||||||
|
$batchInserted++;
|
||||||
|
}
|
||||||
|
log_line(" ✓ Parsed {$batchInserted} intents (running total inserted: {$inserted}).");
|
||||||
|
|
||||||
|
// Polite delay between API calls — Groq TPM limit needs ~8s between batches
|
||||||
|
if ($num < $totalBatches) sleep(8);
|
||||||
|
}
|
||||||
|
|
||||||
|
log_line("Generation complete. Inserted/updated: {$inserted} | Short/invalid skipped: {$skipped} | Errors: {$errors}");
|
||||||
|
|
||||||
|
/* ── cleanup phase ── */
|
||||||
|
log_line('Starting cleanup phase...');
|
||||||
|
|
||||||
|
// 1. Remove exact duplicate intent_names (keep the one with the longer response)
|
||||||
|
$dups = JarvisDB::query(
|
||||||
|
'SELECT intent_name, COUNT(*) AS cnt FROM kb_intents GROUP BY intent_name HAVING cnt > 1'
|
||||||
|
);
|
||||||
|
$dupsPruned = 0;
|
||||||
|
foreach ($dups as $dup) {
|
||||||
|
$rows = JarvisDB::query(
|
||||||
|
'SELECT id, LENGTH(response_template) AS rlen FROM kb_intents WHERE intent_name=? ORDER BY rlen DESC',
|
||||||
|
[$dup['intent_name']]
|
||||||
|
);
|
||||||
|
array_shift($rows);
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
JarvisDB::execute('DELETE FROM kb_intents WHERE id=?', [$row['id']]);
|
||||||
|
$dupsPruned++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log_line(" Duplicate intent_names pruned: {$dupsPruned}");
|
||||||
|
|
||||||
|
// 2. Remove intents with very short responses (< 40 chars)
|
||||||
|
$shortPruned = JarvisDB::execute(
|
||||||
|
"DELETE FROM kb_intents WHERE LENGTH(response_template) < 40 AND priority <= 5"
|
||||||
|
);
|
||||||
|
log_line(" Short-response rows pruned: {$shortPruned}");
|
||||||
|
|
||||||
|
// 3. Trim whitespace on all generated intents
|
||||||
|
JarvisDB::execute(
|
||||||
|
"UPDATE kb_intents SET
|
||||||
|
intent_name = TRIM(intent_name),
|
||||||
|
pattern = TRIM(pattern),
|
||||||
|
response_template = TRIM(response_template),
|
||||||
|
fact_category = TRIM(fact_category)
|
||||||
|
WHERE priority = 5"
|
||||||
|
);
|
||||||
|
log_line(' Whitespace trimmed on all generated intents.');
|
||||||
|
|
||||||
|
// 4. Fix and validate PCRE patterns — normalize then deactivate only truly broken ones
|
||||||
|
$all = JarvisDB::query('SELECT id, pattern FROM kb_intents WHERE priority=5');
|
||||||
|
$badPattern = 0;
|
||||||
|
$fixedPattern = 0;
|
||||||
|
foreach ($all as $row) {
|
||||||
|
$pat = normalize_pattern($row['pattern']);
|
||||||
|
if ($pat !== $row['pattern']) {
|
||||||
|
// Update to normalized form
|
||||||
|
JarvisDB::execute('UPDATE kb_intents SET pattern=?, active=1 WHERE id=?', [$pat, $row['id']]);
|
||||||
|
$fixedPattern++;
|
||||||
|
} elseif (@preg_match($pat, '') === false) {
|
||||||
|
JarvisDB::execute('UPDATE kb_intents SET active=0 WHERE id=?', [$row['id']]);
|
||||||
|
$badPattern++;
|
||||||
|
} else {
|
||||||
|
// Valid pattern — make sure it's active
|
||||||
|
JarvisDB::execute('UPDATE kb_intents SET active=1 WHERE id=?', [$row['id']]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log_line(" Patterns normalized: {$fixedPattern} | Bad PCRE deactivated: {$badPattern}");
|
||||||
|
|
||||||
|
// 5. Enable ALL remaining inactive intents (including pre-existing ones)
|
||||||
|
$reactivated = JarvisDB::execute('UPDATE kb_intents SET active=1 WHERE active=0 AND priority <= 5');
|
||||||
|
log_line(" Re-activated previously inactive intents: {$reactivated}");
|
||||||
|
|
||||||
|
// 6. Final stats
|
||||||
|
$stats = JarvisDB::single('SELECT COUNT(*) AS total, SUM(active) AS active FROM kb_intents');
|
||||||
|
log_line("Final KB Intents table: {$stats['total']} total, {$stats['active']} active.");
|
||||||
|
|
||||||
|
/* ── record last-run timestamp ── */
|
||||||
|
JarvisDB::execute(
|
||||||
|
"INSERT INTO kb_facts (category, fact_key, fact_value, host)
|
||||||
|
VALUES ('kb_generator', 'last_run', NOW(), 'local')
|
||||||
|
ON DUPLICATE KEY UPDATE fact_value=NOW(), updated_at=NOW()",
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
JarvisDB::execute(
|
||||||
|
"INSERT INTO kb_facts (category, fact_key, fact_value, host)
|
||||||
|
VALUES ('kb_generator', 'batch_offset', ?, 'local')
|
||||||
|
ON DUPLICATE KEY UPDATE fact_value=VALUES(fact_value), updated_at=NOW()",
|
||||||
|
[$nextOffset]
|
||||||
|
);
|
||||||
|
log_line("Next run will start at topic offset {$nextOffset}/{$totalTopics}.");
|
||||||
|
JarvisDB::execute(
|
||||||
|
"INSERT INTO kb_facts (category, fact_key, fact_value, host)
|
||||||
|
VALUES ('kb_generator', 'last_inserted', ?, 'local')
|
||||||
|
ON DUPLICATE KEY UPDATE fact_value=VALUES(fact_value), updated_at=NOW()",
|
||||||
|
[$inserted]
|
||||||
|
);
|
||||||
|
|
||||||
|
log_line('Done.');
|
||||||
@@ -479,3 +479,58 @@ CREATE TABLE IF NOT EXISTS `guardian_events` (
|
|||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
-- Dump completed on 2026-06-29 23:15:44
|
-- Dump completed on 2026-06-29 23:15:44
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `email_triage` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`msg_id` varchar(255) NOT NULL,
|
||||||
|
`account` varchar(64) NOT NULL DEFAULT 'gmail',
|
||||||
|
`from_name` varchar(255) DEFAULT NULL,
|
||||||
|
`from_email` varchar(255) DEFAULT NULL,
|
||||||
|
`subject` varchar(500) DEFAULT NULL,
|
||||||
|
`date_received` datetime DEFAULT NULL,
|
||||||
|
`category` varchar(32) NOT NULL DEFAULT 'info',
|
||||||
|
`priority` int(11) NOT NULL DEFAULT 3,
|
||||||
|
`summary` text DEFAULT NULL,
|
||||||
|
`draft_reply` text DEFAULT NULL,
|
||||||
|
`action_taken` varchar(32) NOT NULL DEFAULT 'none',
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uq_msg` (`msg_id`),
|
||||||
|
KEY `idx_category` (`category`),
|
||||||
|
KEY `idx_action` (`action_taken`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `email_actions` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`msg_id` varchar(255) DEFAULT NULL,
|
||||||
|
`from_name` varchar(255) DEFAULT NULL,
|
||||||
|
`from_email` varchar(255) DEFAULT NULL,
|
||||||
|
`subject` varchar(500) DEFAULT NULL,
|
||||||
|
`received_at` datetime DEFAULT NULL,
|
||||||
|
`suggested_title` varchar(255) DEFAULT NULL,
|
||||||
|
`suggested_date` date DEFAULT NULL,
|
||||||
|
`task_id` int(11) DEFAULT NULL,
|
||||||
|
`appointment_id` int(11) DEFAULT NULL,
|
||||||
|
`dismissed` tinyint(1) NOT NULL DEFAULT 0,
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_dismissed` (`dismissed`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `email_sent` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`account` varchar(64) NOT NULL DEFAULT 'gmail',
|
||||||
|
`to_email` varchar(255) NOT NULL,
|
||||||
|
`to_name` varchar(255) DEFAULT NULL,
|
||||||
|
`subject` varchar(500) DEFAULT NULL,
|
||||||
|
`body` text DEFAULT NULL,
|
||||||
|
`triage_id` int(11) DEFAULT NULL,
|
||||||
|
`status` varchar(32) NOT NULL DEFAULT 'sent',
|
||||||
|
`sent_at` timestamp NULL DEFAULT current_timestamp(),
|
||||||
|
`error` text DEFAULT NULL,
|
||||||
|
`message_id` varchar(255) DEFAULT NULL,
|
||||||
|
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_account` (`account`),
|
||||||
|
KEY `idx_status` (`status`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|||||||
Executable
+31
@@ -0,0 +1,31 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# JARVIS backup — DB dump as tar.gz, admin-panel compatible
|
||||||
|
BACKUP_DIR="/var/backups/jarvis"
|
||||||
|
LOG="$BACKUP_DIR/backup.log"
|
||||||
|
LOCK="$BACKUP_DIR/backup.lock"
|
||||||
|
DB_NAME="jarvis_db"
|
||||||
|
DB_USER="jarvis_user"
|
||||||
|
DB_PASS="J4rv1s_Pr0t0c0l_2026!"
|
||||||
|
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
|
||||||
|
OUTFILE="$BACKUP_DIR/jarvis_backup_${TIMESTAMP}.tar.gz"
|
||||||
|
TMPDIR=$(mktemp -d)
|
||||||
|
|
||||||
|
mkdir -p "$BACKUP_DIR"
|
||||||
|
touch "$LOCK"
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Starting backup..." >> "$LOG"
|
||||||
|
|
||||||
|
cleanup() { rm -rf "$TMPDIR"; rm -f "$LOCK"; }
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
if mysqldump -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" > "$TMPDIR/jarvis_db.sql" 2>>"$LOG"; then
|
||||||
|
tar -czf "$OUTFILE" -C "$TMPDIR" jarvis_db.sql
|
||||||
|
SIZE=$(du -sh "$OUTFILE" | cut -f1)
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Backup OK: $(basename "$OUTFILE") ($SIYE)" >> "$LOG"
|
||||||
|
else
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: mysqldump failed" >> "$LOG"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
find "$BACKUP_DIR" -name "jarvis_backup_*.tar.gz" -mtime +7 -delete
|
||||||
|
COUNT=$(ls "$BACKUP_DIR"/jarvis_backup_*.tar.gz 2>/dev/null | wc -l)
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Done. Files retained: $COUNT" >> "$LOG"
|
||||||
+67
-107
@@ -41,16 +41,17 @@ DB_PORT = 3306
|
|||||||
DB_USER = "jarvis_user"
|
DB_USER = "jarvis_user"
|
||||||
DB_PASS = "J4rv1s_Pr0t0c0l_2026!"
|
DB_PASS = "J4rv1s_Pr0t0c0l_2026!"
|
||||||
DB_NAME = "jarvis_db"
|
DB_NAME = "jarvis_db"
|
||||||
LOG_FILE = "/home/jarvis.orbishosting.com/logs/arc_reactor.log"
|
LOG_FILE = "/var/log/jarvis/arc_reactor.log"
|
||||||
POLL_INTERVAL = 3
|
POLL_INTERVAL = 3
|
||||||
HEARTBEAT_INTERVAL = 30
|
HEARTBEAT_INTERVAL = 30
|
||||||
|
|
||||||
CLAUDE_API_KEY = "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"
|
CLAUDE_API_KEY = "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"
|
||||||
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.210:11434"
|
OLLAMA_HOST = "http://10.48.200.210:11434"
|
||||||
OLLAMA_MODEL = "llama3.1:8b"
|
OLLAMA_MODEL = "llama3.1:8b"
|
||||||
|
OLLAMA_VISION_MODEL = os.environ.get("OLLAMA_VISION_MODEL", "") # e.g. "llava" or "moondream" -- empty = disabled
|
||||||
|
|
||||||
GMAIL_USER = "myronblair@gmail.com"
|
GMAIL_USER = "myronblair@gmail.com"
|
||||||
GMAIL_PASS = "demsvdylwweacbcx"
|
GMAIL_PASS = "demsvdylwweacbcx"
|
||||||
@@ -175,15 +176,56 @@ 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:
|
# -- VISION CALL ----------------------------------------------------------
|
||||||
async with session.post(
|
async def _vision_call(image_b64: str, prompt: str) -> tuple:
|
||||||
f"{OLLAMA_HOST}/api/generate",
|
"""
|
||||||
json={"model": "llava:7b", "prompt": prompt, "images": [image_b64], "stream": False},
|
Vision provider cascade: Claude -> Ollama vision model -> graceful fallback.
|
||||||
timeout=aiohttp.ClientTimeout(total=120),
|
Returns (analysis: str, provider: str).
|
||||||
) as resp:
|
To enable a local vision model: set OLLAMA_VISION_MODEL env var (e.g. "llava").
|
||||||
data = await resp.json()
|
"""
|
||||||
return data.get("response", "").strip()
|
# 1. Claude (primary)
|
||||||
|
if CLAUDE_API_KEY:
|
||||||
|
try:
|
||||||
|
import anthropic as _anthropic
|
||||||
|
_client = _anthropic.AsyncAnthropic(api_key=CLAUDE_API_KEY)
|
||||||
|
_msg = await _client.messages.create(
|
||||||
|
model="claude-opus-4-8-20251101",
|
||||||
|
max_tokens=2048,
|
||||||
|
messages=[{"role": "user", "content": [
|
||||||
|
{"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": image_b64}},
|
||||||
|
{"type": "text", "text": prompt},
|
||||||
|
]}],
|
||||||
|
)
|
||||||
|
text = _msg.content[0].text if _msg.content else ""
|
||||||
|
log.info("[VISION] Claude vision OK")
|
||||||
|
return text, "claude"
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"[VISION] Claude failed ({e}), trying next provider")
|
||||||
|
|
||||||
|
# 2. Ollama vision model (if configured via OLLAMA_VISION_MODEL env var)
|
||||||
|
if OLLAMA_VISION_MODEL:
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession() as _sess:
|
||||||
|
_payload = {"model": OLLAMA_VISION_MODEL, "prompt": prompt,
|
||||||
|
"images": [image_b64], "stream": False}
|
||||||
|
async with _sess.post(f"{OLLAMA_HOST}/api/generate", json=_payload,
|
||||||
|
timeout=aiohttp.ClientTimeout(total=120)) as _resp:
|
||||||
|
if _resp.status == 200:
|
||||||
|
_data = await _resp.json()
|
||||||
|
text = _data.get("response", "")
|
||||||
|
log.info(f"[VISION] Ollama vision OK ({OLLAMA_VISION_MODEL})")
|
||||||
|
return text, f"ollama/{OLLAMA_VISION_MODEL}"
|
||||||
|
raise RuntimeError(f"Ollama HTTP {_resp.status}")
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"[VISION] Ollama vision failed ({e})")
|
||||||
|
|
||||||
|
# 3. No vision provider available
|
||||||
|
msg = ("[Vision unavailable] Claude credits depleted and no local vision model configured. "
|
||||||
|
"To enable: pull a vision model on Ollama (e.g. 'ollama pull llava') then set "
|
||||||
|
"OLLAMA_VISION_MODEL=llava in the jarvis-arc systemd environment.")
|
||||||
|
log.warning("[VISION] All vision providers unavailable")
|
||||||
|
return msg, "none"
|
||||||
|
|
||||||
async def handle_llm(payload: dict) -> dict:
|
async def handle_llm(payload: dict) -> dict:
|
||||||
message = payload.get("message", "")
|
message = payload.get("message", "")
|
||||||
@@ -668,44 +710,19 @@ 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 vision analysis if we have an image — llava first, Claude fallback
|
# Run Claude vision analysis if we have an image
|
||||||
analysis = ""
|
analysis = ""
|
||||||
provider_used = ""
|
provider_used = ""
|
||||||
if do_analyze and image_b64:
|
if do_analyze and image_b64:
|
||||||
try:
|
analysis, provider_used = await _vision_call(image_b64, analyze_prompt)
|
||||||
analysis = await _ollama_vision_call(image_b64, analyze_prompt)
|
log.info(f"[VISION] Analysis complete via {provider_used} ({len(analysis)} chars)")
|
||||||
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:
|
|
||||||
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 Ollama
|
# Text-only sysinfo snapshot — summarize with LLM
|
||||||
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}], "ollama")
|
analysis = await llm_call([{"role": "user", "content": prompt}], "groq")
|
||||||
provider_used = "ollama"
|
provider_used = "groq"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
analysis = f"Analysis unavailable: {e}"
|
analysis = f"Analysis unavailable: {e}"
|
||||||
|
|
||||||
@@ -758,41 +775,9 @@ 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} provider={provider}")
|
log.info(f"[VISION] Analysis: screenshot_id={screenshot_id} agent={hostname}")
|
||||||
|
|
||||||
analysis = ""
|
analysis, provider_used = await _vision_call(image_b64, prompt)
|
||||||
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:
|
|
||||||
import anthropic
|
|
||||||
client = anthropic.AsyncAnthropic(api_key=CLAUDE_API_KEY)
|
|
||||||
msg = await client.messages.create(
|
|
||||||
model="claude-opus-4-8-20251101",
|
|
||||||
max_tokens=2048,
|
|
||||||
messages=[{
|
|
||||||
"role": "user",
|
|
||||||
"content": [
|
|
||||||
{"type": "image", "source": {"type": "base64",
|
|
||||||
"media_type": "image/png", "data": image_b64}},
|
|
||||||
{"type": "text", "text": prompt},
|
|
||||||
],
|
|
||||||
}],
|
|
||||||
)
|
|
||||||
analysis = msg.content[0].text if msg.content else ""
|
|
||||||
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:
|
||||||
@@ -1053,7 +1038,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}], "ollama")
|
ai_msg = await llm_call([{"role": "user", "content": ai_prompt}], "groq")
|
||||||
# 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
|
||||||
@@ -1082,7 +1067,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, content, created_at)
|
"""INSERT INTO conversations (session_id, role, message, created_at)
|
||||||
VALUES ('guardian', 'assistant', %s, NOW())""",
|
VALUES ('guardian', 'assistant', %s, NOW())""",
|
||||||
(message,)
|
(message,)
|
||||||
)
|
)
|
||||||
@@ -1096,7 +1081,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", "ollama")
|
provider = payload.get("provider", "groq")
|
||||||
|
|
||||||
log.info(f"[GUARDIAN] SITREP requested (detail={detail})")
|
log.info(f"[GUARDIAN] SITREP requested (detail={detail})")
|
||||||
|
|
||||||
@@ -1837,7 +1822,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, content, created_at) VALUES ('guardian','assistant',%s,NOW())",
|
"INSERT INTO conversations (session_id, role, message, created_at) VALUES ('guardian','assistant',%s,NOW())",
|
||||||
(review_text,)
|
(review_text,)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2240,31 +2225,6 @@ 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())
|
||||||
@@ -2784,13 +2744,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, content AS message, created_at FROM conversations "
|
"SELECT id, 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, content AS message, created_at FROM conversations "
|
"SELECT id, 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 []
|
||||||
|
|||||||
@@ -1,738 +0,0 @@
|
|||||||
# INFRASTRUCTURE REFERENCE — COMPLETE SYSTEM MAP
|
|
||||||
**Last Updated:** 2026-06-18
|
|
||||||
**Owner:** Myron Blair — myronblair@outlook.com
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## TABLE OF CONTENTS
|
|
||||||
1. [Network Overview](#1-network-overview)
|
|
||||||
2. [Cloud Servers](#2-cloud-servers)
|
|
||||||
3. [On-Premise — Proxmox Hypervisors](#3-on-premise--proxmox-hypervisors)
|
|
||||||
4. [On-Premise — Virtual Machines](#4-on-premise--virtual-machines)
|
|
||||||
5. [NAS Storage](#5-nas-storage)
|
|
||||||
6. [Websites (all on DO)](#6-websites--all-on-do)
|
|
||||||
7. [JARVIS AI System](#7-jarvis-ai-system)
|
|
||||||
8. [Phone System (FusionPBX)](#8-phone-system-fusionpbx)
|
|
||||||
9. [Networking & VPN](#9-networking--vpn)
|
|
||||||
10. [Backup Systems](#10-backup-systems)
|
|
||||||
11. [SSH Quick Reference](#11-ssh-quick-reference)
|
|
||||||
12. [Critical Credentials Master List](#12-critical-credentials-master-list)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. NETWORK OVERVIEW
|
|
||||||
|
|
||||||
```
|
|
||||||
INTERNET
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
[Cloudflare CDN] ──────────────────────────────────────────────────────────────
|
|
||||||
│ (proxied DNS for public sites)
|
|
||||||
│
|
|
||||||
├─► [DigitalOcean 165.22.1.228] — CyberPanel/OLS — All websites (6 sites)
|
|
||||||
│
|
|
||||||
└─► [FusionPBX 134.209.72.226] — FreeSWITCH PBX (SSH via DO relay)
|
|
||||||
|
|
||||||
HOME NETWORK (FortiGate router at 10.48.200.1)
|
|
||||||
WAN: 97.154.109.245 (dynamic, DDNS: orbisne.fortiddns.com)
|
|
||||||
│
|
|
||||||
├─► PVE1 Proxmox 10.48.200.90 (primary hypervisor)
|
|
||||||
│ ├── VM 101 10.48.200.97 Home Assistant
|
|
||||||
│ ├── VM 112 10.48.200.33 Jellyfin
|
|
||||||
│ ├── VM 113 10.48.200.35 MediaStack (Sonarr/Radarr/qBT/Prowlarr)
|
|
||||||
│ ├── VM 118 10.48.200.18 Homebridge
|
|
||||||
│ ├── VM 120 10.48.200.110 NovaCPX hosting panel
|
|
||||||
│ ├── VM 210 10.48.200.210 Ollama (local LLM) (local LLM)
|
|
||||||
│ └── CT110 10.48.200.19 WireGuard exit container
|
|
||||||
│
|
|
||||||
├─► PVE2 Proxmox 10.48.200.91 (secondary hypervisor)
|
|
||||||
│ └── VM 302 10.48.200.99 NetworkBackup
|
|
||||||
│
|
|
||||||
├─► Synology NAS 10.48.200.249 — Media & backup storage
|
|
||||||
├─► Yealink T48S 10.48.200.2 — Ext 1000 (Myron Blair, Desk)
|
|
||||||
├─► Yealink T48S 10.48.200.43 — Ext 1001 (Tommy Ivy, Desk)
|
|
||||||
├─► Yealink AX86R 10.48.200.65 — Ext 1002 (Myron Blair, WiFi Work)
|
|
||||||
├─► Yealink T57W 10.48.200.3 — External SIP (United Mirror & Glass)
|
|
||||||
├─► Yealink T57W 10.48.200.83 — Ext 1003 (Kitchen)
|
|
||||||
└─► Yealink T57W 10.48.200.85 — Ext 1004 (Master Bedroom)
|
|
||||||
|
|
||||||
FortiGate Port Forwards:
|
|
||||||
orbisne.fortiddns.com:8006 → PVE1:8006 (Proxmox web UI)
|
|
||||||
orbisne.fortiddns.com:8123 → HA:8123 (Home Assistant)
|
|
||||||
orbisne.fortiddns.com:22 → HA VM:22 (SSH — key only, unreliable)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. CLOUD SERVERS
|
|
||||||
|
|
||||||
### 2A. DigitalOcean — Main Server
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **IP** | 165.22.1.228 |
|
|
||||||
| **OS** | Ubuntu 22.04 LTS |
|
|
||||||
| **Panel** | CyberPanel (OpenLiteSpeed) |
|
|
||||||
| **SSH** | `ssh root@165.22.1.228` — password: `Gonewalk1974!@#` |
|
|
||||||
| **Purpose** | All public websites (6 sites) — webhook deploy for websites |
|
|
||||||
|
|
||||||
**Key Paths:**
|
|
||||||
- All sites: `/home/<domain>/public_html/`
|
|
||||||
|
|
||||||
- Deploy log: per-site (website deploys only)
|
|
||||||
- Watchdog log: `/usr/local/lsws/logs/watchdog.log`
|
|
||||||
- Infra repo: `/opt/infra`
|
|
||||||
|
|
||||||
**Services running:**
|
|
||||||
- OpenLiteSpeed web server (`lsws`) — serves all 7 sites
|
|
||||||
- MySQL 8 — all site databases on localhost
|
|
||||||
- Redis — session/cache
|
|
||||||
- PHP 8.5 (`lsphp85`) — runtime for all sites
|
|
||||||
- Cron jobs: website deploy runner (every 1 min), watchdog (every 5 min)
|
|
||||||
|
|
||||||
**CyberPanel Web UI:** `https://165.22.1.228:8090`
|
|
||||||
Login: `myron / Joker1974!!!`
|
|
||||||
|
|
||||||
**phpMyAdmin:** `https://165.22.1.228/phpmyadmin`
|
|
||||||
Login: `myron / Joker1974!!!`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2B. FusionPBX / FreeSWITCH — PBX Server
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **IP** | 134.209.72.226 |
|
|
||||||
| **OS** | Debian (DigitalOcean droplet) |
|
|
||||||
| **SSH** | Direct via Tailscale: `ssh root@100.74.46.120` — password: `Joker1974!@#` |
|
|
||||||
| **Direct SSH** | Only from: 107.178.2.130 / 97.154.109.245 |
|
|
||||||
| **Purpose** | VoIP phone system — handles all inbound/outbound calls |
|
|
||||||
|
|
||||||
**Web UI:** `https://fusion.orbishosting.com`
|
|
||||||
Login: `admin / fY7XP5swgtpbzrYLhkeVYkA4744`
|
|
||||||
|
|
||||||
**Database:** PostgreSQL
|
|
||||||
User: `fusionpbx` / Password: `pSJaF9mUJqPr4Sj5mwJyRqvCCpc` / Host: 127.0.0.1
|
|
||||||
|
|
||||||
**SIP Trunk:** SignalWire
|
|
||||||
DID: +1 (817) 764-5007
|
|
||||||
Gateway: `signalwire` on external profile (port 5080, UDP)
|
|
||||||
|
|
||||||
**How calls flow:**
|
|
||||||
```
|
|
||||||
Caller → SignalWire SIP → FusionPBX:5080 → IVR (ext 900) → Ring extensions
|
|
||||||
Outbound: Phone → FusionPBX:5080 → SignalWire → PSTN
|
|
||||||
```
|
|
||||||
|
|
||||||
**SSH Relay Command:**
|
|
||||||
```bash
|
|
||||||
sshpass -p 'Gonewalk1974!@#' ssh -o StrictHostKeyChecking=no root@165.22.1.228 \
|
|
||||||
'sshpass -p "Joker1974!@#" ssh -o StrictHostKeyChecking=no root@134.209.72.226 "COMMAND"'
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. ON-PREMISE — PROXMOX HYPERVISORS
|
|
||||||
|
|
||||||
### PVE1 — Primary Hypervisor
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **Local IP** | 10.48.200.90 |
|
|
||||||
| **External** | orbisne.fortiddns.com (FortiGate DDNS — auto-updates on WAN IP change) |
|
|
||||||
| **OS** | Proxmox VE 8.x |
|
|
||||||
| **SSH** | `ssh root@orbisne.fortiddns.com` OR `ssh root@10.48.200.90` — password: `Joker1974!!!` |
|
|
||||||
| **Web UI** | `https://orbisne.fortiddns.com:8006` — `root / Joker1974!!!` |
|
|
||||||
| **Purpose** | Runs VMs 101, 112, 113, 118, 120, 210, CT110 |
|
|
||||||
|
|
||||||
**Useful commands:**
|
|
||||||
```bash
|
|
||||||
qm list # list all VMs
|
|
||||||
qm start/stop/restart <VMID> # control VMs
|
|
||||||
qm guest exec <VMID> -- bash -c "cmd" # run command inside VM (requires QEMU agent)
|
|
||||||
```
|
|
||||||
|
|
||||||
**JARVIS API Token:** `root@pam!jarvis=c45b5feb-f9a9-445d-a626-14fbb959f78b`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### PVE2 — Secondary Hypervisor
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **Local IP** | 10.48.200.91 |
|
|
||||||
| **OS** | Proxmox VE 8.x |
|
|
||||||
| **SSH** | `ssh root@10.48.200.91` — password: `Joker1974!!!` |
|
|
||||||
| **Web UI** | `https://10.48.200.91:8006` — `root / Joker1974!!!` |
|
|
||||||
| **Purpose** | Runs VM 302 (NetworkBackup); part of shared Proxmox cluster with PVE1 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. ON-PREMISE — VIRTUAL MACHINES
|
|
||||||
|
|
||||||
### VM 101 — Home Assistant (PVE1)
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **IP** | 10.48.200.97 |
|
|
||||||
| **OS** | Ubuntu + Home Assistant OS/Supervised |
|
|
||||||
| **Web UI** | `http://orbisne.fortiddns.com:8123` — `myron / [HA password]` |
|
|
||||||
| **SSH** | Via HA web terminal only (Settings → Add-ons → Advanced SSH & Web Terminal) |
|
|
||||||
| **Purpose** | Smart home automation — 212 entities (lights, switches, scenes, sensors) |
|
|
||||||
| **JARVIS Agent** | ID: `homeassistant_ha` — pushes entity states to JARVIS every 10s |
|
|
||||||
|
|
||||||
**JARVIS ↔ HA Integration:**
|
|
||||||
- HA custom component at `/config/custom_components/jarvis_agent/`
|
|
||||||
- Pushes all entity state changes to JARVIS `/api/agent/ha_state` (debounced 2s)
|
|
||||||
- JARVIS admin toggles → queued in `agent_commands` table → HA executes natively
|
|
||||||
- HA Long-lived Token (Jarvis2): `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIzNmI0N2I1Njk5ZGQ0MTQ2ODMwZWFmYjZiYTQ1MjJkMSIsImlhdCI6MTc4MDIwMzU5NCwiZXhwIjoyMDk1NTYzNTk0fQ.sYRok-jRDlA4lFgWxLQELcEjkJNGQdprk6ZziLwLtXE`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### VM 112 — Jellyfin Media Server (PVE1)
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **IP** | 10.48.200.33 |
|
|
||||||
| **OS** | Ubuntu 22.04 LTS |
|
|
||||||
| **SSH** | `ssh root@10.48.200.33` — password: `Joker1974!!!` (enabled 2026-06-14) |
|
|
||||||
| **Web UI** | `http://10.48.200.33:8096` |
|
|
||||||
| **Purpose** | Media streaming server — Movies and TV shows |
|
|
||||||
| **JARVIS Agent** | Not yet installed |
|
|
||||||
|
|
||||||
**Media Libraries:**
|
|
||||||
- Movies: `/mnt/mediastack/movies` — NFS from MediaStack (10.48.200.35:/media/movies)
|
|
||||||
- TV: `/mnt/mediastack/tv` — NFS from MediaStack (10.48.200.35:/media/tv)
|
|
||||||
|
|
||||||
**NFS chain:** Jellyfin → MediaStack → Synology NAS (`/volume1/video/movies` and `/volume1/video/tv`)
|
|
||||||
|
|
||||||
**Admin token:** `7c0ccf78b91d4b5bafa607f585f24f2d`
|
|
||||||
|
|
||||||
**If library scan needed:**
|
|
||||||
```bash
|
|
||||||
curl -X POST "http://10.48.200.33:8096/Library/Refresh" \
|
|
||||||
-H "X-Emby-Token: 7c0ccf78b91d4b5bafa607f585f24f2d"
|
|
||||||
```
|
|
||||||
|
|
||||||
**If NFS stale after MediaStack changes:**
|
|
||||||
```bash
|
|
||||||
umount -l /mnt/mediastack/movies && umount -l /mnt/mediastack/tv
|
|
||||||
mount /mnt/mediastack/movies && mount /mnt/mediastack/tv
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### VM 113 — MediaStack (PVE1)
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **IP** | 10.48.200.35 |
|
|
||||||
| **OS** | Ubuntu 24.04 LTS |
|
|
||||||
| **SSH** | Via PVE1: `ssh -i /root/.ssh/id_rsa root@10.48.200.35` (no direct access from DO) |
|
|
||||||
| **Purpose** | Automated media download pipeline + NFS server to Jellyfin |
|
|
||||||
| **JARVIS Agent** | ID: `MediaStack_2c00b1b8` |
|
|
||||||
|
|
||||||
**Services:**
|
|
||||||
| Service | Port | Login | API Key |
|
|
||||||
|---------|------|-------|---------|
|
|
||||||
| qBittorrent | :8080 | `admin / Joker1974!!!` | — |
|
|
||||||
| Sonarr | :8989 | `admin / Joker1974!!!` | `b43e04350a594846b4ee95261c29e9e0` |
|
|
||||||
| Radarr | :7878 | `admin / Joker1974!!!` | `53c4268360444feeae5f98c0cc24e0e3` |
|
|
||||||
| Prowlarr | :9696 | `admin / Joker1974!!!` | `9d0ce6c5660743b5bf1c7951efc62252` |
|
|
||||||
|
|
||||||
**All services run as root** — required by Synology NFS ACL (only root can write).
|
|
||||||
|
|
||||||
**VPN:** NordVPN — `nordlynx` WireGuard interface — exit IP 181.214.226.188 (US Dallas)
|
|
||||||
All download traffic exits via NordVPN. If downloads stall, check: `ip rule show` for rules 32764/32765.
|
|
||||||
|
|
||||||
**Media Flow:**
|
|
||||||
```
|
|
||||||
IPTorrents (Prowlarr) → Sonarr/Radarr search → qBittorrent download
|
|
||||||
→ /mnt/nas/video/downloads (NAS)
|
|
||||||
→ Sonarr/Radarr import → /mnt/nas/video/tv or /mnt/nas/video/movies (NAS)
|
|
||||||
→ NFS → Jellyfin /mnt/mediastack/movies or /mnt/mediastack/tv
|
|
||||||
```
|
|
||||||
|
|
||||||
**Indexer:** IPTorrents via Prowlarr cookie auth
|
|
||||||
Cookie: `uid=2237410; pass=JzLP2niTWxBJAZIU3yvtLbJzD55kdLeB`
|
|
||||||
(Expires — if search fails, log into iptorrents.com, copy uid+pass cookies)
|
|
||||||
|
|
||||||
**If Radarr/Sonarr shows "0 active indexers":**
|
|
||||||
```bash
|
|
||||||
systemctl stop radarr
|
|
||||||
sqlite3 /var/lib/radarr/radarr.db "DELETE FROM IndexerStatus WHERE ProviderId=1;"
|
|
||||||
systemctl start radarr
|
|
||||||
```
|
|
||||||
|
|
||||||
**SSH from DO:**
|
|
||||||
```bash
|
|
||||||
sshpass -p 'Joker1974!!!' ssh -o StrictHostKeyChecking=no root@10.48.200.90 \
|
|
||||||
'ssh -o StrictHostKeyChecking=no -i /root/.ssh/id_rsa root@10.48.200.35 "COMMAND"'
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### VM 118 — Homebridge (PVE1)
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **IP** | 10.48.200.18 |
|
|
||||||
| **OS** | Linux |
|
|
||||||
| **SSH** | `ssh myron@10.48.200.18` — password: `Joker1974!` |
|
|
||||||
| **Purpose** | Apple HomeKit bridge — exposes non-HomeKit devices to Apple Home app |
|
|
||||||
| **JARVIS Agent** | ID: `homebridge_b57cbaea` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### VM 120 — NovaCPX Hosting Panel (PVE1)
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **IP** | 10.48.200.110 |
|
|
||||||
| **OS** | Ubuntu 24.04 LTS |
|
|
||||||
| **SSH** | `ssh root@10.48.200.110` — password: `Joker1974!!!` (direct, no PVE hop) |
|
|
||||||
| **Purpose** | Custom web hosting control panel (cPanel alternative), v1.0.27 |
|
|
||||||
| **JARVIS Agent** | ID: `novacpx_e3b07264` |
|
|
||||||
|
|
||||||
**Ports:**
|
|
||||||
| Port | Panel |
|
|
||||||
|------|-------|
|
|
||||||
| :8880 | User panel |
|
|
||||||
| :8881 | Reseller panel |
|
|
||||||
| :8882 | Admin panel |
|
|
||||||
| :8883 | Roundcube webmail |
|
|
||||||
|
|
||||||
**Admin:** `https://10.48.200.110:8882` — `admin / Admin2026!`
|
|
||||||
**phpMyAdmin:** `http://10.48.200.110/phpmyadmin`
|
|
||||||
|
|
||||||
**File Paths:**
|
|
||||||
- Web root: `/srv/novacpx/public/`
|
|
||||||
- DB (SQLite): `/var/lib/novacpx/panel.db`
|
|
||||||
- Config: `/etc/novacpx/config.ini`
|
|
||||||
- Git repo: `/opt/novacpx-src/`
|
|
||||||
- GitHub: `myronblair/novacpx` (auto-deploy on push to `main`)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### VM 210 — Ollama Local LLM (PVE1)
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **IP** | 10.48.200.210 |
|
|
||||||
| **OS** | Ubuntu (cloud image) |
|
|
||||||
| **SSH** | `ssh myron@10.48.200.95` — password: `Joker1974!` (then `sudo`) |
|
|
||||||
| **Purpose** | Local AI inference — runs llama3.2 model for JARVIS Tier 1 chat |
|
|
||||||
| **API** | `http://10.48.200.210:11434` (Ollama REST API) |
|
|
||||||
| **JARVIS Agent** | ID: `ollama-ai_ubuntu` |
|
|
||||||
|
|
||||||
**JARVIS uses this as Tier 1 AI** — if Ollama is down, falls back to Groq (cloud).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### VM 302 — NetworkBackup (PVE2)
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **IP** | 10.48.200.99 |
|
|
||||||
| **OS** | Ubuntu/Linux |
|
|
||||||
| **SSH** | `ssh myron@10.48.200.99` — password: `Joker1974!` (then `sudo`) |
|
|
||||||
| **Purpose** | Network backup storage / backup operations |
|
|
||||||
| **JARVIS Agent** | ID: `networkbackup_NetworkB` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### CT110 — WireGuard Exit Container (PVE1)
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **IP** | 10.48.200.19 / 10.48.200.67 |
|
|
||||||
| **Purpose** | Legacy WireGuard exit tunnel to DO (10.200.0.4 via wg-exit) — currently NOT used by MediaStack/Jellyfin |
|
|
||||||
| **Note** | MediaStack uses NordVPN directly; Jellyfin uses wg1 peer on MediaStack for NFS only |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. NAS STORAGE
|
|
||||||
|
|
||||||
### Synology NAS
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **IP** | 10.48.200.249 |
|
|
||||||
| **Login** | `nas / Joker1974!!!` |
|
|
||||||
| **DSM Web UI** | `http://10.48.200.249:5000` |
|
|
||||||
| **Purpose** | Primary media and download storage |
|
|
||||||
|
|
||||||
**NFS Share:** `/volume1/video` (exported to MediaStack only)
|
|
||||||
|
|
||||||
**Directory structure:**
|
|
||||||
```
|
|
||||||
/volume1/video/
|
|
||||||
movies/ ← Radarr imports here; NFS-exported to Jellyfin via MediaStack
|
|
||||||
tv/ ← Sonarr imports here; NFS-exported to Jellyfin via MediaStack
|
|
||||||
downloads/ ← qBittorrent downloads here (temp)
|
|
||||||
incomplete/ ← in-progress torrents
|
|
||||||
```
|
|
||||||
|
|
||||||
**Important:** Synology NFS ACL only allows root to write. All services on MediaStack run as root.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. WEBSITES (ALL ON DO)
|
|
||||||
|
|
||||||
All sites are at `/home/<domain>/public_html/` on DO (165.22.1.228).
|
|
||||||
**Auto-deploy:** Push to `main` on GitHub → webhook → server pulls in ~1 min.
|
|
||||||
**GitHub PAT:** `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` (expires ~2026-08-20)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### jarvis.orbishosting.com — JARVIS AI Dashboard (MOVED TO PVE1 VM 211)
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **URL** | http://jarvis.orbishosting.com:1972 |
|
|
||||||
| **Path** | `/var/www/jarvis/ (on JARVIS VM 10.48.200.211)` |
|
|
||||||
| **GitHub** | `myronblair/jarvis` |
|
|
||||||
| **Login** | `myron / Joker1974!!!` |
|
|
||||||
| **Purpose** | Iron Man-style AI home dashboard with voice control, smart home, media, planner |
|
|
||||||
|
|
||||||
See Section 7 for full JARVIS details.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### tomsjavajive.com — Tom's Java Jive
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **URL** | https://tomsjavajive.com |
|
|
||||||
| **Path** | `/home/tomsjavajive.com/public_html/` |
|
|
||||||
| **GitHub** | `myronblair/tomsjavajive` |
|
|
||||||
| **Purpose** | Coffee shop e-commerce — products, orders, loyalty, wallet, reviews |
|
|
||||||
| **Admin URL** | `https://tomsjavajive.com/admin/` |
|
|
||||||
| **Admin Login** | `admin@tomsjavajive.com / Joker1974!!!` OR `myronblair@outlook.com / Joker1974!!!` |
|
|
||||||
| **DB** | `toms_tjj_db / toms_tjj_user / +60wlPc+55e@gFq4` |
|
|
||||||
| **Email** | CyberMail API key: `sk_live_7f9b0f9a29f6de31a0d229d4af75d56b094ad724fc58a57d` |
|
|
||||||
| **Email From** | `noreply@tomsjavajive.com` / `Toms Java Jive` (set in DB settings table) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### epictravelexpeditions.com — Epic Travel Expeditions
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **URL** | https://epictravelexpeditions.com |
|
|
||||||
| **Path** | `/home/epictravelexpeditions.com/public_html/` |
|
|
||||||
| **GitHub** | `myronblair/epictravelexpeditions` |
|
|
||||||
| **Purpose** | Travel booking / expeditions website |
|
|
||||||
| **DB** | `epic_travel_db` (see `api/config.php`) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### parkerslingshot.epictravelexpeditions.com — Parker Slingshot (OLD)
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **URL** | https://parkerslingshot.epictravelexpeditions.com |
|
|
||||||
| **Path** | `/home/epictravelexpeditions.com/parkerslingshot/` |
|
|
||||||
| **GitHub** | `myronblair/parkerslingshot` |
|
|
||||||
| **Purpose** | Old slingshot rental site (superseded by parkerslingshotrentals.com) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### parkerslingshotrentals.com — Parker Slingshot Rentals (LIVE)
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **URL** | https://www.parkerslingshotrentals.com |
|
|
||||||
| **Path** | `/home/parkerslingshotrentals.com/public_html/` |
|
|
||||||
| **GitHub** | `myronblair/parkerslingshotrentals` |
|
|
||||||
| **Purpose** | Polaris Slingshot rental — bookings, e-signature waiver, admin management |
|
|
||||||
| **Admin** | `/admin/index.php` — `admin / Parker2026!` |
|
|
||||||
| **DB** | `park_slingshot / park_slingshotuser / 4@rxg*8kovxCr7w6` |
|
|
||||||
| **Square** | Production token: `EAAAl3FsAu_2ri8kZE_ENEyi2T_C8HXXm5XQFY6Lbnd8SX6FqYp8J_upUeXNYh7v` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### orbishosting.com — Orbis Hosting (Landing Page)
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **URL** | https://orbishosting.com |
|
|
||||||
| **Path** | `/home/orbishosting.com/public_html/` |
|
|
||||||
| **GitHub** | `myronblair/orbishosting` |
|
|
||||||
| **Purpose** | Public landing page for Orbis Hosting brand |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### orbis.orbishosting.com — Orbis Hosting Portal
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **URL** | https://orbis.orbishosting.com |
|
|
||||||
| **Path** | `/home/orbis.orbishosting.com/public_html/` |
|
|
||||||
| **GitHub** | `myronblair/orbis-hosting-portal` |
|
|
||||||
| **Purpose** | Customer-facing hosting portal |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### tomtomgames.com — TomTom Games
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **URL** | https://tomtomgames.com |
|
|
||||||
| **Path** | `/home/tomtomgames.com/public_html/` |
|
|
||||||
| **GitHub** | `myronblair/tomtomgames` |
|
|
||||||
| **Purpose** | Gaming website |
|
|
||||||
| **DB** | `tomtom_games_db` (see config) |
|
|
||||||
| **Email** | CyberMail API key: `sk_live_7f9b...` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. JARVIS AI SYSTEM
|
|
||||||
|
|
||||||
**URL:** http://jarvis.orbishosting.com:1972
|
|
||||||
**Files:** `/var/www/jarvis/` on JARVIS VM (PVE1 VM 211 — 10.48.200.211, 8 cores, 16GB RAM)
|
|
||||||
**DB:** `jarvis_db` — `jarvis_user / J4rv1s_Pr0t0c0l_2026!`
|
|
||||||
**Login:** `myron / Joker1974!!!`
|
|
||||||
**Admin portal:** http://jarvis.orbishosting.com:1972/admin
|
|
||||||
|
|
||||||
### Architecture (end-to-end)
|
|
||||||
|
|
||||||
```
|
|
||||||
Voice (browser mic)
|
|
||||||
→ SpeechRecognition API
|
|
||||||
→ Wake phrase: "wake up JARVIS" / "daddy's home"
|
|
||||||
→ "JARVIS [command]" triggers action
|
|
||||||
→ /api/chat.php (4-tier AI)
|
|
||||||
Tier 0.7: KB intents / planner (tasks, appointments)
|
|
||||||
Tier 1: Knowledge Base (MySQL)
|
|
||||||
Tier 1.5: Ollama (10.48.200.210:11434, llama3.2) — local LLM
|
|
||||||
Tier 2: Groq (cloud, model: compound-beta-mini)
|
|
||||||
Tier 3: Claude API (Anthropic, fallback)
|
|
||||||
→ ElevenLabs TTS → browser speaker
|
|
||||||
```
|
|
||||||
|
|
||||||
### Deploy Pipeline
|
|
||||||
```
|
|
||||||
Code edit → git push → GitHub webhook → /webhook.php (HMAC verified)
|
|
||||||
→ /tmp/jarvis-deploy-queue.txt → /usr/local/bin/jarvis-deploy.sh (cron 1min)
|
|
||||||
→ git pull + PHP syntax check → deploy or auto-revert
|
|
||||||
```
|
|
||||||
Webhook secret: `4c8805f0285214ff0a0602b5880270b935f36a896946c7f1`
|
|
||||||
|
|
||||||
### Agent System
|
|
||||||
Agents installed on all servers — phone home every 10s (heartbeat) / 30s (metrics).
|
|
||||||
Registration key: `f846a9aaf7ce9a61742c63c87c4186052a71d2a580c65518`
|
|
||||||
Install command: `curl -sk http://10.48.200.211/install-agent.sh | bash -s <hostname> <linux|proxmox>`
|
|
||||||
|
|
||||||
### Self-Healing Watchdog
|
|
||||||
`/usr/local/bin/jarvis-watchdog.sh` — runs every 5 min (root cron on DO)
|
|
||||||
Restarts: lsws, mysql, redis if down
|
|
||||||
Restarts offline Proxmox VM agents via `qm guest exec`
|
|
||||||
|
|
||||||
### Cron Jobs (DO server)
|
|
||||||
| Schedule | Script | Purpose |
|
|
||||||
|----------|--------|---------|
|
|
||||||
| Every 1 min | `jarvis-deploy.sh` | Process GitHub deploy queue |
|
|
||||||
| Every 3 min | `facts_collector.php` | Collect agent metrics, KB facts, site health |
|
|
||||||
| Every 5 min | `stats_cache.php` | Weather, news, Proxmox stats refresh |
|
|
||||||
| Every 5 min | `jarvis-watchdog.sh` | Self-healing: restart dead services |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. PHONE SYSTEM (FUSIONPBX)
|
|
||||||
|
|
||||||
### Extensions
|
|
||||||
| Ext | Name | Phone | IP | SIP Password |
|
|
||||||
|-----|------|-------|----|-------------|
|
|
||||||
| 1000 | Myron Blair — Desk | Yealink T48S | 10.48.200.2 | `Xk9mPw3nQv7rLs2t` |
|
|
||||||
| 1001 | Tommy Ivy — Desk | Yealink T48S | 10.48.200.43 | `Tv8xNm4pWq6rZs3k` |
|
|
||||||
| 1002 | Myron Blair — WiFi Work | Yealink AX86R | 10.48.200.65 | `yXHaJTwa8rj?$GkrVFQB` |
|
|
||||||
| 1003 | Kitchen | Yealink T57W | 10.48.200.83 | — |
|
|
||||||
| 1004 | Master Bedroom | Yealink T57W | 10.48.200.85 | — |
|
|
||||||
| 1010 | Parker County Slingshot | Virtual (voicemail only) | — | — |
|
|
||||||
| 1011 | Epic Travel Expeditions | Virtual (voicemail only) | — | — |
|
|
||||||
| 1012 | Tom's Java Jive | Virtual (voicemail only) | — | — |
|
|
||||||
| 900 | IVR | — | — | (auto-attendant) |
|
|
||||||
|
|
||||||
**Phone SIP Settings (all phones):**
|
|
||||||
- Server: `134.209.72.226`
|
|
||||||
- Port: `5080`
|
|
||||||
- Transport: UDP
|
|
||||||
|
|
||||||
**Provisioning URL:** `https://fusion.orbishosting.com/app/provision/`
|
|
||||||
(Username: `provision-master`, Password: `Joker1974!!!`)
|
|
||||||
|
|
||||||
### Call Flow
|
|
||||||
```
|
|
||||||
Inbound (+18177645007)
|
|
||||||
→ SignalWire → FusionPBX:5080 (UDP)
|
|
||||||
→ signalwire-inbound dialplan (catch-all ^.*$)
|
|
||||||
→ IVR ext 900 (ivr_menu_16k.wav)
|
|
||||||
→ Routes to extensions 1000/1001/1002/1003/1004
|
|
||||||
|
|
||||||
Outbound
|
|
||||||
→ Phone → FusionPBX:5080
|
|
||||||
→ signalwire gateway → SignalWire → PSTN
|
|
||||||
```
|
|
||||||
|
|
||||||
### FreeSWITCH CLI Commands
|
|
||||||
```bash
|
|
||||||
fs_cli -x "sofia status profile external reg" # check registrations
|
|
||||||
fs_cli -x "sofia xmlstatus gateway" # check SignalWire gateway
|
|
||||||
fs_cli -x "reloadxml" # reload config (safe)
|
|
||||||
fs_cli -x "reloadacl" # reload ACL (safe)
|
|
||||||
# AVOID: sofia profile external restart (drops all phone registrations)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. NETWORKING & VPN
|
|
||||||
|
|
||||||
### FortiGate Firewall
|
|
||||||
- WAN IP: 97.154.109.245 (dynamic)
|
|
||||||
- DDNS: `orbisne.fortiddns.com` (FortiGate auto-updates on IP change)
|
|
||||||
- Blocks: outbound port 53 (DNS) — MediaStack uses PVE1 dnsmasq (10.48.200.90) as resolver → 100.100.100.100
|
|
||||||
|
|
||||||
**Port Forwards:**
|
|
||||||
| External Port | Internal Destination | Purpose |
|
|
||||||
|--------------|---------------------|---------|
|
|
||||||
| :8006 | PVE1:8006 | Proxmox web UI |
|
|
||||||
| :8123 | HA VM:8123 | Home Assistant |
|
|
||||||
| :22 | HA VM:22 | HA SSH (unreliable) |
|
|
||||||
|
|
||||||
### WireGuard — Jellyfin ↔ MediaStack
|
|
||||||
- MediaStack runs WireGuard server on `wg1` (port 51820, subnet 10.200.0.1/24)
|
|
||||||
- Jellyfin peer: 10.200.0.3 (active handshake)
|
|
||||||
- Used for NFS media file access ONLY — not internet VPN
|
|
||||||
|
|
||||||
### NordVPN — MediaStack Internet Traffic
|
|
||||||
- Interface: `nordlynx` on MediaStack
|
|
||||||
- Exit IP: 181.214.226.188 (US Dallas)
|
|
||||||
- Policy routing: table 205 (all traffic via nordlynx), managed by `nordvpn-routing.service`
|
|
||||||
- Required for IPTorrents access (blocks non-VPN IPs)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. BACKUP SYSTEMS
|
|
||||||
|
|
||||||
### DO Server Backup
|
|
||||||
- **Repo:** `myronblair/do-server-config`
|
|
||||||
- **Schedule:** Weekly, Sunday 4am
|
|
||||||
- **Launcher:** `/usr/local/bin/do-server-backup` on DO
|
|
||||||
- **Covers:** Scripts, systemd units, WireGuard, OLS vhosts, cron, MySQL credentials
|
|
||||||
- **Restore:** 8-phase wizard in `restore.sh`
|
|
||||||
- **DB backups:** `jarvis-backup.sh` runs daily (separate)
|
|
||||||
|
|
||||||
### Proxmox Config Backup
|
|
||||||
- **Repo:** `myronblair/proxmox-config`
|
|
||||||
- **Schedule:** Weekly, Sunday 3am (both PVE1 and PVE2)
|
|
||||||
- **Launcher:** `/usr/local/bin/proxmox-backup` on each node
|
|
||||||
- **Covers:** VM .conf files, network, cron, systemd, scripts
|
|
||||||
- **VM disks:** Covered by Proxmox Backup Server (PBS)
|
|
||||||
|
|
||||||
### FusionPBX Backup
|
|
||||||
- **Repo:** `myronblair/fusionpbx-config`
|
|
||||||
- **Schedule:** Weekly, Sunday 5am
|
|
||||||
- **Launcher:** `/usr/local/bin/fusionpbx-backup`
|
|
||||||
- **Covers:** PostgreSQL dump (gzip, ~29MB) + FreeSWITCH configs
|
|
||||||
- **Restore:** 10-phase wizard in `restore.sh`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 11. SSH QUICK REFERENCE
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# DO (main web server)
|
|
||||||
sshpass -p 'Gonewalk1974!@#' ssh -o StrictHostKeyChecking=no root@165.22.1.228
|
|
||||||
|
|
||||||
# FusionPBX (must relay via DO)
|
|
||||||
sshpass -p 'Gonewalk1974!@#' ssh root@165.22.1.228 \
|
|
||||||
'sshpass -p "Joker1974!@#" ssh root@134.209.72.226 "CMD"'
|
|
||||||
|
|
||||||
# PVE1 (direct or via DDNS)
|
|
||||||
sshpass -p 'Joker1974!!!' ssh -o StrictHostKeyChecking=no root@orbisne.fortiddns.com
|
|
||||||
sshpass -p 'Joker1974!!!' ssh -o StrictHostKeyChecking=no root@10.48.200.90
|
|
||||||
|
|
||||||
# PVE2
|
|
||||||
sshpass -p 'Joker1974!!!' ssh -o StrictHostKeyChecking=no root@10.48.200.91
|
|
||||||
|
|
||||||
# MediaStack (via PVE1)
|
|
||||||
sshpass -p 'Joker1974!!!' ssh root@10.48.200.90 \
|
|
||||||
'ssh -i /root/.ssh/id_rsa root@10.48.200.35 "CMD"'
|
|
||||||
|
|
||||||
# Jellyfin (direct, password enabled 2026-06-14)
|
|
||||||
sshpass -p 'Joker1974!!!' ssh -o StrictHostKeyChecking=no root@10.48.200.33
|
|
||||||
|
|
||||||
# NovaCPX (direct)
|
|
||||||
sshpass -p 'Joker1974!!!' ssh -o StrictHostKeyChecking=no root@10.48.200.110
|
|
||||||
|
|
||||||
# Ollama / Homebridge / NetworkBackup (myron user, then sudo)
|
|
||||||
sshpass -p 'Joker1974!' ssh myron@10.48.200.95 # Ollama
|
|
||||||
sshpass -p 'Joker1974!' ssh myron@10.48.200.18 # Homebridge
|
|
||||||
sshpass -p 'Joker1974!' ssh myron@10.48.200.99 # NetworkBackup
|
|
||||||
|
|
||||||
# Run command inside VM via Proxmox (requires QEMU agent installed)
|
|
||||||
sshpass -p 'Joker1974!!!' ssh root@10.48.200.90 \
|
|
||||||
'qm guest exec 210 -- bash -c "CMD"'
|
|
||||||
```
|
|
||||||
|
|
||||||
**Password fallback order:** `Joker1974!@#` → `Joker1974!!!` → `Joker1974!`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 12. CRITICAL CREDENTIALS MASTER LIST
|
|
||||||
|
|
||||||
### SSH / Root Access
|
|
||||||
| System | User | Password | Notes |
|
|
||||||
|--------|------|----------|-------|
|
|
||||||
| DO (165.22.1.228) | root | `Gonewalk1974!@#` | Main web server |
|
|
||||||
| FusionPBX (134.209.72.226) | root | `Joker1974!@#` | Via DO relay |
|
|
||||||
| PVE1 (10.48.200.90) | root | `Joker1974!!!` | Also via DDNS |
|
|
||||||
| PVE2 (10.48.200.91) | root | `Joker1974!!!` | |
|
|
||||||
| MediaStack (10.48.200.35) | root | key only | Via PVE1 (`/root/.ssh/id_rsa`) |
|
|
||||||
| Jellyfin (10.48.200.33) | root | `Joker1974!!!` | Enabled 2026-06-14 |
|
|
||||||
| NovaCPX (10.48.200.110) | root | `Joker1974!!!` | Direct SSH works |
|
|
||||||
| Ollama / Homebridge / Backup VMs | myron | `Joker1974!` | Then sudo |
|
|
||||||
|
|
||||||
### Web Panels & Admin
|
|
||||||
| System | URL | User | Password |
|
|
||||||
|--------|-----|------|----------|
|
|
||||||
| CyberPanel | https://165.22.1.228:8090 | myron | `Joker1974!!!` |
|
|
||||||
| phpMyAdmin (DO) | https://165.22.1.228/phpmyadmin | myron | `Joker1974!!!` |
|
|
||||||
| Proxmox PVE1 | https://orbisne.fortiddns.com:8006 | root | `Joker1974!!!` |
|
|
||||||
| Proxmox PVE2 | https://10.48.200.91:8006 | root | `Joker1974!!!` |
|
|
||||||
| JARVIS | http://jarvis.orbishosting.com:1972 | myron | `Joker1974!!!` |
|
|
||||||
| JARVIS Admin | http://jarvis.orbishosting.com:1972/admin | myron | `Joker1974!!!` |
|
|
||||||
| FusionPBX | https://fusion.orbishosting.com | admin | `fY7XP5swgtpbzrYLhkeVYkA4744` |
|
|
||||||
| Home Assistant | http://orbisne.fortiddns.com:8123 | myron | (HA password) |
|
|
||||||
| NovaCPX Admin | https://10.48.200.110:8882 | admin | `Admin2026!` |
|
|
||||||
| Jellyfin | http://10.48.200.33:8096 | — | token: `7c0ccf78b91d4b5bafa607f585f24f2d` |
|
|
||||||
| qBittorrent | http://10.48.200.35:8080 | admin | `Joker1974!!!` |
|
|
||||||
| Sonarr | http://10.48.200.35:8989 | admin | `Joker1974!!!` |
|
|
||||||
| Radarr | http://10.48.200.35:7878 | admin | `Joker1974!!!` |
|
|
||||||
| Prowlarr | http://10.48.200.35:9696 | admin | `Joker1974!!!` |
|
|
||||||
| Synology NAS | http://10.48.200.249:5000 | nas | `Joker1974!!!` |
|
|
||||||
| Parker Slingshot Admin | https://parkerslingshotrentals.com/admin | admin | `Parker2026!` |
|
|
||||||
| TJJ Admin | https://tomsjavajive.com/admin | `admin@tomsjavajive.com` OR `myronblair@outlook.com` | `Joker1974!!!` |
|
|
||||||
|
|
||||||
### Databases
|
|
||||||
| Site | DB Name | DB User | DB Password |
|
|
||||||
|------|---------|---------|-------------|
|
|
||||||
| JARVIS | `jarvis_db` | `jarvis_user` | `J4rv1s_Pr0t0c0l_2026!` |
|
|
||||||
| Tom's Java Jive | `toms_tjj_db` | `toms_tjj_user` | `+60wlPc+55e@gFq4` |
|
|
||||||
| Parker Slingshot Rentals | `park_slingshot` | `park_slingshotuser` | `4@rxg*8kovxCr7w6` |
|
|
||||||
| Epic Travel | `epic_travel_db` | (see config.php) | (see config.php) |
|
|
||||||
| Epic/Parker Slingshot | `epic_parkersling` | `epic_parkersling` | `Joker1974!!!` |
|
|
||||||
| NovaCPX | SQLite: `/var/lib/novacpx/panel.db` | — | — |
|
|
||||||
| FusionPBX | PostgreSQL | `fusionpbx` | `pSJaF9mUJqPr4Sj5mwJyRqvCCpc` |
|
|
||||||
| MySQL root (DO) | — | root | `b71e5c1a8c7457541b9c1db822de37adfa271926a38b6c20` |
|
|
||||||
|
|
||||||
### API Keys
|
|
||||||
| Service | Key |
|
|
||||||
|---------|-----|
|
|
||||||
| GitHub PAT | `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` (exp ~2026-08-20) |
|
|
||||||
| JARVIS Agent Registration | `f846a9aaf7ce9a61742c63c87c4186052a71d2a580c65518` |
|
|
||||||
| Proxmox API Token | `root@pam!jarvis=c45b5feb-f9a9-445d-a626-14fbb959f78b` |
|
|
||||||
| HA Long-lived Token | `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIzNmI0N2I1Njk5ZGQ0MTQ2ODMwZWFmYjZiYTQ1MjJkMSIsImlhdCI6MTc4MDIwMzU5NCwiZXhwIjoyMDk1NTYzNTk0fQ.sYRok-jRDlA4lFgWxLQELcEjkJNGQdprk6ZziLwLtXE` |
|
|
||||||
| Sonarr API | `b43e04350a594846b4ee95261c29e9e0` |
|
|
||||||
| Radarr API | `53c4268360444feeae5f98c0cc24e0e3` |
|
|
||||||
| Prowlarr API | `9d0ce6c5660743b5bf1c7951efc62252` |
|
|
||||||
| Jellyfin Admin Token | `7c0ccf78b91d4b5bafa607f585f24f2d` |
|
|
||||||
| Square (Parker) Production | `EAAAl3FsAu_2ri8kZE_ENEyi2T_C8HXXm5XQFY6Lbnd8SX6FqYp8J_upUeXNYh7v` |
|
|
||||||
| Square App ID (Parker) | `sq0idp-YSM7BU9IVyOWSzpeP-0nzQ` |
|
|
||||||
| Webhook HMAC Secret | `4c8805f0285214ff0a0602b5880270b935f36a896946c7f1` |
|
|
||||||
|
|
||||||
### SIP / Phone
|
|
||||||
| Extension | Name | SIP Password |
|
|
||||||
|-----------|------|-------------|
|
|
||||||
| 1000 | Myron Blair — Desk (10.48.200.2) | `Xk9mPw3nQv7rLs2t` |
|
|
||||||
| 1001 | Tommy Ivy — Desk (10.48.200.43) | `Tv8xNm4pWq6rZs3k` |
|
|
||||||
| 1002 | Myron Blair — WiFi Work (10.48.200.65) | `yXHaJTwa8rj?$GkrVFQB` |
|
|
||||||
| 1003 | Kitchen (10.48.200.83) | — |
|
|
||||||
| 1004 | Master Bedroom (10.48.200.85) | — |
|
|
||||||
| 1010 | Parker County Slingshot (voicemail only) | — |
|
|
||||||
| 1011 | Epic Travel Expeditions (voicemail only) | — |
|
|
||||||
| 1012 | Tom's Java Jive (voicemail only) | — |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*This document contains sensitive credentials. Store securely and do not share.*
|
|
||||||
+5475
-4760
File diff suppressed because it is too large
Load Diff
@@ -57,7 +57,7 @@ else
|
|||||||
"poll_interval": 30,
|
"poll_interval": 30,
|
||||||
"heartbeat_every": 10,
|
"heartbeat_every": 10,
|
||||||
"update_check_hours": 24,
|
"update_check_hours": 24,
|
||||||
"watch_services": ["ollama", "homeassistant", "mysql", "mariadb", "nginx", "apache2", "docker"]
|
"watch_services": []
|
||||||
}
|
}
|
||||||
JSONEOF
|
JSONEOF
|
||||||
chmod 600 "$CONFIG_DIR/config.json"
|
chmod 600 "$CONFIG_DIR/config.json"
|
||||||
|
|||||||
@@ -281,7 +281,7 @@ def get_uptime() -> dict:
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
def get_services(cfg: dict) -> list:
|
def get_services(cfg: dict) -> list:
|
||||||
watch = cfg.get("watch_services", ["ollama", "homeassistant", "mysql", "nginx", "apache2"])
|
watch = cfg.get("watch_services", [])
|
||||||
statuses = []
|
statuses = []
|
||||||
for svc in watch:
|
for svc in watch:
|
||||||
try:
|
try:
|
||||||
|
|||||||
+128
-123
@@ -1,123 +1,128 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* JARVIS API Router — fault-isolated per endpoint
|
* JARVIS API Router — fault-isolated per endpoint
|
||||||
* A ParseError or fatal in any endpoint file returns JSON 500 for that
|
* A ParseError or fatal in any endpoint file returns JSON 500 for that
|
||||||
* endpoint only; all other endpoints continue to work normally.
|
* endpoint only; all other endpoints continue to work normally.
|
||||||
*/
|
*/
|
||||||
require_once __DIR__ . '/../api/config.php';
|
require_once __DIR__ . '/../api/config.php';
|
||||||
require_once __DIR__ . '/../api/lib/db.php';
|
require_once __DIR__ . '/../api/lib/db.php';
|
||||||
require_once __DIR__ . '/../api/lib/kb_engine.php';
|
require_once __DIR__ . '/../api/lib/kb_engine.php';
|
||||||
|
|
||||||
// Skip session for machine-agent calls and netscan/ping — each heartbeat would
|
// Skip session for machine-agent calls and netscan/ping — each heartbeat would
|
||||||
// otherwise create an empty session file, producing millions of files that slow
|
// otherwise create an empty session file, producing millions of files that slow
|
||||||
// session GC for all requests. Browser-facing agent sub-actions (list/status/myip)
|
// session GC for all requests. Browser-facing agent sub-actions (list/status/myip)
|
||||||
// still need a session to verify auth, so we only skip for machine-agent actions.
|
// still need a session to verify auth, so we only skip for machine-agent actions.
|
||||||
$_earlyParts = explode('/', trim(parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH), '/'));
|
$_earlyParts = explode('/', trim(parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH), '/'));
|
||||||
if (($_earlyParts[0] ?? '') === 'api') array_shift($_earlyParts);
|
if (($_earlyParts[0] ?? '') === 'api') array_shift($_earlyParts);
|
||||||
$_e0 = $_earlyParts[0] ?? '';
|
$_e0 = $_earlyParts[0] ?? '';
|
||||||
$_e1 = $_earlyParts[1] ?? '';
|
$_e1 = $_earlyParts[1] ?? '';
|
||||||
$_skipSession = match(true) {
|
$_skipSession = match(true) {
|
||||||
$_e0 === 'ping' => true,
|
$_e0 === 'ping' => true,
|
||||||
$_e0 === 'netscan' => true,
|
$_e0 === 'netscan' => true,
|
||||||
$_e0 === 'agent' && !in_array($_e1, ['list','status','myip'], true) => true,
|
$_e0 === 'agent' && !in_array($_e1, ['list','status','myip'], true) => true,
|
||||||
default => false,
|
default => false,
|
||||||
};
|
};
|
||||||
if (!$_skipSession) {
|
if (!$_skipSession) {
|
||||||
session_start();
|
session_start();
|
||||||
}
|
}
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
header('Access-Control-Allow-Origin: *');
|
$_allowedOrigins = ['https://jarvis.orbishosting.com', 'http://jarvis.orbishosting.com'];
|
||||||
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
|
$_origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
||||||
header('Access-Control-Allow-Headers: Content-Type, X-Session-Token');
|
if (in_array($_origin, $_allowedOrigins, true)) {
|
||||||
|
header('Access-Control-Allow-Origin: ' . $_origin);
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit; }
|
header('Access-Control-Allow-Credentials: true');
|
||||||
|
}
|
||||||
$uri = $_SERVER['REQUEST_URI'] ?? '/';
|
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
|
||||||
$method = $_SERVER['REQUEST_METHOD'];
|
header('Access-Control-Allow-Headers: Content-Type, X-Session-Token');
|
||||||
$path = trim(parse_url($uri, PHP_URL_PATH), '/');
|
|
||||||
$parts = explode('/', $path);
|
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit; }
|
||||||
|
|
||||||
if (($parts[0] ?? '') === 'api') array_shift($parts);
|
$uri = $_SERVER['REQUEST_URI'] ?? '/';
|
||||||
$endpoint = $parts[0] ?? '';
|
$method = $_SERVER['REQUEST_METHOD'];
|
||||||
$action = $parts[1] ?? '';
|
$path = trim(parse_url($uri, PHP_URL_PATH), '/');
|
||||||
|
$parts = explode('/', $path);
|
||||||
// ── Auth check (skip for auth / agent / netscan) ──────────────────────
|
|
||||||
if (!\in_array($endpoint, ['auth', 'agent', 'netscan'], true)) {
|
if (($parts[0] ?? '') === 'api') array_shift($parts);
|
||||||
$token = $_SESSION['jarvis_token'] ?? ($_SERVER['HTTP_X_SESSION_TOKEN'] ?? '');
|
$endpoint = $parts[0] ?? '';
|
||||||
$isValid = !empty($token) && $token === ($_SESSION['jarvis_token'] ?? '');
|
$action = $parts[1] ?? '';
|
||||||
if (!$isValid) {
|
|
||||||
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
|
// ── Auth check (skip for auth / agent / netscan) ──────────────────────
|
||||||
$isLocal = \in_array($ip, ['127.0.0.1', '::1', JARVIS_IP], true);
|
if (!\in_array($endpoint, ['auth', 'agent', 'netscan'], true)) {
|
||||||
if (!$isLocal && $endpoint !== 'ping') {
|
$token = $_SESSION['jarvis_token'] ?? ($_SERVER['HTTP_X_SESSION_TOKEN'] ?? '');
|
||||||
http_response_code(401);
|
$isValid = !empty($token) && $token === ($_SESSION['jarvis_token'] ?? '');
|
||||||
echo json_encode(['error' => 'Unauthorized', 'code' => 401]);
|
if (!$isValid) {
|
||||||
exit;
|
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
|
||||||
}
|
$isLocal = \in_array($ip, ['127.0.0.1', '::1', JARVIS_IP], true);
|
||||||
}
|
if (!$isLocal && $endpoint !== 'ping') {
|
||||||
}
|
http_response_code(401);
|
||||||
|
echo json_encode(['error' => 'Unauthorized', 'code' => 401]);
|
||||||
if ($endpoint !== 'auth') session_write_close();
|
exit;
|
||||||
|
}
|
||||||
$body = file_get_contents('php://input');
|
}
|
||||||
$data = json_decode($body, true) ?? [];
|
}
|
||||||
|
|
||||||
// ── Fast ping (no file dispatch needed) ──────────────────────────────
|
if ($endpoint !== 'auth') session_write_close();
|
||||||
if ($endpoint === 'ping') {
|
|
||||||
echo json_encode(['status' => 'online', 'time' => date('c'), 'codename' => JARVIS_CODENAME]);
|
$body = file_get_contents('php://input');
|
||||||
exit;
|
$data = json_decode($body, true) ?? [];
|
||||||
}
|
|
||||||
|
// ── Fast ping (no file dispatch needed) ──────────────────────────────
|
||||||
// ── Endpoint → file map ───────────────────────────────────────────────
|
if ($endpoint === 'ping') {
|
||||||
$endpoints = [
|
echo json_encode(['status' => 'online', 'time' => date('c'), 'codename' => JARVIS_CODENAME]);
|
||||||
'auth' => 'auth.php',
|
exit;
|
||||||
'chat' => 'chat.php',
|
}
|
||||||
'system' => 'system.php',
|
|
||||||
'netscan' => 'netscan.php',
|
// ── Endpoint → file map ───────────────────────────────────────────────
|
||||||
'network' => 'network.php',
|
$endpoints = [
|
||||||
'proxmox' => 'proxmox.php',
|
'auth' => 'auth.php',
|
||||||
'ha' => 'ha.php',
|
'chat' => 'chat.php',
|
||||||
'tts' => 'tts.php',
|
'system' => 'system.php',
|
||||||
'email' => 'email.php',
|
'netscan' => 'netscan.php',
|
||||||
'do' => 'do_server.php',
|
'network' => 'network.php',
|
||||||
'alerts' => 'alerts.php',
|
'proxmox' => 'proxmox.php',
|
||||||
'facts' => 'facts_collector.php',
|
'ha' => 'ha.php',
|
||||||
'weather' => 'weather.php',
|
'tts' => 'tts.php',
|
||||||
'news' => 'news.php',
|
'email' => 'email.php',
|
||||||
'sites' => 'sites.php',
|
'do' => 'do_server.php',
|
||||||
'agent' => 'agent.php',
|
'alerts' => 'alerts.php',
|
||||||
'planner' => 'planner.php',
|
'facts' => 'facts_collector.php',
|
||||||
'jellyfin' => 'jellyfin.php',
|
'weather' => 'weather.php',
|
||||||
'history' => 'history.php',
|
'news' => 'news.php',
|
||||||
'metrics' => 'metrics.php',
|
'sites' => 'sites.php',
|
||||||
'suggestions' => 'suggestions.php',
|
'agent' => 'agent.php',
|
||||||
'arc' => 'arc.php',
|
'planner' => 'planner.php',
|
||||||
'directives' => 'directives.php',
|
'jellyfin' => 'jellyfin.php',
|
||||||
'memory' => 'memory.php',
|
'history' => 'history.php',
|
||||||
'calendar' => 'calendar_sync.php',
|
'metrics' => 'metrics.php',
|
||||||
];
|
'suggestions' => 'suggestions.php',
|
||||||
|
'arc' => 'arc.php',
|
||||||
if (!isset($endpoints[$endpoint])) {
|
'directives' => 'directives.php',
|
||||||
http_response_code(404);
|
'memory' => 'memory.php',
|
||||||
echo json_encode(['error' => 'Unknown endpoint: ' . $endpoint]);
|
'calendar' => 'calendar_sync.php',
|
||||||
exit;
|
];
|
||||||
}
|
|
||||||
|
if (!isset($endpoints[$endpoint])) {
|
||||||
$file = __DIR__ . '/../api/endpoints/' . $endpoints[$endpoint];
|
http_response_code(404);
|
||||||
|
echo json_encode(['error' => 'Unknown endpoint: ' . $endpoint]);
|
||||||
// ── Fault-isolated dispatch ───────────────────────────────────────────
|
exit;
|
||||||
// ob_start() buffers any partial output so a mid-execution fatal doesn't
|
}
|
||||||
// send a broken response. catch(Throwable) catches ParseError, TypeError,
|
|
||||||
// and all other Errors + Exceptions in PHP 7+.
|
$file = __DIR__ . '/../api/endpoints/' . $endpoints[$endpoint];
|
||||||
ob_start();
|
|
||||||
try {
|
// ── Fault-isolated dispatch ───────────────────────────────────────────
|
||||||
require $file;
|
// ob_start() buffers any partial output so a mid-execution fatal doesn't
|
||||||
ob_end_flush();
|
// send a broken response. catch(Throwable) catches ParseError, TypeError,
|
||||||
} catch (\Throwable $e) {
|
// and all other Errors + Exceptions in PHP 7+.
|
||||||
ob_end_clean();
|
ob_start();
|
||||||
http_response_code(500);
|
try {
|
||||||
echo json_encode(['error' => 'Endpoint unavailable', 'endpoint' => $endpoint, 'code' => 500]);
|
require $file;
|
||||||
error_log(sprintf('JARVIS API [%s] %s: %s in %s:%d',
|
ob_end_flush();
|
||||||
$endpoint, get_class($e), $e->getMessage(), $e->getFile(), $e->getLine()
|
} catch (\Throwable $e) {
|
||||||
));
|
ob_end_clean();
|
||||||
}
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => 'Endpoint unavailable', 'endpoint' => $endpoint, 'code' => 500]);
|
||||||
|
error_log(sprintf('JARVIS API [%s] %s: %s in %s:%d',
|
||||||
|
$endpoint, get_class($e), $e->getMessage(), $e->getFile(), $e->getLine()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|||||||
+1832
-1821
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -57,7 +57,7 @@ else
|
|||||||
"poll_interval": 30,
|
"poll_interval": 30,
|
||||||
"heartbeat_every": 10,
|
"heartbeat_every": 10,
|
||||||
"update_check_hours": 24,
|
"update_check_hours": 24,
|
||||||
"watch_services": ["ollama", "homeassistant", "mysql", "mariadb", "nginx", "apache2", "docker"]
|
"watch_services": []
|
||||||
}
|
}
|
||||||
JSONEOF
|
JSONEOF
|
||||||
chmod 600 "$CONFIG_DIR/config.json"
|
chmod 600 "$CONFIG_DIR/config.json"
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ if (!defined('WEBHOOK_SECRET')) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
define('DEPLOY_QUEUE', '/tmp/jarvis-deploy-queue.txt');
|
define('DEPLOY_QUEUE', '/tmp/jarvis-deploy-queue.txt');
|
||||||
define('DEPLOY_LOG', '/var/www/jarvis/logs/deploy.log');
|
define('DEPLOY_LOG', '/var/log/jarvis/deploy.log');
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
@@ -33,9 +33,10 @@ $repo = $data['repository']['name'] ?? '';
|
|||||||
$ref = $data['ref'] ?? '';
|
$ref = $data['ref'] ?? '';
|
||||||
$pusher = $data['pusher']['name'] ?? 'unknown';
|
$pusher = $data['pusher']['name'] ?? 'unknown';
|
||||||
|
|
||||||
// Only deploy on pushes to main
|
// Only deploy on pushes to the repo's actual default branch (master, not main —
|
||||||
if ($ref !== 'refs/heads/main') {
|
// this was checking 'main' for a while even though the jarvis repo has always used 'master')
|
||||||
echo json_encode(['ok' => true, 'skipped' => "ref $ref is not main"]);
|
if ($ref !== 'refs/heads/master') {
|
||||||
|
echo json_encode(['ok' => true, 'skipped' => "ref $ref is not master"]);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user