From 8b9bb89797e95ba04b858e82b40b927defe593c1 Mon Sep 17 00:00:00 2001 From: TheFlow Date: Sat, 11 Oct 2025 17:14:34 +1300 Subject: [PATCH] feat: complete Priority 2 - Enhanced Koha Transparency Dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .claude/instruction-history.json | 76 ++++++++- public/index.html | 8 + public/js/koha-transparency.js | 280 +++++++++++++++++++++++++++++++ public/koha/transparency.html | 120 +++---------- 4 files changed, 378 insertions(+), 106 deletions(-) create mode 100644 public/js/koha-transparency.js diff --git a/.claude/instruction-history.json b/.claude/instruction-history.json index ee26a79e..82dc2fa7 100644 --- a/.claude/instruction-history.json +++ b/.claude/instruction-history.json @@ -1,6 +1,6 @@ { "version": "1.0", - "last_updated": "2025-10-07T19:30:00Z", + "last_updated": "2025-10-11T04:05:00Z", "description": "Persistent instruction database for Tractatus framework governance", "instructions": [ { @@ -363,20 +363,84 @@ }, "active": true, "notes": "IDENTIFIED 2025-10-10 - User observed frequent compaction events despite ContextPressureMonitor reporting 'NORMAL' (6.7%) pressure at 50k token checkpoint. Actual context consumption much higher due to tool results (reading instruction-history.json twice = 12k tokens, concurrent-session doc = large, multiple bash outputs). Current monitor only accurately tracks response generation, not total context window usage. This gap causes unexpected compactions and poor handoff timing. API Memory may reduce impact but won't eliminate root cause." + }, + { + "id": "inst_020", + "text": "Web application deployments MUST ensure correct file permissions before going live. All public-facing directories need 755 permissions (world-readable+executable), static files (HTML/CSS/JS/images) need 644 permissions (world-readable). Deployment scripts should verify nginx/apache can access all public paths. Add automated permission validation to deployment workflows to prevent 403 Forbidden errors.", + "timestamp": "2025-10-11T02:20:00Z", + "quadrant": "SYSTEM", + "persistence": "HIGH", + "temporal_scope": "PROJECT", + "verification_required": "MANDATORY", + "explicitness": 1.0, + "source": "system", + "session_id": "2025-10-07-001", + "parameters": { + "directory_permissions": "755", + "file_permissions": "644", + "directories_requiring_755": ["/public", "/public/admin", "/public/js", "/public/js/admin", "/public/css", "/public/images", "/public/downloads"], + "deployment_check": "stat -c '%a %n' /path/to/public/* | grep -v '755\\|644'", + "prevention": "Add to deployment scripts or CI/CD pipeline" + }, + "active": true, + "notes": "DEPLOYMENT ISSUE 2025-10-11 - Priority 1 blog deployment: /public/admin/ directory had 0700 permissions (owner-only), causing nginx to return 403 Forbidden for all admin pages (/admin/login.html, /admin/project-manager.html, etc.). rsync preserved restrictive local permissions during deployment. Fixed with 'chmod 755 /public/admin && chmod 644 /public/admin/*.html'. This is preventable with automated permission validation in deployment workflow." + }, + { + "id": "inst_021", + "text": "When implementing new features with dedicated models/controllers/routes, document the API-Model-Controller relationship clearly. Controller file headers should include endpoint examples, route files should document the model they operate on, and create API reference documentation in docs/api/. Update the API root endpoint (/api) with new route listings. This prevents confusion when multiple overlapping concepts exist (e.g., Projects for governance vs Blog for content).", + "timestamp": "2025-10-11T02:25:00Z", + "quadrant": "OPERATIONAL", + "persistence": "HIGH", + "temporal_scope": "PROJECT", + "verification_required": "REQUIRED", + "explicitness": 0.95, + "source": "system", + "session_id": "2025-10-07-001", + "parameters": { + "documentation_locations": ["controller file header", "route file comments", "docs/api/ directory", "/api root endpoint"], + "controller_header_template": "Model: X.model.js | Routes: /api/path | Endpoints: GET /api/path, POST /api/path", + "route_file_comments": "Document model, validation requirements, authentication, examples", + "api_docs_format": "Markdown with endpoint details, request/response examples, error codes", + "update_api_root": "Add new routes to src/routes/index.js root handler" + }, + "active": true, + "notes": "DEVELOPMENT CONFUSION 2025-10-11 - Priority 1 blog testing: Initially tried using /api/admin/projects for blog posts instead of /api/blog, because both 'Projects' (governance system) and 'Blog' (content system) deal with project-like entities. BlogPost.model.js exists separately from Project.model.js, with dedicated blog.controller.js and blog.routes.js, but this wasn't immediately obvious. Clear Model-Controller-Route documentation would have prevented this 10-minute detour. The API confusion delayed testing and could confuse future developers." + }, + { + "id": "inst_022", + "text": "ALL deployment scripts (rsync, scp, git pull) MUST include automated post-deployment permission correction as a standard step, not a reactive fix after errors. Use '--chmod=D755,F644' with rsync or equivalent automated permission setting for other tools. Directory creation during deployment MUST explicitly set 755 (directories) and 644 (files) permissions.", + "timestamp": "2025-10-11T04:05:00Z", + "quadrant": "SYSTEM", + "persistence": "HIGH", + "temporal_scope": "PERMANENT", + "verification_required": "MANDATORY", + "explicitness": 1.0, + "source": "system", + "session_id": "2025-10-11-priority-2-koha", + "parameters": { + "rsync_chmod_flag": "--chmod=D755,F644", + "rsync_example": "rsync -avz --chmod=D755,F644 -e 'ssh -i key' local/ remote:/path/", + "post_deploy_verification": "ssh remote 'find /var/www/tractatus/public -type d -exec chmod 755 {} + && find /var/www/tractatus/public -type f -name \"*.html\" -o -name \"*.js\" -o -name \"*.css\" -exec chmod 644 {} +'", + "deployment_script_requirement": "scripts/deploy-full-project-SAFE.sh and any ad-hoc rsync commands MUST use --chmod flag or include post-deployment permission fix as standard final step", + "applies_to": ["rsync", "scp", "git pull", "docker volumes", "manual copies"] + }, + "related_instructions": ["inst_020"], + "active": true, + "notes": "RECURRING DEPLOYMENT ISSUE 2025-10-11 - Despite inst_020 requiring permission validation, /public/koha/ directory had 0700 permissions (same pattern as /public/admin/ in previous session). Root cause: rsync creates directories with restrictive umask defaults, and inst_020 focuses on reactive validation rather than proactive automation. This shifts from 'MUST ensure permissions' (principle) to 'USE --chmod flag or automated fix' (automation requirement). Prevents manual permission fixing after discovering 403 errors." } ], "stats": { - "total_instructions": 19, - "active_instructions": 19, + "total_instructions": 22, + "active_instructions": 22, "by_quadrant": { "STRATEGIC": 6, - "OPERATIONAL": 5, + "OPERATIONAL": 6, "TACTICAL": 1, - "SYSTEM": 7, + "SYSTEM": 9, "STOCHASTIC": 0 }, "by_persistence": { - "HIGH": 17, + "HIGH": 20, "MEDIUM": 2, "LOW": 0, "VARIABLE": 0 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 @@ + +
+

Support This Work

+
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 + +