feat: wire homepage to live API (blog feed, server stats, weather, now playing)

This commit is contained in:
jae 2026-03-31 21:14:30 +00:00
parent e41bd916f7
commit 271f933b6e
3 changed files with 199 additions and 37 deletions

View file

@ -1600,3 +1600,35 @@ a:hover { color: #fff; text-shadow: 0 0 10px var(--accent-glow); }
.metric-row { grid-template-columns: 60px 1fr 35px; gap: 0.5rem; }
.graph-stats { flex-direction: column; gap: 0.5rem; }
}
/* ─── Blog Loading Placeholder ─── */
.blog-loading-placeholder {
grid-column: 1 / -1;
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
padding: 3rem;
font-family: 'JetBrains Mono', monospace;
font-size: 0.8rem;
color: rgba(0, 255, 200, 0.4);
letter-spacing: 2px;
}
.loading-spinner {
width: 16px;
height: 16px;
border: 2px solid rgba(0, 255, 200, 0.1);
border-top-color: #00ffc8;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.blog-view-all-link:hover {
background: rgba(0, 255, 200, 0.08) !important;
box-shadow: 0 0 15px rgba(0, 255, 200, 0.15);
}

View file

@ -349,43 +349,15 @@
<h2 class="section-title">BLOG<span class="accent">_</span>FEED</h2>
<div class="section-line"></div>
</div>
<div class="blog-grid">
<article class="blog-card" data-animate>
<div class="blog-card-header">
<span class="blog-date">2026.03.31</span>
<span class="blog-tag">INFRASTRUCTURE</span>
<div class="blog-grid" id="blogGrid">
<!-- Posts loaded dynamically from API -->
<div class="blog-loading-placeholder">
<span class="loading-spinner"></span>
<span>LOADING TRANSMISSIONS...</span>
</div>
<h3 class="blog-title">Self-Hosting Everything: A Complete Guide</h3>
<p class="blog-excerpt">How I migrated away from cloud services and built a fully self-hosted infrastructure stack...</p>
<div class="blog-footer">
<span class="blog-read-time">◷ 8 MIN READ</span>
<a href="#" class="blog-link">READ →</a>
</div>
</article>
<article class="blog-card" data-animate>
<div class="blog-card-header">
<span class="blog-date">2026.03.28</span>
<span class="blog-tag">SECURITY</span>
</div>
<h3 class="blog-title">Hardening Your VPS: Beyond the Basics</h3>
<p class="blog-excerpt">Essential security configurations that most tutorials skip — from kernel parameters to network isolation...</p>
<div class="blog-footer">
<span class="blog-read-time">◷ 12 MIN READ</span>
<a href="#" class="blog-link">READ →</a>
</div>
</article>
<article class="blog-card" data-animate>
<div class="blog-card-header">
<span class="blog-date">2026.03.22</span>
<span class="blog-tag">DEV</span>
</div>
<h3 class="blog-title">Building AI Agents That Actually Work</h3>
<p class="blog-excerpt">My journey deploying Agent Zero and customising autonomous AI assistants for real-world tasks...</p>
<div class="blog-footer">
<span class="blog-read-time">◷ 10 MIN READ</span>
<a href="#" class="blog-link">READ →</a>
</div>
</article>
<div class="blog-view-all" style="text-align:center; margin-top:2rem;">
<a href="blog.html" class="blog-view-all-link" style="font-family:'JetBrains Mono',monospace; font-size:0.8rem; color:#00ffc8; letter-spacing:2px; text-decoration:none; border:1px solid rgba(0,255,200,0.2); padding:0.6rem 2rem; transition:all 0.3s;">VIEW ALL TRANSMISSIONS →</a>
</div>
</div>
</section>

View file

@ -741,6 +741,158 @@
}, 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();
@ -761,6 +913,12 @@
initPowerFlicker();
initServerHealth();
// Live API integrations
initBlogFeed();
initLiveStats();
initLiveWeather();
initNowPlaying();
// Page load animation
document.body.style.opacity = '0';