mirror of
https://github.com/myronblair/jarvis
synced 2026-06-30 17:50:23 -05:00
feat: tier source badge + VM resource suggestions
- Tier badge (#9): addMessage() gains source param; sourceBadge() maps source string to KB/GROQ/CLAUDE/OLLAMA pill rendered after typing finishes; sendMessage() passes data.source through; CSS badges styled with domain colors - VM suggestions (#10): vm_suggestions intent queries 24h avg CPU+MEM per agent, flags hosts with >80% avg CPU, <5% CPU on 2GB+ RAM, >85% avg MEM, or <25% MEM on 2GB+ RAM; 8 KB intents; say "VM resource suggestions" or "optimize VMs" Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+49
-1
@@ -1647,6 +1647,54 @@ if (!$reply) {
|
|||||||
if ($matched && $matched['action'] === 'action') {
|
if ($matched && $matched['action'] === 'action') {
|
||||||
switch ($matched['intent']) {
|
switch ($matched['intent']) {
|
||||||
|
|
||||||
|
case 'vm_suggestions': {
|
||||||
|
$rows = JarvisDB::query(
|
||||||
|
"SELECT a.hostname, a.agent_type,
|
||||||
|
ROUND(AVG(CAST(JSON_EXTRACT(m.metric_data,'$.cpu_percent') AS DECIMAL(5,1))),1) as avg_cpu,
|
||||||
|
ROUND(AVG(CAST(JSON_EXTRACT(m.metric_data,'$.memory.percent') AS DECIMAL(5,1))),1) as avg_mem,
|
||||||
|
ROUND(MAX(CAST(JSON_EXTRACT(m.metric_data,'$.memory.total_mb') AS UNSIGNED))/1024,1) as ram_gb,
|
||||||
|
COUNT(*) as samples
|
||||||
|
FROM agent_metrics m
|
||||||
|
JOIN registered_agents a ON a.agent_id = m.agent_id
|
||||||
|
WHERE m.recorded_at > DATE_SUB(NOW(), INTERVAL 24 HOUR)
|
||||||
|
AND a.agent_type IN ('linux','proxmox')
|
||||||
|
GROUP BY a.hostname, a.agent_type
|
||||||
|
HAVING samples > 20
|
||||||
|
ORDER BY avg_mem DESC"
|
||||||
|
) ?? [];
|
||||||
|
|
||||||
|
$suggestions = [];
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$cpu = (float)($r['avg_cpu'] ?? 0);
|
||||||
|
$mem = (float)($r['avg_mem'] ?? 0);
|
||||||
|
$ram = (float)($r['ram_gb'] ?? 0);
|
||||||
|
$host = $r['hostname'];
|
||||||
|
if (!$ram) continue;
|
||||||
|
|
||||||
|
// High CPU
|
||||||
|
if ($cpu >= 80)
|
||||||
|
$suggestions[] = "{$host} is averaging {$cpu}% CPU — consider increasing vCPU allocation or investigating load.";
|
||||||
|
// Low CPU + reasonable RAM (suggest checking allocation)
|
||||||
|
elseif ($cpu < 5 && $ram >= 2)
|
||||||
|
$suggestions[] = "{$host} averages only {$cpu}% CPU — vCPU allocation may be generous.";
|
||||||
|
|
||||||
|
// High MEM
|
||||||
|
if ($mem >= 85)
|
||||||
|
$suggestions[] = "{$host} is averaging {$mem}% memory use ({$ram}GB allocated) — consider increasing RAM.";
|
||||||
|
// Low MEM
|
||||||
|
elseif ($mem < 25 && $ram >= 2)
|
||||||
|
$suggestions[] = "{$host} uses only {$mem}% of its {$ram}GB RAM on average — allocation could be reduced.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$suggestions) {
|
||||||
|
$reply = "All VM resources look well-balanced, {$userAddr}. No over or under-allocation detected across the past 24 hours.";
|
||||||
|
} else {
|
||||||
|
$reply = "Resource analysis for the past 24 hours, {$userAddr}: " . implode(' ', $suggestions);
|
||||||
|
}
|
||||||
|
$source = 'intent:vm_suggestions';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'ha_scene': {
|
case 'ha_scene': {
|
||||||
// Fetch all HA scenes and fuzzy-match against the message
|
// Fetch all HA scenes and fuzzy-match against the message
|
||||||
$haScenes = @json_decode(@file_get_contents(
|
$haScenes = @json_decode(@file_get_contents(
|
||||||
@@ -1681,7 +1729,7 @@ if (!$reply) {
|
|||||||
false,
|
false,
|
||||||
stream_context_create(['http' => [
|
stream_context_create(['http' => [
|
||||||
'method' => 'POST',
|
'method' => 'POST',
|
||||||
'header' => "Authorization: Bearer " . HA_TOKEN . "
|
'header' => "Authorization: Bearer " . HA_TOKEN . "
|
||||||
Content-Type: application/json",
|
Content-Type: application/json",
|
||||||
'content' => json_encode(['entity_id' => $best['entity_id']]),
|
'content' => json_encode(['entity_id' => $best['entity_id']]),
|
||||||
'timeout' => 5,
|
'timeout' => 5,
|
||||||
|
|||||||
+27
-2
@@ -586,6 +586,15 @@ body::after{
|
|||||||
.device-item.ctx-active{background:rgba(0,212,255,0.1);border-color:rgba(0,212,255,0.4)}
|
.device-item.ctx-active{background:rgba(0,212,255,0.1);border-color:rgba(0,212,255,0.4)}
|
||||||
.alert-item{cursor:pointer}
|
.alert-item{cursor:pointer}
|
||||||
.alert-item.ctx-active{border-color:var(--cyan) !important;box-shadow:0 0 8px rgba(0,212,255,0.15)}
|
.alert-item.ctx-active{border-color:var(--cyan) !important;box-shadow:0 0 8px rgba(0,212,255,0.15)}
|
||||||
|
.tier-badge{
|
||||||
|
display:inline-block;font-family:var(--font-display);font-size:0.42rem;
|
||||||
|
letter-spacing:1.5px;padding:1px 5px;border-radius:2px;margin-top:4px;
|
||||||
|
opacity:0.7;border:1px solid;vertical-align:middle;
|
||||||
|
}
|
||||||
|
.tier-badge.kb {color:#00d4ff;border-color:rgba(0,212,255,0.4);background:rgba(0,212,255,0.06)}
|
||||||
|
.tier-badge.groq {color:#f5a623;border-color:rgba(245,166,35,0.4);background:rgba(245,166,35,0.06)}
|
||||||
|
.tier-badge.claude{color:#b57cf5;border-color:rgba(181,124,245,0.4);background:rgba(181,124,245,0.06)}
|
||||||
|
.tier-badge.ollama{color:#7ef55a;border-color:rgba(126,245,90,0.4);background:rgba(126,245,90,0.06)}
|
||||||
.news-item{cursor:pointer;transition:background 0.15s}
|
.news-item{cursor:pointer;transition:background 0.15s}
|
||||||
.news-item:hover{background:rgba(0,212,255,0.04)}
|
.news-item:hover{background:rgba(0,212,255,0.04)}
|
||||||
.news-item.ctx-active{background:rgba(0,212,255,0.08);border-color:rgba(0,212,255,0.4)}
|
.news-item.ctx-active{background:rgba(0,212,255,0.08);border-color:rgba(0,212,255,0.4)}
|
||||||
@@ -3439,7 +3448,21 @@ function switchTab(name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── CHAT ──────────────────────────────────────────────────────────────
|
// ── CHAT ──────────────────────────────────────────────────────────────
|
||||||
function addMessage(role, text) {
|
function sourceBadge(source) {
|
||||||
|
if (!source) return '';
|
||||||
|
let cls, label;
|
||||||
|
if (/^intent:|^planner:|^kb:/.test(source)) { cls = 'kb'; label = 'KB'; }
|
||||||
|
else if (/^groq:/.test(source)) { cls = 'groq'; label = 'GROQ'; }
|
||||||
|
else if (source === 'claude' || /^claude/.test(source)) { cls = 'claude'; label = 'CLAUDE'; }
|
||||||
|
else if (/^ollama/.test(source)) { cls = 'ollama'; label = 'LOCAL AI'; }
|
||||||
|
else return '';
|
||||||
|
const s = document.createElement('div');
|
||||||
|
s.style.cssText = 'margin-top:4px;text-align:right';
|
||||||
|
s.innerHTML = `<span class="tier-badge ${cls}">${label}</span>`;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addMessage(role, text, source=null) {
|
||||||
const log = document.getElementById('chatLog');
|
const log = document.getElementById('chatLog');
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'msg ' + role;
|
div.className = 'msg ' + role;
|
||||||
@@ -3459,6 +3482,8 @@ function addMessage(role, text) {
|
|||||||
setTimeout(type, msPerChar + (text[i-1] === '.' || text[i-1] === ',' ? msPerChar * 4 : 0));
|
setTimeout(type, msPerChar + (text[i-1] === '.' || text[i-1] === ',' ? msPerChar * 4 : 0));
|
||||||
} else {
|
} else {
|
||||||
cursor.remove();
|
cursor.remove();
|
||||||
|
const badge = sourceBadge(source);
|
||||||
|
if (badge) div.appendChild(badge);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
setTimeout(type, 0);
|
setTimeout(type, 0);
|
||||||
@@ -3556,7 +3581,7 @@ async function sendMessage() {
|
|||||||
if (bubble) bubble.remove();
|
if (bubble) bubble.remove();
|
||||||
|
|
||||||
if (data.reply) {
|
if (data.reply) {
|
||||||
addMessage('jarvis', data.reply);
|
addMessage('jarvis', data.reply, data.source || null);
|
||||||
speak(data.reply);
|
speak(data.reply);
|
||||||
}
|
}
|
||||||
if (data.open_network_map) { openNetMap(); }
|
if (data.open_network_map) { openNetMap(); }
|
||||||
|
|||||||
Reference in New Issue
Block a user