Complete donation form, transparency dashboard, and success pages. **Frontend Pages:** Donation Form (public/koha.html): - Three monthly tiers: $5, $15, $50 NZD - One-time custom donations - Anonymous by default with opt-in public acknowledgement - Donor information form (name optional, email required) - Stripe Checkout integration - Allocation transparency (40/30/20/10 breakdown) - Māori cultural acknowledgement (Koha meaning) - Comprehensive FAQ section - Accessible design (WCAG 2.1 AA compliant) Transparency Dashboard (public/koha/transparency.html): - Live metrics: total received, monthly supporters, recurring revenue - Allocation breakdown with animated progress bars - Recent public donor acknowledgements - One-time donation statistics - Auto-refresh every 5 minutes - Call-to-action to donate Success Page (public/koha/success.html): - Animated success confirmation with checkmark - Donation details verification via session ID - Next steps explanation (receipt, allocation, dashboard) - Monthly donor management information - Links to transparency dashboard and docs - Error state handling **Database & Scripts:** Initialization Script (scripts/init-koha.js): - Creates MongoDB indexes for koha_donations collection - Verifies Stripe configuration (keys, price IDs) - Tests transparency metrics calculation - Validates database setup - Provides next steps guide - npm script: `npm run init:koha` Package Updates: - Added Stripe SDK dependency (v14.25.0) - Added init:koha script to package.json **Features:** Privacy-First Design: ✅ Anonymous donations by default ✅ Opt-in public acknowledgement ✅ Email only for receipts ✅ No payment details stored User Experience: ✅ Responsive mobile design ✅ Keyboard navigation support ✅ Focus indicators for accessibility ✅ Loading/error states ✅ Form validation Transparency: ✅ Public metrics API integration ✅ Real-time donor acknowledgements ✅ Clear allocation breakdown ✅ Automatic dashboard updates Cultural Sensitivity: ✅ Māori term "Koha" explained ✅ Te Tiriti acknowledgement ✅ Indigenous partnership values **API Integration:** - POST /api/koha/checkout - Create donation session - GET /api/koha/transparency - Fetch public metrics - GET /api/koha/verify/:sessionId - Verify payment status **Testing Checklist:** □ Form validation (email required, minimum amount) □ Tier selection (monthly $5/$15/$50) □ One-time custom amount input □ Anonymous vs public acknowledgement toggle □ Stripe Checkout redirect □ Success page verification □ Transparency dashboard data display □ Mobile responsiveness □ Keyboard navigation **Next Steps:** 1. Create Stripe products with currency_options (all 10 currencies) 2. Test with Stripe test cards 3. Implement multi-currency support 4. Add Privacy Policy page 5. Deploy to production 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
296 lines
13 KiB
HTML
296 lines
13 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Transparency Dashboard | Tractatus Koha</title>
|
|
<meta name="description" content="Full transparency on donations received and how they're allocated to support the Tractatus AI Safety Framework.">
|
|
<link rel="stylesheet" href="/css/tailwind.css?v=1759833751">
|
|
<style>
|
|
.skip-link { position: absolute; left: -9999px; }
|
|
.skip-link:focus { left: 0; z-index: 100; background: white; padding: 1rem; }
|
|
|
|
/* Accessibility: Focus indicators (WCAG 2.4.7) */
|
|
a:focus, button:focus {
|
|
outline: 3px solid #3b82f6;
|
|
outline-offset: 2px;
|
|
}
|
|
a:focus:not(:focus-visible) { outline: none; }
|
|
a:focus-visible { outline: 3px solid #3b82f6; outline-offset: 2px; }
|
|
|
|
/* Progress bar animation */
|
|
.progress-bar {
|
|
transition: width 1s ease-out;
|
|
}
|
|
|
|
/* Stats card */
|
|
.stat-card {
|
|
transition: transform 0.2s;
|
|
}
|
|
.stat-card:hover {
|
|
transform: translateY(-4px);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="bg-gray-50">
|
|
|
|
<!-- Skip Link for Keyboard Navigation -->
|
|
<a href="#main-content" class="skip-link">Skip to main content</a>
|
|
|
|
<!-- Navigation (injected by navbar.js) -->
|
|
<script src="/js/components/navbar.js?v=1759875690"></script>
|
|
|
|
<!-- Main Content -->
|
|
<main id="main-content" class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
|
|
|
<!-- Header -->
|
|
<div class="text-center mb-12">
|
|
<h1 class="text-4xl md:text-5xl font-bold text-gray-900 mb-4">
|
|
Transparency Dashboard
|
|
</h1>
|
|
<p class="text-xl text-gray-600 max-w-3xl mx-auto">
|
|
Full visibility into donations received and how we allocate them to support the Tractatus Framework.
|
|
</p>
|
|
<p class="text-sm text-gray-500 mt-4" id="last-updated">
|
|
Last updated: Loading...
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Key Metrics -->
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-12">
|
|
|
|
<!-- Total Received -->
|
|
<div class="stat-card bg-white shadow rounded-lg p-6">
|
|
<div class="flex items-center justify-between mb-2">
|
|
<h2 class="text-sm font-medium text-gray-600">Total Received</h2>
|
|
<svg class="w-6 h-6 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
</svg>
|
|
</div>
|
|
<div class="text-4xl font-bold text-gray-900" id="total-received">$0</div>
|
|
<div class="text-sm text-gray-500 mt-1">NZD lifetime</div>
|
|
</div>
|
|
|
|
<!-- Monthly Supporters -->
|
|
<div class="stat-card bg-white shadow rounded-lg p-6">
|
|
<div class="flex items-center justify-between mb-2">
|
|
<h2 class="text-sm font-medium text-gray-600">Monthly Supporters</h2>
|
|
<svg class="w-6 h-6 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>
|
|
</svg>
|
|
</div>
|
|
<div class="text-4xl font-bold text-gray-900" id="monthly-supporters">0</div>
|
|
<div class="text-sm text-gray-500 mt-1">Active subscriptions</div>
|
|
</div>
|
|
|
|
<!-- Monthly Recurring -->
|
|
<div class="stat-card bg-white shadow rounded-lg p-6">
|
|
<div class="flex items-center justify-between mb-2">
|
|
<h2 class="text-sm font-medium text-gray-600">Monthly Recurring</h2>
|
|
<svg class="w-6 h-6 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
|
</svg>
|
|
</div>
|
|
<div class="text-4xl font-bold text-gray-900" id="monthly-revenue">$0</div>
|
|
<div class="text-sm text-gray-500 mt-1">NZD per month</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- Allocation Breakdown -->
|
|
<div class="bg-white shadow rounded-lg p-8 mb-12">
|
|
<h2 class="text-2xl font-bold text-gray-900 mb-6">How Donations Are Allocated</h2>
|
|
|
|
<div class="space-y-6">
|
|
<!-- Development: 40% -->
|
|
<div>
|
|
<div class="flex justify-between items-center mb-2">
|
|
<div>
|
|
<span class="font-semibold text-gray-900">Development</span>
|
|
<span class="text-sm text-gray-600 ml-2">Feature development, bug fixes, security updates</span>
|
|
</div>
|
|
<span class="font-bold text-blue-600">40%</span>
|
|
</div>
|
|
<div class="w-full bg-gray-200 rounded-full h-3">
|
|
<div class="progress-bar bg-blue-600 h-3 rounded-full" style="width: 0%;" data-width="40"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Hosting: 30% -->
|
|
<div>
|
|
<div class="flex justify-between items-center mb-2">
|
|
<div>
|
|
<span class="font-semibold text-gray-900">Hosting & Infrastructure</span>
|
|
<span class="text-sm text-gray-600 ml-2">Servers, database, CDN, domain, SSL</span>
|
|
</div>
|
|
<span class="font-bold text-green-600">30%</span>
|
|
</div>
|
|
<div class="w-full bg-gray-200 rounded-full h-3">
|
|
<div class="progress-bar bg-green-600 h-3 rounded-full" style="width: 0%;" data-width="30"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Research: 20% -->
|
|
<div>
|
|
<div class="flex justify-between items-center mb-2">
|
|
<div>
|
|
<span class="font-semibold text-gray-900">Research</span>
|
|
<span class="text-sm text-gray-600 ml-2">AI safety research, academic partnerships, publications</span>
|
|
</div>
|
|
<span class="font-bold text-purple-600">20%</span>
|
|
</div>
|
|
<div class="w-full bg-gray-200 rounded-full h-3">
|
|
<div class="progress-bar bg-purple-600 h-3 rounded-full" style="width: 0%;" data-width="20"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Community: 10% -->
|
|
<div>
|
|
<div class="flex justify-between items-center mb-2">
|
|
<div>
|
|
<span class="font-semibold text-gray-900">Community</span>
|
|
<span class="text-sm text-gray-600 ml-2">Documentation, outreach, contributor support</span>
|
|
</div>
|
|
<span class="font-bold text-orange-600">10%</span>
|
|
</div>
|
|
<div class="w-full bg-gray-200 rounded-full h-3">
|
|
<div class="progress-bar bg-orange-600 h-3 rounded-full" style="width: 0%;" data-width="10"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- One-Time Donations -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-12">
|
|
<div class="bg-white shadow rounded-lg p-6">
|
|
<h3 class="text-lg font-semibold text-gray-900 mb-3">One-Time Donations</h3>
|
|
<div class="text-3xl font-bold text-gray-900" id="onetime-count">0</div>
|
|
<div class="text-sm text-gray-500">Total gifts received</div>
|
|
</div>
|
|
|
|
<div class="bg-white shadow rounded-lg p-6">
|
|
<h3 class="text-lg font-semibold text-gray-900 mb-3">Average Donation</h3>
|
|
<div class="text-3xl font-bold text-gray-900" id="average-donation">$0</div>
|
|
<div class="text-sm text-gray-500">NZD across all donations</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Recent Supporters (Public Acknowledgements) -->
|
|
<div class="bg-white shadow rounded-lg p-8">
|
|
<h2 class="text-2xl font-bold text-gray-900 mb-6">Recent Supporters</h2>
|
|
<p class="text-gray-600 mb-6">
|
|
Thank you to these generous supporters who have opted to be publicly acknowledged. All supporters are valued equally, whether listed here or donating anonymously.
|
|
</p>
|
|
|
|
<div id="recent-donors" class="space-y-3">
|
|
<p class="text-gray-500 text-center py-8">Loading supporters...</p>
|
|
</div>
|
|
|
|
<div id="no-donors" class="text-center py-8 text-gray-500" style="display: none;">
|
|
No public acknowledgements yet. Be the first to support Tractatus!
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Call to Action -->
|
|
<div class="mt-12 bg-gradient-to-r from-blue-600 to-purple-600 rounded-lg p-8 text-center text-white">
|
|
<h2 class="text-2xl font-bold mb-4">Support the Tractatus Framework</h2>
|
|
<p class="text-blue-100 mb-6 max-w-2xl mx-auto">
|
|
Help us build architectural AI safety constraints that preserve human agency. Every contribution makes a difference.
|
|
</p>
|
|
<a href="/koha.html" class="inline-block bg-white text-blue-600 px-8 py-3 rounded-lg font-semibold hover:bg-blue-50 transition">
|
|
Make a Donation
|
|
</a>
|
|
</div>
|
|
|
|
</main>
|
|
|
|
<script>
|
|
// Load transparency metrics
|
|
async function loadMetrics() {
|
|
try {
|
|
const response = await fetch('/api/koha/transparency');
|
|
const data = await response.json();
|
|
|
|
if (data.success && data.data) {
|
|
const metrics = data.data;
|
|
|
|
// Update stats
|
|
document.getElementById('total-received').textContent = `$${metrics.total_received.toFixed(2)}`;
|
|
document.getElementById('monthly-supporters').textContent = metrics.monthly_supporters;
|
|
document.getElementById('monthly-revenue').textContent = `$${metrics.monthly_recurring_revenue.toFixed(2)}`;
|
|
document.getElementById('onetime-count').textContent = metrics.one_time_donations || 0;
|
|
|
|
// Calculate average
|
|
const totalCount = metrics.monthly_supporters + (metrics.one_time_donations || 0);
|
|
const avgDonation = totalCount > 0 ? metrics.total_received / totalCount : 0;
|
|
document.getElementById('average-donation').textContent = `$${avgDonation.toFixed(2)}`;
|
|
|
|
// Update last updated time
|
|
const lastUpdated = new Date(metrics.last_updated);
|
|
document.getElementById('last-updated').textContent = `Last updated: ${lastUpdated.toLocaleString()}`;
|
|
|
|
// Animate progress bars
|
|
setTimeout(() => {
|
|
document.querySelectorAll('.progress-bar').forEach(bar => {
|
|
const width = bar.getAttribute('data-width');
|
|
bar.style.width = width + '%';
|
|
});
|
|
}, 100);
|
|
|
|
// Display recent donors
|
|
if (metrics.recent_donors && metrics.recent_donors.length > 0) {
|
|
const donorsHtml = metrics.recent_donors.map(donor => {
|
|
const date = new Date(donor.date);
|
|
const dateStr = date.toLocaleDateString('en-NZ', { year: 'numeric', month: 'short' });
|
|
const freqBadge = donor.frequency === 'monthly'
|
|
? '<span class="inline-block bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded">Monthly</span>'
|
|
: '<span class="inline-block bg-green-100 text-green-800 text-xs px-2 py-1 rounded">One-time</span>';
|
|
|
|
return `
|
|
<div class="flex items-center justify-between py-3 border-b border-gray-200 last:border-0">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
|
|
<span class="text-blue-600 font-semibold">${donor.name.charAt(0).toUpperCase()}</span>
|
|
</div>
|
|
<div>
|
|
<div class="font-medium text-gray-900">${donor.name}</div>
|
|
<div class="text-sm text-gray-500">${dateStr}</div>
|
|
</div>
|
|
</div>
|
|
<div class="text-right">
|
|
<div class="font-semibold text-gray-900">$${donor.amount.toFixed(2)} NZD</div>
|
|
${freqBadge}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
|
|
document.getElementById('recent-donors').innerHTML = donorsHtml;
|
|
} else {
|
|
document.getElementById('recent-donors').style.display = 'none';
|
|
document.getElementById('no-donors').style.display = 'block';
|
|
}
|
|
|
|
} else {
|
|
throw new Error('Failed to load metrics');
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error loading transparency metrics:', error);
|
|
document.getElementById('recent-donors').innerHTML = `
|
|
<div class="text-center py-8 text-red-600">
|
|
Failed to load transparency data. Please try again later.
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
// Load metrics on page load
|
|
loadMetrics();
|
|
|
|
// Refresh every 5 minutes
|
|
setInterval(loadMetrics, 5 * 60 * 1000);
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|