mirror of
https://github.com/myronblair/do-server-config
synced 2026-06-30 17:50:59 -05:00
[orbis] Weekly backup 2026-06-21 — 8 files changed, 218 insertions(+), 302 deletions(-)
This commit is contained in:
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
* * * * * /usr/local/bin/jarvis-deploy.sh
|
|
||||||
*/5 * * * * /usr/local/bin/jarvis-watchdog.sh
|
*/5 * * * * /usr/local/bin/jarvis-watchdog.sh
|
||||||
0 2 * * * /usr/local/bin/jarvis-backup.sh >> /var/backups/jarvis/backup.log 2>&1
|
0 2 * * * /usr/local/bin/jarvis-backup.sh >> /var/backups/jarvis/backup.log 2>&1
|
||||||
0 2 * * * su -s /bin/bash tomto8868 -c '/usr/local/bin/ttg-backup.sh' >> /home/tomtomgames.com/backups/backup.log 2>&1
|
0 2 * * * su -s /bin/bash tomto8868 -c '/usr/local/bin/ttg-backup.sh' >> /home/tomtomgames.com/backups/backup.log 2>&1
|
||||||
0 4 * * 0 /usr/local/bin/do-server-backup >> /var/log/do-server-backup.log 2>&1
|
0 4 * * 0 /usr/local/bin/do-server-backup >> /var/log/do-server-backup.log 2>&1
|
||||||
|
* * * * * /usr/local/bin/jarvis-deploy.sh
|
||||||
|
|||||||
+1
-4
@@ -22,11 +22,8 @@
|
|||||||
* * * * * /usr/local/CyberCP/bin/python /usr/local/CyberCP/manage.py run_scheduled_scans >/usr/local/lscp/logs/scheduled_scans.log 2>&1
|
* * * * * /usr/local/CyberCP/bin/python /usr/local/CyberCP/manage.py run_scheduled_scans >/usr/local/lscp/logs/scheduled_scans.log 2>&1
|
||||||
|
|
||||||
*/5 * * * * /usr/local/CyberCP/bin/python /usr/local/CyberCP/plogical/pdnsHealthCheck.py >/dev/null 2>&1
|
*/5 * * * * /usr/local/CyberCP/bin/python /usr/local/CyberCP/plogical/pdnsHealthCheck.py >/dev/null 2>&1
|
||||||
*/3 * * * * /usr/local/lsws/lsphp85/bin/lsphp /home/jarvis.orbishosting.com/api/endpoints/facts_collector.php >> /home/jarvis.orbishosting.com/logs/cron.log 2>&1
|
|
||||||
*/5 * * * * /usr/local/lsws/lsphp85/bin/lsphp /home/jarvis.orbishosting.com/api/endpoints/stats_cache.php >> /home/jarvis.orbishosting.com/logs/cron.log 2>&1
|
|
||||||
* * * * * /usr/local/bin/jarvis-deploy.sh
|
|
||||||
*/5 * * * * /usr/local/bin/jarvis-watchdog.sh
|
*/5 * * * * /usr/local/bin/jarvis-watchdog.sh
|
||||||
0 2 * * * /usr/local/bin/jarvis-backup.sh >> /var/backups/jarvis/backup.log 2>&1
|
0 2 * * * /usr/local/bin/jarvis-backup.sh >> /var/backups/jarvis/backup.log 2>&1
|
||||||
*/15 * * * * /usr/local/lsws/lsphp85/bin/lsphp /home/jarvis.orbishosting.com/api/endpoints/calendar_sync.php >> /home/jarvis.orbishosting.com/logs/cron.log 2>&1
|
|
||||||
0 2 * * * su -s /bin/bash tomto8868 -c '/usr/local/bin/ttg-backup.sh' >> /home/tomtomgames.com/backups/backup.log 2>&1
|
0 2 * * * su -s /bin/bash tomto8868 -c '/usr/local/bin/ttg-backup.sh' >> /home/tomtomgames.com/backups/backup.log 2>&1
|
||||||
0 4 * * 0 /usr/local/bin/do-server-backup >> /var/log/do-server-backup.log 2>&1
|
0 4 * * 0 /usr/local/bin/do-server-backup >> /var/log/do-server-backup.log 2>&1
|
||||||
|
* * * * * /usr/local/bin/jarvis-deploy.sh
|
||||||
|
|||||||
+139
-23
@@ -6,18 +6,44 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
This is a home-lab / managed-hosting environment. There is no local codebase to build or test — work consists of editing PHP/JS files on remote servers via SSH and managing infrastructure across several machines. All tool calls use `sshpass` with password auth.
|
This is a home-lab / managed-hosting environment. There is no local codebase to build or test — work consists of editing PHP/JS files on remote servers via SSH and managing infrastructure across several machines. All tool calls use `sshpass` with password auth.
|
||||||
|
|
||||||
|
## Tailscale Network
|
||||||
|
|
||||||
|
All key hosts are on Tailscale (myronblair@gmail.com). Use Tailscale IPs for SSH — no relay or port forwarding needed.
|
||||||
|
|
||||||
|
| Host | Tailscale IP | LAN IP | Password |
|
||||||
|
|------|-------------|--------|----------|
|
||||||
|
| Claude VM (this) | 100.69.120.58 | 10.48.200.29 | — |
|
||||||
|
| PVE1 | 100.80.188.8 | 10.48.200.90 | `Joker1974!!!` |
|
||||||
|
| PVE2 | 100.87.186.12 | 10.48.200.91 | `Joker1974!!!` |
|
||||||
|
| DO server (orbis) | 100.121.13.34 | 165.22.1.228 | `Gonewalk1974!@#` |
|
||||||
|
| FusionPBX | 100.74.46.120 | 134.209.72.226 | `Joker1974!@#` |
|
||||||
|
| JARVIS VM | 100.77.178.42 | 10.48.200.211 | `Joker1974!!!` |
|
||||||
|
| NPM VM | 100.110.239.71 | 10.48.200.201 | `Joker1974!!!` |
|
||||||
|
| Ollama VM | 100.96.100.113 | 10.48.200.210 | `Joker1974!!!` |
|
||||||
|
| NovaCPX (hostpanel-110) | 100.86.51.18 | 10.48.200.110 | `Joker1974!!!` |
|
||||||
|
| HomeBridge | 100.124.182.18 | 10.48.200.18 | — |
|
||||||
|
| WireGuard CT | 100.122.55.10 | 10.48.200.19 | — |
|
||||||
|
| Synology NAS | 100.118.175.5 | 10.48.200.249 | — |
|
||||||
|
| mini-it12 (Windows) | 100.98.151.120 | 10.48.200.87 | — |
|
||||||
|
|
||||||
|
**DNS note:** FortiGate blocks outbound port 53. All PVE1 VMs must use `10.48.200.90` (PVE1 dnsmasq → 100.100.100.100) as their DNS server, not 8.8.8.8 directly.
|
||||||
|
|
||||||
## Server Map
|
## Server Map
|
||||||
|
|
||||||
| Host | IP | SSH | Purpose |
|
| Host | IP | SSH | Purpose |
|
||||||
|------|-----|-----|---------|
|
|------|-----|-----|---------|
|
||||||
| DO (main) | 165.22.1.228 | `root / Gonewalk1974!@#` | CyberPanel/OLS — all websites + JARVIS |
|
| DO (main) | 165.22.1.228 | `root / Gonewalk1974!@#` | CyberPanel/OLS — all websites (not JARVIS after migration) |
|
||||||
| FusionPBX | 134.209.72.226 | `root / Joker1974!@#` | FreeSWITCH PBX |
|
| FusionPBX | 134.209.72.226 | `root / Joker1974!@#` (via Tailscale 100.74.46.120) | FreeSWITCH PBX |
|
||||||
| PVE1 (Proxmox) | orbisne.fortiddns.com (10.48.200.90) | `root / Joker1974!!!` | Primary hypervisor — FortiGate DDNS, auto-updates if IP changes |
|
| PVE1 (Proxmox) | orbisne.fortiddns.com (10.48.200.90) | `root / Joker1974!!!` (via Tailscale 100.80.188.8) | Primary hypervisor |
|
||||||
| PVE2 (Proxmox) | 10.48.200.91 | `root / Joker1974!!!` | Secondary hypervisor |
|
| PVE2 (Proxmox) | 10.48.200.91 | `root / Joker1974!!!` | Secondary hypervisor |
|
||||||
| Ollama VM | 10.48.200.95 | `myron → sudo` | llama3.2 local LLM (PVE1 VM 210) |
|
| JARVIS VM | 10.48.200.211 | `root / Joker1974!!!` (via Tailscale 100.77.178.42) | JARVIS dashboard — PVE1 VM 211, 8c/16GB |
|
||||||
|
| NPM VM | 10.48.200.201 | `root / Joker1974!!!` (via Tailscale 100.110.239.71) | Nginx Proxy Manager — PVE1 VM 200 (LAN IP .201 despite VM ID 200) |
|
||||||
|
| Ollama VM | 10.48.200.210 | `root / Joker1974!!!` (via Tailscale 100.96.100.113) | Local LLM — PVE1 VM 210, 4c/8GB |
|
||||||
| Home Assistant | 10.48.200.97 | `myron → sudo` | HA VM 101 |
|
| Home Assistant | 10.48.200.97 | `myron → sudo` | HA VM 101 |
|
||||||
| NetworkBackup | 10.48.200.99 | `myron → sudo` | Backup VM (PVE2 VM 302) |
|
| NetworkBackup | 10.48.200.99 | `myron → sudo` | Backup VM (PVE2 VM 302) |
|
||||||
| MediaStack | 10.48.200.35 | `root via PVE1 key` | Sonarr/Radarr/Prowlarr/qBittorrent (PVE1 VM 113) |
|
| MediaStack | 10.48.200.35 | `root via PVE1 key` | Sonarr/Radarr/Prowlarr/qBittorrent (PVE1 VM 113) |
|
||||||
|
| NovaCPX | 10.48.200.110 | `root / Joker1974!!!` (direct SSH — Tailscale 100.86.51.18 needs re-auth) | Custom hosting control panel (PVE1 VM 120) |
|
||||||
|
| NPM | 10.48.200.201 | `root / Joker1974!!!` (via Tailscale 100.110.239.71) | Nginx Proxy Manager — PVE1 VM 200 · NPM API: `POST http://localhost:81/api/tokens` identity=myronblair@outlook.com |
|
||||||
|
|
||||||
**SSH password order** (try in sequence if first fails): `Joker1974!@#` → `Joker1974!!!` → `Joker1974!`
|
**SSH password order** (try in sequence if first fails): `Joker1974!@#` → `Joker1974!!!` → `Joker1974!`
|
||||||
|
|
||||||
@@ -34,7 +60,7 @@ sshpass -p 'Joker1974!!!' ssh -o StrictHostKeyChecking=no root@orbisne.fortiddns
|
|||||||
For commands inside VMs on PVE1:
|
For commands inside VMs on PVE1:
|
||||||
```bash
|
```bash
|
||||||
sshpass -p 'Joker1974!!!' ssh -o StrictHostKeyChecking=no root@orbisne.fortiddns.com \
|
sshpass -p 'Joker1974!!!' ssh -o StrictHostKeyChecking=no root@orbisne.fortiddns.com \
|
||||||
'qm guest exec 210 -- bash -c "commands here"'
|
'qm guest exec <VMID> -- bash -c "commands here"'
|
||||||
```
|
```
|
||||||
|
|
||||||
## Websites on DO (165.22.1.228)
|
## Websites on DO (165.22.1.228)
|
||||||
@@ -43,7 +69,7 @@ All sites live at `/home/<domain>/public_html/` on DO. CyberPanel/OpenLiteSpeed
|
|||||||
|
|
||||||
| Site | Path | GitHub |
|
| Site | Path | GitHub |
|
||||||
|------|------|--------|
|
|------|------|--------|
|
||||||
| jarvis.orbishosting.com | /home/jarvis.orbishosting.com/ | myronblair/jarvis |
|
| ~~jarvis.orbishosting.com~~ | ~~removed from DO~~ | myronblair/jarvis (now on PVE1 VM 211) |
|
||||||
| tomsjavajive.com | /home/tomsjavajive.com/public_html/ | myronblair/tomsjavajive |
|
| tomsjavajive.com | /home/tomsjavajive.com/public_html/ | myronblair/tomsjavajive |
|
||||||
| epictravelexpeditions.com | /home/epictravelexpeditions.com/public_html/ | myronblair/epictravelexpeditions |
|
| epictravelexpeditions.com | /home/epictravelexpeditions.com/public_html/ | myronblair/epictravelexpeditions |
|
||||||
| parkerslingshotrentals.com | /home/parkerslingshotrentals.com/public_html/ | myronblair/parkerslingshotrentals |
|
| parkerslingshotrentals.com | /home/parkerslingshotrentals.com/public_html/ | myronblair/parkerslingshotrentals |
|
||||||
@@ -61,12 +87,18 @@ All sites live at `/home/<domain>/public_html/` on DO. CyberPanel/OpenLiteSpeed
|
|||||||
|
|
||||||
**Auto-deploy is active.** Push to `main` on any site repo → GitHub webhook → server pulls automatically within 1 minute. PHP syntax is validated before deploy; bad commits are auto-reverted.
|
**Auto-deploy is active.** Push to `main` on any site repo → GitHub webhook → server pulls automatically within 1 minute. PHP syntax is validated before deploy; bad commits are auto-reverted.
|
||||||
|
|
||||||
Webhook handler: `https://jarvis.orbishosting.com/webhook.php` (HMAC secret: `4c8805f0285214ff0a0602b5880270b935f36a896946c7f1`)
|
**Two separate webhook handlers:**
|
||||||
Deploy queue: `/tmp/jarvis-deploy-queue.txt` | Runner: `/usr/local/bin/jarvis-deploy.sh` (cron every min)
|
- **JARVIS repo** → `http://jarvis.orbishosting.com:1972/webhook.php` — deploys to JARVIS VM (`/var/www/jarvis/`). Deploy log: `/var/www/jarvis/logs/deploy.log`
|
||||||
Deploy log: `/home/jarvis.orbishosting.com/logs/deploy.log`
|
- **All 6 website repos → `https://tomtomgames.com/webhook.php` on DO — deploys to `/home/<site>/public_html/` on DO. Deploy log: `/home/tomtomgames.com/logs/deploy.log`. Deploy log: `/home/<site>/logs/deploy.log`
|
||||||
|
|
||||||
|
HMAC secret (both handlers): `4c8805f0285214ff0a0602b5880270b935f36a896946c7f1`
|
||||||
|
Deploy queue: `/tmp/jarvis-deploy-queue.txt` | Runner: `/usr/local/bin/jarvis-deploy.sh` (cron every min, on both DO and JARVIS VM)
|
||||||
|
|
||||||
For hotfixes that can't wait 1 min, SCP directly:
|
For hotfixes that can't wait 1 min, SCP directly:
|
||||||
```bash
|
```bash
|
||||||
|
# JARVIS VM
|
||||||
|
scp -o StrictHostKeyChecking=no /tmp/changed.php root@100.77.178.42:/var/www/jarvis/public_html/changed.php
|
||||||
|
# DO websites
|
||||||
sshpass -p 'Gonewalk1974!@#' scp -o StrictHostKeyChecking=no /tmp/changed.php \
|
sshpass -p 'Gonewalk1974!@#' scp -o StrictHostKeyChecking=no /tmp/changed.php \
|
||||||
root@165.22.1.228:/home/site.com/public_html/changed.php
|
root@165.22.1.228:/home/site.com/public_html/changed.php
|
||||||
```
|
```
|
||||||
@@ -80,18 +112,33 @@ Gitignored credentials (never in GitHub): `api/config.php` (JARVIS, epictravelex
|
|||||||
|
|
||||||
## JARVIS System
|
## JARVIS System
|
||||||
|
|
||||||
Iron Man-style AI dashboard at `https://jarvis.orbishosting.com`. All files on DO at `/home/jarvis.orbishosting.com/`.
|
Iron Man-style AI dashboard at `http://jarvis.orbishosting.com:1972`. **Migrated from DO to PVE1 VM 211 (2026-06-18).** All files on JARVIS VM at `/var/www/jarvis/`.
|
||||||
|
|
||||||
|
**Access:**
|
||||||
|
- Dashboard: `http://jarvis.orbishosting.com:1972`
|
||||||
|
- Admin: `http://jarvis.orbishosting.com:1972/admin`
|
||||||
|
- Internal (LAN): `http://10.48.200.211` or via Tailscale `http://100.77.178.42`
|
||||||
|
- FortiGate VIP: external port `1972` → `10.48.200.211:80`
|
||||||
|
- Cloudflare: DNS only (grey cloud) — no CF proxy, no SSL overhead on origin
|
||||||
|
|
||||||
|
**Stack on JARVIS VM:**
|
||||||
|
- nginx + PHP 8.3-FPM (replaces OLS/lsphp on DO)
|
||||||
|
- MariaDB (jarvis_db local, `jarvis_user / J4rv1s_Pr0t0c0l_2026!`)
|
||||||
|
- Redis (`redis-server`)
|
||||||
|
- Python 3 + Arc Reactor daemon
|
||||||
|
|
||||||
**Architecture:**
|
**Architecture:**
|
||||||
- `public_html/api.php` — API router; has `session_write_close()` guard (must skip for `auth` endpoint to prevent LSAPI session deadlock)
|
- `public_html/api.php` — API router; `session_start()` skipped only for machine-agent sub-actions (heartbeat/metrics/ha_state/command_result/register); browser-facing agent routes (list/status/myip) need session. Has `session_write_close()` guard (must skip for `auth` endpoint to prevent LSAPI session deadlock).
|
||||||
- `api/config.php` — all credentials/constants (gitignored)
|
- `api/config.php` — all credentials/constants (gitignored)
|
||||||
- `api/endpoints/chat.php` — 4-tier AI: KB intent → Ollama (10.48.200.95:11434) → Groq (`compound-beta-mini`) → Claude API; includes Tier 0.7 planner intents (tasks/appointments/briefing)
|
- `api/endpoints/chat.php` — 4-tier AI: KB intent → Groq (`compound-beta-mini`) → Claude API; includes Tier 0.7 planner intents (tasks/appointments/briefing). Ollama at `http://10.48.200.210:11434`.
|
||||||
- `api/endpoints/agent.php` — push-based agent registration/heartbeat/metrics
|
- `api/endpoints/agent.php` — push-based agent registration/heartbeat/metrics; browser actions (list/status/myip) auth via `$_SESSION`, machine actions auth via `X-Agent-Key` header
|
||||||
- `api/endpoints/alerts.php` — auto-generates alerts (CPU >85%, RAM >85%, disk >88%, offline agents, site down); dispatches restart commands to agents when their services fail
|
- `api/endpoints/alerts.php` — auto-generates alerts (CPU >85%, RAM >85%, disk >88%, offline agents, site down); dispatches restart commands to agents when their services fail
|
||||||
- `api/endpoints/facts_collector.php` — runs every 3 min via cron; collects agent metrics, KB facts, Proxmox/HA/Ollama status, and all 7 site HTTP health checks
|
- `api/endpoints/facts_collector.php` — runs every 3 min via cron (php8.3); collects agent metrics, KB facts, Proxmox/HA status, and all 7 site HTTP health checks. Site checks use external URLs (JARVIS VM is NOT the web host). `$fresh()` queries `WHERE category=?` (not `fact_category`).
|
||||||
- `api/endpoints/stats_cache.php` — runs every 5 min via cron; weather/news/Proxmox cache refresh
|
- `api/endpoints/stats_cache.php` — runs every 5 min via cron; weather/news/Proxmox cache refresh. Proxmox API at `https://10.48.200.90:8006` (direct LAN).
|
||||||
|
- `api/endpoints/do_server.php` — reads `/proc` for JARVIS VM stats; also includes DO server agent metrics (`do_server` key from jarvis-do agent via Tailscale).
|
||||||
- `api/endpoints/planner.php` — tasks & appointments CRUD; routes: `planner/tasks`, `planner/appointments`, `planner/today`, `planner/done`
|
- `api/endpoints/planner.php` — tasks & appointments CRUD; routes: `planner/tasks`, `planner/appointments`, `planner/today`, `planner/done`
|
||||||
- `api/endpoints/ha.php` — HA entity list reads from `ha_entities` table (real-time agent push); service calls go direct to HA_URL (`http://orbisne.fortiddns.com:8123`)
|
- `api/endpoints/ha.php` — HA entity list reads from `ha_entities` table (real-time agent push); service calls go direct to HA_URL (`http://10.48.200.97:8123`)
|
||||||
|
- `api/lib/kb_engine.php` — `storeFact()` ON DUPLICATE KEY UPDATE always sets `updated_at=NOW()` explicitly; without this, unchanged values don't bump the timestamp and freshness checks break.
|
||||||
|
|
||||||
**Voice system (index.html):**
|
**Voice system (index.html):**
|
||||||
- Continuous SpeechRecognition; mic stays open always (mute toggle button)
|
- Continuous SpeechRecognition; mic stays open always (mute toggle button)
|
||||||
@@ -107,14 +154,34 @@ Iron Man-style AI dashboard at `https://jarvis.orbishosting.com`. All files on D
|
|||||||
- Home page: small top-bar badge "N TASKS · N APPTS" when items due today (no panel added)
|
- Home page: small top-bar badge "N TASKS · N APPTS" when items due today (no panel added)
|
||||||
- Admin CRUD at `/admin` under PLANNER section → TASKS and APPOINTMENTS tabs
|
- Admin CRUD at `/admin` under PLANNER section → TASKS and APPOINTMENTS tabs
|
||||||
|
|
||||||
**Agent system:** Agents phone home every 10s (heartbeat) / 30s (metrics) to `https://165.22.1.228` with `Host: jarvis.orbishosting.com` header (bypasses Cloudflare). Config at `/opt/jarvis-agent/config.json` on each Linux agent.
|
**Agent system:** Agents phone home every 10s (heartbeat) / 30s (metrics) to `http://10.48.200.211` (direct LAN — no Cloudflare). Config at `/etc/jarvis-agent/config.json` or `/opt/jarvis-agent/config.json` on each Linux agent.
|
||||||
Agent installer (one-liner for any Linux host): `curl -sk https://jarvis.orbishosting.com/install-agent.sh | bash -s <hostname> <linux|homeassistant|proxmox>`
|
Agent installer (one-liner for any Linux host): `curl -sk http://10.48.200.211/install-agent.sh | bash -s <hostname> <linux|homeassistant|proxmox>`
|
||||||
|
DO server agent (jarvis-do) uses Tailscale: `jarvis_url: http://100.77.178.42`
|
||||||
|
|
||||||
**Self-healing:** `/usr/local/bin/jarvis-watchdog.sh` runs every 5 min (root cron). Restarts lsws/mysql/redis if down, restarts offline Proxmox VM agents via `qm guest exec`, inserts alerts to DB, rotates logs.
|
**Agent file paths by host** (for manual updates — push to correct path then restart service):
|
||||||
Watchdog log: `/home/jarvis.orbishosting.com/logs/watchdog.log`
|
- Most Linux hosts: `/opt/jarvis-agent/jarvis-agent.py` · service: `systemctl restart jarvis-agent`
|
||||||
|
- WireGuard CT (10.48.200.19, Alpine): `/opt/jarvis-agent/agent.py` · service: `rc-service jarvis-agent restart`
|
||||||
|
- `public_html/agent/jarvis-agent.py` is the self-update URL — must be kept in sync with `agent/jarvis-agent.py` (both are tracked in git; auto-deploy keeps them in sync after 2026-06-17)
|
||||||
|
|
||||||
**JARVIS DB:** `jarvis_db` on localhost. User: `jarvis_user / J4rv1s_Pr0t0c0l_2026!`. phpMyAdmin at `/phpmyadmin` (myron / Joker1974!!!).
|
**Self-healing:** `/usr/local/bin/jarvis-watchdog.sh` runs every 5 min (root cron on DO). Restarts lsws/mysql/redis on DO if down. Log: `/usr/local/lsws/logs/watchdog.log` on DO.
|
||||||
Tables (18 total): agent_commands, agent_metrics, alerts, api_cache, appointments, conversations, ha_entities, kb_facts, kb_intents, kb_ollama_models, kb_preferences, known_commands, metrics_history, network_devices, registered_agents, tasks, users + (see schema for full column list)
|
|
||||||
|
**JARVIS DB:** `jarvis_db` on JARVIS VM localhost (MariaDB). User: `jarvis_user / J4rv1s_Pr0t0c0l_2026!`. phpMyAdmin at `/phpmyadmin` on JARVIS VM (myron / Joker1974!!!).
|
||||||
|
Core tables: agent_commands, agent_metrics, alerts, api_cache, appointments, conversations, ha_entities, kb_facts, kb_intents, kb_ollama_models, kb_preferences, known_commands, metrics_history, network_devices, registered_agents, tasks, users. Arc Reactor adds: arc_jobs, guardian_events, guardian_config, agent_screenshots.
|
||||||
|
|
||||||
|
`kb_facts` schema: `(id, category, fact_key, fact_value, host, expires_at, updated_at)` — column is `category` not `fact_category`.
|
||||||
|
|
||||||
|
**Arc Reactor daemon:** Python service at `/opt/jarvis-arc/reactor.py` on JARVIS VM, port 7474, managed by `systemctl restart jarvis-arc`. Deploy source: `deploy/reactor.py` in the jarvis repo. After pushing to GitHub, auto-deploy pulls to `/var/www/jarvis/deploy/reactor.py` — then manually `cp /var/www/jarvis/deploy/reactor.py /opt/jarvis-arc/reactor.py && systemctl restart jarvis-arc`. Log: `/var/www/jarvis/logs/arc_reactor.log`.
|
||||||
|
|
||||||
|
**Arc Reactor AI routing:**
|
||||||
|
| Feature | Provider | Model |
|
||||||
|
|---------|----------|-------|
|
||||||
|
| Guardian anomaly alerts | Groq | `llama-3.3-70b-versatile` |
|
||||||
|
| SITREP | Groq | `llama-3.3-70b-versatile` |
|
||||||
|
| Vision: text-only sysinfo snapshot | Groq | `llama-3.3-70b-versatile` |
|
||||||
|
| Vision: actual screenshot image | Claude | `claude-opus-4-8-20251101` |
|
||||||
|
| Email drafting, research, tool_loop | Claude | `claude-sonnet-4-6` |
|
||||||
|
|
||||||
|
`llm_call(messages, provider)` cascades: groq → ollama on failure. Pass `"groq"` or `"claude"` as provider.
|
||||||
|
|
||||||
**Groq API note:** Use model name `compound-beta-mini` directly — NOT `groq/compound-beta-mini` (that's OpenAI router syntax and will 404 on api.groq.com).
|
**Groq API note:** Use model name `compound-beta-mini` directly — NOT `groq/compound-beta-mini` (that's OpenAI router syntax and will 404 on api.groq.com).
|
||||||
|
|
||||||
@@ -147,6 +214,35 @@ Automated media server on PVE1 VM 113. All traffic routes through WireGuard VPN
|
|||||||
**GitHub:** `myronblair/mediastack` (private) — config files, systemd units, README with full setup notes.
|
**GitHub:** `myronblair/mediastack` (private) — config files, systemd units, README with full setup notes.
|
||||||
**JARVIS agent quirks:** needs `jarvis_url`, `registration_key` (`f846a9aaf7ce9a61742c63c87c4186052a71d2a580c65518`), `ssl_verify: false` in config.
|
**JARVIS agent quirks:** needs `jarvis_url`, `registration_key` (`f846a9aaf7ce9a61742c63c87c4186052a71d2a580c65518`), `ssl_verify: false` in config.
|
||||||
|
|
||||||
|
## NovaCPX Panel
|
||||||
|
|
||||||
|
Custom web hosting control panel (PVE1 VM 120, 10.48.200.110). Root SSH: `sshpass -p 'Joker1974!!!' ssh -o StrictHostKeyChecking=no root@10.48.200.110` (direct LAN — use this, Tailscale 100.86.51.18 requires re-auth periodically).
|
||||||
|
|
||||||
|
**Public URLs (via NPM → FortiGate VIP port 443 → 10.48.200.201):**
|
||||||
|
- Admin: `https://admin.novacpx.orbishosting.com` (→ port 8882) · `admin / Admin2026!` or `myron / Joker1974!!!`
|
||||||
|
- Reseller: `https://reseller.novacpx.orbishosting.com` (→ port 8881)
|
||||||
|
- User: `https://panel.novacpx.orbishosting.com` or `https://novacpx.orbishosting.com` (→ port 8880)
|
||||||
|
- Webmail: port 8883 (Roundcube) — no public NPM proxy yet
|
||||||
|
- `https://web.orbishosting.com` → port 80 (placeholder for a new hosted website)
|
||||||
|
|
||||||
|
**Ports:** 8880 (user) · 8881 (reseller) · 8882 (admin) · 8883 (Roundcube webmail)
|
||||||
|
|
||||||
|
**Paths:** Panel web root `/srv/novacpx/public/` · Git repo `/opt/novacpx-src/` · DB `/var/lib/novacpx/panel.db` (SQLite) · Config `/etc/novacpx/config.ini`
|
||||||
|
|
||||||
|
**Config notes:** `/etc/novacpx/config.ini` must have `server = nginx` (not apache) — VhostManager checks this to write the correct vhost format.
|
||||||
|
|
||||||
|
**GitHub:** `myronblair/novacpx` (private). Auto-deploy active: push to `main` (stable) or `beta` → webhook → VM pulls. GitHub Actions auto-bumps VERSION: main→PATCH, beta→-beta.N suffix. Current version: 1.0.40.
|
||||||
|
|
||||||
|
**Update channels:** `stable` tracks `origin/main`, `beta` tracks `origin/beta`. Set in Admin → Settings → Update Channel.
|
||||||
|
|
||||||
|
**Local clone:** `/tmp/novacpx/` on this machine. All edits go here first, then `git push origin main`. The deploy runner syncs `panel/` → `/srv/novacpx/public/` and `panel/lib/` → `/srv/novacpx/public/lib/`. For immediate changes use SCP to `root@10.48.200.110:/srv/novacpx/public/`.
|
||||||
|
|
||||||
|
**PHP-FPM:** Per-account pools in `/etc/php/8.3/fpm/pool.d/`. If php8.3-fpm fails to start, check for orphaned pool configs referencing deleted Linux users — remove them and `systemctl start php8.3-fpm`.
|
||||||
|
|
||||||
|
**JARVIS agent:** Installed, online. Agent ID: `novacpx_e3b07264`.
|
||||||
|
|
||||||
|
**SQLite quirk:** Never use MySQL syntax (ON DUPLICATE KEY, NOW(), DATE_ADD, etc.). DB.php has translate() layer but endpoints must also use SQLite syntax directly.
|
||||||
|
|
||||||
## Parker Slingshot Rentals
|
## Parker Slingshot Rentals
|
||||||
|
|
||||||
Admin portal at `/admin/index.php` uses HMAC-signed cookie auth (not PHP sessions — sessions were unreliable under LiteSpeed caching). Admin: `admin / Parker2026!`. DB: `epic_parkersling / epic_parkersling / Joker1974!!!`.
|
Admin portal at `/admin/index.php` uses HMAC-signed cookie auth (not PHP sessions — sessions were unreliable under LiteSpeed caching). Admin: `admin / Parker2026!`. DB: `epic_parkersling / epic_parkersling / Joker1974!!!`.
|
||||||
@@ -155,7 +251,11 @@ Admin portal at `/admin/index.php` uses HMAC-signed cookie auth (not PHP session
|
|||||||
|
|
||||||
Production at 134.209.72.226. Web: `https://fusion.orbishosting.com` (admin / fY7XP5swgtpbzrYLhkeVYkA4744). SIP profiles served via Lua XML handler — config changes require deleting `/var/cache/fusionpbx/FusionPBX.configuration.sofia.conf` to force reload. Extension 1000 (Yealink T48S at 10.48.200.43) registered on production server via port 5080 with `aggressive-nat-detection=true` to bypass FortiGate SIP ALG.
|
Production at 134.209.72.226. Web: `https://fusion.orbishosting.com` (admin / fY7XP5swgtpbzrYLhkeVYkA4744). SIP profiles served via Lua XML handler — config changes require deleting `/var/cache/fusionpbx/FusionPBX.configuration.sofia.conf` to force reload. Extension 1000 (Yealink T48S at 10.48.200.43) registered on production server via port 5080 with `aggressive-nat-detection=true` to bypass FortiGate SIP ALG.
|
||||||
|
|
||||||
**SSH access:** Port 22 firewalled from internet — only from 107.178.2.130 / 97.154.109.245. Relay all SSH through DO:
|
**SSH access:** Direct via Tailscale (preferred):
|
||||||
|
```bash
|
||||||
|
sshpass -p 'Joker1974!@#' ssh -o StrictHostKeyChecking=no root@100.74.46.120
|
||||||
|
```
|
||||||
|
Fallback if Tailscale down — relay through DO:
|
||||||
```bash
|
```bash
|
||||||
sshpass -p 'Gonewalk1974!@#' ssh -o StrictHostKeyChecking=no root@165.22.1.228 \
|
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"'
|
'sshpass -p "Joker1974!@#" ssh -o StrictHostKeyChecking=no root@134.209.72.226 "command"'
|
||||||
@@ -182,6 +282,20 @@ CyberPanel uses `lsphp85`. Run PHP scripts directly with:
|
|||||||
For PHP syntax checking use `php8.3 -l file.php` — lsphp segfaults on `-l` flag.
|
For PHP syntax checking use `php8.3 -l file.php` — lsphp segfaults on `-l` flag.
|
||||||
When a PHP endpoint uses `ob_start()` + `header.php` pattern, add `ob_end_clean()` before any CSV/JSON response output.
|
When a PHP endpoint uses `ob_start()` + `header.php` pattern, add `ob_end_clean()` before any CSV/JSON response output.
|
||||||
|
|
||||||
|
**Cloudflare Rocket Loader:** JARVIS uses `data-cfasync="false"` on every `<script>` tag in `index.html` (including CDN scripts like face-api.js). One untagged script is enough to trigger Rocket Loader's bootstrap, which injects `mainScript.js` and causes `SyntaxError: Identifier 'mainScriptFlag' has already been declared`. `Cache-Control: no-store, no-cache, must-revalidate, no-transform` is set in `index.php` but tag every new script with `data-cfasync="false"` anyway.
|
||||||
|
|
||||||
|
**Cloudflare auto-deploy cache problem:** After pushing JS fixes, Cloudflare CDN serves stale cached files even on hard refresh. Bump the `?v=YYYYMMDD` query param on script tags in `index.html` to force a cache miss. Current version param: `?v=20260617b`.
|
||||||
|
|
||||||
|
**JS file structure (as of 2026-06-17):**
|
||||||
|
- `assets/js/jarvis-effects.js` — canvas particle effects, sparklines
|
||||||
|
- `assets/js/jarvis-overlays.js` — sleep overlay, network map canvas
|
||||||
|
- `assets/js/jarvis-app.js` — globals, init, chat, voice, system/network/HA/alerts/weather panels
|
||||||
|
- `assets/js/panels/jarvis-arc.js` — Arc Reactor, Intel Protocol, Comms Protocol, Guardian Mode
|
||||||
|
- `assets/js/panels/jarvis-agents.js` — Missions, Directives, Memory, Clearance, Agents tab, Sites, Vision Protocol
|
||||||
|
- `assets/js/panels/jarvis-assistant.js` — Chat History, Command Palette, Suggestions, Mobile switcher, Topology map
|
||||||
|
|
||||||
|
A SyntaxError in any panels/ file breaks only that group — other panels stay functional. `escHtml()` is defined in jarvis-arc.js (loads first) and is global to all subsequent files.
|
||||||
|
|
||||||
## GitHub Repos
|
## GitHub Repos
|
||||||
|
|
||||||
| Repo | Site | DB Schema |
|
| Repo | Site | DB Schema |
|
||||||
@@ -198,6 +312,8 @@ When a PHP endpoint uses `ob_start()` + `header.php` pattern, add `ob_end_clean(
|
|||||||
| myronblair/mediastack | MediaStack VM 113 | config/, systemd units, wg0.conf, README |
|
| myronblair/mediastack | MediaStack VM 113 | config/, systemd units, wg0.conf, README |
|
||||||
| myronblair/do-server-config | DO server backup | scripts, systemd, WG, OLS vhosts, restore.sh |
|
| myronblair/do-server-config | DO server backup | scripts, systemd, WG, OLS vhosts, restore.sh |
|
||||||
| myronblair/proxmox-config | PVE1+PVE2 backup | shared cluster configs + per-node, restore.sh |
|
| myronblair/proxmox-config | PVE1+PVE2 backup | shared cluster configs + per-node, restore.sh |
|
||||||
|
| myronblair/novacpx | admin.novacpx.orbishosting.com | db/schema.sql (SQLite, 19+ tables) |
|
||||||
|
| myronblair/web-dashboard | web.orbishosting.com | — |
|
||||||
| myronblair/fusionpbx-config | FusionPBX backup | PostgreSQL dump (gzip) + FS configs, restore.sh |
|
| myronblair/fusionpbx-config | FusionPBX backup | PostgreSQL dump (gzip) + FS configs, restore.sh |
|
||||||
|
|
||||||
All repos are private. Each has `config/vhost/` with OLS vhost config. The jarvis repo also has `deploy/` (watchdog, deploy runner, systemd units) and `agent/jarvis-agent.py`.
|
All repos are private. Each has `config/vhost/` with OLS vhost config. The jarvis repo also has `deploy/` (watchdog, deploy runner, systemd units) and `agent/jarvis-agent.py`.
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ virtualHost Example{
|
|||||||
vhRoot Example/
|
vhRoot Example/
|
||||||
configFile conf/vhosts/Example/vhconf.conf
|
configFile conf/vhosts/Example/vhconf.conf
|
||||||
listener Default{
|
listener Default{
|
||||||
map mail.jarvis.orbishosting.com mail.jarvis.orbishosting.com
|
|
||||||
map jarvis.orbishosting.com jarvis.orbishosting.com
|
|
||||||
map orbis.orbishosting.com orbis.orbishosting.com
|
map orbis.orbishosting.com orbis.orbishosting.com
|
||||||
map mail.parkerslingshotrentals.com mail.parkerslingshotrentals.com
|
map mail.parkerslingshotrentals.com mail.parkerslingshotrentals.com
|
||||||
map parkerslingshotrentals.com parkerslingshotrentals.com
|
map parkerslingshotrentals.com parkerslingshotrentals.com
|
||||||
@@ -23,8 +21,6 @@ virtualHost tomtomgames.com {
|
|||||||
vhRoot /home/$VH_NAME
|
vhRoot /home/$VH_NAME
|
||||||
configFile $SERVER_ROOT/conf/vhosts/$VH_NAME/vhost.conf
|
configFile $SERVER_ROOT/conf/vhosts/$VH_NAME/vhost.conf
|
||||||
listener SSL {
|
listener SSL {
|
||||||
map mail.jarvis.orbishosting.com mail.jarvis.orbishosting.com
|
|
||||||
map jarvis.orbishosting.com jarvis.orbishosting.com
|
|
||||||
map mail.tomtomgames.com mail.tomtomgames.com
|
map mail.tomtomgames.com mail.tomtomgames.com
|
||||||
map orbis.orbishosting.com orbis.orbishosting.com
|
map orbis.orbishosting.com orbis.orbishosting.com
|
||||||
map mail.parkerslingshotrentals.com mail.parkerslingshotrentals.com
|
map mail.parkerslingshotrentals.com mail.parkerslingshotrentals.com
|
||||||
@@ -41,8 +37,6 @@ virtualHost mail.tomtomgames.com {
|
|||||||
vhRoot /home/tomtomgames.com
|
vhRoot /home/tomtomgames.com
|
||||||
configFile $SERVER_ROOT/conf/vhosts/$VH_NAME/vhost.conf
|
configFile $SERVER_ROOT/conf/vhosts/$VH_NAME/vhost.conf
|
||||||
listener SSL IPv6 {
|
listener SSL IPv6 {
|
||||||
map mail.jarvis.orbishosting.com mail.jarvis.orbishosting.com
|
|
||||||
map jarvis.orbishosting.com jarvis.orbishosting.com
|
|
||||||
map mail.tomtomgames.com mail.tomtomgames.com
|
map mail.tomtomgames.com mail.tomtomgames.com
|
||||||
map orbis.orbishosting.com orbis.orbishosting.com
|
map orbis.orbishosting.com orbis.orbishosting.com
|
||||||
map mail.parkerslingshotrentals.com mail.parkerslingshotrentals.com
|
map mail.parkerslingshotrentals.com mail.parkerslingshotrentals.com
|
||||||
@@ -82,9 +76,3 @@ virtualHost mail.parkerslingshotrentals.com {
|
|||||||
virtualHost orbis.orbishosting.com {
|
virtualHost orbis.orbishosting.com {
|
||||||
vhRoot /home/$VH_NAME
|
vhRoot /home/$VH_NAME
|
||||||
configFile $SERVER_ROOT/conf/vhosts/$VH_NAME/vhost.conf
|
configFile $SERVER_ROOT/conf/vhosts/$VH_NAME/vhost.conf
|
||||||
virtualHost jarvis.orbishosting.com {
|
|
||||||
vhRoot /home/$VH_NAME
|
|
||||||
configFile $SERVER_ROOT/conf/vhosts/$VH_NAME/vhost.conf
|
|
||||||
virtualHost mail.jarvis.orbishosting.com {
|
|
||||||
vhRoot /home/jarvis.orbishosting.com
|
|
||||||
configFile $SERVER_ROOT/conf/vhosts/$VH_NAME/vhost.conf
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
# Websites on DO server — 2026-06-15
|
# Websites on DO server — 2026-06-21
|
||||||
|
|
||||||
- epictravelexpeditions.com (5.4M)
|
- epictravelexpeditions.com (5.4M)
|
||||||
- jarvis.orbishosting.com (764K)
|
|
||||||
- orbishosting.com (114M)
|
- orbishosting.com (114M)
|
||||||
- orbis.orbishosting.com (312K)
|
- orbis.orbishosting.com (312K)
|
||||||
- parkerslingshotrentals.com (1.6M)
|
- parkerslingshotrentals.com (1.6M)
|
||||||
- tomsjavajive.com (7.6M)
|
- tomsjavajive.com (8.0M)
|
||||||
- tomtomgames.com (4.3M)
|
- tomtomgames.com (4.4M)
|
||||||
|
|||||||
+72
-255
@@ -23,16 +23,50 @@ from pathlib import Path
|
|||||||
|
|
||||||
CONFIG_PATH = "/etc/jarvis-agent/config.json"
|
CONFIG_PATH = "/etc/jarvis-agent/config.json"
|
||||||
STATE_PATH = "/var/lib/jarvis-agent/state.json"
|
STATE_PATH = "/var/lib/jarvis-agent/state.json"
|
||||||
AGENT_VERSION = "3.0" # Phase 4: screenshot + sysinfo commands
|
AGENT_VERSION = "3.1"
|
||||||
|
|
||||||
# ── Config helpers ────────────────────────────────────────────────────────────
|
# ── Config helpers ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def load_config() -> dict:
|
def load_config() -> dict:
|
||||||
|
legacy_path = "/opt/jarvis-agent/config.json"
|
||||||
|
|
||||||
if not os.path.exists(CONFIG_PATH):
|
if not os.path.exists(CONFIG_PATH):
|
||||||
print(f"[ERROR] Config not found at {CONFIG_PATH}. Run the installer first.", flush=True)
|
if os.path.exists(legacy_path):
|
||||||
sys.exit(1)
|
print(f"[JARVIS] Config found at legacy path {legacy_path} - migrating...", flush=True)
|
||||||
with open(CONFIG_PATH) as f:
|
Path(CONFIG_PATH).parent.mkdir(parents=True, exist_ok=True)
|
||||||
return json.load(f)
|
with open(legacy_path) as f:
|
||||||
|
cfg = json.load(f)
|
||||||
|
else:
|
||||||
|
print(f"[ERROR] Config not found at {CONFIG_PATH}. Run the installer first.", flush=True)
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
with open(CONFIG_PATH) as f:
|
||||||
|
cfg = json.load(f)
|
||||||
|
|
||||||
|
# Migrate old key names so the agent self-heals instead of crash-looping
|
||||||
|
import re as _re
|
||||||
|
changed = False
|
||||||
|
if "server_url" in cfg and "jarvis_url" not in cfg:
|
||||||
|
cfg["jarvis_url"] = cfg.pop("server_url")
|
||||||
|
print("[JARVIS] Config migrated: server_url -> jarvis_url", flush=True)
|
||||||
|
changed = True
|
||||||
|
if "api_key" in cfg and "registration_key" not in cfg:
|
||||||
|
cfg["registration_key"] = cfg.pop("api_key")
|
||||||
|
print("[JARVIS] Config migrated: api_key -> registration_key", flush=True)
|
||||||
|
changed = True
|
||||||
|
if "hostname" not in cfg:
|
||||||
|
cfg["hostname"] = socket.gethostname()
|
||||||
|
changed = True
|
||||||
|
if "ssl_verify" not in cfg:
|
||||||
|
cfg["ssl_verify"] = not bool(_re.match(r"https?://\d+\.\d+\.\d+\.\d+", cfg.get("jarvis_url", "")))
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
if changed:
|
||||||
|
with open(CONFIG_PATH, "w") as f:
|
||||||
|
json.dump(cfg, f, indent=2)
|
||||||
|
print("[JARVIS] Config saved after migration.", flush=True)
|
||||||
|
|
||||||
|
return cfg
|
||||||
|
|
||||||
def load_state() -> dict:
|
def load_state() -> dict:
|
||||||
if os.path.exists(STATE_PATH):
|
if os.path.exists(STATE_PATH):
|
||||||
@@ -119,12 +153,6 @@ def detect_capabilities(cfg: dict) -> list:
|
|||||||
# Check for Home Assistant
|
# Check for Home Assistant
|
||||||
if os.path.exists("/etc/homeassistant") or os.path.exists("/config/configuration.yaml"):
|
if os.path.exists("/etc/homeassistant") or os.path.exists("/config/configuration.yaml"):
|
||||||
caps.append("homeassistant")
|
caps.append("homeassistant")
|
||||||
# Phase 4: screenshot capability
|
|
||||||
import shutil as _shutil
|
|
||||||
if (_shutil.which("scrot") or _shutil.which("import") or
|
|
||||||
_shutil.which("fbcat") or _shutil.which("convert")):
|
|
||||||
caps.append("screenshot")
|
|
||||||
caps.append("sysinfo")
|
|
||||||
return caps
|
return caps
|
||||||
|
|
||||||
def register(cfg: dict, state: dict) -> str:
|
def register(cfg: dict, state: dict) -> str:
|
||||||
@@ -142,6 +170,7 @@ def register(cfg: dict, state: dict) -> str:
|
|||||||
f"{cfg['jarvis_url']}/api/agent/register",
|
f"{cfg['jarvis_url']}/api/agent/register",
|
||||||
{
|
{
|
||||||
"hostname": hostname,
|
"hostname": hostname,
|
||||||
|
"version": AGENT_VERSION,
|
||||||
"agent_type": agent_type,
|
"agent_type": agent_type,
|
||||||
"ip_address": ip,
|
"ip_address": ip,
|
||||||
"capabilities": capabilities,
|
"capabilities": capabilities,
|
||||||
@@ -270,11 +299,23 @@ def get_load() -> list:
|
|||||||
except Exception:
|
except Exception:
|
||||||
return [0, 0, 0]
|
return [0, 0, 0]
|
||||||
|
|
||||||
|
def get_nordvpn_status() -> dict | None:
|
||||||
|
"""Check nordlynx WireGuard interface. Returns None if nordlynx not present on this host."""
|
||||||
|
try:
|
||||||
|
r = subprocess.run(["ip", "link", "show", "nordlynx"],
|
||||||
|
capture_output=True, text=True, timeout=3)
|
||||||
|
if r.returncode != 0:
|
||||||
|
return None
|
||||||
|
active = "UP,LOWER_UP" in r.stdout or "state UP" in r.stdout
|
||||||
|
return {"active": active, "interface": "nordlynx"}
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
def collect_metrics(cfg: dict) -> dict:
|
def collect_metrics(cfg: dict) -> dict:
|
||||||
# First reading for CPU delta
|
# First reading for CPU delta
|
||||||
get_cpu_percent()
|
get_cpu_percent()
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
return {
|
metrics = {
|
||||||
"hostname": cfg.get("hostname", socket.gethostname()),
|
"hostname": cfg.get("hostname", socket.gethostname()),
|
||||||
"cpu_percent": get_cpu_percent(),
|
"cpu_percent": get_cpu_percent(),
|
||||||
"memory": get_memory(),
|
"memory": get_memory(),
|
||||||
@@ -285,6 +326,10 @@ def collect_metrics(cfg: dict) -> dict:
|
|||||||
"platform": platform.system(),
|
"platform": platform.system(),
|
||||||
"timestamp": datetime.utcnow().isoformat() + "Z",
|
"timestamp": datetime.utcnow().isoformat() + "Z",
|
||||||
}
|
}
|
||||||
|
nordvpn = get_nordvpn_status()
|
||||||
|
if nordvpn is not None:
|
||||||
|
metrics["nordvpn"] = nordvpn
|
||||||
|
return metrics
|
||||||
|
|
||||||
# ── Proxmox metrics ───────────────────────────────────────────────────────────
|
# ── Proxmox metrics ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -304,198 +349,6 @@ def collect_proxmox_metrics(cfg: dict) -> dict | None:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"error": str(e)}
|
return {"error": str(e)}
|
||||||
|
|
||||||
# ── Screenshot / Vision helpers ───────────────────────────────────────────────
|
|
||||||
|
|
||||||
def _take_screenshot(cmd_data: dict) -> dict:
|
|
||||||
"""
|
|
||||||
Attempts to capture a screenshot using available tools.
|
|
||||||
For headless servers, falls back to a rich text system snapshot.
|
|
||||||
Returns base64-encoded PNG and metadata.
|
|
||||||
"""
|
|
||||||
import base64, tempfile, shutil
|
|
||||||
|
|
||||||
tmp = tempfile.mktemp(suffix=".png")
|
|
||||||
width = height = 0
|
|
||||||
method = "unknown"
|
|
||||||
|
|
||||||
# 1. Try scrot (X11 desktop)
|
|
||||||
if shutil.which("scrot") and os.environ.get("DISPLAY"):
|
|
||||||
try:
|
|
||||||
r = subprocess.run(["scrot", "-z", tmp], capture_output=True, timeout=10)
|
|
||||||
if r.returncode == 0 and os.path.exists(tmp):
|
|
||||||
method = "scrot"
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 2. Try import (ImageMagick X11)
|
|
||||||
if method == "unknown" and shutil.which("import") and os.environ.get("DISPLAY"):
|
|
||||||
try:
|
|
||||||
r = subprocess.run(["import", "-window", "root", tmp], capture_output=True, timeout=10)
|
|
||||||
if r.returncode == 0 and os.path.exists(tmp):
|
|
||||||
method = "import"
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 3. Try fbcat (Linux framebuffer — headless VMs with framebuffer)
|
|
||||||
if method == "unknown" and shutil.which("fbcat") and os.path.exists("/dev/fb0"):
|
|
||||||
try:
|
|
||||||
ppm = tempfile.mktemp(suffix=".ppm")
|
|
||||||
r = subprocess.run(["fbcat", "-s", "/dev/fb0"], stdout=open(ppm, "wb"),
|
|
||||||
stderr=subprocess.PIPE, timeout=10)
|
|
||||||
if r.returncode == 0 and shutil.which("convert"):
|
|
||||||
subprocess.run(["convert", ppm, tmp], capture_output=True, timeout=10)
|
|
||||||
os.unlink(ppm)
|
|
||||||
if os.path.exists(tmp):
|
|
||||||
method = "framebuffer"
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 4. Headless fallback: build a PNG system dashboard from text stats
|
|
||||||
if method == "unknown":
|
|
||||||
try:
|
|
||||||
result = _render_sysinfo_png(tmp)
|
|
||||||
if result:
|
|
||||||
method = "sysinfo_render"
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if method == "unknown" or not os.path.exists(tmp):
|
|
||||||
# Last resort: return text snapshot only
|
|
||||||
snap = _sysinfo_snapshot()
|
|
||||||
snap["screenshot_available"] = False
|
|
||||||
snap["method"] = "text_only"
|
|
||||||
return snap
|
|
||||||
|
|
||||||
# Read image
|
|
||||||
try:
|
|
||||||
with open(tmp, "rb") as f:
|
|
||||||
raw = f.read()
|
|
||||||
b64 = base64.b64encode(raw).decode()
|
|
||||||
fsize = len(raw)
|
|
||||||
os.unlink(tmp)
|
|
||||||
|
|
||||||
# Try to get dimensions via file command
|
|
||||||
try:
|
|
||||||
r = subprocess.run(["identify", "-format", "%wx%h", tmp],
|
|
||||||
capture_output=True, text=True, timeout=5)
|
|
||||||
if "x" in r.stdout:
|
|
||||||
w, h = r.stdout.strip().split("x", 1)
|
|
||||||
width, height = int(w), int(h)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"method": method,
|
|
||||||
"image_b64": b64,
|
|
||||||
"image_mime": "image/png",
|
|
||||||
"file_size": fsize,
|
|
||||||
"width": width,
|
|
||||||
"height": height,
|
|
||||||
"hostname": socket.gethostname(),
|
|
||||||
}
|
|
||||||
except Exception as e:
|
|
||||||
return {"success": False, "error": str(e), "method": method}
|
|
||||||
|
|
||||||
|
|
||||||
def _render_sysinfo_png(out_path: str) -> bool:
|
|
||||||
"""Render a system info text snapshot as a PNG using ansi2image or ImageMagick."""
|
|
||||||
import shutil
|
|
||||||
snap = _build_sysinfo_text()
|
|
||||||
# Try convert (ImageMagick) to render text → PNG
|
|
||||||
if shutil.which("convert"):
|
|
||||||
try:
|
|
||||||
r = subprocess.run([
|
|
||||||
"convert",
|
|
||||||
"-size", "900x600", "xc:#0a0f14",
|
|
||||||
"-font", "Courier-New",
|
|
||||||
"-pointsize", "13",
|
|
||||||
"-fill", "#00d4ff",
|
|
||||||
"-annotate", "+20+30", snap[:3000],
|
|
||||||
out_path,
|
|
||||||
], capture_output=True, timeout=15)
|
|
||||||
return r.returncode == 0 and os.path.exists(out_path)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def _build_sysinfo_text() -> str:
|
|
||||||
"""Build a rich text system snapshot for headless machines."""
|
|
||||||
lines = [f"JARVIS FIELD STATION — {socket.gethostname()}",
|
|
||||||
f"Timestamp: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')}",
|
|
||||||
"─" * 60]
|
|
||||||
try:
|
|
||||||
# CPU / mem / disk
|
|
||||||
with open("/proc/loadavg") as f:
|
|
||||||
load = f.read().split()[:3]
|
|
||||||
lines.append(f"Load avg: {' '.join(load)}")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
with open("/proc/meminfo") as f:
|
|
||||||
minfo = {l.split(":")[0].strip(): int(l.split()[1]) for l in f if ":" in l}
|
|
||||||
total = minfo.get("MemTotal", 0)
|
|
||||||
avail = minfo.get("MemAvailable", 0)
|
|
||||||
used = total - avail
|
|
||||||
lines.append(f"Memory: {used//1024}MB used / {total//1024}MB total")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
r = subprocess.run(["df", "-h", "/"], capture_output=True, text=True, timeout=5)
|
|
||||||
lines.append("Disk:\n" + r.stdout.strip())
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
r = subprocess.run(["ps", "aux", "--sort=-%cpu"], capture_output=True, text=True, timeout=5)
|
|
||||||
lines.append("Top processes:\n" + "\n".join(r.stdout.splitlines()[1:8]))
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
r = subprocess.run(["ss", "-tlnp"], capture_output=True, text=True, timeout=5)
|
|
||||||
lines.append("Listening ports:\n" + r.stdout.strip()[:500])
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
|
|
||||||
def _sysinfo_snapshot() -> dict:
|
|
||||||
"""Return structured system snapshot (no image) for text-based analysis."""
|
|
||||||
data = {"success": True, "hostname": socket.gethostname(),
|
|
||||||
"snapshot_type": "text", "screenshot_available": False}
|
|
||||||
try:
|
|
||||||
with open("/proc/loadavg") as f:
|
|
||||||
parts = f.read().split()
|
|
||||||
data["load_1m"], data["load_5m"], data["load_15m"] = parts[0], parts[1], parts[2]
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
with open("/proc/meminfo") as f:
|
|
||||||
m = {l.split(":")[0].strip(): int(l.split()[1])
|
|
||||||
for l in f if ":" in l and len(l.split()) >= 2}
|
|
||||||
data["mem_total_mb"] = m.get("MemTotal", 0) // 1024
|
|
||||||
data["mem_avail_mb"] = m.get("MemAvailable", 0) // 1024
|
|
||||||
data["mem_used_mb"] = data["mem_total_mb"] - data["mem_avail_mb"]
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
r = subprocess.run(["df", "-h", "/"], capture_output=True, text=True, timeout=5)
|
|
||||||
data["disk"] = r.stdout.splitlines()[1] if r.stdout else ""
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
r = subprocess.run(["ps", "aux", "--sort=-%cpu"], capture_output=True, text=True, timeout=5)
|
|
||||||
data["top_procs"] = r.stdout.splitlines()[1:8]
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
r = subprocess.run(["ss", "-tlnp"], capture_output=True, text=True, timeout=5)
|
|
||||||
data["listening_ports"] = r.stdout.strip()[:800]
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
# ── Command execution ─────────────────────────────────────────────────────────
|
# ── Command execution ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def execute_command(cmd: dict) -> dict:
|
def execute_command(cmd: dict) -> dict:
|
||||||
@@ -525,25 +378,17 @@ def execute_command(cmd: dict) -> dict:
|
|||||||
return {"success": r.returncode == 0, "output": r.stdout}
|
return {"success": r.returncode == 0, "output": r.stdout}
|
||||||
|
|
||||||
elif cmd_type == "update":
|
elif cmd_type == "update":
|
||||||
_cfg = load_config()
|
updated = self_update(cfg)
|
||||||
updated = self_update(_cfg)
|
|
||||||
return {"success": True, "updated": updated}
|
return {"success": True, "updated": updated}
|
||||||
|
|
||||||
elif cmd_type == "shell":
|
elif cmd_type == "shell":
|
||||||
# Guard reads LOCAL config, not the server-supplied payload
|
# Only allow if explicitly enabled in config
|
||||||
_cfg = load_config()
|
if not cmd_data.get("allowed", False):
|
||||||
if not _cfg.get("allow_shell_commands", False):
|
return {"success": False, "error": "Shell commands not enabled"}
|
||||||
return {"success": False, "error": "Shell commands not enabled in agent config"}
|
|
||||||
cmd_str = cmd_data.get("command", "")
|
cmd_str = cmd_data.get("command", "")
|
||||||
r = subprocess.run(cmd_str, shell=True, capture_output=True, text=True, timeout=30)
|
r = subprocess.run(cmd_str, shell=True, capture_output=True, text=True, timeout=30)
|
||||||
return {"success": True, "stdout": r.stdout[:2000], "stderr": r.stderr[:500]}
|
return {"success": True, "stdout": r.stdout[:2000], "stderr": r.stderr[:500]}
|
||||||
|
|
||||||
elif cmd_type == "screenshot":
|
|
||||||
return _take_screenshot(cmd_data)
|
|
||||||
|
|
||||||
elif cmd_type == "sysinfo":
|
|
||||||
return _sysinfo_snapshot()
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return {"success": False, "error": f"Unknown command type: {cmd_type}"}
|
return {"success": False, "error": f"Unknown command type: {cmd_type}"}
|
||||||
|
|
||||||
@@ -565,18 +410,15 @@ def main():
|
|||||||
poll_interval = int(cfg.get("poll_interval", 30))
|
poll_interval = int(cfg.get("poll_interval", 30))
|
||||||
heartbeat_every = int(cfg.get("heartbeat_every", 10))
|
heartbeat_every = int(cfg.get("heartbeat_every", 10))
|
||||||
|
|
||||||
# Always re-register on startup to refresh capabilities, version, and IP.
|
# Register if no API key yet
|
||||||
# Server does an UPDATE when agent_id already exists, so api_key is preserved.
|
|
||||||
api_key = state.get("api_key", "")
|
api_key = state.get("api_key", "")
|
||||||
registered_key = register(cfg, state)
|
if not api_key:
|
||||||
if registered_key:
|
api_key = register(cfg, state)
|
||||||
api_key = registered_key
|
if not api_key:
|
||||||
elif not api_key:
|
print("[ERROR] Could not register with JARVIS. Retrying in 60s...", flush=True)
|
||||||
while not api_key:
|
time.sleep(60)
|
||||||
api_key = register(cfg, state)
|
main()
|
||||||
if not api_key:
|
return
|
||||||
print("[ERROR] Could not register with JARVIS. Retrying in 60s...", flush=True)
|
|
||||||
time.sleep(60)
|
|
||||||
|
|
||||||
headers = {"X-Agent-Key": api_key}
|
headers = {"X-Agent-Key": api_key}
|
||||||
last_metrics = 0
|
last_metrics = 0
|
||||||
@@ -592,7 +434,7 @@ def main():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Heartbeat + get commands
|
# Heartbeat + get commands
|
||||||
hb = api_post(f"{jarvis_url}/api/agent/heartbeat", {}, headers, ssl_verify=ssl_verify)
|
hb = api_post(f"{jarvis_url}/api/agent/heartbeat", {"version": AGENT_VERSION}, headers, ssl_verify=ssl_verify)
|
||||||
if "error" in hb:
|
if "error" in hb:
|
||||||
print(f"[WARN] Heartbeat failed: {hb['error']}", flush=True)
|
print(f"[WARN] Heartbeat failed: {hb['error']}", flush=True)
|
||||||
else:
|
else:
|
||||||
@@ -633,9 +475,7 @@ def main():
|
|||||||
# ── Self-update ────────────────────────────────────────────────────────────────
|
# ── Self-update ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def self_update(cfg: dict) -> bool:
|
def self_update(cfg: dict) -> bool:
|
||||||
"""Check JARVIS server for a newer version of this script.
|
"""Check JARVIS server for a newer version of this script. If different, replace and restart."""
|
||||||
Verifies SHA-256 hash from <update_url>.sha256 before replacing."""
|
|
||||||
import hashlib
|
|
||||||
jarvis_url = cfg.get("jarvis_url", "").rstrip("/")
|
jarvis_url = cfg.get("jarvis_url", "").rstrip("/")
|
||||||
default_update_url = f"{jarvis_url}/agent/jarvis-agent.py" if jarvis_url else ""
|
default_update_url = f"{jarvis_url}/agent/jarvis-agent.py" if jarvis_url else ""
|
||||||
update_url = cfg.get("update_url", default_update_url)
|
update_url = cfg.get("update_url", default_update_url)
|
||||||
@@ -643,37 +483,14 @@ def self_update(cfg: dict) -> bool:
|
|||||||
return False
|
return False
|
||||||
script_path = os.path.abspath(__file__)
|
script_path = os.path.abspath(__file__)
|
||||||
try:
|
try:
|
||||||
# Download expected hash first
|
|
||||||
hash_url = update_url + ".sha256"
|
|
||||||
req_hash = urllib.request.Request(hash_url)
|
|
||||||
req_hash.add_header("User-Agent", "JARVIS-Agent/1.0")
|
|
||||||
if _host_header:
|
|
||||||
req_hash.add_header("Host", _host_header)
|
|
||||||
try:
|
|
||||||
with urllib.request.urlopen(req_hash, timeout=10) as resp:
|
|
||||||
expected_hash = resp.read().decode().strip().split()[0]
|
|
||||||
except Exception:
|
|
||||||
expected_hash = None
|
|
||||||
|
|
||||||
# Download new script
|
|
||||||
req = urllib.request.Request(update_url)
|
req = urllib.request.Request(update_url)
|
||||||
req.add_header("User-Agent", "JARVIS-Agent/1.0")
|
req.add_header("User-Agent", "JARVIS-Agent/1.0")
|
||||||
if _host_header:
|
|
||||||
req.add_header("Host", _host_header)
|
|
||||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||||
new_content = resp.read()
|
new_content = resp.read()
|
||||||
|
|
||||||
# Verify hash if available — abort if mismatch
|
|
||||||
if expected_hash:
|
|
||||||
actual_hash = hashlib.sha256(new_content).hexdigest()
|
|
||||||
if actual_hash != expected_hash:
|
|
||||||
print(f"[JARVIS] Update hash mismatch (expected {expected_hash[:16]}… got {actual_hash[:16]}…) — aborting", flush=True)
|
|
||||||
return False
|
|
||||||
|
|
||||||
with open(script_path, "rb") as f:
|
with open(script_path, "rb") as f:
|
||||||
current = f.read()
|
current = f.read()
|
||||||
if new_content != current:
|
if new_content != current:
|
||||||
print(f"[JARVIS] Update verified — replacing {script_path} and restarting...", flush=True)
|
print(f"[JARVIS] Update available — replacing {script_path} and restarting...", flush=True)
|
||||||
with open(script_path, "wb") as f:
|
with open(script_path, "wb") as f:
|
||||||
f.write(new_content)
|
f.write(new_content)
|
||||||
os.execv(sys.executable, [sys.executable] + sys.argv)
|
os.execv(sys.executable, [sys.executable] + sys.argv)
|
||||||
|
|||||||
+1
-3
@@ -1,4 +1,2 @@
|
|||||||
# Added and Managed by DigitalOcean Droplet Agent (code name: DOTTY)
|
# Added and Managed by DigitalOcean Droplet Agent (code name: DOTTY)
|
||||||
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNIbUSWm6l0NcFEOukQGx5br54nFbjP2FzR7QYBT0dYs5j/AeuQ9WiOomkJMV6zcfUTKpAjUJJtbk1S9SXWrnq8= {"os_user":"root","actor_email":"myronblair@gmail.com","expire_at":"2026-05-27T18:25:20Z"}-dotty_ssh
|
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFuD94cTFSBqw9+Qdq8yOvF2ca6KtbtRBDFD3cFpm7kHCDqDbIGMjwgnaDGoEeexABr3+1itCnepvKono6JFMoI= {"os_user":"root","actor_email":"myronblair@gmail.com","expire_at":"2026-06-17T23:17:20Z"}-dotty_ssh
|
||||||
# Added and Managed by DigitalOcean Droplet Agent (code name: DOTTY)
|
|
||||||
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMZhO/c3NsDBamiit4eMAIlm/zpg02RzEUNpeupB4azjrvTgAAij1UGlJ5wu4fkT7U//2GRdL4QmDbPYR0uBapQ= {"os_user":"root","actor_email":"myronblair@gmail.com","expire_at":"2026-05-27T18:24:59Z"}-dotty_ssh
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCqrGQSywH1r8h8cpIZ4qAIematYXmOLaz/7wBktsUI6ugxpBjhFyu4Rl+E+8WcN9icBoEfT0LSk9Ql+v2AjFIOo8pu1ktN5OnFQ28N3WfMU+jUBrZjTg0GaQCwTeiT53eb/+hZYY4JkKNlESZlEIiswM3Gqtm5EteU5Szq2r+eGub1cu0juk+WM+r/CexIXBInPspAP6v/hkRrABlemaA/D+4KB+PNAorMHZab8Vbn+RnEE7C5ppohPsalnowxLy3VHs+cbMAKpiBYfuRGzgbiIf1k/5V+0epl38QajJ0UEUwZwhZ3UgWklR+l1jd8B4VeYilAdaiMg60E64ans9FFEoud+r5DKuyz2GdjSFshOgJPtyoZr7Vw+IqNv4rdnxZv5OBTQprAzx8HlwMtduS8uXIPbDz2QYEQRAeSp8CT9O4auU25hArgLb+lTRN4PdcCIlQM2ZZU8xQG4Upy64Ol5yEbCTYFfc49Hs5EJqv/snNiDJLiH1BMR9r5rye0Y3e0HeM+MwvqgZ0ExQzJgAJbmlLejGmv3p0XUiT3UtIDdfBLL5QTPDYlmrwRpELTPasAc3KkoicWTch/8eRp5mcpRyo4Tb2PVrWmGw/qSkuxoTXzmPgS2//UpYY+6gC+kDqJSJYr0LUgPOfYG1LBYIhrVRM5RNTRaCbnLXFanvzf0Q== root@orbis
|
||||||
Reference in New Issue
Block a user