jaeswift-website/js/main.js

935 lines
34 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(0, 255, 200, ${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(0, 255, 200, ${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(0, 255, 200, ${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', (e) => {
e.preventDefault();
const btn = form.querySelector('.form-submit');
const originalText = btn.querySelector('.submit-text').textContent;
btn.querySelector('.submit-text').textContent = 'TRANSMITTING...';
btn.disabled = true;
btn.style.borderColor = 'var(--warning)';
btn.style.color = 'var(--warning)';
setTimeout(() => {
btn.querySelector('.submit-text').textContent = '✓ TRANSMITTED';
btn.style.borderColor = 'var(--accent)';
btn.style.color = 'var(--accent)';
setTimeout(() => {
btn.querySelector('.submit-text').textContent = originalText;
btn.disabled = false;
btn.style.borderColor = '';
btn.style.color = '';
form.reset();
}, 2000);
}, 1500);
});
}
// ─── 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(0, 255, 200, 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 initial data
for (let i = 0; i < maxPoints; i++) {
dlData.push(30 + Math.random() * 40);
ulData.push(20 + Math.random() * 30);
}
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 y = h - (data[i] / 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();
}
}
function updateStats() {
const dl = dlData[dlData.length - 1];
const ul = ulData[ulData.length - 1];
const dlEl = document.getElementById('dlSpeed');
const ulEl = document.getElementById('ulSpeed');
const pkEl = document.getElementById('packetCount');
if (dlEl) dlEl.textContent = (dl * 0.032).toFixed(1) + ' Gbps';
if (ulEl) ulEl.textContent = (ul * 0.028).toFixed(1) + ' Gbps';
if (pkEl) pkEl.textContent = Math.floor(dl * 180).toLocaleString() + ' pkt/s';
}
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawGrid();
// Push new data
const lastDl = dlData[dlData.length - 1];
const lastUl = ulData[ulData.length - 1];
dlData.push(Math.max(10, Math.min(95, lastDl + (Math.random() - 0.48) * 12)));
ulData.push(Math.max(5, Math.min(85, lastUl + (Math.random() - 0.48) * 10)));
if (dlData.length > maxPoints) dlData.shift();
if (ulData.length > maxPoints) ulData.shift();
drawLine(dlData, '#00ffc8', 0.9);
drawLine(ulData, '#00a8ff', 0.6);
updateStats();
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, #ff4757, rgba(255,71,87,0.4))';
barEl.style.boxShadow = '0 0 8px rgba(255,71,87,0.4)';
valEl.style.color = '#ff4757';
} else if (val > 70) {
barEl.style.background = 'linear-gradient(90deg, #ffa502, rgba(255,165,2,0.4))';
barEl.style.boxShadow = '0 0 8px rgba(255,165,2,0.3)';
valEl.style.color = '#ffa502';
} 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="post.html?slug=${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) ───
function initLiveStats() {
function fetchStats() {
fetch(API_BASE + '/stats')
.then(r => r.json())
.then(d => {
// Update metric bars with real data
const metrics = [
{ bar: 'cpuBar', val: 'cpuVal', value: d.cpu_percent },
{ bar: 'memBar', val: 'memVal', value: d.memory_percent },
{ bar: 'diskBar', val: 'diskVal', value: d.disk_percent },
];
metrics.forEach(m => {
const barEl = document.getElementById(m.bar);
const valEl = document.getElementById(m.val);
if (!barEl || !valEl) return;
barEl.style.width = m.value + '%';
valEl.textContent = Math.round(m.value) + '%';
if (m.value > 85) {
barEl.style.background = 'linear-gradient(90deg, #ff4757, rgba(255,71,87,0.4))';
barEl.style.boxShadow = '0 0 8px rgba(255,71,87,0.4)';
valEl.style.color = '#ff4757';
} else if (m.value > 70) {
barEl.style.background = 'linear-gradient(90deg, #ffa502, rgba(255,165,2,0.4))';
barEl.style.boxShadow = '0 0 8px rgba(255,165,2,0.3)';
valEl.style.color = '#ffa502';
} else {
barEl.style.background = '';
barEl.style.boxShadow = '';
valEl.style.color = '';
}
});
// Update uptime from real data
const uptimeEl = document.getElementById('uptime');
if (uptimeEl && d.uptime_seconds) {
const totalSec = Math.floor(d.uptime_seconds);
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 server health
const healthEl = document.getElementById('serverHealth');
if (healthEl) {
const health = Math.round(100 - (d.cpu_percent * 0.3 + d.memory_percent * 0.3 + d.disk_percent * 0.4) / 3);
healthEl.textContent = Math.min(99, Math.max(80, health)) + '%';
}
// Container counts
const containerEl = document.getElementById('containerUp');
const containerTotalEl = document.querySelector('.container-total');
if (containerEl) containerEl.textContent = d.container_running;
if (containerTotalEl) containerTotalEl.textContent = d.container_total;
})
.catch(() => {});
}
fetchStats();
setInterval(fetchStats, 10000); // refresh every 10s
}
// ─── 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;
})
.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();
initScanBar();
initPowerFlicker();
initServerHealth();
// 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: #00ffc8; font-weight: bold; font-size: 14px;',
'color: #c8d6e5; font-size: 12px;');
});
})();