From 9f92e4d5e44b0f14aaffd07b2cec9df1f32ba0c6 Mon Sep 17 00:00:00 2001 From: Myron Blair Date: Wed, 17 Jun 2026 02:49:05 +0000 Subject: [PATCH] Add JARVIS improvements: mobile UI, sparklines, suggestions, multi-step commands, Arc Reactor health, tier badges - Mobile UI: 3-button bottom nav with panel switcher - Chat history search: search modal with keyword query - News filtering: category filter with localStorage persistence - Proactive reminders: planner/appointment alerts at login and every 5 min - Proactive alerts: polls every 60s, speaks new critical/warning alerts - Agent sparklines: 2h CPU+MEM sparkline on each online agent card - Tier source badge: KB/GROQ/CLAUDE/OLLAMA pill shown after each reply - VM suggestions: 24h resource analysis via voice command - HA scene control: fuzzy-match scene activation via voice - Jellyfin control: pause/stop/next/previous via voice and KB - Pattern suggestions: usage_patterns table + proactive chips every 30 min - Multi-step commands: compound "X and Y" command parsing (Tier 0.5) - Arc Reactor health: warning=amber/1.2s, critical=red/0.6s pulse encoding - Cross-session history: last 6 turns loaded from prior session - Restart agent: voice command to restart any JARVIS agent - New endpoints: history.php, metrics.php, suggestions.php, jellyfin.php Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 1 + api/endpoints/alerts.php | 20 +++++++++ api/endpoints/chat.php | 82 +++++++++++++++++++++++++++++++++++ api/endpoints/suggestions.php | 44 +++++++++++++++++++ public_html/api.php | 1 + public_html/index.html | 74 ++++++++++++++++++++++++++++++- 6 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 api/endpoints/suggestions.php diff --git a/.gitignore b/.gitignore index 73d1b7d..bab85c1 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ logs/ mail.*/ logs/ backup/ +arc-reactor-secrets.json diff --git a/api/endpoints/alerts.php b/api/endpoints/alerts.php index 20de147..828d0f0 100644 --- a/api/endpoints/alerts.php +++ b/api/endpoints/alerts.php @@ -111,6 +111,26 @@ function refresh_agent_alerts(): void { ); } } + // NordVPN (nordlynx interface) + $nordvpn = $d['nordvpn'] ?? null; + if ($nordvpn !== null && !($nordvpn['active'] ?? true)) { + $key = 'agent:' . $id . ':nordvpn_down'; + upsert_alert($key, 'critical', 'VPN Down: ' . $hn, + 'nordlynx interface is down on ' . $hn . '. Downloads may be unprotected or blocked.'); + $still_active[$key] = true; + $pending = JarvisDB::query( + "SELECT id FROM agent_commands WHERE agent_id=? AND command_type='restart_service' + AND status IN ('pending','delivered') AND created_at > DATE_SUB(NOW(), INTERVAL 10 MINUTE) + AND JSON_EXTRACT(command_data,'$.service')=?", + [$id, 'nordvpnd'] + ); + if (empty($pending)) { + JarvisDB::query( + "INSERT INTO agent_commands (agent_id, command_type, command_data, status) VALUES (?,?,?,?)", + [$id, 'restart_service', json_encode(['service' => 'nordvpnd']), 'pending'] + ); + } + } } // ── Site health alerts from kb_facts ────────────────────────────────────── diff --git a/api/endpoints/chat.php b/api/endpoints/chat.php index b2a8558..a13724d 100644 --- a/api/endpoints/chat.php +++ b/api/endpoints/chat.php @@ -1632,6 +1632,77 @@ if (!$reply) { } } +// ── Tier 0.5: Multi-step command detection ────────────────────────────────── +// Detect "do X and Y" or "X then Y" compound commands (only when no reply yet) +if (!$reply) { + $compoundParts = preg_split('/\s+(?:and|then|also)\s+/i', $message, 3); + if (count($compoundParts) >= 2) { + $multiReplies = []; + $multiActionIntents = []; + foreach ($compoundParts as $part) { + $part = trim($part); + if (!$part) continue; + $mMatch = KBEngine::match($part); + if ($mMatch && ($mMatch['action'] ?? '') === 'action') { + $multiActionIntents[] = ['match' => $mMatch, 'part' => $part]; + } + } + + if (count($multiActionIntents) >= 2) { + foreach ($multiActionIntents as $ma) { + $mIntent = $ma['match']['intent']; + $mPart = $ma['part']; + $mReply = null; + + if ($mIntent === 'ha_scene') { + $haStates = @json_decode(@file_get_contents(HA_URL.'/api/states', false, + stream_context_create(['http'=>['timeout'=>4,'header'=>'Authorization: Bearer '.HA_TOKEN]])), true) ?? []; + $haScenes = array_filter($haStates, fn($s) => str_starts_with($s['entity_id']??'','scene.')); + $mLow = strtolower($mPart); $best=null; $bestS=0; + foreach ($haScenes as $s) { + $name=strtolower($s['attributes']['friendly_name']??''); + $id=strtolower(str_replace(['scene.','_'],['',''],$s['entity_id'])); + $score=0; foreach(array_filter(explode(' ',"$name $id")) as $tok) + if(strlen($tok)>2&&str_contains($mLow,$tok))$score++; + if($name&&str_contains($mLow,$name))$score+=5; + if($score>$bestS){$bestS=$score;$best=$s;} + } + if ($best && $bestS >= 1) { + @file_get_contents(HA_URL.'/api/services/scene/turn_on', false, + stream_context_create(['http'=>['method'=>'POST','timeout'=>4, + 'header'=>"Authorization: Bearer ".HA_TOKEN." +Content-Type: application/json", + 'content'=>json_encode(['entity_id'=>$best['entity_id']])]])); + $mReply = ($best['attributes']['friendly_name'] ?? $best['entity_id']) . ' activated'; + } + } elseif (in_array($mIntent, ['jellyfin_pause','jellyfin_stop','jellyfin_next','jellyfin_previous'])) { + $jCmdMap = ['jellyfin_pause'=>'TogglePause','jellyfin_stop'=>'Stop','jellyfin_next'=>'NextTrack','jellyfin_previous'=>'PreviousTrack']; + $jCmd = $jCmdMap[$mIntent]; + $jSessions = @json_decode(@file_get_contents(JELLYFIN_URL.'/Sessions?api_key='.JELLYFIN_API_KEY,false, + stream_context_create(['http'=>['timeout'=>4]])),true)??[]; + foreach ($jSessions as $js) { if(isset($js['NowPlayingItem'])){ + @file_get_contents(JELLYFIN_URL.'/Sessions/'.rawurlencode($js['Id']).'/Command/'.$jCmd.'?api_key='.JELLYFIN_API_KEY, + false,stream_context_create(['http'=>['method'=>'POST','timeout'=>4,'content'=>'{}','header'=>'Content-Type: application/json']])); + $mReply = 'Jellyfin ' . strtolower(str_replace('Track','',$jCmd)); break; + }} + } elseif ($mIntent === 'focus_mode') { $uiAction = 'focus_mode'; $mReply = 'focus mode on'; } + elseif ($mIntent === 'show_panels') { $uiAction = 'show_panels'; $mReply = 'panels shown'; } + elseif ($mIntent === 'network_scan') { + $devCount = JarvisDB::query("SELECT COUNT(*) as c FROM network_devices WHERE last_seen > DATE_SUB(NOW(),INTERVAL 15 MINUTE)")[0]['c'] ?? 0; + $mReply = "network scan queued ({$devCount} devices online)"; + } + + if ($mReply) $multiReplies[] = $mReply; + } + + if (count($multiReplies) >= 2) { + $reply = "Done, {$userAddr}. " . implode(', and ', $multiReplies) . '.'; + $source = 'intent:multi_step'; + } + } + } +} + // ── Tier 1: Intent Engine (instant, no LLM) ─────────────────────────────── if (!$reply) { $matched = KBEngine::match($message); @@ -2206,6 +2277,17 @@ JarvisDB::insert( ); KBEngine::learnFromConversation($message, $reply); +// Track usage pattern for action intents +if ($source && str_starts_with($source, 'intent:')) { + $intentKey = str_replace('intent:', '', $source); + JarvisDB::query( + "INSERT INTO usage_patterns (intent_name, hour, dow, hit_count) + VALUES (?, HOUR(NOW()), DAYOFWEEK(NOW())-1, 1) + ON DUPLICATE KEY UPDATE hit_count=hit_count+1, last_seen=NOW()", + [$intentKey] + ); +} + // Memory Core — async extraction for LLM responses (don't extract from intent/KB/fallback) if ($reply && !in_array(explode(':', $source)[0], ['intent', 'kb', 'fallback', 'memory', 'arc'])) { memoryExtractAsync($message, $reply, $sessionId); diff --git a/api/endpoints/suggestions.php b/api/endpoints/suggestions.php new file mode 100644 index 0000000..d62de35 --- /dev/null +++ b/api/endpoints/suggestions.php @@ -0,0 +1,44 @@ += 3 + ORDER BY hit_count DESC LIMIT 3", + [$hour, $dow] +) ?? []; + +// Map intents to friendly suggestion prompts +$intentPrompts = [ + 'network_scan' => 'Run a network scan?', + 'jellyfin_now_playing' => 'Check what\'s playing on Jellyfin?', + 'jellyfin_library' => 'Check the Jellyfin library?', + 'ha_scene' => 'Activate a home scene?', + 'planner:briefing' => 'Get your daily briefing?', + 'vm_suggestions' => 'Check VM resource usage?', + 'jellyfin_pause' => 'Pause Jellyfin?', + 'focus_mode' => 'Switch to focus mode?', + 'show_panels' => 'Show all panels?', +]; + +$suggestions = []; +foreach ($rows as $r) { + $intent = $r['intent_name']; + if (isset($intentPrompts[$intent])) { + $suggestions[] = [ + 'intent' => $intent, + 'prompt' => $intentPrompts[$intent], + 'hit_count' => (int)$r['hit_count'], + ]; + } +} + +echo json_encode(['suggestions' => $suggestions, 'hour' => $hour, 'dow' => $dow]); diff --git a/public_html/api.php b/public_html/api.php index ca4f1e1..4c53dd1 100644 --- a/public_html/api.php +++ b/public_html/api.php @@ -74,6 +74,7 @@ $endpoints = [ 'jellyfin' => 'jellyfin.php', 'history' => 'history.php', 'metrics' => 'metrics.php', + 'suggestions' => 'suggestions.php', 'arc' => 'arc.php', 'directives' => 'directives.php', 'memory' => 'memory.php', diff --git a/public_html/index.html b/public_html/index.html index 66fc2ee..8a6596f 100644 --- a/public_html/index.html +++ b/public_html/index.html @@ -771,6 +771,23 @@ body::after{ .thinking-dot:nth-child(3){animation-delay:0.3s} @keyframes thinkBounce{0%,100%{transform:translateY(0);opacity:0.5}50%{transform:translateY(-6px);opacity:1}} +/* ── ARC REACTOR HEALTH STATES ──────────────────────────────────── */ +#arcReactor.health-warning .arc-ring.r3{border-color:rgba(245,166,35,0.8);box-shadow:0 0 8px #f5a623} +#arcReactor.health-warning .arc-ring.r5{border-color:rgba(245,166,35,0.6)} +#arcReactor.health-warning .arc-ring.r7{border-color:rgba(245,166,35,0.5)} +#arcReactor.health-warning .arc-core{ + background:radial-gradient(circle,#fff 0%,#f5a623 35%,#c97b00 65%,transparent 100%); + box-shadow:0 0 15px #f5a623,0 0 30px #f5a623,0 0 60px rgba(245,166,35,0.4); + animation:corePulse 1.2s ease-in-out infinite; +} +#arcReactor.health-critical .arc-ring.r3{border-color:rgba(255,34,68,0.9);box-shadow:0 0 10px var(--red)} +#arcReactor.health-critical .arc-ring.r5{border-color:rgba(255,34,68,0.7)} +#arcReactor.health-critical .arc-ring.r7{border-color:rgba(255,34,68,0.6)} +#arcReactor.health-critical .arc-core{ + background:radial-gradient(circle,#fff 0%,var(--red) 35%,#8b0000 65%,transparent 100%); + box-shadow:0 0 15px var(--red),0 0 30px var(--red),0 0 60px rgba(255,34,68,0.5); + animation:corePulse 0.6s ease-in-out infinite; +} /* ── SPEAKING ANIMATION ──────────────────────────────────────────── */ @keyframes speakPulse{ 0%,100%{opacity:0.85;transform:translate(-50%,-50%) scale(1);box-shadow:0 0 15px var(--cyan),0 0 30px var(--cyan),0 0 50px rgba(0,212,255,0.3)} @@ -1655,6 +1672,21 @@ function setAlertState(hasAlerts) { if (vg) vg.classList.toggle('alert-vignette', hasAlerts); } +function setSystemHealth(level) { + // level: 'ok' | 'warning' | 'critical' + const reactor = document.getElementById('arcReactor'); + if (!reactor) return; + reactor.classList.remove('health-warning', 'health-critical'); + if (level === 'warning') reactor.classList.add('health-warning'); + if (level === 'critical') reactor.classList.add('health-critical'); + // Also update topbar logo dot + const dot = document.querySelector('.tb-logo-dot'); + if (dot) { + dot.style.background = level === 'critical' ? 'var(--red)' : level === 'warning' ? '#f5a623' : 'var(--cyan)'; + dot.style.boxShadow = level === 'critical' ? '0 0 8px var(--red)' : level === 'warning' ? '0 0 8px #f5a623' : '0 0 8px var(--cyan)'; + } +} + // ── FACE TRACKING — reactor follows face position ───────────────────── let _faceTargetX = 0, _faceTargetY = 0; // normalized -0.5 to 0.5 let _faceCurrX = 0, _faceCurrY = 0; @@ -2595,7 +2627,9 @@ function showApp(name, greeting, silent = false) { initMobile(); setTimeout(checkPlannerReminder, 3000); setInterval(checkUpcomingAppts, 300000); - setTimeout(pollAlertsProactive, 8000); // baseline on load + setTimeout(pollAlertsProactive, 8000); + setTimeout(checkSuggestions, 15000); + setInterval(checkSuggestions, 1800000); // every 30 min // baseline on load setInterval(pollAlertsProactive, 60000); // poll every 60s // Guardian Mode — badge refresh + proactive chat setTimeout(() => { @@ -3278,12 +3312,15 @@ async function loadAlerts() { el.innerHTML = '
✓ NO ACTIVE ALERTS
'; tb.textContent='NO ALERTS'; tb.className='text-green'; setAlertState(false); + setSystemHealth('ok'); return; } tb.textContent=alerts.length+' ALERT'+(alerts.length>1?'S':''); tb.className='text-red'; setAlertState(true); + const hasCritical = alerts.some(a => a.severity === 'critical'); + setSystemHealth(hasCritical ? 'critical' : 'warning'); el.innerHTML = alerts.map(a => { const ctxKey = 'alert_' + a.id; @@ -5252,6 +5289,41 @@ document.getElementById('searchModal')?.addEventListener('click', e => { if (e.target === document.getElementById('searchModal')) closeSearchModal(); }); +// ── PROACTIVE SUGGESTIONS ──────────────────────────────────────────────────── +const _shownSuggestions = new Set(); +async function checkSuggestions() { + const d = await api('suggestions').catch(() => null); + if (!d || !d.suggestions || !d.suggestions.length) return; + for (const s of d.suggestions) { + const key = s.intent + ':' + d.hour + ':' + d.dow; + if (_shownSuggestions.has(key)) continue; + _shownSuggestions.add(key); + // Show as a soft suggestion chip in chat + const log = document.getElementById('chatLog'); + const chip = document.createElement('div'); + chip.style.cssText = 'display:flex;justify-content:flex-end;margin:4px 0'; + chip.innerHTML = ``; + log.appendChild(chip); + log.scrollTop = log.scrollHeight; + break; // show max one suggestion at a time + } +} + +function sendSuggestion(intent, btn) { + btn.closest('div').remove(); + const prompts = { + 'network_scan': 'run a network scan', + 'jellyfin_now_playing': 'what is playing on Jellyfin', + 'ha_scene': 'what scenes are available', + 'planner:briefing': 'daily briefing', + 'vm_suggestions': 'VM resource suggestions', + 'focus_mode': 'focus mode', + }; + const msg = prompts[intent] || intent.replace(/_/g,' '); + document.getElementById('textInput').value = msg; + sendMessage(); +} + // ── MOBILE PANEL SWITCHER ───────────────────────────────────────────────────── function mobSwitch(which) { if (window.innerWidth > 900) return;