From 462ce257a882f4557280031338fba6f0b28d1cf4 Mon Sep 17 00:00:00 2001 From: Myron Blair Date: Wed, 17 Jun 2026 02:55:35 +0000 Subject: [PATCH] Modularize JARVIS frontend into separate CSS/JS files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split monolithic 261KB index.html into maintainable modules: - assets/css/jarvis.css (65KB, 1103 lines) — all styles - assets/js/jarvis-effects.js (23KB) — particle canvas, sparklines, panel float, face tracking, glitch - assets/js/jarvis-overlays.js (17KB) — sleep mode, network map - assets/js/jarvis-app.js (60KB) — globals, init, login, API, panels, chat, voice, alerts, weather, news, planner - assets/js/jarvis-protocols.js (69KB) — arc reactor, intel/comms/guardian/mission/directives/clearance/sites/vision, history search, suggestions, mobile index.html is now a 25KB thin HTML shell with link/script tags. Load order preserved; all cross-file dependencies resolve at runtime after window.load. Co-Authored-By: Claude Sonnet 4.6 --- public_html/assets/css/jarvis.css | 1103 +++++ public_html/assets/js/jarvis-app.js | 1482 ++++++ public_html/assets/js/jarvis-effects.js | 590 +++ public_html/assets/js/jarvis-overlays.js | 357 ++ public_html/assets/js/jarvis-protocols.js | 1413 ++++++ public_html/index.html | 4959 +-------------------- 6 files changed, 4950 insertions(+), 4954 deletions(-) create mode 100644 public_html/assets/css/jarvis.css create mode 100644 public_html/assets/js/jarvis-app.js create mode 100644 public_html/assets/js/jarvis-effects.js create mode 100644 public_html/assets/js/jarvis-overlays.js create mode 100644 public_html/assets/js/jarvis-protocols.js diff --git a/public_html/assets/css/jarvis.css b/public_html/assets/css/jarvis.css new file mode 100644 index 0000000..76a8746 --- /dev/null +++ b/public_html/assets/css/jarvis.css @@ -0,0 +1,1103 @@ +*,*::before,*::after{box-sizing:border-box;margin:0;padding:0} +:root{ + --bg:#000810; + --bg2:#000d1a; + --cyan:#00d4ff; + --cyan2:#00a8cc; + --cyan3:rgba(0,212,255,0.15); + --orange:#ff6600; + --orange2:#ff4400; + --green:#00ff88; + --red:#ff2244; + --yellow:#ffd700; + --dim:rgba(0,212,255,0.4); + --dimmer:rgba(0,212,255,0.12); + --grid:rgba(0,180,255,0.07); + --text:#c8e6ff; + --text-dim:rgba(200,230,255,0.5); + --panel-bg:rgba(0,15,35,0.85); + --panel-border:rgba(0,212,255,0.2); + --font-display:'Orbitron',monospace; + --font-body:'Rajdhani',sans-serif; + --font-mono:'Share Tech Mono',monospace; + --r:8px; +} +html,body{width:100%;height:100%;overflow:hidden;background:var(--bg);color:var(--text);font-family:var(--font-body)} + +/* ── GRID BACKGROUND — animated drift ─────────────────────────────── */ +@keyframes gridDrift{from{background-position:0 0,0 0}to{background-position:40px 40px,40px 40px}} +body::before{ + content:''; + position:fixed;inset:0; + background-image: + linear-gradient(var(--grid) 1px,transparent 1px), + linear-gradient(90deg,var(--grid) 1px,transparent 1px); + background-size:40px 40px; + z-index:0; + pointer-events:none; + animation:gridDrift 18s linear infinite; +} +body::after{ + content:''; + position:fixed;inset:0; + background:radial-gradient(ellipse at 50% 50%,rgba(0,80,160,0.08) 0%,transparent 70%); + z-index:0; + pointer-events:none; +} + +/* ── SCAN LINES ───────────────────────────────────────────────────── */ +.scanlines{ + position:fixed;inset:0;z-index:1;pointer-events:none; + background:repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(0,0,0,0.03) 2px,rgba(0,0,0,0.03) 4px); + animation:scanMove 8s linear infinite; +} +@keyframes scanMove{0%{background-position:0 0}100%{background-position:0 100%}} + +/* ── SCANLINE SWEEP ───────────────────────────────────────────────── */ +.scanline-sweep{ + position:fixed;top:0;left:0;right:0;height:120px; + background:linear-gradient(180deg,transparent 0%,rgba(0,212,255,0.04) 40%,rgba(0,212,255,0.12) 50%,rgba(0,212,255,0.04) 60%,transparent 100%); + pointer-events:none;z-index:2; + animation:sweepDown 7s linear infinite; + box-shadow:0 0 12px rgba(0,212,255,0.15); +} +@keyframes sweepDown{ + 0%{transform:translateY(-120px);opacity:0} + 3%{opacity:1} + 97%{opacity:0.7} + 100%{transform:translateY(100vh);opacity:0} +} + +/* ── PARTICLE CANVAS ──────────────────────────────────────────────── */ +#particleCanvas{position:fixed;inset:0;z-index:0;pointer-events:none;opacity:0.7} + +/* ── PANEL FLOAT + GLOW ───────────────────────────────────────────── */ +@keyframes panelFloat{ + 0%,100%{transform:translateY(var(--pty,0px)) rotateX(var(--prx,0deg)) rotateY(var(--pry,0deg));box-shadow:0 4px 20px rgba(0,0,0,0.4),0 0 0px rgba(0,212,255,0)} + 50%{transform:translateY(calc(var(--pty,0px) - 7px)) rotateX(var(--prx,0deg)) rotateY(var(--pry,0deg));box-shadow:0 16px 40px rgba(0,0,0,0.5),0 0 30px rgba(0,212,255,0.06),0 0 60px rgba(0,212,255,0.02)} +} +/* Panel flash when data updates */ +@keyframes panelFlash{0%{box-shadow:0 0 0 1px rgba(0,212,255,0.8),0 0 20px rgba(0,212,255,0.3)}100%{box-shadow:none}} +.panel.data-flash{animation:panelFlash 0.5s ease-out,panelFloat 7s ease-in-out infinite} + +/* Alert pulse — red ambient glow on body when alerts active */ +@keyframes alertPulse{0%,100%{opacity:0}50%{opacity:1}} +#alertOverlay{position:fixed;inset:0;pointer-events:none;z-index:1;background:radial-gradient(ellipse at 50% 50%,rgba(255,30,60,0.07) 0%,transparent 70%);animation:alertPulse 3s ease-in-out infinite;display:none} + +/* Glitch keyframes */ +@keyframes glitch1{ + 0%,100%{clip-path:inset(0 0 100% 0);transform:translate(0)} + 10%{clip-path:inset(10% 0 60% 0);transform:translate(-3px,1px)} + 20%{clip-path:inset(40% 0 30% 0);transform:translate(3px,-1px)} + 30%{clip-path:inset(70% 0 10% 0);transform:translate(-2px,2px)} + 40%{clip-path:inset(0 0 100% 0);transform:translate(0)} +} +@keyframes glitch2{ + 0%,100%{clip-path:inset(0 0 100% 0);transform:translate(0)} + 10%{clip-path:inset(60% 0 10% 0);transform:translate(3px,-1px)} + 25%{clip-path:inset(20% 0 50% 0);transform:translate(-3px,1px)} + 35%{clip-path:inset(80% 0 5% 0);transform:translate(2px,2px)} + 45%{clip-path:inset(0 0 100% 0);transform:translate(0)} +} +.tb-logo-text{position:relative;display:inline-block} +.tb-logo-text::before,.tb-logo-text::after{ + content:attr(data-text);position:absolute;top:0;left:0; + color:var(--cyan);font-family:var(--font-display);font-size:inherit;font-weight:inherit;letter-spacing:inherit; + pointer-events:none;opacity:0; +} +.tb-logo-text::before{color:rgba(255,0,80,0.8);text-shadow:2px 0 rgba(255,0,80,0.5)} +.tb-logo-text::after{color:rgba(0,255,255,0.8);text-shadow:-2px 0 rgba(0,255,255,0.5)} +.tb-logo-text.glitching::before{animation:glitch1 0.25s steps(1) forwards;opacity:1} +.tb-logo-text.glitching::after{animation:glitch2 0.25s steps(1) forwards;opacity:1} + +/* Metric bar shimmer */ +@keyframes barShimmer{0%{transform:translateX(-100%)}100%{transform:translateX(400%)}} +.metric-bar-fill::after{ + content:'';position:absolute;top:0;left:0;width:30%;height:100%; + background:linear-gradient(90deg,transparent,rgba(255,255,255,0.25),transparent); + animation:barShimmer 2.5s ease-in-out infinite; + border-radius:inherit; +} +.metric-bar-fill{position:relative;overflow:hidden} + +/* Panel hover rise */ +.panel:hover{ + transform:translateY(calc(var(--pty,0px) - 11px)) !important; + border-color:rgba(0,212,255,0.45) !important; + box-shadow:0 20px 50px rgba(0,0,0,0.55),0 0 40px rgba(0,212,255,0.1) !important; + transition:transform 0.3s ease,border-color 0.3s ease,box-shadow 0.3s ease; +} + +/* Sparkline canvas */ +.sparkline-wrap{margin:4px 0 8px;height:32px;position:relative} +.sparkline-wrap canvas{display:block;width:100%;height:32px} + +/* ── HUD CORNER BRACKETS ──────────────────────────────────────────── */ +/* ── MINI ARC REACTOR ─────────────────────────────────────────────── */ +.tb-reactor{width:30px;height:30px;position:relative;flex-shrink:0} +.tbr-ring{position:absolute;border-radius:50%;top:50%;left:50%;transform:translate(-50%,-50%)} +.tbr-r1{width:30px;height:30px;border:1px solid rgba(0,212,255,0.35);animation:spinRing 9s linear infinite} +.tbr-r2{width:20px;height:20px;border:1px solid var(--orange);box-shadow:0 0 6px var(--orange);animation:spinRing 4s linear infinite reverse} +.tbr-core{position:absolute;width:8px;height:8px;border-radius:50%;background:radial-gradient(circle,#fff 0%,var(--cyan) 50%,var(--cyan2) 100%);box-shadow:0 0 10px var(--cyan),0 0 20px rgba(0,212,255,0.5);top:50%;left:50%;transform:translate(-50%,-50%);animation:corePulse 2s ease-in-out infinite} + +/* ── LOGIN SCREEN ─────────────────────────────────────────────────── */ +#loginScreen{ + position:fixed;inset:0;z-index:1000; + display:flex;align-items:center;justify-content:center;flex-direction:column; + background:var(--bg); +} +.login-reactor{ + width:160px;height:160px;position:relative;margin-bottom:40px;cursor:pointer; +} +.login-reactor .ring{ + position:absolute;border-radius:50%;border:2px solid var(--cyan); + top:50%;left:50%;transform:translate(-50%,-50%); + box-shadow:0 0 8px var(--cyan),inset 0 0 8px rgba(0,212,255,0.1); + animation:spinRing var(--spd,4s) linear infinite; +} +.login-reactor .r1{width:160px;height:160px;--spd:8s;border-color:rgba(0,212,255,0.3)} +.login-reactor .r2{width:130px;height:130px;--spd:6s;animation-direction:reverse} +.login-reactor .r3{width:100px;height:100px;--spd:4s;border-color:var(--orange);box-shadow:0 0 12px var(--orange)} +.login-reactor .r4{width:70px;height:70px;--spd:3s;animation-direction:reverse} +.login-reactor .core{ + position:absolute;top:50%;left:50%;transform:translate(-50%,-50%); + width:40px;height:40px;border-radius:50%; + background:radial-gradient(circle,#ffffff 0%,var(--cyan) 40%,var(--cyan2) 70%,transparent 100%); + box-shadow:0 0 20px var(--cyan),0 0 40px var(--cyan),0 0 80px rgba(0,212,255,0.3); + animation:corePulse 2s ease-in-out infinite; +} +@keyframes spinRing{from{transform:translate(-50%,-50%) rotate(0deg)}to{transform:translate(-50%,-50%) rotate(360deg)}} +@keyframes corePulse{0%,100%{opacity:0.8;transform:translate(-50%,-50%) scale(1)}50%{opacity:1;transform:translate(-50%,-50%) scale(1.1)}} +#loginScreen h1{ + font-family:var(--font-display);font-size:2.5rem;font-weight:900;letter-spacing:8px; + color:var(--cyan);text-shadow:0 0 20px var(--cyan),0 0 40px rgba(0,212,255,0.4); + margin-bottom:8px; +} +#loginScreen p{color:var(--text-dim);font-size:0.85rem;letter-spacing:4px;text-transform:uppercase;margin-bottom:40px} +.login-form{display:flex;flex-direction:column;gap:14px;width:320px} +.login-form input{ + background:rgba(0,212,255,0.05); + border:1px solid var(--panel-border); + border-radius:var(--r); + padding:12px 16px; + color:var(--cyan); + font-family:var(--font-mono);font-size:0.95rem; + outline:none; + transition:border-color 0.2s,box-shadow 0.2s; + letter-spacing:1px; +} +.login-form input:focus{border-color:var(--cyan);box-shadow:0 0 12px rgba(0,212,255,0.2)} +.login-form input::placeholder{color:var(--dim);letter-spacing:1px} +.login-form button{ + background:linear-gradient(135deg,rgba(0,212,255,0.15),rgba(0,212,255,0.05)); + border:1px solid var(--cyan); + border-radius:var(--r); + padding:14px; + color:var(--cyan); + font-family:var(--font-display);font-size:0.8rem;font-weight:700; + letter-spacing:3px;text-transform:uppercase; + cursor:pointer; + transition:all 0.2s; + box-shadow:0 0 12px rgba(0,212,255,0.1); +} +.login-form button:hover{background:rgba(0,212,255,0.2);box-shadow:0 0 20px rgba(0,212,255,0.3)} +#loginError{color:var(--red);font-size:0.85rem;text-align:center;letter-spacing:1px;min-height:20px} + +/* ── MAIN APP (hidden until login) ───────────────────────────────── */ +#app{position:fixed;inset:0;z-index:2;display:none;flex-direction:column} + +/* ── TOP NAV BAR ─────────────────────────────────────────────────── */ +#topBar{ + display:flex;align-items:center;justify-content:space-between; + padding:0 20px;height:48px; + background:rgba(0,8,22,0.9); + border-bottom:1px solid var(--panel-border); + flex-shrink:0; +} +.tb-logo{ + font-family:var(--font-display);font-size:1rem;font-weight:900;letter-spacing:4px; + color:var(--cyan);text-shadow:0 0 10px var(--cyan);display:flex;align-items:center;gap:10px; + transition:filter 0.4s ease; + will-change:transform; +} +.tb-logo.face-tracking{ + filter:drop-shadow(0 0 12px rgba(0,212,255,0.9)) drop-shadow(0 0 24px rgba(0,212,255,0.4)); +} +/* Face scan crosshair overlay */ +#faceScanOverlay{ + position:fixed;pointer-events:none;z-index:9; + width:60px;height:60px; + display:none; +} +#faceScanOverlay::before,#faceScanOverlay::after{ + content:'';position:absolute; + border-color:rgba(0,212,255,0.7);border-style:solid; +} +#faceScanOverlay::before{ + top:0;left:0;width:16px;height:16px; + border-width:2px 0 0 2px; +} +#faceScanOverlay::after{ + bottom:0;right:0;width:16px;height:16px; + border-width:0 2px 2px 0; +} +#faceScanOverlay .fso-tr{position:absolute;top:0;right:0;width:16px;height:16px;border:2px solid rgba(0,212,255,0.7);border-left:0;border-bottom:0} +#faceScanOverlay .fso-bl{position:absolute;bottom:0;left:0;width:16px;height:16px;border:2px solid rgba(0,212,255,0.7);border-right:0;border-top:0} +#faceScanOverlay .fso-dot{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:4px;height:4px;border-radius:50%;background:rgba(0,212,255,0.6);box-shadow:0 0 6px var(--cyan)} +#faceScanOverlay .fso-label{position:absolute;bottom:-18px;left:50%;transform:translateX(-50%);font-family:var(--font-mono);font-size:0.45rem;color:rgba(0,212,255,0.7);letter-spacing:1px;white-space:nowrap} +@keyframes fsoSpin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}} +#faceScanOverlay .fso-ring{position:absolute;top:50%;left:50%;width:40px;height:40px;margin:-20px;border-radius:50%;border:1px solid rgba(0,212,255,0.3);border-top-color:rgba(0,212,255,0.7);animation:fsoSpin 1.2s linear infinite} +.tb-logo-dot{width:8px;height:8px;border-radius:50%;background:var(--cyan);box-shadow:0 0 8px var(--cyan);animation:corePulse 1.5s infinite} +.tb-center{ + display:flex;gap:24px;align-items:center; +} +.tb-stat{font-family:var(--font-mono);font-size:0.75rem;color:var(--text-dim)} +.tb-stat span{color:var(--cyan)} +.tb-right{display:flex;align-items:center;gap:16px} +#clock{font-family:var(--font-mono);font-size:1rem;color:var(--cyan);letter-spacing:2px} +#date-display{font-family:var(--font-mono);font-size:0.7rem;color:var(--text-dim)} +.status-dot{width:8px;height:8px;border-radius:50%;background:var(--green);box-shadow:0 0 6px var(--green);animation:blink 2s infinite} +@keyframes blink{0%,100%{opacity:1}50%{opacity:0.4}} +.btn-logout{ + background:none;border:1px solid rgba(255,34,68,0.3);border-radius:4px; + color:rgba(255,34,68,0.7);font-family:var(--font-mono);font-size:0.7rem; + padding:4px 10px;cursor:pointer;letter-spacing:1px;transition:all 0.2s; +} +.btn-logout:hover{border-color:var(--red);color:var(--red);box-shadow:0 0 8px rgba(255,34,68,0.2)} + +/* ── MAIN LAYOUT ─────────────────────────────────────────────────── */ +#mainLayout{ + flex:1;display:grid; + grid-template-columns:280px 1fr 280px; + grid-template-rows:1fr; + gap:10px;padding:10px; + overflow:hidden; + perspective:1200px; + transition:grid-template-columns 0.45s cubic-bezier(0.4,0,0.2,1); +} +#mainLayout.focus-mode{grid-template-columns:0px 1fr 0px} +#leftPanel,#rightPanel{ + transition:opacity 0.35s ease,transform 0.45s cubic-bezier(0.4,0,0.2,1); + overflow:hidden; +} +#mainLayout.focus-mode #leftPanel{opacity:0;transform:translateX(-20px);pointer-events:none} +#mainLayout.focus-mode #rightPanel{opacity:0;transform:translateX(20px);pointer-events:none} + +#leftPanel{grid-area:left} +#centerPanel{grid-area:center} +#rightPanel{grid-area:right} +#mainLayout{grid-template-areas:"left center right"} +#mainLayout.swapped{grid-template-areas:"right center left"} +#mainLayout.swapped.focus-mode #leftPanel{transform:translateX(20px)} +#mainLayout.swapped.focus-mode #rightPanel{transform:translateX(-20px)} +#btn-swap-panels{background:none;border:1px solid var(--panel-border);color:var(--text-dim);padding:3px 8px;cursor:pointer;font-family:var(--font-mono);font-size:0.6rem;letter-spacing:1px;transition:all 0.2s} +#btn-swap-panels:hover,#btn-swap-panels.active{color:var(--cyan);border-color:var(--cyan)} +/* ── MOBILE RESPONSIVE ──────────────────────────────────────────────── */ +@media(max-width:900px){ + #mainLayout{grid-template-columns:1fr!important;grid-template-rows:1fr!important;padding:6px;gap:6px} + #leftPanel,#rightPanel{display:none} + #leftPanel.mob-active,#rightPanel.mob-active{display:flex!important;flex-direction:column} + #centerPanel{display:none} + #centerPanel.mob-active{display:flex!important;flex-direction:column} + #topBar{padding:0 10px;height:42px} + .tb-center{display:none} + .tb-logo{font-size:0.85rem;letter-spacing:2px} + #clock{font-size:0.85rem} + #date-display{display:none} + #btn-swap-panels,.btn-panels{display:none} + #inputArea{padding:8px 6px} + #textInput{font-size:0.85rem;padding:8px 10px;min-height:40px} + #sendBtn{min-height:40px;padding:0 14px;font-size:0.65rem} + #micBtn{min-height:40px;min-width:40px;font-size:1.1rem} + #app{padding-bottom:48px} + #mobileNav{display:flex!important} +} +@media(min-width:901px){#mobileNav{display:none!important}} +#mobileNav{ + display:none;position:fixed;bottom:0;left:0;right:0;z-index:100; + background:rgba(0,8,22,0.96);border-top:1px solid var(--panel-border);height:48px; +} +.mob-nav-btn{ + flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center; + font-family:var(--font-display);font-size:0.5rem;letter-spacing:1.5px; + color:var(--text-dim);cursor:pointer;gap:2px;border:none;background:none;transition:color 0.2s; +} +.mob-nav-btn .mob-icon{font-size:1rem;line-height:1} +.mob-nav-btn.active{color:var(--cyan)} +.mob-nav-btn.active .mob-icon{filter:drop-shadow(0 0 4px var(--cyan))} +/* Panel toggle button */ +.btn-panels{ + background:rgba(0,212,255,0.06); + border:1px solid rgba(0,212,255,0.25); + border-radius:4px;color:var(--cyan); + font-family:var(--font-display);font-size:0.55rem; + letter-spacing:1px;padding:4px 10px;cursor:pointer; + transition:all 0.2s;margin-right:6px; +} +.btn-panels:hover{border-color:var(--cyan);box-shadow:0 0 8px rgba(0,212,255,0.2)} +.btn-panels.focus-active{background:rgba(0,212,255,0.15);border-color:var(--cyan)} +/* Camera auto-mic button */ +.btn-camera{ + background:rgba(0,212,255,0.06); + border:1px solid rgba(0,212,255,0.25); + border-radius:4px;color:var(--cyan); + font-family:var(--font-display);font-size:0.55rem; + letter-spacing:1px;padding:4px 10px;cursor:pointer; + transition:all 0.2s;margin-right:6px; +} +.btn-camera:hover{border-color:var(--cyan);box-shadow:0 0 8px rgba(0,212,255,0.2)} +.btn-camera.cam-active{background:rgba(0,255,100,0.1);border-color:var(--green);color:var(--green)} +.btn-camera.cam-sensing{animation:camPulse 1.5s ease-in-out infinite} +.btn-agent{background:transparent;border:1px solid var(--panel-border);color:var(--text-dim);font-family:var(--font-mono);font-size:0.62rem;letter-spacing:2px;padding:6px 10px;cursor:pointer;display:flex;align-items:center;gap:6px;transition:all 0.2s} +.btn-agent:hover{border-color:var(--cyan);box-shadow:0 0 8px rgba(0,212,255,0.2)} +.btn-agent .agent-dot{width:7px;height:7px;border-radius:50%;background:var(--red);flex-shrink:0;transition:all 0.3s} +.btn-agent.agent-online .agent-dot{background:var(--green);box-shadow:0 0 6px var(--green)} +#agentModal{position:fixed;inset:0;background:rgba(0,0,0,0.85);z-index:9999;display:none;align-items:center;justify-content:center} +#agentModal.open{display:flex} +.agent-modal-box{background:var(--panel-bg);border:1px solid var(--panel-border);padding:24px 32px;max-width:500px;width:90%;font-family:var(--font-mono)} +.agent-modal-box h3{color:var(--cyan);font-size:0.75rem;letter-spacing:4px;margin:0 0 16px} +.agent-modal-box pre{background:rgba(0,0,0,0.4);padding:12px;font-size:0.65rem;color:var(--green);overflow-x:auto;margin:8px 0;white-space:pre-wrap;word-break:break-all} +.agent-modal-close{float:right;background:transparent;border:1px solid var(--panel-border);color:var(--text-dim);cursor:pointer;font-family:var(--font-mono);font-size:0.6rem;padding:4px 10px;letter-spacing:2px} +.agent-modal-close:hover{border-color:var(--red);color:var(--red)} +.agent-dl-btn{display:inline-block;margin-top:12px;background:rgba(0,212,255,0.1);border:1px solid var(--cyan);color:var(--cyan);font-family:var(--font-mono);font-size:0.65rem;letter-spacing:2px;padding:8px 16px;cursor:pointer;text-decoration:none;transition:all 0.2s} +.agent-dl-btn:hover{background:rgba(0,212,255,0.2)} +@keyframes camPulse{0%,100%{box-shadow:0 0 4px rgba(0,255,100,0.3)}50%{box-shadow:0 0 12px rgba(0,255,100,0.6)}} + +/* ── PANELS ───────────────────────────────────────────────────────── */ +.panel{ + background:var(--panel-bg); + border:1px solid var(--panel-border); + border-radius:var(--r); + padding:14px; + overflow:hidden; + position:relative; + backdrop-filter:blur(4px); + animation:panelFloat 7s ease-in-out infinite; + will-change:transform; +} +.panel::before{ + content:'';position:absolute;top:0;left:0;right:0;height:1px; + background:linear-gradient(90deg,transparent,var(--cyan),transparent); + opacity:0.4; + z-index:2; +} +/* HUD corner brackets */ +.panel::after{ + content:'';position:absolute;inset:0;pointer-events:none;z-index:2; + background: + linear-gradient(to right,rgba(0,212,255,0.8),rgba(0,212,255,0.8)) top left / 14px 1px no-repeat, + linear-gradient(to bottom,rgba(0,212,255,0.8),rgba(0,212,255,0.8)) top left / 1px 14px no-repeat, + linear-gradient(to right,rgba(0,212,255,0.8),rgba(0,212,255,0.8)) top right / 14px 1px no-repeat, + linear-gradient(to bottom,rgba(0,212,255,0.8),rgba(0,212,255,0.8)) top right / 1px 14px no-repeat, + linear-gradient(to right,rgba(0,212,255,0.8),rgba(0,212,255,0.8)) bottom left / 14px 1px no-repeat, + linear-gradient(to bottom,rgba(0,212,255,0.8),rgba(0,212,255,0.8)) bottom left / 1px 14px no-repeat, + linear-gradient(to right,rgba(0,212,255,0.8),rgba(0,212,255,0.8)) bottom right / 14px 1px no-repeat, + linear-gradient(to bottom,rgba(0,212,255,0.8),rgba(0,212,255,0.8)) bottom right / 1px 14px no-repeat; + opacity:0.55; +} +.panel-title{ + font-family:var(--font-display);font-size:0.6rem;font-weight:700; + letter-spacing:3px;color:var(--cyan);text-transform:uppercase; + margin-bottom:12px;display:flex;align-items:center;justify-content:space-between; +} +.panel-title .indicator{ + width:6px;height:6px;border-radius:50%;background:var(--cyan); + box-shadow:0 0 6px var(--cyan);animation:blink 3s infinite; +} +.panel-title{cursor:pointer;user-select:none} +.panel-collapse-btn{ + background:none;border:none;color:rgba(0,212,255,0.35);cursor:pointer; + font-size:0.65rem;padding:0;margin-left:6px;flex-shrink:0;line-height:1; + transition:transform 0.25s ease,color 0.2s; +} +.panel-collapse-btn:hover{color:var(--cyan)!important} +.panel.collapsed .panel-collapse-btn{transform:rotate(-90deg);color:rgba(0,212,255,0.5)} +.panel.collapsed > *:not(.panel-title){ + display:none!important; +} +.panel.collapsed{ + flex:0 0 auto!important;min-height:0!important;overflow:visible!important; +} + +/* ── LEFT PANEL — SYSTEM ─────────────────────────────────────────── */ +#leftPanel{display:flex;flex-direction:column;gap:10px;overflow-y:auto} + +.metric-row{margin-bottom:10px} +.metric-label{ + font-family:var(--font-mono);font-size:0.7rem;color:var(--text-dim); + display:flex;justify-content:space-between;margin-bottom:4px; +} +.metric-label span{color:var(--cyan)} +.metric-bar{ + height:4px;border-radius:2px; + background:rgba(0,212,255,0.1); + overflow:hidden;position:relative; +} +.metric-bar-fill{ + height:100%;border-radius:2px; + background:linear-gradient(90deg,var(--cyan2),var(--cyan)); + box-shadow:0 0 6px var(--cyan); + transition:width 1s ease; +} +.metric-bar-fill.warn{background:linear-gradient(90deg,#cc6600,var(--orange));box-shadow:0 0 6px var(--orange)} +.metric-bar-fill.danger{background:linear-gradient(90deg,#cc0022,var(--red));box-shadow:0 0 6px var(--red)} + +.service-row{ + display:flex;align-items:center;justify-content:space-between; + padding:5px 0;border-bottom:1px solid rgba(0,212,255,0.06); + font-family:var(--font-mono);font-size:0.72rem; +} +.service-row:last-child{border-bottom:none} +.svc-name{color:var(--text-dim)} +.svc-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0} +.svc-dot.on{background:var(--green);box-shadow:0 0 4px var(--green)} +.svc-dot.off{background:var(--red);box-shadow:0 0 4px var(--red)} + +.do-section{margin-top:8px} +.val-row{ + display:flex;justify-content:space-between; + font-family:var(--font-mono);font-size:0.72rem;padding:3px 0; +} +.val-row .lbl{color:var(--text-dim)} +.val-row .val{color:var(--cyan)} +.val-row .val.ok{color:var(--green)} +.val-row .val.warn{color:var(--orange)} +.val-row .val.danger{color:var(--red)} + +/* ── CENTER PANEL ────────────────────────────────────────────────── */ +#centerPanel{ + display:flex;flex-direction:column;align-items:center;gap:10px;overflow:hidden; +} + +/* ARC REACTOR ─────────────────────────────────────────────────────── */ +#arcReactor{position:relative;width:220px;height:220px;flex-shrink:0} +.arc-ring{ + position:absolute;top:50%;left:50%;transform:translate(-50%,-50%); + border-radius:50%;border-style:solid;border-color:var(--cyan); + animation:spinRing var(--spd,6s) linear infinite var(--dir,normal); +} +.arc-ring.r1{width:220px;height:220px;border-width:1px;border-color:rgba(0,212,255,0.15);--spd:20s} +.arc-ring.r2{width:195px;height:195px;border-width:1px;border-color:rgba(0,212,255,0.25);--spd:15s;--dir:reverse} +.arc-ring.r3{ + width:170px;height:170px;border-width:2px;--spd:10s; + border-top-color:transparent;border-right-color:transparent; + box-shadow:0 0 6px var(--cyan); +} +.arc-ring.r4{ + width:145px;height:145px;border-width:1px;--spd:8s;--dir:reverse; + border-bottom-color:transparent;border-left-color:transparent; +} +.arc-ring.r5{ + width:115px;height:115px;border-width:2px;--spd:5s; + border-color:var(--orange);border-right-color:transparent; + box-shadow:0 0 8px var(--orange); +} +.arc-ring.r6{width:88px;height:88px;border-width:1px;--spd:4s;--dir:reverse;border-color:rgba(0,212,255,0.6)} +.arc-ring.r7{ + width:62px;height:62px;border-width:2px;--spd:3s; + border-left-color:transparent;border-bottom-color:transparent; +} +.arc-core{ + position:absolute;top:50%;left:50%;transform:translate(-50%,-50%); + width:36px;height:36px;border-radius:50%; + background:radial-gradient(circle,#fff 0%,var(--cyan) 35%,var(--cyan2) 65%,transparent 100%); + box-shadow:0 0 15px var(--cyan),0 0 30px var(--cyan),0 0 60px rgba(0,212,255,0.4),0 0 100px rgba(0,212,255,0.15); + animation:corePulse 2s ease-in-out infinite; +} + +/* HUD TICKS ───────────────────────────────────────────────────────── */ +.hud-ticks{ + position:absolute;inset:0;border-radius:50%; +} +.hud-ticks::before,.hud-ticks::after{ + content:'';position:absolute; + background:var(--cyan);opacity:0.5; +} +.hud-ticks::before{left:50%;top:0;width:1px;height:12px;transform:translateX(-50%)} +.hud-ticks::after{left:0;top:50%;height:1px;width:12px;transform:translateY(-50%)} + +/* ── CHAT AREA ───────────────────────────────────────────────────── */ +#chatArea{ + flex:1;width:100%;display:flex;flex-direction:column;gap:8px;overflow:hidden; +} +#chatLog{ + flex:1;overflow-y:auto;display:flex;flex-direction:column;gap:8px; + padding:4px 0; + scrollbar-width:thin;scrollbar-color:var(--dim) transparent; +} +.msg{ + padding:10px 14px;border-radius:var(--r);max-width:100%; + font-family:var(--font-body);font-size:0.9rem;line-height:1.5; + animation:msgIn 0.3s ease; +} +@keyframes msgIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}} +.msg.user{ + background:rgba(0,212,255,0.08);border:1px solid rgba(0,212,255,0.15); + border-left:3px solid var(--cyan); + font-family:var(--font-mono);font-size:0.8rem;color:var(--text); +} +.msg.user::before{content:'';display:none} +.msg.jarvis{ + background:rgba(0,40,80,0.4);border:1px solid rgba(0,212,255,0.1); + border-left:3px solid var(--orange); +} +.msg.jarvis::before{content:'◆ JARVIS ';color:var(--orange);font-weight:700;font-size:0.7rem;letter-spacing:2px} +.msg.system{ + border:1px solid rgba(255,215,0,0.2);background:rgba(255,215,0,0.03); + font-family:var(--font-mono);font-size:0.75rem;color:var(--text-dim);text-align:center; + border-radius:4px; +} + +/* ── CONTEXT CHIP ───────────────────────────────────────────────── */ +#contextChip{ + display:none;flex-shrink:0; + padding:5px 10px;margin-bottom:6px; + background:rgba(0,212,255,0.07); + border:1px solid rgba(0,212,255,0.35); + border-radius:var(--r); + display:flex;align-items:center;gap:8px; + font-family:var(--font-display);font-size:0.58rem;letter-spacing:1px; +} +#contextChip.visible{display:flex} +#contextLabel{color:var(--cyan);flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +#contextType{color:var(--text-dim)} +#contextClear{ + background:none;border:none;color:var(--dim);cursor:pointer; + font-size:1rem;line-height:1;padding:0 2px;flex-shrink:0; +} +#contextClear:hover{color:var(--red)} + +/* Planner mini panel */ +/* Clickable panel items */ +.vm-card{cursor:pointer;transition:background 0.15s,border-color 0.15s} +.vm-card:hover{background:rgba(0,212,255,0.07);border-color:rgba(0,212,255,0.4)} +.vm-card.ctx-active{border-color:var(--cyan);background:rgba(0,212,255,0.1)} +.device-item{cursor:pointer;transition:background 0.15s} +.device-item:hover{background:rgba(0,212,255,0.05)} +.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)} +.ha-ask-btn{ + background:none;border:1px solid rgba(0,212,255,0.2);border-radius:3px; + color:var(--text-dim);font-size:0.55rem;padding:1px 5px;cursor:pointer; + font-family:var(--font-display);letter-spacing:1px;flex-shrink:0; + transition:all 0.15s; +} +.ha-ask-btn:hover{border-color:var(--cyan);color:var(--cyan);background:rgba(0,212,255,0.1)} + +/* ── VOICE INPUT ─────────────────────────────────────────────────── */ +#inputArea{ + display:flex;gap:10px;align-items:center;flex-shrink:0; +} +#textInput{ + flex:1;background:rgba(0,212,255,0.04); + border:1px solid var(--panel-border);border-radius:var(--r); + padding:10px 14px;color:var(--text); + font-family:var(--font-mono);font-size:0.85rem;outline:none; + transition:border-color 0.2s; +} +#textInput:focus{border-color:var(--cyan);box-shadow:0 0 10px rgba(0,212,255,0.1)} +#textInput::placeholder{color:var(--dim)} +#sendBtn{ + background:rgba(0,212,255,0.1);border:1px solid var(--dim); + border-radius:var(--r);padding:10px 16px; + color:var(--cyan);font-family:var(--font-display);font-size:0.65rem;font-weight:700; + letter-spacing:2px;cursor:pointer;transition:all 0.2s;white-space:nowrap; +} +#sendBtn:hover{background:rgba(0,212,255,0.2);box-shadow:0 0 12px rgba(0,212,255,0.2)} + +/* MIC BUTTON ──────────────────────────────────────────────────────── */ +#micBtn{ + width:48px;height:48px;border-radius:50%; + background:radial-gradient(circle,rgba(255,102,0,0.15),rgba(0,8,22,0.9)); + border:2px solid var(--orange); + display:flex;align-items:center;justify-content:center; + cursor:pointer;transition:all 0.2s;flex-shrink:0; + box-shadow:0 0 10px rgba(255,102,0,0.2); +} +#micBtn:hover{box-shadow:0 0 20px rgba(255,102,0,0.4);transform:scale(1.05)} +#micBtn.listening{ + border-color:var(--red);background:radial-gradient(circle,rgba(255,34,68,0.2),rgba(0,8,22,0.9)); + box-shadow:0 0 25px rgba(255,34,68,0.5); + animation:micPulse 0.8s ease-in-out infinite; +} +@keyframes micPulse{0%,100%{box-shadow:0 0 25px rgba(255,34,68,0.5)}50%{box-shadow:0 0 40px rgba(255,34,68,0.8),0 0 60px rgba(255,34,68,0.3)}} +#micBtn.muted{border-color:var(--text-dim);background:radial-gradient(circle,rgba(200,230,255,0.05),rgba(0,8,22,0.9));box-shadow:0 0 8px rgba(200,230,255,0.1);} +#micIcon{font-size:20px} + +/* WAVEFORM ─────────────────────────────────────────────────────────── */ +#waveform{ + display:none;align-items:center;justify-content:center;gap:3px;height:30px; +} +#waveform.active{display:flex} +.wave-bar{ + width:3px;border-radius:2px;background:var(--red); + animation:waveBounce var(--d,0.6s) ease-in-out infinite alternate; + box-shadow:0 0 4px var(--red); +} +@keyframes waveBounce{from{height:4px}to{height:24px}} + +/* ── RIGHT PANEL ─────────────────────────────────────────────────── */ +#rightPanel{display:flex;flex-direction:column;gap:10px;overflow-y:auto} + +/* NETWORK DEVICE LIST ─────────────────────────────────────────────── */ +.device-item{ + display:flex;align-items:center;gap:8px;padding:6px 0; + border-bottom:1px solid rgba(0,212,255,0.06); + font-family:var(--font-mono);font-size:0.72rem; +} +.device-item:last-child{border-bottom:none} +.device-status{width:6px;height:6px;border-radius:50%;flex-shrink:0} +.device-status.on{background:var(--green);box-shadow:0 0 4px var(--green)} +.device-status.off{background:var(--red);box-shadow:0 0 4px var(--red)} +.device-status.unk{background:var(--yellow);box-shadow:0 0 4px var(--yellow);opacity:0.6} +.device-info{flex:1;min-width:0} +.device-name{color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.device-ip{color:var(--text-dim);font-size:0.65rem} + +/* VM CARDS ─────────────────────────────────────────────────────────── */ +.vm-card{ + background:rgba(0,212,255,0.04); + border:1px solid rgba(0,212,255,0.12);border-radius:6px; + padding:8px 10px;margin-bottom:6px; + font-family:var(--font-mono);font-size:0.72rem; +} +.vm-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:4px} +.vm-name{color:var(--cyan);font-weight:600} +.vm-id{color:var(--text-dim);font-size:0.65rem} +.vm-metrics{display:grid;grid-template-columns:1fr 1fr;gap:4px} +.vm-metric{color:var(--text-dim)} +.vm-metric span{color:var(--text)} + +/* HA DEVICES ────────────────────────────────────────────────────────── */ +.ha-table{width:100%;border-collapse:collapse} +.ha-thead th{ + font-family:var(--font-display);font-size:0.5rem;letter-spacing:2px; + color:var(--text-dim);padding:4px 2px 6px;border-bottom:1px solid rgba(0,212,255,0.15); + text-align:left; +} +.ha-thead th:nth-child(3){text-align:center} +.ha-thead th:nth-child(4){text-align:center} +.ha-row{transition:background 0.12s} +.ha-row:hover{background:rgba(0,212,255,0.05)} +.ha-row td{ + padding:4px 2px;border-bottom:1px solid rgba(0,212,255,0.05); + font-family:var(--font-mono);font-size:0.70rem;vertical-align:middle; +} +.ha-col-domain{font-size:0.85rem;text-align:center;width:20px;padding-right:4px!important} +.ha-col-name{color:var(--text);max-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.ha-col-state{text-align:center;width:30px;font-size:0.62rem;font-weight:700;white-space:nowrap} +.ha-col-state.on{color:var(--green)} +.ha-col-state.off{color:var(--text-dim)} +.ha-col-ctrl{text-align:center;width:36px} +/* toggle switch */ +.ha-toggle{ + position:relative;display:inline-block;width:30px;height:15px;cursor:pointer; +} +.ha-toggle input{opacity:0;width:0;height:0;position:absolute} +.ha-slider{ + position:absolute;inset:0;border-radius:8px; + background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.14); + transition:background 0.18s,border-color 0.18s; +} +.ha-slider::before{ + content:'';position:absolute;left:2px;top:2px; + width:9px;height:9px;border-radius:50%; + background:var(--text-dim);transition:transform 0.18s,background 0.18s; +} +.ha-toggle input:checked + .ha-slider{background:rgba(0,255,100,0.22);border-color:var(--green)} +.ha-toggle input:checked + .ha-slider::before{transform:translateX(15px);background:var(--green)} +/* scene activate button */ +.ha-scene-btn{ + background:transparent;border:1px solid var(--cyan);border-radius:3px; + color:var(--cyan);font-size:0.58rem;padding:1px 4px;cursor:pointer; + font-family:var(--font-mono);transition:background 0.15s; +} +.ha-scene-btn:hover{background:rgba(0,212,255,0.15)} + +/* ALERTS BADGE ─────────────────────────────────────────────────────── */ +.alert-item{ + padding:7px 10px;border-radius:6px; + font-family:var(--font-mono);font-size:0.72rem; + margin-bottom:6px;border-left:3px solid var(--yellow); + background:rgba(255,215,0,0.05); + display:flex;justify-content:space-between;align-items:center; +} +.alert-item.critical{border-color:var(--red);background:rgba(255,34,68,0.05)} +.alert-item.info{border-color:var(--cyan);background:rgba(0,212,255,0.04)} + +/* ── BOTTOM STATUS BAR ──────────────────────────────────────────── */ +#bottomBar{ + height:32px;flex-shrink:0; + background:rgba(0,8,22,0.9); + border-top:1px solid var(--panel-border); + display:flex;align-items:center;padding:0 20px;gap:24px; + font-family:var(--font-mono);font-size:0.68rem;color:var(--text-dim); +} +#bottomBar span{color:var(--cyan)} +.bb-item{display:flex;align-items:center;gap:6px} +.bb-dot{width:5px;height:5px;border-radius:50%} +.bb-dot.online{background:var(--green);box-shadow:0 0 4px var(--green)} +.bb-dot.offline{background:var(--red)} + +/* ── THINKING INDICATOR ──────────────────────────────────────────── */ +.thinking{display:flex;gap:4px;align-items:center;padding:8px 14px} +.thinking-dot{ + width:6px;height:6px;border-radius:50%;background:var(--orange); + animation:thinkBounce 0.6s ease-in-out infinite; +} +.thinking-dot:nth-child(2){animation-delay:0.15s} +.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)} + 50%{opacity:1;transform:translate(-50%,-50%) scale(1);box-shadow:0 0 25px var(--cyan),0 0 55px var(--cyan),0 0 100px rgba(0,212,255,0.6),0 0 150px rgba(0,212,255,0.25)} +} +#arcReactor.speaking .arc-core{ + animation:speakPulse 0.45s ease-in-out infinite; +} +#arcReactor.speaking .arc-ring.r3{border-color:rgba(0,212,255,0.7)} +#arcReactor.speaking .arc-ring.r5{border-color:rgba(255,165,0,0.6)} + +/* ── SCROLLBAR ───────────────────────────────────────────────────── */ +::-webkit-scrollbar{width:4px} +::-webkit-scrollbar-track{background:transparent} +::-webkit-scrollbar-thumb{background:var(--dim);border-radius:2px} + +/* ── TABS (for right panel) ────────────────────────────────────── */ +.tab-bar{display:flex;gap:0;margin-bottom:10px;border-bottom:1px solid var(--panel-border);flex-wrap:wrap} +.forecast-card{background:rgba(0,212,255,0.04);border:1px solid rgba(0,212,255,0.15);border-radius:4px;padding:5px 3px;text-align:center;min-width:0} +.forecast-card .fc-day{font-family:var(--font-display);font-size:0.55rem;color:var(--text-dim);letter-spacing:1px;font-weight:700} +.forecast-card .fc-temps{font-size:0.6rem;color:var(--cyan);font-family:var(--font-display);margin-top:2px} +.forecast-card .fc-rain{font-size:0.5rem;color:#4fc3f7;min-height:0.7rem} +.news-item{padding:6px 0;border-bottom:1px solid rgba(0,212,255,0.08)} +.news-item:last-child{border-bottom:none} +.news-source{font-size:0.5rem;color:var(--cyan);font-family:var(--font-display);letter-spacing:1px} +.news-title{font-size:0.62rem;color:var(--text-primary);line-height:1.3;margin-top:2px} +.news-time{font-size:0.5rem;color:var(--text-dim);margin-top:2px} +.news-cat-header{font-family:var(--font-display);font-size:0.55rem;letter-spacing:2px;color:var(--cyan);margin:8px 0 4px;border-bottom:1px solid rgba(0,212,255,0.2);padding-bottom:3px} +.tab{ + font-family:var(--font-display);font-size:0.55rem;font-weight:700;letter-spacing:2px; + padding:6px 10px;cursor:pointer;color:var(--text-dim); + border-bottom:2px solid transparent;margin-bottom:-1px;transition:all 0.2s; +} +.tab.active{color:var(--cyan);border-bottom-color:var(--cyan)} +.tab-pane{display:none} +.tab-pane.active{display:block} + +/* ── UTILITY ─────────────────────────────────────────────────────── */ +.loading-shimmer{ + background:linear-gradient(90deg,rgba(0,212,255,0.04) 0%,rgba(0,212,255,0.12) 50%,rgba(0,212,255,0.04) 100%); + background-size:200% 100%; + animation:shimmer 1.5s infinite;border-radius:4px;height:12px; +} +@keyframes shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}} +.text-cyan{color:var(--cyan)} +.text-green{color:var(--green)} +.text-orange{color:var(--orange)} +.text-red{color:var(--red)} +.text-dim{color:var(--text-dim)} + +/* ① HUD CORNER RINGS */ +#hudCornersCanvas{position:fixed;inset:0;z-index:3;pointer-events:none} + +/* ② DATA STREAM COLUMNS */ +#dataStreamCanvas{position:fixed;inset:0;z-index:0;pointer-events:none} + +/* ③ NETWORK TOPOLOGY */ +#topoCanvas{display:block;width:100%;flex-shrink:0;cursor:default;border-bottom:1px solid var(--panel-border);margin-bottom:6px} + +/* ④ BOOT SEQUENCE */ +@keyframes bootLeft{0%{opacity:0;transform:translateX(-70px)}100%{opacity:1;transform:none}} +@keyframes bootRight{0%{opacity:0;transform:translateX(70px)}100%{opacity:1;transform:none}} +@keyframes bootDown{0%{opacity:0;transform:translateY(-18px)}100%{opacity:1;transform:none}} +@keyframes bootCenter{0%{opacity:0;transform:scale(0.94) translateY(14px)}100%{opacity:1;transform:none}} +.boot-left{animation:bootLeft 0.55s cubic-bezier(0.4,0,0.2,1) both} +.boot-right{animation:bootRight 0.55s cubic-bezier(0.4,0,0.2,1) both} +.boot-top{animation:bootDown 0.4s ease both} +.boot-center{animation:bootCenter 0.65s cubic-bezier(0.4,0,0.2,1) both} + +/* ⑤ BREATHING EDGE VIGNETTE */ +#vignetteOverlay{position:fixed;inset:0;pointer-events:none;z-index:1; + background:radial-gradient(ellipse at 50% 50%,transparent 32%,rgba(0,2,18,0.6) 100%); + animation:vignettePulse 5s ease-in-out infinite} +#vignetteOverlay.alert-vignette{background:radial-gradient(ellipse at 50% 50%,transparent 32%,rgba(20,0,8,0.65) 100%)} +@keyframes vignettePulse{0%,100%{opacity:0.75}50%{opacity:1}} + +/* ⑥ EKG HEARTBEAT */ +#ekgWrap{flex:1;max-width:180px;display:flex;align-items:center;overflow:hidden} +#ekgCanvas{display:block;width:100%;height:22px;opacity:0.8} + +/* ⑦ AUDIO RING */ +.tb-reactor{position:relative} +#audioRingCanvas{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%); + width:60px;height:60px;pointer-events:none;z-index:4} + +/* ⑧ TYPEWRITER CURSOR */ +@keyframes cursorBlink{0%,100%{opacity:1}49%{opacity:1}50%,99%{opacity:0}} +.type-cursor{display:inline-block;width:6px;height:0.82em;background:var(--cyan);margin-left:1px; + vertical-align:text-bottom;animation:cursorBlink 0.7s step-end infinite} + +/* ⑨ STATIC NOISE BURST */ +@keyframes staticBurst{0%{opacity:0}10%{opacity:1}90%{opacity:1}100%{opacity:0}} +.panel-noise-layer{position:absolute;inset:0;pointer-events:none;z-index:20;border-radius:var(--r); + background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.1'/%3E%3C/svg%3E"); + background-size:100% 100%;mix-blend-mode:screen;animation:staticBurst 0.28s ease forwards} + +/* ── NETWORK MAP OVERLAY ─────────────────────────────────────────────── */ +#netMapOverlay{position:fixed;top:0;left:0;width:100vw;height:100vh; + z-index:200;display:none;flex-direction:column; + background:rgba(0,4,18,0.96);border:1px solid rgba(0,212,255,0.28); + border-top:none;border-left:none;transform-origin:0 0;backdrop-filter:blur(14px); + overflow:hidden;box-shadow:6px 6px 40px rgba(0,0,0,0.75),0 0 50px rgba(0,212,255,0.05)} +#netMapOverlay::after{content:'';position:absolute;inset:0;pointer-events:none;z-index:1; + background: + linear-gradient(to right,rgba(0,212,255,0.7),rgba(0,212,255,0.7)) top left / 18px 1px no-repeat, + linear-gradient(to bottom,rgba(0,212,255,0.7),rgba(0,212,255,0.7)) top left / 1px 18px no-repeat, + linear-gradient(to right,rgba(0,212,255,0.7),rgba(0,212,255,0.7)) top right / 18px 1px no-repeat, + linear-gradient(to bottom,rgba(0,212,255,0.7),rgba(0,212,255,0.7)) top right / 1px 18px no-repeat, + linear-gradient(to right,rgba(0,212,255,0.7),rgba(0,212,255,0.7)) bottom left / 18px 1px no-repeat, + linear-gradient(to bottom,rgba(0,212,255,0.7),rgba(0,212,255,0.7)) bottom left / 1px 18px no-repeat, + linear-gradient(to right,rgba(0,212,255,0.7),rgba(0,212,255,0.7)) bottom right / 18px 1px no-repeat, + linear-gradient(to bottom,rgba(0,212,255,0.7),rgba(0,212,255,0.7)) bottom right / 1px 18px no-repeat} +#netMapOverlay.nm-open{display:flex;animation:nmExplode 0.45s cubic-bezier(0.4,0,0.2,1) forwards} +#netMapOverlay.nm-closing{animation:nmCollapse 0.3s cubic-bezier(0.4,0,0.2,1) forwards} +@keyframes nmExplode{0%{transform:scale(0.04,0.06);opacity:0}60%{opacity:1}100%{transform:scale(1);opacity:1}} +@keyframes nmCollapse{0%{transform:scale(1);opacity:1}100%{transform:scale(0.04,0.06);opacity:0}} +#nmHeader{display:flex;align-items:center;justify-content:space-between;padding:7px 16px; + flex-shrink:0;border-bottom:1px solid rgba(0,212,255,0.16);background:rgba(0,8,28,0.6);z-index:2;position:relative} +#nmTitle{font-family:var(--font-display);font-size:0.62rem;letter-spacing:4px;color:var(--cyan);display:flex;align-items:center;gap:10px} +#nmTitle .nm-pulse{width:6px;height:6px;border-radius:50%;background:var(--cyan);box-shadow:0 0 7px var(--cyan);animation:corePulse 1.5s infinite} +#nmStats{font-family:var(--font-mono);font-size:0.58rem;color:var(--text-dim);display:flex;gap:16px} +#nmStats span{color:var(--cyan)} +#nmClose{background:none;border:1px solid rgba(0,212,255,0.3);color:var(--text-dim);font-family:var(--font-mono);font-size:0.56rem;padding:3px 10px;cursor:pointer;letter-spacing:2px;transition:all 0.2s} +#nmClose:hover{border-color:var(--red);color:var(--red)} +#nmCanvas{flex:1;display:block;z-index:2;position:relative} +#nmLegend{display:flex;gap:16px;align-items:center;padding:5px 16px;flex-shrink:0; + border-top:1px solid rgba(0,212,255,0.1);font-family:var(--font-mono);font-size:0.54rem; + color:var(--text-dim);background:rgba(0,8,28,0.6);z-index:2;position:relative} +.nm-leg-dot{width:7px;height:7px;border-radius:50%;display:inline-block;margin-right:4px;flex-shrink:0} +#nmNodeInfo{position:absolute;pointer-events:none;z-index:10;background:rgba(0,8,30,0.95); + border:1px solid rgba(0,212,255,0.4);padding:7px 11px;font-family:var(--font-mono); + font-size:0.6rem;display:none;min-width:150px;box-shadow:0 0 18px rgba(0,212,255,0.12)} +#nmNodeInfo .ni-title{color:var(--cyan);font-size:0.62rem;letter-spacing:2px;margin-bottom:3px} +#nmNodeInfo .ni-row{color:var(--text-dim);margin:2px 0} + +/* ── SLEEP MODE ──────────────────────────────────────────────────────── */ +#sleepOverlay{ + position:fixed;inset:0;z-index:500;display:none; + flex-direction:column;align-items:center;justify-content:center; + background:rgba(0,2,10,0.94); + backdrop-filter:blur(6px); +} +#sleepOverlay.active{display:flex} +@keyframes sleepPulse{0%,100%{opacity:0.25;transform:scale(1)}50%{opacity:0.6;transform:scale(1.06)}} +@keyframes sleepCoreGlow{0%,100%{box-shadow:0 0 30px rgba(0,212,255,0.15),0 0 60px rgba(0,212,255,0.05)}50%{box-shadow:0 0 50px rgba(0,212,255,0.3),0 0 100px rgba(0,212,255,0.1)}} +.sleep-reactor{position:relative;width:120px;height:120px;margin-bottom:40px} +.sleep-ring{position:absolute;border-radius:50%;border:1px solid rgba(0,212,255,0.2);top:50%;left:50%;transform:translate(-50%,-50%)} +.sleep-ring.sr1{width:120px;height:120px;animation:spinRing 18s linear infinite;border-color:rgba(0,212,255,0.12)} +.sleep-ring.sr2{width:85px;height:85px;animation:spinRing 12s linear infinite reverse;border-color:rgba(0,212,255,0.18)} +.sleep-ring.sr3{width:52px;height:52px;animation:spinRing 8s linear infinite;border-color:rgba(0,80,160,0.3)} +.sleep-core{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%); + width:24px;height:24px;border-radius:50%; + background:radial-gradient(circle,rgba(0,150,220,0.6) 0%,rgba(0,80,160,0.3) 60%,transparent 100%); + animation:sleepCoreGlow 4s ease-in-out infinite,sleepPulse 4s ease-in-out infinite} +.sleep-label{font-family:var(--font-display);font-size:0.6rem;letter-spacing:6px; + color:rgba(0,212,255,0.35);animation:sleepPulse 4s ease-in-out infinite;margin-bottom:12px} +.sleep-sub{font-family:var(--font-mono);font-size:0.55rem;letter-spacing:3px; + color:rgba(0,212,255,0.2);animation:sleepPulse 4s ease-in-out infinite;animation-delay:0.5s} +/* App dims on sleep */ +#app.sleeping #mainLayout,#app.sleeping #topBar,#app.sleeping #bottomBar{ + pointer-events:none; + filter:brightness(0.08) saturate(0.3); + transition:filter 1.2s ease; +} +#app.sleeping #sleepOverlay{display:flex} +/* ── GUARDIAN MODE ────────────────────────────────────────────────── */ +.guardian-event{display:flex;align-items:flex-start;gap:8px;padding:7px 10px;border-bottom:1px solid var(--panel-border);cursor:pointer} +.guardian-event:hover{background:rgba(0,212,255,0.04)} +.guardian-event.critical{border-left:3px solid var(--red)} +.guardian-event.warning{border-left:3px solid #f5a623} +.guardian-event.info{border-left:3px solid rgba(0,212,255,0.3)} +.guardian-event.acked{opacity:0.45} +.guardian-sev{font-family:var(--font-mono);font-size:0.5rem;padding:2px 4px;border-radius:2px;flex-shrink:0;letter-spacing:1px;margin-top:1px} +.guardian-sev.critical{background:rgba(255,34,68,0.15);color:var(--red);border:1px solid rgba(255,34,68,0.3)} +.guardian-sev.warning{background:rgba(245,166,35,0.12);color:#f5a623;border:1px solid rgba(245,166,35,0.3)} +.guardian-sev.info{background:rgba(0,212,255,0.08);color:var(--cyan);border:1px solid rgba(0,212,255,0.2)} +.guardian-msg{flex:1;font-size:0.62rem;line-height:1.4;color:var(--text)} +.guardian-time{font-family:var(--font-mono);font-size:0.52rem;color:var(--text-dim);flex-shrink:0} +.guardian-ai{font-size:0.6rem;color:rgba(0,212,255,0.6);margin-top:3px;font-style:italic} +#bb-guardian-dot.all-clear{background:var(--green);box-shadow:0 0 5px var(--green)} +#bb-guardian-dot.warning{background:#f5a623;box-shadow:0 0 5px #f5a623} +#bb-guardian-dot.critical{background:var(--red);box-shadow:0 0 5px var(--red);animation:pulse 1.2s ease-in-out infinite} +.guardian-ack-btn{background:none;border:1px solid var(--panel-border);color:var(--text-dim);padding:1px 5px;border-radius:2px;font-size:0.5rem;cursor:pointer;font-family:var(--font-mono);letter-spacing:1px;flex-shrink:0} +.guardian-ack-btn:hover{color:var(--cyan);border-color:var(--cyan)} +/* ── VISION PROTOCOL — screenshot lightbox ───────────────────────── */ +#vision-lightbox{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.88);z-index:9999;flex-direction:column;align-items:center;justify-content:flex-start;padding:20px;overflow-y:auto} +#vision-lightbox.open{display:flex} +#vision-lb-header{width:100%;max-width:960px;display:flex;align-items:center;gap:10px;margin-bottom:12px} +#vision-lb-title{font-family:var(--font-display);font-size:0.65rem;letter-spacing:2px;color:var(--cyan);flex:1} +#vision-lb-close{background:none;border:1px solid var(--panel-border);color:var(--text-dim);padding:3px 10px;border-radius:3px;cursor:pointer;font-family:var(--font-display);font-size:0.6rem} +#vision-lb-img{max-width:960px;width:100%;border:1px solid var(--panel-border);border-radius:4px;margin-bottom:12px} +#vision-lb-analysis{max-width:960px;width:100%;background:rgba(0,212,255,0.04);border:1px solid var(--panel-border);border-radius:4px;padding:14px 16px;font-size:0.65rem;line-height:1.7;color:var(--text);white-space:pre-wrap} +#vision-lb-spinner{color:var(--cyan);font-family:var(--font-display);font-size:0.65rem;letter-spacing:2px;animation:pulse 1.5s ease-in-out infinite;margin:30px auto} +/* ── COMMS PROTOCOL — email triage cards ─────────────────────────── */ +.comms-card{background:rgba(0,212,255,0.04);border:1px solid var(--panel-border);border-radius:var(--r);margin-bottom:7px;overflow:hidden} +.comms-card-head{display:flex;align-items:center;gap:7px;padding:7px 10px;cursor:pointer;user-select:none} +.comms-card-head:hover{background:rgba(0,212,255,0.06)} +.comms-card-subject{font-family:var(--font-display);font-size:0.58rem;letter-spacing:1px;color:var(--cyan);flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.comms-card-cat{font-family:var(--font-mono);font-size:0.52rem;padding:2px 5px;border-radius:2px;flex-shrink:0;text-transform:uppercase;letter-spacing:1px} +.comms-card-cat.urgent{color:#ff2244;border:1px solid rgba(255,34,68,0.4);animation:pulse 1.5s ease-in-out infinite} +.comms-card-cat.action{color:#ffd700;border:1px solid rgba(255,215,0,0.4)} +.comms-card-cat.reply{color:var(--cyan);border:1px solid rgba(0,212,255,0.3)} +.comms-card-cat.meeting{color:#a78bfa;border:1px solid rgba(167,139,250,0.4)} +.comms-card-cat.info{color:var(--text-dim);border:1px solid rgba(255,255,255,0.1)} +.comms-card-cat.promo,.comms-card-cat.spam{color:rgba(255,255,255,0.25);border:1px solid rgba(255,255,255,0.08)} +.comms-card-body{display:none;padding:0 10px 10px;border-top:1px solid var(--panel-border)} +.comms-card.open .comms-card-body{display:block} +.comms-card-from{font-family:var(--font-mono);font-size:0.55rem;color:var(--text-dim);margin:7px 0 3px} +.comms-card-summary{font-size:0.62rem;line-height:1.5;color:var(--text);margin:5px 0} +.comms-draft-label{font-family:var(--font-display);font-size:0.5rem;letter-spacing:2px;color:var(--text-dim);margin:8px 0 4px} +.comms-draft{font-size:0.6rem;line-height:1.5;color:rgba(0,212,255,0.7);background:rgba(0,212,255,0.04);border:1px solid rgba(0,212,255,0.15);border-radius:3px;padding:7px 9px;white-space:pre-wrap;max-height:160px;overflow-y:auto} +.comms-empty{text-align:center;padding:24px 10px;font-family:var(--font-mono);font-size:0.6rem;color:var(--text-dim);letter-spacing:1px} +.comms-header-bar{display:flex;gap:5px;margin-bottom:7px;flex-wrap:wrap} +.comms-filter-btn{flex:1;min-width:50px;background:rgba(0,212,255,0.05);border:1px solid var(--panel-border);border-radius:3px;padding:4px 6px;color:var(--text-dim);font-family:var(--font-display);font-size:0.5rem;letter-spacing:1px;cursor:pointer;text-align:center} +.comms-filter-btn.active,.comms-filter-btn:hover{background:rgba(0,212,255,0.12);color:var(--cyan);border-color:var(--cyan)} +.comms-triage-btn{width:100%;background:rgba(0,212,255,0.06);border:1px solid var(--panel-border);border-radius:4px;padding:5px;color:var(--cyan);font-family:var(--font-display);font-size:0.52rem;letter-spacing:2px;cursor:pointer;margin-bottom:7px} +.comms-triage-btn:hover{background:rgba(0,212,255,0.12)} +.comms-prio{font-family:var(--font-mono);font-size:0.52rem;color:var(--text-dim);flex-shrink:0} +.comms-section-label{font-family:var(--font-display);font-size:0.5rem;letter-spacing:2px;color:var(--text-dim);border-bottom:1px solid var(--panel-border);padding:6px 0 4px;margin:8px 0 6px} +.comms-compose-btn{width:100%;background:rgba(0,212,255,0.08);border:1px solid rgba(0,212,255,0.3);border-radius:4px;padding:5px;color:var(--cyan);font-family:var(--font-display);font-size:0.52rem;letter-spacing:2px;cursor:pointer;margin-bottom:5px} +.comms-compose-btn:hover{background:rgba(0,212,255,0.15)} +.comms-send-btn{flex:1;background:rgba(0,212,255,0.1);border:1px solid rgba(0,212,255,0.4);border-radius:3px;padding:3px 6px;color:var(--cyan);font-family:var(--font-display);font-size:0.5rem;letter-spacing:1px;cursor:pointer} +.comms-send-btn:hover{background:rgba(0,212,255,0.2)} +.comms-send-btn:disabled{opacity:0.4;cursor:not-allowed} +.comms-outbox-card{background:rgba(0,212,255,0.03);border:1px solid rgba(0,212,255,0.1);border-radius:3px;padding:6px 9px;margin-bottom:5px} +.comms-outbox-to{font-family:var(--font-mono);font-size:0.52rem;color:var(--text-dim)} +.comms-outbox-subj{font-family:var(--font-display);font-size:0.55rem;color:var(--cyan);margin:2px 0} +.comms-outbox-status{font-family:var(--font-mono);font-size:0.48rem;padding:1px 4px;border-radius:2px} +.comms-outbox-status.sent{color:#00ff88;border:1px solid rgba(0,255,136,0.3)} +.comms-outbox-status.failed{color:#ff2244;border:1px solid rgba(255,34,68,0.3)} +.comms-outbox-status.queued{color:#ffd700;border:1px solid rgba(255,215,0,0.3)} +/* compose modal */ +.comms-compose-modal{position:fixed;inset:0;background:rgba(0,0,0,0.7);display:flex;align-items:center;justify-content:center;z-index:9000} +.comms-compose-inner{background:#0a0e14;border:1px solid var(--cyan);border-radius:6px;padding:16px;width:min(90vw,480px);max-height:80vh;overflow-y:auto} +.comms-compose-title{font-family:var(--font-display);font-size:0.65rem;letter-spacing:2px;color:var(--cyan);margin-bottom:12px} +.comms-compose-field{width:100%;background:rgba(0,212,255,0.04);border:1px solid var(--panel-border);border-radius:3px;padding:6px 8px;color:var(--text);font-family:var(--font-mono);font-size:0.6rem;box-sizing:border-box;margin-bottom:7px} +.comms-compose-field:focus{outline:none;border-color:var(--cyan)} +.comms-compose-actions{display:flex;gap:6px;margin-top:8px} +/* ── DIRECTIVES HUD ──────────────────────────────────────────────── */ +.dir-card{background:rgba(0,212,255,0.03);border:1px solid var(--panel-border);border-radius:var(--r);margin-bottom:7px;overflow:hidden} +.dir-card-head{display:flex;align-items:center;gap:8px;padding:8px 10px;cursor:pointer;user-select:none} +.dir-card-head:hover{background:rgba(0,212,255,0.06)} +.dir-card-title{font-family:var(--font-display);font-size:0.6rem;letter-spacing:1px;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.dir-card-body{display:none;padding:0 10px 10px;border-top:1px solid var(--panel-border)} +.dir-card.open .dir-card-body{display:block} +.dir-progress-bar{height:5px;background:rgba(255,255,255,0.08);border-radius:3px;margin:6px 0} +.dir-progress-fill{height:100%;border-radius:3px;transition:width 0.4s ease} +.dir-kr-row{display:flex;align-items:center;gap:6px;margin:4px 0;font-family:var(--font-mono);font-size:0.52rem;color:var(--text-dim)} +.dir-kr-bar{flex:1;height:3px;background:rgba(255,255,255,0.06);border-radius:2px} +.dir-kr-fill{height:100%;border-radius:2px;background:rgba(0,212,255,0.5)} +.dir-admin-btn{width:100%;background:rgba(0,212,255,0.06);border:1px solid rgba(0,212,255,0.3);border-radius:4px;padding:5px;color:var(--cyan);font-family:var(--font-display);font-size:0.52rem;letter-spacing:2px;cursor:pointer;margin-bottom:7px} +.dir-admin-btn:hover{background:rgba(0,212,255,0.12)} +/* ── MISSION OPS HUD ─────────────────────────────────────────────── */ +.mission-card{background:rgba(0,212,255,0.03);border:1px solid var(--panel-border);border-radius:var(--r);margin-bottom:7px;overflow:hidden} +.mission-card-head{display:flex;align-items:center;gap:8px;padding:8px 10px;cursor:pointer;user-select:none} +.mission-card-head:hover{background:rgba(0,212,255,0.06)} +.mission-card-name{font-family:var(--font-display);font-size:0.6rem;letter-spacing:1px;color:var(--cyan);flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.mission-card-trigger{font-family:var(--font-mono);font-size:0.5rem;padding:2px 5px;border-radius:2px;color:var(--text-dim);border:1px solid rgba(255,255,255,0.1)} +.mission-card-body{display:none;padding:0 10px 10px;border-top:1px solid var(--panel-border)} +.mission-card.open .mission-card-body{display:block} +.mission-run-bar{display:flex;gap:5px;margin-top:8px} +.mission-run-btn{flex:1;background:rgba(0,212,255,0.08);border:1px solid rgba(0,212,255,0.3);border-radius:3px;padding:3px 6px;color:var(--cyan);font-family:var(--font-display);font-size:0.5rem;letter-spacing:1px;cursor:pointer} +.mission-run-btn:hover{background:rgba(0,212,255,0.15)} +.mission-run-btn:disabled{opacity:0.4;cursor:not-allowed} +.mission-run-item{display:flex;align-items:center;gap:6px;padding:3px 0;border-bottom:1px solid rgba(255,255,255,0.04);font-family:var(--font-mono);font-size:0.52rem} +.mission-run-status.done{color:#00ff88} +.mission-run-status.failed{color:#ff2244} +.mission-run-status.running{color:#ffd700;animation:pulse 1.5s ease-in-out infinite} +.mission-new-btn{width:100%;background:rgba(0,212,255,0.06);border:1px solid rgba(0,212,255,0.3);border-radius:4px;padding:5px;color:var(--cyan);font-family:var(--font-display);font-size:0.52rem;letter-spacing:2px;cursor:pointer;margin-bottom:7px} +.mission-new-btn:hover{background:rgba(0,212,255,0.12)} +/* ── CLEARANCE PROTOCOL ──────────────────────────────────────────── */ +#clearance-banner{display:none;background:rgba(255,34,68,0.08);border:1px solid rgba(255,34,68,0.4);border-radius:var(--r);padding:6px 10px;margin:0 0 8px;font-family:var(--font-display);font-size:0.55rem;letter-spacing:1px;color:#ff6680;animation:borderPulse 2s ease-in-out infinite} +@keyframes borderPulse{0%,100%{border-color:rgba(255,34,68,0.4)}50%{border-color:rgba(255,34,68,0.9)}} +#clearance-banner.active{display:flex;align-items:center;gap:8px} +#clearance-banner .clr-count{background:rgba(255,34,68,0.3);border-radius:3px;padding:1px 5px;font-size:0.6rem;color:#ff2244} +#clearance-banner .clr-view{margin-left:auto;cursor:pointer;color:#ff6680;text-decoration:underline} +.clr-card{background:rgba(255,34,68,0.04);border:1px solid rgba(255,34,68,0.3);border-radius:var(--r);margin-bottom:7px;overflow:hidden} +.clr-card-head{display:flex;align-items:center;gap:8px;padding:8px 10px;cursor:pointer;user-select:none} +.clr-card-head:hover{background:rgba(255,34,68,0.06)} +.clr-card-type{font-family:var(--font-display);font-size:0.6rem;letter-spacing:1px;flex:1;color:#ff8899} +.clr-card-risk{font-family:var(--font-mono);font-size:0.5rem;padding:2px 5px;border-radius:2px;border:1px solid} +.clr-card-risk.critical{color:#ff2244;border-color:rgba(255,34,68,0.5)} +.clr-card-risk.high{color:#ffd700;border-color:rgba(255,215,0,0.4)} +.clr-card-risk.medium{color:#ff9900;border-color:rgba(255,153,0,0.4)} +.clr-card-body{display:none;padding:8px 10px 10px;border-top:1px solid rgba(255,34,68,0.2)} +.clr-card.open .clr-card-body{display:block} +.clr-card-desc{font-family:var(--font-mono);font-size:0.55rem;color:var(--text-dim);margin-bottom:8px;line-height:1.5;white-space:pre-wrap} +.clr-action-bar{display:flex;gap:6px;margin-top:8px} +.clr-approve-btn{flex:1;background:rgba(0,255,136,0.08);border:1px solid rgba(0,255,136,0.4);border-radius:3px;padding:4px 8px;color:#00ff88;font-family:var(--font-display);font-size:0.5rem;letter-spacing:2px;cursor:pointer} +.clr-approve-btn:hover{background:rgba(0,255,136,0.18)} +.clr-deny-btn{flex:1;background:rgba(255,34,68,0.08);border:1px solid rgba(255,34,68,0.4);border-radius:3px;padding:4px 8px;color:#ff2244;font-family:var(--font-display);font-size:0.5rem;letter-spacing:2px;cursor:pointer} +.clr-deny-btn:hover{background:rgba(255,34,68,0.18)} +.clr-history-row{display:flex;align-items:center;gap:6px;padding:3px 0;border-bottom:1px solid rgba(255,255,255,0.04);font-family:var(--font-mono);font-size:0.52rem;color:var(--text-dim)} +.clr-status-approved{color:#00ff88}.clr-status-denied{color:#ff2244}.clr-status-pending{color:#ffd700}.clr-status-expired{color:rgba(255,255,255,0.3)} +.clr-rule-row{display:flex;align-items:center;gap:6px;padding:5px 0;border-bottom:1px solid rgba(255,255,255,0.04);font-family:var(--font-mono);font-size:0.52rem} +.clr-rule-type{flex:1;color:var(--cyan)} +.clr-rule-toggle{cursor:pointer;padding:2px 6px;border-radius:2px;font-size:0.48rem;border:1px solid} +.clr-rule-enabled{color:#00ff88;border-color:rgba(0,255,136,0.4)} +.clr-rule-disabled{color:rgba(255,255,255,0.3);border-color:rgba(255,255,255,0.15)} +.clr-admin-btn{width:100%;background:rgba(255,34,68,0.06);border:1px solid rgba(255,34,68,0.3);border-radius:4px;padding:5px;color:#ff6680;font-family:var(--font-display);font-size:0.52rem;letter-spacing:2px;cursor:pointer;margin-bottom:7px} +.clr-admin-btn:hover{background:rgba(255,34,68,0.12)} +/* ── INTEL PROTOCOL — research result cards ──────────────────────── */ +.intel-card{background:rgba(0,212,255,0.04);border:1px solid var(--panel-border);border-radius:var(--r);margin-bottom:8px;overflow:hidden} +.intel-card-head{display:flex;align-items:center;gap:8px;padding:8px 10px;cursor:pointer;user-select:none} +.intel-card-head:hover{background:rgba(0,212,255,0.06)} +.intel-card-query{font-family:var(--font-display);font-size:0.6rem;letter-spacing:1px;color:var(--cyan);flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.intel-card-status{font-family:var(--font-mono);font-size:0.55rem;padding:2px 6px;border-radius:2px;flex-shrink:0} +.intel-card-status.running{color:#ffd700;border:1px solid rgba(255,215,0,0.4);animation:pulse 1.5s ease-in-out infinite} +.intel-card-status.done{color:var(--green);border:1px solid rgba(0,255,136,0.3)} +.intel-card-status.failed{color:var(--red);border:1px solid rgba(255,34,68,0.3)} +.intel-card-body{display:none;padding:0 10px 10px;border-top:1px solid var(--panel-border)} +.intel-card.open .intel-card-body{display:block} +.intel-card-body .synthesis{font-size:0.65rem;line-height:1.6;color:var(--text);margin:8px 0;white-space:pre-wrap} +.intel-sources{margin-top:8px} +.intel-source{font-size:0.58rem;color:var(--text-dim);padding:2px 0;border-bottom:1px solid rgba(0,212,255,0.06)} +.intel-source a{color:var(--cyan2);text-decoration:none} +.intel-source a:hover{text-decoration:underline} +.intel-empty{text-align:center;padding:24px 10px;font-family:var(--font-mono);font-size:0.6rem;color:var(--text-dim);letter-spacing:1px} +.intel-new-btn{width:100%;background:rgba(0,212,255,0.06);border:1px solid var(--panel-border);border-radius:4px;padding:5px;color:var(--cyan);font-family:var(--font-display);font-size:0.55rem;letter-spacing:2px;cursor:pointer;margin-bottom:8px} +.intel-new-btn:hover{background:rgba(0,212,255,0.12)} +@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.4}} + diff --git a/public_html/assets/js/jarvis-app.js b/public_html/assets/js/jarvis-app.js new file mode 100644 index 0000000..e2a41bd --- /dev/null +++ b/public_html/assets/js/jarvis-app.js @@ -0,0 +1,1482 @@ +// ── GLOBALS ────────────────────────────────────────────────────────── +let sessionToken = ''; +let sessionUser = ''; +let sessionId = 'session_' + Date.now(); +let isListening = false; +let recognition = null; +let synth = window.speechSynthesis; +let selectedVoice = null; +let refreshTimer = null; +let isSpeaking = false; +let panelsVisible = true; +let cameraActive = false; +let faceLoopId = null; +let lastFaceSeen = 0; +let autoMicCooldown = 0; +let faceApiReady = false; +let lastActivity = Date.now(); +const IDLE_RELOAD_MS = 5 * 60 * 1000; // 5 min inactivity → full reload +let voiceMode = false; // true = JARVIS awake (listening for commands) +let voiceMuted = false; // true = awake but mic muted +let voiceLastCmd = 0; +const VOICE_SLEEP_MS = 30 * 60 * 1000; // 30 min voice inactivity → sleep +const VOICE_ACTIVE_MS = 17000; // 17s active window after each command +let voiceActive = 0; // timestamp of last issued command +// Phase 1: full phrase required to wake from sleep +const WAKE_PHRASES = ["wake up jarvis", "daddy's home", "wake up, jarvis", "daddys home"]; +// Phase 2: command prefix — "jarvis "; then 17s free-listen window +const CMD_PREFIX = 'jarvis'; +const FACE_MODEL_URL = 'https://cdn.jsdelivr.net/gh/justadudewhohacks/face-api.js@0.22.2/weights'; + +// ── INIT ───────────────────────────────────────────────────────────── +function initCollapsiblePanels() { + document.querySelectorAll('.panel').forEach(panel => { + const title = panel.querySelector('.panel-title'); + if (!title) return; + const btn = document.createElement('button'); + btn.className = 'panel-collapse-btn'; + btn.textContent = '▾'; + btn.title = 'Collapse / expand'; + title.appendChild(btn); + const key = 'pnl_' + (title.textContent||'').trim().substring(0,24).replace(/\s+/g,'_').toLowerCase().replace(/[^a-z0-9_]/g,''); + if (localStorage.getItem(key) === '1') panel.classList.add('collapsed'); + title.addEventListener('click', e => { + if (e.target.closest('button:not(.panel-collapse-btn),a,input,select')) return; + const col = panel.classList.toggle('collapsed'); + localStorage.setItem(key, col ? '1' : '0'); + }); + }); +} + +window.addEventListener("load", () => { + ["mousemove","keydown","touchstart","click"].forEach(e => + window.addEventListener(e, () => { lastActivity = Date.now(); }, {passive:true}) + ); + updateClock(); + setInterval(updateClock, 1000); + initVoice(); + loadVoices(); + + // Check if already logged in — prefer PHP-injected global, fall back to sessionStorage + const saved = (typeof __jarvisToken !== 'undefined' ? __jarvisToken : null) + || sessionStorage.getItem('jarvis_token'); + const savedUser = (typeof __jarvisUser !== 'undefined' ? __jarvisUser : null) + || sessionStorage.getItem('jarvis_user') || ''; + const autoReload = sessionStorage.getItem('jarvis_autoreload') === '1'; + sessionStorage.removeItem('jarvis_autoreload'); + if (saved) { + sessionToken = saved; + sessionUser = savedUser; + try { sessionStorage.setItem('jarvis_token', saved); sessionStorage.setItem('jarvis_user', savedUser); } catch(e) {} + if (localStorage.getItem('jarvis_panels_swapped') === '1') swapPanels(); + showApp(savedUser, null, autoReload); + } +}); + +function updateClock() { + const now = new Date(); + document.getElementById('clock').textContent = + now.toLocaleTimeString('en-US',{hour12:false,hour:'2-digit',minute:'2-digit',second:'2-digit'}); + document.getElementById('date-display').textContent = + now.toLocaleDateString('en-US',{weekday:'short',year:'numeric',month:'short',day:'numeric'}).toUpperCase(); +} + +// ── LOGIN ───────────────────────────────────────────────────────────── +document.getElementById('loginForm').addEventListener('submit', async (e) => { + e.preventDefault(); + const user = document.getElementById('loginUser').value; + const pass = document.getElementById('loginPass').value; + const errEl = document.getElementById('loginError'); + errEl.textContent = ''; + + try { + const res = await api('auth', 'POST', {username:user, password:pass}); + if (res.success) { + sessionToken = res.token; + sessionUser = res.display_name; + sessionStorage.setItem('jarvis_token', sessionToken); + sessionStorage.setItem('jarvis_user', sessionUser); + showApp(sessionUser, res.greeting); + } else { + errEl.textContent = 'ACCESS DENIED'; + } + } catch(err) { + errEl.textContent = 'CONNECTION FAILED'; + } +}); + +function showApp(name, greeting, silent = false) { + document.getElementById('loginScreen').style.display = 'none'; + const app = document.getElementById('app'); + app.style.display = 'flex'; + + // HUD boot sequence — staggered slide-in + const topBar = document.getElementById('topBar'); + const leftPanel = document.getElementById('leftPanel'); + const rightPanel = document.getElementById('rightPanel'); + const centerPanel= document.getElementById('centerPanel'); + [topBar, leftPanel, rightPanel, centerPanel].forEach(el => el && (el.style.opacity = '0')); + requestAnimationFrame(() => { + if (topBar) { topBar.style.opacity=''; topBar.style.animationDelay='0s'; topBar.classList.add('boot-top'); } + setTimeout(()=>{ if(leftPanel) { leftPanel.style.opacity=''; leftPanel.style.animationDelay='0s'; leftPanel.classList.add('boot-left'); }}, 120); + setTimeout(()=>{ if(rightPanel) { rightPanel.style.opacity=''; rightPanel.style.animationDelay='0s'; rightPanel.classList.add('boot-right'); }}, 180); + setTimeout(()=>{ if(centerPanel){ centerPanel.style.opacity='';centerPanel.style.animationDelay='0s';centerPanel.classList.add('boot-center');}}, 240); + setTimeout(()=>{ [topBar,leftPanel,rightPanel,centerPanel].forEach(el=>el?.classList.remove('boot-top','boot-left','boot-right','boot-center')); }, 1200); + }); + + if (!silent) { + if (greeting) { + addMessage('jarvis', greeting); + speak(greeting); + } else { + const g = `Welcome back, ${name}. All systems online and standing by.`; + addMessage('jarvis', g); + speak(g); + } + } + + // Start data refresh + initCollapsiblePanels(); + refreshAll(); + refreshTimer = setInterval(refreshAll, 10000); // every 10s + setInterval(() => { + if (!isAsleep && Date.now() - lastActivity > IDLE_RELOAD_MS) { + sessionStorage.setItem('jarvis_autoreload', '1'); + location.reload(); + } + }, 30000); + setInterval(() => { + if (voiceMode && voiceLastCmd > 0 && Date.now() - voiceLastCmd > VOICE_SLEEP_MS) { + exitVoiceMode(); + } + }, 60000); + // Watchdog: reset isSpeaking if stuck; heartbeat keeps mic alive + setInterval(() => { + if (isSpeaking && !_ttsAudio && !window.speechSynthesis?.speaking) { + isSpeaking = false; + if (isListening) _scheduleRecStart(200); + } + }, 4000); + // Heartbeat: if mic should be on but recognition has gone quiet, nudge it + setInterval(() => { + if (isListening && !isSpeaking) { + try { + recognition.start(); // throws if already running — that's fine + } catch(_) {} + } + }, 12000); + startListening(); + loadNetwork(); + loadHA(); + checkAgentStatus(); + checkArcStatus().catch(() => {}); + loadAgents(); + loadAlerts(); + loadWeather(); + loadNews(); + initMobile(); + setTimeout(checkPlannerReminder, 3000); + setInterval(checkUpcomingAppts, 300000); + 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(() => { + _refreshGuardianBadge(); + _pollProactiveChat(); + startGuardianPolling(); + setInterval(_pollProactiveChat, 30000); + }, 5000); + // Clearance banner — poll every 30s + setTimeout(() => { + updateClearanceBanner(); + setInterval(updateClearanceBanner, 30000); + }, 6000); + // Memory Core — poll count every 60s + setTimeout(() => { + updateMemoryCount(); + setInterval(updateMemoryCount, 60000); + }, 8000); +} + +async function logout() { + clearInterval(refreshTimer); + await api('auth', 'DELETE', {}); + sessionStorage.clear(); + location.reload(); +} + +// ── API HELPER ──────────────────────────────────────────────────────── +async function api(endpoint, method='GET', body=null) { + const opts = { + method, + headers: {'Content-Type':'application/json','X-Session-Token':sessionToken}, + credentials:'include', + }; + if (body && method !== 'GET') opts.body = JSON.stringify(body); + const res = await fetch('/api/' + endpoint, opts); + if (res.status === 401) { logout(); return {}; } + return res.json(); +} + +// ── PANEL TOGGLE ───────────────────────────────────────────────────── +function swapPanels() { + const layout = document.getElementById('mainLayout'); + const btn = document.getElementById('btn-swap-panels'); + const isSwapped = layout.classList.toggle('swapped'); + btn.classList.toggle('active', isSwapped); + localStorage.setItem('jarvis_panels_swapped', isSwapped ? '1' : '0'); +} + +function togglePanels(silent) { + panelsVisible = !panelsVisible; + const layout = document.getElementById('mainLayout'); + const btn = document.getElementById('panelToggleBtn'); + if (panelsVisible) { + layout.classList.remove('focus-mode'); + btn.classList.remove('focus-active'); + btn.textContent = '◧ PANELS'; + if (!silent) speak('Full view restored.'); + } else { + layout.classList.add('focus-mode'); + btn.classList.add('focus-active'); + btn.textContent = '◫ FOCUS'; + if (!silent) speak('Focus mode activated. Side panels hidden.'); + } +} + +// Keyboard shortcut: backslash to toggle panels +document.addEventListener('keydown', (e) => { + if (e.key === '\\' && document.activeElement.id !== 'textInput') { + togglePanels(); + } +}); + +// ── CAMERA FACE DETECTION / AUTO-MIC ───────────────────────────────── +async function loadFaceApi() { + if (faceApiReady) return true; + try { + if (typeof faceapi === 'undefined') { + addMessage('system', 'Face detection library not available.'); + return false; + } + await faceapi.nets.tinyFaceDetector.loadFromUri(FACE_MODEL_URL); + faceApiReady = true; + return true; + } catch(e) { + addMessage('system', 'Could not load face detection model: ' + e.message); + return false; + } +} + +async function startCamera() { + if (cameraActive) return; + const btn = document.getElementById('cameraBtn'); + btn.textContent = '◉ LOADING…'; + try { + const stream = await navigator.mediaDevices.getUserMedia( + {video:{facingMode:'user', width:{ideal:320}, height:{ideal:240}}, audio:false} + ); + const video = document.getElementById('faceVideo'); + video.srcObject = stream; + await video.play(); + + const ok = await loadFaceApi(); + if (!ok) { stopCamera(); return; } + + cameraActive = true; + btn.classList.add('cam-active'); + btn.textContent = '◉ SENSING'; + startFaceTracking(); + addMessage('system', 'Face detection active — reactor tracking engaged.'); + + faceLoopId = setInterval(async () => { + if (!cameraActive) return; + // Run detection even while speaking — needed for tracking + prevents lastFaceSeen staling out + try { + const detection = await faceapi.detectSingleFace( + document.getElementById('faceVideo'), + new faceapi.TinyFaceDetectorOptions({inputSize:160, scoreThreshold:0.45}) + ); + const now = Date.now(); + if (detection) { + lastFaceSeen = now; + const ratio = (detection.box.width * detection.box.height) / (320 * 240); + + // Always drive the reactor + updateFaceTarget(detection.box, 320, 240); + + // Only auto-trigger voice when not already speaking/active, cooldown passed + if (ratio > 0.03 && !voiceMode && !isSpeaking && now > autoMicCooldown) { + autoMicCooldown = now + 9000; + document.getElementById('cameraBtn').classList.add('cam-sensing'); + enterVoiceMode(); + } + } else { + // While JARVIS is speaking, keep lastFaceSeen fresh so the exit timer doesn't tick down + if (isSpeaking) { lastFaceSeen = now; } + else { clearFaceTarget(); } + + document.getElementById('cameraBtn').classList.remove('cam-sensing'); + + // Exit voice mode only if: face gone >12s AND no command in that same window AND not speaking + const noFaceMs = now - lastFaceSeen; + const noCommandMs = now - (voiceLastCmd || 0); + if (voiceMode && !isSpeaking && noFaceMs > 12000 && noCommandMs > 12000) { + exitVoiceMode(); + } + } + } catch(_) {} + }, 600); + + } catch(e) { + btn.textContent = '◉ CAMERA'; + if (e.name === 'NotAllowedError') { + addMessage('system', 'Camera permission denied. Grant camera access in browser settings to enable hands-free mode.'); + } else { + addMessage('system', 'Camera unavailable: ' + e.message); + } + } +} + +function stopCamera() { + cameraActive = false; + clearInterval(faceLoopId); + faceLoopId = null; + const video = document.getElementById('faceVideo'); + if (video && video.srcObject) { + video.srcObject.getTracks().forEach(t => t.stop()); + video.srcObject = null; + } + const btn = document.getElementById('cameraBtn'); + if (btn) { + btn.classList.remove('cam-active', 'cam-sensing'); + btn.textContent = '◉ CAMERA'; + } + stopFaceTracking(); +} + +function toggleCamera() { + if (cameraActive) { + stopCamera(); + addMessage('system', 'Face detection disabled.'); + } else { + startCamera(); + } +} + +// ── REFRESH ALL ─────────────────────────────────────────────────────── +let _refreshTick = 0; +let selectedContext = null; +const _panelCtx = {}; +let _haEntities = {}; +const _svcLabels = {lshttpd:'WEB',mysql:'MYSQL',redis:'REDIS',memcached:'MEMCACHE',postfix:'POSTFIX',dovecot:'DOVECOT','jarvis-agent':'AGENT'}; + +async function refreshAll() { + _refreshTick++; + const el = document.getElementById('last-refresh'); + if (el) el.textContent = new Date().toLocaleTimeString('en-US',{hour12:false}); + + // Fire core calls in parallel — cuts refresh latency from ~3s to ~600ms + const [s, n, d] = await Promise.all([ + api('system').catch(() => null), + api('network').catch(() => null), + api('do').catch(() => null), + ]); + if (s) renderSystem(s); + if (n) renderNetworkStatus(n); + if (d) renderDO(d); + + // Agent status every tick (fire and forget — doesn't block) + checkAgentStatus().catch(() => {}); + + // Refresh right-panel tabs every 3rd tick (~30s) — all parallel + if (_refreshTick % 3 === 0) { + Promise.all([ + loadHA().catch(() => {}), + loadAlerts().catch(() => {}), + loadAgents().catch(() => {}), + loadProxmox().catch(() => {}), + loadPlannerSummary().catch(() => {}), + ]); + } + // Refresh Arc Reactor status every 6th tick (~60s) + if (_refreshTick % 6 === 0) { + checkArcStatus().catch(() => {}); + } + // Refresh weather + news every 18th tick (~3 min) + if (_refreshTick % 18 === 0) { + Promise.all([ + loadWeather().catch(() => {}), + loadNews().catch(() => {}), + ]); + } +} + +// ── ANIMATED NUMBER COUNTER ─────────────────────────────────────────── +const _prevVals = {}; +function tickTo(id, newVal, unit='%', decimals=0) { + const el = document.getElementById(id); + if (!el) return; + const prev = _prevVals[id] ?? newVal; + _prevVals[id] = newVal; + if (Math.abs(newVal - prev) < 0.5) { el.textContent = newVal.toFixed(decimals) + unit; return; } + const start = performance.now(), dur = 700; + (function frame(now) { + const p = Math.min((now - start) / dur, 1); + const ease = 1 - Math.pow(1 - p, 3); + el.textContent = (prev + (newVal - prev) * ease).toFixed(decimals) + unit; + if (p < 1) requestAnimationFrame(frame); + })(performance.now()); +} + +// ── RENDER: SYSTEM ──────────────────────────────────────────────────── +function renderSystem(s) { + if (!s || s.error) return; + const cpu = s.cpu || 0; + const mem = s.memory?.percent || 0; + const disk = s.disk?.percent || 0; + + // Top bar (animated) + tickTo('tb-cpu', cpu, ''); + tickTo('tb-mem', mem, ''); + + // Metric bars + setBar('cpu', cpu); + setBar('mem', mem); + setBar('disk', disk); + + tickTo('cpu-val', cpu); + tickTo('mem-val', mem); + tickTo('disk-val', disk); + + // Sparklines + pushSparkData('cpu', cpu); + pushSparkData('mem', mem); + pushSparkData('disk', disk); + drawSparkline('spark-cpu', _sparkData.cpu, 'rgb(0,212,255)'); + drawSparkline('spark-mem', _sparkData.mem, 'rgb(0,255,136)'); + drawSparkline('spark-disk', _sparkData.disk, 'rgb(255,166,0)'); + + // Flash the system panel on data arrival + flashPanel(document.querySelector('#leftPanel .panel')); + document.getElementById('uptime-val').textContent = s.uptime || '--'; + document.getElementById('load-val').textContent = s.load?.['1m'] || '--'; + document.getElementById('host-val').textContent = s.hostname || 'jarvis'; + + // Services + if (s.services) { + const svcEl = document.getElementById('services-list'); + svcEl.innerHTML = Object.entries(s.services).map(([k,v]) => + `
+ ${_svcLabels[k]||k.toUpperCase()} +
+
` + ).join(''); + } + + // Processes + if (s.processes?.length) { + document.getElementById('procs-list').innerHTML = s.processes.map(p => + `
+
${p.cmd}
+
${p.cpu}%
+
` + ).join(''); + } +} + +function setBar(id, pct) { + const el = document.getElementById(id+'-bar'); + if (!el) return; + el.style.width = Math.min(pct,100) + '%'; + el.className = 'metric-bar-fill' + (pct>90?' danger':pct>75?' warn':''); +} + +// ── RENDER: DO SERVER (site health only — metrics merged into system panel) ─── +function renderDO(d) { + const dot = document.getElementById('bb-do-dot'); + const status = document.getElementById('bb-do-status'); + const sitesEl = document.getElementById('sites-list'); + + if (!d || d.error || !d.reachable) { + if (dot) dot.className = 'bb-dot offline'; + if (status) status.textContent = 'OFFLINE'; + document.getElementById('tb-do').className = 'text-red'; + document.getElementById('tb-do').textContent = 'OFFLINE'; + if (sitesEl) sitesEl.innerHTML = '
Unavailable
'; + return; + } + + dot.className = 'bb-dot online'; + status.textContent = 'ONLINE'; + document.getElementById('tb-do').className = 'text-green'; + document.getElementById('tb-do').textContent = 'ONLINE'; + + if (sitesEl && d.sites && Object.keys(d.sites).length) { + sitesEl.innerHTML = Object.entries(d.sites).map(([k, v]) => { + const cls = v === 'up' ? 'ok' : v === 'down' ? 'danger' : 'warn'; + const lbl = k.replace(/^https?:\/\//, '').replace(/\.orbishosting\.com$/, '').replace(/\.com$/, ''); + return `
+
${lbl}
+
${v.toUpperCase()}
+
`; + }).join(''); + } +} + +async function loadNetwork() { + try { + const n = await api('network'); + renderNetworkStatus(n); + } catch(e) {} +} + +// ── RENDER: NETWORK ─────────────────────────────────────────────────── +function renderNetworkStatus(n) { + if (!n) return; + renderTopology(n.devices || []); + const el = document.getElementById('network-list'); + if (!el) return; + const devices = n.devices || []; + const online = devices.filter(d => d.alive || d.status === 'online').length; + const countEl = document.getElementById('net-agent-count'); + if (countEl) countEl.textContent = online + '/' + devices.length + ' ONLINE'; + + const agents = devices.filter(d => d.source === 'agent'); + const others = devices.filter(d => d.source !== 'agent'); + + function renderDev(d) { + const alive = d.alive || d.status === 'online'; + const ctxKey = d.source === 'agent' ? 'agent_' + d.agent_id : 'net_' + (d.ip||'').replace(/\./g,'_'); + _panelCtx[ctxKey] = {type: d.source === 'agent' ? 'agent' : 'network', + label: d.name || d.ip, ip: d.ip, status: d.status || (alive ? 'online' : 'offline'), + agent_id: d.agent_id, hostname: d.name}; + const lat = d.latency_ms ? ' · ' + d.latency_ms + 'ms' : ''; + const badge = d.source === 'agent' + ? `${(d.agent_type||'AGENT').toUpperCase()}` : ''; + const del = d.deletable + ? `` : ''; + const bl = d.source === 'agent' ? 'border-left:2px solid ' + (alive ? 'var(--green)' : 'var(--red)') + ';' : ''; + return `
+
+
+
${d.name||d.ip}${badge}
+
${d.ip||''}${lat}
+
${del} +
`; + } + + let out = ''; + if (agents.length) { + const agOn = agents.filter(d => d.alive || d.status === 'online').length; + out += `
AGENTS (${agOn}/${agents.length})
`; + out += agents.map(renderDev).join(''); + } + if (others.length) { + if (agents.length) out += '
'; + out += `
DEVICES
`; + out += others.map(renderDev).join(''); + } + if (!out) out = '
No devices
'; + el.innerHTML = out; +} + +// ── NETWORK SCAN ────────────────────────────────────────────────────── +async function scanNetwork() { + const btn = document.getElementById('scanBtn'); + btn.textContent = 'QUEUING...'; + btn.disabled = true; + + try { + const data = await api('network/scan'); + const count = data.count ?? 0; + const msg = data.queued + ? `Network scan dispatched to PVE1 probe, Sir. Currently showing ${count} active device${count!==1?'s':''} — panel will refresh with live results in approximately 40 seconds.` + : `Showing last known network data: ${count} active device${count!==1?'s':''} on 10.48.200.0/24. PVE1 probe scans automatically every 3 minutes.`; + addMessage('jarvis', msg); + speak(count + ' devices online.'); + // Refresh the network panel with current data + loadNetwork(); + // Auto-refresh again after 45s to catch PVE1 scan results + if (data.queued) setTimeout(loadNetwork, 45000); + } catch(e) { + addMessage('jarvis', 'Network scan request failed, Sir.'); + } + + btn.textContent = 'RUN NETWORK SCAN'; + btn.disabled = false; +} + +// ── PROXMOX ─────────────────────────────────────────────────────────── +async function loadProxmox() { + const data = await api('proxmox'); + const el = document.getElementById('vm-list'); + const dot = document.getElementById('bb-pve-dot'); + const status = document.getElementById('bb-pve-status'); + + if (!data.configured) { + el.innerHTML = `
+
⚠ NOT CONFIGURED
+ Set PROXMOX_HOST and PROXMOX_TOKEN_VAL in config.php to enable VM monitoring. +
`; + dot.className='bb-dot offline'; status.textContent='NOT CONFIGURED'; + return; + } + + dot.className='bb-dot online'; status.textContent='ONLINE'; + + const vms = [...(data.vms||[]), ...(data.containers||[])]; + if (!vms.length) { + el.innerHTML = '
No VMs found.
'; + return; + } + + el.innerHTML = vms.map(vm => { + const statusColor = vm.status==='running'?'var(--green)':vm.status==='stopped'?'var(--red)':'var(--yellow)'; + const cpuClass = vm.cpu>80?'text-red':vm.cpu>60?'text-orange':'text-cyan'; + const ctxKey = 'vm_' + vm.vmid; + _panelCtx[ctxKey] = {type:'vm', label:vm.name, + vmid:vm.vmid, name:vm.name, status:vm.status, + cpu:vm.cpu, mem_mb:vm.mem_mb, maxmem_mb:vm.maxmem_mb, + type_label:vm.type||'qemu', uptime:vm.uptime||0}; + return `
+
+ ${vm.name} + ● ${(vm.status||'').toUpperCase()} +
+
+
CPU ${vm.cpu}%
+
RAM ${vm.mem_mb||0}/${vm.maxmem_mb||0}MB
+
ID ${vm.vmid}
+
TYPE ${vm.type||'qemu'}
+
+
`; + }).join(''); +} + +// ── HOME ASSISTANT ──────────────────────────────────────────────────── +async function loadHA() { + const data = await api('ha'); + const el = document.getElementById('ha-list'); + const dot = document.getElementById('bb-ha-dot'); + const sta = document.getElementById('bb-ha-status'); + + if (!data.configured) { + el.innerHTML = `
+
⚠ NOT CONFIGURED
+ Set HA_URL and HA_TOKEN in config.php to enable smart home control. +
`; + dot.className='bb-dot offline'; sta.textContent='NOT CONFIGURED'; + return; + } + + dot.className='bb-dot online'; sta.textContent='ONLINE'; + + const entities = data.entities || {}; + _haEntities = entities; + if (!Object.keys(entities).length) { + el.innerHTML = '
No entities found.
'; + return; + } + + renderHATable(entities); +} + +const _domainIcon = { + light:'\u{1F4A1}', switch:'\u{1F50C}', scene:'\u{1F3AC}', + media_player:'\u{1F4FA}', alarm_control_panel:'\u{1F512}', + lawn_mower:'\u{1F33F}', water_heater:'\u{1F321}', fan:'\u{1F4A8}', + lock:'\u{1F511}', cover:'\u{1FA9F}', climate:'☃', input_boolean:'⚙' + }; + +function renderHATable(entities) { + const el = document.getElementById('ha-list'); + if (!el) return; + let rows = ''; + let totalShown = 0; + for (const [domain, items] of Object.entries(entities)) { + const icon = _domainIcon[domain] || '•'; + const available = items.filter(e => e.state !== 'unavailable' && e.state !== 'unknown'); + if (!available.length) continue; + available.forEach(e => { + totalShown++; + const isOn = ['on','home','open','locked','playing','mowing','armed_home','armed_away','armed_night'].includes(e.state); + const isScene = domain === 'scene'; + const ctxKey = 'ha_' + e.entity_id.replace(/[^a-z0-9]/gi,'_'); + _panelCtx[ctxKey] = {type:'ha', label:e.name, + entity_id:e.entity_id, name:e.name, state:e.state, domain:domain}; + const stateLabel = isScene ? '—' : (isOn ? 'ON' : 'OFF'); + const stateClass = isOn ? 'on' : 'off'; + const eid = e.entity_id.replace(/'/g,"\\'"); + const ctrl = isScene + ? `` + : ``; + rows += ` + ${icon} + ${e.name} + ${stateLabel} + ${ctrl} + `; + }); + } + if (!totalShown) { + el.innerHTML = '
No available entities.
'; + return; + } + el.innerHTML = ` + + ${rows}
DEVICESTATECTRL
`; +} + +async function toggleHA(entityId, domain, currentState) { + let service; + const ON_STATES = ['on','home','open','locked','playing','mowing','armed_home','armed_away','armed_night','active']; + const wasOn = ON_STATES.includes(currentState); + if (domain === 'scene') { + service = 'turn_on'; + } else if (domain === 'alarm_control_panel') { + service = currentState === 'disarmed' ? 'alarm_arm_away' : 'alarm_disarm'; + } else { + service = wasOn ? 'turn_off' : 'turn_on'; + } + try { + await api('ha/service', 'POST', {domain, service, entity_id: entityId}); + // Optimistic update — flip state immediately so toggle doesn't snap back + if (_haEntities[domain]) { + const ent = _haEntities[domain].find(e => e.entity_id === entityId); + if (ent && domain !== 'scene') ent.state = wasOn ? 'off' : 'on'; + } + renderHATable(_haEntities); + // Full sync after 4s — HA executes + agent pushes new state + setTimeout(loadHA, 4000); + } catch(e) {} +} + +// ── PROACTIVE REMINDERS ────────────────────────────────────────────────────── +let _reminderShown = false; +async function checkPlannerReminder() { + if (_reminderShown || sessionStorage.getItem('reminderShown')) return; + _reminderShown = true; + sessionStorage.setItem('reminderShown', '1'); + const d = await api('planner/today').catch(() => null); + if (!d) return; + const tasks = [...(d.tasks_overdue||[]), ...(d.tasks_today||[])]; + const appts = d.appts_today || []; + const overdue = d.tasks_overdue?.length || 0; + if (!tasks.length && !appts.length) return; + + const parts = []; + if (overdue) parts.push(overdue + ' overdue task' + (overdue > 1 ? 's' : '')); + if (tasks.length - overdue > 0) parts.push((tasks.length - overdue) + ' task' + (tasks.length - overdue > 1 ? 's' : '') + ' due today'); + if (appts.length) { + const nextAppt = appts[0]; + const t = nextAppt.start_at ? new Date(nextAppt.start_at).toLocaleTimeString('en-US',{hour:'2-digit',minute:'2-digit',hour12:true}) : ''; + parts.push((t ? 'appointment at ' + t : appts.length + ' appointment' + (appts.length > 1 ? 's' : '') + ' today')); + } + + const msg = 'Heads up, ' + (sessionUser||'Sir') + '. You have ' + parts.join(' and ') + '.'; + addMessage('jarvis', msg); + if (typeof speak === 'function' && isVoiceActive) speak(msg); +} + +// Check for upcoming appointments (fires every 5 min after load) +let _apptAlerted = new Set(); +async function checkUpcomingAppts() { + const d = await api('planner/today').catch(() => null); + if (!d) return; + const now = Date.now(); + for (const a of (d.appts_today||[])) { + if (!a.start_at || _apptAlerted.has(a.id)) continue; + const start = new Date(a.start_at).getTime(); + const minsUntil = (start - now) / 60000; + if (minsUntil > 0 && minsUntil <= 15) { + _apptAlerted.add(a.id); + const msg = 'Reminder: ' + a.title + ' starts in ' + Math.round(minsUntil) + ' minutes' + (a.location ? ' at ' + a.location : '') + '.'; + addMessage('jarvis', msg); + if (typeof speak === 'function' && isVoiceActive) speak(msg); + } + } +} + +// ── PLANNER SUMMARY (top bar badge only) ───────────────────────────────── +async function loadPlannerSummary() { + const d = await api('planner/today'); + const el = document.getElementById('tb-planner'); + const tx = document.getElementById('tb-planner-text'); + if (el && tx) { + const tasksDue = (d.tasks_today || []).length + (d.tasks_overdue || []).length; + const appts = (d.appts_today || []).length; + if (!tasksDue && !appts) { el.style.display = 'none'; } + else { + const parts = []; + if (tasksDue) parts.push(tasksDue + ' TASK' + (tasksDue > 1 ? 'S' : '')); + if (appts) parts.push(appts + ' APPT' + (appts > 1 ? 'S' : '')); + tx.textContent = parts.join(' · '); + el.style.display = ''; + } + } + + // Render planner mini panel + const pEl = document.getElementById('planner-tasks'); + const badge = document.getElementById('planner-badge'); + if (!pEl) return; + + const priClass = {urgent:'pri-urgent',high:'pri-high',normal:'pri-normal',low:'pri-low'}; + const fmtTime = s => { if(!s) return ''; const d=new Date(s); return d.toLocaleTimeString('en-US',{hour:'2-digit',minute:'2-digit',hour12:true}); }; + const fmtDate = s => { if(!s) return ''; const d=new Date(s+'T00:00:00'); return d.toLocaleDateString('en-US',{month:'short',day:'numeric'}); }; + + const tasks = [...(d.tasks_overdue||[]).map(t=>({...t,_overdue:true})), ...(d.tasks_today||[])]; + const appts = d.appts_today || []; + + let html = ''; + if (!tasks.length && !appts.length) { + html = '
No tasks or appointments today.
'; + } else { + if (appts.length) { + html += '
TODAY\'S SCHEDULE
'; + html += appts.map(a => `
${fmtTime(a.start_at)}${a.title}${a.location?' · '+a.location+'':''}
`).join(''); + } + if (tasks.length) { + html += '
TASKS DUE
'; + html += tasks.map(t => `
${t.title}${t._overdue?'OVERDUE':''}
`).join(''); + } + if (d.pending_count > tasks.length) { + html += `
${d.pending_count} pending total
`; + } + } + pEl.innerHTML = html; + const total = tasks.length + appts.length; + if (badge) badge.textContent = total ? total + ' TODAY' : ''; +} + +// ── ALERTS ──────────────────────────────────────────────────────────── +async function loadAlerts() { + const data = await api('alerts'); + const el = document.getElementById('alerts-list'); + const tb = document.getElementById('tb-alerts'); + + const alerts = data.alerts || []; + if (!alerts.length) { + 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; + _panelCtx[ctxKey] = {type:'alert', label:a.title, + id:a.id, title:a.title, message:a.message, severity:a.severity}; + return `
+
+
${a.title}
+
${a.message}
+
+ +
`; + }).join(''); +} + +async function resolveAlert(id) { + await api('alerts/resolve', 'POST', {id}); + loadAlerts(); +} + +// ── PROACTIVE ALERT POLLING ─────────────────────────────────────────────────── +let _knownAlertIds = null; +let _spokenAlertIds = new Set(); + +async function pollAlertsProactive() { + const data = await api('alerts').catch(() => null); + if (!data) return; + const alerts = (data.alerts || []); + + if (_knownAlertIds === null) { + // First run: baseline existing alerts — do not speak them + _knownAlertIds = new Set(alerts.map(a => a.id)); + return; + } + + for (const a of alerts) { + if (_knownAlertIds.has(a.id) || _spokenAlertIds.has(a.id)) continue; + _knownAlertIds.add(a.id); + _spokenAlertIds.add(a.id); + + if (a.severity === 'critical' || a.severity === 'warning') { + const prefix = a.severity === 'critical' ? '🚨' : '⚠'; + addMessage('jarvis', `${prefix} ${a.title}: ${a.message}`); + const tts = (a.severity === 'critical' ? 'Critical alert. ' : 'Warning. ') + a.title + '. ' + a.message; + if (typeof speak === 'function' && isVoiceActive) speak(tts); + } + } + + // Remove resolved alerts from known set so they can re-trigger if they come back + const liveIds = new Set(alerts.map(a => a.id)); + for (const id of _knownAlertIds) { + if (!liveIds.has(id)) _knownAlertIds.delete(id); + } +} + +// ── WEATHER ─────────────────────────────────────────────────────────── +async function loadWeather() { + const d = await api('weather'); + if (!d || !d.current) return; + const c = d.current; + document.getElementById('weather-temp').textContent = c.temp; + document.getElementById('weather-desc').textContent = (c.desc || '').toUpperCase(); + document.getElementById('weather-feels').textContent = c.feels + '°F'; + document.getElementById('weather-humidity').textContent = c.humidity + '%'; + document.getElementById('weather-details').textContent = + 'Wind ' + c.wind + ' mph · Cloud ' + c.cloud + '% · Vis ' + c.vis + ' mi'; + + const fc = d.forecast || []; + document.getElementById('weather-forecast').innerHTML = fc.slice(0, 4).map(day => ` +
+
${day.day}
+
${day.icon}
+
${day.high}°${day.low}°
+
${day.rain_pct > 0 ? day.rain_pct+'%' : ''}
+
`).join(''); +} + +// ── NEWS ────────────────────────────────────────────────────────────── +function getNewsHidden() { + try { return JSON.parse(localStorage.getItem('news_hidden_cats') || '[]'); } catch(e) { return []; } +} +function setNewsHidden(arr) { localStorage.setItem('news_hidden_cats', JSON.stringify(arr)); } + +function toggleNewsFilter() { + const panel = document.getElementById('news-filter-panel'); + panel.style.display = panel.style.display === 'none' ? 'block' : 'none'; +} + +let _newsCats = []; +async function loadNews() { + const d = await api('news'); + const el = document.getElementById('news-list'); + if (!d || !d.categories || Object.keys(d.categories).length === 0) { + el.innerHTML = '
News loading...
'; + return; + } + const catLabels = { headlines: '📰 TOP HEADLINES', technology: '💻 TECHNOLOGY', pinned: '📌 JARVIS PINNED' }; + const hidden = getNewsHidden(); + _newsCats = Object.keys(d.categories); + + // Build filter checkboxes + const cbContainer = document.getElementById('news-filter-checkboxes'); + if (cbContainer) { + cbContainer.innerHTML = _newsCats.map(cat => ` + `).join(''); + } + + let html = ''; + for (const [cat, articles] of Object.entries(d.categories)) { + if (!articles.length || hidden.includes(cat)) continue; + html += `
${catLabels[cat] || cat.toUpperCase()}
`; + for (const a of articles.slice(0, 5)) { + const ctxKey = 'news_' + (cat + '_' + a.title).replace(/[^a-z0-9]/gi,'').slice(0,30); + _panelCtx[ctxKey] = {type:'news', label:a.title, + title:a.title, source:a.source, pub:a.pub||'', category:cat}; + html += `
+
${a.source}
+
${a.title.length > 90 ? a.title.slice(0,87)+'…' : a.title}
+ ${a.pub ? '
' + a.pub + '
' : ''} +
`; + } + } + if (!html) html = '
All categories hidden — use ⚙ to show sources
'; + const ageMin = d.cache_age_s > 0 ? Math.round(d.cache_age_s/60) : 0; + html += `
Updated ${ageMin}m ago
`; + el.innerHTML = html; +} + +function toggleNewsCat(cat, show) { + const hidden = getNewsHidden(); + if (show) { + const idx = hidden.indexOf(cat); + if (idx > -1) hidden.splice(idx, 1); + } else { + if (!hidden.includes(cat)) hidden.push(cat); + } + setNewsHidden(hidden); + loadNews(); +} + +// ── TABS ────────────────────────────────────────────────────────────── +function switchTab(name) { + if (name === 'sites') { openSitesModal(); return; } + document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); + document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active')); + event.target.classList.add('active'); + const pane = document.getElementById('tab-'+name); + if (pane) pane.classList.add('active'); + if (name === 'news') loadNews(); + if (name === 'agents') loadAgents(); + if (name === 'intel') loadIntel(); + if (name === 'comms') { loadComms(); loadCommsOutbox(); } + if (name === 'guardian') loadGuardian(); + if (name === 'missions') loadMissionsHud(); + if (name === 'directives') loadDirectivesHud(); + if (name === 'clearance') loadClearanceHud(); + if (name === 'alerts') loadAlerts(); +} + +// ── CHAT ────────────────────────────────────────────────────────────── +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; + log.appendChild(div); + + if (role === 'jarvis' && text && text.length > 0) { + // Adaptive speed: fast for short, slower for long (feels intentional either way) + const msPerChar = Math.max(9, Math.min(25, 1600 / text.length)); + const cursor = document.createElement('span'); + cursor.className = 'type-cursor'; + div.appendChild(cursor); + let i = 0; + const type = () => { + if (i < text.length) { + cursor.insertAdjacentText('beforebegin', text[i++]); + log.scrollTop = log.scrollHeight; + 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); + } else { + div.textContent = text; + } + + log.scrollTop = log.scrollHeight; + return div; +} + +function showThinking() { + const log = document.getElementById('chatLog'); + const div = document.createElement('div'); + div.className = 'msg jarvis'; + div.innerHTML = '
'; + div.id = 'thinking-bubble'; + log.appendChild(div); + log.scrollTop = log.scrollHeight; +} + +// ── PANEL CONTEXT SELECTION ─────────────────────────────────────────── +function selectContext(key) { + const ctx = _panelCtx[key]; + if (!ctx) return; + + // Clear previous active highlight + document.querySelectorAll('.ctx-active').forEach(el => el.classList.remove('ctx-active')); + + selectedContext = ctx; + + // Highlight clicked element + const el = document.querySelector('[data-ctx-key="' + key + '"]'); + if (el) el.classList.add('ctx-active'); + + // Show chip + const chip = document.getElementById('contextChip'); + const typeLabels = {vm:'VM', network:'DEVICE', alert:'ALERT', news:'NEWS', ha:'HOME'}; + document.getElementById('contextType').textContent = typeLabels[ctx.type] || ctx.type.toUpperCase(); + document.getElementById('contextLabel').textContent = ctx.label; + chip.classList.add('visible'); + + // Focus input for immediate question + document.getElementById('textInput').focus(); +} + +function clearContext() { + selectedContext = null; + document.querySelectorAll('.ctx-active').forEach(el => el.classList.remove('ctx-active')); + const chip = document.getElementById('contextChip'); + chip.classList.remove('visible'); +} + +async function sendMessage() { + const input = document.getElementById('textInput'); + const text = input.value.trim(); + if (!text) return; + + // Local commands — no API round-trip + var t2 = text.toLowerCase(); + + // Sleep command + if (SLEEP_CMDS.test(t2)) { + input.value = ''; + addMessage('user', text); + enterSleepMode(); + return; + } + + if (NM_OPEN_RE.test(t2)) { + input.value=''; addMessage('user',text); + addMessage('jarvis','Launching network topology display.'); + speak('Launching network topology display.'); + openNetMap(); return; + } + if (NM_CLOSE_RE.test(t2)) { + input.value=''; addMessage('user',text); + var isOpen=document.getElementById('netMapOverlay')&&document.getElementById('netMapOverlay').classList.contains('nm-open'); + if(isOpen){closeNetMap();addMessage('jarvis','Network map closed.');speak('Network map closed.');} + else addMessage('jarvis','Network map is not currently active.'); + return; + } + input.value = ''; + addMessage('user', text); + showThinking(); + + try { + const payload = {message:text, session_id:sessionId}; + if (selectedContext) { + payload.context = selectedContext; + clearContext(); + } + const data = await api('chat', 'POST', payload); + const bubble = document.getElementById('thinking-bubble'); + if (bubble) bubble.remove(); + + if (data.reply) { + addMessage('jarvis', data.reply, data.source || null); + speak(data.reply); + } + if (data.open_network_map) { openNetMap(); } + if (data.ui_action === 'focus_mode') { if (panelsVisible) togglePanels(true); } + if (data.ui_action === 'show_panels') { if (!panelsVisible) togglePanels(true); } + if (data.arc_job) { onArcJobStarted(data.arc_job, data.source || ''); } + } catch(e) { + const bubble = document.getElementById('thinking-bubble'); + if (bubble) bubble.remove(); + addMessage('jarvis', 'I encountered a communication error, Sir. Please check my API connection.'); + } +} + +// ── VOICE RECOGNITION ───────────────────────────────────────────────── +function initVoice() { + const SR = window.SpeechRecognition || window.webkitSpeechRecognition; + if (!SR) { + if (window.isSecureContext === false) { + console.warn('Speech Recognition blocked: not a secure context'); + } else { + console.warn('Speech Recognition not supported in this browser'); + } + return; + } + recognition = new SR(); + recognition.continuous = false; // restart-per-utterance — most reliable in Chrome + recognition.interimResults = false; + recognition.lang = 'en-US'; + recognition.maxAlternatives = 1; + + recognition.onresult = (e) => { + if (isSpeaking) return; + const transcript = (e.results[0][0].transcript || '').trim(); + if (!transcript) return; + const lc = transcript.toLowerCase(); + + // Sleeping: ONLY respond to master wake phrases + if (isAsleep) { + if (WAKE_PHRASES.some(p => lc.includes(p))) wakeFromSleep(); + return; + } + + if (!voiceMode) { + if (WAKE_PHRASES.some(p => lc.includes(p))) enterVoiceMode(); + } else if (!voiceMuted) { + voiceLastCmd = Date.now(); + voiceActive = Date.now(); + const cmd = lc.startsWith(CMD_PREFIX) + ? transcript.substring(CMD_PREFIX.length).trim() + : transcript; + if (cmd) { + // Check for sleep command by voice + if (SLEEP_CMDS.test(cmd)) { + addMessage('user', transcript); + enterSleepMode(); + return; + } + _showTranscript(cmd); + document.getElementById('textInput').value = cmd; + sendMessage(); + } + } + }; + + recognition.onend = () => { + // Restart immediately unless TTS is playing or mic is off + if (isListening && !isSpeaking) { + _scheduleRecStart(100); + } + }; + + recognition.onerror = (e) => { + if (e.error === 'not-allowed') { + isListening = false; + updateMicBtn(); + addMessage('system', 'Microphone access denied. Please allow microphone permission in your browser, then reload.'); + } else if (e.error === 'audio-capture') { + isListening = false; + updateMicBtn(); + addMessage('system', 'No microphone detected. Please connect a microphone and try again.'); + } + // no-speech / aborted / network: onend will fire and restart + }; +} + +function _showTranscript(text) { + const el = document.getElementById('textInput'); + if (el) { el.placeholder = '▶ ' + text.substring(0, 60); setTimeout(() => { el.placeholder = 'Enter command or speak to JARVIS...'; }, 3000); } +} + +function enterVoiceMode(source) { + voiceMode = true; + voiceMuted = false; + voiceLastCmd = Date.now(); + voiceActive = Date.now(); + updateMicBtn(); + // Focus/notify when woken from minimized or sleep + _focusWindow(); + if (source === 'wake') { + const g = 'All systems back online, ' + (sessionUser || 'Sir') + '. Good to have you back.'; + addMessage('jarvis', g); + speak(g); + } else { + speak('Yes, ' + (sessionUser || 'Sir') + '?'); + } +} + +function exitVoiceMode() { + voiceMode = false; + voiceMuted = false; + updateMicBtn(); +} + +function updateMicBtn() { + const btn = document.getElementById('micBtn'); + const icon = document.getElementById('micIcon'); + const wave = document.getElementById('waveform'); + if (!btn) return; + if (!voiceMode) { + btn.classList.remove('listening', 'muted'); + btn.title = 'Click to activate, or say: wake up JARVIS / daddy\'s home'; + icon.textContent = '🎤'; + wave.classList.remove('active'); + } else if (voiceMuted) { + btn.classList.remove('listening'); + btn.classList.add('muted'); + btn.title = 'Muted — click to unmute'; + icon.textContent = '🔇'; + wave.classList.remove('active'); + } else { + btn.classList.add('listening'); + btn.classList.remove('muted'); + btn.title = 'Listening — click to mute'; + icon.textContent = '🟢'; + wave.classList.add('active'); + } +} + +function toggleVoice() { + if (!voiceMode) { + enterVoiceMode(); + } else { + voiceMuted = !voiceMuted; + if (!voiceMuted) voiceLastCmd = Date.now(); + updateMicBtn(); + } +} + +let _recTimer = null; +function _scheduleRecStart(ms = 100) { + clearTimeout(_recTimer); + _recTimer = setTimeout(() => { + if (isListening && !isSpeaking) { + try { recognition.start(); } catch(_) {} + } + }, ms); +} + +function startListening() { + if (!recognition) { + if (!window.isSecureContext) { + addMessage('system', 'Voice recognition requires a trusted HTTPS connection. Please access JARVIS via https://jarvis.orbishosting.com for voice support.'); + } else { + addMessage('system', 'Voice recognition requires Chrome or Edge browser.'); + } + return; + } + isListening = true; + _scheduleRecStart(50); +} + +function stopListening() { + isListening = false; + voiceMode = false; + voiceMuted = false; + updateMicBtn(); + clearTimeout(_recTimer); + try { recognition.abort(); } catch(_) {} +} + +// ── SPEECH SYNTHESIS ────────────────────────────────────────────────── +function loadVoices() { + const set = () => { + const voices = synth.getVoices(); + // Priority: Australian male → Australian → British male → British → any English + selectedVoice = + voices.find(v => v.name === 'Nathan') // macOS Australian male + || voices.find(v => v.name === 'Google Australian English') // Chrome Australian + || voices.find(v => v.name === 'Karen') // macOS Australian female + || voices.find(v => v.lang === 'en-AU') // any Australian + || voices.find(v => v.name === 'Daniel') // macOS British male + || voices.find(v => v.name === 'Google UK English Male') // Chrome British male + || voices.find(v => v.lang === 'en-GB') // any British + || voices.find(v => v.lang.startsWith('en')) // any English + || voices[0] + || null; + }; + set(); + synth.onvoiceschanged = set; +} + +let _ttsAudio = null; + +async function speak(text) { + if (!text) return; + if (_ttsAudio) { _ttsAudio.pause(); _ttsAudio = null; } + synth?.cancel(); + isSpeaking = true; + // Pause recognition while JARVIS speaks to avoid mic feedback + try { recognition?.abort(); } catch(_) {} + const reactor = document.getElementById('arcReactor'); + reactor?.classList.add('speaking'); + const _resumeMic = () => { + isSpeaking = false; + reactor?.classList.remove('speaking'); + // onend will fire from the abort we did before TTS, and restart cleanly + if (isListening) _scheduleRecStart(900); + }; + try { + const res = await fetch('/api/tts', { + method: 'POST', + headers: {'Content-Type':'application/json','X-Session-Token': sessionToken}, + body: JSON.stringify({text: text.substring(0, 400)}), + }); + if (!res.ok) throw new Error('tts'); + const blob = await res.blob(); + const url = URL.createObjectURL(blob); + _ttsAudio = new Audio(url); + _ttsAudio.onended = () => { URL.revokeObjectURL(url); _ttsAudio = null; _resumeMic(); }; + _ttsAudio.onerror = () => { _ttsAudio = null; _resumeMic(); }; + await _ttsAudio.play(); + } catch(e) { + _resumeMic(); + _speakFallback(text); + } +} + +function _speakFallback(text) { + if (!synth || !text) return; + synth.cancel(); + isSpeaking = true; + const utter = new SpeechSynthesisUtterance(text); + if (selectedVoice) utter.voice = selectedVoice; + utter.rate = 0.92; utter.pitch = 0.85; utter.volume = 1; + const reactor = document.getElementById('arcReactor'); + utter.onstart = () => reactor?.classList.add('speaking'); + utter.onend = () => { + reactor?.classList.remove('speaking'); + isSpeaking = false; + if (isListening) _scheduleRecStart(900); + }; + synth.speak(utter); +} + +// ── AGENT DETECTION & BROWSER INSTALL ───────────────────────────────── +let _agentOnline = false; +let _myAgent = null; + +function detectOS() { + const ua = navigator.userAgent; + const p = (navigator.platform || '').toLowerCase(); + // Tablets — check before desktop OS (iPads spoof MacIntel) + if (/iPad|Android/.test(ua) || (p.includes('mac') && navigator.maxTouchPoints > 1)) return 'tablet'; + if (/iPhone/.test(ua)) return 'tablet'; + if (p.includes('win') || ua.includes('Windows')) return 'windows'; + if (p.includes('mac') || ua.includes('Macintosh')) return 'mac'; + if (p.includes('linux') || ua.includes('Linux')) return 'linux'; + return 'unknown'; +} + +async function checkAgentStatus() { + const dot = document.getElementById('bb-agent-dot'); + const sta = document.getElementById('bb-agent-status'); + const btn = document.getElementById('agentBtn'); + if (!dot || !sta) return; + try { + const data = await api('agent/list'); + const agents = data.agents || []; + const online = agents.filter(a => a.status === 'online'); + dot.className = 'bb-dot ' + (online.length > 0 ? 'online' : 'offline'); + sta.textContent = online.length > 0 ? online.length + ' ONLINE' : 'NONE'; + const cnt = document.getElementById('net-agent-count'); + if (cnt) cnt.textContent = online.length + ' AGENT' + (online.length !== 1 ? 'S' : '') + ' ONLINE'; + const myIp = data.my_ip || ''; + // Match by exact IP first, then by same /24 subnet (handles NAT behind same router) + const mySubnet = myIp.split('.').slice(0,3).join('.'); + _myAgent = online.find(a => a.ip_address === myIp) + || online.find(a => a.ip_address && a.ip_address.startsWith(mySubnet + '.')); + _agentOnline = !!_myAgent; + if (btn) { + const isTablet = detectOS() === 'tablet'; + if (isTablet) { + btn.title = 'JARVIS Agent — not available for tablets'; + btn.style.opacity = '0.5'; + } else if (_agentOnline) { + btn.classList.add('agent-online'); + btn.title = 'Agent active: ' + _myAgent.hostname; + } else { + btn.classList.remove('agent-online'); + btn.title = 'Click to install JARVIS Agent on this machine'; + } + } + // Also refresh the AGENTS tab if it's visible + if (document.getElementById('tab-agents').classList.contains('active')) { + renderAgentsTab(agents, data.metrics || {}); + } + } catch(e) { + if (dot) dot.className = 'bb-dot offline'; + if (sta) sta.textContent = 'ERROR'; + } +} diff --git a/public_html/assets/js/jarvis-effects.js b/public_html/assets/js/jarvis-effects.js new file mode 100644 index 0000000..67bea88 --- /dev/null +++ b/public_html/assets/js/jarvis-effects.js @@ -0,0 +1,590 @@ +// ── PARTICLE CANVAS ─────────────────────────────────────────────────── +(function initParticles() { + const canvas = document.getElementById('particleCanvas'); + if (!canvas) return; + const ctx = canvas.getContext('2d'); + const N = 65; + const CONNECT_DIST = 130; + let W, H, particles = []; + + function resize() { + W = canvas.width = window.innerWidth; + H = canvas.height = window.innerHeight; + } + + function spawn() { + particles = []; + for (let i = 0; i < N; i++) { + particles.push({ + x: Math.random() * W, + y: Math.random() * H, + vx: (Math.random() - 0.5) * 0.25, + vy: (Math.random() - 0.5) * 0.25, + r: Math.random() * 1.2 + 0.4, + a: Math.random() * 0.35 + 0.08, + }); + } + } + + function draw() { + ctx.clearRect(0, 0, W, H); + for (let i = 0; i < N; i++) { + const p = particles[i]; + for (let j = i + 1; j < N; j++) { + const q = particles[j]; + const dx = p.x - q.x, dy = p.y - q.y; + const d = Math.sqrt(dx * dx + dy * dy); + if (d < CONNECT_DIST) { + ctx.strokeStyle = `rgba(0,180,255,${0.09 * (1 - d / CONNECT_DIST)})`; + ctx.lineWidth = 0.5; + ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(q.x, q.y); ctx.stroke(); + } + } + ctx.fillStyle = `rgba(0,200,255,${p.a})`; + ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2); ctx.fill(); + p.x += p.vx; p.y += p.vy; + if (p.x < 0) p.x = W; if (p.x > W) p.x = 0; + if (p.y < 0) p.y = H; if (p.y > H) p.y = 0; + } + requestAnimationFrame(draw); + } + + resize(); spawn(); draw(); + window.addEventListener('resize', () => { resize(); spawn(); }); +})(); + +// ── PANEL FLOAT STAGGER — different phase per panel ─────────────────── +document.addEventListener('DOMContentLoaded', () => { + document.querySelectorAll('.panel').forEach((p, i) => { + p.style.animationDelay = `-${(i * 1.37).toFixed(2)}s`; + }); +}); + +// ── MOUSE PARALLAX — panels tilt toward cursor ──────────────────────── +(function initParallax() { + const MAX_TILT = 3.5; // degrees + let mouseX = 0, mouseY = 0, raf = null; + + window.addEventListener('mousemove', e => { + mouseX = e.clientX / window.innerWidth - 0.5; // -0.5 to 0.5 + mouseY = e.clientY / window.innerHeight - 0.5; + if (!raf) raf = requestAnimationFrame(applyTilt); + }); + + function applyTilt() { + raf = null; + const rx = mouseY * MAX_TILT; + const ry = -mouseX * MAX_TILT; + document.querySelectorAll('.panel').forEach(p => { + p.style.setProperty('--prx', rx.toFixed(2) + 'deg'); + p.style.setProperty('--pry', ry.toFixed(2) + 'deg'); + }); + // Column-level parallax (skip in focus mode) + if (!document.getElementById('mainLayout')?.classList.contains('focus-mode')) { + const lp = document.getElementById('leftPanel'); + const rp = document.getElementById('rightPanel'); + const cp = document.getElementById('centerPanel'); + if (lp) lp.style.transform = `translateX(${mouseX * 5}px) translateY(${mouseY * 3}px)`; + if (rp) rp.style.transform = `translateX(${-mouseX * 5}px) translateY(${mouseY * 3}px)`; + if (cp) cp.style.transform = `translateX(${mouseX * 2}px)`; + } + } +})(); + +// ── SPARKLINES ──────────────────────────────────────────────────────── +const _sparkData = {cpu: [], mem: [], disk: []}; +const SPARK_MAX = 25; + +function pushSparkData(key, val) { + _sparkData[key].push(val); + if (_sparkData[key].length > SPARK_MAX) _sparkData[key].shift(); +} + +function drawSparkline(canvasId, data, color) { + const canvas = document.getElementById(canvasId); + if (!canvas || !data.length) return; + const wrap = canvas.parentElement; + canvas.width = wrap.clientWidth || 240; + canvas.height = 32; + const ctx = canvas.getContext('2d'); + ctx.clearRect(0, 0, canvas.width, canvas.height); + + const W = canvas.width, H = canvas.height; + const min = 0, max = 100; + const step = W / (SPARK_MAX - 1); + + // Fill area under line + const grad = ctx.createLinearGradient(0, 0, 0, H); + grad.addColorStop(0, color.replace(')', ',0.35)').replace('rgb','rgba')); + grad.addColorStop(1, color.replace(')', ',0)').replace('rgb','rgba')); + ctx.beginPath(); + data.forEach((v, i) => { + const x = i * step; + const y = H - ((v - min) / (max - min)) * H; + i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y); + }); + ctx.lineTo((data.length - 1) * step, H); + ctx.lineTo(0, H); + ctx.closePath(); + ctx.fillStyle = grad; + ctx.fill(); + + // Line + ctx.beginPath(); + data.forEach((v, i) => { + const x = i * step; + const y = H - ((v - min) / (max - min)) * H; + i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y); + }); + ctx.strokeStyle = color; + ctx.lineWidth = 1.5; + ctx.stroke(); + + // Current value dot + if (data.length > 1) { + const last = data[data.length - 1]; + const x = (data.length - 1) * step; + const y = H - ((last - min) / (max - min)) * H; + ctx.beginPath(); + ctx.arc(x, y, 2.5, 0, Math.PI * 2); + ctx.fillStyle = color; + ctx.fill(); + ctx.strokeStyle = 'rgba(0,0,0,0.5)'; + ctx.lineWidth = 1; + ctx.stroke(); + } +} + +// ── PANEL DATA FLASH ────────────────────────────────────────────────── +function flashPanel(panelEl) { + if (!panelEl) return; + panelEl.classList.remove('data-flash'); + void panelEl.offsetWidth; // reflow to restart animation + panelEl.classList.add('data-flash'); + setTimeout(() => panelEl.classList.remove('data-flash'), 600); +} + +// ── ALERT PULSE ─────────────────────────────────────────────────────── +function setAlertState(hasAlerts) { + const ov = document.getElementById('alertOverlay'); + if (ov) ov.style.display = hasAlerts ? 'block' : 'none'; + const vg = document.getElementById('vignetteOverlay'); + 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; +let _faceVisible = false; +let _faceTrackRaf = null; +const FACE_MAX_X = 48; // max px left/right travel +const FACE_MAX_Y = 10; // max px up/down travel +const FACE_LERP = 0.07; // smoothing (lower = slower/smoother) + +function _faceTrackLoop() { + _faceTrackRaf = requestAnimationFrame(_faceTrackLoop); + + // Lerp toward target (or back to 0 when no face) + const targetX = _faceVisible ? _faceTargetX : 0; + const targetY = _faceVisible ? _faceTargetY : 0; + _faceCurrX += (targetX - _faceCurrX) * FACE_LERP; + _faceCurrY += (targetY - _faceCurrY) * FACE_LERP; + + const tx = _faceCurrX * FACE_MAX_X; + const ty = _faceCurrY * FACE_MAX_Y; + + const logo = document.querySelector('.tb-logo'); + if (logo) logo.style.transform = `translateX(${tx.toFixed(2)}px) translateY(${ty.toFixed(2)}px)`; +} + +function updateFaceTarget(box, videoW, videoH) { + // Face center, normalized — flip X because front camera is mirrored + const cx = box.x + box.width / 2; + const cy = box.y + box.height / 2; + _faceTargetX = 0.5 - (cx / videoW); // flipped: move right when face is left + _faceTargetY = (cy / videoH) - 0.5; // positive = face low = logo drops slightly + _faceVisible = true; + + // Position the scan overlay over the detected face in the viewport. + // The video feed is 320×240 but hidden; map to viewport coords. + const scaleX = window.innerWidth / videoW; + const scaleY = window.innerHeight / videoH; + const faceVx = box.x * scaleX; + const faceVy = box.y * scaleY; + const faceVw = box.width * scaleX; + const faceVh = box.height * scaleY; + const ov = document.getElementById('faceScanOverlay'); + if (ov) { + ov.style.display = 'block'; + // Center overlay on face, flipped X to match mirror + const ovX = window.innerWidth - faceVx - faceVw / 2 - 30; + const ovY = faceVy + faceVh / 2 - 30; + ov.style.left = Math.max(0, Math.min(window.innerWidth - 60, ovX)) + 'px'; + ov.style.top = Math.max(0, Math.min(window.innerHeight - 80, ovY)) + 'px'; + } +} + +function clearFaceTarget() { + _faceVisible = false; + const ov = document.getElementById('faceScanOverlay'); + if (ov) ov.style.display = 'none'; +} + +function startFaceTracking() { + const logo = document.querySelector('.tb-logo'); + if (logo) logo.classList.add('face-tracking'); + if (!_faceTrackRaf) _faceTrackLoop(); +} + +function stopFaceTracking() { + clearFaceTarget(); + const logo = document.querySelector('.tb-logo'); + if (logo) { logo.classList.remove('face-tracking'); logo.style.transform = ''; } + // Let the loop coast to zero naturally rather than snapping — cancel after settling + setTimeout(() => { + if (!_faceVisible && _faceTrackRaf) { + cancelAnimationFrame(_faceTrackRaf); + _faceTrackRaf = null; + } + }, 1500); +} + +// ── GLITCH EFFECT ───────────────────────────────────────────────────── +(function initGlitch() { + function triggerGlitch() { + const el = document.querySelector('.tb-logo-text'); + if (!el) return; + el.classList.add('glitching'); + setTimeout(() => el.classList.remove('glitching'), 280); + setTimeout(triggerGlitch, 35000 + Math.random() * 25000); + } + setTimeout(triggerGlitch, 20000); +})(); + +// ① HUD CORNER RINGS ────────────────────────────────────────────────── +(function initHudCorners() { + const canvas = document.getElementById('hudCornersCanvas'); + if (!canvas) return; + const ctx = canvas.getContext('2d'); + let W, H, t = 0; + function resize() { W = canvas.width = window.innerWidth; H = canvas.height = window.innerHeight; } + + function drawCorner(cx, cy, a0, a1) { + const R = 62, R2 = R + 16; + // Edge lines + ctx.strokeStyle = 'rgba(0,212,255,0.35)'; ctx.lineWidth = 1; + const edgeLen = 28; + // two short lines along the screen edges from the corner point + const midA = (a0 + a1) / 2; + const ax = Math.cos(a0), ay = Math.sin(a0); + const bx = Math.cos(a1), by = Math.sin(a1); + ctx.beginPath(); ctx.moveTo(cx + ax*(R+6), cy + ay*(R+6)); ctx.lineTo(cx + ax*(R+6+edgeLen), cy + ay*(R+6+edgeLen)); ctx.stroke(); + ctx.beginPath(); ctx.moveTo(cx + bx*(R+6), cy + by*(R+6)); ctx.lineTo(cx + bx*(R+6+edgeLen), cy + by*(R+6+edgeLen)); ctx.stroke(); + + // Primary arc + ctx.beginPath(); ctx.arc(cx, cy, R, a0, a1); + ctx.strokeStyle = 'rgba(0,212,255,0.5)'; ctx.lineWidth = 1.2; ctx.stroke(); + // Outer arc + ctx.beginPath(); ctx.arc(cx, cy, R2, a0 + 0.12, a1 - 0.12); + ctx.strokeStyle = 'rgba(0,212,255,0.18)'; ctx.lineWidth = 0.6; ctx.stroke(); + + // Tick marks + const ticks = 14; + for (let i = 0; i <= ticks; i++) { + const a = a0 + (a1 - a0) * (i / ticks); + const big = i % 7 === 0; + const len = big ? 9 : (i % 2 === 0 ? 5 : 3); + ctx.beginPath(); + ctx.moveTo(cx + Math.cos(a) * (R - 2), cy + Math.sin(a) * (R - 2)); + ctx.lineTo(cx + Math.cos(a) * (R - 2 - len), cy + Math.sin(a) * (R - 2 - len)); + ctx.strokeStyle = big ? 'rgba(0,212,255,0.8)' : 'rgba(0,212,255,0.3)'; + ctx.lineWidth = big ? 1 : 0.5; ctx.stroke(); + } + + // Animated scanning dot + const dotA = a0 + ((a1 - a0) * ((t * 0.35) % 1)); + ctx.beginPath(); ctx.arc(cx + Math.cos(dotA) * R, cy + Math.sin(dotA) * R, 2.5, 0, Math.PI*2); + ctx.fillStyle = 'rgba(0,212,255,1)'; + ctx.shadowColor = 'rgba(0,212,255,0.9)'; ctx.shadowBlur = 8; ctx.fill(); ctx.shadowBlur = 0; + + // Small numeric labels + ctx.font = '7px Share Tech Mono,monospace'; ctx.fillStyle = 'rgba(0,212,255,0.55)'; + ctx.fillText(Math.round((Math.abs(a0) / (Math.PI*2)) * 360) + '°', + cx + Math.cos(a0) * (R + 20), cy + Math.sin(a0) * (R + 20)); + } + + function draw() { + ctx.clearRect(0, 0, W, H); t += 0.01; + drawCorner(0, 0, 0, Math.PI*0.5); + drawCorner(W, 0, Math.PI*0.5, Math.PI); + drawCorner(0, H, Math.PI*1.5, Math.PI*2); + drawCorner(W, H, Math.PI, Math.PI*1.5); + requestAnimationFrame(draw); + } + resize(); draw(); + window.addEventListener('resize', resize); +})(); + +// ② DATA STREAM COLUMNS ─────────────────────────────────────────────── +(function initDataStream() { + const canvas = document.getElementById('dataStreamCanvas'); + if (!canvas) return; + const ctx = canvas.getContext('2d'); + let W, H; + const CHARS = '0123456789ABCDEF◈◉▸▹⟩⟨⬡░▒'; + const COL_COUNT = 22; + let cols = []; + + function resize() { + W = canvas.width = window.innerWidth; + H = canvas.height = window.innerHeight; + cols = []; + for (let i = 0; i < COL_COUNT; i++) { + const x = (i / COL_COUNT) * W + Math.random() * (W / COL_COUNT) * 0.6; + cols.push({ x, y: Math.random() * H, speed: Math.random() * 0.7 + 0.25, + len: Math.floor(Math.random() * 14 + 5), chars: [], + alpha: Math.random() * 0.035 + 0.015, tick: 0 }); + } + } + + function draw() { + ctx.clearRect(0, 0, W, H); + ctx.font = '11px Share Tech Mono,monospace'; + for (const c of cols) { + c.y += c.speed; + if (c.y - c.len * 14 > H) { c.y = -c.len * 14; c.alpha = Math.random() * 0.035 + 0.015; } + if (++c.tick > 7) { c.tick = 0; c.chars[0] = CHARS[Math.floor(Math.random() * CHARS.length)]; } + for (let i = 0; i < c.len; i++) { + if (!c.chars[i]) c.chars[i] = CHARS[Math.floor(Math.random() * CHARS.length)]; + const cy = c.y - i * 14; + if (cy < -14 || cy > H + 14) continue; + const a = c.alpha * (1 - i / c.len); + ctx.fillStyle = i === 0 ? `rgba(180,240,255,${Math.min(a*4,0.15)})` : `rgba(0,185,225,${a})`; + ctx.fillText(c.chars[i], c.x, cy); + } + } + requestAnimationFrame(draw); + } + resize(); draw(); + window.addEventListener('resize', resize); +})(); + +// ③ NETWORK TOPOLOGY ────────────────────────────────────────────────── +let _topoNodes = [], _topoT = 0, _topoRunning = false; + +function renderTopology(devices) { + const canvas = document.getElementById('topoCanvas'); + if (!canvas) return; + const W = canvas.parentElement?.clientWidth || 260; + canvas.width = W; canvas.height = 118; + + _topoNodes = devices.slice(0, 18).map((d, i, arr) => { + const angle = (i / arr.length) * Math.PI * 2 - Math.PI / 2; + const rx = W * 0.36, ry = 36; + return { + x: W/2 + Math.cos(angle) * rx * (0.6 + (i%3)*0.18), + y: 52 + Math.sin(angle) * ry * (0.65 + (i%2)*0.25), + label: (d.name || d.ip || '?').split('.')[0].substring(0, 9), + on: !!(d.alive || d.status === 'online'), + agent: d.source === 'agent', + phase: Math.random() * Math.PI * 2, + }; + }); + + if (!_topoRunning) { _topoRunning = true; _drawTopo(); } +} + +function _drawTopo() { + requestAnimationFrame(_drawTopo); + const canvas = document.getElementById('topoCanvas'); + if (!canvas || !_topoNodes.length) return; + const ctx = canvas.getContext('2d'); + const W = canvas.width, H = canvas.height; + _topoT += 0.018; + ctx.clearRect(0, 0, W, H); + + const floatY = n => Math.sin(_topoT * 0.9 + n.phase) * 2.5; + + // Connections + for (let i = 0; i < _topoNodes.length; i++) { + for (let j = i+1; j < _topoNodes.length; j++) { + const a = _topoNodes[i], b = _topoNodes[j]; + if (!a.on || !b.on) continue; + const ax = a.x, ay = a.y + floatY(a), bx = b.x, by = b.y + floatY(b); + const dist = Math.hypot(bx-ax, by-ay); + if (dist > W * 0.55) continue; + const lg = ctx.createLinearGradient(ax, ay, bx, by); + const col = a.agent ? '0,255,136' : '0,212,255'; + lg.addColorStop(0, `rgba(${col},0.25)`); lg.addColorStop(0.5, `rgba(${col},0.1)`); lg.addColorStop(1, `rgba(${col},0.25)`); + ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(bx, by); + ctx.strokeStyle = lg; ctx.lineWidth = 0.8; ctx.stroke(); + // travelling pulse + const p = (_topoT * 0.4 + a.phase) % 1; + ctx.beginPath(); ctx.arc(ax+(bx-ax)*p, ay+(by-ay)*p, 1.8, 0, Math.PI*2); + ctx.fillStyle = 'rgba(0,212,255,0.85)'; ctx.fill(); + } + } + + // Bubble nodes + for (const n of _topoNodes) { + const pulse = 0.5 + Math.sin(_topoT * 1.4 + n.phase) * 0.3; + const col = n.on ? (n.agent ? '0,255,136' : '0,212,255') : '255,50,80'; + const r = n.agent ? 10 : 7; + const nx = n.x, ny = n.y + floatY(n); + + if (n.on) { + // Ambient bloom + const bloom = ctx.createRadialGradient(nx, ny, r*0.4, nx, ny, r*3); + bloom.addColorStop(0, `rgba(${col},${(pulse*0.18).toFixed(3)})`); + bloom.addColorStop(1, `rgba(${col},0)`); + ctx.beginPath(); ctx.arc(nx, ny, r*3, 0, Math.PI*2); ctx.fillStyle = bloom; ctx.fill(); + } + + // Frosted glass fill + const fg = ctx.createRadialGradient(nx, ny - r*0.3, 0, nx, ny, r); + const fa = n.on ? 0.18 + pulse*0.1 : 0.07; + fg.addColorStop(0, `rgba(${col},${(fa*1.8).toFixed(3)})`); + fg.addColorStop(0.65, `rgba(${col},${fa.toFixed(3)})`); + fg.addColorStop(1, `rgba(${col},${(fa*0.2).toFixed(3)})`); + ctx.beginPath(); ctx.arc(nx, ny, r, 0, Math.PI*2); ctx.fillStyle = fg; ctx.fill(); + + // Border + ctx.beginPath(); ctx.arc(nx, ny, r, 0, Math.PI*2); + ctx.strokeStyle = `rgba(${col},${n.on ? (0.5 + pulse*0.32).toFixed(3) : '0.2'})`; + ctx.lineWidth = 1; ctx.stroke(); + + // Label below bubble + ctx.font = '6px Share Tech Mono,monospace'; + ctx.textAlign = 'center'; + ctx.fillStyle = n.on ? `rgba(${col},0.82)` : 'rgba(255,100,100,0.5)'; + ctx.fillText(n.label, nx, ny + r + 7); + ctx.textAlign = 'left'; + } +} + +// ④ EKG HEARTBEAT ──────────────────────────────────────────────────── +(function initEKG() { + const canvas = document.getElementById('ekgCanvas'); + if (!canvas) return; + const ctx = canvas.getContext('2d'); + let W, H, data = [], phase = 0; + + function resize() { + const wrap = document.getElementById('ekgWrap'); + W = canvas.width = wrap ? Math.max(100, wrap.clientWidth) : 180; + H = canvas.height = 22; + data = new Array(W).fill(0); + } + + function ekgVal(p) { + const c = p % 1; + if (c < 0.28) return 0; + if (c < 0.38) return Math.sin((c-0.28)/0.10*Math.PI) * 0.22; // P bump + if (c < 0.43) return 0; // PR flat + if (c < 0.45) return -(c-0.43)/0.02 * 0.18; // Q dip + if (c < 0.465){ const f=(c-0.45)/0.015; return f<0.5?f*2*0.95:(2-f*2)*0.95; } // R spike + if (c < 0.49) return -(c-0.465)/0.025 * 0.22; // S dip + if (c < 0.52) return -(0.22-(c-0.49)/0.03*0.22); // back to baseline + if (c < 0.70) return Math.sin((c-0.52)/0.18*Math.PI) * 0.28; // T wave + return 0; + } + + function draw() { + phase += 0.0038; + data.shift(); data.push(ekgVal(phase)); + ctx.clearRect(0, 0, W, H); + // Glow layer + ctx.beginPath(); + data.forEach((v,i) => { const y=H*0.5-v*H*0.44; i===0?ctx.moveTo(i,y):ctx.lineTo(i,y); }); + ctx.strokeStyle='rgba(0,220,100,0.2)'; ctx.lineWidth=4; ctx.stroke(); + // Line + ctx.beginPath(); + data.forEach((v,i) => { const y=H*0.5-v*H*0.44; i===0?ctx.moveTo(i,y):ctx.lineTo(i,y); }); + ctx.strokeStyle='rgba(0,255,120,0.85)'; ctx.lineWidth=1.2; ctx.stroke(); + requestAnimationFrame(draw); + } + resize(); draw(); + window.addEventListener('resize', resize); +})(); + +// ⑤ AUDIO WAVEFORM RING ─────────────────────────────────────────────── +(function initAudioRing() { + const canvas = document.getElementById('audioRingCanvas'); + if (!canvas) return; + const ctx = canvas.getContext('2d'); + const CX = 30, CY = 30, BARS = 28, INNER = 20; + + let t = 0; + function draw() { + t += 0.055; + ctx.clearRect(0, 0, 60, 60); + const listening = window.isListening; + const speaking = window.isSpeaking; + if (!listening && !speaking) { requestAnimationFrame(draw); return; } + + for (let i = 0; i < BARS; i++) { + const angle = (i / BARS) * Math.PI * 2; + let amp; + if (speaking) { + amp = 0.35 + Math.abs(Math.sin(t*4.1+i*0.9))*0.5 + Math.abs(Math.sin(t*9+i*1.7))*0.15; + } else { + amp = 0.08 + Math.abs(Math.sin(t*1.1+i*0.6))*0.18; + } + const barLen = 3 + amp * 11; + ctx.beginPath(); + ctx.moveTo(CX+Math.cos(angle)*INNER, CY+Math.sin(angle)*INNER); + ctx.lineTo(CX+Math.cos(angle)*(INNER+barLen), CY+Math.sin(angle)*(INNER+barLen)); + const alpha = speaking ? 0.55+amp*0.45 : 0.25+amp*0.35; + ctx.strokeStyle = speaking ? `rgba(0,255,175,${alpha})` : `rgba(0,212,255,${alpha})`; + ctx.lineWidth = 1.8; ctx.stroke(); + } + requestAnimationFrame(draw); + } + draw(); +})(); + +// ⑥ STATIC NOISE BURSTS ─────────────────────────────────────────────── +(function initStaticBursts() { + function burst() { + const panels = document.querySelectorAll('#app .panel'); + if (panels.length) { + const p = panels[Math.floor(Math.random() * panels.length)]; + const n = document.createElement('div'); + n.className = 'panel-noise-layer'; + p.appendChild(n); + setTimeout(() => n.remove(), 320); + } + setTimeout(burst, 75000 + Math.random() * 55000); + } + setTimeout(burst, 40000 + Math.random() * 30000); +})(); + +// ⑦ AMBIENT COLOR CYCLE ─────────────────────────────────────────────── +(function initAmbientColor() { + let t = 0; + const root = document.documentElement; + function tick() { + t += 0.00025; + const g = Math.round(210 + Math.sin(t) * 22); + const b = Math.round(248 + Math.cos(t * 1.15) * 28); + root.style.setProperty('--cyan', `rgb(0,${g},${b})`); + root.style.setProperty('--cyan2', `rgb(0,${Math.round(g*.82)},${Math.round(b*.87)})`); + requestAnimationFrame(tick); + } + tick(); +})(); diff --git a/public_html/assets/js/jarvis-overlays.js b/public_html/assets/js/jarvis-overlays.js new file mode 100644 index 0000000..f1f8c88 --- /dev/null +++ b/public_html/assets/js/jarvis-overlays.js @@ -0,0 +1,357 @@ +// ── SLEEP MODE ──────────────────────────────────────────────────────────────── +var isAsleep = false; +var _sleepRefreshTimer = null; + +var SLEEP_CMDS = /\b(good\s*night(\s*jarvis)?|go\s*to\s*sleep|sleep\s*mode|shut\s*(down|off)\s*(jarvis|for\s*the\s*night)|go\s*offline|going\s*offline|jarvis\s*(go\s*)?(offline|sleep|shutdown)|stand\s*by\s*mode|power\s*down(\s*jarvis)?|signing\s*off)\b/i; + +function enterSleepMode() { + if (isAsleep) return; + isAsleep = true; + + // Pause voice mode + voiceMode = false; + voiceMuted = false; + updateMicBtn(); + + // Slow or pause the refresh loop — keep mic alive for wake word + clearInterval(refreshTimer); + refreshTimer = null; + // Light polling every 2 min just to stay alive + _sleepRefreshTimer = setInterval(function() { + // heartbeat only — keep session alive without hammering APIs + try { fetch('/api/auth', {method:'GET', headers:{'Authorization':'Bearer '+sessionToken}}); } catch(e) {} + }, 120000); + + // Dim the UI + var app = document.getElementById('app'); + if (app) app.classList.add('sleeping'); + + // Flash title to confirm + document.title = 'JARVIS — STANDBY'; + + addMessage('jarvis', 'Understood. Going offline. Say "wake up JARVIS" when you need me.'); +} + +function wakeFromSleep() { + if (!isAsleep) return; + isAsleep = false; + + // Restore full polling + clearInterval(_sleepRefreshTimer); + _sleepRefreshTimer = null; + refreshAll(); + refreshTimer = setInterval(refreshAll, 10000); + + // Remove dim overlay + var app = document.getElementById('app'); + if (app) app.classList.remove('sleeping'); + document.title = 'JARVIS — Integrated Defense and Logistics System'; + + // Boot sequence + var topBar=document.getElementById('topBar'), lp=document.getElementById('leftPanel'); + var rp=document.getElementById('rightPanel'), cp=document.getElementById('centerPanel'); + [topBar,lp,rp,cp].forEach(function(el){if(el){el.style.opacity='0';}}); + requestAnimationFrame(function(){ + setTimeout(function(){if(topBar){topBar.style.opacity='';topBar.classList.add('boot-top');}},0); + setTimeout(function(){if(lp){lp.style.opacity='';lp.classList.add('boot-left');}},140); + setTimeout(function(){if(rp){rp.style.opacity='';rp.classList.add('boot-right');}},200); + setTimeout(function(){if(cp){cp.style.opacity='';cp.classList.add('boot-center');}},260); + setTimeout(function(){[topBar,lp,rp,cp].forEach(function(el){if(el)el.classList.remove('boot-top','boot-left','boot-right','boot-center');});},1400); + }); + + // Enter voice mode and greet + enterVoiceMode('wake'); +} + +function _focusWindow() { + // Attempt to bring browser window to front + try { window.focus(); } catch(e) {} + + // Flash title to grab attention if tab is backgrounded + var _origTitle = 'JARVIS — Integrated Defense and Logistics System'; + var _flashCount = 0; + var _titleFlash = setInterval(function() { + document.title = _flashCount % 2 === 0 ? '⚡ JARVIS — ONLINE' : _origTitle; + if (++_flashCount >= 8) { clearInterval(_titleFlash); document.title = _origTitle; } + }, 400); + + // Notify if already have permission — never request during voice activity + if ('Notification' in window && Notification.permission === 'granted') { + try { new Notification('JARVIS', { body: 'Wake word detected.', tag: 'jarvis-wake', requireInteraction: false }); } catch(e) {} + } +} + +// ── NETWORK MAP ────────────────────────────────────────────────────────────── +var _nmNodes=[], _nmEdges=[], _nmParticles=[], _nmRaf=null, _nmT=0, _nmHoverNode=null; +var _nmRot=[0,0,0,0,0,0]; +var NM_RINGS=[ + {name:'hub', label:'', rFrac:0, speed:0, rgb:'0,212,255', nodeR:30, cap:1 }, + {name:'proxmox', label:'PROXMOX', rFrac:0.16, speed:0.006, rgb:'0,255,136', nodeR:22, cap:4 }, + {name:'services',label:'SERVICES', rFrac:0.30, speed:-0.004, rgb:'255,215,0', nodeR:20, cap:7 }, + {name:'agents', label:'AGENTS', rFrac:0.45, speed:0.0025, rgb:'0,190,255', nodeR:17, cap:12 }, + {name:'devices', label:'DEVICES', rFrac:0.62, speed:-0.002, rgb:'0,160,200', nodeR:14, cap:14 }, + {name:'network', label:'NETWORK', rFrac:0.82, speed:0.0015, rgb:'0,110,170', nodeR:11, cap:28 }, +]; +var NM_OPEN_RE = /\b(show|open|display|launch|pull\s*up|bring\s*up)\b.*\b(network(\s*(map|topology|viz|visual|graph|panel|status|scan|view|overlay))?|topology|node\s*map)\b|\bnetwork\s*(map|topology|viz|visual|graph)\b|\b(show|open|display|launch|pull\s*up|bring\s*up)\s+(?:me\s+)?(?:the\s+)?network\b/i; +var NM_CLOSE_RE = /\b(close|hide|dismiss|exit|collapse)\b.*\b(network|map|topology|overlay)\b|\b(close|hide|dismiss)\s*map\b/i; + +function _nmClassify(d){ + var h=(d.name||'').toLowerCase(); + if(d.source==='agent'){ + if(d.agent_type==='proxmox'||h.indexOf('pve')>=0||h.indexOf('proxmox')>=0) return 'proxmox'; + if(d.agent_type==='homeassistant'||h.indexOf('homeassist')>=0||h.indexOf('_ha')>=0|| + h.indexOf('ollama')>=0||h.indexOf('ai')>=0||h.indexOf('fusion')>=0||h.indexOf('pbx')>=0|| + h.indexOf('jellyfin')>=0||h.indexOf('homebridge')>=0) return 'services'; + return 'agents'; + } + // Named/pinned DB devices and static hosts get the inner device ring + if(d.source==='db'||d.source==='static') return 'devices'; + // Netscan-discovered devices go to the outer network ring + return 'network'; +} +function _nmRgb(n){ + if(!n.online) return '255,50,80'; + if(n.ringIdx===1) return '0,255,136'; + if(n.ringIdx===2) return '255,215,0'; + if(n.ringIdx===3) return '0,190,255'; + if(n.ringIdx===4) return '0,160,200'; + if(n.ringIdx===5) return '0,110,170'; + return '0,212,255'; +} +function _nmNodePos(n,W,H){ + if(n.ringIdx===0) return {x:W/2, y:H/2}; + var rd=NM_RINGS[n.ringIdx], rot=_nmRot[n.ringIdx]||0; + var r=Math.min(W/2,H/2)*rd.rFrac; + return {x:W/2+Math.cos(n.angle+rot)*r, y:H/2+Math.sin(n.angle+rot)*r}; +} + +async function openNetMap(){ + var ov=document.getElementById('netMapOverlay'); if(!ov) return; + ov.classList.remove('nm-closing'); ov.classList.add('nm-open'); + var devices=[]; + try{ var n=await api('network'); devices=n.devices||[]; }catch(e){} + _nmBuild(devices); _nmDraw(); +} +function closeNetMap(){ + var ov=document.getElementById('netMapOverlay'); if(!ov) return; + ov.classList.add('nm-closing'); + setTimeout(function(){ ov.classList.remove('nm-open','nm-closing'); }, 350); + if(_nmRaf){ cancelAnimationFrame(_nmRaf); _nmRaf=null; } +} +function _nmBuild(devices){ + _nmNodes=[]; _nmEdges=[]; _nmParticles=[]; + // Hub + _nmNodes.push({id:'jarvis',label:'JARVIS',sub:'165.22.1.228',online:true,agent:true,ringIdx:0,angle:0,r:NM_RINGS[0].nodeR,pulse:0}); + // Bucket + var buckets={proxmox:[],services:[],agents:[],devices:[],network:[]}; + for(var i=0;i0.45) _nmParticles.push({edge:_nmEdges.length-1,t:Math.random(),dir:'out',speed:0.0013+Math.random()*0.0017,r:1.5+Math.random()*0.7}); + } + } + // Stats + var online=_nmNodes.filter(function(n){return n.online;}).length; + var agt=_nmNodes.filter(function(n){return n.agent;}).length; + function sg(id){return document.getElementById(id);} + if(sg('nm-node-count')) sg('nm-node-count').textContent=_nmNodes.length; + if(sg('nm-online-count')) sg('nm-online-count').textContent=online; + if(sg('nm-agent-count')) sg('nm-agent-count').textContent=agt; +} +function _nmDraw(){ + if(_nmRaf) cancelAnimationFrame(_nmRaf); + var canvas=document.getElementById('nmCanvas'); if(!canvas) return; + var ov=document.getElementById('netMapOverlay'); + canvas.width = ov ? ov.clientWidth : 820; + canvas.height= ov ? ov.clientHeight-48 : 490; + var W=canvas.width, H=canvas.height, cx=W/2, cy=H/2, minR=Math.min(cx,cy); + var ctx=canvas.getContext('2d'); + + function frame(){ + if(!document.getElementById('netMapOverlay').classList.contains('nm-open')) return; + _nmRaf=requestAnimationFrame(frame); + _nmT+=0.016; + for(var i=0;i0){ctx.font='6.5px Share Tech Mono,monospace';ctx.fillStyle='rgba('+rd.rgb+',0.28)';ctx.fillText(zOn+'/'+zTot,cx+r+5,cy+11);} + ctx.textAlign='left'; + } + + // Compute node positions + var pos=[]; + for(var i=0;i<_nmNodes.length;i++) pos.push(_nmNodePos(_nmNodes[i],W,H)); + + // Spokes + for(var ei=0;ei<_nmEdges.length;ei++){ + var e=_nmEdges[ei], pa=pos[e.from], pb=pos[e.to]; if(!pa||!pb) continue; + var n=_nmNodes[e.from], rgb=_nmRgb(n); + // Apply float offset to spoke endpoints + var fya=Math.sin(_nmT*0.85+_nmNodes[e.from].pulse)*4; + var fyb=Math.sin(_nmT*0.85+_nmNodes[e.to].pulse)*4; + var lg=ctx.createLinearGradient(pa.x,pa.y+fya,pb.x,pb.y+fyb); + if(n.online){lg.addColorStop(0,'rgba('+rgb+',0.22)');lg.addColorStop(0.5,'rgba('+rgb+',0.08)');lg.addColorStop(1,'rgba(0,212,255,0.15)');} + else{lg.addColorStop(0,'rgba(80,20,30,0.07)');lg.addColorStop(1,'rgba(80,20,30,0.07)');} + ctx.beginPath();ctx.moveTo(pa.x,pa.y+fya);ctx.lineTo(pb.x,pb.y+fyb); + ctx.strokeStyle=lg;ctx.lineWidth=e.strength*1.1;ctx.stroke(); + } + + // Particles + for(var pi=0;pi<_nmParticles.length;pi++){ + var p=_nmParticles[pi]; p.t=(p.t+p.speed)%1; + var e=_nmEdges[p.edge]; if(!e) continue; + var pa=pos[e.from],pb=pos[e.to]; if(!pa||!pb) continue; + if(!_nmNodes[e.from].online) continue; + var t=p.dir==='in'?p.t:1-p.t; + var px=pa.x+(pb.x-pa.x)*t, py=pa.y+(pb.y-pa.y)*t; + var fade=Math.min(t*8,(1-t)*8,1); + ctx.beginPath();ctx.arc(px,py,p.r,0,Math.PI*2); + if(p.dir==='in'){ctx.fillStyle='rgba(0,210,255,'+(((0.6+Math.sin(_nmT*3+p.t*10)*0.3)*fade).toFixed(3))+')';ctx.shadowColor='rgba(0,200,255,0.7)';} + else{ctx.fillStyle='rgba(255,130,0,'+(((0.5+Math.sin(_nmT*4+p.t*8)*0.25)*fade).toFixed(3))+')';ctx.shadowColor='rgba(255,110,0,0.6)';} + ctx.shadowBlur=6;ctx.fill();ctx.shadowBlur=0; + } + + // Bubble nodes + for(var ni=0;ni<_nmNodes.length;ni++){ + var n=_nmNodes[ni], p=pos[ni], rgb=_nmRgb(n); + var pulse=0.5+Math.sin(_nmT*1.4+n.pulse)*0.3; + var isHub=n.ringIdx===0, isHov=_nmHoverNode===ni; + // Float offset — each node drifts on Y with unique phase + var fy=Math.sin(_nmT*0.85+n.pulse)*4; + var px=p.x, py=p.y+fy; + var baseR=n.r*(isHov?1.28:1.0); + + // Ambient glow bloom + if(n.online){ + var bloomR=baseR*2.6+Math.sin(_nmT*1.1+n.pulse)*3; + var bloom=ctx.createRadialGradient(px,py,baseR*0.4,px,py,bloomR); + bloom.addColorStop(0,'rgba('+rgb+','+(pulse*0.2).toFixed(3)+')'); + bloom.addColorStop(1,'rgba('+rgb+',0)'); + ctx.beginPath();ctx.arc(px,py,bloomR,0,Math.PI*2);ctx.fillStyle=bloom;ctx.fill(); + + // Sonar ping — expanding ring that fades out + var pingR=baseR+(((_nmT*0.6+n.pulse)%1)*baseR*2.5); + var pingA=(1-(pingR-baseR)/(baseR*2.5))*0.3; + ctx.beginPath();ctx.arc(px,py,pingR,0,Math.PI*2); + ctx.strokeStyle='rgba('+rgb+','+pingA.toFixed(3)+')'; + ctx.lineWidth=0.7;ctx.stroke(); + } + + // Frosted glass fill + var fg=ctx.createRadialGradient(px,py-baseR*0.28,0,px,py,baseR); + var fa=n.online?0.17+pulse*0.1:0.06; + fg.addColorStop(0,'rgba('+rgb+','+(fa*2.0).toFixed(3)+')'); + fg.addColorStop(0.55,'rgba('+rgb+','+fa.toFixed(3)+')'); + fg.addColorStop(1,'rgba('+rgb+','+(fa*0.15).toFixed(3)+')'); + ctx.beginPath();ctx.arc(px,py,baseR,0,Math.PI*2);ctx.fillStyle=fg;ctx.fill(); + + // Glassy highlight sheen (top-left arc) + if(n.online){ + var sh=ctx.createRadialGradient(px-baseR*0.3,py-baseR*0.35,0,px,py,baseR); + sh.addColorStop(0,'rgba(255,255,255,0.12)');sh.addColorStop(0.45,'rgba(255,255,255,0.03)');sh.addColorStop(1,'rgba(255,255,255,0)'); + ctx.beginPath();ctx.arc(px,py,baseR,0,Math.PI*2);ctx.fillStyle=sh;ctx.fill(); + } + + // Border + ctx.beginPath();ctx.arc(px,py,baseR,0,Math.PI*2); + ctx.strokeStyle='rgba('+rgb+','+(n.online?(0.45+pulse*0.35).toFixed(3):'0.18')+')'; + ctx.lineWidth=isHub?1.8:1.1;ctx.stroke(); + + // Hub crosshairs (softer) + if(isHub){ + ctx.strokeStyle='rgba('+rgb+',0.15)';ctx.lineWidth=0.6; + var ext=50; + var hlines=[[px-ext,py,px-baseR-3,py],[px+baseR+3,py,px+ext,py],[px,py-ext,px,py-baseR-3],[px,py+baseR+3,px,py+ext]]; + for(var li=0;li=0?found:null; + var info=document.getElementById('nmNodeInfo'); if(!info) return; + if(found>=0){ + var n=_nmNodes[found],rgb=_nmRgb(n); + document.getElementById('ni-name').textContent=n.label; document.getElementById('ni-name').style.color='rgb('+rgb+')'; + document.getElementById('ni-ip').textContent='IP: '+(n.sub||'—'); + document.getElementById('ni-status').textContent='STATUS: '+(n.online?'ONLINE':'OFFLINE'); + document.getElementById('ni-type').textContent='RING: '+(NM_RINGS[n.ringIdx]?NM_RINGS[n.ringIdx].name.toUpperCase():'HUB'); + info.style.display='block'; info.style.left=(mx+14)+'px'; info.style.top=(my-6)+'px'; + } else { info.style.display='none'; } + }; + canvas.onmouseleave=function(){_nmHoverNode=null;var i=document.getElementById('nmNodeInfo');if(i)i.style.display='none';}; +} diff --git a/public_html/assets/js/jarvis-protocols.js b/public_html/assets/js/jarvis-protocols.js new file mode 100644 index 0000000..4399087 --- /dev/null +++ b/public_html/assets/js/jarvis-protocols.js @@ -0,0 +1,1413 @@ +// ── ARC REACTOR STATUS ──────────────────────────────────────────────── +let _arcOnline = false; +let _arcJobs = { queued: 0, running: 0, done: 0, failed: 0 }; + +async function checkArcStatus() { + const dot = document.getElementById('bb-arc-dot'); + const sta = document.getElementById('bb-arc-status'); + if (!dot || !sta) return; + try { + const d = await api('arc?action=status'); + if (d && d.online) { + _arcOnline = true; + dot.className = 'bb-dot online'; + const active = (d.active_jobs || 0) + (d.queued_jobs || 0); + sta.textContent = active > 0 ? active + ' JOB' + (active !== 1 ? 'S' : '') : 'ONLINE'; + _arcJobs = { queued: d.queued_jobs||0, running: d.running_jobs||0, + done: d.jobs_done||0, failed: d.jobs_failed||0 }; + } else { + _arcOnline = false; + dot.className = 'bb-dot offline'; + sta.textContent = 'OFFLINE'; + } + } catch(e) { + _arcOnline = false; + dot.className = 'bb-dot offline'; + sta.textContent = 'OFFLINE'; + } +} + +// Submit a job to the Arc Reactor and return job_id +async function arcSubmitJob(type, payload, priority) { + payload = payload || {}; + priority = priority || 5; + const d = await api('arc', { action: 'job_create', type: type, payload: payload, priority: priority }); + return d.job_id || null; +} + +// Poll a job until done or failed (max 120s), calling onProgress each tick +async function arcWaitJob(jobId, onProgress) { + var start = Date.now(); + while (Date.now() - start < 120000) { + const d = await api('arc?action=job_get&id=' + jobId); + if (onProgress) onProgress(d); + if (d.status === 'done') return d; + if (d.status === 'failed') throw new Error(d.error || 'Job failed'); + await new Promise(function(r){ setTimeout(r, 1500); }); + } + throw new Error('Arc Reactor job timed out'); +} + + +// ── INTEL PROTOCOL — HUD panel ──────────────────────────────────────── +let _intelPollTimer = null; +let _intelActiveJobs = new Set(); +let _intelLastLoad = 0; + +async function loadIntel() { + const el = document.getElementById('intel-list'); + if (!el) return; + _intelLastLoad = Date.now(); + + try { + // Fetch recent research + tool_loop jobs + const [resJobs, toolJobs] = await Promise.all([ + api('arc?action=jobs&status=&limit=20').catch(() => []), + Promise.resolve([]), + ]); + const jobs = Array.isArray(resJobs) ? resJobs.filter(j => ['research','tool_loop','llm'].includes(j.job_type)) : []; + + if (!jobs.length) { + el.innerHTML = '
◈ NO INTEL JOBS
Say "research [topic]" to activate
'; + stopIntelPolling(); + return; + } + + // Check for active jobs + const hasActive = jobs.some(j => j.status === 'queued' || j.status === 'running'); + if (hasActive) startIntelPolling(); else stopIntelPolling(); + + let html = ''; + for (const job of jobs) { + const isOpen = _intelActiveJobs.has(job.id) || job.status === 'running'; + const statusClass = job.status === 'done' ? 'done' : job.status === 'failed' ? 'failed' : 'running'; + const statusLabel = job.status === 'queued' ? 'QUEUED' : job.status === 'running' ? '● ACTIVE' : job.status.toUpperCase(); + const typeLabel = job.job_type === 'research' ? '◈ INTEL' : job.job_type === 'tool_loop' ? '⚡ IRON' : '◈ LLM'; + + // Get result details if done + let bodyHtml = ''; + if (job.status === 'done' && job.result) { + let r = job.result; + if (typeof r === 'string') { try { r = JSON.parse(r); } catch(e) {} } + if (typeof r === 'object') { + const synthesis = (r.synthesis || r.result || r.response || '').trim(); + const sources = r.sources || []; + const query = r.query || r.task || ''; + const provider = r.provider || ''; + + bodyHtml = `
`; + if (provider) bodyHtml += `
PROVIDER: ${provider.toUpperCase()} · SOURCES: ${r.source_count||sources.length||'—'}
`; + if (synthesis) bodyHtml += `
${escHtml(synthesis.substring(0, 1500))}${synthesis.length>1500?'\n\n[...truncated — view in admin]':''}
`; + if (sources.length) { + bodyHtml += '
SOURCES
'; + sources.slice(0,5).forEach((s,i) => { + const title = escHtml((s.title||s.url||'').substring(0,60)); + const url = escHtml(s.url||''); + bodyHtml += ``; + }); + bodyHtml += '
'; + } + bodyHtml += '
'; + } + } else if (job.status === 'running' || job.status === 'queued') { + const typeMsg = job.job_type === 'research' ? 'Searching sources and extracting content...' : 'Executing tool loop...'; + bodyHtml = `
${typeMsg}
`; + } else if (job.status === 'failed' && job.error) { + bodyHtml = `
${escHtml(job.error.substring(0,200))}
`; + } + + const queryText = job.created_by ? job.created_by.replace('chat:', '').replace(/session.*/, '') : ''; + const ts = job.created_at ? new Date(job.created_at).toLocaleTimeString() : ''; + + html += `
+
+ ${typeLabel} + #${job.id} ${escHtml((job.created_by||'').replace('chat:','').substring(0,40))} + ${ts} + ${statusLabel} +
+ ${bodyHtml} +
`; + } + el.innerHTML = html; + + } catch(e) { + if (el) el.innerHTML = '
INTEL OFFLINE
'; + } +} + +function toggleIntelCard(id) { + const card = document.getElementById('intel-card-' + id); + if (!card) return; + if (_intelActiveJobs.has(id)) _intelActiveJobs.delete(id); + else _intelActiveJobs.add(id); + card.classList.toggle('open'); +} + +function startIntelPolling() { + if (_intelPollTimer) return; + _intelPollTimer = setInterval(() => { + if (document.getElementById('tab-intel')?.classList.contains('active')) { + loadIntel(); + } + }, 4000); +} + +function stopIntelPolling() { + if (_intelPollTimer) { clearInterval(_intelPollTimer); _intelPollTimer = null; } +} + +function escHtml(s) { + return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); +} + +function intelPrompt() { + const input = document.getElementById('textInput'); + if (input) { input.value = 'research '; input.focus(); } +} + +// Called when arc_job is returned from chat response +function onArcJobStarted(jobId, jobType) { + const commsTypes = ['arc:gmail_triage', 'arc:send_email', 'arc:compose_email', 'arc:schedule_event', 'arc:meeting_prep']; + if (commsTypes.includes(jobType)) { + const commsBtn = document.getElementById('tab-btn-comms'); + if (commsBtn) commsBtn.click(); + startCommsPolling(); + } else { + _intelActiveJobs.add(jobId); + const intelTab = document.querySelector('[onclick*="switchTab(\'intel\')"]'); + if (intelTab) intelTab.click(); + startIntelPolling(); + } +} + +// ── COMMS PROTOCOL — email triage HUD ──────────────────────────────────── +let _commsPollTimer = null; +let _commsFilter = 'priority'; +let _commsOpenCards = new Set(); + +async function loadComms() { + const el = document.getElementById('comms-list'); + if (!el) return; + + try { + const res = await api('arc?action=triage&limit=50&filter=' + _commsFilter); + const items = Array.isArray(res) ? res : (res.items || []); + + if (!items.length) { + el.innerHTML = '' + + '
◈ NO TRIAGE DATA
Say "check my email" to activate
'; + stopCommsPolling(); + return; + } + + const catOrder = {urgent:0, action:1, reply:2, meeting:3, info:4, promo:5, spam:6}; + const catIcons = {urgent:'🔴', action:'⚡', reply:'◈', meeting:'📅', info:'ℹ', promo:'📢', spam:'🗑'}; + + let html = '
'; + html += ''; + html += ''; + html += '
'; + html += '
'; + for (const [f, label] of [['priority','PRIORITY'],['urgent','URGENT'],['action','ACTION'],['all','ALL']]) { + html += `
${label}
`; + } + html += '
'; + + for (const item of items) { + const cat = item.category || 'info'; + const icon = catIcons[cat] || '◈'; + const prio = item.priority || 0; + const isOpen = _commsOpenCards.has(item.id); + const hasReply = item.draft_reply && item.draft_reply.trim().length > 5; + + html += `
+
+ ${icon} ${cat.toUpperCase()} + ${escHtml((item.subject||'(no subject)').substring(0,60))} + ${prio}/10 +
+
+
FROM: ${escHtml((item.from_name||item.from_email||'').substring(0,50))}
+
${escHtml(item.summary||'')}
+ ${hasReply ? `
DRAFT REPLY
${escHtml(item.draft_reply)}
` : ''} +
+ ${hasReply ? `` : ''} + ${hasReply ? `` : ''} + +
+
+
`; + } + + el.innerHTML = html; + + } catch(e) { + if (el) el.innerHTML = '
COMMS OFFLINE
'; + } +} + +function toggleCommsCard(id) { + const card = document.getElementById('comms-card-' + id); + if (!card) return; + if (_commsOpenCards.has(id)) _commsOpenCards.delete(id); + else _commsOpenCards.add(id); + card.classList.toggle('open'); +} + +function commsSetFilter(f) { + _commsFilter = f; + loadComms(); +} + +async function commsDismiss(id) { + await api('arc?action=triage_action&id=' + id, 'POST', {action: 'dismissed'}).catch(() => {}); + loadComms(); +} + +async function commsCopyReply(id) { + const draft = document.querySelector(`#comms-draft-${id}`); + if (draft) { + navigator.clipboard.writeText(draft.innerText).catch(() => {}); + const btn = document.querySelector(`#comms-card-${id} [onclick*="commsCopyReply"]`); + if (btn) { btn.textContent = 'COPIED!'; setTimeout(() => btn.textContent = 'COPY', 1500); } + } +} + +async function commsSendReply(id) { + const btn = document.getElementById('comms-send-' + id); + const draft = document.getElementById('comms-draft-' + id); + if (!btn || !draft) return; + btn.disabled = true; + btn.textContent = '◈ SENDING…'; + try { + const res = await api('arc', 'POST', { + action: 'job_create', + type: 'send_email', + payload: { triage_id: id, content: draft.innerText }, + priority: 8, + }); + if (res.job_id) { + btn.textContent = '◈ SENT ✓'; + btn.style.color = '#00ff88'; + setTimeout(() => loadComms(), 3000); + loadCommsOutbox(); + } else { + btn.disabled = false; + btn.textContent = '◈ SEND REPLY'; + alert('Send failed: ' + (res.error || 'unknown error')); + } + } catch(e) { + btn.disabled = false; + btn.textContent = '◈ SEND REPLY'; + } +} + +function commsShowCompose() { + const existing = document.getElementById('comms-compose-modal'); + if (existing) existing.remove(); + const modal = document.createElement('div'); + modal.className = 'comms-compose-modal'; + modal.id = 'comms-compose-modal'; + modal.innerHTML = ` +
+
◈ COMPOSE MESSAGE
+ + + + + +
+ + + +
+
+
`; + document.body.appendChild(modal); + modal.addEventListener('click', e => { if (e.target === modal) modal.remove(); }); +} + +let _ccDraftedBody = ''; + +async function commsComposeDraft() { + const to = document.getElementById('cc-to')?.value.trim(); + const subject = document.getElementById('cc-subject')?.value.trim(); + const instructions = document.getElementById('cc-instructions')?.value.trim(); + const account = document.getElementById('cc-account')?.value; + const status = document.getElementById('cc-status'); + if (!to || !instructions) { if (status) status.textContent = 'Please fill in To and message description.'; return; } + if (status) status.textContent = '◈ DRAFTING…'; + try { + const res = await api('arc', 'POST', { + action: 'job_create', type: 'compose_email', + payload: { recipient: to, subject, instructions, account, auto_send: false }, + priority: 7, + }); + if (!res.job_id) throw new Error(res.error || 'No job'); + // poll for result + let attempts = 0; + const poll = async () => { + const job = await api('arc?action=job_get&id=' + res.job_id); + if (job.status === 'done' && job.result?.drafted_body) { + _ccDraftedBody = job.result.drafted_body; + document.getElementById('cc-preview-body').textContent = _ccDraftedBody; + document.getElementById('cc-preview').style.display = 'block'; + document.getElementById('cc-send-btn').style.display = ''; + if (status) status.textContent = '◈ DRAFT READY — Review and send'; + } else if (job.status === 'failed') { + if (status) status.textContent = '✗ Draft failed: ' + (job.error || 'unknown'); + } else if (attempts++ < 20) { + setTimeout(poll, 1500); + } else { + if (status) status.textContent = '◈ Job still running — check INTEL tab'; + } + }; + setTimeout(poll, 1500); + } catch(e) { + if (status) status.textContent = '✗ Error: ' + e.message; + } +} + +async function commsComposeAndSend() { + const to = document.getElementById('cc-to')?.value.trim(); + const subject = document.getElementById('cc-subject')?.value.trim(); + const account = document.getElementById('cc-account')?.value; + const status = document.getElementById('cc-status'); + const btn = document.getElementById('cc-send-btn'); + if (!to || !_ccDraftedBody) return; + if (btn) { btn.disabled = true; btn.textContent = '◈ SENDING…'; } + if (status) status.textContent = '◈ TRANSMITTING…'; + try { + const res = await api('arc', 'POST', { + action: 'job_create', type: 'send_email', + payload: { to_email: to, subject, body: _ccDraftedBody, account }, + priority: 9, + }); + if (res.job_id) { + if (status) status.textContent = '◈ SENT ✓ (Job #' + res.job_id + ')'; + setTimeout(() => { + document.getElementById('comms-compose-modal')?.remove(); + loadCommsOutbox(); + }, 1500); + } else { + if (btn) { btn.disabled = false; btn.textContent = '◈ SEND NOW'; } + if (status) status.textContent = '✗ Send failed: ' + (res.error || 'unknown'); + } + } catch(e) { + if (btn) { btn.disabled = false; btn.textContent = '◈ SEND NOW'; } + if (status) status.textContent = '✗ Error: ' + e.message; + } +} + +async function loadCommsOutbox() { + const el = document.getElementById('comms-outbox'); + if (!el) return; + try { + const data = await api('arc?action=comms_sent&limit=20'); + const sent = Array.isArray(data) ? data : (data.sent || []); + if (!sent.length) { + el.innerHTML = '
No sent messages yet
'; + return; + } + const statusColor = {sent:'#00ff88', failed:'#ff2244', queued:'#ffd700'}; + let html = ''; + for (const m of sent) { + const ts = m.sent_at ? new Date(m.sent_at + 'Z').toLocaleString() : '—'; + const sc = m.status || 'sent'; + html += `
+
+
TO: ${escHtml((m.to_email||'').substring(0,40))}
+ ${sc.toUpperCase()} +
+
${escHtml((m.subject||'(no subject)').substring(0,60))}
+
${ts} · ${m.account||'gmail'}
+
`; + } + el.innerHTML = html; + } catch(e) { + el.innerHTML = '
OUTBOX OFFLINE
'; + } +} + +function commsTriageNow() { + const input = document.getElementById('textInput'); + if (input) { input.value = 'check my email'; input.dispatchEvent(new KeyboardEvent('keydown', {key:'Enter',keyCode:13,bubbles:true})); } +} + +function startCommsPolling() { + if (_commsPollTimer) return; + _commsPollTimer = setInterval(() => { + if (document.getElementById('tab-comms')?.classList.contains('active')) { loadComms(); loadCommsOutbox(); } + }, 8000); +} + +function stopCommsPolling() { + if (_commsPollTimer) { clearInterval(_commsPollTimer); _commsPollTimer = null; } +} + +// ── GUARDIAN MODE ───────────────────────────────────────────────────────────── +let _guardianPollTimer = null; +let _guardianChatTimer = null; +let _guardianLastChat = ''; +let _guardianUnread = 0; + +async function loadGuardian() { + const el = document.getElementById('guardian-list'); + if (!el) return; + + try { + const [statusData, eventsData] = await Promise.all([ + api('arc?action=guardian_status').catch(() => ({})), + api('arc?action=guardian_events&limit=40').catch(() => []), + ]); + + const events = Array.isArray(eventsData) ? eventsData : []; + const status = statusData || {}; + const counts = status.counts || {}; + const unread = parseInt(counts.unread || 0); + const critU = parseInt(counts.critical_unread || 0); + + _guardianUnread = unread; + _updateGuardianBadge(unread, critU); + + const lastScan = status.last_scan + ? new Date(status.last_scan + 'Z').toLocaleTimeString() + : '—'; + + let html = `
+ ◈ GUARDIAN MODE + + ${status.enabled ? '● ACTIVE' : '○ INACTIVE'} + + SCAN: ${lastScan} + ${unread ? `` : ''} + +
`; + + if (!events.length) { + html += '
◈ ALL CLEAR
Guardian is watching...
'; + } else { + for (const ev of events) { + const sev = ev.severity || 'info'; + const acked = ev.acknowledged; + const ts = ev.created_at ? new Date(ev.created_at).toLocaleTimeString() : ''; + const typeIco = {agent_offline:'⚠',agent_online:'✓',cpu_high:'⚡', + mem_high:'⚡',disk_high:'💾',service_down:'✗', + service_recovered:'✓',sitrep:'◈',anomaly:'◈'}[ev.event_type] || '◈'; + html += `
+ ${sev.toUpperCase()} +
+
${typeIco} ${escHtml(ev.message||'')}
+ ${ev.ai_analysis ? `
${escHtml(ev.ai_analysis.substring(0,200))}
` : ''} +
+
+ ${ts} + ${!acked ? `` : ''} +
+
`; + } + } + el.innerHTML = html; + startGuardianPolling(); + + } catch(e) { + if (el) el.innerHTML = '
GUARDIAN OFFLINE
'; + } +} + +function _updateGuardianBadge(unread, critical) { + const dot = document.getElementById('bb-guardian-dot'); + const badge = document.getElementById('bb-guardian-badge'); + const status = document.getElementById('bb-guardian-status'); + if (!dot) return; + dot.className = 'bb-dot'; + if (critical > 0) { + dot.classList.add('critical'); status.textContent = 'ALERT'; status.style.color = 'var(--red)'; + } else if (unread > 0) { + dot.classList.add('warning'); status.textContent = 'WARNING'; status.style.color = '#f5a623'; + } else { + dot.classList.add('all-clear'); status.textContent = 'CLEAR'; status.style.color = 'var(--green)'; + } + if (unread > 0) { + badge.textContent = unread; badge.style.display = 'inline'; + } else { + badge.style.display = 'none'; + } +} + +async function guardianAck(id) { + await api('arc?action=guardian_ack&id=' + id).catch(() => {}); + const ev = document.getElementById('gev-' + id); + if (ev) ev.classList.add('acked'); + _guardianUnread = Math.max(0, _guardianUnread - 1); + _updateGuardianBadge(_guardianUnread, 0); +} + +async function guardianAckAll() { + await api('arc?action=guardian_ack').catch(() => {}); + loadGuardian(); +} + +function guardianSitrep() { + const input = document.getElementById('textInput'); + if (input) { input.value = 'sitrep'; input.dispatchEvent(new KeyboardEvent('keydown', {key:'Enter',keyCode:13,bubbles:true})); } +} + +function switchGuardianTab() { + const btn = document.getElementById('tab-btn-guardian'); + if (btn) btn.click(); +} + +function startGuardianPolling() { + if (_guardianPollTimer) return; + _guardianPollTimer = setInterval(() => { + if (document.getElementById('tab-guardian')?.classList.contains('active')) loadGuardian(); + else _refreshGuardianBadge(); + }, 30000); +} + +async function _refreshGuardianBadge() { + const s = await api('arc?action=guardian_status').catch(() => null); + if (!s) return; + const counts = s.counts || {}; + _updateGuardianBadge(parseInt(counts.unread||0), parseInt(counts.critical_unread||0)); +} + +// Proactive chat polling — checks for guardian-injected messages every 30s +let _proactiveChatLastId = 0; +async function _pollProactiveChat() { + try { + const rows = await api('arc?action=guardian_chat').catch(() => []); + if (!Array.isArray(rows)) return; + for (const row of rows) { + if (row.id > _proactiveChatLastId) { + _proactiveChatLastId = row.id; + // Don't spam on first load — only show messages from last 5 min + const age = Date.now() - new Date(row.created_at + 'Z').getTime(); + if (age < 300000) { + addMessage('jarvis', row.message); + speak(row.message); + } + } + } + } catch(e) {} +} + +// ── MISSION OPS HUD ─────────────────────────────────────────────────────────── +let _missionsOpenCards = new Set(); + +async function loadMissionsHud() { + const el = document.getElementById('missions-hud'); + if (!el) return; + try { + const missions = await api('arc?action=missions'); + const list = Array.isArray(missions) ? missions : []; + + let html = ''; + + if (!list.length) { + html += '
◈ NO MISSIONS
Create workflows in Admin → Mission Ops
'; + el.innerHTML = html; + return; + } + + const trigIcons = {manual:'🖐', schedule:'⏱', guardian_event:'🛡', email_keyword:'📧'}; + for (const m of list) { + const isOpen = _missionsOpenCards.has(m.id); + const icon = trigIcons[m.trigger_type] || '◈'; + const enabled = m.enabled; + const lastRun = m.last_run_at ? new Date(m.last_run_at+'Z').toLocaleTimeString() : 'never'; + html += `
+
+ ${icon} + ${escHtml(m.name)} + ${m.trigger_type.replace('_',' ').toUpperCase()} + ${m.run_count||0} runs +
+
+ ${m.description ? `
${escHtml(m.description)}
` : ''} +
Last run: ${lastRun} · ${m.run_count||0} total runs
+
+ +
+
+
+
`; + } + el.innerHTML = html; + } catch(e) { + if (el) el.innerHTML = '
MISSIONS OFFLINE
'; + } +} + +function toggleMissionCard(id) { + const card = document.getElementById('mission-card-' + id); + if (!card) return; + if (_missionsOpenCards.has(id)) _missionsOpenCards.delete(id); + else _missionsOpenCards.add(id); + card.classList.toggle('open'); +} + +async function hudRunMission(id) { + const btn = document.getElementById('mission-run-btn-' + id); + const res = document.getElementById('mission-run-result-' + id); + if (btn) { btn.disabled = true; btn.textContent = '◈ RUNNING…'; } + if (res) res.textContent = ''; + try { + const data = await api('arc?action=mission_run&id=' + id, 'POST', {trigger_source: 'hud'}); + const s = data.status || 'done'; + const color = s === 'done' ? '#00ff88' : s === 'failed' ? '#ff2244' : '#ffd700'; + if (res) res.style.color = color; + if (res) res.textContent = `◈ ${s.toUpperCase()} — Run #${data.run_id||'?'} · ${data.steps||0} steps completed`; + if (btn) { btn.disabled = false; btn.textContent = '▶ RUN NOW'; } + setTimeout(loadMissionsHud, 2000); + } catch(e) { + if (btn) { btn.disabled = false; btn.textContent = '▶ RUN NOW'; } + if (res) res.textContent = '✗ Run failed'; + } +} + +// ── DIRECTIVES HUD ──────────────────────────────────────────────────────────── +let _dirOpenCards = new Set(); + +async function loadDirectivesHud() { + const el = document.getElementById('directives-hud'); + if (!el) return; + try { + const d = await api('directives/list?status=active'); + const list = (d.directives || []); + + let html = ''; + + if (!list.length) { + html += '
◈ NO ACTIVE DIRECTIVES
Create objectives in Admin → Directives
'; + el.innerHTML = html; + return; + } + + const catColors = {work:'var(--cyan)',personal:'#a78bfa',health:'#00ff88',finance:'#ffd700',home:'var(--panel-border)',other:'var(--text-dim)'}; + for (const dir of list) { + const pct = Math.min(100, Math.round(dir.progress || 0)); + const isOpen = _dirOpenCards.has(dir.id); + const color = catColors[dir.category] || 'var(--cyan)'; + const fillColor = pct >= 80 ? '#00ff88' : pct >= 40 ? '#ffd700' : '#ff6644'; + const daysLeft = dir.target_date + ? Math.ceil((new Date(dir.target_date) - new Date()) / 86400000) : null; + const dueTxt = daysLeft !== null + ? (daysLeft < 0 ? `OVERDUE ${Math.abs(daysLeft)}d` : `${daysLeft}d left`) + : ''; + const dueColor = daysLeft !== null && daysLeft < 0 ? '#ff2244' : daysLeft < 14 ? '#ffd700' : 'var(--text-dim)'; + + html += `
+
+ ${dir.category.toUpperCase()} + ${escHtml(dir.title)} + ${pct}% + ${dueTxt ? `${dueTxt}` : ''} +
+
+
+
${dir.kr_count||0} KEY RESULTS · ${dir.link_count||0} LINKED ITEMS
+ +
+
`; + } + el.innerHTML = html; + } catch(e) { + if (el) el.innerHTML = '
DIRECTIVES OFFLINE
'; + } +} + +function toggleDirCard(id) { + const card = document.getElementById('dir-card-' + id); + if (!card) return; + if (_dirOpenCards.has(id)) _dirOpenCards.delete(id); + else _dirOpenCards.add(id); + card.classList.toggle('open'); +} + +async function hudDirectiveReview(id) { + const res = await api('arc?action=job_create', 'POST', { + type: 'directive_review', payload: {directive_id: id, provider: 'claude'}, priority: 6, + }); + if (res.job_id) { + addMessage('jarvis', `◈ DIRECTIVE REVIEW initiated (Job #${res.job_id}). Analyzing objectives and key results now. Results will appear here shortly.`); + speak(`Directive review underway. I'll brief you on your progress in a moment.`); + } +} + +// ── MEMORY CORE — bottom bar count ──────────────────────────────────────────── +async function updateMemoryCount() { + try { + const stats = await api('memory?action=stats'); + const el = document.getElementById('bb-memory-count'); + const dot = document.getElementById('bb-memory-dot'); + if (el && stats) { + const total = stats.total || 0; + el.textContent = total + ' FACTS'; + if (dot) dot.style.background = total > 0 ? 'var(--cyan)' : 'rgba(0,212,255,0.3)'; + } + } catch(e) {} +} + +// ── CLEARANCE PROTOCOL HUD ───────────────────────────────────────────────────── +const _clrOpenCards = new Set(); + +async function updateClearanceBanner() { + try { + const pending = await api('arc?action=clearance_pending'); + const list = Array.isArray(pending) ? pending : []; + const count = list.length; + const banner = document.getElementById('clearance-banner'); + const badge = document.getElementById('clr-tab-badge'); + const bcount = document.getElementById('clr-banner-count'); + if (banner) { + if (count > 0) { + banner.classList.add('active'); + if (bcount) bcount.textContent = count; + } else { + banner.classList.remove('active'); + } + } + if (badge) { + if (count > 0) { badge.style.display = 'inline'; badge.textContent = count; } + else badge.style.display = 'none'; + } + } catch(e) {} +} + +async function loadClearanceHud() { + const el = document.getElementById('clearance-hud'); + if (!el) return; + try { + const [pendingRes, rulesRes, historyRes] = await Promise.all([ + api('arc?action=clearance_pending'), + api('arc?action=clearance_rules'), + api('arc?action=clearance_history&limit=20') + ]); + const pending = Array.isArray(pendingRes) ? pendingRes : []; + const rules = Array.isArray(rulesRes) ? rulesRes : []; + const history = Array.isArray(historyRes) ? historyRes : []; + + let html = ''; + + // Pending requests + html += `
PENDING AUTHORIZATION (${pending.length})
`; + if (!pending.length) { + html += '
◈ NO PENDING CLEARANCE REQUESTS
'; + } else { + for (const cr of pending) { + const isOpen = _clrOpenCards.has(cr.id); + const pl = typeof cr.job_payload === 'string' ? JSON.parse(cr.job_payload || '{}') : (cr.job_payload || {}); + const created = cr.created_at ? new Date(cr.created_at).toLocaleString() : ''; + const expires = cr.expires_at ? new Date(cr.expires_at).toLocaleString() : ''; + html += `
+
+ ${escHtml(cr.job_type.toUpperCase().replace(/_/g,' '))} + ${cr.risk_level.toUpperCase()} + #${cr.id} +
+
+
${escHtml(cr.description || 'No description')}
+
+ Requested: ${created}${expires ? ' · Expires: ' + expires : ''} +
+
+ Payload: ${escHtml(JSON.stringify(pl))} +
+
+ + +
+
+
`; + } + } + + // Rules + html += `
CLEARANCE RULES
`; + if (!rules.length) { + html += '
No rules configured
'; + } else { + html += '
'; + for (const r of rules) { + const enClass = r.enabled ? 'clr-rule-enabled' : 'clr-rule-disabled'; + const enLabel = r.enabled ? 'ON' : 'OFF'; + const reqLabel = r.require_approval ? 'REQUIRES APPROVAL' : 'AUTO-ALLOW'; + const autoTxt = r.auto_approve_after_min ? ` · AUTO ${r.auto_approve_after_min}m` : ''; + html += `
+ ${r.job_type.replace(/_/g,' ').toUpperCase()} + ${r.risk_level.toUpperCase()} + ${reqLabel}${autoTxt} + +
`; + } + html += '
'; + } + + // Recent history + html += `
RECENT HISTORY
`; + const recentDecided = history.filter(h => h.status !== 'pending').slice(0, 10); + if (!recentDecided.length) { + html += '
No history yet
'; + } else { + html += '
'; + for (const h of recentDecided) { + const ts = h.decided_at ? new Date(h.decided_at).toLocaleString() : ''; + html += `
+ + ${h.job_type.replace(/_/g,' ').toUpperCase()} + ${h.status.toUpperCase()} + ${ts} +
`; + } + html += '
'; + } + + el.innerHTML = html; + await updateClearanceBanner(); + } catch(e) { + if (el) el.innerHTML = '
CLEARANCE SYSTEM OFFLINE
'; + } +} + +function toggleClrCard(id) { + const card = document.getElementById('clr-card-' + id); + if (!card) return; + if (_clrOpenCards.has(id)) _clrOpenCards.delete(id); + else _clrOpenCards.add(id); + card.classList.toggle('open'); +} + +async function hudClearanceDecide(id, action) { + const label = action === 'approve' ? 'AUTHORIZE' : 'DENY'; + if (!confirm(`${label} clearance request #${id}?`)) return; + const note = action === 'deny' ? (prompt('Reason for denial (optional):') || '') : ''; + try { + const res = await api(`arc?action=clearance_${action}&id=${id}`, 'POST', { decided_by: 'admin', note }); + const msg = action === 'approve' + ? `◈ Clearance #${id} authorized. Job dispatched.` + : `◈ Clearance #${id} denied${note ? ': ' + note : ''}.`; + addMessage('jarvis', msg); + speak(action === 'approve' ? 'Clearance granted. Job dispatched.' : 'Request denied.'); + await loadClearanceHud(); + } catch(e) { + addMessage('system', 'Clearance action failed.'); + } +} + +async function hudClearanceRuleToggle(id, newEnabled) { + try { + await api(`arc?action=clearance_rule_update&id=${id}`, 'POST', { enabled: newEnabled }); + await loadClearanceHud(); + } catch(e) {} +} + +async function loadAgents() { + const [listData, metricsData] = await Promise.all([ + api('agent/list'), + api('agent/status') + ]); + const agents = listData.agents || []; + const metrics = metricsData.metrics || {}; + // Fetch sparkline data (non-blocking) + api('metrics').then(d => { _sparkData = d || {}; renderAgentsTab(agents, metrics); }).catch(() => {}); + renderAgentsTab(agents, metrics); +} + +async function addNetworkDevice() { + const ip = prompt('IP address (e.g. 10.48.200.43):'); + if (!ip) return; + const name = prompt('Device name (e.g. Yealink Phone):'); + if (!name) return; + const type = prompt('Type (server, voip, nas, printer, device):', 'device') || 'device'; + const r = await api('network/add', 'POST', {ip, alias: name, type}); + if (r.error) { alert('Error: ' + r.error); return; } + loadNetwork(); +} + +async function deleteNetworkDevice(ip, evt) { + evt.stopPropagation(); + if (!confirm('Remove ' + ip + ' from the network list?')) return; + const r = await api('network/delete', 'POST', {ip}); + if (r.error) { alert('Error: ' + r.error); return; } + loadNetwork(); +} + +let _agentSparkData = {}; +function sparkline(points, width=80, height=20, color='var(--cyan)') { + if (!points || points.length < 2) return ''; + const max = Math.max(...points, 1); + const min = Math.min(...points); + const range = max - min || 1; + const step = width / (points.length - 1); + const pts = points.map((v, i) => { + const x = i * step; + const y = height - ((v - min) / range) * (height - 2) - 1; + return `${x.toFixed(1)},${y.toFixed(1)}`; + }).join(' '); + return ` + + + `; +} + +function renderAgentsTab(agents, metrics) { + const el = document.getElementById('agents-list'); + if (!el) return; + if (!agents.length) { + el.innerHTML = '
NO AGENTS REGISTERED
'; + return; + } + el.innerHTML = agents.map(ag => { + const m = metrics[ag.agent_id] || {}; + const sys = m.system || {}; + const alive = ag.status === 'online'; + const cpu = sys.cpu_percent != null ? Math.round(sys.cpu_percent) : '--'; + const mem = sys.memory ? Math.round(sys.memory.percent) : '--'; + const memUsed = sys.memory ? Math.round(sys.memory.used_mb / 1024 * 10) / 10 + 'GB' : '--'; + const memTot = sys.memory ? Math.round(sys.memory.total_mb / 1024 * 10) / 10 + 'GB' : '--'; + const disks = sys.disk || []; + const maxDisk = disks.length ? Math.max(...disks.map(d => parseInt(d.percent)||0)) : null; + const uptime = sys.uptime ? sys.uptime.human : (alive ? 'ONLINE' : 'OFFLINE'); + const since = ag.last_seen ? ag.last_seen.replace('T',' ').replace(/\.\d+Z$/,'') : '--'; + + const gauge = (val, unit='%', warn=80, crit=90) => { + const v = typeof val === 'number' ? val : parseInt(val); + if (isNaN(v)) return `--`; + const col = v >= crit ? 'var(--red)' : v >= warn ? '#f5a623' : 'var(--green)'; + return `
+
+
+
+ ${v}${unit} +
`; + }; + + const svcs = (sys.services || []).filter(s => s.status !== 'inactive' || true) + .map(s => `${s.service}: ${s.status}`) + .join(''); + + const ctxKey = 'agent_' + ag.agent_id; + _panelCtx[ctxKey] = {type:'agent', label: ag.hostname, agent_id: ag.agent_id, + hostname: ag.hostname, status: ag.status, cpu, mem}; + + return `
+
+
+ ${ag.hostname} + ${ag.agent_type.toUpperCase()} · ${ag.ip_address} + ${alive ? 'ONLINE' : 'OFFLINE'} +
+ ${alive ? `
+
CPU
${gauge(cpu)}
+
MEM ${memUsed}/${memTot}
${gauge(mem)}
+
DISK
${maxDisk != null ? gauge(maxDisk) : '--'}
+
+
+
+
CPU 2H
+ ${sparkline((_agentSparkData[ag.agent_id]||[]).map(p=>p.cpu), 100, 18, 'rgba(0,212,255,0.7)')} +
+
+
MEM 2H
+ ${sparkline((_agentSparkData[ag.agent_id]||[]).map(p=>p.mem), 100, 18, 'rgba(0,255,136,0.7)')} +
+
` : ''} +
+
UP: ${uptime} · SEEN: ${since}
+ ${svcs ? `
${svcs}
` : ''} +
+ ${alive ? `
+ + +
` : ''} +
`; + }).join(''); +} + +function openAgentModal() { + const os = detectOS(); + const title = document.getElementById('agentModalTitle'); + const content = document.getElementById('agentModalContent'); + const modal = document.getElementById('agentModal'); + const regKey = 'f846a9aaf7ce9a61742c63c87c4186052a71d2a580c65518'; + const baseUrl = 'https://jarvis.orbishosting.com/agent'; + const jUrl = 'https://jarvis.orbishosting.com'; + + if (os === 'tablet') { + title.textContent = '● JARVIS — TABLET / MOBILE'; + content.innerHTML = + '
✓ You\'re viewing JARVIS on a tablet or mobile device.
' + + '
The JARVIS Agent runs on desktop and server platforms (Windows, macOS, Linux).

' + + 'Tablets and phones can browse the full JARVIS dashboard but do not need an agent installed — all data comes from your other monitored machines.
'; + } else if (_agentOnline) { + title.textContent = '● AGENT CONNECTED'; + content.innerHTML = + '
✓ JARVIS Agent is active on this machine.
' + + '
' + + 'Host: ' + (_myAgent?.hostname||'—') + '
' + + 'IP: ' + (_myAgent?.ip_address||'—') + '
' + + 'Type: ' + (_myAgent?.agent_type||'—').toUpperCase() + '
' + + 'Reporting: CPU · Memory · Disk · Services · Uptime
'; + } else { + const inst = { + windows: { + label:'Windows', + cmd:'# Run PowerShell as Administrator:\nSet-ExecutionPolicy Bypass -Scope Process -Force\nInvoke-WebRequest -Uri "'+baseUrl+'/install-windows.ps1" -OutFile "$env:TEMP\\install.ps1"\n& "$env:TEMP\\install.ps1" -JarvisUrl '+jUrl+' -Key '+regKey, + dl: baseUrl+'/install-windows.ps1', + note:'Run PowerShell as Administrator. Installs as a Windows Task Scheduler service.' + }, + mac: { + label:'macOS', + cmd:'bash <(curl -sSL '+baseUrl+'/install-mac.sh) \\\n --jarvis-url '+jUrl+' \\\n --key '+regKey, + dl: baseUrl+'/install-mac.sh', + note:'Run in Terminal. Installs as a launchd background service.' + }, + linux: { + label:'Linux', + cmd:'curl -sSL '+baseUrl+'/install.sh | sudo bash -s -- \\\n --jarvis-url '+jUrl+' \\\n --key '+regKey, + dl: baseUrl+'/install.sh', + note:'Run in terminal. Installs as a systemd service.' + }, + unknown: { + label:'Your System', + cmd:'# Browse installers:\nhttps://jarvis.orbishosting.com/agent/', + dl: 'https://jarvis.orbishosting.com/agent/', + note:'Choose your platform installer from the JARVIS agent directory.' + } + }; + const i = inst[os] || inst.unknown; + const osBadge = {windows:'🪟 WINDOWS', mac:'🍎 MACOS', linux:'🐧 LINUX', unknown:'❓ UNKNOWN'}[os] || os.toUpperCase(); + title.textContent = '● INSTALL AGENT · ' + (inst[os] ? inst[os].label.toUpperCase() : 'YOUR SYSTEM'); + content.innerHTML = + '
DETECTED: ' + osBadge + '
' + + '
'+i.note+'
' + + '
'+i.cmd+'
' + + '↓ DOWNLOAD INSTALLER' + + '
After install, the AGENT indicator turns green within 30 seconds.
'; + } + modal.classList.add('open'); +} + +document.addEventListener('click', function(e) { + if (e.target === document.getElementById('agentModal')) + document.getElementById('agentModal').classList.remove('open'); +}); + + + + +// ── SITES MANAGER ──────────────────────────────────────────────────── +let sitesData = {}; + +function openSitesModal() { + document.getElementById('sitesModal').style.display = 'flex'; + loadSites(); +} +function closeSitesModal() { + document.getElementById('sitesModal').style.display = 'none'; +} +// Close on backdrop click +document.getElementById('sitesModal').addEventListener('click', function(e) { + if (e.target === this) closeSitesModal(); +}); + +async function loadSites() { + document.getElementById('sites-grid').innerHTML = '
LOADING SITE SETTINGS...
'; + const res = await api('sites'); + if (!res.success) { + document.getElementById('sites-grid').innerHTML = '
FAILED TO LOAD SETTINGS
'; + return; + } + sitesData = res.sites; + // Pre-fill global key from first site + const firstKey = Object.values(res.sites)[0]?.api_key || ''; + document.getElementById('global-api-key').value = firstKey; + renderSiteCards(); +} + +function renderSiteCards() { + const grid = document.getElementById('sites-grid'); + let html = ''; + for (const [id, s] of Object.entries(sitesData)) { + html += ` +
+
+
${s.name.toUpperCase()}
+
${s.url}
+
+
+
FROM EMAIL
+ +
+
+
FROM NAME
+ +
+
+
ADMIN NOTIFICATION EMAIL
+ +
+
+ + +
+
`; + } + grid.innerHTML = html; +} + +async function pushApiKey() { + const key = document.getElementById('global-api-key').value.trim(); + const status = document.getElementById('push-status'); + if (!key) { status.style.color='#f44'; status.textContent='✗ API KEY REQUIRED'; return; } + status.style.color='var(--text-dim)'; status.textContent='PUSHING TO ALL SITES...'; + const res = await api('sites', 'POST', {action:'push_key', api_key:key}); + if (res.success) { + const ok = Object.values(res.results).filter(Boolean).length; + const total = Object.keys(res.results).length; + status.style.color = ok === total ? 'var(--cyan)' : '#fa0'; + status.textContent = `✓ PUSHED TO ${ok}/${total} SITES`; + for (const id of Object.keys(sitesData)) sitesData[id].api_key = key; + } else { + status.style.color='#f44'; status.textContent='✗ ' + (res.error || 'FAILED'); + } +} + +async function saveSite(id) { + const status = document.getElementById(id + '-status'); + status.style.color='var(--text-dim)'; status.textContent='SAVING...'; + const res = await api('sites', 'POST', { + action: 'save', + site: id, + from_email: document.getElementById(id+'-from_email').value.trim(), + from_name: document.getElementById(id+'-from_name').value.trim(), + admin_email: document.getElementById(id+'-admin_email').value.trim(), + }); + if (res.success) { + status.style.color='var(--cyan)'; status.textContent='✓ SAVED'; + setTimeout(() => { status.textContent=''; }, 3000); + } else { + status.style.color='#f44'; status.textContent='✗ ' + (res.error || 'FAILED'); + } +} + +// ── VISION PROTOCOL — screenshot lightbox ──────────────────────────────────── +function openVisionLightbox(title) { + const lb = document.getElementById('vision-lightbox'); + document.getElementById('vision-lb-title').textContent = title || '◈ VISION PROTOCOL'; + document.getElementById('vision-lb-img').style.display = 'none'; + document.getElementById('vision-lb-img').src = ''; + document.getElementById('vision-lb-analysis').textContent = ''; + document.getElementById('vision-lb-spinner').style.display = 'block'; + lb.classList.add('open'); +} + +function closeVisionLightbox() { + document.getElementById('vision-lightbox').classList.remove('open'); +} + +async function agentScreenshot(hostname) { + openVisionLightbox('◈ VISION PROTOCOL — ' + hostname.toUpperCase()); + const arcRes = await api('arc?action=job_create', 'POST', { + type: 'screenshot', + payload: {agent: hostname, analyze: true}, + priority: 8, + }).catch(() => null); + + if (!arcRes || !arcRes.job_id) { + document.getElementById('vision-lb-spinner').style.display = 'none'; + document.getElementById('vision-lb-analysis').textContent = 'Failed to submit screenshot job — Arc Reactor may be offline.'; + return; + } + + // Poll for result + const jobId = arcRes.job_id; + let tries = 0; + const poll = async () => { + tries++; + const job = await api('arc?action=job_get&id=' + jobId).catch(() => null); + if (job && job.status === 'done') { + const r = job.result || {}; + document.getElementById('vision-lb-spinner').style.display = 'none'; + if (r.has_image && r.screenshot_id) { + // Fetch full screenshot with image + const full = await api('arc?action=screenshot_get&id=' + r.screenshot_id).catch(() => null); + if (full && full.image_b64) { + const img = document.getElementById('vision-lb-img'); + img.src = 'data:image/png;base64,' + full.image_b64; + img.style.display = 'block'; + } + } + document.getElementById('vision-lb-analysis').textContent = + r.analysis || (r.has_image ? 'Screenshot captured — no analysis available.' : JSON.stringify(r.snapshot || r, null, 2)); + } else if (job && job.status === 'failed') { + document.getElementById('vision-lb-spinner').style.display = 'none'; + document.getElementById('vision-lb-analysis').textContent = 'Screenshot failed: ' + (job.error || 'Unknown error'); + } else if (tries < 30) { + setTimeout(poll, 2000); + } else { + document.getElementById('vision-lb-spinner').style.display = 'none'; + document.getElementById('vision-lb-analysis').textContent = 'Timed out waiting for screenshot.'; + } + }; + setTimeout(poll, 2000); +} + +async function agentSysinfo(hostname) { + openVisionLightbox('⚡ FIELD SYSINFO — ' + hostname.toUpperCase()); + const arcRes = await api('arc?action=job_create', 'POST', { + type: 'sysinfo', + payload: {agent: hostname, analyze: true}, + priority: 7, + }).catch(() => null); + + if (!arcRes || !arcRes.job_id) { + document.getElementById('vision-lb-spinner').style.display = 'none'; + document.getElementById('vision-lb-analysis').textContent = 'Failed to submit sysinfo job.'; + return; + } + + const jobId = arcRes.job_id; + let tries = 0; + const poll = async () => { + tries++; + const job = await api('arc?action=job_get&id=' + jobId).catch(() => null); + if (job && job.status === 'done') { + const r = job.result || {}; + document.getElementById('vision-lb-spinner').style.display = 'none'; + const snap = r.snapshot || {}; + const snapText = Object.entries(snap) + .filter(([k]) => !['success','screenshot_available','snapshot_type'].includes(k)) + .map(([k,v]) => `${k.toUpperCase().replace(/_/g,' ')}: ${Array.isArray(v) ? v.join('\n ') : v}`) + .join('\n'); + document.getElementById('vision-lb-analysis').textContent = + (r.analysis ? r.analysis + '\n\n─────────────────────\n\n' : '') + (snapText || JSON.stringify(r, null, 2)); + } else if (job && job.status === 'failed') { + document.getElementById('vision-lb-spinner').style.display = 'none'; + document.getElementById('vision-lb-analysis').textContent = 'Sysinfo failed: ' + (job.error || 'Unknown error'); + } else if (tries < 20) { + setTimeout(poll, 2000); + } else { + document.getElementById('vision-lb-spinner').style.display = 'none'; + document.getElementById('vision-lb-analysis').textContent = 'Timed out.'; + } + }; + setTimeout(poll, 2000); +} + +document.addEventListener('keydown', e => { + if (e.key === 'Escape') closeVisionLightbox(); +}); + +// ── CHAT HISTORY SEARCH ─────────────────────────────────────────────────────── +function openSearchModal() { + document.getElementById('searchModal').style.display = 'flex'; + document.getElementById('searchInput').focus(); +} +function closeSearchModal() { + document.getElementById('searchModal').style.display = 'none'; + document.getElementById('searchResults').innerHTML = '
Type to search your JARVIS conversations
'; + document.getElementById('searchInput').value = ''; +} +async function runSearch() { + const q = document.getElementById('searchInput').value.trim(); + if (!q) return; + const el = document.getElementById('searchResults'); + el.innerHTML = '
Searching...
'; + try { + const d = await api('history?q=' + encodeURIComponent(q)); + if (!d.results || !d.results.length) { + el.innerHTML = '
No results for "' + q + '"
'; + return; + } + el.innerHTML = d.results.map(r => { + const role = r.role === 'user' ? '👤' : '🤖'; + const ts = new Date(r.created_at).toLocaleString('en-US', {month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'}); + const snippet = r.content.length > 200 ? r.content.slice(0,197) + '…' : r.content; + return `
+
+ ${role} ${r.role.toUpperCase()} + ${ts} +
+
${snippet.replace(/ +
`; + }).join(''); + } catch(e) { + el.innerHTML = '
Search failed
'; + } +} +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; + const panels = {left:'leftPanel', center:'centerPanel', right:'rightPanel'}; + Object.entries(panels).forEach(([k, id]) => { + document.getElementById(id)?.classList.toggle('mob-active', k === which); + }); + document.querySelectorAll('.mob-nav-btn').forEach(b => b.classList.remove('active')); + document.getElementById('mob-btn-' + which)?.classList.add('active'); + if (which === 'right') loadNews(); +} +function initMobile() { + if (window.innerWidth > 900) return; + ['leftPanel','centerPanel','rightPanel'].forEach(id => + document.getElementById(id)?.classList.remove('mob-active')); + document.getElementById('leftPanel')?.classList.add('mob-active'); + document.querySelectorAll('.mob-nav-btn').forEach(b => b.classList.remove('active')); + document.getElementById('mob-btn-left')?.classList.add('active'); +} +window.addEventListener('resize', initMobile); diff --git a/public_html/index.html b/public_html/index.html index 8a6596f..2169d6e 100644 --- a/public_html/index.html +++ b/public_html/index.html @@ -6,1111 +6,7 @@ JARVIS — Integrated Defense and Logistics System - + @@ -1497,3855 +393,10 @@ body::after{ style="position:fixed;top:-9999px;left:-9999px;width:320px;height:240px"> - + + + +