Compare commits

46 Commits

Author SHA1 Message Date
myron ee0e116963 Add copyright notice 2026-07-26 23:53:37 -05:00
myron adbd1a7a24 Stop flagging parkerslingshotrentals as DOWN: it's intentionally behind a Basic Auth Coming Soon gate during rebuild, so 401 is expected there, not a failure
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-19 00:51:54 -05:00
myron 73aac8ab01 Fix weather widget location label stuck on Fort Worth: the span was a static HTML placeholder never wired to the API response. Wire loadWeather() to update it from d.location, and correct the fallback text to Weatherford, TX
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-19 00:45:04 -05:00
myron 646da21b86 Update weather section location to Weatherford, TX 76088 (from Fort Worth)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-18 19:55:05 -05:00
myron d97a345672 Fix network map duplicate-device bug: drop nmap --send-ip (was causing MAC misattribution/stale ARP entries) and dedupe network_devices by MAC on every scan push so IP changes don't leave orphaned rows
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-18 19:53:17 -05:00
Claude 141191bcdd Fix jarvis-health.sh: watchdog-restart alerts never deduped or auto-resolved
source_key was minute-stamped (health:wd_restart:YYYYMMDDHHMM), so the same restart event seen across two 5-min cron runs within the logs 6-min lookback window got two different keys -> two alert rows + two emails, and the key never matched a clear_cond call so these rows stayed resolved=0 forever, accumulating in the active-alerts view.

Fixed to a stable key (health:wd_restart), matching the pattern used by the other three checks in this script: raise() now dedups via the existing COUNT..resolved=0 check, and clear_cond() runs when no recent restart line is found.

Verified live: injected a fake watchdog restart log line, ran the script twice -> exactly 1 alert row + 1 email (not 2). Removed the line -> alert auto-resolved (resolved=1).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 07:43:20 -05:00
Claude 8399048252 Backup: capture Phase 1/2 env files, systemd drop-ins, and ops scripts
jarvis-backup.sh now also archives /etc/jarvis-arc (reactor.env), /etc/jarvis (db.env), the jarvis-arc.service.d systemd drop-ins, and /usr/local/bin/jarvis-*.sh. Previously a restore would have come back with no API keys or DB password since those moved outside /var/www/jarvis + /opt/jarvis-arc during the secrets sweep.

Verified: ran a real backup (45M) and confirmed all new paths are present in the archive.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 23:48:04 -05:00
Claude 652e44f7b3 Add JARVIS health self-check (Phase 2 reliability monitoring)
deploy/jarvis-health.sh (cron */5): detects silent failures the service watchdog cannot — stalled crons (cron.log >10min stale = 2+ missed runs), Arc jobs stuck running >30min, disk >85%, and watchdog service restarts. Writes auto-resolving rows to the alerts table (shown in admin panel) and emails myronblair@gmail.com on any NEW finding via the reactor Gmail SMTP creds.

Verified live end-to-end: injected a fake stuck job -> alert raised + email sent; cleared it -> alert auto-resolved. Installed in root crontab.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 20:43:37 -05:00
Claude 43be3e1105 Stop tracking INFRASTRUCTURE-REFERENCE.md (full credentials doc)
Untracked from git and added to .gitignore. The file stays on disk at public_html/admin/downloads/ (served behind admin auth) and in the jarvis-private + VM110 copies — it is no longer pushed to GitHub going forward.

NOTE: prior revisions remain in GitHub history; a git filter-repo purge + force-push is still pending user sign-off, as are rotations of the secrets that were in it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 20:35:41 -05:00
Claude 3d16061709 Auth hardening: login rate-limiting + session fixation defenses
- login.php: Redis-backed per-IP rate limit (10 fails / 15 min lockout), keyed off CF-Connecting-IP/X-Forwarded-For so it sees the real client behind NPM; fails open if Redis is down

- login.php: session_regenerate_id(true) on successful auth (prevents session fixation)

- php.ini: session.use_strict_mode = 1 (reject unknown/attacker-supplied session IDs)

- netscan.php: constant-time hash_equals for the registration-key check (matches agent.php)

Cookie flags already HttpOnly + SameSite=Lax (verified live). Agent auth verified: missing/bad X-Agent-Key -> 401 on every machine action.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 20:31:57 -05:00
Claude 80588efa7a Secrets sweep: move hardcoded credentials out of tracked files into env files
Removed live secret literals from git-tracked code (all were on GitHub):

- deploy/reactor.py: Claude/Groq API keys, DB pass, Gmail/iCloud app passwords now from os.environ (loaded via systemd EnvironmentFile=/etc/jarvis-arc/reactor.env, root:www-data 0640)

- public_html/login.php: used a private hardcoded PDO connection; now uses config.php DB_* constants

- deploy/jarvis-backup.sh (runs via cron), jarvis-deploy.sh, jarvis-watchdog.sh: DB pass now sourced from /etc/jarvis/db.env (root:root 0600)

- removed dead agent/jarvis-arc-reactor.py (unreferenced old duplicate leaking an old Groq key + stale Ollama IP)

- added deploy/reactor.env.example and deploy/db.env.example templates

Verified live: reactor restarted with all 21 handlers + DB poller (job round-trip OK), login works, mysqldump auth via env OK.

NOTE: these keys remain in GitHub history and should be rotated (Claude/Groq/Gmail/iCloud/DB). Separate decision needed on INFRASTRUCTURE-REFERENCE.md (full cred doc still tracked) + history purge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 20:28:42 -05:00
Claude f7309a15fc Rotate agent registration key; remove key literals from public scripts/UI
- install.sh, install-agent.sh: require JARVIS_REG_KEY env var or interactive prompt instead of baked-in key (matches install-mac.sh/install-windows.ps1 behavior)

- netscan.php: reuse AGENT_REGISTRATION_KEY constant instead of a duplicate literal

- agent.php + api.php: add session-authed "regkey" action so the admin install modal fetches the current key at runtime

- jarvis-agents.js: fetch reg key via /api/agent/regkey instead of hardcoding it; pass JARVIS_REG_KEY in the Linux install one-liner

- INFRASTRUCTURE-REFERENCE.md: scrub old key literal (rotated; real value lives only in api/config.php on VM211)

Key rotated on the box + rolled out to all 11 agents (verified all re-register online). New value is in the gitignored api/config.php only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 20:22:00 -05:00
Claude 18783dc137 Fix errors unmasked by re-enabling error_reporting(E_ALL)
- config.example.php: error_reporting(0) -> E_ALL (live config.php matches); add JELLYFIN_URL/JELLYFIN_API_KEY placeholders

- history.php, jellyfin.php: drop require of nonexistent includes/auth.php + AuthMiddleware call (router enforces auth centrally) — both endpoints were fataling on every request

- remove stale kb_intent_generator .bak files from deployed tree

DB (not in repo): kb_facts.fact_value TEXT -> MEDIUMTEXT; ha/entity_map had failed every write since Jul 2 at >64KB

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 19:43:37 -05:00
root 8f6be645ed Fix auto-detect agent install button: commands shown for Windows/Linux no longer use flags the scripts dont actually support (install-windows.ps1 has no -JarvisUrl/-Key params, install.sh has no --jarvis-url/--key flags), download links use the current page origin instead of a hardcoded URL thats unreachable from outside the LAN, Windows command reflects the new no-Python exe install, fixed the top-level install-agent.sh which still pointed at the pre-migration DO server IP (165.22.1.228) and would have failed outright 2026-07-07 12:43:54 -05:00
root 185d2c889f Fix AGENT button showing the wrong machine: same-subnet fallback matched whichever agent happened to be first in the list, not the actual browsing machine — happens whenever a LAN client hits the dashboard via hairpin NAT/external hostname and the server sees the same reflected IP for every visitor. Exact-match only now; falls through to not-detected instead of misreporting another real machine as yours. 2026-07-07 12:41:53 -05:00
root b5c47d898b Windows agent v3.2: build standalone PyInstaller exe (no Python/pywin32 install needed on target machines), add frozen-mode self-update (rename-swap since a running exe cant be overwritten in place), simplify installer accordingly, fix default install URL (jarvis.orbishosting.com default port is unreachable from outside the LAN - use :1972), fix stale windows=3.0 in the admin panel version-check map 2026-07-07 12:20:22 -05:00
root ffc01c3d4a Update infra doc: WAN IP change + full FortiGate VIP list, JARVIS auto-deploy pipeline restoration, admin panel fixes (Works/Site Health/Email Intelligence), backup systems audit + fixes across all 4 systems, Blair HQ Backup Downloads feature 2026-07-07 11:29:45 -05:00
root 156b210184 jarvis-backup.sh: was DB-only, now also backs up /var/www/jarvis, /opt/jarvis-arc, nginx site config, systemd unit, and root crontab — a DB dump alone was useless without the app code and daemon needed to actually restore JARVIS. Also fixed a typo (SIYE->SIZE) that silently broke the backup-size log line. 2026-07-07 10:54:24 -05:00
root 09f73edb0b Fix silent compose failures: poll actual job status in the compose modal instead of a fire-and-forget dispatch toast; add Groq 429 retry-with-backoff using its own rate-limit-reset header (12k tokens/min tier gets exhausted by gmail_triage alone) 2026-07-07 10:49:08 -05:00
root a4336ebdd6 Fix Email Intelligence inbox 404: was proxying to the old DO-hosted JARVIS (165.22.1.228/api/email), a dead endpoint since the move to VM211. Now reads email_triage directly, matching the pattern email_action_items already used. 2026-07-07 10:06:48 -05:00
root e7f555fff1 reactor.py: fix Ollama silent-empty-return bug (no error checking on API response), bump Ollama timeout 30s->90s (cold model loads took 15s+ alone), add exception type to fallback log lines 2026-07-07 09:25:25 -05:00
root 8034fc67e9 Fix field-name mismatches between PHP dispatcher and Python reactor: compose_email (recipient/subject/auto_send -> to_email/subject_hint/send) and send_reply (content -> body) were silently dropping the actual recipient, subject, and user-edited content on every dispatch 2026-07-07 09:01:47 -05:00
root 48b2a8523a reactor.py: fix llm_call() silently skipping fallback for explicit provider requests, add /comms/sent/{id}/send endpoint for approving queued drafts 2026-07-07 08:58:36 -05:00
root 5ff4f3e311 Outbox: add SEND button for queued drafts (compose previously had no way to actually send), fix VIEW showing body via list endpoint that omits it 2026-07-07 08:58:20 -05:00
root 24b96e809c Site Health tab: fix parkerslingshotrentals.com label + widen cards for full domain names; fix sites freshness-check interval mismatch (300s vs 180s cron) causing checks to feel far apart 2026-07-07 08:41:52 -05:00
root 8567018cc3 Test: verify auto-deploy pipeline end-to-end 2026-07-07 08:25:12 -05:00
root f8a095f783 Fix Works-tab last-run detection bugs, add generic live-log popup for Facts/Stats/Calendar workers, fix webhook branch check (master not main) and log path 2026-07-07 07:58:55 -05:00
root e060ff0c63 Merge origin/master history (ours: live production content is authoritative, origin was stale) 2026-07-07 07:55:58 -05:00
root 588cfe3f10 Adopt live production state as git baseline 2026-07-07 07:55:47 -05:00
root 3db329e925 Correct Pioneer VSX-822: wired through a 4th WiFi extender, not dual-NIC 2026-07-06 23:41:34 -05:00
root c01805316a Correct Pioneer VSX-822 attribution: confirmed at .100, note dual-MAC discrepancy 2026-07-06 23:38:34 -05:00
root 20b510186a Add network equipment and client device inventory (Section 14) from live ARP scan + manual confirmation 2026-07-06 23:34:31 -05:00
root 9b5e16660a Jellyfin: regenerate stale admin API token, document real admin username 2026-07-06 14:29:43 -05:00
root 7020d445b3 Update infra doc: MediaStack/Jellyfin verified detail, NordVPN LAN Discovery + wg0 lockout fix, VMID corrections 2026-07-06 13:09:02 -05:00
myron 80b0dfc583 Document code review + fixes across all 4 DO-hosted business sites (2026-07-06) 2026-07-06 02:58:28 -05:00
myron 30e2bc093c Document WEB HOST agent install and timezone freshness-check fix 2026-07-05 21:56:23 -05:00
myron a29670e93d Fix WEB HOST stats never populating (DO server had no monitoring agent installed) and a timezone bug in freshness checks (America/Chicago default timezone + strtotime() on naive UTC MySQL timestamps caused sites/proxmox/ollama facts to never refresh, and could have suppressed the KB intent generator's scheduled runs) 2026-07-05 21:30:53 -05:00
myron 10350cbcd8 Update infrastructure reference: document 2026-07-06 security review and fixes 2026-07-05 20:13:21 -05:00
myron 24bc876c1d Fix code review findings: unauthenticated infra doc + backup file exposure, onclick-attribute XSS breakouts (admin panel and front-end), plaintext calendar passwords, k()->j() typo, wildcard CORS, session cookie hardening (HttpOnly/SameSite) 2026-07-05 20:09:52 -05:00
myron cfb5b2a3f9 Document ChuckCo Time Keeper code review fixes and admin login rate limiting 2026-07-05 19:17:14 -05:00
myron 0b1a19d9de Update infrastructure reference: fix stale JARVIS port, rotate GitHub PAT reference, add ChuckCo Time Keeper site, FortiGate DNS change, git/repo management section 2026-07-05 18:58:56 -05:00
myron 66a5443f22 Document Homebridge backup switch from MSP360 to Proxmox vzdump 2026-07-04 12:21:53 -05:00
myron e5536b077c Document MSP360 backup status dashboard integration 2026-07-03 08:03:54 -05:00
myron a292411d52 Stop hardcoding a generic watch_services list on every agent install
The installer wrote the same service list (ollama, homeassistant, mysql,
mariadb, nginx, apache2, docker) onto every host regardless of what it
actually runs, causing false "Service Down" alerts on hosts that never
installed those services. Default is now empty; watch_services should be
set per-host to match what's actually running there.
2026-07-02 20:28:42 -05:00
myron 26b501b600 fix: address 8 code-review findings in KB topic manager
- Fix 1 (ORDER BY): use table alias t.id to force integer PK ordering;
  topic_id alias was causing alphabetical sort, breaking rotation offsets
- Fix 2 (PDOException): wrap INSERT/UPDATE in try/catch in kb_topic_save;
  duplicate topic_id now returns clean JSON error instead of HTTP 500
- Fix 3 (XSS/onclick): DEL button passes only t.id; tmDelete looks up
  name from _topicsData, removing JSON.stringify from onclick attribute
- Fix 4 (validation): topic_id must contain at least one [a-z0-9] after
  sanitization; "@@@" to "___" no longer passes required-field check
- Fix 5 (stale logs): guard comment and log messages updated from 20h to
  4h to match the actual 14400s threshold
- Fix 6 (dedup): tmEsc() replaced with existing esc() throughout; removed
  duplicate function definition
- Fix 7 (toggle rollback): tmToggle uses inline fetch to revert el.checked
  on API failure instead of leaving the UI in wrong state
- Fix 8 (confirm): tmDelete uses openModal confirmation instead of
  confirm() dialog, consistent with project live-popup convention

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014p87VFec84hNaf2WpvmLrW
2026-07-02 10:26:47 -05:00
myron 7327104612 feat: DB-driven KB topic manager — admin UI + migration
- Create kb_generator_topics table (140 topics migrated from hardcoded array)
- kb_intent_generator.php now loads active topics from DB instead of $BATCHES array
- Admin panel: MANAGE TOPICS button opens full topic manager modal
  - Browse all 140 topics with search, category, and active/disabled filters
  - Inline active toggle per topic (checkbox)
  - Add new topic form (topic_id, category, name, description)
  - Edit existing topics (all fields)
  - Delete topics with confirmation
  - Run count display
- Backend API cases: kb_topics, kb_topic_save, kb_topic_delete, kb_topic_toggle
- Topics can now be added/managed without any code changes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014p87VFec84hNaf2WpvmLrW
2026-07-02 07:04:20 -05:00
41 changed files with 8642 additions and 11505 deletions
+1
View File
@@ -1,4 +1,5 @@
# Credentials - never commit # Credentials - never commit
public_html/admin/downloads/INFRASTRUCTURE-REFERENCE.md
api/config.php api/config.php
backup/ backup/
+1
View File
@@ -0,0 +1 @@
Property of TomTom Enterprises.
+17 -38
View File
@@ -6,20 +6,26 @@
.DESCRIPTION .DESCRIPTION
Installs JARVIS Agent as a Windows Service that auto-starts at boot. Installs JARVIS Agent as a Windows Service that auto-starts at boot.
Requires: PowerShell 5.1+, internet access, and Administrator rights. Requires: PowerShell 5.1+, internet access, and Administrator rights.
No Python installation needed — this installs the standalone .exe build.
.EXAMPLE .EXAMPLE
# Interactive install (prompts for registration key): # Interactive install (prompts for registration key):
irm https://jarvis.orbishosting.com/agent/install-windows.ps1 | iex irm https://jarvis.orbishosting.com:1972/agent/install-windows.ps1 | iex
# Silent install with key: # Silent install with key:
$env:JARVIS_REG_KEY='your_key_here'; irm https://jarvis.orbishosting.com/agent/install-windows.ps1 | iex $env:JARVIS_REG_KEY='your_key_here'; irm https://jarvis.orbishosting.com:1972/agent/install-windows.ps1 | iex
#> #>
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$JARVIS_URL = 'https://jarvis.orbishosting.com' # Fixed 2026-07-07: jarvis.orbishosting.com on the default port (80/443) is not
# reachable from outside the LAN at all (no FortiGate VIP forwards it) — every
# external install using the old default URL would have failed outright. Port
# 1972 is the confirmed-working external path (same fix applied to the GitHub
# webhook the same day).
$JARVIS_URL = 'http://jarvis.orbishosting.com:1972'
$INSTALL_DIR = 'C:\ProgramData\jarvis-agent' $INSTALL_DIR = 'C:\ProgramData\jarvis-agent'
$SERVICE_NAME = 'JARVISAgent' $SERVICE_NAME = 'JARVISAgent'
$AGENT_SCRIPT = "$INSTALL_DIR\jarvis-agent-windows.py" $AGENT_EXE = "$INSTALL_DIR\jarvis-agent-windows.exe"
$CONFIG_FILE = "$INSTALL_DIR\config.json" $CONFIG_FILE = "$INSTALL_DIR\config.json"
function Write-Step { param($msg) Write-Host "`n[JARVIS] $msg" -ForegroundColor Cyan } function Write-Step { param($msg) Write-Host "`n[JARVIS] $msg" -ForegroundColor Cyan }
@@ -39,49 +45,23 @@ if ($existing) {
Start-Sleep 2 Start-Sleep 2
} }
try { try {
& python "$INSTALL_DIR\jarvis-agent-windows.py" remove 2>$null if (Test-Path $AGENT_EXE) { & $AGENT_EXE remove 2>$null }
} catch {} } catch {}
Write-OK "Existing service removed." Write-OK "Existing service removed."
} }
# ── Check / install Python ────────────────────────────────────────────────────
Write-Step "Checking Python..."
$py = Get-Command python -ErrorAction SilentlyContinue
if (-not $py) {
Write-Host " Python not found. Installing via winget..." -ForegroundColor Yellow
if (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
Write-Fail "winget not available. Please install Python 3.11+ from https://python.org and re-run."
}
winget install -e --id Python.Python.3.11 --silent --accept-package-agreements --accept-source-agreements
$env:PATH = [System.Environment]::GetEnvironmentVariable("PATH","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("PATH","User")
$py = Get-Command python -ErrorAction SilentlyContinue
if (-not $py) { Write-Fail "Python install failed. Please install manually from https://python.org" }
}
$pyVersion = & python --version 2>&1
Write-OK $pyVersion
# ── Install pywin32 ───────────────────────────────────────────────────────────
Write-Step "Checking pywin32..."
$checkWin32 = & python -c "import win32service; print('ok')" 2>&1
if ($checkWin32 -ne 'ok') {
Write-Host " Installing pywin32..." -ForegroundColor Yellow
& python -m pip install --quiet pywin32
& python -m pywin32_postinstall -install 2>$null
Write-OK "pywin32 installed."
} else {
Write-OK "pywin32 already installed."
}
# ── Create install dir ──────────────────────────────────────────────────────── # ── Create install dir ────────────────────────────────────────────────────────
Write-Step "Creating install directory..." Write-Step "Creating install directory..."
New-Item -ItemType Directory -Path $INSTALL_DIR -Force | Out-Null New-Item -ItemType Directory -Path $INSTALL_DIR -Force | Out-Null
Write-OK $INSTALL_DIR Write-OK $INSTALL_DIR
# ── Download agent script ───────────────────────────────────────────────────── # ── Download agent exe ─────────────────────────────────────────────────────────
# No Python/pywin32 dependency anymore — this is a self-contained PyInstaller
# build with everything it needs bundled in.
Write-Step "Downloading JARVIS agent..." Write-Step "Downloading JARVIS agent..."
try { try {
Invoke-WebRequest -Uri "$JARVIS_URL/agent/jarvis-agent-windows.py" -OutFile $AGENT_SCRIPT -UseBasicParsing Invoke-WebRequest -Uri "$JARVIS_URL/agent/jarvis-agent-windows.exe" -OutFile $AGENT_EXE -UseBasicParsing
Write-OK "Agent downloaded to $AGENT_SCRIPT" Write-OK "Agent downloaded to $AGENT_EXE"
} catch { } catch {
Write-Fail "Failed to download agent: $_" Write-Fail "Failed to download agent: $_"
} }
@@ -121,8 +101,7 @@ Write-OK "Config written to $CONFIG_FILE"
# ── Install Windows Service ─────────────────────────────────────────────────── # ── Install Windows Service ───────────────────────────────────────────────────
Write-Step "Installing Windows service..." Write-Step "Installing Windows service..."
$pyPath = (Get-Command python).Source & $AGENT_EXE --startup auto install
& $pyPath "$AGENT_SCRIPT" --startup auto install
if ($LASTEXITCODE -ne 0) { Write-Fail "Service install failed." } if ($LASTEXITCODE -ne 0) { Write-Fail "Service install failed." }
Write-OK "Service '$SERVICE_NAME' installed." Write-OK "Service '$SERVICE_NAME' installed."
Binary file not shown.
+45 -13
View File
@@ -35,7 +35,7 @@ INSTALL_DIR = Path(r"C:\ProgramData\jarvis-agent")
CONFIG_PATH = INSTALL_DIR / "config.json" CONFIG_PATH = INSTALL_DIR / "config.json"
STATE_PATH = INSTALL_DIR / "state.json" STATE_PATH = INSTALL_DIR / "state.json"
LOG_PATH = INSTALL_DIR / "jarvis-agent.log" LOG_PATH = INSTALL_DIR / "jarvis-agent.log"
AGENT_VERSION = "3.1" AGENT_VERSION = "3.2"
# Set by the service wrapper so self_update knows to stop instead of exec # Set by the service wrapper so self_update knows to stop instead of exec
_is_service = False _is_service = False
@@ -92,7 +92,7 @@ def api_post(url: str, payload: dict, headers: dict = {}, timeout: int = 15,
body = json.dumps(payload).encode() body = json.dumps(payload).encode()
req = urllib.request.Request(url, data=body, method="POST") req = urllib.request.Request(url, data=body, method="POST")
req.add_header("Content-Type", "application/json") req.add_header("Content-Type", "application/json")
req.add_header("User-Agent", "JARVIS-Agent-Windows/3.0") req.add_header("User-Agent", "JARVIS-Agent-Windows/3.2")
if _host_header: if _host_header:
req.add_header("Host", _host_header) req.add_header("Host", _host_header)
for k, v in headers.items(): for k, v in headers.items():
@@ -109,7 +109,7 @@ def api_post(url: str, payload: dict, headers: dict = {}, timeout: int = 15,
def api_get(url: str, headers: dict = {}, timeout: int = 10, def api_get(url: str, headers: dict = {}, timeout: int = 10,
ssl_verify: bool = True) -> dict: ssl_verify: bool = True) -> dict:
req = urllib.request.Request(url) req = urllib.request.Request(url)
req.add_header("User-Agent", "JARVIS-Agent-Windows/3.0") req.add_header("User-Agent", "JARVIS-Agent-Windows/3.2")
if _host_header: if _host_header:
req.add_header("Host", _host_header) req.add_header("Host", _host_header)
for k, v in headers.items(): for k, v in headers.items():
@@ -375,18 +375,28 @@ def _sysinfo_snapshot() -> dict:
# ── Self-update ──────────────────────────────────────────────────────────────── # ── Self-update ────────────────────────────────────────────────────────────────
def self_update(cfg: dict) -> bool: def self_update(cfg: dict) -> bool:
# Added: supports both script-mode (plain .py, run via a system Python) and
# frozen-mode (standalone PyInstaller .exe — sys.frozen is set, __file__ isn't
# meaningful/writable the way it is for a real .py file on disk). A running
# .exe can't be overwritten in place on Windows, but CAN be renamed while
# running, so frozen mode uses a download-new/rename-old/rename-new swap
# instead of the direct overwrite the script-mode path uses.
jarvis_url = cfg.get("jarvis_url", "").rstrip("/") jarvis_url = cfg.get("jarvis_url", "").rstrip("/")
is_frozen = bool(getattr(sys, "frozen", False))
if is_frozen:
default_update_url = f"{jarvis_url}/agent/jarvis-agent-windows.exe" if jarvis_url else ""
else:
default_update_url = f"{jarvis_url}/agent/jarvis-agent-windows.py" if jarvis_url else "" default_update_url = f"{jarvis_url}/agent/jarvis-agent-windows.py" if jarvis_url else ""
update_url = cfg.get("update_url", default_update_url) update_url = cfg.get("update_url", default_update_url)
if not update_url: if not update_url:
return False return False
script_path = os.path.abspath(__file__) target_path = os.path.abspath(sys.executable) if is_frozen else os.path.abspath(__file__)
ssl_verify = bool(cfg.get("ssl_verify", True)) ssl_verify = bool(cfg.get("ssl_verify", True))
try: try:
# Download expected hash # Download expected hash
hash_url = update_url + ".sha256" hash_url = update_url + ".sha256"
req_hash = urllib.request.Request(hash_url) req_hash = urllib.request.Request(hash_url)
req_hash.add_header("User-Agent", "JARVIS-Agent-Windows/3.0") req_hash.add_header("User-Agent", "JARVIS-Agent-Windows/3.2")
if _host_header: if _host_header:
req_hash.add_header("Host", _host_header) req_hash.add_header("Host", _host_header)
expected_hash = None expected_hash = None
@@ -397,13 +407,13 @@ def self_update(cfg: dict) -> bool:
except Exception: except Exception:
pass pass
# Download new script # Download new script/exe
req = urllib.request.Request(update_url) req = urllib.request.Request(update_url)
req.add_header("User-Agent", "JARVIS-Agent-Windows/3.0") req.add_header("User-Agent", "JARVIS-Agent-Windows/3.2")
if _host_header: if _host_header:
req.add_header("Host", _host_header) req.add_header("Host", _host_header)
ctx = _make_ssl_ctx(ssl_verify) ctx = _make_ssl_ctx(ssl_verify)
with urllib.request.urlopen(req, timeout=30, context=ctx) as resp: with urllib.request.urlopen(req, timeout=60, context=ctx) as resp:
new_content = resp.read() new_content = resp.read()
# Verify hash # Verify hash
@@ -413,20 +423,42 @@ def self_update(cfg: dict) -> bool:
log(f"Update hash mismatch (expected {expected_hash[:16]}… got {actual_hash[:16]}…) — aborting") log(f"Update hash mismatch (expected {expected_hash[:16]}… got {actual_hash[:16]}…) — aborting")
return False return False
with open(script_path, "rb") as f: with open(target_path, "rb") as f:
current = f.read() current = f.read()
if new_content != current: if new_content == current:
log(f"Update verified — replacing {script_path} and restarting...") return False
with open(script_path, "wb") as f:
log(f"Update verified — replacing {target_path} and restarting...")
if is_frozen:
# Can't overwrite a running exe, but can rename it and drop the new
# one in its place; the old copy is cleaned up on the next update.
old_path = target_path + ".old"
new_path = target_path + ".new"
with open(new_path, "wb") as f:
f.write(new_content) f.write(new_content)
try:
if os.path.exists(old_path):
os.remove(old_path)
except Exception:
pass
os.rename(target_path, old_path)
os.rename(new_path, target_path)
else:
with open(target_path, "wb") as f:
f.write(new_content)
if _is_service: if _is_service:
# Signal the main loop to exit; SCM failure-recovery will restart us # Signal the main loop to exit; SCM failure-recovery will restart us
log("Running as service — stopping for SCM-managed restart after update.") log("Running as service — stopping for SCM-managed restart after update.")
_stop_event.set() _stop_event.set()
elif is_frozen:
# sys.argv[0] is already the exe's own path for a frozen app — don't
# prepend sys.executable again or the new process misreads its own
# path as a command-line argument.
os.execv(sys.executable, sys.argv)
else: else:
os.execv(sys.executable, [sys.executable] + sys.argv) os.execv(sys.executable, [sys.executable] + sys.argv)
return True return True
return False
except Exception as e: except Exception as e:
log(f"Self-update check failed: {e}") log(f"Self-update check failed: {e}")
return False return False
+1 -1
View File
@@ -281,7 +281,7 @@ def get_uptime() -> dict:
return {} return {}
def get_services(cfg: dict) -> list: def get_services(cfg: dict) -> list:
watch = cfg.get("watch_services", ["ollama", "homeassistant", "mysql", "nginx", "apache2"]) watch = cfg.get("watch_services", [])
statuses = [] statuses = []
for svc in watch: for svc in watch:
try: try:
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -12,7 +12,7 @@ fi
SUBNET="10.48.200.0/24" SUBNET="10.48.200.0/24"
TMPFILE=$(mktemp) TMPFILE=$(mktemp)
nmap -sn --send-ip "$SUBNET" 2>/dev/null > "$TMPFILE" nmap -sn "$SUBNET" 2>/dev/null > "$TMPFILE"
if [ ! -s "$TMPFILE" ]; then if [ ! -s "$TMPFILE" ]; then
echo "$(date): nmap produced no output" >&2 echo "$(date): nmap produced no output" >&2
+4 -1
View File
@@ -47,8 +47,11 @@ define(chr(39)+'HA_TOKEN'.chr(39), chr(39)+'YOUR_HA_LONG_LIVED_TOKEN'.chr(39));
define(chr(39)+'SESSION_LIFETIME'.chr(39), 86400 * 7); define(chr(39)+'SESSION_LIFETIME'.chr(39), 86400 * 7);
define(chr(39)+'SITE_URL'.chr(39), chr(39)+'https://jarvis.orbishosting.com'.chr(39)); define(chr(39)+'SITE_URL'.chr(39), chr(39)+'https://jarvis.orbishosting.com'.chr(39));
error_reporting(0); error_reporting(E_ALL);
ini_set(chr(39)+'display_errors'.chr(39), 0); ini_set(chr(39)+'display_errors'.chr(39), 0);
ini_set(chr(39)+'log_errors'.chr(39), 1); ini_set(chr(39)+'log_errors'.chr(39), 1);
ini_set(chr(39)+'error_log'.chr(39), chr(39)+'/var/log/apache2/jarvis_errors.log'.chr(39)); ini_set(chr(39)+'error_log'.chr(39), chr(39)+'/var/log/apache2/jarvis_errors.log'.chr(39));
date_default_timezone_set(chr(39)+'America/Chicago'.chr(39)); date_default_timezone_set(chr(39)+'America/Chicago'.chr(39));
define('JELLYFIN_URL', 'http://10.48.200.33:8096');
define('JELLYFIN_API_KEY', 'your-jellyfin-api-key');
+5 -1
View File
@@ -54,7 +54,7 @@ function update_agent_seen(string $agentId, string $status = 'online', ?string $
// ── Auth (all actions except register) ─────────────────────────────────────── // ── Auth (all actions except register) ───────────────────────────────────────
$agentKey = $_SERVER['HTTP_X_AGENT_KEY'] ?? ''; $agentKey = $_SERVER['HTTP_X_AGENT_KEY'] ?? '';
$browserActions = ['list', 'status', 'myip']; $browserActions = ['list', 'status', 'myip', 'regkey'];
if ($agentAction !== 'register') { if ($agentAction !== 'register') {
if (in_array($agentAction, $browserActions)) { if (in_array($agentAction, $browserActions)) {
@@ -212,6 +212,10 @@ switch ($agentAction) {
); );
agent_ok(); agent_ok();
// ── REGKEY (browser: session-authed fetch of registration key) ───────────
case 'regkey':
agent_ok(['registration_key' => AGENT_REGISTRATION_KEY]);
// ── LIST (admin: get all agents status) ────────────────────────────────── // ── LIST (admin: get all agents status) ──────────────────────────────────
case 'list': case 'list':
// Mark agents offline if last_seen > 2 minutes ago // Mark agents offline if last_seen > 2 minutes ago
+1 -1
View File
@@ -39,7 +39,7 @@ foreach ($svcNames as $s) {
// Site health from kb_facts // Site health from kb_facts
$siteLabels = [ $siteLabels = [
"jarvis" => "jarvis.orbishosting.com:1972", "jarvis" => "jarvis.orbishosting.com",
"tomsjavajive" => "tomsjavajive.com", "tomsjavajive" => "tomsjavajive.com",
"epictravelexp"=> "epictravelexpeditions.com", "epictravelexp"=> "epictravelexpeditions.com",
"parkersling" => "parkerslingshotrentals.com", "parkersling" => "parkerslingshotrentals.com",
+20 -6
View File
@@ -21,13 +21,18 @@ function collect_all(): array {
// Returns true if a fact category has been updated within $secs seconds. // Returns true if a fact category has been updated within $secs seconds.
// Prevents expensive external calls when data is still fresh. // Prevents expensive external calls when data is still fresh.
// Comparison is done entirely in SQL (via NOW()) rather than PHP's time()/strtotime()
// — this file's config.php sets date_default_timezone_set('America/Chicago'), which
// makes strtotime() misinterpret MySQL's naive (UTC) datetime strings as being in
// Chicago time, throwing every freshness check off by the UTC offset (previously
// caused "sites" to always look artificially fresh and never actually refresh).
$fresh = function(string $cat, int $secs): bool { $fresh = function(string $cat, int $secs): bool {
$row = JarvisDB::query( $row = JarvisDB::query(
'SELECT updated_at FROM kb_facts WHERE category=? ORDER BY updated_at DESC LIMIT 1', 'SELECT (updated_at > DATE_SUB(NOW(), INTERVAL ? SECOND)) AS is_fresh FROM kb_facts WHERE category=? ORDER BY updated_at DESC LIMIT 1',
[$cat] [$secs, $cat]
); );
if (empty($row[0]['updated_at'])) return false; if (empty($row)) return false;
return (time() - strtotime($row[0]['updated_at'])) < $secs; return (bool) $row[0]['is_fresh'];
}; };
@@ -177,7 +182,10 @@ function collect_all(): array {
} }
// ── Site Health (TTL 5 min) ─────────────────────────────────────────── // ── Site Health (TTL 5 min) ───────────────────────────────────────────
if ($fresh('sites', 300)) { // Fixed 2026-07-07: this guard was 300s but cron only runs every 180s, so sites
// were effectively only re-checked every OTHER run (~6 min gaps, felt "far apart").
// 170s keeps it just under the cron cadence so it re-checks on every run.
if ($fresh('sites', 170)) {
$results['sites'] = 'skipped (fresh)'; $results['sites'] = 'skipped (fresh)';
} else try { } else try {
$sites = [ $sites = [
@@ -189,6 +197,11 @@ function collect_all(): array {
'orbisportal' => 'https://orbis.orbishosting.com', 'orbisportal' => 'https://orbis.orbishosting.com',
'tomtomgames' => 'https://tomtomgames.com', 'tomtomgames' => 'https://tomtomgames.com',
]; ];
// Sites intentionally gated behind HTTP Basic Auth (e.g. a password-protected
// "Coming Soon" page during a rebuild) — treat these status codes as up, not down.
$expectedCodes = [
'parkerslingshotrentals' => [401],
];
$down = []; $down = [];
foreach ($sites as $key => $url) { foreach ($sites as $key => $url) {
$parsed = parse_url($url); $parsed = parse_url($url);
@@ -208,7 +221,8 @@ function collect_all(): array {
curl_exec($ch); curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch); curl_close($ch);
$status = ($code >= 200 && $code < 400) ? 'up' : "down-$code"; $ok = ($code >= 200 && $code < 400) || in_array($code, $expectedCodes[$key] ?? [], true);
$status = $ok ? 'up' : "down-$code";
KBEngine::storeFact('sites', $key, $status, $url, 180); KBEngine::storeFact('sites', $key, $status, $url, 180);
if ($status !== 'up') $down[] = "$key($code)"; if ($status !== 'up') $down[] = "$key($code)";
} }
-2
View File
@@ -1,10 +1,8 @@
<?php <?php
// Chat history search endpoint // Chat history search endpoint
require_once __DIR__ . '/../config.php'; require_once __DIR__ . '/../config.php';
require_once __DIR__ . '/../../includes/auth.php';
header('Content-Type: application/json'); header('Content-Type: application/json');
AuthMiddleware::requireAuth();
$q = trim($_GET['q'] ?? ''); $q = trim($_GET['q'] ?? '');
if (strlen($q) < 2) { if (strlen($q) < 2) {
-2
View File
@@ -1,9 +1,7 @@
<?php <?php
require_once __DIR__ . '/../config.php'; require_once __DIR__ . '/../config.php';
require_once __DIR__ . '/../../includes/auth.php';
header('Content-Type: application/json'); header('Content-Type: application/json');
AuthMiddleware::requireAuth();
$action = $_GET['action'] ?? 'sessions'; $action = $_GET['action'] ?? 'sessions';
+78 -289
View File
@@ -69,303 +69,33 @@ function normalize_pattern(string $pat): string {
return '/' . $pat . '/i'; return '/' . $pat . '/i';
} }
/* ── daily guard: skip if already ran within last 20 hours ── */ /* ── run guard: skip if ran within last 4 hours ── */
/* Set JARVIS_FORCE_RUN=1 (env) or pass --force (argv) to bypass */ /* Set JARVIS_FORCE_RUN=1 (env) or pass --force (argv) to bypass */
$lastRun = JarvisDB::single( /* Comparison done in SQL (NOW()) rather than PHP time()/strtotime() this process's
"SELECT updated_at FROM kb_facts WHERE category='kb_generator' AND fact_key='last_run'" date_default_timezone_set('America/Chicago') makes strtotime() misread MySQL's naive
(UTC) timestamps as Chicago time, throwing elapsed-time checks off by the UTC offset. */
$recentRun = JarvisDB::single(
"SELECT (updated_at > DATE_SUB(NOW(), INTERVAL 14400 SECOND)) AS is_recent FROM kb_facts WHERE category='kb_generator' AND fact_key='last_run'"
); );
$forceRun = !empty(getenv('JARVIS_FORCE_RUN')) || (isset($argv[1]) && $argv[1] === '--force'); $forceRun = !empty(getenv('JARVIS_FORCE_RUN')) || (isset($argv[1]) && $argv[1] === '--force');
if (!$forceRun && $lastRun && (time() - strtotime($lastRun['updated_at'])) < 14400) { if (!$forceRun && $recentRun && $recentRun['is_recent']) {
log_line('Skipping ran within last 20 hours (next run tomorrow 3am). Use --force to override.'); log_line('Skipping ran within last 4 hours. Use --force to override.');
exit(0); exit(0);
} }
if ($forceRun) log_line('Force-run flag set — bypassing 20-hour guard.'); if ($forceRun) log_line('Force-run flag set — bypassing 4-hour guard.');
log_line('Starting daily KB intent generation run.'); log_line('Starting daily KB intent generation run.');
/* ── topic batches (25 topics × ~40 intents = 1,000+) ── */ /* ── load active topics from database ── */
$BATCHES = [ $BATCHES = JarvisDB::query(
['id' => 'math_arith', 'category' => 'mathematics', 'topic' => 'Arithmetic and number sense', "SELECT t.topic_id AS id, t.category, t.topic_name AS topic, t.description AS `desc`
'desc' => 'place value to billions, prime vs composite, GCF/LCM, order of operations (PEMDAS), integer arithmetic, absolute value, rounding and estimation, scientific notation, divisibility rules, factors and multiples, mental math strategies, number line, comparing and ordering decimals'], FROM kb_generator_topics t WHERE t.active=1 ORDER BY t.id ASC"
['id' => 'math_fractions', 'category' => 'mathematics', 'topic' => 'Fractions, decimals, and percentages', );
'desc' => 'equivalent fractions, simplifying fractions, adding/subtracting unlike denominators, multiplying/dividing fractions, mixed numbers vs improper fractions, converting between fractions/decimals/percentages, percent increase/decrease, discount and tax calculations, ratio and proportion, unit rate, cross-multiplication'], if (empty($BATCHES)) {
['id' => 'math_algebra1', 'category' => 'mathematics', 'topic' => 'Algebra I — equations and inequalities', log_line('ERROR: No active topics in kb_generator_topics table. Add topics via the JARVIS admin panel.');
'desc' => 'one-step and two-step equations, distributing and combining like terms, solving inequalities, graphing on a number line, slope-intercept form (y=mx+b), point-slope form, standard form, graphing linear equations, systems of equations (substitution, elimination, graphing), functions vs relations, domain and range'], exit(1);
['id' => 'math_algebra2', 'category' => 'mathematics', 'topic' => 'Algebra II — advanced functions', }
'desc' => 'quadratic formula and discriminant, completing the square, factoring (trinomials, difference of squares, grouping), polynomial long division, synthetic division, rational expressions, radical expressions, complex numbers, exponential functions, logarithms and log properties, sequences (arithmetic/geometric), binomial theorem'], log_line('Loaded ' . count($BATCHES) . ' active topics from database.');
['id' => 'math_geometry', 'category' => 'mathematics', 'topic' => 'Geometry — shapes, proofs, and measurements',
'desc' => 'types of angles (complementary, supplementary, vertical, corresponding), triangle congruence (SSS, SAS, ASA, AAS, HL), similarity and scale factors, Pythagorean theorem and its converse, special right triangles (30-60-90, 45-45-90), circle theorems (inscribed angles, chords, arcs), area and perimeter of all polygons, surface area and volume of 3D solids, coordinate geometry, transformations (translation, rotation, reflection, dilation), two-column proofs'],
['id' => 'math_trig', 'category' => 'mathematics', 'topic' => 'Trigonometry',
'desc' => 'SOH-CAH-TOA, unit circle (all quadrants), reference angles, reciprocal trig functions (csc, sec, cot), inverse trig functions, trig identities (Pythagorean, sum/difference, double-angle), Law of Sines and Law of Cosines, solving trig equations, graphing sine and cosine (amplitude, period, phase shift), radians vs degrees, polar coordinates'],
['id' => 'math_precalc', 'category' => 'mathematics', 'topic' => 'Pre-calculus',
'desc' => 'function composition and inverses, piecewise functions, transformations of functions, polynomial end behavior, rational function asymptotes, partial fractions, conic sections (parabola, ellipse, hyperbola, circle), parametric equations, vectors (magnitude, dot product, component form), matrices (operations, determinants, inverses), systems of nonlinear equations, limits concept introduction'],
['id' => 'math_calc1', 'category' => 'mathematics', 'topic' => 'Calculus I — limits and derivatives',
'desc' => 'epsilon-delta definition of limit, limit laws, one-sided limits, limits at infinity, continuity, Intermediate Value Theorem, definition of derivative, power rule, product rule, quotient rule, chain rule, implicit differentiation, related rates, Mean Value Theorem, Rolle\'s theorem, critical points, first and second derivative tests, optimization problems, curve sketching'],
['id' => 'math_calc2', 'category' => 'mathematics', 'topic' => 'Calculus II — integrals and series',
'desc' => 'Riemann sums, Fundamental Theorem of Calculus, u-substitution, integration by parts, trig substitution, partial fractions, improper integrals, area between curves, volumes of revolution (disk/washer/shell), arc length, sequences vs series, convergence tests (integral, comparison, ratio, root, alternating series), Taylor and Maclaurin series, power series radius of convergence'],
['id' => 'math_stats', 'category' => 'mathematics', 'topic' => 'Statistics and probability',
'desc' => 'mean/median/mode/range, standard deviation and variance, normal distribution (68-95-99.7 rule), z-scores, sampling methods (random, stratified, cluster), bias in studies, correlation vs causation, scatter plots and regression lines, probability rules (addition, multiplication, conditional), permutations vs combinations, binomial distribution, hypothesis testing basics, p-values, confidence intervals, Type I and II errors'],
['id' => 'math_discrete', 'category' => 'mathematics', 'topic' => 'Discrete mathematics',
'desc' => 'set theory (union, intersection, complement, De Morgan\'s laws), logic gates and truth tables, proof techniques (direct, contradiction, induction), graph theory (vertices, edges, paths, trees), Euler and Hamiltonian paths, counting principles (multiplication rule, pigeonhole), modular arithmetic, cryptography basics (RSA overview), recursion, finite automata, Boolean algebra'],
['id' => 'math_linear', 'category' => 'mathematics', 'topic' => 'Linear algebra',
'desc' => 'vectors in R2/R3, vector addition and scalar multiplication, linear combinations and span, matrix multiplication, matrix transpose, determinant (2x2 and 3x3), inverse matrix, row reduction and RREF, systems of linear equations as matrices, rank and nullity, eigenvalues and eigenvectors, diagonalization, dot product and cross product, linear transformations, projections'],
['id' => 'sci_scientific', 'category' => 'science', 'topic' => 'Scientific method and experimental design',
'desc' => 'forming a hypothesis, independent vs dependent variables, control groups and constants, experimental vs observational studies, data collection methods, accuracy vs precision, significant figures, error analysis, scientific notation in measurements, peer review process, correlation vs causation, pseudoscience red flags, famous experiments in science history'],
['id' => 'bio_cell2', 'category' => 'biology', 'topic' => 'Cell biology — structure and function',
'desc' => 'prokaryotic vs eukaryotic cells, plant vs animal cell differences, organelle functions (nucleus, mitochondria, ribosome, ER rough/smooth, Golgi, lysosome, vacuole, chloroplast), cell membrane structure (phospholipid bilayer, membrane proteins), passive transport (diffusion, osmosis, facilitated diffusion), active transport, endocytosis/exocytosis, cell cycle phases (G1/S/G2/M), mitosis stages in detail, cytokinesis'],
['id' => 'bio_genetics', 'category' => 'biology', 'topic' => 'Genetics — inheritance and molecular biology',
'desc' => 'Mendel\'s laws (segregation, independent assortment), monohybrid and dihybrid crosses, Punnett squares, incomplete vs codominance, sex-linked traits, pedigree analysis, DNA double helix structure, base pairing (A-T, G-C), DNA replication (helicase, polymerase, ligase), transcription (DNA→mRNA), translation (mRNA→protein via ribosomes and tRNA), mutations (point, frameshift, silent, nonsense), genetic disorders (Down syndrome, sickle cell, Huntington\'s, cystic fibrosis)'],
['id' => 'bio_evolution', 'category' => 'biology', 'topic' => 'Evolution and natural selection',
'desc' => 'Darwin\'s voyage and observations, four conditions for natural selection, artificial selection examples, types of variation (genetic, phenotypic), genetic drift and founder effect, bottleneck effect, gene flow, Hardy-Weinberg equilibrium, speciation (allopatric vs sympatric), reproductive isolation mechanisms, convergent vs divergent evolution, homologous vs analogous structures, vestigial structures, fossil record as evidence, comparative anatomy and embryology, molecular phylogenetics, tree of life'],
['id' => 'bio_ecology2', 'category' => 'biology', 'topic' => 'Ecology — populations and communities',
'desc' => 'population growth (exponential vs logistic), carrying capacity, predator-prey cycles (Lotka-Volterra), competitive exclusion principle, keystone species, ecological succession (primary vs secondary), trophic levels (producers/consumers/decomposers), energy flow (10% rule), nutrient cycles (carbon, nitrogen, water, phosphorus), biome types and characteristics, invasive species impacts, island biogeography, biodiversity indices'],
['id' => 'bio_human', 'category' => 'biology', 'topic' => 'Human physiology — organs and systems',
'desc' => 'cardiovascular system (heart chambers, valves, blood pressure, cardiac output), respiratory system (mechanics of breathing, gas exchange at alveoli, pulmonary volumes), digestive system (enzyme actions at each stage, absorption in small intestine, large intestine water reabsorption), nervous system (neuron structure, action potential, synapse, CNS vs PNS, reflex arcs), endocrine system (pituitary, thyroid, adrenal, pancreas, hormones), urinary system (nephron function, filtration/reabsorption/secretion), immune system (innate vs adaptive, B-cells, T-cells, antibodies, vaccines)'],
['id' => 'bio_micro', 'category' => 'biology', 'topic' => 'Microbiology — bacteria, viruses, and fungi',
'desc' => 'bacterial cell structure (cell wall, flagella, plasmids), bacterial reproduction (binary fission, conjugation, transformation, transduction), antibiotic mechanisms and resistance, virus structure (capsid, envelope, spike proteins), viral replication cycle (lytic vs lysogenic), HIV/AIDS mechanism, common diseases by pathogen type, Koch\'s postulates, fungal cell wall (chitin), mycology basics, prions, archaea vs bacteria, microbiome and human health'],
['id' => 'chem_atomic', 'category' => 'chemistry', 'topic' => 'Atomic structure and the periodic table',
'desc' => 'subatomic particles (proton/neutron/electron), atomic number vs mass number, isotopes and atomic mass calculation, electron configuration (s/p/d/f orbitals), aufbau principle, Pauli exclusion, Hund\'s rule, periodic table groups and periods, trends (atomic radius, ionization energy, electronegativity, electron affinity), metals/nonmetals/metalloids, alkali metals, halogens, noble gases'],
['id' => 'chem_bonding', 'category' => 'chemistry', 'topic' => 'Chemical bonding and molecular structure',
'desc' => 'ionic bond formation (metal + nonmetal), lattice energy, covalent bonds (single/double/triple), Lewis dot structures, formal charge, resonance structures, VSEPR theory (linear/trigonal planar/tetrahedral/trigonal bipyramidal/octahedral), bond polarity vs molecular polarity, intermolecular forces (London dispersion, dipole-dipole, hydrogen bonding), metallic bonding, network solids, hybrid orbitals (sp/sp2/sp3)'],
['id' => 'chem_reactions', 'category' => 'chemistry', 'topic' => 'Chemical reactions and stoichiometry',
'desc' => 'balancing chemical equations, types of reactions (synthesis, decomposition, single/double displacement, combustion, acid-base, redox), oxidation states, identifying oxidizing/reducing agents, mole concept and Avogadro\'s number, molar mass calculations, percent composition, empirical vs molecular formula, stoichiometric calculations, limiting reagent, theoretical vs actual vs percent yield, solution stoichiometry (molarity, dilution)'],
['id' => 'chem_thermo', 'category' => 'chemistry', 'topic' => 'Thermochemistry and kinetics',
'desc' => 'enthalpy (ΔH), endothermic vs exothermic reactions, Hess\'s law, bond enthalpy, heat capacity and calorimetry (q=mcΔT), entropy (ΔS) and disorder, Gibbs free energy (ΔG = ΔH - TΔS), spontaneity, reaction rate factors (temperature, concentration, surface area, catalysts), collision theory, activation energy, Arrhenius equation, reaction mechanisms, rate laws, zero/first/second order reactions, half-life'],
['id' => 'chem_equil', 'category' => 'chemistry', 'topic' => 'Chemical equilibrium and acids/bases',
'desc' => 'Le Chatelier\'s principle (temperature, pressure, concentration changes), equilibrium constant K (Kc and Kp), reaction quotient Q, ICE tables, Ksp and solubility product, common ion effect, Arrhenius/Brønsted-Lowry/Lewis acid-base definitions, strong vs weak acids and bases, Ka and Kb, pH and pOH calculations, buffer solutions (Henderson-Hasselbalch), titration curves, indicators, hydrolysis of salts'],
['id' => 'phys_mechanics', 'category' => 'physics', 'topic' => 'Classical mechanics',
'desc' => 'kinematics equations (big four), free fall and g = 9.8 m/s², projectile motion (horizontal/vertical components), Newton\'s three laws in detail, free body diagrams, normal force, tension, friction (static vs kinetic, μ), inclined planes, circular motion (centripetal force and acceleration), universal gravitation (F = Gm1m2/r²), work-energy theorem, conservative vs non-conservative forces, elastic vs inelastic collisions, center of mass, rotational motion (torque, moment of inertia, angular momentum)'],
['id' => 'phys_waves', 'category' => 'physics', 'topic' => 'Waves, sound, and optics',
'desc' => 'transverse vs longitudinal waves, wavelength/frequency/amplitude/period relationships (v=fλ), standing waves and harmonics, Doppler effect, sound intensity (decibels), resonance, interference (constructive/destructive), diffraction, reflection (law of reflection), refraction (Snell\'s law, index of refraction), total internal reflection, lenses (converging/diverging, focal length), mirrors (concave/convex), optical instruments (telescope, microscope), polarization, double-slit experiment'],
['id' => 'phys_em', 'category' => 'physics', 'topic' => 'Electricity and magnetism',
'desc' => 'electric charge (Coulomb\'s law), electric field lines, electric potential (voltage), capacitance, Ohm\'s law (V=IR), series vs parallel circuits, Kirchhoff\'s voltage and current laws, electric power (P=IV), magnetic fields (right-hand rules), magnetic force on moving charge (F=qvB), electromagnetic induction, Faraday\'s law, Lenz\'s law, transformers, AC vs DC, Maxwell\'s equations overview, electromagnetic spectrum'],
['id' => 'phys_thermo', 'category' => 'physics', 'topic' => 'Thermodynamics and modern physics',
'desc' => 'temperature scales (Celsius/Fahrenheit/Kelvin conversions), thermal expansion, ideal gas law (PV=nRT), kinetic molecular theory, first law of thermodynamics (ΔU=Q-W), second law (entropy always increases), heat engines and efficiency, Carnot cycle, blackbody radiation, photoelectric effect, Bohr model of hydrogen, de Broglie wavelength, Heisenberg uncertainty principle, nuclear reactions (fission vs fusion), radioactive decay types (alpha/beta/gamma), half-life calculations, E=mc²'],
['id' => 'earth_geo', 'category' => 'science', 'topic' => 'Geology — rocks, minerals, and plate tectonics',
'desc' => 'mineral identification (hardness, luster, cleavage, streak, color), Mohs scale, rock cycle in detail, igneous rocks (intrusive vs extrusive, granite vs basalt, crystal size), sedimentary rocks (clastic/chemical/organic, deposition environments), metamorphic rocks (contact vs regional, foliated vs non-foliated), relative vs absolute dating, index fossils, half-life and radiometric dating, plate boundaries (convergent/divergent/transform), subduction zones, mountain building, seafloor spreading, paleomagnetism as evidence'],
['id' => 'earth_atmos', 'category' => 'science', 'topic' => 'Atmosphere, weather, and meteorology',
'desc' => 'atmospheric layers (troposphere/stratosphere/mesosphere/thermosphere/exosphere), atmospheric composition, air pressure and altitude, Coriolis effect, global wind patterns (trade winds, westerlies, polar easterlies), Hadley/Ferrel/Polar cells, weather fronts (cold/warm/stationary/occluded), air masses and their source regions, cloud types (cumulus/stratus/cirrus/cumulonimbus), dew point and relative humidity, thunderstorm anatomy, tornado formation, hurricane structure and categories, El Niño/La Niña'],
['id' => 'earth_ocean', 'category' => 'science', 'topic' => 'Oceanography and hydrosphere',
'desc' => 'ocean zones (epipelagic/mesopelagic/bathypelagic/abyssopelagic/hadal), ocean currents (surface vs deep thermohaline circulation), tides (gravitational pull of Moon and Sun), wave generation and breaking, ocean chemistry (salinity, pH, oxygen levels), coral reef ecosystems and bleaching, marine food webs, overfishing and bycatch, plastic pollution, ocean acidification mechanism, hydrothermal vents and chemosynthesis, sea level rise and coastal erosion'],
['id' => 'environ_sci', 'category' => 'science', 'topic' => 'Environmental science and sustainability',
'desc' => 'ecosystem services, carbon cycle and carbon sinks, nitrogen cycle (fixation, nitrification, denitrification), greenhouse gases (CO2, methane, N2O, water vapor), greenhouse effect vs global warming, climate feedback loops (positive/negative), renewable energy types (solar/wind/hydro/geothermal), fossil fuel formation and combustion impacts, deforestation rates and consequences, biodiversity hotspots, endangered species classifications (IUCN), sustainable agriculture, circular economy, life cycle assessment'],
['id' => 'hist_ancient', 'category' => 'history', 'topic' => 'Ancient civilizations — Egypt, Greece, Rome, Mesopotamia',
'desc' => 'Mesopotamian city-states (Sumer, Akkad, Babylon), Code of Hammurabi, cuneiform writing, ziggurat architecture, Egyptian Old/Middle/New Kingdoms, pharaohs (Ramesses II, Cleopatra, Tutankhamun), hieroglyphics and Rosetta Stone, pyramids of Giza construction theories, Greek city-states (Athens vs Sparta), Athenian democracy origins, Persian Wars (Marathon, Thermopylae, Salamis), Peloponnesian War, Macedonian Empire under Alexander the Great, Roman Republic institutions (Senate, consuls, tribunes), Punic Wars, Julius Caesar\'s rise and assassination, Pax Romana, causes of Rome\'s fall'],
['id' => 'hist_medieval', 'category' => 'history', 'topic' => 'Medieval period and the Middle Ages (500-1500)',
'desc' => 'fall of Western Roman Empire, Byzantine Empire at Constantinople, feudalism structure (king/lords/knights/serfs), manorialism and serfdom, Catholic Church power (Pope vs monarchs, Investiture Controversy), Crusades (1st through 4th), Reconquista in Spain, Black Death (bubonic plague) and its social impact, Magna Carta (1215) and its significance, Hundred Years\' War, Joan of Arc, Mongol Empire (Genghis and Kublai Khan), Silk Road trade, Islamic Golden Age (algebra, astronomy, medicine), feudal Japan (samurai, shogunate)'],
['id' => 'hist_early_mod', 'category' => 'history', 'topic' => 'Early modern period — Renaissance, Reformation, Exploration (1400-1700)',
'desc' => 'Italian Renaissance origins (Florence, Medici patronage), humanism philosophy, Leonardo da Vinci, Michelangelo, Raphael, Gutenberg\'s printing press impact, Protestant Reformation (Martin Luther\'s 95 Theses, Calvin, Zwingli), Catholic Counter-Reformation and Council of Trent, Spanish Inquisition, Age of Exploration (motivations: gold/god/glory), Portuguese exploration (Vasco da Gama, Magellan), Spanish conquest (Columbus, Cortés/Aztecs, Pizarro/Incas), Columbian Exchange, Atlantic slave trade beginnings, Thirty Years\' War, Scientific Revolution (Copernicus, Galileo, Newton)'],
['id' => 'hist_revolutions', 'category' => 'history', 'topic' => 'Age of Revolutions (1700-1850)',
'desc' => 'Enlightenment thinkers (Locke, Rousseau, Voltaire, Montesquieu) and their ideas, American Revolution causes (taxation without representation, Boston Massacre, Tea Party), Declaration of Independence key ideas, Articles of Confederation weaknesses, Constitutional Convention of 1787, Bill of Rights, French Revolution phases (Estates General, storming Bastille, Reign of Terror, Thermidorian Reaction), Napoleon\'s rise, Code Napoleon, Napoleonic Wars, Congress of Vienna, Latin American independence movements (Bolívar, San Martín, Toussaint L\'Ouverture), Industrial Revolution in Britain (spinning jenny, steam engine, factories, urbanization)'],
['id' => 'hist_19c', 'category' => 'history', 'topic' => '19th century — imperialism and nationalism',
'desc' => 'European colonialism in Africa (Berlin Conference/Scramble for Africa 1884-85), British Empire at peak (India as the crown jewel, Opium Wars in China), Social Darwinism ideology, Meiji Restoration in Japan, Crimean War, unification of Germany (Bismarck) and Italy (Risorgimento), US westward expansion and Manifest Destiny, Trail of Tears and Native American displacement, American Civil War causes (slavery, states\' rights, sectionalism), key battles (Gettysburg, Antietam), Reconstruction, Reconstruction Amendments (13th/14th/15th), Gilded Age robber barons'],
['id' => 'hist_ww1', 'category' => 'history', 'topic' => 'World War I (1914-1918)',
'desc' => 'MAIN causes (Militarism, Alliance system—Triple Alliance vs Triple Entente, Imperialism, Nationalism), assassination of Franz Ferdinand, Schlieffen Plan, trench warfare conditions, Western Front stalemate, Eastern Front collapse, new weapons technology (machine guns, poison gas, tanks, airplanes, submarines), U-boat campaign and sinking of Lusitania, US entry (1917), Zimmermann Telegram, Russian Revolution and withdrawal, Battle of Somme casualties, Treaty of Versailles terms, League of Nations creation and US rejection, redrawing of European map'],
['id' => 'hist_interwar', 'category' => 'history', 'topic' => 'Interwar period and rise of fascism (1919-1939)',
'desc' => 'Great Depression causes (Black Tuesday 1929, bank failures, Smoot-Hawley tariff, Dust Bowl), Hoovervilles, FDR\'s New Deal programs (CCC, WPA, Social Security, FDIC), rise of Nazism in Germany (Weimar Republic failures, hyperinflation, Hitler\'s Mein Kampf), Nuremberg Laws and early persecution, Mussolini\'s fascist Italy, Spanish Civil War as testing ground, Japanese expansionism in Asia (Manchuria, Nanjing), Soviet collectivization and Gulag, Stalin\'s purges, Appeasement policy, Nazi-Soviet Pact'],
['id' => 'hist_ww2', 'category' => 'history', 'topic' => 'World War II (1939-1945)',
'desc' => 'Blitzkrieg tactics, Battle of Britain (RAF vs Luftwaffe), Operation Barbarossa (German invasion of USSR), Battle of Stalingrad as turning point, Pacific Theater (Pearl Harbor, Midway, island-hopping campaign), Holocaust (Nuremberg Laws to Final Solution, Wannsee Conference, six major death camps, six million Jews plus five million others), D-Day (June 6 1944), Battle of the Bulge, firebombing of Dresden and Tokyo, Manhattan Project and atomic bombs (Hiroshima August 6, Nagasaki August 9), V-E Day and V-J Day, war crimes tribunals at Nuremberg'],
['id' => 'hist_cold_war', 'category' => 'history', 'topic' => 'Cold War (1947-1991)',
'desc' => 'Truman Doctrine and containment policy, Marshall Plan, Berlin Blockade and Airlift, NATO formation, Korean War (38th parallel, UN coalition), McCarthyism and Red Scare, Suez Crisis, Hungarian Revolution 1956, Sputnik launch and Space Race, Cuban Revolution and Castro, Bay of Pigs failure, Cuban Missile Crisis (13 days), Berlin Wall construction, Vietnam War escalation (Gulf of Tonkin), Tet Offensive, Nixon\'s détente and visit to China, SALT treaties, Soviet invasion of Afghanistan, Reagan\'s military buildup, fall of Berlin Wall 1989, Soviet collapse 1991'],
['id' => 'hist_civil_rights', 'category' => 'history', 'topic' => 'US Civil Rights Movement',
'desc' => 'Reconstruction\'s end and Jim Crow laws, Plessy v. Ferguson (1896) separate but equal, Great Migration north, NAACP founding and legal strategy, Brown v. Board of Education (1954), Montgomery Bus Boycott and Rosa Parks, Little Rock Nine, sit-in movement (Greensboro), Freedom Riders, March on Washington and \'I Have a Dream\' speech, Birmingham campaign (Bull Connor), Civil Rights Act of 1964, Voting Rights Act of 1965, Malcolm X and Black Power, assassination of MLK, Fair Housing Act 1968, long-term impact and ongoing inequality'],
['id' => 'hist_20c_world', 'category' => 'history', 'topic' => 'Modern world history (1945-2000)',
'desc' => 'decolonization waves (India 1947, African independence 1950s-60s), creation of Israel and Arab-Israeli wars (1948, 1967, 1973), apartheid in South Africa and Mandela, partition of India and Pakistan, Chinese Communist Revolution and Mao Zedong (Great Leap Forward, Cultural Revolution), Korean War armistice, Vietnam War end and reunification, Cambodian genocide (Khmer Rouge), Iran Islamic Revolution 1979, Iran-Iraq War, Gulf War 1991, Yugoslav Wars and ethnic cleansing, Rwandan genocide 1994, Oslo Accords and peace process'],
['id' => 'govt_us', 'category' => 'civics', 'topic' => 'US government — structure and function',
'desc' => 'Article I: Congress (bicameral, House apportionment, Senate 2 per state, legislative process including conference committee), Article II: President (electoral college, cabinet, executive orders, veto power, commander in chief), Article III: Supreme Court (judicial review established by Marbury v. Madison, original vs appellate jurisdiction, lifetime appointments), federalism (enumerated/implied/reserved/concurrent powers, 10th Amendment), checks and balances examples, constitutional amendments process, political parties history'],
['id' => 'govt_state_local', 'category' => 'civics', 'topic' => 'State and local government',
'desc' => 'state constitutions vs US Constitution, governors\' powers, state legislatures (unicameral vs bicameral), state court systems, initiative and referendum process, recall elections, state budget process, county government (commissioners, sheriff, tax assessor), city government types (mayor-council, council-manager, commission), school boards, special districts, home rule charters, municipal bonds, local taxation (property tax), zoning and land use'],
['id' => 'govt_econ_pol', 'category' => 'economics', 'topic' => 'Economic policy and the Federal Reserve',
'desc' => 'monetary policy tools (federal funds rate, open market operations, reserve requirements, discount rate), quantitative easing, inflation targeting (2% goal), Federal Reserve structure (Board of Governors, 12 regional banks, FOMC), fiscal policy (government spending and taxation), Keynesian vs supply-side economics, automatic stabilizers, budget deficit vs national debt, crowding out effect, Laffer curve, trade policy (tariffs, quotas, trade agreements—USMCA, WTO), balance of payments'],
['id' => 'us_const_law', 'category' => 'civics', 'topic' => 'Constitutional law and landmark Supreme Court cases',
'desc' => 'Marbury v. Madison (judicial review), McCulloch v. Maryland (necessary and proper clause), Dred Scott v. Sandford, Plessy v. Ferguson, Brown v. Board of Education, Griswold v. Connecticut (right to privacy), Miranda v. Arizona (Miranda rights), Roe v. Wade and Dobbs v. Jackson, Obergefell v. Hodges (same-sex marriage), Citizens United v. FEC (campaign finance), District of Columbia v. Heller (Second Amendment), NFIB v. Sebelius (ACA), Dobbs v. Jackson Women\'s Health, recent First Amendment cases'],
['id' => 'econ_micro', 'category' => 'economics', 'topic' => 'Microeconomics — consumers and firms',
'desc' => 'utility and marginal utility, consumer surplus, producer surplus, deadweight loss, price elasticity of demand and supply, income elasticity, cross-price elasticity, production function (inputs, outputs), total/average/marginal costs, economies of scale, short run vs long run, perfect competition (many sellers, price taker, normal profit), monopoly (price maker, deadweight loss, barriers to entry), oligopoly (interdependence, game theory, Nash equilibrium, price leadership), monopolistic competition (product differentiation, advertising)'],
['id' => 'econ_macro', 'category' => 'economics', 'topic' => 'Macroeconomics — national and global economy',
'desc' => 'GDP calculation methods (expenditure: C+I+G+NX; income: wages+rents+interest+profits), real vs nominal GDP, GDP deflator, business cycle phases (expansion, peak, contraction, trough), types of unemployment (frictional, structural, cyclical, seasonal), natural rate of unemployment, Phillips curve trade-off, CPI calculation and core inflation, hyperinflation examples (Weimar, Zimbabwe, Venezuela), multiplier effect, aggregate demand/supply model, short-run vs long-run equilibrium, stagflation'],
['id' => 'econ_personal', 'category' => 'economics', 'topic' => 'Personal finance and consumer economics',
'desc' => 'creating a personal budget, 50/30/20 rule, zero-based budgeting, emergency fund sizing (3-6 months expenses), compound interest calculations, Rule of 72, credit score components (FICO: payment history 35%, amounts owed 30%, length of credit history 15%, new credit 10%, credit mix 10%), how credit cards work (APR, minimum payment trap, grace period), types of loans (mortgage, auto, personal, student), debt-to-income ratio, net worth calculation, tax brackets and effective vs marginal tax rates'],
['id' => 'lit_classics', 'category' => 'literature', 'topic' => 'Classic American and British literature',
'desc' => 'The Great Gatsby (American Dream critique, symbolism — green light, Valley of Ashes, Gatsby\'s parties), To Kill a Mockingbird (racial injustice, moral growth, Atticus Finch), Of Mice and Men (friendship, dreams, euthanasia themes), Romeo and Juliet (fate, impulsive love, family conflict), Hamlet (revenge, procrastination, \'To be or not to be\'), Macbeth (ambition, guilt, supernatural), 1984 (totalitarianism, doublethink, surveillance), Brave New World (dystopia, conditioning, soma), Lord of the Flies (human nature, civilization vs savagery), Catcher in the Rye (alienation, phoniness, adolescence)'],
['id' => 'lit_world', 'category' => 'literature', 'topic' => 'World literature and diverse voices',
'desc' => 'One Hundred Years of Solitude (magic realism, Buendía family, Macondo), Things Fall Apart (colonialism\'s impact on Igbo culture, Okonkwo\'s tragedy), The Alchemist (personal legend, journey metaphor), Don Quixote as first modern novel, Dostoevsky\'s Crime and Punishment (guilt and redemption), Tolstoy\'s War and Peace, Kafka\'s The Metamorphosis (alienation, absurdism), Camus and existentialism (The Stranger, The Plague), Chimamanda Ngozi Adichie, Haruki Murakami, postcolonial literature themes'],
['id' => 'lit_poetry', 'category' => 'literature', 'topic' => 'Poetry — forms, devices, and analysis',
'desc' => 'poetry forms (sonnet 14 lines—Shakespearean vs Petrarchan, haiku 5-7-5, villanelle, free verse, ode, elegy, ballad, epic), meter (iambic pentameter, feet: iamb/trochee/spondee/dactyl/anapest), rhyme scheme (ABAB CDCD EFEF GG), sound devices (alliteration, assonance, consonance, onomatopoeia), figurative language (simile, metaphor, personification, hyperbole, understatement, synecdoche, metonymy), imagery and sensory details, tone vs mood, theme vs subject, major poets (Emily Dickinson, Walt Whitman, Langston Hughes, Maya Angelou, Robert Frost, Pablo Neruda)'],
['id' => 'grammar_writing', 'category' => 'literature', 'topic' => 'Grammar, mechanics, and writing craft',
'desc' => 'parts of speech (noun, pronoun, verb, adjective, adverb, preposition, conjunction, interjection), sentence types (simple, compound, complex, compound-complex), clauses (independent vs dependent), phrases (noun, verb, prepositional, participial, gerund, infinitive), common errors (run-ons, comma splices, sentence fragments, dangling modifiers, subject-verb agreement, pronoun-antecedent agreement), punctuation rules (semicolons, colons, dashes, commas in all uses), parallel structure, active vs passive voice, essay structure (thesis, body paragraphs, counterargument, conclusion), MLA/APA citation basics'],
['id' => 'rhetoric', 'category' => 'literature', 'topic' => 'Rhetoric, argument, and persuasion',
'desc' => 'Aristotle\'s three appeals: ethos (credibility), pathos (emotion), logos (logic), rhetorical situation (author, audience, purpose, context), claim types (fact, value, policy), types of evidence (statistical, anecdotal, expert testimony, analogical), logical fallacies in detail (ad hominem, straw man, false dichotomy, slippery slope, appeal to authority, bandwagon, red herring, circular reasoning, hasty generalization, post hoc ergo propter hoc), Toulmin model (claim, grounds, warrant, backing, qualifier, rebuttal), analyzing speeches and op-eds'],
['id' => 'fin_budgeting', 'category' => 'personal_finance', 'topic' => 'Budgeting, saving, and debt management',
'desc' => 'tracking income vs expenses, fixed vs variable expenses, budget apps (Mint, YNAB, EveryDollar), paying yourself first, high-yield savings accounts vs regular savings, CDs and money market accounts, emergency fund where to keep it, good debt vs bad debt, credit card interest calculation (daily periodic rate), minimum payment trap math, debt avalanche (highest interest first) vs snowball (smallest balance first) method, student loan types (subsidized vs unsubsidized, PLUS, private), income-driven repayment plans, loan forgiveness programs'],
['id' => 'fin_investing2', 'category' => 'personal_finance', 'topic' => 'Investing — stocks, bonds, and retirement',
'desc' => 'individual stocks vs index funds vs ETFs, expense ratios and why they matter, S&P 500 historical returns (~10% nominal), asset allocation by age, rebalancing portfolio, tax-advantaged accounts (401k contribution limits, employer match, traditional vs Roth tax treatment, IRA income limits), Social Security benefits calculation, Medicare basics, required minimum distributions, capital gains tax (short-term vs long-term rates), wash sale rule, dividend reinvestment, bond ratings (investment grade vs junk), duration and interest rate risk'],
['id' => 'fin_taxes', 'category' => 'personal_finance', 'topic' => 'Taxes — income, deductions, and filing',
'desc' => 'W-2 vs W-4 vs 1099 forms, filing status (single, MFJ, MFS, HOH, qualifying widow(er)), standard deduction vs itemizing, above-the-line vs below-the-line deductions, credits vs deductions difference, EITC and Child Tax Credit, Schedule C for self-employment, SE tax, quarterly estimated taxes, AMT basics, state income taxes, property taxes and how assessed, sales tax vs use tax, gift tax exclusion, estate tax threshold, IRS audit red flags, free filing options'],
['id' => 'health_chronic', 'category' => 'health', 'topic' => 'Chronic disease prevention and management',
'desc' => 'Type 2 diabetes: insulin resistance mechanism, A1C test, glycemic control strategies, prevention through lifestyle, Type 1 vs Type 2 differences, cardiovascular disease risk factors (LDL vs HDL cholesterol, triglycerides, blood pressure categories—normal/elevated/Stage 1/Stage 2 hypertension, ASCVD risk calculator), metabolic syndrome criteria, cancer screening guidelines (mammogram, colonoscopy, PSA, Pap smear) by age and risk, BMI limitations as metric, waist circumference as predictor, sleep apnea screening, chronic pain management approaches'],
['id' => 'mental_health2', 'category' => 'mental_health', 'topic' => 'Mental health — therapy, medication, and recovery',
'desc' => 'DSM-5 major categories, cognitive behavioral therapy (CBT) techniques (thought records, behavioral activation, exposure hierarchy), dialectical behavior therapy (DBT) skills (mindfulness, distress tolerance, emotion regulation, interpersonal effectiveness), EMDR for trauma, psychodynamic therapy, medication classes (SSRIs, SNRIs, benzodiazepines, mood stabilizers, antipsychotics — mechanisms and side effects), finding a therapist (types of licenses: LCSW, LPC, psychologist, psychiatrist), crisis resources (988 Suicide and Crisis Lifeline), stigma reduction, peer support groups'],
['id' => 'substances', 'category' => 'health', 'topic' => 'Substance use, addiction, and recovery',
'desc' => 'addiction as brain disease (dopamine pathway, nucleus accumbens, prefrontal cortex), tolerance and withdrawal, alcohol (BAC levels and effects, liver disease progression, fetal alcohol syndrome, DSM criteria for AUD), opioids (natural, semi-synthetic, synthetic — fentanyl 100x morphine), opioid overdose signs and naloxone (Narcan) administration, stimulants (cocaine, meth, amphetamines), cannabis effects on developing brain, vaping and e-cigarette risks (EVALI), treatment approaches (MAT with buprenorphine/methadone, 12-step programs, inpatient vs outpatient), harm reduction philosophy'],
['id' => 'nutrition2', 'category' => 'health', 'topic' => 'Advanced nutrition and dietetics',
'desc' => 'macronutrient ratios for different goals (endurance vs strength vs weight loss), complete vs incomplete proteins, essential amino acids, omega-3 vs omega-6 fatty acids (EPA/DHA sources, anti-inflammatory role), fiber types (soluble vs insoluble, prebiotic fiber), micronutrient deficiencies (iron deficiency anemia, vitamin D and bone health, B12 deficiency in vegans, iodine and thyroid, zinc and immune function), food label reading (serving sizes, ingredient order, added sugars), ultra-processed food research, Mediterranean diet evidence, gut microbiome diversity'],
['id' => 'fitness2', 'category' => 'health', 'topic' => 'Exercise science and performance',
'desc' => 'FITT principle (frequency, intensity, time, type), periodization (linear vs undulating), compound lifts (squat, deadlift, bench press, overhead press — form cues), RPE scale and heart rate zones, VO2 max testing and improvement, lactate threshold training, EPOC (afterburn effect), muscle fiber types (Type I slow-twitch vs Type IIa/IIb fast-twitch), DOMS explanation and management, overtraining syndrome signs, sleep and testosterone/cortisol balance, creatine monohydrate evidence, protein timing myth vs reality, progressive overload tracking'],
['id' => 'sleep_science', 'category' => 'health', 'topic' => 'Sleep science and circadian biology',
'desc' => 'sleep stages (N1/N2/N3 NREM and REM cycling), circadian rhythm and suprachiasmatic nucleus, melatonin production timing, sleep debt and recovery, adenosine buildup and caffeine mechanism, blue light and screen exposure, recommended hours by age group, sleep disorders (insomnia, sleep apnea—types and CPAP, narcolepsy, RLS, parasomnias), sleep hygiene evidence-based practices, napping science (20-min power nap vs 90-min full cycle), shift work health effects, chronic sleep deprivation cognitive impacts'],
['id' => 'cooking2', 'category' => 'cooking', 'topic' => 'Cooking techniques and food science',
'desc' => 'Maillard reaction vs caramelization (temperatures, foods, flavors produced), collagen breakdown in braising (why tough cuts get tender), emulsification (mayo, hollandaise — lecithin role), gluten development (flour protein content, kneading, resting), leavening agents (baking soda vs baking powder, yeast fermentation, steam), salt roles (seasoning, texture, curing, fermentation), knife cuts (brunoise, julienne, chiffonade, batonnet, dice sizes), pan sauces (fond, deglazing, reduction), sous vide temperature and time, fermentation (kimchi, sourdough starter maintenance, yogurt making)'],
['id' => 'ai_ml', 'category' => 'technology', 'topic' => 'Artificial intelligence and machine learning',
'desc' => 'supervised vs unsupervised vs reinforcement learning, training data and overfitting, bias in AI systems, neural network layers (input/hidden/output), activation functions, backpropagation, convolutional neural networks for image recognition, recurrent neural networks and LSTMs for sequence data, transformer architecture and attention mechanism, large language models (GPT, Claude, Gemini — how they work), prompt engineering basics, AI hallucination problem, generative AI (image synthesis, DALL-E, Midjourney), AI ethics (fairness, accountability, transparency), AI regulation debates'],
['id' => 'cybersec', 'category' => 'technology', 'topic' => 'Cybersecurity and digital safety',
'desc' => 'CIA triad (confidentiality, integrity, availability), threat actors (nation-states, hacktivists, cybercriminals, insiders), attack vectors: phishing (spear phishing, whaling), social engineering, malware types (ransomware, trojan, rootkit, keylogger, worm, virus), SQL injection, cross-site scripting (XSS), man-in-the-middle attacks, password security (length vs complexity, password managers, 2FA types — SMS vs authenticator vs hardware key), VPN use cases and limitations, zero-day vulnerabilities, patch management importance, NIST cybersecurity framework, GDPR and data privacy basics'],
['id' => 'web_dev', 'category' => 'technology', 'topic' => 'Web development fundamentals',
'desc' => 'HTML semantic elements (header, nav, main, article, aside, footer), CSS box model (content/padding/border/margin), Flexbox vs CSS Grid layout, responsive design (media queries, mobile-first), JavaScript fundamentals (DOM manipulation, event listeners, async/await, fetch API, JSON), HTTP methods (GET/POST/PUT/DELETE/PATCH), REST API design principles, HTTP status codes (200/201/301/302/400/401/403/404/500), cookies vs localStorage vs sessionStorage, CORS, HTTPS and TLS/SSL certificates, web accessibility (WCAG guidelines, ARIA attributes), performance optimization (lazy loading, minification, CDN)'],
['id' => 'networking', 'category' => 'technology', 'topic' => 'Computer networking and protocols',
'desc' => 'OSI model (7 layers — Physical/Data Link/Network/Transport/Session/Presentation/Application), TCP vs UDP (reliability vs speed trade-off), TCP three-way handshake (SYN/SYN-ACK/ACK), IP addressing (IPv4 vs IPv6, CIDR notation, subnetting), private vs public IP addresses (RFC 1918), NAT and PAT, DNS resolution process (recursive vs iterative), DHCP lease process, ARP, routing protocols (OSPF, BGP), VLANs, firewalls (stateful vs stateless), network topologies, Wireshark packet analysis basics, common ports (22/SSH, 80/HTTP, 443/HTTPS, 53/DNS, 25/SMTP, 3306/MySQL)'],
['id' => 'cloud_tech', 'category' => 'technology', 'topic' => 'Cloud computing and modern infrastructure',
'desc' => 'IaaS vs PaaS vs SaaS differences with examples, public vs private vs hybrid cloud, major providers (AWS, Azure, GCP — key services), virtualization (hypervisors Type 1 vs Type 2, containers vs VMs), Docker (images, containers, Dockerfile, volumes, networking), Kubernetes concepts (pods, nodes, deployments, services, ingress), serverless computing (Lambda, Cloud Functions), microservices vs monolith architecture, DevOps principles (CI/CD pipelines, infrastructure as code — Terraform/Ansible), auto-scaling, load balancing, CDN mechanics, object storage vs block storage vs file storage'],
['id' => 'cs_concepts', 'category' => 'computer_science', 'topic' => 'Computer science fundamentals',
'desc' => 'data structures (arrays, linked lists, stacks, queues, hash tables, trees, graphs, heaps), Big O notation (O(1)/O(log n)/O(n)/O(n log n)/O(n²)), sorting algorithms (bubble, selection, insertion, merge, quick, heap — time/space complexity), searching (linear vs binary search), tree traversals (inorder/preorder/postorder, BFS vs DFS), hash table collision resolution (chaining vs open addressing), recursion and memoization, dynamic programming (overlapping subproblems, optimal substructure), greedy algorithms, NP-hard vs P problems, basic compiler theory (lexing, parsing, AST)'],
['id' => 'tx_hist2', 'category' => 'texas', 'topic' => 'Texas independence and the Republic era',
'desc' => 'Stephen F. Austin as Father of Texas, Mexican immigration terms and empresario land grants, Antonio López de Santa Anna\'s centralist policies that angered Texans, Gonzales \'Come and Take It\' cannon skirmish, siege and Battle of the Alamo (February-March 1836 — Bowie, Travis, Crockett, ~200 defenders vs ~2,000 Mexican troops), Goliad Massacre, Sam Houston\'s retreat and strategy, Battle of San Jacinto (18 minutes, \'Remember the Alamo!\'), Texas Declaration of Independence, Republic of Texas presidents (Burnet, Houston, Lamar, Jones), annexation debate and US entry December 1845'],
['id' => 'tx_culture2', 'category' => 'texas', 'topic' => 'Texas food, music, and traditions',
'desc' => 'BBQ regions: East Texas (smoky, tomato sauce), Central Texas (salt/pepper rub, oak-smoked brisket — Lockhart and Taylor), West Texas (direct heat), South Texas (mesquite), Tex-Mex origins (fajitas, puffy tacos, queso, breakfast tacos differ from Mexican cuisine), chili — Texas \'Bowl of Red\' (no beans), kolaches (Czech immigrant legacy, especially in Central Texas), Blue Bell ice cream, Dr Pepper (Waco 1885), Austin as live music capital (6th Street, ACL Fest, SXSW), Willie Nelson, Waylon Jennings, George Strait, Selena, Beyoncé (Houston), rodeo (HLSR largest in world)'],
['id' => 'tx_land', 'category' => 'texas', 'topic' => 'Texas land, law, and property',
'desc' => 'Texas land grant history and republic-era sovereignty over public lands (unique among states — state retains public land, not federal government), homestead exemption and its generosity in Texas, community property state laws, no state income tax (trade-off: higher property taxes), water law (prior appropriation vs riparian doctrine in Texas — Rule of Capture for groundwater), mineral rights vs surface rights separation, oil and gas leases (royalties, working interests), eminent domain and Texas Constitution Article I §17, deed restrictions in unincorporated areas, Texas Open Beaches Act'],
['id' => 'tx_economy2', 'category' => 'texas', 'topic' => 'Texas industries and economic drivers',
'desc' => 'Permian Basin and its resurgence (horizontal drilling and fracking), Texas Railroad Commission regulating oil and gas, refinery corridor along Gulf Coast (Houston Ship Channel), LNG exports from Freeport and Sabine Pass, Texas as top wind energy state (West Texas and Panhandle capacity), semiconductor manufacturing (Samsung Austin, TI Dallas), defense contractors (Lockheed Martin Fort Worth, Raytheon), Dell Technologies (Round Rock), Tesla Gigafactory (Austin), SpaceX Starbase (Boca Chica), healthcare sector (Texas Medical Center in Houston — largest medical complex in world), agricultural exports (cotton, beef, pecans, sorghum)'],
['id' => 'dfw_deep', 'category' => 'texas', 'topic' => 'DFW Metroplex — business, culture, and growth',
'desc' => 'DFW Airport as second busiest by operations in US, American Airlines headquarters (Fort Worth), Fort Worth Stockyards National Historic District (Billy Bob\'s Texas, nightly cattle drive, Cowtown history), Sundance Square entertainment district, Kimbell Art Museum (Kahn building), Modern Art Museum of Fort Worth, Fort Worth Zoo (consistently top-ranked), Dallas Arts District (largest urban arts district in US), AT&T Stadium (Jerry World — Cowboys), Globe Life Field (Rangers), American Airlines Center (Mavs/Stars), Toyota Music Factory, Perot Museum of Nature and Science, ongoing population growth (4th largest metro)'],
['id' => 'us_politics2', 'category' => 'national', 'topic' => 'US electoral system and political parties',
'desc' => 'Electoral College mechanics (538 total, 270 to win, winner-take-all in 48 states, Maine/Nebraska district method), faithless electors, 12th Amendment and tie-breaking by House, presidential primary system (caucuses vs primaries, superdelegates in Democratic Party), gerrymandering types (packing vs cracking), redistricting and census cycle, campaign finance law (FEC, super PACs post-Citizens United, dark money 501c4s, contribution limits), third parties and spoiler effect (Duverger\'s Law), swing states and Electoral College strategy, voter turnout patterns by demographic'],
['id' => 'us_social', 'category' => 'national', 'topic' => 'US social issues and culture wars',
'desc' => 'abortion debate: Roe v. Wade history, Casey v. Planned Parenthood undue burden standard, Dobbs decision and state-level landscape, viability and fetal pain debates, gun control: Second Amendment interpretation, AR-15 and assault weapons ban debate, background check gaps (gun show loophole), red flag laws, mass shooting frequency and response, immigration politics: border security vs humanitarian obligations, DACA recipients, asylum law, Title 42, remain in Mexico policy, transgender issues in sports and healthcare, DEI programs, affirmative action (SFFA v. Harvard decision 2023), drug legalization debate'],
['id' => 'us_media2', 'category' => 'national', 'topic' => 'US media landscape and information ecosystems',
'desc' => 'legacy media decline (newspaper closures, local news desert problem), cable news business model (outrage = ratings), Fox News vs MSNBC audience segmentation, social media news consumption, Twitter/X transformation under Musk, Facebook and political content algorithms, TikTok and national security debate (ByteDance, data collection concerns), YouTube and radicalization pathways, podcasting replacing radio, Substack and newsletter journalism, fact-checking organizations (PolitiFact, Snopes, FactCheck.org), media literacy skills for students, Section 230 debate, AI-generated news and deepfakes'],
['id' => 'world_climate', 'category' => 'world', 'topic' => 'Climate change — science, politics, and impacts',
'desc' => 'IPCC reports and scientific consensus, 1.5°C vs 2°C warming targets (Paris Agreement), tipping points (West Antarctic ice sheet, Amazon dieback, permafrost methane release, Atlantic circulation weakening), observed impacts already occurring (sea level rise rate, Arctic sea ice minimum records, coral bleaching frequency, wildfire seasons lengthening, extreme heat events), climate refugees projections, carbon budget remaining, carbon capture technologies (DAC, BECCS), solar geoengineering controversy (stratospheric aerosol injection), just transition for fossil fuel workers, climate justice and vulnerable nations'],
['id' => 'world_tech_race', 'category' => 'world', 'topic' => 'Global technology competition',
'desc' => 'US-China semiconductor war (CHIPS Act, export controls on advanced chips and chip-making equipment, ASML extreme UV lithography monopoly), 5G infrastructure competition (Huawei bans in Western countries), AI development race (OpenAI/Google vs Alibaba/Baidu/Tencent), quantum computing race (implications for encryption), rare earth minerals as geopolitical leverage (China controls ~60% of production), India\'s tech emergence (Bengaluru, UPI digital payments), Israeli startup ecosystem, data localization laws vs global internet, digital currency competition (e-CNY vs US dollar dominance)'],
['id' => 'africa2', 'category' => 'world', 'topic' => 'Africa — economic potential and challenges',
'desc' => 'African Continental Free Trade Area (AfCFTA) — 54 countries, world\'s largest free trade zone by countries, China\'s investment in Africa via BRI (roads, ports, hospitals — debt trap diplomacy concerns), Sahel security crisis (Mali, Burkina Faso, Niger coups 2021-2023, Wagner Group presence), East African tech scene (M-Pesa mobile money in Kenya, Nairobi\'s Silicon Savannah), Nigeria as largest African economy (oil dependency, currency devaluation), South Africa\'s load-shedding power crisis (Eskom), Ethiopia\'s Grand Renaissance Dam dispute with Egypt, demographic dividend (youngest population globally by 2050), brain drain challenge'],
['id' => 'mid_east2', 'category' => 'world', 'topic' => 'Middle East — religion, oil, and geopolitics',
'desc' => 'Sunni-Shia divide (historical roots — Karbala, Ali\'s succession), Iran as Shia theocracy (Revolutionary Guards, velayat-e faqih), Saudi Arabia as Sunni leadership (Wahhabism, MBS modernization and authoritarianism), proxy conflict map (Iran: Hezbollah Lebanon, Hamas Gaza, Houthis Yemen, Iraqi militias vs Saudi/UAE/US backing), Israel-Palestine conflict: 1948 Nakba, 1967 Six-Day War and occupation, Oslo Accords failure, two-state solution obstacles (settlements, Jerusalem status, right of return), October 7 2023 Hamas attack and Gaza war, Turkish neo-Ottoman ambitions, Qatar gas wealth and Al Jazeera influence'],
['id' => 'sex_psychology', 'category' => 'sexuality', 'topic' => 'Psychology of sexuality and attraction',
'desc' => 'sexual orientation formation theories (biological: fraternal birth order effect, finger length ratio, twin studies; psychological: Kinsey scale, sexual fluidity), attraction science (pheromones debate, symmetry preference, waist-to-hip ratio, halo effect), love triangles (Sternberg: intimacy+passion+commitment), attachment theory in romantic relationships (secure, anxious, avoidant, disorganized styles), jealousy evolutionary theories, sexual fantasy prevalence studies (Joyal research), paraphilias vs paraphilic disorders (DSM-5 distinction), sexual addiction controversy (not in DSM-5), intersex conditions (prevalence ~1.7%, different from trans identity)'],
['id' => 'sex_health2', 'category' => 'sexuality', 'topic' => 'Comprehensive sexual health across the lifespan',
'desc' => 'adolescent sexual development (Tanner stages, first menstruation average age, nocturnal emissions, masturbation normalization), college sexual health (consent education, STI rates in 18-24 age group, hookup culture research), adult sexual health (frequency normalization, \'use it or lose it\' evidence for aging), postpartum sexuality (recovery timeline, breastfeeding and libido, pelvic floor recovery), menopause and sexual changes (vaginal atrophy, GSM—genitourinary syndrome, lubricants, local estrogen, ospemifene), male aging and sexual health (testosterone decline, ED prevalence by decade, PDE5 inhibitors: sildenafil vs tadalafil), older adult sexuality (cognitive decline and consent complexities)'],
['id' => 'astro_planets', 'category' => 'astronomy', 'topic' => 'Planetary science — geology, atmospheres, and moons',
'desc' => 'Mercury: no atmosphere, extreme temperature swings (-180 to 430°C), MESSENGER/BepiColombo missions; Venus: runaway greenhouse effect (464°C), retrograde rotation, sulfuric acid clouds, Magellan radar mapping; Mars: Olympus Mons largest volcano, Valles Marineris, thin CO2 atmosphere, evidence of ancient liquid water, seasonal dust storms, polar ice caps (CO2+H2O); Jupiter: Great Red Spot (shrinking storm), differential rotation, magnetosphere, ring system; Saturn: ring composition (97% water ice), ring gaps (Cassini Division), Titan\'s methane cycle; Uranus/Neptune: ice giants, Uranus axial tilt 98°, Neptune\'s winds 2100 km/h'],
['id' => 'astro_stellar2', 'category' => 'astronomy', 'topic' => 'Stellar astrophysics in depth',
'desc' => 'stellar nucleosynthesis stages (hydrogen burning, helium flash, CNO cycle in massive stars, triple-alpha process, s-process vs r-process for heavy elements), stellar classification (OBAFGKM spectral types, temperature ranges, color correlation), luminosity classes (I supergiant to V main sequence), variable stars (Cepheid period-luminosity relation used as standard candles, RR Lyrae), X-ray binaries (matter transfer, accretion disk), novae vs supernovae (white dwarf thermonuclear vs core collapse), pulsar timing precision (millisecond pulsars as gravitational wave detectors), magnetar flares and fast radio bursts'],
['id' => 'astro_cosmo', 'category' => 'astronomy', 'topic' => 'Observational cosmology and structure of the universe',
'desc' => 'cosmic distance ladder (stellar parallax → Cepheid variables → Type Ia supernovae → Hubble\'s Law), Hubble constant value dispute (H0 tension: CMB measurements ~67 vs local measurements ~73 km/s/Mpc), large-scale structure (filaments, voids, galaxy clusters, superclusters — Laniakea), cosmic web formation (dark matter halos as seeds), cosmic inflation evidence (flatness problem, horizon problem, monopole problem — all solved by inflation), baryon acoustic oscillations as standard ruler, gravitational lensing as mass probe (Einstein rings, cluster lensing maps of dark matter)'],
['id' => 'space_tech', 'category' => 'space', 'topic' => 'Spacecraft systems and engineering',
'desc' => 'thermal control systems (passive: coatings, MLI blankets; active: heat pipes, louvers, heaters), attitude control (reaction wheels, thrusters, star trackers, gyroscopes), power systems (solar panels — degradation rate in radiation, RTGs for outer planets: Pu-238), communication links (deep space network, high-gain vs low-gain antennas, signal delay to Mars: 3-22 minutes), propulsion types (chemical bipropellant: hypergolic vs cryogenic; electric: Hall thrusters, ion drives Isp comparison), radiation shielding approaches (water, polyethylene, depth of soil on Moon/Mars), autonomous navigation (optical navigation, terrain-relative navigation used by Perseverance landing)'],
['id' => 'space_future2', 'category' => 'space', 'topic' => 'Human spaceflight beyond Earth orbit',
'desc' => 'Mars transit timeline (6-9 month journey, radiation dose accumulation, vehicle shielding options), Mars surface challenges (gravity 38% of Earth, atmospheric pressure 0.6% of Earth, perchlorates in soil, dust storm seasons), ISRU (in-situ resource utilization): Martian CO2+H2O→CH4+O2 propellant (MOXIE experiment on Perseverance), extracting water ice at poles, 3D-printed regolith habitats, psychological factors (isolation, confined quarters, communication delay with Earth — delay means no real-time guidance), Mars One failure lessons, NASA Moon-to-Mars architecture, SpaceX Starship reusability economics for Mars'],
['id' => 'music_theory', 'category' => 'arts', 'topic' => 'Music theory and appreciation',
'desc' => 'staff notation (treble/bass clef, ledger lines, note values), time signatures (4/4, 3/4, 6/8, 5/4 odd meters), key signatures and circle of fifths, major vs minor scales and their emotional qualities, modes (Dorian, Phrygian, Lydian, Mixolydian, Aeolian, Locrian), chord construction (triads: major/minor/diminished/augmented; seventh chords: maj7, dom7, min7), chord progressions (I-IV-V-I, ii-V-I in jazz, 12-bar blues, Andalusian cadence), harmony and counterpoint, musical forms (sonata form, rondo, theme and variations, fugue), Western classical periods (Baroque, Classical, Romantic, Modern) with key composers'],
['id' => 'film_study', 'category' => 'arts', 'topic' => 'Film analysis and cinema history',
'desc' => 'film language: mise-en-scène (lighting, set design, costume, actor positioning), cinematography (camera angles — low/high/Dutch tilt, shots — extreme wide/wide/medium/close-up/ECU, camera movement — pan/tilt/tracking/dolly/steadicam), editing (continuity editing, jump cut, cross-cutting, montage — Eisenstein\'s theory, match cut), sound design (diegetic vs non-diegetic sound, Foley, score vs soundtrack), film movements (German Expressionism, Italian Neorealism, French New Wave, New Hollywood, Dogme 95), auteur theory, genre conventions (film noir, western, horror subgenres), three-act structure vs alternative narrative structures'],
['id' => 'visual_art', 'category' => 'arts', 'topic' => 'Visual art — history, movements, and techniques',
'desc' => 'prehistoric cave paintings (Lascaux, Chauvet — ochre and charcoal techniques), Egyptian art conventions (profile face, frontal eye, hierarchical scale), Greek sculpture evolution (Archaic smile → Classical contrapposto → Hellenistic drama), Renaissance techniques (chiaroscuro, sfumato, linear perspective — Brunelleschi\'s discovery), Baroque drama (Caravaggio\'s tenebrism), Impressionism (capturing light and movement — Monet water lilies, Renoir), Post-Impressionism (Van Gogh\'s brushwork, Cézanne\'s geometry as path to Cubism), Cubism (Picasso, Braque — multiple viewpoints), Abstract Expressionism (Pollock drip technique, Rothko color fields), Pop Art (Warhol, Lichtenstein), Contemporary art market and NFTs'],
['id' => 'philosophy2', 'category' => 'philosophy', 'topic' => 'Philosophy — branches and major thinkers',
'desc' => 'epistemology: Plato\'s Forms and cave allegory, Descartes\' cogito and methodological doubt, empiricism (Locke, Berkeley, Hume — tabula rasa, esse est percipi, problem of induction), Kant\'s synthetic a priori, Gettier problem and justified true belief, ethics: Kantian categorical imperative (two formulations), Mill\'s utilitarianism and harm principle, Rawls\' veil of ignorance and difference principle, virtue ethics (Aristotle\'s eudaimonia, four cardinal virtues), care ethics (Gilligan), metaethics (moral realism vs anti-realism), political philosophy: Hobbes\' Leviathan, Locke\'s natural rights, Rousseau\'s social contract, Nozick\'s libertarianism vs Rawls\' liberal egalitarianism, existentialism (Sartre: existence precedes essence, bad faith, Beauvoir, Camus\' absurdism)'],
['id' => 'world_religion2', 'category' => 'religion', 'topic' => 'Comparative religion and philosophy of religion',
'desc' => 'Hinduism: Brahman and Atman, four goals of life (dharma/artha/kama/moksha), four paths to moksha (jnana/bhakti/karma/raja yoga), major deities (Brahma/Vishnu/Shiva trinity, avatars of Vishnu, Devi), caste system history and discrimination, major texts (Vedas, Upanishads, Bhagavad Gita, Mahabharata, Ramayana), Buddhism: Four Noble Truths, Eightfold Path, Theravada vs Mahayana vs Vajrayana, bodhisattva concept, Zen and meditation, nirvana vs nibbana, Islam: Five Pillars, six articles of faith, Quran revelation to Muhammad, Sunni vs Shia split (historical cause), Hadith and Sharia, Judaism: Torah, Talmud, 13 principles of faith (Maimonides), denominations (Orthodox/Conservative/Reform/Reconstructionist), philosophy of religion (cosmological, ontological, teleological arguments for God; problem of evil)'],
['id' => 'mythology2', 'category' => 'culture', 'topic' => 'Mythology deep dive — creation myths and heroes',
'desc' => 'Greek creation: Chaos → Gaia → Titans → Olympians, Titanomachy, Gigantomachy; hero cycle (monomyth per Joseph Campbell — call, threshold, trials, death/rebirth, return); Perseus (Gorgon, Pegasus, Andromeda), Heracles 12 labors in detail, Odyssey themes (nostos, temptation, identity), Orpheus and Eurydice (looking back as metaphor), Norse: Yggdrasil world tree, nine realms, Ragnarök prophecy, Odin\'s sacrifices for wisdom, Loki as trickster, Egyptian: Ma\'at and cosmic order, Osiris-Set-Horus myth as prototype for dying-rising god, Thoth as wisdom deity, Aztec: five suns creation, Quetzalcoatl feathered serpent, Tlaloc rain god, Japanese: Izanagi/Izanami, Amaterasu in cave, Susanoo storm god, Hindu epics as mythology (Ramayana, Mahabharata)'],
['id' => 'sports_history', 'category' => 'sports', 'topic' => 'Sports history and cultural impact',
'desc' => 'Olympic Games history (ancient Greek Olympics 776 BCE, revival 1896 Athens, Jesse Owens 1936 Berlin, 1968 Mexico City Black Power salute, Munich massacre 1972, political boycotts 1980/1984), integration of professional sports (Jackie Robinson breaking MLB color barrier 1947, early NBA Black players, Althea Gibson and Arthur Ashe in tennis, Billie Jean King vs Bobby Riggs \'Battle of Sexes\'), Muhammad Ali\'s cultural impact (Cassius Clay, Vietnam draft refusal, \'Float like a butterfly\'), Title IX impact on women\'s sports, CTE and NFL concussion crisis, PED era in baseball (McGwire, Bonds, Mitchell Report), Lance Armstrong scandal, doping culture in cycling and track'],
['id' => 'auto_cars', 'category' => 'transportation', 'topic' => 'Automobiles — mechanics, history, and culture',
'desc' => 'internal combustion engine four-stroke cycle (intake/compression/power/exhaust), engine configurations (inline-4, V6, V8, flat/boxer), transmission types (manual clutch/gear system, automatic torque converter, CVT, dual-clutch), braking systems (disc vs drum, ABS operation, brake fade), suspension types (MacPherson strut, double wishbone, air suspension), turbocharging vs supercharging, EV drivetrain (battery pack, single-speed transmission, regenerative braking), charging standards (CCS, CHAdeMO, Tesla NACS becoming standard), range anxiety and charging infrastructure, autonomous vehicle SAE levels 0-5, car insurance types (liability/collision/comprehensive), VIN decoding'],
['id' => 'business_101', 'category' => 'business', 'topic' => 'Business fundamentals and entrepreneurship',
'desc' => 'business entity types (sole proprietorship — unlimited liability, partnership — general vs limited, LLC — operating agreement, corporation — C-corp double taxation vs S-corp pass-through, nonprofit 501c3), business plan components (executive summary, market analysis, competitive analysis, operations plan, financial projections), startup funding stages (bootstrapping, friends/family, angel investors typical check $25k-$500k, seed round, Series A/B/C, venture capital structure, IPO process), business model types (subscription, marketplace, freemium, SaaS, franchise), lean startup methodology (MVP, build-measure-learn loop, pivot), cash flow vs profit distinction (can be profitable but insolvent)'],
['id' => 'marketing', 'category' => 'business', 'topic' => 'Marketing and consumer psychology',
'desc' => '4Ps of marketing (product, price, place, promotion), STP framework (segmentation, targeting, positioning), customer personas, buyer\'s journey (awareness/consideration/decision), AIDA model (attention/interest/desire/action), brand equity and brand architecture, pricing strategies (cost-plus, value-based, penetration, skimming, psychological pricing — $9.99 effect), distribution channels (direct vs indirect, omnichannel), content marketing vs advertising, SEO basics (on-page vs off-page, E-E-A-T), social media algorithms, influencer marketing ROI, customer lifetime value (CLV) vs customer acquisition cost (CAC), Net Promoter Score'],
['id' => 'real_estate', 'category' => 'business', 'topic' => 'Real estate investing and the housing market',
'desc' => 'housing market fundamentals (supply/demand, affordability index, months of supply), mortgage types (conventional vs FHA vs VA vs USDA, fixed vs adjustable rate, 15 vs 30 year), mortgage process (pre-qualification vs pre-approval, underwriting, closing costs ~2-5% of loan), real estate investment types (rental properties — gross rent multiplier, cap rate calculation, cash-on-cash return; REITs — publicly traded vs private, dividend yields; house flipping — 70% rule; vacation rentals — Airbnb regulations), 1031 exchange tax deferral, depreciation deduction, home equity and HELOCs, property management basics, foreclosure process'],
['id' => 'law_criminal', 'category' => 'law', 'topic' => 'Criminal law and the justice system',
'desc' => 'elements of a crime (actus reus + mens rea + causation + concurrence), felony vs misdemeanor vs infraction, crime categories (property, violent, white-collar, victimless, organized), arrest and booking process, Miranda rights (when required and what they are), arraignment and initial appearance, bail determination factors, grand jury vs preliminary hearing, discovery process, plea bargaining (why 97% of federal cases), trial phases (jury selection/voir dire, opening statements, direct/cross examination, closing arguments, jury deliberation, verdict), sentencing guidelines (mandatory minimums, three-strikes laws), appeals process, habeas corpus'],
['id' => 'law_civil', 'category' => 'law', 'topic' => 'Civil law — torts, contracts, and family law',
'desc' => 'elements of a tort: duty, breach, causation, damages; intentional torts (battery, assault, false imprisonment, trespass, conversion, defamation — libel vs slander); negligence standard (reasonable person), contributory vs comparative negligence, strict liability (products liability, abnormally dangerous activities), contract elements (offer, acceptance, consideration, capacity, legality), contract defenses (fraud, duress, undue influence, mistake, impossibility), breach remedies (compensatory, consequential, liquidated, punitive damages, rescission, specific performance), family law (divorce types — fault vs no-fault, property division community vs equitable distribution, custody types — legal vs physical, modification standards, child support calculation methods)'],
['id' => 'environ_energy', 'category' => 'environment', 'topic' => 'Energy systems and the clean energy transition',
'desc' => 'energy units (joules, BTUs, kWh, MMBTU), energy density comparison (gasoline vs lithium-ion vs hydrogen), electricity generation mix by source (coal, natural gas, nuclear, hydro, wind, solar — US and global percentages), solar PV technology (monocrystalline vs polycrystalline vs thin-film, efficiency rates, capacity factor ~20% vs wind ~35%), offshore vs onshore wind trade-offs, battery storage (lithium-ion chemistry, grid-scale applications, pumped hydro as largest storage), nuclear power (fission vs fusion, PWR vs BWR reactor types, Chernobyl and Fukushima causes, small modular reactors, waste storage problem), green hydrogen production (electrolysis using renewable electricity), energy poverty globally'],
['id' => 'wildlife_bio', 'category' => 'environment', 'topic' => 'Wildlife biology and conservation',
'desc' => 'population viability analysis, minimum viable population size, extinction vortex, IUCN Red List categories (Extinct/Critically Endangered/Endangered/Vulnerable/Near Threatened/Least Concern), biodiversity hotspots (defined as >1,500 endemic plant species and lost >70% habitat — examples: Amazon, Madagascar, California Floristic Province), rewilding concepts (keystone species reintroduction — wolves in Yellowstone trophic cascade), CITES treaty and wildlife trafficking, poaching economics and anti-poaching technology, captive breeding programs (California condor, Arabian oryx, black-footed ferret), habitat corridors, climate change as extinction driver'],
['id' => 'psych_social', 'category' => 'psychology', 'topic' => 'Social psychology and group behavior',
'desc' => 'Milgram obedience experiment and lessons about authority, Stanford Prison Experiment and situationism (Zimbardo), Asch conformity experiments and social pressure, bystander effect and diffusion of responsibility, groupthink (symptoms: illusion of invulnerability, collective rationalization, stereotyping out-groups, pressure on dissenters, self-censorship), in-group vs out-group bias and minimal group paradigm, social identity theory (Tajfel and Turner), cognitive dissonance reduction strategies, prejudice vs stereotyping vs discrimination, contact hypothesis for reducing prejudice, social facilitation vs social loafing'],
['id' => 'psych_dev', 'category' => 'psychology', 'topic' => 'Developmental psychology across the lifespan',
'desc' => 'prenatal development stages (germinal/embryonic/fetal), teratogens and critical periods, infant attachment (Harlow\'s monkeys, Ainsworth\'s Strange Situation — secure/anxious/avoidant/disorganized), Piaget\'s four stages in detail (sensorimotor: object permanence; preoperational: egocentrism, animism, conservation failure; concrete operational: seriation; formal operational: abstract reasoning), Vygotsky\'s ZPD and scaffolding, theory of mind (autism spectrum connection), Erikson\'s 8 stages detailed (trust vs mistrust through integrity vs despair), identity formation in adolescence (Marcia\'s statuses), Kohlberg\'s moral stages, midlife crisis research (Levinson), late adulthood (wisdom, successful aging theories)'],
['id' => 'psych_cog', 'category' => 'psychology', 'topic' => 'Cognitive psychology — memory, attention, and thinking',
'desc' => 'Atkinson-Shiffrin memory model (sensory/short-term/long-term), working memory model (Baddeley: phonological loop, visuospatial sketchpad, central executive, episodic buffer), encoding specificity principle, elaborative rehearsal vs rote rehearsal, long-term memory types (explicit: semantic vs episodic; implicit: procedural, priming, conditioning), forgetting theories (decay, interference — proactive vs retroactive, motivated forgetting/repression), schemas and their role in comprehension and memory distortion, flashbulb memories and their reliability, false memory research (Loftus misinformation effect), dual-process theory (System 1 vs System 2), attention (selective, divided, sustained), inattentional blindness'],
['id' => 'medicine_basics', 'category' => 'health', 'topic' => 'Medical terminology and healthcare navigation',
'desc' => 'anatomy directional terms (anterior/posterior, superior/inferior, medial/lateral, proximal/distal, dorsal/ventral), body planes (sagittal, frontal/coronal, transverse), organ systems overview, vital signs (normal ranges for HR, BP, RR, temp, O2 sat), common lab values (CBC — RBC/WBC/platelets, CMP — glucose/creatinine/electrolytes, lipid panel, A1C, TSH), medical abbreviations (PRN, QD, BID, TID, QID, STAT, NPO), types of doctors (MD vs DO, primary care vs specialists), insurance terms (deductible, copay, coinsurance, out-of-pocket max, in-network vs out-of-network, formulary, prior authorization), patient rights (HIPAA, informed consent, advance directives, POLST)'],
['id' => 'pharmacology', 'category' => 'health', 'topic' => 'Pharmacology and medication safety',
'desc' => 'pharmacokinetics (ADME: absorption routes—oral bioavailability, first-pass effect; distribution—volume of distribution, blood-brain barrier; metabolism—CYP450 enzymes and drug interactions; elimination—half-life, renal vs hepatic clearance), pharmacodynamics (receptor agonist vs antagonist, dose-response curves, ED50 vs LD50, therapeutic window), drug interaction mechanisms (induction vs inhibition of CYP450), medication classes with mechanisms: beta-blockers, ACE inhibitors, statins, SSRIs, proton pump inhibitors, NSAIDs (GI and renal risks), opioid analgesics (mu receptor, constipation mechanism), antibiotics (bactericidal vs bacteriostatic, mechanisms of classes), vaccine immunology (live-attenuated vs inactivated vs subunit vs mRNA)'],
['id' => 'first_aid2', 'category' => 'health', 'topic' => 'Emergency medicine and first aid advanced',
'desc' => 'adult CPR sequence (check scene/unresponsive/call 911/30 compressions at 2 inches depth at 100-120/min/2 rescue breaths, AED when available), infant vs child vs adult CPR differences, choking adult Heimlich (5 back blows/5 abdominal thrusts) vs infant (5 back blows/5 chest thrusts), stroke recognition FAST (Face drooping, Arm weakness, Speech difficulty, Time to call 911), heart attack recognition (chest pressure radiating to jaw/arm, diaphoresis, nausea), hypoglycemia vs hyperglycemia recognition and response, anaphylaxis epipen technique (outer thigh, hold 10 seconds, call 911), tourniquet application (2 inches above wound, windlass until bleeding stops, note time), wound care (direct pressure, elevation, when to use tourniquet), burn classification and treatment (cool running water 20 min for minor, no ice, cover with clean cloth, hospital for 2nd/3rd degree)'],
['id' => 'prep_emergency', 'category' => 'safety', 'topic' => 'Emergency preparedness and disaster response',
'desc' => '72-hour kit vs full emergency supply list (water: 1 gallon/person/day for 2 weeks, food: non-perishables with 25-year shelf life, manual can opener, first aid kit, medications 30-day supply, important documents in waterproof container, cash in small bills, battery/solar/crank radio, flashlights and extra batteries, multi-tool, phone charger), shelter-in-place vs evacuation decision, FEMA\'s Ready.gov resources, community emergency response team (CERT) training, earthquake protocol (Drop-Cover-Hold On, stay indoors, don\'t run outside), tornado shelter (lowest floor, interior room, away from windows, bathtub with mattress over you), hurricane evacuation timing, flood never drive through water, wildfire defensible space and go-bag'],
['id' => 'auto_maint', 'category' => 'transportation', 'topic' => 'Vehicle maintenance and troubleshooting',
'desc' => 'oil change intervals (conventional every 3k-5k miles vs synthetic every 7.5k-10k miles), how to check oil level and color (black=dirty, milky=coolant leak, low=burn or leak), tire pressure and TPMS (proper inflation improves fuel economy 0.5-3%), tire rotation every 5k-7.5k miles and why, brake pad wear indicators (squeal vs grind), battery testing (CCA rating, 3-5 year lifespan, terminal corrosion cleaning), coolant system (50/50 mix, when to flush, overheating response — pull over, don\'t open cap hot), air filter replacement interval, transmission fluid check and change, serpentine belt inspection, warning lights meanings (check engine, oil pressure, battery, coolant temp, ABS), jump-starting procedure (red to positive then to positive, black to negative then to chassis ground)'],
['id' => 'nfl_football', 'category' => 'sports', 'topic' => 'American football rules and strategy',
'desc' => 'down-and-distance system, scoring (TD 6+PAT/2pt, FG 3, safety 2), offensive formations (shotgun, I-formation, spread, pistol), route trees (slant, curl, post, corner, go, cross), defensive schemes (4-3 vs 3-4, Cover 2/3/4, Tampa 2, zone vs man, blitz packages), clock management (two-minute drill, quarterback kneel, icing kicker), salary cap mechanics, franchise tag, NFL draft combine, Super Bowl history and records, CTE research and rule changes, pass interference vs defensive holding distinction, illegal contact, roughing the passer evolution'],
['id' => 'nba_basketball', 'category' => 'sports', 'topic' => 'Basketball rules, strategy, and analytics',
'desc' => 'shot clock (24s NBA), foul types (personal/flagrant 1 and 2/technical/intentional), offensive systems (triangle, Princeton, pace-and-space), pick-and-roll coverage schemes (drop/hedge/switch/ICE), zone defenses (2-3/1-3-1), intentional fouling late-game strategy, advanced stats (PER, True Shooting%, BPM, VORP, RAPTOR, on-off net rating), three-point revolution history (Curry effect, corner three value), position-less basketball trend, NBA draft lottery odds, salary cap and max contracts, Olympics basketball Dream Team history, global player pipeline'],
['id' => 'mlb_baseball', 'category' => 'sports', 'topic' => 'Baseball rules, analytics, and history',
'desc' => 'nine-inning structure, universal DH (2022), batting order philosophy (leadoff OBP, 3-4-5 heart, platoon splits), pitch types (four-seam, two-seam/sinker, cutter, slider, curveball 12-to-6 vs 11-to-5, changeup — circle/palm/vulcan), Statcast metrics (exit velocity, launch angle, spin rate, expected batting average), shift ban (2023), opener and bulk reliever strategy, baseball WAR components, steroid era and Mitchell Report, Negro Leagues history, integration (Jackie Robinson 1947), farm system and prospect pipeline, international signing rules, umpire evaluation system'],
['id' => 'soccer_rules', 'category' => 'sports', 'topic' => 'Soccer tactics and world football culture',
'desc' => 'offside law nuance (attacker interfering with play at moment of pass), advantage clause, VAR review criteria (clear and obvious error, factual/subjective matters), tactical evolution (4-4-2 to 4-2-3-1 dominance to 4-3-3/3-5-2), pressing intensity metrics (PPDA), xG (expected goals) and xA (expected assists), Opta and StatsBomb data, UEFA Champions League format, FIFA World Cup 2026 expansion to 48 teams and US/Canada/Mexico hosting, women\'s game growth (NWSL, WSL, European investment), player valuation and transfer windows, Financial Fair Play rules, South American football culture (ultras, Copa Libertadores)'],
['id' => 'combat_sports', 'category' => 'sports', 'topic' => 'MMA, boxing, and wrestling',
'desc' => 'UFC weight classes (115 strawweight through 265 heavyweight), MMA scoring criteria (effective striking, effective grappling, aggression, octagon control), striking arts (boxing combinations, muay thai — elbows/knees/clinch, kickboxing leg kicks), grappling foundations (wrestling: single leg/double leg/trip; BJJ: guard positions — closed/open/half/butterfly/spider/lasso, submissions — rear naked choke/triangle choke/armbar/heel hook), boxing 10-must scoring system, pound-for-pound rankings methodology, historical champions by era (Ali/Foreman/Frazier; Tyson era; Mayweather defensive mastery; Khabib grappling dominance; Jon Jones elite all-around), WADA testing in combat sports'],
['id' => 'golf_deep', 'category' => 'sports', 'topic' => 'Golf — technique, rules, and strategy',
'desc' => 'club fitting basics (shaft flex, loft, lie angle), shot shapes (draw — right-to-left for right-hander; fade — left-to-right; hook/slice as exaggerated versions), course management (playing to your miss, laying up vs going for it risk-reward, reading greens — grain, slope, speed), handicap index calculation (lowest 8 of last 20 differentials), Stableford vs stroke play vs match play formats, shotgun starts vs wave starts, PGA Tour mechanics (FedEx Cup points, top-125 exempt status, LIV Golf disruption and merger), major championships prestige ranking debate, Augusta National history (Masters traditions — green jacket, pimento cheese, Par 3 contest), equipment regulations (groove restrictions, MOI limits, anchored putting ban)'],
['id' => 'esports_deep', 'category' => 'sports', 'topic' => 'Esports industry and competitive gaming',
'desc' => 'esports revenue streams (~$1.8B global: media rights, sponsorship, mergers/acquisitions, merchandise, tickets), title-specific ecosystems (League of Legends: Riot Games structure, LCS/LEC/LCK/LPL regional leagues, Worlds format; CS:GO/CS2: Valve Major system, third-party ESL/BLAST tournaments; Dota 2: Valve Pro Circuit, TI $40M+ prize pool; Valorant: franchised VCT; Fortnite: FNCS open qualifiers), team organizational structure (players, head coach, analyst, performance coach, psychologist), streaming as career path (Twitch rev share, YouTube Gaming, Kick), burnout research (wrist/hand injuries, eye strain, isolation), Korean developmental structure influence, Chinese investment in esports'],
['id' => 'tennis_deep', 'category' => 'sports', 'topic' => 'Tennis technique, rules, and tour',
'desc' => 'scoring system (15/30/40/deuce/advantage/game; 6 games = set, tiebreak at 6-6 except Wimbledon final set; 3 or 5 sets depending on tournament), serve motion (toss placement, trophy position, pronation, kick serve vs flat vs slice), return of serve positioning and split-step timing, rally tactics (crosscourt percentages vs down-the-line risk, approach shot selection, net approaches and volley technique), surface differences (clay: high bounce/slow suits baseline grinders; grass: low bounce/fast suits serve-volleyers; hard: medium and varied by court), grand slam records (Djokovic 24, Nadal 22 Roland Garros dominance, Federer grass mastery, Serena Williams 23 Open Era), Davis Cup and Billie Jean King Cup team competition, ATP/WTA ranking point system'],
['id' => 'world_cuisines2', 'category' => 'cooking', 'topic' => 'Global cuisine exploration',
'desc' => 'French classical mother sauces (béchamel/velouté/espagnole/hollandaise/tomat — derivatives of each), Italian regional variation (North: risotto Milanese/ossobuco/pesto Genovese/carbonara egg-only rule; South: pizza Napoletana DOC rules, eggplant parmigiana), Japanese knife skills (santoku vs yanagiba vs deba purposes, honbazuke sharpening on water stone), Indian spice blooming in ghee (whole spices first, ground second), mole negro complexity (30+ ingredients, multiple chili types, chocolate without sweetness), Ethiopian injera fermentation (teff flour, 3-day ferment, communal mesob serving), Peruvian cuisine rise (ceviche — leche de tigre curing; lomo saltado — Chinese-Peruvian fusion; causa; anticuchos)'],
['id' => 'whiskey_deep', 'category' => 'cooking', 'topic' => 'Whiskey — bourbon, Scotch, and world whisky',
'desc' => 'bourbon legal requirements (51%+ corn mash bill, new charred oak barrels only, distilled to no more than 160 proof, barreled at no more than 125 proof, bottled at minimum 80 proof, made in USA — Kentucky not legally required), straight bourbon (minimum 2 years, no added color/flavor/blending), wheated bourbon (substituting wheat for rye — Pappy, Maker\'s Mark softer profile), high-rye bourbon (spicier — Four Roses, Bulleit), Tennessee whiskey difference (Lincoln County Process — charcoal filtering before aging, Jack Daniel\'s and George Dickel), Scotch regions (Speyside: fruit-forward Glenfarclas/Macallan; Islay: peaty phenolic — Laphroaig/Ardbeg/Lagavulin, measured in PPM; Highland: diverse; Lowland: light triple-distilled; Campbeltown: briny), Irish whiskey (triple distillation lightness, pot still style unique to Ireland), Japanese whisky (Yamazaki/Hakushu/Nikka, blending craftsmanship, shortage crisis)'],
['id' => 'cocktails2', 'category' => 'cooking', 'topic' => 'Classic cocktails and home bartending',
'desc' => 'essential home bar setup (bourbon/rye, gin, rum, tequila/mezcal, vodka, triple sec/Cointreau, sweet vermouth, dry vermouth, Campari/Aperol, Angostura bitters, Peychaud\'s, simple syrup, citrus), technique (muddling — gentle pressure for herbs/fruit, vigorous for harder produce; shaking — with ice 10-15 seconds dilutes and chills, use for citrus/egg white drinks; stirring — 30-40 rotations for spirit-only drinks, keeps clear; straining — Hawthorne vs julep vs fine mesh double strain; fat-washing fats with spirits then freezing), seasonal batched cocktails for entertaining, Prohibition era cocktail history (why sours developed — masking bad spirits), Tiki culture and Trader Vic\'s origin, Negroni variations (Boulevardier substitutes bourbon, Americano adds soda)'],
['id' => 'fermentation', 'category' => 'cooking', 'topic' => 'Fermentation — science and practice',
'desc' => 'fermentation categories (lacto-fermentation: vegetables using Lactobacillus in salt brine — kimchi, sauerkraut, pickles, no vinegar; alcohol fermentation: yeast converts sugars to ethanol and CO2; acetic acid fermentation: bacteria converts ethanol to acetic acid — vinegar and kombucha second ferment; miso/soy sauce: koji mold Aspergillus oryzae enzymatic breakdown; cheese: bacterial acidification plus rennet coagulation), sourdough starter maintenance (hydration ratio, feeding schedule, float test for readiness, rye acceleration), kimchi troubleshooting (brine ratio 2-3% by weight, temperature affects speed, white film kahm yeast vs mold identification), kombucha SCOBY care, water kefir vs milk kefir differences, mead making basics (honey ratio for dry vs sweet, yeast nutrients, degassing)'],
['id' => 'home_diy2', 'category' => 'home', 'topic' => 'DIY home repairs and upgrades',
'desc' => 'toilet repair (flapper replacement — check seat type before buying, fill valve replacement, running toilet diagnosis — food coloring dye test for flapper leak, float adjustment for fill height), faucet repair (cartridge vs ball vs ceramic disc types, shutoff valve location, handle removal varies by manufacturer, seat wrench for older faucets), garbage disposal reset and unjamming (Allen wrench hex key in bottom, reset button on bottom, never hands inside), light fixture replacement (shut off breaker, verify with non-contact tester, wire matching — black-to-black/white-to-white/bare-to-bare, using wire nuts properly), installing dimmer switches (load type compatibility — LED vs incandescent dimmers), weatherstripping types (V-strip for sides, door sweep for bottom, foam tape vs felt durability)'],
['id' => 'declutter', 'category' => 'home', 'topic' => 'Organization, minimalism, and home systems',
'desc' => 'KonMari method (category order: clothing/books/papers/komono/sentimental, joy-testing, vertical folding), Swedish death cleaning concept (Margareta Magnusson — organizing for those who will sort through your things), one-in-one-out rule, digital decluttering (photo backup systems — 3-2-1 rule: 3 copies/2 media types/1 offsite, unsubscribe vs filter vs folder email management, password manager setup), paper management system (inbox/pending/action/archive, scanning to PDF, what to keep originals — deed/title/Social Security card/birth certificate), garage organization zones (seasonal rotation, ceiling storage, pegboard for tools), closet organization principles (group by category, color, frequency of use, double hang for short items), storage unit decision framework (annual cost vs item replacement cost)'],
['id' => 'personal_style', 'category' => 'home', 'topic' => 'Personal style and wardrobe building',
'desc' => 'capsule wardrobe concept (30-37 items per season — Courtney Carver Project 333), color palette identification (warm vs cool undertones — vein color/silver-gold jewelry test, skin tone descriptors: fair/light/medium/olive/tan/deep), body shape dressing (inverted triangle: add volume below; pear: structured shoulders/A-line; rectangle: create curves with belts/peplum; hourglass: emphasize waist; apple: empire waist/A-line), fabric quality indicators (thread count for cotton, S-number for wool, denier for synthetics), suit fit checkpoints (shoulder seam, jacket length, trouser break), dress code interpretation (black tie/creative black tie/cocktail/business formal/business casual/smart casual/casual), sustainable fashion metrics (cost-per-wear calculation, natural fiber benefits vs synthetic performance)'],
['id' => 'meditation', 'category' => 'wellness', 'topic' => 'Meditation and mindfulness practices',
'desc' => 'types of meditation (focused attention: breath as anchor, wandering mind recognition and return without judgment; open monitoring: choiceless awareness of all arising phenomena; loving-kindness/metta: generating warmth toward self/loved ones/neutral people/difficult people/all beings; body scan: progressive attention from feet to head; NSDR/yoga nidra: non-sleep deep rest protocol; mantra-based: TM uses personalized mantra, Zen counting breaths, Buddhist chanting), neuroscience of meditation (default mode network quieting in experienced meditators, amygdala reactivity reduction, cortical thickening in attention areas per Sara Lazar Harvard study), MBSR program structure (Jon Kabat-Zinn 8-week, body scan + gentle yoga + sitting meditation), apps comparison (Headspace vs Calm vs Waking Up vs Insight Timer), retreat formats (Vipassana 10-day silent), common obstacles (sleepiness/agitation/doubt/restlessness/hindrances)'],
['id' => 'yoga_stretch', 'category' => 'wellness', 'topic' => 'Yoga, stretching, and mobility work',
'desc' => 'yoga styles (Hatha: foundational, holds longer; Vinyasa: flowing breath-synchronized movement; Ashtanga: fixed sequence, more athletic; Yin: passive holds 3-5 minutes for connective tissue; Restorative: supported with props for nervous system; Bikram/hot yoga: 26-pose sequence at 105°F; Kundalini: breathwork, chanting, kriyas), major pose families (standing balance: warrior series, tree, eagle; forward folds: seated/standing hamstring stretch; backbends: cobra/upward dog/wheel; inversions: headstand/shoulder stand/legs up wall; twists: supine and seated; hip openers: pigeon, lizard, butterfly), flexibility vs mobility distinction (flexibility is passive range, mobility is active control with strength), foam rolling technique (perpendicular to muscle fiber direction, 30-60 seconds per area, avoid rolling directly on joints), stretching timing (static post-workout, dynamic pre-workout)'],
['id' => 'spirituality', 'category' => 'wellness', 'topic' => 'Spirituality, religion, and meaning-making',
'desc' => 'distinction between spirituality and religion (organized doctrine vs personal search), psychological benefits of religious practice (community, meaning, mortality salience buffering, better health outcomes in studies — frequency of attendance correlates), secular alternatives to religious community (Sunday Assembly, ethical culture societies, meditation communities), Viktor Frankl logotherapy (Man\'s Search for Meaning — finding purpose in suffering, will to meaning), positive psychology and meaning (Seligman PERMA model: Positive emotion/Engagement/Relationships/Meaning/Achievement), death and dying psychology (Kübler-Ross five stages — not linear; terror management theory — mortality salience and meaning systems; hospice philosophy), near-death experience research (AWARE study, common elements: tunnel/light/life review/peace, skeptical vs spiritual interpretations), new age beliefs vs scientific evidence (astrology, crystal healing, manifestation law of attraction)'],
['id' => 'anger_emotion', 'category' => 'wellness', 'topic' => 'Emotional intelligence and anger management',
'desc' => 'emotions vs feelings distinction (emotions: physiological response; feelings: subjective interpretation), Plutchik\'s wheel of emotions (8 primary: joy/sadness/anger/fear/anticipation/surprise/trust/disgust + combinations), emotional granularity (ability to distinguish subtle emotional states associated with better outcomes), anger physiology (amygdala hijack — cortisol and adrenaline release, prefrontal cortex offline, 20-minute cortisol clearance time), anger management techniques (STOP acronym, 10-second pause before responding, diaphragmatic breathing to activate parasympathetic, physical exercise for cortisol burn-off, journaling for processing, reframing cognitive restructuring), passive-aggressive behavior patterns and roots, emotional flooding in couples (John Gottman — heart rate over 100 BPM, self-soothing 20+ minute break), emotional intelligence components (Mayer/Salovey/Caruso four-branch model vs Goleman\'s competency model)'],
['id' => 'blockchain', 'category' => 'technology', 'topic' => 'Blockchain, cryptocurrency, and Web3',
'desc' => 'blockchain data structure (chain of blocks each containing hash of previous, transaction data, timestamp, Merkle tree of transactions), consensus mechanisms (Proof of Work: miners compete to solve hash puzzle, energy-intensive, 51% attack vulnerability; Proof of Stake: validators staked as collateral, Ethereum\'s merge to PoS September 2022, energy reduction 99.9%), Bitcoin specifics (21 million cap, halving every 210,000 blocks, UTXO model, Lightning Network for micropayments), Ethereum smart contracts (Solidity language, EVM, gas fees, ERC-20 token standard, ERC-721 NFT standard), DeFi (decentralized finance: liquidity pools, yield farming, AMMs, impermanent loss), NFTs use cases and speculation, CBDC (central bank digital currencies — e-CNY, digital euro), regulatory landscape (SEC vs CFTC jurisdiction, securities classification debate, FTX collapse lessons)'],
['id' => 'iot_devices', 'category' => 'technology', 'topic' => 'Internet of Things and smart home technology',
'desc' => 'IoT architecture (edge devices → gateways → cloud, MQTT protocol for lightweight pub/sub messaging), communication protocols (Zigbee: mesh network, low power, requires hub; Z-Wave: mesh, proprietary, better range; Wi-Fi: high bandwidth but power hungry; Thread/Matter: new open standard, local control without cloud), smart home platforms (Amazon Alexa ecosystem, Google Home, Apple HomeKit privacy-focused local processing, Samsung SmartThings, Home Assistant for open-source local control), security concerns (default credential attacks, firmware update gaps, network segmentation via IoT VLAN, local vs cloud processing privacy), industrial IoT applications (predictive maintenance sensors, smart grid, precision agriculture), wearables (health monitoring accuracy limitations — optical heart rate vs chest strap, SpO2 accuracy in darker skin tones, sleep stage detection algorithm differences)'],
['id' => 'data_science', 'category' => 'technology', 'topic' => 'Data science and analytics',
'desc' => 'data pipeline stages (collection → storage → cleaning → analysis → visualization → decision), data types (structured: SQL databases; semi-structured: JSON/XML/CSV; unstructured: text/images/video), data cleaning steps (handling missing values — deletion/imputation/flagging; outlier detection — IQR method/z-score/DBSCAN; duplicate removal; data type validation; string standardization), exploratory data analysis (summary statistics, distribution visualization — histogram/box plot/violin; correlation heatmap; pair plots), regression types (linear for continuous, logistic for binary, ridge/lasso for regularization), classification algorithms (decision tree, random forest ensemble, gradient boosting XGBoost, SVM), clustering (k-means elbow method, hierarchical, DBSCAN for arbitrary shapes), SQL for data analysts (joins — inner/left/right/full/cross, window functions — ROW_NUMBER/RANK/LAG/LEAD, CTEs, aggregation)'],
['id' => 'robotics', 'category' => 'technology', 'topic' => 'Robotics and automation',
'desc' => 'robot types (articulated arms: 6-DOF industrial robots — FANUC/KUKA/ABB, delta robots for high-speed pick-and-place, SCARA for planar assembly, collaborative robots/cobots — force-limited for human interaction), sensors (encoders for position, LiDAR for 3D mapping, cameras for vision, force/torque sensors for touch), actuators (DC servo motors, stepper motors, hydraulic for heavy load, pneumatic for speed), kinematics (forward kinematics: joint angles → end effector position; inverse kinematics: desired position → required joint angles — multiple solutions), ROS (Robot Operating System) architecture, SLAM (Simultaneous Localization and Mapping), industrial automation ROI and displacement concerns, autonomous mobile robots in warehouses (Amazon Kiva systems), drone autonomy levels, surgical robots (da Vinci system, haptic feedback limitations)'],
['id' => 'us_housing', 'category' => 'national', 'topic' => 'US housing crisis and affordability',
'desc' => 'housing supply shortage causes (zoning restrictions — single-family-only zoning in 75% of residential land in many cities, NIMBYism, permitting delays, construction cost increases, labor shortages), demand factors (remote work migration to secondary markets, population growth in Sun Belt, demographic wave of millennials in prime buying years), rent burden (30% of income standard, severely cost-burdened at 50%+, share of renters cost-burdened rising), homelessness crisis (Housing First evidence vs treatment-first debate, permanent supportive housing cost vs shelter cost, LA and SF policy failures), solutions debated (upzoning — Minneapolis 2040 plan, ADU legalization, inclusionary zoning tradeoffs, construction defect litigation reform, manufactured housing modernization, federal voucher expansion)'],
['id' => 'us_energy', 'category' => 'national', 'topic' => 'US energy policy and grid security',
'desc' => 'US electricity grid structure (Eastern Interconnection, Western Interconnection, Texas ERCOT — reason for isolation and vulnerability exposed by Uri), energy mix evolution (coal decline: 55% to 19% since 2000; natural gas rise: 33%; wind and solar growth: combined 13%; nuclear: 19% of generation but constant baseline), inflation Reduction Act provisions (production tax credit extension for wind/solar, investment tax credit for new nuclear, electric vehicle tax credits, home efficiency rebates), transmission bottleneck as renewable buildout barrier (permitting reform needed), energy storage role (4-hour lithium-ion vs longer duration alternatives — iron-air, flow batteries, pumped hydro siting challenges), LNG export growth and European energy security after Russia invasion, FERC jurisdiction vs state utility regulation'],
['id' => 'asia_economy', 'category' => 'world', 'topic' => 'Asian economic giants — Japan, Korea, China',
'desc' => 'Japan: lost decades (1990 asset bubble collapse, deflation trap, Bank of Japan yield curve control, Abenomics three arrows — monetary stimulus/fiscal stimulus/structural reform, demographic crisis and immigration resistance, anime and soft power, robotics leadership compensating for labor shortage), South Korea: chaebol conglomerate model (Samsung/Hyundai/LG dominance and corruption issues), K-culture global wave (K-pop idol system production, Korean cinema Parasite Oscar, Korean food global spread), DRAM memory semiconductor dominance (Samsung and SK Hynix 70%+ of global market), China: economic growth model shift (export-led to domestic consumption target, Xi\'s common prosperity campaign reining in Alibaba/Tencent, real estate crisis — Evergrande contagion, youth unemployment 20%+, demographic cliff from one-child policy)'],
['id' => 'latin_deep', 'category' => 'world', 'topic' => 'Latin America — history, politics, and culture',
'desc' => 'colonial legacy (Spanish colonial administrative structure — viceroyalties, encomienda labor system, racial caste system castas, Catholic Church land ownership), independence wave 1810-1826 (Bolivar, San Martin, Hidalgo in Mexico), post-independence instability (caudillo strongman tradition, US Monroe Doctrine interference, banana republic term origin — United Fruit Company in Guatemala), 20th century Cold War proxy battles (Cuban Revolution and Bay of Pigs, Chilean coup 1973 and Pinochet backed by US, Nicaragua Sandinistas, El Salvador civil war, dirty wars in Argentina and Brazil), pink tide (Chávez petro-populism in Venezuela, Lula in Brazil, Morales in Bolivia, progressive governments 2000s), current landscape (Bukele in El Salvador, Milei in Argentina, Boric in Chile, Petro in Colombia, Maduro\'s continued authoritarian rule), migration drivers'],
];
/* ── ROTATION ENGINE: process BATCH_SIZE topics per run, cycling through all ── */ /* ── ROTATION ENGINE: process BATCH_SIZE topics per run, cycling through all ── */
define('BATCH_SIZE', 25); define('BATCH_SIZE', 25);
@@ -387,6 +117,65 @@ $endIdx = ($batchOffset + BATCH_SIZE - 1) % $totalTopics;
log_line("Rotation: topics " . ($batchOffset + 1) . "" . ($endIdx + 1) . " of {$totalTopics} | cycle = {$cycleLen} runs × 6h = " . ($cycleLen * 6) . "h full cycle."); log_line("Rotation: topics " . ($batchOffset + 1) . "" . ($endIdx + 1) . " of {$totalTopics} | cycle = {$cycleLen} runs × 6h = " . ($cycleLen * 6) . "h full cycle.");
if ($cycleComplete) log_line(" ↻ Full cycle complete — restarting from topic 1 next run."); if ($cycleComplete) log_line(" ↻ Full cycle complete — restarting from topic 1 next run.");
/* ── system prompt ── */
/* RESTORED 2026-07-07: this and safe_insert() below were lost during the 2026-07-05 rotation-engine
refactor (moving from a hardcoded $BATCHES array to the kb_generator_topics table). Their absence
caused an uncaught TypeError on every run since (undefined $SYSTEM passed to groq()'s non-nullable
string param), silently swallowed by config.php's error_reporting(0) the cron looked like it was
running fine but died instantly on batch 1 every single time. Restored from kb_intent_generator.php.bak2,
with the intent count adjusted from 40 to 20 to match this version's actual per-topic request below. */
$SYSTEM = <<<'SYS'
You are an expert educator generating KB (knowledge-base) intents for an AI assistant called JARVIS.
Each intent is a question/phrase a student might ask, paired with a clear educational answer.
Respond ONLY with a valid JSON array (no markdown, no backticks, no commentary).
Each element must have exactly these keys:
"n" intent_name: unique snake_case identifier 60 chars, prefixed with the batch id given
"p" pattern: a PHP PCRE regex (use (?i) for case-insensitive) that matches the question
"r" response: a thorough but concise educational answer (25 sentences or a short structured list)
"c" category: the category string provided
Rules:
- Patterns must use \\b word boundaries; escape backslashes for JSON (\\b not \b)
- Patterns should NOT start with ^ or end with $ (they are substring matches)
- Responses must be factually accurate
- Do not duplicate intent names; every "n" must be unique within this batch
- Return exactly 20 intents
SYS;
/* ── insert helper ── */
$inserted = 0;
$skipped = 0;
$errors = 0;
function safe_insert(array $intent, string $batchCategory): void {
global $inserted, $skipped, $errors;
$name = trim($intent['n'] ?? '');
$pattern = trim($intent['p'] ?? '');
$response = trim($intent['r'] ?? '');
$category = trim($intent['c'] ?? $batchCategory);
if (!$name || !$pattern || !$response) { $errors++; return; }
if (strlen($name) > 64) $name = substr($name, 0, 64);
if (strlen($pattern) > 512) $pattern = substr($pattern, 0, 512);
if (strlen($response) < 30) { $skipped++; return; } // too short
try {
JarvisDB::execute(
'INSERT INTO kb_intents (intent_name, pattern, response_template, fact_category, action_type, priority, active)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
pattern=VALUES(pattern),
response_template=VALUES(response_template),
fact_category=VALUES(fact_category)',
[$name, $pattern, $response, $category, 'response', 5, 1]
);
$inserted++;
} catch (Exception $e) {
$errors++;
}
}
/* ── main generation loop ── */ /* ── main generation loop ── */
$totalBatches = count($runBatches); $totalBatches = count($runBatches);
foreach ($runBatches as $idx => $batch) { foreach ($runBatches as $idx => $batch) {
+7 -2
View File
@@ -2,14 +2,14 @@
// Network scan push endpoint — called by PVE1 cron with nmap results // Network scan push endpoint — called by PVE1 cron with nmap results
// Authenticates via X-Registration-Key header (same key as agent installer) // Authenticates via X-Registration-Key header (same key as agent installer)
define('NETSCAN_KEY', 'f846a9aaf7ce9a61742c63c87c4186052a71d2a580c65518'); define('NETSCAN_KEY', AGENT_REGISTRATION_KEY);
if ($method !== 'POST') { if ($method !== 'POST') {
echo json_encode(['error' => 'POST only']); exit; echo json_encode(['error' => 'POST only']); exit;
} }
$reqKey = $_SERVER['HTTP_X_REGISTRATION_KEY'] ?? ''; $reqKey = $_SERVER['HTTP_X_REGISTRATION_KEY'] ?? '';
if ($reqKey !== NETSCAN_KEY) { if (!hash_equals(NETSCAN_KEY, $reqKey)) {
http_response_code(401); http_response_code(401);
echo json_encode(['error' => 'Unauthorized']); exit; echo json_encode(['error' => 'Unauthorized']); exit;
} }
@@ -35,6 +35,11 @@ foreach ($devices as $d) {
if (!$ip) continue; if (!$ip) continue;
$discoveredIPs[] = $ip; $discoveredIPs[] = $ip;
if ($mac) {
// Device likely moved to a new IP (DHCP) — drop the stale row so it
// doesn't linger as an orphaned duplicate under the old address.
JarvisDB::execute('DELETE FROM network_devices WHERE mac=? AND ip<>?', [$mac, $ip]);
}
JarvisDB::execute( JarvisDB::execute(
'INSERT INTO network_devices (ip, mac, hostname, status, last_seen) 'INSERT INTO network_devices (ip, mac, hostname, status, last_seen)
VALUES (?,?,?,?,NOW()) VALUES (?,?,?,?,NOW())
+2 -2
View File
@@ -213,7 +213,7 @@ if ($weatherAge > 1800) {
}; };
$weatherRaw = curlGet( $weatherRaw = curlGet(
'https://wttr.in/FortWorth,TX?format=j1', 'https://wttr.in/76088?format=j1',
['User-Agent: curl/7.88 Jarvis/1.0'], ['User-Agent: curl/7.88 Jarvis/1.0'],
15 15
); );
@@ -245,7 +245,7 @@ if ($weatherAge > 1800) {
cacheStore('weather', [ cacheStore('weather', [
'source' => 'wttr.in', 'source' => 'wttr.in',
'location' => 'Fort Worth, TX', 'location' => 'Weatherford, TX',
'current' => [ 'current' => [
'temp' => (int)($cu['temp_F'] ?? 0), 'temp' => (int)($cu['temp_F'] ?? 0),
'feels' => (int)($cu['FeelsLikeF'] ?? 0), 'feels' => (int)($cu['FeelsLikeF'] ?? 0),
+3
View File
@@ -0,0 +1,3 @@
# Copy to /etc/jarvis/db.env (root:root 0600). Sourced by the root cron scripts
# (jarvis-backup.sh, jarvis-deploy.sh, jarvis-watchdog.sh).
JARVIS_DB_PASS=your-db-password
Regular → Executable
+24 -4
View File
@@ -1,11 +1,16 @@
#!/bin/bash #!/bin/bash
# JARVIS backup — DB dump as tar.gz, admin-panel compatible [ -r /etc/jarvis/db.env ] && . /etc/jarvis/db.env
# JARVIS backup — DB dump + all files needed to actually restore JARVIS, as tar.gz
# Fixed 2026-07-07: this only ever backed up the MySQL database. If this VM were
# lost, the DB alone is useless without the application code, the reactor daemon,
# its systemd unit, and the nginx site config — none of which were captured. Also
# fixed a typo ($SIYE -> $SIZE) that silently broke the size line in the log.
BACKUP_DIR="/var/backups/jarvis" BACKUP_DIR="/var/backups/jarvis"
LOG="$BACKUP_DIR/backup.log" LOG="$BACKUP_DIR/backup.log"
LOCK="$BACKUP_DIR/backup.lock" LOCK="$BACKUP_DIR/backup.lock"
DB_NAME="jarvis_db" DB_NAME="jarvis_db"
DB_USER="jarvis_user" DB_USER="jarvis_user"
DB_PASS="J4rv1s_Pr0t0c0l_2026!" DB_PASS="${JARVIS_DB_PASS:?DB pass unset - see /etc/jarvis/db.env}"
TIMESTAMP=$(date +"%Y%m%d_%H%M%S") TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
OUTFILE="$BACKUP_DIR/jarvis_backup_${TIMESTAMP}.tar.gz" OUTFILE="$BACKUP_DIR/jarvis_backup_${TIMESTAMP}.tar.gz"
TMPDIR=$(mktemp -d) TMPDIR=$(mktemp -d)
@@ -18,9 +23,24 @@ cleanup() { rm -rf "$TMPDIR"; rm -f "$LOCK"; }
trap cleanup EXIT trap cleanup EXIT
if mysqldump -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" > "$TMPDIR/jarvis_db.sql" 2>>"$LOG"; then if mysqldump -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" > "$TMPDIR/jarvis_db.sql" 2>>"$LOG"; then
tar -czf "$OUTFILE" -C "$TMPDIR" jarvis_db.sql mkdir -p "$TMPDIR/files/etc"
cp -a /var/www/jarvis "$TMPDIR/files/var-www-jarvis"
cp -a /opt/jarvis-arc "$TMPDIR/files/opt-jarvis-arc"
cp -a /etc/nginx/sites-enabled/jarvis "$TMPDIR/files/etc/nginx-site-jarvis" 2>>"$LOG"
cp -a /etc/systemd/system/jarvis-arc.service "$TMPDIR/files/etc/jarvis-arc.service" 2>>"$LOG"
crontab -l > "$TMPDIR/files/etc/root-crontab.txt" 2>>"$LOG"
# Phase 1/2 additions: secrets + systemd drop-ins + operational scripts
# (these live outside /var/www/jarvis and /opt/jarvis-arc, so must be
# captured explicitly or a restore comes back with no keys/DB pass).
cp -a /etc/jarvis-arc "$TMPDIR/files/etc/jarvis-arc-etc" 2>>"$LOG" # reactor.env
cp -a /etc/jarvis "$TMPDIR/files/etc/jarvis-etc" 2>>"$LOG" # db.env
cp -a /etc/systemd/system/jarvis-arc.service.d "$TMPDIR/files/etc/jarvis-arc.service.d" 2>>"$LOG"
mkdir -p "$TMPDIR/files/usr-local-bin"
cp -a /usr/local/bin/jarvis-*.sh "$TMPDIR/files/usr-local-bin/" 2>>"$LOG" # health/deploy/watchdog/netscan
tar -czf "$OUTFILE" -C "$TMPDIR" jarvis_db.sql files
SIZE=$(du -sh "$OUTFILE" | cut -f1) SIZE=$(du -sh "$OUTFILE" | cut -f1)
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Backup OK: $(basename "$OUTFILE") ($SIYE)" >> "$LOG" echo "[$(date '+%Y-%m-%d %H:%M:%S')] Backup OK: $(basename "$OUTFILE") ($SIZE)" >> "$LOG"
else else
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: mysqldump failed" >> "$LOG" echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: mysqldump failed" >> "$LOG"
exit 1 exit 1
+2 -1
View File
@@ -1,4 +1,5 @@
#!/bin/bash #!/bin/bash
[ -r /etc/jarvis/db.env ] && . /etc/jarvis/db.env
# JARVIS Auto-Deploy Runner — processes GitHub webhook queue every minute. # JARVIS Auto-Deploy Runner — processes GitHub webhook queue every minute.
# Validates PHP syntax before deploying; auto-reverts on bad code. # Validates PHP syntax before deploying; auto-reverts on bad code.
# Restarts OLS after JARVIS deploys to pick up PHP changes. # Restarts OLS after JARVIS deploys to pick up PHP changes.
@@ -64,7 +65,7 @@ while IFS= read -r path; do
fi fi
# Insert alert into JARVIS DB # Insert alert into JARVIS DB
BAD_ESCAPED=$(printf '%s' "$BAD_FILE" | sed "s/'/\\\\\\'/g") BAD_ESCAPED=$(printf '%s' "$BAD_FILE" | sed "s/'/\\\\\\'/g")
mysql -u jarvis_user -pJ4rv1s_Pr0t0c0l_2026! jarvis_db -se \ mysql -u jarvis_user -p"$JARVIS_DB_PASS" jarvis_db -se \
"INSERT INTO alerts (alert_type,title,message,severity) "INSERT INTO alerts (alert_type,title,message,severity)
VALUES ('deploy_fail','Deploy reverted: syntax error', VALUES ('deploy_fail','Deploy reverted: syntax error',
'PHP syntax error in $BAD_ESCAPED. Commit $AFTER was reverted and force-pushed to GitHub.','critical');" 2>/dev/null 'PHP syntax error in $BAD_ESCAPED. Commit $AFTER was reverted and force-pushed to GitHub.','critical');" 2>/dev/null
+97
View File
@@ -0,0 +1,97 @@
#!/bin/bash
# JARVIS Health Self-Check — runs every 5 min via root cron (separate from the
# service watchdog). Detects silent failures the watchdog can't: stalled crons,
# stuck Arc jobs, low disk, and services the watchdog had to restart. Writes
# findings to the `alerts` table (auto-resolving when clear) and emails on any
# NEW finding. Added 2026-07-07 (Phase 2 reliability).
set -u
[ -r /etc/jarvis/db.env ] && . /etc/jarvis/db.env
DB_USER="jarvis_user"; DB_NAME="jarvis_db"
MYSQL=(mysql -u "$DB_USER" -p"${JARVIS_DB_PASS:-}" "$DB_NAME" -N -B -e)
CRONLOG=/var/log/jarvis/cron.log
WDLOG=/var/log/jarvis/watchdog.log
ALERT_TO="myronblair@gmail.com"
REACTOR_ENV=/etc/jarvis-arc/reactor.env
NEW_FINDINGS=""
sql() { "${MYSQL[@]}" "$1" 2>/dev/null; }
esc() { printf '%s' "$1" | sed "s/'/''/g"; }
# Raise (or keep) an alert for a condition. Emails only when it is newly raised.
# $1=source_key $2=severity $3=title $4=message
raise() {
local key sev title msg exists
key=$(esc "$1"); sev=$(esc "$2"); title=$(esc "$3"); msg=$(esc "$4")
exists=$(sql "SELECT COUNT(*) FROM alerts WHERE source_key='$key' AND resolved=0")
if [ "${exists:-0}" = "0" ]; then
sql "INSERT INTO alerts (alert_type,title,message,severity,source_key,auto_resolve,created_at)
VALUES ('health','$title','$msg','$sev','$key',1,NOW())"
NEW_FINDINGS="${NEW_FINDINGS}- [$2] $3: $4"$'\n'
fi
}
# Clear a condition's alert when it's no longer true.
clear_cond() {
local key; key=$(esc "$1")
sql "UPDATE alerts SET resolved=1, resolved_at=NOW()
WHERE source_key='$key' AND resolved=0 AND auto_resolve=1"
}
# 1) Disk usage on / > 85%
DISK=$(df / | tail -1 | awk '{print $5}' | tr -d '%')
if [ "${DISK:-0}" -gt 85 ]; then
raise "health:disk" "critical" "Disk usage high" "Root filesystem at ${DISK}% (threshold 85%)."
else clear_cond "health:disk"; fi
# 2) Arc jobs stuck in 'running' > 30 min
STUCK=$(sql "SELECT COUNT(*) FROM arc_jobs WHERE status='running'
AND COALESCE(started_at, created_at) < NOW() - INTERVAL 30 MINUTE")
if [ "${STUCK:-0}" -gt 0 ]; then
raise "health:arc_stuck" "critical" "Arc jobs stuck" "$STUCK Arc job(s) have been 'running' for over 30 minutes."
else clear_cond "health:arc_stuck"; fi
# 3) Cron stalled — cron.log is written by facts_collector every 3 min. If it
# hasn't changed in 10 min (>2 consecutive missed runs), cron work has stopped.
if [ -f "$CRONLOG" ]; then
AGE=$(( $(date +%s) - $(stat -c %Y "$CRONLOG") ))
if [ "$AGE" -gt 600 ]; then
raise "health:cron" "critical" "Cron jobs stalled" "No cron activity in $((AGE/60)) min (facts_collector runs every 3 min) — crons appear stopped."
else clear_cond "health:cron"; fi
fi
# 4) Watched service restarted by the watchdog in the last 6 min
if [ -f "$WDLOG" ]; then
RECENT=$(awk -v cutoff="$(date -d '6 minutes ago' '+%Y-%m-%d %H:%M:%S')" \
'match($0,/^\[([0-9-]+ [0-9:]+)\]/,m){ if(m[1]>=cutoff && /restarted successfully/) print }' "$WDLOG")
if [ -n "$RECENT" ]; then
SVC=$(printf '%s' "$RECENT" | grep -oE '(nginx|php8.3-fpm|mariadb|redis-server)' | sort -u | tr '\n' ' ')
raise "health:wd_restart" "warning" "Service auto-restarted" "Watchdog restarted: ${SVC:-a service}. Investigate why it died."
else
clear_cond "health:wd_restart"
fi
fi
# Email any NEW findings via the reactor's Gmail SMTP creds.
if [ -n "$NEW_FINDINGS" ]; then
GPASS=""
[ -r "$REACTOR_ENV" ] && GPASS=$(grep -E '^GMAIL_PASS=' "$REACTOR_ENV" | cut -d= -f2-)
if [ -n "$GPASS" ]; then
GMAIL_PASS="$GPASS" ALERT_TO="$ALERT_TO" FINDINGS="$NEW_FINDINGS" python3 - <<'PY'
import os, smtplib, ssl, socket
from email.mime.text import MIMEText
user = "myronblair@gmail.com"
msg = MIMEText("JARVIS health self-check raised new alerts on %s:\n\n%s" % (socket.gethostname(), os.environ["FINDINGS"]))
msg["Subject"] = "JARVIS health alert"
msg["From"] = user; msg["To"] = os.environ["ALERT_TO"]
try:
with smtplib.SMTP("smtp.gmail.com", 587, timeout=20) as s:
s.starttls(context=ssl.create_default_context())
s.login(user, os.environ["GMAIL_PASS"])
s.send_message(msg)
print("health-email: sent")
except Exception as e:
print("health-email: FAILED", e)
PY
else
echo "health-email: no GMAIL_PASS available, skipped"
fi
fi
+2 -1
View File
@@ -1,11 +1,12 @@
#!/bin/bash #!/bin/bash
[ -r /etc/jarvis/db.env ] && . /etc/jarvis/db.env
# JARVIS Self-Healing Watchdog — runs every 5 min via root cron # JARVIS Self-Healing Watchdog — runs every 5 min via root cron
# Checks: lsws, mysql, redis, JARVIS HTTP, disk, memory # Checks: lsws, mysql, redis, JARVIS HTTP, disk, memory
# Auto-heals: restarts failed services, restarts offline Proxmox VM agents # Auto-heals: restarts failed services, restarts offline Proxmox VM agents
# Logs to: /home/jarvis.orbishosting.com/logs/watchdog.log # Logs to: /home/jarvis.orbishosting.com/logs/watchdog.log
LOG=/home/jarvis.orbishosting.com/logs/watchdog.log LOG=/home/jarvis.orbishosting.com/logs/watchdog.log
MYSQL="mysql -u jarvis_user -pJ4rv1s_Pr0t0c0l_2026! jarvis_db -se" MYSQL="mysql -u jarvis_user -p$JARVIS_DB_PASS jarvis_db -se"
TS() { date '+%Y-%m-%d %H:%M:%S'; } TS() { date '+%Y-%m-%d %H:%M:%S'; }
log() { echo "[$(TS)] $1" >> "$LOG"; } log() { echo "[$(TS)] $1" >> "$LOG"; }
+7
View File
@@ -0,0 +1,7 @@
# JARVIS Arc Reactor — required secrets. Copy to /etc/jarvis-arc/reactor.env
# (root:www-data 0640), loaded by systemd EnvironmentFile. Not committed.
JARVIS_DB_PASS=your-db-password
CLAUDE_API_KEY=sk-ant-...
GROQ_API_KEY=gsk_...
GMAIL_PASS=your-gmail-app-password
ICLOUD_PASS=your-icloud-app-password
+90 -16
View File
@@ -39,24 +39,24 @@ VERSION = "9.0.0"
DB_HOST = "localhost" DB_HOST = "localhost"
DB_PORT = 3306 DB_PORT = 3306
DB_USER = "jarvis_user" DB_USER = "jarvis_user"
DB_PASS = "J4rv1s_Pr0t0c0l_2026!" DB_PASS = os.environ.get("JARVIS_DB_PASS", "")
DB_NAME = "jarvis_db" DB_NAME = "jarvis_db"
LOG_FILE = "/var/log/jarvis/arc_reactor.log" LOG_FILE = "/var/log/jarvis/arc_reactor.log"
POLL_INTERVAL = 3 POLL_INTERVAL = 3
HEARTBEAT_INTERVAL = 30 HEARTBEAT_INTERVAL = 30
CLAUDE_API_KEY = "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" CLAUDE_API_KEY = os.environ.get("CLAUDE_API_KEY", "")
CLAUDE_MODEL = "claude-sonnet-4-6" CLAUDE_MODEL = "claude-sonnet-4-6"
GROQ_API_KEY = "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" GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "")
GROQ_MODEL = "llama-3.3-70b-versatile" GROQ_MODEL = "llama-3.3-70b-versatile"
OLLAMA_HOST = "http://10.48.200.210:11434" OLLAMA_HOST = "http://10.48.200.210:11434"
OLLAMA_MODEL = "llama3.1:8b" OLLAMA_MODEL = "llama3.1:8b"
OLLAMA_VISION_MODEL = os.environ.get("OLLAMA_VISION_MODEL", "") # e.g. "llava" or "moondream" -- empty = disabled OLLAMA_VISION_MODEL = os.environ.get("OLLAMA_VISION_MODEL", "") # e.g. "llava" or "moondream" -- empty = disabled
GMAIL_USER = "myronblair@gmail.com" GMAIL_USER = "myronblair@gmail.com"
GMAIL_PASS = "demsvdylwweacbcx" GMAIL_PASS = os.environ.get("GMAIL_PASS", "")
ICLOUD_USER = "myronblair@icloud.com" ICLOUD_USER = "myronblair@icloud.com"
ICLOUD_PASS = "yxfi-yvzu-geqk-japr" ICLOUD_PASS = os.environ.get("ICLOUD_PASS", "")
# ── LOGGING ─────────────────────────────────────────────────────────────────── # ── LOGGING ───────────────────────────────────────────────────────────────────
os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True) os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)
@@ -130,18 +130,35 @@ async def handle_shell(payload: dict) -> dict:
# ═══════════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════════
async def llm_call(messages: list, provider: str = "claude", system: str = "") -> str: async def llm_call(messages: list, provider: str = "claude", system: str = "") -> str:
if provider == "claude" and CLAUDE_API_KEY: # Fixed 2026-07-07: previously, an explicitly-requested provider (e.g. "claude")
return await _claude_call(messages, system) # called its API directly with no exception handling, so any failure (rate limit,
elif provider == "groq" and GROQ_API_KEY: # depleted credits, outage) raised straight up instead of falling back to the other
return await _groq_call(messages, system) # providers below. The fallback loop only ever ran for an unrecognized provider
elif provider == "ollama": # string, which never happens in practice — so callers requesting "claude" got zero
return await _ollama_call(messages, system) # real resilience. Now every explicit request tries its provider first, then falls
for p in ["groq", "ollama"]: # through the remaining ones in order before giving up.
order = {"claude": ["claude", "groq", "ollama"],
"groq": ["groq", "ollama"],
"ollama": ["ollama"]}.get(provider, ["claude", "groq", "ollama"])
last_err = None
for p in order:
try: try:
return await llm_call(messages, p, system) if p == "claude" and CLAUDE_API_KEY:
except Exception: result = await _claude_call(messages, system)
elif p == "groq" and GROQ_API_KEY:
result = await _groq_call(messages, system)
elif p == "ollama":
result = await _ollama_call(messages, system)
else:
continue continue
raise RuntimeError("All LLM providers failed") if not result or not result.strip():
raise RuntimeError(f"{p} returned empty content")
return result
except Exception as e:
log.warning(f"[LLM] Provider {p} failed: {type(e).__name__}: {e}")
last_err = e
continue
raise RuntimeError(f"All LLM providers failed (last error: {last_err})")
async def _claude_call(messages: list, system: str = "") -> str: async def _claude_call(messages: list, system: str = "") -> str:
payload = {"model": CLAUDE_MODEL, "max_tokens": 4096, "messages": messages} payload = {"model": CLAUDE_MODEL, "max_tokens": 4096, "messages": messages}
@@ -156,13 +173,36 @@ async def _claude_call(messages: list, system: str = "") -> str:
raise RuntimeError(f"Claude API error {resp.status}: {data.get('error',{}).get('message','')}") raise RuntimeError(f"Claude API error {resp.status}: {data.get('error',{}).get('message','')}")
return data["content"][0]["text"] return data["content"][0]["text"]
def _parse_groq_reset(val: str) -> float:
"""Parse Groq's Go-style duration strings ('229ms', '2.5s', '2m52.8s') into seconds."""
import re as _re
if not val:
return 0.0
total = 0.0
for num, unit in _re.findall(r'([\d.]+)(ms|s|m|h)', val):
n = float(num)
total += n/1000 if unit == 'ms' else n*60 if unit == 'm' else n*3600 if unit == 'h' else n
return total
async def _groq_call(messages: list, system: str = "") -> str: async def _groq_call(messages: list, system: str = "") -> str:
# Added 2026-07-07: retry once on 429 (rate limit) using Groq's own reset-time
# header, capped short — this account's tier has a low 12k-tokens/min ceiling that
# gmail_triage alone can exhaust, so transient contention here is common and often
# clears in well under a second. Capped so a genuinely long reset just falls
# through to Ollama instead of blocking the whole compose flow.
all_msgs = ([{"role": "system", "content": system}] if system else []) + messages all_msgs = ([{"role": "system", "content": system}] if system else []) + messages
payload = {"model": GROQ_MODEL, "messages": all_msgs, "max_tokens": 4096} payload = {"model": GROQ_MODEL, "messages": all_msgs, "max_tokens": 4096}
headers = {"Authorization": f"Bearer {GROQ_API_KEY}", "Content-Type": "application/json"} headers = {"Authorization": f"Bearer {GROQ_API_KEY}", "Content-Type": "application/json"}
for attempt in range(2):
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.post("https://api.groq.com/openai/v1/chat/completions", json=payload, async with session.post("https://api.groq.com/openai/v1/chat/completions", json=payload,
headers=headers, timeout=aiohttp.ClientTimeout(total=45)) as resp: headers=headers, timeout=aiohttp.ClientTimeout(total=45)) as resp:
if resp.status == 429 and attempt == 0:
wait = min(_parse_groq_reset(resp.headers.get("x-ratelimit-reset-tokens", "")) or
_parse_groq_reset(resp.headers.get("x-ratelimit-reset-requests", "")) or 2.0, 8.0)
log.info(f"[LLM] Groq 429 — retrying in {wait:.1f}s")
await asyncio.sleep(wait)
continue
data = await resp.json() data = await resp.json()
if resp.status != 200: if resp.status != 200:
raise RuntimeError(f"Groq error {resp.status}") raise RuntimeError(f"Groq error {resp.status}")
@@ -172,8 +212,14 @@ async def _ollama_call(messages: list, system: str = "") -> str:
prompt = (system + "\n\n" if system else "") + "\n".join(f"{m['role'].upper()}: {m['content']}" for m in messages) prompt = (system + "\n\n" if system else "") + "\n".join(f"{m['role'].upper()}: {m['content']}" for m in messages)
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.post(f"{OLLAMA_HOST}/api/generate", json={"model": OLLAMA_MODEL, "prompt": prompt, "stream": False}, async with session.post(f"{OLLAMA_HOST}/api/generate", json={"model": OLLAMA_MODEL, "prompt": prompt, "stream": False},
timeout=aiohttp.ClientTimeout(total=30)) as resp: timeout=aiohttp.ClientTimeout(total=90)) as resp: # bumped from 30s 2026-07-07: cold model loads on this CPU-only host alone took 15s+ in testing, leaving no room for actual generation
data = await resp.json() data = await resp.json()
# Fixed 2026-07-07: this had no error checking at all — if the model wasn't
# pulled (or any other Ollama-side error), the API returns {"error": "..."}
# with no "response" key, and this silently returned "" instead of raising,
# which fooled llm_call()'s fallback into treating it as a real success.
if "error" in data:
raise RuntimeError(f"Ollama error: {data['error']}")
return data.get("response", "") return data.get("response", "")
@@ -2785,5 +2831,33 @@ async def comms_sent_delete(sent_id: int):
await db_execute("DELETE FROM email_sent WHERE id=%s", (sent_id,)) await db_execute("DELETE FROM email_sent WHERE id=%s", (sent_id,))
return {"ok": True} return {"ok": True}
@app.post("/comms/sent/{sent_id}/send")
async def comms_sent_send(sent_id: int):
"""
Added 2026-07-07: sends a previously-composed queued draft. Compose always
created a draft (status='queued') for review, but there was no action anywhere
to actually send one this closes that gap. handle_send_email() records its own
fresh row in email_sent (sent/failed), so the original queued draft row is removed
here once we've handed its content off, rather than leaving a stale duplicate.
"""
row = await db_fetchone(
"SELECT account, to_email, to_name, subject, body, triage_id FROM email_sent WHERE id=%s AND status='queued'",
(sent_id,)
)
if not row:
raise HTTPException(status_code=404, detail="Queued draft not found")
result = await handle_send_email({
"account": row["account"],
"to_email": row["to_email"],
"to_name": row["to_name"],
"subject": row["subject"],
"body": row["body"],
"triage_id": row["triage_id"],
})
await db_execute("DELETE FROM email_sent WHERE id=%s", (sent_id,))
return result
if __name__ == "__main__": if __name__ == "__main__":
uvicorn.run("reactor:app", host=HOST, port=PORT, log_level="info", access_log=False) uvicorn.run("reactor:app", host=HOST, port=PORT, log_level="info", access_log=False)
@@ -1,767 +0,0 @@
# INFRASTRUCTURE REFERENCE — COMPLETE SYSTEM MAP
**Last Updated:** 2026-07-01
**Owner:** Myron Blair — myronblair@outlook.com
---
## TABLE OF CONTENTS
1. [Network Overview](#1-network-overview)
2. [Cloud Servers](#2-cloud-servers)
3. [On-Premise — Proxmox Hypervisors](#3-on-premise--proxmox-hypervisors)
4. [On-Premise — Virtual Machines](#4-on-premise--virtual-machines)
5. [NAS Storage](#5-nas-storage)
6. [Websites (all on DO)](#6-websites--all-on-do)
7. [JARVIS AI System](#7-jarvis-ai-system)
8. [Phone System (FusionPBX)](#8-phone-system-fusionpbx)
9. [Networking & VPN](#9-networking--vpn)
10. [Backup Systems](#10-backup-systems)
11. [SSH Quick Reference](#11-ssh-quick-reference)
12. [Critical Credentials Master List](#12-critical-credentials-master-list)
---
## 1. NETWORK OVERVIEW
```
INTERNET
[Cloudflare CDN] ──────────────────────────────────────────────────────────────
│ (proxied DNS for public sites)
├─► [DigitalOcean 165.22.1.228] — CyberPanel/OLS — All websites (6 sites)
└─► [FusionPBX 134.209.72.226] — FreeSWITCH PBX (SSH via DO relay)
HOME NETWORK (FortiGate router at 10.48.200.1)
WAN: 97.154.109.245 (dynamic, DDNS: orbisne.fortiddns.com)
├─► PVE1 Proxmox 10.48.200.90 (primary hypervisor)
│ ├── VM 101 10.48.200.97 Home Assistant
│ ├── VM 112 10.48.200.33 Jellyfin
│ ├── VM 113 10.48.200.35 MediaStack (Sonarr/Radarr/qBT/Prowlarr)
│ ├── VM 118 10.48.200.18 Homebridge
│ ├── VM 120 10.48.200.110 NovaCPX hosting panel
│ ├── VM 210 10.48.200.210 Ollama (local LLM + vision) — llama3.1:8b, llava:7b
│ └── CT110 10.48.200.19 WireGuard exit container
├─► PVE2 Proxmox 10.48.200.91 (secondary hypervisor)
│ └── VM 302 10.48.200.99 NetworkBackup
├─► Synology NAS 10.48.200.249 — Media & backup storage
├─► Yealink T48S 10.48.200.2 — Ext 1000 (Myron Blair, Desk)
├─► Yealink T48S 10.48.200.43 — Ext 1001 (Tommy Ivy, Desk)
├─► Yealink AX86R 10.48.200.65 — Ext 1002 (Myron Blair, WiFi Work)
├─► Yealink T57W 10.48.200.3 — External SIP (United Mirror & Glass)
├─► Yealink T57W 10.48.200.83 — Ext 1003 (Kitchen)
└─► Yealink T57W 10.48.200.85 — Ext 1004 (Master Bedroom)
FortiGate Port Forwards:
orbisne.fortiddns.com:8006 → PVE1:8006 (Proxmox web UI)
orbisne.fortiddns.com:8123 → HA:8123 (Home Assistant)
orbisne.fortiddns.com:22 → HA VM:22 (SSH — key only, unreliable)
```
---
## 2. CLOUD SERVERS
### 2A. DigitalOcean — Main Server
| Field | Value |
|-------|-------|
| **IP** | 165.22.1.228 |
| **OS** | Ubuntu 22.04 LTS |
| **Panel** | CyberPanel (OpenLiteSpeed) |
| **SSH** | `ssh root@165.22.1.228` — password: `Gonewalk1974!@#` |
| **Purpose** | All public websites (6 sites) — webhook deploy for websites |
**Key Paths:**
- All sites: `/home/<domain>/public_html/`
- Deploy log: per-site (website deploys only)
- Watchdog log: `/usr/local/lsws/logs/watchdog.log`
- Infra repo: `/opt/infra`
**Services running:**
- OpenLiteSpeed web server (`lsws`) — serves all 7 sites
- MySQL 8 — all site databases on localhost
- Redis — session/cache
- PHP 8.5 (`lsphp85`) — runtime for all sites
- Cron jobs: website deploy runner (every 1 min), watchdog (every 5 min)
**CyberPanel Web UI:** `https://165.22.1.228:8090`
Login: `myron / Joker1974!!!`
**phpMyAdmin:** `https://165.22.1.228/phpmyadmin`
Login: `myron / Joker1974!!!`
---
### 2B. FusionPBX / FreeSWITCH — PBX Server
| Field | Value |
|-------|-------|
| **IP** | 134.209.72.226 |
| **OS** | Debian (DigitalOcean droplet) |
| **SSH** | Direct via Tailscale: `ssh root@100.74.46.120` — password: `Joker1974!@#` |
| **Direct SSH** | Only from: 107.178.2.130 / 97.154.109.245 |
| **Purpose** | VoIP phone system — handles all inbound/outbound calls |
**Web UI:** `https://fusion.orbishosting.com`
Login: `admin / fY7XP5swgtpbzrYLhkeVYkA4744`
**Database:** PostgreSQL
User: `fusionpbx` / Password: `pSJaF9mUJqPr4Sj5mwJyRqvCCpc` / Host: 127.0.0.1
**SIP Trunk:** SignalWire
DID: +1 (817) 764-5007
Gateway: `signalwire` on external profile (port 5080, UDP)
**How calls flow:**
```
Caller → SignalWire SIP → FusionPBX:5080 → IVR (ext 900) → Ring extensions
Outbound: Phone → FusionPBX:5080 → SignalWire → PSTN
```
**SSH Relay Command:**
```bash
sshpass -p 'Gonewalk1974!@#' ssh -o StrictHostKeyChecking=no root@165.22.1.228 \
'sshpass -p "Joker1974!@#" ssh -o StrictHostKeyChecking=no root@134.209.72.226 "COMMAND"'
```
---
## 3. ON-PREMISE — PROXMOX HYPERVISORS
### PVE1 — Primary Hypervisor
| Field | Value |
|-------|-------|
| **Local IP** | 10.48.200.90 |
| **External** | orbisne.fortiddns.com (FortiGate DDNS — auto-updates on WAN IP change) |
| **OS** | Proxmox VE 8.x |
| **SSH** | `ssh root@orbisne.fortiddns.com` OR `ssh root@10.48.200.90` — password: `Joker1974!!!` |
| **Web UI** | `https://orbisne.fortiddns.com:8006``root / Joker1974!!!` |
| **Purpose** | Runs VMs 101, 112, 113, 118, 120, 210, CT110 |
**Useful commands:**
```bash
qm list # list all VMs
qm start/stop/restart <VMID> # control VMs
qm guest exec <VMID> -- bash -c "cmd" # run command inside VM (requires QEMU agent)
```
**JARVIS API Token:** `root@pam!jarvis=c45b5feb-f9a9-445d-a626-14fbb959f78b`
---
### PVE2 — Secondary Hypervisor
| Field | Value |
|-------|-------|
| **Local IP** | 10.48.200.91 |
| **OS** | Proxmox VE 8.x |
| **SSH** | `ssh root@10.48.200.91` — password: `Joker1974!!!` |
| **Web UI** | `https://10.48.200.91:8006``root / Joker1974!!!` |
| **Purpose** | Runs VM 302 (NetworkBackup); part of shared Proxmox cluster with PVE1 |
---
## 4. ON-PREMISE — VIRTUAL MACHINES
### VM 101 — Home Assistant (PVE1)
| Field | Value |
|-------|-------|
| **IP** | 10.48.200.97 |
| **OS** | Ubuntu + Home Assistant OS/Supervised |
| **Web UI** | `http://orbisne.fortiddns.com:8123``myron / [HA password]` |
| **SSH** | Via HA web terminal only (Settings → Add-ons → Advanced SSH & Web Terminal) |
| **Purpose** | Smart home automation — 212 entities (lights, switches, scenes, sensors) |
| **JARVIS Agent** | ID: `homeassistant_ha` — pushes entity states to JARVIS every 10s |
**JARVIS ↔ HA Integration:**
- HA custom component at `/config/custom_components/jarvis_agent/`
- Pushes all entity state changes to JARVIS `/api/agent/ha_state` (debounced 2s)
- JARVIS admin toggles → queued in `agent_commands` table → HA executes natively
- HA Long-lived Token (Jarvis2): `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIzNmI0N2I1Njk5ZGQ0MTQ2ODMwZWFmYjZiYTQ1MjJkMSIsImlhdCI6MTc4MDIwMzU5NCwiZXhwIjoyMDk1NTYzNTk0fQ.sYRok-jRDlA4lFgWxLQELcEjkJNGQdprk6ZziLwLtXE`
---
### VM 112 — Jellyfin Media Server (PVE1)
| Field | Value |
|-------|-------|
| **IP** | 10.48.200.33 |
| **OS** | Ubuntu 22.04 LTS |
| **SSH** | `ssh root@10.48.200.33` — password: `Joker1974!!!` (enabled 2026-06-14) |
| **Web UI** | `http://10.48.200.33:8096` |
| **Purpose** | Media streaming server — Movies and TV shows |
| **JARVIS Agent** | Not yet installed |
**Media Libraries:**
- Movies: `/mnt/mediastack/movies` — NFS from MediaStack (10.48.200.35:/media/movies)
- TV: `/mnt/mediastack/tv` — NFS from MediaStack (10.48.200.35:/media/tv)
**NFS chain:** Jellyfin → MediaStack → Synology NAS (`/volume1/video/movies` and `/volume1/video/tv`)
**Admin token:** `7c0ccf78b91d4b5bafa607f585f24f2d`
**If library scan needed:**
```bash
curl -X POST "http://10.48.200.33:8096/Library/Refresh" \
-H "X-Emby-Token: 7c0ccf78b91d4b5bafa607f585f24f2d"
```
**If NFS stale after MediaStack changes:**
```bash
umount -l /mnt/mediastack/movies && umount -l /mnt/mediastack/tv
mount /mnt/mediastack/movies && mount /mnt/mediastack/tv
```
---
### VM 113 — MediaStack (PVE1)
| Field | Value |
|-------|-------|
| **IP** | 10.48.200.35 |
| **OS** | Ubuntu 24.04 LTS |
| **SSH** | Via PVE1: `ssh -i /root/.ssh/id_rsa root@10.48.200.35` (no direct access from DO) |
| **Purpose** | Automated media download pipeline + NFS server to Jellyfin |
| **JARVIS Agent** | ID: `MediaStack_2c00b1b8` |
**Services:**
| Service | Port | Login | API Key |
|---------|------|-------|---------|
| qBittorrent | :8080 | `admin / Joker1974!!!` | — |
| Sonarr | :8989 | `admin / Joker1974!!!` | `b43e04350a594846b4ee95261c29e9e0` |
| Radarr | :7878 | `admin / Joker1974!!!` | `53c4268360444feeae5f98c0cc24e0e3` |
| Prowlarr | :9696 | `admin / Joker1974!!!` | `9d0ce6c5660743b5bf1c7951efc62252` |
**All services run as root** — required by Synology NFS ACL (only root can write).
**VPN:** NordVPN — `nordlynx` WireGuard interface — exit IP 181.214.226.188 (US Dallas)
All download traffic exits via NordVPN. If downloads stall, check: `ip rule show` for rules 32764/32765.
**Media Flow:**
```
IPTorrents (Prowlarr) → Sonarr/Radarr search → qBittorrent download
→ /mnt/nas/video/downloads (NAS)
→ Sonarr/Radarr import → /mnt/nas/video/tv or /mnt/nas/video/movies (NAS)
→ NFS → Jellyfin /mnt/mediastack/movies or /mnt/mediastack/tv
```
**Indexer:** IPTorrents via Prowlarr cookie auth
Cookie: `uid=2237410; pass=JzLP2niTWxBJAZIU3yvtLbJzD55kdLeB`
(Expires — if search fails, log into iptorrents.com, copy uid+pass cookies)
**If Radarr/Sonarr shows "0 active indexers":**
```bash
systemctl stop radarr
sqlite3 /var/lib/radarr/radarr.db "DELETE FROM IndexerStatus WHERE ProviderId=1;"
systemctl start radarr
```
**SSH from DO:**
```bash
sshpass -p 'Joker1974!!!' ssh -o StrictHostKeyChecking=no root@10.48.200.90 \
'ssh -o StrictHostKeyChecking=no -i /root/.ssh/id_rsa root@10.48.200.35 "COMMAND"'
```
---
### VM 118 — Homebridge (PVE1)
| Field | Value |
|-------|-------|
| **IP** | 10.48.200.18 |
| **OS** | Linux |
| **SSH** | `ssh myron@10.48.200.18` — password: `Joker1974!` |
| **Purpose** | Apple HomeKit bridge — exposes non-HomeKit devices to Apple Home app |
| **JARVIS Agent** | ID: `homebridge_b57cbaea` |
---
### VM 120 — NovaCPX Hosting Panel (PVE1)
| Field | Value |
|-------|-------|
| **IP** | 10.48.200.110 |
| **OS** | Ubuntu 24.04 LTS |
| **SSH** | `ssh root@10.48.200.110` — password: `Joker1974!!!` (direct, no PVE hop) |
| **Purpose** | Custom web hosting control panel (cPanel alternative), v1.0.27 |
| **JARVIS Agent** | ID: `novacpx_e3b07264` |
**Ports:**
| Port | Panel |
|------|-------|
| :8880 | User panel |
| :8881 | Reseller panel |
| :8882 | Admin panel |
| :8883 | Roundcube webmail |
**Admin:** `https://10.48.200.110:8882``admin / Admin2026!`
**phpMyAdmin:** `http://10.48.200.110/phpmyadmin`
**File Paths:**
- Web root: `/srv/novacpx/public/`
- DB (SQLite): `/var/lib/novacpx/panel.db`
- Config: `/etc/novacpx/config.ini`
- Git repo: `/opt/novacpx-src/`
- GitHub: `myronblair/novacpx` (auto-deploy on push to `main`)
---
### VM 210 — Ollama Local LLM + Vision (PVE1)
| Field | Value |
|-------|-------|
| **IP** | 10.48.200.210 |
| **OS** | Ubuntu (cloud image) |
| **SSH** | `ssh root@10.48.200.210` via PVE1 hop — password: `Joker1974!!!` |
| **Purpose** | Local AI inference — chat (llama3.1:8b) + vision (llava:7b) |
| **API** | `http://10.48.200.210:11434` (Ollama REST API) |
| **JARVIS Agent** | ID: `ollama-ai_ubuntu` |
| **Models** | `llama3.1:8b` (chat/Tier 1), `llava:7b` (vision cascade) |
**JARVIS uses this as Tier 1 AI** — if Ollama is down, falls back to Groq (cloud).
**Vision cascade:** Arc Reactor calls Claude first; if Claude credits depleted, falls back to llava:7b via Ollama.
Vision is enabled via: `/etc/systemd/system/jarvis-arc.service.d/vision.conf``OLLAMA_VISION_MODEL=llava:7b`
---
### VM 302 — NetworkBackup (PVE2)
| Field | Value |
|-------|-------|
| **IP** | 10.48.200.99 |
| **OS** | Ubuntu/Linux |
| **SSH** | `ssh myron@10.48.200.99` — password: `Joker1974!` (then `sudo`) |
| **Purpose** | Network backup storage / backup operations |
| **JARVIS Agent** | ID: `networkbackup_NetworkB` |
---
### CT110 — WireGuard Exit Container (PVE1)
| Field | Value |
|-------|-------|
| **IP** | 10.48.200.19 / 10.48.200.67 |
| **Purpose** | Legacy WireGuard exit tunnel to DO (10.200.0.4 via wg-exit) — currently NOT used by MediaStack/Jellyfin |
| **Note** | MediaStack uses NordVPN directly; Jellyfin uses wg1 peer on MediaStack for NFS only |
---
## 5. NAS STORAGE
### Synology NAS
| Field | Value |
|-------|-------|
| **IP** | 10.48.200.249 |
| **Login** | `nas / Joker1974!!!` |
| **DSM Web UI** | `http://10.48.200.249:5000` |
| **Purpose** | Primary media and download storage |
**NFS Share:** `/volume1/video` (exported to MediaStack only)
**Directory structure:**
```
/volume1/video/
movies/ ← Radarr imports here; NFS-exported to Jellyfin via MediaStack
tv/ ← Sonarr imports here; NFS-exported to Jellyfin via MediaStack
downloads/ ← qBittorrent downloads here (temp)
incomplete/ ← in-progress torrents
```
**Important:** Synology NFS ACL only allows root to write. All services on MediaStack run as root.
---
## 6. WEBSITES (ALL ON DO)
All sites are at `/home/<domain>/public_html/` on DO (165.22.1.228).
**Auto-deploy:** Push to `main` on GitHub → webhook → server pulls in ~1 min.
**GitHub PAT:** `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` (expires ~2026-08-20)
---
### jarvis.orbishosting.com — JARVIS AI Dashboard (MOVED TO PVE1 VM 211)
| Field | Value |
|-------|-------|
| **URL** | http://jarvis.orbishosting.com:1972 |
| **Path** | `/var/www/jarvis/ (on JARVIS VM 10.48.200.211)` |
| **GitHub** | `myronblair/jarvis` |
| **Login** | `myron / Joker1974!!!` |
| **Purpose** | Iron Man-style AI home dashboard with voice control, smart home, media, planner |
See Section 7 for full JARVIS details.
---
### tomsjavajive.com — Tom's Java Jive
| Field | Value |
|-------|-------|
| **URL** | https://tomsjavajive.com |
| **Path** | `/home/tomsjavajive.com/public_html/` |
| **GitHub** | `myronblair/tomsjavajive` |
| **Purpose** | Coffee shop e-commerce — products, orders, loyalty, wallet, reviews |
| **Admin URL** | `https://tomsjavajive.com/admin/` |
| **Admin Login** | `admin@tomsjavajive.com / Joker1974!!!` OR `myronblair@outlook.com / Joker1974!!!` |
| **DB** | `toms_tjj_db / toms_tjj_user / +60wlPc+55e@gFq4` |
| **Email** | CyberMail API key: `sk_live_7f9b0f9a29f6de31a0d229d4af75d56b094ad724fc58a57d` |
| **Email From** | `noreply@tomsjavajive.com` / `Toms Java Jive` (set in DB settings table) |
---
### epictravelexpeditions.com — Epic Travel Expeditions
| Field | Value |
|-------|-------|
| **URL** | https://epictravelexpeditions.com |
| **Path** | `/home/epictravelexpeditions.com/public_html/` |
| **GitHub** | `myronblair/epictravelexpeditions` |
| **Purpose** | Travel booking / expeditions website |
| **DB** | `epic_travel_db` (see `api/config.php`) |
---
### parkerslingshot.epictravelexpeditions.com — Parker Slingshot (OLD)
| Field | Value |
|-------|-------|
| **URL** | https://parkerslingshot.epictravelexpeditions.com |
| **Path** | `/home/epictravelexpeditions.com/parkerslingshot/` |
| **GitHub** | `myronblair/parkerslingshot` |
| **Purpose** | Old slingshot rental site (superseded by parkerslingshotrentals.com) |
---
### parkerslingshotrentals.com — Parker Slingshot Rentals (LIVE)
| Field | Value |
|-------|-------|
| **URL** | https://www.parkerslingshotrentals.com |
| **Path** | `/home/parkerslingshotrentals.com/public_html/` |
| **GitHub** | `myronblair/parkerslingshotrentals` |
| **Purpose** | Polaris Slingshot rental — bookings, e-signature waiver, admin management |
| **Admin** | `/admin/index.php``admin / Parker2026!` |
| **DB** | `park_slingshot / park_slingshotuser / 4@rxg*8kovxCr7w6` |
| **Square** | Production token: `EAAAl3FsAu_2ri8kZE_ENEyi2T_C8HXXm5XQFY6Lbnd8SX6FqYp8J_upUeXNYh7v` |
---
### orbishosting.com — Orbis Hosting (Landing Page)
| Field | Value |
|-------|-------|
| **URL** | https://orbishosting.com |
| **Path** | `/home/orbishosting.com/public_html/` |
| **GitHub** | `myronblair/orbishosting` |
| **Purpose** | Public landing page for Orbis Hosting brand |
---
### orbis.orbishosting.com — Orbis Hosting Portal
| Field | Value |
|-------|-------|
| **URL** | https://orbis.orbishosting.com |
| **Path** | `/home/orbis.orbishosting.com/public_html/` |
| **GitHub** | `myronblair/orbis-hosting-portal` |
| **Purpose** | Customer-facing hosting portal |
---
### tomtomgames.com — TomTom Games
| Field | Value |
|-------|-------|
| **URL** | https://tomtomgames.com |
| **Path** | `/home/tomtomgames.com/public_html/` |
| **GitHub** | `myronblair/tomtomgames` |
| **Purpose** | Gaming website |
| **DB** | `tomtom_games_db` (see config) |
| **Email** | CyberMail API key: `sk_live_7f9b...` |
---
## 7. JARVIS AI SYSTEM
**URL:** http://jarvis.orbishosting.com:1972
**Files:** `/var/www/jarvis/` on JARVIS VM (PVE1 VM 211 — 10.48.200.211, 8 cores, 16GB RAM)
**DB:** `jarvis_db``jarvis_user / J4rv1s_Pr0t0c0l_2026!`
**Login:** `myron / Joker1974!!!`
**Admin portal:** http://jarvis.orbishosting.com:1972/admin
### Architecture (end-to-end)
```
Voice (browser mic)
→ SpeechRecognition API
→ Wake phrase: "wake up JARVIS" / "daddy's home"
→ "JARVIS [command]" triggers action
→ /api/chat.php (4-tier AI)
Tier 0.7: KB intents / planner (tasks, appointments)
Tier 1: Knowledge Base (MySQL)
Tier 1.5: Ollama (10.48.200.210:11434, llama3.1:8b) — local LLM
Vision: Ollama llava:7b (via Arc Reactor _vision_call cascade)
Tier 2: Groq (cloud, model: compound-beta-mini)
Tier 3: Claude API (Anthropic, fallback)
→ ElevenLabs TTS → browser speaker
```
### Arc Reactor (AI Job Processor)
**Service:** `jarvis-arc` (systemd) — port 7474
**Runtime:** `/opt/jarvis-arc/` (Python venv, `reactor.py`)
**Log:** `/var/log/jarvis/arc.log`
**Admin button:** Workers → Daemons → `SETUP` (live popup) / `RESTART`
**Vision:** Claude → Ollama llava:7b → graceful fallback
**Vision config:** `/etc/systemd/system/jarvis-arc.service.d/vision.conf`
```bash
systemctl status jarvis-arc
systemctl restart jarvis-arc
journalctl -u jarvis-arc -f
```
To re-deploy Arc Reactor from source:
Use **Workers → Daemons → SETUP** in JARVIS admin (live log popup shows progress).
### Deploy Pipeline
```
Code edit → git push → GitHub webhook → /webhook.php (HMAC verified)
→ /tmp/jarvis-deploy-queue.txt → /usr/local/bin/jarvis-deploy.sh (cron 1min)
→ git pull + PHP syntax check → deploy or auto-revert
```
Webhook secret: `4c8805f0285214ff0a0602b5880270b935f36a896946c7f1`
### Agent System
Agents installed on all servers — phone home every 10s (heartbeat) / 30s (metrics).
Registration key: `f846a9aaf7ce9a61742c63c87c4186052a71d2a580c65518`
Install command: `curl -sk http://10.48.200.211/install-agent.sh | bash -s <hostname> <linux|proxmox>`
### Self-Healing Watchdog
`/usr/local/bin/jarvis-watchdog.sh` — runs every 5 min (root cron on DO)
Restarts: lsws, mysql, redis if down
Restarts offline Proxmox VM agents via `qm guest exec`
### Cron Jobs (DO server)
| Schedule | Script | Purpose |
|----------|--------|---------|
| Every 1 min | `jarvis-deploy.sh` | Process GitHub deploy queue |
| Every 3 min | `facts_collector.php` | Collect agent metrics, KB facts, site health |
| Every 5 min | `stats_cache.php` | Weather, news, Proxmox stats refresh |
| Every 5 min | `jarvis-watchdog.sh` | Self-healing: restart dead services |
---
## 8. PHONE SYSTEM (FUSIONPBX)
### Extensions
| Ext | Name | Phone | IP | SIP Password |
|-----|------|-------|----|-------------|
| 1000 | Myron Blair — Desk | Yealink T48S | 10.48.200.2 | `Xk9mPw3nQv7rLs2t` |
| 1001 | Tommy Ivy — Desk | Yealink T48S | 10.48.200.43 | `Tv8xNm4pWq6rZs3k` |
| 1002 | Myron Blair — WiFi Work | Yealink AX86R | 10.48.200.65 | `yXHaJTwa8rj?$GkrVFQB` |
| 1003 | Kitchen | Yealink T57W | 10.48.200.83 | — |
| 1004 | Master Bedroom | Yealink T57W | 10.48.200.85 | — |
| 1010 | Parker County Slingshot | Virtual (voicemail only) | — | — |
| 1011 | Epic Travel Expeditions | Virtual (voicemail only) | — | — |
| 1012 | Tom's Java Jive | Virtual (voicemail only) | — | — |
| 900 | IVR | — | — | (auto-attendant) |
**Phone SIP Settings (all phones):**
- Server: `134.209.72.226`
- Port: `5080`
- Transport: UDP
**Provisioning URL:** `https://fusion.orbishosting.com/app/provision/`
(Username: `provision-master`, Password: `Joker1974!!!`)
### Call Flow
```
Inbound (+18177645007)
→ SignalWire → FusionPBX:5080 (UDP)
→ signalwire-inbound dialplan (catch-all ^.*$)
→ IVR ext 900 (ivr_menu_16k.wav)
→ Routes to extensions 1000/1001/1002/1003/1004
Outbound
→ Phone → FusionPBX:5080
→ signalwire gateway → SignalWire → PSTN
```
### FreeSWITCH CLI Commands
```bash
fs_cli -x "sofia status profile external reg" # check registrations
fs_cli -x "sofia xmlstatus gateway" # check SignalWire gateway
fs_cli -x "reloadxml" # reload config (safe)
fs_cli -x "reloadacl" # reload ACL (safe)
# AVOID: sofia profile external restart (drops all phone registrations)
```
---
## 9. NETWORKING & VPN
### FortiGate Firewall
- WAN IP: 97.154.109.245 (dynamic)
- DDNS: `orbisne.fortiddns.com` (FortiGate auto-updates on IP change)
- Blocks: outbound port 53 (DNS) — MediaStack uses PVE1 dnsmasq (10.48.200.90) as resolver → 100.100.100.100
**Port Forwards:**
| External Port | Internal Destination | Purpose |
|--------------|---------------------|---------|
| :8006 | PVE1:8006 | Proxmox web UI |
| :8123 | HA VM:8123 | Home Assistant |
| :22 | HA VM:22 | HA SSH (unreliable) |
### WireGuard — Jellyfin ↔ MediaStack
- MediaStack runs WireGuard server on `wg1` (port 51820, subnet 10.200.0.1/24)
- Jellyfin peer: 10.200.0.3 (active handshake)
- Used for NFS media file access ONLY — not internet VPN
### NordVPN — MediaStack Internet Traffic
- Interface: `nordlynx` on MediaStack
- Exit IP: 181.214.226.188 (US Dallas)
- Policy routing: table 205 (all traffic via nordlynx), managed by `nordvpn-routing.service`
- Required for IPTorrents access (blocks non-VPN IPs)
---
## 10. BACKUP SYSTEMS
### JARVIS Database Backup
- **Script:** `/usr/local/bin/jarvis-backup.sh` (also at `/var/www/jarvis/deploy/`)
- **Output:** `/var/backups/jarvis/jarvis_backup_TIMESTAMP.tar.gz`
- **Log:** `/var/backups/jarvis/backup.log`
- **Retention:** 7 days (auto-purge)
- **Trigger:** JARVIS admin → Backups → RUN BACKUP NOW, or run script directly
- **DB:** `jarvis_db``jarvis_user / J4rv1s_Pr0t0c0l_2026!`
### DO Server Backup
- **Repo:** `myronblair/do-server-config`
- **Schedule:** Weekly, Sunday 4am
- **Launcher:** `/usr/local/bin/do-server-backup` on DO
- **Covers:** Scripts, systemd units, WireGuard, OLS vhosts, cron, MySQL credentials
- **Restore:** 8-phase wizard in `restore.sh`
- **DB backups:** `jarvis-backup.sh` runs daily (separate)
### Proxmox Config Backup
- **Repo:** `myronblair/proxmox-config`
- **Schedule:** Weekly, Sunday 3am (both PVE1 and PVE2)
- **Launcher:** `/usr/local/bin/proxmox-backup` on each node
- **Covers:** VM .conf files, network, cron, systemd, scripts
- **VM disks:** Covered by Proxmox Backup Server (PBS)
### FusionPBX Backup
- **Repo:** `myronblair/fusionpbx-config`
- **Schedule:** Weekly, Sunday 5am
- **Launcher:** `/usr/local/bin/fusionpbx-backup`
- **Covers:** PostgreSQL dump (gzip, ~29MB) + FreeSWITCH configs
- **Restore:** 10-phase wizard in `restore.sh`
---
## 11. SSH QUICK REFERENCE
```bash
# DO (main web server)
sshpass -p 'Gonewalk1974!@#' ssh -o StrictHostKeyChecking=no root@165.22.1.228
# FusionPBX (must relay via DO)
sshpass -p 'Gonewalk1974!@#' ssh root@165.22.1.228 \
'sshpass -p "Joker1974!@#" ssh root@134.209.72.226 "CMD"'
# PVE1 (direct or via DDNS)
sshpass -p 'Joker1974!!!' ssh -o StrictHostKeyChecking=no root@orbisne.fortiddns.com
sshpass -p 'Joker1974!!!' ssh -o StrictHostKeyChecking=no root@10.48.200.90
# PVE2
sshpass -p 'Joker1974!!!' ssh -o StrictHostKeyChecking=no root@10.48.200.91
# MediaStack (via PVE1)
sshpass -p 'Joker1974!!!' ssh root@10.48.200.90 \
'ssh -i /root/.ssh/id_rsa root@10.48.200.35 "CMD"'
# Jellyfin (direct, password enabled 2026-06-14)
sshpass -p 'Joker1974!!!' ssh -o StrictHostKeyChecking=no root@10.48.200.33
# NovaCPX (direct)
sshpass -p 'Joker1974!!!' ssh -o StrictHostKeyChecking=no root@10.48.200.110
# Ollama / Homebridge / NetworkBackup (myron user, then sudo)
sshpass -p 'Joker1974!' ssh myron@10.48.200.95 # Ollama
sshpass -p 'Joker1974!' ssh myron@10.48.200.18 # Homebridge
sshpass -p 'Joker1974!' ssh myron@10.48.200.99 # NetworkBackup
# Run command inside VM via Proxmox (requires QEMU agent installed)
sshpass -p 'Joker1974!!!' ssh root@10.48.200.90 \
'qm guest exec 210 -- bash -c "CMD"'
```
**Password fallback order:** `Joker1974!@#``Joker1974!!!``Joker1974!`
---
## 12. CRITICAL CREDENTIALS MASTER LIST
### SSH / Root Access
| System | User | Password | Notes |
|--------|------|----------|-------|
| DO (165.22.1.228) | root | `Gonewalk1974!@#` | Main web server |
| FusionPBX (134.209.72.226) | root | `Joker1974!@#` | Via DO relay |
| PVE1 (10.48.200.90) | root | `Joker1974!!!` | Also via DDNS |
| PVE2 (10.48.200.91) | root | `Joker1974!!!` | |
| MediaStack (10.48.200.35) | root | key only | Via PVE1 (`/root/.ssh/id_rsa`) |
| Jellyfin (10.48.200.33) | root | `Joker1974!!!` | Enabled 2026-06-14 |
| NovaCPX (10.48.200.110) | root | `Joker1974!!!` | Direct SSH works |
| Ollama / Homebridge / Backup VMs | myron | `Joker1974!` | Then sudo |
### Web Panels & Admin
| System | URL | User | Password |
|--------|-----|------|----------|
| CyberPanel | https://165.22.1.228:8090 | myron | `Joker1974!!!` |
| phpMyAdmin (DO) | https://165.22.1.228/phpmyadmin | myron | `Joker1974!!!` |
| Proxmox PVE1 | https://orbisne.fortiddns.com:8006 | root | `Joker1974!!!` |
| Proxmox PVE2 | https://10.48.200.91:8006 | root | `Joker1974!!!` |
| JARVIS | http://jarvis.orbishosting.com:1972 | myron | `Joker1974!!!` |
| JARVIS Admin | http://jarvis.orbishosting.com:1972/admin | myron | `Joker1974!!!` |
| FusionPBX | https://fusion.orbishosting.com | admin | `fY7XP5swgtpbzrYLhkeVYkA4744` |
| Home Assistant | http://orbisne.fortiddns.com:8123 | myron | (HA password) |
| NovaCPX Admin | https://10.48.200.110:8882 | admin | `Admin2026!` |
| Jellyfin | http://10.48.200.33:8096 | — | token: `7c0ccf78b91d4b5bafa607f585f24f2d` |
| qBittorrent | http://10.48.200.35:8080 | admin | `Joker1974!!!` |
| Sonarr | http://10.48.200.35:8989 | admin | `Joker1974!!!` |
| Radarr | http://10.48.200.35:7878 | admin | `Joker1974!!!` |
| Prowlarr | http://10.48.200.35:9696 | admin | `Joker1974!!!` |
| Synology NAS | http://10.48.200.249:5000 | nas | `Joker1974!!!` |
| Parker Slingshot Admin | https://parkerslingshotrentals.com/admin | admin | `Parker2026!` |
| TJJ Admin | https://tomsjavajive.com/admin | `admin@tomsjavajive.com` OR `myronblair@outlook.com` | `Joker1974!!!` |
### Databases
| Site | DB Name | DB User | DB Password |
|------|---------|---------|-------------|
| JARVIS | `jarvis_db` | `jarvis_user` | `J4rv1s_Pr0t0c0l_2026!` |
| Tom's Java Jive | `toms_tjj_db` | `toms_tjj_user` | `+60wlPc+55e@gFq4` |
| Parker Slingshot Rentals | `park_slingshot` | `park_slingshotuser` | `4@rxg*8kovxCr7w6` |
| Epic Travel | `epic_travel_db` | (see config.php) | (see config.php) |
| Epic/Parker Slingshot | `epic_parkersling` | `epic_parkersling` | `Joker1974!!!` |
| NovaCPX | SQLite: `/var/lib/novacpx/panel.db` | — | — |
| FusionPBX | PostgreSQL | `fusionpbx` | `pSJaF9mUJqPr4Sj5mwJyRqvCCpc` |
| MySQL root (DO) | — | root | `b71e5c1a8c7457541b9c1db822de37adfa271926a38b6c20` |
### API Keys
| Service | Key |
|---------|-----|
| GitHub PAT | `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` (exp ~2026-08-20) |
| JARVIS Agent Registration | `f846a9aaf7ce9a61742c63c87c4186052a71d2a580c65518` |
| Proxmox API Token | `root@pam!jarvis=c45b5feb-f9a9-445d-a626-14fbb959f78b` |
| HA Long-lived Token | `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIzNmI0N2I1Njk5ZGQ0MTQ2ODMwZWFmYjZiYTQ1MjJkMSIsImlhdCI6MTc4MDIwMzU5NCwiZXhwIjoyMDk1NTYzNTk0fQ.sYRok-jRDlA4lFgWxLQELcEjkJNGQdprk6ZziLwLtXE` |
| Sonarr API | `b43e04350a594846b4ee95261c29e9e0` |
| Radarr API | `53c4268360444feeae5f98c0cc24e0e3` |
| Prowlarr API | `9d0ce6c5660743b5bf1c7951efc62252` |
| Jellyfin Admin Token | `7c0ccf78b91d4b5bafa607f585f24f2d` |
| Square (Parker) Production | `EAAAl3FsAu_2ri8kZE_ENEyi2T_C8HXXm5XQFY6Lbnd8SX6FqYp8J_upUeXNYh7v` |
| Square App ID (Parker) | `sq0idp-YSM7BU9IVyOWSzpeP-0nzQ` |
| Webhook HMAC Secret | `4c8805f0285214ff0a0602b5880270b935f36a896946c7f1` |
### SIP / Phone
| Extension | Name | SIP Password |
|-----------|------|-------------|
| 1000 | Myron Blair — Desk (10.48.200.2) | `Xk9mPw3nQv7rLs2t` |
| 1001 | Tommy Ivy — Desk (10.48.200.43) | `Tv8xNm4pWq6rZs3k` |
| 1002 | Myron Blair — WiFi Work (10.48.200.65) | `yXHaJTwa8rj?$GkrVFQB` |
| 1003 | Kitchen (10.48.200.83) | — |
| 1004 | Master Bedroom (10.48.200.85) | — |
| 1010 | Parker County Slingshot (voicemail only) | — |
| 1011 | Epic Travel Expeditions (voicemail only) | — |
| 1012 | Tom's Java Jive (voicemail only) | — |
---
*This document contains sensitive credentials. Store securely and do not share.*
+593 -39
View File
@@ -324,16 +324,35 @@ if ($action) {
// ── USERS ──────────────────────────────────────────────────────────── // ── USERS ────────────────────────────────────────────────────────────
case 'email_inbox': case 'email_inbox':
// Call via server's own IP — REMOTE_ADDR matches JARVIS_IP so auth bypass applies // Fixed 2026-07-07: this used to proxy to the OLD DO-hosted JARVIS
// (165.22.1.228/api/email with a spoofed Host header) — a leftover from
// before JARVIS moved to this VM (211). That endpoint no longer exists
// (404), so the inbox always failed. Reads the local email_triage table
// directly now, same source gmail_triage jobs already write to, matching
// the pattern email_action_items already used.
$acct = $_GET['account'] ?? 'all'; $acct = $_GET['account'] ?? 'all';
$force = !empty($_GET['force']) ? '&force=1' : ''; $where = '1=1'; $params = [];
$ch = curl_init('https://165.22.1.228/api/email?account=' . $acct . $force); if ($acct && $acct !== 'all') { $where .= ' AND account=?'; $params[] = $acct; }
curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>25, $rows = JarvisDB::query(
CURLOPT_SSL_VERIFYPEER=>false,CURLOPT_SSL_VERIFYHOST=>false, "SELECT account, from_name, from_email, subject, date_received, summary, category
CURLOPT_HTTPHEADER=>['Host: jarvis.orbishosting.com']]); FROM email_triage WHERE {$where} ORDER BY date_received DESC LIMIT 100",
$r = curl_exec($ch); $code = curl_getinfo($ch,CURLINFO_HTTP_CODE); curl_close($ch); $params
if($code===200 && $r) j(json_decode($r,true)); ) ?? [];
else j(['error'=>'Email fetch failed (HTTP '.$code.')']); $recent = array_map(function($r) {
return [
'account' => $r['account'],
'from_name' => $r['from_name'],
'from_email'=> $r['from_email'],
'subject' => $r['subject'],
'date' => $r['date_received'],
'preview' => $r['summary'],
'unread' => false,
];
}, $rows);
$actionCount = JarvisDB::single(
"SELECT COUNT(*) AS c FROM email_actions WHERE dismissed=0 AND task_id IS NULL AND appointment_id IS NULL"
);
j(['summary' => ['recent' => $recent], 'action_items_count' => (int)($actionCount['c'] ?? 0)]);
case 'email_action_items': case 'email_action_items':
$rows = JarvisDB::query("SELECT * FROM email_actions WHERE dismissed=0 AND task_id IS NULL AND appointment_id IS NULL ORDER BY received_at DESC LIMIT 100") ?? []; $rows = JarvisDB::query("SELECT * FROM email_actions WHERE dismissed=0 AND task_id IS NULL AND appointment_id IS NULL ORDER BY received_at DESC LIMIT 100") ?? [];
@@ -430,7 +449,7 @@ if ($action) {
case 'cal_feeds_list': case 'cal_feeds_list':
j(JarvisDB::query("SELECT * FROM calendar_feeds ORDER BY source,name") ?? []); j(JarvisDB::query("SELECT id,name,source,ics_url,username,(password != '' AND password IS NOT NULL) AS has_password,active,created_at FROM calendar_feeds ORDER BY source,name") ?? []);
case 'cal_feed_save': case 'cal_feed_save':
$id = (int)($_POST['id'] ?? 0); $id = (int)($_POST['id'] ?? 0);
@@ -474,7 +493,7 @@ if ($action) {
$latestVersions = [ $latestVersions = [
'linux' => '3.1', 'linux' => '3.1',
'proxmox' => '3.1', 'proxmox' => '3.1',
'windows' => '3.0', 'windows' => '3.2',
'macos' => '3.0', 'macos' => '3.0',
'homeassistant' => null, 'homeassistant' => null,
]; ];
@@ -491,9 +510,9 @@ if ($action) {
if (file_exists($cronLog)) { if (file_exists($cronLog)) {
$lines = array_filter(explode("\n", shell_exec("grep -a 'facts\\|stats\\|calendar\\|intent' " . escapeshellarg($cronLog) . " | tail -60"))); $lines = array_filter(explode("\n", shell_exec("grep -a 'facts\\|stats\\|calendar\\|intent' " . escapeshellarg($cronLog) . " | tail -60")));
foreach ($lines as $line) { foreach ($lines as $line) {
if (preg_match('/^\\[(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})\\].*facts/i', $line, $m)) $cronLast['facts_collector'] = $m[1]; if (preg_match('/^\\[?(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})\\]?.*facts/i', $line, $m)) $cronLast['facts_collector'] = $m[1];
if (preg_match('/^\\[(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})\\].*stats/i', $line, $m)) $cronLast['stats_cache'] = $m[1]; if (preg_match('/^\\[?(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})\\]?.*stats/i', $line, $m)) $cronLast['stats_cache'] = $m[1];
if (preg_match('/^\\[(\\d{2}{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})\\].*calendar/i', $line, $m)) $cronLast['calendar_sync'] = $m[1]; if (preg_match('/^\\[?(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})\\]?.*calendar/i', $line, $m)) $cronLast['calendar_sync'] = $m[1];
if (preg_match('/^\\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\\].*intent/i', $line, $m)) $cronLast['kb_intent_generator'] = $m[1]; if (preg_match('/^\\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\\].*intent/i', $line, $m)) $cronLast['kb_intent_generator'] = $m[1];
} }
} }
@@ -552,7 +571,7 @@ if ($action) {
} else { bad('Unknown cron worker'); } } else { bad('Unknown cron worker'); }
} elseif ($wType === 'daemon' && $wId === 'arc_reactor' && $wAction === 'restart') { } elseif ($wType === 'daemon' && $wId === 'arc_reactor' && $wAction === 'restart') {
shell_exec('systemctl restart jarvis-arc 2>&1'); shell_exec('systemctl restart jarvis-arc 2>&1');
k(['ok'=>true,'msg'=>'Arc Reactor restarting via systemd']); j(['ok'=>true,'msg'=>'Arc Reactor restarting via systemd']);
} elseif ($wType === 'daemon' && $wId === 'arc_reactor' && $wAction === 'setup') { } elseif ($wType === 'daemon' && $wId === 'arc_reactor' && $wAction === 'setup') {
$log = '/var/log/jarvis/arc-setup.log'; $log = '/var/log/jarvis/arc-setup.log';
$cmd = implode(' && ', [ $cmd = implode(' && ', [
@@ -567,7 +586,7 @@ if ($action) {
'systemctl restart jarvis-arc', 'systemctl restart jarvis-arc',
]); ]);
shell_exec("($cmd) >> " . escapeshellarg($log) . " 2>&1 &"); shell_exec("($cmd) >> " . escapeshellarg($log) . " 2>&1 &");
k(['ok'=>true,'msg'=>'Arc Reactor setup started — check ' . $log]); j(['ok'=>true,'msg'=>'Arc Reactor setup started — check ' . $log]);
} elseif ($wType === 'agent' && $wAction === 'update_status') { } elseif ($wType === 'agent' && $wAction === 'update_status') {
$ag = JarvisDB::single('SELECT version, status FROM registered_agents WHERE agent_id=?', [$wId]); $ag = JarvisDB::single('SELECT version, status FROM registered_agents WHERE agent_id=?', [$wId]);
j(['ok'=>true,'version'=>$ag['version']??null,'status'=>$ag['status']??'unknown']); j(['ok'=>true,'version'=>$ag['version']??null,'status'=>$ag['status']??'unknown']);
@@ -616,6 +635,113 @@ if ($action) {
} }
j(['lines'=>$out, 'next_line'=>$total]); j(['lines'=>$out, 'next_line'=>$total]);
// ── GENERIC WORKER LOG (live-popup support for Facts/Stats/Calendar) ────────
case 'worker_log':
$wKeywords = [
'facts_collector' => '/facts|system:|network:|proxmox:|ha:|do_server:|ollama:|sites:|nmap_scan:/i',
'stats_cache' => '/\[cache\]/i',
'calendar_sync' => '/calendar/i',
];
$wid = $_REQUEST['worker_id'] ?? '';
if (!isset($wKeywords[$wid])) { bad('Unknown worker'); }
$logFile = '/var/log/jarvis/cron.log';
$since = max(0, (int)($_GET['since'] ?? 0));
if (!file_exists($logFile)) { j(['lines'=>[],'next_line'=>0]); }
$allLines = file($logFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$total = count($allLines);
if (!empty($_GET['snapshot'])) { j(['next_line'=>$total]); }
$out = [];
for ($i = $since; $i < $total; $i++) {
if (preg_match($wKeywords[$wid], $allLines[$i])) $out[] = $allLines[$i];
}
j(['lines'=>$out, 'next_line'=>$total]);
case 'worker_history':
$wKeywords = [
'facts_collector' => '/facts|system:|network:|proxmox:|ha:|do_server:|ollama:|sites:|nmap_scan:/i',
'stats_cache' => '/\[cache\]/i',
'calendar_sync' => '/calendar/i',
];
$wid = $_REQUEST['worker_id'] ?? '';
if (!isset($wKeywords[$wid])) { bad('Unknown worker'); }
$logFile = '/var/log/jarvis/cron.log';
$result = ['last_success'=>null,'last_success_count'=>null,'failures'=>[]];
if (file_exists($logFile)) {
$lines = file($logFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$cutoff = time() - 7 * 86400;
foreach (array_reverse($lines) as $line) {
if (!preg_match($wKeywords[$wid], $line)) continue;
preg_match('/^\[?(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]?/', $line, $tm);
$ts = isset($tm[1]) ? strtotime($tm[1]) : 0;
if (!$result['last_success'] && preg_match('/done|complete|collected|synced/i', $line)) {
$result['last_success'] = $tm[1] ?? null;
}
if ($ts >= $cutoff && preg_match('/error|fail/i', $line)) {
$result['failures'][] = $line;
if (count($result['failures']) >= 20) break;
}
}
$result['failures'] = array_reverse($result['failures']);
}
j($result);
// ── KB GENERATOR TOPICS ─────────────────────────────────────────────
case 'kb_topics':
$where = ['1=1']; $params = [];
if (!empty($_GET['q'])) {
$q2 = '%'.$_GET['q'].'%';
$where[] = '(topic_name LIKE ? OR topic_id LIKE ? OR category LIKE ? OR description LIKE ?)';
$params = array_merge($params, [$q2,$q2,$q2,$q2]);
}
if (isset($_GET['cat']) && $_GET['cat'] !== '') { $where[] = 'category=?'; $params[] = $_GET['cat']; }
if (isset($_GET['active']) && $_GET['active'] !== '') { $where[] = 'active=?'; $params[] = (int)$_GET['active']; }
$topics = JarvisDB::query(
'SELECT id,topic_id,category,topic_name,description,active,run_count,last_run_at
FROM kb_generator_topics WHERE '.implode(' AND ',$where).' ORDER BY category,topic_name',
$params
);
$catRows = JarvisDB::query('SELECT DISTINCT category FROM kb_generator_topics ORDER BY category');
j(['topics'=>$topics, 'categories'=>array_column($catRows,'category')]);
case 'kb_topic_save':
$id = (int)($_POST['id'] ?? 0);
$tid = preg_replace('/[^a-z0-9_]/', '_', strtolower(trim($_POST['topic_id'] ?? '')));
$cat = trim($_POST['category'] ?? '');
$name = trim($_POST['topic_name'] ?? '');
$desc = trim($_POST['description'] ?? '');
$act = (int)!empty($_POST['active']);
if (!$tid || !preg_match('/[a-z0-9]/', $tid) || !$cat || !$name || !$desc)
bad('All fields required — topic_id must contain at least one letter or digit');
try {
if ($id) {
JarvisDB::execute(
'UPDATE kb_generator_topics SET topic_id=?,category=?,topic_name=?,description=?,active=?,updated_at=NOW() WHERE id=?',
[$tid,$cat,$name,$desc,$act,$id]
);
} else {
JarvisDB::execute(
'INSERT INTO kb_generator_topics (topic_id,category,topic_name,description,active) VALUES (?,?,?,?,?)',
[$tid,$cat,$name,$desc,$act]
);
}
} catch (\PDOException $e) {
bad(strpos($e->getMessage(), 'Duplicate entry') !== false
? 'topic_id already in use — choose a different slug'
: 'Database error');
}
j(['ok'=>true,'msg'=> $id ? 'Topic updated' : 'Topic created']);
case 'kb_topic_delete':
$id = (int)($_POST['id'] ?? 0); if (!$id) bad('Missing id');
JarvisDB::execute('DELETE FROM kb_generator_topics WHERE id=?', [$id]);
j(['ok'=>true]);
case 'kb_topic_toggle':
$id = (int)($_POST['id'] ?? 0); if (!$id) bad('Missing id');
JarvisDB::execute('UPDATE kb_generator_topics SET active=NOT active, updated_at=NOW() WHERE id=?', [$id]);
$row = JarvisDB::single('SELECT active FROM kb_generator_topics WHERE id=?', [$id]);
j(['ok'=>true,'active'=>(bool)$row['active']]);
case 'arc_setup_log': case 'arc_setup_log':
$logFile = '/var/log/jarvis/arc-setup.log'; $logFile = '/var/log/jarvis/arc-setup.log';
$since = max(0, (int)($_GET['since'] ?? 0)); $since = max(0, (int)($_GET['since'] ?? 0));
@@ -730,19 +856,44 @@ if ($action) {
$raw = curl_exec($ch); curl_close($ch); $raw = curl_exec($ch); curl_close($ch);
j(json_decode($raw, true) ?: ['error'=>'failed']); j(json_decode($raw, true) ?: ['error'=>'failed']);
// Added 2026-07-07: fetch a single outbox item WITH its body — outbox_list
// deliberately omits body for list-view performance, but VIEW needs the full text.
case 'outbox_get':
$id = (int)($_GET['id'] ?? 0); if (!$id) bad('Missing id');
$ch = curl_init('http://127.0.0.1:7474/comms/sent/' . $id);
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>5]);
$raw = curl_exec($ch); curl_close($ch);
j(json_decode($raw, true) ?: ['error'=>'failed']);
// Added 2026-07-07: closes the "compose creates a draft but nothing can ever
// send it" gap — dispatches a queued draft through the real send path.
case 'outbox_send':
$id = (int)($_GET['id'] ?? 0); if (!$id) bad('Missing id');
$ch = curl_init('http://127.0.0.1:7474/comms/sent/' . $id . '/send');
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>30, CURLOPT_POST=>true, CURLOPT_POSTFIELDS=>'']);
$raw = curl_exec($ch); curl_close($ch);
j(json_decode($raw, true) ?: ['error'=>'failed']);
case 'send_reply': case 'send_reply':
// Fixed 2026-07-07: sent 'content', but handle_send_email() reads 'body' —
// any edits made to the reply text in the UI were silently discarded and it
// always sent the original AI-drafted reply from email_triage instead.
$id = (int)($_GET['id'] ?? 0); if (!$id) bad('Missing triage id'); $id = (int)($_GET['id'] ?? 0); if (!$id) bad('Missing triage id');
$content = $_GET['content'] ?? ''; $content = $_GET['content'] ?? '';
$ch = curl_init('http://127.0.0.1:7474/job'); $ch = curl_init('http://127.0.0.1:7474/job');
curl_setopt_array($ch, [ curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 5, CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 5, CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['type'=>'send_email','payload'=>['triage_id'=>$id,'content'=>$content],'priority'=>8,'created_by'=>'admin']), CURLOPT_POSTFIELDS => json_encode(['type'=>'send_email','payload'=>['triage_id'=>$id,'body'=>$content],'priority'=>8,'created_by'=>'admin']),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
]); ]);
$raw = curl_exec($ch); curl_close($ch); $raw = curl_exec($ch); curl_close($ch);
j(json_decode($raw, true) ?: ['error' => 'Arc Reactor unreachable']); j(json_decode($raw, true) ?: ['error' => 'Arc Reactor unreachable']);
case 'compose_email': case 'compose_email':
// Fixed 2026-07-07: this was sending 'recipient'/'subject'/'auto_send', but
// reactor.py's handle_compose_email() reads 'to_email'/'subject_hint'/'send' —
// every compose dispatch silently produced a draft with blank recipient and
// subject, regardless of whether the LLM drafting step itself succeeded.
$to = $_GET['to'] ?? ''; if (!$to) bad('Missing recipient'); $to = $_GET['to'] ?? ''; if (!$to) bad('Missing recipient');
$subject = $_GET['subject'] ?? ''; $subject = $_GET['subject'] ?? '';
$body = $_GET['body'] ?? ''; $body = $_GET['body'] ?? '';
@@ -750,7 +901,7 @@ if ($action) {
$ch = curl_init('http://127.0.0.1:7474/job'); $ch = curl_init('http://127.0.0.1:7474/job');
curl_setopt_array($ch, [ curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 5, CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 5, CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['type'=>'compose_email','payload'=>['recipient'=>$to,'subject'=>$subject,'instructions'=>$body,'account'=>$account,'auto_send'=>false],'priority'=>7,'created_by'=>'admin']), CURLOPT_POSTFIELDS => json_encode(['type'=>'compose_email','payload'=>['to_email'=>$to,'subject_hint'=>$subject,'instructions'=>$body,'account'=>$account,'send'=>false],'priority'=>7,'created_by'=>'admin']),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
]); ]);
$raw = curl_exec($ch); curl_close($ch); $raw = curl_exec($ch); curl_close($ch);
@@ -1198,6 +1349,17 @@ if ($action) {
readfile($path); readfile($path);
exit; exit;
case 'docs_download':
$path = '/var/www/jarvis-private/INFRASTRUCTURE-REFERENCE.md';
if (!file_exists($path)) bad('File not found', 404);
header('Content-Type: text/markdown');
header('Content-Disposition: attachment; filename="INFRASTRUCTURE-REFERENCE.md"');
header('Content-Length: ' . filesize($path));
header('X-Accel-Buffering: no');
ob_end_clean();
readfile($path);
exit;
default: bad('Unknown action'); default: bad('Unknown action');
} }
} }
@@ -1270,6 +1432,14 @@ a{color:var(--cyan);text-decoration:none}
.stat-card .val{font-size:1.6rem;color:var(--cyan)} .stat-card .val{font-size:1.6rem;color:var(--cyan)}
.stat-card .sub{color:var(--text-dim);font-size:0.65rem;margin-top:4px} .stat-card .sub{color:var(--text-dim);font-size:0.65rem;margin-top:4px}
.stat-card .ok{color:var(--green)}.stat-card .warn{color:var(--yellow)}.stat-card .danger{color:var(--red)} .stat-card .ok{color:var(--green)}.stat-card .warn{color:var(--yellow)}.stat-card .danger{color:var(--red)}
/* Sites tab: domain names are much longer than typical stat labels (e.g.
parkerslingshotrentals.com) wider columns + no letter-spacing so they read cleanly. */
.site-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:12px;margin-bottom:24px}
.site-card{background:var(--surface);border:1px solid var(--border);padding:16px}
.site-card .lbl{color:var(--text-dim);font-size:0.68rem;letter-spacing:0.5px;margin-bottom:8px;white-space:normal;word-break:break-word;line-height:1.4}
.site-card .val{font-size:1.2rem;color:var(--cyan)}
.site-card .sub{color:var(--text-dim);font-size:0.65rem;margin-top:4px}
.site-card .ok{color:var(--green)}.site-card .danger{color:var(--red)}
/* ── TABLE ── */ /* ── TABLE ── */
.tbl-wrap{background:var(--surface);border:1px solid var(--border);overflow-x:auto} .tbl-wrap{background:var(--surface);border:1px solid var(--border);overflow-x:auto}
@@ -1498,6 +1668,7 @@ select.filter-sel:focus{border-color:var(--cyan)}
<button class="btn btn-sm btn-green" onclick="intentModal()">+ ADD INTENT</button> <button class="btn btn-sm btn-green" onclick="intentModal()">+ ADD INTENT</button>
<button class="btn btn-sm btn-yellow" onclick="intentTestModal()">TEST PATTERN</button> <button class="btn btn-sm btn-yellow" onclick="intentTestModal()">TEST PATTERN</button>
<button class="btn btn-sm" onclick="loadIntents()">REFRESH</button> <button class="btn btn-sm" onclick="loadIntents()">REFRESH</button>
<button class="btn btn-sm" style="border-color:#7f5fff;color:#7f5fff" onclick="openTopicsManager()">&#9881; MANAGE TOPICS</button>
<button class="btn btn-sm" style="border-color:#00d4ff;color:#00d4ff" onclick="runIntentGenerator()">&#9654; RUN GENERATOR</button> <button class="btn btn-sm" style="border-color:#00d4ff;color:#00d4ff" onclick="runIntentGenerator()">&#9654; RUN GENERATOR</button>
</div> </div>
</div> </div>
@@ -1586,7 +1757,7 @@ select.filter-sel:focus{border-color:var(--cyan)}
<div class="card" style="padding:24px;margin:20px 0"> <div class="card" style="padding:24px;margin:20px 0">
<div style="font-size:0.7rem;letter-spacing:2px;color:var(--cyan);margin-bottom:8px">INFRASTRUCTURE REFERENCE</div> <div style="font-size:0.7rem;letter-spacing:2px;color:var(--cyan);margin-bottom:8px">INFRASTRUCTURE REFERENCE</div>
<div style="color:var(--text-dim);font-size:0.75rem;margin-bottom:16px">Complete server map, credentials, deployment workflow, service configs, and phone system reference.</div> <div style="color:var(--text-dim);font-size:0.75rem;margin-bottom:16px">Complete server map, credentials, deployment workflow, service configs, and phone system reference.</div>
<a href="downloads/INFRASTRUCTURE-REFERENCE.md" download="INFRASTRUCTURE-REFERENCE.md" <a href="?action=docs_download" download="INFRASTRUCTURE-REFERENCE.md"
style="display:inline-block;padding:8px 20px;background:rgba(0,212,255,0.1);border:1px solid var(--cyan);color:var(--cyan);font-size:0.7rem;letter-spacing:2px;text-decoration:none"> style="display:inline-block;padding:8px 20px;background:rgba(0,212,255,0.1);border:1px solid var(--cyan);color:var(--cyan);font-size:0.7rem;letter-spacing:2px;text-decoration:none">
DOWNLOAD INFRASTRUCTURE-REFERENCE.MD DOWNLOAD INFRASTRUCTURE-REFERENCE.MD
</a> </a>
@@ -2113,6 +2284,12 @@ let _alertFilter = 'active';
let _modalCb = null; let _modalCb = null;
function esc(s){ return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); } function esc(s){ return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
// For values embedded inside a single-quoted JS string literal within an HTML attribute
// (e.g. onclick="fn('${escJs(x)}')"). Escaping the quote via esc() alone is NOT enough:
// the browser HTML-decodes the attribute before parsing it as JS, so &#39;/&quot; would
// just turn back into a literal ' before the JS parser ever sees it. Backslash-escaping
// survives that decode step, so JS-escape first, then HTML-escape on top.
function escJs(s){ return esc(String(s||'').replace(/\\/g,'\\\\').replace(/'/g,"\\'").replace(/\n/g,'\\n').replace(/\r/g,'\\r')); }
function ts(s){ if(!s) return '—'; const d=new Date(s); return d.toLocaleString('en-US',{month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'}); } function ts(s){ if(!s) return '—'; const d=new Date(s); return d.toLocaleString('en-US',{month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'}); }
function ago(s){ if(!s) return '—'; const sec=Math.floor((Date.now()-new Date(s))/1000); if(sec<60) return sec+'s ago'; if(sec<3600) return Math.floor(sec/60)+'m ago'; return Math.floor(sec/3600)+'h ago'; } function ago(s){ if(!s) return '—'; const sec=Math.floor((Date.now()-new Date(s))/1000); if(sec<60) return sec+'s ago'; if(sec<3600) return Math.floor(sec/60)+'m ago'; return Math.floor(sec/3600)+'h ago'; }
function fmtUp(s){ const d=Math.floor(s/86400),h=Math.floor((s%86400)/3600),m=Math.floor((s%3600)/60); return (d>0?d+'d ':'')+h+'h '+m+'m'; } function fmtUp(s){ const d=Math.floor(s/86400),h=Math.floor((s%86400)/3600),m=Math.floor((s%3600)/60); return (d>0?d+'d ':'')+h+'h '+m+'m'; }
@@ -2386,6 +2563,335 @@ async function runIntentGenerator() {
setTimeout(pollLog, 2000); setTimeout(pollLog, 2000);
} }
// Generic live-log popup for simple cron workers (Facts Collector, Stats Cache, Calendar Sync).
// Same pattern as runIntentGenerator() above, parameterized instead of duplicated per worker.
async function runWorkerWithLog(workerId, label, doneRegex) {
const modalHtml = `
<div style="font-family:var(--mono);font-size:0.72rem;line-height:1.8">
<div id="wl-history" style="background:#0a0f14;border:1px solid var(--border);border-radius:3px;padding:8px 12px;margin-bottom:10px;font-size:0.6rem;color:var(--text-dim)">Loading history...</div>
<div id="wl-status" style="color:var(--cyan);margin-bottom:8px;font-size:0.65rem;letter-spacing:1px">LAUNCHING...</div>
<div id="wl-log" style="background:#060a0e;border:1px solid var(--border);border-radius:3px;padding:10px 12px;min-height:160px;max-height:320px;overflow-y:auto;white-space:pre-wrap;font-size:0.62rem;line-height:1.7;color:var(--text-dim)">Waiting for first output...</div>
</div>`;
openModal(`&#9654; ${label.toUpperCase()}`, modalHtml, null, null);
document.getElementById('modalSave').textContent = 'CLOSE';
const logEl = document.getElementById('wl-log');
const statEl = document.getElementById('wl-status');
const historyEl = document.getElementById('wl-history');
let _stop = false, _done = false, lastLine = 0, dotted = 0;
const stopAndClose = () => {
_stop = true;
closeModal();
if (!_done) wToast(`${label} running in background`);
};
document.getElementById('modalSave').onclick = stopAndClose;
document.getElementById('modalClose').onclick = stopAndClose;
api('worker_history', {worker_id: workerId}).then(h => {
if (!historyEl) return;
let html = '';
if (h?.last_success) html += `<span style="color:var(--green)">&#10003; Last success: ${h.last_success}</span>`;
else html += '<span style="color:var(--text-dim)">No recorded successes in log</span>';
if (h?.failures?.length) {
html += `<br><span style="color:var(--red)">&#9888; ${h.failures.length} failure line(s) in last 7 days:</span>`;
h.failures.slice(-3).forEach(f => {
html += `<div style="color:var(--red);opacity:0.7;margin-left:8px">${f.replace(/</g,'&lt;')}</div>`;
});
}
historyEl.innerHTML = html || '<span>No history found</span>';
}).catch(() => { if (historyEl) historyEl.textContent = 'History unavailable'; });
const snap = await api('worker_log', {worker_id: workerId, snapshot:1});
lastLine = snap?.next_line ?? 0;
const kick = await api('worker_action', {worker_type:'cron', worker_id: workerId, waction:'run'});
if (!kick || !kick.ok) {
statEl.style.color = 'var(--red)';
statEl.textContent = 'LAUNCH FAILED: ' + (kick?.error || 'unknown error');
document.getElementById('modalSave').textContent = 'CLOSE';
return;
}
statEl.textContent = 'RUNNING — polling log every 3s...';
async function pollLog() {
if (_stop) return;
try {
const res = await api('worker_log', {worker_id: workerId, since: lastLine});
if (res && Array.isArray(res.lines) && res.lines.length) {
lastLine = res.next_line;
if (logEl.textContent === 'Waiting for first output...') logEl.textContent = '';
res.lines.forEach(line => {
const div = document.createElement('div');
const lower = line.toLowerCase();
if (lower.includes('error') || lower.includes('fail')) div.style.color = 'var(--red)';
else if (lower.includes('skip') || lower.includes('warn')) div.style.color = 'var(--yellow)';
else if (lower.includes('done') || lower.includes('complete') || lower.includes('ok')) div.style.color = 'var(--green)';
else div.style.color = 'var(--text-dim)';
div.textContent = line;
logEl.appendChild(div);
});
logEl.scrollTop = logEl.scrollHeight;
const joined = res.lines.join(' ').toLowerCase();
if (doneRegex.test(joined)) {
_done = true;
statEl.style.color = 'var(--green)';
statEl.textContent = 'COMPLETE';
document.getElementById('modalSave').textContent = 'CLOSE';
return;
}
dotted = 0;
} else {
dotted++;
if (dotted < 30) {
const waitDiv = document.createElement('div');
waitDiv.style.color = 'rgba(0,212,255,0.3)';
waitDiv.textContent = '· waiting for output' + '.'.repeat(dotted % 4);
const last = logEl.lastChild;
if (last && last.textContent && last.textContent.startsWith('· waiting')) {
logEl.replaceChild(waitDiv, last);
} else {
if (logEl.textContent === 'Waiting for first output...') logEl.textContent = '';
logEl.appendChild(waitDiv);
}
logEl.scrollTop = logEl.scrollHeight;
} else {
statEl.style.color = 'var(--yellow)';
statEl.textContent = 'RUNNING IN BACKGROUND (check cron log for results)';
return;
}
}
} catch(e) {}
if (!_stop) setTimeout(pollLog, 3000);
}
setTimeout(pollLog, 2000);
}
let _topicsData = [];
let _topicsCats = [];
async function openTopicsManager() {
const modalHtml = `
<div style="font-family:var(--mono);font-size:0.72rem">
<div id="tm-view-list">
<div style="display:flex;gap:8px;margin-bottom:8px;align-items:center;flex-wrap:wrap">
<input id="tm-search" type="text" placeholder="Search topics…" style="flex:1;min-width:140px;background:var(--bg2);border:1px solid var(--border);color:var(--fg);padding:5px 8px;font-size:0.7rem;border-radius:3px;font-family:inherit" oninput="tmFilter()">
<select id="tm-cat-filter" style="background:var(--bg2);border:1px solid var(--border);color:var(--fg);padding:5px 8px;font-size:0.7rem;border-radius:3px;font-family:inherit" onchange="tmFilter()">
<option value="">ALL CATEGORIES</option>
</select>
<select id="tm-active-filter" style="background:var(--bg2);border:1px solid var(--border);color:var(--fg);padding:5px 8px;font-size:0.7rem;border-radius:3px;font-family:inherit" onchange="tmFilter()">
<option value="">ALL STATUS</option>
<option value="1">ACTIVE</option>
<option value="0">DISABLED</option>
</select>
<button class="btn btn-sm btn-green" onclick="tmEditTopic(null)">+ ADD</button>
</div>
<div id="tm-stats" style="color:var(--text-dim);font-size:0.6rem;margin-bottom:6px;letter-spacing:0.5px"></div>
<div id="tm-list" style="max-height:370px;overflow-y:auto"><div class="loading">LOADING…</div></div>
</div>
<div id="tm-view-edit" style="display:none">
<div style="margin-bottom:12px">
<button class="btn btn-sm" onclick="tmShowList()">&#8592; BACK</button>
<span id="tm-edit-title" style="color:var(--cyan);margin-left:12px;font-size:0.65rem;letter-spacing:1px"></span>
</div>
<div style="display:grid;gap:8px">
<input type="hidden" id="tm-edit-id">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
<label style="font-size:0.6rem;color:var(--text-dim)">TOPIC ID (slug)
<input id="tm-edit-tid" type="text" placeholder="e.g. math_arith" style="width:100%;margin-top:3px;background:var(--bg2);border:1px solid var(--border);color:var(--fg);padding:5px 8px;font-size:0.7rem;border-radius:3px;font-family:inherit;box-sizing:border-box">
</label>
<label style="font-size:0.6rem;color:var(--text-dim)">CATEGORY
<input id="tm-edit-cat" type="text" placeholder="e.g. mathematics" list="tm-cat-dl" style="width:100%;margin-top:3px;background:var(--bg2);border:1px solid var(--border);color:var(--fg);padding:5px 8px;font-size:0.7rem;border-radius:3px;font-family:inherit;box-sizing:border-box">
<datalist id="tm-cat-dl"></datalist>
</label>
</div>
<label style="font-size:0.6rem;color:var(--text-dim)">TOPIC NAME
<input id="tm-edit-name" type="text" placeholder="e.g. Arithmetic and number sense" style="width:100%;margin-top:3px;background:var(--bg2);border:1px solid var(--border);color:var(--fg);padding:5px 8px;font-size:0.7rem;border-radius:3px;font-family:inherit;box-sizing:border-box">
</label>
<label style="font-size:0.6rem;color:var(--text-dim)">DESCRIPTION (subtopics to cover comma-separated)
<textarea id="tm-edit-desc" rows="5" placeholder="e.g. fractions, decimals, order of operations, prime numbers…" style="width:100%;margin-top:3px;background:var(--bg2);border:1px solid var(--border);color:var(--fg);padding:5px 8px;font-size:0.7rem;border-radius:3px;font-family:inherit;resize:vertical;box-sizing:border-box"></textarea>
</label>
<label style="font-size:0.6rem;color:var(--text-dim);display:flex;align-items:center;gap:6px;cursor:pointer">
<input id="tm-edit-active" type="checkbox" checked style="cursor:pointer"> ACTIVE (included in rotation)
</label>
</div>
</div>
</div>`;
openModal('&#9881; KB TOPIC MANAGER', modalHtml, null, null);
const saveBtn = document.getElementById('modalSave');
const closeBtn = document.getElementById('modalClose');
saveBtn.textContent = 'SAVE TOPIC';
saveBtn.style.display = 'none';
saveBtn.onclick = tmSaveTopic;
closeBtn.onclick = closeModal;
await tmLoadTopics();
}
async function tmLoadTopics() {
const res = await api('kb_topics');
_topicsData = res?.topics || [];
_topicsCats = res?.categories || [];
const catSel = document.getElementById('tm-cat-filter');
if (catSel) {
const cur = catSel.value;
while (catSel.options.length > 1) catSel.remove(1);
_topicsCats.forEach(c => catSel.add(new Option(c.toUpperCase(), c)));
if (cur) catSel.value = cur;
}
const dl = document.getElementById('tm-cat-dl');
if (dl) { dl.innerHTML = ''; _topicsCats.forEach(c => { const o = document.createElement('option'); o.value=c; dl.appendChild(o); }); }
tmFilter();
}
function tmFilter() {
const q = (document.getElementById('tm-search')?.value || '').toLowerCase();
const cat = document.getElementById('tm-cat-filter')?.value || '';
const active = document.getElementById('tm-active-filter')?.value;
const rows = _topicsData.filter(t => {
if (cat && t.category !== cat) return false;
if (active !== '' && active !== null && active !== undefined && String(t.active) !== String(active)) return false;
if (q && !`${t.topic_name} ${t.topic_id} ${t.category} ${t.description}`.toLowerCase().includes(q)) return false;
return true;
});
const total = _topicsData.length;
const activeCount = _topicsData.filter(t => t.active == 1).length;
const statsEl = document.getElementById('tm-stats');
if (statsEl) statsEl.innerHTML =
`<span style="color:var(--cyan)">${total} topics</span> &nbsp;|&nbsp; ` +
`<span style="color:var(--green)">${activeCount} active</span> &nbsp;|&nbsp; ` +
`<span style="color:var(--yellow)">${total-activeCount} disabled</span>` +
(rows.length < total ? ` &nbsp;|&nbsp; <span style="color:var(--text-dim)">${rows.length} shown</span>` : '');
const listEl = document.getElementById('tm-list');
if (!listEl) return;
if (!rows.length) {
listEl.innerHTML = '<div style="color:var(--text-dim);padding:20px;text-align:center">No topics match filter</div>';
return;
}
listEl.innerHTML = `<table style="width:100%;border-collapse:collapse">
<thead><tr style="color:var(--cyan);font-size:0.58rem;border-bottom:1px solid var(--border)">
<th style="padding:4px 6px;text-align:left;font-weight:normal">TOPIC ID</th>
<th style="padding:4px 6px;text-align:left;font-weight:normal">CATEGORY</th>
<th style="padding:4px 6px;text-align:left;font-weight:normal">TOPIC NAME</th>
<th style="padding:4px 6px;text-align:center;font-weight:normal">ON</th>
<th style="padding:4px 6px;text-align:center;font-weight:normal">RUNS</th>
<th style="padding:4px 6px;text-align:right;font-weight:normal">ACTIONS</th>
</tr></thead>
<tbody>${rows.map(t => `
<tr style="border-bottom:1px solid rgba(255,255,255,0.03);font-size:0.65rem">
<td style="padding:4px 6px;color:var(--text-dim);font-size:0.57rem">${esc(t.topic_id)}</td>
<td style="padding:4px 6px"><span style="background:var(--bg2);padding:1px 5px;border-radius:2px;font-size:0.57rem;white-space:nowrap">${esc(t.category)}</span></td>
<td style="padding:4px 6px;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(t.description)}">${esc(t.topic_name)}</td>
<td style="padding:4px 6px;text-align:center">
<input type="checkbox" ${t.active==1?'checked':''} onchange="tmToggle(${t.id},this)" style="cursor:pointer;accent-color:var(--green)">
</td>
<td style="padding:4px 6px;text-align:center;color:var(--text-dim);font-size:0.6rem">${t.run_count||0}</td>
<td style="padding:4px 6px;text-align:right;white-space:nowrap">
<button class="btn btn-sm btn-yellow" style="padding:2px 7px;font-size:0.58rem" onclick="tmEditTopic(${t.id})">EDIT</button>
<button class="btn btn-sm btn-red" style="padding:2px 7px;font-size:0.58rem;margin-left:3px" onclick="tmDelete(${t.id})">DEL</button>
</td>
</tr>`).join('')}
</tbody></table>`;
}
function tmShowList() {
document.getElementById('tm-view-list').style.display = '';
document.getElementById('tm-view-edit').style.display = 'none';
document.getElementById('modalSave').style.display = 'none';
}
function tmEditTopic(id) {
document.getElementById('tm-view-list').style.display = 'none';
document.getElementById('tm-view-edit').style.display = '';
document.getElementById('modalSave').style.display = '';
const tidEl = document.getElementById('tm-edit-tid');
if (id) {
const t = _topicsData.find(x => x.id == id);
if (!t) return;
document.getElementById('tm-edit-title').textContent = 'EDIT TOPIC: ' + t.topic_id;
document.getElementById('tm-edit-id').value = t.id;
tidEl.value = t.topic_id;
tidEl.readOnly = true;
tidEl.style.opacity = '0.6';
document.getElementById('tm-edit-cat').value = t.category;
document.getElementById('tm-edit-name').value = t.topic_name;
document.getElementById('tm-edit-desc').value = t.description;
document.getElementById('tm-edit-active').checked = t.active == 1;
} else {
document.getElementById('tm-edit-title').textContent = 'ADD NEW TOPIC';
document.getElementById('tm-edit-id').value = '';
tidEl.value = '';
tidEl.readOnly = false;
tidEl.style.opacity = '1';
document.getElementById('tm-edit-cat').value = '';
document.getElementById('tm-edit-name').value = '';
document.getElementById('tm-edit-desc').value = '';
document.getElementById('tm-edit-active').checked = true;
}
}
async function tmSaveTopic() {
const id = document.getElementById('tm-edit-id').value;
const data = {
id: id ? parseInt(id) : 0,
topic_id: document.getElementById('tm-edit-tid').value.trim(),
category: document.getElementById('tm-edit-cat').value.trim(),
topic_name: document.getElementById('tm-edit-name').value.trim(),
description: document.getElementById('tm-edit-desc').value.trim(),
active: document.getElementById('tm-edit-active').checked ? 1 : 0,
};
if (!data.topic_id || !data.category || !data.topic_name || !data.description) {
toast('All fields are required', 'err'); return;
}
apiPost('kb_topic_save', data, async (res) => {
toast(res?.msg || 'Saved', 'ok');
tmShowList();
await tmLoadTopics();
});
}
async function tmToggle(id, el) {
const prev = !el.checked;
const fd = new FormData();
fd.append('action', 'kb_topic_toggle');
fd.append('id', id);
try {
const r = await fetch(location.href, {method:'POST', body:fd});
const d = await r.json();
if (d.error) { toast(d.error, 'err'); el.checked = prev; return; }
const t = _topicsData.find(x => x.id == id);
if (t) t.active = d.active ? 1 : 0;
tmFilter();
} catch(e) { toast('Toggle failed', 'err'); el.checked = prev; }
}
async function tmDelete(id) {
const t = _topicsData.find(x => x.id == id);
const name = t?.topic_name || 'this topic';
openModal('CONFIRM DELETE',
`<div style="font-family:var(--mono);font-size:0.75rem;line-height:2;padding:4px 0">Delete topic:<br>` +
`<span style="color:var(--red)">${esc(name)}</span><br>` +
`<span style="color:var(--text-dim);font-size:0.65rem">This cannot be undone.</span></div>`,
() => { apiPost('kb_topic_delete', {id}, async () => { toast('Topic deleted', 'ok'); await openTopicsManager(); }); },
'DELETE');
}
async function arcSetup() { async function arcSetup() {
const modalHtml = ` const modalHtml = `
@@ -2574,8 +3080,22 @@ async function loadWorkers() {
} }
// Cron Workers // Cron Workers
const cl=d.cron_last||{}; const cl=d.cron_last||{};
const LIVE_LOG_WORKERS = {
facts_collector: /done|collected/,
stats_cache: /done/,
calendar_sync: /done/,
};
document.getElementById('workers-crons').innerHTML=CRON_DEFS.map(c=>{ document.getElementById('workers-crons').innerHTML=CRON_DEFS.map(c=>{
const runBtn=!c.norun?`<button onclick="workerAction('cron','${c.id}','run')" style="${wBtn('cyan')}">&#9654; RUN</button>`:'<span class="ts">&mdash;</span>'; let runBtn;
if (c.norun) {
runBtn = '<span class="ts">&mdash;</span>';
} else if (c.id === 'kb_intent_generator') {
runBtn = `<button onclick="runIntentGenerator()" style="${wBtn('cyan')}">&#9654; RUN</button>`;
} else if (LIVE_LOG_WORKERS[c.id]) {
runBtn = `<button onclick="runWorkerWithLog('${c.id}','${c.label}',${LIVE_LOG_WORKERS[c.id]})" style="${wBtn('cyan')}">&#9654; RUN</button>`;
} else {
runBtn = `<button onclick="workerAction('cron','${c.id}','run')" style="${wBtn('cyan')}">&#9654; RUN</button>`;
}
return `<tr> return `<tr>
<td><strong>${c.label}</strong></td> <td><strong>${c.label}</strong></td>
<td class="ts">${c.schedule}</td> <td class="ts">${c.schedule}</td>
@@ -2799,7 +3319,7 @@ function renderNetwork() {
<td class="ts">${ago(d.last_seen)}</td> <td class="ts">${ago(d.last_seen)}</td>
<td><div class="actions-col"> <td><div class="actions-col">
<button class="btn btn-xs" onclick="pingDev('${esc(d.ip)}',this)">PING</button> <button class="btn btn-xs" onclick="pingDev('${esc(d.ip)}',this)">PING</button>
<button class="btn btn-xs btn-yellow" onclick="netModal(${d.id},'${esc(d.ip)}','${esc(d.alias||'')}','${esc(d.device_type||'')}')">NAME</button> <button class="btn btn-xs btn-yellow" onclick="netModal(${d.id},'${escJs(d.ip)}','${escJs(d.alias||'')}','${escJs(d.device_type||'')}')">NAME</button>
<button class="btn btn-xs btn-red" onclick="delNet(${d.id},'${esc(name)}')">DEL</button> <button class="btn btn-xs btn-red" onclick="delNet(${d.id},'${esc(name)}')">DEL</button>
</div></td>`; </div></td>`;
}, null, null); }, null, null);
@@ -2878,7 +3398,7 @@ async function loadAlerts() {
<td class="ts">${ts(a.created_at)}</td> <td class="ts">${ts(a.created_at)}</td>
<td><div class="actions-col"> <td><div class="actions-col">
${!a.resolved?`<button class="btn btn-xs btn-green" onclick="apiPost('alerts_resolve',{id:${a.id}},()=>{toast('Resolved','ok');loadAlerts()})">RESOLVE</button>`:''} ${!a.resolved?`<button class="btn btn-xs btn-green" onclick="apiPost('alerts_resolve',{id:${a.id}},()=>{toast('Resolved','ok');loadAlerts()})">RESOLVE</button>`:''}
<button class="btn btn-xs btn-yellow" onclick="alertModal(${a.id},'${esc(a.alert_type)}','${esc(a.title)}','${esc(a.message||'')}','${esc(a.severity)}')">EDIT</button> <button class="btn btn-xs btn-yellow" onclick="alertModal(${a.id},'${escJs(a.alert_type)}','${escJs(a.title)}','${escJs(a.message||'')}','${escJs(a.severity)}')">EDIT</button>
<button class="btn btn-xs btn-red" onclick="apiPost('alerts_delete',{id:${a.id}},()=>{toast('Deleted','ok');loadAlerts()})">DEL</button> <button class="btn btn-xs btn-red" onclick="apiPost('alerts_delete',{id:${a.id}},()=>{toast('Deleted','ok');loadAlerts()})">DEL</button>
</div></td>`, null, null); </div></td>`, null, null);
} }
@@ -2988,7 +3508,7 @@ function renderIntents(intents) {
<td>${i.active?'<span class="badge badge-green">ON</span>':'<span class="badge badge-dim">OFF</span>'}</td> <td>${i.active?'<span class="badge badge-green">ON</span>':'<span class="badge badge-dim">OFF</span>'}</td>
<td><div class="actions-col"> <td><div class="actions-col">
<button class="btn btn-xs" onclick="apiPost('intents_toggle',{id:${i.id}},()=>{toast('Toggled','ok');loadIntents()})">${i.active?'DISABLE':'ENABLE'}</button> <button class="btn btn-xs" onclick="apiPost('intents_toggle',{id:${i.id}},()=>{toast('Toggled','ok');loadIntents()})">${i.active?'DISABLE':'ENABLE'}</button>
<button class="btn btn-xs btn-yellow" onclick='intentModal(${i.id},"${esc(i.intent_name)}","${esc(i.pattern)}",${JSON.stringify(i.response_template||"")},"${esc(i.action_type)}",${i.priority},${i.active})'>EDIT</button> <button class="btn btn-xs btn-yellow" onclick="intentModal(${i.id},'${escJs(i.intent_name)}','${escJs(i.pattern)}','${escJs(i.response_template||'')}','${escJs(i.action_type)}',${i.priority},${i.active})">EDIT</button>
<button class="btn btn-xs btn-red" onclick="apiPost('intents_delete',{id:${i.id}},()=>{toast('Deleted','ok');loadIntents()})">DEL</button> <button class="btn btn-xs btn-red" onclick="apiPost('intents_delete',{id:${i.id}},()=>{toast('Deleted','ok');loadIntents()})">DEL</button>
</div></td>`, null, null); </div></td>`, null, null);
} }
@@ -3052,18 +3572,18 @@ async function loadSites() {
if (!sites.length) { document.getElementById('sites-content').innerHTML='<div class="empty">NO SITE DATA</div>'; return; } if (!sites.length) { document.getElementById('sites-content').innerHTML='<div class="empty">NO SITE DATA</div>'; return; }
const labels = { const labels = {
'jarvis':'jarvis.orbishosting.com','tomsjavajive':'tomsjavajive.com', 'jarvis':'jarvis.orbishosting.com','tomsjavajive':'tomsjavajive.com',
'epictravelexp':'epictravelexpeditions.com','parkersling':'parkerslingshot', 'epictravelexp':'epictravelexpeditions.com','parkerslingshotrentals':'parkerslingshotrentals.com',
'orbishosting':'orbishosting.com','orbisportal':'orbis.orbishosting.com','tomtomgames':'tomtomgames.com' 'orbishosting':'orbishosting.com','orbisportal':'orbis.orbishosting.com','tomtomgames':'tomtomgames.com'
}; };
let cards = sites.map(s => { let cards = sites.map(s => {
const up = s.fact_value==='up'; const up = s.fact_value==='up';
return `<div class="stat-card" style="border-left:3px solid ${up?'var(--green)':'var(--red)'}"> return `<div class="site-card" style="border-left:3px solid ${up?'var(--green)':'var(--red)'}">
<div class="lbl">${esc(labels[s.fact_key]||s.fact_key)}</div> <div class="lbl">${esc(labels[s.fact_key]||s.fact_key)}</div>
<div class="val ${up?'ok':'danger'}" style="font-size:1.2rem">${up?'ONLINE':'OFFLINE'}</div> <div class="val ${up?'ok':'danger'}">${up?'ONLINE':'OFFLINE'}</div>
<div class="sub">checked ${ago(s.updated_at)}</div> <div class="sub">checked ${ago(s.updated_at)}</div>
</div>`; </div>`;
}).join(''); }).join('');
document.getElementById('sites-content').innerHTML = `<div class="stat-grid">${cards}</div>`; document.getElementById('sites-content').innerHTML = `<div class="site-grid">${cards}</div>`;
} }
// ── USERS ───────────────────────────────────────────────────────────────────── // ── USERS ─────────────────────────────────────────────────────────────────────
@@ -3220,7 +3740,7 @@ async function loadNews() {
<div style="font-size:0.75rem">${esc(c.title)}</div> <div style="font-size:0.75rem">${esc(c.title)}</div>
${c.url?`<div style="font-size:0.6rem;color:var(--dim)">${esc(c.url)}</div>`:''} ${c.url?`<div style="font-size:0.6rem;color:var(--dim)">${esc(c.url)}</div>`:''}
</div> </div>
<button class="btn btn-xs btn-yellow" onclick='newsCustomModal(${c.id},"${esc(c.title)}","${esc(c.url||"")}")'>EDIT</button> <button class="btn btn-xs btn-yellow" onclick="newsCustomModal(${c.id},'${escJs(c.title)}','${escJs(c.url||'')}')">EDIT</button>
<button class="btn btn-xs btn-red" onclick="apiPost('news_custom_delete',{id:${c.id}},()=>{toast('Deleted','ok');loadNews()})">DEL</button> <button class="btn btn-xs btn-red" onclick="apiPost('news_custom_delete',{id:${c.id}},()=>{toast('Deleted','ok');loadNews()})">DEL</button>
</div>`).join(''); </div>`).join('');
} }
@@ -3894,6 +4414,7 @@ async function loadOutbox() {
<td style="font-size:0.6rem;color:var(--dim)">${m.account||'gmail'}</td> <td style="font-size:0.6rem;color:var(--dim)">${m.account||'gmail'}</td>
<td style="font-size:0.6rem;color:var(--dim)">${ts}</td> <td style="font-size:0.6rem;color:var(--dim)">${ts}</td>
<td style="white-space:nowrap"> <td style="white-space:nowrap">
${sc==='queued' ? `<button class="btn btn-xs btn-green" onclick="outboxSend(${m.id})">SEND</button>` : ''}
<button class="btn btn-xs" onclick="outboxViewBody(${m.id})">VIEW</button> <button class="btn btn-xs" onclick="outboxViewBody(${m.id})">VIEW</button>
<button class="btn btn-xs btn-red" onclick="outboxDelete(${m.id})">DEL</button> <button class="btn btn-xs btn-red" onclick="outboxDelete(${m.id})">DEL</button>
</td> </td>
@@ -3909,12 +4430,19 @@ async function outboxDelete(id) {
else toast('Delete failed: ' + (d.error||''), 'err'); else toast('Delete failed: ' + (d.error||''), 'err');
} }
// Added 2026-07-07: composing only ever created a queued draft — there was no
// button anywhere to actually send one. This closes that gap.
async function outboxSend(id) {
if (!confirm('Send this draft now?')) return;
const d = await api('outbox_send', {id});
if (d.success) { toast('Sent', 'ok'); loadOutbox(); }
else toast('Send failed: ' + (d.error||'unknown error'), 'err');
}
async function outboxViewBody(id) { async function outboxViewBody(id) {
const d = await api('outbox_list', {limit: 200}); const m = await api('outbox_get', {id});
const sent = Array.isArray(d) ? d : (d.sent || []); if (!m || m.error) { toast('Failed to load message', 'err'); return; }
const m = sent.find(x => x.id == id); openModal((m.status==='queued'?'DRAFT — ':'SENT MESSAGE — ') + esc(m.subject||''), `
if (!m) return;
openModal('SENT MESSAGE — ' + esc(m.subject||''), `
<div style="font-family:var(--mono);font-size:0.65rem;color:var(--text-dim);margin-bottom:8px">TO: ${esc(m.to_email||'')} · ACCOUNT: ${m.account||'gmail'}</div> <div style="font-family:var(--mono);font-size:0.65rem;color:var(--text-dim);margin-bottom:8px">TO: ${esc(m.to_email||'')} · ACCOUNT: ${m.account||'gmail'}</div>
<pre style="font-size:0.65rem;white-space:pre-wrap;max-height:300px;overflow-y:auto;background:rgba(0,212,255,0.03);border:1px solid var(--border);border-radius:3px;padding:8px">${esc(m.body||'(no body)')}</pre> <pre style="font-size:0.65rem;white-space:pre-wrap;max-height:300px;overflow-y:auto;background:rgba(0,212,255,0.03);border:1px solid var(--border);border-radius:3px;padding:8px">${esc(m.body||'(no body)')}</pre>
`, null, null); `, null, null);
@@ -3930,6 +4458,7 @@ function outboxCompose() {
<input id="oc-to" class="inp" placeholder="To: email address" type="email"> <input id="oc-to" class="inp" placeholder="To: email address" type="email">
<input id="oc-subject" class="inp" placeholder="Subject"> <input id="oc-subject" class="inp" placeholder="Subject">
<textarea id="oc-body" class="inp" rows="5" style="resize:vertical" placeholder="Describe what to say — AI will draft the full message"></textarea> <textarea id="oc-body" class="inp" rows="5" style="resize:vertical" placeholder="Describe what to say — AI will draft the full message"></textarea>
<div id="oc-status" style="display:none;font-size:0.65rem;color:var(--text-dim);padding:8px;background:#060a0e;border:1px solid var(--border);border-radius:3px"></div>
</div> </div>
`, async () => { `, async () => {
const to = document.getElementById('oc-to')?.value.trim(); const to = document.getElementById('oc-to')?.value.trim();
@@ -3937,14 +4466,39 @@ function outboxCompose() {
const body = document.getElementById('oc-body')?.value.trim(); const body = document.getElementById('oc-body')?.value.trim();
const account = document.getElementById('oc-account')?.value; const account = document.getElementById('oc-account')?.value;
if (!to || !body) { toast('Please fill To and message description', 'err'); return; } if (!to || !body) { toast('Please fill To and message description', 'err'); return; }
const statusEl = document.getElementById('oc-status');
const d = await api('compose_email', {to, subject, body, account}); const d = await api('compose_email', {to, subject, body, account});
if (d.job_id) { if (!d.job_id) { toast('Failed: ' + (d.error||'Arc Reactor offline'), 'err'); return; }
toast('Compose job dispatched — Job #' + d.job_id, 'ok'); // Fixed 2026-07-07: this used to toast "dispatched" and close immediately, with
// no way to ever find out if the job later failed — it just silently vanished.
// Now polls the actual job status until it finishes, so failures are visible.
if (statusEl) { statusEl.style.display = 'block'; statusEl.textContent = 'Drafting — Job #' + d.job_id + '...'; }
document.getElementById('modalSave').disabled = true;
const jobId = d.job_id;
const deadline = Date.now() + 120000;
while (Date.now() < deadline) {
await new Promise(r => setTimeout(r, 3000));
const j = await api('arc_job_get', {id: jobId}).catch(() => null);
if (!j || j.error) continue;
if (j.status === 'done') {
toast('Draft ready: ' + (j.result?.subject || '(no subject)'), 'ok');
closeModal(); closeModal();
setTimeout(loadOutbox, 5000); loadOutbox();
} else { return;
toast('Failed: ' + (d.error||'Arc Reactor offline'), 'err'); } else if (j.status === 'failed') {
if (statusEl) {
statusEl.style.color = 'var(--red)';
statusEl.textContent = '✗ Failed: ' + (j.error || 'unknown error');
} }
document.getElementById('modalSave').disabled = false;
document.getElementById('modalSave').textContent = 'CLOSE';
document.getElementById('modalSave').onclick = closeModal;
return;
}
if (statusEl) statusEl.textContent = 'Drafting — Job #' + jobId + ' (' + j.status + ')...';
}
if (statusEl) { statusEl.style.color = 'var(--yellow)'; statusEl.textContent = '⚠ Still running in background — check outbox shortly.'; }
document.getElementById('modalSave').disabled = false;
}, 'DISPATCH'); }, 'DISPATCH');
} }
@@ -4605,7 +5159,7 @@ async function loadCalFeeds() {
<td>${ts(f.last_sync)}</td> <td>${ts(f.last_sync)}</td>
<td>${f.last_count||0}</td> <td>${f.last_count||0}</td>
<td>${f.active?'<span class="badge badge-green">ACTIVE</span>':'<span class="badge badge-red">PAUSED</span>'}</td> <td>${f.active?'<span class="badge badge-green">ACTIVE</span>':'<span class="badge badge-red">PAUSED</span>'}</td>
<td><button class="btn btn-xs" onclick='calFeedModal(${JSON.stringify(f)})'>EDIT</button> <td><button class="btn btn-xs" onclick="calFeedModal(${esc(JSON.stringify(f))})">EDIT</button>
<button class="btn btn-xs btn-red" onclick="calFeedDel(${f.id})">DEL</button></td> <button class="btn btn-xs btn-red" onclick="calFeedDel(${f.id})">DEL</button></td>
</tr>`).join('')}</tbody></table>`; </tr>`).join('')}</tbody></table>`;
} }
+17 -38
View File
@@ -6,20 +6,26 @@
.DESCRIPTION .DESCRIPTION
Installs JARVIS Agent as a Windows Service that auto-starts at boot. Installs JARVIS Agent as a Windows Service that auto-starts at boot.
Requires: PowerShell 5.1+, internet access, and Administrator rights. Requires: PowerShell 5.1+, internet access, and Administrator rights.
No Python installation needed this installs the standalone .exe build.
.EXAMPLE .EXAMPLE
# Interactive install (prompts for registration key): # Interactive install (prompts for registration key):
irm https://jarvis.orbishosting.com/agent/install-windows.ps1 | iex irm https://jarvis.orbishosting.com:1972/agent/install-windows.ps1 | iex
# Silent install with key: # Silent install with key:
$env:JARVIS_REG_KEY='your_key_here'; irm https://jarvis.orbishosting.com/agent/install-windows.ps1 | iex $env:JARVIS_REG_KEY='your_key_here'; irm https://jarvis.orbishosting.com:1972/agent/install-windows.ps1 | iex
#> #>
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$JARVIS_URL = 'https://jarvis.orbishosting.com' # Fixed 2026-07-07: jarvis.orbishosting.com on the default port (80/443) is not
# reachable from outside the LAN at all (no FortiGate VIP forwards it) — every
# external install using the old default URL would have failed outright. Port
# 1972 is the confirmed-working external path (same fix applied to the GitHub
# webhook the same day).
$JARVIS_URL = 'http://jarvis.orbishosting.com:1972'
$INSTALL_DIR = 'C:\ProgramData\jarvis-agent' $INSTALL_DIR = 'C:\ProgramData\jarvis-agent'
$SERVICE_NAME = 'JARVISAgent' $SERVICE_NAME = 'JARVISAgent'
$AGENT_SCRIPT = "$INSTALL_DIR\jarvis-agent-windows.py" $AGENT_EXE = "$INSTALL_DIR\jarvis-agent-windows.exe"
$CONFIG_FILE = "$INSTALL_DIR\config.json" $CONFIG_FILE = "$INSTALL_DIR\config.json"
function Write-Step { param($msg) Write-Host "`n[JARVIS] $msg" -ForegroundColor Cyan } function Write-Step { param($msg) Write-Host "`n[JARVIS] $msg" -ForegroundColor Cyan }
@@ -39,49 +45,23 @@ if ($existing) {
Start-Sleep 2 Start-Sleep 2
} }
try { try {
& python "$INSTALL_DIR\jarvis-agent-windows.py" remove 2>$null if (Test-Path $AGENT_EXE) { & $AGENT_EXE remove 2>$null }
} catch {} } catch {}
Write-OK "Existing service removed." Write-OK "Existing service removed."
} }
# ── Check / install Python ────────────────────────────────────────────────────
Write-Step "Checking Python..."
$py = Get-Command python -ErrorAction SilentlyContinue
if (-not $py) {
Write-Host " Python not found. Installing via winget..." -ForegroundColor Yellow
if (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
Write-Fail "winget not available. Please install Python 3.11+ from https://python.org and re-run."
}
winget install -e --id Python.Python.3.11 --silent --accept-package-agreements --accept-source-agreements
$env:PATH = [System.Environment]::GetEnvironmentVariable("PATH","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("PATH","User")
$py = Get-Command python -ErrorAction SilentlyContinue
if (-not $py) { Write-Fail "Python install failed. Please install manually from https://python.org" }
}
$pyVersion = & python --version 2>&1
Write-OK $pyVersion
# ── Install pywin32 ───────────────────────────────────────────────────────────
Write-Step "Checking pywin32..."
$checkWin32 = & python -c "import win32service; print('ok')" 2>&1
if ($checkWin32 -ne 'ok') {
Write-Host " Installing pywin32..." -ForegroundColor Yellow
& python -m pip install --quiet pywin32
& python -m pywin32_postinstall -install 2>$null
Write-OK "pywin32 installed."
} else {
Write-OK "pywin32 already installed."
}
# ── Create install dir ──────────────────────────────────────────────────────── # ── Create install dir ────────────────────────────────────────────────────────
Write-Step "Creating install directory..." Write-Step "Creating install directory..."
New-Item -ItemType Directory -Path $INSTALL_DIR -Force | Out-Null New-Item -ItemType Directory -Path $INSTALL_DIR -Force | Out-Null
Write-OK $INSTALL_DIR Write-OK $INSTALL_DIR
# ── Download agent script ───────────────────────────────────────────────────── # ── Download agent exe ─────────────────────────────────────────────────────────
# No Python/pywin32 dependency anymore — this is a self-contained PyInstaller
# build with everything it needs bundled in.
Write-Step "Downloading JARVIS agent..." Write-Step "Downloading JARVIS agent..."
try { try {
Invoke-WebRequest -Uri "$JARVIS_URL/agent/jarvis-agent-windows.py" -OutFile $AGENT_SCRIPT -UseBasicParsing Invoke-WebRequest -Uri "$JARVIS_URL/agent/jarvis-agent-windows.exe" -OutFile $AGENT_EXE -UseBasicParsing
Write-OK "Agent downloaded to $AGENT_SCRIPT" Write-OK "Agent downloaded to $AGENT_EXE"
} catch { } catch {
Write-Fail "Failed to download agent: $_" Write-Fail "Failed to download agent: $_"
} }
@@ -121,8 +101,7 @@ Write-OK "Config written to $CONFIG_FILE"
# ── Install Windows Service ─────────────────────────────────────────────────── # ── Install Windows Service ───────────────────────────────────────────────────
Write-Step "Installing Windows service..." Write-Step "Installing Windows service..."
$pyPath = (Get-Command python).Source & $AGENT_EXE --startup auto install
& $pyPath "$AGENT_SCRIPT" --startup auto install
if ($LASTEXITCODE -ne 0) { Write-Fail "Service install failed." } if ($LASTEXITCODE -ne 0) { Write-Fail "Service install failed." }
Write-OK "Service '$SERVICE_NAME' installed." Write-OK "Service '$SERVICE_NAME' installed."
+19 -5
View File
@@ -1,20 +1,34 @@
#!/bin/bash #!/bin/bash
# JARVIS Agent Installer — one-liner for any Linux host: # JARVIS Agent Installer — one-liner for any Linux host:
# curl -sk https://jarvis.orbishosting.com/install-agent.sh | bash -s <hostname> <agent_type> # curl -sk http://jarvis.orbishosting.com:1972/agent/install.sh | bash -s <hostname> <agent_type>
# #
# agent_type: linux | proxmox | homeassistant # agent_type: linux | proxmox | homeassistant
# Example: curl -sk https://jarvis.orbishosting.com/install-agent.sh | bash -s myserver linux # Example: curl -sk http://jarvis.orbishosting.com:1972/agent/install.sh | bash -s myserver linux
#
# On the LAN, set JARVIS_URL to the direct internal address instead (faster,
# doesn't hairpin through Cloudflare): JARVIS_URL=http://10.48.200.211 curl ... | bash -s ...
set -e set -e
HOSTNAME_ARG="${1:-$(hostname -s)}" HOSTNAME_ARG="${1:-$(hostname -s)}"
AGENT_TYPE="${2:-linux}" AGENT_TYPE="${2:-linux}"
JARVIS_URL="${JARVIS_URL:-https://jarvis.orbishosting.com}" # Fixed 2026-07-07: jarvis.orbishosting.com on the default port isn't reachable
# from outside the LAN at all (no FortiGate VIP forwards it) — :1972 is the
# confirmed-working external path (same fix as the GitHub webhook and the
# Windows agent installer).
JARVIS_URL="${JARVIS_URL:-http://jarvis.orbishosting.com:1972}"
JARVIS_HOST="" JARVIS_HOST=""
INSTALL_DIR="/opt/jarvis-agent" INSTALL_DIR="/opt/jarvis-agent"
CONFIG_DIR="/etc/jarvis-agent" CONFIG_DIR="/etc/jarvis-agent"
STATE_DIR="/var/lib/jarvis-agent" STATE_DIR="/var/lib/jarvis-agent"
REG_KEY="f846a9aaf7ce9a61742c63c87c4186052a71d2a580c65518" REG_KEY="${JARVIS_REG_KEY:-}"
if [ -z "$REG_KEY" ] && [ -r /dev/tty ]; then
read -rp "Enter JARVIS registration key: " REG_KEY </dev/tty
fi
if [ -z "$REG_KEY" ]; then
echo "ERROR: registration key required (set JARVIS_REG_KEY env var or enter at prompt)" >&2
exit 1
fi
SERVICE_FILE="/etc/systemd/system/jarvis-agent.service" SERVICE_FILE="/etc/systemd/system/jarvis-agent.service"
echo "=== JARVIS Agent Installer v3.0 ===" echo "=== JARVIS Agent Installer v3.0 ==="
@@ -57,7 +71,7 @@ else
"poll_interval": 30, "poll_interval": 30,
"heartbeat_every": 10, "heartbeat_every": 10,
"update_check_hours": 24, "update_check_hours": 24,
"watch_services": ["ollama", "homeassistant", "mysql", "mariadb", "nginx", "apache2", "docker"] "watch_services": []
} }
JSONEOF JSONEOF
chmod 600 "$CONFIG_DIR/config.json" chmod 600 "$CONFIG_DIR/config.json"
Binary file not shown.
@@ -0,0 +1 @@
ad9b59c09e5862c5abc35f73999aa2666f8401817b053c385505fa420ca473e7
+46 -19
View File
@@ -35,12 +35,11 @@ INSTALL_DIR = Path(r"C:\ProgramData\jarvis-agent")
CONFIG_PATH = INSTALL_DIR / "config.json" CONFIG_PATH = INSTALL_DIR / "config.json"
STATE_PATH = INSTALL_DIR / "state.json" STATE_PATH = INSTALL_DIR / "state.json"
LOG_PATH = INSTALL_DIR / "jarvis-agent.log" LOG_PATH = INSTALL_DIR / "jarvis-agent.log"
AGENT_VERSION = "3.1" AGENT_VERSION = "3.2"
# Set by the service wrapper so self_update knows to stop instead of exec # Set by the service wrapper so self_update knows to stop instead of exec
_is_service = False _is_service = False
_stop_event = threading.Event() _stop_event = threading.Event()
_update_restart = False # True when stopping for self-update; triggers SCM restart
# ── Logging ──────────────────────────────────────────────────────────────────── # ── Logging ────────────────────────────────────────────────────────────────────
@@ -93,7 +92,7 @@ def api_post(url: str, payload: dict, headers: dict = {}, timeout: int = 15,
body = json.dumps(payload).encode() body = json.dumps(payload).encode()
req = urllib.request.Request(url, data=body, method="POST") req = urllib.request.Request(url, data=body, method="POST")
req.add_header("Content-Type", "application/json") req.add_header("Content-Type", "application/json")
req.add_header("User-Agent", "JARVIS-Agent-Windows/3.0") req.add_header("User-Agent", "JARVIS-Agent-Windows/3.2")
if _host_header: if _host_header:
req.add_header("Host", _host_header) req.add_header("Host", _host_header)
for k, v in headers.items(): for k, v in headers.items():
@@ -110,7 +109,7 @@ def api_post(url: str, payload: dict, headers: dict = {}, timeout: int = 15,
def api_get(url: str, headers: dict = {}, timeout: int = 10, def api_get(url: str, headers: dict = {}, timeout: int = 10,
ssl_verify: bool = True) -> dict: ssl_verify: bool = True) -> dict:
req = urllib.request.Request(url) req = urllib.request.Request(url)
req.add_header("User-Agent", "JARVIS-Agent-Windows/3.0") req.add_header("User-Agent", "JARVIS-Agent-Windows/3.2")
if _host_header: if _host_header:
req.add_header("Host", _host_header) req.add_header("Host", _host_header)
for k, v in headers.items(): for k, v in headers.items():
@@ -376,18 +375,28 @@ def _sysinfo_snapshot() -> dict:
# ── Self-update ──────────────────────────────────────────────────────────────── # ── Self-update ────────────────────────────────────────────────────────────────
def self_update(cfg: dict) -> bool: def self_update(cfg: dict) -> bool:
# Added: supports both script-mode (plain .py, run via a system Python) and
# frozen-mode (standalone PyInstaller .exe — sys.frozen is set, __file__ isn't
# meaningful/writable the way it is for a real .py file on disk). A running
# .exe can't be overwritten in place on Windows, but CAN be renamed while
# running, so frozen mode uses a download-new/rename-old/rename-new swap
# instead of the direct overwrite the script-mode path uses.
jarvis_url = cfg.get("jarvis_url", "").rstrip("/") jarvis_url = cfg.get("jarvis_url", "").rstrip("/")
is_frozen = bool(getattr(sys, "frozen", False))
if is_frozen:
default_update_url = f"{jarvis_url}/agent/jarvis-agent-windows.exe" if jarvis_url else ""
else:
default_update_url = f"{jarvis_url}/agent/jarvis-agent-windows.py" if jarvis_url else "" default_update_url = f"{jarvis_url}/agent/jarvis-agent-windows.py" if jarvis_url else ""
update_url = cfg.get("update_url", default_update_url) update_url = cfg.get("update_url", default_update_url)
if not update_url: if not update_url:
return False return False
script_path = os.path.abspath(__file__) target_path = os.path.abspath(sys.executable) if is_frozen else os.path.abspath(__file__)
ssl_verify = bool(cfg.get("ssl_verify", True)) ssl_verify = bool(cfg.get("ssl_verify", True))
try: try:
# Download expected hash # Download expected hash
hash_url = update_url + ".sha256" hash_url = update_url + ".sha256"
req_hash = urllib.request.Request(hash_url) req_hash = urllib.request.Request(hash_url)
req_hash.add_header("User-Agent", "JARVIS-Agent-Windows/3.0") req_hash.add_header("User-Agent", "JARVIS-Agent-Windows/3.2")
if _host_header: if _host_header:
req_hash.add_header("Host", _host_header) req_hash.add_header("Host", _host_header)
expected_hash = None expected_hash = None
@@ -398,13 +407,13 @@ def self_update(cfg: dict) -> bool:
except Exception: except Exception:
pass pass
# Download new script # Download new script/exe
req = urllib.request.Request(update_url) req = urllib.request.Request(update_url)
req.add_header("User-Agent", "JARVIS-Agent-Windows/3.0") req.add_header("User-Agent", "JARVIS-Agent-Windows/3.2")
if _host_header: if _host_header:
req.add_header("Host", _host_header) req.add_header("Host", _host_header)
ctx = _make_ssl_ctx(ssl_verify) ctx = _make_ssl_ctx(ssl_verify)
with urllib.request.urlopen(req, timeout=30, context=ctx) as resp: with urllib.request.urlopen(req, timeout=60, context=ctx) as resp:
new_content = resp.read() new_content = resp.read()
# Verify hash # Verify hash
@@ -414,21 +423,42 @@ def self_update(cfg: dict) -> bool:
log(f"Update hash mismatch (expected {expected_hash[:16]}… got {actual_hash[:16]}…) — aborting") log(f"Update hash mismatch (expected {expected_hash[:16]}… got {actual_hash[:16]}…) — aborting")
return False return False
with open(script_path, "rb") as f: with open(target_path, "rb") as f:
current = f.read() current = f.read()
if new_content != current: if new_content == current:
log(f"Update verified — replacing {script_path} and restarting...") return False
with open(script_path, "wb") as f:
log(f"Update verified — replacing {target_path} and restarting...")
if is_frozen:
# Can't overwrite a running exe, but can rename it and drop the new
# one in its place; the old copy is cleaned up on the next update.
old_path = target_path + ".old"
new_path = target_path + ".new"
with open(new_path, "wb") as f:
f.write(new_content) f.write(new_content)
try:
if os.path.exists(old_path):
os.remove(old_path)
except Exception:
pass
os.rename(target_path, old_path)
os.rename(new_path, target_path)
else:
with open(target_path, "wb") as f:
f.write(new_content)
if _is_service: if _is_service:
global _update_restart # Signal the main loop to exit; SCM failure-recovery will restart us
_update_restart = True
log("Running as service — stopping for SCM-managed restart after update.") log("Running as service — stopping for SCM-managed restart after update.")
_stop_event.set() _stop_event.set()
elif is_frozen:
# sys.argv[0] is already the exe's own path for a frozen app — don't
# prepend sys.executable again or the new process misreads its own
# path as a command-line argument.
os.execv(sys.executable, sys.argv)
else: else:
os.execv(sys.executable, [sys.executable] + sys.argv) os.execv(sys.executable, [sys.executable] + sys.argv)
return True return True
return False
except Exception as e: except Exception as e:
log(f"Self-update check failed: {e}") log(f"Self-update check failed: {e}")
return False return False
@@ -592,9 +622,6 @@ if _HAS_WIN32:
(self._svc_name_, ""), (self._svc_name_, ""),
) )
main() main()
if _update_restart:
# Non-zero exit triggers SCM failure recovery → automatic restart
sys.exit(1)
if __name__ == "__main__": if __name__ == "__main__":
@@ -1 +1 @@
224a634375b5d49ccc0a012e0e122ade5f8a1302615450dffbf9a03eac6b7a19 fff217657488830084780115665d0c772af1f7fe31f2084d61a7560424a5a91a
+1 -1
View File
@@ -281,7 +281,7 @@ def get_uptime() -> dict:
return {} return {}
def get_services(cfg: dict) -> list: def get_services(cfg: dict) -> list:
watch = cfg.get("watch_services", ["ollama", "homeassistant", "mysql", "nginx", "apache2"]) watch = cfg.get("watch_services", [])
statuses = [] statuses = []
for svc in watch: for svc in watch:
try: try:
+7 -2
View File
@@ -19,7 +19,7 @@ $_e1 = $_earlyParts[1] ?? '';
$_skipSession = match(true) { $_skipSession = match(true) {
$_e0 === 'ping' => true, $_e0 === 'ping' => true,
$_e0 === 'netscan' => true, $_e0 === 'netscan' => true,
$_e0 === 'agent' && !in_array($_e1, ['list','status','myip'], true) => true, $_e0 === 'agent' && !in_array($_e1, ['list','status','myip','regkey'], true) => true,
default => false, default => false,
}; };
if (!$_skipSession) { if (!$_skipSession) {
@@ -27,7 +27,12 @@ if (!$_skipSession) {
} }
header('Content-Type: application/json'); header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *'); $_allowedOrigins = ['https://jarvis.orbishosting.com', 'http://jarvis.orbishosting.com'];
$_origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($_origin, $_allowedOrigins, true)) {
header('Access-Control-Allow-Origin: ' . $_origin);
header('Access-Control-Allow-Credentials: true');
}
header('Access-Control-Allow-Methods: GET, POST, OPTIONS'); header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, X-Session-Token'); header('Access-Control-Allow-Headers: Content-Type, X-Session-Token');
+32 -13
View File
@@ -1,4 +1,14 @@
// ── GLOBALS ────────────────────────────────────────────────────────── // ── GLOBALS ──────────────────────────────────────────────────────────
function escHtml(s) {
return String(s == null ? '' : s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// For values embedded inside a single-quoted JS string literal within an HTML attribute
// (e.g. onclick="fn('${escJs(x)}')"). escHtml() alone isn't enough there: the browser
// HTML-decodes the attribute before parsing it as JS, so an encoded quote would just
// turn back into a literal ' before the JS parser ever sees it.
function escJs(s) {
return escHtml(String(s == null ? '' : s).replace(/\\/g,'\\\\').replace(/'/g,"\\'").replace(/\n/g,'\\n').replace(/\r/g,'\\r'));
}
let sessionToken = ''; let sessionToken = '';
let sessionUser = ''; let sessionUser = '';
let sessionId = 'session_' + Date.now(); let sessionId = 'session_' + Date.now();
@@ -596,15 +606,15 @@ function renderNetworkStatus(n) {
agent_id: d.agent_id, hostname: d.name}; agent_id: d.agent_id, hostname: d.name};
const lat = d.latency_ms ? ' · ' + d.latency_ms + 'ms' : ''; const lat = d.latency_ms ? ' · ' + d.latency_ms + 'ms' : '';
const badge = d.source === 'agent' const badge = d.source === 'agent'
? `<span style="font-size:0.53rem;color:var(--cyan);letter-spacing:1px;margin-left:4px">${(d.agent_type||'AGENT').toUpperCase()}</span>` : ''; ? `<span style="font-size:0.53rem;color:var(--cyan);letter-spacing:1px;margin-left:4px">${escHtml((d.agent_type||'AGENT').toUpperCase())}</span>` : '';
const del = d.deletable const del = d.deletable
? `<button onclick="deleteNetworkDevice('${d.ip}',event)" style="background:none;border:none;color:var(--red);cursor:pointer;font-size:0.9rem;padding:0 2px;opacity:0.5;flex-shrink:0" title="Remove">×</button>` : ''; ? `<button onclick="deleteNetworkDevice('${escJs(d.ip)}',event)" style="background:none;border:none;color:var(--red);cursor:pointer;font-size:0.9rem;padding:0 2px;opacity:0.5;flex-shrink:0" title="Remove">×</button>` : '';
const bl = d.source === 'agent' ? 'border-left:2px solid ' + (alive ? 'var(--green)' : 'var(--red)') + ';' : ''; const bl = d.source === 'agent' ? 'border-left:2px solid ' + (alive ? 'var(--green)' : 'var(--red)') + ';' : '';
return `<div class="device-item" data-ctx-key="${ctxKey}" onclick="selectContext('${ctxKey}')" style="${bl}display:flex;align-items:center"> return `<div class="device-item" data-ctx-key="${ctxKey}" onclick="selectContext('${escJs(ctxKey)}')" style="${bl}display:flex;align-items:center">
<div class="device-status ${alive?'on':'off'}" style="flex-shrink:0"></div> <div class="device-status ${alive?'on':'off'}" style="flex-shrink:0"></div>
<div class="device-info" style="flex:1;min-width:0"> <div class="device-info" style="flex:1;min-width:0">
<div class="device-name" style="display:flex;align-items:center">${d.name||d.ip}${badge}</div> <div class="device-name" style="display:flex;align-items:center">${escHtml(d.name||d.ip)}${badge}</div>
<div class="device-ip">${d.ip||''}${lat}</div> <div class="device-ip">${escHtml(d.ip||'')}${lat}</div>
</div>${del} </div>${del}
</div>`; </div>`;
} }
@@ -684,7 +694,7 @@ async function loadProxmox() {
type_label:vm.type||'qemu', uptime:vm.uptime||0}; type_label:vm.type||'qemu', uptime:vm.uptime||0};
return `<div class="vm-card" data-ctx-key="${ctxKey}" onclick="selectContext('${ctxKey}')" title="Click to ask Jarvis about this VM"> return `<div class="vm-card" data-ctx-key="${ctxKey}" onclick="selectContext('${ctxKey}')" title="Click to ask Jarvis about this VM">
<div class="vm-header"> <div class="vm-header">
<span class="vm-name">${vm.name}</span> <span class="vm-name">${escHtml(vm.name)}</span>
<span style="color:${statusColor};font-size:0.65rem"> ${(vm.status||'').toUpperCase()}</span> <span style="color:${statusColor};font-size:0.65rem"> ${(vm.status||'').toUpperCase()}</span>
</div> </div>
<div class="vm-metrics"> <div class="vm-metrics">
@@ -972,6 +982,7 @@ async function loadWeather() {
const d = await api('weather'); const d = await api('weather');
if (!d || !d.current) return; if (!d || !d.current) return;
const c = d.current; const c = d.current;
if (d.location) document.getElementById('weather-loc').textContent = d.location.toUpperCase();
document.getElementById('weather-temp').textContent = c.temp; document.getElementById('weather-temp').textContent = c.temp;
document.getElementById('weather-desc').textContent = (c.desc || '').toUpperCase(); document.getElementById('weather-desc').textContent = (c.desc || '').toUpperCase();
document.getElementById('weather-feels').textContent = c.feels + '°F'; document.getElementById('weather-feels').textContent = c.feels + '°F';
@@ -1031,10 +1042,11 @@ async function loadNews() {
const ctxKey = 'news_' + (cat + '_' + a.title).replace(/[^a-z0-9]/gi,'').slice(0,30); const ctxKey = 'news_' + (cat + '_' + a.title).replace(/[^a-z0-9]/gi,'').slice(0,30);
_panelCtx[ctxKey] = {type:'news', label:a.title, _panelCtx[ctxKey] = {type:'news', label:a.title,
title:a.title, source:a.source, pub:a.pub||'', category:cat}; title:a.title, source:a.source, pub:a.pub||'', category:cat};
const titleDisplay = a.title.length > 90 ? a.title.slice(0,87)+'…' : a.title;
html += `<div class="news-item" data-ctx-key="${ctxKey}" onclick="selectContext('${ctxKey}')" title="Click to ask Jarvis about this story"> html += `<div class="news-item" data-ctx-key="${ctxKey}" onclick="selectContext('${ctxKey}')" title="Click to ask Jarvis about this story">
<div class="news-source">${a.source}</div> <div class="news-source">${escHtml(a.source)}</div>
<div class="news-title">${a.title.length > 90 ? a.title.slice(0,87)+'…' : a.title}</div> <div class="news-title">${escHtml(titleDisplay)}</div>
${a.pub ? '<div class="news-time">' + a.pub + '</div>' : ''} ${a.pub ? '<div class="news-time">' + escHtml(a.pub) + '</div>' : ''}
</div>`; </div>`;
} }
} }
@@ -1620,11 +1632,18 @@ async function checkAgentStatus() {
sta.textContent = online.length > 0 ? online.length + ' ONLINE' : 'NONE'; sta.textContent = online.length > 0 ? online.length + ' ONLINE' : 'NONE';
const cnt = document.getElementById('net-agent-count'); const cnt = document.getElementById('net-agent-count');
if (cnt) cnt.textContent = online.length + ' AGENT' + (online.length !== 1 ? 'S' : '') + ' ONLINE'; if (cnt) cnt.textContent = online.length + ' AGENT' + (online.length !== 1 ? 'S' : '') + ' ONLINE';
// Fixed 2026-07-07: this used to fall back to "any agent on the same /24
// subnet" when the exact IP didn't match, meant to handle NAT — but when
// a LAN client hits the dashboard via its external hostname (hairpin NAT,
// or through Cloudflare), the server sees the SAME single reflected/proxy
// IP for every visitor on the LAN, not each machine's real one. The exact
// match then always fails, and the subnet fallback just grabbed whichever
// agent happened to be first in the list — showing a *different real
// machine* as "yours", not a harmless guess. Exact match only now; if it
// fails, the button correctly falls through to "not detected" instead of
// reporting someone else's agent as this one.
const myIp = data.my_ip || ''; const myIp = data.my_ip || '';
// Match by exact IP first, then by same /24 subnet (handles NAT behind same router) _myAgent = online.find(a => a.ip_address === myIp) || null;
const mySubnet = myIp.split('.').slice(0,3).join('.');
_myAgent = online.find(a => a.ip_address === myIp)
|| online.find(a => a.ip_address && a.ip_address.startsWith(mySubnet + '.'));
_agentOnline = !!_myAgent; _agentOnline = !!_myAgent;
if (btn) { if (btn) {
const isTablet = detectOS() === 'tablet'; const isTablet = detectOS() === 'tablet';
+29 -15
View File
@@ -401,8 +401,8 @@ function renderAgentsTab(agents, metrics) {
style="flex-direction:column;align-items:stretch;border-left:3px solid ${alive ? 'var(--green)' : 'var(--red)'}"> style="flex-direction:column;align-items:stretch;border-left:3px solid ${alive ? 'var(--green)' : 'var(--red)'}">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px"> <div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
<div style="width:8px;height:8px;border-radius:50%;background:${alive ? 'var(--green)' : 'var(--red)'};box-shadow:${alive ? '0 0 6px var(--green)' : 'none'};flex-shrink:0"></div> <div style="width:8px;height:8px;border-radius:50%;background:${alive ? 'var(--green)' : 'var(--red)'};box-shadow:${alive ? '0 0 6px var(--green)' : 'none'};flex-shrink:0"></div>
<span style="font-family:var(--font-mono);font-size:0.72rem;color:var(--text);flex:1">${ag.hostname}</span> <span style="font-family:var(--font-mono);font-size:0.72rem;color:var(--text);flex:1">${escHtml(ag.hostname)}</span>
<span style="font-size:0.58rem;color:var(--text-dim)">${ag.agent_type.toUpperCase()} · ${ag.ip_address}</span> <span style="font-size:0.58rem;color:var(--text-dim)">${escHtml(ag.agent_type.toUpperCase())} · ${escHtml(ag.ip_address)}</span>
<span style="font-size:0.58rem;color:${alive ? 'var(--green)' : 'var(--red)'};">${alive ? 'ONLINE' : 'OFFLINE'}</span> <span style="font-size:0.58rem;color:${alive ? 'var(--green)' : 'var(--red)'};">${alive ? 'ONLINE' : 'OFFLINE'}</span>
</div> </div>
${alive ? `<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;margin-bottom:4px"> ${alive ? `<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;margin-bottom:4px">
@@ -425,21 +425,30 @@ function renderAgentsTab(agents, metrics) {
${svcs ? `<div style="font-size:0.58rem">${svcs}</div>` : ''} ${svcs ? `<div style="font-size:0.58rem">${svcs}</div>` : ''}
</div> </div>
${alive ? `<div style="display:flex;gap:5px;margin-top:6px"> ${alive ? `<div style="display:flex;gap:5px;margin-top:6px">
<button onclick="event.stopPropagation();agentScreenshot('${ag.hostname}')" style="flex:1;background:rgba(0,212,255,0.06);border:1px solid var(--panel-border);border-radius:3px;padding:3px 6px;color:var(--cyan);font-family:var(--font-display);font-size:0.48rem;letter-spacing:1px;cursor:pointer"> SCREENSHOT</button> <button onclick="event.stopPropagation();agentScreenshot('${escJs(ag.hostname)}')" style="flex:1;background:rgba(0,212,255,0.06);border:1px solid var(--panel-border);border-radius:3px;padding:3px 6px;color:var(--cyan);font-family:var(--font-display);font-size:0.48rem;letter-spacing:1px;cursor:pointer"> SCREENSHOT</button>
<button onclick="event.stopPropagation();agentSysinfo('${ag.hostname}')" style="flex:1;background:rgba(0,212,255,0.06);border:1px solid var(--panel-border);border-radius:3px;padding:3px 6px;color:var(--text-dim);font-family:var(--font-display);font-size:0.48rem;letter-spacing:1px;cursor:pointer"> SYSINFO</button> <button onclick="event.stopPropagation();agentSysinfo('${escJs(ag.hostname)}')" style="flex:1;background:rgba(0,212,255,0.06);border:1px solid var(--panel-border);border-radius:3px;padding:3px 6px;color:var(--text-dim);font-family:var(--font-display);font-size:0.48rem;letter-spacing:1px;cursor:pointer"> SYSINFO</button>
</div>` : ''} </div>` : ''}
</div>`; </div>`;
}).join(''); }).join('');
} }
function openAgentModal() { async function openAgentModal() {
const os = detectOS(); const os = detectOS();
const title = document.getElementById('agentModalTitle'); const title = document.getElementById('agentModalTitle');
const content = document.getElementById('agentModalContent'); const content = document.getElementById('agentModalContent');
const modal = document.getElementById('agentModal'); const modal = document.getElementById('agentModal');
const regKey = 'f846a9aaf7ce9a61742c63c87c4186052a71d2a580c65518'; let regKey = '<YOUR-REGISTRATION-KEY>';
const baseUrl = 'https://jarvis.orbishosting.com/agent'; try {
const rkResp = await fetch('/api/agent/regkey');
if (rkResp.ok) { const rk = await rkResp.json(); if (rk.registration_key) regKey = rk.registration_key; }
} catch (e) { /* not logged in — placeholder stays */ }
const jUrl = window.location.origin; const jUrl = window.location.origin;
// Fixed 2026-07-07: this used to be hardcoded to https://jarvis.orbishosting.com/agent,
// which isn't reachable from outside the LAN at all (no FortiGate VIP forwards the
// default port there — confirmed HTTP:000). Using the current page's own origin
// instead means the download link always matches wherever the visitor actually
// reached this dashboard from, LAN or external.
const baseUrl = jUrl + '/agent';
if (os === 'tablet') { if (os === 'tablet') {
title.textContent = '● JARVIS — TABLET / MOBILE'; title.textContent = '● JARVIS — TABLET / MOBILE';
@@ -460,9 +469,12 @@ function openAgentModal() {
const inst = { const inst = {
windows: { windows: {
label:'Windows', label:'Windows',
cmd:'# Run PowerShell as Administrator:\nSet-ExecutionPolicy Bypass -Scope Process -Force\nInvoke-WebRequest -Uri "'+baseUrl+'/install-windows.ps1" -OutFile "$env:TEMP\\install.ps1"\n& "$env:TEMP\\install.ps1" -JarvisUrl '+jUrl+' -Key '+regKey, // install-windows.ps1 takes no parameters — it reads the reg key from
dl: baseUrl+'/install-windows.ps1', // $env:JARVIS_REG_KEY and downloads the standalone exe itself (no
note:'Run PowerShell as Administrator. Installs as a Windows Task Scheduler service.' // Python/pywin32 needed on the target machine as of the 2026-07-07 rebuild).
cmd:'# Run PowerShell as Administrator:\n$env:JARVIS_REG_KEY=\''+regKey+'\'\nirm '+baseUrl+'/install-windows.ps1 | iex',
dl: baseUrl+'/install-windows.exe',
note:'Run PowerShell as Administrator. Installs as a real Windows Service (auto-starts at boot, no window to keep open).'
}, },
mac: { mac: {
label:'macOS', label:'macOS',
@@ -472,15 +484,17 @@ function openAgentModal() {
}, },
linux: { linux: {
label:'Linux', label:'Linux',
cmd:'curl -sSL '+baseUrl+'/install.sh | sudo bash -s -- \\\n --jarvis-url '+jUrl+' \\\n --key '+regKey, // install.sh takes positional args (hostname, agent_type); JARVIS URL and
// registration key come from env vars (key is no longer baked into the script).
cmd:'curl -sSL '+baseUrl+'/install.sh | JARVIS_URL='+jUrl+' JARVIS_REG_KEY=\''+regKey+'\' bash -s -- $(hostname) linux',
dl: baseUrl+'/install.sh', dl: baseUrl+'/install.sh',
note:'Run in terminal. Installs as a systemd service.' note:'Run in terminal (sudo). Installs as a systemd service.'
}, },
unknown: { unknown: {
label:'Your System', label:'Your System',
cmd:'# Browse installers:\nhttps://jarvis.orbishosting.com/agent/', cmd:'# Couldn\'t detect your OS automatically. Installers are at:\n'+baseUrl+'/install.sh (Linux)\n'+baseUrl+'/install-mac.sh (macOS)\n'+baseUrl+'/install-windows.ps1 (Windows)',
dl: 'https://jarvis.orbishosting.com/agent/', dl: baseUrl+'/install.sh',
note:'Choose your platform installer from the JARVIS agent directory.' note:'Auto-detection didn\'t recognize this browser/OS — pick the matching installer above.'
} }
}; };
const i = inst[os] || inst.unknown; const i = inst[os] || inst.unknown;
+1 -1
View File
@@ -84,7 +84,7 @@
<div id="leftPanel"> <div id="leftPanel">
<!-- Weather Widget --> <!-- Weather Widget -->
<div class="panel" style="flex:0 0 auto"> <div class="panel" style="flex:0 0 auto">
<div class="panel-title">WEATHER <span id="weather-loc" style="font-size:0.55rem;color:var(--text-dim)">FORT WORTH, TX</span></div> <div class="panel-title">WEATHER <span id="weather-loc" style="font-size:0.55rem;color:var(--text-dim)">WEATHERFORD, TX</span></div>
<div style="display:flex;align-items:flex-start;gap:12px;margin-bottom:8px"> <div style="display:flex;align-items:flex-start;gap:12px;margin-bottom:8px">
<div style="flex:1"> <div style="flex:1">
<div style="display:flex;align-items:baseline;gap:8px"> <div style="display:flex;align-items:baseline;gap:8px">
+22 -8
View File
@@ -1,20 +1,34 @@
#!/bin/bash #!/bin/bash
# JARVIS Agent Installer — one-liner for any Linux host: # JARVIS Agent Installer — one-liner for any Linux host:
# curl -sk https://jarvis.orbishosting.com/install-agent.sh | bash -s <hostname> <agent_type> # curl -sk http://jarvis.orbishosting.com:1972/agent/install.sh | bash -s <hostname> <agent_type>
# #
# agent_type: linux | proxmox | homeassistant # agent_type: linux | proxmox | homeassistant
# Example: curl -sk https://jarvis.orbishosting.com/install-agent.sh | bash -s myserver linux # Example: curl -sk http://jarvis.orbishosting.com:1972/agent/install.sh | bash -s myserver linux
#
# On the LAN, set JARVIS_URL to the direct internal address instead (faster,
# doesn't hairpin through Cloudflare): JARVIS_URL=http://10.48.200.211 curl ... | bash -s ...
set -e set -e
HOSTNAME_ARG="${1:-$(hostname -s)}" HOSTNAME_ARG="${1:-$(hostname -s)}"
AGENT_TYPE="${2:-linux}" AGENT_TYPE="${2:-linux}"
JARVIS_URL="https://165.22.1.228" # Fixed 2026-07-07: jarvis.orbishosting.com on the default port isn't reachable
JARVIS_HOST="jarvis.orbishosting.com" # from outside the LAN at all (no FortiGate VIP forwards it) — :1972 is the
# confirmed-working external path (same fix as the GitHub webhook and the
# Windows agent installer).
JARVIS_URL="${JARVIS_URL:-http://jarvis.orbishosting.com:1972}"
JARVIS_HOST=""
INSTALL_DIR="/opt/jarvis-agent" INSTALL_DIR="/opt/jarvis-agent"
CONFIG_DIR="/etc/jarvis-agent" CONFIG_DIR="/etc/jarvis-agent"
STATE_DIR="/var/lib/jarvis-agent" STATE_DIR="/var/lib/jarvis-agent"
REG_KEY="f846a9aaf7ce9a61742c63c87c4186052a71d2a580c65518" REG_KEY="${JARVIS_REG_KEY:-}"
if [ -z "$REG_KEY" ] && [ -r /dev/tty ]; then
read -rp "Enter JARVIS registration key: " REG_KEY </dev/tty
fi
if [ -z "$REG_KEY" ]; then
echo "ERROR: registration key required (set JARVIS_REG_KEY env var or enter at prompt)" >&2
exit 1
fi
SERVICE_FILE="/etc/systemd/system/jarvis-agent.service" SERVICE_FILE="/etc/systemd/system/jarvis-agent.service"
echo "=== JARVIS Agent Installer v3.0 ===" echo "=== JARVIS Agent Installer v3.0 ==="
@@ -38,7 +52,7 @@ mkdir -p "$INSTALL_DIR" "$CONFIG_DIR" "$STATE_DIR"
# ── Download agent ───────────────────────────────────────────────────────────── # ── Download agent ─────────────────────────────────────────────────────────────
echo "Downloading agent..." echo "Downloading agent..."
curl -sk -H "Host: $JARVIS_HOST" "$JARVIS_URL/agent/jarvis-agent.py" -o "$INSTALL_DIR/jarvis-agent.py" curl -sk "$JARVIS_URL/agent/jarvis-agent.py" -o "$INSTALL_DIR/jarvis-agent.py"
cp "$INSTALL_DIR/jarvis-agent.py" /usr/local/bin/jarvis-agent.py cp "$INSTALL_DIR/jarvis-agent.py" /usr/local/bin/jarvis-agent.py
chmod +x "$INSTALL_DIR/jarvis-agent.py" /usr/local/bin/jarvis-agent.py chmod +x "$INSTALL_DIR/jarvis-agent.py" /usr/local/bin/jarvis-agent.py
@@ -50,14 +64,14 @@ else
{ {
"jarvis_url": "$JARVIS_URL", "jarvis_url": "$JARVIS_URL",
"host_header": "$JARVIS_HOST", "host_header": "$JARVIS_HOST",
"ssl_verify": false, "ssl_verify": true,
"registration_key": "$REG_KEY", "registration_key": "$REG_KEY",
"hostname": "$HOSTNAME_ARG", "hostname": "$HOSTNAME_ARG",
"agent_type": "$AGENT_TYPE", "agent_type": "$AGENT_TYPE",
"poll_interval": 30, "poll_interval": 30,
"heartbeat_every": 10, "heartbeat_every": 10,
"update_check_hours": 24, "update_check_hours": 24,
"watch_services": ["ollama", "homeassistant", "mysql", "mariadb", "nginx", "apache2", "docker"] "watch_services": []
} }
JSONEOF JSONEOF
chmod 600 "$CONFIG_DIR/config.json" chmod 600 "$CONFIG_DIR/config.json"
+23 -2
View File
@@ -1,20 +1,39 @@
<?php <?php
ini_set('session.cache_limiter', ''); ini_set('session.cache_limiter', '');
header('Cache-Control: no-store, no-cache, must-revalidate, no-transform'); header('Cache-Control: no-store, no-cache, must-revalidate, no-transform');
require_once __DIR__ . '/../api/config.php';
session_start(); session_start();
if (!empty($_SESSION['jarvis_token'])) { header('Location: /'); exit; } if (!empty($_SESSION['jarvis_token'])) { header('Location: /'); exit; }
$error = ''; $error = '';
// ── Login rate limiting (Redis, per client IP) ────────────────────────────────
// Blocks brute force: 10 failed attempts within 15 min -> locked out for 15 min.
$clientIp = $_SERVER['HTTP_CF_CONNECTING_IP'] ?? $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? 'unknown';
$clientIp = trim(explode(',', $clientIp)[0]);
$rl = null;
try {
$rl = new Redis();
$rl->connect('127.0.0.1', 6379, 1.5);
} catch (Throwable $e) { $rl = null; } // fail open if Redis is down
$rlKey = 'login_fail:' . $clientIp;
$RL_MAX = 10; $RL_WINDOW = 900;
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$fails = ($rl && $rl->exists($rlKey)) ? (int)$rl->get($rlKey) : 0;
if ($fails >= $RL_MAX) {
$error = 'TOO MANY ATTEMPTS — LOCKED';
} else {
$u = trim($_POST['username'] ?? ''); $u = trim($_POST['username'] ?? '');
$p = $_POST['password'] ?? ''; $p = $_POST['password'] ?? '';
if ($u && $p) { if ($u && $p) {
$pdo = new PDO('mysql:host=localhost;dbname=jarvis_db;charset=utf8mb4', $pdo = new PDO('mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';charset=utf8mb4',
'jarvis_user', 'J4rv1s_Pr0t0c0l_2026!', DB_USER, DB_PASS,
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
$row = $pdo->prepare('SELECT * FROM users WHERE username=? LIMIT 1'); $row = $pdo->prepare('SELECT * FROM users WHERE username=? LIMIT 1');
$row->execute([$u]); $row->execute([$u]);
$user = $row->fetch(PDO::FETCH_ASSOC); $user = $row->fetch(PDO::FETCH_ASSOC);
if ($user && password_verify($p, $user['password_hash'])) { if ($user && password_verify($p, $user['password_hash'])) {
if ($rl) $rl->del($rlKey);
session_regenerate_id(true);
$token = bin2hex(random_bytes(32)); $token = bin2hex(random_bytes(32));
$_SESSION['jarvis_token'] = $token; $_SESSION['jarvis_token'] = $token;
$_SESSION['jarvis_user_id'] = $user['id']; $_SESSION['jarvis_user_id'] = $user['id'];
@@ -23,8 +42,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
header('Location: /'); header('Location: /');
exit; exit;
} }
if ($rl) { $rl->incr($rlKey); $rl->expire($rlKey, $RL_WINDOW); }
$error = 'ACCESS DENIED'; $error = 'ACCESS DENIED';
} else { $error = 'ENTER CREDENTIALS'; } } else { $error = 'ENTER CREDENTIALS'; }
}
} }
?><!DOCTYPE html> ?><!DOCTYPE html>
<html lang="en"><head> <html lang="en"><head>
+6 -4
View File
@@ -14,7 +14,7 @@ if (!defined('WEBHOOK_SECRET')) {
exit; exit;
} }
define('DEPLOY_QUEUE', '/tmp/jarvis-deploy-queue.txt'); define('DEPLOY_QUEUE', '/tmp/jarvis-deploy-queue.txt');
define('DEPLOY_LOG', '/var/www/jarvis/logs/deploy.log'); define('DEPLOY_LOG', '/var/log/jarvis/deploy.log');
header('Content-Type: application/json'); header('Content-Type: application/json');
@@ -33,9 +33,10 @@ $repo = $data['repository']['name'] ?? '';
$ref = $data['ref'] ?? ''; $ref = $data['ref'] ?? '';
$pusher = $data['pusher']['name'] ?? 'unknown'; $pusher = $data['pusher']['name'] ?? 'unknown';
// Only deploy on pushes to main // Only deploy on pushes to the repo's actual default branch (master, not main
if ($ref !== 'refs/heads/main') { // this was checking 'main' for a while even though the jarvis repo has always used 'master')
echo json_encode(['ok' => true, 'skipped' => "ref $ref is not main"]); if ($ref !== 'refs/heads/master') {
echo json_encode(['ok' => true, 'skipped' => "ref $ref is not master"]);
exit; exit;
} }
@@ -56,3 +57,4 @@ file_put_contents(DEPLOY_QUEUE, $path . "\n", FILE_APPEND | LOCK_EX);
file_put_contents(DEPLOY_LOG, "[$ts] Queued deploy: $repo by $pusher -> $path\n", FILE_APPEND | LOCK_EX); file_put_contents(DEPLOY_LOG, "[$ts] Queued deploy: $repo by $pusher -> $path\n", FILE_APPEND | LOCK_EX);
echo json_encode(['ok' => true, 'queued' => $repo, 'path' => $path]); echo json_encode(['ok' => true, 'queued' => $repo, 'path' => $path]);
// deploy pipeline verified working 2026-07-07