mirror of
https://github.com/myronblair/jarvis
synced 2026-07-29 09:12:36 -05:00
Compare commits
4 Commits
af03a2f2d8
...
2f74b98bbc
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f74b98bbc | |||
| ed15ff12dd | |||
| 3f18cec739 | |||
| 8911645c20 |
@@ -479,3 +479,58 @@ CREATE TABLE IF NOT EXISTS `guardian_events` (
|
|||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
-- Dump completed on 2026-06-29 23:15:44
|
-- 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;
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# JARVIS database backup — runs daily, 7-day retention
|
||||||
|
# Lives in repo at /var/www/jarvis/deploy/jarvis-backup.sh
|
||||||
|
|
||||||
|
BACKUP_DIR="/var/backups/jarvis"
|
||||||
|
LOG="/var/log/jarvis/backup.log"
|
||||||
|
DB_NAME="jarvis_db"
|
||||||
|
DB_USER="jarvis_user"
|
||||||
|
DB_PASS="J4rv1s_Pr0t0c0l_2026!"
|
||||||
|
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
|
||||||
|
OUTFILE="$BACKUP_DIR/${DB_NAME}_${TIMESTAMP}.sql.gz"
|
||||||
|
|
||||||
|
mkdir -p "$BACKUP_DIR"
|
||||||
|
|
||||||
|
echo "[$(date)] Starting backup..." >> "$LOG"
|
||||||
|
|
||||||
|
if mysqldump -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" | gzip > "$OUTFILE"; then
|
||||||
|
SIZE=$(du -sh "$OUTFILE" | cut -f1)
|
||||||
|
echo "[$(date)] Backup OK: $OUTFILE ($SIZE)" >> "$LOG"
|
||||||
|
else
|
||||||
|
echo "[$(date)] ERROR: mysqldump failed" >> "$LOG"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 7-day retention
|
||||||
|
find "$BACKUP_DIR" -name "${DB_NAME}_*.sql.gz" -mtime +7 -delete
|
||||||
|
echo "[$(date)] Cleanup done. Files kept: $(ls $BACKUP_DIR | wc -l)" >> "$LOG"
|
||||||
+67
-107
@@ -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.210:11434"
|
OLLAMA_HOST = "http://10.48.200.210:11434"
|
||||||
OLLAMA_MODEL = "llama3.1:8b"
|
OLLAMA_MODEL = "llama3.1:8b"
|
||||||
|
OLLAMA_VISION_MODEL = os.environ.get("OLLAMA_VISION_MODEL", "") # e.g. "llava" or "moondream" -- empty = disabled
|
||||||
|
|
||||||
GMAIL_USER = "myronblair@gmail.com"
|
GMAIL_USER = "myronblair@gmail.com"
|
||||||
GMAIL_PASS = "demsvdylwweacbcx"
|
GMAIL_PASS = "demsvdylwweacbcx"
|
||||||
@@ -175,15 +176,56 @@ 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", "")
|
||||||
|
|
||||||
async def _ollama_vision_call(image_b64: str, prompt: str) -> str:
|
|
||||||
async with aiohttp.ClientSession() as session:
|
# -- VISION CALL ----------------------------------------------------------
|
||||||
async with session.post(
|
async def _vision_call(image_b64: str, prompt: str) -> tuple:
|
||||||
f"{OLLAMA_HOST}/api/generate",
|
"""
|
||||||
json={"model": "llava:7b", "prompt": prompt, "images": [image_b64], "stream": False},
|
Vision provider cascade: Claude -> Ollama vision model -> graceful fallback.
|
||||||
timeout=aiohttp.ClientTimeout(total=120),
|
Returns (analysis: str, provider: str).
|
||||||
) as resp:
|
To enable a local vision model: set OLLAMA_VISION_MODEL env var (e.g. "llava").
|
||||||
data = await resp.json()
|
"""
|
||||||
return data.get("response", "").strip()
|
# 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", "")
|
||||||
@@ -668,44 +710,19 @@ async def handle_screenshot(payload: dict) -> dict:
|
|||||||
height = result.get("height", 0)
|
height = result.get("height", 0)
|
||||||
file_size = result.get("file_size", 0)
|
file_size = result.get("file_size", 0)
|
||||||
|
|
||||||
# Run vision analysis if we have an image — llava first, Claude fallback
|
# Run Claude vision analysis if we have an image
|
||||||
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)
|
||||||
analysis = await _ollama_vision_call(image_b64, analyze_prompt)
|
log.info(f"[VISION] Analysis complete via {provider_used} ({len(analysis)} chars)")
|
||||||
provider_used = "ollama:llava"
|
|
||||||
log.info(f"[VISION] llava analysis complete ({len(analysis)} chars)")
|
|
||||||
except Exception as e:
|
|
||||||
log.warning(f"[VISION] llava failed: {e} — falling back to Claude")
|
|
||||||
try:
|
|
||||||
import anthropic
|
|
||||||
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 fallback analysis complete ({len(analysis)} chars)")
|
|
||||||
except Exception as e2:
|
|
||||||
log.warning(f"[VISION] Claude fallback also failed: {e2}")
|
|
||||||
analysis = f"Vision analysis unavailable: {e2}"
|
|
||||||
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 Ollama
|
# 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}], "ollama")
|
analysis = await llm_call([{"role": "user", "content": prompt}], "groq")
|
||||||
provider_used = "ollama"
|
provider_used = "groq"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
analysis = f"Analysis unavailable: {e}"
|
analysis = f"Analysis unavailable: {e}"
|
||||||
|
|
||||||
@@ -758,41 +775,9 @@ async def handle_vision(payload: dict) -> dict:
|
|||||||
if not image_b64:
|
if not image_b64:
|
||||||
raise ValueError("No image data provided")
|
raise ValueError("No image data provided")
|
||||||
|
|
||||||
log.info(f"[VISION] Analysis: screenshot_id={screenshot_id} agent={hostname} provider={provider}")
|
log.info(f"[VISION] Analysis: screenshot_id={screenshot_id} agent={hostname}")
|
||||||
|
|
||||||
analysis = ""
|
analysis, provider_used = await _vision_call(image_b64, prompt)
|
||||||
provider_used = provider
|
|
||||||
|
|
||||||
if provider == "ollama" or provider == "llava":
|
|
||||||
analysis = await _ollama_vision_call(image_b64, prompt)
|
|
||||||
provider_used = "ollama:llava"
|
|
||||||
else:
|
|
||||||
# Default: llava first, Claude fallback
|
|
||||||
try:
|
|
||||||
analysis = await _ollama_vision_call(image_b64, prompt)
|
|
||||||
provider_used = "ollama:llava"
|
|
||||||
log.info(f"[VISION] llava analysis complete ({len(analysis)} chars)")
|
|
||||||
except Exception as e:
|
|
||||||
log.warning(f"[VISION] llava failed: {e} — falling back to Claude")
|
|
||||||
try:
|
|
||||||
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 ""
|
|
||||||
provider_used = "claude"
|
|
||||||
except Exception as e2:
|
|
||||||
raise RuntimeError(f"Vision analysis failed (llava: {e}, claude: {e2})")
|
|
||||||
|
|
||||||
# Update stored screenshot if we have an ID
|
# Update stored screenshot if we have an ID
|
||||||
if screenshot_id:
|
if screenshot_id:
|
||||||
@@ -1053,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}], "ollama")
|
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
|
||||||
@@ -1082,7 +1067,7 @@ async def _guardian_inject_chat(message: str) -> None:
|
|||||||
"""Write a proactive JARVIS message into the conversations table so the HUD picks it up."""
|
"""Write a proactive JARVIS message into the conversations table so the HUD picks it up."""
|
||||||
try:
|
try:
|
||||||
await db_execute(
|
await db_execute(
|
||||||
"""INSERT INTO conversations (session_id, role, content, created_at)
|
"""INSERT INTO conversations (session_id, role, message, created_at)
|
||||||
VALUES ('guardian', 'assistant', %s, NOW())""",
|
VALUES ('guardian', 'assistant', %s, NOW())""",
|
||||||
(message,)
|
(message,)
|
||||||
)
|
)
|
||||||
@@ -1096,7 +1081,7 @@ async def handle_sitrep(payload: dict) -> dict:
|
|||||||
payload: { detail: brief|full, provider: groq }
|
payload: { detail: brief|full, provider: groq }
|
||||||
"""
|
"""
|
||||||
detail = payload.get("detail", "full")
|
detail = payload.get("detail", "full")
|
||||||
provider = payload.get("provider", "ollama")
|
provider = payload.get("provider", "groq")
|
||||||
|
|
||||||
log.info(f"[GUARDIAN] SITREP requested (detail={detail})")
|
log.info(f"[GUARDIAN] SITREP requested (detail={detail})")
|
||||||
|
|
||||||
@@ -1837,7 +1822,7 @@ Keep the tone confident and action-oriented. Format with clear sections. Under 4
|
|||||||
|
|
||||||
# Store in conversations so HUD can surface it
|
# Store in conversations so HUD can surface it
|
||||||
await db_execute(
|
await db_execute(
|
||||||
"INSERT INTO conversations (session_id, role, content, created_at) VALUES ('guardian','assistant',%s,NOW())",
|
"INSERT INTO conversations (session_id, role, message, created_at) VALUES ('guardian','assistant',%s,NOW())",
|
||||||
(review_text,)
|
(review_text,)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2240,31 +2225,6 @@ async def lifespan(app: FastAPI):
|
|||||||
log.info(f"◈ JARVIS Arc Reactor v{VERSION} starting on {HOST}:{PORT}")
|
log.info(f"◈ JARVIS Arc Reactor v{VERSION} starting on {HOST}:{PORT}")
|
||||||
await get_pool()
|
await get_pool()
|
||||||
await db_execute("UPDATE arc_status SET started_at=NOW(), last_heartbeat=NOW(), version=%s WHERE id=1", (VERSION,))
|
await db_execute("UPDATE arc_status SET started_at=NOW(), last_heartbeat=NOW(), version=%s WHERE id=1", (VERSION,))
|
||||||
await db_execute("""CREATE TABLE IF NOT EXISTS guardian_config (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
key_name VARCHAR(64) NOT NULL,
|
|
||||||
value VARCHAR(255) NOT NULL DEFAULT '',
|
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
UNIQUE KEY uk_key (key_name)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci""")
|
|
||||||
await db_execute("""CREATE TABLE IF NOT EXISTS guardian_events (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
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,
|
|
||||||
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""")
|
|
||||||
asyncio.create_task(job_poller())
|
asyncio.create_task(job_poller())
|
||||||
asyncio.create_task(heartbeat_loop())
|
asyncio.create_task(heartbeat_loop())
|
||||||
asyncio.create_task(guardian_loop())
|
asyncio.create_task(guardian_loop())
|
||||||
@@ -2784,13 +2744,13 @@ async def guardian_chat_events(since: str = ""):
|
|||||||
"""Return proactive guardian messages injected into conversations."""
|
"""Return proactive guardian messages injected into conversations."""
|
||||||
if since:
|
if since:
|
||||||
rows = await db_fetchall(
|
rows = await db_fetchall(
|
||||||
"SELECT id, content AS message, created_at FROM conversations "
|
"SELECT id, message, created_at FROM conversations "
|
||||||
"WHERE session_id='guardian' AND created_at > %s ORDER BY created_at ASC",
|
"WHERE session_id='guardian' AND created_at > %s ORDER BY created_at ASC",
|
||||||
(since,)
|
(since,)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
rows = await db_fetchall(
|
rows = await db_fetchall(
|
||||||
"SELECT id, content AS message, created_at FROM conversations "
|
"SELECT id, message, created_at FROM conversations "
|
||||||
"WHERE session_id='guardian' ORDER BY created_at DESC LIMIT 10"
|
"WHERE session_id='guardian' ORDER BY created_at DESC LIMIT 10"
|
||||||
)
|
)
|
||||||
return rows or []
|
return rows or []
|
||||||
|
|||||||
+176
-38
@@ -242,9 +242,7 @@ if ($action) {
|
|||||||
$search = strtolower(trim($_GET['search'] ?? ''));
|
$search = strtolower(trim($_GET['search'] ?? ''));
|
||||||
$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','media_player','scene','water_heater',
|
'assist_satellite','input_button'];
|
||||||
'alarm_control_panel','automation','script','calendar','notify',
|
|
||||||
'weather','camera','siren','remote','todo','lawn_mower'];
|
|
||||||
$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_',
|
||||||
'do_not_disturb','matter_server','zerotier','mariadb',
|
'do_not_disturb','matter_server','zerotier','mariadb',
|
||||||
@@ -489,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)) {
|
||||||
@@ -520,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']);
|
||||||
@@ -535,39 +534,61 @@ 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'],
|
||||||
|
'kb_intent_generator' =>[true, '/var/www/jarvis/api/endpoints/kb_intent_generator.php'],
|
||||||
'jarvis_deploy' =>[false,'/usr/local/bin/jarvis-deploy.sh'],
|
'jarvis_deploy' =>[false,'/usr/local/bin/jarvis-deploy.sh'],
|
||||||
'jarvis_watchdog'=>[false,'/usr/local/bin/jarvis-watchdog.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];
|
||||||
$cmd = $isPhp
|
$cmd = $isPhp
|
||||||
? '/usr/local/lsws/lsphp85/bin/lsphp '.escapeshellarg($path).' >> /home/jarvis.orbishosting.com/logs/cron.log 2>&1 &'
|
? '/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('bash -c "mkdir -p /home/jarvis.orbishosting.com/logs; systemctl restart jarvis-arc 2>/dev/null || (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') {
|
} elseif ($wType === 'daemon' && $wId === 'arc_reactor' && $wAction === 'setup') {
|
||||||
$setupLog = '/home/jarvis.orbishosting.com/logs/arc_reactor.log';
|
$log = '/var/log/jarvis/arc-setup.log';
|
||||||
$cmd = 'bash -c "'.
|
$cmd = implode(' && ', [
|
||||||
'mkdir -p /opt/jarvis-arc /home/jarvis.orbishosting.com/logs && '.
|
'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/reactor.py /opt/jarvis-arc/reactor.py',
|
||||||
'cp /var/www/jarvis/deploy/requirements.txt /opt/jarvis-arc/requirements.txt && '.
|
'cp /var/www/jarvis/deploy/requirements.txt /opt/jarvis-arc/requirements.txt',
|
||||||
'if [ ! -d /opt/jarvis-arc/venv ]; then python3 -m venv /opt/jarvis-arc/venv; fi && '.
|
'[ ! -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 && '.
|
'/opt/jarvis-arc/venv/bin/pip install -q -r /opt/jarvis-arc/requirements.txt',
|
||||||
'pkill -f reactor.py 2>/dev/null; sleep 1 && '.
|
'cp /var/www/jarvis/deploy/jarvis-arc.service /etc/systemd/system/jarvis-arc.service',
|
||||||
'cd /opt/jarvis-arc && source venv/bin/activate && '.
|
'systemctl daemon-reload',
|
||||||
'nohup python3 reactor.py >> '.$setupLog.' 2>&1 &'.
|
'systemctl enable jarvis-arc',
|
||||||
'" >> '.$setupLog.' 2>&1';
|
'systemctl restart jarvis-arc',
|
||||||
shell_exec($cmd);
|
]);
|
||||||
j(['ok'=>true,'msg'=>'Arc Reactor setup started — check log in ~30s then restart']);
|
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_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_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]);
|
||||||
@@ -1442,6 +1463,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()">▶ 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">
|
||||||
@@ -2174,7 +2196,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)';
|
||||||
@@ -2201,12 +2224,127 @@ 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() {
|
||||||
|
// Open the working popup
|
||||||
|
const modalHtml = `
|
||||||
|
<div style="font-family:var(--mono);font-size:0.72rem;line-height:1.8">
|
||||||
|
<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:340px;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('▶ KB INTENT GENERATOR', modalHtml, null, null);
|
||||||
|
document.getElementById('modalSave').textContent = 'CLOSE';
|
||||||
|
document.getElementById('modalSave').onclick = () => { _igStop = true; closeModal(); };
|
||||||
|
|
||||||
|
const logEl = document.getElementById('ig-log');
|
||||||
|
const statEl = document.getElementById('ig-status');
|
||||||
|
const statsEl = document.getElementById('ig-stats');
|
||||||
|
|
||||||
|
let _igStop = false;
|
||||||
|
let lastLine = 0;
|
||||||
|
let dotted = 0;
|
||||||
|
|
||||||
|
// Snapshot current log position BEFORE triggering so we only see new lines
|
||||||
|
const snap = await api('intent_gen_log', {snapshot:1});
|
||||||
|
lastLine = snap?.next_line ?? 0;
|
||||||
|
|
||||||
|
// Kick off the generator (fire-and-forget in background on server)
|
||||||
|
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...';
|
||||||
|
|
||||||
|
// Poll the cron log for new output
|
||||||
|
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'))
|
||||||
|
div.style.color = 'var(--green)';
|
||||||
|
else
|
||||||
|
div.style.color = 'var(--text-dim)';
|
||||||
|
div.textContent = line;
|
||||||
|
logEl.appendChild(div);
|
||||||
|
});
|
||||||
|
logEl.scrollTop = logEl.scrollHeight;
|
||||||
|
|
||||||
|
// Detect completion
|
||||||
|
const lastLines = res.lines.join(' ').toLowerCase();
|
||||||
|
if (lastLines.includes('kb intent generator: done') || lastLines.includes('total inserted') || lastLines.includes('cleanup complete')) {
|
||||||
|
statEl.style.color = 'var(--green)';
|
||||||
|
statEl.textContent = 'COMPLETE';
|
||||||
|
// Extract stats from log
|
||||||
|
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>`;
|
||||||
|
return; // stop polling
|
||||||
|
}
|
||||||
|
dotted = 0;
|
||||||
|
} else {
|
||||||
|
// No new lines yet — show a waiting dot
|
||||||
|
dotted++;
|
||||||
|
if (dotted < 30) { // Stop waiting after ~90s with no output
|
||||||
|
const waitDiv = document.createElement('div');
|
||||||
|
waitDiv.style.color = 'rgba(0,212,255,0.3)';
|
||||||
|
waitDiv.textContent = '· waiting for output' + '.'.repeat(dotted % 4);
|
||||||
|
// Replace last waiting line instead of appending
|
||||||
|
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) {
|
||||||
|
// network error — keep trying
|
||||||
|
}
|
||||||
|
if (!_igStop) setTimeout(pollLog, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(pollLog, 2000); // give server 2s head-start
|
||||||
|
}
|
||||||
|
|
||||||
|
async function arcSetup() {
|
||||||
|
if (!confirm('Run Arc Reactor setup?\n\nThis will:\n• Copy reactor.py from deploy/\n• Install Python packages\n• Install/update systemd service\n• Restart jarvis-arc')) return;
|
||||||
|
const btn = event.currentTarget;
|
||||||
|
btn.disabled = true; btn.textContent = '⏳ SETTING UP...';
|
||||||
|
try {
|
||||||
|
const d = await api('worker', {type:'daemon', id:'arc_reactor', action:'setup'});
|
||||||
|
toast(d.msg || (d.ok ? 'Setup started' : 'Setup failed'), d.ok ? 'ok' : 'err');
|
||||||
|
setTimeout(() => { loadWorkers(); btn.disabled=false; btn.innerHTML='⚙ SETUP'; }, 5000);
|
||||||
|
} catch(e) {
|
||||||
|
toast('Setup failed: ' + e.message, 'err');
|
||||||
|
btn.disabled=false; btn.innerHTML='⚙ SETUP';
|
||||||
|
}
|
||||||
|
}
|
||||||
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);
|
||||||
}
|
}
|
||||||
@@ -2231,7 +2369,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 = '';
|
||||||
@@ -2242,7 +2380,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;
|
||||||
@@ -2346,9 +2484,9 @@ 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 style="display:flex;gap:4px">
|
<td style="white-space:nowrap">
|
||||||
<button onclick="workerAction('daemon','arc_reactor','restart')" style="${wBtn('red')}">↻ RESTART</button>
|
<button onclick="workerAction('daemon','arc_reactor','restart')" style="${wBtn('red')}">↻ RESTART</button>
|
||||||
<button onclick="workerAction('daemon','arc_reactor','setup')" style="${wBtn('orange')}">⚙ SETUP</button>
|
<button onclick="arcSetup()" style="${wBtn('cyan')};margin-left:6px">⚙ SETUP</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user