# 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.

## 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

| Host | IP | SSH | Purpose |
|------|-----|-----|---------|
| DO (main) | 165.22.1.228 | `root / Gonewalk1974!@#` | CyberPanel/OLS — all websites (not JARVIS after migration) |
| 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!!!` (via Tailscale 100.80.188.8) | Primary hypervisor |
| PVE2 (Proxmox) | 10.48.200.91 | `root / Joker1974!!!` | Secondary hypervisor |
| 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 105 (was VM200 pre-2026-06-22 restore; cloud-init says .200 but runs at .201) |
| Ollama VM | 10.48.200.210 | `root / Joker1974!!!` (via Tailscale 100.96.100.113) | Local LLM — PVE1 VM 106 (was VM210 pre-2026-06-22 restore), 4c/8GB |
| 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 103, was VM113 pre-2026-06-22 restore) |
| 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 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 <VMID> -- 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~~ | ~~removed from DO~~ | myronblair/jarvis (now on PVE1 VM 211) |
| 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.

**Two separate webhook handlers:**
- **JARVIS repo** → `http://jarvis.orbishosting.com:1972/webhook.php` — deploys to JARVIS VM (`/var/www/jarvis/`). Deploy log: `/var/www/jarvis/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:
```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 \
  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 `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:**
- `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/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; 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/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. 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/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):**
- 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 `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 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`

**Agent file paths by host** (for manual updates — push to correct path then restart service):
- 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)

**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.

**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).

## 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.

## 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.

**⚠ After any restore from PBS backup:** config.ini reverts to `server = apache`. Always run: `sed -i "s/^server = apache/server = nginx/" /etc/novacpx/config.ini` after a restore. Also: PHP-FPM will fail to start if orphaned pool configs exist from pre-restore accounts — run the cleanup: `for f in /etc/php/8.3/fpm/pool.d/*.conf; do [[ "$f" == *"www.conf"* ]] && continue; u=$(basename "$f" .conf); id "$u" &>/dev/null || rm -f "$f"; done && systemctl start php8.3-fpm`. The `webacct` hosting account and its nginx vhost must be recreated after restore (Linux user survives but DB record and vhost are lost).

**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

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:** 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
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.

**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

| 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/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 |

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`.

## Split DNS (added 2026-06-21)

PVE1 dnsmasq (`/etc/dnsmasq.d/forwarder.conf`) has split DNS entries so all NPM-proxied domains resolve to `10.48.200.201` (NPM internal IP) for LAN clients — bypasses FortiGate hairpin NAT for HTTPS:

```
address=/jarvis.orbishosting.com/10.48.200.201
address=/hoa.orbishosting.com/10.48.200.201
address=/novacpx.orbishosting.com/10.48.200.201
address=/admin.novacpx.orbishosting.com/10.48.200.201
address=/reseller.novacpx.orbishosting.com/10.48.200.201
address=/panel.novacpx.orbishosting.com/10.48.200.201
address=/web.orbishosting.com/10.48.200.201
```

For any LAN device to use this: set DNS to `10.48.200.90` in Wi-Fi settings (or update FortiGate DHCP to push 10.48.200.90 as DNS for all clients).

## NovaCPX Panel (updated 2026-06-22)

Post-restore notes added — see `deploy/novacpx-post-restore.sh` which fixes config.ini, cleans pools, rebuilds webacct account. VM IDs changed after restore: MediaStack VM103, NPM VM105, Ollama VM106. Run script after any PBS restore.

**NovaCPX v1.0.54** — security fixes (CORS, open redirect, CSS injection, exception leakage), subdomains (#36), parked domains (#37), account settings page (#38), better default index (#39), post-restore script (#50), collapsible nav (#48).

**web.orbishosting.com** — Blair HQ dashboard with server-backed notes (notes.php → /home/webacct/notes.json). Password: myronblair@outlook.com / Joker1974!!!
