mirror of
https://github.com/myronblair/do-server-config
synced 2026-06-30 17:50:59 -05:00
[orbis] Weekly backup 2026-06-14 — 4 files changed, 433 insertions(+), 12 deletions(-)
This commit is contained in:
+203
@@ -0,0 +1,203 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Server Map
|
||||||
|
|
||||||
|
| Host | IP | SSH | Purpose |
|
||||||
|
|------|-----|-----|---------|
|
||||||
|
| DO (main) | 165.22.1.228 | `root / Gonewalk1974!@#` | CyberPanel/OLS — all websites + JARVIS |
|
||||||
|
| FusionPBX | 134.209.72.226 | `root / Joker1974!@#` | FreeSWITCH PBX |
|
||||||
|
| PVE1 (Proxmox) | orbisne.fortiddns.com (10.48.200.90) | `root / Joker1974!!!` | Primary hypervisor — FortiGate DDNS, auto-updates if IP changes |
|
||||||
|
| 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) |
|
||||||
|
| Home Assistant | 10.48.200.97 | `myron → sudo` | HA VM 101 |
|
||||||
|
| 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) |
|
||||||
|
|
||||||
|
**SSH password order** (try in sequence if first fails): `Joker1974!@#` → `Joker1974!!!` → `Joker1974!`
|
||||||
|
|
||||||
|
**SSH pattern for all remote work:**
|
||||||
|
```bash
|
||||||
|
sshpass -p 'Gonewalk1974!@#' ssh -o StrictHostKeyChecking=no root@165.22.1.228 'commands here'
|
||||||
|
```
|
||||||
|
|
||||||
|
For PVE1 from anywhere (FortiGate DDNS, survives IP changes):
|
||||||
|
```bash
|
||||||
|
sshpass -p 'Joker1974!!!' ssh -o StrictHostKeyChecking=no root@orbisne.fortiddns.com 'commands here'
|
||||||
|
```
|
||||||
|
|
||||||
|
For commands inside VMs on PVE1:
|
||||||
|
```bash
|
||||||
|
sshpass -p 'Joker1974!!!' ssh -o StrictHostKeyChecking=no root@orbisne.fortiddns.com \
|
||||||
|
'qm guest exec 210 -- bash -c "commands here"'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Websites on DO (165.22.1.228)
|
||||||
|
|
||||||
|
All sites live at `/home/<domain>/public_html/` on DO. CyberPanel/OpenLiteSpeed serves them.
|
||||||
|
|
||||||
|
| Site | Path | GitHub |
|
||||||
|
|------|------|--------|
|
||||||
|
| jarvis.orbishosting.com | /home/jarvis.orbishosting.com/ | myronblair/jarvis |
|
||||||
|
| tomsjavajive.com | /home/tomsjavajive.com/public_html/ | myronblair/tomsjavajive |
|
||||||
|
| epictravelexpeditions.com | /home/epictravelexpeditions.com/public_html/ | myronblair/epictravelexpeditions |
|
||||||
|
| parkerslingshotrentals.com | /home/parkerslingshotrentals.com/public_html/ | myronblair/parkerslingshotrentals |
|
||||||
|
| orbishosting.com | /home/orbishosting.com/public_html/ | myronblair/orbishosting |
|
||||||
|
| orbis.orbishosting.com | /home/orbis.orbishosting.com/public_html/ | myronblair/orbis-hosting-portal |
|
||||||
|
| tomtomgames.com | /home/tomtomgames.com/public_html/ | myronblair/tomtomgames |
|
||||||
|
|
||||||
|
**Parker Slingshot** is served from epictravelexpeditions.com, not its own domain:
|
||||||
|
- URL: `https://parkerslingshot.epictravelexpeditions.com`
|
||||||
|
- Path: `/home/epictravelexpeditions.com/parkerslingshot/`
|
||||||
|
- GitHub: `myronblair/parkerslingshot` (own repo, auto-deploy active)
|
||||||
|
- `db.php` and `config.php` are gitignored (credentials); `db.php.example` is the reference template
|
||||||
|
|
||||||
|
## Deployment Workflow
|
||||||
|
|
||||||
|
**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`)
|
||||||
|
Deploy queue: `/tmp/jarvis-deploy-queue.txt` | Runner: `/usr/local/bin/jarvis-deploy.sh` (cron every min)
|
||||||
|
Deploy log: `/home/jarvis.orbishosting.com/logs/deploy.log`
|
||||||
|
|
||||||
|
For hotfixes that can't wait 1 min, SCP directly:
|
||||||
|
```bash
|
||||||
|
sshpass -p 'Gonewalk1974!@#' scp -o StrictHostKeyChecking=no /tmp/changed.php \
|
||||||
|
root@165.22.1.228:/home/site.com/public_html/changed.php
|
||||||
|
```
|
||||||
|
|
||||||
|
GitHub PAT (embedded in remote URLs): `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` — expires ~2026-08-20.
|
||||||
|
Infra repo: `myronblair/infra` — cloned at `/opt/infra` on DO server.
|
||||||
|
|
||||||
|
**DO server backup:** `myronblair/do-server-config` — scripts, systemd units, WireGuard, OLS vhosts, cron, MySQL creds + 8-phase restore wizard. Weekly Sunday 4am. Launcher: `/usr/local/bin/do-server-backup`.
|
||||||
|
|
||||||
|
Gitignored credentials (never in GitHub): `api/config.php` (JARVIS, epictravelexpeditions), `config/database.php` (tomsjavajive).
|
||||||
|
|
||||||
|
## JARVIS System
|
||||||
|
|
||||||
|
Iron Man-style AI dashboard at `https://jarvis.orbishosting.com`. All files on DO at `/home/jarvis.orbishosting.com/`.
|
||||||
|
|
||||||
|
**Architecture:**
|
||||||
|
- `public_html/api.php` — API router; has `session_write_close()` guard (must skip for `auth` endpoint to prevent LSAPI session deadlock)
|
||||||
|
- `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/agent.php` — push-based agent registration/heartbeat/metrics
|
||||||
|
- `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/stats_cache.php` — runs every 5 min via cron; weather/news/Proxmox cache refresh
|
||||||
|
- `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`)
|
||||||
|
|
||||||
|
**Voice system (index.html):**
|
||||||
|
- Continuous SpeechRecognition; mic stays open always (mute toggle button)
|
||||||
|
- **Phase 1 wake**: say "wake up JARVIS" or "daddy's home" → activates voice mode once
|
||||||
|
- **Phase 2 command**: say "JARVIS [command]" → executes; opens 17-second free-listen window (no prefix needed for follow-ups)
|
||||||
|
- After 30 min of no commands → voice sleeps; full wake phrase required again
|
||||||
|
- Mic paused during ElevenLabs TTS playback (`isSpeaking` guard) to prevent feedback loop
|
||||||
|
- Auto-reload after 5 min idle is silent (no greeting speech)
|
||||||
|
|
||||||
|
**Planner system (2026-05-31):**
|
||||||
|
- DB tables: `tasks` (id, title, notes, category, priority, status, due_date, due_time, completed_at) and `appointments` (id, title, description, category, start_at, end_at, location, all_day, reminder_min, alerted)
|
||||||
|
- Voice commands: "add task [title]", "my tasks", "mark [x] done", "schedule [title] on [date]", "my calendar", "daily briefing"
|
||||||
|
- 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
|
||||||
|
|
||||||
|
**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 installer (one-liner for any Linux host): `curl -sk https://jarvis.orbishosting.com/install-agent.sh | bash -s <hostname> <linux|homeassistant|proxmox>`
|
||||||
|
|
||||||
|
**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.
|
||||||
|
Watchdog log: `/home/jarvis.orbishosting.com/logs/watchdog.log`
|
||||||
|
|
||||||
|
**JARVIS DB:** `jarvis_db` on localhost. User: `jarvis_user / J4rv1s_Pr0t0c0l_2026!`. phpMyAdmin at `/phpmyadmin` (myron / Joker1974!!!).
|
||||||
|
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)
|
||||||
|
|
||||||
|
**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).
|
||||||
|
|
||||||
|
## Tom's Java Jive
|
||||||
|
|
||||||
|
PHP e-commerce site. DB: `toms_tjj_db / toms_tjj_user / +60wlPc+55e@gFq4`. Key quirks:
|
||||||
|
- No `slug` column on products — URLs use `?id=product_id`
|
||||||
|
- All tables must be `utf8mb4_unicode_ci` — mixed collation causes MySQL error 1267 on JOINs
|
||||||
|
- `wallet_transactions.type` and `loyalty_transactions.type` have strict enums (see memory for values)
|
||||||
|
|
||||||
|
## MediaStack
|
||||||
|
|
||||||
|
Automated media server on PVE1 VM 113. All traffic routes through WireGuard VPN → DO, bypassing home ISP.
|
||||||
|
|
||||||
|
**SSH:** From PVE1: `ssh -o StrictHostKeyChecking=no -i /root/.ssh/id_rsa root@10.48.200.35`
|
||||||
|
|
||||||
|
**Services:**
|
||||||
|
| Service | Port | Login |
|
||||||
|
|---------|------|-------|
|
||||||
|
| qBittorrent | :8080 | admin / Joker1974!!! |
|
||||||
|
| Sonarr | :8989 | API key: `b43e04350a594846b4ee95261c29e9e0` |
|
||||||
|
| Radarr | :7878 | API key: `53c4268360444feeae5f98c0cc24e0e3` |
|
||||||
|
| Prowlarr | :9696 | API key: `9d0ce6c5660743b5bf1c7951efc62252` |
|
||||||
|
|
||||||
|
**Media paths:** downloads → `/media/downloads/complete` | movies → `/media/movies` | tv → `/media/tv`
|
||||||
|
**Jellyfin NFS mounts** (VM 112, 10.48.200.33): `/mnt/mediastack/movies` and `/mnt/mediastack/tv`
|
||||||
|
**WireGuard:** `wg0` IP `10.200.0.4` → CT110 (10.48.200.19:51821) → DO. Kill-switch active; LAN always allowed.
|
||||||
|
**DNS:** FortiGate blocks port 53 outbound. PVE1 runs dnsmasq on :53 → 100.100.100.100. MediaStack uses `DNS=10.48.200.90`.
|
||||||
|
**Indexer:** IPTorrents via Prowlarr (cookie auth). Prowlarr syncs to Sonarr + Radarr automatically.
|
||||||
|
**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.
|
||||||
|
|
||||||
|
## 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!!!`.
|
||||||
|
|
||||||
|
## FusionPBX / FreeSWITCH
|
||||||
|
|
||||||
|
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:
|
||||||
|
```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"'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Backup:** `myronblair/fusionpbx-config` — PostgreSQL dump (gzip, ~29MB) + FreeSWITCH configs + restore wizard. Weekly Sunday 5am. Launcher: `/usr/local/bin/fusionpbx-backup`.
|
||||||
|
|
||||||
|
## Proxmox
|
||||||
|
|
||||||
|
PVE1 at 10.48.200.90, PVE2 at 10.48.200.91. Root login direct. Run commands inside VMs via:
|
||||||
|
```bash
|
||||||
|
qm guest exec <VMID> -- bash -c 'command'
|
||||||
|
```
|
||||||
|
Proxmox `--nameserver` must be space-separated: `"8.8.8.8 1.1.1.1"` (comma causes netplan bug).
|
||||||
|
|
||||||
|
**Backup:** `myronblair/proxmox-config` — shared cluster configs (VM .conf, storage, HA, SDN) + per-node (network, cron, systemd, scripts). Weekly Sunday 3am. Launcher: `/usr/local/bin/proxmox-backup` on both nodes.
|
||||||
|
|
||||||
|
## PHP / OLS Runtime
|
||||||
|
|
||||||
|
CyberPanel uses `lsphp85`. Run PHP scripts directly with:
|
||||||
|
```bash
|
||||||
|
/usr/local/lsws/lsphp85/bin/lsphp /path/to/script.php
|
||||||
|
```
|
||||||
|
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.
|
||||||
|
|
||||||
|
## GitHub Repos
|
||||||
|
|
||||||
|
| Repo | Site | DB Schema |
|
||||||
|
|------|------|-----------|
|
||||||
|
| myronblair/jarvis | jarvis.orbishosting.com | db/schema.sql (15 tables) |
|
||||||
|
| myronblair/tomsjavajive | tomsjavajive.com | db/schema.sql (45 tables) |
|
||||||
|
| myronblair/epictravelexpeditions | epictravelexpeditions.com | db/schema.sql (7 tables) |
|
||||||
|
| myronblair/parkerslingshot | parkerslingshot.epictravelexpeditions.com | (no schema file — DB managed directly) |
|
||||||
|
| myronblair/parkerslingshotrentals | parkerslingshotrentals.com | db/schema.sql (10 tables) |
|
||||||
|
| myronblair/orbishosting | orbishosting.com | — |
|
||||||
|
| myronblair/orbis-hosting-portal | orbis.orbishosting.com | — |
|
||||||
|
| myronblair/tomtomgames | tomtomgames.com | db/schema.sql (22 tables) |
|
||||||
|
| myronblair/infra | server configs | cron, systemd, agent configs |
|
||||||
|
| 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/proxmox-config | PVE1+PVE2 backup | shared cluster configs + per-node, 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`.
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
# Websites on DO server — 2026-06-09
|
# Websites on DO server — 2026-06-14
|
||||||
|
|
||||||
- epictravelexpeditions.com (5.4M)
|
- epictravelexpeditions.com (5.4M)
|
||||||
- jarvis.orbishosting.com (440K)
|
- jarvis.orbishosting.com (704K)
|
||||||
- orbishosting.com (113M)
|
- orbishosting.com (113M)
|
||||||
- orbis.orbishosting.com (288K)
|
- orbis.orbishosting.com (312K)
|
||||||
- parkerslingshotrentals.com (1.6M)
|
- parkerslingshotrentals.com (1.6M)
|
||||||
- tomsjavajive.com (5.0M)
|
- tomsjavajive.com (5.0M)
|
||||||
- tomtomgames.com (4.3M)
|
- tomtomgames.com (4.3M)
|
||||||
|
|||||||
+220
-9
@@ -23,7 +23,7 @@ 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 = "2.3" # bumped on each release
|
AGENT_VERSION = "3.0" # Phase 4: screenshot + sysinfo commands
|
||||||
|
|
||||||
# ── Config helpers ────────────────────────────────────────────────────────────
|
# ── Config helpers ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -119,6 +119,12 @@ 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:
|
||||||
@@ -298,6 +304,198 @@ 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:
|
||||||
@@ -327,17 +525,25 @@ 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":
|
||||||
updated = self_update(cfg)
|
_cfg = load_config()
|
||||||
|
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
|
# Guard reads LOCAL config, not the server-supplied payload
|
||||||
if not cfg.get("allow_shell_commands", False):
|
_cfg = load_config()
|
||||||
|
if not _cfg.get("allow_shell_commands", False):
|
||||||
return {"success": False, "error": "Shell commands not enabled in agent config"}
|
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}"}
|
||||||
|
|
||||||
@@ -359,13 +565,18 @@ 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))
|
||||||
|
|
||||||
# Register if no API key yet — loop (not recurse) to avoid stack overflow
|
# Always re-register on startup to refresh capabilities, version, and IP.
|
||||||
|
# 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", "")
|
||||||
while not api_key:
|
registered_key = register(cfg, state)
|
||||||
api_key = register(cfg, state)
|
if registered_key:
|
||||||
if not api_key:
|
api_key = registered_key
|
||||||
print("[ERROR] Could not register with JARVIS. Retrying in 60s...", flush=True)
|
elif not api_key:
|
||||||
time.sleep(60)
|
while not api_key:
|
||||||
|
api_key = register(cfg, state)
|
||||||
|
if not api_key:
|
||||||
|
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
|
||||||
|
|||||||
@@ -79,6 +79,13 @@ while IFS= read -r path; do
|
|||||||
if [[ "$path" == *"jarvis"* ]]; then
|
if [[ "$path" == *"jarvis"* ]]; then
|
||||||
systemctl reload lsws 2>/dev/null || systemctl restart lsws 2>/dev/null
|
systemctl reload lsws 2>/dev/null || systemctl restart lsws 2>/dev/null
|
||||||
log "OLS reloaded for JARVIS deploy"
|
log "OLS reloaded for JARVIS deploy"
|
||||||
|
|
||||||
|
# Sync reactor.py to runtime location if it changed
|
||||||
|
if echo "$CHANGED" | grep -q 'deploy/reactor.py'; then
|
||||||
|
cp "$path/deploy/reactor.py" /opt/jarvis-arc/reactor.py
|
||||||
|
systemctl restart jarvis-arc
|
||||||
|
log "Arc Reactor updated and restarted (reactor.py changed)"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
done <<< "$SNAPSHOT"
|
done <<< "$SNAPSHOT"
|
||||||
|
|||||||
Reference in New Issue
Block a user