1021 lines
38 KiB
JavaScript
1021 lines
38 KiB
JavaScript
/* ===================================================
|
|
JAESWIFT.XYZ — Main JavaScript
|
|
Animations, particles, typing, scroll effects
|
|
=================================================== */
|
|
|
|
(function () {
|
|
'use strict';
|
|
|
|
// ─── CONFIG ───
|
|
const CONFIG = {
|
|
typingStrings: [
|
|
'Developer // Tinkerer // Builder',
|
|
'Self-hosted everything.',
|
|
'Linux enthusiast since day one.',
|
|
'Building the future, one commit at a time.',
|
|
'root@jaeswift:~# echo "Hello, World"',
|
|
'Cybersecurity & Infrastructure.',
|
|
'AI Agent Operator.',
|
|
],
|
|
typingSpeed: 60,
|
|
typingDeleteSpeed: 30,
|
|
typingPause: 2500,
|
|
particleCount: 80,
|
|
particleMaxSpeed: 0.3,
|
|
particleConnectionDist: 120,
|
|
startTime: Date.now(),
|
|
};
|
|
|
|
// ─── UTILITIES ───
|
|
const $ = (sel, ctx = document) => ctx.querySelector(sel);
|
|
const $$ = (sel, ctx = document) => [...ctx.querySelectorAll(sel)];
|
|
|
|
// ─── NAVBAR ───
|
|
function initNavbar() {
|
|
const navbar = $('#navbar');
|
|
const toggle = $('#navToggle');
|
|
const menu = $('#navMenu');
|
|
const navItems = $$('.nav-item');
|
|
|
|
// Scroll effect
|
|
let lastScroll = 0;
|
|
window.addEventListener('scroll', () => {
|
|
const scrollY = window.scrollY;
|
|
navbar.classList.toggle('scrolled', scrollY > 50);
|
|
lastScroll = scrollY;
|
|
}, { passive: true });
|
|
|
|
// Mobile toggle
|
|
toggle.addEventListener('click', () => {
|
|
menu.classList.toggle('active');
|
|
toggle.classList.toggle('active');
|
|
});
|
|
|
|
// Mobile dropdown toggle
|
|
if (window.innerWidth <= 768) {
|
|
navItems.forEach(item => {
|
|
const link = item.querySelector('.nav-link');
|
|
link.addEventListener('click', (e) => {
|
|
if (item.querySelector('.dropdown')) {
|
|
e.preventDefault();
|
|
item.classList.toggle('active');
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// Active link on scroll
|
|
const sections = $$('section[id]');
|
|
window.addEventListener('scroll', () => {
|
|
const scrollPos = window.scrollY + 100;
|
|
sections.forEach(section => {
|
|
const top = section.offsetTop;
|
|
const height = section.offsetHeight;
|
|
const id = section.getAttribute('id');
|
|
if (scrollPos >= top && scrollPos < top + height) {
|
|
$$('.nav-link').forEach(l => l.classList.remove('active'));
|
|
const activeLink = $(`.nav-link[href="#${id}"]`);
|
|
if (activeLink) activeLink.classList.add('active');
|
|
}
|
|
});
|
|
}, { passive: true });
|
|
|
|
// Smooth scroll for nav links
|
|
$$('.nav-link, .dropdown a[href^="#"]').forEach(link => {
|
|
link.addEventListener('click', (e) => {
|
|
const href = link.getAttribute('href');
|
|
if (href && href.startsWith('#')) {
|
|
e.preventDefault();
|
|
const target = $(href);
|
|
if (target) {
|
|
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
menu.classList.remove('active');
|
|
toggle.classList.remove('active');
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// ─── LIVE CLOCK ───
|
|
function initClock() {
|
|
const navTime = $('#navTime');
|
|
if (!navTime) return;
|
|
|
|
function update() {
|
|
const now = new Date();
|
|
const h = String(now.getHours()).padStart(2, '0');
|
|
const m = String(now.getMinutes()).padStart(2, '0');
|
|
const s = String(now.getSeconds()).padStart(2, '0');
|
|
navTime.textContent = `${h}:${m}:${s}`;
|
|
}
|
|
|
|
update();
|
|
setInterval(update, 1000);
|
|
}
|
|
|
|
// ─── UPTIME COUNTER ───
|
|
function initUptime() {
|
|
const uptimeEl = $('#uptime');
|
|
if (!uptimeEl) return;
|
|
|
|
function update() {
|
|
const elapsed = Date.now() - CONFIG.startTime;
|
|
const totalSec = Math.floor(elapsed / 1000);
|
|
const days = Math.floor(totalSec / 86400);
|
|
const hours = Math.floor((totalSec % 86400) / 3600);
|
|
const mins = Math.floor((totalSec % 3600) / 60);
|
|
uptimeEl.textContent = `${days}d ${String(hours).padStart(2, '0')}h ${String(mins).padStart(2, '0')}m`;
|
|
}
|
|
|
|
update();
|
|
setInterval(update, 60000);
|
|
}
|
|
|
|
// ─── TYPING EFFECT ───
|
|
function initTyping() {
|
|
const el = $('#typingText');
|
|
if (!el) return;
|
|
|
|
let stringIndex = 0;
|
|
let charIndex = 0;
|
|
let isDeleting = false;
|
|
|
|
function type() {
|
|
const current = CONFIG.typingStrings[stringIndex];
|
|
|
|
if (isDeleting) {
|
|
el.textContent = current.substring(0, charIndex - 1);
|
|
charIndex--;
|
|
} else {
|
|
el.textContent = current.substring(0, charIndex + 1);
|
|
charIndex++;
|
|
}
|
|
|
|
let delay = isDeleting ? CONFIG.typingDeleteSpeed : CONFIG.typingSpeed;
|
|
|
|
if (!isDeleting && charIndex === current.length) {
|
|
delay = CONFIG.typingPause;
|
|
isDeleting = true;
|
|
} else if (isDeleting && charIndex === 0) {
|
|
isDeleting = false;
|
|
stringIndex = (stringIndex + 1) % CONFIG.typingStrings.length;
|
|
delay = 400;
|
|
}
|
|
|
|
setTimeout(type, delay);
|
|
}
|
|
|
|
setTimeout(type, 1200);
|
|
}
|
|
|
|
// ─── SCROLL REVEAL ───
|
|
function initScrollReveal() {
|
|
const elements = $$('[data-animate]');
|
|
if (!elements.length) return;
|
|
|
|
const observer = new IntersectionObserver((entries) => {
|
|
entries.forEach((entry, index) => {
|
|
if (entry.isIntersecting) {
|
|
setTimeout(() => {
|
|
entry.target.classList.add('visible');
|
|
}, index * 100);
|
|
observer.unobserve(entry.target);
|
|
}
|
|
});
|
|
}, {
|
|
threshold: 0.1,
|
|
rootMargin: '0px 0px -50px 0px'
|
|
});
|
|
|
|
elements.forEach(el => observer.observe(el));
|
|
}
|
|
|
|
// ─── SKILL BARS ANIMATION ───
|
|
function initSkillBars() {
|
|
const bars = $$('.bar-fill');
|
|
if (!bars.length) return;
|
|
|
|
const observer = new IntersectionObserver((entries) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
const bar = entry.target;
|
|
const width = bar.getAttribute('data-width');
|
|
bar.style.setProperty('--target-width', width + '%');
|
|
bar.classList.add('animated');
|
|
observer.unobserve(bar);
|
|
}
|
|
});
|
|
}, { threshold: 0.5 });
|
|
|
|
bars.forEach(bar => observer.observe(bar));
|
|
}
|
|
|
|
// ─── PARTICLE SYSTEM ───
|
|
function initParticles() {
|
|
const canvas = $('#particles');
|
|
if (!canvas) return;
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
let particles = [];
|
|
let animationId;
|
|
let mouseX = -1000;
|
|
let mouseY = -1000;
|
|
|
|
function resize() {
|
|
canvas.width = window.innerWidth;
|
|
canvas.height = window.innerHeight;
|
|
}
|
|
|
|
function createParticle() {
|
|
return {
|
|
x: Math.random() * canvas.width,
|
|
y: Math.random() * canvas.height,
|
|
vx: (Math.random() - 0.5) * CONFIG.particleMaxSpeed,
|
|
vy: (Math.random() - 0.5) * CONFIG.particleMaxSpeed,
|
|
size: Math.random() * 1.5 + 0.5,
|
|
opacity: Math.random() * 0.5 + 0.1,
|
|
};
|
|
}
|
|
|
|
function initParticleArray() {
|
|
particles = [];
|
|
for (let i = 0; i < CONFIG.particleCount; i++) {
|
|
particles.push(createParticle());
|
|
}
|
|
}
|
|
|
|
function drawParticle(p) {
|
|
ctx.beginPath();
|
|
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
|
|
ctx.fillStyle = `rgba(51, 51, 51, ${p.opacity})`;
|
|
ctx.fill();
|
|
}
|
|
|
|
function drawConnections() {
|
|
for (let i = 0; i < particles.length; i++) {
|
|
for (let j = i + 1; j < particles.length; j++) {
|
|
const dx = particles[i].x - particles[j].x;
|
|
const dy = particles[i].y - particles[j].y;
|
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
if (dist < CONFIG.particleConnectionDist) {
|
|
const opacity = (1 - dist / CONFIG.particleConnectionDist) * 0.15;
|
|
ctx.beginPath();
|
|
ctx.moveTo(particles[i].x, particles[i].y);
|
|
ctx.lineTo(particles[j].x, particles[j].y);
|
|
ctx.strokeStyle = `rgba(51, 51, 51, ${opacity})`;
|
|
ctx.lineWidth = 0.5;
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Mouse connections
|
|
particles.forEach(p => {
|
|
const dx = p.x - mouseX;
|
|
const dy = p.y - mouseY;
|
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
if (dist < 200) {
|
|
const opacity = (1 - dist / 200) * 0.3;
|
|
ctx.beginPath();
|
|
ctx.moveTo(p.x, p.y);
|
|
ctx.lineTo(mouseX, mouseY);
|
|
ctx.strokeStyle = `rgba(51, 51, 51, ${opacity})`;
|
|
ctx.lineWidth = 0.8;
|
|
ctx.stroke();
|
|
}
|
|
});
|
|
}
|
|
|
|
function update() {
|
|
particles.forEach(p => {
|
|
p.x += p.vx;
|
|
p.y += p.vy;
|
|
|
|
// Wrap around
|
|
if (p.x < 0) p.x = canvas.width;
|
|
if (p.x > canvas.width) p.x = 0;
|
|
if (p.y < 0) p.y = canvas.height;
|
|
if (p.y > canvas.height) p.y = 0;
|
|
|
|
// Mouse repulsion
|
|
const dx = p.x - mouseX;
|
|
const dy = p.y - mouseY;
|
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
if (dist < 100) {
|
|
const force = (100 - dist) / 100 * 0.02;
|
|
p.vx += (dx / dist) * force;
|
|
p.vy += (dy / dist) * force;
|
|
}
|
|
|
|
// Speed limit
|
|
const speed = Math.sqrt(p.vx * p.vx + p.vy * p.vy);
|
|
if (speed > CONFIG.particleMaxSpeed * 2) {
|
|
p.vx *= 0.98;
|
|
p.vy *= 0.98;
|
|
}
|
|
});
|
|
}
|
|
|
|
function animate() {
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
update();
|
|
drawConnections();
|
|
particles.forEach(drawParticle);
|
|
animationId = requestAnimationFrame(animate);
|
|
}
|
|
|
|
// Event listeners
|
|
window.addEventListener('resize', () => {
|
|
resize();
|
|
initParticleArray();
|
|
});
|
|
|
|
window.addEventListener('mousemove', (e) => {
|
|
mouseX = e.clientX;
|
|
mouseY = e.clientY;
|
|
}, { passive: true });
|
|
|
|
window.addEventListener('mouseleave', () => {
|
|
mouseX = -1000;
|
|
mouseY = -1000;
|
|
});
|
|
|
|
// Visibility API - pause when tab hidden
|
|
document.addEventListener('visibilitychange', () => {
|
|
if (document.hidden) {
|
|
cancelAnimationFrame(animationId);
|
|
} else {
|
|
animate();
|
|
}
|
|
});
|
|
|
|
resize();
|
|
initParticleArray();
|
|
animate();
|
|
}
|
|
|
|
// ─── GLITCH EFFECT (random intensification) ───
|
|
function initGlitchEffect() {
|
|
const glitch = $('.glitch');
|
|
if (!glitch) return;
|
|
|
|
setInterval(() => {
|
|
glitch.classList.add('glitch-active');
|
|
setTimeout(() => glitch.classList.remove('glitch-active'), 200);
|
|
}, 5000 + Math.random() * 5000);
|
|
}
|
|
|
|
// ─── TERMINAL ANIMATION ───
|
|
function initTerminal() {
|
|
const terminal = $('#terminal');
|
|
if (!terminal) return;
|
|
|
|
const commands = [
|
|
{ prompt: 'uptime', output: ' up 47 days, 3:22, 1 user, load: 0.12' },
|
|
{ prompt: 'df -h / | tail -1', output: '/dev/sda1 50G 12G 36G 25% /' },
|
|
{ prompt: 'curl -s ifconfig.me', output: '███.███.███.███' },
|
|
{ prompt: 'docker ps --format "table {{.Names}}"', output: 'gitea\nnginx-proxy\nagent-zero\nmonitoring' },
|
|
{ prompt: 'echo $SHELL', output: '/bin/zsh' },
|
|
{ prompt: 'neofetch --off | head -3', output: 'OS: Debian GNU/Linux 12\nKernel: 6.1.0-18-amd64\nUptime: 47 days, 3 hours' },
|
|
];
|
|
|
|
let cmdIndex = 0;
|
|
|
|
function addTermLine(content, isOutput = false) {
|
|
const line = document.createElement('div');
|
|
line.className = 'term-line' + (isOutput ? ' term-output' : '');
|
|
if (isOutput) {
|
|
line.textContent = content;
|
|
} else {
|
|
line.innerHTML = `<span class="term-prompt">jae@swift:~$</span> <span class="term-cmd">${content}</span>`;
|
|
}
|
|
// Insert before the last line (cursor line)
|
|
const cursorLine = terminal.lastElementChild;
|
|
terminal.insertBefore(line, cursorLine);
|
|
}
|
|
|
|
function typeCommand() {
|
|
if (cmdIndex >= commands.length) cmdIndex = 0;
|
|
const cmd = commands[cmdIndex];
|
|
let charIdx = 0;
|
|
|
|
// Remove old cursor line, add new command line being typed
|
|
const cursorLine = terminal.lastElementChild;
|
|
|
|
const typingLine = document.createElement('div');
|
|
typingLine.className = 'term-line';
|
|
typingLine.innerHTML = `<span class="term-prompt">jae@swift:~$</span> <span class="term-cmd"></span><span class="term-cursor">█</span>`;
|
|
terminal.insertBefore(typingLine, cursorLine);
|
|
cursorLine.remove();
|
|
|
|
const cmdSpan = typingLine.querySelector('.term-cmd');
|
|
|
|
function typeChar() {
|
|
if (charIdx < cmd.prompt.length) {
|
|
cmdSpan.textContent += cmd.prompt[charIdx];
|
|
charIdx++;
|
|
setTimeout(typeChar, 50 + Math.random() * 80);
|
|
} else {
|
|
// Remove cursor from typing line
|
|
const cursor = typingLine.querySelector('.term-cursor');
|
|
if (cursor) cursor.remove();
|
|
|
|
// Show output
|
|
setTimeout(() => {
|
|
const outputLines = cmd.output.split('\n');
|
|
outputLines.forEach(line => {
|
|
const outputEl = document.createElement('div');
|
|
outputEl.className = 'term-line term-output';
|
|
outputEl.textContent = line;
|
|
terminal.appendChild(outputEl);
|
|
});
|
|
|
|
// Add new cursor line
|
|
const newCursor = document.createElement('div');
|
|
newCursor.className = 'term-line';
|
|
newCursor.innerHTML = `<span class="term-prompt">jae@swift:~$</span> <span class="term-cursor">█</span>`;
|
|
terminal.appendChild(newCursor);
|
|
|
|
// Scroll terminal
|
|
terminal.scrollTop = terminal.scrollHeight;
|
|
|
|
// Keep only last ~15 lines
|
|
while (terminal.children.length > 15) {
|
|
terminal.removeChild(terminal.firstChild);
|
|
}
|
|
|
|
cmdIndex++;
|
|
}, 300);
|
|
}
|
|
}
|
|
|
|
typeChar();
|
|
}
|
|
|
|
// Run terminal animation every 6-10 seconds
|
|
setInterval(typeCommand, 8000 + Math.random() * 4000);
|
|
}
|
|
|
|
// ─── CONTACT FORM ───
|
|
function initContactForm() {
|
|
const form = $('#contactForm');
|
|
if (!form) return;
|
|
|
|
form.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const btn = form.querySelector('.form-submit');
|
|
const submitText = btn.querySelector('.submit-text');
|
|
const originalText = submitText.textContent;
|
|
|
|
const name = $('#contactName').value.trim();
|
|
const email = $('#contactEmail').value.trim();
|
|
const subject = $('#contactSubject').value.trim();
|
|
const message = $('#contactMessage').value.trim();
|
|
|
|
if (!name || !email || !message) return;
|
|
|
|
submitText.textContent = 'TRANSMITTING...';
|
|
btn.disabled = true;
|
|
btn.style.borderColor = 'var(--warning)';
|
|
btn.style.color = 'var(--warning)';
|
|
|
|
try {
|
|
const payload = {
|
|
name: name,
|
|
email: email,
|
|
message: subject ? `[${subject}] ${message}` : message
|
|
};
|
|
const res = await fetch('/api/contact', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (res.ok) {
|
|
submitText.textContent = '✓ TRANSMITTED';
|
|
btn.style.borderColor = 'var(--accent)';
|
|
btn.style.color = 'var(--accent)';
|
|
form.reset();
|
|
} else {
|
|
submitText.textContent = '✗ FAILED';
|
|
btn.style.borderColor = '#ff2d2d';
|
|
btn.style.color = '#ff2d2d';
|
|
}
|
|
} catch (err) {
|
|
submitText.textContent = '✗ ERROR';
|
|
btn.style.borderColor = '#ff2d2d';
|
|
btn.style.color = '#ff2d2d';
|
|
}
|
|
|
|
setTimeout(() => {
|
|
submitText.textContent = originalText;
|
|
btn.disabled = false;
|
|
btn.style.borderColor = '';
|
|
btn.style.color = '';
|
|
}, 2500);
|
|
});
|
|
}
|
|
|
|
// ─── FOOTER SIGNAL ANIMATION ───
|
|
function initFooterSignal() {
|
|
const signal = $('#footerSignal');
|
|
if (!signal) return;
|
|
|
|
const blocks = ['░', '█'];
|
|
function update() {
|
|
const strength = 6 + Math.floor(Math.random() * 4); // 6-9 out of 10
|
|
let bar = '';
|
|
for (let i = 0; i < 10; i++) {
|
|
bar += i < strength ? blocks[1] : blocks[0];
|
|
}
|
|
signal.textContent = `SIGNAL: ${bar} ${strength * 10}%`;
|
|
}
|
|
|
|
setInterval(update, 3000);
|
|
}
|
|
|
|
// ─── RANDOM HUD DATA FLICKERS ───
|
|
function initHUDFlickers() {
|
|
const statValues = $$('.stat-value:not(.stat-online)');
|
|
|
|
setInterval(() => {
|
|
statValues.forEach(el => {
|
|
if (Math.random() > 0.7) {
|
|
el.style.opacity = '0.3';
|
|
setTimeout(() => {
|
|
el.style.opacity = '1';
|
|
}, 100 + Math.random() * 200);
|
|
}
|
|
});
|
|
}, 2000);
|
|
}
|
|
|
|
// ─── PANEL HOVER GLOW TRAIL ───
|
|
function initPanelGlow() {
|
|
$$('.panel, .blog-card, .dev-card, .link-card').forEach(card => {
|
|
card.addEventListener('mousemove', (e) => {
|
|
const rect = card.getBoundingClientRect();
|
|
const x = e.clientX - rect.left;
|
|
const y = e.clientY - rect.top;
|
|
card.style.setProperty('--mouse-x', `${x}px`);
|
|
card.style.setProperty('--mouse-y', `${y}px`);
|
|
card.style.background = `radial-gradient(circle 200px at ${x}px ${y}px, rgba(51, 51, 51, 0.04), var(--bg-panel))`;
|
|
});
|
|
|
|
card.addEventListener('mouseleave', () => {
|
|
card.style.background = '';
|
|
});
|
|
});
|
|
}
|
|
|
|
// ─── INIT ───
|
|
|
|
// ─── NETWORK GRAPH ───
|
|
function initNetworkGraph() {
|
|
const canvas = document.getElementById('networkGraph');
|
|
if (!canvas) return;
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
function resize() {
|
|
const rect = canvas.parentElement.getBoundingClientRect();
|
|
canvas.width = rect.width;
|
|
canvas.height = 100;
|
|
}
|
|
resize();
|
|
window.addEventListener('resize', resize);
|
|
|
|
const dlData = [];
|
|
const ulData = [];
|
|
const maxPoints = 80;
|
|
|
|
// Seed with zeros — graph populates with real samples from initLiveStats
|
|
for (let i = 0; i < maxPoints; i++) {
|
|
dlData.push(0);
|
|
ulData.push(0);
|
|
}
|
|
// Auto-scaling peak (starts at 10 Mbps floor)
|
|
let peakMbps = 10;
|
|
|
|
function drawLine(data, color, alpha) {
|
|
const w = canvas.width;
|
|
const h = canvas.height;
|
|
const step = w / (maxPoints - 1);
|
|
|
|
ctx.beginPath();
|
|
ctx.strokeStyle = color;
|
|
ctx.lineWidth = 1.5;
|
|
ctx.globalAlpha = alpha;
|
|
|
|
for (let i = 0; i < data.length; i++) {
|
|
const x = i * step;
|
|
const scaled = Math.min(100, (data[i] / peakMbps) * 100);
|
|
const y = h - (scaled / 100) * h;
|
|
if (i === 0) ctx.moveTo(x, y);
|
|
else ctx.lineTo(x, y);
|
|
}
|
|
ctx.stroke();
|
|
|
|
// Fill under the line
|
|
ctx.lineTo(canvas.width, canvas.height);
|
|
ctx.lineTo(0, canvas.height);
|
|
ctx.closePath();
|
|
ctx.fillStyle = color;
|
|
ctx.globalAlpha = alpha * 0.08;
|
|
ctx.fill();
|
|
ctx.globalAlpha = 1;
|
|
}
|
|
|
|
function drawGrid() {
|
|
const w = canvas.width;
|
|
const h = canvas.height;
|
|
ctx.strokeStyle = 'rgba(0,255,200,0.06)';
|
|
ctx.lineWidth = 0.5;
|
|
|
|
// Horizontal
|
|
for (let y = 0; y < h; y += 20) {
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, y);
|
|
ctx.lineTo(w, y);
|
|
ctx.stroke();
|
|
}
|
|
// Vertical
|
|
for (let x = 0; x < w; x += 30) {
|
|
ctx.beginPath();
|
|
ctx.moveTo(x, 0);
|
|
ctx.lineTo(x, h);
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
|
|
// Readouts (dlSpeed/ulSpeed/packetCount) are updated by initLiveStats with real values
|
|
|
|
function animate() {
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
drawGrid();
|
|
|
|
// Pull latest real Mbps from shared state set by initLiveStats
|
|
const stats = window.__serverStats || { dlMbps: 0, ulMbps: 0 };
|
|
dlData.push(stats.dlMbps || 0);
|
|
ulData.push(stats.ulMbps || 0);
|
|
if (dlData.length > maxPoints) dlData.shift();
|
|
if (ulData.length > maxPoints) ulData.shift();
|
|
|
|
// Smooth auto-scaling peak
|
|
const curMax = Math.max.apply(null, dlData.concat(ulData).concat([1]));
|
|
peakMbps = peakMbps * 0.95 + Math.max(10, curMax * 1.2) * 0.05;
|
|
|
|
drawLine(dlData, '#00ff9d', 0.9); // download = green
|
|
drawLine(ulData, '#00a8ff', 0.7); // upload = blue
|
|
|
|
requestAnimationFrame(animate);
|
|
}
|
|
animate();
|
|
}
|
|
|
|
// ─── METRIC BARS (fluctuating) ───
|
|
function initMetricBars() {
|
|
const metrics = [
|
|
{ bar: 'cpuBar', val: 'cpuVal', base: 23, range: 18 },
|
|
{ bar: 'memBar', val: 'memVal', base: 67, range: 8 },
|
|
{ bar: 'diskBar', val: 'diskVal', base: 45, range: 25 },
|
|
{ bar: 'bwBar', val: 'bwVal', base: 78, range: 15 },
|
|
];
|
|
|
|
function updateMetrics() {
|
|
metrics.forEach(m => {
|
|
const barEl = document.getElementById(m.bar);
|
|
const valEl = document.getElementById(m.val);
|
|
if (!barEl || !valEl) return;
|
|
|
|
const val = Math.max(5, Math.min(98, m.base + (Math.random() - 0.5) * m.range));
|
|
barEl.style.width = val + '%';
|
|
valEl.textContent = Math.round(val) + '%';
|
|
|
|
// Colour shift for high values
|
|
if (val > 85) {
|
|
barEl.style.background = 'linear-gradient(90deg, #ff2d2d, rgba(255,71,87,0.4))';
|
|
barEl.style.boxShadow = '0 0 8px rgba(255,71,87,0.4)';
|
|
valEl.style.color = '#ff2d2d';
|
|
} else if (val > 70) {
|
|
barEl.style.background = 'linear-gradient(90deg, #c9a227, rgba(255,165,2,0.4))';
|
|
barEl.style.boxShadow = '0 0 8px rgba(255,165,2,0.3)';
|
|
valEl.style.color = '#c9a227';
|
|
} else {
|
|
barEl.style.background = '';
|
|
barEl.style.boxShadow = '';
|
|
valEl.style.color = '';
|
|
}
|
|
});
|
|
}
|
|
|
|
updateMetrics();
|
|
setInterval(updateMetrics, 2000 + Math.random() * 1000);
|
|
}
|
|
|
|
// ─── SCAN BAR ───
|
|
function initScanBar() {
|
|
const fill = document.getElementById('scanFill');
|
|
const pct = document.getElementById('scanPct');
|
|
if (!fill || !pct) return;
|
|
|
|
let progress = 0;
|
|
function tick() {
|
|
progress += 0.5 + Math.random() * 1.5;
|
|
if (progress >= 100) {
|
|
progress = 0;
|
|
fill.style.transition = 'none';
|
|
fill.style.width = '0%';
|
|
pct.textContent = '0%';
|
|
setTimeout(() => {
|
|
fill.style.transition = 'width 0.3s linear';
|
|
requestAnimationFrame(tick);
|
|
}, 800);
|
|
return;
|
|
}
|
|
fill.style.width = progress + '%';
|
|
pct.textContent = Math.round(progress) + '%';
|
|
setTimeout(tick, 80 + Math.random() * 120);
|
|
}
|
|
|
|
fill.style.transition = 'width 0.3s linear';
|
|
tick();
|
|
}
|
|
|
|
// ─── POWER FLICKER ───
|
|
function initPowerFlicker() {
|
|
const el = document.getElementById('powerPct');
|
|
if (!el) return;
|
|
|
|
setInterval(() => {
|
|
const vals = [97, 98, 99, 98, 100, 98, 97, 99];
|
|
const v = vals[Math.floor(Math.random() * vals.length)];
|
|
el.innerHTML = v + '<span class="power-unit">%</span>';
|
|
}, 4000);
|
|
}
|
|
|
|
// ─── SERVER HEALTH ───
|
|
function initServerHealth() {
|
|
const el = document.getElementById('serverHealth');
|
|
if (!el) return;
|
|
|
|
setInterval(() => {
|
|
const v = 95 + Math.floor(Math.random() * 5);
|
|
el.textContent = v + '%';
|
|
}, 5000);
|
|
}
|
|
|
|
// ─── API CONFIG ───
|
|
const API_BASE = window.location.hostname === 'localhost' ? 'http://localhost:5000' : '/api';
|
|
|
|
// ─── BLOG FEED (dynamic from API) ───
|
|
function initBlogFeed() {
|
|
const grid = document.getElementById('blogGrid');
|
|
if (!grid) return;
|
|
|
|
fetch(API_BASE + '/posts')
|
|
.then(r => r.json())
|
|
.then(posts => {
|
|
posts.sort((a, b) => new Date(b.date) - new Date(a.date));
|
|
const latest = posts.slice(0, 3);
|
|
|
|
if (latest.length === 0) {
|
|
grid.innerHTML = '<div class="blog-loading-placeholder">NO TRANSMISSIONS FOUND</div>';
|
|
return;
|
|
}
|
|
|
|
grid.innerHTML = latest.map((post, i) => `
|
|
<article class="blog-card" data-animate style="opacity:0; transform:translateY(20px); transition:all 0.5s ease ${i * 0.15}s;">
|
|
<div class="blog-card-header">
|
|
<span class="blog-date">${post.date.replace(/-/g, '.')}</span>
|
|
<span class="blog-tag">${(post.tags && post.tags[0]) ? post.tags[0].toUpperCase() : 'POST'}</span>
|
|
</div>
|
|
<h3 class="blog-title">${post.title}</h3>
|
|
<p class="blog-excerpt">${post.excerpt || (post.content || '').substring(0, 120) + '...'}</p>
|
|
<div class="blog-footer">
|
|
<span class="blog-read-time">◷ ${Math.max(1, Math.ceil((post.word_count || 300) / 250))} MIN READ</span>
|
|
<a href="/blog/post/${post.slug}" class="blog-link">READ →</a>
|
|
</div>
|
|
</article>
|
|
`).join('');
|
|
|
|
// Animate cards in
|
|
requestAnimationFrame(() => {
|
|
grid.querySelectorAll('.blog-card').forEach(card => {
|
|
card.style.opacity = '1';
|
|
card.style.transform = 'translateY(0)';
|
|
});
|
|
});
|
|
})
|
|
.catch(() => {
|
|
grid.innerHTML = '<div class="blog-loading-placeholder">SIGNAL LOST — RETRY LATER</div>';
|
|
});
|
|
}
|
|
|
|
// ─── LIVE SERVER STATS (from API) — REAL DATA ───
|
|
// Shared state used by the network graph
|
|
window.__serverStats = {
|
|
lastRx: null, lastTx: null, lastT: null,
|
|
dlMbps: 0, ulMbps: 0, bwPct: 0
|
|
};
|
|
|
|
function formatUptime(sec) {
|
|
sec = Math.floor(sec);
|
|
const d = Math.floor(sec / 86400);
|
|
const h = Math.floor((sec % 86400) / 3600);
|
|
const m = Math.floor((sec % 3600) / 60);
|
|
return d + 'd ' + String(h).padStart(2, '0') + 'h ' + String(m).padStart(2, '0') + 'm';
|
|
}
|
|
|
|
function applyBar(barId, valId, value, unit) {
|
|
const barEl = document.getElementById(barId);
|
|
const valEl = document.getElementById(valId);
|
|
if (!barEl || !valEl) return;
|
|
const v = Math.max(0, Math.min(100, value));
|
|
barEl.style.width = v + '%';
|
|
valEl.textContent = Math.round(v) + (unit || '%');
|
|
if (v > 85) {
|
|
barEl.style.background = 'linear-gradient(90deg, #ff2d2d, rgba(255,71,87,0.4))';
|
|
barEl.style.boxShadow = '0 0 8px rgba(255,71,87,0.4)';
|
|
valEl.style.color = '#ff2d2d';
|
|
} else if (v > 70) {
|
|
barEl.style.background = 'linear-gradient(90deg, #c9a227, rgba(255,165,2,0.4))';
|
|
barEl.style.boxShadow = '0 0 8px rgba(255,165,2,0.3)';
|
|
valEl.style.color = '#c9a227';
|
|
} else {
|
|
barEl.style.background = '';
|
|
barEl.style.boxShadow = '';
|
|
valEl.style.color = '';
|
|
}
|
|
}
|
|
|
|
function initLiveStats() {
|
|
// Assumed max link capacity for bandwidth % scaling (1 Gbps)
|
|
const LINK_MBPS = 1000;
|
|
|
|
function fetchStats() {
|
|
fetch(API_BASE + '/stats')
|
|
.then(r => r.json())
|
|
.then(d => {
|
|
// CPU / MEM / DISK bars
|
|
applyBar('cpuBar', 'cpuVal', d.cpu_percent);
|
|
applyBar('memBar', 'memVal', d.memory_percent);
|
|
applyBar('diskBar', 'diskVal', d.disk_percent);
|
|
|
|
// Bandwidth — compute MB/s delta from network_rx_bytes + network_tx_bytes
|
|
const now = (d.timestamp || Date.now() / 1000);
|
|
const s = window.__serverStats;
|
|
if (s.lastRx !== null && s.lastT !== null && now > s.lastT) {
|
|
const dt = Math.max(0.1, now - s.lastT);
|
|
const rxBps = Math.max(0, (d.network_rx_bytes - s.lastRx) / dt);
|
|
const txBps = Math.max(0, (d.network_tx_bytes - s.lastTx) / dt);
|
|
const rxMbps = (rxBps * 8) / 1e6;
|
|
const txMbps = (txBps * 8) / 1e6;
|
|
s.dlMbps = rxMbps;
|
|
s.ulMbps = txMbps;
|
|
const totalMbps = rxMbps + txMbps;
|
|
s.bwPct = Math.min(100, (totalMbps / LINK_MBPS) * 100);
|
|
applyBar('bwBar', 'bwVal', s.bwPct);
|
|
|
|
// Network graph stat readouts
|
|
const dlEl = document.getElementById('dlSpeed');
|
|
const ulEl = document.getElementById('ulSpeed');
|
|
const pkEl = document.getElementById('packetCount');
|
|
const fmtSpeed = mbps => mbps >= 1000 ? (mbps / 1000).toFixed(2) + ' Gbps'
|
|
: mbps >= 1 ? mbps.toFixed(1) + ' Mbps'
|
|
: (mbps * 1000).toFixed(0) + ' Kbps';
|
|
if (dlEl) dlEl.textContent = fmtSpeed(rxMbps);
|
|
if (ulEl) ulEl.textContent = fmtSpeed(txMbps);
|
|
if (pkEl) pkEl.textContent = (d.active_connections || 0).toLocaleString() + ' conns';
|
|
} else {
|
|
// Seed on first sample
|
|
applyBar('bwBar', 'bwVal', 0);
|
|
}
|
|
s.lastRx = d.network_rx_bytes;
|
|
s.lastTx = d.network_tx_bytes;
|
|
s.lastT = now;
|
|
|
|
// Uptime (HTML id is serverUptime)
|
|
const uptimeEl = document.getElementById('serverUptime') || document.getElementById('uptime');
|
|
if (uptimeEl && d.uptime_seconds) {
|
|
uptimeEl.textContent = formatUptime(d.uptime_seconds);
|
|
}
|
|
|
|
// Server health — weighted score from cpu/mem/disk
|
|
const healthEl = document.getElementById('serverHealth');
|
|
if (healthEl) {
|
|
const penalty = (d.cpu_percent * 0.35 + d.memory_percent * 0.35 + d.disk_percent * 0.30);
|
|
const health = Math.round(100 - penalty * 0.35);
|
|
healthEl.textContent = Math.min(99, Math.max(40, health)) + '%';
|
|
}
|
|
|
|
// Power panel — use inverse of CPU load as "reliability" %
|
|
const powerEl = document.getElementById('powerPct');
|
|
if (powerEl) {
|
|
const rel = Math.max(60, Math.min(100, 100 - d.cpu_percent * 0.4));
|
|
powerEl.innerHTML = Math.round(rel) + '<span class="power-unit">%</span>';
|
|
}
|
|
|
|
// Containers
|
|
const containerEl = document.getElementById('containerUp');
|
|
const containerTotalEl = document.querySelector('.container-total');
|
|
const containerBar = document.getElementById('containerBar');
|
|
if (containerEl) containerEl.textContent = d.container_running;
|
|
if (containerTotalEl) containerTotalEl.textContent = d.container_total;
|
|
if (containerBar && d.container_total) {
|
|
containerBar.style.width = ((d.container_running / d.container_total) * 100) + '%';
|
|
}
|
|
})
|
|
.catch(() => {});
|
|
}
|
|
|
|
fetchStats();
|
|
setInterval(fetchStats, 5000); // refresh every 5s
|
|
}
|
|
|
|
// ─── LIVE WEATHER ───
|
|
function initLiveWeather() {
|
|
const tempEl = document.getElementById('weatherTemp');
|
|
const condEl = document.getElementById('weatherCond');
|
|
if (!tempEl && !condEl) return;
|
|
|
|
fetch(API_BASE + '/weather')
|
|
.then(r => r.json())
|
|
.then(d => {
|
|
if (tempEl) tempEl.textContent = d.temp_c + '°C';
|
|
if (condEl) condEl.textContent = d.condition;
|
|
const feelsEl = document.getElementById('weatherFeels');
|
|
if (feelsEl) feelsEl.textContent = 'FEELS ' + d.feels_like + '°C';
|
|
const windEl = document.getElementById('weatherWind');
|
|
if (windEl) windEl.textContent = d.wind_kph + ' KPH ' + d.wind_dir;
|
|
const humEl = document.getElementById('weatherHumidity');
|
|
if (humEl) humEl.textContent = d.humidity + '% RH';
|
|
})
|
|
.catch(() => {});
|
|
}
|
|
|
|
// ─── NOW PLAYING ───
|
|
function initNowPlaying() {
|
|
const trackEl = document.getElementById('npTrack');
|
|
const artistEl = document.getElementById('npArtist');
|
|
if (!trackEl && !artistEl) return;
|
|
|
|
function fetchTrack() {
|
|
fetch(API_BASE + '/nowplaying')
|
|
.then(r => r.json())
|
|
.then(d => {
|
|
if (trackEl) trackEl.textContent = d.track;
|
|
if (artistEl) artistEl.textContent = d.artist;
|
|
const albumEl = document.getElementById('npAlbum');
|
|
if (albumEl) albumEl.textContent = d.album;
|
|
})
|
|
.catch(() => {});
|
|
}
|
|
|
|
fetchTrack();
|
|
setInterval(fetchTrack, 30000);
|
|
}
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
initNavbar();
|
|
initClock();
|
|
initUptime();
|
|
initTyping();
|
|
initScrollReveal();
|
|
initSkillBars();
|
|
initParticles();
|
|
initGlitchEffect();
|
|
initTerminal();
|
|
initContactForm();
|
|
initFooterSignal();
|
|
initHUDFlickers();
|
|
initPanelGlow();
|
|
initNetworkGraph();
|
|
// initMetricBars(); // disabled — real data from /api/stats via initLiveStats
|
|
initScanBar();
|
|
// initPowerFlicker(); // disabled — real load_avg-based via initLiveStats
|
|
// initServerHealth(); // disabled — real health computed in initLiveStats
|
|
|
|
// Live API integrations
|
|
initBlogFeed();
|
|
initLiveStats();
|
|
initLiveWeather();
|
|
initNowPlaying();
|
|
|
|
|
|
// Page load animation
|
|
document.body.style.opacity = '0';
|
|
document.body.style.transition = 'opacity 0.8s ease';
|
|
requestAnimationFrame(() => {
|
|
document.body.style.opacity = '1';
|
|
});
|
|
|
|
console.log('%c[JAESWIFT] %cSystems Online',
|
|
'color: #d0d0d0; font-weight: bold; font-size: 14px;',
|
|
'color: #c0c0c0; font-size: 12px;');
|
|
});
|
|
|
|
})();
|