diff --git a/public/index.html b/public/index.html
index 1ec39e9e..66b330bf 100644
--- a/public/index.html
+++ b/public/index.html
@@ -477,6 +477,14 @@
diff --git a/public/js/koha-transparency.js b/public/js/koha-transparency.js
new file mode 100644
index 00000000..e1c452f3
--- /dev/null
+++ b/public/js/koha-transparency.js
@@ -0,0 +1,280 @@
+/**
+ * Koha Transparency Dashboard
+ * Real-time donation metrics with privacy-preserving analytics
+ */
+
+// Chart instances (global for updates)
+let allocationChart = null;
+let trendChart = null;
+
+/**
+ * Load and display 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
+ updateStats(metrics);
+
+ // Update allocation chart
+ updateAllocationChart(metrics);
+
+ // Update progress bars (legacy display)
+ animateProgressBars();
+
+ // Display recent donors
+ displayRecentDonors(metrics.recent_donors || []);
+
+ // Update last updated time
+ updateLastUpdated(metrics.last_updated);
+
+ } else {
+ throw new Error('Failed to load metrics');
+ }
+
+ } catch (error) {
+ console.error('Error loading transparency metrics:', error);
+ document.getElementById('recent-donors').innerHTML = `
+
+ Failed to load transparency data. Please try again later.
+
+ `;
+ }
+}
+
+/**
+ * Update stats display
+ */
+function updateStats(metrics) {
+ 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 allocation pie chart
+ */
+function updateAllocationChart(metrics) {
+ const ctx = document.getElementById('allocation-chart');
+ if (!ctx) return;
+
+ const allocation = metrics.allocation || {
+ development: 0.4,
+ hosting: 0.3,
+ research: 0.2,
+ community: 0.1
+ };
+
+ const data = {
+ labels: ['Development (40%)', 'Hosting & Infrastructure (30%)', 'Research (20%)', 'Community (10%)'],
+ datasets: [{
+ data: [
+ allocation.development * 100,
+ allocation.hosting * 100,
+ allocation.research * 100,
+ allocation.community * 100
+ ],
+ backgroundColor: [
+ '#3B82F6', // blue-600
+ '#10B981', // green-600
+ '#A855F7', // purple-600
+ '#F59E0B' // orange-600
+ ],
+ borderWidth: 2,
+ borderColor: '#FFFFFF'
+ }]
+ };
+
+ const config = {
+ type: 'doughnut',
+ data: data,
+ options: {
+ responsive: true,
+ maintainAspectRatio: true,
+ plugins: {
+ legend: {
+ position: 'bottom',
+ labels: {
+ padding: 15,
+ font: {
+ size: 12
+ }
+ }
+ },
+ tooltip: {
+ callbacks: {
+ label: function(context) {
+ const label = context.label || '';
+ const value = context.parsed;
+ return `${label}: ${value.toFixed(1)}%`;
+ }
+ }
+ }
+ }
+ }
+ };
+
+ if (allocationChart) {
+ allocationChart.data = data;
+ allocationChart.update();
+ } else {
+ allocationChart = new Chart(ctx, config);
+ }
+}
+
+/**
+ * Animate progress bars (legacy display)
+ */
+function animateProgressBars() {
+ setTimeout(() => {
+ document.querySelectorAll('.progress-bar').forEach(bar => {
+ const width = bar.getAttribute('data-width');
+ bar.style.width = width + '%';
+ });
+ }, 100);
+}
+
+/**
+ * Display recent donors
+ */
+function displayRecentDonors(donors) {
+ const donorsContainer = document.getElementById('recent-donors');
+ const noDonorsMessage = document.getElementById('no-donors');
+
+ if (donors.length > 0) {
+ const donorsHtml = donors.map(donor => {
+ const date = new Date(donor.date);
+ const dateStr = date.toLocaleDateString('en-NZ', { year: 'numeric', month: 'short' });
+ const freqBadge = donor.frequency === 'monthly'
+ ? '
Monthly'
+ : '
One-time';
+
+ // Format currency display
+ const currency = (donor.currency || 'nzd').toUpperCase();
+ const amountDisplay = `$${donor.amount.toFixed(2)} ${currency}`;
+
+ // Show NZD equivalent if different currency
+ const nzdEquivalent = currency !== 'NZD'
+ ? `
≈ $${donor.amount_nzd.toFixed(2)} NZD
`
+ : '';
+
+ return `
+
+
+
+ ${donor.name.charAt(0).toUpperCase()}
+
+
+
${donor.name}
+
${dateStr}
+
+
+
+
${amountDisplay}
+ ${nzdEquivalent}
+ ${freqBadge}
+
+
+ `;
+ }).join('');
+
+ donorsContainer.innerHTML = donorsHtml;
+ donorsContainer.style.display = 'block';
+ if (noDonorsMessage) noDonorsMessage.style.display = 'none';
+ } else {
+ donorsContainer.style.display = 'none';
+ if (noDonorsMessage) noDonorsMessage.style.display = 'block';
+ }
+}
+
+/**
+ * Update last updated timestamp
+ */
+function updateLastUpdated(timestamp) {
+ const lastUpdated = new Date(timestamp);
+ const elem = document.getElementById('last-updated');
+ if (elem) {
+ elem.textContent = `Last updated: ${lastUpdated.toLocaleString()}`;
+ }
+}
+
+/**
+ * Export transparency data as CSV
+ */
+async function exportCSV() {
+ try {
+ const response = await fetch('/api/koha/transparency');
+ const data = await response.json();
+
+ if (!data.success || !data.data) {
+ throw new Error('Failed to load metrics for export');
+ }
+
+ const metrics = data.data;
+
+ // Build CSV content
+ let csv = 'Tractatus Koha Transparency Report\\n';
+ csv += `Generated: ${new Date().toISOString()}\\n\\n`;
+
+ csv += 'Metric,Value\\n';
+ csv += `Total Received,${metrics.total_received}\\n`;
+ csv += `Monthly Supporters,${metrics.monthly_supporters}\\n`;
+ csv += `One-Time Donations,${metrics.one_time_donations || 0}\\n`;
+ csv += `Monthly Recurring Revenue,${metrics.monthly_recurring_revenue}\\n\\n`;
+
+ csv += 'Allocation Category,Percentage\\n';
+ csv += `Development,${(metrics.allocation.development * 100).toFixed(1)}%\\n`;
+ csv += `Hosting & Infrastructure,${(metrics.allocation.hosting * 100).toFixed(1)}%\\n`;
+ csv += `Research,${(metrics.allocation.research * 100).toFixed(1)}%\\n`;
+ csv += `Community,${(metrics.allocation.community * 100).toFixed(1)}%\\n\\n`;
+
+ if (metrics.recent_donors && metrics.recent_donors.length > 0) {
+ csv += 'Recent Public Supporters\\n';
+ csv += 'Name,Date,Amount,Currency,Frequency\\n';
+ metrics.recent_donors.forEach(donor => {
+ csv += `"${donor.name}",${donor.date},${donor.amount},${donor.currency || 'NZD'},${donor.frequency}\\n`;
+ });
+ }
+
+ // Create download
+ const blob = new Blob([csv], { type: 'text/csv' });
+ const url = window.URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `tractatus-transparency-${new Date().toISOString().split('T')[0]}.csv`;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ window.URL.revokeObjectURL(url);
+
+ } catch (error) {
+ console.error('Error exporting CSV:', error);
+ alert('Failed to export transparency data. Please try again.');
+ }
+}
+
+// Initialize on page load
+document.addEventListener('DOMContentLoaded', () => {
+ // Load metrics
+ loadMetrics();
+
+ // Refresh every 5 minutes
+ setInterval(loadMetrics, 5 * 60 * 1000);
+
+ // Setup CSV export button
+ const exportBtn = document.getElementById('export-csv');
+ if (exportBtn) {
+ exportBtn.addEventListener('click', exportCSV);
+ }
+});
diff --git a/public/koha/transparency.html b/public/koha/transparency.html
index 6911ff01..bc02a867 100644
--- a/public/koha/transparency.html
+++ b/public/koha/transparency.html
@@ -6,6 +6,8 @@
Transparency Dashboard | Tractatus Koha
+
+