feat: /live overhaul — visitor-friendly titles, project filters, incident log cleanup

This commit is contained in:
2026-03-06 00:04:08 +08:00
parent 9e82633a5e
commit 46eef75555
2 changed files with 533 additions and 262 deletions
+192 -88
View File
@@ -842,199 +842,295 @@ nav {
}
/* ═══════════════════════════════════════════
/LIVE PAGE — Full Shipping Log
/LIVE PAGE — Build Log
═══════════════════════════════════════════ */
.live-page {
padding-top: 96px;
padding-bottom: 5rem;
padding-bottom: 6rem;
min-height: 100vh;
}
.live-page-header {
margin-bottom: 3rem;
}
.live-page-eyebrow {
/* Header */
.lp-header { margin-bottom: 3rem; }
.lp-eyebrow {
display: flex;
align-items: center;
gap: 0.6rem;
font-family: var(--mono);
font-size: 0.78rem;
font-size: 0.72rem;
font-weight: 600;
color: var(--accent);
text-transform: uppercase;
letter-spacing: 0.12em;
letter-spacing: 0.14em;
margin-bottom: 1.25rem;
}
.live-page-title {
.lp-title {
font-family: var(--sans);
font-size: clamp(2rem, 5vw, 3.2rem);
font-size: clamp(2.2rem, 5vw, 3.6rem);
font-weight: 900;
letter-spacing: -0.03em;
line-height: 1.1;
letter-spacing: -0.04em;
line-height: 1.08;
color: var(--text);
margin-bottom: 1rem;
}
.live-page-sub {
.lp-sub {
font-size: 1.05rem;
color: var(--text-secondary);
line-height: 1.75;
max-width: 600px;
max-width: 560px;
}
/* Stats */
.lp-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1rem;
margin-bottom: 2.5rem;
}
.lp-stat {
text-align: center;
padding: 1.5rem 0.75rem;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 12px;
}
.lp-stat-num {
font-family: var(--mono);
font-size: 2.2rem;
font-weight: 800;
line-height: 1;
color: var(--text);
}
.lp-stat-green { color: var(--accent); }
.lp-stat-amber { color: var(--amber); }
.lp-stat-red { color: var(--red); }
.lp-stat-label {
font-family: var(--mono);
font-size: 0.65rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--text-muted);
margin-top: 0.4rem;
}
/* Filters */
.log-filters {
.lp-filters {
display: flex;
gap: 0.5rem;
margin: 2.5rem 0 2rem;
gap: 0.35rem;
margin-bottom: 2.5rem;
flex-wrap: wrap;
}
.filter-btn {
.lp-filter {
display: inline-flex;
align-items: center;
gap: 0.4rem;
font-family: var(--mono);
font-size: 0.78rem;
font-size: 0.72rem;
font-weight: 600;
padding: 0.45rem 1rem;
padding: 0.4rem 0.85rem;
border-radius: 99px;
border: 1px solid var(--border-light);
background: transparent;
color: var(--text-muted);
cursor: pointer;
transition: all 0.15s ease;
white-space: nowrap;
}
.filter-btn:hover {
.lp-filter:hover {
color: var(--text);
border-color: var(--text-muted);
}
.filter-btn.active {
background: var(--accent);
color: #050505;
border-color: var(--accent);
.lp-filter.active {
background: var(--bg-card-hover);
color: var(--text);
border-color: var(--text-secondary);
}
.lp-filter-dot {
width: 7px;
height: 7px;
border-radius: 50%;
flex-shrink: 0;
}
.lp-filter-count {
color: var(--text-dim);
font-weight: 500;
}
/* Day groups */
.log-day {
margin-bottom: 2rem;
}
.log-day-header {
/* Day group */
.lp-day { margin-bottom: 2.5rem; }
.lp-day-header {
display: flex;
align-items: center;
gap: 0.75rem;
gap: 0.6rem;
margin-bottom: 0.75rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--border);
}
.log-day-date {
.lp-day-date {
font-family: var(--mono);
font-size: 0.82rem;
font-size: 0.75rem;
font-weight: 700;
color: var(--text);
color: var(--text-secondary);
}
.log-day-count {
.lp-day-count {
font-family: var(--mono);
font-size: 0.7rem;
font-size: 0.65rem;
color: var(--text-dim);
background: var(--bg-card);
padding: 0.1rem 0.5rem;
border-radius: 99px;
}
/* Log entries */
.log-entry {
padding: 0.85rem 1.25rem;
/* Ship card */
.lp-card {
padding: 1rem 1.25rem;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 10px;
margin-bottom: 0.5rem;
margin-bottom: 0.4rem;
transition: border-color 0.15s ease;
}
.log-entry:hover {
.lp-card:hover {
border-color: var(--border-light);
}
.log-entry-head {
.lp-card-active {
border-left: 3px solid var(--amber);
}
.lp-card-row {
display: flex;
align-items: center;
gap: 0.6rem;
flex-wrap: wrap;
}
.log-entry-id {
.lp-card-tag {
font-family: var(--mono);
font-size: 0.7rem;
font-size: 0.62rem;
font-weight: 700;
color: var(--text-dim);
min-width: 2.2rem;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--tag-color, var(--text-muted));
background: color-mix(in srgb, var(--tag-color, #666) 12%, transparent);
padding: 0.2rem 0.55rem;
border-radius: 4px;
flex-shrink: 0;
white-space: nowrap;
}
.log-entry-title {
.lp-card-title {
font-size: 0.88rem;
color: var(--text-secondary);
font-weight: 500;
color: var(--text);
flex: 1;
min-width: 0;
line-height: 1.45;
}
.log-metric {
.lp-card-status {
font-family: var(--mono);
font-size: 0.75rem;
font-size: 0.62rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--amber);
background: rgba(245, 158, 11, 0.1);
padding: 0.2rem 0.55rem;
border-radius: 4px;
flex-shrink: 0;
animation: pulse-text 2s ease-in-out infinite;
}
@keyframes pulse-text {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.lp-card-summary {
font-size: 0.8rem;
color: var(--text-muted);
line-height: 1.65;
margin-top: 0.5rem;
padding-top: 0.5rem;
border-top: 1px solid var(--border);
line-height: 1.6;
padding-left: 0;
}
/* Oops section */
.oops-section {
.lp-oops {
margin-top: 4rem;
padding-top: 3rem;
border-top: 1px solid var(--border);
}
.oops-title {
.lp-oops-header { margin-bottom: 1.5rem; }
.lp-oops-title {
font-family: var(--mono);
font-size: 1.2rem;
font-size: 1.1rem;
font-weight: 700;
color: var(--text);
margin-bottom: 0.35rem;
margin-bottom: 0.3rem;
}
.oops-sub {
.lp-oops-sub {
font-size: 0.88rem;
color: var(--text-muted);
margin-bottom: 1.5rem;
}
.oops-entry {
display: flex;
align-items: flex-start;
gap: 0.75rem;
font-size: 0.82rem;
color: var(--text-muted);
padding: 0.75rem 1rem;
.lp-oops-card {
padding: 0.85rem 1.1rem;
background: var(--bg-card);
border: 1px solid var(--border);
border-left: 3px solid var(--red);
border-radius: 8px;
border-left: 3px solid #ef4444;
margin-bottom: 0.5rem;
line-height: 1.6;
margin-bottom: 0.4rem;
}
.oops-id {
.lp-oops-what {
font-size: 0.85rem;
color: var(--text-secondary);
line-height: 1.5;
}
.lp-oops-fix {
font-family: var(--mono);
font-size: 0.7rem;
font-weight: 700;
font-size: 0.72rem;
color: var(--text-dim);
flex-shrink: 0;
margin-top: 0.35rem;
}
.lp-oops-empty {
font-family: var(--mono);
font-size: 0.82rem;
color: var(--text-dim);
padding: 2rem;
}
/* Live page CTA */
.live-page-cta {
/* Bottom CTA */
.lp-bottom-cta {
text-align: center;
margin-top: 5rem;
padding-top: 3rem;
border-top: 1px solid var(--border);
}
.live-page-cta p {
.lp-bottom-line {
font-family: var(--mono);
font-size: 0.95rem;
font-weight: 600;
color: var(--text-secondary);
margin-bottom: 1.5rem;
font-size: 1.1rem;
font-weight: 700;
color: var(--text);
margin-bottom: 0.5rem;
}
.lp-bottom-sub {
font-size: 0.92rem;
color: var(--text-muted);
margin-bottom: 2rem;
}
/* Loading state */
.log-loading {
/* Loading */
.lp-loading {
font-family: var(--mono);
font-size: 0.82rem;
color: var(--text-dim);
padding: 2rem;
padding: 3rem 2rem;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.lp-loading-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--accent);
animation: pulse-text 1.5s ease-in-out infinite;
}
/* Active nav link */
@@ -1043,11 +1139,19 @@ nav {
background: var(--bg-card);
}
/* ── Responsive: /live page ── */
/* ── Responsive: /live ── */
@media (max-width: 900px) {
.lp-stats { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 640px) {
.live-page { padding-top: 80px; }
.log-entry-head { gap: 0.4rem; }
.log-entry-id { display: none; }
.log-filters { flex-wrap: wrap; }
.oops-entry { flex-direction: column; gap: 0.25rem; }
.lp-stats { gap: 0.5rem; }
.lp-stat { padding: 1rem 0.5rem; }
.lp-stat-num { font-size: 1.6rem; }
.lp-filters { gap: 0.25rem; }
.lp-filter { font-size: 0.65rem; padding: 0.35rem 0.6rem; }
.lp-card-row { gap: 0.4rem; }
.lp-card-tag { font-size: 0.58rem; }
.lp-card-title { font-size: 0.82rem; }
.lp-oops-card { padding: 0.75rem 0.85rem; }
}
+270 -103
View File
@@ -3,10 +3,10 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>QuikCue — Live Shipping Log</title>
<meta name="description" content="Every deployment, every fix, every system — logged and live. Real-time engineering feed from QuikCue.">
<meta property="og:title" content="QuikCue — Live Shipping Log">
<meta property="og:description" content="Real-time engineering feed. Every deployment logged. Full transparency.">
<title>QuikCue — Live Build Log</title>
<meta name="description" content="100 systems shipped. Every deployment, every fix — logged live from production. See what we've built for CharityRight, Pledge Now Pay Later, JustVitamin, and QuikCue infrastructure.">
<meta property="og:title" content="QuikCue — Live Build Log">
<meta property="og:description" content="100 systems shipped. Real-time production log. Full transparency.">
<meta property="og:type" content="website">
<meta property="og:url" content="https://quikcue.com/live">
<link rel="canonical" href="https://quikcue.com/live">
@@ -32,63 +32,64 @@
<div class="container">
<!-- Header -->
<header class="live-page-header">
<div class="live-page-eyebrow">
<header class="lp-header">
<div class="lp-eyebrow">
<div class="live-pulse"></div>
<span>Live Engineering Feed</span>
<span>Live from production</span>
</div>
<h1 class="live-page-title">Full Shipping Log</h1>
<p class="live-page-sub">
Every system we've shipped. Every fix we've made. Unfakeable, unedited, pulled
straight from our PostgreSQL database in real time.
<h1 class="lp-title">Build Log</h1>
<p class="lp-sub">
Every system we've deployed. Every bug we've caught. Pulled live from our
production database — unedited, unfakeable.
</p>
</header>
<!-- Stats -->
<div class="live-stats" id="stats-bar">
<div class="live-stat">
<div class="live-stat-num" id="stat-total"></div>
<div class="live-stat-label">Total ships</div>
<!-- Stats row -->
<div class="lp-stats" id="stats-bar">
<div class="lp-stat">
<div class="lp-stat-num" id="stat-total"></div>
<div class="lp-stat-label">Systems shipped</div>
</div>
<div class="live-stat">
<div class="live-stat-num live-stat-green" id="stat-shipped"></div>
<div class="live-stat-label">Shipped</div>
<div class="lp-stat">
<div class="lp-stat-num lp-stat-green" id="stat-shipped"></div>
<div class="lp-stat-label">Live in production</div>
</div>
<div class="live-stat">
<div class="live-stat-num live-stat-amber" id="stat-shipping"></div>
<div class="live-stat-label">In flight</div>
<div class="lp-stat">
<div class="lp-stat-num lp-stat-amber" id="stat-shipping"></div>
<div class="lp-stat-label">Building now</div>
</div>
<div class="live-stat">
<div class="live-stat-num live-stat-red" id="stat-oops"></div>
<div class="live-stat-label">Oops</div>
<div class="lp-stat">
<div class="lp-stat-num lp-stat-red" id="stat-oops"></div>
<div class="lp-stat-label">Incidents caught</div>
</div>
</div>
<!-- Filters -->
<div class="log-filters">
<button class="filter-btn active" data-filter="all">All</button>
<button class="filter-btn" data-filter="shipped">Shipped</button>
<button class="filter-btn" data-filter="shipping">In flight</button>
<!-- Project filters -->
<div class="lp-filters" id="filters">
<button class="lp-filter active" data-filter="all">All projects</button>
</div>
<!-- Full Ship Log -->
<div class="log-list" id="log-list">
<div class="log-loading">Loading from database…</div>
<!-- Ship log -->
<div class="lp-log" id="log-list">
<div class="lp-loading"><span class="lp-loading-dot"></span> Loading from database…</div>
</div>
<!-- Oops Log -->
<section class="oops-section" id="oops-section">
<h2 class="oops-title">Oops Log</h2>
<p class="oops-sub">If it's not here, we haven't broken it yet.</p>
<div class="oops-list" id="oops-list">
<div class="log-loading">Loading…</div>
<!-- Oops section -->
<section class="lp-oops" id="oops-section">
<div class="lp-oops-header">
<h2 class="lp-oops-title">Incident Log</h2>
<p class="lp-oops-sub">Things that broke. How fast we fixed them. Full transparency.</p>
</div>
<div class="lp-oops-list" id="oops-list">
<div class="lp-loading"><span class="lp-loading-dot"></span> Loading…</div>
</div>
</section>
<!-- CTA -->
<div class="live-page-cta">
<p>This is what autonomous operations looks like.</p>
<a href="/#contact" class="btn btn-primary">Start a conversation →</a>
<div class="lp-bottom-cta">
<p class="lp-bottom-line">This is what shipping looks like.</p>
<p class="lp-bottom-sub">Most agencies show you a portfolio. We show you the production database.</p>
<a href="/#contact" class="btn btn-primary btn-large">Talk to us →</a>
</div>
</div>
@@ -117,7 +118,162 @@
<script>
const API = 'https://calvana.quikcue.com/api';
// ── Load everything ──
// ═══════════════════════════════════════════════════════════
// VISITOR-FRIENDLY TRANSLATIONS
// Every entry gets: project tag, clean title, optional summary
// ═══════════════════════════════════════════════════════════
const DISPLAY = {
100: { project: 'quikcue', title: 'Live build log — UI overhaul and visitor-friendly entries' },
99: { project: 'quikcue', title: 'Build log page added to quikcue.com', summary: 'Full shipping history with filters, stats, and incident log — all pulled live from the production database.' },
98: { project: 'charityright', title: 'Automated milestone emails for 40,000+ supporters', summary: 'Recognises donation milestones (first gift through to £1,000) and sends personalised emails via Mailchimp — fully automated.' },
97: { project: 'quikcue', title: 'QuikCue site deployed to Git + production pipeline', summary: 'Source code on Gitea, Docker Swarm service with automatic SSL across 3 domains.' },
96: { project: 'quikcue', title: 'QuikCue company homepage launched', summary: 'Full company site: hero, three pillars, live engineering feed, client showcase. Dark premium design.' },
95: { project: 'pnpl', title: 'Dashboard redesign — header, sidebar, and onboarding', summary: 'New layout with contextual upsell prompts and a lead capture flow with qualification fields.' },
94: { project: 'pnpl', title: 'Embeddable mini widget for charity websites', summary: 'Compact 160px widget that expands on click. Charities can embed it on any page.' },
93: { project: 'pnpl', title: 'WhatsApp notification redesign + quick-add appeal picker', summary: 'Redesigned notification bar. Multi-appeal organisations now see a visual picker for adding appeals.' },
92: { project: 'pnpl', title: 'Conditional and match funding pledges', summary: 'Supporters can pledge conditionally ("I\'ll give if 100 others do") or match others. Tracked across the entire product — dashboard, reports, and donor-facing views.' },
91: { project: 'charityright', title: 'Seasonal giving data audit and dashboard overhaul', summary: 'Audited all giving data. Built seasonal view showing Ramadan 2026 campaign performance vs. all-time.' },
90: { project: 'pnpl', title: 'Complete redesign of the fundraiser link creation flow', summary: '3-step wizard: choose fundraiser → payment method → name your link. Payment method visible on every link card. Embed widget as sharing option.' },
89: { project: 'infra', title: 'Git organisation cleanup across platforms', summary: 'Separated repos: CharityRight on GitLab, QuikCue on Gitea. Cleaned up duplicates.' },
88: { project: 'pnpl', title: 'Full-width landing page design across all dashboard pages', summary: 'Every page (Home, Money, Reports, Settings) now uses edge-to-edge layout with no dead space.' },
87: { project: 'charityright', title: 'Seasonal giving dashboard with campaign analytics', summary: 'Per-campaign analytics, navigation split between seasonal campaigns and daily giving, subscriber progress tracking.' },
86: { project: 'pnpl', title: 'Education-first UX overhaul — automation and collection pages', summary: 'Two-column layouts with step-by-step guides, platform clarity indicators, and dual education panels.' },
85: { project: 'pnpl', title: 'Every page redesigned as a landing page', summary: '7 pages overhauled with brand photography, dark panels, editorial sidebar. All technical language replaced with human language.' },
84: { project: 'charityright', title: 'Full supporter care — refunds, cancellations, and edits', summary: 'Admins can now refund, cancel, or edit any donation or recurring gift with Stripe sync.' },
83: { project: 'pnpl', title: 'World-class WhatsApp message templates with A/B testing', summary: 'Professional receipt and reminder templates. A/B test variants automatically. Winners promote themselves.' },
82: { project: 'pnpl', title: 'Universal payment call-to-action system', summary: 'Smart CTAs that adapt based on payment method — bank transfer, card, direct debit, or external platform.' },
81: { project: 'pnpl', title: 'AI message generation — enforced quality and style matching', summary: 'AI generates messages that match each charity\'s tone. Validates required info per message type. Regenerate with one click.' },
80: { project: 'pnpl', title: 'Brand photography integrated into the dashboard', summary: 'Surgical image placement during onboarding — compacts once the charity is set up.' },
79: { project: 'pnpl', title: 'AI-powered message optimisation — fully automated', summary: 'One button starts AI optimisation. Generates challenger messages, runs A/B tests, picks winners automatically.' },
78: { project: 'pnpl', title: 'Automation page rebuilt as a single conversation view', summary: 'Removed 15 UI panels. The page is now one WhatsApp conversation thread — click to edit, hover for AI suggestions.' },
77: { project: 'pnpl', title: 'Switched AI model to reduce costs to ~£0.08 per 1M messages' },
76: { project: 'pnpl', title: 'AI provider switch to Gemini Flash (free tier)' },
75: { project: 'pnpl', title: 'AI-native A/B testing — messages that improve themselves', summary: 'One-click AI challengers across 8 strategies. Statistical winner detection. Continuous improvement loop.' },
74: { project: 'pnpl', title: 'Message design studio with WhatsApp preview', summary: 'Phone mockup previews for WhatsApp, email, and SMS. Edit templates per step. Built-in A/B testing with conversion tracking.' },
73: { project: 'pnpl', title: 'Multi-channel messaging engine — WhatsApp, SMS, and email', summary: 'Waterfall delivery: tries WhatsApp first, falls back to SMS, then email. Channel status dashboard and message feed.' },
72: { project: 'pnpl', title: 'Settings page redesigned as a guided checklist', summary: 'Step-by-step setup: completed items collapse to one-liners, uncompleted items stay expanded. Progress bar tracks readiness.' },
71: { project: 'charityright', title: 'Admin panel v4 — command centre redesign', summary: 'Sidebar reorganised into 3 tiers. Every list page now uses tabs with live counts instead of hidden filter drawers.' },
70: { project: 'pnpl', title: 'Stripe integration — charities connect their own account', summary: 'Each charity pastes their Stripe key. Donors see card payment option. Webhook auto-confirms payments.' },
69: { project: 'pnpl', title: 'Removed 716 lines of dead payment code' },
68: { project: 'pnpl', title: 'Settings page redesign with readiness tracking', summary: 'Dark hero with readiness bar, donor bank details preview, brand preview, feature discovery grid.' },
67: { project: 'pnpl', title: 'Team management with role-based access control', summary: 'Community Leader role added. Permission matrix across all write actions. Login redirects by role.' },
66: { project: 'pnpl', title: 'Payment reconciliation embedded directly in the Money page', summary: 'Upload and confirm payments without leaving the page. Zero extra clicks for the most critical action.' },
65: { project: 'pnpl', title: 'Context-aware Money and Reports pages', summary: 'Money page shows confirm/nudge sections based on what needs attention. Reports show Gift Aid reclaimable amounts.' },
64: { project: 'pnpl', title: 'Fundraiser collection page — flattened, zero extra clicks', summary: 'Share any link instantly without navigating into sub-pages. Inline link creation with embedded leaderboard.' },
63: { project: 'charityright', title: 'Admin panel v3 — persona-driven operational design', summary: 'Donor lookup in one search. Fundraiser nurture queue with priority sorting. Major donor identification. Donation investigation by payment reference.' },
62: { project: 'charityright', title: 'Admin panel v2 — AI-powered approval engine', summary: 'AI auto-approved 44% of stale review items. Queue reduced from 151 to 84 in one click.' },
61: { project: 'pnpl', title: 'Complete dashboard UX overhaul — welcome flow to sharing', summary: 'New charities go from signup to shareable link in 90 seconds. Context-aware dashboard with 5 different states.' },
60: { project: 'pnpl', title: 'Smart timing, duplicate detection, and bank presets shipped' },
59: { project: 'charityright', title: 'Admin panel — full audit and major rebuild', summary: '4 dashboard widgets, sidebar navigation, global search, internal notes system. All deprecated code removed.' },
58: { project: 'pnpl', title: '36 product improvements deployed in one sprint', summary: 'All pages returning 200, all API endpoints live at pledge.quikcue.com.' },
57: { project: 'pnpl', title: 'Product gap analysis v2 — revised for WhatsApp-first reality', summary: '43 gaps re-evaluated, 8 downgraded (WhatsApp handles natively), 3 new gaps identified.' },
56: { project: 'pnpl', title: 'Product gap analysis — landing pages vs. actual product', summary: '43 gaps identified across 5 landing pages. 28 hidden needs catalogued. 10 AI features specced.' },
55: { project: 'charityright', title: 'Stacking card scroll effect on the marketing site', summary: 'CSS sticky cards that overlap as you scroll. Dark dinner photography regenerated to match brand.' },
53: { project: 'charityright', title: 'Persona section rebuilt with editorial photography grid', summary: 'Sticky stacking cards, shadow depth, scenario-based personas with cinematic photography.' },
52: { project: 'charityright', title: 'Fixed 7 conversion killers across in-app browsers', summary: 'Session handling, bot protection, 3D Secure payments, and form UX — all fixed across 13 different in-app browsers.' },
51: { project: 'charityright', title: 'Hero image + full site image optimisation', summary: 'Total image size cut 85% (13.6MB → 2.1MB). WebP format. 10x faster page loads.' },
50: { project: 'charityright', title: 'New cinematic hero section — typography + documentary photo', summary: '3 sections consolidated into 1 cinematic hero with stat strip and brand signature patterns.' },
49: { project: 'charityright', title: 'Brand identity applied across every page', summary: '52 brand assets in the codebase. Single brand document as source of truth. Zero violations.' },
48: { project: 'charityright', title: 'Complete brand identity system generated in 116 seconds', summary: '1 brand identity document + 32 visual assets. Generated using 8 parallel AI sessions.' },
47: { project: 'charityright', title: 'AI-generated photography for all landing pages', summary: '20 AI-generated images across 5 pages. Zero placeholder images remaining.' },
46: { project: 'charityright', title: 'All landing pages redesigned — sharp flat typography', summary: '5 pages redesigned with typography-driven layout. "Donors" renamed to "Organisations" throughout.' },
45: { project: 'charityright', title: '4 persona landing pages + main page rewrite', summary: '5 pages total: 1 rewritten main + 4 new pages targeting specific audience types. Every section earns its place.' },
44: { project: 'charityright', title: 'Bulletproof consent system — Gift Aid, email, and WhatsApp', summary: 'HMRC-compliant Gift Aid declarations. Email and WhatsApp opt-in with immutable audit trail. Messages only send if opted in.' },
43: { project: 'pnpl', title: 'Replaced internal abbreviations with full product name' },
42: { project: 'pnpl', title: 'Simplified fund types + external payment tracking', summary: '5 fund types simplified to 1 toggle. Added "I\'ve Donated" button for external platform pledges.' },
41: { project: 'pnpl', title: 'Terminology overhaul — clearer language throughout', summary: 'Events → Campaigns, QR codes → Pledge Links. Sharing via WhatsApp, email, social, and QR.' },
40: { project: 'pnpl', title: 'Zakat fund types and campaign fund allocation' },
39: { project: 'pnpl', title: 'Fundraiser mode — role-aware onboarding for external platforms', summary: '2 user types, 5 platform integrations, role-aware signup. Dashboard never blocks access.' },
38: { project: 'pnpl', title: 'Social login — Google, Apple, and passwordless authentication', summary: 'Auth0 integration with auto-provisioning on first social login.' },
37: { project: 'pnpl', title: 'Onboarding cut from 8 screens to 2', summary: 'Signup simplified to 1 screen → instant dashboard. Onboarding checklist replaces the old wizard.' },
36: { project: 'pnpl', title: 'Demo login, admin view, and password recovery' },
35: { project: 'charityright', title: 'Last 10 Nights campaign + Zakat toggle live' },
31: { project: 'pnpl', title: 'Production launch — authentication, multi-tenant, and signup', summary: 'Auth, signup, and landing page live. Dashboard protected. WhatsApp QR setup in settings.' },
30: { project: 'pnpl', title: 'Landing page v2 with updated design language' },
29: { project: 'pnpl', title: 'Automated reminder system + dashboard rebuild', summary: 'Reminder cron running every 15 minutes. 12 test pledges including instalment plans.' },
28: { project: 'pnpl', title: 'Deferred payments and instalment plans', summary: 'Core product shift: a pledge is a promise to pay on a date. Schedule step, instalment plans (212 months), date-aware reminders.' },
27: { project: 'pnpl', title: 'Premium UI + AI messaging + WhatsApp integration', summary: 'New interface with AI-powered messaging and WhatsApp delivery. Live at pledge.quikcue.com.' },
26: { project: 'charityright', title: 'Match funding donation widget for Last 10 Nights' },
25: { project: 'pnpl', title: 'Persona-driven features for UK charity market', summary: '6 new pages and APIs targeting 4 UK charity personas. Removed non-UK payment methods.' },
24: { project: 'justvitamin', title: 'Board decision summary layout fix' },
23: { project: 'pnpl', title: 'Pledge Now Pay Later deployed to production', summary: 'Next.js app with PostgreSQL, 10 database tables, automatic SSL. Seeded with demo organisation.' },
22: { project: 'justvitamin', title: 'Dynamic analytics dashboard with API-driven charts', summary: '7 insights that recalculate per date range. Offer charts, hero stats, and ROI calculator — all live data.' },
21: { project: 'justvitamin', title: 'Conversion audit — 23 data claims verified', summary: 'Every number on the site checked against raw database. 10 fixes applied. Zero unverified stats.' },
20: { project: 'justvitamin', title: 'AI image generation + product page builder', summary: '3 live AI demos powered by Gemini. Image-to-image from real product photos. Renders as real e-commerce pages.' },
19: { project: 'justvitamin', title: 'PostgreSQL analytics backend', summary: '17 tables, 20,735 rows from 728K orders. Dashboard loads in 0.2 seconds (previously failed to load).' },
17: { project: 'justvitamin', title: 'Password-protected access for the analytics site' },
15: { project: 'comms', title: 'WhatsApp gateway upgraded — multi-session support restored', summary: 'Upgraded from free to Plus tier. 2 WhatsApp sessions restored with external HTTPS access.' },
14: { project: 'comms', title: 'Live chat system restored after restart loop', summary: 'Stale process file was causing infinite restart. Now serving normally.' },
13: { project: 'infra', title: 'Database-backed build log with live API', summary: 'This page. Ships and incidents stored in PostgreSQL. API serves data in real time. Auto-refreshes.' },
12: { project: 'infra', title: 'Staging health check — WordPress + Laravel', summary: 'Fixed malware on WordPress. Fixed Laravel (59 pending database migrations, empty tables).' },
11: { project: 'justvitamin', title: 'Analytics app deployed to production', summary: 'Python app + PostgreSQL live on Docker Swarm.' },
10: { project: 'infra', title: 'Separate Git servers for each organisation', summary: 'Gitea for QuikCue, GitLab for CharityRight. SSH over port 443 through load balancer.' },
9: { project: 'quikcue', title: 'Calvana microsite launched', summary: 'Manifesto, live shipping log, and hire page. Running on Docker Swarm.' },
8: { project: 'quikcue', title: 'QuikCue operations hub deployed', summary: 'Internal B2B operations platform with background worker.' },
7: { project: 'comms', title: 'Full communications stack deployed', summary: 'Live chat (Chatwoot), workflow automation (n8n), and WhatsApp gateway (WAHA) — all self-hosted.' },
6: { project: 'charityright', title: 'Laravel admin staging environment', summary: 'Full admin panel with Filament UI, MySQL, and Redis.' },
5: { project: 'charityright', title: 'Analytics dashboard deployed' },
4: { project: 'charityright', title: 'Checkout flow v2 deployed', summary: 'Next.js checkout with automatic SSL.' },
3: { project: 'charityright', title: 'WordPress marketing site (staging)', summary: 'WordPress + MySQL on managed hosting with automatic SSL.' },
2: { project: 'charityright', title: 'Data analytics dashboard with Metabase', summary: 'Metabase + PostgreSQL for internal data analytics and reporting.' },
1: { project: 'infra', title: 'Full server migration — 14 databases, zero downtime', summary: '2 server containers, load balancer with TLS passthrough, 14 databases migrated, Docker Swarm orchestration. 63GB backup.' },
};
// Oops translations
const OOPS_DISPLAY = {
19: { title: 'Deployment script accidentally overwrote API server config', fix: 'Restored correct config. 3 min fix.' },
18: { title: 'Routing config couldn\'t reach the application container', fix: 'Switched to direct service routing. 15 min fix.' },
17: { title: 'Database data lost during deployment path change', fix: 'Copied data from old storage volume to new one. All records restored. 4 min fix.' },
16: { title: 'Admin panel crashed after deployment — null reference errors', fix: 'Replaced query methods that broke with null models. 8 min fix.' },
15: { title: 'Navigation group name collision in admin panel', fix: 'Renamed conflicting group. Cleared server cache. 5 min fix.' },
14: { title: 'Deployment script missed a required variable in admin code', fix: 'Manually corrected 2 files and pushed. 12 min fix.' },
13: { title: 'Admin filter crashed before data was ready', fix: 'Removed premature default filter. Restarted server cache. 25 min fix.' },
12: { title: 'Two bugs: wrong database syntax + missing code import', fix: 'Fixed MySQL syntax and corrected all 4 broken files. 6 min fix.' },
11: { title: 'Dashboard widgets were invisible — registration issue', fix: 'Re-registered the default dashboard page. 5 min fix.' },
10: { title: 'AI review failed — rejection messages too long for database column', fix: 'Truncated messages to fit. 3 min fix.' },
9: { title: 'Dashboard widget property declaration incompatible with framework', fix: 'Removed the conflicting property. 2 min fix.' },
8: { title: 'Deployment script corrupted PHP files containing dollar signs', fix: 'Switched to base64-encoded file transfers. 8 min fix.' },
7: { title: 'Product scraper completely broken — pages returned garbled data', fix: 'Removed unsupported compression format from requests. 8 min fix.' },
6: { title: 'Analytics claim was unverifiable — 97.4% figure had no data source', fix: 'Corrected to 85% with source data. Fixed across 4 pages. 5 min fix.' },
5: { title: 'Email buttons on JustVitamin had no email address', fix: 'Added proper mailto links with subject and body. 3 min fix.' },
4: { title: 'WhatsApp gateway only supported 1 session instead of required multiple', fix: 'Upgraded to correct tier and restored sessions. 25 min fix.' },
3: { title: 'Log file permissions broken after debugging as wrong user', fix: 'Corrected file ownership and permissions.' },
2: { title: 'Application returning errors on every page — 59 pending database updates', fix: 'Ran all migrations and seeded data.' },
1: { title: 'WordPress site hacked via malicious plugin', fix: 'Malware deleted, rogue user removed, attack vector blocked.' },
};
const PROJECTS = {
charityright: { label: 'CharityRight', color: '#3b82f6', desc: 'UK charity donation platform' },
pnpl: { label: 'Pledge Now Pay Later', color: '#8b5cf6', desc: 'SaaS pledging product' },
justvitamin: { label: 'JustVitamin', color: '#f59e0b', desc: 'E-commerce analytics & AI' },
infra: { label: 'Infrastructure', color: '#6b7280', desc: 'Servers, deployment, DevOps' },
comms: { label: 'Communications', color: '#10b981', desc: 'WhatsApp, email, automation' },
quikcue: { label: 'QuikCue', color: '#00ff9f', desc: 'Company & internal tools' },
};
// Auto-detect project from title keywords
function detectProject(title) {
const t = title.toLowerCase();
if (t.includes('charityright') || t.includes('charity right') || t.includes('scheduled giving')
|| t.includes('regular giving') || t.includes('supporter care') || t.includes('admin panel')
|| t.includes('admin v') || t.includes('last 10') || t.includes('gift aid')
|| t.includes('persona') || t.includes('landing page') && !t.includes('pnpl'))
return 'charityright';
if (t.includes('pnpl') || t.includes('pledge') || t.includes('collect') || t.includes('fundrais'))
return 'pnpl';
if (t.includes('justvitamin') || t.includes('just vitamin'))
return 'justvitamin';
if (t.includes('comms') || t.includes('chatwoot') || t.includes('waha') || t.includes('whatsapp gateway'))
return 'comms';
if (t.includes('dokploy') || t.includes('git ') || t.includes('gitea') || t.includes('gitlab')
|| t.includes('migration') || t.includes('staging') || t.includes('calvana'))
return 'infra';
if (t.includes('quikcue') || t.includes('hub'))
return 'quikcue';
return 'infra';
}
// ── Init ──
async function init() {
const [ships, oops, stats] = await Promise.all([
fetch(`${API}/ships`).then(r => r.json()).catch(() => []),
@@ -126,9 +282,9 @@
]);
renderStats(stats, ships);
renderFilters(ships);
renderShips(ships);
renderOops(oops);
initFilters(ships);
}
// ── Stats ──
@@ -139,25 +295,58 @@
animateNum('stat-shipping', inFlight);
animateNum('stat-oops', stats.oops);
}
function animateNum(id, target) {
const el = document.getElementById(id);
if (!el || !target) { if (el) el.textContent = target || '0'; return; }
const start = performance.now();
const dur = 800;
if (!el) return;
if (!target) { el.textContent = '0'; return; }
const start = performance.now(), dur = 900;
(function step(now) {
const p = Math.min((now - start) / dur, 1);
const eased = 1 - Math.pow(1 - p, 3);
el.textContent = Math.floor(target * eased);
el.textContent = Math.floor(target * (1 - Math.pow(1 - p, 3)));
if (p < 1) requestAnimationFrame(step);
})(start);
}
// ── Filters ──
function renderFilters(ships) {
const counts = {};
ships.forEach(s => {
const d = DISPLAY[s.id];
const proj = d ? d.project : detectProject(s.title);
counts[proj] = (counts[proj] || 0) + 1;
});
const container = document.getElementById('filters');
const order = ['charityright', 'pnpl', 'justvitamin', 'comms', 'infra', 'quikcue'];
order.forEach(key => {
if (!counts[key]) return;
const p = PROJECTS[key];
const btn = document.createElement('button');
btn.className = 'lp-filter';
btn.dataset.filter = key;
btn.innerHTML = `<span class="lp-filter-dot" style="background:${p.color}"></span>${p.label} <span class="lp-filter-count">${counts[key]}</span>`;
container.appendChild(btn);
});
container.querySelectorAll('.lp-filter').forEach(btn => {
btn.addEventListener('click', () => {
container.querySelectorAll('.lp-filter').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const f = btn.dataset.filter;
document.querySelectorAll('.lp-card').forEach(card => {
card.style.display = (f === 'all' || card.dataset.project === f) ? '' : 'none';
});
document.querySelectorAll('.lp-day').forEach(day => {
const vis = day.querySelectorAll('.lp-card:not([style*="display: none"])');
day.style.display = vis.length ? '' : 'none';
});
});
});
}
// ── Ships ──
function renderShips(ships) {
const container = document.getElementById('log-list');
// Group by date
const groups = {};
ships.forEach(s => {
const date = (s.created_at || '').split('T')[0] || 'Unknown';
@@ -167,87 +356,65 @@
let html = '';
for (const [date, entries] of Object.entries(groups)) {
const formatted = formatDate(date);
html += `<div class="log-day">
<div class="log-day-header">
<span class="log-day-date">${formatted}</span>
<span class="log-day-count">${entries.length} ship${entries.length !== 1 ? 's' : ''}</span>
html += `<div class="lp-day">
<div class="lp-day-header">
<span class="lp-day-date">${formatDate(date)}</span>
<span class="lp-day-count">${entries.length}</span>
</div>`;
entries.forEach(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);
const metric = s.metric && s.metric !== '-' && s.metric !== '—'
? `<div class="log-metric">${escapeHtml(s.metric)}</div>` : '';
const d = DISPLAY[s.id] || {};
const proj = d.project || detectProject(s.title);
const p = PROJECTS[proj] || PROJECTS.infra;
const title = d.title || s.title;
const summary = d.summary || '';
const isActive = s.status === 'shipping';
html += `<div class="log-entry" data-status="${s.status}">
<div class="log-entry-head">
<span class="log-entry-id">#${s.id}</span>
<span class="live-entry-badge ${badgeClass}">${label}</span>
<span class="log-entry-title">${escapeHtml(s.title)}</span>
html += `<div class="lp-card ${isActive ? 'lp-card-active' : ''}" data-project="${proj}" data-status="${s.status}">
<div class="lp-card-row">
<span class="lp-card-tag" style="--tag-color:${p.color}">${p.label}</span>
<span class="lp-card-title">${esc(title)}</span>
${isActive ? '<span class="lp-card-status">Building</span>' : ''}
</div>
${metric}
${summary ? `<p class="lp-card-summary">${esc(summary)}</p>` : ''}
</div>`;
});
html += '</div>';
}
container.innerHTML = html;
container.innerHTML = html || '<div class="lp-loading">No entries found.</div>';
}
// ── Oops ──
function renderOops(oops) {
const container = document.getElementById('oops-list');
if (!oops.length) {
container.innerHTML = '<div class="oops-entry">Nothing broken yet. Give it time.</div>';
container.innerHTML = '<p class="lp-oops-empty">Nothing broken yet. Give it time.</p>';
return;
}
container.innerHTML = oops.map(o => {
const time = o.fix_time && o.fix_time !== '-' ? ` — fixed in ${escapeHtml(o.fix_time)}` : '';
return `<div class="oops-entry">
<span class="oops-id">#${o.id}</span>
<span>${escapeHtml(o.description)}${time}</span>
const d = OOPS_DISPLAY[o.id] || {};
const title = d.title || o.description;
const fix = d.fix || (o.fix_time ? `Fixed in ${o.fix_time}` : '');
return `<div class="lp-oops-card">
<div class="lp-oops-what">${esc(title)}</div>
${fix ? `<div class="lp-oops-fix">${esc(fix)}</div>` : ''}
</div>`;
}).join('');
}
// ── Filters ──
function initFilters(ships) {
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const filter = btn.dataset.filter;
document.querySelectorAll('.log-entry').forEach(entry => {
if (filter === 'all' || entry.dataset.status === filter) {
entry.style.display = '';
} else {
entry.style.display = 'none';
}
});
// Hide empty day headers
document.querySelectorAll('.log-day').forEach(day => {
const visible = day.querySelectorAll('.log-entry:not([style*="display: none"])');
day.style.display = visible.length ? '' : 'none';
});
});
});
}
// ── Helpers ──
function formatDate(str) {
try {
const d = new Date(str + 'T00:00:00');
return d.toLocaleDateString('en-GB', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
return d.toLocaleDateString('en-GB', { weekday: 'short', day: 'numeric', month: 'short', year: 'numeric' });
} catch { return str; }
}
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
function esc(str) {
const d = document.createElement('div');
d.textContent = str;
return d.innerHTML;
}
// Live pulse