feat: live feed from calvana API + fallback data

This commit is contained in:
2026-03-05 21:39:04 +08:00
parent 69b0571857
commit 849a81c184
+103 -64
View File
@@ -18,7 +18,7 @@
</head> </head>
<body> <body>
<!-- ═══════════════════════════════════════════ NAV ═══════════════════════════════════════════ --> <!-- ═══════════════════════ NAV ═══════════════════════ -->
<nav> <nav>
<div class="nav-inner"> <div class="nav-inner">
<a href="/" class="logo">QuikCue</a> <a href="/" class="logo">QuikCue</a>
@@ -30,7 +30,7 @@
</div> </div>
</nav> </nav>
<!-- ═══════════════════════════════════════════ HERO ═══════════════════════════════════════════ --> <!-- ═══════════════════════ HERO ═══════════════════════ -->
<header class="hero"> <header class="hero">
<div class="hero-inner"> <div class="hero-inner">
<div class="hero-eyebrow">Autonomous Systems Studio</div> <div class="hero-eyebrow">Autonomous Systems Studio</div>
@@ -51,7 +51,7 @@
<div class="hero-glow"></div> <div class="hero-glow"></div>
</header> </header>
<!-- ═══════════════════════════════════════════ PROBLEM ═══════════════════════════════════════════ --> <!-- ═══════════════════════ PROBLEM ═══════════════════════ -->
<section class="section section-problem" id="problem"> <section class="section section-problem" id="problem">
<div class="container"> <div class="container">
<div class="section-label">The problem we keep seeing</div> <div class="section-label">The problem we keep seeing</div>
@@ -99,7 +99,7 @@
</div> </div>
</section> </section>
<!-- ═══════════════════════════════════════════ CATEGORY ═══════════════════════════════════════════ --> <!-- ═══════════════════════ CATEGORY / PILLARS ═══════════════════════ -->
<section class="section section-category" id="work"> <section class="section section-category" id="work">
<div class="container"> <div class="container">
<div class="section-label">One discipline. Not five services.</div> <div class="section-label">One discipline. Not five services.</div>
@@ -112,7 +112,6 @@
</p> </p>
<div class="pillars"> <div class="pillars">
<!-- PILLAR 1 -->
<div class="pillar"> <div class="pillar">
<div class="pillar-number">01</div> <div class="pillar-number">01</div>
<h3 class="pillar-title">Infrastructure</h3> <h3 class="pillar-title">Infrastructure</h3>
@@ -130,7 +129,6 @@
</div> </div>
</div> </div>
<!-- PILLAR 2 -->
<div class="pillar"> <div class="pillar">
<div class="pillar-number">02</div> <div class="pillar-number">02</div>
<h3 class="pillar-title">Intelligence</h3> <h3 class="pillar-title">Intelligence</h3>
@@ -148,7 +146,6 @@
</div> </div>
</div> </div>
<!-- PILLAR 3 -->
<div class="pillar"> <div class="pillar">
<div class="pillar-number">03</div> <div class="pillar-number">03</div>
<h3 class="pillar-title">Independence</h3> <h3 class="pillar-title">Independence</h3>
@@ -169,7 +166,7 @@
</div> </div>
</section> </section>
<!-- ═══════════════════════════════════════════ LIVE FEED ═══════════════════════════════════════════ --> <!-- ═══════════════════════ LIVE FEED ═══════════════════════ -->
<section class="section section-live" id="live"> <section class="section section-live" id="live">
<div class="container"> <div class="container">
<div class="live-header"> <div class="live-header">
@@ -186,15 +183,15 @@
<div class="live-stats"> <div class="live-stats">
<div class="live-stat"> <div class="live-stat">
<div class="live-stat-num" id="stat-shipped">90</div> <div class="live-stat-num" id="stat-shipped"></div>
<div class="live-stat-label">Systems shipped</div> <div class="live-stat-label">Systems shipped</div>
</div> </div>
<div class="live-stat"> <div class="live-stat">
<div class="live-stat-num live-stat-green" id="stat-rate">98%</div> <div class="live-stat-num live-stat-green" id="stat-rate"></div>
<div class="live-stat-label">Ship rate</div> <div class="live-stat-label">Ship rate</div>
</div> </div>
<div class="live-stat"> <div class="live-stat">
<div class="live-stat-num live-stat-amber" id="stat-oops">17</div> <div class="live-stat-num live-stat-amber" id="stat-oops"></div>
<div class="live-stat-label">Oops (transparent)</div> <div class="live-stat-label">Oops (transparent)</div>
</div> </div>
<div class="live-stat"> <div class="live-stat">
@@ -207,39 +204,10 @@
<div class="live-feed-header"> <div class="live-feed-header">
<div class="live-pulse"></div> <div class="live-pulse"></div>
<span>Live Engineering Feed</span> <span>Live Engineering Feed</span>
<a href="https://calvana.quikcue.com/live" target="_blank" class="live-feed-link">Open full feed →</a>
</div> </div>
<div class="live-feed-entries" id="live-entries"> <div class="live-feed-entries" id="live-entries">
<!-- Populated by JS from calvana API or static snapshot --> <div class="live-entry live-entry-loading">
<div class="live-entry"> <span class="live-entry-title" style="opacity:0.3">Loading live feed…</span>
<span class="live-entry-id">#96</span>
<span class="live-entry-badge badge-shipping">Shipping</span>
<span class="live-entry-title">QuikCue Homepage — New site alongside Calvana</span>
</div>
<div class="live-entry">
<span class="live-entry-id">#95</span>
<span class="live-entry-badge badge-shipped">Shipped</span>
<span class="live-entry-title">Self-healing deploy pipeline — auto-rollback on health check failure</span>
</div>
<div class="live-entry">
<span class="live-entry-id">#94</span>
<span class="live-entry-badge badge-shipped">Shipped</span>
<span class="live-entry-title">Calvana ship log — DB-backed persistence across sessions</span>
</div>
<div class="live-entry">
<span class="live-entry-id">#93</span>
<span class="live-entry-badge badge-shipped">Shipped</span>
<span class="live-entry-title">Remote command tool — run anything on any container</span>
</div>
<div class="live-entry">
<span class="live-entry-id">#92</span>
<span class="live-entry-badge badge-shipped">Shipped</span>
<span class="live-entry-title">Deploy tool — auto-discovers apps from Dokploy DB</span>
</div>
<div class="live-entry">
<span class="live-entry-id">#91</span>
<span class="live-entry-badge badge-shipped">Shipped</span>
<span class="live-entry-title">PNPL — Pledge Now Pay Later checkout deployed</span>
</div> </div>
</div> </div>
<div class="live-feed-fade"></div> <div class="live-feed-fade"></div>
@@ -258,7 +226,7 @@
</div> </div>
</section> </section>
<!-- ═══════════════════════════════════════════ CLIENTS ═══════════════════════════════════════════ --> <!-- ═══════════════════════ CLIENTS ═══════════════════════ -->
<section class="section section-clients"> <section class="section section-clients">
<div class="container"> <div class="container">
<div class="section-label">Who we build for</div> <div class="section-label">Who we build for</div>
@@ -307,7 +275,7 @@
</div> </div>
</section> </section>
<!-- ═══════════════════════════════════════════ CTA ═══════════════════════════════════════════ --> <!-- ═══════════════════════ CTA ═══════════════════════ -->
<section class="section section-cta" id="contact"> <section class="section section-cta" id="contact">
<div class="container"> <div class="container">
<div class="cta-block"> <div class="cta-block">
@@ -334,7 +302,7 @@
</div> </div>
</section> </section>
<!-- ═══════════════════════════════════════════ FOOTER ═══════════════════════════════════════════ --> <!-- ═══════════════════════ FOOTER ═══════════════════════ -->
<footer class="site-footer"> <footer class="site-footer">
<div class="container"> <div class="container">
<div class="footer-inner"> <div class="footer-inner">
@@ -343,7 +311,7 @@
<span class="footer-tagline">We build the machine. You run the mission.</span> <span class="footer-tagline">We build the machine. You run the mission.</span>
</div> </div>
<div class="footer-links"> <div class="footer-links">
<a href="https://calvana.quikcue.com/live" target="_blank">Live Feed</a> <a href="https://calvana.quikcue.com/live" target="_blank">Calvana</a>
<a href="https://git.quikcue.com" target="_blank">Git</a> <a href="https://git.quikcue.com" target="_blank">Git</a>
<a href="mailto:omair@quikcue.com">Contact</a> <a href="mailto:omair@quikcue.com">Contact</a>
</div> </div>
@@ -355,9 +323,9 @@
</div> </div>
</footer> </footer>
<!-- ═══════════════════════════════════════════ SCRIPTS ═══════════════════════════════════════════ --> <!-- ═══════════════════════ SCRIPTS ═══════════════════════ -->
<script> <script>
// Smooth scroll for anchor links // ── Smooth scroll ──
document.querySelectorAll('a[href^="#"]').forEach(a => { document.querySelectorAll('a[href^="#"]').forEach(a => {
a.addEventListener('click', e => { a.addEventListener('click', e => {
e.preventDefault(); e.preventDefault();
@@ -366,7 +334,7 @@
}); });
}); });
// Intersection Observer for fade-in animations // ── Intersection Observer for fade-in ──
const observer = new IntersectionObserver((entries) => { const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => { entries.forEach(entry => {
if (entry.isIntersecting) { if (entry.isIntersecting) {
@@ -376,42 +344,113 @@
}); });
}, { threshold: 0.1, rootMargin: '0px 0px -60px 0px' }); }, { threshold: 0.1, rootMargin: '0px 0px -60px 0px' });
document.querySelectorAll('.pillar, .client-card, .problem-stat, .live-stat, .live-entry').forEach(el => { document.querySelectorAll('.pillar, .client-card, .problem-stat, .live-stat').forEach(el => {
el.classList.add('fade-in'); el.classList.add('fade-in');
observer.observe(el); observer.observe(el);
}); });
// Animate stat numbers on scroll // ── Animate numbers ──
const animateValue = (el, start, end, duration) => { const animateValue = (el, start, end, duration, suffix) => {
const range = end - start; const range = end - start;
const startTime = performance.now(); const startTime = performance.now();
const step = (now) => { const step = (now) => {
const progress = Math.min((now - startTime) / duration, 1); const progress = Math.min((now - startTime) / duration, 1);
const eased = 1 - Math.pow(1 - progress, 3); const eased = 1 - Math.pow(1 - progress, 3);
const val = Math.floor(start + range * eased); el.textContent = Math.floor(start + range * eased) + (suffix || '');
el.textContent = val + (el.dataset.suffix || '');
if (progress < 1) requestAnimationFrame(step); if (progress < 1) requestAnimationFrame(step);
}; };
requestAnimationFrame(step); requestAnimationFrame(step);
}; };
// ── Live Feed: fetch from calvana API ──
const CALVANA_API = 'https://calvana.quikcue.com/api';
async function loadLiveFeed() {
try {
const [shipsRes, statsRes] = await Promise.all([
fetch(`${CALVANA_API}/ships?limit=12`),
fetch(`${CALVANA_API}/stats`)
]);
if (shipsRes.ok) {
const ships = await shipsRes.json();
renderShips(ships);
}
if (statsRes.ok) {
const stats = await statsRes.json();
renderStats(stats);
}
} catch (e) {
// Fallback: render static data baked at build time
renderShips(FALLBACK_SHIPS);
renderStats(FALLBACK_STATS);
}
}
// Fallback data (updated at deploy time)
const FALLBACK_STATS = { total: 92, shipped: 89, oops: 17 };
const FALLBACK_SHIPS = [
{ id: 98, title: "Milestone Recognition: n8n → Mailchimp via donation_warehouse", status: "shipping" },
{ id: 97, title: "QuikCue site — Git + Dokploy proper deployment", status: "shipping" },
{ id: 96, title: "QuikCue Homepage — Elite Rebrand of Calvana", status: "shipped" },
{ id: 95, title: "PNPL: Dashboard revamp — header, sidebar, upsell funnel", status: "shipped" },
{ id: 94, title: "PNPL: Mini embed widget + visible back button", status: "shipped" },
{ id: 93, title: "PNPL — WhatsApp banner redesign + Collect quick-add appeal picker", status: "shipped" },
{ id: 92, title: "PNPL — Conditional & match funding pledges", status: "shipped" },
{ id: 91, title: "Scheduled Giving data audit + junk filtering + error fixes", status: "shipped" },
{ id: 90, title: "PNPL — Fundamental Collect redesign: unified link creation", status: "shipped" },
{ id: 89, title: "Git org cleanup — CharityRight on GitLab, QuikCue on Gitea", status: "shipped" },
{ id: 88, title: "PNPL — Landing page philosophy across ALL dashboard pages", status: "shipped" },
{ id: 87, title: "Scheduled Giving Dashboard & Navigation Overhaul", status: "shipped" },
];
function renderStats(stats) {
const shipped = stats.shipped || 0;
const total = stats.total || 0;
const oops = stats.oops || 0;
const rate = total > 0 ? Math.round((shipped / total) * 100) : 0;
const elShipped = document.getElementById('stat-shipped');
const elRate = document.getElementById('stat-rate');
const elOops = document.getElementById('stat-oops');
// Animate if visible, otherwise set directly
const statsObserver = new IntersectionObserver((entries) => { const statsObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => { entries.forEach(entry => {
if (entry.isIntersecting) { if (entry.isIntersecting) {
const el = entry.target; animateValue(elShipped, 0, shipped, 1200, '');
const text = el.textContent; animateValue(elRate, 0, rate, 1200, '%');
const num = parseInt(text); animateValue(elOops, 0, oops, 1200, '');
const suffix = text.replace(/\d+/, ''); statsObserver.disconnect();
if (!isNaN(num)) {
el.dataset.suffix = suffix;
animateValue(el, 0, num, 1200);
}
statsObserver.unobserve(el);
} }
}); });
}, { threshold: 0.5 }); }, { threshold: 0.5 });
statsObserver.observe(elShipped.parentElement);
}
document.querySelectorAll('.live-stat-num').forEach(el => statsObserver.observe(el)); function renderShips(ships) {
const container = document.getElementById('live-entries');
container.innerHTML = ships.map(s => {
const badgeClass = s.status === 'shipped' ? 'badge-shipped' :
s.status === 'shipping' ? 'badge-shipping' : 'badge-planned';
const label = s.status.charAt(0).toUpperCase() + s.status.slice(1);
return `<div class="live-entry fade-in visible">
<span class="live-entry-id">#${s.id}</span>
<span class="live-entry-badge ${badgeClass}">${label}</span>
<span class="live-entry-title">${escapeHtml(s.title)}</span>
</div>`;
}).join('');
}
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
// Load feed — try API first, fallback to baked data
loadLiveFeed();
// Live pulse animation // Live pulse animation
const pulse = document.querySelector('.live-pulse'); const pulse = document.querySelector('.live-pulse');