mirror of
https://github.com/myronblair/jarvis
synced 2026-06-30 17:50:23 -05:00
Restore working page + clean orbital network map rebuild
Reverted to c8e0020 (all 10 effects working) then added net map cleanly:
- Used var/for instead of const/let/arrow-functions to avoid any closure/scope issues
- Orbital ring layout: JARVIS hub center, 4 concentric rings (proxmox/services/agents/devices)
- Rings rotate at different speeds/directions independently
- Spoke lines hub-to-each-node with cyan inbound and orange outbound particles
- Node labels point outward from center, never overlap
- Tiny green/red status dot on every non-hub node
- Hover shows node info card (name/IP/status/ring)
- Open: say/type show network map / network topology / show connections
- Close: say/type close map / close network / dismiss map
- All other features (mic, voice, text, panels) unaffected
This commit is contained in:
+249
-366
@@ -754,69 +754,6 @@ body::after{
|
||||
animation:shimmer 1.5s infinite;border-radius:4px;height:12px;
|
||||
}
|
||||
@keyframes shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}
|
||||
|
||||
/* ── NETWORK MAP OVERLAY ─────────────────────────────────────────────── */
|
||||
#netMapOverlay{
|
||||
position:fixed;top:52px;left:0;
|
||||
width:min(860px,86vw);
|
||||
height:min(570px,76vh);
|
||||
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 / 20px 1px no-repeat,
|
||||
linear-gradient(to bottom,rgba(0,212,255,0.7),rgba(0,212,255,0.7)) top left / 1px 20px no-repeat,
|
||||
linear-gradient(to right,rgba(0,212,255,0.7),rgba(0,212,255,0.7)) top right / 20px 1px no-repeat,
|
||||
linear-gradient(to bottom,rgba(0,212,255,0.7),rgba(0,212,255,0.7)) top right / 1px 20px no-repeat,
|
||||
linear-gradient(to right,rgba(0,212,255,0.7),rgba(0,212,255,0.7)) bottom left / 20px 1px no-repeat,
|
||||
linear-gradient(to bottom,rgba(0,212,255,0.7),rgba(0,212,255,0.7)) bottom left / 1px 20px no-repeat,
|
||||
linear-gradient(to right,rgba(0,212,255,0.7),rgba(0,212,255,0.7)) bottom right / 20px 1px no-repeat,
|
||||
linear-gradient(to bottom,rgba(0,212,255,0.7),rgba(0,212,255,0.7)) bottom right / 1px 20px no-repeat;
|
||||
}
|
||||
#netMapOverlay.nm-open{display:flex;animation:nmExplode 0.5s cubic-bezier(0.4,0,0.2,1) forwards}
|
||||
#netMapOverlay.nm-closing{animation:nmCollapse 0.32s cubic-bezier(0.4,0,0.2,1) forwards}
|
||||
@keyframes nmExplode{0%{transform:scale(0.04,0.06);opacity:0;clip-path:inset(0 100% 100% 0)}60%{opacity:1}100%{transform:scale(1,1);opacity:1;clip-path:inset(0 0% 0% 0)}}
|
||||
@keyframes nmCollapse{0%{transform:scale(1,1);opacity:1}100%{transform:scale(0.04,0.06);opacity:0}}
|
||||
#nmHeader{
|
||||
display:flex;align-items:center;justify-content:space-between;
|
||||
padding:8px 18px;flex-shrink:0;
|
||||
border-bottom:1px solid rgba(0,212,255,0.18);
|
||||
background:rgba(0,8,28,0.6);
|
||||
z-index:2;position:relative;
|
||||
}
|
||||
#nmTitle{font-family:var(--font-display);font-size:0.65rem;letter-spacing:4px;color:var(--cyan);display:flex;align-items:center;gap:12px}
|
||||
#nmTitle .nm-pulse{width:7px;height:7px;border-radius:50%;background:var(--cyan);box-shadow:0 0 8px var(--cyan);animation:corePulse 1.5s infinite}
|
||||
#nmStats{font-family:var(--font-mono);font-size:0.6rem;color:var(--text-dim);display:flex;gap:20px}
|
||||
#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.58rem;padding:4px 12px;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:18px;align-items:center;
|
||||
padding:6px 18px;flex-shrink:0;
|
||||
border-top:1px solid rgba(0,212,255,0.12);
|
||||
font-family:var(--font-mono);font-size:0.56rem;color:var(--text-dim);
|
||||
background:rgba(0,8,28,0.6);z-index:2;position:relative;
|
||||
}
|
||||
.nm-leg-dot{width:8px;height:8px;border-radius:50%;display:inline-block;margin-right:5px;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:8px 12px;font-family:var(--font-mono);font-size:0.62rem;
|
||||
display:none;min-width:160px;
|
||||
box-shadow:0 0 20px rgba(0,212,255,0.15);
|
||||
}
|
||||
#nmNodeInfo .ni-title{color:var(--cyan);font-size:0.65rem;letter-spacing:2px;margin-bottom:4px}
|
||||
#nmNodeInfo .ni-row{color:var(--text-dim);margin:2px 0}
|
||||
.text-cyan{color:var(--cyan)}
|
||||
.text-green{color:var(--green)}
|
||||
.text-orange{color:var(--orange)}
|
||||
@@ -868,6 +805,45 @@ body::after{
|
||||
.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:52px;left:0;width:min(820px,84vw);height:min(540px,74vh);
|
||||
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}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -1120,35 +1096,28 @@ body::after{
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- NETWORK MAP OVERLAY -->
|
||||
<div id="netMapOverlay">
|
||||
<div id="nmHeader">
|
||||
<div id="nmTitle">
|
||||
<div class="nm-pulse"></div>
|
||||
◈ NETWORK TOPOLOGY — LIVE FEED
|
||||
</div>
|
||||
<div id="nmStats">
|
||||
NODES <span id="nm-node-count">—</span> |
|
||||
ONLINE <span id="nm-online-count">—</span> |
|
||||
AGENTS <span id="nm-agent-count">—</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:14px">
|
||||
<div style="font-family:var(--font-mono);font-size:0.55rem;color:var(--text-dim);letter-spacing:1px">SAY "CLOSE MAP" TO DISMISS</div>
|
||||
<div id="nmTitle"><div class="nm-pulse"></div>◈ NETWORK TOPOLOGY — LIVE</div>
|
||||
<div id="nmStats">NODES <span id="nm-node-count">—</span> · ONLINE <span id="nm-online-count">—</span> · AGENTS <span id="nm-agent-count">—</span></div>
|
||||
<div style="display:flex;align-items:center;gap:12px">
|
||||
<span style="font-family:var(--font-mono);font-size:0.52rem;color:var(--text-dim);letter-spacing:1px">SAY "CLOSE MAP"</span>
|
||||
<button id="nmClose" onclick="closeNetMap()">✕ CLOSE</button>
|
||||
</div>
|
||||
</div>
|
||||
<canvas id="nmCanvas"></canvas>
|
||||
<div id="nmLegend">
|
||||
<span><span class="nm-leg-dot" style="background:#00d4ff;box-shadow:0 0 5px #00d4ff"></span>AGENT ONLINE</span>
|
||||
<span><span class="nm-leg-dot" style="background:#ff2244;box-shadow:0 0 5px #ff2244"></span>AGENT OFFLINE</span>
|
||||
<span><span class="nm-leg-dot" style="background:#00ff88;box-shadow:0 0 5px #00ff88"></span>PROXMOX</span>
|
||||
<span><span class="nm-leg-dot" style="background:#ffd700;box-shadow:0 0 5px #ffd700"></span>HA / AI</span>
|
||||
<span><span class="nm-leg-dot" style="background:rgba(0,180,200,0.4)"></span>DEVICE</span>
|
||||
<span style="margin-left:auto;opacity:0.5">CYAN FLOW = DATA IN · ORANGE FLOW = COMMANDS OUT</span>
|
||||
<span><span class="nm-leg-dot" style="background:#00ff88;box-shadow:0 0 4px #00ff88"></span>PROXMOX</span>
|
||||
<span><span class="nm-leg-dot" style="background:#ffd700;box-shadow:0 0 4px #ffd700"></span>SERVICES</span>
|
||||
<span><span class="nm-leg-dot" style="background:#00d4ff;box-shadow:0 0 4px #00d4ff"></span>AGENTS</span>
|
||||
<span><span class="nm-leg-dot" style="background:rgba(0,140,180,0.8)"></span>DEVICES</span>
|
||||
<span><span class="nm-leg-dot" style="background:#ff2244;box-shadow:0 0 4px #ff2244"></span>OFFLINE</span>
|
||||
<span style="margin-left:auto;opacity:0.4;font-size:0.5rem">CYAN = DATA IN · ORANGE = CMD OUT</span>
|
||||
</div>
|
||||
<div id="nmNodeInfo"><div class="ni-title" id="ni-name">—</div><div class="ni-row" id="ni-ip">—</div><div class="ni-row" id="ni-status">—</div><div class="ni-row" id="ni-type">—</div></div>
|
||||
<div id="nmNodeInfo"><div class="ni-title" id="ni-name">—</div><div class="ni-row" id="ni-ip"></div><div class="ni-row" id="ni-status"></div><div class="ni-row" id="ni-type"></div></div>
|
||||
</div>
|
||||
|
||||
<div id="sitesModal" style="position:fixed;inset:0;background:rgba(0,0,0,0.92);z-index:9999;display:none;align-items:flex-start;justify-content:center;padding:24px;overflow-y:auto">
|
||||
<div style="background:var(--panel-bg);border:1px solid var(--panel-border);width:100%;max-width:960px;font-family:var(--font-mono)">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;padding:16px 24px;border-bottom:1px solid var(--panel-border)">
|
||||
@@ -1743,306 +1712,230 @@ function _drawTopo() {
|
||||
tick();
|
||||
})();
|
||||
|
||||
// ── NETWORK MAP OVERLAY ───────────────────────────────────────────────
|
||||
let _nmNodes = [], _nmEdges = [], _nmParticles = [], _nmRaf = null, _nmT = 0;
|
||||
let _nmHoverNode = null;
|
||||
const NM_OPEN_RE = /\b(show|open|display|launch|pull\s*up|bring\s*up)\b.*\b(network\s*(map|topology|viz|visual|status|graph|diagram)|topology|node\s*map|connection\s*map)\b|\b(network\s*(map|topology|viz|visual|graph|diagram))\b/i;
|
||||
const NM_CLOSE_RE = /\b(close|hide|dismiss|exit|shut|collapse)\b.*\b(network|map|topology|overlay|viz)\b|\b(close\s*map|hide\s*map|dismiss\s*map)\b/i;
|
||||
|
||||
async function openNetMap() {
|
||||
const ov = document.getElementById('netMapOverlay');
|
||||
if (!ov) return;
|
||||
ov.classList.remove('nm-closing');
|
||||
ov.classList.add('nm-open');
|
||||
|
||||
// Fetch network data
|
||||
let devices = [];
|
||||
try { const n = await api('network'); devices = n.devices || []; } catch(_) {}
|
||||
|
||||
_buildNetGraph(devices);
|
||||
_startNetDraw();
|
||||
}
|
||||
|
||||
function closeNetMap() {
|
||||
const ov = document.getElementById('netMapOverlay');
|
||||
if (!ov) return;
|
||||
ov.classList.add('nm-closing');
|
||||
setTimeout(() => { ov.classList.remove('nm-open','nm-closing'); }, 350);
|
||||
if (_nmRaf) { cancelAnimationFrame(_nmRaf); _nmRaf = null; }
|
||||
}
|
||||
|
||||
|
||||
// Orbital ring definitions — each ring has its own radius ratio, speed, color, node cap
|
||||
const NM_RINGS = [
|
||||
{ name:'hub', label:'', rFrac:0, speed: 0, rgb:'0,212,255', nodeR:20, cap:1 },
|
||||
{ name:'proxmox', label:'PROXMOX', rFrac:0.18, speed: 0.006, rgb:'0,255,136', nodeR:13, cap:4 },
|
||||
{ name:'services', label:'SERVICES', rFrac:0.34, speed:-0.004, rgb:'255,215,0', nodeR:12, cap:5 },
|
||||
{ name:'agents', label:'AGENTS', rFrac:0.52, speed: 0.0025, rgb:'0,190,255', nodeR:10, cap:8 },
|
||||
{ name:'devices', label:'NETWORK', rFrac:0.70, speed:-0.0015, rgb:'0,140,180', nodeR: 7, cap:10 },
|
||||
// ── NETWORK MAP ──────────────────────────────────────────────────────────────
|
||||
var _nmNodes=[], _nmEdges=[], _nmParticles=[], _nmRaf=null, _nmT=0, _nmHoverNode=null;
|
||||
var _nmRot=[0,0,0,0,0];
|
||||
var NM_RINGS=[
|
||||
{name:'hub', label:'', rFrac:0, speed:0, rgb:'0,212,255', nodeR:20, cap:1 },
|
||||
{name:'proxmox', label:'PROXMOX', rFrac:0.18, speed:0.006, rgb:'0,255,136', nodeR:13, cap:4 },
|
||||
{name:'services',label:'SERVICES',rFrac:0.33, speed:-0.004, rgb:'255,215,0', nodeR:12, cap:5 },
|
||||
{name:'agents', label:'AGENTS', rFrac:0.50, speed:0.0025, rgb:'0,190,255', nodeR:10, cap:8 },
|
||||
{name:'devices', label:'NETWORK', rFrac:0.68, speed:-0.002, rgb:'0,140,180', nodeR:7, cap:9 },
|
||||
];
|
||||
var NM_OPEN_RE = /\b(show|open|display|launch|pull\s*up|bring\s*up)\b.*\b(network\s*(map|topology|viz|visual|graph)|topology|node\s*map)\b|\bnetwork\s*(map|topology|viz|visual|graph)\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 _classifyDevice(d) {
|
||||
const h = (d.name||'').toLowerCase();
|
||||
if (d.source !== 'agent') return 'devices';
|
||||
if (d.agent_type==='proxmox' || h.includes('pve') || h.includes('proxmox')) return 'proxmox';
|
||||
if (d.agent_type==='homeassistant' || h.includes('homeassist') || h.includes('_ha') ||
|
||||
h.includes('ollama') || h.includes('ai') || h.includes('fusion') || h.includes('pbx') ||
|
||||
h.includes('jellyfin') || h.includes('homebridge')) return 'services';
|
||||
function _nmClassify(d){
|
||||
var h=(d.name||'').toLowerCase();
|
||||
if(d.source!=='agent') return 'devices';
|
||||
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';
|
||||
}
|
||||
|
||||
function _nodeRgb(n) {
|
||||
if (n.ring === 'hub') return '0,212,255';
|
||||
if (n.ring === 'proxmox') return '0,255,136';
|
||||
if (n.ring === 'services') return '255,215,0';
|
||||
if (n.ring === 'agents') return '0,190,255';
|
||||
if (!n.online) return '255,50,80';
|
||||
return '0,140,180';
|
||||
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,140,180';
|
||||
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};
|
||||
}
|
||||
|
||||
// Ring rotation offsets (persist across redraws)
|
||||
const _nmRot = [0, 0, 0, 0, 0];
|
||||
|
||||
function _buildNetGraph(devices) {
|
||||
const ov = document.getElementById('netMapOverlay');
|
||||
const W = ov ? ov.clientWidth : 860;
|
||||
const H = (ov ? ov.clientHeight : 570) - 70; // minus header+legend
|
||||
const cx = W / 2, cy = H / 2;
|
||||
_nmNodes = []; _nmEdges = []; _nmParticles = [];
|
||||
const cx = W/2, cy = H/2, minDim = Math.min(cx, cy);
|
||||
|
||||
// Hub at center
|
||||
_nmNodes.push({ id:'jarvis', label:'JARVIS', sub:'165.22.1.228',
|
||||
type:'hub', online:true, agent:true, ring:'hub', angle:0, r:20, pulse:0, ringIdx:0 });
|
||||
|
||||
// Bucket into rings
|
||||
const buckets = { proxmox:[], services:[], agents:[], devices:[] };
|
||||
devices.forEach(d => buckets[_classifyDevice(d)].push(d));
|
||||
|
||||
['proxmox','services','agents','devices'].forEach((ringName, ri) => {
|
||||
const rd = NM_RINGS[ri + 1];
|
||||
const list = buckets[ringName].slice(0, rd.cap);
|
||||
list.forEach((d, i) => {
|
||||
const baseAngle = (ri % 2 === 0 ? -Math.PI/2 : -Math.PI/3);
|
||||
const angle = baseAngle + (i / Math.max(list.length, 1)) * Math.PI * 2;
|
||||
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:20,pulse:0});
|
||||
// Bucket
|
||||
var buckets={proxmox:[],services:[],agents:[],devices:[]};
|
||||
for(var i=0;i<devices.length;i++) buckets[_nmClassify(devices[i])].push(devices[i]);
|
||||
var rings=['proxmox','services','agents','devices'];
|
||||
for(var ri=0;ri<rings.length;ri++){
|
||||
var rname=rings[ri], rd=NM_RINGS[ri+1], list=buckets[rname].slice(0,rd.cap);
|
||||
for(var j=0;j<list.length;j++){
|
||||
var d=list[j];
|
||||
var baseA=(ri%2===0?-Math.PI/2:-Math.PI/3);
|
||||
var angle=baseA+(j/Math.max(list.length,1))*Math.PI*2;
|
||||
_nmNodes.push({
|
||||
id: d.agent_id || d.ip || (ringName+i),
|
||||
label: (d.name||d.ip||'?').replace(/_[a-f0-9]{6,}$/,'').substring(0,11),
|
||||
sub: d.ip||'', type: ringName,
|
||||
online: !!(d.alive||d.status==='online'),
|
||||
agent: d.source==='agent',
|
||||
ring: ringName, ringIdx: ri+1,
|
||||
angle, r: rd.nodeR, pulse: Math.random()*Math.PI*2,
|
||||
id:d.agent_id||d.ip||(rname+j),
|
||||
label:(d.name||d.ip||'?').replace(/_[a-f0-9]{6,}$/,'').substring(0,11),
|
||||
sub:d.ip||'', online:!!(d.alive||d.status==='online'),
|
||||
agent:d.source==='agent', ringIdx:ri+1, angle:angle,
|
||||
r:rd.nodeR, pulse:Math.random()*Math.PI*2,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Edges: each node → hub
|
||||
for (let i = 1; i < _nmNodes.length; i++) {
|
||||
const n = _nmNodes[i];
|
||||
_nmEdges.push({ from:i, to:0, strength: n.agent ? 0.9 : 0.45 });
|
||||
}
|
||||
}
|
||||
// Edges + particles
|
||||
for(var i=1;i<_nmNodes.length;i++){
|
||||
var n=_nmNodes[i], str=n.agent?0.9:0.45;
|
||||
_nmEdges.push({from:i,to:0,strength:str});
|
||||
if(n.online){
|
||||
var cnt=n.agent?3:2;
|
||||
for(var p=0;p<cnt;p++) _nmParticles.push({edge:_nmEdges.length-1,t:Math.random(),dir:'in',speed:0.002+Math.random()*0.0025,r:1.8+Math.random()*0.9});
|
||||
if(n.agent&&Math.random()>0.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});
|
||||
}
|
||||
}
|
||||
|
||||
// Particles
|
||||
_nmEdges.forEach((e, ei) => {
|
||||
const n = _nmNodes[e.from];
|
||||
const cnt = n.online ? (n.agent ? 3 : 2) : 0;
|
||||
for (let p = 0; p < cnt; p++)
|
||||
_nmParticles.push({ edge:ei, t:Math.random(), dir:'in',
|
||||
speed:0.002+Math.random()*0.0025, r:1.8+Math.random()*0.9 });
|
||||
if (n.online && n.agent && Math.random()>0.45)
|
||||
_nmParticles.push({ edge:ei, t:Math.random(), dir:'out',
|
||||
speed:0.0013+Math.random()*0.0017, r:1.5+Math.random()*0.7 });
|
||||
});
|
||||
|
||||
// Stats
|
||||
const online = _nmNodes.filter(n=>n.online).length;
|
||||
const agt = _nmNodes.filter(n=>n.agent).length;
|
||||
const g = id => document.getElementById(id);
|
||||
if(g('nm-node-count')) g('nm-node-count').textContent = _nmNodes.length;
|
||||
if(g('nm-online-count')) g('nm-online-count').textContent = online;
|
||||
if(g('nm-agent-count')) g('nm-agent-count').textContent = agt;
|
||||
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-62 : 478;
|
||||
var W=canvas.width, H=canvas.height, cx=W/2, cy=H/2, minR=Math.min(cx,cy);
|
||||
var ctx=canvas.getContext('2d');
|
||||
|
||||
function _startNetDraw() {
|
||||
if (_nmRaf) cancelAnimationFrame(_nmRaf);
|
||||
const canvas = document.getElementById('nmCanvas');
|
||||
if (!canvas) return;
|
||||
const ov = document.getElementById('netMapOverlay');
|
||||
canvas.width = ov ? ov.clientWidth : 860;
|
||||
canvas.height = (ov ? ov.clientHeight : 570) - 72;
|
||||
const W = canvas.width, H = canvas.height;
|
||||
const cx = W/2, cy = H/2, minDim = Math.min(cx, cy);
|
||||
function frame(){
|
||||
if(!document.getElementById('netMapOverlay').classList.contains('nm-open')) return;
|
||||
_nmRaf=requestAnimationFrame(frame);
|
||||
_nmT+=0.016;
|
||||
for(var i=0;i<NM_RINGS.length;i++) _nmRot[i]=(_nmRot[i]||0)+NM_RINGS[i].speed;
|
||||
ctx.clearRect(0,0,W,H);
|
||||
|
||||
function nodePos(n) {
|
||||
if (n.ring === 'hub') return { x:cx, y:cy };
|
||||
const rd = NM_RINGS[n.ringIdx];
|
||||
const rot = _nmRot[n.ringIdx] || 0;
|
||||
const r = minDim * rd.rFrac;
|
||||
return { x: cx + Math.cos(n.angle+rot)*r, y: cy + Math.sin(n.angle+rot)*r };
|
||||
}
|
||||
// Dot grid
|
||||
ctx.fillStyle='rgba(0,180,255,0.05)';
|
||||
for(var gx=26;gx<W;gx+=38) for(var gy=26;gy<H;gy+=38){ctx.beginPath();ctx.arc(gx,gy,0.7,0,Math.PI*2);ctx.fill();}
|
||||
|
||||
function draw() {
|
||||
if (!document.getElementById('netMapOverlay')?.classList.contains('nm-open')) return;
|
||||
_nmRaf = requestAnimationFrame(draw);
|
||||
_nmT += 0.016;
|
||||
NM_RINGS.forEach((rd,i) => { _nmRot[i] = (_nmRot[i]||0) + rd.speed; });
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.clearRect(0, 0, W, H);
|
||||
|
||||
// Subtle dot grid
|
||||
ctx.fillStyle = 'rgba(0,180,255,0.055)';
|
||||
for (let x = 26; x < W; x += 40) for (let y = 26; y < H; y += 40) {
|
||||
ctx.beginPath(); ctx.arc(x,y,0.7,0,Math.PI*2); ctx.fill();
|
||||
// Ring tracks
|
||||
for(var ri=1;ri<NM_RINGS.length;ri++){
|
||||
var rd=NM_RINGS[ri], r=minR*rd.rFrac;
|
||||
var hasOn=false;
|
||||
for(var i=0;i<_nmNodes.length;i++) if(_nmNodes[i].ringIdx===ri&&_nmNodes[i].online){hasOn=true;break;}
|
||||
ctx.beginPath(); ctx.arc(cx,cy,r,0,Math.PI*2);
|
||||
ctx.strokeStyle=hasOn?'rgba('+rd.rgb+',0.12)':'rgba(60,60,80,0.08)';
|
||||
ctx.lineWidth=0.8; ctx.setLineDash([3,9]); ctx.stroke(); ctx.setLineDash([]);
|
||||
// Ticks
|
||||
for(var a=0;a<Math.PI*2;a+=Math.PI/6){
|
||||
ctx.beginPath();ctx.moveTo(cx+Math.cos(a)*(r-3),cy+Math.sin(a)*(r-3));ctx.lineTo(cx+Math.cos(a)*(r+3),cy+Math.sin(a)*(r+3));
|
||||
ctx.strokeStyle='rgba('+rd.rgb+',0.18)';ctx.lineWidth=0.6;ctx.stroke();
|
||||
}
|
||||
// Label at 3-o'clock
|
||||
var zOn=0,zTot=0;
|
||||
for(var i=0;i<_nmNodes.length;i++){if(_nmNodes[i].ringIdx===ri){zTot++;if(_nmNodes[i].online)zOn++;}}
|
||||
ctx.font='700 8px Share Tech Mono,monospace'; ctx.textAlign='left';
|
||||
ctx.fillStyle='rgba('+rd.rgb+',0.4)'; ctx.fillText(rd.label,cx+r+5,cy+2);
|
||||
if(zTot>0){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';
|
||||
}
|
||||
|
||||
// Orbital ring tracks
|
||||
NM_RINGS.slice(1).forEach((rd, i) => {
|
||||
const ri = i+1, r = minDim * rd.rFrac;
|
||||
const hasOn = _nmNodes.some(n=>n.ringIdx===ri&&n.online);
|
||||
ctx.beginPath(); ctx.arc(cx,cy,r,0,Math.PI*2);
|
||||
ctx.strokeStyle = hasOn ? `rgba(${rd.rgb},0.12)` : 'rgba(60,60,80,0.08)';
|
||||
ctx.lineWidth=0.8; ctx.setLineDash([3,9]); ctx.stroke(); ctx.setLineDash([]);
|
||||
// Compute node positions
|
||||
var pos=[];
|
||||
for(var i=0;i<_nmNodes.length;i++) pos.push(_nmNodePos(_nmNodes[i],W,H));
|
||||
|
||||
// Tick marks every 30°
|
||||
for (let a=0; a<Math.PI*2; a+=Math.PI/6) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(cx+Math.cos(a)*(r-3), cy+Math.sin(a)*(r-3));
|
||||
ctx.lineTo(cx+Math.cos(a)*(r+3), cy+Math.sin(a)*(r+3));
|
||||
ctx.strokeStyle=`rgba(${rd.rgb},0.18)`; ctx.lineWidth=0.6; ctx.stroke();
|
||||
}
|
||||
|
||||
// Ring label + count at 3 o'clock
|
||||
const zOn = _nmNodes.filter(n=>n.ringIdx===ri&&n.online).length;
|
||||
const zTot = _nmNodes.filter(n=>n.ringIdx===ri).length;
|
||||
ctx.font='700 8px Share Tech Mono,monospace'; ctx.textAlign='left';
|
||||
ctx.fillStyle=`rgba(${rd.rgb},0.42)`;
|
||||
ctx.fillText(rd.label, cx+r+6, cy+2);
|
||||
if (zTot>0) {
|
||||
ctx.font='6.5px Share Tech Mono,monospace';
|
||||
ctx.fillStyle=`rgba(${rd.rgb},0.28)`;
|
||||
ctx.fillText(`${zOn}/${zTot}`, cx+r+6, cy+11);
|
||||
}
|
||||
ctx.textAlign='left';
|
||||
});
|
||||
|
||||
// Live node positions for this frame
|
||||
const pos = _nmNodes.map(n=>nodePos(n));
|
||||
|
||||
// Spoke lines
|
||||
_nmEdges.forEach(e => {
|
||||
const pa=pos[e.from], pb=pos[e.to]; if(!pa||!pb) return;
|
||||
const n=_nmNodes[e.from], rgb=_nodeRgb(n);
|
||||
ctx.beginPath(); ctx.moveTo(pa.x,pa.y); ctx.lineTo(pb.x,pb.y);
|
||||
ctx.strokeStyle = n.online ? `rgba(${rgb},0.1)` : 'rgba(80,20,30,0.07)';
|
||||
// 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);
|
||||
ctx.beginPath();ctx.moveTo(pa.x,pa.y);ctx.lineTo(pb.x,pb.y);
|
||||
ctx.strokeStyle=n.online?'rgba('+rgb+',0.1)':'rgba(80,20,30,0.07)';
|
||||
ctx.lineWidth=e.strength*0.9; ctx.stroke();
|
||||
});
|
||||
}
|
||||
|
||||
// Particles
|
||||
_nmParticles.forEach(p => {
|
||||
p.t=(p.t+p.speed)%1;
|
||||
const e=_nmEdges[p.edge]; if(!e) return;
|
||||
const pa=pos[e.from], pb=pos[e.to]; if(!pa||!pb) return;
|
||||
if(!_nmNodes[e.from]?.online) return;
|
||||
const t = p.dir==='in' ? p.t : 1-p.t;
|
||||
const px=pa.x+(pb.x-pa.x)*t, py=pa.y+(pb.y-pa.y)*t;
|
||||
const 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})`;
|
||||
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})`;
|
||||
ctx.shadowColor='rgba(255,110,0,0.6)';
|
||||
}
|
||||
ctx.shadowBlur=6; ctx.fill(); ctx.shadowBlur=0;
|
||||
});
|
||||
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;
|
||||
}
|
||||
|
||||
// Nodes
|
||||
_nmNodes.forEach((n,ni) => {
|
||||
const p=pos[ni], rgb=_nodeRgb(n);
|
||||
const pulse=0.5+Math.sin(_nmT*1.5+n.pulse)*0.3;
|
||||
const isHub=n.ring==='hub', isHov=_nmHoverNode===ni;
|
||||
const baseR=n.r+(isHov?5:0);
|
||||
|
||||
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.5+n.pulse)*0.3;
|
||||
var isHub=n.ringIdx===0, isHov=_nmHoverNode===ni, baseR=n.r+(isHov?5:0);
|
||||
// Glow
|
||||
if(n.online){
|
||||
const gr=baseR+10+Math.sin(_nmT*1.2+n.pulse)*3;
|
||||
const grd=ctx.createRadialGradient(p.x,p.y,baseR*0.3,p.x,p.y,gr);
|
||||
grd.addColorStop(0,`rgba(${rgb},${pulse*0.22})`); grd.addColorStop(1,`rgba(${rgb},0)`);
|
||||
ctx.beginPath(); ctx.arc(p.x,p.y,gr,0,Math.PI*2); ctx.fillStyle=grd; ctx.fill();
|
||||
var gr=baseR+10+Math.sin(_nmT+n.pulse)*3;
|
||||
var g=ctx.createRadialGradient(p.x,p.y,baseR*0.3,p.x,p.y,gr);
|
||||
g.addColorStop(0,'rgba('+rgb+','+(pulse*0.22).toFixed(3)+')');g.addColorStop(1,'rgba('+rgb+',0)');
|
||||
ctx.beginPath();ctx.arc(p.x,p.y,gr,0,Math.PI*2);ctx.fillStyle=g;ctx.fill();
|
||||
// Rotating arc
|
||||
var arcSpd=isHub?0.6:0.38,arcLen=isHub?Math.PI*1.5:Math.PI*0.85;
|
||||
ctx.beginPath();ctx.arc(p.x,p.y,baseR+4,_nmT*arcSpd,_nmT*arcSpd+arcLen);
|
||||
ctx.strokeStyle='rgba('+rgb+','+(0.2+pulse*0.15).toFixed(3)+')';ctx.lineWidth=isHub?1.5:0.8;ctx.stroke();
|
||||
if(isHub){ctx.beginPath();ctx.arc(p.x,p.y,baseR+8,-_nmT*0.35,-_nmT*0.35+Math.PI*1.1);ctx.strokeStyle='rgba('+rgb+',0.12)';ctx.lineWidth=0.6;ctx.stroke();}
|
||||
}
|
||||
|
||||
// Rotating arc decoration
|
||||
if(n.online){
|
||||
const arcSpd=isHub?0.6:0.38, arcLen=isHub?Math.PI*1.5:Math.PI*0.85;
|
||||
ctx.beginPath(); ctx.arc(p.x,p.y,baseR+4,_nmT*arcSpd,_nmT*arcSpd+arcLen);
|
||||
ctx.strokeStyle=`rgba(${rgb},${0.2+pulse*0.15})`; ctx.lineWidth=isHub?1.5:0.8; ctx.stroke();
|
||||
if(isHub){
|
||||
ctx.beginPath(); ctx.arc(p.x,p.y,baseR+8,-_nmT*0.35,-_nmT*0.35+Math.PI*1.1);
|
||||
ctx.strokeStyle=`rgba(${rgb},0.12)`; ctx.lineWidth=0.6; ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
// Fill + border
|
||||
const fg=ctx.createRadialGradient(p.x,p.y,0,p.x,p.y,baseR);
|
||||
const fa=n.online?pulse*0.4:0.09;
|
||||
fg.addColorStop(0,`rgba(${rgb},${fa})`); fg.addColorStop(1,`rgba(${rgb},${fa*0.2})`);
|
||||
ctx.beginPath(); ctx.arc(p.x,p.y,baseR,0,Math.PI*2);
|
||||
ctx.fillStyle=fg; ctx.fill();
|
||||
ctx.strokeStyle=`rgba(${rgb},${n.online?0.58+pulse*0.3:0.18})`; ctx.lineWidth=isHub?2:1; ctx.stroke();
|
||||
|
||||
var fg=ctx.createRadialGradient(p.x,p.y,0,p.x,p.y,baseR);
|
||||
var fa=n.online?pulse*0.4:0.09;
|
||||
fg.addColorStop(0,'rgba('+rgb+','+fa.toFixed(3)+')');fg.addColorStop(1,'rgba('+rgb+','+(fa*0.2).toFixed(3)+')');
|
||||
ctx.beginPath();ctx.arc(p.x,p.y,baseR,0,Math.PI*2);ctx.fillStyle=fg;ctx.fill();
|
||||
ctx.strokeStyle='rgba('+rgb+','+(n.online?0.58+pulse*0.3:0.18).toFixed(3)+')';ctx.lineWidth=isHub?2:1;ctx.stroke();
|
||||
// Hub crosshairs
|
||||
if(isHub){
|
||||
ctx.strokeStyle=`rgba(${rgb},0.2)`; ctx.lineWidth=0.6;
|
||||
[[p.x-44,p.y,p.x-baseR-3,p.y],[p.x+baseR+3,p.y,p.x+44,p.y],
|
||||
[p.x,p.y-44,p.x,p.y-baseR-3],[p.x,p.y+baseR+3,p.x,p.y+44]
|
||||
].forEach(([x1,y1,x2,y2])=>{ ctx.beginPath();ctx.moveTo(x1,y1);ctx.lineTo(x2,y2);ctx.stroke(); });
|
||||
ctx.strokeStyle='rgba('+rgb+',0.2)';ctx.lineWidth=0.6;
|
||||
var lines=[[p.x-42,p.y,p.x-baseR-3,p.y],[p.x+baseR+3,p.y,p.x+42,p.y],[p.x,p.y-42,p.x,p.y-baseR-3],[p.x,p.y+baseR+3,p.x,p.y+42]];
|
||||
for(var li=0;li<lines.length;li++){ctx.beginPath();ctx.moveTo(lines[li][0],lines[li][1]);ctx.lineTo(lines[li][2],lines[li][3]);ctx.stroke();}
|
||||
}
|
||||
|
||||
// Tiny status dot on non-hub nodes
|
||||
if(!isHub){
|
||||
ctx.beginPath(); ctx.arc(p.x+baseR*0.65,p.y-baseR*0.65,2.2,0,Math.PI*2);
|
||||
ctx.fillStyle=n.online?'rgba(0,255,120,0.9)':'rgba(255,50,80,0.9)'; ctx.fill();
|
||||
}
|
||||
|
||||
// Label — outward from center
|
||||
const labelAngle=isHub?-Math.PI/2:Math.atan2(p.y-cy,p.x-cx);
|
||||
const lDist=baseR+(isHub?14:10);
|
||||
const lx=p.x+Math.cos(labelAngle)*lDist, ly=p.y+Math.sin(labelAngle)*lDist+3;
|
||||
ctx.font=`${isHub?'700 10':'7.5'}px Share Tech Mono,monospace`;
|
||||
const ca=Math.cos(labelAngle);
|
||||
ctx.textAlign=isHub?'center': ca>0.2?'left': ca<-0.2?'right':'center';
|
||||
ctx.fillStyle=n.online?`rgba(${rgb},0.92)`:'rgba(200,80,80,0.65)';
|
||||
// Status dot
|
||||
if(!isHub){ctx.beginPath();ctx.arc(p.x+baseR*0.65,p.y-baseR*0.65,2.2,0,Math.PI*2);ctx.fillStyle=n.online?'rgba(0,255,120,0.9)':'rgba(255,50,80,0.9)';ctx.fill();}
|
||||
// Label outward
|
||||
var la=isHub?-Math.PI/2:Math.atan2(p.y-cy,p.x-cx);
|
||||
var ld=baseR+(isHub?14:10);
|
||||
var lx=p.x+Math.cos(la)*ld, ly=p.y+Math.sin(la)*ld+3;
|
||||
ctx.font=(isHub?'700 10':'7.5')+'px Share Tech Mono,monospace';
|
||||
var ca=Math.cos(la);
|
||||
ctx.textAlign=isHub?'center':ca>0.2?'left':ca<-0.2?'right':'center';
|
||||
ctx.fillStyle=n.online?'rgba('+rgb+',0.92)':'rgba(200,80,80,0.65)';
|
||||
ctx.fillText(n.label,lx,ly);
|
||||
if(n.sub&&(isHub||isHov)){
|
||||
ctx.font='6px Share Tech Mono,monospace';
|
||||
ctx.fillStyle='rgba(130,185,205,0.5)';
|
||||
ctx.fillText(n.sub,lx,ly+10);
|
||||
}
|
||||
if(n.sub&&(isHub||isHov)){ctx.font='6px Share Tech Mono,monospace';ctx.fillStyle='rgba(130,185,205,0.5)';ctx.fillText(n.sub,lx,ly+10);}
|
||||
ctx.textAlign='left';
|
||||
});
|
||||
}
|
||||
}
|
||||
draw();
|
||||
frame();
|
||||
|
||||
canvas.onmousemove=function(e){
|
||||
const r=canvas.getBoundingClientRect(), mx=e.clientX-r.left, my=e.clientY-r.top;
|
||||
const positions=_nmNodes.map(n=>nodePos(n));
|
||||
let found=-1;
|
||||
positions.forEach((p,i)=>{ if(Math.hypot(p.x-mx,p.y-my)<_nmNodes[i].r+10) found=i; });
|
||||
var rect=canvas.getBoundingClientRect(), mx=e.clientX-rect.left, my=e.clientY-rect.top;
|
||||
var found=-1;
|
||||
for(var i=0;i<_nmNodes.length;i++){var p=_nmNodePos(_nmNodes[i],W,H);if(Math.sqrt((p.x-mx)*(p.x-mx)+(p.y-my)*(p.y-my))<_nmNodes[i].r+10){found=i;break;}}
|
||||
_nmHoverNode=found>=0?found:null;
|
||||
const info=document.getElementById('nmNodeInfo'); if(!info) return;
|
||||
var info=document.getElementById('nmNodeInfo'); if(!info) return;
|
||||
if(found>=0){
|
||||
const n=_nmNodes[found], rgb=_nodeRgb(n);
|
||||
document.getElementById('ni-name').textContent=n.label;
|
||||
document.getElementById('ni-name').style.color=`rgb(${rgb})`;
|
||||
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='TYPE: '+n.ring.toUpperCase();
|
||||
info.style.display='block'; info.style.left=(mx+16)+'px'; info.style.top=(my-6)+'px';
|
||||
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=()=>{ _nmHoverNode=null; const i=document.getElementById('nmNodeInfo'); if(i) i.style.display='none'; };
|
||||
canvas.onmouseleave=function(){_nmHoverNode=null;var i=document.getElementById('nmNodeInfo');if(i)i.style.display='none';};
|
||||
}
|
||||
|
||||
// ── GLOBALS ──────────────────────────────────────────────────────────
|
||||
@@ -2974,33 +2867,23 @@ async function sendMessage() {
|
||||
const text = input.value.trim();
|
||||
if (!text) return;
|
||||
|
||||
// Local commands — handled client-side without API round-trip
|
||||
const t = text.toLowerCase();
|
||||
|
||||
// Network map open
|
||||
if (NM_OPEN_RE.test(t)) {
|
||||
input.value = '';
|
||||
addMessage('user', text);
|
||||
addMessage('jarvis', 'Launching network topology display. Stand by.');
|
||||
// Local commands — no API round-trip
|
||||
var t2 = text.toLowerCase();
|
||||
if (NM_OPEN_RE.test(t2)) {
|
||||
input.value=''; addMessage('user',text);
|
||||
addMessage('jarvis','Launching network topology display.');
|
||||
speak('Launching network topology display.');
|
||||
openNetMap();
|
||||
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;
|
||||
}
|
||||
// Network map close
|
||||
if (NM_CLOSE_RE.test(t)) {
|
||||
input.value = '';
|
||||
addMessage('user', text);
|
||||
const isOpen = document.getElementById('netMapOverlay')?.classList.contains('nm-open');
|
||||
if (isOpen) {
|
||||
closeNetMap();
|
||||
addMessage('jarvis', 'Network topology display closed.');
|
||||
speak('Network topology display closed.');
|
||||
} else {
|
||||
addMessage('jarvis', 'Network map is not currently active.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Local panel-toggle voice commands (handled without API call)
|
||||
const t = text.toLowerCase();
|
||||
if (/\b(focus\s*mode|hide\s*(panels?|stats?|statistics)|full\s*screen\s*jarvis)\b/.test(t)) {
|
||||
input.value = '';
|
||||
addMessage('user', text);
|
||||
|
||||
Reference in New Issue
Block a user