feat: complete Priority 2 - Enhanced Koha Transparency Dashboard
Priority 2 Implementation: - Extract inline JavaScript to /public/js/koha-transparency.js (CSP compliant) - Add Chart.js 4.4.0 for visual allocation breakdown (doughnut chart) - Implement CSV export functionality with comprehensive transparency report - Link transparency dashboard from homepage footer (Support This Work section) - Deploy to production: https://agenticgovernance.digital/koha/transparency.html Homepage Enhancement: - Add "Support This Work" section to footer with donation links - Include Blog link in Community section Governance Framework: - Add inst_022: Automated deployment permission correction requirement - Addresses recurring permission issues (0700 directories causing 403 errors) - Mandates rsync --chmod=D755,F644 or post-deployment automation - Related to inst_020, but shifts from validation to prevention Technical Details: - Responsive design with Tailwind breakpoints - Auto-refresh metrics every 5 minutes - WCAG-compliant accessibility features - Minimal footprint: ~8.5KB JavaScript Fixes: - /public/koha/ directory permissions (755 required for nginx) - Added inst_022 to prevent future permission issues 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
8ee0a33aa5
commit
0dccf8b660
3 changed files with 308 additions and 100 deletions
|
|
@ -477,6 +477,14 @@
|
||||||
<ul class="space-y-2 text-sm">
|
<ul class="space-y-2 text-sm">
|
||||||
<li><a href="/media-inquiry.html" class="hover:text-white transition">Media Inquiries</a></li>
|
<li><a href="/media-inquiry.html" class="hover:text-white transition">Media Inquiries</a></li>
|
||||||
<li><a href="/case-submission.html" class="hover:text-white transition">Submit Case Study</a></li>
|
<li><a href="/case-submission.html" class="hover:text-white transition">Submit Case Study</a></li>
|
||||||
|
<li><a href="/blog.html" class="hover:text-white transition">Blog</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-white font-semibold mb-4">Support This Work</h3>
|
||||||
|
<ul class="space-y-2 text-sm">
|
||||||
|
<li><a href="/koha.html" class="hover:text-white transition">Make a Donation</a></li>
|
||||||
|
<li><a href="/koha/transparency.html" class="hover:text-white transition">Transparency Dashboard</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
280
public/js/koha-transparency.js
Normal file
280
public/js/koha-transparency.js
Normal file
|
|
@ -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 = `
|
||||||
|
<div class="text-center py-8 text-red-600">
|
||||||
|
Failed to load transparency data. Please try again later.
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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'
|
||||||
|
? '<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>';
|
||||||
|
|
||||||
|
// 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'
|
||||||
|
? `<div class="text-xs text-gray-500">≈ $${donor.amount_nzd.toFixed(2)} NZD</div>`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
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">${amountDisplay}</div>
|
||||||
|
${nzdEquivalent}
|
||||||
|
${freqBadge}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -6,6 +6,8 @@
|
||||||
<title>Transparency Dashboard | Tractatus Koha</title>
|
<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.">
|
<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">
|
<link rel="stylesheet" href="/css/tailwind.css?v=1759833751">
|
||||||
|
<!-- Chart.js for visual analytics -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js" crossorigin="anonymous"></script>
|
||||||
<style>
|
<style>
|
||||||
.skip-link { position: absolute; left: -9999px; }
|
.skip-link { position: absolute; left: -9999px; }
|
||||||
.skip-link:focus { left: 0; z-index: 100; background: white; padding: 1rem; }
|
.skip-link:focus { left: 0; z-index: 100; background: white; padding: 1rem; }
|
||||||
|
|
@ -51,9 +53,17 @@
|
||||||
<p class="text-xl text-gray-600 max-w-3xl mx-auto">
|
<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.
|
Full visibility into donations received and how we allocate them to support the Tractatus Framework.
|
||||||
</p>
|
</p>
|
||||||
<p class="text-sm text-gray-500 mt-4" id="last-updated">
|
<div class="flex items-center justify-center gap-4 mt-6">
|
||||||
Last updated: Loading...
|
<p class="text-sm text-gray-500" id="last-updated">
|
||||||
</p>
|
Last updated: Loading...
|
||||||
|
</p>
|
||||||
|
<button id="export-csv" class="text-sm bg-white border border-gray-300 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-50 transition flex items-center gap-2">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||||
|
</svg>
|
||||||
|
Export CSV
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Key Metrics -->
|
<!-- Key Metrics -->
|
||||||
|
|
@ -101,6 +111,11 @@
|
||||||
<div class="bg-white shadow rounded-lg p-8 mb-12">
|
<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>
|
<h2 class="text-2xl font-bold text-gray-900 mb-6">How Donations Are Allocated</h2>
|
||||||
|
|
||||||
|
<!-- Allocation Chart -->
|
||||||
|
<div class="mb-8 max-w-md mx-auto">
|
||||||
|
<canvas id="allocation-chart" aria-label="Donation allocation pie chart" role="img"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<!-- Development: 40% -->
|
<!-- Development: 40% -->
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -210,103 +225,8 @@
|
||||||
<!-- Coming Soon Overlay (remove when Stripe is configured) -->
|
<!-- Coming Soon Overlay (remove when Stripe is configured) -->
|
||||||
<script src="/js/components/coming-soon-overlay.js"></script>
|
<script src="/js/components/coming-soon-overlay.js"></script>
|
||||||
|
|
||||||
<script>
|
<!-- Transparency Dashboard JavaScript -->
|
||||||
// Load transparency metrics
|
<script src="/js/koha-transparency.js"></script>
|
||||||
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>';
|
|
||||||
|
|
||||||
// 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'
|
|
||||||
? `<div class="text-xs text-gray-500">≈ $${donor.amount_nzd.toFixed(2)} NZD</div>`
|
|
||||||
: '';
|
|
||||||
|
|
||||||
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">${amountDisplay}</div>
|
|
||||||
${nzdEquivalent}
|
|
||||||
${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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue