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:
2026-06-02 02:20:07 +00:00
parent 57d5d7f51e
commit afff54e43b
+231 -348
View File
@@ -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> &nbsp;|&nbsp;
ONLINE <span id="nm-online-count"></span> &nbsp;|&nbsp;
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> &nbsp;·&nbsp; ONLINE <span id="nm-online-count"></span> &nbsp;·&nbsp; 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 &nbsp;·&nbsp; 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 = [
// ── 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.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 },
{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();
function _nmClassify(d){
var 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';
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';
function _nmRgb(n){
if(!n.online) return '255,50,80';
return '0,140,180';
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;
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=[];
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;
// 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),
id:d.agent_id||d.ip||(rname+j),
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,
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 });
}
// 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 });
});
}
// 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});
}
}
// 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 _startNetDraw() {
function _nmDraw(){
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);
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 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 };
}
function draw() {
if (!document.getElementById('netMapOverlay')?.classList.contains('nm-open')) return;
_nmRaf = requestAnimationFrame(draw);
function frame(){
if(!document.getElementById('netMapOverlay').classList.contains('nm-open')) return;
_nmRaf=requestAnimationFrame(frame);
_nmT+=0.016;
NM_RINGS.forEach((rd,i) => { _nmRot[i] = (_nmRot[i]||0) + rd.speed; });
const ctx = canvas.getContext('2d');
for(var i=0;i<NM_RINGS.length;i++) _nmRot[i]=(_nmRot[i]||0)+NM_RINGS[i].speed;
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();
}
// 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();}
// 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);
// 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.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([]);
// 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();
// 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();
}
// 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;
// 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.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.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';
});
}
// Live node positions for this frame
const pos = _nmNodes.map(n=>nodePos(n));
// Compute node positions
var pos=[];
for(var i=0;i<_nmNodes.length;i++) pos.push(_nmNodePos(_nmNodes[i],W,H));
// 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);
// 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.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);
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})`;
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)';
}
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();
}
// Rotating arc decoration
if(n.online){
const arcSpd=isHub?0.6:0.38, arcLen=isHub?Math.PI*1.5:Math.PI*0.85;
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})`; 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();
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();}
}
}
// 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);
// 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.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);