Compare commits

58 Commits

Author SHA1 Message Date
myron a13f750846 feat(kb-intent): expand to 140 topics, 109 KB topic library
Rich topic library across mathematics (12), sciences (13), history (10),
government/economics, literature, life skills (13), technology (5),
Texas/local (4), national US (3), world affairs (5), human sexuality (2),
astronomy (3), space (2), culture/arts (8), sports (8), food/drink (4),
home/DIY (3), wellness (4), tech continued (3), national continued (2),
world continued (2), medicine (3), math continued (2), communication (2).

Each topic has detailed multi-subtopic descriptions (~200 chars each)
vs prior 8-word descriptions — significantly richer Groq prompts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014p87VFec84hNaf2WpvmLrW
2026-07-02 06:50:12 -05:00
myron 3c8dd8e206 feat(kb-intent): 122-topic rotation engine, every-6h cron
- Expanded $BATCHES from 25 to 122 topics across: life skills, personal
  finance, Texas/local, US national, world geopolitics, human sexuality
  (educational), deep astronomy (15 topics), and space exploration (15 topics)
- Rotation engine: 25 topics per run cycling through all 122 in order;
  wraps to topic 1 on cycle complete (~30h full cycle at 6h interval)
- batch_offset tracked in kb_facts for cross-run persistence
- Cron changed from 0 3 * * * to 0 */6 * * * (every 6 hours)
- 20h skip guard reduced to 4h so 6h cron isn't blocked
- max_tokens bumped 3500 → 5000 to prevent JSON truncation errors
- JSON extraction: partial recovery + raw snippet logged on failure

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014p87VFec84hNaf2WpvmLrW
2026-07-01 22:53:57 -05:00
myron 554488aefa fix(kb-intent): reduce batch size 40→20 intents, raise max_tokens to 5000
Token budget was too low for 40 intents — responses were truncated mid-JSON,
causing "No JSON array found" on ~88% of batches. Fixes 880 errors/run.
Also adds partial-JSON truncation recovery and raw response debug logging.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014p87VFec84hNaf2WpvmLrW
2026-07-01 22:44:22 -05:00
myron e99c3aa171 feat(admin): KB intent generator — force run, background on close, history panel
- Always runs when triggered from admin (JARVIS_FORCE_RUN=1 bypasses 20h guard)
- Closing popup stops UI polling but server job continues; toast confirms background run
- History panel at top of popup shows last success time/count + failures from last 7 days
- New intent_gen_history backend case parses cron.log for status summary

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014p87VFec84hNaf2WpvmLrW
2026-07-01 22:39:41 -05:00
myron 2528b37ea1 docs: update infrastructure reference for July 2026 session
- VM 210: correct SSH path, add llava:7b vision model
- Add Arc Reactor section (service, logs, SETUP button, vision cascade)
- JARVIS AI tiers: llama3.2 → llama3.1:8b, add vision tier
- Backup Systems: add JARVIS database backup (path, format, retention)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014p87VFec84hNaf2WpvmLrW
2026-07-01 22:32:31 -05:00
myron 46dabe3c31 feat(admin): Arc Reactor SETUP button — live log popup
- Replace arcSetup() confirm/toast with openModal() live popup
- Add arc_setup_log PHP backend (polls /var/log/jarvis/arc-setup.log)
- Color-coded line output: green=ok/done/active, red=error/fail, yellow=warn
- Auto-detects completion keywords and updates status header
- CLOSE button stops polling and refreshes worker table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014p87VFec84hNaf2WpvmLrW
2026-07-01 22:30:39 -05:00
myron d6a1fc9456 Fix backup script: tar.gz output, lockfile, correct log path, install at /usr/local/bin
- Renamed output to jarvis_backup_TIMESTAMP.tar.gz (admin panel pattern)
- Log now goes to /var/backups/jarvis/backup.log (admin panel reads this)
- Lockfile at /var/backups/jarvis/backup.lock (prevents concurrent runs)
- tmpdir + trap cleanup for safe temp handling
- Install at /usr/local/bin/jarvis-backup.sh (admin panel trigger path)
2026-07-01 22:20:58 -05:00
myron 2f74b98bbc Admin panel: add Arc Reactor SETUP button, fix RESTART to use systemctl
- SETUP button in Workers > Daemons runs full deploy:
  copies reactor.py + requirements.txt from deploy/, creates venv,
  pip install, installs jarvis-arc.service, systemctl enable + restart
- RESTART button now calls systemctl restart jarvis-arc (was nohup pkill)
- arcSetup() JS function with confirm dialog and loading state
- Setup log written to /var/log/jarvis/arc-setup.log
2026-07-01 22:13:28 -05:00
myron ed15ff12dd Add _vision_call() helper with Claude -> Ollama -> graceful fallback
- Centralises all vision logic in one place instead of duplicated inline blocks
- Claude remains primary; set OLLAMA_VISION_MODEL env var to enable a local
  vision model (e.g. llava, moondream) as automatic fallback
- Graceful degradation message when no vision provider is available
- Both handle_screenshot and handle_vision now use _vision_call()
2026-07-01 21:37:11 -05:00
myron 3f18cec739 Add missing email_sent/email_actions/email_triage tables to schema; add sent_at column 2026-07-01 21:29:48 -05:00
myron 8911645c20 Add jarvis-backup.sh — daily mysqldump with 7-day retention
Backup script was never migrated when JARVIS moved from DO to VM 211.
Runs daily at 3am, stores compressed dumps in /var/backups/jarvis/,
logs to /var/log/jarvis/backup.log.
2026-07-01 21:22:02 -05:00
myron af03a2f2d8 Fix reactor startup: auto-create venv, requirements.txt, SETUP button, self-install
- deploy/requirements.txt: explicit pip deps (fastapi, uvicorn, aiomysql, aiohttp, anthropic, trafilatura)
- jarvis-deploy.sh: self-installs to /usr/local/bin/ on every run so updates propagate;
  auto-creates venv and installs packages if missing before restart attempt
- admin/index.php: add SETUP button next to RESTART — runs full setup+start in one click
  (creates venv, installs deps, copies reactor.py, starts it)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X8tDRrQqgLjqXebMCBNcP3
2026-07-01 04:11:12 -05:00
myron dfc92a6791 Merge branch 'master' 2026-07-01 04:09:28 -05:00
myron 05522edb1d Add llava vision: use Ollama llava:7b for all image analysis, Claude as fallback
- Add _ollama_vision_call() using Ollama /api/generate with images array
- handle_screenshot: try llava first (free, on-LAN), fall back to Claude on error;
  text-only snapshots now use ollama instead of groq
- handle_vision: same llava-first/Claude-fallback pattern; caller can force
  provider='ollama' or 'claude' explicitly; stored provider_used reflects actual
  provider that ran

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X8tDRrQqgLjqXebMCBNcP3
2026-07-01 04:05:37 -05:00
myron 435af8ccc9 Wire Ollama properly: fix IP, upgrade to llama3.1:8b, use for guardian/sitrep
- reactor.py: fix OLLAMA_HOST .95 → .210 (actual Ollama VM IP)
- reactor.py: upgrade OLLAMA_MODEL 1b → 8b (llama3.1:8b has tool-calling, 131K ctx)
- reactor.py: guardian alerts and sitrep now use ollama instead of groq (free, on-LAN, no quota)
- config.example.php: same IP/model fixes + fix GROQ_MODEL_SEARCH to compound-beta-mini (groq/ prefix causes 404)
- deploy script: patch live config.php on every deploy to correct Ollama IP/model and Groq model name

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X8tDRrQqgLjqXebMCBNcP3
2026-07-01 04:02:30 -05:00
myron a9ea75db98 Fix Arc Reactor restart: use bash for source, add systemd service, fix deploy
The admin panel restart command used shell_exec which runs /bin/sh (dash on
Ubuntu) — dash doesn't support 'source', so the venv never activated and the
reactor silently never started.

- admin/index.php: wrap restart in bash -c so 'source' works; try systemctl
  first then fall back to nohup
- deploy/jarvis-arc.service: add proper systemd unit so reactor auto-starts
  on boot and auto-restarts on crash
- deploy/jarvis-deploy.sh: install+enable service file when it changes; mkdir
  log dir before restart; fall back to nohup if systemctl not set up yet

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X8tDRrQqgLjqXebMCBNcP3
2026-07-01 03:56:11 -05:00
myron 651455cb47 Fix guardian mode: create missing tables on startup, fix conversations column name
- Add CREATE TABLE IF NOT EXISTS for guardian_config and guardian_events in
  reactor lifespan so tables are created automatically on next restart
- Fix conversations column bug: reactor used 'message' but schema has 'content';
  fix INSERT and SELECT queries to use content (SELECT aliases it as 'message'
  so the JSON response key stays the same)
- Add guardian tables to schema.sql for documentation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X8tDRrQqgLjqXebMCBNcP3
2026-07-01 03:49:10 -05:00
myron 90e4ded7c9 Fix 8 issues from code review
- ha-poller: replace recursive main() retry with while loop (stack overflow fix)
- ha-poller: advance last_push on empty HA response (log spam fix)
- ha-poller: use datetime.now(timezone.utc) instead of deprecated utcnow()
- ping-probe: always call update_status() unconditionally so offline devices register as offline
- agent.php: heartbeat reads status from payload instead of hardcoding 'online'
- phone-probe: delegate JSON building to python3 (bash concatenation injection fix)
- netscan + phone-probe: read registration key from /etc/jarvis-agent/reg-key
- admin/index.php: sync ha_list skipDomains with ha.php (14 missing domains added)
- facts_collector: self-check JARVIS via 127.0.0.1 instead of Cloudflare hairpin
2026-06-29 20:58:22 -05:00
myron c1275d47a6 Add PVE1 probe scripts to repo (netscan, ping-probe, phone-probe)
Scripts were running on PVE1 but not tracked in git. Pulling current
versions that push to http://10.48.200.211 (was old DO server IP).
2026-06-29 19:44:39 -05:00
myron 08fbfaa3e4 Seed kb_intents/preferences, fix usage_patterns column, update schema, fix site URL
- db/seed_kb.sql: 25 intent patterns + user prefs (Myron / Mr. Blair)
- usage_patterns: renamed last_used→last_seen to match chat.php
- facts_collector: JARVIS self-check URL was port 1972 (DO), now correct URL
- db/schema.sql: reflects current live DB schema

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 18:15:53 -05:00
myron 1f25b5d04d Fix facts_collector JARVIS site URL (was :1972 DO port, now correct URL)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 18:13:12 -05:00
myron 84cd2ded50 Add HA poller, fix domain filters, create missing DB tables, update schema
- jarvis-ha-poller.py: new service polling HA entities → JARVIS (running on VM211)
- ha.php: add camera/siren/remote/todo/lawn_mower to skipDomains
- db/schema.sql: add tasks, appointments, usage_patterns tables; fix registered_agents enum (windows/macos) + version column

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 15:44:28 -05:00
myron 89a82a1573 Add Windows agent installer, fix Linux install URL
- install-windows.ps1: one-liner PowerShell installs Python, pywin32,
  downloads agent, creates config, installs Windows Service (auto-start)
- install.sh: fix JARVIS_URL from hardcoded LAN IP to https://jarvis.orbishosting.com
- install.sh: fix ssl_verify default to true for external agents

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 13:56:37 -05:00
myron 874f6e8c5c fix: rename parkersling site key to parkerslingshotrentals in facts_collector 2026-06-23 18:09:30 +00:00
myron 42a82c40cb fix: remove dead __NOVACPX_VM__ branch, dedupe date() call in webhook 2026-06-23 18:03:22 +00:00
myron e68bb7d165 feat: HA filter — remove floodlights, water heater, scenes, media players 2026-06-22 03:58:57 +00:00
myron 2f4b4ef5c3 feat: HA tab — filter scenes/media_player, nightly full resync cron, remove JS polling
- ha.php skipDomains: added media_player, scene
- ha.php skipKeywords: konnected, energy/power/voltage/current, full camera list
- stats_cache.php: same filter updates, removed scene/media_player from sync
- Removed JS setInterval polling; entity state kept fresh by HA agent push
- Added nightly 3am cron for full HA entity resync
2026-06-22 03:56:47 +00:00
myron 21e0b81a98 feat: HA tab — filter konnected/energy/camera/media_player, add 30s auto-refresh
- Added to skipDomains: media_player
- Added to skipKeywords: konnected, energy/power/voltage/current,
  camera controls (infrared, email, FTP, push, siren, hub ringtone, manual record),
  system noise (CEC scanner, ESPHome builder, Echo DND)
- Auto-refresh every 30s when HA tab is active
2026-06-22 03:53:06 +00:00
myron 95d49f15cb fix: kiosk voice reliability — stopListening on exit, exitVoiceMode kiosk guard
- stopListening() called in both toggleKiosk exit and _onFsChange so mic
  stops when leaving kiosk (was staying live indefinitely)
- exitVoiceMode() now returns early if kiosk-mode active so the 30-min
  idle timer and face-detection loop cannot kill the always-on mic
2026-06-21 14:26:54 +00:00
myron 51b598dd5d fix: kiosk voice — use startListening() directly, no TTS greeting blocking mic 2026-06-21 05:20:56 +00:00
myron 6f0459be85 feat: kiosk auto-starts voice mode and blocks sleep — isolated patches only 2026-06-21 05:17:55 +00:00
myron a6d4365f16 feat: kiosk mode CSS hiding (safe) — no voice JS patches 2026-06-21 05:15:38 +00:00
myron 383de0146c revert: restore all files to 52ddee3 — kiosk JS patches broke JARVIS completely 2026-06-21 05:10:30 +00:00
myron aaf9f9d56a revert: restore safe JS, keep only kiosk-mode CSS class toggle — voice patches caused JS crash 2026-06-21 05:03:56 +00:00
myron aa88a2f73b fix: missing quotes around kiosk-mode string caused ReferenceError breaking all buttons 2026-06-21 04:59:21 +00:00
myron f1d73e7b6a feat: kiosk always-on mic — auto-start voice on kiosk entry, no sleep, no wake word needed 2026-06-21 04:55:29 +00:00
myron 572f1b1816 feat: show HTTPS redirect banner on Silk/tablet when loaded via HTTP (mic/camera fix) 2026-06-21 04:45:40 +00:00
myron 1838e02d56 feat: hide network status panel in kiosk mode; bump cache version 2026-06-21 04:41:33 +00:00
myron 178040c18b chore: bump asset version to 20260621 to bust Silk browser cache 2026-06-21 04:30:28 +00:00
myron 45845a1f61 feat: kiosk-mode hides server, agents, guardian panels + HA/agents/memory/proxmox from bottom bar
- Adds body.kiosk-mode class on fullscreen entry/exit
- Hides: #server-panel, #tab-agents, #tab-guardian, tab buttons
- Hides bottom bar: Home Assistant, Agents, Memory, Proxmox
- Falls back to INTEL tab if agents/guardian was active on kiosk entry
- All elements remain visible in normal/tablet mode
2026-06-21 04:07:32 +00:00
myron 52ddee3e78 Fire HD 8 tablet mode: auto-detect Silk UA, optimised layout + touch targets 2026-06-19 16:17:48 +00:00
myron ab1aa16ac8 Add kiosk mode button for Fire tablet Silk browser 2026-06-19 16:02:51 +00:00
myron 1979c5f667 fix: install-agent.sh default URL updated to http://10.48.200.211 (JARVIS VM) 2026-06-18 12:34:32 +00:00
myron 1b071f4f67 fix: repair broken define in webhook.php (missing closing quote from prior sed) 2026-06-18 04:44:36 +00:00
myron 5cbaeda730 docs: update INFRASTRUCTURE-REFERENCE and CLAUDE.md for JARVIS VM migration
- JARVIS moved from DO to PVE1 VM 211 (10.48.200.211, 8c/16GB)
- Access: http://jarvis.orbishosting.com:1972 (FortiGate VIP)
- Stack: nginx + PHP 8.3 + MariaDB + Redis + Arc Reactor
- Ollama VM IP: 10.48.200.95 → 10.48.200.210 (Reolink owns .95)
- FusionPBX SSH now direct via Tailscale (100.74.46.120)
- DO role: websites only (JARVIS fully removed)
- Agent URLs updated: http://10.48.200.211 (LAN direct)
- DO agent uses Tailscale: http://100.77.178.42

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 04:38:02 +00:00
myron 5140573be0 fix: update system.php service list for JARVIS VM (nginx/php-fpm/mariadb/redis/arc/agent) 2026-06-18 04:18:07 +00:00
myron b7aea1371c feat: add DO server (web host) monitoring block to JARVIS Server panel
- /api/do now includes do_server key with jarvis-do agent metrics
  (CPU, RAM, disk, uptime from Tailscale-connected DO server agent)
- Front page JARVIS SERVER panel has WEB HOST section with live
  CPU/RAM/DISK bars from DO server agent data
- Panel title updated to show 10.48.200.211 (JARVIS VM IP)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 04:08:54 +00:00
myron 49694e76e1 fix: update service monitor for JARVIS VM (nginx/php-fpm/mariadb instead of OLS/mysql) 2026-06-18 04:01:36 +00:00
myron 04510ac39f fix: update facts_collector for JARVIS VM (not DO web host)
- Site checks use external URLs instead of 127.0.0.1 loopback (JARVIS
  no longer shares a server with the websites)
- JARVIS site URL updated to port 1972
- Fixed syntax error in DO server ping exec call
- Removed Host header injection (not needed for external checks)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 03:53:44 +00:00
myron 38ab8d2977 migrate: update all references from DO server to PVE1 JARVIS VM
- config.php: JARVIS_IP → 10.48.200.211, HA_URL → direct LAN 10.48.200.97
- facts_collector/stats_cache: Proxmox API → direct 10.48.200.90 (not DDNS)
- chat.php: system context updated to reflect PVE1/nginx instead of DO/OLS
- do_server.php: display IP → 10.48.200.211 (reads /proc for JARVIS VM stats)
- jarvis-app.js: service labels nginx/mariadb instead of lshttpd
- jarvis-overlays.js: network map JARVIS node IP → 10.48.200.211
- index.html: DO SERVER labels → JARVIS VM, cache bust v=20260618a
- jarvis-agents.js: agent install URL uses window.location.origin

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 02:25:36 +00:00
myron ca66152f45 perf: fix facts_collector blocking cron that was saturating PHP workers
Three issues caused periodic worker saturation:
1. Network section pinged 5 private LAN IPs (10.48.200.x) unreachable
   from DO — each failed after 1s timeout = 5s wasted per run.
   Replaced with a fast DB query on registered_agents.
2. pve_api_get() had no CURLOPT_CONNECTTIMEOUT — added 3s limit so
   unreachable Proxmox fails fast instead of blocking the full 8s.
3. Ollama curl timeout reduced from 5s→3s total, added 2s connect limit.

Cron interpreter also changed from lsphp85 to php8.3 in crontab
(done directly on server) — lsphp85 adds ~8s LSAPI startup overhead
and consumes a PHP worker slot; php8.3 runs standalone.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 19:40:13 +00:00
myron 0b7f2d013b refactor: Phase 3 — split jarvis-protocols.js into 3 panel files
A single SyntaxError in the 1668-line monolith kills every panel
(proven by the apostrophe bug on 2026-06-17). Split into:

  panels/jarvis-arc.js       (608 lines) — Arc Reactor, Intel, Comms, Guardian
  panels/jarvis-agents.js    (715 lines) — Missions, Directives, Memory,
                                           Clearance, Agents tab, Sites, Vision
  panels/jarvis-assistant.js (345 lines) — Chat History, Suggestions,
                                           Mobile, Command Palette, Topo map

A parse error in any one file now fails only that group of panels.
escHtml() stays in jarvis-arc.js (loads first) and remains global.
All other dependencies (api, speak, addMessage) come from jarvis-app.js.
Version param bumped to ?v=20260617b to force Cloudflare cache miss.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 19:10:31 +00:00
myron 8085a113d5 fix: sync public_html/agent/jarvis-agent.py with agent source
public_html/agent/ is what agents download for self-update.
It was 5 days out of date — missing the version-in-heartbeat fix
and all other v3.1 changes. Now mirrors agent/jarvis-agent.py exactly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 18:45:54 +00:00
myron b85e8dd16f fix: include version in heartbeat payload so Workers tab shows real versions
Heartbeat was sending {} — version only appeared in registration.
Agents that never re-register (most of them) stayed NULL in the DB.
Now every heartbeat carries {"version": AGENT_VERSION} so agent.php
can update the column on every check-in.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 18:36:51 +00:00
myron 188f6f8f10 fix: persist agent version on every heartbeat
update_agent_seen() now updates version column when agents include it
in their heartbeat payload. Previously version was only stored on
registration, leaving the Workers tab showing NULL for agents that
hadn't re-registered since v3.1.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 17:19:56 +00:00
myron 7f6397b514 perf: route Guardian and Vision text analysis to Groq instead of Claude
Guardian anomaly alerts and SITREP are pure text reasoning — Groq's
llama-3.3-70b-versatile handles them at near-zero cost with lower
latency. Vision Protocol image analysis stays on Claude (claude-opus-
4-8) because Groq has no vision models. Text-only sysinfo snapshots
(no image captured) also move to Groq.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 17:06:15 +00:00
myron dd2f48193b fix: add data-cfasync=false to face-api.js to suppress Rocket Loader
One untagged script tag is enough for Cloudflare Rocket Loader to
activate its bootstrap and inject mainScript.js, which declares
mainScriptFlag. When mainScript.js loads twice (script + eval), it
throws SyntaxError: Identifier 'mainScriptFlag' has already been
declared. All script tags now have data-cfasync=false.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 14:43:29 +00:00
myron 1e57a7c90c fix: check sites locally to avoid Cloudflare CDN timeouts
facts_collector was checking https://jarvis.orbishosting.com from the
DO server itself — traffic routes through Cloudflare CDN which can
return 524 timeouts. All sites are hosted on this same OLS instance,
so check via http://127.0.0.1 with a Host header instead. This gives
direct OLS response without CDN overhead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 14:30:40 +00:00
38 changed files with 4504 additions and 2151 deletions
+151
View File
@@ -0,0 +1,151 @@
#Requires -RunAsAdministrator
<#
.SYNOPSIS
JARVIS Agent installer for Windows.
.DESCRIPTION
Installs JARVIS Agent as a Windows Service that auto-starts at boot.
Requires: PowerShell 5.1+, internet access, and Administrator rights.
.EXAMPLE
# Interactive install (prompts for registration key):
irm https://jarvis.orbishosting.com/agent/install-windows.ps1 | iex
# Silent install with key:
$env:JARVIS_REG_KEY='your_key_here'; irm https://jarvis.orbishosting.com/agent/install-windows.ps1 | iex
#>
$ErrorActionPreference = 'Stop'
$JARVIS_URL = 'https://jarvis.orbishosting.com'
$INSTALL_DIR = 'C:\ProgramData\jarvis-agent'
$SERVICE_NAME = 'JARVISAgent'
$AGENT_SCRIPT = "$INSTALL_DIR\jarvis-agent-windows.py"
$CONFIG_FILE = "$INSTALL_DIR\config.json"
function Write-Step { param($msg) Write-Host "`n[JARVIS] $msg" -ForegroundColor Cyan }
function Write-OK { param($msg) Write-Host " OK: $msg" -ForegroundColor Green }
function Write-Fail { param($msg) Write-Host " ERROR: $msg" -ForegroundColor Red; exit 1 }
Write-Host "`n========================================" -ForegroundColor Yellow
Write-Host " JARVIS Agent Installer for Windows" -ForegroundColor Yellow
Write-Host "========================================`n" -ForegroundColor Yellow
# ── Stop existing service if running ─────────────────────────────────────────
$existing = Get-Service -Name $SERVICE_NAME -ErrorAction SilentlyContinue
if ($existing) {
Write-Step "Stopping existing JARVIS Agent service..."
if ($existing.Status -eq 'Running') {
Stop-Service -Name $SERVICE_NAME -Force
Start-Sleep 2
}
try {
& python "$INSTALL_DIR\jarvis-agent-windows.py" remove 2>$null
} catch {}
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 ────────────────────────────────────────────────────────
Write-Step "Creating install directory..."
New-Item -ItemType Directory -Path $INSTALL_DIR -Force | Out-Null
Write-OK $INSTALL_DIR
# ── Download agent script ─────────────────────────────────────────────────────
Write-Step "Downloading JARVIS agent..."
try {
Invoke-WebRequest -Uri "$JARVIS_URL/agent/jarvis-agent-windows.py" -OutFile $AGENT_SCRIPT -UseBasicParsing
Write-OK "Agent downloaded to $AGENT_SCRIPT"
} catch {
Write-Fail "Failed to download agent: $_"
}
# ── Get registration key ──────────────────────────────────────────────────────
$regKey = $env:JARVIS_REG_KEY
if (-not $regKey -and (Test-Path $CONFIG_FILE)) {
$existingCfg = Get-Content $CONFIG_FILE | ConvertFrom-Json
$regKey = $existingCfg.registration_key
if ($regKey) { Write-OK "Using existing registration key from config." }
}
if (-not $regKey) {
$regKey = Read-Host "`n Enter JARVIS registration key"
if (-not $regKey) { Write-Fail "Registration key required." }
}
# ── Get hostname ──────────────────────────────────────────────────────────────
$hostname = $env:COMPUTERNAME
$customHostname = $env:JARVIS_HOSTNAME
if ($customHostname) { $hostname = $customHostname }
# ── Write config ──────────────────────────────────────────────────────────────
Write-Step "Writing config..."
$cfg = @{
jarvis_url = $JARVIS_URL
registration_key = $regKey
hostname = $hostname
agent_type = 'windows'
ssl_verify = $true
poll_interval = 30
heartbeat_every = 10
update_check_hours = 24
watch_services = @('WinDefend', 'Spooler', 'wuauserv')
} | ConvertTo-Json -Depth 5
$cfg | Out-File -FilePath $CONFIG_FILE -Encoding utf8
Write-OK "Config written to $CONFIG_FILE"
# ── Install Windows Service ───────────────────────────────────────────────────
Write-Step "Installing Windows service..."
$pyPath = (Get-Command python).Source
& $pyPath "$AGENT_SCRIPT" --startup auto install
if ($LASTEXITCODE -ne 0) { Write-Fail "Service install failed." }
Write-OK "Service '$SERVICE_NAME' installed."
# ── Start service ─────────────────────────────────────────────────────────────
Write-Step "Starting service..."
Start-Service -Name $SERVICE_NAME
Start-Sleep 3
$svc = Get-Service -Name $SERVICE_NAME
if ($svc.Status -ne 'Running') { Write-Fail "Service failed to start. Check C:\ProgramData\jarvis-agent\jarvis-agent.log" }
Write-OK "Service is running."
# ── Test connectivity ─────────────────────────────────────────────────────────
Write-Step "Testing JARVIS connection..."
try {
$ping = Invoke-RestMethod -Uri "$JARVIS_URL/api/ping" -TimeoutSec 10
Write-OK "JARVIS is online: $($ping.codename)"
} catch {
Write-Host " WARNING: Could not reach JARVIS at $JARVIS_URL - check connectivity." -ForegroundColor Yellow
}
Write-Host "`n========================================" -ForegroundColor Green
Write-Host " JARVIS Agent installed successfully!" -ForegroundColor Green
Write-Host " Hostname: $hostname" -ForegroundColor Green
Write-Host " Service: $SERVICE_NAME (auto-start at boot)" -ForegroundColor Green
Write-Host " Logs: C:\ProgramData\jarvis-agent\jarvis-agent.log" -ForegroundColor Green
Write-Host "========================================`n" -ForegroundColor Green
+1 -1
View File
@@ -434,7 +434,7 @@ def main():
try: try:
# Heartbeat + get commands # Heartbeat + get commands
hb = api_post(f"{jarvis_url}/api/agent/heartbeat", {}, headers, ssl_verify=ssl_verify) hb = api_post(f"{jarvis_url}/api/agent/heartbeat", {"version": AGENT_VERSION}, headers, ssl_verify=ssl_verify)
if "error" in hb: if "error" in hb:
print(f"[WARN] Heartbeat failed: {hb['error']}", flush=True) print(f"[WARN] Heartbeat failed: {hb['error']}", flush=True)
else: else:
+235
View File
@@ -0,0 +1,235 @@
#!/usr/bin/env python3
"""
JARVIS HA Poller — pulls entity states from Home Assistant REST API
and pushes them to JARVIS as a homeassistant-type agent.
Runs on VM211 as a systemd service (jarvis-ha-poller).
Config: /etc/jarvis-agent/ha-poller.json
"""
import json
import os
import socket
import sys
import time
import urllib.request
import urllib.error
import ssl
from datetime import datetime, timezone
from pathlib import Path
CONFIG_PATH = "/etc/jarvis-agent/ha-poller.json"
STATE_PATH = "/var/lib/jarvis-agent/ha-poller-state.json"
AGENT_VERSION = "1.0"
AGENT_ID = "homeassistant_ha"
HOSTNAME = "homeassistant"
# Domains to skip — don't send to JARVIS (saves DB space, keeps UI clean)
SKIP_DOMAINS = {
'sensor', 'binary_sensor', 'button', 'update', 'select', 'number',
'device_tracker', 'event', 'image', 'person', 'zone', 'tts',
'conversation', 'assist_satellite', 'input_button', 'media_player',
'scene', 'water_heater', 'alarm_control_panel', 'automation',
'script', 'calendar', 'notify', 'weather', 'sun', 'persistent_notification',
'tag', 'system_health', 'timer', 'counter',
'camera', 'siren', 'remote', 'todo', 'lawn_mower',
}
def log(msg: str):
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(f"[{ts}] {msg}", flush=True)
def load_config() -> dict:
if not os.path.exists(CONFIG_PATH):
print(f"[ERROR] Config not found at {CONFIG_PATH}", flush=True)
sys.exit(1)
with open(CONFIG_PATH) as f:
return json.load(f)
def load_state() -> dict:
if os.path.exists(STATE_PATH):
with open(STATE_PATH) as f:
return json.load(f)
return {}
def save_state(state: dict):
Path(STATE_PATH).parent.mkdir(parents=True, exist_ok=True)
with open(STATE_PATH, "w") as f:
json.dump(state, f, indent=2)
def _ssl_ctx(verify: bool):
if not verify:
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
return ctx
return None
def jarvis_post(url: str, payload: dict, headers: dict, ssl_verify: bool, timeout: int = 15) -> dict:
body = json.dumps(payload).encode()
req = urllib.request.Request(url, data=body, method="POST")
req.add_header("Content-Type", "application/json")
for k, v in headers.items():
req.add_header(k, v)
try:
ctx = _ssl_ctx(ssl_verify)
with urllib.request.urlopen(req, timeout=timeout, context=ctx) as resp:
return json.loads(resp.read().decode())
except urllib.error.HTTPError as e:
return {"error": f"HTTP {e.code}: {e.read().decode()[:200]}"}
except Exception as e:
return {"error": str(e)}
def ha_get(url: str, token: str, timeout: int = 15) -> dict | list | None:
req = urllib.request.Request(url)
req.add_header("Authorization", f"Bearer {token}")
req.add_header("Content-Type", "application/json")
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
return json.loads(resp.read().decode())
except Exception as e:
log(f"HA API error: {e}")
return None
def register(cfg: dict, state: dict) -> str:
jarvis_url = cfg["jarvis_url"].rstrip("/")
ssl_verify = bool(cfg.get("ssl_verify", False))
reg_key = cfg["registration_key"]
log(f"Registering HA poller with JARVIS at {jarvis_url}...")
result = jarvis_post(
f"{jarvis_url}/api/agent/register",
{
"hostname": HOSTNAME,
"version": AGENT_VERSION,
"agent_type": "homeassistant",
"ip_address": cfg.get("ha_url", "").split("//")[-1].split(":")[0],
"capabilities": ["ha_entities", "ha_state"],
"agent_id": AGENT_ID,
},
{"X-Registration-Key": reg_key},
ssl_verify,
)
if "error" in result:
log(f"Registration failed: {result['error']}")
return ""
api_key = result.get("api_key", "")
if api_key:
state["api_key"] = api_key
state["agent_id"] = AGENT_ID
save_state(state)
log(f"Registered. agent_id={AGENT_ID}")
return api_key
def push_entities(cfg: dict, api_key: str, entities: list) -> bool:
jarvis_url = cfg["jarvis_url"].rstrip("/")
ssl_verify = bool(cfg.get("ssl_verify", False))
headers = {"X-Agent-Key": api_key}
# Send in batches of 200
batch_size = 200
total = len(entities)
ok = True
for i in range(0, total, batch_size):
batch = entities[i:i+batch_size]
result = jarvis_post(
f"{jarvis_url}/api/agent/ha_state",
{"entities": batch},
headers,
ssl_verify,
)
if "error" in result:
log(f"Push batch {i//batch_size+1} failed: {result['error']}")
ok = False
return ok
def heartbeat(cfg: dict, api_key: str) -> bool:
jarvis_url = cfg["jarvis_url"].rstrip("/")
ssl_verify = bool(cfg.get("ssl_verify", False))
result = jarvis_post(
f"{jarvis_url}/api/agent/heartbeat",
{"version": AGENT_VERSION},
{"X-Agent-Key": api_key},
ssl_verify,
timeout=10,
)
return "error" not in result
def fetch_ha_states(cfg: dict) -> list:
ha_url = cfg["ha_url"].rstrip("/")
token = cfg["ha_token"]
states = ha_get(f"{ha_url}/api/states", token)
if not states or not isinstance(states, list):
return []
entities = []
for s in states:
entity_id = s.get("entity_id", "")
domain = entity_id.split(".")[0] if "." in entity_id else ""
if domain in SKIP_DOMAINS:
continue
attrs = s.get("attributes", {})
# Convert ISO 8601 (e.g. "2026-06-28T21:26:01.922366+00:00") to MySQL datetime
lc = s.get("last_changed", "")
try:
dt = datetime.fromisoformat(lc.replace("Z", "+00:00"))
lc = dt.astimezone(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
except Exception:
lc = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
entities.append({
"entity_id": entity_id,
"name": attrs.get("friendly_name") or entity_id,
"state": s.get("state", ""),
"attributes": attrs,
"last_changed": lc,
})
return entities
def main():
cfg = load_config()
state = load_state()
poll_interval = int(cfg.get("poll_interval", 30))
heartbeat_every = int(cfg.get("heartbeat_every", 10))
api_key = state.get("api_key", "")
while not api_key:
api_key = register(cfg, state)
if not api_key:
log("Could not register. Retrying in 60s...")
time.sleep(60)
headers = {"X-Agent-Key": api_key}
last_push = 0
log(f"HA Poller v{AGENT_VERSION} running. Polling HA every {poll_interval}s, heartbeat every {heartbeat_every}s.")
while True:
now = time.time()
# Heartbeat
if not heartbeat(cfg, api_key):
log("Heartbeat failed (401?) — re-registering...")
state.clear()
save_state(state)
api_key = register(cfg, state)
if not api_key:
time.sleep(60)
continue
# Push entity states every poll_interval
if now - last_push >= poll_interval:
entities = fetch_ha_states(cfg)
if entities:
ok = push_entities(cfg, api_key, entities)
if ok:
log(f"Pushed {len(entities)} HA entities to JARVIS.")
last_push = now
else:
log("No HA entities fetched (HA down or token invalid?)")
last_push = now
time.sleep(heartbeat_every)
if __name__ == "__main__":
main()
+67
View File
@@ -0,0 +1,67 @@
#!/bin/bash
# JARVIS Network Scanner — runs on PVE1, pushes nmap results to JARVIS
# Cron: */3 * * * * /usr/local/bin/jarvis-netscan.sh >/dev/null 2>&1
JARVIS_URL="http://10.48.200.211"
JARVIS_HOST="jarvis.orbishosting.com"
REG_KEY=$(cat /etc/jarvis-agent/reg-key 2>/dev/null)
if [ -z "$REG_KEY" ]; then
echo "$(date): ERROR: /etc/jarvis-agent/reg-key not found" >&2
exit 1
fi
SUBNET="10.48.200.0/24"
TMPFILE=$(mktemp)
nmap -sn --send-ip "$SUBNET" 2>/dev/null > "$TMPFILE"
if [ ! -s "$TMPFILE" ]; then
echo "$(date): nmap produced no output" >&2
rm -f "$TMPFILE"
exit 1
fi
JSON=$(python3 - "$TMPFILE" <<'PYEOF'
import sys, re, json
with open(sys.argv[1]) as f:
data = f.read()
devices = []
cur = None
for line in data.splitlines():
line = line.strip()
m = re.match(r'Nmap scan report for (?:(\S+) \()?(\d+\.\d+\.\d+\.\d+)\)?', line)
if m:
if cur:
devices.append(cur)
hn = m.group(1) if m.group(1) and m.group(1) != m.group(2) else ''
cur = {'ip': m.group(2), 'hostname': hn, 'mac': '', 'vendor': ''}
elif cur:
m2 = re.match(r'MAC Address: ([0-9A-Fa-f:]{17}) \(([^)]+)\)', line)
if m2:
cur['mac'] = m2.group(1).lower()
cur['vendor'] = '' if m2.group(2) == 'Unknown' else m2.group(2)
if cur:
devices.append(cur)
print(json.dumps({'devices': devices}))
PYEOF
)
rm -f "$TMPFILE"
if [ -z "$JSON" ]; then
echo "$(date): JSON parse failed" >&2
exit 1
fi
RESPONSE=$(curl -sk --max-time 15 \
-X POST "$JARVIS_URL/api/netscan" \
-H "Host: $JARVIS_HOST" \
-H "Content-Type: application/json" \
-H "X-Registration-Key: $REG_KEY" \
-d "$JSON" 2>/dev/null)
echo "$(date): $RESPONSE"
+74
View File
@@ -0,0 +1,74 @@
#!/bin/bash
# JARVIS VoIP Phone Probe — runs every minute on PVE1
# Pings all Yealink phones + checks FusionPBX SIP registration (read-only)
# 200.3 is on an external FusionPBX — ping only, no SIP check
JARVIS_URL="http://10.48.200.211"
JARVIS_HOST="jarvis.orbishosting.com"
REG_KEY=$(cat /etc/jarvis-agent/reg-key 2>/dev/null)
if [ -z "$REG_KEY" ]; then
echo "$(date): ERROR: /etc/jarvis-agent/reg-key not found" >&2
exit 1
fi
FUSION_HOST="134.209.72.226"
# IP|alias|extension(none=skip SIP check)|mac
PHONES=(
"10.48.200.2|Yealink — Myron Main (Ext 1000)|1000|80:5e:c0:35:04:77"
"10.48.200.3|Yealink — United Mirror & Glass (External SIP)|none|c4:fc:22:28:63:71"
"10.48.200.43|Yealink T48S — Tommy Main (Ext 1001)|1001|80:5e:0c:15:0c:4f"
"10.48.200.86|Yealink — Myron Vanguard WiFi (Offline During Work Hrs)|none|"
"10.48.200.65|Yealink — Myron Vanguard Work (Ext 1003)|1003|c4:fc:22:13:e1:89"
)
# Get SIP registrations from FusionPBX (read-only)
REG_OUTPUT=$(ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 -o BatchMode=yes \
root@$FUSION_HOST "fs_cli -x 'show registrations'" 2>/dev/null || echo "")
# Collect results as TSV, delegate JSON building to python3 to avoid injection
RESULTS=""
for PHONE in "${PHONES[@]}"; do
IFS='|' read -r IP ALIAS EXT MAC <<< "$PHONE"
if ping -c 1 -W 2 "$IP" > /dev/null 2>&1; then
STATUS="online"
else
STATUS="offline"
fi
if [ "$EXT" = "none" ]; then
SIP="external"
elif [ -n "$REG_OUTPUT" ] && echo "$REG_OUTPUT" | grep -q "^${EXT},"; then
SIP="registered"
else
SIP="unregistered"
fi
RESULTS="${RESULTS}${IP}\t${ALIAS}\t${MAC}\t${STATUS}\t${SIP}\t${EXT}\n"
done
JSON=$(printf "%b" "$RESULTS" | python3 -c "
import sys, json
devices = []
for line in sys.stdin:
line = line.rstrip('\n')
if not line:
continue
parts = line.split('\t')
if len(parts) < 6:
continue
ip, alias, mac, status, sip, ext = parts[:6]
devices.append({
'ip': ip, 'alias': alias, 'mac': mac,
'vendor': 'Yealink', 'status': status,
'sip_status': sip, 'extension': ext,
})
print(json.dumps({'devices': devices}))
")
curl -sk --max-time 10 \
-X POST "$JARVIS_URL/api/netscan" \
-H "Host: $JARVIS_HOST" \
-H "Content-Type: application/json" \
-H "X-Registration-Key: $REG_KEY" \
-d "$JSON" > /dev/null 2>&1
+99
View File
@@ -0,0 +1,99 @@
#!/usr/bin/env python3
"""
JARVIS Ping Probe — runs on PVE1 (10.48.200.90), which is on the LAN.
Pings devices that can't run the full agent, then calls JARVIS heartbeat
on their behalf so the dashboard shows live status.
"""
import json
import subprocess
import urllib.request
import urllib.error
import ssl
JARVIS_URL = "http://10.48.200.211"
HOST_HEADER = "jarvis.orbishosting.com"
# Devices to probe: agent_id → api_key
DEVICES = {
"fortigate_gw": "00103aea6fcbf837bc55e11b445a3620",
"yealink_t48s": "2bf8bd7ca8dd31c28fd16aa956e15f88",
"homeassistant_ha": "6f8077dee7a7b4af202bc80886f1223d",
}
# Map agent_id → IP (for ping)
IPS = {
"fortigate_gw": "10.48.200.1",
"yealink_t48s": "10.48.200.43",
"homeassistant_ha": "10.48.200.97",
}
def ping(ip: str) -> bool:
result = subprocess.run(
["ping", "-c", "1", "-W", "2", ip],
capture_output=True, timeout=5
)
return result.returncode == 0
def heartbeat(agent_id: str, api_key: str, alive: bool):
# If device is down we still send heartbeat so JARVIS updates last_seen
# and sets status based on the alive flag via the metric payload
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
payload = json.dumps({}).encode()
req = urllib.request.Request(
f"{JARVIS_URL}/api/agent/heartbeat",
data=payload, method="POST"
)
req.add_header("Content-Type", "application/json")
req.add_header("X-Agent-Key", api_key)
req.add_header("Host", HOST_HEADER)
try:
with urllib.request.urlopen(req, timeout=10, context=ctx):
pass
except Exception:
pass
def update_status(agent_id: str, api_key: str, status: str):
"""Push a minimal metric so JARVIS knows if device is up or down."""
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
payload = json.dumps({
"type": "system",
"data": {
"hostname": agent_id,
"cpu_percent": 0,
"ping_only": True,
"ping_status": status,
}
}).encode()
req = urllib.request.Request(
f"{JARVIS_URL}/api/agent/metrics",
data=payload, method="POST"
)
req.add_header("Content-Type", "application/json")
req.add_header("X-Agent-Key", api_key)
req.add_header("Host", HOST_HEADER)
try:
with urllib.request.urlopen(req, timeout=10, context=ctx):
pass
except Exception:
pass
def main():
for agent_id, api_key in DEVICES.items():
ip = IPS.get(agent_id, "")
alive = ping(ip) if ip else False
status = "online" if alive else "offline"
print(f"{agent_id} ({ip}): {status}", flush=True)
heartbeat(agent_id, api_key, alive)
update_status(agent_id, api_key, status)
if __name__ == "__main__":
main()
+4 -4
View File
@@ -18,13 +18,13 @@ define(chr(39)+'CLAUDE_MODEL'.chr(39), chr(39)+'claude-sonnet-4-6'.chr(39))
define(chr(39)+'CLAUDE_MAX_TOKENS'.chr(39), 1024); define(chr(39)+'CLAUDE_MAX_TOKENS'.chr(39), 1024);
define(chr(39)+'GROQ_API_KEY'.chr(39), chr(39)+'gsk_...'.chr(39)); define(chr(39)+'GROQ_API_KEY'.chr(39), chr(39)+'gsk_...'.chr(39));
define(chr(39)+'GROQ_MODEL_SEARCH'.chr(39), chr(39)+'groq/compound-mini'.chr(39)); define(chr(39)+'GROQ_MODEL_SEARCH'.chr(39), chr(39)+'compound-beta-mini'.chr(39));
define(chr(39)+'GROQ_MODEL_GENERAL'.chr(39), chr(39)+'llama-3.3-70b-versatile'.chr(39)); define(chr(39)+'GROQ_MODEL_GENERAL'.chr(39), chr(39)+'llama-3.3-70b-versatile'.chr(39));
define(chr(39)+'GROQ_TIMEOUT'.chr(39), 30); define(chr(39)+'GROQ_TIMEOUT'.chr(39), 30);
define(chr(39)+'OLLAMA_HOST'.chr(39), chr(39)+'http://10.48.200.95:11434'.chr(39)); define(chr(39)+'OLLAMA_HOST'.chr(39), chr(39)+'http://10.48.200.210:11434'.chr(39));
define(chr(39)+'OLLAMA_MODEL_PRIMARY'.chr(39), chr(39)+'llama3.2:1b'.chr(39)); define(chr(39)+'OLLAMA_MODEL_PRIMARY'.chr(39), chr(39)+'llama3.1:8b'.chr(39));
define(chr(39)+'OLLAMA_MODEL_HEAVY'.chr(39), chr(39)+'llama3.1:70b'.chr(39)); define(chr(39)+'OLLAMA_MODEL_HEAVY'.chr(39), chr(39)+'llama3.1:8b'.chr(39));
define(chr(39)+'OLLAMA_TIMEOUT'.chr(39), 90); define(chr(39)+'OLLAMA_TIMEOUT'.chr(39), 90);
define(chr(39)+'LOCAL_SUBNET'.chr(39), chr(39)+'10.48.200'.chr(39)); define(chr(39)+'LOCAL_SUBNET'.chr(39), chr(39)+'10.48.200'.chr(39));
+14 -6
View File
@@ -37,11 +37,18 @@ function get_agent_by_key(string $key): ?array {
return $rows[0] ?? null; return $rows[0] ?? null;
} }
function update_agent_seen(string $agentId, string $status = 'online'): void { function update_agent_seen(string $agentId, string $status = 'online', ?string $version = null): void {
JarvisDB::query( if ($version !== null) {
'UPDATE registered_agents SET last_seen = NOW(), status = ? WHERE agent_id = ?', JarvisDB::query(
[$status, $agentId] 'UPDATE registered_agents SET last_seen = NOW(), status = ?, version = ? WHERE agent_id = ?',
); [$status, $version, $agentId]
);
} else {
JarvisDB::query(
'UPDATE registered_agents SET last_seen = NOW(), status = ? WHERE agent_id = ?',
[$status, $agentId]
);
}
} }
// ── Auth (all actions except register) ─────────────────────────────────────── // ── Auth (all actions except register) ───────────────────────────────────────
@@ -104,7 +111,8 @@ switch ($agentAction) {
// ── HEARTBEAT ──────────────────────────────────────────────────────────── // ── HEARTBEAT ────────────────────────────────────────────────────────────
case 'heartbeat': case 'heartbeat':
update_agent_seen($agent['agent_id']); $hbStatus = in_array($data['status'] ?? '', ['online','offline']) ? $data['status'] : 'online';
update_agent_seen($agent['agent_id'], $hbStatus, trim($data['version'] ?? '') ?: null);
// Return any pending commands for this agent // Return any pending commands for this agent
$commands = JarvisDB::query( $commands = JarvisDB::query(
+3 -3
View File
@@ -2302,7 +2302,7 @@ if (!$reply) {
$sec = (int) file_get_contents('/proc/uptime'); $sec = (int) file_get_contents('/proc/uptime');
$uptime = intdiv($sec, 86400) . 'd ' . intdiv($sec % 86400, 3600) . 'h'; $uptime = intdiv($sec, 86400) . 'd ' . intdiv($sec % 86400, 3600) . 'h';
$load = explode(' ', file_get_contents('/proc/loadavg')); $load = explode(' ', file_get_contents('/proc/loadavg'));
$systemContext .= "Jarvis server (165.22.1.228 DO): Memory {$memPct}%, Uptime {$uptime}, Load {$load[0]}.\n"; $systemContext .= "Jarvis server (10.48.200.211 PVE1): Memory {$memPct}%, Uptime {$uptime}, Load {$load[0]}.\n";
} catch (Exception $e) {} } catch (Exception $e) {}
$alerts = JarvisDB::query( $alerts = JarvisDB::query(
@@ -2317,12 +2317,12 @@ if (!$reply) {
$systemPrompt = "You are JARVIS — Just A Rather Very Intelligent System — the AI of {$userName} (address him as \"{$userAddr}\"). You manage his home network, servers, Proxmox VMs, websites, and Home Assistant smart home. Your personality: formal, efficient, British butler — like the AI in Iron Man. Be concise. Use technical precision. $systemPrompt = "You are JARVIS — Just A Rather Very Intelligent System — the AI of {$userName} (address him as \"{$userAddr}\"). You manage his home network, servers, Proxmox VMs, websites, and Home Assistant smart home. Your personality: formal, efficient, British butler — like the AI in Iron Man. Be concise. Use technical precision.
Infrastructure: Infrastructure:
- Jarvis Server: 165.22.1.228 (DigitalOcean, CyberPanel/OLS, Ubuntu 24.04) - Jarvis Server: 10.48.200.211 (PVE1, nginx/PHP-FPM, Ubuntu 24.04)
- Ollama AI VM: 10.48.200.95 (local LLM server, llama3.1:8b + 70b) - Ollama AI VM: 10.48.200.95 (local LLM server, llama3.1:8b + 70b)
- Proxmox Host: 10.48.200.90 (manages all VMs) - Proxmox Host: 10.48.200.90 (manages all VMs)
- Home Assistant: 10.48.200.97:8123 - Home Assistant: 10.48.200.97:8123
- FusionPBX: 134.209.72.226 / fusion.orbishosting.com (production DO server), Yealink T48S: 10.48.200.43 - FusionPBX: 134.209.72.226 / fusion.orbishosting.com (production DO server), Yealink T48S: 10.48.200.43
- Digital Ocean: 165.22.1.228 (tomsjavajive.com, epictravelexpeditions.com, tomtomgames.com, parkerslingshotrentals.com, orbishosting.com) - Digital Ocean: 165.22.1.228 (website hosting — tomsjavajive.com, epictravelexpeditions.com, tomtomgames.com, parkerslingshotrentals.com, orbishosting.com)
- Network: 10.48.200.0/24, FortiGate firewall - Network: 10.48.200.0/24, FortiGate firewall
Live data: Live data:
+20 -3
View File
@@ -30,7 +30,7 @@ $dfOut = shell_exec("df / | tail -1 | awk {print }") ?? "";
$diskPct = trim($dfOut); $diskPct = trim($dfOut);
// Services // Services
$svcNames = ["lshttpd", "mysql", "redis"]; $svcNames = ["nginx", "php8.3-fpm", "mariadb", "redis-server", "jarvis-arc", "jarvis-agent"];
$svcMap = []; $svcMap = [];
foreach ($svcNames as $s) { foreach ($svcNames as $s) {
$status = trim(shell_exec("systemctl is-active " . escapeshellarg($s) . " 2>/dev/null") ?? ""); $status = trim(shell_exec("systemctl is-active " . escapeshellarg($s) . " 2>/dev/null") ?? "");
@@ -39,7 +39,7 @@ foreach ($svcNames as $s) {
// Site health from kb_facts // Site health from kb_facts
$siteLabels = [ $siteLabels = [
"jarvis" => "jarvis.orbishosting.com", "jarvis" => "jarvis.orbishosting.com:1972",
"tomsjavajive" => "tomsjavajive.com", "tomsjavajive" => "tomsjavajive.com",
"epictravelexp"=> "epictravelexpeditions.com", "epictravelexp"=> "epictravelexpeditions.com",
"parkersling" => "parkerslingshotrentals.com", "parkersling" => "parkerslingshotrentals.com",
@@ -59,8 +59,24 @@ foreach ($rows as $r) {
$uptimeDays = intdiv($uptime, 86400); $uptimeDays = intdiv($uptime, 86400);
$uptimeHrs = intdiv($uptime % 86400, 3600); $uptimeHrs = intdiv($uptime % 86400, 3600);
// DO server agent metrics (jarvis-do agent reporting via Tailscale)
$doAgent = JarvisDB::query(
"SELECT metric_data FROM agent_metrics WHERE agent_id='jarvis-do_orbis' AND metric_type='system' ORDER BY recorded_at DESC LIMIT 1"
);
$doMet = [];
if (!empty($doAgent[0]['metric_data'])) {
$dm = json_decode($doAgent[0]['metric_data'], true) ?? [];
$doMet = [
"cpu" => $dm['cpu_percent'] ?? 0,
"mem" => $dm['memory']['percent'] ?? 0,
"disk" => (int)($dm['disk'][0]['percent'] ?? 0),
"uptime" => $dm['uptime']['human'] ?? "--",
"online" => true,
];
}
echo json_encode([ echo json_encode([
"ip" => DO_SERVER_IP, "ip" => "10.48.200.211", // JARVIS VM (PVE1)
"reachable" => true, "reachable" => true,
"cpu_pct" => getCpuPct(), "cpu_pct" => getCpuPct(),
"memory" => [ "memory" => [
@@ -73,5 +89,6 @@ echo json_encode([
"uptime" => "{$uptimeDays}d {$uptimeHrs}h", "uptime" => "{$uptimeDays}d {$uptimeHrs}h",
"services" => $svcMap, "services" => $svcMap,
"sites" => $sites, "sites" => $sites,
"do_server" => $doMet,
"timestamp" => date("c"), "timestamp" => date("c"),
]); ]);
+22 -26
View File
@@ -80,26 +80,15 @@ function collect_all(): array {
$results['system'] = 'error: ' . $e->getMessage(); $results['system'] = 'error: ' . $e->getMessage();
} }
// ── Network ─────────────────────────────────────────────────────────── // ── Network — read from agent DB (agents push status, DO can't ping LAN IPs) ──
try { try {
$watchlist = [ $rows = JarvisDB::query(
'gateway' => '10.48.200.1', "SELECT status FROM registered_agents WHERE last_seen > DATE_SUB(NOW(), INTERVAL 5 MINUTE)"
'proxmox' => '10.48.200.90', );
'ollama' => '10.48.200.95', $online = count(array_filter($rows, fn($r) => $r['status'] === 'online'));
'fusionpbx' => '10.48.200.96', $total = count($rows);
'ha' => '10.48.200.97', KBEngine::storeFact('network', 'online_count', $online, 'local', $ttl);
'do_server' => '165.22.1.228', KBEngine::storeFact('network', 'total_count', $total, 'local', $ttl);
];
$online = 0;
$total = count($watchlist);
foreach ($watchlist as $name => $ip) {
exec('ping -c1 -W1 ' . escapeshellarg($ip) . ' > /dev/null 2>&1', $o, $code);
$up = ($code === 0);
if ($up) $online++;
KBEngine::storeFact('network', "host_{$name}", $up ? 'online' : 'offline', $ip, $ttl);
}
KBEngine::storeFact('network', 'online_count', $online, 'local', $ttl);
KBEngine::storeFact('network', 'total_count', $total, 'local', $ttl);
KBEngine::storeFact('network', 'gateway_status', $online > 0 ? 'online' : 'offline', 'local', $ttl); KBEngine::storeFact('network', 'gateway_status', $online > 0 ? 'online' : 'offline', 'local', $ttl);
$results['network'] = "ok ({$online}/{$total} online)"; $results['network'] = "ok ({$online}/{$total} online)";
} catch (Exception $e) { } catch (Exception $e) {
@@ -111,7 +100,7 @@ function collect_all(): array {
$results['proxmox'] = 'skipped (fresh)'; $results['proxmox'] = 'skipped (fresh)';
} else try { } else try {
if (defined('PROXMOX_TOKEN_ID') && PROXMOX_TOKEN_ID) { if (defined('PROXMOX_TOKEN_ID') && PROXMOX_TOKEN_ID) {
$base = 'https://orbisne.fortiddns.com:' . PROXMOX_PORT . '/api2/json'; $base = 'https://10.48.200.90:' . PROXMOX_PORT . '/api2/json';
$auth = 'Authorization: PVEAPIToken=' . PROXMOX_USER . '!' . PROXMOX_TOKEN_ID . '=' . PROXMOX_TOKEN_VAL; $auth = 'Authorization: PVEAPIToken=' . PROXMOX_USER . '!' . PROXMOX_TOKEN_ID . '=' . PROXMOX_TOKEN_VAL;
$nd = pve_api_get("{$base}/nodes/" . PROXMOX_NODE . "/status", $auth); $nd = pve_api_get("{$base}/nodes/" . PROXMOX_NODE . "/status", $auth);
@@ -146,7 +135,7 @@ function collect_all(): array {
// ── Digital Ocean ───────────────────────────────────────────────────── // ── Digital Ocean ─────────────────────────────────────────────────────
try { try {
exec('ping -c1 -W2 165.22.1.228 > /dev/null 2>&1', $o2, $doCode); exec("ping -c1 -W1 165.22.1.228 > /dev/null 2>&1", $o2, $doCode);;
$doStatus = ($doCode === 0) ? 'online' : 'unreachable'; $doStatus = ($doCode === 0) ? 'online' : 'unreachable';
KBEngine::storeFact('do_server', 'do_status', $doStatus, '165.22.1.228', $ttl); KBEngine::storeFact('do_server', 'do_status', $doStatus, '165.22.1.228', $ttl);
$results['do_server'] = "ok ({$doStatus})"; $results['do_server'] = "ok ({$doStatus})";
@@ -160,7 +149,7 @@ function collect_all(): array {
} else try { } else try {
$ollamaHost = defined('OLLAMA_HOST') ? OLLAMA_HOST : 'http://10.48.200.95:11434'; $ollamaHost = defined('OLLAMA_HOST') ? OLLAMA_HOST : 'http://10.48.200.95:11434';
$ch = curl_init($ollamaHost . '/api/tags'); $ch = curl_init($ollamaHost . '/api/tags');
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 5]); curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_CONNECTTIMEOUT => 2, CURLOPT_TIMEOUT => 3]);
$resp = curl_exec($ch); $resp = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch); curl_close($ch);
@@ -192,19 +181,25 @@ function collect_all(): array {
$results['sites'] = 'skipped (fresh)'; $results['sites'] = 'skipped (fresh)';
} else try { } else try {
$sites = [ $sites = [
'jarvis' => 'https://jarvis.orbishosting.com', "jarvis" => "http://127.0.0.1",
'tomsjavajive' => 'https://tomsjavajive.com', 'tomsjavajive' => 'https://tomsjavajive.com',
'epictravelexp'=> 'https://epictravelexpeditions.com', 'epictravelexp'=> 'https://epictravelexpeditions.com',
'parkersling' => 'https://parkerslingshotrentals.com', 'parkerslingshotrentals' => 'https://parkerslingshotrentals.com',
'orbishosting' => 'https://orbishosting.com', 'orbishosting' => 'https://orbishosting.com',
'orbisportal' => 'https://orbis.orbishosting.com', 'orbisportal' => 'https://orbis.orbishosting.com',
'tomtomgames' => 'https://tomtomgames.com', 'tomtomgames' => 'https://tomtomgames.com',
]; ];
$down = []; $down = [];
foreach ($sites as $key => $url) { foreach ($sites as $key => $url) {
$ch = curl_init($url); $parsed = parse_url($url);
$host = $parsed['host'] ?? $url;
// Check sites on the local server directly to avoid Cloudflare CDN timeouts.
// All JARVIS-hosted sites are served from this same OLS instance.
$localUrl = $url; // external check
$ch = curl_init($localUrl);
curl_setopt_array($ch, [ curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true, CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 3,
CURLOPT_FOLLOWLOCATION => true, CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => 10, CURLOPT_TIMEOUT => 10,
CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_CONNECTTIMEOUT => 5,
@@ -234,9 +229,10 @@ function pve_api_get(string $url, string $authHeader): array {
$ch = curl_init($url); $ch = curl_init($url);
curl_setopt_array($ch, [ curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true, CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 3,
CURLOPT_HTTPHEADER => [$authHeader], CURLOPT_HTTPHEADER => [$authHeader],
CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_TIMEOUT => 8, CURLOPT_TIMEOUT => 5,
]); ]);
$resp = curl_exec($ch); $resp = curl_exec($ch);
curl_close($ch); curl_close($ch);
+22 -9
View File
@@ -83,15 +83,28 @@ if ($method === 'POST' && $action === 'service') {
// Serve entities from ha_entities table (real-time agent push data) // Serve entities from ha_entities table (real-time agent push data)
$skipDomains = ['sensor','binary_sensor','button','update','select','number', $skipDomains = ['sensor','binary_sensor','button','update','select','number',
'device_tracker','event','image','person','zone','tts','conversation', 'device_tracker','event','image','person','zone','tts','conversation',
'assist_satellite','input_button']; 'assist_satellite','input_button','media_player','scene','water_heater',
$skipKeywords = ['pre_release','_record','_ftp_','_push_','_hub_ringtone', 'alarm_control_panel','automation','script','calendar','notify','weather','camera','siren','remote','todo','lawn_mower'];
'_siren_on','_email_on','_manual_record','_infrared_', $skipKeywords = [
'do_not_disturb','matter_server','zerotier','mariadb', // HACS / system toggles
'spotify_connect','file_editor','ssh_web','uptime_kuma', 'pre_release','get_hacs','matter_server','zerotier','mariadb',
'adguard_','folding_home','music_assistant','get_hacs','mealie', 'spotify_connect','file_editor','ssh_web','uptime_kuma','adguard_',
'mosquitto','social_to','motion_detection', 'folding_home','music_assistant','mealie','mosquitto','social_to',
'front_yard_record','down_hill_record','camera1_record', 'assist_microphone','cec_scanner','esphome_device_builder',
'back_yard_record','nvr_','assist_microphone']; // Camera controls
'_record','_ftp_','_push_','_hub_ringtone','_siren_on',
'_email_on','_manual_record','_infrared_','motion_detection',
'front_yard_record','down_hill_record','camera1_record',
'back_yard_record','nvr_',
// Echo / smart display noise
'do_not_disturb',
// Konnected security panel switches
'floodlight',
'konnected',
// Energy / power monitoring (sensors, not controls)
'_energy','_power','_voltage','_current','_consumption',
'electricity_maps',
];
$rows = JarvisDB::query( $rows = JarvisDB::query(
"SELECT entity_id, entity_name, domain, state, UNIX_TIMESTAMP(updated_at) as updated_ts "SELECT entity_id, entity_name, domain, state, UNIX_TIMESTAMP(updated_at) as updated_ts
+540
View File
@@ -0,0 +1,540 @@
<?php
/**
* JARVIS KB Intent Generator
* Generates 1,000+ educational KB intents via Groq LLM and imports to kb_intents table.
* Also runs a cleanup pass: deduplication, short-response pruning, pattern normalisation.
*
* CLI / cron: /usr/bin/php8.3 /var/www/jarvis/api/endpoints/kb_intent_generator.php
* Schedule: Daily 3 am
*/
require_once __DIR__ . '/../config.php';
require_once __DIR__ . '/../lib/db.php';
/* ── helpers ── */
function ts(): string { return '[' . date('Y-m-d H:i:s') . ']'; }
function log_line(string $msg): void { echo ts() . ' KB Intent Generator: ' . $msg . "\n"; flush(); }
function groq(string $system, string $user, int $max = 3000, int $retries = 2): ?string {
for ($attempt = 0; $attempt <= $retries; $attempt++) {
if ($attempt > 0) {
log_line(" Retry {$attempt}/{$retries} after rate-limit pause...");
sleep(25);
}
$ch = curl_init('https://api.groq.com/openai/v1/chat/completions');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_TIMEOUT => 60,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . GROQ_API_KEY,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'model' => 'llama-3.3-70b-versatile',
'max_tokens' => $max,
'temperature' => 0.7,
'messages' => [
['role' => 'system', 'content' => $system],
['role' => 'user', 'content' => $user],
],
]),
]);
$raw = curl_exec($ch);
$err = curl_error($ch);
$info = curl_getinfo($ch);
curl_close($ch);
if ($err || !$raw) continue;
// Check for rate limit response (429)
if (($info['http_code'] ?? 0) === 429) continue;
$d = json_decode($raw, true);
$content = $d['choices'][0]['message']['content'] ?? null;
if ($content !== null) return $content;
}
return null;
}
/* ── normalize a pattern to valid PHP PCRE ── */
function normalize_pattern(string $pat): string {
$pat = trim($pat);
// If pattern already has PCRE delimiters, leave it alone
if (preg_match('/^[\/|~#!@%]/', $pat)) return $pat;
// Strip any (?i) inline flag — we'll add /i at the delimiter level
$pat = preg_replace('/^\(\?i\)/', '', $pat);
// Escape forward slashes inside the pattern
$pat = str_replace('/', '\\/', $pat);
return '/' . $pat . '/i';
}
/* ── daily guard: skip if already ran within last 20 hours ── */
/* Set JARVIS_FORCE_RUN=1 (env) or pass --force (argv) to bypass */
$lastRun = JarvisDB::single(
"SELECT updated_at FROM kb_facts WHERE category='kb_generator' AND fact_key='last_run'"
);
$forceRun = !empty(getenv('JARVIS_FORCE_RUN')) || (isset($argv[1]) && $argv[1] === '--force');
if (!$forceRun && $lastRun && (time() - strtotime($lastRun['updated_at'])) < 14400) {
log_line('Skipping ran within last 20 hours (next run tomorrow 3am). Use --force to override.');
exit(0);
}
if ($forceRun) log_line('Force-run flag set — bypassing 20-hour guard.');
log_line('Starting daily KB intent generation run.');
/* ── topic batches (25 topics × ~40 intents = 1,000+) ── */
$BATCHES = [
['id' => 'math_arith', 'category' => 'mathematics', 'topic' => 'Arithmetic and number sense',
'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'],
['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'],
['id' => 'math_algebra1', 'category' => 'mathematics', 'topic' => 'Algebra I — equations and inequalities',
'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'],
['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'],
['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 ── */
define('BATCH_SIZE', 25);
$totalTopics = count($BATCHES);
$offsetRow = JarvisDB::single(
"SELECT CAST(fact_value AS SIGNED) AS v FROM kb_facts WHERE category='kb_generator' AND fact_key='batch_offset'"
);
$batchOffset = max(0, (int)($offsetRow['v'] ?? 0));
if ($batchOffset >= $totalTopics) $batchOffset = 0;
$nextOffset = ($batchOffset + BATCH_SIZE) % $totalTopics;
$cycleComplete = ($batchOffset + BATCH_SIZE) >= $totalTopics;
$cycleLen = (int)ceil($totalTopics / BATCH_SIZE);
$runBatches = [];
for ($i = 0; $i < BATCH_SIZE; $i++) {
$runBatches[] = $BATCHES[($batchOffset + $i) % $totalTopics];
}
$endIdx = ($batchOffset + BATCH_SIZE - 1) % $totalTopics;
log_line("Rotation: topics " . ($batchOffset + 1) . "" . ($endIdx + 1) . " of {$totalTopics} | cycle = {$cycleLen} runs × 6h = " . ($cycleLen * 6) . "h full cycle.");
if ($cycleComplete) log_line(" ↻ Full cycle complete — restarting from topic 1 next run.");
/* ── main generation loop ── */
$totalBatches = count($runBatches);
foreach ($runBatches as $idx => $batch) {
$num = $idx + 1;
log_line("Batch {$num}/{$totalBatches}: {$batch['topic']}");
$user = "Generate 20 KB intents for the topic: {$batch['topic']}.\n"
. "Subtopics to cover: {$batch['desc']}.\n"
. "Prefix every intent_name with \"{$batch['id']}_\".\n"
. "Category string to use: \"{$batch['category']}\".";
$raw = groq($SYSTEM, $user, 5000);
if ($raw === null) {
log_line(" ✗ API call failed after retries skipping batch.");
$errors += 20;
sleep(8);
continue;
}
// Strip markdown code fences if model added them
$raw = preg_replace('/^```(?:json)?\s*/m', '', $raw);
$raw = preg_replace('/^```\s*/m', '', $raw);
$raw = trim($raw);
// Extract JSON array; fall back to partial recovery for truncated responses
$items = null;
if (preg_match('/\[\s*\{.*\}\s*\]/s', $raw, $m)) {
$items = json_decode($m[0], true);
if (!is_array($items)) {
log_line(" ✗ JSON parse failed skipping batch.");
$errors += 20; sleep(8); continue;
}
} else {
$start = strpos($raw, '[');
if ($start !== false) {
$partial = substr($raw, $start);
if (preg_match_all('/\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/s', $partial, $objs) && !empty($objs[0])) {
$recovered = '[' . implode(',', $objs[0]) . ']';
$items = json_decode($recovered, true);
if (is_array($items) && count($items) > 0)
log_line(" ⚠ Truncated — recovered " . count($items) . " items.");
}
}
if (!is_array($items) || count($items) === 0) {
log_line(" ✗ No JSON array found — raw[0:120]: " . substr(str_replace("\n", ' ', $raw), 0, 120));
$errors += 20; sleep(8); continue;
}
}
$batchInserted = 0;
foreach ($items as $item) {
if (!is_array($item)) continue;
safe_insert($item, $batch['category']);
$batchInserted++;
}
log_line(" ✓ Parsed {$batchInserted} intents (running total inserted: {$inserted}).");
// Polite delay between API calls — Groq TPM limit needs ~8s between batches
if ($num < $totalBatches) sleep(8);
}
log_line("Generation complete. Inserted/updated: {$inserted} | Short/invalid skipped: {$skipped} | Errors: {$errors}");
/* ── cleanup phase ── */
log_line('Starting cleanup phase...');
// 1. Remove exact duplicate intent_names (keep the one with the longer response)
$dups = JarvisDB::query(
'SELECT intent_name, COUNT(*) AS cnt FROM kb_intents GROUP BY intent_name HAVING cnt > 1'
);
$dupsPruned = 0;
foreach ($dups as $dup) {
$rows = JarvisDB::query(
'SELECT id, LENGTH(response_template) AS rlen FROM kb_intents WHERE intent_name=? ORDER BY rlen DESC',
[$dup['intent_name']]
);
array_shift($rows);
foreach ($rows as $row) {
JarvisDB::execute('DELETE FROM kb_intents WHERE id=?', [$row['id']]);
$dupsPruned++;
}
}
log_line(" Duplicate intent_names pruned: {$dupsPruned}");
// 2. Remove intents with very short responses (< 40 chars)
$shortPruned = JarvisDB::execute(
"DELETE FROM kb_intents WHERE LENGTH(response_template) < 40 AND priority <= 5"
);
log_line(" Short-response rows pruned: {$shortPruned}");
// 3. Trim whitespace on all generated intents
JarvisDB::execute(
"UPDATE kb_intents SET
intent_name = TRIM(intent_name),
pattern = TRIM(pattern),
response_template = TRIM(response_template),
fact_category = TRIM(fact_category)
WHERE priority = 5"
);
log_line(' Whitespace trimmed on all generated intents.');
// 4. Fix and validate PCRE patterns — normalize then deactivate only truly broken ones
$all = JarvisDB::query('SELECT id, pattern FROM kb_intents WHERE priority=5');
$badPattern = 0;
$fixedPattern = 0;
foreach ($all as $row) {
$pat = normalize_pattern($row['pattern']);
if ($pat !== $row['pattern']) {
// Update to normalized form
JarvisDB::execute('UPDATE kb_intents SET pattern=?, active=1 WHERE id=?', [$pat, $row['id']]);
$fixedPattern++;
} elseif (@preg_match($pat, '') === false) {
JarvisDB::execute('UPDATE kb_intents SET active=0 WHERE id=?', [$row['id']]);
$badPattern++;
} else {
// Valid pattern — make sure it's active
JarvisDB::execute('UPDATE kb_intents SET active=1 WHERE id=?', [$row['id']]);
}
}
log_line(" Patterns normalized: {$fixedPattern} | Bad PCRE deactivated: {$badPattern}");
// 5. Enable ALL remaining inactive intents (including pre-existing ones)
$reactivated = JarvisDB::execute('UPDATE kb_intents SET active=1 WHERE active=0 AND priority <= 5');
log_line(" Re-activated previously inactive intents: {$reactivated}");
// 6. Final stats
$stats = JarvisDB::single('SELECT COUNT(*) AS total, SUM(active) AS active FROM kb_intents');
log_line("Final KB Intents table: {$stats['total']} total, {$stats['active']} active.");
/* ── record last-run timestamp ── */
JarvisDB::execute(
"INSERT INTO kb_facts (category, fact_key, fact_value, host)
VALUES ('kb_generator', 'last_run', NOW(), 'local')
ON DUPLICATE KEY UPDATE fact_value=NOW(), updated_at=NOW()",
[]
);
JarvisDB::execute(
"INSERT INTO kb_facts (category, fact_key, fact_value, host)
VALUES ('kb_generator', 'batch_offset', ?, 'local')
ON DUPLICATE KEY UPDATE fact_value=VALUES(fact_value), updated_at=NOW()",
[$nextOffset]
);
log_line("Next run will start at topic offset {$nextOffset}/{$totalTopics}.");
JarvisDB::execute(
"INSERT INTO kb_facts (category, fact_key, fact_value, host)
VALUES ('kb_generator', 'last_inserted', ?, 'local')
ON DUPLICATE KEY UPDATE fact_value=VALUES(fact_value), updated_at=NOW()",
[$inserted]
);
log_line('Done.');
+3 -3
View File
@@ -36,7 +36,7 @@ function cacheStore(string $key, $data): void {
// ── Proxmox ────────────────────────────────────────────────────────────── // ── Proxmox ──────────────────────────────────────────────────────────────
if (PROXMOX_HOST !== '10.48.200.X' && PROXMOX_TOKEN_VAL !== 'YOUR_TOKEN_VALUE_HERE') { if (PROXMOX_HOST !== '10.48.200.X' && PROXMOX_TOKEN_VAL !== 'YOUR_TOKEN_VALUE_HERE') {
$pveBase = 'https://orbisne.fortiddns.com:' . PROXMOX_PORT . '/api2/json'; $pveBase = 'https://10.48.200.90:' . PROXMOX_PORT . '/api2/json';
$pveAuth = ['Authorization: PVEAPIToken=' . PROXMOX_USER . '!' . PROXMOX_TOKEN_ID . '=' . PROXMOX_TOKEN_VAL]; $pveAuth = ['Authorization: PVEAPIToken=' . PROXMOX_USER . '!' . PROXMOX_TOKEN_ID . '=' . PROXMOX_TOKEN_VAL];
// Cluster resources API — returns all VMs/CTs from ALL nodes (pve + pve2) // Cluster resources API — returns all VMs/CTs from ALL nodes (pve + pve2)
@@ -134,8 +134,8 @@ if (HA_TOKEN !== 'YOUR_HA_TOKEN_HERE' && strpos(HA_URL, '10.48.200.X') === false
$config = $configRaw ? json_decode($configRaw, true) : []; $config = $configRaw ? json_decode($configRaw, true) : [];
// Controllable domains only — skip read-only sensors to keep list manageable // Controllable domains only — skip read-only sensors to keep list manageable
$interesting = ['light','switch','scene','media_player','alarm_control_panel', $interesting = ['light','switch','alarm_control_panel',
'lawn_mower','water_heater','fan','lock','cover','climate','input_boolean']; 'lawn_mower','fan','lock','cover','climate','input_boolean'];
// Switches that are HA internals / camera settings, not physical devices // Switches that are HA internals / camera settings, not physical devices
$skipKeywords = ['pre_release','_record','_ftp_','_push_','_hub_ringtone', $skipKeywords = ['pre_release','_record','_ftp_','_push_','_hub_ringtone',
'_siren_on','_email_on','_manual_record','_infrared_', '_siren_on','_email_on','_manual_record','_infrared_',
+1 -1
View File
@@ -89,7 +89,7 @@ function getNetworkIO(): array {
} }
function getServices(): array { function getServices(): array {
$services = ["lshttpd","mysql","redis","memcached","postfix","dovecot","jarvis-agent"]; $services = ["nginx","php8.3-fpm","mariadb","redis-server","jarvis-arc","jarvis-agent"];
$result = []; $result = [];
foreach ($services as $svc) { foreach ($services as $svc) {
$out = shell_exec('systemctl is-active ' . escapeshellarg($svc) . ' 2>/dev/null'); $out = shell_exec('systemctl is-active ' . escapeshellarg($svc) . ' 2>/dev/null');
+299 -13
View File
@@ -1,4 +1,9 @@
/*M!999999\- enable the sandbox mode */ /*M!999999\- enable the sandbox mode */
-- MariaDB dump 10.19 Distrib 10.11.14-MariaDB, for debian-linux-gnu (x86_64)
--
-- Host: localhost Database: jarvis_db
-- ------------------------------------------------------
-- Server version 10.11.14-MariaDB-0ubuntu0.24.04.1
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
@@ -10,6 +15,11 @@
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Table structure for table `agent_commands`
--
DROP TABLE IF EXISTS `agent_commands`; DROP TABLE IF EXISTS `agent_commands`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */; /*!40101 SET character_set_client = utf8mb4 */;
@@ -28,6 +38,11 @@ CREATE TABLE `agent_commands` (
KEY `idx_created` (`created_at`) KEY `idx_created` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `agent_metrics`
--
DROP TABLE IF EXISTS `agent_metrics`; DROP TABLE IF EXISTS `agent_metrics`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */; /*!40101 SET character_set_client = utf8mb4 */;
@@ -40,8 +55,13 @@ CREATE TABLE `agent_metrics` (
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
KEY `idx_agent_time` (`agent_id`,`recorded_at`), KEY `idx_agent_time` (`agent_id`,`recorded_at`),
KEY `idx_recorded` (`recorded_at`) KEY `idx_recorded` (`recorded_at`)
) ENGINE=InnoDB AUTO_INCREMENT=28329 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB AUTO_INCREMENT=31422 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `alerts`
--
DROP TABLE IF EXISTS `alerts`; DROP TABLE IF EXISTS `alerts`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */; /*!40101 SET character_set_client = utf8mb4 */;
@@ -58,8 +78,13 @@ CREATE TABLE `alerts` (
`auto_resolve` tinyint(1) DEFAULT 0, `auto_resolve` tinyint(1) DEFAULT 0,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
KEY `idx_source_key` (`source_key`) KEY `idx_source_key` (`source_key`)
) ENGINE=InnoDB AUTO_INCREMENT=30 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB AUTO_INCREMENT=31 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `api_cache`
--
DROP TABLE IF EXISTS `api_cache`; DROP TABLE IF EXISTS `api_cache`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */; /*!40101 SET character_set_client = utf8mb4 */;
@@ -70,6 +95,80 @@ CREATE TABLE `api_cache` (
PRIMARY KEY (`cache_key`) PRIMARY KEY (`cache_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `appointments`
--
DROP TABLE IF EXISTS `appointments`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `appointments` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(255) NOT NULL,
`description` text DEFAULT NULL,
`category` varchar(64) DEFAULT 'personal',
`start_at` datetime NOT NULL,
`end_at` datetime DEFAULT NULL,
`location` varchar(255) DEFAULT NULL,
`all_day` tinyint(1) DEFAULT 0,
`reminder_min` int(11) DEFAULT 30,
`alerted` tinyint(1) DEFAULT 0,
`created_at` datetime DEFAULT current_timestamp(),
`updated_at` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`id`),
KEY `idx_start` (`start_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `arc_jobs`
--
DROP TABLE IF EXISTS `arc_jobs`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `arc_jobs` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`job_type` varchar(64) NOT NULL,
`payload` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
`priority` int(11) DEFAULT 0,
`status` enum('queued','running','done','failed','cancelled') DEFAULT 'queued',
`result` longtext DEFAULT NULL,
`error` varchar(2000) DEFAULT NULL,
`created_by` varchar(128) DEFAULT NULL,
`created_at` datetime DEFAULT current_timestamp(),
`started_at` datetime DEFAULT NULL,
`completed_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_status` (`status`),
KEY `idx_created` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `arc_status`
--
DROP TABLE IF EXISTS `arc_status`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `arc_status` (
`id` int(11) NOT NULL DEFAULT 1,
`version` varchar(20) DEFAULT NULL,
`started_at` datetime DEFAULT NULL,
`last_heartbeat` datetime DEFAULT NULL,
`active_jobs` int(11) DEFAULT 0,
`jobs_done` int(11) DEFAULT 0,
`jobs_failed` int(11) DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `conversations`
--
DROP TABLE IF EXISTS `conversations`; DROP TABLE IF EXISTS `conversations`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */; /*!40101 SET character_set_client = utf8mb4 */;
@@ -83,8 +182,13 @@ CREATE TABLE `conversations` (
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
KEY `idx_session` (`session_id`), KEY `idx_session` (`session_id`),
KEY `idx_created` (`created_at`) KEY `idx_created` (`created_at`)
) ENGINE=InnoDB AUTO_INCREMENT=325 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB AUTO_INCREMENT=335 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `ha_entities`
--
DROP TABLE IF EXISTS `ha_entities`; DROP TABLE IF EXISTS `ha_entities`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */; /*!40101 SET character_set_client = utf8mb4 */;
@@ -102,8 +206,13 @@ CREATE TABLE `ha_entities` (
UNIQUE KEY `uk_agent_entity` (`agent_id`,`entity_id`), UNIQUE KEY `uk_agent_entity` (`agent_id`,`entity_id`),
KEY `idx_domain` (`domain`), KEY `idx_domain` (`domain`),
KEY `idx_updated` (`updated_at`) KEY `idx_updated` (`updated_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB AUTO_INCREMENT=77909 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `kb_facts`
--
DROP TABLE IF EXISTS `kb_facts`; DROP TABLE IF EXISTS `kb_facts`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */; /*!40101 SET character_set_client = utf8mb4 */;
@@ -117,8 +226,13 @@ CREATE TABLE `kb_facts` (
`updated_at` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), `updated_at` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE KEY `unique_fact` (`category`,`fact_key`,`host`) UNIQUE KEY `unique_fact` (`category`,`fact_key`,`host`)
) ENGINE=InnoDB AUTO_INCREMENT=26088 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB AUTO_INCREMENT=41478 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `kb_intents`
--
DROP TABLE IF EXISTS `kb_intents`; DROP TABLE IF EXISTS `kb_intents`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */; /*!40101 SET character_set_client = utf8mb4 */;
@@ -133,8 +247,13 @@ CREATE TABLE `kb_intents` (
`active` tinyint(1) DEFAULT 1, `active` tinyint(1) DEFAULT 1,
`created_at` timestamp NULL DEFAULT current_timestamp(), `created_at` timestamp NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`) PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=22 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB AUTO_INCREMENT=47 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `kb_ollama_models`
--
DROP TABLE IF EXISTS `kb_ollama_models`; DROP TABLE IF EXISTS `kb_ollama_models`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */; /*!40101 SET character_set_client = utf8mb4 */;
@@ -147,8 +266,13 @@ CREATE TABLE `kb_ollama_models` (
`pulled_at` timestamp NULL DEFAULT current_timestamp(), `pulled_at` timestamp NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE KEY `model_name` (`model_name`) UNIQUE KEY `model_name` (`model_name`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `kb_preferences`
--
DROP TABLE IF EXISTS `kb_preferences`; DROP TABLE IF EXISTS `kb_preferences`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */; /*!40101 SET character_set_client = utf8mb4 */;
@@ -159,8 +283,13 @@ CREATE TABLE `kb_preferences` (
`updated_at` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), `updated_at` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE KEY `pref_key` (`pref_key`) UNIQUE KEY `pref_key` (`pref_key`)
) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB AUTO_INCREMENT=19 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `known_commands`
--
DROP TABLE IF EXISTS `known_commands`; DROP TABLE IF EXISTS `known_commands`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */; /*!40101 SET character_set_client = utf8mb4 */;
@@ -173,6 +302,11 @@ CREATE TABLE `known_commands` (
PRIMARY KEY (`id`) PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `metrics_history`
--
DROP TABLE IF EXISTS `metrics_history`; DROP TABLE IF EXISTS `metrics_history`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */; /*!40101 SET character_set_client = utf8mb4 */;
@@ -184,8 +318,13 @@ CREATE TABLE `metrics_history` (
`recorded_at` timestamp NULL DEFAULT current_timestamp(), `recorded_at` timestamp NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
KEY `idx_metric_time` (`metric_name`,`recorded_at`) KEY `idx_metric_time` (`metric_name`,`recorded_at`)
) ENGINE=InnoDB AUTO_INCREMENT=33415 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB AUTO_INCREMENT=34771 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `network_devices`
--
DROP TABLE IF EXISTS `network_devices`; DROP TABLE IF EXISTS `network_devices`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */; /*!40101 SET character_set_client = utf8mb4 */;
@@ -201,8 +340,13 @@ CREATE TABLE `network_devices` (
`created_at` timestamp NULL DEFAULT current_timestamp(), `created_at` timestamp NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE KEY `uk_ip` (`ip`) UNIQUE KEY `uk_ip` (`ip`)
) ENGINE=InnoDB AUTO_INCREMENT=409 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB AUTO_INCREMENT=5556 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `registered_agents`
--
DROP TABLE IF EXISTS `registered_agents`; DROP TABLE IF EXISTS `registered_agents`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */; /*!40101 SET character_set_client = utf8mb4 */;
@@ -210,10 +354,11 @@ CREATE TABLE `registered_agents` (
`id` int(11) NOT NULL AUTO_INCREMENT, `id` int(11) NOT NULL AUTO_INCREMENT,
`agent_id` varchar(128) NOT NULL, `agent_id` varchar(128) NOT NULL,
`hostname` varchar(255) NOT NULL, `hostname` varchar(255) NOT NULL,
`agent_type` enum('linux','homeassistant','proxmox') NOT NULL DEFAULT 'linux', `agent_type` enum('linux','homeassistant','proxmox','windows','macos') NOT NULL DEFAULT 'linux',
`ip_address` varchar(45) DEFAULT NULL, `ip_address` varchar(45) DEFAULT NULL,
`api_key` varchar(64) NOT NULL, `api_key` varchar(64) NOT NULL,
`capabilities` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`capabilities`)), `capabilities` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`capabilities`)),
`version` varchar(32) DEFAULT NULL,
`last_seen` datetime DEFAULT NULL, `last_seen` datetime DEFAULT NULL,
`status` enum('online','offline','unknown') NOT NULL DEFAULT 'unknown', `status` enum('online','offline','unknown') NOT NULL DEFAULT 'unknown',
`created_at` datetime DEFAULT current_timestamp(), `created_at` datetime DEFAULT current_timestamp(),
@@ -221,8 +366,56 @@ CREATE TABLE `registered_agents` (
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE KEY `uk_agent_id` (`agent_id`), UNIQUE KEY `uk_agent_id` (`agent_id`),
KEY `idx_status` (`status`) KEY `idx_status` (`status`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `tasks`
--
DROP TABLE IF EXISTS `tasks`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `tasks` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(255) NOT NULL,
`notes` text DEFAULT NULL,
`category` varchar(64) DEFAULT 'personal',
`priority` enum('urgent','high','normal','low') DEFAULT 'normal',
`status` enum('pending','in_progress','done','cancelled') DEFAULT 'pending',
`due_date` date DEFAULT NULL,
`due_time` time DEFAULT NULL,
`completed_at` datetime DEFAULT NULL,
`created_at` datetime DEFAULT current_timestamp(),
`updated_at` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`id`),
KEY `idx_status_due` (`status`,`due_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `usage_patterns`
--
DROP TABLE IF EXISTS `usage_patterns`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `usage_patterns` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`intent_name` varchar(64) NOT NULL,
`hour` tinyint(2) NOT NULL,
`dow` tinyint(1) NOT NULL,
`hit_count` int(11) DEFAULT 1,
`last_seen` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`id`),
UNIQUE KEY `uk_intent_time` (`intent_name`,`hour`,`dow`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `users`
--
DROP TABLE IF EXISTS `users`; DROP TABLE IF EXISTS `users`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */; /*!40101 SET character_set_client = utf8mb4 */;
@@ -236,7 +429,7 @@ CREATE TABLE `users` (
`created_at` timestamp NULL DEFAULT current_timestamp(), `created_at` timestamp NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`) UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
@@ -248,3 +441,96 @@ CREATE TABLE `users` (
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
--
-- Table structure for table `guardian_config`
--
CREATE TABLE IF NOT EXISTS `guardian_config` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`key_name` varchar(64) NOT NULL,
`value` varchar(255) NOT NULL DEFAULT '',
`updated_at` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`id`),
UNIQUE KEY `uk_key` (`key_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Table structure for table `guardian_events`
--
CREATE TABLE IF NOT EXISTS `guardian_events` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`event_type` varchar(64) NOT NULL,
`severity` enum('info','warning','critical') NOT NULL DEFAULT 'info',
`agent_id` varchar(64) NOT NULL DEFAULT '',
`hostname` varchar(128) NOT NULL DEFAULT '',
`metric` varchar(64) NOT NULL DEFAULT '',
`value` float NOT NULL DEFAULT 0,
`threshold` float NOT NULL DEFAULT 0,
`message` text NOT NULL,
`ai_analysis` text NOT NULL DEFAULT '',
`acknowledged` tinyint(1) NOT NULL DEFAULT 0,
`created_at` datetime NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
KEY `idx_severity` (`severity`),
KEY `idx_ack` (`acknowledged`),
KEY `idx_created` (`created_at`),
KEY `idx_agent` (`agent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Dump completed on 2026-06-29 23:15:44
CREATE TABLE IF NOT EXISTS `email_triage` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`msg_id` varchar(255) NOT NULL,
`account` varchar(64) NOT NULL DEFAULT 'gmail',
`from_name` varchar(255) DEFAULT NULL,
`from_email` varchar(255) DEFAULT NULL,
`subject` varchar(500) DEFAULT NULL,
`date_received` datetime DEFAULT NULL,
`category` varchar(32) NOT NULL DEFAULT 'info',
`priority` int(11) NOT NULL DEFAULT 3,
`summary` text DEFAULT NULL,
`draft_reply` text DEFAULT NULL,
`action_taken` varchar(32) NOT NULL DEFAULT 'none',
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
UNIQUE KEY `uq_msg` (`msg_id`),
KEY `idx_category` (`category`),
KEY `idx_action` (`action_taken`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `email_actions` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`msg_id` varchar(255) DEFAULT NULL,
`from_name` varchar(255) DEFAULT NULL,
`from_email` varchar(255) DEFAULT NULL,
`subject` varchar(500) DEFAULT NULL,
`received_at` datetime DEFAULT NULL,
`suggested_title` varchar(255) DEFAULT NULL,
`suggested_date` date DEFAULT NULL,
`task_id` int(11) DEFAULT NULL,
`appointment_id` int(11) DEFAULT NULL,
`dismissed` tinyint(1) NOT NULL DEFAULT 0,
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
KEY `idx_dismissed` (`dismissed`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `email_sent` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`account` varchar(64) NOT NULL DEFAULT 'gmail',
`to_email` varchar(255) NOT NULL,
`to_name` varchar(255) DEFAULT NULL,
`subject` varchar(500) DEFAULT NULL,
`body` text DEFAULT NULL,
`triage_id` int(11) DEFAULT NULL,
`status` varchar(32) NOT NULL DEFAULT 'sent',
`sent_at` timestamp NULL DEFAULT current_timestamp(),
`error` text DEFAULT NULL,
`message_id` varchar(255) DEFAULT NULL,
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
KEY `idx_account` (`account`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+70
View File
@@ -0,0 +1,70 @@
-- JARVIS KB Seed Data
-- Preferences
INSERT INTO kb_preferences (pref_key, pref_value) VALUES
('user_name', 'Myron'),
('user_title', 'Mr. Blair'),
('ai_model', 'llama3.1:8b'),
('timezone', 'America/Chicago')
ON DUPLICATE KEY UPDATE pref_value = VALUES(pref_value);
-- Intents: greeting, time, system, network, proxmox, ollama, tasks, HA
INSERT INTO kb_intents (intent_name, pattern, response_template, fact_category, action_type, priority, active) VALUES
-- Greetings
('greeting', '(?i)^(hello|hi|hey|good (morning|afternoon|evening)|what.?s up|howdy)\\b', 'Good {current_time}, {user_title}. All systems are online. How can I assist you?', 'system', 'response', 10, 1),
-- Time / date
('current_time', '(?i)\\b(what.?s the (time|current time)|what time is it|tell me the time)\\b', 'It is currently {current_time}, {user_title}.', NULL, 'response', 9, 1),
('current_date', '(?i)\\b(what.?s (today.?s date|the date)|what day is it|today.?s date)\\b', 'Today is {current_date}, {user_title}.', NULL, 'response', 9, 1),
-- System status
('system_status', '(?i)\\b(system (status|health)|how.?s (the system|everything)|jarvis status|all systems)\\b', 'JARVIS is fully operational, {user_title}. CPU: {cpu_usage}%, Memory: {mem_percent}% used ({mem_used_gb}GB / {mem_total_gb}GB). Disk: {disk_used} used of {disk_total}. Uptime: {uptime}. Network agents: {online_count}/{total_count} online.', 'system', 'response', 8, 1),
('cpu_status', '(?i)\\b(cpu|processor) (usage|load|status|percent|utilization)\\b', 'Current CPU usage is {cpu_usage}%, {user_title}. Load averages: {load_1m} (1m), {load_5m} (5m), {load_15m} (15m).', 'system', 'response', 8, 1),
('memory_status', '(?i)\\b(memory|ram|mem) (usage|status|free|used|available)\\b', 'Memory: {mem_used_gb}GB used of {mem_total_gb}GB ({mem_percent}% utilized), {user_title}. Free: {mem_free_gb}GB.', 'system', 'response', 8, 1),
('disk_status', '(?i)\\b(disk|storage|drive) (usage|space|status|free|used|available)\\b', 'Disk status: {disk_used} used of {disk_total} total, {disk_free} free, {user_title}.', 'system', 'response', 8, 1),
('uptime', '(?i)\\b(uptime|how long.*running|how long.*up|server uptime)\\b', 'JARVIS has been running for {uptime}, {user_title}.', 'system', 'response', 7, 1),
-- Network status
('network_status', '(?i)\\b(network (status|health|agents)|agents (online|status)|how many (agents|devices) (online|running))\\b', 'Network status: {online_count} of {total_count} agents are online, {user_title}.', 'network', 'response', 8, 1),
('network_scan', '(?i)\\b(run (a )?network scan|scan (the )?network|nmap scan|network devices)\\b', 'Initiating network scan, {user_title}.', NULL, 'action', 7, 1),
-- Proxmox
('proxmox_status', '(?i)\\b(proxmox (status|health)|vm (status|count|summary)|virtual machines|how many vms)\\b', 'Proxmox: {vm_running} of {vm_total} VMs/containers running, {user_title}. Host CPU: {pve_cpu_percent}%, Memory: {pve_mem_used_gb}GB / {pve_mem_total_gb}GB ({pve_mem_percent}%).', 'proxmox', 'response', 8, 1),
('vm_suggestions', '(?i)\\b(vm (resources|performance|usage)|check vms|resource usage)\\b', 'Checking VM resource usage, {user_title}.', 'proxmox', 'action', 7, 1),
-- Ollama / AI
('ollama_status', '(?i)\\b(ollama (status|health|models)|ai models|llm status|local (ai|models))\\b', 'Ollama is {status} with {model_count} model(s) available: {available_models}, {user_title}.', 'ollama', 'response', 7, 1),
-- Site health
('site_status', '(?i)\\b(site(s)? (status|health|up|down)|website status|are (the )?sites (up|down))\\b', 'Site health — jarvis: {jarvis}, orbishosting: {orbishosting}, tomtomgames: {tomtomgames}, tomsjavajive: {tomsjavajive}, parkerslingshotrentals: {parkersling}, epictravelexpeditions: {epictravelexp}, {user_title}.', 'sites', 'response', 7, 1),
-- Tasks / planner
('task_count', '(?i)\\b(how many tasks|pending tasks|task (count|summary)|my tasks)\\b', 'You have {pending_count} pending tasks and {overdue_count} overdue, {user_title}.', NULL, 'response', 7, 1),
('planner_briefing', '(?i)\\b((daily )?briefing|what.?s (on|happening) today|today.?s schedule|morning briefing)\\b', 'Fetching your daily briefing, {user_title}.', NULL, 'action', 8, 1),
-- Home Assistant
('ha_lights_on', '(?i)\\b(turn (on|off) (the |all )?lights?|lights? (on|off)|switch (on|off) (the )?lights?)\\b', 'Sending light command, {user_title}.', NULL, 'action', 8, 1),
('ha_scene', '(?i)\\b(activate (a |the )?scene|set (a |the )?scene|home scene)\\b', 'Activating home scene, {user_title}.', NULL, 'action', 7, 1),
-- Jellyfin
('jellyfin_now_playing', '(?i)\\b(what.?s (playing|on)|now playing|jellyfin.*playing|playing.*jellyfin)\\b', 'Checking Jellyfin now playing, {user_title}.', NULL, 'action', 7, 1),
('jellyfin_library', '(?i)\\b(jellyfin (library|media|shows?|movies?)|media library|show.*library)\\b', 'Fetching Jellyfin library, {user_title}.', NULL, 'action', 6, 1),
('jellyfin_pause', '(?i)\\b(pause (jellyfin|playback|media)|stop (playing|jellyfin))\\b', 'Pausing Jellyfin, {user_title}.', NULL, 'action', 7, 1),
-- DO server
('do_status', '(?i)\\b(do (server|status)|digital ocean (status|server)|vps status)\\b', 'Digital Ocean server is {do_status}, {user_title}.', 'do_server', 'response', 7, 1),
-- Focus / panels
('focus_mode', '(?i)\\b(focus (mode|on)|enable focus|concentration mode)\\b', 'Enabling focus mode, {user_title}.', NULL, 'action', 6, 1),
('show_panels', '(?i)\\b(show (all )?panels|expand (all|everything)|full view)\\b', 'Expanding all panels, {user_title}.', NULL, 'action', 6, 1),
-- Help
('help', '(?i)^(help|what can you do|commands|capabilities|what do you know)\\s*\\??$', 'I can help you with: system status, network status, VM/Proxmox status, Ollama AI models, site health, tasks and planner briefings, Jellyfin media, Home Assistant lights and devices, and general questions via Ollama. What would you like to know, {user_title}?', NULL, 'response', 5, 1)
ON DUPLICATE KEY UPDATE
pattern = VALUES(pattern),
response_template = VALUES(response_template),
active = 1;
SELECT COUNT(*) AS intents_seeded FROM kb_intents;
SELECT COUNT(*) AS prefs_seeded FROM kb_preferences;
+17
View File
@@ -0,0 +1,17 @@
[Unit]
Description=JARVIS Arc Reactor
After=network-online.target mysql.service
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/opt/jarvis-arc
ExecStart=/opt/jarvis-arc/venv/bin/python3 /opt/jarvis-arc/reactor.py
Restart=always
RestartSec=10
User=root
StandardOutput=append:/home/jarvis.orbishosting.com/logs/arc_reactor.log
StandardError=append:/home/jarvis.orbishosting.com/logs/arc_reactor.log
[Install]
WantedBy=multi-user.target
+31
View File
@@ -0,0 +1,31 @@
#!/bin/bash
# JARVIS backup — DB dump as tar.gz, admin-panel compatible
BACKUP_DIR="/var/backups/jarvis"
LOG="$BACKUP_DIR/backup.log"
LOCK="$BACKUP_DIR/backup.lock"
DB_NAME="jarvis_db"
DB_USER="jarvis_user"
DB_PASS="J4rv1s_Pr0t0c0l_2026!"
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
OUTFILE="$BACKUP_DIR/jarvis_backup_${TIMESTAMP}.tar.gz"
TMPDIR=$(mktemp -d)
mkdir -p "$BACKUP_DIR"
touch "$LOCK"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Starting backup..." >> "$LOG"
cleanup() { rm -rf "$TMPDIR"; rm -f "$LOCK"; }
trap cleanup EXIT
if mysqldump -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" > "$TMPDIR/jarvis_db.sql" 2>>"$LOG"; then
tar -czf "$OUTFILE" -C "$TMPDIR" jarvis_db.sql
SIZE=$(du -sh "$OUTFILE" | cut -f1)
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Backup OK: $(basename "$OUTFILE") ($SIYE)" >> "$LOG"
else
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: mysqldump failed" >> "$LOG"
exit 1
fi
find "$BACKUP_DIR" -name "jarvis_backup_*.tar.gz" -mtime +7 -delete
COUNT=$(ls "$BACKUP_DIR"/jarvis_backup_*.tar.gz 2>/dev/null | wc -l)
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Done. Files retained: $COUNT" >> "$LOG"
+35 -4
View File
@@ -80,11 +80,42 @@ while IFS= read -r path; do
systemctl reload lsws 2>/dev/null || systemctl restart lsws 2>/dev/null systemctl reload lsws 2>/dev/null || systemctl restart lsws 2>/dev/null
log "OLS reloaded for JARVIS deploy" log "OLS reloaded for JARVIS deploy"
# Sync reactor.py to runtime location if it changed # Patch live config.php with correct Ollama host/model and Groq search model
if echo "$CHANGED" | grep -q 'deploy/reactor.py'; then CONFIG="$path/api/config.php"
if [ -f "$CONFIG" ]; then
sed -i "s|http://10\.48\.200\.95:11434|http://10.48.200.210:11434|g" "$CONFIG"
sed -i "s|'llama3\.2:1b'|'llama3.1:8b'|g" "$CONFIG"
sed -i "s|\"llama3\.2:1b\"|\"llama3.1:8b\"|g" "$CONFIG"
sed -i "s|'llama3\.1:70b'|'llama3.1:8b'|g" "$CONFIG"
sed -i "s|\"llama3\.1:70b\"|\"llama3.1:8b\"|g" "$CONFIG"
sed -i "s|'groq/compound-mini'|'compound-beta-mini'|g;s|\"groq/compound-mini\"|\"compound-beta-mini\"|g" "$CONFIG"
log "Patched config.php: Ollama IP→.210, model→llama3.1:8b, Groq search→compound-beta-mini"
fi
# Self-install the deploy script on every run
cp "$path/deploy/jarvis-deploy.sh" /usr/local/bin/jarvis-deploy.sh 2>/dev/null && chmod +x /usr/local/bin/jarvis-deploy.sh
# Sync reactor.py + service file to runtime location if they changed
if echo "$CHANGED" | grep -q 'deploy/reactor.py\|deploy/jarvis-arc.service\|deploy/requirements.txt'; then
mkdir -p /opt/jarvis-arc /home/jarvis.orbishosting.com/logs
cp "$path/deploy/reactor.py" /opt/jarvis-arc/reactor.py cp "$path/deploy/reactor.py" /opt/jarvis-arc/reactor.py
systemctl restart jarvis-arc cp "$path/deploy/requirements.txt" /opt/jarvis-arc/requirements.txt 2>/dev/null
log "Arc Reactor updated and restarted (reactor.py changed)" # Bootstrap venv if it doesn't exist
if [ ! -f /opt/jarvis-arc/venv/bin/activate ]; then
log "Arc Reactor venv missing — creating and installing packages"
python3 -m venv /opt/jarvis-arc/venv
/opt/jarvis-arc/venv/bin/pip install -q -r /opt/jarvis-arc/requirements.txt
log "Arc Reactor venv ready"
fi
if echo "$CHANGED" | grep -q 'deploy/jarvis-arc.service'; then
cp "$path/deploy/jarvis-arc.service" /etc/systemd/system/jarvis-arc.service
systemctl daemon-reload
systemctl enable jarvis-arc
log "Arc Reactor service file installed and enabled"
fi
systemctl restart jarvis-arc 2>/dev/null || \
bash -c "pkill -f reactor.py 2>/dev/null; sleep 1; cd /opt/jarvis-arc && source venv/bin/activate && nohup python3 reactor.py >> /home/jarvis.orbishosting.com/logs/arc_reactor.log 2>&1 &"
log "Arc Reactor updated and restarted"
fi fi
fi fi
+66 -50
View File
@@ -41,16 +41,17 @@ DB_PORT = 3306
DB_USER = "jarvis_user" DB_USER = "jarvis_user"
DB_PASS = "J4rv1s_Pr0t0c0l_2026!" DB_PASS = "J4rv1s_Pr0t0c0l_2026!"
DB_NAME = "jarvis_db" DB_NAME = "jarvis_db"
LOG_FILE = "/home/jarvis.orbishosting.com/logs/arc_reactor.log" LOG_FILE = "/var/log/jarvis/arc_reactor.log"
POLL_INTERVAL = 3 POLL_INTERVAL = 3
HEARTBEAT_INTERVAL = 30 HEARTBEAT_INTERVAL = 30
CLAUDE_API_KEY = "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" CLAUDE_API_KEY = "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"
CLAUDE_MODEL = "claude-sonnet-4-6" CLAUDE_MODEL = "claude-sonnet-4-6"
GROQ_API_KEY = "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" GROQ_API_KEY = "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"
GROQ_MODEL = "llama-3.3-70b-versatile" GROQ_MODEL = "llama-3.3-70b-versatile"
OLLAMA_HOST = "http://10.48.200.95:11434" OLLAMA_HOST = "http://10.48.200.210:11434"
OLLAMA_MODEL = "llama3.2:1b" OLLAMA_MODEL = "llama3.1:8b"
OLLAMA_VISION_MODEL = os.environ.get("OLLAMA_VISION_MODEL", "") # e.g. "llava" or "moondream" -- empty = disabled
GMAIL_USER = "myronblair@gmail.com" GMAIL_USER = "myronblair@gmail.com"
GMAIL_PASS = "demsvdylwweacbcx" GMAIL_PASS = "demsvdylwweacbcx"
@@ -175,6 +176,57 @@ async def _ollama_call(messages: list, system: str = "") -> str:
data = await resp.json() data = await resp.json()
return data.get("response", "") return data.get("response", "")
# -- VISION CALL ----------------------------------------------------------
async def _vision_call(image_b64: str, prompt: str) -> tuple:
"""
Vision provider cascade: Claude -> Ollama vision model -> graceful fallback.
Returns (analysis: str, provider: str).
To enable a local vision model: set OLLAMA_VISION_MODEL env var (e.g. "llava").
"""
# 1. Claude (primary)
if CLAUDE_API_KEY:
try:
import anthropic as _anthropic
_client = _anthropic.AsyncAnthropic(api_key=CLAUDE_API_KEY)
_msg = await _client.messages.create(
model="claude-opus-4-8-20251101",
max_tokens=2048,
messages=[{"role": "user", "content": [
{"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": image_b64}},
{"type": "text", "text": prompt},
]}],
)
text = _msg.content[0].text if _msg.content else ""
log.info("[VISION] Claude vision OK")
return text, "claude"
except Exception as e:
log.warning(f"[VISION] Claude failed ({e}), trying next provider")
# 2. Ollama vision model (if configured via OLLAMA_VISION_MODEL env var)
if OLLAMA_VISION_MODEL:
try:
async with aiohttp.ClientSession() as _sess:
_payload = {"model": OLLAMA_VISION_MODEL, "prompt": prompt,
"images": [image_b64], "stream": False}
async with _sess.post(f"{OLLAMA_HOST}/api/generate", json=_payload,
timeout=aiohttp.ClientTimeout(total=120)) as _resp:
if _resp.status == 200:
_data = await _resp.json()
text = _data.get("response", "")
log.info(f"[VISION] Ollama vision OK ({OLLAMA_VISION_MODEL})")
return text, f"ollama/{OLLAMA_VISION_MODEL}"
raise RuntimeError(f"Ollama HTTP {_resp.status}")
except Exception as e:
log.warning(f"[VISION] Ollama vision failed ({e})")
# 3. No vision provider available
msg = ("[Vision unavailable] Claude credits depleted and no local vision model configured. "
"To enable: pull a vision model on Ollama (e.g. 'ollama pull llava') then set "
"OLLAMA_VISION_MODEL=llava in the jarvis-arc systemd environment.")
log.warning("[VISION] All vision providers unavailable")
return msg, "none"
async def handle_llm(payload: dict) -> dict: async def handle_llm(payload: dict) -> dict:
message = payload.get("message", "") message = payload.get("message", "")
system = payload.get("system", "You are JARVIS, an Iron Man-style AI assistant.") system = payload.get("system", "You are JARVIS, an Iron Man-style AI assistant.")
@@ -662,34 +714,15 @@ async def handle_screenshot(payload: dict) -> dict:
analysis = "" analysis = ""
provider_used = "" provider_used = ""
if do_analyze and image_b64: if do_analyze and image_b64:
try: analysis, provider_used = await _vision_call(image_b64, analyze_prompt)
import anthropic log.info(f"[VISION] Analysis complete via {provider_used} ({len(analysis)} chars)")
client = anthropic.AsyncAnthropic(api_key=CLAUDE_API_KEY)
msg = await client.messages.create(
model="claude-opus-4-8-20251101",
max_tokens=1024,
messages=[{
"role": "user",
"content": [
{"type": "image", "source": {"type": "base64",
"media_type": "image/png", "data": image_b64}},
{"type": "text", "text": analyze_prompt},
],
}],
)
analysis = msg.content[0].text if msg.content else ""
provider_used = "claude"
log.info(f"[VISION] Claude analysis complete ({len(analysis)} chars)")
except Exception as e:
log.warning(f"[VISION] Claude vision failed: {e}")
analysis = f"Vision analysis unavailable: {e}"
elif do_analyze and not image_b64 and result.get("snapshot_type") == "text": elif do_analyze and not image_b64 and result.get("snapshot_type") == "text":
# Text-only sysinfo snapshot — summarize with LLM # Text-only sysinfo snapshot — summarize with LLM
try: try:
snap_text = json.dumps(result, indent=2)[:3000] snap_text = json.dumps(result, indent=2)[:3000]
prompt = f"Summarize this server system snapshot for JARVIS. Highlight any concerns:\n\n{snap_text}" prompt = f"Summarize this server system snapshot for JARVIS. Highlight any concerns:\n\n{snap_text}"
analysis = await llm_call([{"role": "user", "content": prompt}], "claude") analysis = await llm_call([{"role": "user", "content": prompt}], "groq")
provider_used = "claude" provider_used = "groq"
except Exception as e: except Exception as e:
analysis = f"Analysis unavailable: {e}" analysis = f"Analysis unavailable: {e}"
@@ -744,30 +777,13 @@ async def handle_vision(payload: dict) -> dict:
log.info(f"[VISION] Analysis: screenshot_id={screenshot_id} agent={hostname}") log.info(f"[VISION] Analysis: screenshot_id={screenshot_id} agent={hostname}")
try: analysis, provider_used = await _vision_call(image_b64, prompt)
import anthropic
client = anthropic.AsyncAnthropic(api_key=CLAUDE_API_KEY)
msg = await client.messages.create(
model="claude-opus-4-8-20251101",
max_tokens=2048,
messages=[{
"role": "user",
"content": [
{"type": "image", "source": {"type": "base64",
"media_type": "image/png", "data": image_b64}},
{"type": "text", "text": prompt},
],
}],
)
analysis = msg.content[0].text if msg.content else ""
except Exception as e:
raise RuntimeError(f"Vision analysis failed: {e}")
# Update stored screenshot if we have an ID # Update stored screenshot if we have an ID
if screenshot_id: if screenshot_id:
await db_execute( await db_execute(
"UPDATE agent_screenshots SET vision_analysis=%s, vision_provider=%s WHERE id=%s", "UPDATE agent_screenshots SET vision_analysis=%s, vision_provider=%s WHERE id=%s",
(analysis, "claude", int(screenshot_id)) (analysis, provider_used, int(screenshot_id))
) )
return { return {
@@ -775,7 +791,7 @@ async def handle_vision(payload: dict) -> dict:
"screenshot_id": screenshot_id, "screenshot_id": screenshot_id,
"prompt": prompt, "prompt": prompt,
"analysis": analysis, "analysis": analysis,
"provider": "claude", "provider": provider_used,
} }
@@ -1022,7 +1038,7 @@ async def guardian_loop() -> None:
"for Myron. Be direct about severity and what action to take. " "for Myron. Be direct about severity and what action to take. "
"No markdown, no headers." "No markdown, no headers."
) )
ai_msg = await llm_call([{"role": "user", "content": ai_prompt}], "claude") ai_msg = await llm_call([{"role": "user", "content": ai_prompt}], "groq")
# Update the most recent guardian event with AI analysis # Update the most recent guardian event with AI analysis
await db_execute( await db_execute(
"""UPDATE guardian_events SET ai_analysis=%s """UPDATE guardian_events SET ai_analysis=%s
@@ -1062,10 +1078,10 @@ async def _guardian_inject_chat(message: str) -> None:
async def handle_sitrep(payload: dict) -> dict: async def handle_sitrep(payload: dict) -> dict:
""" """
Situation Report comprehensive health briefing across all field stations. Situation Report comprehensive health briefing across all field stations.
payload: { detail: brief|full, provider: claude } payload: { detail: brief|full, provider: groq }
""" """
detail = payload.get("detail", "full") detail = payload.get("detail", "full")
provider = payload.get("provider", "claude") provider = payload.get("provider", "groq")
log.info(f"[GUARDIAN] SITREP requested (detail={detail})") log.info(f"[GUARDIAN] SITREP requested (detail={detail})")
+6
View File
@@ -0,0 +1,6 @@
fastapi
uvicorn[standard]
aiomysql
aiohttp
anthropic
trafilatura
@@ -1,5 +1,5 @@
# INFRASTRUCTURE REFERENCE — COMPLETE SYSTEM MAP # INFRASTRUCTURE REFERENCE — COMPLETE SYSTEM MAP
**Last Updated:** 2026-06-14 **Last Updated:** 2026-07-01
**Owner:** Myron Blair — myronblair@outlook.com **Owner:** Myron Blair — myronblair@outlook.com
--- ---
@@ -29,7 +29,7 @@ INTERNET
[Cloudflare CDN] ────────────────────────────────────────────────────────────── [Cloudflare CDN] ──────────────────────────────────────────────────────────────
│ (proxied DNS for public sites) │ (proxied DNS for public sites)
├─► [DigitalOcean 165.22.1.228] — CyberPanel/OLS — All websites + JARVIS ├─► [DigitalOcean 165.22.1.228] — CyberPanel/OLS — All websites (6 sites)
└─► [FusionPBX 134.209.72.226] — FreeSWITCH PBX (SSH via DO relay) └─► [FusionPBX 134.209.72.226] — FreeSWITCH PBX (SSH via DO relay)
@@ -42,7 +42,7 @@ HOME NETWORK (FortiGate router at 10.48.200.1)
│ ├── VM 113 10.48.200.35 MediaStack (Sonarr/Radarr/qBT/Prowlarr) │ ├── VM 113 10.48.200.35 MediaStack (Sonarr/Radarr/qBT/Prowlarr)
│ ├── VM 118 10.48.200.18 Homebridge │ ├── VM 118 10.48.200.18 Homebridge
│ ├── VM 120 10.48.200.110 NovaCPX hosting panel │ ├── VM 120 10.48.200.110 NovaCPX hosting panel
│ ├── VM 210 10.48.200.95 Ollama (local LLM) │ ├── VM 210 10.48.200.210 Ollama (local LLM + vision) — llama3.1:8b, llava:7b
│ └── CT110 10.48.200.19 WireGuard exit container │ └── CT110 10.48.200.19 WireGuard exit container
├─► PVE2 Proxmox 10.48.200.91 (secondary hypervisor) ├─► PVE2 Proxmox 10.48.200.91 (secondary hypervisor)
@@ -73,13 +73,13 @@ FortiGate Port Forwards:
| **OS** | Ubuntu 22.04 LTS | | **OS** | Ubuntu 22.04 LTS |
| **Panel** | CyberPanel (OpenLiteSpeed) | | **Panel** | CyberPanel (OpenLiteSpeed) |
| **SSH** | `ssh root@165.22.1.228` — password: `Gonewalk1974!@#` | | **SSH** | `ssh root@165.22.1.228` — password: `Gonewalk1974!@#` |
| **Purpose** | All public websites + JARVIS AI + webhook deploy system | | **Purpose** | All public websites (6 sites) — webhook deploy for websites |
**Key Paths:** **Key Paths:**
- All sites: `/home/<domain>/public_html/` - All sites: `/home/<domain>/public_html/`
- JARVIS: `/home/jarvis.orbishosting.com/`
- Deploy log: `/home/jarvis.orbishosting.com/logs/deploy.log` - Deploy log: per-site (website deploys only)
- Watchdog log: `/home/jarvis.orbishosting.com/logs/watchdog.log` - Watchdog log: `/usr/local/lsws/logs/watchdog.log`
- Infra repo: `/opt/infra` - Infra repo: `/opt/infra`
**Services running:** **Services running:**
@@ -87,7 +87,7 @@ FortiGate Port Forwards:
- MySQL 8 — all site databases on localhost - MySQL 8 — all site databases on localhost
- Redis — session/cache - Redis — session/cache
- PHP 8.5 (`lsphp85`) — runtime for all sites - PHP 8.5 (`lsphp85`) — runtime for all sites
- Cron jobs: JARVIS deploy runner (every 1 min), facts collector (every 3 min), stats cache (every 5 min), watchdog (every 5 min) - Cron jobs: website deploy runner (every 1 min), watchdog (every 5 min)
**CyberPanel Web UI:** `https://165.22.1.228:8090` **CyberPanel Web UI:** `https://165.22.1.228:8090`
Login: `myron / Joker1974!!!` Login: `myron / Joker1974!!!`
@@ -102,7 +102,7 @@ Login: `myron / Joker1974!!!`
|-------|-------| |-------|-------|
| **IP** | 134.209.72.226 | | **IP** | 134.209.72.226 |
| **OS** | Debian (DigitalOcean droplet) | | **OS** | Debian (DigitalOcean droplet) |
| **SSH** | Must relay via DO: `ssh root@165.22.1.228``ssh root@134.209.72.226` — password: `Joker1974!@#` | | **SSH** | Direct via Tailscale: `ssh root@100.74.46.120` — password: `Joker1974!@#` |
| **Direct SSH** | Only from: 107.178.2.130 / 97.154.109.245 | | **Direct SSH** | Only from: 107.178.2.130 / 97.154.109.245 |
| **Purpose** | VoIP phone system — handles all inbound/outbound calls | | **Purpose** | VoIP phone system — handles all inbound/outbound calls |
@@ -305,17 +305,20 @@ sshpass -p 'Joker1974!!!' ssh -o StrictHostKeyChecking=no root@10.48.200.90 \
--- ---
### VM 210 — Ollama Local LLM (PVE1) ### VM 210 — Ollama Local LLM + Vision (PVE1)
| Field | Value | | Field | Value |
|-------|-------| |-------|-------|
| **IP** | 10.48.200.95 | | **IP** | 10.48.200.210 |
| **OS** | Ubuntu (cloud image) | | **OS** | Ubuntu (cloud image) |
| **SSH** | `ssh myron@10.48.200.95` — password: `Joker1974!` (then `sudo`) | | **SSH** | `ssh root@10.48.200.210` via PVE1 hop — password: `Joker1974!!!` |
| **Purpose** | Local AI inference — runs llama3.2 model for JARVIS Tier 1 chat | | **Purpose** | Local AI inference — chat (llama3.1:8b) + vision (llava:7b) |
| **API** | `http://10.48.200.95:11434` (Ollama REST API) | | **API** | `http://10.48.200.210:11434` (Ollama REST API) |
| **JARVIS Agent** | ID: `ollama-ai_ubuntu` | | **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). **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`
--- ---
@@ -372,11 +375,11 @@ All sites are at `/home/<domain>/public_html/` on DO (165.22.1.228).
--- ---
### jarvis.orbishosting.com — JARVIS AI Dashboard ### jarvis.orbishosting.com — JARVIS AI Dashboard (MOVED TO PVE1 VM 211)
| Field | Value | | Field | Value |
|-------|-------| |-------|-------|
| **URL** | https://jarvis.orbishosting.com | | **URL** | http://jarvis.orbishosting.com:1972 |
| **Path** | `/home/jarvis.orbishosting.com/` | | **Path** | `/var/www/jarvis/ (on JARVIS VM 10.48.200.211)` |
| **GitHub** | `myronblair/jarvis` | | **GitHub** | `myronblair/jarvis` |
| **Login** | `myron / Joker1974!!!` | | **Login** | `myron / Joker1974!!!` |
| **Purpose** | Iron Man-style AI home dashboard with voice control, smart home, media, planner | | **Purpose** | Iron Man-style AI home dashboard with voice control, smart home, media, planner |
@@ -468,11 +471,11 @@ See Section 7 for full JARVIS details.
## 7. JARVIS AI SYSTEM ## 7. JARVIS AI SYSTEM
**URL:** https://jarvis.orbishosting.com **URL:** http://jarvis.orbishosting.com:1972
**Files:** `/home/jarvis.orbishosting.com/` on DO **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!` **DB:** `jarvis_db``jarvis_user / J4rv1s_Pr0t0c0l_2026!`
**Login:** `myron / Joker1974!!!` **Login:** `myron / Joker1974!!!`
**Admin portal:** https://jarvis.orbishosting.com/admin **Admin portal:** http://jarvis.orbishosting.com:1972/admin
### Architecture (end-to-end) ### Architecture (end-to-end)
@@ -484,12 +487,30 @@ Voice (browser mic)
→ /api/chat.php (4-tier AI) → /api/chat.php (4-tier AI)
Tier 0.7: KB intents / planner (tasks, appointments) Tier 0.7: KB intents / planner (tasks, appointments)
Tier 1: Knowledge Base (MySQL) Tier 1: Knowledge Base (MySQL)
Tier 1.5: Ollama (10.48.200.95:11434, llama3.2) — local LLM 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 2: Groq (cloud, model: compound-beta-mini)
Tier 3: Claude API (Anthropic, fallback) Tier 3: Claude API (Anthropic, fallback)
→ ElevenLabs TTS → browser speaker → 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 ### Deploy Pipeline
``` ```
Code edit → git push → GitHub webhook → /webhook.php (HMAC verified) Code edit → git push → GitHub webhook → /webhook.php (HMAC verified)
@@ -501,7 +522,7 @@ Webhook secret: `4c8805f0285214ff0a0602b5880270b935f36a896946c7f1`
### Agent System ### Agent System
Agents installed on all servers — phone home every 10s (heartbeat) / 30s (metrics). Agents installed on all servers — phone home every 10s (heartbeat) / 30s (metrics).
Registration key: `f846a9aaf7ce9a61742c63c87c4186052a71d2a580c65518` Registration key: `f846a9aaf7ce9a61742c63c87c4186052a71d2a580c65518`
Install command: `curl -sk https://jarvis.orbishosting.com/install-agent.sh | bash -s <hostname> <linux|proxmox>` Install command: `curl -sk http://10.48.200.211/install-agent.sh | bash -s <hostname> <linux|proxmox>`
### Self-Healing Watchdog ### Self-Healing Watchdog
`/usr/local/bin/jarvis-watchdog.sh` — runs every 5 min (root cron on DO) `/usr/local/bin/jarvis-watchdog.sh` — runs every 5 min (root cron on DO)
@@ -594,6 +615,14 @@ fs_cli -x "reloadacl" # reload ACL (safe)
## 10. BACKUP SYSTEMS ## 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 ### DO Server Backup
- **Repo:** `myronblair/do-server-config` - **Repo:** `myronblair/do-server-config`
- **Schedule:** Weekly, Sunday 4am - **Schedule:** Weekly, Sunday 4am
@@ -680,8 +709,8 @@ sshpass -p 'Joker1974!!!' ssh root@10.48.200.90 \
| phpMyAdmin (DO) | https://165.22.1.228/phpmyadmin | myron | `Joker1974!!!` | | phpMyAdmin (DO) | https://165.22.1.228/phpmyadmin | myron | `Joker1974!!!` |
| Proxmox PVE1 | https://orbisne.fortiddns.com:8006 | root | `Joker1974!!!` | | Proxmox PVE1 | https://orbisne.fortiddns.com:8006 | root | `Joker1974!!!` |
| Proxmox PVE2 | https://10.48.200.91:8006 | root | `Joker1974!!!` | | Proxmox PVE2 | https://10.48.200.91:8006 | root | `Joker1974!!!` |
| JARVIS | https://jarvis.orbishosting.com | myron | `Joker1974!!!` | | JARVIS | http://jarvis.orbishosting.com:1972 | myron | `Joker1974!!!` |
| JARVIS Admin | https://jarvis.orbishosting.com/admin | myron | `Joker1974!!!` | | JARVIS Admin | http://jarvis.orbishosting.com:1972/admin | myron | `Joker1974!!!` |
| FusionPBX | https://fusion.orbishosting.com | admin | `fY7XP5swgtpbzrYLhkeVYkA4744` | | FusionPBX | https://fusion.orbishosting.com | admin | `fY7XP5swgtpbzrYLhkeVYkA4744` |
| Home Assistant | http://orbisne.fortiddns.com:8123 | myron | (HA password) | | Home Assistant | http://orbisne.fortiddns.com:8123 | myron | (HA password) |
| NovaCPX Admin | https://10.48.200.110:8882 | admin | `Admin2026!` | | NovaCPX Admin | https://10.48.200.110:8882 | admin | `Admin2026!` |
+288 -22
View File
@@ -487,25 +487,26 @@ if ($action) {
$arcCounts = []; $arcCounts = [];
foreach ($arcStats as $r) $arcCounts[$r['status']] = (int)$r['cnt']; foreach ($arcStats as $r) $arcCounts[$r['status']] = (int)$r['cnt'];
$cronLast = []; $cronLast = [];
$cronLog = '/home/jarvis.orbishosting.com/logs/cron.log'; $cronLog = '/var/log/jarvis/cron.log';
if (file_exists($cronLog)) { if (file_exists($cronLog)) {
$lines = array_filter(explode("\n", shell_exec("grep -a 'facts\\|stats\\|calendar' " . 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{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})\\].*calendar/i', $line, $m)) $cronLast['calendar_sync'] = $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})\\].*intent/i', $line, $m)) $cronLast['kb_intent_generator'] = $m[1];
} }
} }
if (empty($cronLast['stats_cache'])) { if (empty($cronLast['stats_cache'])) {
$row = JarvisDB::query('SELECT MAX(updated_at) as t FROM api_cache WHERE cache_key IN ("weather","news")'); $row = JarvisDB::query('SELECT MAX(updated_at) as t FROM api_cache WHERE cache_key IN ("weather","news")');
if (!empty($row[0]['t'])) $cronLast['stats_cache'] = $row[0]['t']; if (!empty($row[0]['t'])) $cronLast['stats_cache'] = $row[0]['t'];
} }
$deployLog = '/home/jarvis.orbishosting.com/logs/deploy.log'; $deployLog = '/var/log/jarvis/deploy.log';
if (file_exists($deployLog)) { if (file_exists($deployLog)) {
$last = shell_exec("grep -a '\\[' " . escapeshellarg($deployLog) . " | tail -1"); $last = shell_exec("grep -a '\\[' " . escapeshellarg($deployLog) . " | tail -1");
if (preg_match('/^\\[(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})\\]/', trim($last), $m)) $cronLast['jarvis_deploy'] = $m[1]; if (preg_match('/^\\[(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})\\]/', trim($last), $m)) $cronLast['jarvis_deploy'] = $m[1];
} }
$wdLog = '/home/jarvis.orbishosting.com/logs/watchdog.log'; $wdLog = '/var/log/jarvis/watchdog.log';
if (file_exists($wdLog)) $cronLast['jarvis_watchdog'] = date('Y-m-d H:i:s', filemtime($wdLog)); if (file_exists($wdLog)) $cronLast['jarvis_watchdog'] = date('Y-m-d H:i:s', filemtime($wdLog));
$bkLog = '/var/backups/jarvis/backup.log'; $bkLog = '/var/backups/jarvis/backup.log';
if (file_exists($bkLog)) { if (file_exists($bkLog)) {
@@ -518,9 +519,9 @@ if ($action) {
break; break;
case 'worker_action': case 'worker_action':
$wType = $data['worker_type'] ?? ''; $wType = $_REQUEST['worker_type'] ?? '';
$wId = $data['worker_id'] ?? ''; $wId = $_REQUEST['worker_id'] ?? '';
$wAction = $data['action'] ?? ''; $wAction = $_REQUEST['waction'] ?? $_REQUEST['action'] ?? '';
if ($wType === 'agent' && $wAction === 'update') { if ($wType === 'agent' && $wAction === 'update') {
JarvisDB::execute('INSERT INTO agent_commands (agent_id,command_type,command_data,status) VALUES (?,?,?,?)', JarvisDB::execute('INSERT INTO agent_commands (agent_id,command_type,command_data,status) VALUES (?,?,?,?)',
[$wId,'update','{}','pending']); [$wId,'update','{}','pending']);
@@ -533,25 +534,96 @@ if ($action) {
j(json_decode(curl_exec($ch),true)?:['error'=>'reactor unreachable']); j(json_decode(curl_exec($ch),true)?:['error'=>'reactor unreachable']);
} elseif ($wType === 'cron' && $wAction === 'run') { } elseif ($wType === 'cron' && $wAction === 'run') {
$scripts = [ $scripts = [
'facts_collector'=>[true, '/home/jarvis.orbishosting.com/api/endpoints/facts_collector.php'], 'facts_collector' =>[true, '/var/www/jarvis/api/endpoints/facts_collector.php'],
'stats_cache' =>[true, '/home/jarvis.orbishosting.com/api/endpoints/stats_cache.php'], 'stats_cache' =>[true, '/var/www/jarvis/api/endpoints/stats_cache.php'],
'calendar_sync' =>[true, '/home/jarvis.orbishosting.com/api/endpoints/calendar_sync.php'], 'calendar_sync' =>[true, '/var/www/jarvis/api/endpoints/calendar_sync.php'],
'jarvis_deploy' =>[false,'/usr/local/bin/jarvis-deploy.sh'], 'kb_intent_generator' =>[true, '/var/www/jarvis/api/endpoints/kb_intent_generator.php'],
'jarvis_watchdog'=>[false,'/usr/local/bin/jarvis-watchdog.sh'], 'jarvis_deploy' =>[false,'/usr/local/bin/jarvis-deploy.sh'],
'jarvis_watchdog' =>[false,'/usr/local/bin/jarvis-watchdog.sh'],
]; ];
if (isset($scripts[$wId])) { if (isset($scripts[$wId])) {
[$isPhp,$path] = $scripts[$wId]; [$isPhp,$path] = $scripts[$wId];
$env = ($wId === 'kb_intent_generator') ? 'JARVIS_FORCE_RUN=1 ' : '';
$cmd = $isPhp $cmd = $isPhp
? '/usr/local/lsws/lsphp85/bin/lsphp '.escapeshellarg($path).' >> /home/jarvis.orbishosting.com/logs/cron.log 2>&1 &' ? $env.'/usr/bin/php8.3 '.escapeshellarg($path).' >> /var/log/jarvis/cron.log 2>&1 &'
: escapeshellcmd($path).' >> /home/jarvis.orbishosting.com/logs/deploy.log 2>&1 &'; : escapeshellcmd($path).' >> /var/log/jarvis/deploy.log 2>&1 &';
shell_exec($cmd); shell_exec($cmd);
j(['ok'=>true,'msg'=>ucwords(str_replace('_',' ',$wId)).' triggered']); j(['ok'=>true,'msg'=>ucwords(str_replace('_',' ',$wId)).' triggered']);
} 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('pkill -f reactor.py 2>/dev/null; sleep 1; cd /opt/jarvis-arc && source venv/bin/activate && nohup python3 reactor.py >> /home/jarvis.orbishosting.com/logs/arc_reactor.log 2>&1 &'); shell_exec('systemctl restart jarvis-arc 2>&1');
j(['ok'=>true,'msg'=>'Arc Reactor restarting']); k(['ok'=>true,'msg'=>'Arc Reactor restarting via systemd']);
} elseif ($wType === 'daemon' && $wId === 'arc_reactor' && $wAction === 'setup') {
$log = '/var/log/jarvis/arc-setup.log';
$cmd = implode(' && ', [
'mkdir -p /opt/jarvis-arc /var/log/jarvis',
'cp /var/www/jarvis/deploy/reactor.py /opt/jarvis-arc/reactor.py',
'cp /var/www/jarvis/deploy/requirements.txt /opt/jarvis-arc/requirements.txt',
'[ ! -f /opt/jarvis-arc/venv/bin/activate ] && python3 -m venv /opt/jarvis-arc/venv || true',
'/opt/jarvis-arc/venv/bin/pip install -q -r /opt/jarvis-arc/requirements.txt',
'cp /var/www/jarvis/deploy/jarvis-arc.service /etc/systemd/system/jarvis-arc.service',
'systemctl daemon-reload',
'systemctl enable jarvis-arc',
'systemctl restart jarvis-arc',
]);
shell_exec("($cmd) >> " . escapeshellarg($log) . " 2>&1 &");
k(['ok'=>true,'msg'=>'Arc Reactor setup started — check ' . $log]);
} elseif ($wType === 'agent' && $wAction === 'update_status') {
$ag = JarvisDB::single('SELECT version, status FROM registered_agents WHERE agent_id=?', [$wId]);
j(['ok'=>true,'version'=>$ag['version']??null,'status'=>$ag['status']??'unknown']);
} else { bad('Invalid worker action'); } } else { bad('Invalid worker action'); }
break; break;
case 'intent_gen_history':
$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 (stripos($line, 'KB Intent Generator') === false) continue;
// parse timestamp
preg_match('/^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]/', $line, $tm);
$ts = isset($tm[1]) ? strtotime($tm[1]) : 0;
// last success (done/complete)
if (!$result['last_success'] && preg_match('/done|complete/i', $line)) {
$result['last_success'] = $tm[1] ?? null;
preg_match('/inserted[:\s]+(\d+)/i', $line, $cnt);
$result['last_success_count'] = isset($cnt[1]) ? (int)$cnt[1] : null;
}
// failures in last 7 days
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);
case 'intent_gen_log':
$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);
// snapshot=1 just returns the current line count (call BEFORE triggering run)
if (!empty($_GET['snapshot'])) { j(['next_line'=>$total]); }
// Return only KB Intent Generator lines from position $since onward
$out = [];
for ($i = $since; $i < $total; $i++) {
if (stripos($allLines[$i], 'KB Intent Generator') !== false) {
$out[] = $allLines[$i];
}
}
j(['lines'=>$out, 'next_line'=>$total]);
case 'arc_setup_log':
$logFile = '/var/log/jarvis/arc-setup.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]); }
j(['lines'=>array_values(array_slice($allLines, $since)), 'next_line'=>$total]);
case 'arc_status': case 'arc_status':
$ch = curl_init('http://127.0.0.1:7474/status'); $ch = curl_init('http://127.0.0.1:7474/status');
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>5, CURLOPT_CONNECTTIMEOUT=>3]); curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>5, CURLOPT_CONNECTTIMEOUT=>3]);
@@ -1426,6 +1498,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:#00d4ff;color:#00d4ff" onclick="runIntentGenerator()">&#9654; RUN GENERATOR</button>
</div> </div>
</div> </div>
<div style="display:flex;gap:8px;margin-bottom:10px;align-items:center"> <div style="display:flex;gap:8px;margin-bottom:10px;align-items:center">
@@ -2158,7 +2231,8 @@ const CRON_DEFS = [
{id:'jarvis_deploy', label:'Deploy Runner', schedule:'Every 1 min', host:'jarvis-do'}, {id:'jarvis_deploy', label:'Deploy Runner', schedule:'Every 1 min', host:'jarvis-do'},
{id:'jarvis_watchdog', label:'Watchdog', schedule:'Every 5 min', host:'jarvis-do'}, {id:'jarvis_watchdog', label:'Watchdog', schedule:'Every 5 min', host:'jarvis-do'},
{id:'jarvis_backup', label:'JARVIS Backup', schedule:'Daily 2am', host:'jarvis-do', norun:true}, {id:'jarvis_backup', label:'JARVIS Backup', schedule:'Daily 2am', host:'jarvis-do', norun:true},
{id:'do_server_backup',label:'DO Server Backup', schedule:'Weekly Sun 4am', host:'jarvis-do', norun:true}, {id:'do_server_backup', label:'DO Server Backup', schedule:'Weekly Sun 4am', host:'jarvis-do', norun:true},
{id:'kb_intent_generator', label:'KB Intent Generator', schedule:'Daily 3am', host:'jarvis'},
]; ];
function wBtn(col) { function wBtn(col) {
const c={cyan:'var(--cyan)',red:'var(--red)',green:'var(--green)',dim:'var(--dim)'}[col]||'var(--dim)'; const c={cyan:'var(--cyan)',red:'var(--red)',green:'var(--green)',dim:'var(--dim)'}[col]||'var(--dim)';
@@ -2185,12 +2259,201 @@ function wToast(msg,err=false) {
t.style.opacity='1';t.textContent=msg; t.style.opacity='1';t.textContent=msg;
setTimeout(()=>{t.style.opacity='0';},3000); setTimeout(()=>{t.style.opacity='0';},3000);
} }
async function runIntentGenerator() {
const modalHtml = `
<div style="font-family:var(--mono);font-size:0.72rem;line-height:1.8">
<div id="ig-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="ig-status" style="color:var(--cyan);margin-bottom:8px;font-size:0.65rem;letter-spacing:1px">LAUNCHING...</div>
<div id="ig-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 id="ig-stats" style="margin-top:10px;display:flex;gap:20px;font-size:0.6rem;color:var(--text-dim)"></div>
</div>`;
openModal('&#9654; KB INTENT GENERATOR', modalHtml, null, null);
document.getElementById('modalSave').textContent = 'CLOSE';
const logEl = document.getElementById('ig-log');
const statEl = document.getElementById('ig-status');
const statsEl = document.getElementById('ig-stats');
const historyEl = document.getElementById('ig-history');
let _igStop = false;
let _igDone = false;
let lastLine = 0;
let dotted = 0;
// Close just stops polling — server job keeps running
const stopAndClose = () => {
_igStop = true;
closeModal();
if (!_igDone) toast('KB generator running in background', 'ok');
};
document.getElementById('modalSave').onclick = stopAndClose;
document.getElementById('modalClose').onclick = stopAndClose;
// Load history panel (async, non-blocking)
api('intent_gen_history').then(h => {
if (!historyEl) return;
let html = '';
if (h?.last_success) {
html += `<span style="color:var(--green)">&#10003; Last success: ${h.last_success}`;
if (h.last_success_count != null) html += ` (+${h.last_success_count} intents)`;
html += '</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'; });
// Snapshot log position BEFORE triggering
const snap = await api('intent_gen_log', {snapshot:1});
lastLine = snap?.next_line ?? 0;
// Kick off the generator (JARVIS_FORCE_RUN=1 set server-side — bypasses 20h guard)
const kick = await api('worker_action', {worker_type:'cron', worker_id:'kb_intent_generator', 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 (_igStop) return;
try {
const res = await api('intent_gen_log', {since: lastLine});
if (res && Array.isArray(res.lines) && res.lines.length) {
lastLine = res.next_line;
const isFirst = logEl.textContent === 'Waiting for first output...';
if (isFirst) 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('insert') || lower.includes('added') || lower.includes('done') || lower.includes('complete') || lower.includes('cleanup') || lower.includes('force-run'))
div.style.color = 'var(--green)';
else
div.style.color = 'var(--text-dim)';
div.textContent = line;
logEl.appendChild(div);
});
logEl.scrollTop = logEl.scrollHeight;
const lastLines = res.lines.join(' ').toLowerCase();
if (lastLines.includes('kb intent generator: done') || lastLines.includes('total inserted') || lastLines.includes('cleanup complete')) {
_igDone = true;
statEl.style.color = 'var(--green)';
statEl.textContent = 'COMPLETE';
const total = (res.lines.join('\n').match(/inserted[:\s]+(\d+)/i) || [])[1];
if (total) statsEl.innerHTML = `<span style="color:var(--green)">+${total} intents added</span>`;
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 (!_igStop) setTimeout(pollLog, 3000);
}
setTimeout(pollLog, 2000);
}
async function arcSetup() {
const modalHtml = `
<div style="font-family:var(--mono);font-size:0.72rem;line-height:1.8">
<div id="as-status" style="color:var(--cyan);margin-bottom:8px;font-size:0.65rem;letter-spacing:1px">LAUNCHING...</div>
<div id="as-log" style="background:#060a0e;border:1px solid var(--border);border-radius:3px;padding:10px 12px;min-height:160px;max-height:360px;overflow-y:auto;white-space:pre-wrap;font-size:0.62rem;line-height:1.7;color:var(--text-dim)">Waiting for output...</div>
</div>`;
openModal("&#9881; ARC REACTOR SETUP", modalHtml, null, null);
document.getElementById("modalSave").textContent = "CLOSE";
let _stop = false;
document.getElementById("modalSave").onclick = () => { _stop = true; closeModal(); loadWorkers(); };
document.getElementById("modalClose").onclick = () => { _stop = true; closeModal(); loadWorkers(); };
const logEl = document.getElementById("as-log");
const statEl = document.getElementById("as-status");
const snap = await api("arc_setup_log", {snapshot:1});
let lastLine = snap?.next_line ?? 0;
const kick = await api("worker", {type:"daemon", id:"arc_reactor", action:"setup"});
if (!kick?.ok) {
statEl.style.color = "var(--red)";
statEl.textContent = "LAUNCH FAILED: " + (kick?.msg || kick?.error || "unknown");
return;
}
statEl.textContent = "RUNNING — polling every 2s...";
async function pollLog() {
if (_stop) return;
try {
const res = await api("arc_setup_log", {since: lastLine});
if (res?.lines?.length) {
lastLine = res.next_line;
if (logEl.textContent === "Waiting for output...") logEl.textContent = "";
res.lines.forEach(line => {
const d = document.createElement("div");
const l = line.toLowerCase();
if (l.includes("error") || l.includes("fail")) d.style.color = "var(--red)";
else if (l.includes("warn") || l.includes("skip")) d.style.color = "var(--yellow)";
else if (l.includes("ok") || l.includes("done") || l.includes("active") ||
l.includes("success") || l.includes("restart") || l.includes("enabled")) d.style.color = "var(--green)";
else d.style.color = "var(--text-dim)";
d.textContent = line;
logEl.appendChild(d);
});
logEl.scrollTop = logEl.scrollHeight;
const joined = res.lines.join(" ").toLowerCase();
if (joined.includes("systemctl restart") || joined.includes("active") ||
joined.includes("done") || joined.includes("failed")) {
statEl.style.color = (joined.includes("error") || joined.includes("fail")) ? "var(--red)" : "var(--green)";
statEl.textContent = (joined.includes("error") || joined.includes("fail")) ? "SETUP FAILED" : "SETUP COMPLETE";
document.getElementById("modalSave").textContent = "CLOSE";
loadWorkers(); return;
}
}
} catch(e) {}
setTimeout(pollLog, 2000);
}
setTimeout(pollLog, 1500);
}
async function workerAction(type,id,action) { async function workerAction(type,id,action) {
if (type === 'agent' && action === 'update') { if (type === 'agent' && action === 'update') {
await agentUpdateFlow(id); await agentUpdateFlow(id);
return; return;
} }
const res=await api('worker_action',{worker_type:type,worker_id:id,action}); const res=await api('worker_action',{worker_type:type,worker_id:id,waction:action});
if(res&&res.ok){wToast(res.msg||'Done');setTimeout(loadWorkers,2500);} if(res&&res.ok){wToast(res.msg||'Done');setTimeout(loadWorkers,2500);}
else wToast((res&&res.error)||'Action failed',true); else wToast((res&&res.error)||'Action failed',true);
} }
@@ -2215,7 +2478,7 @@ async function agentUpdateFlow(agentId) {
}; };
// Dispatch command // Dispatch command
const res = await api('worker_action', {worker_type:'agent', worker_id:agentId, action:'update'}); const res = await api('worker_action', {worker_type:'agent', worker_id:agentId, waction:'update'});
if (!res || !res.ok) { if (!res || !res.ok) {
log('✗ Failed to dispatch: ' + (res?.error||'unknown'), 'var(--red)'); log('✗ Failed to dispatch: ' + (res?.error||'unknown'), 'var(--red)');
document.getElementById('modalSave').style.display = ''; document.getElementById('modalSave').style.display = '';
@@ -2226,7 +2489,7 @@ async function agentUpdateFlow(agentId) {
log('✓ Command dispatched — waiting for agent to pick up...', 'var(--cyan)'); log('✓ Command dispatched — waiting for agent to pick up...', 'var(--cyan)');
// Poll agent_commands for the result (max 90s) // Poll agent_commands for the result (max 90s)
const cmdRes = await api('worker_action', {worker_type:'agent', worker_id:agentId, action:'update_status'}).catch(()=>null); const cmdRes = await api('worker_action', {worker_type:'agent', worker_id:agentId, waction:'update_status'}).catch(()=>null);
// Actually poll via workers_list for version change // Actually poll via workers_list for version change
const deadline = Date.now() + 90000; const deadline = Date.now() + 90000;
let done = false; let done = false;
@@ -2330,7 +2593,10 @@ async function loadWorkers() {
<td class="ts">jarvis-do :7474</td> <td class="ts">jarvis-do :7474</td>
<td>${rdot}${ron?'<span style="color:var(--green)">ONLINE</span>':'<span style="color:var(--red)">OFFLINE</span>'}</td> <td>${rdot}${ron?'<span style="color:var(--green)">ONLINE</span>':'<span style="color:var(--red)">OFFLINE</span>'}</td>
<td class="ts">${rinfo}</td> <td class="ts">${rinfo}</td>
<td><button onclick="workerAction('daemon','arc_reactor','restart')" style="${wBtn('red')}">&#8635; RESTART</button></td> <td style="white-space:nowrap">
<button onclick="workerAction('daemon','arc_reactor','restart')" style="${wBtn('red')}">&#8635; RESTART</button>
<button onclick="arcSetup()" style="${wBtn('cyan')};margin-left:6px">&#9881; SETUP</button>
</td>
</tr>`; </tr>`;
} }
async function loadDashboard() { async function loadDashboard() {
+135 -262
View File
@@ -1,278 +1,151 @@
# JARVIS Agent Installer — Windows (PowerShell) #Requires -RunAsAdministrator
# Registers the agent as a proper Windows Service (Win 8.1+, no open window required). <#
# Requires pywin32. Runs the service as LocalSystem. .SYNOPSIS
# JARVIS Agent installer for Windows.
# Run as Administrator:
# Set-ExecutionPolicy Bypass -Scope Process
# .\install-windows.ps1 -JarvisUrl https://jarvis.orbishosting.com -Key YOUR_KEY
#
# One-liner (PowerShell as Admin):
# irm https://jarvis.orbishosting.com/agent/install-windows.ps1 | iex
param( .DESCRIPTION
[string]$JarvisUrl = "", Installs JARVIS Agent as a Windows Service that auto-starts at boot.
[string]$Key = "", Requires: PowerShell 5.1+, internet access, and Administrator rights.
[string]$AgentName = ""
)
# param() defaults don't apply when piped through iex — set here as fallback .EXAMPLE
if (-not $JarvisUrl) { $JarvisUrl = "https://jarvis.orbishosting.com" } # Interactive install (prompts for registration key):
if (-not $Key) { $Key = "f846a9aaf7ce9a61742c63c87c4186052a71d2a580c65518" } irm https://jarvis.orbishosting.com/agent/install-windows.ps1 | iex
if (-not $AgentName) { $AgentName = $env:COMPUTERNAME.ToLower() }
$ErrorActionPreference = "Stop" # Silent install with key:
$InstallDir = "C:\ProgramData\jarvis-agent" $env:JARVIS_REG_KEY='your_key_here'; irm https://jarvis.orbishosting.com/agent/install-windows.ps1 | iex
$AgentScript = "$InstallDir\jarvis-agent-windows.py" #>
$ConfigFile = "$InstallDir\config.json"
$ServiceName = "JARVISAgent"
$OldTaskName = "JARVIS-Agent" # legacy scheduled-task name
Write-Host "" $ErrorActionPreference = 'Stop'
Write-Host " ====================================" -ForegroundColor Cyan $JARVIS_URL = 'https://jarvis.orbishosting.com'
Write-Host " JARVIS Agent Installer v3.1 " -ForegroundColor Cyan $INSTALL_DIR = 'C:\ProgramData\jarvis-agent'
Write-Host " Windows Service Edition " -ForegroundColor Cyan $SERVICE_NAME = 'JARVISAgent'
Write-Host " ====================================" -ForegroundColor Cyan $AGENT_SCRIPT = "$INSTALL_DIR\jarvis-agent-windows.py"
Write-Host "" $CONFIG_FILE = "$INSTALL_DIR\config.json"
# ── Require admin ────────────────────────────────────────────────────────────── function Write-Step { param($msg) Write-Host "`n[JARVIS] $msg" -ForegroundColor Cyan }
if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole( function Write-OK { param($msg) Write-Host " OK: $msg" -ForegroundColor Green }
[Security.Principal.WindowsBuiltInRole]::Administrator)) { function Write-Fail { param($msg) Write-Host " ERROR: $msg" -ForegroundColor Red; exit 1 }
Write-Error "Run PowerShell as Administrator and try again."
}
# ── Prompt if not provided ───────────────────────────────────────────────────── Write-Host "`n========================================" -ForegroundColor Yellow
$JarvisUrl = $JarvisUrl.TrimEnd("/") Write-Host " JARVIS Agent Installer for Windows" -ForegroundColor Yellow
Write-Host "========================================`n" -ForegroundColor Yellow
# ── Find or install Python 3 (system-wide so LocalSystem service can reach it) # ── Stop existing service if running ────────────────────────────────────────
Write-Host "[1/6] Checking for Python 3..." -ForegroundColor Cyan $existing = Get-Service -Name $SERVICE_NAME -ErrorAction SilentlyContinue
$pythonPath = $null
# System-wide paths — accessible by LocalSystem service account
$systemPaths = @(
"C:\Program Files\Python313\python.exe",
"C:\Program Files\Python312\python.exe",
"C:\Program Files\Python311\python.exe",
"C:\Program Files\Python310\python.exe",
"C:\Program Files\Python39\python.exe",
"C:\Python313\python.exe",
"C:\Python312\python.exe",
"C:\Python311\python.exe",
"C:\Python310\python.exe"
)
function Install-PythonSystemWide {
# Try winget first (Win 10 1709+ / Win 11)
$wingetOk = $false
try {
$null = Get-Command winget -ErrorAction Stop
Write-Host " Using winget (system-wide)..." -NoNewline
winget install Python.Python.3.12 --silent --scope machine `
--accept-package-agreements --accept-source-agreements 2>&1 | Out-Null
if ($LASTEXITCODE -eq 0) { $wingetOk = $true; Write-Host " done." -ForegroundColor Green }
} catch {}
if (-not $wingetOk) {
# Direct download — works on Win 8.1 without winget
# Python 3.11 explicitly supports Win 8.1+
Write-Host " Downloading Python 3.11 (Win 8.1+ compatible)..." -NoNewline
$pyInstaller = "$env:TEMP\python-installer.exe"
$pyUrl = "https://www.python.org/ftp/python/3.11.9/python-3.11.9-amd64.exe"
try {
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
$wc = New-Object System.Net.WebClient
$wc.DownloadFile($pyUrl, $pyInstaller)
Write-Host " downloaded." -ForegroundColor Green
} catch {
Write-Error "Could not download Python. Install from https://python.org choosing 'Install for all users', then re-run."
}
Write-Host " Installing system-wide (silent)..." -NoNewline
$proc = Start-Process -FilePath $pyInstaller `
-ArgumentList "/quiet InstallAllUsers=1 PrependPath=1 Include_test=0" `
-Wait -PassThru
if ($proc.ExitCode -ne 0) {
Write-Error "Python installer exited $($proc.ExitCode). Install manually from https://python.org then re-run."
}
Write-Host " done." -ForegroundColor Green
Remove-Item $pyInstaller -ErrorAction SilentlyContinue
}
# Refresh PATH after install
$env:PATH = [System.Environment]::GetEnvironmentVariable("PATH","Machine") + ";" +
[System.Environment]::GetEnvironmentVariable("PATH","User")
}
# ── Search for system-wide Python first ───────────────────────────────────────
foreach ($p in $systemPaths) {
if (Test-Path $p) {
try {
$ver = & $p --version 2>&1
if ("$ver" -match "Python 3") { $pythonPath = $p; break }
} catch {}
}
}
# ── Fall back to PATH — but flag if it's per-user ─────────────────────────────
if (-not $pythonPath) {
foreach ($cmd in @("python", "python3", "py")) {
try {
$ver = & $cmd --version 2>&1
if ("$ver" -match "Python 3") {
$resolved = (Get-Command $cmd -ErrorAction SilentlyContinue)
if ($resolved) { $pythonPath = $resolved.Source; break }
}
} catch {}
}
}
# ── If Python is per-user (AppData), install system-wide so LocalSystem can use it ──
$needsSystemPython = $false
if ($pythonPath -and ($pythonPath -match "AppData")) {
Write-Host " Found per-user Python: $pythonPath" -ForegroundColor Yellow
Write-Host " LocalSystem service needs system-wide Python. Installing..." -ForegroundColor Yellow
$needsSystemPython = $true
} elseif (-not $pythonPath) {
Write-Host " Python 3 not found. Installing system-wide..." -ForegroundColor Yellow
$needsSystemPython = $true
}
if ($needsSystemPython) {
Install-PythonSystemWide
# Locate the newly installed system-wide Python
$pythonPath = $null
foreach ($p in $systemPaths) {
if (Test-Path $p) {
try {
$ver = & $p --version 2>&1
if ("$ver" -match "Python 3") { $pythonPath = $p; break }
} catch {}
}
}
if (-not $pythonPath) {
Write-Error "System-wide Python not found after install. Open a new Admin PowerShell and re-run."
}
}
Write-Host " Python: $pythonPath" -ForegroundColor Green
# ── Install pywin32 (required for Windows service support) ────────────────────
Write-Host "[2/6] Installing pywin32..." -ForegroundColor Cyan
# pip install
$pipResult = & $pythonPath -m pip install --upgrade pywin32 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Error "pip install pywin32 failed (exit $LASTEXITCODE).`n$pipResult`nTry manually: $pythonPath -m pip install pywin32"
}
# postinstall registers service runner DLLs — non-fatal if it fails
try {
$postResult = & $pythonPath -c "import pywin32_postinstall; pywin32_postinstall.install()" 2>&1
Write-Host " pywin32 installed." -ForegroundColor Green
} catch {
Write-Host " pywin32 installed (postinstall skipped — service should still work)." -ForegroundColor Yellow
}
# ── Create install directory and download agent ────────────────────────────────
Write-Host "[3/6] Downloading agent..." -ForegroundColor Cyan
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
try {
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true }
$wc = New-Object System.Net.WebClient
$wc.Headers.Add("User-Agent", "JARVIS-Installer/3.1")
$wc.DownloadFile("$JarvisUrl/agent/jarvis-agent-windows.py", $AgentScript)
Write-Host " Downloaded to $AgentScript" -ForegroundColor Green
} catch {
Write-Error "Download failed: $_"
}
# ── Write config ───────────────────────────────────────────────────────────────
Write-Host "[4/6] Writing config..." -ForegroundColor Cyan
$agentId = "${AgentName}_windows"
$config = [ordered]@{
jarvis_url = $JarvisUrl
host_header = ""
ssl_verify = $true
registration_key = $Key
agent_type = "windows"
hostname = $AgentName
agent_id = $agentId
poll_interval = 30
heartbeat_every = 10
update_check_hours = 24
watch_services = @("WinDefend", "Spooler")
} | ConvertTo-Json -Depth 3
[System.IO.File]::WriteAllText($ConfigFile, $config, [System.Text.UTF8Encoding]::new($false))
Write-Host " Config: $ConfigFile" -ForegroundColor Green
# ── Remove legacy scheduled task if present ────────────────────────────────────
try {
$oldTask = Get-ScheduledTask -TaskName $OldTaskName -ErrorAction SilentlyContinue
if ($oldTask) {
Stop-ScheduledTask -TaskName $OldTaskName -ErrorAction SilentlyContinue
Unregister-ScheduledTask -TaskName $OldTaskName -Confirm:$false -ErrorAction SilentlyContinue
Write-Host " Removed legacy scheduled task '$OldTaskName'." -ForegroundColor Yellow
}
} catch {}
# ── Register Windows service ───────────────────────────────────────────────────
Write-Host "[5/6] Registering Windows service '$ServiceName'..." -ForegroundColor Cyan
# Stop + remove any existing service first
$existing = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
if ($existing) { if ($existing) {
if ($existing.Status -eq "Running") { Write-Step "Stopping existing JARVIS Agent service..."
Write-Host " Stopping existing service..." -NoNewline if ($existing.Status -eq 'Running') {
& $pythonPath $AgentScript stop 2>&1 | Out-Null Stop-Service -Name $SERVICE_NAME -Force
Start-Sleep -Seconds 3 Start-Sleep 2
Write-Host " stopped." -ForegroundColor Yellow
} }
Write-Host " Removing existing service..." -NoNewline try {
& $pythonPath $AgentScript remove 2>&1 | Out-Null & python "$INSTALL_DIR\jarvis-agent-windows.py" remove 2>$null
Start-Sleep -Seconds 2 } catch {}
Write-Host " removed." -ForegroundColor Yellow Write-OK "Existing service removed."
} }
# Install the service (--startup auto = start at boot) # ── Check / install Python ────────────────────────────────────────────────────
& $pythonPath $AgentScript --startup auto install Write-Step "Checking Python..."
if ($LASTEXITCODE -ne 0) { $py = Get-Command python -ErrorAction SilentlyContinue
Write-Error "Service registration failed (exit $LASTEXITCODE). Check that pywin32 postinstall completed." 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."
} }
# Configure failure recovery: restart after 5s, 10s, 30s # ── Create install dir ────────────────────────────────────────────────────────
sc.exe failure $ServiceName reset= 86400 actions= restart/5000/restart/10000/restart/30000 | Out-Null Write-Step "Creating install directory..."
Write-Host " Service registered with auto-restart on failure." -ForegroundColor Green New-Item -ItemType Directory -Path $INSTALL_DIR -Force | Out-Null
Write-OK $INSTALL_DIR
# ── Start the service ────────────────────────────────────────────────────────── # ── Download agent script ─────────────────────────────────────────────────────
Write-Host "[6/6] Starting service..." -ForegroundColor Cyan Write-Step "Downloading JARVIS agent..."
& $pythonPath $AgentScript start try {
Start-Sleep -Seconds 4 Invoke-WebRequest -Uri "$JARVIS_URL/agent/jarvis-agent-windows.py" -OutFile $AGENT_SCRIPT -UseBasicParsing
Write-OK "Agent downloaded to $AGENT_SCRIPT"
} catch {
Write-Fail "Failed to download agent: $_"
}
$svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue # ── Get registration key ──────────────────────────────────────────────────────
$status = if ($svc) { $svc.Status } else { "NotFound" } $regKey = $env:JARVIS_REG_KEY
$color = if ($status -eq "Running") { "Green" } else { "Yellow" } if (-not $regKey -and (Test-Path $CONFIG_FILE)) {
Write-Host " Service status: $status" -ForegroundColor $color $existingCfg = Get-Content $CONFIG_FILE | ConvertFrom-Json
$regKey = $existingCfg.registration_key
if ($regKey) { Write-OK "Using existing registration key from config." }
}
if (-not $regKey) {
$regKey = Read-Host "`n Enter JARVIS registration key"
if (-not $regKey) { Write-Fail "Registration key required." }
}
Write-Host "" # ── Get hostname ──────────────────────────────────────────────────────────────
Write-Host " ====================================" -ForegroundColor Green $hostname = $env:COMPUTERNAME
Write-Host " Installation complete! " -ForegroundColor Green $customHostname = $env:JARVIS_HOSTNAME
Write-Host " ====================================" -ForegroundColor Green if ($customHostname) { $hostname = $customHostname }
Write-Host ""
Write-Host " Machine : $AgentName ($agentId)" -ForegroundColor White # ── Write config ──────────────────────────────────────────────────────────────
Write-Host " JARVIS : $JarvisUrl" -ForegroundColor White Write-Step "Writing config..."
Write-Host " Python : $pythonPath" -ForegroundColor White $cfg = @{
Write-Host " Logs : $InstallDir\jarvis-agent.log" -ForegroundColor White jarvis_url = $JARVIS_URL
Write-Host "" registration_key = $regKey
Write-Host " Manage the service:" -ForegroundColor Gray hostname = $hostname
Write-Host " Get-Service JARVISAgent" -ForegroundColor Gray agent_type = 'windows'
Write-Host " Start-Service JARVISAgent" -ForegroundColor Gray ssl_verify = $true
Write-Host " Stop-Service JARVISAgent" -ForegroundColor Gray poll_interval = 30
Write-Host " Restart-Service JARVISAgent" -ForegroundColor Gray heartbeat_every = 10
Write-Host " Get-Content '$InstallDir\jarvis-agent.log' -Tail 30 -Wait" -ForegroundColor Gray update_check_hours = 24
Write-Host "" watch_services = @('WinDefend', 'Spooler', 'wuauserv')
Write-Host " To uninstall:" -ForegroundColor Gray } | ConvertTo-Json -Depth 5
Write-Host " Stop-Service JARVISAgent" -ForegroundColor Gray $cfg | Out-File -FilePath $CONFIG_FILE -Encoding utf8
Write-Host " & '$pythonPath' '$AgentScript' remove" -ForegroundColor Gray Write-OK "Config written to $CONFIG_FILE"
Write-Host ""
# ── Install Windows Service ───────────────────────────────────────────────────
Write-Step "Installing Windows service..."
$pyPath = (Get-Command python).Source
& $pyPath "$AGENT_SCRIPT" --startup auto install
if ($LASTEXITCODE -ne 0) { Write-Fail "Service install failed." }
Write-OK "Service '$SERVICE_NAME' installed."
# ── Start service ─────────────────────────────────────────────────────────────
Write-Step "Starting service..."
Start-Service -Name $SERVICE_NAME
Start-Sleep 3
$svc = Get-Service -Name $SERVICE_NAME
if ($svc.Status -ne 'Running') { Write-Fail "Service failed to start. Check C:\ProgramData\jarvis-agent\jarvis-agent.log" }
Write-OK "Service is running."
# ── Test connectivity ─────────────────────────────────────────────────────────
Write-Step "Testing JARVIS connection..."
try {
$ping = Invoke-RestMethod -Uri "$JARVIS_URL/api/ping" -TimeoutSec 10
Write-OK "JARVIS is online: $($ping.codename)"
} catch {
Write-Host " WARNING: Could not reach JARVIS at $JARVIS_URL - check connectivity." -ForegroundColor Yellow
}
Write-Host "`n========================================" -ForegroundColor Green
Write-Host " JARVIS Agent installed successfully!" -ForegroundColor Green
Write-Host " Hostname: $hostname" -ForegroundColor Green
Write-Host " Service: $SERVICE_NAME (auto-start at boot)" -ForegroundColor Green
Write-Host " Logs: C:\ProgramData\jarvis-agent\jarvis-agent.log" -ForegroundColor Green
Write-Host "========================================`n" -ForegroundColor Green
+4 -4
View File
@@ -9,8 +9,8 @@ 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" JARVIS_URL="${JARVIS_URL:-https://jarvis.orbishosting.com}"
JARVIS_HOST="jarvis.orbishosting.com" 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"
@@ -38,7 +38,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,7 +50,7 @@ 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",
+56 -6
View File
@@ -28,11 +28,45 @@ AGENT_VERSION = "3.1"
# ── Config helpers ──────────────────────────────────────────────────────────── # ── Config helpers ────────────────────────────────────────────────────────────
def load_config() -> dict: def load_config() -> dict:
legacy_path = "/opt/jarvis-agent/config.json"
if not os.path.exists(CONFIG_PATH): if not os.path.exists(CONFIG_PATH):
print(f"[ERROR] Config not found at {CONFIG_PATH}. Run the installer first.", flush=True) if os.path.exists(legacy_path):
sys.exit(1) print(f"[JARVIS] Config found at legacy path {legacy_path} - migrating...", flush=True)
with open(CONFIG_PATH) as f: Path(CONFIG_PATH).parent.mkdir(parents=True, exist_ok=True)
return json.load(f) with open(legacy_path) as f:
cfg = json.load(f)
else:
print(f"[ERROR] Config not found at {CONFIG_PATH}. Run the installer first.", flush=True)
sys.exit(1)
else:
with open(CONFIG_PATH) as f:
cfg = json.load(f)
# Migrate old key names so the agent self-heals instead of crash-looping
import re as _re
changed = False
if "server_url" in cfg and "jarvis_url" not in cfg:
cfg["jarvis_url"] = cfg.pop("server_url")
print("[JARVIS] Config migrated: server_url -> jarvis_url", flush=True)
changed = True
if "api_key" in cfg and "registration_key" not in cfg:
cfg["registration_key"] = cfg.pop("api_key")
print("[JARVIS] Config migrated: api_key -> registration_key", flush=True)
changed = True
if "hostname" not in cfg:
cfg["hostname"] = socket.gethostname()
changed = True
if "ssl_verify" not in cfg:
cfg["ssl_verify"] = not bool(_re.match(r"https?://\d+\.\d+\.\d+\.\d+", cfg.get("jarvis_url", "")))
changed = True
if changed:
with open(CONFIG_PATH, "w") as f:
json.dump(cfg, f, indent=2)
print("[JARVIS] Config saved after migration.", flush=True)
return cfg
def load_state() -> dict: def load_state() -> dict:
if os.path.exists(STATE_PATH): if os.path.exists(STATE_PATH):
@@ -265,11 +299,23 @@ def get_load() -> list:
except Exception: except Exception:
return [0, 0, 0] return [0, 0, 0]
def get_nordvpn_status() -> dict | None:
"""Check nordlynx WireGuard interface. Returns None if nordlynx not present on this host."""
try:
r = subprocess.run(["ip", "link", "show", "nordlynx"],
capture_output=True, text=True, timeout=3)
if r.returncode != 0:
return None
active = "UP,LOWER_UP" in r.stdout or "state UP" in r.stdout
return {"active": active, "interface": "nordlynx"}
except Exception:
return None
def collect_metrics(cfg: dict) -> dict: def collect_metrics(cfg: dict) -> dict:
# First reading for CPU delta # First reading for CPU delta
get_cpu_percent() get_cpu_percent()
time.sleep(1) time.sleep(1)
return { metrics = {
"hostname": cfg.get("hostname", socket.gethostname()), "hostname": cfg.get("hostname", socket.gethostname()),
"cpu_percent": get_cpu_percent(), "cpu_percent": get_cpu_percent(),
"memory": get_memory(), "memory": get_memory(),
@@ -280,6 +326,10 @@ def collect_metrics(cfg: dict) -> dict:
"platform": platform.system(), "platform": platform.system(),
"timestamp": datetime.utcnow().isoformat() + "Z", "timestamp": datetime.utcnow().isoformat() + "Z",
} }
nordvpn = get_nordvpn_status()
if nordvpn is not None:
metrics["nordvpn"] = nordvpn
return metrics
# ── Proxmox metrics ─────────────────────────────────────────────────────────── # ── Proxmox metrics ───────────────────────────────────────────────────────────
@@ -384,7 +434,7 @@ def main():
try: try:
# Heartbeat + get commands # Heartbeat + get commands
hb = api_post(f"{jarvis_url}/api/agent/heartbeat", {}, headers, ssl_verify=ssl_verify) hb = api_post(f"{jarvis_url}/api/agent/heartbeat", {"version": AGENT_VERSION}, headers, ssl_verify=ssl_verify)
if "error" in hb: if "error" in hb:
print(f"[WARN] Heartbeat failed: {hb['error']}", flush=True) print(f"[WARN] Heartbeat failed: {hb['error']}", flush=True)
else: else:
+1 -1
View File
@@ -1 +1 @@
1a9e8e24e5aee8f27a5900b6340373023ff2171e844e71e451eecdbf3b2b0f03 jarvis-agent.py 6ba92a1ad4f91a218cbc4ce6834c55e8f56a0e22fca04278d77260958e429d5b
+236
View File
@@ -0,0 +1,236 @@
#!/usr/bin/env python3
"""
JARVIS HA Poller pulls entity states from Home Assistant REST API
and pushes them to JARVIS as a homeassistant-type agent.
Runs on VM211 as a systemd service (jarvis-ha-poller).
Config: /etc/jarvis-agent/ha-poller.json
"""
import json
import os
import socket
import sys
import time
import urllib.request
import urllib.error
import ssl
from datetime import datetime, timezone
from pathlib import Path
CONFIG_PATH = "/etc/jarvis-agent/ha-poller.json"
STATE_PATH = "/var/lib/jarvis-agent/ha-poller-state.json"
AGENT_VERSION = "1.0"
AGENT_ID = "homeassistant_ha"
HOSTNAME = "homeassistant"
# Domains to skip — don't send to JARVIS (saves DB space, keeps UI clean)
SKIP_DOMAINS = {
'sensor', 'binary_sensor', 'button', 'update', 'select', 'number',
'device_tracker', 'event', 'image', 'person', 'zone', 'tts',
'conversation', 'assist_satellite', 'input_button', 'media_player',
'scene', 'water_heater', 'alarm_control_panel', 'automation',
'script', 'calendar', 'notify', 'weather', 'sun', 'persistent_notification',
'tag', 'system_health', 'timer', 'counter',
'camera', 'siren', 'remote', 'todo', 'lawn_mower',
}
def log(msg: str):
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(f"[{ts}] {msg}", flush=True)
def load_config() -> dict:
if not os.path.exists(CONFIG_PATH):
print(f"[ERROR] Config not found at {CONFIG_PATH}", flush=True)
sys.exit(1)
with open(CONFIG_PATH) as f:
return json.load(f)
def load_state() -> dict:
if os.path.exists(STATE_PATH):
with open(STATE_PATH) as f:
return json.load(f)
return {}
def save_state(state: dict):
Path(STATE_PATH).parent.mkdir(parents=True, exist_ok=True)
with open(STATE_PATH, "w") as f:
json.dump(state, f, indent=2)
def _ssl_ctx(verify: bool):
if not verify:
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
return ctx
return None
def jarvis_post(url: str, payload: dict, headers: dict, ssl_verify: bool, timeout: int = 15) -> dict:
body = json.dumps(payload).encode()
req = urllib.request.Request(url, data=body, method="POST")
req.add_header("Content-Type", "application/json")
for k, v in headers.items():
req.add_header(k, v)
try:
ctx = _ssl_ctx(ssl_verify)
with urllib.request.urlopen(req, timeout=timeout, context=ctx) as resp:
return json.loads(resp.read().decode())
except urllib.error.HTTPError as e:
return {"error": f"HTTP {e.code}: {e.read().decode()[:200]}"}
except Exception as e:
return {"error": str(e)}
def ha_get(url: str, token: str, timeout: int = 15) -> dict | list | None:
req = urllib.request.Request(url)
req.add_header("Authorization", f"Bearer {token}")
req.add_header("Content-Type", "application/json")
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
return json.loads(resp.read().decode())
except Exception as e:
log(f"HA API error: {e}")
return None
def register(cfg: dict, state: dict) -> str:
jarvis_url = cfg["jarvis_url"].rstrip("/")
ssl_verify = bool(cfg.get("ssl_verify", False))
reg_key = cfg["registration_key"]
log(f"Registering HA poller with JARVIS at {jarvis_url}...")
result = jarvis_post(
f"{jarvis_url}/api/agent/register",
{
"hostname": HOSTNAME,
"version": AGENT_VERSION,
"agent_type": "homeassistant",
"ip_address": cfg.get("ha_url", "").split("//")[-1].split(":")[0],
"capabilities": ["ha_entities", "ha_state"],
"agent_id": AGENT_ID,
},
{"X-Registration-Key": reg_key},
ssl_verify,
)
if "error" in result:
log(f"Registration failed: {result['error']}")
return ""
api_key = result.get("api_key", "")
if api_key:
state["api_key"] = api_key
state["agent_id"] = AGENT_ID
save_state(state)
log(f"Registered. agent_id={AGENT_ID}")
return api_key
def push_entities(cfg: dict, api_key: str, entities: list) -> bool:
jarvis_url = cfg["jarvis_url"].rstrip("/")
ssl_verify = bool(cfg.get("ssl_verify", False))
headers = {"X-Agent-Key": api_key}
# Send in batches of 200
batch_size = 200
total = len(entities)
ok = True
for i in range(0, total, batch_size):
batch = entities[i:i+batch_size]
result = jarvis_post(
f"{jarvis_url}/api/agent/ha_state",
{"entities": batch},
headers,
ssl_verify,
)
if "error" in result:
log(f"Push batch {i//batch_size+1} failed: {result['error']}")
ok = False
return ok
def heartbeat(cfg: dict, api_key: str) -> bool:
jarvis_url = cfg["jarvis_url"].rstrip("/")
ssl_verify = bool(cfg.get("ssl_verify", False))
result = jarvis_post(
f"{jarvis_url}/api/agent/heartbeat",
{"version": AGENT_VERSION},
{"X-Agent-Key": api_key},
ssl_verify,
timeout=10,
)
return "error" not in result
def fetch_ha_states(cfg: dict) -> list:
ha_url = cfg["ha_url"].rstrip("/")
token = cfg["ha_token"]
states = ha_get(f"{ha_url}/api/states", token)
if not states or not isinstance(states, list):
return []
entities = []
for s in states:
entity_id = s.get("entity_id", "")
domain = entity_id.split(".")[0] if "." in entity_id else ""
if domain in SKIP_DOMAINS:
continue
attrs = s.get("attributes", {})
# Convert ISO 8601 (e.g. "2026-06-28T21:26:01.922366+00:00") to MySQL datetime
lc = s.get("last_changed", "")
try:
dt = datetime.fromisoformat(lc.replace("Z", "+00:00"))
lc = dt.astimezone(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
except Exception:
lc = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
entities.append({
"entity_id": entity_id,
"name": attrs.get("friendly_name") or entity_id,
"state": s.get("state", ""),
"attributes": attrs,
"last_changed": lc,
})
return entities
def main():
cfg = load_config()
state = load_state()
poll_interval = int(cfg.get("poll_interval", 30))
heartbeat_every = int(cfg.get("heartbeat_every", 10))
api_key = state.get("api_key", "")
if not api_key:
api_key = register(cfg, state)
if not api_key:
log("Could not register. Retrying in 60s...")
time.sleep(60)
main()
return
headers = {"X-Agent-Key": api_key}
last_push = 0
log(f"HA Poller v{AGENT_VERSION} running. Polling HA every {poll_interval}s, heartbeat every {heartbeat_every}s.")
while True:
now = time.time()
# Heartbeat
if not heartbeat(cfg, api_key):
log("Heartbeat failed (401?) — re-registering...")
state.clear()
save_state(state)
api_key = register(cfg, state)
if not api_key:
time.sleep(60)
continue
# Push entity states every poll_interval
if now - last_push >= poll_interval:
entities = fetch_ha_states(cfg)
if entities:
ok = push_entities(cfg, api_key, entities)
if ok:
log(f"Pushed {len(entities)} HA entities to JARVIS.")
last_push = now
else:
log("No HA entities fetched (HA down or token invalid?)")
time.sleep(heartbeat_every)
if __name__ == "__main__":
main()
+171
View File
@@ -1208,3 +1208,174 @@ body::after{
/* ── AGENT TOPOLOGY ────────────────────────────────────────────────── */ /* ── AGENT TOPOLOGY ────────────────────────────────────────────────── */
#agentTopoCanvas{background:transparent;border-top:1px solid rgba(0,212,255,0.08);display:block} #agentTopoCanvas{background:transparent;border-top:1px solid rgba(0,212,255,0.08);display:block}
#agent-topo-btn.active{background:rgba(0,212,255,0.15);border-color:rgba(0,212,255,0.5)} #agent-topo-btn.active{background:rgba(0,212,255,0.15);border-color:rgba(0,212,255,0.5)}
/*
FIRE HD 8 (12th Gen) TABLET MODE
Applied via body.tablet-mode set automatically on Silk UA detection
Target: 1280×800 landscape, 189 PPI, touch-only input
*/
/* Prevent accidental text selection on touch; restore for inputs */
body.tablet-mode { -webkit-user-select:none; user-select:none; }
body.tablet-mode input,
body.tablet-mode textarea { -webkit-user-select:auto; user-select:auto; }
/* ── TOPBAR — taller row, bigger tap zones ─────────────────────── */
body.tablet-mode #topBar {
height:54px;
padding:0 12px;
}
body.tablet-mode #clock { font-size:1.1rem; letter-spacing:3px; }
body.tablet-mode .tb-logo { font-size:0.95rem; }
/* Toolbar buttons — min 40px touch target */
body.tablet-mode .btn-panels,
body.tablet-mode .btn-camera {
font-size:0.62rem;
letter-spacing:1.5px;
padding:9px 13px;
min-height:40px;
margin-right:4px;
}
/* Theme color dots — bigger tap area */
body.tablet-mode #themeBar { gap:6px; }
body.tablet-mode .theme-btn {
width:20px; height:20px;
font-size:0.75rem;
}
/* Swap + logout */
body.tablet-mode #btn-swap-panels {
font-size:0.65rem;
padding:7px 11px;
}
body.tablet-mode .btn-logout {
font-size:0.72rem;
padding:7px 12px;
}
/* ── MAIN LAYOUT — narrower side panels → wider center ──────────── */
/* 220+220 side cols → center gets ~808px instead of ~660px */
body.tablet-mode #mainLayout {
grid-template-columns:220px 1fr 220px;
padding:8px;
gap:8px;
}
/* ── PANELS — tighter padding, larger text ──────────────────────── */
body.tablet-mode .panel { padding:11px; }
body.tablet-mode .panel-title {
font-size:0.67rem;
letter-spacing:2.5px;
margin-bottom:9px;
}
/* Metric rows */
body.tablet-mode .metric-label { font-size:0.75rem; }
body.tablet-mode .service-row { font-size:0.75rem; padding:6px 0; }
body.tablet-mode .val-row { font-size:0.75rem; padding:4px 0; }
body.tablet-mode .device-item { font-size:0.75rem; padding:7px 0; }
body.tablet-mode .device-name { font-size:0.75rem; }
body.tablet-mode .device-ip { font-size:0.68rem; }
body.tablet-mode .vm-card { font-size:0.75rem; padding:9px 10px; }
/* Scrollable side panels — smooth touch inertia */
body.tablet-mode #leftPanel,
body.tablet-mode #rightPanel {
-webkit-overflow-scrolling:touch;
overscroll-behavior:contain;
}
/* ── CENTER — arc reactor + chat ────────────────────────────────── */
/* Scale reactor down so chat gets more vertical room */
body.tablet-mode #arcReactor { width:180px; height:180px; }
body.tablet-mode .arc-ring.r1 { width:180px; height:180px; }
body.tablet-mode .arc-ring.r2 { width:159px; height:159px; }
body.tablet-mode .arc-ring.r3 { width:139px; height:139px; }
body.tablet-mode .arc-ring.r4 { width:118px; height:118px; }
body.tablet-mode .arc-ring.r5 { width:94px; height:94px; }
body.tablet-mode .arc-ring.r6 { width:72px; height:72px; }
body.tablet-mode .arc-ring.r7 { width:51px; height:51px; }
body.tablet-mode .arc-core { width:30px; height:30px; }
/* Chat messages — comfortable reading size */
body.tablet-mode .msg {
font-size:0.95rem;
line-height:1.55;
padding:11px 14px;
}
body.tablet-mode .msg.user { font-size:0.88rem; }
body.tablet-mode .msg.system { font-size:0.78rem; }
/* Touch-scroll chat log */
body.tablet-mode #chatLog {
-webkit-overflow-scrolling:touch;
overscroll-behavior:contain;
}
/* Input row — 16px prevents Silk from zooming on focus */
body.tablet-mode #textInput {
font-size:1rem;
min-height:46px;
padding:12px 14px;
}
body.tablet-mode #sendBtn {
font-size:0.68rem;
min-height:46px;
padding:0 18px;
}
body.tablet-mode #micBtn {
width:52px; height:52px;
flex-shrink:0;
}
body.tablet-mode #searchBtn {
min-height:46px !important;
padding:0 13px !important;
font-size:1.1rem !important;
}
/* ── TABS — bigger tap targets ──────────────────────────────────── */
body.tablet-mode .tab {
font-size:0.58rem;
letter-spacing:1.5px;
padding:9px 12px;
}
/* ── HA TABLE — more readable on 8" ────────────────────────────── */
body.tablet-mode .ha-thead th { font-size:0.55rem; padding:6px 3px 8px; }
body.tablet-mode .ha-row td { font-size:0.74rem; padding:6px 3px; }
/* Toggle slider — bigger for fat fingers */
body.tablet-mode .ha-toggle { width:36px; height:18px; }
body.tablet-mode .ha-slider::before { width:12px; height:12px; left:2px; top:2px; }
body.tablet-mode .ha-toggle input:checked + .ha-slider::before { transform:translateX(18px); }
/* ── DISABLE HOVER-RISE — not meaningful on touch ───────────────── */
body.tablet-mode .panel:hover {
transform:translateY(var(--pty,0px)) !important;
border-color:var(--panel-border) !important;
box-shadow:none !important;
transition:none !important;
}
/* ── ALERTS ──────────────────────────────────────────────────────── */
body.tablet-mode .alert-item { font-size:0.75rem; padding:9px 11px; }
/* ── BOTTOM BAR ─────────────────────────────────────────────────── */
body.tablet-mode #bottomBar { font-size:0.7rem; height:34px; }
/*
KIOSK MODE hide noisy panels, keep it clean on Fire tablet
Only active when body.kiosk-mode (fullscreen)
*/
body.kiosk-mode #server-panel { display:none !important; }
body.kiosk-mode #network-status-panel { display:none !important; }
body.kiosk-mode #tab-btn-agents { display:none !important; }
body.kiosk-mode #tab-btn-guardian { display:none !important; }
body.kiosk-mode #tab-agents { display:none !important; }
body.kiosk-mode #tab-guardian { display:none !important; }
body.kiosk-mode #bb-ha-item { display:none !important; }
body.kiosk-mode #bb-agents-item { display:none !important; }
body.kiosk-mode #bb-memory-item { display:none !important; }
body.kiosk-mode #bb-pve-item { display:none !important; }
+81 -1
View File
@@ -397,7 +397,7 @@ let _refreshTick = 0;
let selectedContext = null; let selectedContext = null;
const _panelCtx = {}; const _panelCtx = {};
let _haEntities = {}; let _haEntities = {};
const _svcLabels = {lshttpd:'WEB',mysql:'MYSQL',redis:'REDIS',memcached:'MEMCACHE',postfix:'POSTFIX',dovecot:'DOVECOT','jarvis-agent':'AGENT'}; const _svcLabels = {nginx:'WEB','php8.3-fpm':'PHP',mariadb:'DB','redis-server':'REDIS','jarvis-arc':'ARC','jarvis-agent':'AGENT'};
async function refreshAll() { async function refreshAll() {
_refreshTick++; _refreshTick++;
@@ -550,6 +550,21 @@ function renderDO(d) {
</div>`; </div>`;
}).join(''); }).join('');
} }
// WEB HOST (DO server agent metrics)
const ds = d.do_server || {};
const doStatus = document.getElementById('do-host-status');
const doCpu = document.getElementById('do-cpu');
const doMem = document.getElementById('do-mem');
const doDisk = document.getElementById('do-disk');
if (ds.online) {
if (doStatus) { doStatus.textContent = '●'; doStatus.style.color = 'var(--green)'; }
if (doCpu) doCpu.textContent = (ds.cpu || 0) + '%';
if (doMem) doMem.textContent = (ds.mem || 0) + '%';
if (doDisk) doDisk.textContent = (ds.disk || 0) + '%';
} else {
if (doStatus) { doStatus.textContent = '○'; doStatus.style.color = 'var(--red)'; }
}
} }
async function loadNetwork() { async function loadNetwork() {
@@ -1390,6 +1405,7 @@ function enterVoiceMode(source) {
} }
function exitVoiceMode() { function exitVoiceMode() {
if (document.body.classList.contains('kiosk-mode')) return;
voiceMode = false; voiceMode = false;
voiceMuted = false; voiceMuted = false;
updateMicBtn(); updateMicBtn();
@@ -1739,3 +1755,67 @@ document.addEventListener('keydown', function(e) {
}); });
} }
}); });
// ── FIRE HD 8 TABLET DETECTION ────────────────────────────────────────────────────────
const IS_SILK = /Silk\//i.test(navigator.userAgent);
const IS_FIRE = /KFTT|KFOT|KFJWI|KFSOWI|KFTHWI|KFTHWA|KFAPWI|KFAPWA|KFARWI|KFASWI|KFMEWI|KFFOWI|KFSAWA|KFMAWI|KFGIWI|KFDOWI|KFTBWI|KFTRWI|KFKAWI/i.test(navigator.userAgent);
function isTablet() { return IS_SILK || IS_FIRE; }
function applyTabletMode() {
document.body.classList.add("tablet-mode");
const kb = document.getElementById("kioskBtn");
if (kb) kb.title = "Full-screen kiosk (Fire HD 8 layout active)";
}
if (isTablet()) applyTabletMode();
// ── KIOSK MODE ────────────────────────────────────────────────────────────────────────
let _wakeLock = null;
async function toggleKiosk() {
const btn = document.getElementById("kioskBtn");
const isFs = !!(document.fullscreenElement || document.webkitFullscreenElement);
if (!isFs) {
applyTabletMode();
const el = document.documentElement;
const req = el.requestFullscreen || el.webkitRequestFullscreen || el.mozRequestFullScreen || el.msRequestFullscreen;
if (req) req.call(el).catch(() => {});
if ("wakeLock" in navigator) {
try { _wakeLock = await navigator.wakeLock.request("screen"); } catch(e) {}
}
document.body.classList.add("kiosk-mode");
// Kiosk: silently activate mic + voice mode (no TTS greeting)
if (typeof wakeFromSleep === "function" && isAsleep) wakeFromSleep();
voiceMode = true; voiceMuted = false; voiceLastCmd = Date.now(); updateMicBtn();
if (typeof startListening === "function" && !isListening) startListening();
if (btn) { btn.textContent = "⧞ EXIT"; btn.style.color = "var(--cyan)"; }
} else {
const ex = document.exitFullscreen || document.webkitExitFullscreen || document.mozCancelFullScreen || document.msExitFullscreen;
if (ex) ex.call(document).catch(() => {});
if (_wakeLock) { _wakeLock.release().catch(() => {}); _wakeLock = null; }
document.body.classList.remove("kiosk-mode");
if (typeof stopListening === "function") stopListening();
if (btn) { btn.textContent = "⧞ KIOSK"; btn.style.color = ""; }
if (!isTablet()) document.body.classList.remove("tablet-mode");
}
}
document.addEventListener("visibilitychange", async () => {
if (_wakeLock && document.visibilityState === "visible") {
try { _wakeLock = await navigator.wakeLock.request("screen"); } catch(e) {}
}
});
function _onFsChange() {
const btn = document.getElementById("kioskBtn");
if (!document.fullscreenElement && !document.webkitFullscreenElement) {
if (_wakeLock) { _wakeLock.release().catch(() => {}); _wakeLock = null; }
document.body.classList.remove("kiosk-mode");
if (typeof stopListening === "function") stopListening();
if (btn) { btn.textContent = "⧞ KIOSK"; btn.style.color = ""; }
if (!isTablet()) document.body.classList.remove("tablet-mode");
}
}
document.addEventListener("fullscreenchange", _onFsChange);
document.addEventListener("webkitfullscreenchange", _onFsChange);
+2 -1
View File
@@ -5,6 +5,7 @@ var _sleepRefreshTimer = null;
var SLEEP_CMDS = /\b(good\s*night(\s*jarvis)?|go\s*to\s*sleep|sleep\s*mode|shut\s*(down|off)\s*(jarvis|for\s*the\s*night)|go\s*offline|going\s*offline|jarvis\s*(go\s*)?(offline|sleep|shutdown)|stand\s*by\s*mode|power\s*down(\s*jarvis)?|signing\s*off)\b/i; var SLEEP_CMDS = /\b(good\s*night(\s*jarvis)?|go\s*to\s*sleep|sleep\s*mode|shut\s*(down|off)\s*(jarvis|for\s*the\s*night)|go\s*offline|going\s*offline|jarvis\s*(go\s*)?(offline|sleep|shutdown)|stand\s*by\s*mode|power\s*down(\s*jarvis)?|signing\s*off)\b/i;
function enterSleepMode() { function enterSleepMode() {
if (document.body.classList.contains("kiosk-mode")) return;
if (isAsleep) return; if (isAsleep) return;
isAsleep = true; isAsleep = true;
@@ -141,7 +142,7 @@ function closeNetMap(){
function _nmBuild(devices){ function _nmBuild(devices){
_nmNodes=[]; _nmEdges=[]; _nmParticles=[]; _nmNodes=[]; _nmEdges=[]; _nmParticles=[];
// Hub // Hub
_nmNodes.push({id:'jarvis',label:'JARVIS',sub:'165.22.1.228',online:true,agent:true,ringIdx:0,angle:0,r:NM_RINGS[0].nodeR,pulse:0}); _nmNodes.push({id:'jarvis',label:'JARVIS',sub:'10.48.200.211',online:true,agent:true,ringIdx:0,angle:0,r:NM_RINGS[0].nodeR,pulse:0});
// Bucket // Bucket
var buckets={proxmox:[],services:[],agents:[],devices:[],network:[]}; var buckets={proxmox:[],services:[],agents:[],devices:[],network:[]};
// Deduplicate agent devices by hostname (same logical host registered twice) // Deduplicate agent devices by hostname (same logical host registered twice)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,715 @@
// ── MISSION OPS HUD ───────────────────────────────────────────────────────────
let _missionsOpenCards = new Set();
async function loadMissionsHud() {
const el = document.getElementById('missions-hud');
if (!el) return;
try {
const missions = await api('arc?action=missions');
const list = Array.isArray(missions) ? missions : [];
let html = '<button class="mission-new-btn" onclick="window.open(\'/admin#missions\',\'_blank\')">◈ MANAGE MISSIONS IN ADMIN</button>';
if (!list.length) {
html += '<div class="comms-empty">◈ NO MISSIONS<br><span style="opacity:0.5">Create workflows in Admin → Mission Ops</span></div>';
el.innerHTML = html;
return;
}
const trigIcons = {manual:'🖐', schedule:'⏱', guardian_event:'🛡', email_keyword:'📧'};
for (const m of list) {
const isOpen = _missionsOpenCards.has(m.id);
const icon = trigIcons[m.trigger_type] || '◈';
const enabled = m.enabled;
const lastRun = m.last_run_at ? new Date(m.last_run_at+'Z').toLocaleTimeString() : 'never';
html += `<div class="mission-card${isOpen?' open':''}" id="mission-card-${m.id}">
<div class="mission-card-head" onclick="toggleMissionCard(${m.id})">
<span style="opacity:${enabled?1:0.35}">${icon}</span>
<span class="mission-card-name" style="opacity:${enabled?1:0.45}">${escHtml(m.name)}</span>
<span class="mission-card-trigger">${m.trigger_type.replace('_',' ').toUpperCase()}</span>
<span style="font-family:var(--font-mono);font-size:0.48rem;color:var(--text-dim)">${m.run_count||0} runs</span>
</div>
<div class="mission-card-body">
${m.description ? `<div style="font-size:0.58rem;color:var(--text-dim);margin:6px 0">${escHtml(m.description)}</div>` : ''}
<div style="font-family:var(--font-mono);font-size:0.52rem;color:var(--text-dim);margin:4px 0">Last run: ${lastRun} · ${m.run_count||0} total runs</div>
<div class="mission-run-bar">
<button class="mission-run-btn" id="mission-run-btn-${m.id}" onclick="hudRunMission(${m.id})"${!enabled?' disabled title="Mission disabled"':''}> RUN NOW</button>
</div>
<div id="mission-run-result-${m.id}" style="font-family:var(--font-mono);font-size:0.52rem;margin-top:6px;min-height:12px"></div>
</div>
</div>`;
}
el.innerHTML = html;
} catch(e) {
if (el) el.innerHTML = '<div class="comms-empty">MISSIONS OFFLINE</div>';
}
}
function toggleMissionCard(id) {
const card = document.getElementById('mission-card-' + id);
if (!card) return;
if (_missionsOpenCards.has(id)) _missionsOpenCards.delete(id);
else _missionsOpenCards.add(id);
card.classList.toggle('open');
}
async function hudRunMission(id) {
const btn = document.getElementById('mission-run-btn-' + id);
const res = document.getElementById('mission-run-result-' + id);
if (btn) { btn.disabled = true; btn.textContent = '◈ RUNNING…'; }
if (res) res.textContent = '';
try {
const data = await api('arc?action=mission_run&id=' + id, 'POST', {trigger_source: 'hud'});
const s = data.status || 'done';
const color = s === 'done' ? '#00ff88' : s === 'failed' ? '#ff2244' : '#ffd700';
if (res) res.style.color = color;
if (res) res.textContent = `${s.toUpperCase()} — Run #${data.run_id||'?'} · ${data.steps||0} steps completed`;
if (btn) { btn.disabled = false; btn.textContent = '▶ RUN NOW'; }
setTimeout(loadMissionsHud, 2000);
} catch(e) {
if (btn) { btn.disabled = false; btn.textContent = '▶ RUN NOW'; }
if (res) res.textContent = '✗ Run failed';
}
}
// ── DIRECTIVES HUD ────────────────────────────────────────────────────────────
let _dirOpenCards = new Set();
async function loadDirectivesHud() {
const el = document.getElementById('directives-hud');
if (!el) return;
try {
const d = await api('directives/list?status=active');
const list = (d.directives || []);
let html = '<button class="dir-admin-btn" onclick="window.open(\'/admin#directives\',\'_blank\')">◈ MANAGE IN ADMIN</button>';
if (!list.length) {
html += '<div class="comms-empty">◈ NO ACTIVE DIRECTIVES<br><span style="opacity:0.5">Create objectives in Admin → Directives</span></div>';
el.innerHTML = html;
return;
}
const catColors = {work:'var(--cyan)',personal:'#a78bfa',health:'#00ff88',finance:'#ffd700',home:'var(--panel-border)',other:'var(--text-dim)'};
for (const dir of list) {
const pct = Math.min(100, Math.round(dir.progress || 0));
const isOpen = _dirOpenCards.has(dir.id);
const color = catColors[dir.category] || 'var(--cyan)';
const fillColor = pct >= 80 ? '#00ff88' : pct >= 40 ? '#ffd700' : '#ff6644';
const daysLeft = dir.target_date
? Math.ceil((new Date(dir.target_date) - new Date()) / 86400000) : null;
const dueTxt = daysLeft !== null
? (daysLeft < 0 ? `OVERDUE ${Math.abs(daysLeft)}d` : `${daysLeft}d left`)
: '';
const dueColor = daysLeft !== null && daysLeft < 0 ? '#ff2244' : daysLeft < 14 ? '#ffd700' : 'var(--text-dim)';
html += `<div class="dir-card${isOpen?' open':''}" id="dir-card-${dir.id}">
<div class="dir-card-head" onclick="toggleDirCard(${dir.id})">
<span style="font-family:var(--font-mono);font-size:0.55rem;color:${color};flex-shrink:0">${dir.category.toUpperCase()}</span>
<span class="dir-card-title" style="color:${color}">${escHtml(dir.title)}</span>
<span style="font-family:var(--font-mono);font-size:0.55rem;color:${fillColor};flex-shrink:0">${pct}%</span>
${dueTxt ? `<span style="font-family:var(--font-mono);font-size:0.48rem;color:${dueColor};flex-shrink:0">${dueTxt}</span>` : ''}
</div>
<div class="dir-card-body">
<div class="dir-progress-bar"><div class="dir-progress-fill" style="width:${pct}%;background:${fillColor}"></div></div>
<div style="font-family:var(--font-mono);font-size:0.5rem;color:var(--text-dim);margin-bottom:6px">${dir.kr_count||0} KEY RESULTS · ${dir.link_count||0} LINKED ITEMS</div>
<button onclick="hudDirectiveReview(${dir.id})" style="background:rgba(0,212,255,0.06);border:1px solid rgba(0,212,255,0.2);border-radius:3px;padding:3px 8px;color:var(--cyan);font-family:var(--font-display);font-size:0.48rem;letter-spacing:1px;cursor:pointer"> AI REVIEW</button>
</div>
</div>`;
}
el.innerHTML = html;
} catch(e) {
if (el) el.innerHTML = '<div class="comms-empty">DIRECTIVES OFFLINE</div>';
}
}
function toggleDirCard(id) {
const card = document.getElementById('dir-card-' + id);
if (!card) return;
if (_dirOpenCards.has(id)) _dirOpenCards.delete(id);
else _dirOpenCards.add(id);
card.classList.toggle('open');
}
async function hudDirectiveReview(id) {
const res = await api('arc?action=job_create', 'POST', {
type: 'directive_review', payload: {directive_id: id, provider: 'claude'}, priority: 6,
});
if (res.job_id) {
addMessage('jarvis', `◈ DIRECTIVE REVIEW initiated (Job #${res.job_id}). Analyzing objectives and key results now. Results will appear here shortly.`);
speak(`Directive review underway. I'll brief you on your progress in a moment.`);
}
}
// ── MEMORY CORE — bottom bar count ────────────────────────────────────────────
async function updateMemoryCount() {
try {
const stats = await api('memory?action=stats');
const el = document.getElementById('bb-memory-count');
const dot = document.getElementById('bb-memory-dot');
if (el && stats) {
const total = stats.total || 0;
el.textContent = total + ' FACTS';
if (dot) dot.style.background = total > 0 ? 'var(--cyan)' : 'rgba(0,212,255,0.3)';
}
} catch(e) {}
}
// ── CLEARANCE PROTOCOL HUD ─────────────────────────────────────────────────────
const _clrOpenCards = new Set();
async function updateClearanceBanner() {
try {
const pending = await api('arc?action=clearance_pending');
const list = Array.isArray(pending) ? pending : [];
const count = list.length;
const banner = document.getElementById('clearance-banner');
const badge = document.getElementById('clr-tab-badge');
const bcount = document.getElementById('clr-banner-count');
if (banner) {
if (count > 0) {
banner.classList.add('active');
if (bcount) bcount.textContent = count;
} else {
banner.classList.remove('active');
}
}
if (badge) {
if (count > 0) { badge.style.display = 'inline'; badge.textContent = count; }
else badge.style.display = 'none';
}
} catch(e) {}
}
async function loadClearanceHud() {
const el = document.getElementById('clearance-hud');
if (!el) return;
try {
const [pendingRes, rulesRes, historyRes] = await Promise.all([
api('arc?action=clearance_pending'),
api('arc?action=clearance_rules'),
api('arc?action=clearance_history&limit=20')
]);
const pending = Array.isArray(pendingRes) ? pendingRes : [];
const rules = Array.isArray(rulesRes) ? rulesRes : [];
const history = Array.isArray(historyRes) ? historyRes : [];
let html = '<button class="clr-admin-btn" onclick="window.open(\'/admin#clearance\',\'_blank\')">◈ MANAGE CLEARANCE RULES IN ADMIN</button>';
// Pending requests
html += `<div style="font-family:var(--font-display);font-size:0.55rem;letter-spacing:2px;color:#ff6680;margin:8px 0 4px">PENDING AUTHORIZATION (${pending.length})</div>`;
if (!pending.length) {
html += '<div class="comms-empty" style="color:rgba(255,255,255,0.3);margin-bottom:10px">◈ NO PENDING CLEARANCE REQUESTS</div>';
} else {
for (const cr of pending) {
const isOpen = _clrOpenCards.has(cr.id);
const pl = typeof cr.job_payload === 'string' ? JSON.parse(cr.job_payload || '{}') : (cr.job_payload || {});
const created = cr.created_at ? new Date(cr.created_at).toLocaleString() : '';
const expires = cr.expires_at ? new Date(cr.expires_at).toLocaleString() : '';
html += `<div class="clr-card${isOpen?' open':''}" id="clr-card-${cr.id}">
<div class="clr-card-head" onclick="toggleClrCard(${cr.id})">
<span class="clr-card-type">${escHtml(cr.job_type.toUpperCase().replace(/_/g,' '))}</span>
<span class="clr-card-risk ${cr.risk_level}">${cr.risk_level.toUpperCase()}</span>
<span style="font-family:var(--font-mono);font-size:0.48rem;color:var(--text-dim)">#${cr.id}</span>
</div>
<div class="clr-card-body">
<div class="clr-card-desc">${escHtml(cr.description || 'No description')}</div>
<div style="font-family:var(--font-mono);font-size:0.48rem;color:var(--text-dim);margin-bottom:4px">
Requested: ${created}${expires ? ' · Expires: ' + expires : ''}
</div>
<div style="font-family:var(--font-mono);font-size:0.48rem;color:var(--text-dim);margin-bottom:6px;word-break:break-all">
Payload: ${escHtml(JSON.stringify(pl))}
</div>
<div class="clr-action-bar">
<button class="clr-approve-btn" onclick="hudClearanceDecide(${cr.id},'approve')"> AUTHORIZE</button>
<button class="clr-deny-btn" onclick="hudClearanceDecide(${cr.id},'deny')"> DENY</button>
</div>
</div>
</div>`;
}
}
// Rules
html += `<div style="font-family:var(--font-display);font-size:0.55rem;letter-spacing:2px;color:var(--text-dim);margin:12px 0 4px">CLEARANCE RULES</div>`;
if (!rules.length) {
html += '<div class="comms-empty" style="color:rgba(255,255,255,0.3);margin-bottom:10px">No rules configured</div>';
} else {
html += '<div style="background:rgba(0,0,0,0.2);border-radius:var(--r);padding:6px 10px;margin-bottom:8px">';
for (const r of rules) {
const enClass = r.enabled ? 'clr-rule-enabled' : 'clr-rule-disabled';
const enLabel = r.enabled ? 'ON' : 'OFF';
const reqLabel = r.require_approval ? 'REQUIRES APPROVAL' : 'AUTO-ALLOW';
const autoTxt = r.auto_approve_after_min ? ` · AUTO ${r.auto_approve_after_min}m` : '';
html += `<div class="clr-rule-row">
<span class="clr-rule-type">${r.job_type.replace(/_/g,' ').toUpperCase()}</span>
<span class="clr-card-risk ${r.risk_level}" style="font-family:var(--font-mono);font-size:0.48rem;padding:1px 4px;border-radius:2px;border:1px solid">${r.risk_level.toUpperCase()}</span>
<span style="font-family:var(--font-mono);font-size:0.48rem;color:var(--text-dim)">${reqLabel}${autoTxt}</span>
<button class="clr-rule-toggle ${enClass}" onclick="hudClearanceRuleToggle(${r.id},${r.enabled?0:1})">${enLabel}</button>
</div>`;
}
html += '</div>';
}
// Recent history
html += `<div style="font-family:var(--font-display);font-size:0.55rem;letter-spacing:2px;color:var(--text-dim);margin:8px 0 4px">RECENT HISTORY</div>`;
const recentDecided = history.filter(h => h.status !== 'pending').slice(0, 10);
if (!recentDecided.length) {
html += '<div class="comms-empty" style="color:rgba(255,255,255,0.3)">No history yet</div>';
} else {
html += '<div style="background:rgba(0,0,0,0.2);border-radius:var(--r);padding:6px 10px">';
for (const h of recentDecided) {
const ts = h.decided_at ? new Date(h.decided_at).toLocaleString() : '';
html += `<div class="clr-history-row">
<span class="clr-status-${h.status}"></span>
<span style="flex:1">${h.job_type.replace(/_/g,' ').toUpperCase()}</span>
<span class="clr-status-${h.status}">${h.status.toUpperCase()}</span>
<span style="color:rgba(255,255,255,0.3)">${ts}</span>
</div>`;
}
html += '</div>';
}
el.innerHTML = html;
await updateClearanceBanner();
} catch(e) {
if (el) el.innerHTML = '<div class="comms-empty">CLEARANCE SYSTEM OFFLINE</div>';
}
}
function toggleClrCard(id) {
const card = document.getElementById('clr-card-' + id);
if (!card) return;
if (_clrOpenCards.has(id)) _clrOpenCards.delete(id);
else _clrOpenCards.add(id);
card.classList.toggle('open');
}
async function hudClearanceDecide(id, action) {
const label = action === 'approve' ? 'AUTHORIZE' : 'DENY';
if (!confirm(`${label} clearance request #${id}?`)) return;
const note = action === 'deny' ? (prompt('Reason for denial (optional):') || '') : '';
try {
const res = await api(`arc?action=clearance_${action}&id=${id}`, 'POST', { decided_by: 'admin', note });
const msg = action === 'approve'
? `◈ Clearance #${id} authorized. Job dispatched.`
: `◈ Clearance #${id} denied${note ? ': ' + note : ''}.`;
addMessage('jarvis', msg);
speak(action === 'approve' ? 'Clearance granted. Job dispatched.' : 'Request denied.');
await loadClearanceHud();
} catch(e) {
addMessage('system', 'Clearance action failed.');
}
}
async function hudClearanceRuleToggle(id, newEnabled) {
try {
await api(`arc?action=clearance_rule_update&id=${id}`, 'POST', { enabled: newEnabled });
await loadClearanceHud();
} catch(e) {}
}
async function loadAgents() {
const [listData, metricsData] = await Promise.all([
api('agent/list'),
api('agent/status')
]);
const agents = listData.agents || [];
const metrics = metricsData.metrics || {};
// Fetch sparkline data (non-blocking)
api('metrics').then(d => { _sparkData = d || {}; renderAgentsTab(agents, metrics); }).catch(() => {});
renderAgentsTab(agents, metrics);
}
async function addNetworkDevice() {
const ip = prompt('IP address (e.g. 10.48.200.43):');
if (!ip) return;
const name = prompt('Device name (e.g. Yealink Phone):');
if (!name) return;
const type = prompt('Type (server, voip, nas, printer, device):', 'device') || 'device';
const r = await api('network/add', 'POST', {ip, alias: name, type});
if (r.error) { alert('Error: ' + r.error); return; }
loadNetwork();
}
async function deleteNetworkDevice(ip, evt) {
evt.stopPropagation();
if (!confirm('Remove ' + ip + ' from the network list?')) return;
const r = await api('network/delete', 'POST', {ip});
if (r.error) { alert('Error: ' + r.error); return; }
loadNetwork();
}
let _agentSparkData = {};
function sparkline(points, width=80, height=20, color='var(--cyan)') {
if (!points || points.length < 2) return '';
const max = Math.max(...points, 1);
const min = Math.min(...points);
const range = max - min || 1;
const step = width / (points.length - 1);
const pts = points.map((v, i) => {
const x = i * step;
const y = height - ((v - min) / range) * (height - 2) - 1;
return `${x.toFixed(1)},${y.toFixed(1)}`;
}).join(' ');
return `<svg width="${width}" height="${height}" style="overflow:visible;display:block">
<polyline points="${pts}" fill="none" stroke="${color}" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round" opacity="0.8"/>
<circle cx="${((points.length-1)*step).toFixed(1)}" cy="${(height - ((points[points.length-1]-min)/range)*(height-2)-1).toFixed(1)}" r="2" fill="${color}"/>
</svg>`;
}
function renderAgentsTab(agents, metrics) {
const el = document.getElementById('agents-list');
if (!el) return;
if (!agents.length) {
el.innerHTML = '<div style="font-family:var(--font-mono);font-size:0.75rem;color:var(--text-dim);text-align:center;margin-top:20px">NO AGENTS REGISTERED</div>';
return;
}
el.innerHTML = agents.map(ag => {
const m = metrics[ag.agent_id] || {};
const sys = m.system || {};
const alive = ag.status === 'online';
const cpu = sys.cpu_percent != null ? Math.round(sys.cpu_percent) : '--';
const mem = sys.memory ? Math.round(sys.memory.percent) : '--';
const memUsed = sys.memory ? Math.round(sys.memory.used_mb / 1024 * 10) / 10 + 'GB' : '--';
const memTot = sys.memory ? Math.round(sys.memory.total_mb / 1024 * 10) / 10 + 'GB' : '--';
const disks = sys.disk || [];
const maxDisk = disks.length ? Math.max(...disks.map(d => parseInt(d.percent)||0)) : null;
const uptime = sys.uptime ? sys.uptime.human : (alive ? 'ONLINE' : 'OFFLINE');
const since = ag.last_seen ? ag.last_seen.replace('T',' ').replace(/\.\d+Z$/,'') : '--';
const gauge = (val, unit='%', warn=80, crit=90) => {
const v = typeof val === 'number' ? val : parseInt(val);
if (isNaN(v)) return `<span style="color:var(--text-dim)">--</span>`;
const col = v >= crit ? 'var(--red)' : v >= warn ? '#f5a623' : 'var(--green)';
return `<div style="display:flex;align-items:center;gap:4px">
<div style="width:50px;height:5px;background:rgba(255,255,255,0.1);border-radius:3px;flex-shrink:0">
<div style="width:${Math.min(v,100)}%;height:100%;background:${col};border-radius:3px;transition:width 0.5s"></div>
</div>
<span style="color:${col};font-size:0.65rem">${v}${unit}</span>
</div>`;
};
const svcs = (sys.services || []).filter(s => s.status !== 'inactive' || true)
.map(s => `<span style="color:${s.status==='active'?'var(--green)':'var(--red)'};font-size:0.58rem;margin-right:6px">${s.service}: ${s.status}</span>`)
.join('');
const ctxKey = 'agent_' + ag.agent_id;
_panelCtx[ctxKey] = {type:'agent', label: ag.hostname, agent_id: ag.agent_id,
hostname: ag.hostname, status: ag.status, cpu, mem};
return `<div class="alert-item ${alive ? '' : 'critical'}" data-ctx-key="${ctxKey}" onclick="selectContext('${ctxKey}')"
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="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-size:0.58rem;color:var(--text-dim)">${ag.agent_type.toUpperCase()} · ${ag.ip_address}</span>
<span style="font-size:0.58rem;color:${alive ? 'var(--green)' : 'var(--red)'};">${alive ? 'ONLINE' : 'OFFLINE'}</span>
</div>
${alive ? `<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;margin-bottom:4px">
<div><div style="font-size:0.58rem;color:var(--text-dim);margin-bottom:2px">CPU</div>${gauge(cpu)}</div>
<div><div style="font-size:0.58rem;color:var(--text-dim);margin-bottom:2px">MEM ${memUsed}/${memTot}</div>${gauge(mem)}</div>
<div><div style="font-size:0.58rem;color:var(--text-dim);margin-bottom:2px">DISK</div>${maxDisk != null ? gauge(maxDisk) : '<span style="color:var(--text-dim)">--</span>'}</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:4px">
<div>
<div style="font-size:0.52rem;color:var(--text-dim);margin-bottom:2px">CPU 2H</div>
${sparkline((_agentSparkData[ag.agent_id]||[]).map(p=>p.cpu), 100, 18, 'rgba(0,212,255,0.7)')}
</div>
<div>
<div style="font-size:0.52rem;color:var(--text-dim);margin-bottom:2px">MEM 2H</div>
${sparkline((_agentSparkData[ag.agent_id]||[]).map(p=>p.mem), 100, 18, 'rgba(0,255,136,0.7)')}
</div>
</div>` : ''}
<div style="display:flex;align-items:center;justify-content:space-between">
<div style="font-size:0.58rem;color:var(--text-dim)">UP: ${uptime} · SEEN: ${since}</div>
${svcs ? `<div style="font-size:0.58rem">${svcs}</div>` : ''}
</div>
${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();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>
</div>` : ''}
</div>`;
}).join('');
}
function openAgentModal() {
const os = detectOS();
const title = document.getElementById('agentModalTitle');
const content = document.getElementById('agentModalContent');
const modal = document.getElementById('agentModal');
const regKey = 'f846a9aaf7ce9a61742c63c87c4186052a71d2a580c65518';
const baseUrl = 'https://jarvis.orbishosting.com/agent';
const jUrl = window.location.origin;
if (os === 'tablet') {
title.textContent = '● JARVIS — TABLET / MOBILE';
content.innerHTML =
'<div style="color:var(--cyan);font-size:0.75rem;margin-bottom:12px">✓ You\'re viewing JARVIS on a tablet or mobile device.</div>' +
'<div style="color:var(--text-dim);font-size:0.65rem;line-height:1.6">The JARVIS Agent runs on desktop and server platforms (Windows, macOS, Linux).<br><br>' +
'Tablets and phones can browse the full JARVIS dashboard but do not need an agent installed — all data comes from your other monitored machines.</div>';
} else if (_agentOnline) {
title.textContent = '● AGENT CONNECTED';
content.innerHTML =
'<div style="color:var(--green);font-size:0.75rem;margin-bottom:12px">✓ JARVIS Agent is active on this machine.</div>' +
'<div style="color:var(--text-dim);font-size:0.65rem;line-height:1.8">' +
'<b style="color:var(--text)">Host:</b> ' + (_myAgent?.hostname||'—') + '<br>' +
'<b style="color:var(--text)">IP:</b> ' + (_myAgent?.ip_address||'—') + '<br>' +
'<b style="color:var(--text)">Type:</b> ' + (_myAgent?.agent_type||'—').toUpperCase() + '<br>' +
'<b style="color:var(--text)">Reporting:</b> CPU · Memory · Disk · Services · Uptime</div>';
} else {
const inst = {
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,
dl: baseUrl+'/install-windows.ps1',
note:'Run PowerShell as Administrator. Installs as a Windows Task Scheduler service.'
},
mac: {
label:'macOS',
cmd:'bash <(curl -sSL '+baseUrl+'/install-mac.sh) \\\n --jarvis-url '+jUrl+' \\\n --key '+regKey,
dl: baseUrl+'/install-mac.sh',
note:'Run in Terminal. Installs as a launchd background service.'
},
linux: {
label:'Linux',
cmd:'curl -sSL '+baseUrl+'/install.sh | sudo bash -s -- \\\n --jarvis-url '+jUrl+' \\\n --key '+regKey,
dl: baseUrl+'/install.sh',
note:'Run in terminal. Installs as a systemd service.'
},
unknown: {
label:'Your System',
cmd:'# Browse installers:\nhttps://jarvis.orbishosting.com/agent/',
dl: 'https://jarvis.orbishosting.com/agent/',
note:'Choose your platform installer from the JARVIS agent directory.'
}
};
const i = inst[os] || inst.unknown;
const osBadge = {windows:'🪟 WINDOWS', mac:'🍎 MACOS', linux:'🐧 LINUX', unknown:'❓ UNKNOWN'}[os] || os.toUpperCase();
title.textContent = '● INSTALL AGENT · ' + (inst[os] ? inst[os].label.toUpperCase() : 'YOUR SYSTEM');
content.innerHTML =
'<div style="color:var(--cyan);font-size:0.65rem;letter-spacing:1px;margin-bottom:8px">DETECTED: ' + osBadge + '</div>' +
'<div style="color:var(--text-dim);font-size:0.65rem;margin-bottom:12px">'+i.note+'</div>' +
'<pre id="agentCmdPre">'+i.cmd+'</pre>' +
'<a class="agent-dl-btn" href="'+i.dl+'" target="_blank">↓ DOWNLOAD INSTALLER</a>' +
'<div style="color:var(--text-dim);font-size:0.6rem;margin-top:16px;opacity:0.7">After install, the AGENT indicator turns green within 30 seconds.</div>';
}
modal.classList.add('open');
}
document.addEventListener('click', function(e) {
if (e.target === document.getElementById('agentModal'))
document.getElementById('agentModal').classList.remove('open');
});
// ── SITES MANAGER ────────────────────────────────────────────────────
let sitesData = {};
function openSitesModal() {
document.getElementById('sitesModal').style.display = 'flex';
loadSites();
}
function closeSitesModal() {
document.getElementById('sitesModal').style.display = 'none';
}
// Close on backdrop click
document.getElementById('sitesModal').addEventListener('click', function(e) {
if (e.target === this) closeSitesModal();
});
async function loadSites() {
document.getElementById('sites-grid').innerHTML = '<div style="grid-column:1/-1;color:var(--text-dim);font-size:0.65rem;letter-spacing:2px">LOADING SITE SETTINGS...</div>';
const res = await api('sites');
if (!res.success) {
document.getElementById('sites-grid').innerHTML = '<div style="grid-column:1/-1;color:#f44;font-size:0.65rem">FAILED TO LOAD SETTINGS</div>';
return;
}
sitesData = res.sites;
// Pre-fill global key from first site
const firstKey = Object.values(res.sites)[0]?.api_key || '';
document.getElementById('global-api-key').value = firstKey;
renderSiteCards();
}
function renderSiteCards() {
const grid = document.getElementById('sites-grid');
let html = '';
for (const [id, s] of Object.entries(sitesData)) {
html += `
<div style="background:rgba(0,212,255,0.02);border:1px solid rgba(0,212,255,0.12);padding:16px">
<div style="margin-bottom:12px">
<div style="color:var(--cyan);font-size:0.65rem;letter-spacing:2px;margin-bottom:2px">${s.name.toUpperCase()}</div>
<div style="color:var(--text-dim);font-size:0.58rem">${s.url}</div>
</div>
<div style="margin-bottom:10px">
<div style="color:var(--text-dim);font-size:0.58rem;letter-spacing:1px;margin-bottom:4px">FROM EMAIL</div>
<input id="${id}-from_email" type="text" value="${s.from_email || ''}"
style="width:100%;background:#0a0f1a;border:1px solid rgba(0,212,255,0.15);color:var(--text);font-family:var(--font-mono);font-size:0.65rem;padding:6px 10px;outline:none;box-sizing:border-box">
</div>
<div style="margin-bottom:10px">
<div style="color:var(--text-dim);font-size:0.58rem;letter-spacing:1px;margin-bottom:4px">FROM NAME</div>
<input id="${id}-from_name" type="text" value="${s.from_name || ''}"
style="width:100%;background:#0a0f1a;border:1px solid rgba(0,212,255,0.15);color:var(--text);font-family:var(--font-mono);font-size:0.65rem;padding:6px 10px;outline:none;box-sizing:border-box">
</div>
<div style="margin-bottom:12px">
<div style="color:var(--text-dim);font-size:0.58rem;letter-spacing:1px;margin-bottom:4px">ADMIN NOTIFICATION EMAIL</div>
<input id="${id}-admin_email" type="text" value="${s.admin_email || ''}"
style="width:100%;background:#0a0f1a;border:1px solid rgba(0,212,255,0.15);color:var(--text);font-family:var(--font-mono);font-size:0.65rem;padding:6px 10px;outline:none;box-sizing:border-box">
</div>
<div style="display:flex;align-items:center;gap:10px">
<button onclick="saveSite('${id}')"
style="background:rgba(0,212,255,0.08);border:1px solid rgba(0,212,255,0.3);color:var(--cyan);font-family:var(--font-mono);font-size:0.58rem;letter-spacing:2px;padding:6px 16px;cursor:pointer">
SAVE
</button>
<span id="${id}-status" style="font-size:0.58rem;color:var(--text-dim)"></span>
</div>
</div>`;
}
grid.innerHTML = html;
}
async function pushApiKey() {
const key = document.getElementById('global-api-key').value.trim();
const status = document.getElementById('push-status');
if (!key) { status.style.color='#f44'; status.textContent='✗ API KEY REQUIRED'; return; }
status.style.color='var(--text-dim)'; status.textContent='PUSHING TO ALL SITES...';
const res = await api('sites', 'POST', {action:'push_key', api_key:key});
if (res.success) {
const ok = Object.values(res.results).filter(Boolean).length;
const total = Object.keys(res.results).length;
status.style.color = ok === total ? 'var(--cyan)' : '#fa0';
status.textContent = `✓ PUSHED TO ${ok}/${total} SITES`;
for (const id of Object.keys(sitesData)) sitesData[id].api_key = key;
} else {
status.style.color='#f44'; status.textContent='✗ ' + (res.error || 'FAILED');
}
}
async function saveSite(id) {
const status = document.getElementById(id + '-status');
status.style.color='var(--text-dim)'; status.textContent='SAVING...';
const res = await api('sites', 'POST', {
action: 'save',
site: id,
from_email: document.getElementById(id+'-from_email').value.trim(),
from_name: document.getElementById(id+'-from_name').value.trim(),
admin_email: document.getElementById(id+'-admin_email').value.trim(),
});
if (res.success) {
status.style.color='var(--cyan)'; status.textContent='✓ SAVED';
setTimeout(() => { status.textContent=''; }, 3000);
} else {
status.style.color='#f44'; status.textContent='✗ ' + (res.error || 'FAILED');
}
}
// ── VISION PROTOCOL — screenshot lightbox ────────────────────────────────────
function openVisionLightbox(title) {
const lb = document.getElementById('vision-lightbox');
document.getElementById('vision-lb-title').textContent = title || '◈ VISION PROTOCOL';
document.getElementById('vision-lb-img').style.display = 'none';
document.getElementById('vision-lb-img').src = '';
document.getElementById('vision-lb-analysis').textContent = '';
document.getElementById('vision-lb-spinner').style.display = 'block';
lb.classList.add('open');
}
function closeVisionLightbox() {
document.getElementById('vision-lightbox').classList.remove('open');
}
async function agentScreenshot(hostname) {
openVisionLightbox('◈ VISION PROTOCOL — ' + hostname.toUpperCase());
const arcRes = await api('arc?action=job_create', 'POST', {
type: 'screenshot',
payload: {agent: hostname, analyze: true},
priority: 8,
}).catch(() => null);
if (!arcRes || !arcRes.job_id) {
document.getElementById('vision-lb-spinner').style.display = 'none';
document.getElementById('vision-lb-analysis').textContent = 'Failed to submit screenshot job — Arc Reactor may be offline.';
return;
}
// Poll for result
const jobId = arcRes.job_id;
let tries = 0;
const poll = async () => {
tries++;
const job = await api('arc?action=job_get&id=' + jobId).catch(() => null);
if (job && job.status === 'done') {
const r = job.result || {};
document.getElementById('vision-lb-spinner').style.display = 'none';
if (r.has_image && r.screenshot_id) {
// Fetch full screenshot with image
const full = await api('arc?action=screenshot_get&id=' + r.screenshot_id).catch(() => null);
if (full && full.image_b64) {
const img = document.getElementById('vision-lb-img');
img.src = 'data:image/png;base64,' + full.image_b64;
img.style.display = 'block';
}
}
document.getElementById('vision-lb-analysis').textContent =
r.analysis || (r.has_image ? 'Screenshot captured — no analysis available.' : JSON.stringify(r.snapshot || r, null, 2));
} else if (job && job.status === 'failed') {
document.getElementById('vision-lb-spinner').style.display = 'none';
document.getElementById('vision-lb-analysis').textContent = 'Screenshot failed: ' + (job.error || 'Unknown error');
} else if (tries < 30) {
setTimeout(poll, 2000);
} else {
document.getElementById('vision-lb-spinner').style.display = 'none';
document.getElementById('vision-lb-analysis').textContent = 'Timed out waiting for screenshot.';
}
};
setTimeout(poll, 2000);
}
async function agentSysinfo(hostname) {
openVisionLightbox('⚡ FIELD SYSINFO — ' + hostname.toUpperCase());
const arcRes = await api('arc?action=job_create', 'POST', {
type: 'sysinfo',
payload: {agent: hostname, analyze: true},
priority: 7,
}).catch(() => null);
if (!arcRes || !arcRes.job_id) {
document.getElementById('vision-lb-spinner').style.display = 'none';
document.getElementById('vision-lb-analysis').textContent = 'Failed to submit sysinfo job.';
return;
}
const jobId = arcRes.job_id;
let tries = 0;
const poll = async () => {
tries++;
const job = await api('arc?action=job_get&id=' + jobId).catch(() => null);
if (job && job.status === 'done') {
const r = job.result || {};
document.getElementById('vision-lb-spinner').style.display = 'none';
const snap = r.snapshot || {};
const snapText = Object.entries(snap)
.filter(([k]) => !['success','screenshot_available','snapshot_type'].includes(k))
.map(([k,v]) => `${k.toUpperCase().replace(/_/g,' ')}: ${Array.isArray(v) ? v.join('\n ') : v}`)
.join('\n');
document.getElementById('vision-lb-analysis').textContent =
(r.analysis ? r.analysis + '\n\n─────────────────────\n\n' : '') + (snapText || JSON.stringify(r, null, 2));
} else if (job && job.status === 'failed') {
document.getElementById('vision-lb-spinner').style.display = 'none';
document.getElementById('vision-lb-analysis').textContent = 'Sysinfo failed: ' + (job.error || 'Unknown error');
} else if (tries < 20) {
setTimeout(poll, 2000);
} else {
document.getElementById('vision-lb-spinner').style.display = 'none';
document.getElementById('vision-lb-analysis').textContent = 'Timed out.';
}
};
setTimeout(poll, 2000);
}
document.addEventListener('keydown', e => {
if (e.key === 'Escape') closeVisionLightbox();
});
+608
View File
@@ -0,0 +1,608 @@
// ── ARC REACTOR STATUS ────────────────────────────────────────────────
let _arcOnline = false;
let _arcJobs = { queued: 0, running: 0, done: 0, failed: 0 };
async function checkArcStatus() {
const dot = document.getElementById('bb-arc-dot');
const sta = document.getElementById('bb-arc-status');
if (!dot || !sta) return;
try {
const d = await api('arc?action=status');
if (d && d.online) {
_arcOnline = true;
dot.className = 'bb-dot online';
const active = (d.active_jobs || 0) + (d.queued_jobs || 0);
sta.textContent = active > 0 ? active + ' JOB' + (active !== 1 ? 'S' : '') : 'ONLINE';
_arcJobs = { queued: d.queued_jobs||0, running: d.running_jobs||0,
done: d.jobs_done||0, failed: d.jobs_failed||0 };
} else {
_arcOnline = false;
dot.className = 'bb-dot offline';
sta.textContent = 'OFFLINE';
}
} catch(e) {
_arcOnline = false;
dot.className = 'bb-dot offline';
sta.textContent = 'OFFLINE';
}
}
// Submit a job to the Arc Reactor and return job_id
async function arcSubmitJob(type, payload, priority) {
payload = payload || {};
priority = priority || 5;
const d = await api('arc', { action: 'job_create', type: type, payload: payload, priority: priority });
return d.job_id || null;
}
// Poll a job until done or failed (max 120s), calling onProgress each tick
async function arcWaitJob(jobId, onProgress) {
var start = Date.now();
while (Date.now() - start < 120000) {
const d = await api('arc?action=job_get&id=' + jobId);
if (onProgress) onProgress(d);
if (d.status === 'done') return d;
if (d.status === 'failed') throw new Error(d.error || 'Job failed');
await new Promise(function(r){ setTimeout(r, 1500); });
}
throw new Error('Arc Reactor job timed out');
}
// ── INTEL PROTOCOL — HUD panel ────────────────────────────────────────
let _intelPollTimer = null;
let _intelActiveJobs = new Set();
let _intelLastLoad = 0;
async function loadIntel() {
const el = document.getElementById('intel-list');
if (!el) return;
_intelLastLoad = Date.now();
try {
// Fetch recent research + tool_loop jobs
const [resJobs, toolJobs] = await Promise.all([
api('arc?action=jobs&status=&limit=20').catch(() => []),
Promise.resolve([]),
]);
const jobs = Array.isArray(resJobs) ? resJobs.filter(j => ['research','tool_loop','llm'].includes(j.job_type)) : [];
if (!jobs.length) {
el.innerHTML = '<div class="intel-empty">◈ NO INTEL JOBS<br><span style="opacity:0.5">Say "research [topic]" to activate</span></div>';
stopIntelPolling();
return;
}
// Check for active jobs
const hasActive = jobs.some(j => j.status === 'queued' || j.status === 'running');
if (hasActive) startIntelPolling(); else stopIntelPolling();
let html = '<button class="intel-new-btn" onclick="intelPrompt()">⚡ NEW RESEARCH</button>';
for (const job of jobs) {
const isOpen = _intelActiveJobs.has(job.id) || job.status === 'running';
const statusClass = job.status === 'done' ? 'done' : job.status === 'failed' ? 'failed' : 'running';
const statusLabel = job.status === 'queued' ? 'QUEUED' : job.status === 'running' ? '● ACTIVE' : job.status.toUpperCase();
const typeLabel = job.job_type === 'research' ? '◈ INTEL' : job.job_type === 'tool_loop' ? '⚡ IRON' : '◈ LLM';
// Get result details if done
let bodyHtml = '';
if (job.status === 'done' && job.result) {
let r = job.result;
if (typeof r === 'string') { try { r = JSON.parse(r); } catch(e) {} }
if (typeof r === 'object') {
const synthesis = (r.synthesis || r.result || r.response || '').trim();
const sources = r.sources || [];
const query = r.query || r.task || '';
const provider = r.provider || '';
bodyHtml = `<div class="intel-card-body">`;
if (provider) bodyHtml += `<div style="font-size:0.55rem;color:var(--text-dim);margin:6px 0 2px;font-family:var(--font-mono)">PROVIDER: ${provider.toUpperCase()} · SOURCES: ${r.source_count||sources.length||'—'}</div>`;
if (synthesis) bodyHtml += `<div class="synthesis">${escHtml(synthesis.substring(0, 1500))}${synthesis.length>1500?'\n\n[...truncated — view in admin]':''}</div>`;
if (sources.length) {
bodyHtml += '<div class="intel-sources"><div style="font-size:0.55rem;letter-spacing:2px;color:var(--text-dim);margin-bottom:4px;font-family:var(--font-display)">SOURCES</div>';
sources.slice(0,5).forEach((s,i) => {
const title = escHtml((s.title||s.url||'').substring(0,60));
const url = escHtml(s.url||'');
bodyHtml += `<div class="intel-source">${i+1}. <a href="${url}" target="_blank" rel="noopener">${title||url}</a></div>`;
});
bodyHtml += '</div>';
}
bodyHtml += '</div>';
}
} else if (job.status === 'running' || job.status === 'queued') {
const typeMsg = job.job_type === 'research' ? 'Searching sources and extracting content...' : 'Executing tool loop...';
bodyHtml = `<div class="intel-card-body"><div style="font-size:0.62rem;color:var(--text-dim);padding:8px 0;font-family:var(--font-mono)">${typeMsg}</div></div>`;
} else if (job.status === 'failed' && job.error) {
bodyHtml = `<div class="intel-card-body"><div style="font-size:0.62rem;color:var(--red);padding:8px 0;font-family:var(--font-mono)">${escHtml(job.error.substring(0,200))}</div></div>`;
}
const queryText = job.created_by ? job.created_by.replace('chat:', '').replace(/session.*/, '') : '';
const ts = job.created_at ? new Date(job.created_at).toLocaleTimeString() : '';
html += `<div class="intel-card${(isOpen && bodyHtml) ? ' open':''}" id="intel-card-${job.id}">
<div class="intel-card-head" onclick="toggleIntelCard(${job.id})">
<span style="font-size:0.55rem;color:var(--text-dim);font-family:var(--font-mono);flex-shrink:0">${typeLabel}</span>
<span class="intel-card-query">#${job.id} ${escHtml((job.created_by||'').replace('chat:','').substring(0,40))}</span>
<span style="font-size:0.55rem;color:var(--text-dim);flex-shrink:0;font-family:var(--font-mono)">${ts}</span>
<span class="intel-card-status ${statusClass}">${statusLabel}</span>
</div>
${bodyHtml}
</div>`;
}
el.innerHTML = html;
} catch(e) {
if (el) el.innerHTML = '<div class="intel-empty">INTEL OFFLINE</div>';
}
}
function toggleIntelCard(id) {
const card = document.getElementById('intel-card-' + id);
if (!card) return;
if (_intelActiveJobs.has(id)) _intelActiveJobs.delete(id);
else _intelActiveJobs.add(id);
card.classList.toggle('open');
}
function startIntelPolling() {
if (_intelPollTimer) return;
_intelPollTimer = setInterval(() => {
if (document.getElementById('tab-intel')?.classList.contains('active')) {
loadIntel();
}
}, 4000);
}
function stopIntelPolling() {
if (_intelPollTimer) { clearInterval(_intelPollTimer); _intelPollTimer = null; }
}
function escHtml(s) {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function intelPrompt() {
const input = document.getElementById('textInput');
if (input) { input.value = 'research '; input.focus(); }
}
// Called when arc_job is returned from chat response
function onArcJobStarted(jobId, jobType) {
const commsTypes = ['arc:gmail_triage', 'arc:send_email', 'arc:compose_email', 'arc:schedule_event', 'arc:meeting_prep'];
if (commsTypes.includes(jobType)) {
const commsBtn = document.getElementById('tab-btn-comms');
if (commsBtn) commsBtn.click();
startCommsPolling();
} else {
_intelActiveJobs.add(jobId);
const intelTab = document.querySelector('[onclick*="switchTab(\'intel\')"]');
if (intelTab) intelTab.click();
startIntelPolling();
}
}
// ── COMMS PROTOCOL — email triage HUD ────────────────────────────────────
let _commsPollTimer = null;
let _commsFilter = 'priority';
let _commsOpenCards = new Set();
async function loadComms() {
const el = document.getElementById('comms-list');
if (!el) return;
try {
const res = await api('arc?action=triage&limit=50&filter=' + _commsFilter);
const items = Array.isArray(res) ? res : (res.items || []);
if (!items.length) {
el.innerHTML = '<button class="comms-triage-btn" onclick="commsTriageNow()">◈ TRIAGE INBOX NOW</button>'
+ '<div class="comms-empty">◈ NO TRIAGE DATA<br><span style="opacity:0.5">Say "check my email" to activate</span></div>';
stopCommsPolling();
return;
}
const catOrder = {urgent:0, action:1, reply:2, meeting:3, info:4, promo:5, spam:6};
const catIcons = {urgent:'🔴', action:'⚡', reply:'◈', meeting:'📅', info:'', promo:'📢', spam:'🗑'};
let html = '<div style="display:flex;gap:5px;margin-bottom:5px">';
html += '<button class="comms-triage-btn" style="flex:3;margin-bottom:0" onclick="commsTriageNow()">◈ TRIAGE INBOX</button>';
html += '<button class="comms-compose-btn" style="flex:2;margin-bottom:0" onclick="commsShowCompose()">+ COMPOSE</button>';
html += '</div>';
html += '<div class="comms-header-bar">';
for (const [f, label] of [['priority','PRIORITY'],['urgent','URGENT'],['action','ACTION'],['all','ALL']]) {
html += `<div class="comms-filter-btn${_commsFilter===f?' active':''}" onclick="commsSetFilter('${f}')">${label}</div>`;
}
html += '</div>';
for (const item of items) {
const cat = item.category || 'info';
const icon = catIcons[cat] || '◈';
const prio = item.priority || 0;
const isOpen = _commsOpenCards.has(item.id);
const hasReply = item.draft_reply && item.draft_reply.trim().length > 5;
html += `<div class="comms-card${isOpen?' open':''}" id="comms-card-${item.id}">
<div class="comms-card-head" onclick="toggleCommsCard(${item.id})">
<span class="comms-card-cat ${cat}">${icon} ${cat.toUpperCase()}</span>
<span class="comms-card-subject">${escHtml((item.subject||'(no subject)').substring(0,60))}</span>
<span class="comms-prio">${prio}/10</span>
</div>
<div class="comms-card-body">
<div class="comms-card-from">FROM: ${escHtml((item.from_name||item.from_email||'').substring(0,50))}</div>
<div class="comms-card-summary">${escHtml(item.summary||'')}</div>
${hasReply ? `<div class="comms-draft-label">DRAFT REPLY</div><div class="comms-draft" id="comms-draft-${item.id}">${escHtml(item.draft_reply)}</div>` : ''}
<div style="display:flex;gap:5px;margin-top:8px">
${hasReply ? `<button class="comms-send-btn" id="comms-send-${item.id}" onclick="commsSendReply(${item.id})">◈ SEND REPLY</button>` : ''}
${hasReply ? `<button onclick="commsCopyReply(${item.id})" style="flex:1;background:rgba(0,212,255,0.05);border:1px solid var(--panel-border);border-radius:3px;padding:3px 6px;color:var(--text-dim);font-family:var(--font-display);font-size:0.5rem;letter-spacing:1px;cursor:pointer">COPY</button>` : ''}
<button onclick="commsDismiss(${item.id})" style="flex:1;background:rgba(255,255,255,0.03);border:1px solid var(--panel-border);border-radius:3px;padding:3px 6px;color:var(--text-dim);font-family:var(--font-display);font-size:0.5rem;letter-spacing:1px;cursor:pointer">DISMISS</button>
</div>
</div>
</div>`;
}
el.innerHTML = html;
} catch(e) {
if (el) el.innerHTML = '<div class="comms-empty">COMMS OFFLINE</div>';
}
}
function toggleCommsCard(id) {
const card = document.getElementById('comms-card-' + id);
if (!card) return;
if (_commsOpenCards.has(id)) _commsOpenCards.delete(id);
else _commsOpenCards.add(id);
card.classList.toggle('open');
}
function commsSetFilter(f) {
_commsFilter = f;
loadComms();
}
async function commsDismiss(id) {
await api('arc?action=triage_action&id=' + id, 'POST', {action: 'dismissed'}).catch(() => {});
loadComms();
}
async function commsCopyReply(id) {
const draft = document.querySelector(`#comms-draft-${id}`);
if (draft) {
navigator.clipboard.writeText(draft.innerText).catch(() => {});
const btn = document.querySelector(`#comms-card-${id} [onclick*="commsCopyReply"]`);
if (btn) { btn.textContent = 'COPIED!'; setTimeout(() => btn.textContent = 'COPY', 1500); }
}
}
async function commsSendReply(id) {
const btn = document.getElementById('comms-send-' + id);
const draft = document.getElementById('comms-draft-' + id);
if (!btn || !draft) return;
btn.disabled = true;
btn.textContent = '◈ SENDING…';
try {
const res = await api('arc', 'POST', {
action: 'job_create',
type: 'send_email',
payload: { triage_id: id, content: draft.innerText },
priority: 8,
});
if (res.job_id) {
btn.textContent = '◈ SENT ✓';
btn.style.color = '#00ff88';
setTimeout(() => loadComms(), 3000);
loadCommsOutbox();
} else {
btn.disabled = false;
btn.textContent = '◈ SEND REPLY';
alert('Send failed: ' + (res.error || 'unknown error'));
}
} catch(e) {
btn.disabled = false;
btn.textContent = '◈ SEND REPLY';
}
}
function commsShowCompose() {
const existing = document.getElementById('comms-compose-modal');
if (existing) existing.remove();
const modal = document.createElement('div');
modal.className = 'comms-compose-modal';
modal.id = 'comms-compose-modal';
modal.innerHTML = `
<div class="comms-compose-inner">
<div class="comms-compose-title"> COMPOSE MESSAGE</div>
<select id="cc-account" class="comms-compose-field" style="cursor:pointer">
<option value="gmail">Gmail</option>
<option value="icloud">iCloud</option>
</select>
<input id="cc-to" class="comms-compose-field" placeholder="To: email address" type="email">
<input id="cc-subject" class="comms-compose-field" placeholder="Subject">
<textarea id="cc-instructions" class="comms-compose-field" rows="4" placeholder="Describe what to say (AI will draft it)"></textarea>
<div id="cc-preview" style="display:none">
<div class="comms-draft-label">DRAFTED MESSAGE</div>
<div class="comms-draft" id="cc-preview-body" style="max-height:200px"></div>
</div>
<div class="comms-compose-actions">
<button class="comms-send-btn" style="flex:1" onclick="commsComposeDraft()"> DRAFT</button>
<button class="comms-send-btn" style="flex:1;display:none" id="cc-send-btn" onclick="commsComposeAndSend()"> SEND NOW</button>
<button onclick="document.getElementById('comms-compose-modal').remove()" style="flex:1;background:rgba(255,255,255,0.03);border:1px solid var(--panel-border);border-radius:3px;padding:3px 6px;color:var(--text-dim);font-family:var(--font-display);font-size:0.5rem;letter-spacing:1px;cursor:pointer">CANCEL</button>
</div>
<div id="cc-status" style="font-family:var(--font-mono);font-size:0.55rem;color:var(--cyan);margin-top:6px;min-height:14px"></div>
</div>`;
document.body.appendChild(modal);
modal.addEventListener('click', e => { if (e.target === modal) modal.remove(); });
}
let _ccDraftedBody = '';
async function commsComposeDraft() {
const to = document.getElementById('cc-to')?.value.trim();
const subject = document.getElementById('cc-subject')?.value.trim();
const instructions = document.getElementById('cc-instructions')?.value.trim();
const account = document.getElementById('cc-account')?.value;
const status = document.getElementById('cc-status');
if (!to || !instructions) { if (status) status.textContent = 'Please fill in To and message description.'; return; }
if (status) status.textContent = '◈ DRAFTING…';
try {
const res = await api('arc', 'POST', {
action: 'job_create', type: 'compose_email',
payload: { recipient: to, subject, instructions, account, auto_send: false },
priority: 7,
});
if (!res.job_id) throw new Error(res.error || 'No job');
// poll for result
let attempts = 0;
const poll = async () => {
const job = await api('arc?action=job_get&id=' + res.job_id);
if (job.status === 'done' && job.result?.drafted_body) {
_ccDraftedBody = job.result.drafted_body;
document.getElementById('cc-preview-body').textContent = _ccDraftedBody;
document.getElementById('cc-preview').style.display = 'block';
document.getElementById('cc-send-btn').style.display = '';
if (status) status.textContent = '◈ DRAFT READY — Review and send';
} else if (job.status === 'failed') {
if (status) status.textContent = '✗ Draft failed: ' + (job.error || 'unknown');
} else if (attempts++ < 20) {
setTimeout(poll, 1500);
} else {
if (status) status.textContent = '◈ Job still running — check INTEL tab';
}
};
setTimeout(poll, 1500);
} catch(e) {
if (status) status.textContent = '✗ Error: ' + e.message;
}
}
async function commsComposeAndSend() {
const to = document.getElementById('cc-to')?.value.trim();
const subject = document.getElementById('cc-subject')?.value.trim();
const account = document.getElementById('cc-account')?.value;
const status = document.getElementById('cc-status');
const btn = document.getElementById('cc-send-btn');
if (!to || !_ccDraftedBody) return;
if (btn) { btn.disabled = true; btn.textContent = '◈ SENDING…'; }
if (status) status.textContent = '◈ TRANSMITTING…';
try {
const res = await api('arc', 'POST', {
action: 'job_create', type: 'send_email',
payload: { to_email: to, subject, body: _ccDraftedBody, account },
priority: 9,
});
if (res.job_id) {
if (status) status.textContent = '◈ SENT ✓ (Job #' + res.job_id + ')';
setTimeout(() => {
document.getElementById('comms-compose-modal')?.remove();
loadCommsOutbox();
}, 1500);
} else {
if (btn) { btn.disabled = false; btn.textContent = '◈ SEND NOW'; }
if (status) status.textContent = '✗ Send failed: ' + (res.error || 'unknown');
}
} catch(e) {
if (btn) { btn.disabled = false; btn.textContent = '◈ SEND NOW'; }
if (status) status.textContent = '✗ Error: ' + e.message;
}
}
async function loadCommsOutbox() {
const el = document.getElementById('comms-outbox');
if (!el) return;
try {
const data = await api('arc?action=comms_sent&limit=20');
const sent = Array.isArray(data) ? data : (data.sent || []);
if (!sent.length) {
el.innerHTML = '<div class="comms-empty" style="padding:10px">No sent messages yet</div>';
return;
}
const statusColor = {sent:'#00ff88', failed:'#ff2244', queued:'#ffd700'};
let html = '';
for (const m of sent) {
const ts = m.sent_at ? new Date(m.sent_at + 'Z').toLocaleString() : '—';
const sc = m.status || 'sent';
html += `<div class="comms-outbox-card">
<div style="display:flex;justify-content:space-between;align-items:center">
<div class="comms-outbox-to">TO: ${escHtml((m.to_email||'').substring(0,40))}</div>
<span class="comms-outbox-status ${sc}">${sc.toUpperCase()}</span>
</div>
<div class="comms-outbox-subj">${escHtml((m.subject||'(no subject)').substring(0,60))}</div>
<div style="font-family:var(--font-mono);font-size:0.48rem;color:var(--text-dim)">${ts} · ${m.account||'gmail'}</div>
</div>`;
}
el.innerHTML = html;
} catch(e) {
el.innerHTML = '<div class="comms-empty" style="padding:10px">OUTBOX OFFLINE</div>';
}
}
function commsTriageNow() {
const input = document.getElementById('textInput');
if (input) { input.value = 'check my email'; input.dispatchEvent(new KeyboardEvent('keydown', {key:'Enter',keyCode:13,bubbles:true})); }
}
function startCommsPolling() {
if (_commsPollTimer) return;
_commsPollTimer = setInterval(() => {
if (document.getElementById('tab-comms')?.classList.contains('active')) { loadComms(); loadCommsOutbox(); }
}, 8000);
}
function stopCommsPolling() {
if (_commsPollTimer) { clearInterval(_commsPollTimer); _commsPollTimer = null; }
}
// ── GUARDIAN MODE ─────────────────────────────────────────────────────────────
let _guardianPollTimer = null;
let _guardianChatTimer = null;
let _guardianLastChat = '';
let _guardianUnread = 0;
async function loadGuardian() {
const el = document.getElementById('guardian-list');
if (!el) return;
try {
const [statusData, eventsData] = await Promise.all([
api('arc?action=guardian_status').catch(() => ({})),
api('arc?action=guardian_events&limit=40').catch(() => []),
]);
const events = Array.isArray(eventsData) ? eventsData : [];
const status = statusData || {};
const counts = status.counts || {};
const unread = parseInt(counts.unread || 0);
const critU = parseInt(counts.critical_unread || 0);
_guardianUnread = unread;
_updateGuardianBadge(unread, critU);
if (critU > 0 && document.hidden && 'Notification' in window && Notification.permission === 'granted') {
new Notification('JARVIS ALERT', {
body: critU + ' critical alert' + (critU > 1 ? 's' : '') + ' require your attention.',
icon: '/favicon.ico',
});
}
const lastScan = status.last_scan
? new Date(status.last_scan + 'Z').toLocaleTimeString()
: '—';
let html = `<div style="padding:6px 10px 4px;display:flex;align-items:center;gap:8px;flex-wrap:wrap">
<span style="font-family:var(--font-display);font-size:0.5rem;letter-spacing:2px;color:var(--cyan)"> GUARDIAN MODE</span>
<span style="font-family:var(--font-mono);font-size:0.5rem;color:${status.enabled?'var(--green)':'var(--red)'}">
${status.enabled ? '● ACTIVE' : '○ INACTIVE'}
</span>
<span style="font-family:var(--font-mono);font-size:0.5rem;color:var(--text-dim)">SCAN: ${lastScan}</span>
${unread ? `<button onclick="guardianAckAll()" class="guardian-ack-btn" style="margin-left:auto">ACK ALL (${unread})</button>` : '<span style="margin-left:auto"></span>'}
<button onclick="guardianSitrep()" style="background:rgba(0,212,255,0.08);border:1px solid var(--panel-border);color:var(--cyan);padding:3px 7px;border-radius:3px;font-family:var(--font-display);font-size:0.48rem;letter-spacing:1px;cursor:pointer"> SITREP</button>
</div>`;
if (!events.length) {
html += '<div style="text-align:center;padding:24px 10px;font-family:var(--font-mono);font-size:0.6rem;color:var(--text-dim);letter-spacing:1px">◈ ALL CLEAR<br><span style="opacity:0.5">Guardian is watching...</span></div>';
} else {
for (const ev of events) {
const sev = ev.severity || 'info';
const acked = ev.acknowledged;
const ts = ev.created_at ? new Date(ev.created_at).toLocaleTimeString() : '';
const typeIco = {agent_offline:'⚠',agent_online:'✓',cpu_high:'⚡',
mem_high:'⚡',disk_high:'💾',service_down:'✗',
service_recovered:'✓',sitrep:'◈',anomaly:'◈'}[ev.event_type] || '◈';
html += `<div class="guardian-event ${sev}${acked?' acked':''}" id="gev-${ev.id}">
<span class="guardian-sev ${sev}">${sev.toUpperCase()}</span>
<div style="flex:1">
<div class="guardian-msg">${typeIco} ${escHtml(ev.message||'')}</div>
${ev.ai_analysis ? `<div class="guardian-ai">${escHtml(ev.ai_analysis.substring(0,200))}</div>` : ''}
</div>
<div style="display:flex;flex-direction:column;align-items:flex-end;gap:4px;flex-shrink:0">
<span class="guardian-time">${ts}</span>
${!acked ? `<button class="guardian-ack-btn" onclick="guardianAck(${ev.id})">ACK</button>` : ''}
</div>
</div>`;
}
}
el.innerHTML = html;
startGuardianPolling();
} catch(e) {
if (el) el.innerHTML = '<div style="text-align:center;padding:20px;font-family:var(--font-mono);font-size:0.6rem;color:var(--text-dim)">GUARDIAN OFFLINE</div>';
}
}
function _updateGuardianBadge(unread, critical) {
const dot = document.getElementById('bb-guardian-dot');
const badge = document.getElementById('bb-guardian-badge');
const status = document.getElementById('bb-guardian-status');
if (!dot) return;
dot.className = 'bb-dot';
if (critical > 0) {
dot.classList.add('critical'); status.textContent = 'ALERT'; status.style.color = 'var(--red)';
} else if (unread > 0) {
dot.classList.add('warning'); status.textContent = 'WARNING'; status.style.color = '#f5a623';
} else {
dot.classList.add('all-clear'); status.textContent = 'CLEAR'; status.style.color = 'var(--green)';
}
if (unread > 0) {
badge.textContent = unread; badge.style.display = 'inline';
} else {
badge.style.display = 'none';
}
}
async function guardianAck(id) {
await api('arc?action=guardian_ack&id=' + id).catch(() => {});
const ev = document.getElementById('gev-' + id);
if (ev) ev.classList.add('acked');
_guardianUnread = Math.max(0, _guardianUnread - 1);
_updateGuardianBadge(_guardianUnread, 0);
}
async function guardianAckAll() {
await api('arc?action=guardian_ack').catch(() => {});
loadGuardian();
}
function guardianSitrep() {
const input = document.getElementById('textInput');
if (input) { input.value = 'sitrep'; input.dispatchEvent(new KeyboardEvent('keydown', {key:'Enter',keyCode:13,bubbles:true})); }
}
function switchGuardianTab() {
const btn = document.getElementById('tab-btn-guardian');
if (btn) btn.click();
}
function startGuardianPolling() {
if (_guardianPollTimer) return;
_guardianPollTimer = setInterval(() => {
if (document.getElementById('tab-guardian')?.classList.contains('active')) loadGuardian();
else _refreshGuardianBadge();
}, 30000);
}
async function _refreshGuardianBadge() {
const s = await api('arc?action=guardian_status').catch(() => null);
if (!s) return;
const counts = s.counts || {};
_updateGuardianBadge(parseInt(counts.unread||0), parseInt(counts.critical_unread||0));
}
// Proactive chat polling — checks for guardian-injected messages every 30s
let _proactiveChatLastId = 0;
async function _pollProactiveChat() {
try {
const rows = await api('arc?action=guardian_chat').catch(() => []);
if (!Array.isArray(rows)) return;
for (const row of rows) {
if (row.id > _proactiveChatLastId) {
_proactiveChatLastId = row.id;
// Don't spam on first load — only show messages from last 5 min
const age = Date.now() - new Date(row.created_at + 'Z').getTime();
if (age < 300000) {
addMessage('jarvis', row.message);
speak(row.message);
}
}
}
} catch(e) {}
}
@@ -0,0 +1,345 @@
// ── CHAT HISTORY SEARCH ───────────────────────────────────────────────────────
function openSearchModal() {
document.getElementById('searchModal').style.display = 'flex';
document.getElementById('searchInput').focus();
}
function closeSearchModal() {
document.getElementById('searchModal').style.display = 'none';
document.getElementById('searchResults').innerHTML = '<div style="color:var(--text-dim);font-size:0.65rem;text-align:center;padding:20px">Type to search your JARVIS conversations</div>';
document.getElementById('searchInput').value = '';
}
async function runSearch() {
const q = document.getElementById('searchInput').value.trim();
if (!q) return;
const el = document.getElementById('searchResults');
el.innerHTML = '<div style="color:var(--text-dim);font-size:0.65rem;text-align:center;padding:20px">Searching...</div>';
try {
const d = await api('history?q=' + encodeURIComponent(q));
if (!d.results || !d.results.length) {
el.innerHTML = '<div style="color:var(--text-dim);font-size:0.65rem;text-align:center;padding:20px">No results for "' + q + '"</div>';
return;
}
el.innerHTML = d.results.map(r => {
const role = r.role === 'user' ? '👤' : '🤖';
const ts = new Date(r.created_at).toLocaleString('en-US', {month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'});
const snippet = r.content.length > 200 ? r.content.slice(0,197) + '…' : r.content;
return `<div style="background:rgba(0,212,255,0.04);border:1px solid var(--panel-border);border-radius:4px;padding:10px 12px">
<div style="display:flex;justify-content:space-between;margin-bottom:4px">
<span style="font-family:var(--font-display);font-size:0.55rem;letter-spacing:1px;color:var(--cyan)">${role} ${r.role.toUpperCase()}</span>
<span style="font-size:0.52rem;color:var(--text-dim)">${ts}</span>
</div>
<div style="font-size:0.68rem;color:var(--text-primary);line-height:1.4">${snippet.replace(/</g,'&lt;')}</div>
</div>`;
}).join('');
} catch(e) {
el.innerHTML = '<div style="color:var(--red);font-size:0.65rem;text-align:center;padding:20px">Search failed</div>';
}
}
document.getElementById('searchModal')?.addEventListener('click', e => {
if (e.target === document.getElementById('searchModal')) closeSearchModal();
});
// ── PROACTIVE SUGGESTIONS ────────────────────────────────────────────────────
const _shownSuggestions = new Set();
async function checkSuggestions() {
const d = await api('suggestions').catch(() => null);
if (!d || !d.suggestions || !d.suggestions.length) return;
for (const s of d.suggestions) {
const key = s.intent + ':' + d.hour + ':' + d.dow;
if (_shownSuggestions.has(key)) continue;
_shownSuggestions.add(key);
// Show as a soft suggestion chip in chat
const log = document.getElementById('chatLog');
const chip = document.createElement('div');
chip.style.cssText = 'display:flex;justify-content:flex-end;margin:4px 0';
chip.innerHTML = `<button onclick="sendSuggestion('${s.intent}',this)" style="background:rgba(0,212,255,0.06);border:1px solid rgba(0,212,255,0.25);border-radius:12px;color:var(--cyan);font-family:var(--font-display);font-size:0.52rem;letter-spacing:1px;padding:4px 12px;cursor:pointer;transition:all 0.2s" onmouseover="this.style.background='rgba(0,212,255,0.12)'" onmouseout="this.style.background='rgba(0,212,255,0.06)'">◈ ${s.prompt}</button>`;
log.appendChild(chip);
log.scrollTop = log.scrollHeight;
break; // show max one suggestion at a time
}
}
function sendSuggestion(intent, btn) {
btn.closest('div').remove();
const prompts = {
'network_scan': 'run a network scan',
'jellyfin_now_playing': 'what is playing on Jellyfin',
'ha_scene': 'what scenes are available',
'planner:briefing': 'daily briefing',
'vm_suggestions': 'VM resource suggestions',
'focus_mode': 'focus mode',
};
const msg = prompts[intent] || intent.replace(/_/g,' ');
document.getElementById('textInput').value = msg;
sendMessage();
}
// ── MOBILE PANEL SWITCHER ─────────────────────────────────────────────────────
function mobSwitch(which) {
if (window.innerWidth > 900) return;
const panels = {left:'leftPanel', center:'centerPanel', right:'rightPanel'};
Object.entries(panels).forEach(([k, id]) => {
document.getElementById(id)?.classList.toggle('mob-active', k === which);
});
document.querySelectorAll('.mob-nav-btn').forEach(b => b.classList.remove('active'));
document.getElementById('mob-btn-' + which)?.classList.add('active');
if (which === 'right') loadNews();
}
function initMobile() {
if (window.innerWidth > 900) return;
['leftPanel','centerPanel','rightPanel'].forEach(id =>
document.getElementById(id)?.classList.remove('mob-active'));
document.getElementById('leftPanel')?.classList.add('mob-active');
document.querySelectorAll('.mob-nav-btn').forEach(b => b.classList.remove('active'));
document.getElementById('mob-btn-left')?.classList.add('active');
}
window.addEventListener('resize', initMobile);
// ── COMMAND PALETTE (Ctrl+K) ──────────────────────────────────────────────
const _PALETTE_COMMANDS = [
{ label: 'Run a network scan', q: 'run a network scan', group: 'Network' },
{ label: 'Show online devices', q: 'who is online on the network', group: 'Network' },
{ label: 'Proxmox status', q: 'proxmox status', group: 'Network' },
{ label: 'Check agent status', q: 'check all agents', group: 'Agents' },
{ label: 'Restart JARVIS agent', q: 'restart jarvis agent', group: 'Agents' },
{ label: 'Check VM resources', q: 'VM resource suggestions', group: 'Agents' },
{ label: 'Daily briefing', q: 'daily briefing', group: 'Planner' },
{ label: 'My tasks today', q: 'my tasks today', group: 'Planner' },
{ label: 'My calendar', q: 'my calendar', group: 'Planner' },
{ label: "What's playing on Jellyfin", q: 'what is playing on Jellyfin', group: 'Media' },
{ label: 'Pause Jellyfin', q: 'pause Jellyfin', group: 'Media' },
{ label: 'Next track on Jellyfin', q: 'next track on Jellyfin', group: 'Media' },
{ label: 'Stop Jellyfin', q: 'stop Jellyfin', group: 'Media' },
{ label: 'List HA scenes', q: 'show home assistant scenes', group: 'Smart Home'},
{ label: 'Activate scene…', q: 'activate scene ', group: 'Smart Home'},
{ label: 'Focus mode', q: 'focus mode', group: 'UI' },
{ label: 'Show all panels', q: 'show all panels', group: 'UI' },
{ label: 'Check alerts', q: 'check alerts', group: 'System' },
{ label: 'Site health', q: 'site health', group: 'System' },
{ label: 'System status', q: 'system status', group: 'System' },
{ label: 'Check inbox', q: 'check inbox', group: 'Comms' },
{ label: 'Search history…', q: '', group: 'Chat', search: true },
];
let _paletteOpen = false;
function openPalette() {
if (_paletteOpen) return;
_paletteOpen = true;
const ov = document.getElementById('cmdPalette');
if (!ov) return;
ov.style.display = 'flex';
const inp = document.getElementById('cmdPaletteInput');
inp.value = '';
renderPaletteItems('');
requestAnimationFrame(() => { ov.classList.add('open'); inp.focus(); });
}
function closePalette() {
if (!_paletteOpen) return;
_paletteOpen = false;
const ov = document.getElementById('cmdPalette');
if (!ov) return;
ov.classList.remove('open');
setTimeout(() => { ov.style.display = 'none'; }, 180);
}
function renderPaletteItems(q) {
const list = document.getElementById('cmdPaletteList');
if (!list) return;
const low = q.toLowerCase().trim();
const filtered = low
? _PALETTE_COMMANDS.filter(c => c.label.toLowerCase().includes(low) || c.group.toLowerCase().includes(low))
: _PALETTE_COMMANDS;
let currentGroup = null;
list.innerHTML = '';
filtered.forEach((cmd, i) => {
if (cmd.group !== currentGroup) {
currentGroup = cmd.group;
const g = document.createElement('div');
g.className = 'cp-group';
g.textContent = cmd.group;
list.appendChild(g);
}
const row = document.createElement('div');
row.className = 'cp-item' + (i === 0 ? ' cp-active' : '');
row.dataset.q = cmd.q;
row.dataset.search = cmd.search ? '1' : '';
const lbl = cmd.label.replace(new RegExp(low.replace(/[.*+?^${}()|[\]\\]/g,'\\$&'), 'gi'),
m => `<mark>${m}</mark>`);
row.innerHTML = `<span class="cp-icon">◈</span><span class="cp-label">${lbl}</span><kbd class="cp-kbd">↵</kbd>`;
row.addEventListener('click', () => firePaletteItem(row));
list.appendChild(row);
});
}
function movePaletteSelection(dir) {
const items = Array.from(document.querySelectorAll('#cmdPaletteList .cp-item'));
if (!items.length) return;
const cur = items.findIndex(el => el.classList.contains('cp-active'));
const next = (cur + dir + items.length) % items.length;
items.forEach(el => el.classList.remove('cp-active'));
items[next].classList.add('cp-active');
items[next].scrollIntoView({ block: 'nearest' });
}
function firePaletteItem(el) {
if (!el) {
const active = document.querySelector('#cmdPaletteList .cp-active');
if (!active) return;
el = active;
}
const q = el.dataset.q;
const isSearch = el.dataset.search === '1';
closePalette();
if (isSearch) {
if (typeof openSearchModal === 'function') openSearchModal();
return;
}
if (q) {
document.getElementById('textInput').value = q;
sendMessage();
}
}
// Keyboard events
document.addEventListener('keydown', e => {
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
_paletteOpen ? closePalette() : openPalette();
return;
}
if (!_paletteOpen) return;
if (e.key === 'Escape') { e.preventDefault(); closePalette(); }
if (e.key === 'ArrowDown') { e.preventDefault(); movePaletteSelection(1); }
if (e.key === 'ArrowUp') { e.preventDefault(); movePaletteSelection(-1); }
if (e.key === 'Enter') { e.preventDefault(); firePaletteItem(null); }
});
// Filter on type
document.getElementById('cmdPaletteInput')?.addEventListener('input', e => {
renderPaletteItems(e.target.value);
});
// Close on backdrop click
document.getElementById('cmdPalette')?.addEventListener('click', e => {
if (e.target.id === 'cmdPalette') closePalette();
});
// ── AGENT TOPOLOGY MAP ─────────────────────────────────────────────────────────────
let _agentTopoMode = false, _agentTopoRaf = null, _agentTopoData = [];
function toggleAgentTopo() {
_agentTopoMode = !_agentTopoMode;
const btn = document.getElementById('agent-topo-btn');
const list = document.getElementById('agents-list');
const cvs = document.getElementById('agentTopoCanvas');
if (!btn || !list || !cvs) return;
btn.classList.toggle('active', _agentTopoMode);
if (_agentTopoMode) {
list.style.display = 'none'; cvs.style.display = 'block';
_buildAgentTopoData(); _drawAgentTopo();
} else {
list.style.display = 'block'; cvs.style.display = 'none';
if (_agentTopoRaf) { cancelAnimationFrame(_agentTopoRaf); _agentTopoRaf = null; }
}
}
function _buildAgentTopoData() {
// Build node list from rendered agent cards
_agentTopoData = [{id:'jarvis',label:'JARVIS',online:true,type:'hub'}];
document.querySelectorAll('.agent-card').forEach(el => {
const nameEl = el.querySelector('.agent-name, [class*="name"]');
if (!nameEl) return;
const name = nameEl.textContent.trim();
const online = el.classList.contains('online') || !!el.querySelector('.agent-dot.online, .dot.online');
const lname = name.toLowerCase();
let type = 'linux';
if (lname.includes('pve') || lname.includes('proxmox') || el.querySelector('[class*="proxmox"]')) type = 'proxmox';
else if (lname.includes('ha') || lname.includes('homeassist')) type = 'homeassistant';
else if (lname.includes('windows') || lname.includes('mini')) type = 'windows';
_agentTopoData.push({id:name, label:name.substring(0,12), online, type});
});
// Fallback: use last known registered agent list if cards not rendered
if (_agentTopoData.length <= 1 && typeof _lastAgents !== 'undefined') {
(_lastAgents || []).forEach(a => {
_agentTopoData.push({id:a.agent_id,label:(a.hostname||a.agent_id).substring(0,12),online:a.status==='online',type:a.agent_type||'linux'});
});
}
}
function _drawAgentTopo() {
const cvs = document.getElementById('agentTopoCanvas');
if (!cvs || !_agentTopoMode) return;
const ctx = cvs.getContext('2d');
const rect = cvs.getBoundingClientRect();
const W = rect.width || 280, H = rect.height || 260;
const dpr = window.devicePixelRatio || 1;
cvs.width = W * dpr; cvs.height = H * dpr;
ctx.scale(dpr, dpr);
const typeRing = {hub:0, proxmox:0.28, homeassistant:0.48, linux:0.68, windows:0.68};
const typeColor = {hub:'0,212,255', proxmox:'0,255,136', homeassistant:'255,215,0', linux:'0,190,255', windows:'180,120,255'};
// Assign positions
const byType = {};
_agentTopoData.slice(1).forEach(n => { (byType[n.type]=byType[n.type]||[]).push(n); });
_agentTopoData[0].x = W/2; _agentTopoData[0].y = H/2;
Object.entries(byType).forEach(([tp, nodes]) => {
const rf = typeRing[tp] || 0.68;
const r = Math.min(W, H) / 2 * rf;
nodes.forEach((n, i) => {
const a = -Math.PI/2 + (i / nodes.length) * Math.PI * 2;
n.x = W/2 + Math.cos(a)*r; n.y = H/2 + Math.sin(a)*r;
});
});
let t = 0;
function frame() {
if (!_agentTopoMode) return;
t += 0.007; ctx.clearRect(0, 0, W, H);
// Orbit rings
[0.28, 0.48, 0.68].forEach(rf => {
ctx.beginPath(); ctx.arc(W/2, H/2, Math.min(W,H)/2*rf, 0, Math.PI*2);
ctx.strokeStyle = 'rgba(0,212,255,0.05)'; ctx.lineWidth = 0.5; ctx.stroke();
});
// Edges
_agentTopoData.slice(1).forEach(n => {
if (!n.x) return;
const col = typeColor[n.type] || '0,190,255';
ctx.beginPath(); ctx.moveTo(W/2, H/2); ctx.lineTo(n.x, n.y);
ctx.strokeStyle = n.online ? 'rgba('+col+',0.18)' : 'rgba(255,50,80,0.08)';
ctx.lineWidth = n.online ? 1 : 0.5; ctx.stroke();
});
// Particles
_agentTopoData.slice(1).filter(n=>n.online&&n.x).forEach((n,i) => {
const p = ((t*0.35+i*0.41)%1);
const col = typeColor[n.type]||'0,190,255';
const px = W/2+(n.x-W/2)*p, py = H/2+(n.y-H/2)*p;
ctx.beginPath(); ctx.arc(px,py,1.4,0,Math.PI*2);
ctx.fillStyle='rgba('+col+',0.75)'; ctx.fill();
});
// Nodes
_agentTopoData.forEach((n,i) => {
if (!n.x) return;
const col = typeColor[n.type]||'0,190,255';
const nr = n.type==='hub' ? 13 : 7;
const pulse = Math.sin(t+i*0.9)*0.25+0.75;
if (n.online||n.type==='hub') {
const g = ctx.createRadialGradient(n.x,n.y,0,n.x,n.y,nr*3.5);
g.addColorStop(0,'rgba('+col+','+(0.15*pulse)+')');
g.addColorStop(1,'transparent');
ctx.beginPath(); ctx.arc(n.x,n.y,nr*3.5,0,Math.PI*2);
ctx.fillStyle=g; ctx.fill();
}
ctx.beginPath(); ctx.arc(n.x,n.y,nr,0,Math.PI*2);
ctx.fillStyle = n.online||n.type==='hub' ? 'rgba('+col+',0.9)' : 'rgba(255,50,80,0.5)';
ctx.fill();
ctx.strokeStyle='rgba('+col+',0.6)'; ctx.lineWidth=1; ctx.stroke();
ctx.fillStyle = n.online||n.type==='hub' ? 'rgba('+col+',0.85)' : 'rgba(255,80,80,0.7)';
ctx.font = (n.type==='hub'?'600 8px':'6px')+' "Share Tech Mono",monospace';
ctx.textAlign='center';
ctx.fillText(n.label, n.x, n.y+nr+9);
});
_agentTopoRaf = requestAnimationFrame(frame);
}
frame();
}
+24 -14
View File
@@ -53,7 +53,7 @@
<div class="tb-center"> <div class="tb-center">
<div class="tb-stat">LOCAL&nbsp;<span id="tb-cpu">--</span>% CPU</div> <div class="tb-stat">LOCAL&nbsp;<span id="tb-cpu">--</span>% CPU</div>
<div class="tb-stat">MEM&nbsp;<span id="tb-mem">--</span>%</div> <div class="tb-stat">MEM&nbsp;<span id="tb-mem">--</span>%</div>
<div class="tb-stat">DO SERVER&nbsp;<span id="tb-do" class="text-dim">--</span></div> <div class="tb-stat">JARVIS VM&nbsp;<span id="tb-do" class="text-dim">--</span></div>
<div class="tb-stat"><span id="tb-alerts" class="text-green">NO ALERTS</span></div> <div class="tb-stat"><span id="tb-alerts" class="text-green">NO ALERTS</span></div>
<div class="tb-stat" id="tb-planner" style="display:none"><span id="tb-planner-text" class="text-yellow"></span></div> <div class="tb-stat" id="tb-planner" style="display:none"><span id="tb-planner-text" class="text-yellow"></span></div>
</div> </div>
@@ -65,6 +65,7 @@
<div class="status-dot"></div> <div class="status-dot"></div>
<button id="cameraBtn" class="btn-camera" onclick="toggleCamera()" title="Auto-mic when face detected (hands-free)">◉ CAMERA</button> <button id="cameraBtn" class="btn-camera" onclick="toggleCamera()" title="Auto-mic when face detected (hands-free)">◉ CAMERA</button>
<button id="panelToggleBtn" class="btn-panels" onclick="togglePanels()" title="Toggle side panels (or say 'focus mode')">◧ PANELS</button> <button id="panelToggleBtn" class="btn-panels" onclick="togglePanels()" title="Toggle side panels (or say 'focus mode')">◧ PANELS</button>
<button id="kioskBtn" class="btn-panels" onclick="toggleKiosk()" title="Full-screen kiosk mode">⛶ KIOSK</button>
<button id="agentBtn" class="btn-agent" onclick="openAgentModal()" title="Install JARVIS Agent on this machine"><div class="agent-dot"></div>AGENT</button> <button id="agentBtn" class="btn-agent" onclick="openAgentModal()" title="Install JARVIS Agent on this machine"><div class="agent-dot"></div>AGENT</button>
<div id="themeBar" style="display:flex;gap:3px;align-items:center;margin-right:2px"> <div id="themeBar" style="display:flex;gap:3px;align-items:center;margin-right:2px">
@@ -102,8 +103,8 @@
</div> </div>
<div id="weather-forecast" style="display:grid;grid-template-columns:repeat(4,1fr);gap:4px"></div> <div id="weather-forecast" style="display:grid;grid-template-columns:repeat(4,1fr);gap:4px"></div>
</div> </div>
<div class="panel"> <div class="panel" id="server-panel">
<div class="panel-title">JARVIS SERVER <span style="font-size:0.5rem;color:var(--text-dim)">165.22.1.228</span><div class="indicator"></div></div> <div class="panel-title">JARVIS SERVER <span style="font-size:0.5rem;color:var(--text-dim)">10.48.200.211</span><div class="indicator"></div></div>
<!-- Metric bars + sparklines --> <!-- Metric bars + sparklines -->
<div class="metric-row"> <div class="metric-row">
@@ -143,6 +144,13 @@
<div class="loading-shimmer" style="margin-bottom:4px"></div> <div class="loading-shimmer" style="margin-bottom:4px"></div>
</div> </div>
</div> </div>
<!-- Web Host (DO Server) -->
<div style="font-family:var(--font-display);font-size:0.5rem;letter-spacing:2px;color:var(--text-dim);margin:10px 0 5px">WEB HOST <span id="do-host-status" style="color:var(--green)"></span></div>
<div id="do-host-stats" style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px;font-family:var(--font-mono);font-size:0.62rem">
<div><div style="color:var(--text-dim);font-size:0.52rem">CPU</div><div id="do-cpu">--%</div></div>
<div><div style="color:var(--text-dim);font-size:0.52rem">RAM</div><div id="do-mem">--%</div></div>
<div><div style="color:var(--text-dim);font-size:0.52rem">DISK</div><div id="do-disk">--%</div></div>
</div>
</div> </div>
<!-- CENTER: Arc Reactor + Chat --> <!-- CENTER: Arc Reactor + Chat -->
@@ -195,7 +203,7 @@
<div id="rightPanel"> <div id="rightPanel">
<!-- Network Status --> <!-- Network Status -->
<div class="panel" style="flex:0 1 auto;max-height:35%;display:flex;flex-direction:column;min-height:100px"> <div class="panel" id="network-status-panel" style="flex:0 1 auto;max-height:35%;display:flex;flex-direction:column;min-height:100px">
<div class="panel-title">NETWORK STATUS <div class="indicator"></div><span id="net-agent-count" style="font-size:0.6rem;color:var(--cyan);margin-left:auto"></span><button onclick="addNetworkDevice()" title="Add device" style="background:none;border:none;color:var(--cyan);cursor:pointer;font-size:1rem;padding:0 4px;margin-left:4px;line-height:1">+</button></div> <div class="panel-title">NETWORK STATUS <div class="indicator"></div><span id="net-agent-count" style="font-size:0.6rem;color:var(--cyan);margin-left:auto"></span><button onclick="addNetworkDevice()" title="Add device" style="background:none;border:none;color:var(--cyan);cursor:pointer;font-size:1rem;padding:0 4px;margin-left:4px;line-height:1">+</button></div>
<canvas id="topoCanvas" height="100"></canvas> <canvas id="topoCanvas" height="100"></canvas>
<div id="network-list" style="overflow-y:auto;flex:1;padding-right:2px"> <div id="network-list" style="overflow-y:auto;flex:1;padding-right:2px">
@@ -220,7 +228,7 @@
<div class="tab active" onclick="switchTab('ha')">HOME</div> <div class="tab active" onclick="switchTab('ha')">HOME</div>
<div class="tab" onclick="switchTab('alerts')">ALERTS</div> <div class="tab" onclick="switchTab('alerts')">ALERTS</div>
<div class="tab" onclick="switchTab('news')">NEWS</div> <div class="tab" onclick="switchTab('news')">NEWS</div>
<div class="tab" onclick="switchTab('agents')">AGENTS</div> <div class="tab" id="tab-btn-agents" onclick="switchTab('agents')">AGENTS</div>
<div class="tab" onclick="switchTab('sites')">SITES</div> <div class="tab" onclick="switchTab('sites')">SITES</div>
<div class="tab" id="tab-btn-intel" onclick="switchTab('intel')">INTEL</div> <div class="tab" id="tab-btn-intel" onclick="switchTab('intel')">INTEL</div>
<div class="tab" id="tab-btn-comms" onclick="switchTab('comms')">COMMS</div> <div class="tab" id="tab-btn-comms" onclick="switchTab('comms')">COMMS</div>
@@ -292,17 +300,17 @@
</div> </div>
<div class="bb-item"> <div class="bb-item">
<div class="bb-dot" id="bb-do-dot"></div> <div class="bb-dot" id="bb-do-dot"></div>
<span>DO SERVER</span> <span id="bb-do-status">CHECKING</span> <span>JARVIS VM</span> <span id="bb-do-status">CHECKING</span>
</div> </div>
<div class="bb-item"> <div class="bb-item" id="bb-pve-item">
<div class="bb-dot" id="bb-pve-dot"></div> <div class="bb-dot" id="bb-pve-dot"></div>
<span>PROXMOX</span> <span id="bb-pve-status">CHECKING</span> <span>PROXMOX</span> <span id="bb-pve-status">CHECKING</span>
</div> </div>
<div class="bb-item"> <div class="bb-item" id="bb-ha-item">
<div class="bb-dot" id="bb-ha-dot"></div> <div class="bb-dot" id="bb-ha-dot"></div>
<span>HOME ASSISTANT</span> <span id="bb-ha-status">CHECKING</span> <span>HOME ASSISTANT</span> <span id="bb-ha-status">CHECKING</span>
</div> </div>
<div class="bb-item"> <div class="bb-item" id="bb-agents-item">
<div class="bb-dot" id="bb-agent-dot"></div> <div class="bb-dot" id="bb-agent-dot"></div>
<span>AGENTS</span> <span id="bb-agent-status">--</span> <span>AGENTS</span> <span id="bb-agent-status">--</span>
</div> </div>
@@ -416,12 +424,14 @@
<!-- Hidden camera feed for face detection --> <!-- Hidden camera feed for face detection -->
<video id="faceVideo" autoplay muted playsinline <video id="faceVideo" autoplay muted playsinline
style="position:fixed;top:-9999px;left:-9999px;width:320px;height:240px"></video> style="position:fixed;top:-9999px;left:-9999px;width:320px;height:240px"></video>
<script src="https://cdn.jsdelivr.net/npm/face-api.js@0.22.2/dist/face-api.min.js" crossorigin="anonymous"></script> <script data-cfasync="false" src="https://cdn.jsdelivr.net/npm/face-api.js@0.22.2/dist/face-api.min.js" crossorigin="anonymous"></script>
<script data-cfasync="false" src="assets/js/jarvis-effects.js?v=20260617"></script> <script data-cfasync="false" src="assets/js/jarvis-effects.js?v=20260621k"></script>
<script data-cfasync="false" src="assets/js/jarvis-overlays.js?v=20260617"></script> <script data-cfasync="false" src="assets/js/jarvis-overlays.js?v=20260621k"></script>
<script data-cfasync="false" src="assets/js/jarvis-app.js?v=20260617"></script> <script data-cfasync="false" src="assets/js/jarvis-app.js?v=20260621k"></script>
<script data-cfasync="false" src="assets/js/jarvis-protocols.js?v=20260617"></script> <script data-cfasync="false" src="assets/js/panels/jarvis-arc.js?v=20260621k"></script>
<script data-cfasync="false" src="assets/js/panels/jarvis-agents.js?v=20260621k"></script>
<script data-cfasync="false" src="assets/js/panels/jarvis-assistant.js?v=20260621k"></script>
<!-- VISION LIGHTBOX --> <!-- VISION LIGHTBOX -->
<div id="vision-lightbox"> <div id="vision-lightbox">
+5 -25
View File
@@ -13,8 +13,8 @@ if (!defined('WEBHOOK_SECRET')) {
echo json_encode(['error' => 'Webhook not configured']); echo json_encode(['error' => 'Webhook not configured']);
exit; exit;
} }
define('DEPLOY_QUEUE', '/tmp/jarvis-deploy-queue.txt'); define('DEPLOY_QUEUE', '/tmp/jarvis-deploy-queue.txt');
define('DEPLOY_LOG', '/home/jarvis.orbishosting.com/logs/deploy.log'); define('DEPLOY_LOG', '/var/www/jarvis/logs/deploy.log');
header('Content-Type: application/json'); header('Content-Type: application/json');
@@ -40,16 +40,7 @@ if ($ref !== 'refs/heads/main') {
} }
$repoMap = [ $repoMap = [
'jarvis' => '/home/jarvis.orbishosting.com', 'jarvis' => '/var/www/jarvis',
'tomsjavajive' => '/home/tomsjavajive.com/public_html',
'epictravelexpeditions' => '/home/epictravelexpeditions.com/public_html',
'parkerslingshot' => '/home/epictravelexpeditions.com/parkerslingshot',
'parkerslingshotrentals' => '/home/parkerslingshotrentals.com/public_html',
'orbishosting' => '/home/orbishosting.com/public_html',
'orbis-hosting-portal' => '/home/orbis.orbishosting.com/public_html',
'tomtomgames' => '/home/tomtomgames.com/public_html',
'infra' => '/opt/infra',
'novacpx' => '__NOVACPX_VM__',
]; ];
if (!isset($repoMap[$repo])) { if (!isset($repoMap[$repo])) {
@@ -59,20 +50,9 @@ if (!isset($repoMap[$repo])) {
} }
$path = $repoMap[$repo]; $path = $repoMap[$repo];
$ts = date('Y-m-d H:i:s');
// NovaCPX lives on a private VM — the VM polls GitHub every minute via cron
// This webhook receipt confirms GitHub delivered the push notification
if ($path === '__NOVACPX_VM__') {
$commit = $data['after'] ?? 'HEAD';
$msg = "[" . date('Y-m-d H:i:s') . "] NovaCPX push by $pusher (commit: $commit) — VM will deploy within 1 min";
file_put_contents(DEPLOY_LOG, $msg . "\n", FILE_APPEND | LOCK_EX);
echo json_encode(['ok' => true, 'queued' => 'novacpx', 'commit' => $commit]);
exit;
}
file_put_contents(DEPLOY_QUEUE, $path . "\n", FILE_APPEND | LOCK_EX); 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);
$msg = "[" . date('Y-m-d H:i:s') . "] Queued deploy: $repo by $pusher -> $path";
file_put_contents(DEPLOY_LOG, $msg . "\n", FILE_APPEND | LOCK_EX);
echo json_encode(['ok' => true, 'queued' => $repo, 'path' => $path]); echo json_encode(['ok' => true, 'queued' => $repo, 'path' => $path]);