diff --git a/api/endpoints/chat.php b/api/endpoints/chat.php index 785bee8..b2a8558 100644 --- a/api/endpoints/chat.php +++ b/api/endpoints/chat.php @@ -1647,6 +1647,54 @@ if (!$reply) { if ($matched && $matched['action'] === 'action') { 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': { // Fetch all HA scenes and fuzzy-match against the message $haScenes = @json_decode(@file_get_contents( @@ -1681,7 +1729,7 @@ if (!$reply) { false, stream_context_create(['http' => [ 'method' => 'POST', - 'header' => "Authorization: Bearer " . HA_TOKEN . " + 'header' => "Authorization: Bearer " . HA_TOKEN . " Content-Type: application/json", 'content' => json_encode(['entity_id' => $best['entity_id']]), 'timeout' => 5, diff --git a/public_html/index.html b/public_html/index.html index 78f5b2a..66fc2ee 100644 --- a/public_html/index.html +++ b/public_html/index.html @@ -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)} .alert-item{cursor:pointer} .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: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)} @@ -3439,7 +3448,21 @@ function switchTab(name) { } // ── 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 = `${label}`; + return s; +} + +function addMessage(role, text, source=null) { const log = document.getElementById('chatLog'); const div = document.createElement('div'); div.className = 'msg ' + role; @@ -3459,6 +3482,8 @@ function addMessage(role, text) { setTimeout(type, msPerChar + (text[i-1] === '.' || text[i-1] === ',' ? msPerChar * 4 : 0)); } else { cursor.remove(); + const badge = sourceBadge(source); + if (badge) div.appendChild(badge); } }; setTimeout(type, 0); @@ -3556,7 +3581,7 @@ async function sendMessage() { if (bubble) bubble.remove(); if (data.reply) { - addMessage('jarvis', data.reply); + addMessage('jarvis', data.reply, data.source || null); speak(data.reply); } if (data.open_network_map) { openNetMap(); }