diff --git a/public_html/index.html b/public_html/index.html
index 6fc2a64..0e44042 100644
--- a/public_html/index.html
+++ b/public_html/index.html
@@ -82,9 +82,64 @@ body::after{
/* ── PANEL FLOAT + GLOW ───────────────────────────────────────────── */
@keyframes panelFloat{
- 0%,100%{transform:translateY(0px);box-shadow:0 4px 20px rgba(0,0,0,0.4),0 0 0px rgba(0,212,255,0)}
- 50%{transform:translateY(-7px);box-shadow:0 16px 40px rgba(0,0,0,0.5),0 0 30px rgba(0,212,255,0.06),0 0 60px rgba(0,212,255,0.02)}
+ 0%,100%{transform:translateY(var(--pty,0px)) rotateX(var(--prx,0deg)) rotateY(var(--pry,0deg));box-shadow:0 4px 20px rgba(0,0,0,0.4),0 0 0px rgba(0,212,255,0)}
+ 50%{transform:translateY(calc(var(--pty,0px) - 7px)) rotateX(var(--prx,0deg)) rotateY(var(--pry,0deg));box-shadow:0 16px 40px rgba(0,0,0,0.5),0 0 30px rgba(0,212,255,0.06),0 0 60px rgba(0,212,255,0.02)}
}
+/* Panel flash when data updates */
+@keyframes panelFlash{0%{box-shadow:0 0 0 1px rgba(0,212,255,0.8),0 0 20px rgba(0,212,255,0.3)}100%{box-shadow:none}}
+.panel.data-flash{animation:panelFlash 0.5s ease-out,panelFloat 7s ease-in-out infinite}
+
+/* Alert pulse — red ambient glow on body when alerts active */
+@keyframes alertPulse{0%,100%{opacity:0}50%{opacity:1}}
+#alertOverlay{position:fixed;inset:0;pointer-events:none;z-index:1;background:radial-gradient(ellipse at 50% 50%,rgba(255,30,60,0.07) 0%,transparent 70%);animation:alertPulse 3s ease-in-out infinite;display:none}
+
+/* Glitch keyframes */
+@keyframes glitch1{
+ 0%,100%{clip-path:inset(0 0 100% 0);transform:translate(0)}
+ 10%{clip-path:inset(10% 0 60% 0);transform:translate(-3px,1px)}
+ 20%{clip-path:inset(40% 0 30% 0);transform:translate(3px,-1px)}
+ 30%{clip-path:inset(70% 0 10% 0);transform:translate(-2px,2px)}
+ 40%{clip-path:inset(0 0 100% 0);transform:translate(0)}
+}
+@keyframes glitch2{
+ 0%,100%{clip-path:inset(0 0 100% 0);transform:translate(0)}
+ 10%{clip-path:inset(60% 0 10% 0);transform:translate(3px,-1px)}
+ 25%{clip-path:inset(20% 0 50% 0);transform:translate(-3px,1px)}
+ 35%{clip-path:inset(80% 0 5% 0);transform:translate(2px,2px)}
+ 45%{clip-path:inset(0 0 100% 0);transform:translate(0)}
+}
+.tb-logo-text{position:relative;display:inline-block}
+.tb-logo-text::before,.tb-logo-text::after{
+ content:attr(data-text);position:absolute;top:0;left:0;
+ color:var(--cyan);font-family:var(--font-display);font-size:inherit;font-weight:inherit;letter-spacing:inherit;
+ pointer-events:none;opacity:0;
+}
+.tb-logo-text::before{color:rgba(255,0,80,0.8);text-shadow:2px 0 rgba(255,0,80,0.5)}
+.tb-logo-text::after{color:rgba(0,255,255,0.8);text-shadow:-2px 0 rgba(0,255,255,0.5)}
+.tb-logo-text.glitching::before{animation:glitch1 0.25s steps(1) forwards;opacity:1}
+.tb-logo-text.glitching::after{animation:glitch2 0.25s steps(1) forwards;opacity:1}
+
+/* Metric bar shimmer */
+@keyframes barShimmer{0%{transform:translateX(-100%)}100%{transform:translateX(400%)}}
+.metric-bar-fill::after{
+ content:'';position:absolute;top:0;left:0;width:30%;height:100%;
+ background:linear-gradient(90deg,transparent,rgba(255,255,255,0.25),transparent);
+ animation:barShimmer 2.5s ease-in-out infinite;
+ border-radius:inherit;
+}
+.metric-bar-fill{position:relative;overflow:hidden}
+
+/* Panel hover rise */
+.panel:hover{
+ transform:translateY(calc(var(--pty,0px) - 11px)) !important;
+ border-color:rgba(0,212,255,0.45) !important;
+ box-shadow:0 20px 50px rgba(0,0,0,0.55),0 0 40px rgba(0,212,255,0.1) !important;
+ transition:transform 0.3s ease,border-color 0.3s ease,box-shadow 0.3s ease;
+}
+
+/* Sparkline canvas */
+.sparkline-wrap{margin:4px 0 8px;height:32px;position:relative}
+.sparkline-wrap canvas{display:block;width:100%;height:32px}
/* ── HUD CORNER BRACKETS ──────────────────────────────────────────── */
/* ── MINI ARC REACTOR ─────────────────────────────────────────────── */
@@ -197,6 +252,7 @@ body::after{
grid-template-rows:1fr;
gap:10px;padding:10px;
overflow:hidden;
+ perspective:1200px;
transition:grid-template-columns 0.45s cubic-bezier(0.4,0,0.2,1);
}
#mainLayout.focus-mode{grid-template-columns:0px 1fr 0px}
@@ -678,6 +734,7 @@ body::after{
+
@@ -705,7 +762,7 @@ body::after{
- JARVIS SYSTEM
+
JARVIS SYSTEM
LOCAL --% CPU
@@ -735,18 +792,21 @@ body::after{
JARVIS SERVER
165.22.1.228
-
+
@@ -1015,6 +1075,131 @@ document.addEventListener('DOMContentLoaded', () => {
});
});
+// ── 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) return;
+ ov.style.display = hasAlerts ? 'block' : 'none';
+}
+
+// ── 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);
+ // Schedule next glitch: 35-60s random interval
+ setTimeout(triggerGlitch, 35000 + Math.random() * 25000);
+ }
+ // First glitch after 20s
+ setTimeout(triggerGlitch, 20000);
+})();
+
// ── GLOBALS ──────────────────────────────────────────────────────────
let sessionToken = '';
let sessionUser = '';
@@ -1382,6 +1567,17 @@ function renderSystem(s) {
tickTo('cpu-val', cpu);
tickTo('mem-val', mem);
tickTo('disk-val', disk);
+
+ // Sparklines
+ pushSparkData('cpu', cpu);
+ pushSparkData('mem', mem);
+ pushSparkData('disk', disk);
+ drawSparkline('spark-cpu', _sparkData.cpu, 'rgb(0,212,255)');
+ drawSparkline('spark-mem', _sparkData.mem, 'rgb(0,255,136)');
+ drawSparkline('spark-disk', _sparkData.disk, 'rgb(255,166,0)');
+
+ // Flash the system panel on data arrival
+ flashPanel(document.querySelector('#leftPanel .panel'));
document.getElementById('uptime-val').textContent = s.uptime || '--';
document.getElementById('load-val').textContent = s.load?.['1m'] || '--';
document.getElementById('host-val').textContent = s.hostname || 'jarvis';
@@ -1735,11 +1931,13 @@ async function loadAlerts() {
if (!alerts.length) {
el.innerHTML = '
✓ NO ACTIVE ALERTS
';
tb.textContent='NO ALERTS'; tb.className='text-green';
+ setAlertState(false);
return;
}
tb.textContent=alerts.length+' ALERT'+(alerts.length>1?'S':'');
tb.className='text-red';
+ setAlertState(true);
el.innerHTML = alerts.map(a => {
const ctxKey = 'alert_' + a.id;