mirror of
https://github.com/myronblair/jarvis
synced 2026-06-30 17:50:23 -05:00
Fix intent placeholders, add alert/agent/deploy action intents, admin search+tester
- Restore {user_title} in ~200 intents where placeholder was stripped (comma-period, em-dash variants)
- Replace 20 hardcoded Mr. Blair with {user_title} token
- Remove duplicate pve_storage and tech_ssh intents
- Add action intents: alerts_show, alerts_count, alerts_clear, agents_offline, agents_all, agents_count, deploy_status, deploy_force, pbx_status, pbx_extension
- Admin KB Intents: add search/filter bar (name/pattern/response + type/status dropdowns)
- Admin KB Intents: add TEST PATTERN button — tests any phrase client-side against full intent list
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -778,9 +778,23 @@ select.filter-sel:focus{border-color:var(--cyan)}
|
||||
<div class="page-title">KB INTENTS <span id="intents-count" style="color:var(--cyan);font-size:0.6rem;letter-spacing:2px"></span>
|
||||
<div class="actions">
|
||||
<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" onclick="loadIntents()">REFRESH</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;margin-bottom:10px;align-items:center">
|
||||
<input id="intents-search" type="text" placeholder="Filter by name, pattern, or response…" style="flex:1;background:var(--bg2);border:1px solid var(--border);color:var(--fg);padding:6px 10px;font-size:0.75rem;border-radius:3px;font-family:inherit" oninput="filterIntents(this.value)">
|
||||
<select id="intents-filter-type" style="background:var(--bg2);border:1px solid var(--border);color:var(--fg);padding:6px 8px;font-size:0.75rem;border-radius:3px;font-family:inherit" onchange="filterIntents(document.getElementById('intents-search').value)">
|
||||
<option value="">ALL TYPES</option>
|
||||
<option value="response">RESPONSE</option>
|
||||
<option value="action">ACTION</option>
|
||||
</select>
|
||||
<select id="intents-filter-status" style="background:var(--bg2);border:1px solid var(--border);color:var(--fg);padding:6px 8px;font-size:0.75rem;border-radius:3px;font-family:inherit" onchange="filterIntents(document.getElementById('intents-search').value)">
|
||||
<option value="">ALL STATUS</option>
|
||||
<option value="1">ACTIVE</option>
|
||||
<option value="0">DISABLED</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="tbl-wrap" id="intents-tbl"><div class="loading">SCANNING...</div></div>
|
||||
</div>
|
||||
|
||||
@@ -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='<div class="empty">NO INTENTS</div>'; 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='<div class="empty">NO INTENTS MATCH</div>'; return; }
|
||||
document.getElementById('intents-tbl').innerHTML = `<table>
|
||||
<thead><tr><th>NAME</th><th>PATTERN</th><th>RESPONSE</th><th>TYPE</th><th style="text-align:center">PRI</th><th>STATUS</th><th>ACTIONS</th></tr></thead>
|
||||
<tbody id="intents-tbl-tbody"></tbody></table>`;
|
||||
@@ -1439,6 +1476,39 @@ function intentModal(id=0, name='', pattern='', response='', type='response', pr
|
||||
});
|
||||
}
|
||||
|
||||
function intentTestModal() {
|
||||
openModal('TEST INTENT PATTERN', `
|
||||
<div class="form-row" style="display:flex;gap:8px;align-items:center">
|
||||
<input id="t-phrase" placeholder="say something JARVIS should handle…" style="flex:1" onkeydown="if(event.key==='Enter')runIntentTest()">
|
||||
<button class="btn btn-sm btn-yellow" onclick="runIntentTest()">TEST</button>
|
||||
</div>
|
||||
<div id="t-result" style="margin-top:12px;padding:10px;background:var(--bg2);border:1px solid var(--border);border-radius:3px;min-height:60px;font-size:0.75rem;color:var(--fg2);line-height:1.6">Enter a phrase and click TEST or press Enter.</div>
|
||||
`, ()=>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 = '<span style="color:var(--green)">✓ MATCHED:</span> <strong>' + esc(matched.intent_name) + '</strong> (priority ' + matched.priority + ' · ' + matched.action_type + ')<br><span style="color:var(--yellow)">Pattern:</span> <code style="color:var(--yellow)">' + esc(matched.pattern) + '</code><br><span style="color:var(--cyan)">Response:</span> ' + esc((matched.response_template||'[action handler in chat.php]').substring(0,300));
|
||||
} else {
|
||||
resultEl.innerHTML = '<span style="color:var(--red)">✗ NO MATCH</span> — this phrase falls through to Ollama → Groq → Claude.';
|
||||
}
|
||||
}
|
||||
|
||||
// ── SITES ─────────────────────────────────────────────────────────────────────
|
||||
async function loadSites() {
|
||||
document.getElementById('sites-content').innerHTML='<div class="loading">SCANNING...</div>';
|
||||
@@ -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(); });
|
||||
|
||||
Reference in New Issue
Block a user