diff --git a/api/endpoints/chat.php b/api/endpoints/chat.php index 7d2f878..3d1b5b2 100644 --- a/api/endpoints/chat.php +++ b/api/endpoints/chat.php @@ -1102,6 +1102,93 @@ if (!$reply) { 'Automatic scan via PVE1 runs every 3 minutes.'); $source = 'intent:network_scan'; break; + + case 'alerts_show': + $activeAlerts = JarvisDB::query( + "SELECT title, severity, message FROM alerts WHERE resolved=0 ORDER BY created_at DESC LIMIT 10" + ); + if (!$activeAlerts) { + $reply = "No active alerts, {$userAddr}. All systems appear nominal."; + } else { + $lines = array_map(fn($a) => "[{$a['severity']}] {$a['title']}: {$a['message']}", $activeAlerts); + $reply = count($activeAlerts) . " active alert" . (count($activeAlerts)>1?'s':'') . ", {$userAddr}: " . implode('; ', $lines) . '.'; + } + $source = 'intent:alerts_show'; + break; + + case 'alerts_count': + $alertCount = JarvisDB::single("SELECT COUNT(*) cnt FROM alerts WHERE resolved=0"); + $cnt = (int)($alertCount['cnt'] ?? 0); + $reply = $cnt > 0 + ? "There are currently {$cnt} unresolved alert" . ($cnt>1?'s':'') . ", {$userAddr}. Say 'show alerts' for details." + : "No active alerts at this time, {$userAddr}. All systems nominal."; + $source = 'intent:alerts_count'; + break; + + case 'alerts_clear': + $cleared = JarvisDB::single("SELECT COUNT(*) cnt FROM alerts WHERE resolved=0"); + JarvisDB::execute("UPDATE alerts SET resolved=1 WHERE resolved=0"); + $cnt = (int)($cleared['cnt'] ?? 0); + $reply = "Resolved {$cnt} alert" . ($cnt!==1?'s':'') . ", {$userAddr}. Alert panel cleared."; + $source = 'intent:alerts_clear'; + break; + + case 'agents_offline': + $offline = JarvisDB::query( + "SELECT hostname, ip_address, agent_type FROM registered_agents WHERE status='offline' ORDER BY last_seen DESC LIMIT 10" + ); + if (!$offline) { + $reply = "All registered agents are currently online, {$userAddr}."; + } else { + $names = array_map(fn($a) => $a['hostname'] . ' (' . $a['ip_address'] . ')', $offline); + $reply = count($offline) . " agent" . (count($offline)>1?'s are':' is') . " offline, {$userAddr}: " . implode(', ', $names) . '.'; + } + $source = 'intent:agents_offline'; + break; + + case 'agents_all': + $allAgents = JarvisDB::query( + "SELECT hostname, ip_address, status, agent_type FROM registered_agents ORDER BY FIELD(status,'online','offline','unknown'), hostname ASC" + ); + if (!$allAgents) { + $reply = "No registered agents found, {$userAddr}."; + } else { + $onlineList = array_filter($allAgents, fn($a) => $a['status'] === 'online'); + $offlineList = array_filter($allAgents, fn($a) => $a['status'] !== 'online'); + $reply = count($allAgents) . " registered agents — " . count($onlineList) . " online, " . count($offlineList) . " offline, {$userAddr}."; + if ($onlineList) $reply .= ' Online: ' . implode(', ', array_map(fn($a) => $a['hostname'], $onlineList)) . '.'; + if ($offlineList) $reply .= ' Offline: ' . implode(', ', array_map(fn($a) => $a['hostname'], $offlineList)) . '.'; + } + $source = 'intent:agents_all'; + break; + + case 'agents_count': + $agentStats = JarvisDB::single( + "SELECT COUNT(*) total, SUM(status='online') online FROM registered_agents" + ); + $t = (int)($agentStats['total'] ?? 0); + $o = (int)($agentStats['online'] ?? 0); + $reply = "{$t} agents registered — {$o} online, " . ($t-$o) . " offline, {$userAddr}."; + $source = 'intent:agents_count'; + break; + + case 'deploy_status': + $deployLog = '/home/jarvis.orbishosting.com/logs/deploy.log'; + if (file_exists($deployLog)) { + $lines = array_filter(array_map('trim', array_slice(file($deployLog), -20))); + $recent = array_slice(array_values($lines), -5); + $last = end($recent); + $reply = "Last deploy entry, {$userAddr}: " . htmlspecialchars_decode(strip_tags($last)) . '. Say "deploy log" to see the full recent history.'; + } else { + $reply = "Deploy log not found, {$userAddr}. Check /home/jarvis.orbishosting.com/logs/deploy.log on the server."; + } + $source = 'intent:deploy_status'; + break; + + case 'deploy_force': + $reply = "Manual deploy is triggered by pushing to the GitHub main branch, {$userAddr}. The webhook at jarvis.orbishosting.com/webhook.php handles it automatically within 60 seconds. To hot-fix without a push, SCP the file directly to the server."; + $source = 'intent:deploy_force'; + break; } } } diff --git a/public_html/admin/index.php b/public_html/admin/index.php index 835d0c9..3b13a0d 100644 --- a/public_html/admin/index.php +++ b/public_html/admin/index.php @@ -778,9 +778,23 @@ select.filter-sel:focus{border-color:var(--cyan)}
KB INTENTS
+
+
+ + + +
SCANNING...
@@ -1398,11 +1412,34 @@ function factModal(id=0, category='', key='', value='') { } // ── KB INTENTS ──────────────────────────────────────────────────────────────── +let _allIntents = []; + async function loadIntents() { scanShell('intents-tbl', ['NAME','PATTERN','RESPONSE','TYPE','PRI','STATUS','ACTIONS'], null, null); - const intents = await api('intents_list'); - const cntEl=document.getElementById('intents-count'); if(cntEl) cntEl.textContent=intents.length.toLocaleString()+' INTENTS'; - if (!intents.length) { document.getElementById('intents-tbl').innerHTML='
NO INTENTS
'; return; } + _allIntents = await api('intents_list'); + const cntEl=document.getElementById('intents-count'); if(cntEl) cntEl.textContent=_allIntents.length.toLocaleString()+' INTENTS'; + renderIntents(_allIntents); +} + +function filterIntents(q) { + q = (q||'').toLowerCase().trim(); + const typeFilter = (document.getElementById('intents-filter-type')?.value || '').toLowerCase(); + const statusFilter = document.getElementById('intents-filter-status')?.value ?? ''; + let filtered = _allIntents; + if (q) filtered = filtered.filter(i => + i.intent_name.toLowerCase().includes(q) || + (i.pattern||'').toLowerCase().includes(q) || + (i.response_template||'').toLowerCase().includes(q) + ); + if (typeFilter) filtered = filtered.filter(i => i.action_type === typeFilter); + if (statusFilter !== '') filtered = filtered.filter(i => String(i.active) === statusFilter); + const cntEl=document.getElementById('intents-count'); + if(cntEl) cntEl.textContent = (q||typeFilter||statusFilter!=='' ? filtered.length+'/'+_allIntents.length : _allIntents.length.toLocaleString())+' INTENTS'; + renderIntents(filtered); +} + +function renderIntents(intents) { + if (!intents.length) { document.getElementById('intents-tbl').innerHTML='
NO INTENTS MATCH
'; return; } document.getElementById('intents-tbl').innerHTML = `
NAMEPATTERNRESPONSETYPEPRISTATUSACTIONS
`; @@ -1439,6 +1476,39 @@ function intentModal(id=0, name='', pattern='', response='', type='response', pr }); } +function intentTestModal() { + openModal('TEST INTENT PATTERN', ` +
+ + +
+
Enter a phrase and click TEST or press Enter.
+ `, ()=>closeModal(), 'CLOSE'); + setTimeout(()=>document.getElementById('t-phrase')?.focus(), 60); +} + +function runIntentTest() { + const phrase = (document.getElementById('t-phrase')?.value || '').trim(); + if (!phrase) return; + const resultEl = document.getElementById('t-result'); + if (!resultEl) return; + // Sort by priority desc, id asc (same order as PHP KBEngine::match) + const sorted = [..._allIntents].filter(i=>i.active).sort((a,b)=> b.priority-a.priority || a.id-b.id); + let matched = null; + for (const i of sorted) { + try { + let pat = i.pattern.replace(/^\(\?i\)/, ''); + const re = new RegExp(pat, 'i'); + if (re.test(phrase)) { matched = i; break; } + } catch(e) { /* invalid regex, skip */ } + } + if (matched) { + resultEl.innerHTML = '✓ MATCHED: ' + esc(matched.intent_name) + '  (priority ' + matched.priority + ' · ' + matched.action_type + ')
Pattern: ' + esc(matched.pattern) + '
Response: ' + esc((matched.response_template||'[action handler in chat.php]').substring(0,300)); + } else { + resultEl.innerHTML = '✗ NO MATCH — this phrase falls through to Ollama → Groq → Claude.'; + } +} + // ── SITES ───────────────────────────────────────────────────────────────────── async function loadSites() { document.getElementById('sites-content').innerHTML='
SCANNING...
'; @@ -1489,16 +1559,18 @@ function userModal(id, display) { } // ── MODAL ───────────────────────────────────────────────────────────────────── -function openModal(title, body, saveCb) { +function openModal(title, body, saveCb, saveLabel) { document.getElementById('modalTitle').textContent = title; document.getElementById('modalBody').innerHTML = body; _modalCb = saveCb; + const saveBtn = document.getElementById('modalSave'); + if (saveBtn) saveBtn.textContent = saveLabel || 'SAVE'; document.getElementById('modalBg').classList.add('open'); const first = document.querySelector('#modalBody input, #modalBody textarea, #modalBody select'); if (first) setTimeout(()=>first.focus(), 50); } -function closeModal() { document.getElementById('modalBg').classList.remove('open'); _modalCb=null; } +function closeModal() { document.getElementById('modalBg').classList.remove('open'); _modalCb=null; const sb=document.getElementById('modalSave'); if(sb) sb.textContent='SAVE'; } function modalSave() { if (_modalCb) _modalCb(); } document.getElementById('modalBg').addEventListener('click', e => { if (e.target===document.getElementById('modalBg')) closeModal(); });