feat: wire homepage to live API (blog feed, server stats, weather, now playing)
This commit is contained in:
parent
e41bd916f7
commit
271f933b6e
3 changed files with 199 additions and 37 deletions
|
|
@ -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; }
|
.metric-row { grid-template-columns: 60px 1fr 35px; gap: 0.5rem; }
|
||||||
.graph-stats { flex-direction: column; 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);
|
||||||
|
}
|
||||||
|
|
|
||||||
42
index.html
42
index.html
|
|
@ -349,43 +349,15 @@
|
||||||
<h2 class="section-title">BLOG<span class="accent">_</span>FEED</h2>
|
<h2 class="section-title">BLOG<span class="accent">_</span>FEED</h2>
|
||||||
<div class="section-line"></div>
|
<div class="section-line"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="blog-grid">
|
<div class="blog-grid" id="blogGrid">
|
||||||
<article class="blog-card" data-animate>
|
<!-- Posts loaded dynamically from API -->
|
||||||
<div class="blog-card-header">
|
<div class="blog-loading-placeholder">
|
||||||
<span class="blog-date">2026.03.31</span>
|
<span class="loading-spinner"></span>
|
||||||
<span class="blog-tag">INFRASTRUCTURE</span>
|
<span>LOADING TRANSMISSIONS...</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</article>
|
<div class="blog-view-all" style="text-align:center; margin-top:2rem;">
|
||||||
<article class="blog-card" data-animate>
|
<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 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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
158
js/main.js
158
js/main.js
|
|
@ -741,6 +741,158 @@
|
||||||
}, 5000);
|
}, 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', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
initNavbar();
|
initNavbar();
|
||||||
initClock();
|
initClock();
|
||||||
|
|
@ -761,6 +913,12 @@
|
||||||
initPowerFlicker();
|
initPowerFlicker();
|
||||||
initServerHealth();
|
initServerHealth();
|
||||||
|
|
||||||
|
// Live API integrations
|
||||||
|
initBlogFeed();
|
||||||
|
initLiveStats();
|
||||||
|
initLiveWeather();
|
||||||
|
initNowPlaying();
|
||||||
|
|
||||||
|
|
||||||
// Page load animation
|
// Page load animation
|
||||||
document.body.style.opacity = '0';
|
document.body.style.opacity = '0';
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue