Modularize JARVIS frontend into separate CSS/JS files

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 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 02:55:35 +00:00
parent 9f92e4d5e4
commit 462ce257a8
6 changed files with 4950 additions and 4954 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+590
View File
@@ -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();
})();
+357
View File
@@ -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;i<devices.length;i++) buckets[_nmClassify(devices[i])].push(devices[i]);
// Sort netscan devices: online first, then those with meaningful hostnames
buckets.network.sort(function(a,b){
var sa=a.alive?1:0, sb=b.alive?1:0;
if(sb!==sa) return sb-sa;
var ha=(a.name&&a.name!==a.ip&&a.name.indexOf('10.48')!==0)?1:0;
var hb=(b.name&&b.name!==b.ip&&b.name.indexOf('10.48')!==0)?1:0;
return hb-ha;
});
var rings=['proxmox','services','agents','devices','network'];
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||(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 + 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
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;i<NM_RINGS.length;i++) _nmRot[i]=(_nmRot[i]||0)+NM_RINGS[i].speed;
ctx.clearRect(0,0,W,H);
// 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();}
// 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';
}
// 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<hlines.length;li++){ctx.beginPath();ctx.moveTo(hlines[li][0],hlines[li][1]);ctx.lineTo(hlines[li][2],hlines[li][3]);ctx.stroke();}
}
// Status dot
if(!isHub){
ctx.beginPath();ctx.arc(px+baseR*0.62,py-baseR*0.62,2.5,0,Math.PI*2);
ctx.fillStyle=n.online?'rgba(0,255,120,0.95)':'rgba(255,50,80,0.95)';ctx.fill();
}
// Label — centered below bubble
var lblY=py+baseR+11;
ctx.font=(isHub?'700 11':'8')+'px Share Tech Mono,monospace';
ctx.textAlign='center';
ctx.fillStyle=n.online?'rgba('+rgb+',0.95)':'rgba(220,90,90,0.7)';
ctx.fillText(n.label,px,lblY);
if(n.sub&&(isHub||isHov)){
ctx.font='6.5px Share Tech Mono,monospace';
ctx.fillStyle='rgba(140,195,215,0.55)';
ctx.fillText(n.sub,px,lblY+10);
}
ctx.textAlign='left';
}
}
frame();
canvas.onmousemove=function(e){
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;
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';};
}
File diff suppressed because it is too large Load Diff